@bevel-software/platform-core-backend 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (305) hide show
  1. package/dist/core/core-ports.d.ts +15 -0
  2. package/dist/core/core-ports.d.ts.map +1 -1
  3. package/dist/core/core-ports.js.map +1 -1
  4. package/dist/core/create-core-server.d.ts.map +1 -1
  5. package/dist/core/create-core-server.js +40 -6
  6. package/dist/core/create-core-server.js.map +1 -1
  7. package/dist/core/create-core-services.d.ts +17 -1
  8. package/dist/core/create-core-services.d.ts.map +1 -1
  9. package/dist/core/create-core-services.js +153 -29
  10. package/dist/core/create-core-services.js.map +1 -1
  11. package/dist/core-config.d.ts +70 -21
  12. package/dist/core-config.d.ts.map +1 -1
  13. package/dist/core-config.js +134 -58
  14. package/dist/core-config.js.map +1 -1
  15. package/dist/modules/access/access-control.interface.d.ts +6 -0
  16. package/dist/modules/access/access-control.interface.d.ts.map +1 -1
  17. package/dist/modules/access/access-control.service.d.ts +74 -5
  18. package/dist/modules/access/access-control.service.d.ts.map +1 -1
  19. package/dist/modules/access/access-control.service.js +116 -15
  20. package/dist/modules/access/access-control.service.js.map +1 -1
  21. package/dist/modules/access/access-declarations.d.ts +93 -0
  22. package/dist/modules/access/access-declarations.d.ts.map +1 -0
  23. package/dist/modules/access/access-declarations.js +168 -0
  24. package/dist/modules/access/access-declarations.js.map +1 -0
  25. package/dist/modules/access/access-mutation.service.js +4 -4
  26. package/dist/modules/access/access-mutation.service.js.map +1 -1
  27. package/dist/modules/access/access-splice.d.ts +16 -1
  28. package/dist/modules/access/access-splice.d.ts.map +1 -1
  29. package/dist/modules/access/access-splice.js +40 -21
  30. package/dist/modules/access/access-splice.js.map +1 -1
  31. package/dist/modules/access/access.routes.d.ts.map +1 -1
  32. package/dist/modules/access/access.routes.js +51 -0
  33. package/dist/modules/access/access.routes.js.map +1 -1
  34. package/dist/modules/access/creator-access.d.ts +8 -0
  35. package/dist/modules/access/creator-access.d.ts.map +1 -1
  36. package/dist/modules/access/creator-access.js +21 -2
  37. package/dist/modules/access/creator-access.js.map +1 -1
  38. package/dist/modules/admin/admin-access.service.d.ts +1 -1
  39. package/dist/modules/admin/admin-access.service.d.ts.map +1 -1
  40. package/dist/modules/admin/admin-access.service.js +3 -2
  41. package/dist/modules/admin/admin-access.service.js.map +1 -1
  42. package/dist/modules/auth/auth.routes.d.ts.map +1 -1
  43. package/dist/modules/auth/auth.routes.js +50 -1
  44. package/dist/modules/auth/auth.routes.js.map +1 -1
  45. package/dist/modules/auth/auth.service.d.ts +39 -10
  46. package/dist/modules/auth/auth.service.d.ts.map +1 -1
  47. package/dist/modules/auth/auth.service.js +38 -16
  48. package/dist/modules/auth/auth.service.js.map +1 -1
  49. package/dist/modules/database/core-schema.d.ts +124 -0
  50. package/dist/modules/database/core-schema.d.ts.map +1 -1
  51. package/dist/modules/database/core-schema.js +45 -0
  52. package/dist/modules/database/core-schema.js.map +1 -1
  53. package/dist/modules/groups/group-provision.service.d.ts +130 -0
  54. package/dist/modules/groups/group-provision.service.d.ts.map +1 -0
  55. package/dist/modules/groups/group-provision.service.js +288 -0
  56. package/dist/modules/groups/group-provision.service.js.map +1 -0
  57. package/dist/modules/groups/groups.contract.d.ts +106 -0
  58. package/dist/modules/groups/groups.contract.d.ts.map +1 -0
  59. package/dist/modules/groups/groups.contract.js +36 -0
  60. package/dist/modules/groups/groups.contract.js.map +1 -0
  61. package/dist/modules/groups/groups.routes.d.ts +42 -0
  62. package/dist/modules/groups/groups.routes.d.ts.map +1 -0
  63. package/dist/modules/groups/groups.routes.js +379 -0
  64. package/dist/modules/groups/groups.routes.js.map +1 -0
  65. package/dist/modules/groups/groups.service.d.ts +60 -0
  66. package/dist/modules/groups/groups.service.d.ts.map +1 -0
  67. package/dist/modules/groups/groups.service.js +172 -0
  68. package/dist/modules/groups/groups.service.js.map +1 -0
  69. package/dist/modules/groups/index.d.ts +7 -0
  70. package/dist/modules/groups/index.d.ts.map +1 -0
  71. package/dist/modules/groups/index.js +6 -0
  72. package/dist/modules/groups/index.js.map +1 -0
  73. package/dist/modules/groups/join-proposals.d.ts +53 -0
  74. package/dist/modules/groups/join-proposals.d.ts.map +1 -0
  75. package/dist/modules/groups/join-proposals.js +67 -0
  76. package/dist/modules/groups/join-proposals.js.map +1 -0
  77. package/dist/modules/groups/join-requests.service.d.ts +81 -0
  78. package/dist/modules/groups/join-requests.service.d.ts.map +1 -0
  79. package/dist/modules/groups/join-requests.service.js +135 -0
  80. package/dist/modules/groups/join-requests.service.js.map +1 -0
  81. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts +20 -0
  82. package/dist/modules/secrets-vault/secrets-vault.routes.d.ts.map +1 -1
  83. package/dist/modules/secrets-vault/secrets-vault.routes.js +59 -11
  84. package/dist/modules/secrets-vault/secrets-vault.routes.js.map +1 -1
  85. package/dist/modules/settings/deployment-settings.service.d.ts +129 -0
  86. package/dist/modules/settings/deployment-settings.service.d.ts.map +1 -0
  87. package/dist/modules/settings/deployment-settings.service.js +368 -0
  88. package/dist/modules/settings/deployment-settings.service.js.map +1 -0
  89. package/dist/modules/settings/setup.routes.d.ts +49 -0
  90. package/dist/modules/settings/setup.routes.d.ts.map +1 -0
  91. package/dist/modules/settings/setup.routes.js +292 -0
  92. package/dist/modules/settings/setup.routes.js.map +1 -0
  93. package/dist/modules/skills/index.d.ts +2 -1
  94. package/dist/modules/skills/index.d.ts.map +1 -1
  95. package/dist/modules/skills/index.js +1 -0
  96. package/dist/modules/skills/index.js.map +1 -1
  97. package/dist/modules/skills/pending-skills.service.d.ts +40 -0
  98. package/dist/modules/skills/pending-skills.service.d.ts.map +1 -0
  99. package/dist/modules/skills/pending-skills.service.js +147 -0
  100. package/dist/modules/skills/pending-skills.service.js.map +1 -0
  101. package/dist/modules/skills/skills.contract.d.ts +32 -3
  102. package/dist/modules/skills/skills.contract.d.ts.map +1 -1
  103. package/dist/modules/skills/skills.contract.js +1 -1
  104. package/dist/modules/skills/skills.routes.d.ts +3 -2
  105. package/dist/modules/skills/skills.routes.d.ts.map +1 -1
  106. package/dist/modules/skills/skills.routes.js +18 -1
  107. package/dist/modules/skills/skills.routes.js.map +1 -1
  108. package/dist/modules/skills/skills.service.d.ts +22 -0
  109. package/dist/modules/skills/skills.service.d.ts.map +1 -1
  110. package/dist/modules/skills/skills.service.js +24 -12
  111. package/dist/modules/skills/skills.service.js.map +1 -1
  112. package/dist/modules/tool-manuals/index.d.ts +1 -1
  113. package/dist/modules/tool-manuals/index.d.ts.map +1 -1
  114. package/dist/modules/tool-manuals/index.js.map +1 -1
  115. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts +47 -2
  116. package/dist/modules/tool-manuals/tool-manuals.contract.d.ts.map +1 -1
  117. package/dist/modules/tool-manuals/tool-manuals.contract.js +1 -1
  118. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts +4 -0
  119. package/dist/modules/tool-manuals/tool-manuals.routes.d.ts.map +1 -1
  120. package/dist/modules/tool-manuals/tool-manuals.routes.js +23 -0
  121. package/dist/modules/tool-manuals/tool-manuals.routes.js.map +1 -1
  122. package/dist/modules/tool-manuals/tool-manuals.service.d.ts +3 -1
  123. package/dist/modules/tool-manuals/tool-manuals.service.d.ts.map +1 -1
  124. package/dist/modules/tool-manuals/tool-manuals.service.js +84 -17
  125. package/dist/modules/tool-manuals/tool-manuals.service.js.map +1 -1
  126. package/dist/modules/tool-manuals/tool-manuals.tools.d.ts.map +1 -1
  127. package/dist/modules/tool-manuals/tool-manuals.tools.js +6 -2
  128. package/dist/modules/tool-manuals/tool-manuals.tools.js.map +1 -1
  129. package/dist/modules/workflow/agent-tools/workflow.tools.d.ts.map +1 -1
  130. package/dist/modules/workflow/agent-tools/workflow.tools.js +4 -2
  131. package/dist/modules/workflow/agent-tools/workflow.tools.js.map +1 -1
  132. package/dist/modules/workflow/event-bus.d.ts +11 -0
  133. package/dist/modules/workflow/event-bus.d.ts.map +1 -1
  134. package/dist/modules/workflow/event-bus.js +22 -0
  135. package/dist/modules/workflow/event-bus.js.map +1 -1
  136. package/dist/modules/workflow/file-lock.service.d.ts +9 -0
  137. package/dist/modules/workflow/file-lock.service.d.ts.map +1 -1
  138. package/dist/modules/workflow/file-lock.service.js +16 -0
  139. package/dist/modules/workflow/file-lock.service.js.map +1 -1
  140. package/dist/modules/workflow/git/branch-name.d.ts +1 -1
  141. package/dist/modules/workflow/git/branch-name.d.ts.map +1 -1
  142. package/dist/modules/workflow/git/branch-name.js +1 -1
  143. package/dist/modules/workflow/git/branch-name.js.map +1 -1
  144. package/dist/modules/workflow/git/git.service.d.ts +31 -2
  145. package/dist/modules/workflow/git/git.service.d.ts.map +1 -1
  146. package/dist/modules/workflow/git/git.service.js +157 -54
  147. package/dist/modules/workflow/git/git.service.js.map +1 -1
  148. package/dist/modules/workflow/git/pull-request.service.d.ts +12 -6
  149. package/dist/modules/workflow/git/pull-request.service.d.ts.map +1 -1
  150. package/dist/modules/workflow/git/pull-request.service.js +19 -9
  151. package/dist/modules/workflow/git/pull-request.service.js.map +1 -1
  152. package/dist/modules/workflow/review-workflow/review-workflow.service.d.ts.map +1 -1
  153. package/dist/modules/workflow/review-workflow/review-workflow.service.js +12 -3
  154. package/dist/modules/workflow/review-workflow/review-workflow.service.js.map +1 -1
  155. package/dist/modules/workflow/workflow.routes.d.ts.map +1 -1
  156. package/dist/modules/workflow/workflow.routes.js +95 -14
  157. package/dist/modules/workflow/workflow.routes.js.map +1 -1
  158. package/dist/modules/workflow/workflow.service.d.ts +97 -5
  159. package/dist/modules/workflow/workflow.service.d.ts.map +1 -1
  160. package/dist/modules/workflow/workflow.service.js +393 -114
  161. package/dist/modules/workflow/workflow.service.js.map +1 -1
  162. package/dist/modules/workspace/bevel-ignore.d.ts +5 -0
  163. package/dist/modules/workspace/bevel-ignore.d.ts.map +1 -1
  164. package/dist/modules/workspace/bevel-ignore.js +5 -1
  165. package/dist/modules/workspace/bevel-ignore.js.map +1 -1
  166. package/dist/modules/workspace/kb-seed.interface.d.ts +1 -1
  167. package/dist/modules/workspace/kb-seed.service.d.ts +60 -5
  168. package/dist/modules/workspace/kb-seed.service.d.ts.map +1 -1
  169. package/dist/modules/workspace/kb-seed.service.js +228 -51
  170. package/dist/modules/workspace/kb-seed.service.js.map +1 -1
  171. package/dist/modules/workspace/workspace.routes.d.ts +2 -1
  172. package/dist/modules/workspace/workspace.routes.d.ts.map +1 -1
  173. package/dist/modules/workspace/workspace.routes.js +51 -18
  174. package/dist/modules/workspace/workspace.routes.js.map +1 -1
  175. package/dist/modules/workspace/workspace.service.d.ts +16 -4
  176. package/dist/modules/workspace/workspace.service.d.ts.map +1 -1
  177. package/dist/modules/workspace/workspace.service.js +36 -9
  178. package/dist/modules/workspace/workspace.service.js.map +1 -1
  179. package/dist/modules/workspace/workspace.tools.d.ts.map +1 -1
  180. package/dist/modules/workspace/workspace.tools.js +8 -3
  181. package/dist/modules/workspace/workspace.tools.js.map +1 -1
  182. package/dist/shared/kb-layout.test.js +2 -2
  183. package/dist/test-setup.d.ts +2 -0
  184. package/dist/test-setup.d.ts.map +1 -0
  185. package/dist/test-setup.js +18 -0
  186. package/dist/test-setup.js.map +1 -0
  187. package/kb-template/.bevelignore +6 -2
  188. package/kb-template/AGENTS.md +272 -0
  189. package/kb-template/KnowledgeBase/How to get started.md +65 -0
  190. package/kb-template/access.md +36 -36
  191. package/migrations/0002_user_onboarding_done.sql +1 -0
  192. package/migrations/0003_deployment_settings.sql +9 -0
  193. package/migrations/meta/0001_snapshot.json +1486 -0
  194. package/migrations/meta/0002_snapshot.json +1493 -0
  195. package/migrations/meta/0003_snapshot.json +1552 -0
  196. package/migrations/meta/_journal.json +15 -1
  197. package/package.json +3 -3
  198. package/src/__tests__/branch-model.test.ts +79 -0
  199. package/src/__tests__/core-config.admin.test.ts +114 -0
  200. package/src/__tests__/database-url.test.ts +70 -0
  201. package/src/core/core-ports.ts +15 -0
  202. package/src/core/create-core-server.ts +55 -6
  203. package/src/core/create-core-services.ts +201 -36
  204. package/src/core-config.ts +139 -59
  205. package/src/modules/access/__tests__/access-declarations.test.ts +195 -0
  206. package/src/modules/access/__tests__/access-md-format.test.ts +191 -0
  207. package/src/modules/access/__tests__/access-splice.test.ts +21 -0
  208. package/src/modules/access/__tests__/access.routes.overrides.test.ts +214 -0
  209. package/src/modules/access/access-control.interface.ts +11 -0
  210. package/src/modules/access/access-control.service.ts +113 -15
  211. package/src/modules/access/access-declarations.ts +251 -0
  212. package/src/modules/access/access-mutation.service.ts +4 -4
  213. package/src/modules/access/access-splice.ts +64 -21
  214. package/src/modules/access/access.routes.ts +61 -0
  215. package/src/modules/access/creator-access.ts +22 -2
  216. package/src/modules/admin/admin-access.service.ts +4 -3
  217. package/src/modules/auth/__tests__/auth.service.test.ts +57 -0
  218. package/src/modules/auth/__tests__/onboarding-done.route.test.ts +147 -0
  219. package/src/modules/auth/auth.routes.ts +50 -1
  220. package/src/modules/auth/auth.service.ts +55 -17
  221. package/src/modules/database/core-schema.ts +46 -0
  222. package/src/modules/groups/__tests__/group-index.service.test.ts +225 -0
  223. package/src/modules/groups/__tests__/group-provision.service.test.ts +247 -0
  224. package/src/modules/groups/__tests__/groups.routes.test.ts +493 -0
  225. package/src/modules/groups/__tests__/join-proposals.test.ts +101 -0
  226. package/src/modules/groups/__tests__/join-requests.service.test.ts +175 -0
  227. package/src/modules/groups/group-provision.service.ts +328 -0
  228. package/src/modules/groups/groups.contract.ts +107 -0
  229. package/src/modules/groups/groups.routes.ts +433 -0
  230. package/src/modules/groups/groups.service.ts +193 -0
  231. package/src/modules/groups/index.ts +17 -0
  232. package/src/modules/groups/join-proposals.ts +114 -0
  233. package/src/modules/groups/join-requests.service.ts +185 -0
  234. package/src/modules/secrets-vault/__tests__/oauth-return-to.route.test.ts +283 -0
  235. package/src/modules/secrets-vault/secrets-vault.routes.ts +68 -12
  236. package/src/modules/settings/__tests__/deployment-settings.service.test.ts +256 -0
  237. package/src/modules/settings/__tests__/setup.routes.test.ts +201 -0
  238. package/src/modules/settings/deployment-settings.service.ts +419 -0
  239. package/src/modules/settings/setup.routes.ts +317 -0
  240. package/src/modules/skills/__tests__/pending-skills.service.test.ts +240 -0
  241. package/src/modules/skills/__tests__/skills.service.test.ts +13 -13
  242. package/src/modules/skills/index.ts +3 -0
  243. package/src/modules/skills/pending-skills.service.ts +168 -0
  244. package/src/modules/skills/skills.contract.ts +34 -3
  245. package/src/modules/skills/skills.routes.ts +23 -2
  246. package/src/modules/skills/skills.service.ts +24 -16
  247. package/src/modules/tool-manuals/__tests__/tool-manuals.detail.route.test.ts +262 -0
  248. package/src/modules/tool-manuals/__tests__/tool-manuals.mcp-oauth.test.ts +2 -2
  249. package/src/modules/tool-manuals/__tests__/tool-manuals.service.test.ts +61 -23
  250. package/src/modules/tool-manuals/__tests__/tool-manuals.tools.test.ts +156 -156
  251. package/src/modules/tool-manuals/index.ts +2 -0
  252. package/src/modules/tool-manuals/tool-manuals.contract.ts +50 -2
  253. package/src/modules/tool-manuals/tool-manuals.routes.ts +21 -0
  254. package/src/modules/tool-manuals/tool-manuals.service.ts +86 -17
  255. package/src/modules/tool-manuals/tool-manuals.tools.ts +206 -202
  256. package/src/modules/workflow/__tests__/event-bus.test.ts +35 -0
  257. package/src/modules/workflow/__tests__/pending-commits.enqueue-if-absent.test.ts +82 -72
  258. package/src/modules/workflow/__tests__/workflow.service.facade.test.ts +295 -5
  259. package/src/modules/workflow/agent-tools/workflow.tools.ts +4 -2
  260. package/src/modules/workflow/event-bus.ts +25 -0
  261. package/src/modules/workflow/file-lock.service.ts +17 -0
  262. package/src/modules/workflow/git/__tests__/git.service.accessGating.test.ts +9 -49
  263. package/src/modules/workflow/git/__tests__/git.service.changedFilesForPr.test.ts +101 -0
  264. package/src/modules/workflow/git/__tests__/git.service.commitFile.test.ts +219 -211
  265. package/src/modules/workflow/git/__tests__/git.service.fileContentsAtCommit.test.ts +132 -0
  266. package/src/modules/workflow/git/__tests__/git.service.logForFile.test.ts +4 -2
  267. package/src/modules/workflow/git/__tests__/git.service.postMergePull.test.ts +275 -272
  268. package/src/modules/workflow/git/__tests__/git.service.pull.test.ts +311 -306
  269. package/src/modules/workflow/git/__tests__/pull-request.service.viewer-can-cancel.test.ts +44 -0
  270. package/src/modules/workflow/git/branch-name.ts +5 -1
  271. package/src/modules/workflow/git/git.service.ts +172 -64
  272. package/src/modules/workflow/git/pull-request.service.ts +22 -11
  273. package/src/modules/workflow/review-workflow/__tests__/cancel-pr.test.ts +4 -1
  274. package/src/modules/workflow/review-workflow/review-workflow.service.ts +12 -3
  275. package/src/modules/workflow/workflow.routes.ts +91 -13
  276. package/src/modules/workflow/workflow.service.ts +365 -19
  277. package/src/modules/workspace/__tests__/kb-seed.service.test.ts +255 -24
  278. package/src/modules/workspace/__tests__/workspace.routes.create-grant.test.ts +236 -233
  279. package/src/modules/workspace/__tests__/workspace.routes.delete.test.ts +179 -176
  280. package/src/modules/workspace/__tests__/workspace.routes.download.test.ts +320 -303
  281. package/src/modules/workspace/__tests__/workspace.routes.read-gate.test.ts +337 -276
  282. package/src/modules/workspace/__tests__/workspace.tools.test.ts +14 -6
  283. package/src/modules/workspace/bevel-ignore.ts +66 -62
  284. package/src/modules/workspace/kb-seed.interface.ts +1 -1
  285. package/src/modules/workspace/kb-seed.service.ts +247 -53
  286. package/src/modules/workspace/workspace.routes.ts +54 -18
  287. package/src/modules/workspace/workspace.service.ts +45 -7
  288. package/src/modules/workspace/workspace.tools.ts +922 -917
  289. package/src/shared/__tests__/frontmatter-id.test.ts +53 -0
  290. package/src/shared/__tests__/join-request.test.ts +96 -0
  291. package/src/shared/__tests__/kb-layout.group.test.ts +45 -0
  292. package/src/shared/kb-layout.test.ts +2 -2
  293. package/src/test-setup.ts +18 -0
  294. package/kb-template/Agents/README.md +0 -38
  295. package/kb-template/CLAUDE.md +0 -419
  296. package/kb-template/Data/.gitkeep +0 -0
  297. package/kb-template/Data/README.md +0 -39
  298. package/kb-template/KnowledgeBase/README.md +0 -37
  299. package/kb-template/Pipelines/.gitkeep +0 -0
  300. package/kb-template/Pipelines/README.md +0 -38
  301. package/kb-template/Skills/.gitkeep +0 -0
  302. package/kb-template/Skills/README.md +0 -32
  303. package/kb-template/Tools/.gitkeep +0 -0
  304. package/kb-template/Tools/README.md +0 -42
  305. /package/kb-template/{Agents → Groups}/.gitkeep +0 -0
