@bevel-software/platform-core-backend 0.11.0 → 0.11.2

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 (312) hide show
  1. package/dist/core/create-core-services.d.ts +1 -1
  2. package/dist/core/create-core-services.d.ts.map +1 -1
  3. package/dist/core/create-core-services.js +2 -2
  4. package/dist/core/create-core-services.js.map +1 -1
  5. package/dist/modules/access/access-control.service.d.ts +1 -253
  6. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  7. package/dist/modules/access/access-control.service.js +3 -632
  8. package/dist/modules/access/access-control.service.js.map +1 -1
  9. package/dist/modules/access/access-declarations.d.ts +2 -2
  10. package/dist/modules/access/access-declarations.d.ts.map +1 -1
  11. package/dist/modules/access/access-declarations.js +2 -2
  12. package/dist/modules/access/access-declarations.js.map +1 -1
  13. package/dist/modules/access/access-mutation.service.d.ts +3 -3
  14. package/dist/modules/access/access-mutation.service.d.ts.map +1 -1
  15. package/dist/modules/access/access-mutation.service.js +3 -3
  16. package/dist/modules/access/access-mutation.service.js.map +1 -1
  17. package/dist/modules/access/access.routes.js +4 -4
  18. package/dist/modules/access/access.routes.js.map +1 -1
  19. package/dist/modules/access/admin-locked-commit.d.ts +2 -2
  20. package/dist/modules/access/admin-locked-commit.d.ts.map +1 -1
  21. package/dist/modules/access/admin-locked-commit.js +2 -2
  22. package/dist/modules/access/admin-locked-commit.js.map +1 -1
  23. package/dist/modules/access/admin-route-helpers.js +1 -1
  24. package/dist/modules/access/admin-route-helpers.js.map +1 -1
  25. package/dist/modules/access/capability-registry.js +1 -1
  26. package/dist/modules/access/capability-registry.js.map +1 -1
  27. package/dist/modules/access/creator-access.d.ts +1 -45
  28. package/dist/modules/access/creator-access.d.ts.map +1 -1
  29. package/dist/modules/access/creator-access.js +4 -24
  30. package/dist/modules/access/creator-access.js.map +1 -1
  31. package/dist/modules/access/groups-admin.routes.js +1 -1
  32. package/dist/modules/access/groups-admin.routes.js.map +1 -1
  33. package/dist/modules/access/groups-admin.service.d.ts +1 -1
  34. package/dist/modules/access/groups-admin.service.d.ts.map +1 -1
  35. package/dist/modules/access/groups-admin.service.js +6 -5
  36. package/dist/modules/access/groups-admin.service.js.map +1 -1
  37. package/dist/modules/access/groups-edit.js +2 -2
  38. package/dist/modules/access/groups-edit.js.map +1 -1
  39. package/dist/modules/access/reference-scan.js +1 -1
  40. package/dist/modules/access/reference-scan.js.map +1 -1
  41. package/dist/modules/access/roles-admin.service.d.ts +1 -1
  42. package/dist/modules/access/roles-admin.service.d.ts.map +1 -1
  43. package/dist/modules/access/roles-admin.service.js +8 -7
  44. package/dist/modules/access/roles-admin.service.js.map +1 -1
  45. package/dist/modules/access/roles-edit.js +1 -1
  46. package/dist/modules/access/roles-edit.js.map +1 -1
  47. package/dist/modules/access/synced-groups-committer.js +4 -4
  48. package/dist/modules/access/synced-groups-committer.js.map +1 -1
  49. package/dist/modules/access/synced-groups-writer.js +2 -2
  50. package/dist/modules/access/synced-groups-writer.js.map +1 -1
  51. package/dist/modules/access-model/access-errors.d.ts +34 -0
  52. package/dist/modules/access-model/access-errors.d.ts.map +1 -0
  53. package/dist/modules/access-model/access-errors.js +40 -0
  54. package/dist/modules/access-model/access-errors.js.map +1 -0
  55. package/dist/modules/access-model/access-grammar.d.ts +266 -0
  56. package/dist/modules/access-model/access-grammar.d.ts.map +1 -0
  57. package/dist/modules/access-model/access-grammar.js +644 -0
  58. package/dist/modules/access-model/access-grammar.js.map +1 -0
  59. package/dist/modules/access-model/access-splice.d.ts +140 -0
  60. package/dist/modules/access-model/access-splice.d.ts.map +1 -0
  61. package/dist/modules/access-model/access-splice.js +389 -0
  62. package/dist/modules/access-model/access-splice.js.map +1 -0
  63. package/dist/modules/access-model/creator.d.ts +54 -0
  64. package/dist/modules/access-model/creator.d.ts.map +1 -0
  65. package/dist/modules/access-model/creator.js +30 -0
  66. package/dist/modules/access-model/creator.js.map +1 -0
  67. package/dist/modules/access-model/group-files.d.ts +83 -0
  68. package/dist/modules/access-model/group-files.d.ts.map +1 -0
  69. package/dist/modules/access-model/group-files.js +167 -0
  70. package/dist/modules/access-model/group-files.js.map +1 -0
  71. package/dist/modules/access-model/kb-read-filter.d.ts +41 -0
  72. package/dist/modules/access-model/kb-read-filter.d.ts.map +1 -0
  73. package/dist/modules/access-model/kb-read-filter.js +60 -0
  74. package/dist/modules/access-model/kb-read-filter.js.map +1 -0
  75. package/dist/modules/access-model/render-roles-yaml.d.ts +22 -0
  76. package/dist/modules/access-model/render-roles-yaml.d.ts.map +1 -0
  77. package/dist/modules/access-model/render-roles-yaml.js +56 -0
  78. package/dist/modules/access-model/render-roles-yaml.js.map +1 -0
  79. package/dist/modules/access-model/roles-yaml-guard.d.ts +56 -0
  80. package/dist/modules/access-model/roles-yaml-guard.d.ts.map +1 -0
  81. package/dist/modules/access-model/roles-yaml-guard.js +79 -0
  82. package/dist/modules/access-model/roles-yaml-guard.js.map +1 -0
  83. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  84. package/dist/modules/admin/admin-access.service.js +1 -1
  85. package/dist/modules/admin/admin-access.service.js.map +1 -1
  86. package/dist/modules/diff/diff.routes.js +4 -4
  87. package/dist/modules/diff/diff.routes.js.map +1 -1
  88. package/dist/modules/diff/diff.service.d.ts +1 -1
  89. package/dist/modules/diff/diff.service.d.ts.map +1 -1
  90. package/dist/modules/kb-fs/branch-name.d.ts +10 -0
  91. package/dist/modules/kb-fs/branch-name.d.ts.map +1 -0
  92. package/dist/modules/kb-fs/branch-name.js +76 -0
  93. package/dist/modules/kb-fs/branch-name.js.map +1 -0
  94. package/dist/modules/kb-fs/clone-config.d.ts +61 -0
  95. package/dist/modules/kb-fs/clone-config.d.ts.map +1 -0
  96. package/dist/modules/kb-fs/clone-config.js +69 -0
  97. package/dist/modules/kb-fs/clone-config.js.map +1 -0
  98. package/dist/modules/kb-fs/file-change-notifier.d.ts +38 -0
  99. package/dist/modules/kb-fs/file-change-notifier.d.ts.map +1 -0
  100. package/dist/modules/kb-fs/file-change-notifier.js +22 -0
  101. package/dist/modules/kb-fs/file-change-notifier.js.map +1 -0
  102. package/dist/modules/kb-fs/locking-filesystem.d.ts +137 -0
  103. package/dist/modules/kb-fs/locking-filesystem.d.ts.map +1 -0
  104. package/dist/modules/kb-fs/locking-filesystem.js +553 -0
  105. package/dist/modules/kb-fs/locking-filesystem.js.map +1 -0
  106. package/dist/modules/kb-fs/mutex.d.ts +11 -0
  107. package/dist/modules/kb-fs/mutex.d.ts.map +1 -0
  108. package/dist/modules/kb-fs/mutex.js +23 -0
  109. package/dist/modules/kb-fs/mutex.js.map +1 -0
  110. package/dist/modules/kb-fs/read-only-filesystem.d.ts +24 -0
  111. package/dist/modules/kb-fs/read-only-filesystem.d.ts.map +1 -0
  112. package/dist/modules/kb-fs/read-only-filesystem.js +39 -0
  113. package/dist/modules/kb-fs/read-only-filesystem.js.map +1 -0
  114. package/dist/modules/plugins/join-proposals.d.ts +1 -1
  115. package/dist/modules/plugins/join-proposals.d.ts.map +1 -1
  116. package/dist/modules/plugins/join-proposals.js +1 -1
  117. package/dist/modules/plugins/join-proposals.js.map +1 -1
  118. package/dist/modules/plugins/join-requests.service.d.ts.map +1 -1
  119. package/dist/modules/plugins/join-requests.service.js +1 -1
  120. package/dist/modules/plugins/join-requests.service.js.map +1 -1
  121. package/dist/modules/plugins/plugin-provision.service.js +3 -3
  122. package/dist/modules/plugins/plugin-provision.service.js.map +1 -1
  123. package/dist/modules/plugins/plugins.routes.js +2 -2
  124. package/dist/modules/plugins/plugins.routes.js.map +1 -1
  125. package/dist/modules/plugins/plugins.service.js +1 -1
  126. package/dist/modules/plugins/plugins.service.js.map +1 -1
  127. package/dist/modules/secrets-vault/secrets-vault.routes.js +1 -1
  128. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  129. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -1
  130. package/dist/modules/skills/pending-skills.service.js +1 -1
  131. package/dist/modules/skills/pending-skills.service.js.map +1 -1
  132. package/dist/modules/skills/skills.service.js +1 -1
  133. package/dist/modules/skills/skills.service.js.map +1 -1
  134. package/dist/modules/tool-helpers/tool-context.d.ts +2 -2
  135. package/dist/modules/tool-helpers/tool-context.d.ts.map +1 -1
  136. package/dist/modules/tool-helpers/tool-context.js +4 -4
  137. package/dist/modules/tool-helpers/tool-context.js.map +1 -1
  138. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts +1 -1
  139. package/dist/modules/tool-manuals/mcp-server-edit.service.d.ts.map +1 -1
  140. package/dist/modules/tool-manuals/mcp-server-edit.service.js +1 -1
  141. package/dist/modules/tool-manuals/mcp-server-edit.service.js.map +1 -1
  142. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +1 -1
  143. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  144. package/dist/modules/tool-manuals/tool-manuals.routes.js +1 -1
  145. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  146. package/dist/modules/tool-manuals/tool-manuals.service.js +1 -1
  147. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  148. package/dist/modules/tool-manuals/tool-manuals.tools.js +1 -1
  149. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  150. package/dist/modules/workflow/agent-tools/workflow.tools.js +1 -1
  151. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  152. package/dist/modules/workflow/file-lock.service.js +1 -1
  153. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  154. package/dist/modules/workflow/git/git.service.d.ts +2 -2
  155. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  156. package/dist/modules/workflow/git/git.service.js +5 -5
  157. package/dist/modules/workflow/git/git.service.js.map +1 -1
  158. package/dist/modules/workflow/git/pull-request.service.js +1 -1
  159. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  160. package/dist/modules/workflow/review-workflow/review-workflow.service.js +2 -2
  161. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  162. package/dist/modules/workflow/session-ontology.service.js +1 -1
  163. package/dist/modules/workflow/session-ontology.service.js.map +1 -1
  164. package/dist/modules/workflow/workflow.routes.js +2 -2
  165. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  166. package/dist/modules/workflow/workflow.service.d.ts +1 -1
  167. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  168. package/dist/modules/workflow/workflow.service.js +4 -4
  169. package/dist/modules/workflow/workflow.service.js.map +1 -1
  170. package/dist/modules/workspace/startup/kb-startup-runner.js +1 -1
  171. package/dist/modules/workspace/startup/kb-startup-runner.js.map +1 -1
  172. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js +1 -1
  173. package/dist/modules/workspace/startup/steps/groups-to-plugins.step.js.map +1 -1
  174. package/dist/modules/workspace/startup/steps/roles-yaml.step.d.ts +1 -1
  175. package/dist/modules/workspace/startup/steps/roles-yaml.step.js +2 -2
  176. package/dist/modules/workspace/startup/steps/roles-yaml.step.js.map +1 -1
  177. package/dist/modules/workspace/startup/steps/seed-tree.js +1 -1
  178. package/dist/modules/workspace/startup/steps/seed-tree.js.map +1 -1
  179. package/dist/modules/workspace/workspace.routes.d.ts +1 -1
  180. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  181. package/dist/modules/workspace/workspace.routes.js +5 -4
  182. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  183. package/dist/modules/workspace/workspace.service.d.ts +2 -9
  184. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  185. package/dist/modules/workspace/workspace.service.js +14 -15
  186. package/dist/modules/workspace/workspace.service.js.map +1 -1
  187. package/dist/modules/workspace/workspace.tools.js +3 -3
  188. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  189. package/dist/shared/domain-errors.d.ts +202 -0
  190. package/dist/shared/domain-errors.d.ts.map +1 -0
  191. package/dist/shared/domain-errors.js +303 -0
  192. package/dist/shared/domain-errors.js.map +1 -0
  193. package/dist/shared/workspace-id.d.ts +27 -0
  194. package/dist/shared/workspace-id.d.ts.map +1 -0
  195. package/dist/shared/workspace-id.js +36 -0
  196. package/dist/shared/workspace-id.js.map +1 -0
  197. package/package.json +3 -3
  198. package/src/core/create-core-services.ts +2 -2
  199. package/src/modules/access/__tests__/access-control.service.test.ts +1 -1
  200. package/src/modules/access/__tests__/access-groups.test.ts +6 -5
  201. package/src/modules/access/__tests__/access-md-format.test.ts +3 -3
  202. package/src/modules/access/__tests__/access-mutation.service.test.ts +1 -1
  203. package/src/modules/access/__tests__/admin-locked-commit.test.ts +1 -1
  204. package/src/modules/access/__tests__/admin-route-helpers.test.ts +1 -1
  205. package/src/modules/access/__tests__/roles-admin.service.test.ts +2 -2
  206. package/src/modules/access/__tests__/roles-edit.test.ts +1 -1
  207. package/src/modules/access/__tests__/synced-groups-committer.test.ts +1 -1
  208. package/src/modules/access/__tests__/synced-groups-writer.test.ts +1 -1
  209. package/src/modules/access/access-control.service.ts +23 -768
  210. package/src/modules/access/access-declarations.ts +2 -2
  211. package/src/modules/access/access-mutation.service.ts +3 -3
  212. package/src/modules/access/access.routes.ts +5 -5
  213. package/src/modules/access/admin-locked-commit.ts +2 -2
  214. package/src/modules/access/admin-route-helpers.ts +1 -1
  215. package/src/modules/access/capability-registry.ts +1 -1
  216. package/src/modules/access/creator-access.ts +9 -72
  217. package/src/modules/access/groups-admin.routes.ts +1 -1
  218. package/src/modules/access/groups-admin.service.ts +6 -7
  219. package/src/modules/access/groups-edit.ts +2 -2
  220. package/src/modules/access/reference-scan.ts +1 -1
  221. package/src/modules/access/roles-admin.service.ts +8 -8
  222. package/src/modules/access/roles-edit.ts +1 -1
  223. package/src/modules/access/synced-groups-committer.ts +4 -4
  224. package/src/modules/access/synced-groups-writer.ts +2 -2
  225. package/src/modules/access-model/__tests__/access-grammar.test.ts +31 -0
  226. package/src/modules/{access → access-model}/__tests__/access-splice.test.ts +268 -268
  227. package/src/modules/{access → access-model}/access-errors.ts +1 -1
  228. package/src/modules/access-model/access-grammar.ts +778 -0
  229. package/src/modules/{access → access-model}/access-splice.ts +1 -1
  230. package/src/modules/access-model/creator.ts +79 -0
  231. package/src/modules/{access → access-model}/group-files.ts +1 -1
  232. package/src/modules/{access → access-model}/render-roles-yaml.ts +1 -1
  233. package/src/modules/{access → access-model}/roles-yaml-guard.ts +2 -2
  234. package/src/modules/admin/admin-access.service.ts +2 -1
  235. package/src/modules/diff/__tests__/diff.routes.rejectPathsLocked.test.ts +2 -2
  236. package/src/modules/diff/__tests__/diff.service.seed-atomicity.test.ts +3 -2
  237. package/src/modules/diff/__tests__/diff.service.test.ts +3 -2
  238. package/src/modules/diff/diff.routes.ts +4 -4
  239. package/src/modules/diff/diff.service.ts +1 -1
  240. package/src/modules/{workflow/git → kb-fs}/__tests__/branch-name.test.ts +1 -1
  241. package/src/modules/{workflow → kb-fs}/__tests__/locking-filesystem.test.ts +1 -1
  242. package/src/modules/{workflow/git → kb-fs}/branch-name.ts +1 -1
  243. package/src/modules/{workflow → kb-fs}/locking-filesystem.ts +2 -2
  244. package/src/modules/plugins/__tests__/join-requests.service.test.ts +2 -4
  245. package/src/modules/plugins/__tests__/plugin-index.service.test.ts +1 -1
  246. package/src/modules/plugins/__tests__/plugins.routes.test.ts +1 -1
  247. package/src/modules/plugins/join-proposals.ts +1 -1
  248. package/src/modules/plugins/join-requests.service.ts +2 -1
  249. package/src/modules/plugins/plugin-provision.service.ts +3 -3
  250. package/src/modules/plugins/plugins.routes.ts +2 -2
  251. package/src/modules/plugins/plugins.service.ts +1 -1
  252. package/src/modules/secrets-vault/secrets-vault.routes.ts +1 -1
  253. package/src/modules/skills/__tests__/skills.service.test.ts +1 -1
  254. package/src/modules/skills/pending-skills.service.ts +2 -1
  255. package/src/modules/skills/skills.service.ts +1 -1
  256. package/src/modules/tool-helpers/tool-context.ts +6 -5
  257. package/src/modules/tool-manuals/__tests__/mcp-server-edit.service.test.ts +1 -1
  258. package/src/modules/tool-manuals/__tests__/tool-manuals.archive.route.test.ts +1 -1
  259. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +1 -1
  260. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +1 -1
  261. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +1 -1
  262. package/src/modules/tool-manuals/mcp-server-edit.service.ts +2 -1
  263. package/src/modules/tool-manuals/tool-manuals.routes.ts +2 -1
  264. package/src/modules/tool-manuals/tool-manuals.service.ts +1 -1
  265. package/src/modules/tool-manuals/tool-manuals.tools.ts +1 -1
  266. package/src/modules/workflow/__tests__/preserve-roles-yaml.test.ts +1 -1
  267. package/src/modules/workflow/__tests__/workflow.service.commitFileWhileLocked.test.ts +1 -1
  268. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +1 -1
  269. package/src/modules/workflow/__tests__/workflow.service.releaseLock.test.ts +1 -1
  270. package/src/modules/workflow/agent-tools/workflow.tools.ts +1 -1
  271. package/src/modules/workflow/file-lock.service.ts +1 -1
  272. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +1 -1
  273. package/src/modules/workflow/git/__tests__/git.service.deleteBranch.test.ts +1 -1
  274. package/src/modules/workflow/git/__tests__/git.service.diffFileAtCommit.test.ts +1 -1
  275. package/src/modules/workflow/git/__tests__/git.service.diffFileBetweenBranches.test.ts +1 -1
  276. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +1 -1
  277. package/src/modules/workflow/git/git.service.ts +5 -5
  278. package/src/modules/workflow/git/pull-request.service.ts +1 -1
  279. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +1 -1
  280. package/src/modules/workflow/review-workflow/review-workflow.service.ts +2 -2
  281. package/src/modules/workflow/session-ontology.service.ts +1 -1
  282. package/src/modules/workflow/workflow.routes.ts +2 -2
  283. package/src/modules/workflow/workflow.service.ts +5 -5
  284. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +1 -1
  285. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +1 -1
  286. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +2 -2
  287. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +1 -1
  288. package/src/modules/workspace/__tests__/workspace.service.read-filter.test.ts +2 -5
  289. package/src/modules/workspace/__tests__/workspace.service.test.ts +13 -1
  290. package/src/modules/workspace/__tests__/workspace.tools.test.ts +1 -1
  291. package/src/modules/workspace/startup/kb-startup-runner.ts +1 -1
  292. package/src/modules/workspace/startup/steps/__tests__/steps.test.ts +1 -1
  293. package/src/modules/workspace/startup/steps/groups-to-plugins.step.ts +1 -1
  294. package/src/modules/workspace/startup/steps/roles-yaml.step.ts +2 -2
  295. package/src/modules/workspace/startup/steps/seed-tree.ts +1 -1
  296. package/src/modules/workspace/workspace.routes.ts +6 -5
  297. package/src/modules/workspace/workspace.service.ts +14 -18
  298. package/src/modules/workspace/workspace.tools.ts +3 -3
  299. package/src/shared/__tests__/join-request.test.ts +1 -1
  300. package/src/shared/__tests__/workspace-id.test.ts +17 -0
  301. package/src/{modules/workflow/workflow.errors.ts → shared/domain-errors.ts} +6 -1
  302. package/src/shared/workspace-id.ts +36 -0
  303. /package/src/modules/{access → access-model}/__tests__/kb-read-filter.test.ts +0 -0
  304. /package/src/modules/{access → access-model}/__tests__/roles-yaml-guard.test.ts +0 -0
  305. /package/src/modules/{access → access-model}/kb-read-filter.ts +0 -0
  306. /package/src/modules/{workflow/git → kb-fs}/__tests__/clone-config.test.ts +0 -0
  307. /package/src/modules/{workflow → kb-fs}/__tests__/file-change-notifier.test.ts +0 -0
  308. /package/src/modules/{workflow/git → kb-fs}/__tests__/mutex.test.ts +0 -0
  309. /package/src/modules/{workflow/git → kb-fs}/clone-config.ts +0 -0
  310. /package/src/modules/{workflow → kb-fs}/file-change-notifier.ts +0 -0
  311. /package/src/modules/{workflow/git → kb-fs}/mutex.ts +0 -0
  312. /package/src/modules/{workflow → kb-fs}/read-only-filesystem.ts +0 -0
