@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,444 +0,0 @@
1
- import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
- import { execFile } from 'node:child_process';
3
- import { promisify } from 'node:util';
4
- import fs from 'node:fs/promises';
5
- import path from 'node:path';
6
- import os from 'node:os';
7
- import { KbSeedService } from '../kb-seed.service.js';
8
- import { defaultKbTemplateDir } from '../../../assets.js';
9
-
10
- const execFileAsync = promisify(execFile);
11
-
12
- /** The real seed template shipped inside this package (see assets.ts). */
13
- const TEMPLATE_DIR = defaultKbTemplateDir();
14
-
15
- const PROTECTED = ['current-company-state', 'target-company-state'];
16
- const DEFAULT_BRANCH = 'current-company-state';
17
- const ADMINS = ['alice@example.com', 'bob@example.com'];
18
-
19
- async function git(cwd: string, args: string[]): Promise<string> {
20
- const { stdout } = await execFileAsync('git', args, {
21
- cwd,
22
- env: {
23
- ...process.env,
24
- GIT_AUTHOR_NAME: 'Test', GIT_AUTHOR_EMAIL: 't@x.com',
25
- GIT_COMMITTER_NAME: 'Test', GIT_COMMITTER_EMAIL: 't@x.com',
26
- },
27
- });
28
- return stdout.toString();
29
- }
30
-
31
- function makeSeeder(
32
- upstream: string,
33
- admins: readonly string[] = ADMINS,
34
- extraDirs: readonly string[] = [],
35
- ): KbSeedService {
36
- return new KbSeedService(
37
- upstream,
38
- TEMPLATE_DIR,
39
- PROTECTED,
40
- DEFAULT_BRANCH,
41
- admins,
42
- 'x-access-token',
43
- extraDirs,
44
- );
45
- }
46
-
47
- /** Clone a branch of the upstream into a fresh checkout for inspection. */
48
- async function checkout(root: string, upstream: string, branch: string): Promise<string> {
49
- const dir = path.join(root, `checkout-${branch}-${Math.random().toString(36).slice(2)}`);
50
- await git(root, ['clone', '-b', branch, upstream, dir]);
51
- return dir;
52
- }
53
-
54
- async function exists(p: string): Promise<boolean> {
55
- try {
56
- await fs.access(p);
57
- return true;
58
- } catch {
59
- return false;
60
- }
61
- }
62
-
63
- async function headCommitCount(root: string, upstream: string, branch: string): Promise<number> {
64
- const dir = await checkout(root, upstream, branch);
65
- const out = await git(dir, ['rev-list', '--count', 'HEAD']);
66
- return Number(out.trim());
67
- }
68
-
69
- describe('KbSeedService', () => {
70
- let root: string;
71
-
72
- beforeEach(async () => {
73
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'kb-seed-test-'));
74
- });
75
- afterEach(async () => {
76
- await fs.rm(root, { recursive: true, force: true }).catch(() => {});
77
- });
78
-
79
- /** A bare, branch-less "empty" remote. */
80
- async function emptyUpstream(): Promise<string> {
81
- const upstream = path.join(root, 'upstream.git');
82
- await git(root, ['init', '--bare', upstream]);
83
- return upstream;
84
- }
85
-
86
- /** A bare remote pre-seeded on `branches`, each with `files` written at the root. */
87
- async function seededUpstream(
88
- branches: string[],
89
- files: Record<string, string>,
90
- ): Promise<string> {
91
- const upstream = path.join(root, 'upstream.git');
92
- await git(root, ['init', '--bare', upstream]);
93
- const seed = path.join(root, '.seed');
94
- await fs.mkdir(seed);
95
- await git(seed, ['init', '-b', branches[0]]);
96
- await git(seed, ['remote', 'add', 'origin', upstream]);
97
- for (const [name, content] of Object.entries(files)) {
98
- const abs = path.join(seed, name);
99
- await fs.mkdir(path.dirname(abs), { recursive: true });
100
- await fs.writeFile(abs, content, 'utf8');
101
- }
102
- await git(seed, ['add', '-A']);
103
- await git(seed, ['commit', '-m', 'pre-seed']);
104
- for (const b of branches.slice(1)) await git(seed, ['branch', b]);
105
- await git(seed, ['push', 'origin', ...branches]);
106
- return upstream;
107
- }
108
-
109
- describe('empty remote → full seed', () => {
110
- it('seeds every protected branch with base scaffolding + generated roles.yaml', async () => {
111
- const upstream = await emptyUpstream();
112
- await makeSeeder(upstream).ensureRemoteSeeded();
113
-
114
- for (const branch of PROTECTED) {
115
- const dir = await checkout(root, upstream, branch);
116
- expect(await exists(path.join(dir, 'access.md'))).toBe(true);
117
- expect(await exists(path.join(dir, 'AGENTS.md'))).toBe(true);
118
- expect(await exists(path.join(dir, '.bevelignore'))).toBe(true);
119
- expect(await exists(path.join(dir, '.gitignore'))).toBe(true);
120
- // Core's two roots are seeded (kept present via their .gitkeep).
121
- expect(await exists(path.join(dir, 'KnowledgeBase/.gitkeep'))).toBe(true);
122
- expect(await exists(path.join(dir, 'Groups/.gitkeep'))).toBe(true);
123
- // …and only those two. `Data/`, `Agents/` and `Pipelines/` scaffold an
124
- // agentic execution layer this platform does not have, so seeding them
125
- // would hand every operator three empty folders nothing can fill.
126
- for (const enterpriseOnly of ['Data', 'Agents', 'Pipelines']) {
127
- expect(await exists(path.join(dir, enterpriseOnly))).toBe(false);
128
- }
129
-
130
- const roles = await fs.readFile(path.join(dir, 'roles.yaml'), 'utf8');
131
- for (const email of ADMINS) expect(roles).toContain(email);
132
- // The template's illustrative placeholder must NOT leak into the seed.
133
- expect(roles).not.toContain('admin@example.com');
134
- }
135
- });
136
-
137
- // Unreachable through the app — `ADMIN_EMAIL` is required at boot, so the
138
- // list always has exactly one entry. Kept as a guard on the seeder's own
139
- // contract: a KB seeded with no Admin cannot resolve access at all, and
140
- // silently writing an Admin-less roles.yaml would be worse than refusing.
141
- it('refuses to seed an empty remote with no initial Admin', async () => {
142
- const upstream = await emptyUpstream();
143
- await expect(makeSeeder(upstream, []).ensureRemoteSeeded()).rejects.toThrow(
144
- /no initial Admin/,
145
- );
146
- });
147
- });
148
-
149
- describe('existing remote → conservative top-up', () => {
150
- it('creates a missing protected branch off an existing one', async () => {
151
- // Only current-company-state exists; target-company-state is missing.
152
- const upstream = await seededUpstream([DEFAULT_BRANCH], {
153
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
154
- 'access.md': 'existing',
155
- 'AGENTS.md': 'existing',
156
- '.bevelignore': 'x',
157
- '.gitignore': 'x',
158
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
159
- 'Groups/.gitkeep': '',
160
- });
161
-
162
- await makeSeeder(upstream).ensureRemoteSeeded();
163
-
164
- const heads = await git(root, ['ls-remote', '--heads', upstream]);
165
- expect(heads).toContain('refs/heads/target-company-state');
166
- });
167
-
168
- it('topUpWorkspace adds only missing scaffolding and never overwrites existing files', async () => {
169
- const upstream = await seededUpstream(PROTECTED, {
170
- // roles.yaml + access.md already exist with custom content — must survive.
171
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
172
- 'access.md': 'CUSTOM ACCESS RULES',
173
- // AGENTS.md / .bevelignore / .gitignore are absent → should be added.
174
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
175
- 'Groups/.gitkeep': '',
176
- });
177
-
178
- // Simulate the app loading the branch: a fresh clone, then top-up on it.
179
- const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
180
- await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
181
-
182
- // Verify the pushed remote state via an independent clone.
183
- const dir = await checkout(root, upstream, DEFAULT_BRANCH);
184
- // Existing files preserved verbatim.
185
- expect(await fs.readFile(path.join(dir, 'access.md'), 'utf8')).toBe('CUSTOM ACCESS RULES');
186
- expect(await fs.readFile(path.join(dir, 'roles.yaml'), 'utf8')).toContain('keep@example.com');
187
- // Missing scaffolding added.
188
- expect(await exists(path.join(dir, 'AGENTS.md'))).toBe(true);
189
- expect(await exists(path.join(dir, '.bevelignore'))).toBe(true);
190
- // Sample content is NOT restored — the real KnowledgeBase already had content,
191
- // so no placeholder .gitkeep is dropped into it, and no ExampleOntology appears.
192
- expect(await exists(path.join(dir, 'KnowledgeBase/.gitkeep'))).toBe(false);
193
- expect(await exists(path.join(dir, 'KnowledgeBase/ExampleOntology'))).toBe(false);
194
- });
195
-
196
- it('replaces an edited AGENTS.md with the current template — the file is managed', async () => {
197
- const upstream = await seededUpstream(PROTECTED, {
198
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
199
- 'AGENTS.md': '# my own conventions\n',
200
- '.bevelignore': 'AGENTS.md\n',
201
- '.gitignore': '',
202
- 'access.md': 'CUSTOM ACCESS RULES',
203
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
204
- 'Groups/.gitkeep': '',
205
- });
206
-
207
- const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
208
- await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
209
-
210
- const dir = await checkout(root, upstream, DEFAULT_BRANCH);
211
- const agents = await fs.readFile(path.join(dir, 'AGENTS.md'), 'utf8');
212
- // The template's copy, not the edit — and the template says WHY, so the
213
- // overwrite is a stated contract rather than a surprise.
214
- expect(agents).not.toContain('my own conventions');
215
- expect(agents).toContain('managed by the platform');
216
- // Everything the platform does NOT manage is preserved verbatim.
217
- expect(await fs.readFile(path.join(dir, 'access.md'), 'utf8')).toBe('CUSTOM ACCESS RULES');
218
- const log = await git(dir, ['log', '-1', '--pretty=%s']);
219
- expect(log.trim()).toBe('Update AGENTS.md to the current platform template');
220
- });
221
-
222
- it('a current AGENTS.md makes no refresh commit, even with CRLF endings', async () => {
223
- const template = await fs.readFile(
224
- path.join(__dirname, '../../../../kb-template/AGENTS.md'),
225
- 'utf8',
226
- );
227
- const upstream = await seededUpstream(PROTECTED, {
228
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
229
- // Same content, Windows endings — must read as "same", or the managed
230
- // refresh would commit churn on every boot forever.
231
- //
232
- // `\r?\n`, not `\n`: on a Windows checkout (`core.autocrlf=true`) the
233
- // template already HAS CRLF, so converting every `\n` would produce
234
- // `\r\r\n` — a file that differs from the template for real, failing
235
- // this test for a reason that has nothing to do with what it asserts.
236
- // Matching the optional `\r` normalizes first, so the fixture is
237
- // exactly-CRLF on every platform.
238
- 'AGENTS.md': template.replace(/\r?\n/g, '\r\n'),
239
- '.bevelignore': 'AGENTS.md\n',
240
- '.gitignore': '',
241
- 'access.md': 'CUSTOM ACCESS RULES',
242
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
243
- 'Groups/.gitkeep': '',
244
- });
245
-
246
- const first = await checkout(root, upstream, DEFAULT_BRANCH);
247
- await makeSeeder(upstream).topUpWorkspace(first, DEFAULT_BRANCH);
248
- const after = await headCommitCount(root, upstream, DEFAULT_BRANCH);
249
- // Only the pre-seed commit: nothing was missing, nothing was stale.
250
- expect(after).toBe(1);
251
- });
252
-
253
- it('topUpWorkspace is idempotent — a second load makes no new commit', async () => {
254
- const upstream = await seededUpstream(PROTECTED, {
255
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
256
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
257
- 'Groups/.gitkeep': '',
258
- });
259
-
260
- const first = await checkout(root, upstream, DEFAULT_BRANCH);
261
- await makeSeeder(upstream).topUpWorkspace(first, DEFAULT_BRANCH);
262
- const after1 = await headCommitCount(root, upstream, DEFAULT_BRANCH);
263
-
264
- // A later reload: fresh clone reflects the pushed scaffolding, so top-up
265
- // finds nothing missing and makes no new commit.
266
- const second = await checkout(root, upstream, DEFAULT_BRANCH);
267
- await makeSeeder(upstream).topUpWorkspace(second, DEFAULT_BRANCH);
268
- const after2 = await headCommitCount(root, upstream, DEFAULT_BRANCH);
269
-
270
- expect(after2).toBe(after1);
271
- });
272
-
273
- });
274
-
275
- /**
276
- * A distribution that owns folders core has no feature for — the agentic
277
- * execution layer's `Data/`, `Agents/`, `Pipelines/` — claims them through
278
- * `extraDirs` (wired from `CorePorts.kbExtraRootDirs`).
279
- *
280
- * The point is that claiming a root does NOT require forking the packaged
281
- * template: `TEMPLATE_DIR` here is core's, which carries no entry for any of
282
- * these, and the folders still arrive. Their `.gitkeep` is written rather
283
- * than copied for exactly that reason.
284
- */
285
- /**
286
- * A knowledge base seeded before AGENTS.md was named that: top-up adds the
287
- * file, and the `.bevelignore` already there lists only the old name — so
288
- * without this the conventions doc starts showing in the file tree and the
289
- * agent view on every existing deployment. We created the mismatch by adding
290
- * the file, so top-up closes it.
291
- */
292
- describe('the pre-rename ignore file', () => {
293
- const legacy = (ignore: string): Record<string, string> => ({
294
- 'KnowledgeBase/.gitkeep': '',
295
- 'Groups/.gitkeep': '',
296
- 'access.md': '---\nwrite:\n - Admin\n---\n',
297
- 'roles.yaml': 'roles:\n Admin:\n - a@example.com\n',
298
- '.gitignore': '',
299
- '.bevelignore': ignore,
300
- });
301
-
302
- it('learns about AGENTS.md when top-up adds one', async () => {
303
- const upstream = await seededUpstream([DEFAULT_BRANCH], {
304
- ...legacy('# mine\n.gitignore\nCLAUDE.md\nMy-Own-Rule/\n'),
305
- 'CLAUDE.md': '# conventions',
306
- });
307
- const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
308
- await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
309
-
310
- const dir = await checkout(root, upstream, DEFAULT_BRANCH);
311
- expect(await exists(path.join(dir, 'AGENTS.md'))).toBe(true);
312
- const ignore = await fs.readFile(path.join(dir, '.bevelignore'), 'utf8');
313
- expect(ignore.split('\n').map((l) => l.trim())).toContain('AGENTS.md');
314
- // The operator's own rules survive — this appends, it does not rewrite.
315
- expect(ignore).toContain('My-Own-Rule/');
316
- expect(ignore).toContain('CLAUDE.md');
317
- });
318
-
319
- it('is left alone when it already knows the pattern', async () => {
320
- const upstream = await seededUpstream([DEFAULT_BRANCH], legacy('AGENTS.md\n'));
321
- const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
322
- await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
323
-
324
- const dir = await checkout(root, upstream, DEFAULT_BRANCH);
325
- const ignore = await fs.readFile(path.join(dir, '.bevelignore'), 'utf8');
326
- expect(ignore.split('\n').filter((l) => l.trim() === 'AGENTS.md')).toHaveLength(1);
327
- });
328
- });
329
-
330
- describe('distribution-reserved roots', () => {
331
- it('creates extra roots that the template knows nothing about', async () => {
332
- const upstream = await emptyUpstream();
333
- await makeSeeder(upstream, ADMINS, ['Data', 'Agents', 'Pipelines']).ensureRemoteSeeded();
334
-
335
- const dir = await checkout(root, upstream, DEFAULT_BRANCH);
336
- // Core's two, still there.
337
- expect(await exists(path.join(dir, 'KnowledgeBase/.gitkeep'))).toBe(true);
338
- expect(await exists(path.join(dir, 'Groups/.gitkeep'))).toBe(true);
339
- // …and the three this distribution asked for.
340
- for (const claimed of ['Data', 'Agents', 'Pipelines']) {
341
- expect(await exists(path.join(dir, `${claimed}/.gitkeep`))).toBe(true);
342
- }
343
- });
344
-
345
- /**
346
- * The name is joined onto the repo root and onto `<dir>/.gitkeep`, so a
347
- * separator or a `..` would write outside the repo being seeded. Refused
348
- * at CONSTRUCTION, so a bad value surfaces at boot rather than part-way
349
- * through seeding somebody's knowledge base.
350
- */
351
- it.each(['../escape', 'nested/dir', '/absolute', '..', '.', ''])(
352
- 'refuses %j as a reserved root',
353
- (bad) => {
354
- expect(() => makeSeeder('unused', ADMINS, [bad])).toThrow(/single path segment/);
355
- },
356
- );
357
-
358
- /**
359
- * Being reserved is what makes a name worth claiming — the file tree
360
- * renders it as its own root instead of folding it into Knowledge — so the
361
- * names in `kb-layout.ts` must stay ACCEPTED here. A guard that rejected
362
- * them would reject the only real use of this argument.
363
- */
364
- /**
365
- * A root named after a required FILE is a typo whose outcome is silence:
366
- * the file is laid down first in both seed paths, so `ensureRequiredDirs`
367
- * finds the path taken and skips it, and the directory never appears with
368
- * nothing said about why. Refused at construction instead.
369
- */
370
- it.each(['access.md', 'AGENTS.md', '.bevelignore', '.gitignore'])(
371
- 'refuses %j, which is a required file rather than a root',
372
- (collide) => {
373
- expect(() => makeSeeder('unused', ADMINS, [collide])).toThrow(/collides with a required file/);
374
- },
375
- );
376
-
377
- it('accepts the reserved names a distribution actually claims', () => {
378
- expect(() => makeSeeder('unused', ADMINS, ['Data', 'Agents', 'Pipelines'])).not.toThrow();
379
- });
380
-
381
- /**
382
- * A FILE named `Groups` used to satisfy the presence check — `fs.access`
383
- * answers "is there something here?" — so the seeder skipped it, said
384
- * nothing, and left a knowledge base permanently missing a root it claims
385
- * to guarantee. Now it says so.
386
- *
387
- * Surfaces as a warning rather than a crash on this path: `topUpWorkspace`
388
- * is documented best-effort and must never block a user from loading a
389
- * branch. The point is that the condition is REPORTED at all.
390
- */
391
- it('refuses a required root that exists as a file, instead of skipping it', async () => {
392
- const upstream = await seededUpstream([DEFAULT_BRANCH], {
393
- 'AGENTS.md': '# x',
394
- 'access.md': 'read:\n - everyone\n',
395
- '.bevelignore': '',
396
- '.gitignore': '',
397
- 'KnowledgeBase/.gitkeep': '',
398
- // Not a folder.
399
- Groups: 'this is a file, not the groups root',
400
- });
401
- const repoDir = await checkout(root, upstream, DEFAULT_BRANCH);
402
- const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
403
- try {
404
- await makeSeeder(upstream).topUpWorkspace(repoDir, DEFAULT_BRANCH);
405
- expect(warn.mock.calls.flat().join(' ')).toMatch(/Groups.*not a directory/);
406
- } finally {
407
- warn.mockRestore();
408
- }
409
- // …and it did not quietly replace the file either.
410
- expect((await fs.lstat(path.join(repoDir, 'Groups'))).isFile()).toBe(true);
411
- });
412
-
413
- /**
414
- * A non-protected branch is somebody's change-in-waiting — a suggestions
415
- * branch behind an open change request, a personal draft — and a
416
- * scaffolding commit landing on it shows up as noise in their diff
417
- * against the default branch (a stray scaffolding file riding along in a
418
- * skill proposal was the observed symptom). Whatever the protected
419
- * branches lack, they get when THEY load; drafts fork from them.
420
- */
421
- it('topUpWorkspace leaves non-protected branches alone', async () => {
422
- const upstream = await seededUpstream(PROTECTED, {
423
- 'roles.yaml': 'roles:\n Admin:\n - keep@example.com\n',
424
- 'KnowledgeBase/Real/Knowledge/.gitkeep': '',
425
- 'Groups/.gitkeep': '',
426
- });
427
- // Branch a suggestion off the default and load it.
428
- const seed = await checkout(root, upstream, DEFAULT_BRANCH);
429
- await git(seed, ['checkout', '-b', 'suggestions/alice/knowledge']);
430
- await git(seed, ['push', 'origin', 'suggestions/alice/knowledge']);
431
-
432
- const before = await headCommitCount(root, upstream, 'suggestions/alice/knowledge');
433
- const repoDir = await checkout(root, upstream, 'suggestions/alice/knowledge');
434
- await makeSeeder(upstream).topUpWorkspace(repoDir, 'suggestions/alice/knowledge');
435
-
436
- // No scaffolding commit: the branch's diff against the default branch
437
- // stays exactly what its author put there.
438
- const after = await headCommitCount(root, upstream, 'suggestions/alice/knowledge');
439
- expect(after).toBe(before);
440
- const dir = await checkout(root, upstream, 'suggestions/alice/knowledge');
441
- expect(await exists(path.join(dir, 'AGENTS.md'))).toBe(false);
442
- });
443
- });
444
- });
@@ -1,36 +0,0 @@
1
- /**
2
- * Bootstraps the KB *remote* so the rest of the app's assumption — that the KB
3
- * repo it clones already carries the protected branches and base scaffolding —
4
- * holds even for a brand-new (or partially-populated) GitHub repo.
5
- *
6
- * The platform used to require the KB remote to have been forked from the
7
- * standalone `knowledge-base-template` repo. That template now ships inside the
8
- * platform (`kb-template/` at the repo root) and this service seeds from it.
9
- */
10
- export interface IKbSeedService {
11
- /**
12
- * Ensure the KB remote can be cloned: seed a fully-empty repo, and make sure
13
- * every protected branch exists as a ref (so `clone -b <branch>` succeeds).
14
- * Idempotent and single-flight — safe to await before every clone; the real
15
- * work runs at most once per process.
16
- *
17
- * - **Empty remote** → full seed: all template files (incl. the sample
18
- * ontology) committed to every protected branch. Uses `ADMIN_EMAIL`
19
- * (a repo with no Admin is unusable), otherwise throws.
20
- * - **Existing remote** → create only the protected branches that don't exist
21
- * yet, off an existing one. No file changes here — per-branch base files are
22
- * filled in by {@link topUpWorkspace} when a branch is loaded.
23
- */
24
- ensureRemoteSeeded(): Promise<void>;
25
-
26
- /**
27
- * Fill in any missing base scaffolding on the freshly-cloned workspace of the
28
- * branch a user just loaded, then commit + push it to that branch. Add-only —
29
- * never overwrites existing files, and never restores the sample ontology.
30
- * Best-effort: never throws, so a top-up hiccup can't block loading the branch.
31
- *
32
- * @param repoDir Absolute path to the checked-out KB clone for `branch`.
33
- * @param branch The branch that clone is on (the push target).
34
- */
35
- topUpWorkspace(repoDir: string, branch: string): Promise<void>;
36
- }