@@ -14,6 +14,7 @@ describe('computeViewerCanCancel', () => {
14
14
  authorId: AUTHOR_HASH,
15
15
  viewerEmail: undefined,
16
16
  viewerCanBypassMerge: false,
17
+ viewerWritesAllFiles: false,
17
18
  }),
18
19
  ).toBe(false);
19
20
  });
@@ -25,6 +26,7 @@ describe('computeViewerCanCancel', () => {
25
26
  authorId: AUTHOR_HASH,
26
27
  viewerEmail: EMAIL,
27
28
  viewerCanBypassMerge: false,
29
+ viewerWritesAllFiles: false,
28
30
  }),
29
31
  ).toBe(true);
30
32
  });
@@ -36,6 +38,7 @@ describe('computeViewerCanCancel', () => {
36
38
  authorId: OTHER_AUTHOR_HASH,
37
39
  viewerEmail: EMAIL,
38
40
  viewerCanBypassMerge: true,
41
+ viewerWritesAllFiles: false,
39
42
  }),
40
43
  ).toBe(true);
41
44
  });
@@ -47,6 +50,7 @@ describe('computeViewerCanCancel', () => {
47
50
  authorId: AUTHOR_HASH,
48
51
  viewerEmail: EMAIL,
49
52
  viewerCanBypassMerge: true,
53
+ viewerWritesAllFiles: false,
50
54
  }),