@@ -3,11 +3,9 @@ import path from 'node:path';
3
3
  import { execFile, spawn } from 'node:child_process';
4
4
  import { promisify } from 'node:util';
5
5
  import { createHash } from 'node:crypto';
6
- import { AccessConfigError } from './access-errors.js';
7
- // NOTE: deliberate module cycle — group-files.ts imports this module's parsing
8
- // primitives. Benign: both sides only dereference the other's exports inside
9
- // function bodies, never at module-evaluation time.
10
- import { GROUPS_YAML, SYNCED_GROUPS_YAML, parseGroupsFile, } from './group-files.js';
6
+ import { AccessConfigError } from '../access-model/access-errors.js';
7
+ import { GROUPS_YAML, SYNCED_GROUPS_YAML, parseGroupsFile, } from '../access-model/group-files.js';
8
+ import { ADMIN_CANONICAL, EVERYONE_CANONICAL, EVERYONE_DISPLAY, KNOWN_VERBS, ROLE_TOKEN_PREFIX, accessMdSelfEntries, canonicalEmail, canonicalRoleName, isAccessMdPath, mergeGroupsIntoRoles, parseAccessFile, parseOwnAccessEntries, parseRolesYaml, sourceVerbsFor, } from '../access-model/access-grammar.js';
11
9
  import { DIRECTORY_SYNC_BOT_EMAIL, DIRECTORY_SYNC_BOT_NAME, } from './directory-sync-bot.js';
