@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,717 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import {
3
+ existsSync,
4
+ lstatSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ renameSync,
9
+ rmSync,
10
+ writeFileSync
11
+ } from 'node:fs';
12
+ import { homedir } from 'node:os';
13
+ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
14
+ import { projectPaths } from './paths.mjs';
15
+ import { listPersonas, personalPersonaRoot, projectPersonaRoot } from './personas.mjs';
16
+ import { selectContextualPersonas } from './runtime/persona-engagement.mjs';
17
+ import { publishLifecycleEventSafely } from './runtime/lifecycle-hooks.mjs';
18
+
19
+ export const KNOWLEDGE_PROPOSALS_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.';
20
+
21
+ const PREPARATION_SCHEMA = 'ewai.knowledge-proposal-preparation/v1';
22
+ const BUNDLE_SCHEMA = 'ewai.knowledge-proposal-bundle/v1';
23
+ const RECORDING_SCHEMA = 'ewai.knowledge-proposal-recording/v1';
24
+ const REVIEW_SCHEMA = 'ewai.knowledge-proposal-review/v1';
25
+ const WORKSPACE_SCHEMA = 'ewai.knowledge-proposal-workspace/v1';
26
+ const MATERIALISATION_SCHEMA = 'ewai.knowledge-materialisation/v1';
27
+ const sourceRefPattern = /^(meeting-evidence|retrospective):([a-z0-9][a-z0-9.-]{0,119})$/;
28
+ const proposalIdPattern = /^KNP-[0-9]{3,}$/;
29
+ const bundleIdPattern = /^knowledge\.[a-z0-9][a-z0-9.-]{0,119}\.[a-f0-9]{12}$/;
30
+ const digestPattern = /^[a-f0-9]{64}$/;
31
+ const MAX_SOURCE_BYTES = 1024 * 1024;
32
+ const MAX_PROPOSALS = 200;
33
+ const decisions = new Set(['accepted', 'rejected', 'amended', 'deferred']);
34
+
35
+ const taxonomy = Object.freeze({
36
+ 'project-persona': '1.Scope/personas/project',
37
+ system: '1.Scope/domain/systems',
38
+ process: '1.Scope/domain/processes',
39
+ 'data-concept': '1.Scope/domain/data',
40
+ journey: '2.Purpose/journeys',
41
+ requirement: '2.Purpose/requirements/pending',
42
+ 'feature-candidate': '2.Purpose/explorations/feature-candidates',
43
+ risk: '3.Evidence/risk',
44
+ policy: '4.Constraints/compliance/policies',
45
+ constraint: '4.Constraints',
46
+ standard: '4.Constraints/standards',
47
+ decision: '5.Strategy/decisions',
48
+ pattern: '5.Strategy/patterns',
49
+ 'anti-pattern': '5.Strategy/anti-patterns',
50
+ runbook: '5.Strategy/runbooks',
51
+ sop: '5.Strategy/sops'
52
+ });
53
+
54
+ function normalised(path) {
55
+ return path.split(sep).join('/');
56
+ }
57
+
58
+ function inside(parent, child) {
59
+ const path = relative(parent, child);
60
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
61
+ }
62
+
63
+ function sha256(value) {
64
+ return createHash('sha256').update(value).digest('hex');
65
+ }
66
+
67
+ function canonicalDigest(value) {
68
+ return sha256(JSON.stringify(value));
69
+ }
70
+
71
+ function slugify(value, max = 80) {
72
+ return String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, max) || 'knowledge';
73
+ }
74
+
75
+ function cleanText(value, label, max = 500, required = true) {
76
+ const text = String(value ?? '').trim();
77
+ if (required && !text) throw new Error(`Knowledge proposals require ${label}`);
78
+ if (text.length > max) throw new Error(`Knowledge proposals ${label} exceeds ${max} characters`);
79
+ return text;
80
+ }
81
+
82
+ function safeRelative(projectRoot, path) {
83
+ const value = normalised(relative(projectRoot, path));
84
+ if (!value || value.startsWith('../') || isAbsolute(value)) throw new Error('Knowledge proposal path escapes the project');
85
+ return value;
86
+ }
87
+
88
+ function atomicJson(path, value) {
89
+ mkdirSync(resolve(path, '..'), { recursive: true });
90
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
91
+ writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
92
+ renameSync(temporary, path);
93
+ }
94
+
95
+ function assertInitialized(paths) {
96
+ if (!existsSync(paths.configPath)) throw new Error('Initialize EWAI before using knowledge proposals');
97
+ }
98
+
99
+ function readBoundedFile(path, label) {
100
+ const stats = lstatSync(path);
101
+ if (stats.isSymbolicLink() || !stats.isFile()) throw new Error(`${label} must be a regular non-symbolic file`);
102
+ if (stats.size > MAX_SOURCE_BYTES) throw new Error(`${label} exceeds the 1 MiB limit`);
103
+ return readFileSync(path, 'utf8');
104
+ }
105
+
106
+ function sourceRoots(paths) {
107
+ return {
108
+ meetings: resolve(paths.specsRoot, '3.Evidence/meeting-evidence'),
109
+ retros: resolve(paths.specsRoot, '3.Evidence/retros')
110
+ };
111
+ }
112
+
113
+ function parseHeadings(markdown) {
114
+ const lines = markdown.split(/\r?\n/);
115
+ const anchors = [];
116
+ for (let index = 0; index < lines.length; index += 1) {
117
+ const match = lines[index].match(/^#{2,4}\s+(.+)$/);
118
+ if (!match) continue;
119
+ const level = lines[index].match(/^#+/)?.[0].length ?? 2;
120
+ let end = lines.length;
121
+ for (let cursor = index + 1; cursor < lines.length; cursor += 1) {
122
+ const next = lines[cursor].match(/^(#+)\s+/);
123
+ if (next && next[1].length <= level) { end = cursor; break; }
124
+ }
125
+ anchors.push({
126
+ id: `heading:${slugify(match[1])}`,
127
+ label: match[1].trim(),
128
+ lineStart: index + 1,
129
+ lineEnd: end,
130
+ excerpt: lines.slice(index + 1, end).join('\n').trim().slice(0, 1200)
131
+ });
132
+ }
133
+ return anchors;
134
+ }
135
+
136
+ function meetingSource(paths, id) {
137
+ const roots = sourceRoots(paths);
138
+ const root = resolve(roots.meetings, id);
139
+ if (!inside(roots.meetings, root) || !existsSync(root) || lstatSync(root).isSymbolicLink() || !lstatSync(root).isDirectory()) {
140
+ throw new Error(`Unknown knowledge source: meeting-evidence:${id}`);
141
+ }
142
+ const jsonPath = resolve(root, 'evidence.json');
143
+ if (!inside(root, jsonPath) || !existsSync(jsonPath)) throw new Error(`Unknown knowledge source: meeting-evidence:${id}`);
144
+ const content = readBoundedFile(jsonPath, 'Meeting evidence');
145
+ let record;
146
+ try { record = JSON.parse(content); } catch { throw new Error(`Invalid meeting evidence source: ${id}`); }
147
+ if (record.schema !== 'ewai.meeting-evidence/v1' || record.source?.sourceId !== id || !Array.isArray(record.evidence)) {
148
+ throw new Error(`Invalid meeting evidence source: ${id}`);
149
+ }
150
+ const anchors = record.evidence.map((item) => ({
151
+ id: `candidate:${cleanText(item?.id, 'meeting evidence candidate ID', 80)}`,
152
+ label: `${String(item?.type ?? 'evidence')} · ${String(item?.id ?? '')}`,
153
+ lineAnchors: Array.isArray(item?.lineAnchors) ? item.lineAnchors : [],
154
+ excerpt: [item?.statement, item?.interpretation].filter(Boolean).join('\n').slice(0, 1200)
155
+ }));
156
+ const modelContext = record.evidence.map((item) => [
157
+ `### ${item.id} · ${item.type}`,
158
+ `Statement: ${item.statement}`,
159
+ `Interpretation: ${item.interpretation}`,
160
+ `Source lines: ${(item.lineAnchors ?? []).map(({ start, end }) => start === end ? start : `${start}-${end}`).join(', ')}`
161
+ ].join('\n')).join('\n\n');
162
+ return {
163
+ ref: `meeting-evidence:${id}`,
164
+ family: 'meeting-evidence',
165
+ id,
166
+ label: record.source?.label || id,
167
+ path: jsonPath,
168
+ projectPath: safeRelative(paths.projectRoot, jsonPath),
169
+ digest: sha256(content),
170
+ anchors,
171
+ modelContext
172
+ };
173
+ }
174
+
175
+ function retrospectiveSource(paths, slug) {
176
+ if (!/^[a-z0-9][a-z0-9-]{0,119}$/.test(slug)) throw new Error(`Invalid knowledge source reference: retrospective:${slug}`);
177
+ const roots = sourceRoots(paths);
178
+ const path = resolve(roots.retros, `${slug}.md`);
179
+ if (!inside(roots.retros, path) || !existsSync(path)) throw new Error(`Unknown knowledge source: retrospective:${slug}`);
180
+ const markdown = readBoundedFile(path, 'Retrospective');
181
+ const title = markdown.match(/^#\s+(?:Retrospective:\s*)?(.+)$/m)?.[1]?.trim() || slug;
182
+ const anchors = parseHeadings(markdown);
183
+ return {
184
+ ref: `retrospective:${slug}`,
185
+ family: 'retrospective',
186
+ id: slug,
187
+ label: title,
188
+ path,
189
+ projectPath: safeRelative(paths.projectRoot, path),
190
+ digest: sha256(markdown),
191
+ anchors,
192
+ modelContext: markdown
193
+ };
194
+ }
195
+
196
+ function resolveSource(paths, sourceRef) {
197
+ const ref = cleanText(sourceRef, 'source reference', 180);
198
+ const match = ref.match(sourceRefPattern);
199
+ if (!match) throw new Error(`Invalid knowledge source reference: ${ref}`);
200
+ if (match[1] === 'meeting-evidence') return meetingSource(paths, match[2]);
201
+ return retrospectiveSource(paths, match[2]);
202
+ }
203
+
204
+ function safeSource(source) {
205
+ return {
206
+ ref: source.ref,
207
+ family: source.family,
208
+ id: source.id,
209
+ label: source.label,
210
+ digest: source.digest,
211
+ anchors: source.anchors.map(({ excerpt: _excerpt, ...anchor }) => anchor),
212
+ anchorCount: source.anchors.length
213
+ };
214
+ }
215
+
216
+ function inferTier(persona) {
217
+ const path = normalised(String(persona.path ?? ''));
218
+ if (persona.tier) return persona.tier;
219
+ if (path.includes('/premium-personas/')) return 'premium';
220
+ if (path.includes('/1.Scope/personas/project/')) return 'project';
221
+ if (path.includes('/.ewai/personas/')) return 'personal';
222
+ return 'core';
223
+ }
224
+
225
+ function installedPersonaCatalogue(projectRoot) {
226
+ const roots = [
227
+ resolve(import.meta.dirname, '../packs/personas/core/personas'),
228
+ resolve(homedir(), '.ewai/packs/ewai.personas.professional/premium-personas'),
229
+ personalPersonaRoot(),
230
+ projectPersonaRoot(projectRoot)
231
+ ];
232
+ const ranks = { project: 4, premium: 3, personal: 2, core: 1 };
233
+ const found = new Map();
234
+ for (const persona of listPersonas(roots)) {
235
+ const candidate = { ...persona, tier: inferTier(persona) };
236
+ const existing = found.get(candidate.id);
237
+ if (!existing || ranks[candidate.tier] > ranks[existing.tier]) found.set(candidate.id, candidate);
238
+ }
239
+ return [...found.values()];
240
+ }
241
+
242
+ function contextualPersonas(projectRoot, source, options = {}) {
243
+ const focus = Array.isArray(options.focus)
244
+ ? options.focus.map((item) => String(item).trim()).filter(Boolean)
245
+ : String(options.focus ?? '').split(',').map((item) => item.trim()).filter(Boolean);
246
+ const signals = focus.length ? focus : [source.family, 'knowledge', 'provenance', 'review', ...source.anchors.slice(0, 8).map(({ label }) => label)];
247
+ const catalogue = Array.isArray(options.personaCatalogue) ? options.personaCatalogue : installedPersonaCatalogue(projectRoot);
248
+ return selectContextualPersonas({
249
+ signals,
250
+ context: { source: safeSource(source), focus },
251
+ personaCatalogue: catalogue.map((persona) => ({ ...persona, tier: inferTier(persona) })),
252
+ contextLabel: 'evidence-to-knowledge proposals',
253
+ limit: options.personaLimit ?? 6
254
+ });
255
+ }
256
+
257
+ function proposalRoot(paths) {
258
+ return resolve(paths.specsRoot, '3.Evidence/knowledge-proposals');
259
+ }
260
+
261
+ function bundlePath(paths, bundleId) {
262
+ if (!bundleIdPattern.test(String(bundleId ?? ''))) throw new Error(`Invalid knowledge proposal bundle ID: ${bundleId}`);
263
+ const root = proposalRoot(paths);
264
+ const path = resolve(root, bundleId);
265
+ if (!inside(root, path)) throw new Error('Unsafe knowledge proposal bundle path');
266
+ return path;
267
+ }
268
+
269
+ function expectedDestination(paths, kind, destination) {
270
+ const root = taxonomy[kind];
271
+ if (!root) throw new Error(`Unsupported knowledge proposal kind: ${kind}`);
272
+ const value = normalised(cleanText(destination, 'proposal destination', 300));
273
+ if (isAbsolute(value) || value.split('/').includes('..') || !value.endsWith('.md')) throw new Error(`Unsafe knowledge proposal destination: ${value}`);
274
+ const prefix = paths.specsRelative === '.' ? '' : `${paths.specsRelative}/`;
275
+ const expectedPrefix = `${prefix}${root}/`;
276
+ if (!value.startsWith(expectedPrefix)) throw new Error(`Knowledge proposal destination does not match kind ${kind}: ${value}`);
277
+ const rest = value.slice(expectedPrefix.length);
278
+ if (!/^[a-z0-9][a-z0-9-]{0,119}\.md$/.test(rest)) throw new Error(`Unsafe knowledge proposal destination: ${value}`);
279
+ const absolute = resolve(paths.projectRoot, value);
280
+ if (!inside(resolve(paths.projectRoot, prefix || '.'), absolute)) throw new Error(`Unsafe knowledge proposal destination: ${value}`);
281
+ return { value, absolute };
282
+ }
283
+
284
+ function normalisePersonas(raw) {
285
+ if (!Array.isArray(raw)) return [];
286
+ return raw.slice(0, 8).map((persona) => ({
287
+ id: cleanText(persona?.id, 'active persona ID', 160),
288
+ name: cleanText(persona?.name, 'active persona name', 160),
289
+ tier: cleanText(persona?.tier || 'core', 'active persona tier', 40),
290
+ category: cleanText(persona?.category, 'active persona category', 100, false),
291
+ description: cleanText(persona?.description, 'active persona description', 240, false),
292
+ matchedSignals: Array.isArray(persona?.matchedSignals) ? persona.matchedSignals.slice(0, 20).map((value) => cleanText(value, 'persona signal', 100)) : [],
293
+ engagementReason: cleanText(persona?.engagementReason, 'persona engagement reason', 400)
294
+ }));
295
+ }
296
+
297
+ function destinationState(path, markdown) {
298
+ if (!existsSync(path)) return 'additive';
299
+ const stats = lstatSync(path);
300
+ if (stats.isSymbolicLink() || !stats.isFile()) return 'conflict';
301
+ return readFileSync(path, 'utf8') === markdown ? 'already-current' : 'conflict';
302
+ }
303
+
304
+ function normaliseProposal(paths, source, raw) {
305
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Each knowledge proposal must be an object');
306
+ const allowed = new Set(['id', 'kind', 'title', 'destination', 'evidenceAnchors', 'rationale', 'uncertainty', 'relationships', 'proposedMarkdown']);
307
+ for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Knowledge proposal field is not allowed: ${key}`);
308
+ const id = cleanText(raw.id, 'proposal ID', 30);
309
+ if (!proposalIdPattern.test(id)) throw new Error(`Invalid knowledge proposal ID: ${id}`);
310
+ const kind = cleanText(raw.kind, 'proposal kind', 50);
311
+ const title = cleanText(raw.title, 'proposal title', 200);
312
+ const destination = expectedDestination(paths, kind, raw.destination);
313
+ if (!Array.isArray(raw.evidenceAnchors) || !raw.evidenceAnchors.length || raw.evidenceAnchors.length > 30) throw new Error(`${id} requires one to 30 evidence anchors`);
314
+ const knownAnchors = new Set(source.anchors.map(({ id: anchorId }) => anchorId));
315
+ const evidenceAnchors = raw.evidenceAnchors.map((anchor) => cleanText(anchor, 'evidence anchor', 180));
316
+ if (evidenceAnchors.some((anchor) => !knownAnchors.has(anchor))) throw new Error(`${id} references an unknown evidence anchor`);
317
+ const rationale = cleanText(raw.rationale, 'proposal rationale', 1600);
318
+ const uncertainty = cleanText(raw.uncertainty, 'proposal uncertainty', 1200, false);
319
+ const relationships = Array.isArray(raw.relationships) ? raw.relationships.slice(0, 30).map((item) => cleanText(item, 'proposal relationship', 300)) : [];
320
+ const proposedMarkdown = cleanText(raw.proposedMarkdown, 'proposed Markdown', 100_000);
321
+ if (!/^#\s+.+/m.test(proposedMarkdown) || !/^##\s+Provenance\s*$/mi.test(proposedMarkdown)) throw new Error(`${id} proposed Markdown requires a provenance section`);
322
+ if (!proposedMarkdown.includes(source.ref) || evidenceAnchors.some((anchor) => !proposedMarkdown.includes(anchor))) throw new Error(`${id} proposed Markdown provenance does not cite its source and anchors`);
323
+ if (/<script\b|javascript:|-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----|\u0000/i.test(proposedMarkdown)) throw new Error(`${id} proposed Markdown contains executable content or credentials`);
324
+ return {
325
+ id, kind, title, destination: destination.value, evidenceAnchors, rationale, uncertainty, relationships, proposedMarkdown,
326
+ state: destinationState(destination.absolute, proposedMarkdown)
327
+ };
328
+ }
329
+
330
+ function normaliseBundle(paths, source, raw) {
331
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) throw new Error('Knowledge proposal bundle must be an object');
332
+ const allowed = new Set(['schema', 'sourceRef', 'sourceDigest', 'proposals']);
333
+ for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`Knowledge proposal bundle field is not allowed: ${key}`);
334
+ if (raw.schema !== BUNDLE_SCHEMA) throw new Error(`Knowledge proposal bundle schema must be ${BUNDLE_SCHEMA}`);
335
+ if (raw.sourceRef !== source.ref) throw new Error('Knowledge proposal bundle source does not match');
336
+ if (raw.sourceDigest !== source.digest) throw new Error('Knowledge proposal bundle source digest is stale');
337
+ if (!Array.isArray(raw.proposals) || !raw.proposals.length || raw.proposals.length > MAX_PROPOSALS) throw new Error(`Knowledge proposal bundle must contain one to ${MAX_PROPOSALS} proposals`);
338
+ const proposals = raw.proposals.map((proposal) => normaliseProposal(paths, source, proposal));
339
+ if (new Set(proposals.map(({ id }) => id)).size !== proposals.length) throw new Error('Knowledge proposal bundle contains a duplicate proposal ID');
340
+ if (new Set(proposals.map(({ destination }) => destination)).size !== proposals.length) throw new Error('Knowledge proposal bundle contains a duplicate proposal destination');
341
+ return { schema: BUNDLE_SCHEMA, sourceRef: source.ref, sourceDigest: source.digest, proposals };
342
+ }
343
+
344
+ function readBundle(paths, bundleId) {
345
+ const root = bundlePath(paths, bundleId);
346
+ const path = resolve(root, 'bundle.json');
347
+ if (!existsSync(path)) throw new Error(`Unknown knowledge proposal bundle: ${bundleId}`);
348
+ const record = JSON.parse(readFileSync(path, 'utf8'));
349
+ if (record.schema !== BUNDLE_SCHEMA || record.bundleId !== bundleId) throw new Error(`Invalid knowledge proposal bundle: ${bundleId}`);
350
+ const { bundleDigest, ...payload } = record;
351
+ if (!digestPattern.test(String(bundleDigest ?? '')) || canonicalDigest(payload) !== bundleDigest) throw new Error(`Knowledge proposal bundle digest is invalid: ${bundleId}`);
352
+ return record;
353
+ }
354
+
355
+ function reviewPath(paths, bundleId) {
356
+ return resolve(bundlePath(paths, bundleId), 'review.json');
357
+ }
358
+
359
+ function readReview(paths, bundleId) {
360
+ const path = reviewPath(paths, bundleId);
361
+ if (!existsSync(path)) throw new Error(`Knowledge proposal bundle has no named review: ${bundleId}`);
362
+ const review = JSON.parse(readFileSync(path, 'utf8'));
363
+ const { reviewDigest, ...payload } = review;
364
+ if (review.schema !== REVIEW_SCHEMA || review.bundleId !== bundleId || canonicalDigest(payload) !== reviewDigest) throw new Error(`Knowledge proposal review digest is invalid: ${bundleId}`);
365
+ return review;
366
+ }
367
+
368
+ function materialisationPath(paths, bundleId) {
369
+ return resolve(bundlePath(paths, bundleId), 'materialisation.json');
370
+ }
371
+
372
+ function journalPath(paths, bundleId) {
373
+ return resolve(paths.runtimeRoot, 'knowledge-proposals/transactions', `${bundleId}.json`);
374
+ }
375
+
376
+ function materialisedProposal(bundleProposal, disposition) {
377
+ if (disposition.decision !== 'amended') return { ...bundleProposal };
378
+ return {
379
+ ...bundleProposal,
380
+ title: disposition.replacementTitle,
381
+ proposedMarkdown: disposition.replacementMarkdown,
382
+ state: undefined
383
+ };
384
+ }
385
+
386
+ export function listKnowledgeSources(projectRoot) {
387
+ const paths = projectPaths(projectRoot);
388
+ assertInitialized(paths);
389
+ const roots = sourceRoots(paths);
390
+ const sources = [];
391
+ if (existsSync(roots.meetings)) {
392
+ for (const entry of readdirSync(roots.meetings, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
393
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
394
+ try { sources.push(safeSource(meetingSource(paths, entry.name))); } catch { /* malformed evidence is not eligible */ }
395
+ }
396
+ }
397
+ if (existsSync(roots.retros)) {
398
+ for (const entry of readdirSync(roots.retros, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
399
+ if (!entry.isFile() || entry.isSymbolicLink() || extname(entry.name).toLowerCase() !== '.md') continue;
400
+ const slug = basename(entry.name, '.md');
401
+ if (!/^[a-z0-9][a-z0-9-]{0,119}$/.test(slug)) continue;
402
+ try { sources.push(safeSource(retrospectiveSource(paths, slug))); } catch { /* malformed evidence is not eligible */ }
403
+ }
404
+ }
405
+ return sources.sort((left, right) => left.ref.localeCompare(right.ref));
406
+ }
407
+
408
+ export function prepareKnowledgeProposals(projectRoot, sourceRef, options = {}) {
409
+ const paths = projectPaths(projectRoot);
410
+ assertInitialized(paths);
411
+ const source = resolveSource(paths, sourceRef);
412
+ const activePersonas = contextualPersonas(paths.projectRoot, source, options);
413
+ return {
414
+ schema: PREPARATION_SCHEMA,
415
+ source: safeSource(source),
416
+ modelContext: source.modelContext,
417
+ proposalContract: {
418
+ schema: BUNDLE_SCHEMA,
419
+ schemaPath: 'config/knowledge-proposals-proposal.schema.json',
420
+ allowedKinds: Object.keys(taxonomy),
421
+ destinationRoots: Object.fromEntries(Object.entries(taxonomy).map(([kind, root]) => [kind, `${paths.specsRelative === '.' ? '' : `${paths.specsRelative}/`}${root}`])),
422
+ proposalIdPattern: proposalIdPattern.source,
423
+ maxProposals: MAX_PROPOSALS,
424
+ provenanceRequired: true,
425
+ canonicalWritesAllowed: false
426
+ },
427
+ activePersonas,
428
+ authority: { personasAreAdvisory: true, namedReviewRequired: true, materialisationRequiresSeparateApproval: true, overwriteMergeDeleteAllowed: false },
429
+ notices: [
430
+ 'Preparation is advisory and writes no canonical project knowledge.',
431
+ 'Project/core personas are complete; relevant installed premium and personal personas are optional visible enrichment only.',
432
+ KNOWLEDGE_PROPOSALS_DISCLAIMER
433
+ ]
434
+ };
435
+ }
436
+
437
+ export function recordKnowledgeProposalBundle(projectRoot, sourceRef, input = {}) {
438
+ const paths = projectPaths(projectRoot);
439
+ assertInitialized(paths);
440
+ const source = resolveSource(paths, sourceRef);
441
+ const bundle = normaliseBundle(paths, source, input.bundle);
442
+ const activePersonas = normalisePersonas(input.activePersonas);
443
+ const bundleId = `knowledge.${slugify(`${source.family}-${source.id}`, 110)}.${source.digest.slice(0, 12)}`;
444
+ const root = bundlePath(paths, bundleId);
445
+ const recordedAt = new Date(input.now ?? Date.now()).toISOString();
446
+ const payload = { ...bundle, bundleId, recordedAt, activePersonas };
447
+ const bundleDigest = canonicalDigest(payload);
448
+ const record = { ...payload, bundleDigest };
449
+ if (existsSync(root)) {
450
+ const existing = readBundle(paths, bundleId);
451
+ if (existing.bundleDigest === bundleDigest) return { schema: RECORDING_SCHEMA, bundleId, bundleDigest, bundlePath: safeRelative(paths.projectRoot, resolve(root, 'bundle.json')), counts: stateCounts(existing.proposals), idempotent: true };
452
+ throw new Error('Conflicting knowledge proposal bundle already exists and will not be overwritten');
453
+ }
454
+ const stage = `${root}.${process.pid}.${randomUUID()}.tmp`;
455
+ try {
456
+ mkdirSync(stage, { recursive: true });
457
+ writeFileSync(resolve(stage, 'bundle.json'), `${JSON.stringify(record, null, 2)}\n`, 'utf8');
458
+ for (const proposal of bundle.proposals) {
459
+ const path = resolve(stage, 'proposals', proposal.destination);
460
+ if (!inside(stage, path)) throw new Error('Unsafe staged knowledge proposal path');
461
+ mkdirSync(resolve(path, '..'), { recursive: true });
462
+ writeFileSync(path, proposal.proposedMarkdown, 'utf8');
463
+ }
464
+ mkdirSync(resolve(root, '..'), { recursive: true });
465
+ renameSync(stage, root);
466
+ } catch (error) {
467
+ rmSync(stage, { recursive: true, force: true });
468
+ throw error;
469
+ }
470
+ return { schema: RECORDING_SCHEMA, bundleId, bundleDigest, bundlePath: safeRelative(paths.projectRoot, resolve(root, 'bundle.json')), counts: stateCounts(bundle.proposals), idempotent: false };
471
+ }
472
+
473
+ function stateCounts(proposals) {
474
+ return {
475
+ proposals: proposals.length,
476
+ additive: proposals.filter(({ state }) => state === 'additive').length,
477
+ alreadyCurrent: proposals.filter(({ state }) => state === 'already-current').length,
478
+ conflicts: proposals.filter(({ state }) => state === 'conflict').length
479
+ };
480
+ }
481
+
482
+ function normaliseDispositions(bundle, raw) {
483
+ if (!Array.isArray(raw) || raw.length !== bundle.proposals.length) throw new Error('Knowledge proposal review must dispose of every proposal exactly once');
484
+ const known = new Map(bundle.proposals.map((proposal) => [proposal.id, proposal]));
485
+ const dispositions = raw.map((item) => {
486
+ const proposalId = cleanText(item?.proposalId, 'review proposal ID', 30);
487
+ if (!known.has(proposalId)) throw new Error(`Knowledge proposal review references an unknown proposal: ${proposalId}`);
488
+ const decision = cleanText(item?.decision, 'review decision', 20);
489
+ if (!decisions.has(decision)) throw new Error(`Unsupported knowledge proposal review decision: ${decision}`);
490
+ const rationale = cleanText(item?.rationale, 'review rationale', 1600, false);
491
+ const replacementTitle = cleanText(item?.replacementTitle, 'amendment replacement title', 200, false);
492
+ const replacementMarkdown = cleanText(item?.replacementMarkdown, 'amendment replacement Markdown', 100_000, false);
493
+ if (decision === 'amended') {
494
+ if (!replacementTitle || !replacementMarkdown || !rationale) throw new Error(`${proposalId} amendment requires replacement title, Markdown and rationale`);
495
+ if (!/^##\s+Provenance\s*$/mi.test(replacementMarkdown) || !replacementMarkdown.includes(bundle.sourceRef)) throw new Error(`${proposalId} amendment requires source provenance`);
496
+ } else if (replacementTitle || replacementMarkdown) {
497
+ throw new Error(`${proposalId} replacement content is allowed only for an amendment`);
498
+ }
499
+ if (['rejected', 'deferred'].includes(decision) && !rationale) throw new Error(`${proposalId} ${decision} decision requires rationale`);
500
+ return { proposalId, decision, ...(rationale ? { rationale } : {}), ...(replacementTitle ? { replacementTitle, replacementMarkdown } : {}) };
501
+ });
502
+ if (new Set(dispositions.map(({ proposalId }) => proposalId)).size !== dispositions.length) throw new Error('Knowledge proposal review must dispose of every proposal exactly once');
503
+ return dispositions;
504
+ }
505
+
506
+ export function recordKnowledgeProposalReview(projectRoot, bundleId, input = {}) {
507
+ const paths = projectPaths(projectRoot);
508
+ assertInitialized(paths);
509
+ const reviewedBy = cleanText(input.reviewedBy, 'named reviewer', 160);
510
+ const bundle = readBundle(paths, bundleId);
511
+ const source = resolveSource(paths, bundle.sourceRef);
512
+ if (source.digest !== bundle.sourceDigest) throw new Error('Knowledge proposal source is stale relative to the recorded bundle');
513
+ const dispositions = normaliseDispositions(bundle, input.dispositions);
514
+ const reviewedAt = new Date(input.now ?? Date.now()).toISOString();
515
+ const payload = { schema: REVIEW_SCHEMA, bundleId, bundleDigest: bundle.bundleDigest, sourceDigest: source.digest, reviewedBy, reviewedAt, dispositions };
516
+ const reviewDigest = canonicalDigest(payload);
517
+ const review = { ...payload, reviewDigest };
518
+ const path = reviewPath(paths, bundleId);
519
+ if (existsSync(path)) {
520
+ const existing = readReview(paths, bundleId);
521
+ if (existing.reviewDigest === reviewDigest) return { schema: 'ewai.knowledge-proposal-review-recording/v1', bundleId, reviewDigest, reviewedBy, reviewedAt: existing.reviewedAt, counts: decisionCounts(existing.dispositions), idempotent: true };
522
+ throw new Error('Conflicting knowledge proposal review already exists and will not be overwritten');
523
+ }
524
+ atomicJson(path, review);
525
+ return { schema: 'ewai.knowledge-proposal-review-recording/v1', bundleId, reviewDigest, reviewedBy, reviewedAt, counts: decisionCounts(dispositions), idempotent: false };
526
+ }
527
+
528
+ function decisionCounts(dispositions) {
529
+ return Object.fromEntries([...decisions].map((decision) => [decision, dispositions.filter((item) => item.decision === decision).length]));
530
+ }
531
+
532
+ export function materialiseKnowledgeProposals(projectRoot, bundleId, input = {}) {
533
+ if (input.confirmed !== true) throw new Error('Knowledge proposal materialisation requires exact confirmation');
534
+ const approvedBy = cleanText(input.approvedBy, 'named approver', 160);
535
+ const paths = projectPaths(projectRoot);
536
+ assertInitialized(paths);
537
+ const existingLedgerPath = materialisationPath(paths, bundleId);
538
+ if (existsSync(existingLedgerPath)) {
539
+ const existing = JSON.parse(readFileSync(existingLedgerPath, 'utf8'));
540
+ if (existsSync(journalPath(paths, bundleId))) rmSync(journalPath(paths, bundleId), { force: true });
541
+ return { ...existing.result, ledgerPath: safeRelative(paths.projectRoot, existingLedgerPath), idempotent: true };
542
+ }
543
+ if (existsSync(journalPath(paths, bundleId))) throw new Error('Knowledge materialisation has an interrupted transaction; recover it before retrying');
544
+ const bundle = readBundle(paths, bundleId);
545
+ const review = readReview(paths, bundleId);
546
+ const source = resolveSource(paths, bundle.sourceRef);
547
+ if (source.digest !== bundle.sourceDigest || source.digest !== review.sourceDigest) throw new Error('Knowledge proposal source is stale relative to review');
548
+ if (review.bundleDigest !== bundle.bundleDigest) throw new Error('Knowledge proposal bundle changed after review');
549
+ const byId = new Map(bundle.proposals.map((proposal) => [proposal.id, proposal]));
550
+ const selected = review.dispositions.filter(({ decision }) => ['accepted', 'amended'].includes(decision)).map((disposition) => materialisedProposal(byId.get(disposition.proposalId), disposition));
551
+ if (!selected.length) throw new Error('Knowledge proposal review has no accepted or amended records to materialise');
552
+ const outcomes = selected.map((proposal) => {
553
+ const destination = expectedDestination(paths, proposal.kind, proposal.destination);
554
+ return { proposalId: proposal.id, destination: proposal.destination, absolute: destination.absolute, markdown: proposal.proposedMarkdown, state: destinationState(destination.absolute, proposal.proposedMarkdown) };
555
+ });
556
+ const toWrite = outcomes.filter(({ state }) => state === 'additive');
557
+ const journal = { schema: 'ewai.knowledge-materialisation-transaction/v1', bundleId, phase: 'prepared', created: [], createdDigests: {}, destinations: toWrite.map(({ destination }) => destination) };
558
+ const transactionPath = journalPath(paths, bundleId);
559
+ atomicJson(transactionPath, journal);
560
+ try {
561
+ let writeCount = 0;
562
+ for (const item of toWrite) {
563
+ if (destinationState(item.absolute, item.markdown) !== 'additive') throw new Error(`Knowledge destination changed during materialisation: ${item.destination}`);
564
+ mkdirSync(resolve(item.absolute, '..'), { recursive: true });
565
+ writeFileSync(item.absolute, item.markdown, { encoding: 'utf8', flag: 'wx' });
566
+ journal.created.push(item.destination);
567
+ journal.createdDigests[item.destination] = sha256(item.markdown);
568
+ journal.phase = 'writing';
569
+ atomicJson(transactionPath, journal);
570
+ writeCount += 1;
571
+ if (input.testHooks?.failAfterWrites === writeCount) throw new Error('Knowledge materialisation was interrupted after a canonical write');
572
+ }
573
+ } catch (error) {
574
+ throw error;
575
+ }
576
+ const materialisedAt = new Date(input.now ?? Date.now()).toISOString();
577
+ const result = {
578
+ schema: 'ewai.knowledge-materialisation-result/v1',
579
+ bundleId,
580
+ approvedBy,
581
+ materialisedAt,
582
+ counts: { added: outcomes.filter(({ state }) => state === 'additive').length, alreadyCurrent: outcomes.filter(({ state }) => state === 'already-current').length, conflicts: outcomes.filter(({ state }) => state === 'conflict').length },
583
+ outcomes: outcomes.map(({ absolute: _absolute, markdown: _markdown, ...outcome }) => outcome),
584
+ idempotent: false
585
+ };
586
+ const ledgerPayload = { schema: MATERIALISATION_SCHEMA, bundleId, sourceDigest: source.digest, bundleDigest: bundle.bundleDigest, reviewDigest: review.reviewDigest, approvedBy, materialisedAt, result };
587
+ const ledger = { ...ledgerPayload, materialisationDigest: canonicalDigest(ledgerPayload) };
588
+ atomicJson(existingLedgerPath, ledger);
589
+ if (input.testHooks?.failAfterLedger === true) throw new Error('Knowledge materialisation was interrupted after the immutable ledger was persisted');
590
+ rmSync(transactionPath, { force: true });
591
+ const ledgerRelative = safeRelative(paths.projectRoot, existingLedgerPath);
592
+ const publication = publishLifecycleEventSafely(paths.projectRoot, 'ewai.knowledge-proposals.materialised', {
593
+ now: materialisedAt,
594
+ sourceKey: `knowledge-proposals:${bundleId}:${ledger.materialisationDigest}`,
595
+ sourceRevision: ledger.materialisationDigest,
596
+ occurredAt: materialisedAt,
597
+ streamId: `knowledge-proposals:${bundleId}`,
598
+ facts: { bundleId, materialisationDigest: ledger.materialisationDigest, addedCount: result.counts.added, alreadyCurrentCount: result.counts.alreadyCurrent, conflictCount: result.counts.conflicts, materialisedAt },
599
+ evidence: [ledgerRelative]
600
+ });
601
+ return { ...result, ledgerPath: ledgerRelative, lifecycle: { ...publication, status: publication.event ? 'published' : publication.status ?? 'publication-failed' } };
602
+ }
603
+
604
+ function recoveryFileState(paths, absolute, expectedDigest) {
605
+ // Inspect each component before reading: a lexical SPECS prefix alone does
606
+ // not prevent a replaced parent directory from redirecting recovery.
607
+ let current = paths.projectRoot;
608
+ const components = ['', ...relative(paths.projectRoot, absolute).split(sep)];
609
+ try {
610
+ for (let index = 0; index < components.length; index += 1) {
611
+ current = resolve(current, components[index]);
612
+ const stats = lstatSync(current, { throwIfNoEntry: false });
613
+ if (!stats) return { state: 'missing' };
614
+ if (stats.isSymbolicLink()) return { state: 'preserved', reason: 'symbolic link' };
615
+ if (index < components.length - 1) {
616
+ if (!stats.isDirectory()) return { state: 'preserved', reason: 'parent is not a directory' };
617
+ continue;
618
+ }
619
+ if (!stats.isFile() || stats.nlink !== 1) return { state: 'preserved', reason: 'not a single regular file' };
620
+ if (typeof expectedDigest !== 'string' || !digestPattern.test(expectedDigest)) return { state: 'preserved', reason: 'original digest unavailable' };
621
+ if (stats.size > MAX_SOURCE_BYTES) return { state: 'preserved', reason: 'file exceeds the recovery read limit' };
622
+ if (sha256(readFileSync(current)) !== expectedDigest) return { state: 'preserved', reason: 'content changed' };
623
+ const after = lstatSync(current);
624
+ if (!after.isFile() || after.nlink !== 1 || after.dev !== stats.dev || after.ino !== stats.ino || after.size !== stats.size || after.mtimeMs !== stats.mtimeMs || after.ctimeMs !== stats.ctimeMs) {
625
+ return { state: 'preserved', reason: 'file changed during inspection' };
626
+ }
627
+ return { state: 'unchanged' };
628
+ }
629
+ } catch {
630
+ return { state: 'preserved', reason: 'could not safely inspect the file' };
631
+ }
632
+ }
633
+
634
+ export function recoverKnowledgeMaterialisation(projectRoot, bundleId, input = {}) {
635
+ if (input.confirmed !== true) throw new Error('Knowledge materialisation recovery requires exact confirmation');
636
+ const paths = projectPaths(projectRoot);
637
+ assertInitialized(paths);
638
+ const path = journalPath(paths, bundleId);
639
+ if (!existsSync(path)) return { schema: 'ewai.knowledge-materialisation-recovery/v1', bundleId, status: 'not-required', removed: [] };
640
+ if (existsSync(materialisationPath(paths, bundleId))) {
641
+ rmSync(path, { force: true });
642
+ return { schema: 'ewai.knowledge-materialisation-recovery/v1', bundleId, status: 'finalised', removed: [] };
643
+ }
644
+ const journal = JSON.parse(readFileSync(path, 'utf8'));
645
+ if (journal.schema !== 'ewai.knowledge-materialisation-transaction/v1' || journal.bundleId !== bundleId || !['prepared', 'writing', 'committed'].includes(journal.phase) || !Array.isArray(journal.created) || !Array.isArray(journal.destinations)) throw new Error('Knowledge materialisation journal is not recoverable');
646
+ // Validate every owned path before removing any. Old v1 journals remain
647
+ // readable, but missing digests are not permission to delete existing files.
648
+ const owned = journal.created.map(destination => {
649
+ const valid = typeof destination === 'string' && journal.destinations.includes(destination) && Object.keys(taxonomy).some(kind => {
650
+ try { return expectedDestination(paths, kind, destination).value === destination; } catch { return false; }
651
+ });
652
+ if (!valid) throw new Error('Knowledge materialisation journal contains an unsafe destination');
653
+ return { destination, absolute: resolve(paths.projectRoot, destination) };
654
+ });
655
+ const removed = [];
656
+ const preserved = [];
657
+ for (const { destination, absolute } of owned) {
658
+ const state = recoveryFileState(paths, absolute, journal.createdDigests?.[destination]);
659
+ if (state.state === 'unchanged') { rmSync(absolute); removed.push(destination); }
660
+ else if (state.state === 'preserved') preserved.push({ destination, reason: state.reason });
661
+ }
662
+ if (preserved.length) {
663
+ const details = preserved.slice(0, 5).map(item => `${item.destination} (${item.reason})`).join('; ');
664
+ const extra = preserved.length > 5 ? `; and ${preserved.length - 5} more` : '';
665
+ const error = new Error(`Recovery paused: ${preserved.length} file(s) preserved: ${details}${extra}. The transaction journal has been kept. Review and back up those files before resolving them and retrying recovery.`);
666
+ error.code = 'KNOWLEDGE_RECOVERY_REQUIRES_REVIEW';
667
+ error.preserved = preserved;
668
+ error.removed = removed;
669
+ throw error;
670
+ }
671
+ rmSync(path, { force: true });
672
+ return { schema: 'ewai.knowledge-materialisation-recovery/v1', bundleId, status: 'recovered', removed };
673
+ }
674
+
675
+ export function readKnowledgeProposalWorkspace(projectRoot, options = {}) {
676
+ const paths = projectPaths(projectRoot);
677
+ assertInitialized(paths);
678
+ const sources = listKnowledgeSources(paths.projectRoot);
679
+ const root = proposalRoot(paths);
680
+ const bundles = existsSync(root)
681
+ ? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.isSymbolicLink() && bundleIdPattern.test(entry.name)).sort((a, b) => a.name.localeCompare(b.name)).flatMap((entry) => {
682
+ try {
683
+ const bundle = readBundle(paths, entry.name);
684
+ const review = existsSync(reviewPath(paths, entry.name)) ? readReview(paths, entry.name) : null;
685
+ const materialisation = existsSync(materialisationPath(paths, entry.name)) ? JSON.parse(readFileSync(materialisationPath(paths, entry.name), 'utf8')) : null;
686
+ return [{ bundleId: entry.name, sourceRef: bundle.sourceRef, sourceDigest: bundle.sourceDigest, bundleDigest: bundle.bundleDigest, recordedAt: bundle.recordedAt, proposalCount: bundle.proposals.length, reviewState: review ? 'reviewed' : 'pending', materialisationState: materialisation ? 'materialised' : existsSync(journalPath(paths, entry.name)) ? 'recovery-required' : 'not-materialised', counts: stateCounts(bundle.proposals) }];
687
+ } catch { return []; }
688
+ }) : [];
689
+ const selectedBundleId = options.bundleId ?? bundles[0]?.bundleId ?? null;
690
+ if (options.bundleId && !bundles.some(({ bundleId }) => bundleId === options.bundleId)) throw new Error(`Unknown knowledge proposal bundle: ${options.bundleId}`);
691
+ let bundle = null;
692
+ let review = null;
693
+ let materialisation = null;
694
+ let activePersonas = [];
695
+ if (selectedBundleId) {
696
+ const record = readBundle(paths, selectedBundleId);
697
+ const source = resolveSource(paths, record.sourceRef);
698
+ const dispositionMap = existsSync(reviewPath(paths, selectedBundleId)) ? new Map(readReview(paths, selectedBundleId).dispositions.map((item) => [item.proposalId, item])) : new Map();
699
+ bundle = { bundleId: record.bundleId, sourceRef: record.sourceRef, sourceDigest: record.sourceDigest, bundleDigest: record.bundleDigest, recordedAt: record.recordedAt, proposals: record.proposals.map((proposal) => ({ ...proposal, state: destinationState(resolve(paths.projectRoot, proposal.destination), proposal.proposedMarkdown), disposition: dispositionMap.get(proposal.id) ?? null })) };
700
+ review = existsSync(reviewPath(paths, selectedBundleId)) ? readReview(paths, selectedBundleId) : null;
701
+ materialisation = existsSync(materialisationPath(paths, selectedBundleId)) ? JSON.parse(readFileSync(materialisationPath(paths, selectedBundleId), 'utf8')) : null;
702
+ activePersonas = record.activePersonas?.length ? normalisePersonas(record.activePersonas) : contextualPersonas(paths.projectRoot, source, options);
703
+ }
704
+ return {
705
+ schema: WORKSPACE_SCHEMA,
706
+ sources,
707
+ bundles,
708
+ selectedBundleId,
709
+ bundle,
710
+ review: review ? { reviewDigest: review.reviewDigest, reviewedBy: review.reviewedBy, reviewedAt: review.reviewedAt, dispositions: review.dispositions } : null,
711
+ materialisation: materialisation?.result ?? null,
712
+ activePersonas,
713
+ permittedActions: { prepare: sources.length > 0, record: false, review: Boolean(bundle && !review), materialise: Boolean(review && !materialisation), recover: Boolean(selectedBundleId && existsSync(journalPath(paths, selectedBundleId))) },
714
+ authority: { personasAreAdvisory: true, reviewIsNotMaterialisation: true, conflictsRequireSeparateReconciliation: true, releaseAuthorityGranted: false },
715
+ notices: ['Proposals are advisory until named review and separate named materialisation.', 'Existing differing knowledge remains unchanged and is shown as a conflict.', KNOWLEDGE_PROPOSALS_DISCLAIMER]
716
+ };
717
+ }