51
55
  ).toBe(true);
52
56
  });
@@ -58,6 +62,7 @@ describe('computeViewerCanCancel', () => {
58
62
  authorId: AUTHOR_HASH,
59
63
  viewerEmail: EMAIL,
60
64
  viewerCanBypassMerge: false,
65
+ viewerWritesAllFiles: false,
61
66
  }),
62
67
  ).toBe(false);
63
68
  });
@@ -69,6 +74,7 @@ describe('computeViewerCanCancel', () => {
69
74
  authorId: OTHER_AUTHOR_HASH,
70
75
  viewerEmail: EMAIL,
71
76
  viewerCanBypassMerge: true,
77
+ viewerWritesAllFiles: false,
72
78
  }),
73
79
  ).toBe(false);
74
80
  });
@@ -80,6 +86,7 @@ describe('computeViewerCanCancel', () => {
80
86
  authorId: undefined,
81
87
  viewerEmail: EMAIL,
82
88
  viewerCanBypassMerge: false,
89
+ viewerWritesAllFiles: false,
83
90
  }),
84
91
  ).toBe(false);
85
92
  });
@@ -95,7 +102,44 @@ describe('computeViewerCanCancel', () => {
95
102
  authorId: AUTHOR_HASH,
96
103
  viewerEmail: ' Juan@Bevel.Software ',
97
104
  viewerCanBypassMerge: false,
105
+ viewerWritesAllFiles: false,
98
106
  }),