12
10
  const execFileAsync = promisify(execFile);
13
11
  /**
@@ -74,475 +72,12 @@ function catFileBatch(repoDir, specs) {
74
72
  function sha256Email(email) {
75
73
  return createHash('sha256').update(email.trim().toLowerCase()).digest('hex');
76
74
  }
77
- // ---------------------------------------------------------------------------
78
- // Constants — kept in lockstep with knowledge-base/lib/access-control.js
79
- // ---------------------------------------------------------------------------
80
- export const ADMIN_CANONICAL = 'admin';
81
- /**
82
- * Verbs the resolver understands in an `access.md` frontmatter. Each verb
83
- * is a list of grants (role or `Name <email>` references, optionally
84
- * prefixed with `deny `). Keep `Verb` and `KNOWN_VERBS` in lockstep —
85
- * `AccessFile.entries` is statically keyed on this union.
86
- *
87
- * `read` controls who may VIEW a path (the file viewer, embed surface, and
88
- * the agent's read tools). It is **default-deny**: a path with no effective
89
- * `read:` or `owner:` grant is not readable. To make content public, list the
90
- * built-in role `everyone` under `read:`.
91
- *
92
- * The verbs nest: `owner` is a superset of `read` + `write` + `download`, and
93
- * `write` is itself a superset of `read` (anyone who can edit can view). An
94
- * `owner` grant therefore confers all three lower verbs, a `write` grant
95
- * additionally confers `read`, and `owner` also marks the principal as a
96
- * contact point for the node (surfaced in the UI so users know who to ask).
97
- * See `sourceVerbsFor` for how these implications fold into resolution.
98
- */
99
- export const KNOWN_VERBS = ['read', 'write', 'download', 'owner'];
100
- const KNOWN_VERBS_SET = new Set(KNOWN_VERBS);
101
- export const EVERYONE_CANONICAL = 'everyone';
102
- /** Display name for the built-in `everyone` role in the share UI. */
103
- export const EVERYONE_DISPLAY = 'Everyone';
104
- /**
105
- * Verbs whose entries contribute to resolving `verb`, target verb first.
106
- * `owner` implies `read`, `write`, and `download`; `write` additionally
107
- * implies `read`. So resolving `read` folds in `write` and `owner`, resolving
108
- * `write`/`download` folds in `owner`, and resolving `owner` uses only `owner`.
109
- *
110
- * The implication is **grant-only** (see `resolveAtPath`): a superset grant
111
- * confers the lower verb, but a superset *denial* does not — `deny write` says
112
- * nothing about `read`, so it never strips a separate read grant. The target
113
- * verb itself contributes both its grants and its denials.
114
- */
115
- export function sourceVerbsFor(verb) {
116
- switch (verb) {
117
- case 'owner':
118
- return ['owner'];
119
- case 'read':
120
- return ['read', 'write', 'owner'];
121
- default:
122
- return [verb, 'owner'];
123
- }
124
- }
125
- export const RESERVED_ROLE_NAMES = new Set(['deny', EVERYONE_CANONICAL]);
126
- export const DENY_PREFIX = 'deny ';
127
- /**
128
- * Member-entry prefix in roles.yaml that references a GROUP instead of an
129
- * email: `- group:Engineering`. Explicit on purpose — membership kind is
130
- * never guessed from string shape. Valid on EVERY role including Admin, with
131
- * one kept invariant (see `parseRolesYaml`): Admin must always retain at
132
- * least one direct email member, so a misconfigured or unreachable directory
133
- * can never leave the deployment without a rescuable admin.
134
- */
135
- export const GROUP_REF_PREFIX = 'group:';
136
- /**
137
- * Explicit ROLE token prefix in access.md entries: `role/<Name>` resolves to
138
- * the roles.yaml role only, never a group. A BARE name resolves GROUP-FIRST
139
- * and falls back to the role — so `role/` is the escape hatch when a group
140
- * shares the role's name. The prefix is reserved in the group name-safety
141
- * rules (a group may never be named `role/...`), and every role is also
142
- * registered in the principal index under its `role/<canonical>` alias.
143
- */
144
- export const ROLE_TOKEN_PREFIX = 'role/';
145
- export const USER_REF_REGEX = /^(.+?)\s+<\s*([^<>\s]+@[^<>\s]+)\s*>\s*$/;
146
- export const EMAIL_REGEX = /^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/;
147
- function emptyEntries() {
148
- const out = {};
149
- for (const v of KNOWN_VERBS)
150
- out[v] = [];
151
- return out;
152
- }
153
- export function isAccessMdPath(p) {
154
- return p === 'access.md' || p.endsWith('/access.md');
155
- }
156
- /**
157
- * Extensions of node files whose OWN `---` frontmatter can carry access verbs
158
- * the resolver enforces (`readOwnEntries` → `parseOwnAccessEntries`). The
159
- * SINGLE source of truth for every surface that enumerates candidate files —
160
- * the access-declarations scan and the shared `KbReferenceScanner` (which
161
- * must scan/rewrite the same set, or a rename strands a live `.tool`
162
- * frontmatter grant). `access.md` is covered by `.md`.
163
- */
164
- export const ACCESS_FRONTMATTER_EXTENSIONS = ['.md', '.tool'];
165
- /** True when `p` is a file the resolver reads access frontmatter from. */
166
- export function hasAccessFrontmatterExtension(p) {
167
- return ACCESS_FRONTMATTER_EXTENSIONS.some((ext) => p.endsWith(ext));
168
- }
169
75
  function isBuiltInRole(canonicalRole) {
170
76
  return canonicalRole === EVERYONE_CANONICAL;
171
77
  }
