@bevel-software/platform-core-backend 0.7.5 → 0.8.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 (230) hide show
  1. package/LICENSE +202 -202
  2. package/THIRD-PARTY-NOTICES.md +428 -454
  3. package/dist/core/core-ports.d.ts +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +51 -7
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +5 -3
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +30 -18
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/modules/access/access-control.interface.d.ts +8 -7
  12. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  13. package/dist/modules/access/access-control.service.d.ts +1 -1
  14. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-control.service.js +6 -6
  16. package/dist/modules/access/access-control.service.js.map +1 -1
  17. package/dist/modules/access/access-declarations.d.ts +5 -5
  18. package/dist/modules/access/access-declarations.js +3 -3
  19. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  20. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  21. package/dist/modules/access/access-mutation.service.js +3 -3
  22. package/dist/modules/access/access-mutation.service.js.map +1 -1
  23. package/dist/modules/access/access-splice.js +4 -4
  24. package/dist/modules/access/access-splice.js.map +1 -1
  25. package/dist/modules/access/access.routes.js +19 -19
  26. package/dist/modules/access/access.routes.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +2 -2
  28. package/dist/modules/access/creator-access.js +5 -5
  29. package/dist/modules/access/creator-access.js.map +1 -1
  30. package/dist/modules/access/roles-admin.service.d.ts +18 -4
  31. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  32. package/dist/modules/access/roles-admin.service.js +14 -4
  33. package/dist/modules/access/roles-admin.service.js.map +1 -1
  34. package/dist/modules/code-mode/code-mode-names.d.ts +5 -13
  35. package/dist/modules/code-mode/code-mode-names.d.ts.map +1 -1
  36. package/dist/modules/code-mode/code-mode-names.js +5 -27
  37. package/dist/modules/code-mode/code-mode-names.js.map +1 -1
  38. package/dist/modules/code-mode/code-mode.tool.d.ts.map +1 -1
  39. package/dist/modules/code-mode/code-mode.tool.js +29 -7
  40. package/dist/modules/code-mode/code-mode.tool.js.map +1 -1
  41. package/dist/modules/mcp/mcp.service.d.ts +11 -52
  42. package/dist/modules/mcp/mcp.service.d.ts.map +1 -1
  43. package/dist/modules/mcp/mcp.service.js +33 -395
  44. package/dist/modules/mcp/mcp.service.js.map +1 -1
  45. package/dist/modules/plugins/index.d.ts +7 -0
  46. package/dist/modules/plugins/index.d.ts.map +1 -0
  47. package/dist/modules/plugins/index.js +6 -0
  48. package/dist/modules/plugins/index.js.map +1 -0
  49. package/dist/modules/plugins/join-proposals.d.ts +53 -0
  50. package/dist/modules/plugins/join-proposals.d.ts.map +1 -0
  51. package/dist/modules/plugins/join-proposals.js +67 -0
  52. package/dist/modules/plugins/join-proposals.js.map +1 -0
  53. package/dist/modules/plugins/join-requests.service.d.ts +81 -0
  54. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -0
  55. package/dist/modules/plugins/join-requests.service.js +135 -0
  56. package/dist/modules/plugins/join-requests.service.js.map +1 -0
  57. package/dist/modules/plugins/plugin-provision.service.d.ts +134 -0
  58. package/dist/modules/plugins/plugin-provision.service.d.ts.map +1 -0
  59. package/dist/modules/plugins/plugin-provision.service.js +344 -0
  60. package/dist/modules/plugins/plugin-provision.service.js.map +1 -0
  61. package/dist/modules/plugins/plugins.contract.d.ts +106 -0
  62. package/dist/modules/plugins/plugins.contract.d.ts.map +1 -0
  63. package/dist/modules/plugins/plugins.contract.js +36 -0
  64. package/dist/modules/plugins/plugins.contract.js.map +1 -0
  65. package/dist/modules/plugins/plugins.routes.d.ts +42 -0
  66. package/dist/modules/plugins/plugins.routes.d.ts.map +1 -0
  67. package/dist/modules/plugins/plugins.routes.js +379 -0
  68. package/dist/modules/plugins/plugins.routes.js.map +1 -0
  69. package/dist/modules/plugins/plugins.service.d.ts +60 -0
  70. package/dist/modules/plugins/plugins.service.d.ts.map +1 -0
  71. package/dist/modules/plugins/plugins.service.js +172 -0
  72. package/dist/modules/plugins/plugins.service.js.map +1 -0
  73. package/dist/modules/skills/pending-skills.service.d.ts +2 -2
  74. package/dist/modules/skills/pending-skills.service.js +7 -7
  75. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  76. package/dist/modules/skills/skills.contract.d.ts +4 -4
  77. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  78. package/dist/modules/skills/skills.contract.js +1 -1
  79. package/dist/modules/skills/skills.service.js +5 -5
  80. package/dist/modules/skills/skills.service.js.map +1 -1
  81. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts +65 -0
  82. package/dist/modules/tool-manuals/mcp-json-discovery.d.ts.map +1 -0
  83. package/dist/modules/tool-manuals/mcp-json-discovery.js +276 -0
  84. package/dist/modules/tool-manuals/mcp-json-discovery.js.map +1 -0
  85. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +92 -0
  86. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -0
  87. package/dist/modules/tool-manuals/mcp-server-edit.service.js +328 -0
  88. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -0
  89. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +38 -12
  90. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  91. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  92. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +13 -2
  93. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  94. package/dist/modules/tool-manuals/tool-manuals.routes.js +233 -2
  95. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  96. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  97. package/dist/modules/tool-manuals/tool-manuals.service.js +74 -37
  98. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  99. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -3
  100. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  101. package/dist/modules/workflow/git/git.service.js +2 -2
  102. package/dist/modules/workflow/git/git.service.js.map +1 -1
  103. package/dist/modules/workspace/kb-seed.service.d.ts +2 -2
  104. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  105. package/dist/modules/workspace/kb-seed.service.js +43 -10
  106. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  107. package/dist/modules/workspace/plugins-migration.d.ts +50 -0
  108. package/dist/modules/workspace/plugins-migration.d.ts.map +1 -0
  109. package/dist/modules/workspace/plugins-migration.js +379 -0
  110. package/dist/modules/workspace/plugins-migration.js.map +1 -0
  111. package/dist/modules/workspace/workspace.routes.js +3 -3
  112. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  113. package/dist/modules/workspace/workspace.service.d.ts +26 -0
  114. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  115. package/dist/modules/workspace/workspace.service.js +83 -12
  116. package/dist/modules/workspace/workspace.service.js.map +1 -1
  117. package/dist/shared/kb-layout.test.js +3 -3
  118. package/dist/shared/kb-layout.test.js.map +1 -1
  119. package/dist/shared/utcp-namespace.d.ts +6 -27
  120. package/dist/shared/utcp-namespace.d.ts.map +1 -1
  121. package/dist/shared/utcp-namespace.js +6 -63
  122. package/dist/shared/utcp-namespace.js.map +1 -1
  123. package/dist/shared/variable-refs.d.ts +42 -0
  124. package/dist/shared/variable-refs.d.ts.map +1 -0
  125. package/dist/shared/variable-refs.js +60 -0
  126. package/dist/shared/variable-refs.js.map +1 -0
  127. package/kb-template/.bevelignore +1 -1
  128. package/kb-template/AGENTS.md +88 -35
  129. package/kb-template/KnowledgeBase/How to get started.md +10 -10
  130. package/kb-template/access.md +36 -36
  131. package/migrations/meta/0000_snapshot.json +1479 -1479
  132. package/package.json +5 -4
  133. package/src/assets.ts +25 -25
  134. package/src/core/core-ports.ts +106 -106
  135. package/src/core/create-core-server.ts +55 -9
  136. package/src/core/create-core-services.ts +40 -20
  137. package/src/index.ts +69 -69
  138. package/src/modules/access/__tests__/access-control.atref-batch.test.ts +98 -98
  139. package/src/modules/access/__tests__/access-declarations.test.ts +28 -28
  140. package/src/modules/access/__tests__/access-md-format.test.ts +18 -18
  141. package/src/modules/access/__tests__/access-mutation.service.test.ts +5 -5
  142. package/src/modules/access/__tests__/access-splice.test.ts +2 -2
  143. package/src/modules/access/__tests__/access.routes.overrides.test.ts +16 -16
  144. package/src/modules/access/__tests__/grant-sources.test.ts +12 -12
  145. package/src/modules/access/__tests__/roles-admin.service.test.ts +13 -1
  146. package/src/modules/access/access-control.interface.ts +8 -7
  147. package/src/modules/access/access-control.service.ts +7 -7
  148. package/src/modules/access/access-declarations.ts +5 -5
  149. package/src/modules/access/access-mutation.service.ts +3 -3
  150. package/src/modules/access/access-splice.ts +4 -4
  151. package/src/modules/access/access.routes.ts +20 -20
  152. package/src/modules/access/creator-access.ts +5 -5
  153. package/src/modules/access/roles-admin.service.ts +13 -2
  154. package/src/modules/admin/admin-access.routes.ts +29 -29
  155. package/src/modules/auth/__tests__/auth.routes.test.ts +91 -91
  156. package/src/modules/auth/__tests__/rate-limit.test.ts +36 -36
  157. package/src/modules/auth/rate-limit.ts +45 -45
  158. package/src/modules/code-mode/__tests__/code-mode.tool.test.ts +67 -0
  159. package/src/modules/code-mode/code-mode-names.ts +10 -36
  160. package/src/modules/code-mode/code-mode.tool.ts +27 -7
  161. package/src/modules/database/connection.ts +15 -15
  162. package/src/modules/database/schema.ts +11 -11
  163. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +150 -150
  164. package/src/modules/mcp/mcp.service.ts +57 -435
  165. package/src/modules/{groups → plugins}/__tests__/join-proposals.test.ts +1 -1
  166. package/src/modules/{groups → plugins}/__tests__/join-requests.service.test.ts +7 -7
  167. package/src/modules/{groups/__tests__/group-index.service.test.ts → plugins/__tests__/plugin-index.service.test.ts} +41 -41
  168. package/src/modules/plugins/__tests__/plugin-provision.service.test.ts +312 -0
  169. package/src/modules/{groups/__tests__/groups.routes.test.ts → plugins/__tests__/plugins.routes.test.ts} +100 -100
  170. package/src/modules/plugins/index.ts +17 -0
  171. package/src/modules/{groups → plugins}/join-proposals.ts +2 -2
  172. package/src/modules/{groups → plugins}/join-requests.service.ts +8 -8
  173. package/src/modules/{groups/group-provision.service.ts → plugins/plugin-provision.service.ts} +139 -69
  174. package/src/modules/{groups/groups.contract.ts → plugins/plugins.contract.ts} +26 -26
  175. package/src/modules/{groups/groups.routes.ts → plugins/plugins.routes.ts} +102 -102
  176. package/src/modules/{groups/groups.service.ts → plugins/plugins.service.ts} +43 -43
  177. package/src/modules/secrets-vault/__tests__/tool-owner-gate.route.test.ts +143 -143
  178. package/src/modules/skills/__tests__/pending-skills.service.test.ts +14 -14
  179. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  180. package/src/modules/skills/pending-skills.service.ts +7 -7
  181. package/src/modules/skills/skills.contract.ts +4 -4
  182. package/src/modules/skills/skills.service.ts +5 -5
  183. package/src/modules/tool-auth/llm-usage-meter.ts +19 -19
  184. package/src/modules/tool-manuals/__tests__/mcp-json-discovery.test.ts +198 -0
  185. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +346 -0
  186. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +101 -0
  187. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +3 -3
  188. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  189. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +27 -27
  190. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +3 -3
  191. package/src/modules/tool-manuals/mcp-json-discovery.ts +328 -0
  192. package/src/modules/tool-manuals/mcp-server-edit.service.ts +434 -0
  193. package/src/modules/tool-manuals/tool-manuals.contract.ts +35 -12
  194. package/src/modules/tool-manuals/tool-manuals.routes.ts +222 -1
  195. package/src/modules/tool-manuals/tool-manuals.service.ts +82 -42
  196. package/src/modules/tool-manuals/tool-manuals.tools.ts +6 -3
  197. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  198. package/src/modules/workflow/git/__tests__/branch-name.test.ts +3 -3
  199. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +3 -3
  200. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +13 -8
  201. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  202. package/src/modules/workflow/git/__tests__/pull-request.service.test.ts +1 -1
  203. package/src/modules/workflow/git/git.service.ts +2 -2
  204. package/src/modules/workflow/review-workflow/__tests__/approval-states.test.ts +1 -1
  205. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  206. package/src/modules/workflow/workflow-hooks.ts +101 -101
  207. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +81 -13
  208. package/src/modules/workspace/__tests__/plugins-migration.test.ts +427 -0
  209. package/src/modules/workspace/__tests__/session-ontology.gate.test.ts +237 -237
  210. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -236
  211. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -179
  212. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -320
  213. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -337
  214. package/src/modules/workspace/__tests__/workspace.service.test.ts +116 -0
  215. package/src/modules/workspace/bevel-ignore.ts +66 -66
  216. package/src/modules/workspace/kb-seed.service.ts +38 -9
  217. package/src/modules/workspace/plugins-migration.ts +479 -0
  218. package/src/modules/workspace/session-sink.ts +25 -25
  219. package/src/modules/workspace/workspace.routes.ts +3 -3
  220. package/src/modules/workspace/workspace.service.ts +85 -14
  221. package/src/modules/workspace/workspace.tools.ts +922 -922
  222. package/src/shared/__tests__/join-request.test.ts +13 -13
  223. package/src/shared/__tests__/kb-layout.plugin.test.ts +45 -0
  224. package/src/shared/kb-layout.test.ts +3 -3
  225. package/src/shared/utcp-namespace.ts +10 -68
  226. package/src/shared/variable-refs.ts +64 -0
  227. package/src/modules/groups/__tests__/group-provision.service.test.ts +0 -247
  228. package/src/modules/groups/index.ts +0 -17
  229. package/src/shared/__tests__/kb-layout.group.test.ts +0 -45
  230. /package/kb-template/{Groups → Plugins}/.gitkeep +0 -0
