@bevel-software/platform-core-backend 0.7.5 → 0.9.0

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 (282) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +8 -1
  4. package/dist/core/core-ports.d.ts.map +1 -1
  5. package/dist/core/core-ports.js.map +1 -1
  6. package/dist/core/create-core-server.d.ts.map +1 -1
  7. package/dist/core/create-core-server.js +75 -18
  8. package/dist/core/create-core-server.js.map +1 -1
  9. package/dist/core/create-core-services.d.ts +12 -5
  10. package/dist/core/create-core-services.d.ts.map +1 -1
  11. package/dist/core/create-core-services.js +69 -36
  12. package/dist/core/create-core-services.js.map +1 -1
  13. package/dist/modules/access/access-control.interface.d.ts +8 -7
  14. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.d.ts +1 -1
  16. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.js +6 -6
  18. package/dist/modules/access/access-control.service.js.map +1 -1
  19. package/dist/modules/access/access-declarations.d.ts +5 -5
  20. package/dist/modules/access/access-declarations.js +3 -3
  21. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  22. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  23. package/dist/modules/access/access-mutation.service.js +3 -3
  24. package/dist/modules/access/access-mutation.service.js.map +1 -1
  25. package/dist/modules/access/access-splice.js +4 -4
  26. package/dist/modules/access/access-splice.js.map +1 -1
  27. package/dist/modules/access/access.routes.d.ts +3 -1
  28. package/dist/modules/access/access.routes.d.ts.map +1 -1
  29. package/dist/modules/access/access.routes.js +22 -20
  30. package/dist/modules/access/access.routes.js.map +1 -1
  31. package/dist/modules/access/creator-access.d.ts +2 -2
  32. package/dist/modules/access/creator-access.js +5 -5
  33. package/dist/modules/access/creator-access.js.map +1 -1
  34. package/dist/modules/access/render-roles-yaml.d.ts +22 -0
  35. package/dist/modules/access/render-roles-yaml.d.ts.map +1 -0
  36. package/dist/modules/access/render-roles-yaml.js +56 -0
  37. package/dist/modules/access/render-roles-yaml.js.map +1 -0
  38. package/dist/modules/access/roles-admin.service.d.ts +33 -5
  39. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  40. package/dist/modules/access/roles-admin.service.js +44 -19
  41. package/dist/modules/access/roles-admin.service.js.map +1 -1
  42. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  43. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  44. package/dist/modules/code-mode/code-mode-names.js +5 -27
  45. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  46. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  47. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  48. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  49. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  50. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  51. package/dist/modules/mcp/mcp.service.js +33 -395
  52. package/dist/modules/mcp/mcp.service.js.map +1 -1
  53. package/dist/modules/plugins/index.d.ts +7 -0
  54. package/dist/modules/plugins/index.d.ts.map +1 -0
  55. package/dist/modules/plugins/index.js +6 -0
  56. package/dist/modules/plugins/index.js.map +1 -0
  57. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  58. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  59. package/dist/modules/plugins/join-proposals.js +67 -0
  60. package/dist/modules/plugins/join-proposals.js.map +1 -0
  61. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  62. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  63. package/dist/modules/plugins/join-requests.service.js +135 -0
  64. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  65. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  66. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  68. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  69. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  70. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.contract.js +36 -0
  72. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  73. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  74. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  75. package/dist/modules/plugins/plugins.routes.js +379 -0
  76. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  77. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  78. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  79. package/dist/modules/plugins/plugins.service.js +172 -0
  80. package/dist/modules/plugins/plugins.service.js.map +1 -0
  81. package/dist/modules/settings/setup.routes.d.ts +10 -1
  82. package/dist/modules/settings/setup.routes.d.ts.map +1 -1
  83. package/dist/modules/settings/setup.routes.js +111 -6
  84. package/dist/modules/settings/setup.routes.js.map +1 -1
  85. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  86. package/dist/modules/skills/pending-skills.service.js +7 -7
  87. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  88. package/dist/modules/skills/skills.contract.d.ts +4 -4
  89. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  90. package/dist/modules/skills/skills.contract.js +1 -1
  91. package/dist/modules/skills/skills.service.js +5 -5
  92. package/dist/modules/skills/skills.service.js.map +1 -1
  93. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  94. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  95. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  96. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  97. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  98. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  99. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  100. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  101. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  102. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  103. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  104. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  105. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  106. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  107. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  108. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  109. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  110. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  111. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  112. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  113. package/dist/modules/workflow/git/git.service.js +2 -2
  114. package/dist/modules/workflow/git/git.service.js.map +1 -1
  115. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  116. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  117. package/dist/modules/workspace/kb-seed.service.js +43 -10
  118. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  119. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  120. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  121. package/dist/modules/workspace/plugins-migration.js +379 -0
  122. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  123. package/dist/modules/workspace/startup/kb-git.d.ts +23 -0
  124. package/dist/modules/workspace/startup/kb-git.d.ts.map +1 -0
  125. package/dist/modules/workspace/startup/kb-git.js +86 -0
  126. package/dist/modules/workspace/startup/kb-git.js.map +1 -0
  127. package/dist/modules/workspace/startup/kb-startup-runner.d.ts +74 -0
  128. package/dist/modules/workspace/startup/kb-startup-runner.d.ts.map +1 -0
  129. package/dist/modules/workspace/startup/kb-startup-runner.js +528 -0
  130. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -0
  131. package/dist/modules/workspace/startup/on-server-start.d.ts +105 -0
  132. package/dist/modules/workspace/startup/on-server-start.d.ts.map +1 -0
  133. package/dist/modules/workspace/startup/on-server-start.js +21 -0
  134. package/dist/modules/workspace/startup/on-server-start.js.map +1 -0
  135. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts +46 -0
  136. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.d.ts.map +1 -0
  137. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +492 -0
  138. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -0
  139. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +23 -0
  140. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts.map +1 -0
  141. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +69 -0
  142. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -0
  143. package/dist/modules/workspace/startup/steps/seed-tree.d.ts +17 -0
  144. package/dist/modules/workspace/startup/steps/seed-tree.d.ts.map +1 -0
  145. package/dist/modules/workspace/startup/steps/seed-tree.js +109 -0
  146. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -0
  147. package/dist/modules/workspace/startup/steps/template-files.step.d.ts +103 -0
  148. package/dist/modules/workspace/startup/steps/template-files.step.d.ts.map +1 -0
  149. package/dist/modules/workspace/startup/steps/template-files.step.js +337 -0
  150. package/dist/modules/workspace/startup/steps/template-files.step.js.map +1 -0
  151. package/dist/modules/workspace/workspace.routes.js +3 -3
  152. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  153. package/dist/modules/workspace/workspace.service.d.ts +0 -9
  154. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  155. package/dist/modules/workspace/workspace.service.js +2 -30
  156. package/dist/modules/workspace/workspace.service.js.map +1 -1
  157. package/dist/shared/kb-layout.test.js +3 -3
  158. package/dist/shared/kb-layout.test.js.map +1 -1
  159. package/dist/shared/utcp-namespace.d.ts +6 -27
  160. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  161. package/dist/shared/utcp-namespace.js +6 -63
  162. package/dist/shared/utcp-namespace.js.map +1 -1
  163. package/dist/shared/variable-refs.d.ts +42 -0
  164. package/dist/shared/variable-refs.d.ts.map +1 -0
  165. package/dist/shared/variable-refs.js +60 -0
  166. package/dist/shared/variable-refs.js.map +1 -0
  167. package/kb-template/.bevelignore +1 -1
  168. package/kb-template/AGENTS.md +88 -35
  169. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  170. package/kb-template/access.md +36 -36
  171. package/kb-template/gitignore.template +16 -0
  172. package/migrations/meta/0000_snapshot.json +1479 -1479
  173. package/package.json +5 -4
  174. package/src/assets.ts +25 -25
  175. package/src/core/core-ports.ts +113 -106
  176. package/src/core/create-core-server.ts +85 -20
  177. package/src/core/create-core-services.ts +83 -42
  178. package/src/index.ts +69 -69
  179. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  180. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  181. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  182. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  183. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  184. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  185. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  186. package/src/modules/access/__tests__/roles-admin.service.test.ts +36 -2
  187. package/src/modules/access/access-control.interface.ts +8 -7
  188. package/src/modules/access/access-control.service.ts +7 -7
  189. package/src/modules/access/access-declarations.ts +5 -5
  190. package/src/modules/access/access-mutation.service.ts +3 -3
  191. package/src/modules/access/access-splice.ts +4 -4
  192. package/src/modules/access/access.routes.ts +23 -20
  193. package/src/modules/access/creator-access.ts +5 -5
  194. package/src/modules/access/render-roles-yaml.ts +65 -0
  195. package/src/modules/access/roles-admin.service.ts +45 -16
  196. package/src/modules/admin/admin-access.routes.ts +29 -29
  197. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  198. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  199. package/src/modules/auth/rate-limit.ts +45 -45
  200. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  201. package/src/modules/code-mode/code-mode-names.ts +10 -36
  202. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  203. package/src/modules/database/connection.ts +15 -15
  204. package/src/modules/database/schema.ts +11 -11
  205. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  206. package/src/modules/mcp/mcp.service.ts +57 -435
  207. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  208. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  209. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  210. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  211. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  212. package/src/modules/plugins/index.ts +17 -0
  213. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  214. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  215. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  216. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  217. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  218. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  219. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  220. package/src/modules/settings/__tests__/setup.routes.test.ts +124 -3
  221. package/src/modules/settings/setup.routes.ts +112 -5
  222. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  223. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  224. package/src/modules/skills/pending-skills.service.ts +7 -7
  225. package/src/modules/skills/skills.contract.ts +4 -4
  226. package/src/modules/skills/skills.service.ts +5 -5
  227. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  228. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  229. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  230. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  231. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  232. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  233. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  234. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  235. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  236. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  237. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  238. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  239. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  240. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  241. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  242. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  243. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  244. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  245. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  246. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  247. package/src/modules/workflow/git/git.service.ts +2 -2
  248. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  249. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  250. package/src/modules/workflow/workflow-hooks.ts +101 -101
  251. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  252. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  253. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  254. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  255. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  256. package/src/modules/workspace/__tests__/workspace.service.test.ts +27 -0
  257. package/src/modules/workspace/bevel-ignore.ts +66 -66
  258. package/src/modules/workspace/session-sink.ts +25 -25
  259. package/src/modules/workspace/startup/__tests__/kb-startup-runner.test.ts +495 -0
  260. package/src/modules/workspace/startup/kb-git.ts +94 -0
  261. package/src/modules/workspace/startup/kb-startup-runner.ts +597 -0
  262. package/src/modules/workspace/startup/on-server-start.ts +97 -0
  263. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +636 -0
  264. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +561 -0
  265. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +71 -0
  266. package/src/modules/workspace/startup/steps/seed-tree.ts +115 -0
  267. package/src/modules/workspace/startup/steps/template-files.step.ts +360 -0
  268. package/src/modules/workspace/workspace.routes.ts +3 -3
  269. package/src/modules/workspace/workspace.service.ts +2 -35
  270. package/src/modules/workspace/workspace.tools.ts +922 -922
  271. package/src/shared/__tests__/join-request.test.ts +13 -13
  272. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  273. package/src/shared/kb-layout.test.ts +3 -3
  274. package/src/shared/utcp-namespace.ts +10 -68
  275. package/src/shared/variable-refs.ts +64 -0
  276. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  277. package/src/modules/groups/index.ts +0 -17
  278. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +0 -444
  279. package/src/modules/workspace/kb-seed.interface.ts +0 -36
  280. package/src/modules/workspace/kb-seed.service.ts +0 -555
  281. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  282. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