172
78
  function roleKnown(roles, canonicalRole) {
173
79
  return roles.byCanonical.has(canonicalRole) || isBuiltInRole(canonicalRole);
174
80
  }
175
- /**
176
- * Strip a trailing `# comment` the way the YAML-subset tokeniser reads a
177
- * line: a `#` at the start (after only whitespace) or preceded by whitespace
178
- * begins a comment. Exported so the reference scanner matches tokens against
179
- * the SAME comment rule the resolver parses with (a `- GTM Team # sales`
180
- * entry is the token `GTM Team`, never `GTM Team # sales`).
181
- */
182
- export function stripComment(line) {
183
- let inWs = true;
184
- for (let i = 0; i < line.length; i++) {
185
- const ch = line[i];
186
- if (ch === '#' && (inWs || (i > 0 && /\s/.test(line[i - 1])))) {
187
- return line.slice(0, i);
188
- }
189
- if (!/\s/.test(ch))
190
- inWs = false;
191
- }
192
- return line;
193
- }
194
- function tokenise(text, opts) {
195
- const lines = text.split(/\r?\n/);
196
- const tokens = [];
197
- // Tolerated blank keys are uniquified as runs of spaces so a SECOND blank
198
- // key doesn't trip the duplicate-key check (which would retire every valid
199
- // group after it). All-whitespace keys still canonicalize to '' downstream,
200
- // so the entry-level "empty group name — skipped" handling sees them all.
201
- let blankKeySeq = 0;
202
- for (let i = 0; i < lines.length; i++) {
203
- const stripped = stripComment(lines[i]).replace(/\s+$/, '');
204
- if (!stripped.trim())
205
- continue;
206
- const indentMatch = stripped.match(/^( *)/);
207
- const indent = indentMatch ? indentMatch[1].length : 0;
208
- const content = stripped.slice(indent);
209
- const lineNum = i + 1;
210
- if (content.startsWith('- ') || content === '-') {
211
- const value = content === '-' ? '' : content.slice(2).trim();
212
- tokens.push({ lineNum, indent, kind: 'item', value });
213
- continue;
214
- }
215
- const colonIdx = content.indexOf(':');
216
- if (colonIdx < 0) {
217
- return { ok: false, error: `line ${lineNum}: expected 'key:' or '- value' but got '${content}'` };
218
- }
219
- let key = content.slice(0, colonIdx).trim();
220
- const valuePart = content.slice(colonIdx + 1).trim();
221
- // An empty key is normally a hard error (roles.yaml/access.md want loud
222
- // failures), but a parser with ENTRY-level forgiveness (the group files:
223
- // one bad entry must not retire every other group) keeps it as a
224
- // whitespace key for its own skip-with-warning handling.
225
- if (!key) {
226
- if (!opts?.tolerateEmptyKeys) {
227
- return { ok: false, error: `line ${lineNum}: empty mapping key` };
228
- }
229
- key = ' '.repeat(++blankKeySeq);
230
- }
231
- tokens.push({ lineNum, indent, kind: 'kv', key, value: valuePart });
232
- }
233
- return { ok: true, tokens };
234
- }
235
- export function parseYamlSubset(text, opts) {
236
- const tok = tokenise(text, opts);
237
- if (!tok.ok)
238
- return tok;
239
- if (tok.tokens.length === 0)
240
- return { ok: true, value: {} };
241
- const root = {};
242
- const stack = [{ indent: -1, container: root, kind: 'map' }];
243
- for (let t = 0; t < tok.tokens.length; t++) {
244
- const cur = tok.tokens[t];
245
- while (stack.length > 1 && stack[stack.length - 1].indent >= cur.indent) {
246
- stack.pop();
247
- }
248
- const top = stack[stack.length - 1];
249
- if (cur.kind === 'item') {
250
- if (top.kind !== 'list') {
251
- return {
252
- ok: false,
253
- error: `line ${cur.lineNum}: list item with no enclosing list (indent ${cur.indent})`,
254
- };
255
- }
256
- top.container.push(cur.value);
257
- continue;
258
- }
259
- if (top.kind !== 'map') {
260
- return {
261
- ok: false,
262
- error: `line ${cur.lineNum}: mapping key '${cur.key}' inside a list — not supported`,
263
- };
264
- }
265
- const map = top.container;
266
- const key = cur.key;
267
- if (Object.prototype.hasOwnProperty.call(map, key)) {
268
- return { ok: false, error: `line ${cur.lineNum}: duplicate key '${key}'` };
269
- }
270
- if (cur.value !== '') {
271
- // Inline empty collections are the only flow-style YAML we accept, so
272
- // `owner: []` / `download: []` read as an empty list rather than the
273
- // scalar string "[]".
274
- if (cur.value === '[]') {
275
- map[key] = [];
276
- continue;
277
- }
278
- map[key] = cur.value;
279
- continue;
280
- }
281
- const next = tok.tokens[t + 1];
282
- if (!next || next.indent <= cur.indent) {
283
- map[key] = null;
284
- continue;
285
- }
286
- if (next.kind === 'item') {
287
- const list = [];
288
- map[key] = list;
289
- stack.push({ indent: cur.indent, container: list, kind: 'list' });
290
- }
291
- else {
292
- const sub = {};
293
- map[key] = sub;
294
- stack.push({ indent: cur.indent, container: sub, kind: 'map' });
295
- }
296
- }
297
- return { ok: true, value: root };
298
- }
299
- // ---------------------------------------------------------------------------
300
- // Frontmatter extraction
301
- // ---------------------------------------------------------------------------
302
- export function extractFrontmatter(text) {
303
- const lines = text.split(/\r?\n/);
304
- if (lines.length === 0 || lines[0].trim() !== '---') {
305
- return { ok: false, error: 'expected `---` on the first line' };
306
- }
307
- for (let i = 1; i < lines.length; i++) {
308
- if (lines[i].trim() === '---') {
309
- return { ok: true, frontmatter: lines.slice(1, i).join('\n') };
310
- }
311
- }
312
- return { ok: false, error: 'unterminated frontmatter — no closing `---` found' };
313
- }
314
- /** The text AFTER the closing frontmatter fence ('' when there is no fence). */
315
- export function bodyAfterFrontmatter(text) {
316
- const lines = text.split(/\r?\n/);
317
- if (lines.length === 0 || lines[0].trim() !== '---')
318
- return '';
319
- for (let i = 1; i < lines.length; i++) {
320
- if (lines[i].trim() === '---')
321
- return lines.slice(i + 1).join('\n');
322
- }
323
- return '';
324
- }
325
- // ---------------------------------------------------------------------------
326
- // Canonicalisation
327
- // ---------------------------------------------------------------------------
328
- export function canonicalRoleName(name) {
329
- return name.trim().toLowerCase().replace(/\s+/g, ' ');
330
- }
331
- export function canonicalEmail(email) {
332
- return email.trim().toLowerCase();
333
- }
334
- // ---------------------------------------------------------------------------
335
- // Entry parser
336
- // ---------------------------------------------------------------------------
337
- export function parseAccessEntry(raw) {
338
- if (typeof raw !== 'string')
339
- return { ok: false, error: 'entry must be a string' };
340
- const trimmed = raw.trim();
341
- if (!trimmed)
342
- return { ok: false, error: 'empty entry' };
343
- let deny = false;
344
- let body = trimmed;
345
- if (trimmed.startsWith(DENY_PREFIX)) {
346
- deny = true;
347
- body = trimmed.slice(DENY_PREFIX.length).trim();
348
- if (!body)
349
- return { ok: false, error: `'deny' with no principal` };
350
- }
351
- const userMatch = body.match(USER_REF_REGEX);
352
- if (userMatch) {
353
- const displayName = userMatch[1].trim();
354
- const email = canonicalEmail(userMatch[2]);
355
- if (!displayName)
356
- return { ok: false, error: `user reference '${body}' has no name` };
357
- if (!EMAIL_REGEX.test(email)) {
358
- return { ok: false, error: `user reference '${body}' has malformed email '${email}'` };
359
- }
360
- return { ok: true, entry: { kind: 'user', email, displayName, deny } };
361
- }
362
- if (body.includes('<') || body.includes('>')) {
363
- return {
364
- ok: false,
365
- error: `entry '${body}' looks like a user reference but doesn't match 'Name <email>' shape`,
366
- };
367
- }
368
- let role = canonicalRoleName(body);
369
- if (!role)
370
- return { ok: false, error: `empty role name in entry '${raw}'` };
371
- // Explicit role token: normalize `role/ <Name>` spacing so the canonical
372
- // form is always `role/<canonicalName>` — the exact alias key the principal
373
- // index registers for every role.
374
- if (role.startsWith(ROLE_TOKEN_PREFIX)) {
375
- const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
376
- if (!suffix)
377
- return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
378
- role = `${ROLE_TOKEN_PREFIX}${suffix}`;
379
- }
380
- return { ok: true, entry: { kind: 'role', role, displayRole: body, deny } };
381
- }
382
- // ---------------------------------------------------------------------------
383
- // roles.yaml + access.md parsers
384
- // ---------------------------------------------------------------------------
385
- export function parseRolesYaml(text) {
386
- const parsed = parseYamlSubset(text);
387
- if (!parsed.ok)
388
- return { ok: false, errors: [`roles.yaml: ${parsed.error}`] };
389
- const errors = [];
390
- const root = parsed.value;
391
- if (root == null || typeof root !== 'object' || Array.isArray(root)) {
392
- return { ok: false, errors: [`roles.yaml: must be a top-level mapping`] };
393
- }
394
- const rolesNode = root.roles;
395
- if (rolesNode == null || typeof rolesNode !== 'object' || Array.isArray(rolesNode)) {
396
- return { ok: false, errors: [`roles.yaml: missing top-level 'roles:' mapping`] };
397
- }
398
- const index = {
399
- byCanonical: new Map(),
400
- byEmail: new Map(),
401
- };
402
- for (const [displayName, value] of Object.entries(rolesNode)) {
403
- const canonical = canonicalRoleName(displayName);
404
- if (!canonical) {
405
- errors.push(`roles.yaml: empty role name`);
406
- continue;
407
- }
408
- if (RESERVED_ROLE_NAMES.has(canonical)) {
409
- errors.push(`roles.yaml: role '${displayName}' uses reserved name '${canonical}' — this token has special meaning in access entries and cannot be a roles.yaml role`);
410
- continue;
411
- }
412
- if (canonical.startsWith(ROLE_TOKEN_PREFIX)) {
413
- errors.push(`roles.yaml: role '${displayName}' starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix — that spelling is the explicit role token in access entries`);
414
- continue;
415
- }
416
- if (index.byCanonical.has(canonical)) {
417
- const prev = index.byCanonical.get(canonical).displayName;
418
- errors.push(`roles.yaml: role '${displayName}' canonicalises to '${canonical}', which is already declared as '${prev}'`);
419
- continue;
420
- }
421
- if (!Array.isArray(value)) {
422
- errors.push(`roles.yaml: role '${displayName}' must be a list of emails`);
423
- continue;
424
- }
425
- const emails = new Set();
426
- const groupRefs = new Set();
427
- for (const rawEmail of value) {
428
- if (typeof rawEmail !== 'string') {
429
- errors.push(`roles.yaml: role '${displayName}' has a non-string entry`);
430
- continue;
431
- }
432
- // `- group:<Name>` assigns the role to a whole group (expanded against
433
- // the active group source by `mergeGroupsIntoRoles`). Allowed on every
434
- // role, Admin included — the Admin invariant below only demands at
435
- // least one DIRECT email member so a broken directory can never leave
436
- // the deployment adminless.
437
- if (rawEmail.trim().toLowerCase().startsWith(GROUP_REF_PREFIX)) {
438
- const refName = canonicalRoleName(rawEmail.trim().slice(GROUP_REF_PREFIX.length));
439
- if (!refName) {
440
- errors.push(`roles.yaml: role '${displayName}' has an empty group reference '${rawEmail}'`);
441
- continue;
442
- }
443
- groupRefs.add(refName);
444
- continue;
445
- }
446
- const email = canonicalEmail(rawEmail);
447
- if (!EMAIL_REGEX.test(email)) {
448
- errors.push(`roles.yaml: role '${displayName}' has malformed email '${rawEmail}'`);
449
- continue;
450
- }
451
- emails.add(email);
452
- let set = index.byEmail.get(email);
453
- if (!set) {
454
- set = new Set();
455
- index.byEmail.set(email, set);
456
- }
457
- set.add(canonical);
458
- }
459
- index.byCanonical.set(canonical, { displayName: displayName.trim(), emails, groupRefs });
460
- }
461
- if (!index.byCanonical.has(ADMIN_CANONICAL)) {
462
- errors.push(`roles.yaml: must declare at least one 'Admin' role`);
463
- }
464
- else if (index.byCanonical.get(ADMIN_CANONICAL).emails.size === 0) {
465
- // The kept invariant: Admin may reference groups, but must ALWAYS retain
466
- // at least one direct email member — the rescue story requires an admin
467
- // whose membership does not depend on a reachable, well-configured
468
- // directory. A group-only Admin is as hard an error as an adminless one.
469
- errors.push(`roles.yaml: 'Admin' role has no direct email members — Admin must keep at least one individual email (group references alone are not enough)`);
470
- }
471
- if (errors.length)
472
- return { ok: false, errors };
473
- return { ok: true, index };
474
- }
475
- /**
476
- * Merge the active group source into the principal index and expand role →
477
- * group assignments. Mutates `index` in place; returns human-readable
478
- * warnings (callers log them — nothing here ever throws, because group
479
- * problems must degrade, not brick access resolution).
480
- *
481
- * Rules (the grant-grammar precedence):
482
- * - Every role is ALSO registered under its explicit `role/<canonical>`
483
- * alias — the token that always resolves to the role.
484
- * - A BARE name resolves GROUP-FIRST: when a group's canonical name
485
- * collides with a role's, the bare key resolves to the GROUP (warned);
486
- * the role stays reachable via `role/<canonical>`.
487
- * - Role `group:<Name>` refs — Admin's included — expand against the
488
- * merged groups; an unknown ref contributes nothing (warned).
489
- * - `byEmail` is rebuilt so each member holds exactly the tokens that
490
- * resolve to a principal they belong to (bare + `role/` alias for roles,
491
- * bare for groups).
492
- */
493
- export function mergeGroupsIntoRoles(index, groups, sourceFile) {
494
- const warnings = [];
495
- // Snapshot before any group lands: at this point the index holds roles only.
496
- const roleRecords = new Map(index.byCanonical);
497
- // 1. Expand role → group assignments (Admin included — its safety net is
498
- // the parse-time "at least one direct email" invariant, not a merge skip).
499
- for (const [, principal] of roleRecords) {
500
- if (!principal.groupRefs?.size)
501
- continue;
502
- for (const ref of principal.groupRefs) {
503
- const group = groups.get(ref);
504
- if (!group) {
505
- warnings.push(`roles.yaml: role '${principal.displayName}' references unknown group '${ref}' — reference ignored`);
506
- continue;
507
- }
508
- for (const email of group.emails)
509
- principal.emails.add(email);
510
- }
511
- }
512
- // 2. Bare-name precedence: groups win the bare token; the role keeps its
513
- // `role/<canonical>` alias registered below.
514
- for (const [canonical, def] of groups) {
515
- if (roleRecords.has(canonical)) {
516
- warnings.push(`${sourceFile}: group '${def.displayName}' shares its name with a role — the bare name now resolves to the GROUP; use '${ROLE_TOKEN_PREFIX}${canonical}' to reference the role`);
517
- }
518
- index.byCanonical.set(canonical, {
519
- displayName: def.displayName,
520
- emails: new Set(def.emails),
521
- kind: 'group',
522
- });
523
- }
524
- // 3. Explicit `role/<canonical>` alias for every role (same record — the
525
- // alias and the bare key, when the role still owns it, stay in lockstep).
526
- for (const [canonical, principal] of roleRecords) {
527
- principal.kind = 'role';
528
- index.byCanonical.set(`${ROLE_TOKEN_PREFIX}${canonical}`, principal);
529
- }
530
- // 4. Rebuild the email → tokens map from the final index so membership
531
- // reflects the post-precedence keys (a collided role's members no longer
532
- // hold the bare token unless the group also contains them).
533
- index.byEmail.clear();
534
- for (const [key, principal] of index.byCanonical) {
535
- for (const email of principal.emails) {
536
- let set = index.byEmail.get(email);
537
- if (!set) {
538
- set = new Set();
539
- index.byEmail.set(email, set);
540
- }
541
- set.add(key);
542
- }
543
- }
544
- return warnings;
545
- }
546
81
  /** True for the errno codes that mean "the file genuinely is not there". */