99
107
  ).toBe(true);
100
108
  });
109
+
110
+ it('returns true for a non-author, non-admin who writes EVERY changed file (the reject route’s third grant)', () => {
111
+ expect(
112
+ computeViewerCanCancel({
113
+ state: 'open',
114
+ authorId: OTHER_AUTHOR_HASH,
115
+ viewerEmail: EMAIL,
116
+ viewerCanBypassMerge: false,
117
+ viewerWritesAllFiles: true,
118
+ }),
119
+ ).toBe(true);
120
+ });
121
+
122
+ it('fails closed for an anonymous viewer even with the owner grant', () => {
123
+ expect(
124
+ computeViewerCanCancel({
125
+ state: 'open',
126
+ authorId: OTHER_AUTHOR_HASH,
127
+ viewerEmail: undefined,
128
+ viewerCanBypassMerge: false,
129
+ viewerWritesAllFiles: true,
130
+ }),
131
+ ).toBe(false);
132
+ });
133
+
134
+ it('terminal states refuse the owner grant too', () => {
135
+ expect(
136
+ computeViewerCanCancel({
137
+ state: 'merged',
138
+ authorId: OTHER_AUTHOR_HASH,
139
+ viewerEmail: EMAIL,
140
+ viewerCanBypassMerge: false,
141
+ viewerWritesAllFiles: true,
142
+ }),
143
+ ).toBe(false);
144
+ });
101
145
  });
