@bevel-software/platform-core-backend 0.11.1 → 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
@@ -0,0 +1,644 @@
1
+ /**
2
+ * The pure access grammar — every parser, canonicaliser, constant, and type
3
+ * that defines WHAT the roles/groups/access files mean, with no I/O and no
4
+ * service state. Extracted from `modules/access/access-control.service.ts`;
5
+ * the resolver and the `AccessControlService` (filesystem/git model loading,
6
+ * caching, permission gates) stay there and consume this grammar.
7
+ *
8
+ * This module is a LEAF: files here import each other freely, but from the
9
+ * outside world only node builtins, platform-shared, `@mastra/core/workspace`
10
+ * types, and `src/shared/*` — never another `modules/*` directory. (The
11
+ * `./group-files.js` import below is access-model's own file, not a breach.)
12
+ */
13
+ // ---------------------------------------------------------------------------
14
+ // Constants — kept in lockstep with knowledge-base/lib/access-control.js
15
+ // ---------------------------------------------------------------------------
16
+ export const ADMIN_CANONICAL = 'admin';
17
+ /**
18
+ * Verbs the resolver understands in an `access.md` frontmatter. Each verb
19
+ * is a list of grants (role or `Name <email>` references, optionally
20
+ * prefixed with `deny `). Keep `Verb` and `KNOWN_VERBS` in lockstep —
21
+ * `AccessFile.entries` is statically keyed on this union.
22
+ *
23
+ * `read` controls who may VIEW a path (the file viewer, embed surface, and
24
+ * the agent's read tools). It is **default-deny**: a path with no effective
25
+ * `read:` or `owner:` grant is not readable. To make content public, list the
26
+ * built-in role `everyone` under `read:`.
27
+ *
28
+ * The verbs nest: `owner` is a superset of `read` + `write` + `download`, and
29
+ * `write` is itself a superset of `read` (anyone who can edit can view). An
30
+ * `owner` grant therefore confers all three lower verbs, a `write` grant
31
+ * additionally confers `read`, and `owner` also marks the principal as a
32
+ * contact point for the node (surfaced in the UI so users know who to ask).
33
+ * See `sourceVerbsFor` for how these implications fold into resolution.
34
+ */
35
+ export const KNOWN_VERBS = ['read', 'write', 'download', 'owner'];
36
+ const KNOWN_VERBS_SET = new Set(KNOWN_VERBS);
37
+ export const EVERYONE_CANONICAL = 'everyone';
38
+ /** Display name for the built-in `everyone` role in the share UI. */
39
+ export const EVERYONE_DISPLAY = 'Everyone';
40
+ /**
41
+ * Verbs whose entries contribute to resolving `verb`, target verb first.
42
+ * `owner` implies `read`, `write`, and `download`; `write` additionally
43
+ * implies `read`. So resolving `read` folds in `write` and `owner`, resolving
44
+ * `write`/`download` folds in `owner`, and resolving `owner` uses only `owner`.
45
+ *
46
+ * The implication is **grant-only** (see `resolveAtPath`): a superset grant
47
+ * confers the lower verb, but a superset *denial* does not — `deny write` says
48
+ * nothing about `read`, so it never strips a separate read grant. The target
49
+ * verb itself contributes both its grants and its denials.
50
+ */
51
+ export function sourceVerbsFor(verb) {
52
+ switch (verb) {
53
+ case 'owner':
54
+ return ['owner'];
55
+ case 'read':
56
+ return ['read', 'write', 'owner'];
57
+ default:
58
+ return [verb, 'owner'];
59
+ }
60
+ }
61
+ export const RESERVED_ROLE_NAMES = new Set(['deny', EVERYONE_CANONICAL]);
62
+ export const DENY_PREFIX = 'deny ';
63
+ /**
64
+ * Member-entry prefix in roles.yaml that references a GROUP instead of an
65
+ * email: `- group:Engineering`. Explicit on purpose — membership kind is
66
+ * never guessed from string shape. Valid on EVERY role including Admin, with
67
+ * one kept invariant (see `parseRolesYaml`): Admin must always retain at
68
+ * least one direct email member, so a misconfigured or unreachable directory
69
+ * can never leave the deployment without a rescuable admin.
70
+ */
71
+ export const GROUP_REF_PREFIX = 'group:';
72
+ /**
73
+ * Explicit ROLE token prefix in access.md entries: `role/<Name>` resolves to
74
+ * the roles.yaml role only, never a group. A BARE name resolves GROUP-FIRST
75
+ * and falls back to the role — so `role/` is the escape hatch when a group
76
+ * shares the role's name. The prefix is reserved in the group name-safety
77
+ * rules (a group may never be named `role/...`), and every role is also
78
+ * registered in the principal index under its `role/<canonical>` alias.
79
+ */
80
+ export const ROLE_TOKEN_PREFIX = 'role/';
81
+ export const USER_REF_REGEX = /^(.+?)\s+<\s*([^<>\s]+@[^<>\s]+)\s*>\s*$/;
82
+ export const EMAIL_REGEX = /^[^<>\s@]+@[^<>\s@]+\.[^<>\s@]+$/;
83
+ function emptyEntries() {
84
+ const out = {};
85
+ for (const v of KNOWN_VERBS)
86
+ out[v] = [];
87
+ return out;
88
+ }
89
+ export function isAccessMdPath(p) {
90
+ return p === 'access.md' || p.endsWith('/access.md');
91
+ }
92
+ /**
93
+ * Extensions of node files whose OWN `---` frontmatter can carry access verbs
94
+ * the resolver enforces (`readOwnEntries` → `parseOwnAccessEntries`). The
95
+ * SINGLE source of truth for every surface that enumerates candidate files —
96
+ * the access-declarations scan and the shared `KbReferenceScanner` (which
97
+ * must scan/rewrite the same set, or a rename strands a live `.tool`
98
+ * frontmatter grant). `access.md` is covered by `.md`.
99
+ */
100
+ export const ACCESS_FRONTMATTER_EXTENSIONS = ['.md', '.tool'];
101
+ /** True when `p` is a file the resolver reads access frontmatter from. */
102
+ export function hasAccessFrontmatterExtension(p) {
103
+ return ACCESS_FRONTMATTER_EXTENSIONS.some((ext) => p.endsWith(ext));
104
+ }
105
+ /**
106
+ * Strip a trailing `# comment` the way the YAML-subset tokeniser reads a
107
+ * line: a `#` at the start (after only whitespace) or preceded by whitespace
108
+ * begins a comment. Exported so the reference scanner matches tokens against
109
+ * the SAME comment rule the resolver parses with (a `- GTM Team # sales`
110
+ * entry is the token `GTM Team`, never `GTM Team # sales`).
111
+ */
112
+ export function stripComment(line) {
113
+ let inWs = true;
114
+ for (let i = 0; i < line.length; i++) {
115
+ const ch = line[i];
116
+ if (ch === '#' && (inWs || (i > 0 && /\s/.test(line[i - 1])))) {
117
+ return line.slice(0, i);
118
+ }
119
+ if (!/\s/.test(ch))
120
+ inWs = false;
121
+ }
122
+ return line;
123
+ }
124
+ function tokenise(text, opts) {
125
+ const lines = text.split(/\r?\n/);
126
+ const tokens = [];
127
+ // Tolerated blank keys are uniquified as runs of spaces so a SECOND blank
128
+ // key doesn't trip the duplicate-key check (which would retire every valid
129
+ // group after it). All-whitespace keys still canonicalize to '' downstream,
130
+ // so the entry-level "empty group name — skipped" handling sees them all.
131
+ let blankKeySeq = 0;
132
+ for (let i = 0; i < lines.length; i++) {
133
+ const stripped = stripComment(lines[i]).replace(/\s+$/, '');
134
+ if (!stripped.trim())
135
+ continue;
136
+ const indentMatch = stripped.match(/^( *)/);
137
+ const indent = indentMatch ? indentMatch[1].length : 0;
138
+ const content = stripped.slice(indent);
139
+ const lineNum = i + 1;
140
+ if (content.startsWith('- ') || content === '-') {
141
+ const value = content === '-' ? '' : content.slice(2).trim();
142
+ tokens.push({ lineNum, indent, kind: 'item', value });
143
+ continue;
144
+ }
145
+ const colonIdx = content.indexOf(':');
146
+ if (colonIdx < 0) {
147
+ return { ok: false, error: `line ${lineNum}: expected 'key:' or '- value' but got '${content}'` };
148
+ }
149
+ let key = content.slice(0, colonIdx).trim();
150
+ const valuePart = content.slice(colonIdx + 1).trim();
151
+ // An empty key is normally a hard error (roles.yaml/access.md want loud
152
+ // failures), but a parser with ENTRY-level forgiveness (the group files:
153
+ // one bad entry must not retire every other group) keeps it as a
154
+ // whitespace key for its own skip-with-warning handling.
155
+ if (!key) {
156
+ if (!opts?.tolerateEmptyKeys) {
157
+ return { ok: false, error: `line ${lineNum}: empty mapping key` };
158
+ }
159
+ key = ' '.repeat(++blankKeySeq);
160
+ }
161
+ tokens.push({ lineNum, indent, kind: 'kv', key, value: valuePart });
162
+ }
163
+ return { ok: true, tokens };
164
+ }
165
+ export function parseYamlSubset(text, opts) {
166
+ const tok = tokenise(text, opts);
167
+ if (!tok.ok)
168
+ return tok;
169
+ if (tok.tokens.length === 0)
170
+ return { ok: true, value: {} };
171
+ const root = {};
172
+ const stack = [{ indent: -1, container: root, kind: 'map' }];
173
+ for (let t = 0; t < tok.tokens.length; t++) {
174
+ const cur = tok.tokens[t];
175
+ while (stack.length > 1 && stack[stack.length - 1].indent >= cur.indent) {
176
+ stack.pop();
177
+ }
178
+ const top = stack[stack.length - 1];
179
+ if (cur.kind === 'item') {
180
+ if (top.kind !== 'list') {
181
+ return {
182
+ ok: false,
183
+ error: `line ${cur.lineNum}: list item with no enclosing list (indent ${cur.indent})`,
184
+ };
185
+ }
186
+ top.container.push(cur.value);
187
+ continue;
188
+ }
189
+ if (top.kind !== 'map') {
190
+ return {
191
+ ok: false,
192
+ error: `line ${cur.lineNum}: mapping key '${cur.key}' inside a list — not supported`,
193
+ };
194
+ }
195
+ const map = top.container;
196
+ const key = cur.key;
197
+ if (Object.prototype.hasOwnProperty.call(map, key)) {
198
+ return { ok: false, error: `line ${cur.lineNum}: duplicate key '${key}'` };
199
+ }
200
+ if (cur.value !== '') {
201
+ // Inline empty collections are the only flow-style YAML we accept, so
202
+ // `owner: []` reads as an empty list and `groups: {}` as an empty
203
+ // mapping rather than the scalar strings "[]" / "{}".
204
+ if (cur.value === '[]') {
205
+ map[key] = [];
206
+ continue;
207
+ }
208
+ if (cur.value === '{}') {
209
+ map[key] = {};
210
+ continue;
211
+ }
212
+ map[key] = cur.value;
213
+ continue;
214
+ }
215
+ const next = tok.tokens[t + 1];
216
+ if (!next || next.indent <= cur.indent) {
217
+ map[key] = null;
218
+ continue;
219
+ }
220
+ if (next.kind === 'item') {
221
+ const list = [];
222
+ map[key] = list;
223
+ stack.push({ indent: cur.indent, container: list, kind: 'list' });
224
+ }
225
+ else {
226
+ const sub = {};
227
+ map[key] = sub;
228
+ stack.push({ indent: cur.indent, container: sub, kind: 'map' });
229
+ }
230
+ }
231
+ return { ok: true, value: root };
232
+ }
233
+ // ---------------------------------------------------------------------------
234
+ // Frontmatter extraction
235
+ // ---------------------------------------------------------------------------
236
+ export function extractFrontmatter(text) {
237
+ const lines = text.split(/\r?\n/);
238
+ if (lines.length === 0 || lines[0].trim() !== '---') {
239
+ return { ok: false, error: 'expected `---` on the first line' };
240
+ }
241
+ for (let i = 1; i < lines.length; i++) {
242
+ if (lines[i].trim() === '---') {
243
+ return { ok: true, frontmatter: lines.slice(1, i).join('\n') };
244
+ }
245
+ }
246
+ return { ok: false, error: 'unterminated frontmatter — no closing `---` found' };
247
+ }
248
+ /** The text AFTER the closing frontmatter fence ('' when there is no fence). */
249
+ export function bodyAfterFrontmatter(text) {
250
+ const lines = text.split(/\r?\n/);
251
+ if (lines.length === 0 || lines[0].trim() !== '---')
252
+ return '';
253
+ for (let i = 1; i < lines.length; i++) {
254
+ if (lines[i].trim() === '---')
255
+ return lines.slice(i + 1).join('\n');
256
+ }
257
+ return '';
258
+ }
259
+ // ---------------------------------------------------------------------------
260
+ // Canonicalisation
261
+ // ---------------------------------------------------------------------------
262
+ export function canonicalRoleName(name) {
263
+ return name.trim().toLowerCase().replace(/\s+/g, ' ');
264
+ }
265
+ export function canonicalEmail(email) {
266
+ return email.trim().toLowerCase();
267
+ }
268
+ // ---------------------------------------------------------------------------
269
+ // Entry parser
270
+ // ---------------------------------------------------------------------------
271
+ export function parseAccessEntry(raw) {
272
+ if (typeof raw !== 'string')
273
+ return { ok: false, error: 'entry must be a string' };
274
+ const trimmed = raw.trim();
275
+ if (!trimmed)
276
+ return { ok: false, error: 'empty entry' };
277
+ let deny = false;
278
+ let body = trimmed;
279
+ if (trimmed.startsWith(DENY_PREFIX)) {
280
+ deny = true;
281
+ body = trimmed.slice(DENY_PREFIX.length).trim();
282
+ if (!body)
283
+ return { ok: false, error: `'deny' with no principal` };
284
+ }
285
+ const userMatch = body.match(USER_REF_REGEX);
286
+ if (userMatch) {
287
+ const displayName = userMatch[1].trim();
288
+ const email = canonicalEmail(userMatch[2]);
289
+ if (!displayName)
290
+ return { ok: false, error: `user reference '${body}' has no name` };
291
+ if (!EMAIL_REGEX.test(email)) {
292
+ return { ok: false, error: `user reference '${body}' has malformed email '${email}'` };
293
+ }
294
+ return { ok: true, entry: { kind: 'user', email, displayName, deny } };
295
+ }
296
+ if (body.includes('<') || body.includes('>')) {
297
+ return {
298
+ ok: false,
299
+ error: `entry '${body}' looks like a user reference but doesn't match 'Name <email>' shape`,
300
+ };
301
+ }
302
+ let role = canonicalRoleName(body);
303
+ if (!role)
304
+ return { ok: false, error: `empty role name in entry '${raw}'` };
305
+ // Explicit role token: normalize `role/ <Name>` spacing so the canonical
306
+ // form is always `role/<canonicalName>` — the exact alias key the principal
307
+ // index registers for every role.
308
+ if (role.startsWith(ROLE_TOKEN_PREFIX)) {
309
+ const suffix = canonicalRoleName(role.slice(ROLE_TOKEN_PREFIX.length));
310
+ if (!suffix)
311
+ return { ok: false, error: `entry '${body}' names no role after '${ROLE_TOKEN_PREFIX}'` };
312
+ role = `${ROLE_TOKEN_PREFIX}${suffix}`;
313
+ }
314
+ return { ok: true, entry: { kind: 'role', role, displayRole: body, deny } };
315
+ }
316
+ // ---------------------------------------------------------------------------
317
+ // roles.yaml + access.md parsers
318
+ // ---------------------------------------------------------------------------
319
+ export function parseRolesYaml(text) {
320
+ const parsed = parseYamlSubset(text);
321
+ if (!parsed.ok)
322
+ return { ok: false, errors: [`roles.yaml: ${parsed.error}`] };
323
+ const errors = [];
324
+ const root = parsed.value;
325
+ if (root == null || typeof root !== 'object' || Array.isArray(root)) {
326
+ return { ok: false, errors: [`roles.yaml: must be a top-level mapping`] };
327
+ }
328
+ const rolesNode = root.roles;
329
+ if (rolesNode == null || typeof rolesNode !== 'object' || Array.isArray(rolesNode)) {
330
+ return { ok: false, errors: [`roles.yaml: missing top-level 'roles:' mapping`] };
331
+ }
332
+ const index = {
333
+ byCanonical: new Map(),
334
+ byEmail: new Map(),
335
+ };
336
+ for (const [displayName, value] of Object.entries(rolesNode)) {
337
+ const canonical = canonicalRoleName(displayName);
338
+ if (!canonical) {
339
+ errors.push(`roles.yaml: empty role name`);
340
+ continue;
341
+ }
342
+ if (RESERVED_ROLE_NAMES.has(canonical)) {
343
+ 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`);
344
+ continue;
345
+ }
346
+ if (canonical.startsWith(ROLE_TOKEN_PREFIX)) {
347
+ errors.push(`roles.yaml: role '${displayName}' starts with the reserved '${ROLE_TOKEN_PREFIX}' prefix — that spelling is the explicit role token in access entries`);
348
+ continue;
349
+ }
350
+ if (index.byCanonical.has(canonical)) {
351
+ const prev = index.byCanonical.get(canonical).displayName;
352
+ errors.push(`roles.yaml: role '${displayName}' canonicalises to '${canonical}', which is already declared as '${prev}'`);
353
+ continue;
354
+ }
355
+ if (!Array.isArray(value)) {
356
+ errors.push(`roles.yaml: role '${displayName}' must be a list of emails`);
357
+ continue;
358
+ }
359
+ const emails = new Set();
360
+ const groupRefs = new Set();
361
+ for (const rawEmail of value) {
362
+ if (typeof rawEmail !== 'string') {
363
+ errors.push(`roles.yaml: role '${displayName}' has a non-string entry`);
364
+ continue;
365
+ }
366
+ // `- group:<Name>` assigns the role to a whole group (expanded against
367
+ // the active group source by `mergeGroupsIntoRoles`). Allowed on every
368
+ // role, Admin included — the Admin invariant below only demands at
369
+ // least one DIRECT email member so a broken directory can never leave
370
+ // the deployment adminless.
371
+ if (rawEmail.trim().toLowerCase().startsWith(GROUP_REF_PREFIX)) {
372
+ const refName = canonicalRoleName(rawEmail.trim().slice(GROUP_REF_PREFIX.length));
373
+ if (!refName) {
374
+ errors.push(`roles.yaml: role '${displayName}' has an empty group reference '${rawEmail}'`);
375
+ continue;
376
+ }
377
+ groupRefs.add(refName);
378
+ continue;
379
+ }
380
+ const email = canonicalEmail(rawEmail);
381
+ if (!EMAIL_REGEX.test(email)) {
382
+ errors.push(`roles.yaml: role '${displayName}' has malformed email '${rawEmail}'`);
383
+ continue;
384
+ }
385
+ emails.add(email);
386
+ let set = index.byEmail.get(email);
387
+ if (!set) {
388
+ set = new Set();
389
+ index.byEmail.set(email, set);
390
+ }
391
+ set.add(canonical);
392
+ }
393
+ index.byCanonical.set(canonical, { displayName: displayName.trim(), emails, groupRefs });
394
+ }
395
+ if (!index.byCanonical.has(ADMIN_CANONICAL)) {
396
+ errors.push(`roles.yaml: must declare at least one 'Admin' role`);
397
+ }
398
+ else if (index.byCanonical.get(ADMIN_CANONICAL).emails.size === 0) {
399
+ // The kept invariant: Admin may reference groups, but must ALWAYS retain
400
+ // at least one direct email member — the rescue story requires an admin
401
+ // whose membership does not depend on a reachable, well-configured
402
+ // directory. A group-only Admin is as hard an error as an adminless one.
403
+ 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)`);
404
+ }
405
+ if (errors.length)
406
+ return { ok: false, errors };
407
+ return { ok: true, index };
408
+ }
409
+ /**
410
+ * Merge the active group source into the principal index and expand role →
411
+ * group assignments. Mutates `index` in place; returns human-readable
412
+ * warnings (callers log them — nothing here ever throws, because group
413
+ * problems must degrade, not brick access resolution).
414
+ *
415
+ * Rules (the grant-grammar precedence):
416
+ * - Every role is ALSO registered under its explicit `role/<canonical>`
417
+ * alias — the token that always resolves to the role.
418
+ * - A BARE name resolves GROUP-FIRST: when a group's canonical name
419
+ * collides with a role's, the bare key resolves to the GROUP (warned);
420
+ * the role stays reachable via `role/<canonical>`.
421
+ * - Role `group:<Name>` refs — Admin's included — expand against the
422
+ * merged groups; an unknown ref contributes nothing (warned).
423
+ * - `byEmail` is rebuilt so each member holds exactly the tokens that
424
+ * resolve to a principal they belong to (bare + `role/` alias for roles,
425
+ * bare for groups).
426
+ */
427
+ export function mergeGroupsIntoRoles(index, groups, sourceFile) {
428
+ const warnings = [];
429
+ // Snapshot before any group lands: at this point the index holds roles only.
430
+ const roleRecords = new Map(index.byCanonical);
431
+ // 1. Expand role → group assignments (Admin included — its safety net is
432
+ // the parse-time "at least one direct email" invariant, not a merge skip).
433
+ for (const [, principal] of roleRecords) {
434
+ if (!principal.groupRefs?.size)
435
+ continue;
436
+ for (const ref of principal.groupRefs) {
437
+ const group = groups.get(ref);
438
+ if (!group) {
439
+ warnings.push(`roles.yaml: role '${principal.displayName}' references unknown group '${ref}' — reference ignored`);
440
+ continue;
441
+ }
442
+ for (const email of group.emails)
443
+ principal.emails.add(email);
444
+ }
445
+ }
446
+ // 2. Bare-name precedence: groups win the bare token; the role keeps its
447
+ // `role/<canonical>` alias registered below.
448
+ for (const [canonical, def] of groups) {
449
+ if (roleRecords.has(canonical)) {
450
+ 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`);
451
+ }
452
+ index.byCanonical.set(canonical, {
453
+ displayName: def.displayName,
454
+ emails: new Set(def.emails),
455
+ kind: 'group',
456
+ });
457
+ }
458
+ // 3. Explicit `role/<canonical>` alias for every role (same record — the
459
+ // alias and the bare key, when the role still owns it, stay in lockstep).
460
+ for (const [canonical, principal] of roleRecords) {
461
+ principal.kind = 'role';
462
+ index.byCanonical.set(`${ROLE_TOKEN_PREFIX}${canonical}`, principal);
463
+ }
464
+ // 4. Rebuild the email → tokens map from the final index so membership
465
+ // reflects the post-precedence keys (a collided role's members no longer
466
+ // hold the bare token unless the group also contains them).
467
+ index.byEmail.clear();
468
+ for (const [key, principal] of index.byCanonical) {
469
+ for (const email of principal.emails) {
470
+ let set = index.byEmail.get(email);
471
+ if (!set) {
472
+ set = new Set();
473
+ index.byEmail.set(email, set);
474
+ }
475
+ set.add(key);
476
+ }
477
+ }
478
+ return warnings;
479
+ }
480
+ /**
481
+ * Does an `access.md` body declare access rules — i.e. is the file in the NEW
482
+ * (body-governs-the-folder) format?
483
+ *
484
+ * The two-format story: historically the FRONTMATTER carried the folder's
485
+ * rules and the file could not govern itself. The new format follows the
486
+ * convention every other file uses — frontmatter is about the FILE (who may
487
+ * read/write `access.md` itself), and the content (the body) is the folder's
488
+ * rules. The compat rule, applied per file: **when the body is not parsable as
489
+ * rules, the frontmatter is resolved for the folder as before.**
490
+ *
491
+ * "Parsable as rules" is deliberately shallow — the body YAML-parses to a
492
+ * mapping that names at least one known verb. A prose body (the repo-root
493
+ * README-style file) fails the YAML parse or carries no verb key and stays
494
+ * legacy. A body that DOES name a verb has claimed to be rules: shape errors
495
+ * inside it (a scalar verb, a malformed entry) are then hard parse ERRORS of
496
+ * the file, never a silent fallback to the frontmatter — falling back would
497
+ * let a typo in the body hand the folder to the frontmatter's (possibly
498
+ * `read: everyone`) self-rules.
499
+ */
500
+ export function accessMdDeclaresBodyRules(text) {
501
+ const body = bodyAfterFrontmatter(text);
502
+ if (!body.trim())
503
+ return false;
504
+ const parsed = parseYamlSubset(body);
505
+ if (!parsed.ok)
506
+ return false;
507
+ const root = parsed.value;
508
+ if (root == null || typeof root !== 'object' || Array.isArray(root))
509
+ return false;
510
+ return Object.keys(root).some((k) => KNOWN_VERBS_SET.has(k));
511
+ }
512
+ /**
513
+ * The access verbs an `access.md` declares FOR ITSELF — its frontmatter, and
514
+ * only in the new format (body-governed). A legacy file cannot govern itself:
515
+ * its frontmatter IS the folder's rules, so returning them as own-entries
516
+ * would double-apply them at the file scope.
517
+ *
518
+ * Parsed forgivingly (like node frontmatter): the new-format frontmatter may
519
+ * carry non-access keys, and a typo there must not make the file unreadable.
520
+ */
521
+ export function accessMdSelfEntries(text) {
522
+ if (!accessMdDeclaresBodyRules(text))
523
+ return null;
524
+ return parseOwnAccessEntries(text);
525
+ }
526
+ /**
527
+ * Parse one `access.md`'s FOLDER rules into its per-verb entry lists.
528
+ *
529
+ * Two formats (see {@link accessMdDeclaresBodyRules}): when the body declares
530
+ * rules, the body is parsed (new format — frontmatter then governs the file
531
+ * itself via {@link accessMdSelfEntries}); otherwise the frontmatter is parsed
532
+ * (legacy format), exactly as before.
533
+ *
534
+ * Exported so read-only surfaces can report WHERE rules are declared without
535
+ * reimplementing the parse (see `access-declarations.ts`). Exporting changes
536
+ * nothing about resolution — this is the same function the resolver builds its
537
+ * model from, so a display of declarations can never drift from the rules that
538
+ * are actually enforced.
539
+ */
540
+ export function parseAccessFile(text, relativePath) {
541
+ const bodyFormat = accessMdDeclaresBodyRules(text);
542
+ let ruleSource;
543
+ if (bodyFormat) {
544
+ ruleSource = bodyAfterFrontmatter(text);
545
+ }
546
+ else {
547
+ const fm = extractFrontmatter(text);
548
+ if (!fm.ok)
549
+ return { ok: false, errors: [`${relativePath}: ${fm.error}`] };
550
+ ruleSource = fm.frontmatter;
551
+ }
552
+ const parsed = parseYamlSubset(ruleSource);
553
+ if (!parsed.ok)
554
+ return { ok: false, errors: [`${relativePath}: ${parsed.error}`] };
555
+ const root = parsed.value;
556
+ if (root == null || typeof root !== 'object' || Array.isArray(root)) {
557
+ return {
558
+ ok: false,
559
+ errors: [`${relativePath}: ${bodyFormat ? 'body' : 'frontmatter'} must be a mapping`],
560
+ };
561
+ }
562
+ const errors = [];
563
+ const warnings = [];
564
+ const entries = emptyEntries();
565
+ for (const [key, value] of Object.entries(root)) {
566
+ if (!KNOWN_VERBS_SET.has(key)) {
567
+ // Forgiving by design: unknown keys (typos, future verbs, custom
568
+ // metadata an operator chose to colocate) must not break the access
569
+ // tree. Warn so operators see the typo in logs, then move on as if
570
+ // the key didn't exist. The verbs we DO understand still parse.
571
+ warnings.push(`${relativePath}: unknown access key '${key}' — ignored (known: ${[...KNOWN_VERBS].join(', ')})`);
572
+ continue;
573
+ }
574
+ const verb = key;
575
+ if (!Array.isArray(value)) {
576
+ errors.push(`${relativePath}: '${key}:' must be a list`);
577
+ continue;
578
+ }
579
+ const list = [];
580
+ for (const raw of value) {
581
+ const result = parseAccessEntry(raw);
582
+ if (!result.ok) {
583
+ errors.push(`${relativePath}: ${result.error}`);
584
+ continue;
585
+ }
586
+ list.push(result.entry);
587
+ }
588
+ entries[verb] = list;
589
+ }
590
+ if (errors.length)
591
+ return { ok: false, errors };
592
+ const slash = relativePath.lastIndexOf('/');
593
+ const dir = slash === -1 ? '' : relativePath.slice(0, slash);
594
+ return { ok: true, file: { path: relativePath, dir, entries }, warnings };
595
+ }
596
+ /**
597
+ * Parse the access verbs a node file declares in its own YAML frontmatter.
598
+ * Returns the per-verb entry lists, or null when the file has no frontmatter
599
+ * or declares no access verb at all.
600
+ *
601
+ * Forgiving by design — a node's frontmatter legitimately carries non-access
602
+ * keys (notably `nodeType:`), so unknown keys are ignored, and a malformed
603
+ * entry is dropped rather than failing the file. A typo in a node's `owner:`
604
+ * must never make the node unreadable; it just doesn't grant anything.
605
+ */
606
+ export function parseOwnAccessEntries(text) {
607
+ const fm = extractFrontmatter(text);
608
+ if (!fm.ok)
609
+ return null;
610
+ const parsed = parseYamlSubset(fm.frontmatter);
611
+ if (!parsed.ok)
612
+ return null;
613
+ const root = parsed.value;
614
+ if (root == null || typeof root !== 'object' || Array.isArray(root))
615
+ return null;
616
+ const entries = emptyEntries();
617
+ let sawVerb = false;
618
+ for (const [key, value] of Object.entries(root)) {
619
+ if (!KNOWN_VERBS_SET.has(key))
620
+ continue; // ignore nodeType, etc.
621
+ // Accept both the list form (`owner:\n - A\n - B`) and the convenience
622
+ // single-value scalar form (`owner: Test <test@test.com>`) — the latter
623
+ // is the natural way to name one owner in a node's own frontmatter.
624
+ // Anything else (a mapping, null) is malformed → skip forgivingly.
625
+ let raws;
626
+ if (Array.isArray(value))
627
+ raws = value;
628
+ else if (typeof value === 'string' && value.trim())
629
+ raws = [value];
630
+ else
631
+ continue;
632
+ const verb = key;
633
+ const list = [];
634
+ for (const raw of raws) {
635
+ const result = parseAccessEntry(raw);
636
+ if (result.ok)
637
+ list.push(result.entry);
638
+ }
639
+ entries[verb] = list;
640
+ sawVerb = true;
641
+ }
642
+ return sawVerb ? entries : null;
643
+ }
644
+ //# sourceMappingURL=access-grammar.js.map