547
82
  function isAbsenceError(err) {
548
83
  const code = err?.code;
@@ -608,170 +143,6 @@ export async function loadActiveGroups(read) {
608
143
  return broken(sourceFile, parsed.errors.join('; '));
609
144
  return { groups: parsed.groups, sourceFile, warnings: parsed.warnings, health: { ok: true } };
610
145
  }
611
- /**
612
- * Does an `access.md` body declare access rules — i.e. is the file in the NEW
613
- * (body-governs-the-folder) format?
614
- *
615
- * The two-format story: historically the FRONTMATTER carried the folder's
616
- * rules and the file could not govern itself. The new format follows the
617
- * convention every other file uses — frontmatter is about the FILE (who may
618
- * read/write `access.md` itself), and the content (the body) is the folder's
619
- * rules. The compat rule, applied per file: **when the body is not parsable as
620
- * rules, the frontmatter is resolved for the folder as before.**
621
- *
622
- * "Parsable as rules" is deliberately shallow — the body YAML-parses to a
623
- * mapping that names at least one known verb. A prose body (the repo-root
624
- * README-style file) fails the YAML parse or carries no verb key and stays
625
- * legacy. A body that DOES name a verb has claimed to be rules: shape errors
626
- * inside it (a scalar verb, a malformed entry) are then hard parse ERRORS of
627
- * the file, never a silent fallback to the frontmatter — falling back would
628
- * let a typo in the body hand the folder to the frontmatter's (possibly
629
- * `read: everyone`) self-rules.
630
- */
631
- export function accessMdDeclaresBodyRules(text) {
632
- const body = bodyAfterFrontmatter(text);
633
- if (!body.trim())
634
- return false;
635
- const parsed = parseYamlSubset(body);
636
- if (!parsed.ok)
637
- return false;
638
- const root = parsed.value;
639
- if (root == null || typeof root !== 'object' || Array.isArray(root))
640
- return false;
641
- return Object.keys(root).some((k) => KNOWN_VERBS_SET.has(k));
642
- }
643
- /**
644
- * The access verbs an `access.md` declares FOR ITSELF — its frontmatter, and
645
- * only in the new format (body-governed). A legacy file cannot govern itself:
646
- * its frontmatter IS the folder's rules, so returning them as own-entries
647
- * would double-apply them at the file scope.
648
- *
649
- * Parsed forgivingly (like node frontmatter): the new-format frontmatter may
650
- * carry non-access keys, and a typo there must not make the file unreadable.
651
- */
652
- export function accessMdSelfEntries(text) {
653
- if (!accessMdDeclaresBodyRules(text))
654
- return null;
655
- return parseOwnAccessEntries(text);
656
- }
657
- /**
658
- * Parse one `access.md`'s FOLDER rules into its per-verb entry lists.
659
- *
660
- * Two formats (see {@link accessMdDeclaresBodyRules}): when the body declares
661
- * rules, the body is parsed (new format — frontmatter then governs the file
662
- * itself via {@link accessMdSelfEntries}); otherwise the frontmatter is parsed
663
- * (legacy format), exactly as before.
664
- *
665
- * Exported so read-only surfaces can report WHERE rules are declared without
666
- * reimplementing the parse (see `access-declarations.ts`). Exporting changes
667
- * nothing about resolution — this is the same function the resolver builds its
668
- * model from, so a display of declarations can never drift from the rules that
669
- * are actually enforced.
670
- */
671
- export function parseAccessFile(text, relativePath) {
672
- const bodyFormat = accessMdDeclaresBodyRules(text);
673
- let ruleSource;
674
- if (bodyFormat) {
675
- ruleSource = bodyAfterFrontmatter(text);
676
- }
677
- else {
678
- const fm = extractFrontmatter(text);
679
- if (!fm.ok)
680
- return { ok: false, errors: [`${relativePath}: ${fm.error}`] };
681
- ruleSource = fm.frontmatter;
682
- }
683
- const parsed = parseYamlSubset(ruleSource);
684
- if (!parsed.ok)
685
- return { ok: false, errors: [`${relativePath}: ${parsed.error}`] };
686
- const root = parsed.value;
687
- if (root == null || typeof root !== 'object' || Array.isArray(root)) {
688
- return {
689
- ok: false,
690
- errors: [`${relativePath}: ${bodyFormat ? 'body' : 'frontmatter'} must be a mapping`],
691
- };
692
- }
693
- const errors = [];
694
- const warnings = [];
695
- const entries = emptyEntries();
696
- for (const [key, value] of Object.entries(root)) {
697
- if (!KNOWN_VERBS_SET.has(key)) {
698
- // Forgiving by design: unknown keys (typos, future verbs, custom
699
- // metadata an operator chose to colocate) must not break the access
700
- // tree. Warn so operators see the typo in logs, then move on as if
701
- // the key didn't exist. The verbs we DO understand still parse.
702
- warnings.push(`${relativePath}: unknown access key '${key}' — ignored (known: ${[...KNOWN_VERBS].join(', ')})`);
703
- continue;
704
- }
705
- const verb = key;
706
- if (!Array.isArray(value)) {
707
- errors.push(`${relativePath}: '${key}:' must be a list`);
708
- continue;
709
- }
710
- const list = [];
711
- for (const raw of value) {
712
- const result = parseAccessEntry(raw);
713
- if (!result.ok) {
714
- errors.push(`${relativePath}: ${result.error}`);
715
- continue;
716
- }
717
- list.push(result.entry);
718
- }
719
- entries[verb] = list;
720
- }
721
- if (errors.length)
722
- return { ok: false, errors };
723
- const slash = relativePath.lastIndexOf('/');
724
- const dir = slash === -1 ? '' : relativePath.slice(0, slash);
725
- return { ok: true, file: { path: relativePath, dir, entries }, warnings };
726
- }
727
- /**
728
- * Parse the access verbs a node file declares in its own YAML frontmatter.
729
- * Returns the per-verb entry lists, or null when the file has no frontmatter
730
- * or declares no access verb at all.
731
- *
732
- * Forgiving by design — a node's frontmatter legitimately carries non-access
733
- * keys (notably `nodeType:`), so unknown keys are ignored, and a malformed
734
- * entry is dropped rather than failing the file. A typo in a node's `owner:`
735
- * must never make the node unreadable; it just doesn't grant anything.
736
- */
737
- export function parseOwnAccessEntries(text) {
738
- const fm = extractFrontmatter(text);
739
- if (!fm.ok)
740
- return null;
741
- const parsed = parseYamlSubset(fm.frontmatter);
742
- if (!parsed.ok)
743
- return null;
744
- const root = parsed.value;
745
- if (root == null || typeof root !== 'object' || Array.isArray(root))
746
- return null;
747
- const entries = emptyEntries();
748
- let sawVerb = false;
749
- for (const [key, value] of Object.entries(root)) {
750
- if (!KNOWN_VERBS_SET.has(key))
751
- continue; // ignore nodeType, etc.
752
- // Accept both the list form (`owner:\n - A\n - B`) and the convenience
753
- // single-value scalar form (`owner: Test <test@test.com>`) — the latter
754
- // is the natural way to name one owner in a node's own frontmatter.
755
- // Anything else (a mapping, null) is malformed → skip forgivingly.
756
- let raws;
757
- if (Array.isArray(value))
758
- raws = value;
759
- else if (typeof value === 'string' && value.trim())
760
- raws = [value];
761
- else
762
- continue;
763
- const verb = key;
764
- const list = [];
765
- for (const raw of raws) {
766
- const result = parseAccessEntry(raw);
767
- if (result.ok)
768
- list.push(result.entry);
769
- }
770
- entries[verb] = list;
771
- sawVerb = true;
772
- }
773
- return sawVerb ? entries : null;
774
- }
775
146
  /**
776
147
  * Resolve the per-scope verdicts for `verb` at `relativePath`, ordered
777
148
  * **closest-to-the-file first**: the node's own frontmatter, then its