@@ -1,555 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
- import { execFile } from 'node:child_process';
5
- import { promisify } from 'node:util';
6
- import { KNOWLEDGE_BASE_DIR, GROUPS_DIR } from '@bevel-software/platform-shared';
7
- import { IGNORE_FILENAME } from './bevel-ignore.js';
8
- import type { IKbSeedService } from './kb-seed.interface.js';
9
-
10
- const execFileAsync = promisify(execFile);
11
-
12
- /**
13
- * Fallback committer identity for seed commits. Mirrors the workspace clone's
14
- * bot identity so seed commits attribute to the same bot in `git log`.
15
- */
16
- const BOT_NAME = 'Bevel Workflow';
17
- const BOT_EMAIL = 'bevel-workflow@bevel.software';
18
-
19
- /**
20
- * The **required scaffolding** — the minimum an operational KB needs. When a
21
- * branch is loaded, any of these that are missing are added to it; the sample
22
- * ontology is NOT (it only seeds a fully-empty repo).
23
- *
24
- * Two kinds:
25
- * - {@link REQUIRED_FILES}: repo-root files added when the file is missing.
26
- * - {@link CORE_REQUIRED_DIRS} (plus any `extraDirs`): well-known root dirs the
27
- * app expects to exist; when a dir is entirely absent it's created by adding
28
- * its `<dir>/.gitkeep`. Keyed on the *directory's* existence, not the
29
- * `.gitkeep` file — so a branch that already has content under
30
- * `KnowledgeBase/` never gets a pointless placeholder.
31
- *
32
- * `roles.yaml` is in neither, and is not part of the template at all: it is
33
- * generated from `ADMIN_EMAIL` (see {@link renderRolesYaml}) and written
34
- * directly, so a repo can't be seeded with a stale hard-coded Admin list.
35
- */
36
- const REQUIRED_FILES: readonly string[] = ['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'];
37
-
38
- /**
39
- * The two roots CORE gives a knowledge base: the ontologies, and the groups
40
- * that hold skills and tools.
41
- *
42
- * `Data/`, `Agents/` and `Pipelines/` are deliberately absent. They scaffold
43
- * the agentic execution layer, which is not part of this platform — a core
44
- * deployment that created them would be handing every operator three empty
45
- * folders it has no feature to fill. A distribution that DOES own that layer
46
- * passes them as `extraDirs` (and ships a template carrying their READMEs);
47
- * the names stay reserved in `kb-layout.ts` either way, so a KB that has them
48
- * still renders them as roots rather than folding them into Knowledge.
49
- */
50
- export const CORE_REQUIRED_DIRS: readonly string[] = [KNOWLEDGE_BASE_DIR, GROUPS_DIR];
51
-
52
- /**
53
- * A reserved root must be ONE path segment — `Data`, not `Data/x`, `../x` or
54
- * `/x`. The name is joined onto the repo root, so anything else writes outside
55
- * the repo being seeded.
56
- *
57
- * Deliberately NOT a check against the reserved-root set in `kb-layout.ts`:
58
- * `Data`, `Agents` and `Pipelines` are all in that set, and they are precisely
59
- * what a distribution passes here. Being reserved is what makes a name worth
60
- * claiming — the file tree renders it as its own root instead of folding it
61
- * into Knowledge — so rejecting reserved names would reject the only real use.
62
- */
63
- function assertRootSegment(dir: string): void {
64
- if (
65
- !dir ||
66
- dir === '.' ||
67
- dir === '..' ||
68
- dir.includes('/') ||
69
- dir.includes('\\') ||
70
- path.isAbsolute(dir)
71
- ) {
72
- throw new Error(
73
- `Reserved KB root must be a single path segment (no separators, no ".."); got "${dir}"`,
74
- );
75
- }
76
- }
77
-
78
- /** Redact the token from any string that might surface in a log or error. */
79
- function redact(s: string): string {
80
- const token = process.env.GITHUB_TOKEN;
81
- return token ? s.replaceAll(token, '***') : s;
82
- }
83
-
84
- /** Generate `roles.yaml` granting Admin to each seed email. */
85
- function renderRolesYaml(adminEmails: readonly string[]): string {
86
- const lines = adminEmails.map((e) => ` - ${e}`).join('\n');
87
- return (
88
- '# Identity → role mapping for access control.\n' +
89
- '# Role names are case- and whitespace-insensitive. The `Admin` role is special:\n' +
90
- '# only Admins may edit this file, and at least one Admin must always exist.\n' +
91
- '#\n' +
92
- '# Generated by the Bevel platform at KB-seed time from ADMIN_EMAIL.\n' +
93
- 'roles:\n' +
94
- ' Admin:\n' +
95
- `${lines}\n`
96
- );
97
- }
98
-
99
- /**
100
- * Seeds/tops-up the KB *remote* so the rest of the app's assumption — that the
101
- * repo it clones already carries the protected branches and base scaffolding —
102
- * holds even for a brand-new or partially-populated GitHub repo. The template
103
- * ships inside the platform (`kb-template/`); this reads from it.
104
- *
105
- * Split into two moments so the file-level top-up rides along with the clone the
106
- * app already performs, rather than doing its own extra clones:
107
- * - {@link ensureRemoteSeeded} runs once before the first clone (an empty remote
108
- * can't be `clone -b`'d): it seeds an empty repo and makes sure every
109
- * protected branch *exists* as a ref.
110
- * - {@link topUpWorkspace} runs on the freshly-cloned workspace of whatever
111
- * branch a user loads: it fills in any missing base files on that branch.
112
- */
113
- export class KbSeedService implements IKbSeedService {
114
- /** Single-flight: the first caller runs the remote pass; the rest await its result. */
115
- private inFlight: Promise<void> | null = null;
116
-
117
- /**
118
- * @param kbRepoUrl Clone/push URL of the KB remote.
119
- * @param kbTemplateDir Filesystem path to the `kb-template/` seed source.
120
- * @param protectedBranches Branch names that must exist (e.g. current/target-company-state).
121
- * @param defaultBranch The branch new checkouts land on — used as the init
122
- * branch of the empty-seed commit and the preferred
123
- * base when creating a missing protected branch.
124
- * @param seedAdminEmails Admins written into the generated `roles.yaml`.
125
- * @param gitUsername Basic-auth username for git-over-HTTPS (provider-specific).
126
- * @param extraDirs Additional root folders this distribution reserves,
127
- * on top of {@link CORE_REQUIRED_DIRS}. Their
128
- * `.gitkeep` is written directly rather than copied,
129
- * so a distribution can claim a root without also
130
- * shipping a template entry for it.
131
- */
132
- constructor(
133
- kbRepoUrl: string | (() => string),
134
- private readonly kbTemplateDir: string,
135
- protectedBranches: readonly string[] | (() => readonly string[]),
136
- defaultBranch: string | (() => string),
137
- private readonly seedAdminEmails: readonly string[],
138
- gitUsername: string | (() => string) = 'x-access-token',
139
- extraDirs: readonly string[] = [],
140
- ) {
141
- // A getter is read per-operation, so a remote supplied through the setup
142
- // screen is seeded against without restarting; a string is still accepted.
143
- this.kbRepoUrl = typeof kbRepoUrl === 'function' ? kbRepoUrl : () => kbRepoUrl;
144
- // Read when seeding, not at construction: the branch model can be supplied
145
- // through the setup screen, which happens after this object exists.
146
- this.protectedBranches =
147
- typeof protectedBranches === 'function' ? protectedBranches : () => protectedBranches;
148
- this.defaultBranch = typeof defaultBranch === 'function' ? defaultBranch : () => defaultBranch;
149
- this.gitUsername = typeof gitUsername === 'function' ? gitUsername : () => gitUsername;
150
- // NOT a getter, unlike its neighbours above: the reserved roots are named
151
- // by the composition root in code, not collected on the setup screen, so
152
- // there is nothing to re-read. Validated once, here — every entry is joined
153
- // onto the repo root and onto `<dir>/.gitkeep`, so a separator or a `..`
154
- // would write outside the repo being seeded, and a bad value should fail at
155
- // boot beside the rest of the wiring rather than part-way through seeding
156
- // somebody's knowledge base. Same contract as `KB_DIR_NAME` in `CoreConfig`.
157
- for (const dir of extraDirs) {
158
- assertRootSegment(dir);
159
- // A root named after a required FILE is a typo with a silent outcome:
160
- // the file is laid down first in both seed paths, so `ensureRequiredDirs`
161
- // finds the path taken and skips it, and the directory the caller asked
162
- // for never appears with nothing said about why.
163
- if (REQUIRED_FILES.includes(dir)) {
164
- throw new Error(`Reserved KB root "${dir}" collides with a required file of the same name`);
165
- }
166
- }
167
- this.requiredDirs = [...CORE_REQUIRED_DIRS, ...extraDirs];
168
- }
169
-
170
- private readonly kbRepoUrl: () => string;
171
- private readonly protectedBranches: () => readonly string[];
172
- private readonly defaultBranch: () => string;
173
- private readonly gitUsername: () => string;
174
- /** Root folders this deployment guarantees — core's two plus any extras. */
175
- private readonly requiredDirs: readonly string[];
176
-
177
- ensureRemoteSeeded(): Promise<void> {
178
- // Cache the promise, not just a boolean, so concurrent first-callers share
179
- // one run. On failure we clear it so a later call can retry (a transient
180
- // network blip on ls-remote shouldn't wedge seeding for the whole process).
181
- if (!this.inFlight) {
182
- this.inFlight = this.run().catch((err) => {
183
- this.inFlight = null;
184
- throw err;
185
- });
186
- }
187
- return this.inFlight;
188
- }
189
-
190
- private async run(): Promise<void> {
191
- const heads = await this.lsRemoteHeads();
192
-
193
- if (heads.size === 0) {
194
- if (this.seedAdminEmails.length === 0) {
195
- throw new Error(
196
- `KB remote ${redact(this.kbRepoUrl())} is empty and cannot be seeded: ` +
197
- 'no initial Admin was supplied. A seeded KB with no Admin is unusable ' +
198
- '(access resolution requires at least one Admin). Unreachable in ' +
199
- 'normal operation — ADMIN_EMAIL is required at boot — so this guards ' +
200
- 'a caller that constructed the seeder with an empty list.',
201
- );
202
- }
203
- await this.seedEmptyRemote();
204
- return;
205
- }
206
-
207
- // Non-empty remote: make sure every protected branch exists as a ref so a
208
- // later `clone -b <branch>` succeeds. This creates the branch pointer only;
209
- // per-branch file top-up happens in `topUpWorkspace` when the branch loads.
210
- const base = this.pickBase(heads);
211
- for (const branch of this.protectedBranches()) {
212
- if (!heads.has(branch)) {
213
- await this.createBranchOnRemote(branch, base);
214
- }
215
- }
216
- }
217
-
218
- /**
219
- * Add any missing base scaffolding to an already-cloned workspace of `branch`,
220
- * then commit + push it. Called once per fresh clone (any branch a user loads),
221
- * so the check reuses the clone the app already made instead of a dedicated one.
222
- *
223
- * Best-effort: never throws. If the push is rejected (e.g. a concurrent update
224
- * to the branch), the local scaffolding commit is rolled back so the workspace
225
- * stays consistent with origin, and the top-up is retried on a future clone.
226
- */
227
- async topUpWorkspace(repoDir: string, branch: string): Promise<void> {
228
- // Scaffolding lands on PROTECTED branches only. A draft or suggestions
229
- // branch is somebody's change-in-waiting, and a seeder commit there
230
- // surfaces as noise in their change request's diff against the default
231
- // branch (a stray scaffolding file riding along in a skill proposal was
232
- // exactly this). Whatever the protected branches are missing, they get
233
- // when THEY load — and drafts fork from them.
234
- if (!this.protectedBranches().includes(branch)) return;
235
- try {
236
- const added: string[] = [];
237
- for (const rel of REQUIRED_FILES) {
238
- if (!(await this.exists(path.join(repoDir, rel)))) {
239
- await this.copyTemplateFile(rel, repoDir);
240
- added.push(rel);
241
- // Adding AGENTS.md to a knowledge base seeded before the rename
242
- // leaves it VISIBLE: that repo's `.bevelignore` lists CLAUDE.md and
243
- // knows nothing of the new name, so the conventions doc starts
244
- // showing up in the file tree and the agent view. We created the
245
- // mismatch by adding the file, so we close it here.
246
- if (rel === 'AGENTS.md') added.push(...(await this.mergeIgnorePattern(repoDir, rel)));
247
- }
248
- }
249
- // AGENTS.md is MANAGED, not merely seeded: the platform owns its
250
- // content, and a stale copy is replaced with the packaged template's on
251
- // every top-up (each fresh clone of a protected branch — in practice,
252
- // every server restart). The file's own header says so, which is what
253
- // makes overwriting edits a stated contract instead of a surprise.
254
- let agentsRefreshed = false;
255
- if (
256
- !added.includes('AGENTS.md') &&
257
- (await this.templateDiffers(repoDir, 'AGENTS.md'))
258
- ) {
259
- await this.copyTemplateFile('AGENTS.md', repoDir);
260
- added.push('AGENTS.md');
261
- agentsRefreshed = true;
262
- }
263
- added.push(...(await this.ensureRequiredDirs(repoDir)));
264
- if (!(await this.exists(path.join(repoDir, 'roles.yaml')))) {
265
- if (this.seedAdminEmails.length > 0) {
266
- await fs.writeFile(
267
- path.join(repoDir, 'roles.yaml'),
268
- renderRolesYaml(this.seedAdminEmails),
269
- 'utf8',
270
- );
271
- added.push('roles.yaml');
272
- } else {
273
- console.warn(
274
- `[kb-seed] Branch "${branch}" is missing roles.yaml and ADMIN_EMAIL is ` +
275
- 'unset — leaving it absent. Access resolution will fail until an ' +
276
- 'Admin roles.yaml exists; set ADMIN_EMAIL or add roles.yaml manually.',
277
- );
278
- }
279
- }
280
-
281
- if (added.length === 0) return; // already fully scaffolded → no-op
282
-
283
- // Stamp the bot identity so the commit has an author even if the clone
284
- // wasn't configured with one (the workspace clone already sets the same
285
- // values, so this is a harmless no-op there).
286
- await this.stampIdentity(repoDir);
287
- await this.git(repoDir, ['add', '--', ...added]);
288
- const message =
289
- agentsRefreshed && added.length === 1
290
- ? 'Update AGENTS.md to the current platform template'
291
- : `Add missing KB scaffolding: ${added.join(', ')}`;
292
- await this.git(repoDir, ['commit', '-m', message]);
293
- try {
294
- await this.git(repoDir, ['push', 'origin', `HEAD:refs/heads/${branch}`]);
295
- console.log(`[kb-seed] Topped up "${branch}" with: ${added.join(', ')}`);
296
- } catch (err) {
297
- // Roll the local branch back to origin so the workspace never sits ahead
298
- // of the remote (which would confuse the workflow git layer). The files
299
- // simply aren't present this session; a later fresh clone retries.
300
- console.warn(
301
- `[kb-seed] Could not push scaffolding to "${branch}", rolling back local commit:`,
302
- err instanceof Error ? redact(err.message) : String(err),
303
- );
304
- await this.git(repoDir, ['reset', '--hard', `origin/${branch}`]).catch(() => {});
305
- }
306
- } catch (err) {
307
- // Top-up is best-effort infrastructure — a failure here must not block the
308
- // user from loading the branch.
309
- console.warn(
310
- `[kb-seed] Scaffolding top-up for "${branch}" failed (non-fatal):`,
311
- err instanceof Error ? redact(err.message) : String(err),
312
- );
313
- }
314
- }
315
-
316
- /** Prefer the default branch as a base, else the first protected branch present, else any head. */
317
- private pickBase(heads: Set<string>): string {
318
- if (heads.has(this.defaultBranch())) return this.defaultBranch();
319
- const protectedPresent = this.protectedBranches().find((b) => heads.has(b));
320
- if (protectedPresent) return protectedPresent;
321
- // Deterministic pick so repeated runs behave identically.
322
- return [...heads].sort()[0];
323
- }
324
-
325
- /** `-c` args that inject credentials + longpaths, mirroring the workspace clone. */
326
- private credArgs(): string[] {
327
- const args = ['-c', 'core.longpaths=true'];
328
- const token = process.env.GITHUB_TOKEN;
329
- if (token) {
330
- // The helper reads GITHUB_TOKEN at runtime, so the value never appears in argv.
331
- // Username is provider-specific; the token is always the Basic-auth password.
332
- args.push(
333
- '-c',
334
- `credential.helper=!f() { echo "username=${this.gitUsername()}"; echo "password=$GITHUB_TOKEN"; }; f`,
335
- );
336
- }
337
- return args;
338
- }
339
-
340
- private async git(cwd: string | null, args: string[]): Promise<string> {
341
- try {
342
- const { stdout } = await execFileAsync('git', args, {
343
- cwd: cwd ?? undefined,
344
- env: { ...process.env, LC_ALL: 'C', LANG: 'C' },
345
- maxBuffer: 32 * 1024 * 1024,
346
- });
347
- return stdout.toString();
348
- } catch (err) {
349
- const msg = err instanceof Error ? err.message : String(err);
350
- let i = 0;
351
- while (i < args.length && args[i] === '-c') i += 2;
352
- throw new Error(`git ${args[i] ?? args[0]} failed: ${redact(msg)}`);
353
- }
354
- }
355
-
356
- /** Set of branch names on the remote (empty ⇒ uninitialised repo). */
357
- private async lsRemoteHeads(): Promise<Set<string>> {
358
- const out = await this.git(null, [...this.credArgs(), 'ls-remote', '--heads', this.kbRepoUrl()]);
359
- const heads = new Set<string>();
360
- for (const line of out.split('\n')) {
361
- const m = line.match(/\srefs\/heads\/(.+)$/);
362
- if (m) heads.add(m[1].trim());
363
- }
364
- return heads;
365
- }
366
-
367
- /** Run a block against a throwaway temp dir, always cleaned up. */
368
- private async withTempDir<T>(fn: (dir: string) => Promise<T>): Promise<T> {
369
- const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'kb-seed-'));
370
- try {
371
- return await fn(dir);
372
- } finally {
373
- await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
374
- }
375
- }
376
-
377
- private async stampIdentity(repo: string): Promise<void> {
378
- await this.git(repo, ['config', 'user.name', BOT_NAME]);
379
- await this.git(repo, ['config', 'user.email', BOT_EMAIL]);
380
- }
381
-
382
- /** Copy the entire template tree into `dest` (roles.yaml isn't in it — it's generated). */
383
- private async copyTemplateTree(dest: string): Promise<void> {
384
- const walk = async (relDir: string): Promise<void> => {
385
- const abs = path.join(this.kbTemplateDir, relDir);
386
- const entries = await fs.readdir(abs, { withFileTypes: true });
387
- for (const entry of entries) {
388
- const rel = relDir ? path.join(relDir, entry.name) : entry.name;
389
- if (entry.isDirectory()) {
390
- await walk(rel);
391
- } else {
392
- await this.copyTemplateFile(rel, dest);
393
- }
394
- }
395
- };
396
- await walk('');
397
- }
398
-
399
- /** Copy one template file (by repo-relative path) into `dest`, creating parents. */
400
- /**
401
- * Whether the repo's copy of `relPath` differs from the template's, modulo
402
- * line endings — a CRLF checkout of identical content must read as "same",
403
- * or the managed-file refresh would commit churn on every boot forever.
404
- */
405
- private async templateDiffers(repoDir: string, relPath: string): Promise<boolean> {
406
- const norm = (text: string) => text.replace(/\r\n?/g, '\n');
407
- const [current, template] = await Promise.all([
408
- fs.readFile(path.join(repoDir, relPath), 'utf8'),
409
- fs.readFile(path.join(this.kbTemplateDir, relPath), 'utf8'),
410
- ]);
411
- return norm(current) !== norm(template);
412
- }
413
-
414
- private async copyTemplateFile(relPath: string, dest: string): Promise<void> {
415
- const from = path.join(this.kbTemplateDir, relPath);
416
- const to = path.join(dest, relPath);
417
- await fs.mkdir(path.dirname(to), { recursive: true });
418
- await fs.copyFile(from, to);
419
- }
420
-
421
- /**
422
- * Ensure `.bevelignore` carries `pattern`, appending it when absent. Returns
423
- * the paths changed, for the commit.
424
- *
425
- * APPENDS — never rewrites. The file is the operator's, and every rule
426
- * already in it is theirs to keep; this adds one line under a comment saying
427
- * where it came from. Absent file, or a file that already lists the pattern,
428
- * is a no-op, so running it on every clone changes nothing after the first.
429
- *
430
- * Matched line-wise rather than by substring: a rule for `Groups/AGENTS.md`
431
- * is not a rule for the root `AGENTS.md`, and treating it as one would leave
432
- * the mismatch this exists to close.
433
- */
434
- private async mergeIgnorePattern(repoDir: string, pattern: string): Promise<string[]> {
435
- const ignorePath = path.join(repoDir, IGNORE_FILENAME);
436
- let current: string;
437
- try {
438
- current = await fs.readFile(ignorePath, 'utf8');
439
- } catch {
440
- return []; // No ignore file — the template's copy arrives with the pattern in it.
441
- }
442
- const lines = current.split('\n').map((l) => l.trim());
443
- if (lines.includes(pattern)) return [];
444
- const separator = current.endsWith('\n') ? '' : '\n';
445
- await fs.appendFile(
446
- ignorePath,
447
- `${separator}\n# Added by the platform: the conventions doc is not node content.\n${pattern}\n`,
448
- 'utf8',
449
- );
450
- return [IGNORE_FILENAME];
451
- }
452
-
453
- /**
454
- * Create any reserved root folder this repo is missing, as an empty
455
- * `<dir>/.gitkeep`. Returns the paths added, for the commit message.
456
- *
457
- * WRITTEN, not copied from the template. A `.gitkeep` is empty by definition,
458
- * and requiring a template entry per root would mean a distribution could not
459
- * reserve one without forking the packaged template.
460
- *
461
- * Keyed on the DIRECTORY's existence, not the `.gitkeep` file — a branch that
462
- * already has content under `KnowledgeBase/` never gets a pointless
463
- * placeholder alongside it.
464
- */
465
- private async ensureRequiredDirs(repoDir: string): Promise<string[]> {
466
- const added: string[] = [];
467
- for (const rootDir of this.requiredDirs) {
468
- const abs = path.join(repoDir, rootDir);
469
- // `lstat`, not `exists`: `fs.access` answers "is there something here?",
470
- // which is true of a FILE named `Groups` — and the old skip-if-present
471
- // check then did nothing and reported success, leaving a knowledge base
472
- // permanently missing a root it claims to guarantee. `lstat` rather than
473
- // `stat` so a SYMLINK is rejected too: a link named `Groups` is not a KB
474
- // layout, and one pointing outside the repo would make every later write
475
- // into it land somewhere nobody asked for.
476
- const found = await this.lstatOrNull(abs);
477
- if (found) {
478
- if (found.isDirectory()) continue;
479
- throw new Error(
480
- `KB root "${rootDir}" exists but is not a directory ` +
481
- `(${found.isSymbolicLink() ? 'symlink' : 'file'}). Remove or rename it — ` +
482
- 'the platform requires this name to be a folder.',
483
- );
484
- }
485
- await fs.mkdir(abs, { recursive: true });
486
- await fs.writeFile(path.join(abs, '.gitkeep'), '', 'utf8');
487
- added.push(`${rootDir}/.gitkeep`);
488
- }
489
- return added;
490
- }
491
-
492
- /** `lstat` without the throw — null when nothing is at `p`. */
493
- private async lstatOrNull(p: string): Promise<import('node:fs').Stats | null> {
494
- try {
495
- return await fs.lstat(p);
496
- } catch {
497
- return null;
498
- }
499
- }
500
-
501
- /** `git init` a temp repo, lay down the full template + generated roles.yaml, commit. */
502
- private async buildSeedCommit(dir: string): Promise<void> {
503
- await this.git(dir, ['init', '-b', this.defaultBranch()]);
504
- await this.stampIdentity(dir);
505
- await this.copyTemplateTree(dir);
506
- // Reserved roots the template does not carry. Without this the seed commit
507
- // would hold only what the template has, and a distribution's own roots
508
- // would appear a step later, when the first clone gets topped up — the same
509
- // folders, arriving in a second commit for no reason.
510
- await this.ensureRequiredDirs(dir);
511
- await fs.writeFile(path.join(dir, 'roles.yaml'), renderRolesYaml(this.seedAdminEmails), 'utf8');
512
- await this.git(dir, ['add', '-A']);
513
- await this.git(dir, ['commit', '-m', 'Seed knowledge base from Bevel template']);
514
- }
515
-
516
- /** Empty remote → one seed commit pushed to every protected branch. */
517
- private async seedEmptyRemote(): Promise<void> {
518
- await this.withTempDir(async (dir) => {
519
- await this.buildSeedCommit(dir);
520
- // Point every protected branch at the seed commit. The init branch is
521
- // already `defaultBranch`; create the rest as refs to HEAD.
522
- for (const branch of this.protectedBranches()) {
523
- if (branch !== this.defaultBranch()) {
524
- await this.git(dir, ['branch', branch]);
525
- }
526
- }
527
- await this.git(dir, ['remote', 'add', 'origin', this.kbRepoUrl()]);
528
- // Push only the protected branches — never the stray init branch if it
529
- // isn't itself protected.
530
- await this.git(dir, [...this.credArgs(), 'push', '-u', 'origin', ...this.protectedBranches()]);
531
- console.log(
532
- `[kb-seed] Seeded empty KB remote with branches: ${this.protectedBranches().join(', ')}`,
533
- );
534
- });
535
- }
536
-
537
- /** Create a missing protected branch on the remote, pointed at `base`'s tip. */
538
- private async createBranchOnRemote(branch: string, base: string): Promise<void> {
539
- await this.withTempDir(async (dir) => {
540
- await this.git(dir, [...this.credArgs(), 'clone', '--depth', '1', '-b', base, this.kbRepoUrl(), dir]);
541
- // Push base's fetched tip up under the new branch name.
542
- await this.git(dir, [...this.credArgs(), 'push', 'origin', `HEAD:refs/heads/${branch}`]);
543
- console.log(`[kb-seed] Created missing protected branch "${branch}" from "${base}"`);
544
- });
545
- }
546
-
547
- private async exists(p: string): Promise<boolean> {
548
- try {
549
- await fs.access(p);
550
- return true;
551
- } catch {
552
- return false;
553
- }
554
- }
555
- }
@@ -1,45 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { groupOfPath, GROUPS_DIR } from '@bevel-software/platform-shared';
3
-
4
- /**
5
- * `groupOfPath` is the whole of the grouping contract — the sidebar, the
6
- * catalog buckets and the access story all read a group off a path with it.
7
- * The cases that matter are the ones where "has a group" and "is under the
8
- * group root" come apart.
9
- */
10
- describe('groupOfPath', () => {
11
- it('reads the group from the Groups root', () => {
12
- expect(groupOfPath(`${GROUPS_DIR}/GTM/heyreach-campaign/SKILL.md`)).toBe('GTM');
13
- expect(groupOfPath(`${GROUPS_DIR}/GTM/heyreach.tool`)).toBe('GTM');
14
- expect(groupOfPath(`${GROUPS_DIR}/Engineering/review/architecture-review`)).toBe(
15
- 'Engineering',
16
- );
17
- });
18
-
19
- it('returns null for content directly under the root — it has no group folder', () => {
20
- // The regression this guards: `Groups/slack.tool` must NOT report a group
21
- // called "slack.tool", and `Groups/GTM` must not report itself.
22
- expect(groupOfPath(`${GROUPS_DIR}/slack.tool`)).toBeNull();
23
- expect(groupOfPath(`${GROUPS_DIR}/GTM`)).toBeNull();
24
- expect(groupOfPath(GROUPS_DIR)).toBeNull();
25
- });
26
-
27
- it('returns null outside the group root', () => {
28
- expect(groupOfPath('KnowledgeBase/Product/Knowledge/Foo.md')).toBeNull();
29
- expect(groupOfPath('Data/Engineering/Knowledge/Bar.md')).toBeNull();
30
- // The retired pre-merge roots are ordinary non-group folders now.
31
- expect(groupOfPath('Skills/GTM/heyreach-campaign')).toBeNull();
32
- expect(groupOfPath('Tools/NewsAgent/serper.tool')).toBeNull();
33
- expect(groupOfPath('access.md')).toBeNull();
34
- expect(groupOfPath('')).toBeNull();
35
- });
36
-
37
- it('is not fooled by a prefix match on the root name', () => {
38
- expect(groupOfPath('GroupsOld/GTM/x')).toBeNull();
39
- });
40
-
41
- it('tolerates leading and doubled separators', () => {
42
- expect(groupOfPath(`/${GROUPS_DIR}/GTM/x`)).toBe('GTM');
43
- expect(groupOfPath(`${GROUPS_DIR}//GTM//x`)).toBe('GTM');
44
- });
45
- });
File without changes