@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,362 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { basename, dirname, extname, isAbsolute, relative, resolve } from 'node:path';
4
+ import { listKnowledge, readKnowledgeDocument } from './knowledge.mjs';
5
+ import { openRuntimeDatabase } from './database.mjs';
6
+ import { runtimePaths } from './paths.mjs';
7
+
8
+ const markdownExtensions = new Set(['.md', '.markdown']);
9
+
10
+ function hash(value) {
11
+ return createHash('sha256').update(value).digest('hex');
12
+ }
13
+
14
+ function within(root, target) {
15
+ const path = relative(root, target);
16
+ return path === '' || (!path.startsWith('..') && !isAbsolute(path));
17
+ }
18
+
19
+ function normalized(path) {
20
+ return path.replaceAll('\\', '/');
21
+ }
22
+
23
+ function titleFromPath(path) {
24
+ return basename(path, extname(path)).replace(/[-_]+/g, ' ').replace(/\b\w/g, (character) => character.toUpperCase());
25
+ }
26
+
27
+ function stripFrontmatter(content) {
28
+ return content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '');
29
+ }
30
+
31
+ function parseSections(document) {
32
+ const content = stripFrontmatter(document.content);
33
+ const lines = content.split(/\r?\n/);
34
+ const headings = [];
35
+ let title = titleFromPath(document.path);
36
+ let hasTitle = false;
37
+ for (let index = 0; index < lines.length; index += 1) {
38
+ const match = /^(#{1,6})\s+(.+?)\s*$/.exec(lines[index]);
39
+ if (!match) continue;
40
+ const level = match[1].length;
41
+ const heading = match[2].replace(/\s+#+\s*$/, '').trim();
42
+ if (level === 1 && !hasTitle) {
43
+ title = heading;
44
+ hasTitle = true;
45
+ }
46
+ headings.push({ index, line: index + 1, level, heading });
47
+ }
48
+
49
+ if (!markdownExtensions.has(document.extension) || headings.length === 0) {
50
+ return {
51
+ title,
52
+ hasTitle: !markdownExtensions.has(document.extension),
53
+ sections: [{ heading: title, headingPath: title, line: 1, body: content }]
54
+ };
55
+ }
56
+
57
+ const sections = [];
58
+ const stack = [];
59
+ if (headings[0].index > 0 && lines.slice(0, headings[0].index).join('\n').trim()) {
60
+ sections.push({ heading: title, headingPath: title, line: 1, body: lines.slice(0, headings[0].index).join('\n') });
61
+ }
62
+ for (let position = 0; position < headings.length; position += 1) {
63
+ const current = headings[position];
64
+ const end = headings[position + 1]?.index ?? lines.length;
65
+ stack.length = current.level - 1;
66
+ stack[current.level - 1] = current.heading;
67
+ sections.push({
68
+ heading: current.heading,
69
+ headingPath: stack.filter(Boolean).join(' / '),
70
+ line: current.line,
71
+ body: lines.slice(current.index + 1, end).join('\n').trim()
72
+ });
73
+ }
74
+ return { title, hasTitle, sections };
75
+ }
76
+
77
+ function lineFor(content, offset) {
78
+ return content.slice(0, offset).split(/\r?\n/).length;
79
+ }
80
+
81
+ function resolveLink(projectRoot, sourcePath, rawTarget) {
82
+ if (!rawTarget || rawTarget.startsWith('#') || /^(?:https?:|mailto:|tel:|data:)/i.test(rawTarget)) return null;
83
+ const [pathPart, anchor = ''] = rawTarget.replace(/^<|>$/g, '').split('#', 2);
84
+ if (!pathPart) return null;
85
+ let decoded;
86
+ try {
87
+ decoded = decodeURIComponent(pathPart.split('?')[0]);
88
+ } catch {
89
+ decoded = pathPart.split('?')[0];
90
+ }
91
+ const specsRoot = runtimePaths(projectRoot).specsRoot;
92
+ const sourceAbsolute = resolve(specsRoot, sourcePath);
93
+ let absolute;
94
+ if (decoded.startsWith('SPECS/')) absolute = resolve(specsRoot, decoded.slice('SPECS/'.length));
95
+ else if (isAbsolute(decoded)) absolute = resolve(decoded);
96
+ else absolute = resolve(dirname(sourceAbsolute), decoded);
97
+
98
+ if (within(specsRoot, absolute)) {
99
+ return { kind: 'specs', targetPath: normalized(relative(specsRoot, absolute)), anchor, exists: existsSync(absolute) };
100
+ }
101
+ if (within(resolve(projectRoot), absolute)) {
102
+ return { kind: 'project', targetPath: normalized(relative(projectRoot, absolute)), anchor, exists: existsSync(absolute) };
103
+ }
104
+ return { kind: 'absolute-local', targetPath: absolute, anchor, exists: existsSync(absolute) };
105
+ }
106
+
107
+ function parseLinks(projectRoot, document) {
108
+ if (!markdownExtensions.has(document.extension)) return [];
109
+ const links = [];
110
+ const markdownLink = /!?\[[^\]]*]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g;
111
+ for (const match of document.content.matchAll(markdownLink)) {
112
+ const resolved = resolveLink(projectRoot, document.path, match[1]);
113
+ if (!resolved) continue;
114
+ links.push({ ...resolved, sourcePath: document.path, rawTarget: match[1], line: lineFor(document.content, match.index) });
115
+ }
116
+ const codeReference = /`((?:SPECS|src|app|tests?|public|config|docs)\/[A-Za-z0-9_.@/\-[\]]+(?::\d+)?)`/g;
117
+ for (const match of document.content.matchAll(codeReference)) {
118
+ const rawTarget = match[1].replace(/:\d+$/, '');
119
+ if (links.some((link) => link.rawTarget === rawTarget && link.line === lineFor(document.content, match.index))) continue;
120
+ const projectTarget = rawTarget.startsWith('SPECS/')
121
+ ? resolveLink(projectRoot, document.path, rawTarget)
122
+ : { kind: 'project', targetPath: rawTarget, anchor: '', exists: existsSync(resolve(projectRoot, rawTarget)) };
123
+ if (!projectTarget) continue;
124
+ links.push({ ...projectTarget, sourcePath: document.path, rawTarget, line: lineFor(document.content, match.index) });
125
+ }
126
+ return links;
127
+ }
128
+
129
+ function inventory(projectRoot) {
130
+ const documents = listKnowledge(projectRoot).documents
131
+ .map((entry) => readKnowledgeDocument(projectRoot, entry.path))
132
+ .filter(Boolean)
133
+ .map((document) => ({ ...document, contentHash: hash(document.content) }));
134
+ const fingerprint = hash(JSON.stringify(documents.map((document) => [document.path, document.contentHash])));
135
+ return { documents, fingerprint };
136
+ }
137
+
138
+ function currentFingerprint(database) {
139
+ return database.prepare("SELECT value FROM runtime_meta WHERE key = 'palace_source_fingerprint'").get()?.value ?? '';
140
+ }
141
+
142
+ export function refreshPalaceIndex(projectRoot, options = {}) {
143
+ const source = inventory(projectRoot);
144
+ const database = openRuntimeDatabase(projectRoot);
145
+ try {
146
+ const indexedDocuments = database.prepare('SELECT COUNT(*) AS count FROM palace_documents').get().count;
147
+ if (!options.force && currentFingerprint(database) === source.fingerprint && indexedDocuments === source.documents.length) {
148
+ const latest = database.prepare("SELECT * FROM palace_index_runs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
149
+ return {
150
+ schema: 'ewai.palace-index/v1',
151
+ status: 'current',
152
+ runId: latest?.id ?? null,
153
+ documents: indexedDocuments,
154
+ sections: latest?.section_count ?? 0,
155
+ links: latest?.link_count ?? 0,
156
+ fingerprint: source.fingerprint
157
+ };
158
+ }
159
+
160
+ const run = database.prepare(`
161
+ INSERT INTO palace_index_runs (status, source_fingerprint)
162
+ VALUES ('running', ?) RETURNING id
163
+ `).get(source.fingerprint);
164
+ let sectionCount = 0;
165
+ let linkCount = 0;
166
+ database.exec('BEGIN IMMEDIATE');
167
+ try {
168
+ database.exec('DELETE FROM palace_sections_fts; DELETE FROM palace_links; DELETE FROM palace_documents;');
169
+ const insertDocument = database.prepare(`
170
+ INSERT INTO palace_documents (
171
+ path, title, extension, content_hash, content_length, source_mtime, has_title, section_count
172
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
173
+ `);
174
+ const insertSection = database.prepare(`
175
+ INSERT INTO palace_sections_fts (path, title, heading, body, heading_path, line)
176
+ VALUES (?, ?, ?, ?, ?, ?)
177
+ `);
178
+ const insertLink = database.prepare(`
179
+ INSERT INTO palace_links (
180
+ source_path, target_path, target_anchor, kind, exists_on_disk, line, raw_target
181
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
182
+ `);
183
+ for (const document of source.documents) {
184
+ const parsed = parseSections(document);
185
+ const links = parseLinks(projectRoot, document);
186
+ insertDocument.run(
187
+ document.path, parsed.title, document.extension, document.contentHash,
188
+ document.content.length, document.updatedAt, parsed.hasTitle ? 1 : 0, parsed.sections.length
189
+ );
190
+ for (const section of parsed.sections) {
191
+ insertSection.run(document.path, parsed.title, section.heading, section.body, section.headingPath, section.line);
192
+ sectionCount += 1;
193
+ }
194
+ for (const link of links) {
195
+ insertLink.run(
196
+ link.sourcePath, link.targetPath, link.anchor, link.kind,
197
+ link.exists ? 1 : 0, link.line, link.rawTarget
198
+ );
199
+ linkCount += 1;
200
+ }
201
+ }
202
+ database.prepare(`
203
+ INSERT INTO runtime_meta (key, value, updated_at)
204
+ VALUES ('palace_source_fingerprint', ?, CURRENT_TIMESTAMP)
205
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
206
+ `).run(source.fingerprint);
207
+ database.prepare(`
208
+ UPDATE palace_index_runs SET
209
+ status = 'completed', document_count = ?, section_count = ?, link_count = ?,
210
+ completed_at = CURRENT_TIMESTAMP, summary = ?
211
+ WHERE id = ?
212
+ `).run(source.documents.length, sectionCount, linkCount, 'SPECS Mind Palace index refreshed.', run.id);
213
+ database.exec('COMMIT');
214
+ } catch (error) {
215
+ database.exec('ROLLBACK');
216
+ database.prepare(`UPDATE palace_index_runs SET status = 'failed', completed_at = CURRENT_TIMESTAMP, summary = ? WHERE id = ?`)
217
+ .run(error.message, run.id);
218
+ throw error;
219
+ }
220
+ return {
221
+ schema: 'ewai.palace-index/v1', status: 'completed', runId: run.id,
222
+ documents: source.documents.length, sections: sectionCount, links: linkCount,
223
+ fingerprint: source.fingerprint
224
+ };
225
+ } finally {
226
+ database.close();
227
+ }
228
+ }
229
+
230
+ export function palaceIndexStatus(projectRoot) {
231
+ const source = inventory(projectRoot);
232
+ const database = openRuntimeDatabase(projectRoot);
233
+ try {
234
+ const latest = database.prepare("SELECT * FROM palace_index_runs WHERE status = 'completed' ORDER BY id DESC LIMIT 1").get();
235
+ const counts = database.prepare('SELECT COUNT(*) AS documents, COALESCE(SUM(section_count), 0) AS sections FROM palace_documents').get();
236
+ if (!latest) return { schema: 'ewai.palace-index-status/v1', status: 'missing', documents: 0, sections: 0 };
237
+ return {
238
+ schema: 'ewai.palace-index-status/v1',
239
+ status: currentFingerprint(database) === source.fingerprint ? 'fresh' : 'stale',
240
+ runId: latest.id,
241
+ documents: counts.documents,
242
+ sections: counts.sections,
243
+ indexedAt: latest.completed_at
244
+ };
245
+ } finally {
246
+ database.close();
247
+ }
248
+ }
249
+
250
+ function ftsQuery(query) {
251
+ const terms = String(query).normalize('NFKC').match(/[\p{L}\p{N}_-]+/gu) ?? [];
252
+ return terms.map((term) => `"${term.replaceAll('"', '""')}"*`).join(' OR ');
253
+ }
254
+
255
+ export function searchPalace(projectRoot, query, options = {}) {
256
+ const index = refreshPalaceIndex(projectRoot);
257
+ const expression = ftsQuery(query);
258
+ if (!expression) return { schema: 'ewai.palace-search/v1', query, index, results: [] };
259
+ const requestedLimit = Number(options.limit ?? 50);
260
+ const limit = Number.isFinite(requestedLimit) && requestedLimit > 0 ? Math.floor(requestedLimit) : 50;
261
+ const database = openRuntimeDatabase(projectRoot);
262
+ try {
263
+ const rows = database.prepare(`
264
+ SELECT path, title, heading, heading_path, CAST(line AS INTEGER) AS line,
265
+ snippet(palace_sections_fts, 3, '⟦', '⟧', ' … ', 24) AS snippet,
266
+ bm25(palace_sections_fts, 4.0, 3.0, 2.0, 1.0) AS rank
267
+ FROM palace_sections_fts
268
+ WHERE palace_sections_fts MATCH ?
269
+ ORDER BY rank, path, CAST(line AS INTEGER)
270
+ LIMIT ?
271
+ `).all(expression, limit * 3);
272
+ const seen = new Set();
273
+ const results = [];
274
+ for (const row of rows) {
275
+ if (seen.has(row.path)) continue;
276
+ seen.add(row.path);
277
+ results.push(row);
278
+ if (results.length >= limit) break;
279
+ }
280
+ return { schema: 'ewai.palace-search/v1', query, index, results };
281
+ } finally {
282
+ database.close();
283
+ }
284
+ }
285
+
286
+ function finding(code, severity, path, message, details = {}) {
287
+ return { code, severity, path, message, details };
288
+ }
289
+
290
+ export function palaceTidiness(projectRoot) {
291
+ const index = refreshPalaceIndex(projectRoot);
292
+ const database = openRuntimeDatabase(projectRoot);
293
+ try {
294
+ const documents = database.prepare('SELECT * FROM palace_documents ORDER BY path').all();
295
+ const findings = [];
296
+ for (const document of documents) {
297
+ if (document.content_length === 0) findings.push(finding('empty-document', 'warning', document.path, 'The document is empty.'));
298
+ if (markdownExtensions.has(document.extension) && !document.has_title) {
299
+ findings.push(finding('missing-title', 'warning', document.path, 'The Markdown document has no level-one title.'));
300
+ }
301
+ }
302
+ for (const link of database.prepare(`SELECT * FROM palace_links WHERE exists_on_disk = 0 ORDER BY source_path, line`).all()) {
303
+ findings.push(finding(
304
+ 'broken-internal-link', 'warning', link.source_path,
305
+ `The reference ${link.raw_target} does not resolve on disk.`,
306
+ { target: link.target_path, kind: link.kind, line: link.line }
307
+ ));
308
+ }
309
+ for (const group of database.prepare(`
310
+ SELECT content_hash, GROUP_CONCAT(path, '||') AS paths, COUNT(*) AS count
311
+ FROM palace_documents WHERE content_length > 0
312
+ GROUP BY content_hash HAVING COUNT(*) > 1
313
+ `).all()) {
314
+ const paths = group.paths.split('||');
315
+ for (const path of paths) findings.push(finding('duplicate-content', 'notice', path, 'Another Palace document has identical content.', { matches: paths.filter((candidate) => candidate !== path) }));
316
+ }
317
+ const linked = new Set();
318
+ for (const link of database.prepare("SELECT source_path, target_path FROM palace_links WHERE kind = 'specs' AND exists_on_disk = 1").all()) {
319
+ linked.add(link.source_path);
320
+ linked.add(link.target_path);
321
+ }
322
+ for (const document of documents) {
323
+ if (!markdownExtensions.has(document.extension) || linked.has(document.path)) continue;
324
+ if (/(?:^|\/)(?:README|_template)\.md$/i.test(document.path)) continue;
325
+ findings.push(finding('orphaned-document', 'notice', document.path, 'No other indexed SPECS document links to or from this document.'));
326
+ }
327
+
328
+ const counts = { warning: 0, notice: 0 };
329
+ for (const item of findings) counts[item.severity] = (counts[item.severity] ?? 0) + 1;
330
+ return {
331
+ schema: 'ewai.palace-tidiness/v1',
332
+ status: counts.warning > 0 || counts.notice > 0 ? 'housekeeping-recommended' : 'tidy',
333
+ documents: documents.length,
334
+ sections: index.sections,
335
+ links: index.links,
336
+ counts,
337
+ findings
338
+ };
339
+ } finally {
340
+ database.close();
341
+ }
342
+ }
343
+
344
+ export function palaceHousekeeping(projectRoot) {
345
+ const tidiness = palaceTidiness(projectRoot);
346
+ const grouped = new Map();
347
+ for (const item of tidiness.findings) {
348
+ if (!grouped.has(item.code)) grouped.set(item.code, []);
349
+ grouped.get(item.code).push(item);
350
+ }
351
+ return {
352
+ schema: 'ewai.palace-housekeeping/v1',
353
+ status: tidiness.status,
354
+ requiresApproval: true,
355
+ changedFiles: 0,
356
+ groups: [...grouped.entries()].map(([code, findings]) => ({ code, count: findings.length, findings })),
357
+ guidance: tidiness.status === 'tidy'
358
+ ? 'The Mind Palace is tidy. A no-op is the correct housekeeping result.'
359
+ : 'Use $ewai-palace-housekeeping to review these findings with the user before changing canonical SPECS.',
360
+ tidiness
361
+ };
362
+ }
@@ -0,0 +1,58 @@
1
+ import { dirname, resolve } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { projectPaths } from '../paths.mjs';
4
+
5
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
6
+
7
+ export function runtimePaths(projectRoot) {
8
+ const project = projectPaths(projectRoot);
9
+ const root = project.projectRoot;
10
+ const runtimeRoot = project.runtimeRoot;
11
+ return {
12
+ projectRoot: root,
13
+ runtimeRoot,
14
+ dataRoot: resolve(runtimeRoot, 'data'),
15
+ databasePath: resolve(runtimeRoot, 'data/pipeline.sqlite'),
16
+ runtimeStateRoot: resolve(runtimeRoot, 'runtime'),
17
+ dashboardStatePath: resolve(runtimeRoot, 'runtime/dashboard.json'),
18
+ checkinStatePath: resolve(runtimeRoot, 'runtime/checkin.json'),
19
+ guidedDiscoveryDraftPath: resolve(runtimeRoot, 'runtime/guided-discovery.json'),
20
+ guidedIntentDraftPath: resolve(runtimeRoot, 'runtime/guided-intent.json'),
21
+ evidenceDepthRoot: resolve(runtimeRoot, 'runtime/evidence-depth'),
22
+ evidenceDepthPreparationPath: resolve(runtimeRoot, 'runtime/evidence-depth/preparation.json'),
23
+ prototypeIterationsRoot: resolve(runtimeRoot, 'runtime/prototype-iterations'),
24
+ prototypeIterationRoot: (slug) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug),
25
+ prototypePlanPreparationPath: (slug) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug, 'plan-preparation.json'),
26
+ prototypeCyclePreparationPath: (slug, cycle) => resolve(runtimeRoot, 'runtime/prototype-iterations', slug, `cycle-${cycle}-preparation.json`),
27
+ phaseContributionsRoot: resolve(runtimeRoot, 'runtime/phase-contributions'),
28
+ phaseContributionDraftPath: (slug, phase) => resolve(runtimeRoot, 'runtime/phase-contributions', slug, `${phase}.json`),
29
+ contextPacksRoot: resolve(runtimeRoot, 'runtime/context-packs'),
30
+ contextPackManifestRoot: resolve(runtimeRoot, 'runtime/context-packs/manifests'),
31
+ contextPackCacheRoot: resolve(runtimeRoot, 'runtime/context-packs/cache'),
32
+ starterMaterialisationRoot: resolve(runtimeRoot, 'starter-materialisation'),
33
+ starterStagingRoot: resolve(runtimeRoot, 'starter-materialisation/staging'),
34
+ errorReportsRoot: resolve(runtimeRoot, 'error-reports'),
35
+ errorReportDraftsRoot: resolve(runtimeRoot, 'error-reports/drafts'),
36
+ errorReportPackagesRoot: resolve(runtimeRoot, 'error-reports/packages'),
37
+ errorReportReceiptsRoot: resolve(runtimeRoot, 'error-reports/receipts'),
38
+ errorReportArchiveRoot: resolve(runtimeRoot, 'error-reports/archive'),
39
+ errorReportAdaptersRoot: resolve(runtimeRoot, 'error-reports/adapters'),
40
+ errorReportSettingsPath: resolve(runtimeRoot, 'error-reports/settings.json'),
41
+ teamHubRoot: resolve(runtimeRoot, 'team-hub'),
42
+ teamHubConnectionPath: resolve(runtimeRoot, 'team-hub/connection.json'),
43
+ teamHubAttemptsRoot: resolve(runtimeRoot, 'team-hub/attempts'),
44
+ teamHubAcceptedReceiptPath: resolve(runtimeRoot, 'team-hub/accepted-receipt.json'),
45
+ teamHubResourcesRoot: resolve(runtimeRoot, 'team-hub/resources'),
46
+ teamHubResourceCacheRoot: resolve(runtimeRoot, 'team-hub/resources/cache'),
47
+ teamHubResourceStagingRoot: resolve(runtimeRoot, 'team-hub/resources/staging'),
48
+ teamHubResourceReceiptsRoot: resolve(runtimeRoot, 'team-hub/resources/receipts'),
49
+ teamHubResourceInstalledPath: resolve(runtimeRoot, 'team-hub/resources/installed.json'),
50
+ teamHubManagedPackRoot: resolve(runtimeRoot, 'packs/team-hub'),
51
+ logRoot: resolve(runtimeRoot, 'logs'),
52
+ dashboardLogPath: resolve(runtimeRoot, 'logs/dashboard.log'),
53
+ specsRoot: project.specsRoot,
54
+ specsRelative: project.specsRelative,
55
+ intentsRoot: resolve(project.specsRoot, '2.Purpose/intents'),
56
+ publicRoot: resolve(moduleDir, '../../public')
57
+ };
58
+ }
@@ -0,0 +1,255 @@
1
+ import { createHash } from 'node:crypto';
2
+
3
+ const tierRanks = { project: 4, premium: 3, personal: 2, core: 1 };
4
+
5
+ function cleanText(value) {
6
+ return String(value ?? '').trim();
7
+ }
8
+
9
+ function words(value) {
10
+ return cleanText(value)
11
+ .toLowerCase()
12
+ .split(/[^a-z0-9]+/)
13
+ .filter((word) => word.length >= 4);
14
+ }
15
+
16
+ function personaHaystack(persona) {
17
+ return [persona.id, persona.name, persona.category, persona.description, ...(persona.tags ?? []), ...(persona.capabilities ?? [])]
18
+ .map((value) => cleanText(value).toLowerCase())
19
+ .filter(Boolean)
20
+ .join(' ');
21
+ }
22
+
23
+ function engagementReason(persona, contextLabel, matches) {
24
+ const concerns = matches.slice(0, 2).join(' and ');
25
+ return `${persona.name} is engaged to examine ${cleanText(contextLabel).toLowerCase()} through ${concerns || 'its relevant project lens'}.`;
26
+ }
27
+
28
+ export function selectContextualPersonas(options = {}) {
29
+ const signals = [...new Set((options.signals ?? []).map(cleanText).filter(Boolean))];
30
+ const contextText = JSON.stringify(options.context ?? {}).toLowerCase();
31
+ const catalogue = Array.isArray(options.personaCatalogue) ? options.personaCatalogue : [];
32
+ const limit = Math.max(1, Math.min(8, Number(options.limit ?? 4)));
33
+ const ranked = catalogue.map((persona) => {
34
+ const haystack = personaHaystack(persona);
35
+ const matchedSignals = [];
36
+ let score = 0;
37
+ for (const signal of signals) {
38
+ const normalised = signal.toLowerCase();
39
+ const tokenMatches = words(normalised).filter((word) => haystack.includes(word));
40
+ if (haystack.includes(normalised) || tokenMatches.length) {
41
+ matchedSignals.push(signal);
42
+ score += haystack.includes(normalised) ? 6 : tokenMatches.length * 2;
43
+ }
44
+ }
45
+ for (const tag of [...(persona.tags ?? []), ...(persona.capabilities ?? [])]) {
46
+ if (cleanText(tag).length >= 4 && contextText.includes(cleanText(tag).toLowerCase())) score += 1;
47
+ }
48
+ return { persona, score, matchedSignals };
49
+ }).filter((candidate) => candidate.score > 0)
50
+ .sort((left, right) => (
51
+ right.score - left.score
52
+ || (tierRanks[right.persona.tier] ?? 0) - (tierRanks[left.persona.tier] ?? 0)
53
+ || cleanText(left.persona.name).localeCompare(cleanText(right.persona.name))
54
+ || cleanText(left.persona.id).localeCompare(cleanText(right.persona.id))
55
+ ));
56
+
57
+ const selected = [];
58
+ for (const tier of ['project', 'premium']) {
59
+ const candidate = ranked.find((item) => item.persona.tier === tier);
60
+ if (candidate && !selected.includes(candidate)) selected.push(candidate);
61
+ }
62
+ for (const candidate of ranked) {
63
+ if (selected.length >= limit) break;
64
+ if (!selected.includes(candidate)) selected.push(candidate);
65
+ }
66
+
67
+ return selected.slice(0, limit).map(({ persona, matchedSignals }) => ({
68
+ id: cleanText(persona.id),
69
+ name: cleanText(persona.name),
70
+ tier: cleanText(persona.tier) || 'core',
71
+ category: cleanText(persona.category),
72
+ description: cleanText(persona.description).slice(0, 240),
73
+ matchedSignals,
74
+ engagementReason: engagementReason(persona, options.contextLabel || 'this context', matchedSignals)
75
+ }));
76
+ }
77
+
78
+ const policyStageSignals = Object.freeze({
79
+ facts: ['policy', 'evidence', 'data', 'privacy', 'security', 'design facts'],
80
+ evaluation: ['policy', 'evidence', 'security', 'destination', 'environment', 'controls'],
81
+ review: ['policy review', 'decision', 'evidence', 'security', 'risk'],
82
+ exception: ['exception', 'scope', 'expiry', 'ownership', 'compensating controls', 'risk'],
83
+ });
84
+
85
+ export function selectPolicyPersonas(context = {}, personaCatalogue = []) {
86
+ const stage = cleanText(context.stage || 'facts').toLowerCase();
87
+ const dimensions = (context.dimensions ?? []).map((dimension) => cleanText(dimension).replaceAll('_', '-')).filter(Boolean);
88
+ const signals = [...new Set([...(policyStageSignals[stage] ?? policyStageSignals.facts), ...dimensions])];
89
+ const activePersonas = selectContextualPersonas({
90
+ contextLabel: `${stage} policy design work`,
91
+ signals,
92
+ context: context.context ?? {},
93
+ personaCatalogue,
94
+ limit: 4,
95
+ });
96
+ const premiumInstalled = personaCatalogue.some((persona) => cleanText(persona.tier) === 'premium');
97
+ return {
98
+ schema: 'ewai.policy-persona-engagement/v1',
99
+ stage,
100
+ activePersonas,
101
+ baseline: {
102
+ standardModelAvailable: true,
103
+ completeWithoutPremium: true,
104
+ },
105
+ premium: {
106
+ installed: premiumInstalled,
107
+ reason: premiumInstalled
108
+ ? 'Installed premium personas may add relevant challenge depth.'
109
+ : 'The premium persona library is not installed; core policy behaviour remains available.',
110
+ },
111
+ authority: {
112
+ advisory: true,
113
+ mayConfirmFacts: false,
114
+ mayApproveReviews: false,
115
+ mayApproveExceptions: false,
116
+ },
117
+ };
118
+ }
119
+
120
+ const evidenceDepthSignals = Object.freeze({
121
+ architecture: ['architecture', 'dependency', 'integration', 'evidence', 'archaeology'],
122
+ data: ['data', 'privacy', 'retention', 'classification', 'evidence'],
123
+ security: ['security', 'trust boundary', 'identity', 'privacy', 'evidence'],
124
+ product: ['product', 'user journey', 'outcomes', 'acceptance', 'evidence'],
125
+ delivery: ['delivery', 'testing', 'release', 'quality', 'evidence'],
126
+ governance: ['governance', 'policy', 'compliance', 'decision', 'evidence'],
127
+ operations: ['operations', 'hosting', 'recovery', 'observability', 'evidence'],
128
+ });
129
+
130
+ export function selectEvidenceDepthPersonas(context = {}, personaCatalogue = []) {
131
+ const dimension = cleanText(context.dimension || 'architecture').toLowerCase();
132
+ const gapConditions = (context.gapConditions ?? []).map(cleanText).filter(Boolean);
133
+ const signals = [...new Set([...(evidenceDepthSignals[dimension] ?? evidenceDepthSignals.architecture), ...gapConditions])];
134
+ const activePersonas = selectContextualPersonas({
135
+ contextLabel: `${dimension} evidence-depth investigation`,
136
+ signals,
137
+ context: context.context ?? {},
138
+ personaCatalogue,
139
+ limit: 4,
140
+ });
141
+ const premiumInstalled = personaCatalogue.some((persona) => cleanText(persona.tier) === 'premium');
142
+ return {
143
+ schema: 'ewai.evidence-depth-persona-engagement/v1',
144
+ dimension,
145
+ activePersonas,
146
+ baseline: {
147
+ standardModelAvailable: true,
148
+ completeWithoutPremium: true,
149
+ },
150
+ premium: {
151
+ installed: premiumInstalled,
152
+ reason: premiumInstalled
153
+ ? 'Installed premium personas may add relevant specialist challenge depth.'
154
+ : 'The premium persona library is not installed; core and project persona capability remains complete.',
155
+ },
156
+ authority: {
157
+ advisory: true,
158
+ mayDeclareOwnerEvidence: false,
159
+ maySelectDepth: false,
160
+ mayApproveGrouping: false,
161
+ },
162
+ };
163
+ }
164
+
165
+ const prototypeStageSignals = Object.freeze({
166
+ plan: [
167
+ 'product outcomes', 'user journey', 'acceptance', 'information architecture',
168
+ 'decision ownership', 'accessibility', 'prototype plan',
169
+ ],
170
+ design: [
171
+ 'rendered viewport', 'visual hierarchy', 'interaction', 'responsive',
172
+ 'accessibility', 'usability', 'prototype critique',
173
+ ],
174
+ });
175
+
176
+ function personaAvailability(personaCatalogue) {
177
+ const counts = Object.fromEntries(['core', 'project', 'personal', 'premium'].map((tier) => [
178
+ tier,
179
+ personaCatalogue.filter((persona) => cleanText(persona.tier || 'core') === tier).length,
180
+ ]));
181
+ return {
182
+ standardModel: { available: true },
183
+ core: { available: counts.core > 0, count: counts.core },
184
+ project: { available: counts.project > 0, count: counts.project },
185
+ personal: { available: counts.personal > 0, count: counts.personal },
186
+ premium: { available: counts.premium > 0, count: counts.premium },
187
+ };
188
+ }
189
+
190
+ function prototypeFallback(personaCatalogue, stage, limit) {
191
+ const preferredIds = stage === 'plan'
192
+ ? ['project.product-owner', 'ewai.core.end-user']
193
+ : ['ewai.core.end-user', 'project.product-owner'];
194
+ const ranked = [...personaCatalogue].sort((left, right) => {
195
+ const leftPreferred = preferredIds.indexOf(cleanText(left.id));
196
+ const rightPreferred = preferredIds.indexOf(cleanText(right.id));
197
+ const leftRank = leftPreferred === -1 ? 100 : leftPreferred;
198
+ const rightRank = rightPreferred === -1 ? 100 : rightPreferred;
199
+ return leftRank - rightRank
200
+ || (tierRanks[right.tier] ?? 0) - (tierRanks[left.tier] ?? 0)
201
+ || cleanText(left.name).localeCompare(cleanText(right.name));
202
+ });
203
+ const baseline = ranked.filter((persona) => ['project', 'core'].includes(cleanText(persona.tier || 'core'))).slice(0, limit);
204
+ return baseline.map((persona) => ({
205
+ id: cleanText(persona.id),
206
+ name: cleanText(persona.name),
207
+ tier: cleanText(persona.tier) || 'core',
208
+ category: cleanText(persona.category),
209
+ description: cleanText(persona.description).slice(0, 240),
210
+ matchedSignals: ['baseline prototype review'],
211
+ engagementReason: `${cleanText(persona.name)} is engaged as a baseline ${stage} review perspective because no stronger catalogue match was found.`,
212
+ }));
213
+ }
214
+
215
+ export function selectPrototypeReviewPersonas(context = {}, personaCatalogue = []) {
216
+ const stage = cleanText(context.stage || 'plan').toLowerCase();
217
+ if (!['plan', 'design'].includes(stage)) throw new Error('Prototype persona engagement stage must be plan or design');
218
+ const catalogue = Array.isArray(personaCatalogue)
219
+ ? personaCatalogue.map((persona) => ({ ...persona, tier: cleanText(persona.tier) || 'core' }))
220
+ : [];
221
+ const suppliedSignals = (context.signals ?? []).map(cleanText).filter(Boolean);
222
+ const signals = [...new Set(suppliedSignals.length ? suppliedSignals : (prototypeStageSignals[stage] ?? []))];
223
+ const limit = Math.max(1, Math.min(8, Number(context.limit ?? 6)));
224
+ let activePersonas = selectContextualPersonas({
225
+ contextLabel: stage === 'plan' ? 'prototype plan review' : 'rendered prototype review',
226
+ signals,
227
+ context: context.context ?? {},
228
+ personaCatalogue: catalogue,
229
+ limit,
230
+ });
231
+ if (!activePersonas.length) activePersonas = prototypeFallback(catalogue, stage, limit);
232
+ const availability = personaAvailability(catalogue);
233
+ const fingerprintInput = {
234
+ stage,
235
+ signals: [...signals].sort(),
236
+ activePersonas: activePersonas.map(({ id, tier, matchedSignals }) => ({ id, tier, matchedSignals: [...matchedSignals].sort() })).sort((left, right) => left.id.localeCompare(right.id)),
237
+ availability,
238
+ };
239
+ const premiumNotice = availability.premium.available
240
+ ? 'Installed premium personas are eligible when their specialism matches this stage.'
241
+ : 'Premium personas are not installed; standard model, core, and project persona review remains complete.';
242
+ const personalNotice = availability.personal.available
243
+ ? 'Installed personal personas are eligible when their local perspective matches this stage.'
244
+ : 'Personal personas are not installed and are optional enrichment.';
245
+ return {
246
+ schema: 'ewai.prototype-persona-engagement/v1',
247
+ stage,
248
+ selectionFingerprint: `sha256:${createHash('sha256').update(JSON.stringify(fingerprintInput)).digest('hex')}`,
249
+ activePersonas,
250
+ availability,
251
+ baseline: { completeWithoutOptionalPersonas: true },
252
+ notices: [premiumNotice, personalNotice],
253
+ authority: { advisory: true, maySelectPrototype: false, mayApproveManualQa: false },
254
+ };
255
+ }