@@ -7,7 +7,11 @@ export { PROTECTED_BRANCHES, isProtectedBranch } from '@bevel-software/platform-
7
7
  // picker's `canDelete` rule and the backend authorisation check on
8
8
  // `deleteBranch` must agree on every edge case (uppercase email, dotted
9
9
  // localpart, etc.).
10
- export { branchAuthorLocalpart, isBranchAuthoredBy } from '@bevel-software/platform-shared';
10
+ export {
11
+ branchAuthorLocalpart,
12
+ isBranchAuthoredBy,
13
+ isOwnSuggestionsBranch,
14
+ } from '@bevel-software/platform-shared';
11
15
 
12
16
  const VALID_BRANCH_REGEX = /^[A-Za-z0-9][A-Za-z0-9/_\-.]*$/;
13
17
 
@@ -22,6 +22,7 @@ import {
22
22
  assertValidBranchName,
23
23
  assertValidRelativePath,
24
24
  isBranchAuthoredBy,
25
+ isOwnSuggestionsBranch,
25
26
  isProtectedBranch,
26
27
  PROTECTED_BRANCHES,
27
28
  } from './branch-name.js';
@@ -607,7 +608,7 @@ export class GitService implements IGitService {
607
608
  workspaceId: string,
608
609
  name: string,
609
610
  user: AuthUser,
610
- opts: { onlyIfNoRemote?: boolean } = {},
611
+ opts: { onlyIfNoRemote?: boolean; systemCleanup?: boolean } = {},
611
612
  ): Promise<void> {
612
613
  assertValidBranchName(name);
613
614
  if (isProtectedBranch(name)) {
@@ -622,7 +623,12 @@ export class GitService implements IGitService {
622
623
  );
623
624
  }
624
625
 
625
- if (opts.onlyIfNoRemote) {
626
+ if (opts.systemCleanup) {
627
+ // Internal post-merge retirement (never reachable from a route): the
628
+ // merge that emptied this branch was itself authorized, so no
629
+ // authorship check — the protected-branch and checked-out guards
630
+ // above still hold.
631
+ } else if (opts.onlyIfNoRemote) {
626
632
  // Legacy orphan-cleanup path (PR merged + remote head pruned). Skip
627
633
  // the authorship check — callers prune any orphan they encounter,
628
634
  // not just their own — but keep the "refuse if origin still has the
@@ -642,7 +648,13 @@ export class GitService implements IGitService {
642
648
  // that predicate is the existing source of truth for `Admin`-role
643
649
  // membership inside the access model — no separate `isAdmin()` plumbing
644
650
  // needed.
645
- const isAuthor = isBranchAuthoredBy(name, user.email);
651
+ // Two authorship conventions: `<localpart>/…` UI drafts, and the
652
+ // `suggestions/<who>-<id8>/…` namespace the propose flow files its
653
+ // branches under — a user resetting THEIR OWN suggestion bundle is
654
+ // deleting their own branch.
655
+ const isAuthor =
656
+ isBranchAuthoredBy(name, user.email) ||
657
+ isOwnSuggestionsBranch(name, { email: user.email, id: user.id });
646
658
  const isAdmin = this.accessControl
647
659
  ? await this.accessControl.canWrite(workspaceId, user.email, 'roles.yaml')
648
660
  : false;
@@ -1017,7 +1029,11 @@ export class GitService implements IGitService {
1017
1029
  });
1018
1030
  }
1019
1031
 
1020
- async push(workspaceId: string, user: AuthUser): Promise<void> {
1032
+ async push(
1033
+ workspaceId: string,
1034
+ user: AuthUser,
1035
+ opts?: { systemAuthorized?: boolean },
1036
+ ): Promise<void> {
1021
1037
  return this.mutex.run(workspaceId, async () => {
1022
1038
  const cwd = await this.repoDir(workspaceId);
1023
1039
  const branch = await this.currentBranch(cwd);
@@ -1030,7 +1046,14 @@ export class GitService implements IGitService {
1030
1046
  // PR. Gate against `origin/<branch>` — the published state — for
1031
1047
  // protected pushes; that's defence-in-depth against local rebases or
1032
1048
  // `commit-tree` shenanigans that might bypass the commit-time gate.
1033
- if (isProtectedBranch(branch)) {
1049
+ // `systemAuthorized` skips the gate for flows whose ENDPOINT is the
1050
+ // authorization — group provisioning commits an access.md into a
1051
+ // folder that does not exist at origin yet, which this gate can only
1052
+ // ever read as "write: Admin". The provisioning service has already
1053
+ // decided the write is legitimate (unused name, exclusive create);
1054
+ // gating it here again just refuses every non-admin the product
1055
+ // promised a group to.
1056
+ if (isProtectedBranch(branch) && !opts?.systemAuthorized) {
1034
1057
  const touched = await this.unpushedTouchedPaths(cwd);
1035
1058
  await this.assertCanWriteAtRef(
1036
1059
  workspaceId,
@@ -1403,12 +1426,23 @@ export class GitService implements IGitService {
1403
1426
  // A failed rebase leaves the repo in a "REBASE_HEAD" / detached-apply state,
1404
1427
  // which makes every subsequent git command behave unexpectedly. Return the
1405
1428
  // working tree to a clean state before surfacing the original error.
1406
- await this.git(cwd, ['rebase', '--abort']).catch(() => undefined);
1407
- if (conflictedPaths.length > 0) {
1429
+ let abortSucceeded = true;
1430
+ await this.git(cwd, ['rebase', '--abort']).catch(() => {
1431
+ abortSucceeded = false;
1432
+ });
1433
+ if (abortSucceeded && conflictedPaths.length > 0) {
1408
1434
  // Typed so the workflow layer can queue background recovery — this
1409
1435
  // divergence never resolves on its own (every retry pull hits the
1410
1436
  // same conflict) and the unpushed local commits are someone's saved
1411
1437
  // content stuck violating save=share.
1438
+ //
1439
+ // Gated on the abort succeeding: a failed abort means either no
1440
+ // rebase was ever active (the unmerged paths pre-date this pull —
1441
+ // some earlier operation left the index broken) or the clone is in
1442
+ // a state git itself can't unwind. Queueing recovery there would
1443
+ // point the worker's commitFile at a repo mid-conflict, risking a
1444
+ // commit full of conflict markers — surface the raw error instead
1445
+ // and leave diagnosis to a human.
1412
1446
  throw new PullRebaseConflictError(
1413
1447
  branch,
1414
1448
  conflictedPaths,
@@ -1597,60 +1631,6 @@ export class GitService implements IGitService {
1597
1631
  });
1598
1632
  }
1599
1633
 
1600
- async revertCommit(
1601
- workspaceId: string,
1602
- user: AuthUser,
1603
- sha: string,
1604
- ): Promise<CommitAttribution> {
1605
- if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
1606
- throw new WorkflowValidationError('invalid commit sha');
1607
- }
1608
- assertValidAuthor(user);
1609
- return this.mutex.run(workspaceId, async () => {
1610
- const cwd = await this.repoDir(workspaceId);
1611
- const branch = await this.currentBranch(cwd);
1612
-
1613
- // Revert creates a new commit reversing `sha`. Same model as commit:
1614
- // gate only on protected branches; reverts on feature/draft branches
1615
- // are free (they produce a commit that would still need to merge via
1616
- // PR to affect canonical state).
1617
- if (isProtectedBranch(branch)) {
1618
- const { stdout: diffOut } = await this.git(cwd, [
1619
- 'diff-tree', '--no-commit-id', '--name-only', '-r', sha,
1620
- ]);
1621
- const touched = diffOut.split('\n').map((s) => s.trim()).filter(Boolean);
1622
- await this.assertCanWriteAtRef(workspaceId, 'HEAD', user.email, touched);
1623
- }
1624
-
1625
- await this.git(cwd, [
1626
- 'revert',
1627
- '--no-edit',
1628
- `--author=${user.name} <${user.email}>`,
1629
- sha,
1630
- ]);
1631
-
1632
- const { stdout } = await this.git(cwd, [
1633
- 'log',
1634
- '-1',
1635
- '--pretty=format:%H%x00%an%x00%ae%x00%s%x00%aI',
1636
- ]);
1637
- const [headSha, authorName, authorEmail, subj, committedAt] = stdout.split('\x00');
1638
-
1639
- // A revert may have touched roles.yaml or access.md — drop cached
1640
- // access state so the next gate read reflects the rolled-back tree.
1641
- // Mirrors the commit / pull paths.
1642
- this.accessControl?.invalidate(workspaceId);
1643
-
1644
- return {
1645
- sha: headSha?.trim() ?? '',
1646
- authorName: authorName ?? '',
1647
- authorEmail: authorEmail ?? '',
1648
- subject: subj ?? '',
1649
- committedAt: committedAt?.trim() ?? new Date().toISOString(),
1650
- };
1651
- });
1652
- }
1653
-
1654
1634
  async logForFile(
1655
1635
  workspaceId: string,
1656
1636
  relativePath: string,
@@ -1715,6 +1695,44 @@ export class GitService implements IGitService {
1715
1695
  });
1716
1696
  }
1717
1697
 
1698
+ /**
1699
+ * Full file contents on both sides of one commit: `baseline` is the file at
1700
+ * `<sha>^` (the parent), `current` at `<sha>`. Null means the file is absent
1701
+ * at that ref — added files have `baseline: null`, deleted files
1702
+ * `current: null`. A root commit (no parent) also yields `baseline: null`.
1703
+ * Feeds the rendered-markdown history view, which needs before/after text
1704
+ * rather than the patch `diffFileAtCommit` returns.
1705
+ */
1706
+ async fileContentsAtCommit(
1707
+ workspaceId: string,
1708
+ relativePath: string,
1709
+ sha: string,
1710
+ ): Promise<{ baseline: string | null; current: string | null }> {
1711
+ assertValidRelativePath(relativePath);
1712
+ if (!/^[a-f0-9]{7,40}$/i.test(sha)) {
1713
+ throw new WorkflowValidationError('invalid commit sha');
1714
+ }
1715
+ const repoRelativePath = this.stripRepoPrefix(relativePath);
1716
+ return this.mutex.run(workspaceId, async () => {
1717
+ const current = await this.readFileAtRef(workspaceId, sha, repoRelativePath);
1718
+ let baseline: string | null;
1719
+ try {
1720
+ baseline = await this.readFileAtRef(workspaceId, `${sha}^`, repoRelativePath);
1721
+ } catch (err) {
1722
+ // `readFileAtRef` maps "path absent at ref" to null but propagates an
1723
+ // unresolvable ref. `<sha>^` on a root commit is exactly that (git
1724
+ // reports it as "invalid object name '<sha>^'", or "unknown/bad
1725
+ // revision" in other codepaths), and it legitimately means "no parent
1726
+ // side" — fold it into null. Anything else stays fatal.
1727
+ const stderr =
1728
+ (err as { stderr?: string }).stderr ?? (err instanceof Error ? err.message : String(err));
1729
+ if (/invalid object name|unknown revision|bad revision/i.test(stderr)) baseline = null;
1730
+ else throw err;
1731
+ }
1732
+ return { baseline, current };
1733
+ });
1734
+ }
1735
+
1718
1736
  async diffFileBetweenBranches(
1719
1737
  workspaceId: string,
1720
1738
  relativePath: string,
@@ -1848,8 +1866,20 @@ export class GitService implements IGitService {
1848
1866
  this.git(cwd, ['diff', '-M', '-z', '--name-status', range]),
1849
1867
  this.git(cwd, ['diff', '-M', '-z', '--numstat', range]),
1850
1868
  ]);
1851
- const statuses = parseNameStatusZ(nameStatusOut);
1852
- const counts = parseNumstatZ(numstatOut);
1869
+ let statuses = parseNameStatusZ(nameStatusOut);
1870
+ let counts = parseNumstatZ(numstatOut);
1871
+ // roles.yaml can NEVER change through a merge — `preserveBaseRolesYaml`
1872
+ // restores the base copy onto the source branch before every merge — so
1873
+ // listing it as "changed" claims something the merge will not do, and
1874
+ // (worse) makes its approval a requirement for a change that cannot
1875
+ // land. Filter it from the review surface entirely; the neutralisation
1876
+ // reads the raw refs itself and is unaffected. Both lists are filtered
1877
+ // IN STEP so the index-zip below stays aligned.
1878
+ if (statuses.some((s) => s.path === 'roles.yaml')) {
1879
+ const keep = statuses.map((s) => s.path !== 'roles.yaml');
1880
+ statuses = statuses.filter((_, i) => keep[i]);
1881
+ if (counts.length === keep.length) counts = counts.filter((_, i) => keep[i]);
1882
+ }
1853
1883
 
1854
1884
  // `--name-status` and `--numstat` enumerate the same files in the same
1855
1885
  // order (same `-M` over the same range), so we zip by index. If the two
@@ -1913,7 +1943,85 @@ export class GitService implements IGitService {
1913
1943
  const { stdout } = await this.git(cwd, [
1914
1944
  'diff', '-M', '--name-only', `${baseRef}...${headRef}`,
1915
1945
  ]);
1916
- return stdout.split('\n').map((s) => s.trim()).filter(Boolean);
1946
+ return (
1947
+ stdout
1948
+ .split('\n')
1949
+ .map((s) => s.trim())
1950
+ .filter(Boolean)
1951
+ // Same rule as `changedFilesForPr`: a roles.yaml change never
1952
+ // survives a merge, so it is not a touched path for routing or
1953
+ // summaries either.
1954
+ .filter((p) => p !== 'roles.yaml')
1955
+ );
1956
+ });
1957
+ }
1958
+
1959
+ /**
1960
+ * The merge-base commit of a change request's two branches (their origin
1961
+ * refs), or null when they share no history. This is the "before" the CR's
1962
+ * three-dot diff is read against — the ref a per-file revert restores from,
1963
+ * so the reverted file drops OUT of that same diff.
1964
+ */
1965
+ async mergeBaseForPr(
1966
+ workspaceId: string,
1967
+ baseBranch: string,
1968
+ headBranch: string,
1969
+ ): Promise<string | null> {
1970
+ assertValidBranchName(baseBranch);
1971
+ assertValidBranchName(headBranch);
1972
+ const cwd = await this.repoDir(workspaceId);
1973
+ await this.fetchPrRefs(cwd, baseBranch, headBranch);
1974
+ return this.mutex.run(workspaceId, async () => {
1975
+ const baseRef = await this.resolveBranchRef(cwd, baseBranch);
1976
+ const headRef = await this.resolveBranchRef(cwd, headBranch);
1977
+ try {
1978
+ const { stdout } = await this.git(cwd, ['merge-base', baseRef, headRef]);
1979
+ return stdout.trim() || null;
1980
+ } catch (err) {
1981
+ // Exit 1 is git's specific "no common ancestor" answer; anything else
1982
+ // is an infra failure that must not masquerade as it.
1983
+ if ((err as { exitCode?: number }).exitCode === 1) return null;
1984
+ throw err;
1985
+ }
1986
+ });
1987
+ }
1988
+
1989
+ /**
1990
+ * Restore one path in the working tree (and index) to its content at `ref`;
1991
+ * a path ABSENT at `ref` is deleted — the revert of an added file is its
1992
+ * removal. Byte-exact via git itself (`checkout <ref> -- <path>`), never a
1993
+ * read-as-string round-trip that would mangle binary files. The caller owns
1994
+ * committing the result (see `commitFile`).
1995
+ */
1996
+ async restorePathFromRef(
1997
+ workspaceId: string,
1998
+ ref: string,
1999
+ repoRelativePath: string,
2000
+ ): Promise<void> {
2001
+ assertValidRelativePath(repoRelativePath);
2002
+ return this.mutex.run(workspaceId, async () => {
2003
+ const cwd = await this.repoDir(workspaceId);
2004
+ let existsAtRef = true;
2005
+ try {
2006
+ await this.git(cwd, ['cat-file', '-e', `${ref}:${repoRelativePath}`]);
2007
+ } catch {
2008
+ existsAtRef = false;
2009
+ }
2010
+ if (existsAtRef) {
2011
+ await this.git(cwd, ['checkout', ref, '--', repoRelativePath]);
2012
+ } else {
2013
+ // First re-align the INDEX with HEAD for this path. A previous failed
2014
+ // attempt (the git-rm era of this method) can have left a staged
2015
+ // deletion — index entry gone — and `git add` can only stage a
2016
+ // deletion for a path the index still tracks. No-op on a clean tree.
2017
+ await this.git(cwd, ['reset', '-q', 'HEAD', '--', repoRelativePath]);
2018
+ // Then delete from the WORKING TREE only, never `git rm`: the
2019
+ // follow-up `commitFile` stages via `git add -- <path>`, and dropping
2020
+ // the index entry too made that add match nothing — the whole revert
2021
+ // failed with "pathspec did not match any files". A tracked file
2022
+ // missing from disk is exactly what `git add` records as deleted.
2023
+ await fs.rm(path.join(cwd, repoRelativePath), { force: true });
2024
+ }
1917
2025
  });
1918
2026
  }
1919
2027
 
@@ -183,10 +183,13 @@ export class PullRequestService implements IPullRequestService {
183
183
  return summaries;
184
184
  }
185
185
 
186
- async listPrsAuthoredBy(loginOrEmail: string): Promise<PullRequestSummary[]> {
186
+ async listPrsAuthoredBy(
187
+ loginOrEmail: string,
188
+ opts: { fresh?: boolean } = {},
189
+ ): Promise<PullRequestSummary[]> {
187
190
  const needle = loginOrEmail.trim().toLowerCase();
188
191
  if (!needle) return [];
189
- const prs = await this.listOpenPrs();
192
+ const prs = await this.listOpenPrs(opts);
190
193
  const needleIsEmail = needle.includes('@');
191
194
  const needleHash = needleIsEmail ? hashEmail(needle) : null;
192
195
  return prs.filter((p) => {
@@ -378,6 +381,11 @@ export class PullRequestService implements IPullRequestService {
378
381
  authorId: summary.authorId,
379
382
  viewerEmail: opts.viewerEmail,
380
383
  viewerCanBypassMerge,
384
+ // The reject route's third grant (see `rejectChangeRequest`): write on
385
+ // every changed file at origin/<base>. Derived from the per-file
386
+ // `viewerCanApprove` flags — the SAME `canWriteBatchAtRef` predicate the
387
+ // route enforces — so the hint cannot drift from the enforcement.
388
+ viewerWritesAllFiles: approvals.length > 0 && approvals.every((a) => a.viewerCanApprove),
381
389
  });
382
390
 
383
391
  const detail: PullRequestDetail = {
@@ -423,23 +431,26 @@ export class PullRequestService implements IPullRequestService {
423
431
  }
424
432
 
425
433
  /**
426
- * Pure predicate for the cancel-button's enabled state. The viewer can cancel
427
- * iff the PR is open AND they're either the author (hash-match against the
428
- * stored author) or an admin (`viewerCanBypassMerge` is the proxy — same
429
- * `canWriteAtRef('roles.yaml')` predicate). Fail-closed defaults: missing
430
- * email → false.
434
+ * Pure predicate for the `viewerCanCancel` hint. The viewer can cancel iff
435
+ * the PR is open AND they're the author (hash-match against the stored
436
+ * author), an admin (`viewerCanBypassMerge` is the proxy — same
437
+ * `canWriteAtRef('roles.yaml')` predicate), or they hold write on EVERY
438
+ * changed file (`viewerWritesAllFiles`) — the reject route's full
439
+ * authorization set, mirrored exactly so the hint never claims less (or
440
+ * more) than the server enforces. Fail-closed: no viewer email → false,
441
+ * whatever the grants say.
431
442
  */
432
443
  export function computeViewerCanCancel(input: {
433
444
  state: PullRequestState;
434
445
  authorId: string | undefined;
435
446
  viewerEmail: string | undefined;
436
447
  viewerCanBypassMerge: boolean;
448
+ viewerWritesAllFiles: boolean;
437
449
  }): boolean {
438
450
  if (input.state !== 'open') return false;
439
- const viewerIsAuthor = !!(
440
- input.authorId && input.viewerEmail && input.authorId === hashEmail(input.viewerEmail)
441
- );
442
- return viewerIsAuthor || input.viewerCanBypassMerge;
451
+ if (!input.viewerEmail) return false;
452
+ const viewerIsAuthor = !!(input.authorId && input.authorId === hashEmail(input.viewerEmail));
453
+ return viewerIsAuthor || input.viewerCanBypassMerge || input.viewerWritesAllFiles;
443
454
  }
444
455
 
445
456
  export const __testing = {
@@ -100,7 +100,10 @@ describe('ReviewWorkflowService.cancelPr', () => {
100
100
  ).rejects.toMatchObject({
101
101
  name: 'CancelAuthError',
102
102
  status: 403,
103
- message: expect.stringMatching(/only the author or an admin/i),
103
+ // Names the FULL authorization set — the facade's owner grant
104
+ // (write on every changed file) included, so the refusal never
105
+ // undersells who could actually do this.
106
+ message: expect.stringMatching(/author, an admin, or having edit access to every file/i),
104
107
  });
105
108
  expect(updateReturning).not.toHaveBeenCalled();
106
109
  });
@@ -106,7 +106,8 @@ class CancelStateError extends WorkflowDomainError {
106
106
  class CancelAuthError extends WorkflowDomainError {
107
107
  constructor() {
108
108
  super(
109
- "You can't cancel this change request — only the author or an admin can.",
109
+ "You can't cancel this change request — that takes being its author, an admin, " +
110
+ 'or having edit access to every file it changes.',
110
111
  403,
111
112
  );
112
113
  this.name = 'CancelAuthError';
@@ -500,7 +501,11 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
500
501
  })
501
502
  .onConflictDoNothing();
502
503
 
503
- return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId);
504
+ // WITH the caller as viewer: these approvals go straight back to the UI
505
+ // that just clicked, and omitting the viewer computed every
506
+ // `viewerCanApprove` as false — one approval made the apply button and
507
+ // every remaining approve control vanish until a fresh detail fetch.
508
+ return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId, user.email);
504
509
  }
505
510
 
506
511
  evaluateMergeGate(input: MergeGateInput): MergeGateResult {
@@ -801,6 +806,10 @@ export class ReviewWorkflowService implements IReviewWorkflowService {
801
806
  ),
802
807
  );
803
808
 
804
- return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId);
809
+ // WITH the caller as viewer: these approvals go straight back to the UI
810
+ // that just clicked, and omitting the viewer computed every
811
+ // `viewerCanApprove` as false — one approval made the apply button and
812
+ // every remaining approve control vanish until a fresh detail fetch.
813
+ return this.getApprovalStates(prNumber, files, headSha, baseBranch, prAuthorIdHash, workspaceId, user.email);
805
814
  }
806
815
  }