package/src/index.ts CHANGED
@@ -1,69 +1,69 @@
1
- /**
2
- * @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
3
- * git-backed knowledge workspace, workflow (branches / change requests /
4
- * locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
5
- * control, diff/backup ledger, code-mode and the remote MCP surface.
6
- *
7
- * Composition entry points:
8
- * - `new CoreConfig()` — env-driven core configuration
9
- * - `createCoreServices(config, ports)` — construct every core service
10
- * - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
11
- *
12
- * Importing this package ALSO applies the global Express `Request`
13
- * augmentation (`req.userId` / `req.userEmail` / token payloads) — the
14
- * middleware modules re-exported below carry the `declare global` blocks, so
15
- * consumers get the augmented types without a side-effect import of their own.
16
- *
17
- * Finer-grained internals are available via the `./modules/*` subpath exports.
18
- */
19
-
20
- export { CoreConfig } from './core-config.js';
21
- export { createCoreServices, type CoreServices } from './core/create-core-services.js';
22
- export {
23
- createCoreServer,
24
- type ServerExtensions,
25
- type ToolSurfaceCtx,
26
- } from './core/create-core-server.js';
27
- export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
28
-
29
- // Packaged assets (migrations/, kb-template/) + the migration runners.
30
- export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
31
- export {
32
- runCoreMigrations,
33
- runEnterpriseMigrations,
34
- } from './modules/database/migrate.js';
35
- export { getDb, type Database } from './modules/database/connection.js';
36
-
37
- // Build identity surfaced by GET /api/health.
38
- export { GIT_SHA, resolveGitSha } from './version.js';
39
-
40
- // Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
41
- // token payloads — tool-auth + external-api-key interface). Re-exported by
42
- // VALUE/type so the `declare global` blocks land in this package's public
43
- // declaration graph.
44
- export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
45
- export {
46
- createToolAuthMiddleware,
47
- createManualAuthMiddleware,
48
- } from './modules/tool-auth/tool-auth.middleware.js';
49
- export type {
50
- IExternalApiKeyService,
51
- ExternalApiKeySummary,
52
- } from './modules/tool-auth/external-api-key.interface.js';
53
-
54
- // Key port/seam types an overlay implements.
55
- export type { ISessionSink } from './modules/workspace/session-sink.js';
56
- export type {
57
- ISystemNoticeSink,
58
- RecoveryAgentRunner,
59
- } from './modules/workflow/pending-commits.worker.js';
60
- export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
61
- export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
62
- export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
63
- export {
64
- WorkflowHooks,
65
- type CommitValidationHook,
66
- type CommitValidationContext,
67
- type PreWriteHook,
68
- type PreWriteContext,
69
- } from './modules/workflow/workflow-hooks.js';
1
+ /**
2
+ * @bevel-software/platform-core-backend — the open-source core of the Bevel platform:
3
+ * git-backed knowledge workspace, workflow (branches / change requests /
4
+ * locks / SSE), skills, tool manuals + registry + auth, secrets vault, access
5
+ * control, diff/backup ledger, code-mode and the remote MCP surface.
6
+ *
7
+ * Composition entry points:
8
+ * - `new CoreConfig()` — env-driven core configuration
9
+ * - `createCoreServices(config, ports)` — construct every core service
10
+ * - `createCoreServer(core, ext, opts)` — the Express app (fixed mount order)
11
+ *
12
+ * Importing this package ALSO applies the global Express `Request`
13
+ * augmentation (`req.userId` / `req.userEmail` / token payloads) — the
14
+ * middleware modules re-exported below carry the `declare global` blocks, so
15
+ * consumers get the augmented types without a side-effect import of their own.
16
+ *
17
+ * Finer-grained internals are available via the `./modules/*` subpath exports.
18
+ */
19
+
20
+ export { CoreConfig } from './core-config.js';
21
+ export { createCoreServices, type CoreServices } from './core/create-core-services.js';
22
+ export {
23
+ createCoreServer,
24
+ type ServerExtensions,
25
+ type ToolSurfaceCtx,
26
+ } from './core/create-core-server.js';
27
+ export { noopRecoveryAgent, type CorePorts } from './core/core-ports.js';
28
+
29
+ // Packaged assets (migrations/, kb-template/) + the migration runners.
30
+ export { coreMigrationsDir, defaultKbTemplateDir } from './assets.js';
31
+ export {
32
+ runCoreMigrations,
33
+ runEnterpriseMigrations,
34
+ } from './modules/database/migrate.js';
35
+ export { getDb, type Database } from './modules/database/connection.js';
36
+
37
+ // Build identity surfaced by GET /api/health.
38
+ export { GIT_SHA, resolveGitSha } from './version.js';
39
+
40
+ // Express `Request` augmentation carriers (userId/userEmail — auth.middleware;
41
+ // token payloads — tool-auth + external-api-key interface). Re-exported by
42
+ // VALUE/type so the `declare global` blocks land in this package's public
43
+ // declaration graph.
44
+ export { createAuthMiddleware, AUTH_COOKIE_NAME } from './modules/auth/auth.middleware.js';
45
+ export {
46
+ createToolAuthMiddleware,
47
+ createManualAuthMiddleware,
48
+ } from './modules/tool-auth/tool-auth.middleware.js';
49
+ export type {
50
+ IExternalApiKeyService,
51
+ ExternalApiKeySummary,
52
+ } from './modules/tool-auth/external-api-key.interface.js';
53
+
54
+ // Key port/seam types an overlay implements.
55
+ export type { ISessionSink } from './modules/workspace/session-sink.js';
56
+ export type {
57
+ ISystemNoticeSink,
58
+ RecoveryAgentRunner,
59
+ } from './modules/workflow/pending-commits.worker.js';
60
+ export type { ILlmUsageMeter } from './modules/tool-auth/llm-usage-meter.js';
61
+ export type { AuthProviderPlugin } from './modules/auth/auth.routes.js';
62
+ export type { IErasureParticipant } from './modules/auth/account-erasure.service.js';
63
+ export {
64
+ WorkflowHooks,
65
+ type CommitValidationHook,
66
+ type CommitValidationContext,
67
+ type PreWriteHook,
68
+ type PreWriteContext,
69
+ } from './modules/workflow/workflow-hooks.js';
@@ -1,98 +1,98 @@
1
- import { describe, it, expect, beforeAll, afterAll } 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
-
8
- import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
- import { AccessControlService } from '../access-control.service.js';
10
-
11
- const execFileAsync = promisify(execFile);
12
- const PROCESS_MAP_DIR = 'knowledge-base';
13
-
14
- /**
15
- * `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
16
- * `git cat-file --batch` process whose length-prefixed output we parse by
17
- * hand, so exercise it against a REAL git repo: found blobs, a path missing
18
- * at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
19
- * deny resolution — all in one batch, order-aligned.
20
- */
21
- describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
22
- let root: string;
23
- let repo: string;
24
- let svc: AccessControlService;
25
- const workspaceId = 'ws-atref-1';
26
- const admin = 'razvan@bevel.software';
27
-
28
- async function git(...args: string[]): Promise<void> {
29
- await execFileAsync('git', ['-C', repo, ...args]);
30
- }
31
-
32
- beforeAll(async () => {
33
- root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
34
- const workspaceDir = path.join(root, workspaceId);
35
- repo = path.join(workspaceDir, PROCESS_MAP_DIR);
36
- await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
37
-
38
- await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
39
- await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
40
- await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
41
- await fs.writeFile(
42
- path.join(repo, 'Knowledge/denied.md'),
43
- '---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
44
- );
45
- await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
46
-
47
- await execFileAsync('git', ['init', '-b', 'main', repo]);
48
- await git('config', 'user.email', 'test@example.com');
49
- await git('config', 'user.name', 'Test');
50
- await git('add', '-A');
51
- await git('commit', '-m', 'seed');
52
-
53
- const stub = {
54
- getWorkspacePath: async () => workspaceDir,
55
- // Fixture repo has no remote; at-ref model loading fetches best-effort.
56
- ensureRemotesFetched: async () => undefined,
57
- } as unknown as WorkspaceService;
58
- svc = new AccessControlService(stub, PROCESS_MAP_DIR);
59
- });
60
-
61
- afterAll(async () => {
62
- await fs.rm(root, { recursive: true, force: true });
63
- });
64
-
65
- it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
66
- const paths = [
67
- 'Knowledge/plain.md', // dir-chain Admin grant → true
68
- 'Knowledge/denied.md', // own frontmatter deny → false
69
- 'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
70
- 'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
71
- ];
72
- const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
73
- expect(map).not.toBeNull();
74
- expect(map!.get('Knowledge/plain.md')).toBe(true);
75
- expect(map!.get('Knowledge/denied.md')).toBe(false);
76
- expect(map!.get('Knowledge/with space.md')).toBe(true);
77
- expect(map!.get('Knowledge/not-there.md')).toBe(true);
78
- });
79
-
80
- it('matches the single-path at-ref resolution exactly', async () => {
81
- // The batch is a pure performance change — verdicts must be identical to
82
- // the per-path `git show` route the single-path helper still uses.
83
- expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
84
- expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
85
- });
86
-
87
- it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
88
- const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
89
- 'Knowledge/plain.md',
90
- 'Knowledge/denied.md',
91
- ]);
92
- expect(map).not.toBeNull();
93
- expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
94
- // The per-user deny strips razvan from the expanded email set.
95
- expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
96
- expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
97
- });
98
- });
1
+ import { describe, it, expect, beforeAll, afterAll } 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
+
8
+ import type { WorkspaceService } from '../../workspace/workspace.service.js';
9
+ import { AccessControlService } from '../access-control.service.js';
10
+
11
+ const execFileAsync = promisify(execFile);
12
+ const PROCESS_MAP_DIR = 'knowledge-base';
13
+
14
+ /**
15
+ * `canWriteBatchAtRef` / the batched at-ref own-entries read — backed by ONE
16
+ * `git cat-file --batch` process whose length-prefixed output we parse by
17
+ * hand, so exercise it against a REAL git repo: found blobs, a path missing
18
+ * at the ref (the `<spec> missing` line), a path with spaces, and frontmatter
19
+ * deny resolution — all in one batch, order-aligned.
20
+ */
21
+ describe('AccessControlService — at-ref batch reads (git cat-file --batch)', () => {
22
+ let root: string;
23
+ let repo: string;
24
+ let svc: AccessControlService;
25
+ const workspaceId = 'ws-atref-1';
26
+ const admin = 'razvan@bevel.software';
27
+
28
+ async function git(...args: string[]): Promise<void> {
29
+ await execFileAsync('git', ['-C', repo, ...args]);
30
+ }
31
+
32
+ beforeAll(async () => {
33
+ root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-atref-'));
34
+ const workspaceDir = path.join(root, workspaceId);
35
+ repo = path.join(workspaceDir, PROCESS_MAP_DIR);
36
+ await fs.mkdir(path.join(repo, 'Knowledge'), { recursive: true });
37
+
38
+ await fs.writeFile(path.join(repo, 'roles.yaml'), 'roles:\n Admin:\n - razvan@bevel.software\n');
39
+ await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
40
+ await fs.writeFile(path.join(repo, 'Knowledge/plain.md'), '# plain\n');
41
+ await fs.writeFile(
42
+ path.join(repo, 'Knowledge/denied.md'),
43
+ '---\nwrite:\n - deny razvan <razvan@bevel.software>\n---\n# denied\n',
44
+ );
45
+ await fs.writeFile(path.join(repo, 'Knowledge/with space.md'), '# spaced\n');
46
+
47
+ await execFileAsync('git', ['init', '-b', 'main', repo]);
48
+ await git('config', 'user.email', 'test@example.com');
49
+ await git('config', 'user.name', 'Test');
50
+ await git('add', '-A');
51
+ await git('commit', '-m', 'seed');
52
+
53
+ const stub = {
54
+ getWorkspacePath: async () => workspaceDir,
55
+ // Fixture repo has no remote; at-ref model loading fetches best-effort.
56
+ ensureRemotesFetched: async () => undefined,
57
+ } as unknown as WorkspaceService;
58
+ svc = new AccessControlService(stub, PROCESS_MAP_DIR);
59
+ });
60
+
61
+ afterAll(async () => {
62
+ await fs.rm(root, { recursive: true, force: true });
63
+ });
64
+
65
+ it('resolves a mixed batch in one pass: grants, frontmatter deny, spaces, and a missing path', async () => {
66
+ const paths = [
67
+ 'Knowledge/plain.md', // dir-chain Admin grant → true
68
+ 'Knowledge/denied.md', // own frontmatter deny → false
69
+ 'Knowledge/with space.md', // found blob whose SPEC contains spaces → true
70
+ 'Knowledge/not-there.md', // missing at the ref (`<spec> missing` line) → dir-chain verdict
71
+ ];
72
+ const map = await svc.canWriteBatchAtRef(workspaceId, 'main', admin, paths);
73
+ expect(map).not.toBeNull();
74
+ expect(map!.get('Knowledge/plain.md')).toBe(true);
75
+ expect(map!.get('Knowledge/denied.md')).toBe(false);
76
+ expect(map!.get('Knowledge/with space.md')).toBe(true);
77
+ expect(map!.get('Knowledge/not-there.md')).toBe(true);
78
+ });
79
+
80
+ it('matches the single-path at-ref resolution exactly', async () => {
81
+ // The batch is a pure performance change — verdicts must be identical to
82
+ // the per-path `git show` route the single-path helper still uses.
83
+ expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/denied.md')).toBe(false);
84
+ expect(await svc.canWriteAtRef(workspaceId, 'main', admin, 'Knowledge/plain.md')).toBe(true);
85
+ });
86
+
87
+ it('eligibleWritersForPathsAtRef honours the same batched own-entries', async () => {
88
+ const map = await svc.eligibleWritersForPathsAtRef(workspaceId, 'main', [
89
+ 'Knowledge/plain.md',
90
+ 'Knowledge/denied.md',
91
+ ]);
92
+ expect(map).not.toBeNull();
93
+ expect(map!.get('Knowledge/plain.md')!.roles).toContain('Admin');
94
+ // The per-user deny strips razvan from the expanded email set.
95
+ expect(map!.get('Knowledge/denied.md')!.emails.has(admin)).toBe(false);
96
+ expect(map!.get('Knowledge/plain.md')!.emails.has(admin)).toBe(true);
97
+ });
98
+ });
@@ -52,7 +52,7 @@ function makeWorkspace(files: Record<string, string>): IWorkspaceService {
52
52
  } as unknown as IWorkspaceService;
53
53
  }
54
54
 
55
- const scan = (files: Record<string, string>, folder = 'Groups/GTM') =>
55
+ const scan = (files: Record<string, string>, folder = 'Plugins/GTM') =>
56
56
  listAccessDeclarationsUnder(makeWorkspace(files), WS, KB, folder);
57
57
 
58
58
  const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
@@ -60,14 +60,14 @@ const fm = (body: string) => `---\n${body}\n---\n\n# Body\n`;
60
60
  describe('listAccessDeclarationsUnder', () => {
61
61
  it('reports a descendant access.md against the directory it governs', async () => {
62
62
  const { overrides, truncated } = await scan({
63
- [`${KB}/Groups/GTM/battlecards/access.md`]: fm('write:\n - GTM Team'),
63
+ [`${KB}/Plugins/GTM/battlecards/access.md`]: fm('write:\n - GTM Team'),
64
64
  });
65
65
 
66
66
  expect(truncated).toBe(false);
67
67
  expect(overrides).toEqual([
68
68
  {
69
- path: 'Groups/GTM/battlecards/access.md',
70
- governs: 'Groups/GTM/battlecards',
69
+ path: 'Plugins/GTM/battlecards/access.md',
70
+ governs: 'Plugins/GTM/battlecards',
71
71
  source: 'access-md',
72
72
  entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'GTM Team' } }],
73
73
  },
@@ -76,23 +76,23 @@ describe('listAccessDeclarationsUnder', () => {
76
76
 
77
77
  it("excludes the target folder's own access.md — that is the summary, not an override", async () => {
78
78
  const { overrides } = await scan({
79
- [`${KB}/Groups/GTM/access.md`]: fm('read:\n - GTM Team'),
80
- [`${KB}/Groups/GTM/battlecards/access.md`]: fm('read:\n - Developer'),
79
+ [`${KB}/Plugins/GTM/access.md`]: fm('read:\n - GTM Team'),
80
+ [`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read:\n - Developer'),
81
81
  });
82
82
 
83
- expect(overrides.map((o) => o.path)).toEqual(['Groups/GTM/battlecards/access.md']);
83
+ expect(overrides.map((o) => o.path)).toEqual(['Plugins/GTM/battlecards/access.md']);
84
84
  });
85
85
 
86
86
  it('reads verbs out of SKILL.md and .tool frontmatter, governing the file itself', async () => {
87
87
  const { overrides } = await scan({
88
- [`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\nowner: Ali <ali@bevel.software>'),
89
- [`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer'),
88
+ [`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\nowner: Ali <ali@bevel.software>'),
89
+ [`${KB}/Plugins/GTM/slack.tool`]: fm('write:\n - Developer'),
90
90
  });
91
91
 
92
92
  expect(overrides).toEqual([
93
93
  {
94
- path: 'Groups/GTM/outreach/SKILL.md',
95
- governs: 'Groups/GTM/outreach/SKILL.md',
94
+ path: 'Plugins/GTM/outreach/SKILL.md',
95
+ governs: 'Plugins/GTM/outreach/SKILL.md',
96
96
  source: 'frontmatter',
97
97
  entries: [
98
98
  {
@@ -103,8 +103,8 @@ describe('listAccessDeclarationsUnder', () => {
103
103
  ],
104
104
  },
105
105
  {
106
- path: 'Groups/GTM/slack.tool',
107
- governs: 'Groups/GTM/slack.tool',
106
+ path: 'Plugins/GTM/slack.tool',
107
+ governs: 'Plugins/GTM/slack.tool',
108
108
  source: 'frontmatter',
109
109
  entries: [{ verb: 'write', deny: false, principal: { kind: 'role', role: 'Developer' } }],
110
110
  },
@@ -113,7 +113,7 @@ describe('listAccessDeclarationsUnder', () => {
113
113
 
114
114
  it('ignores nodes whose frontmatter declares no access verb', async () => {
115
115
  const { overrides } = await scan({
116
- [`${KB}/Groups/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
116
+ [`${KB}/Plugins/GTM/outreach/SKILL.md`]: fm('nodeType: Skill\ndescription: Runs outreach'),
117
117
  });
118
118
 
119
119
  expect(overrides).toEqual([]);
@@ -121,7 +121,7 @@ describe('listAccessDeclarationsUnder', () => {
121
121
 
122
122
  it('preserves the deny prefix, collapses everyone, and carries user name + email', async () => {
123
123
  const { overrides } = await scan({
124
- [`${KB}/Groups/GTM/battlecards/access.md`]: fm(
124
+ [`${KB}/Plugins/GTM/battlecards/access.md`]: fm(
125
125
  'read:\n - everyone\n - deny Bob Ruiz <bob@bevel.software>\nwrite:\n - deny GTM Team',
126
126
  ),
127
127
  });
@@ -140,13 +140,13 @@ describe('listAccessDeclarationsUnder', () => {
140
140
  it('surfaces an unparseable access.md and silently skips an unparseable node', async () => {
141
141
  const { overrides } = await scan({
142
142
  // `read:` must be a LIST in an access.md — this file controls nothing.
143
- [`${KB}/Groups/GTM/battlecards/access.md`]: fm('read: GTM Team'),
143
+ [`${KB}/Plugins/GTM/battlecards/access.md`]: fm('read: GTM Team'),
144
144
  // Unterminated frontmatter on a node — forgiven, exactly like the resolver.
145
- [`${KB}/Groups/GTM/outreach/SKILL.md`]: '---\nowner: Ali <ali@bevel.software>\n\n# Body\n',
145
+ [`${KB}/Plugins/GTM/outreach/SKILL.md`]: '---\nowner: Ali <ali@bevel.software>\n\n# Body\n',
146
146
  });
147
147
 
148
148
  expect(overrides).toHaveLength(1);
149
- expect(overrides[0].path).toBe('Groups/GTM/battlecards/access.md');
149
+ expect(overrides[0].path).toBe('Plugins/GTM/battlecards/access.md');
150
150
  expect(overrides[0].entries).toEqual([]);
151
151
  expect(overrides[0].parseError).toContain("'read:' must be a list");
152
152
  });
@@ -155,7 +155,7 @@ describe('listAccessDeclarationsUnder', () => {
155
155
  const files: Record<string, string> = {};
156
156
  for (let i = 0; i < DECLARATION_SCAN_FILE_CAP + 5; i++) {
157
157
  const n = String(i).padStart(4, '0');
158
- files[`${KB}/Groups/GTM/n${n}/SKILL.md`] = fm('read:\n - Developer');
158
+ files[`${KB}/Plugins/GTM/n${n}/SKILL.md`] = fm('read:\n - Developer');
159
159
  }
160
160
 
161
161
  const { overrides, truncated } = await scan(files);
@@ -165,31 +165,31 @@ describe('listAccessDeclarationsUnder', () => {
165
165
  });
166
166
 
167
167
  it('returns an empty result for a folder with nothing in it', async () => {
168
- expect(await scan({ [`${KB}/Groups/Product/roadmap/SKILL.md`]: fm('read:\n - Developer') }))
168
+ expect(await scan({ [`${KB}/Plugins/Product/roadmap/SKILL.md`]: fm('read:\n - Developer') }))
169
169
  .toEqual({ overrides: [], truncated: false });
170
170
  });
171
171
 
172
172
  it('returns an empty result for a folder that is not in the clone', async () => {
173
- expect(await scan({}, 'Groups/Nope')).toEqual({ overrides: [], truncated: false });
173
+ expect(await scan({}, 'Plugins/Nope')).toEqual({ overrides: [], truncated: false });
174
174
  });
175
175
 
176
176
  it('refuses a target that is a file', async () => {
177
177
  await expect(
178
- scan({ [`${KB}/Groups/GTM/slack.tool`]: fm('write:\n - Developer') }, 'Groups/GTM/slack.tool'),
178
+ scan({ [`${KB}/Plugins/GTM/slack.tool`]: fm('write:\n - Developer') }, 'Plugins/GTM/slack.tool'),
179
179
  ).rejects.toBeInstanceOf(AccessDeclarationsError);
180
180
  });
181
181
 
182
182
  it('sorts rows by declaring path', async () => {
183
183
  const { overrides } = await scan({
184
- [`${KB}/Groups/GTM/zeta.tool`]: fm('write:\n - Developer'),
185
- [`${KB}/Groups/GTM/alpha/access.md`]: fm('read:\n - Developer'),
186
- [`${KB}/Groups/GTM/mid/SKILL.md`]: fm('owner:\n - Developer'),
184
+ [`${KB}/Plugins/GTM/zeta.tool`]: fm('write:\n - Developer'),
185
+ [`${KB}/Plugins/GTM/alpha/access.md`]: fm('read:\n - Developer'),
186
+ [`${KB}/Plugins/GTM/mid/SKILL.md`]: fm('owner:\n - Developer'),
187
187
  });
188
188
 
189
189
  expect(overrides.map((o) => o.path)).toEqual([
190
- 'Groups/GTM/alpha/access.md',
191
- 'Groups/GTM/mid/SKILL.md',
192
- 'Groups/GTM/zeta.tool',
190
+ 'Plugins/GTM/alpha/access.md',
191
+ 'Plugins/GTM/mid/SKILL.md',
192
+ 'Plugins/GTM/zeta.tool',
193
193
  ]);
194
194
  });
195
195
  });
@@ -19,7 +19,7 @@ import { spliceGrant, spliceRevoke } from '../access-splice.js';
19
19
  * govern itself (its readability follows the folder chain).
20
20
  * - NEW: the body carries the folder's rules; the frontmatter — like every
21
21
  * other file's — is about the FILE itself. `read: everyone` there makes
22
- * the group discoverable (anyone may open the access.md and see who runs
22
+ * the plugin discoverable (anyone may open the access.md and see who runs
23
23
  * the folder) while the folder's contents stay locked.
24
24
  *
25
25
  * Compat rule under test: a file whose body is NOT parsable as rules resolves
@@ -35,7 +35,7 @@ const ROLES_YAML = `roles:
35
35
  - felix@example.com
36
36
  `;
37
37
 
38
- /** New-format group access.md: discoverable by everyone, readable by GTM Team. */
38
+ /** New-format plugin access.md: discoverable by everyone, readable by GTM Team. */
39
39
  const NEW_FORMAT = `---
40
40
  read:
41
41
  - everyone
@@ -58,7 +58,7 @@ Some prose explaining this folder.
58
58
  describe('access.md format detection + parsing', () => {
59
59
  it('detects body rules (new format) and parses the BODY for the folder', () => {
60
60
  expect(accessMdDeclaresBodyRules(NEW_FORMAT)).toBe(true);
61
- const parsed = parseAccessFile(NEW_FORMAT, 'Groups/GTM/access.md');
61
+ const parsed = parseAccessFile(NEW_FORMAT, 'Plugins/GTM/access.md');
62
62
  expect(parsed.ok).toBe(true);
63
63
  if (!parsed.ok) return;
64
64
  expect(parsed.file.entries.read).toEqual([
@@ -89,7 +89,7 @@ describe('access.md format detection + parsing', () => {
89
89
  // (possibly `read: everyone`) self-rules — a typo must fail loudly.
90
90
  const typo = '---\nread:\n - everyone\n---\nread: GTM Team\n';
91
91
  expect(accessMdDeclaresBodyRules(typo)).toBe(true);
92
- const parsed = parseAccessFile(typo, 'Groups/GTM/access.md');
92
+ const parsed = parseAccessFile(typo, 'Plugins/GTM/access.md');
93
93
  expect(parsed.ok).toBe(false);
94
94
  if (parsed.ok) return;
95
95
  expect(parsed.errors.join(' ')).toContain("'read:' must be a list");
@@ -110,7 +110,7 @@ describe('access.md splice targeting', () => {
110
110
  it("target 'folder' on a new-format file lands the grant in the BODY", () => {
111
111
  const r = spliceGrant(NEW_FORMAT, 'read', FELIX, { target: 'folder' });
112
112
  expect(r.changed).toBe(true);
113
- const parsed = parseAccessFile(r.text, 'Groups/GTM/access.md');
113
+ const parsed = parseAccessFile(r.text, 'Plugins/GTM/access.md');
114
114
  expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user' && e.email === 'ali@x.eu')).toBe(true);
115
115
  // The self-frontmatter is untouched.
116
116
  expect(accessMdSelfEntries(r.text)?.read).toEqual([
@@ -132,7 +132,7 @@ describe('access.md splice targeting', () => {
132
132
  expect(r.changed).toBe(true);
133
133
  expect(accessMdSelfEntries(r.text)?.read.some((e) => e.kind === 'user' && e.email === 'ali@x.eu')).toBe(true);
134
134
  // Folder rules untouched.
135
- const parsed = parseAccessFile(r.text, 'Groups/GTM/access.md');
135
+ const parsed = parseAccessFile(r.text, 'Plugins/GTM/access.md');
136
136
  expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user')).toBe(false);
137
137
  });
138
138
 
@@ -140,14 +140,14 @@ describe('access.md splice targeting', () => {
140
140
  const granted = spliceGrant(NEW_FORMAT, 'read', FELIX, { target: 'folder' }).text;
141
141
  const revoked = spliceRevoke(granted, 'read', FELIX, { target: 'folder' });
142
142
  expect(revoked.changed).toBe(true);
143
- const parsed = parseAccessFile(revoked.text, 'Groups/GTM/access.md');
143
+ const parsed = parseAccessFile(revoked.text, 'Plugins/GTM/access.md');
144
144
  expect(parsed.ok && parsed.file.entries.read.some((e) => e.kind === 'user')).toBe(false);
145
145
  });
146
146
  });
147
147
 
148
- describe('end-to-end resolution over a new-format group', () => {
148
+ describe('end-to-end resolution over a new-format plugin', () => {
149
149
  let root: string;
150
- const workspaceId = 'ws-groups-fmt';
150
+ const workspaceId = 'ws-plugins-fmt';
151
151
 
152
152
  beforeEach(async () => {
153
153
  root = await fs.mkdtemp(path.join(os.tmpdir(), 'bevel-accessfmt-'));
@@ -159,12 +159,12 @@ describe('end-to-end resolution over a new-format group', () => {
159
159
  async function seed(): Promise<AccessControlService> {
160
160
  const workspaceDir = path.join(root, workspaceId);
161
161
  const repo = path.join(workspaceDir, KB);
162
- await fs.mkdir(path.join(repo, 'Groups', 'GTM'), { recursive: true });
162
+ await fs.mkdir(path.join(repo, 'Plugins', 'GTM'), { recursive: true });
163
163
  await fs.writeFile(path.join(repo, 'roles.yaml'), ROLES_YAML);
164
164
  await fs.writeFile(path.join(repo, 'access.md'), '---\nwrite:\n - Admin\n---\n');
165
- await fs.writeFile(path.join(repo, 'Groups', 'GTM', 'access.md'), NEW_FORMAT);
166
- await fs.mkdir(path.join(repo, 'Groups', 'GTM', 'outreach'), { recursive: true });
167
- await fs.writeFile(path.join(repo, 'Groups', 'GTM', 'outreach', 'SKILL.md'), '# outreach\n');
165
+ await fs.writeFile(path.join(repo, 'Plugins', 'GTM', 'access.md'), NEW_FORMAT);
166
+ await fs.mkdir(path.join(repo, 'Plugins', 'GTM', 'outreach'), { recursive: true });
167
+ await fs.writeFile(path.join(repo, 'Plugins', 'GTM', 'outreach', 'SKILL.md'), '# outreach\n');
168
168
  const stub = {
169
169
  getWorkspacePath: async (id: string) => {
170
170
  if (id !== workspaceId) throw new Error(`unexpected workspace ${id}`);
@@ -177,15 +177,15 @@ describe('end-to-end resolution over a new-format group', () => {
177
177
  it('a NON-member can read the access.md itself (discovery) but nothing inside the folder', async () => {
178
178
  const svc = await seed();
179
179
  const outsider = 'sara@example.com';
180
- expect(await svc.canRead(workspaceId, outsider, 'Groups/GTM/access.md')).toBe(true);
181
- expect(await svc.canRead(workspaceId, outsider, 'Groups/GTM/outreach/SKILL.md')).toBe(false);
180
+ expect(await svc.canRead(workspaceId, outsider, 'Plugins/GTM/access.md')).toBe(true);
181
+ expect(await svc.canRead(workspaceId, outsider, 'Plugins/GTM/outreach/SKILL.md')).toBe(false);
182
182
  });
183
183
 
184
184
  it('a member reads both; body rules (not frontmatter) decide the folder', async () => {
185
185
  const svc = await seed();
186
186
  const member = 'felix@example.com';
187
- expect(await svc.canRead(workspaceId, member, 'Groups/GTM/access.md')).toBe(true);
188
- expect(await svc.canRead(workspaceId, member, 'Groups/GTM/outreach/SKILL.md')).toBe(true);
189
- expect(await svc.canWrite(workspaceId, member, 'Groups/GTM/outreach/SKILL.md')).toBe(true);
187
+ expect(await svc.canRead(workspaceId, member, 'Plugins/GTM/access.md')).toBe(true);
188
+ expect(await svc.canRead(workspaceId, member, 'Plugins/GTM/outreach/SKILL.md')).toBe(true);
189
+ expect(await svc.canWrite(workspaceId, member, 'Plugins/GTM/outreach/SKILL.md')).toBe(true);
190
190
  });
191
191
  });
@@ -80,11 +80,11 @@ describe('AccessMutationService', () => {
80
80
  expect(text).toContain('# Sales folder');
81
81
  });
82
82
 
83
- it('kbPrincipals surfaces the built-in Everyone group alongside declared roles', async () => {
84
- const { groups } = await access.kbPrincipals(WS);
85
- expect(groups).toContain('Everyone');
86
- expect(groups).toContain('Admin');
87
- expect(groups).toContain('Product Team');
83
+ it('kbPrincipals surfaces the built-in Everyone plugin alongside declared roles', async () => {
84
+ const { plugins } = await access.kbPrincipals(WS);
85
+ expect(plugins).toContain('Everyone');
86
+ expect(plugins).toContain('Admin');
87
+ expect(plugins).toContain('Product Team');
88
88
  });
89
89
 
90
90
  it('grant everyone read makes the folder publicly readable (read: everyone)', async () => {
@@ -220,13 +220,13 @@ describe('validatePrincipal — injection guard', () => {
220
220
  expect(p.kind === 'user' && p.displayName).toBe('Ali');
221
221
  });
222
222
 
223
- it('rejects a group name with a colon (would forge a key)', () => {
223
+ it('rejects a plugin name with a colon (would forge a key)', () => {
224
224
  expect(() => validatePrincipal({ kind: 'role', role: 'write:\n - everyone' })).toThrow(
225
225
  AccessSpliceError,
226
226
  );
227
227
  });
228
228
 
229
- it('rejects the reserved group name deny', () => {
229
+ it('rejects the reserved plugin name deny', () => {
230
230
  expect(() => validatePrincipal({ kind: 'role', role: 'deny' })).toThrow(AccessSpliceError);
231
231
  });
232
232