@tequila99/githuman 0.3.0 → 0.4.1

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 (178) hide show
  1. package/README.md +8 -1
  2. package/dist/cli/config.js +4 -2
  3. package/dist/cli/server-runtime.js +1 -0
  4. package/dist/server/app.js +10 -0
  5. package/dist/server/config/openapi.js +29 -0
  6. package/dist/server/repositories/review.repo.js +31 -5
  7. package/dist/server/routes/agent.js +145 -18
  8. package/dist/server/routes/app-info.js +8 -1
  9. package/dist/server/routes/comments.js +73 -27
  10. package/dist/server/routes/diff.js +76 -15
  11. package/dist/server/routes/events.js +14 -1
  12. package/dist/server/routes/export.js +22 -5
  13. package/dist/server/routes/git.js +52 -21
  14. package/dist/server/routes/health.js +8 -1
  15. package/dist/server/routes/openapi.js +4 -0
  16. package/dist/server/routes/reviews.js +59 -33
  17. package/dist/server/services/agent/agent-session.js +5 -3
  18. package/dist/server/services/diff.service.js +119 -1
  19. package/dist/server/services/git.service.js +104 -3
  20. package/dist/shared/agents/schemas.js +120 -31
  21. package/dist/shared/app/schemas.js +7 -0
  22. package/dist/shared/comments/schemas.js +48 -0
  23. package/dist/shared/diff/schemas.js +77 -0
  24. package/dist/shared/diff/types.js +0 -3
  25. package/dist/shared/events/schemas.js +4 -0
  26. package/dist/shared/git/schemas.js +59 -0
  27. package/dist/shared/http/schemas.js +23 -0
  28. package/dist/shared/reviews/schemas.js +96 -0
  29. package/dist/shared/reviews/summary.js +5 -0
  30. package/dist/shared/utils/schemas.js +9 -0
  31. package/dist/web/assets/AppPage-BSvF7ik1.js +1 -0
  32. package/dist/web/assets/CreateReviewFabButton-M0od1QEZ.js +1 -0
  33. package/dist/web/assets/{ErrorNotFound-B5uYICL4.js → ErrorNotFound-BAP3n0aC.js} +1 -1
  34. package/dist/web/assets/IndexPage-CztuM7ha.css +1 -0
  35. package/dist/web/assets/IndexPage-Do-EZOBf.js +1 -0
  36. package/dist/web/assets/MainLayout-daEN-VtX.js +7 -0
  37. package/dist/web/assets/QBtn-BDbJU6_B.js +1 -0
  38. package/dist/web/assets/{QMenu-BZ1eK_rS.js → QMenu-DXgFwoTJ.js} +1 -1
  39. package/dist/web/assets/QSelect-DzYPZHrW.js +1 -0
  40. package/dist/web/assets/ReviewDetailPage-CP6YqXPT.css +1 -0
  41. package/dist/web/assets/ReviewDetailPage-DEtNg2UN.js +1 -0
  42. package/dist/web/assets/ReviewsPage-Dg-iCxsQ.js +1 -0
  43. package/dist/web/assets/StatusSwitcher-BpebV47C.js +4 -0
  44. package/dist/web/assets/{StatusSwitcher-CZYtI0ID.css → StatusSwitcher-Ca5k9Tjx.css} +1 -1
  45. package/dist/web/assets/{abnfDiagram-O67JEVCF-DkOrqZLn.js → abnfDiagram-O67JEVCF-BUX560Qb.js} +1 -1
  46. package/dist/web/assets/agent-icon-BJbyp2DP.js +17 -0
  47. package/dist/web/assets/agent-store-B9E-aiz3.js +1 -0
  48. package/dist/web/assets/{app-info-store-CIqmSeWK.js → app-info-store-xZiQfdgO.js} +1 -1
  49. package/dist/web/assets/{arc-BALAYY3f.js → arc-dElW-85p.js} +1 -1
  50. package/dist/web/assets/architecture-7GRP2DOG-bgxww3gY.js +1 -0
  51. package/dist/web/assets/{architectureDiagram-NJMV4G6O-C-xtrMea.js → architectureDiagram-NJMV4G6O-CUUKpxro.js} +1 -1
  52. package/dist/web/assets/{blockDiagram-BEXU5L5S-DYo9Os-m.js → blockDiagram-BEXU5L5S-D4r5wIie.js} +1 -1
  53. package/dist/web/assets/{c4Diagram-YGBWAQC7-BpSFjUAX.js → c4Diagram-YGBWAQC7-ww04LJhe.js} +1 -1
  54. package/dist/web/assets/channel-CdThk9yc.js +1 -0
  55. package/dist/web/assets/{chunk-3FUC2YCW-DtUqbu5K.js → chunk-3FUC2YCW-9i3K5FXP.js} +1 -1
  56. package/dist/web/assets/{chunk-5DYCD2WN-DeYTIM8k.js → chunk-5DYCD2WN-BmtGl0Xn.js} +1 -1
  57. package/dist/web/assets/{chunk-742MDFTN-8BfKAsu6.js → chunk-742MDFTN-5p0wX5Yc.js} +1 -1
  58. package/dist/web/assets/{chunk-7INBJB4K-PXK092EZ.js → chunk-7INBJB4K-CWFVY0jp.js} +1 -1
  59. package/dist/web/assets/{chunk-7M6MHVWA-Db9reT-V.js → chunk-7M6MHVWA-DUgZIYj4.js} +1 -1
  60. package/dist/web/assets/{chunk-7PRAP22T-CNFpjNaH.js → chunk-7PRAP22T-BrhA6Tm7.js} +1 -1
  61. package/dist/web/assets/{chunk-GTNCS2PH-C5gqOp1W.js → chunk-GTNCS2PH-B83nsnuR.js} +1 -1
  62. package/dist/web/assets/{chunk-GWA4HPMP-BquHLG8L.js → chunk-GWA4HPMP-cewRmL7f.js} +1 -1
  63. package/dist/web/assets/{chunk-MBY4JIJT-DyOEFgCh.js → chunk-MBY4JIJT-D18SeqyB.js} +1 -1
  64. package/dist/web/assets/{chunk-NETBCI7D-Bd3ij3-b.js → chunk-NETBCI7D-5SjiRygT.js} +1 -1
  65. package/dist/web/assets/{chunk-O7XYJQB3-DQvIvDe6.js → chunk-O7XYJQB3-H6mLYErS.js} +1 -1
  66. package/dist/web/assets/{chunk-UA2S7LBM-BDIHwgCZ.js → chunk-UA2S7LBM-C5VQVDlC.js} +1 -1
  67. package/dist/web/assets/{chunk-WEXAMYUT-DwQKTsK-.js → chunk-WEXAMYUT-CrOcvEcH.js} +1 -1
  68. package/dist/web/assets/{chunk-XXDRQBXY-DTdiON6C.js → chunk-XXDRQBXY-BuVPxQhc.js} +1 -1
  69. package/dist/web/assets/{chunk-Z7XXMR3K-CIvnaGnU.js → chunk-Z7XXMR3K-BVP3f1-g.js} +1 -1
  70. package/dist/web/assets/{chunk-ZIGJFQKS-BdT_T5CV.js → chunk-ZIGJFQKS-CF-OzGXH.js} +1 -1
  71. package/dist/web/assets/{classDiagram-v2-NBCMYWYE-BUoclHlO.js → classDiagram-v2-NBCMYWYE-Bd3VDNaV.js} +1 -1
  72. package/dist/web/assets/{core-B8kUiZoM.js → core-DiiZIGCg.js} +1 -1
  73. package/dist/web/assets/{cose-bilkent-JH36ORCC-C16eW6pc.js → cose-bilkent-JH36ORCC-ifMLrEkd.js} +1 -1
  74. package/dist/web/assets/create-BkZH2Q0U.js +1 -0
  75. package/dist/web/assets/{cynefin-OW5HDTMX-1mS7ZeoM.js → cynefin-OW5HDTMX-6l9c0yRf.js} +1 -1
  76. package/dist/web/assets/{cynefinDiagram-VND7K2PF-D1PC4YT4.js → cynefinDiagram-VND7K2PF-u9Gy0Odz.js} +1 -1
  77. package/dist/web/assets/{dagre-6A5THRUB-amODOP74.js → dagre-6A5THRUB-D1jsEBl4.js} +1 -1
  78. package/dist/web/assets/{diagram-22UHCM2B-wUvcsN_B.js → diagram-22UHCM2B-DpyH5n1y.js} +1 -1
  79. package/dist/web/assets/{diagram-3UASUU5V-CLiOVs2l.js → diagram-3UASUU5V-3X1RJV9-.js} +1 -1
  80. package/dist/web/assets/{diagram-ATOU4E4O-Cs5cFGgz.js → diagram-ATOU4E4O-B_bvvAua.js} +1 -1
  81. package/dist/web/assets/{diagram-CDSNMT55-C8Jb-YeE.js → diagram-CDSNMT55-DheqAuOA.js} +1 -1
  82. package/dist/web/assets/{diagram-MLGK6HIB-DA6C9fVB.js → diagram-MLGK6HIB-CYwe9zYy.js} +1 -1
  83. package/dist/web/assets/{diagram-MPIPVDR6-BOaFKuUi.js → diagram-MPIPVDR6-CVd9i23e.js} +1 -1
  84. package/dist/web/assets/{dist-DuFVAX6Y.js → dist-BzuUT3GV.js} +1 -1
  85. package/dist/web/assets/{ebnfDiagram-ZINNZB2B-e5jvn-EE.js → ebnfDiagram-ZINNZB2B-B5vtXULf.js} +1 -1
  86. package/dist/web/assets/{elk-276RUBZZ-D7Gcu17D.js → elk-276RUBZZ-BmMM0tV5.js} +1 -1
  87. package/dist/web/assets/{engine-oniguruma-HbxpIAu4.js → engine-oniguruma-D6DCTEub.js} +1 -1
  88. package/dist/web/assets/{erDiagram-OPXOYQCR-Cn1bBUF8.js → erDiagram-OPXOYQCR-1ORPVLQR.js} +1 -1
  89. package/dist/web/assets/eventmodeling-NTZA5JFV-BH97sTDs.js +1 -0
  90. package/dist/web/assets/flowDiagram-KWPJA3E3-CRwhE8ka.js +1 -0
  91. package/dist/web/assets/{ganttDiagram-FUAMR5RP-C3U8jeJB.js → ganttDiagram-FUAMR5RP-CAWNdy1S.js} +1 -1
  92. package/dist/web/assets/{gitGraph-4MIJSDKK-C165DC0H.js → gitGraph-4MIJSDKK-CXCCnZ1j.js} +1 -1
  93. package/dist/web/assets/{gitGraphDiagram-X574FWY7-CVHdT1OC.js → gitGraphDiagram-X574FWY7-DPDrqSt6.js} +1 -1
  94. package/dist/web/assets/i18n-D5wmXwHr.js +1 -0
  95. package/dist/web/assets/{index-C6ArNnKs.css → index-CgeU6Wuu.css} +1 -1
  96. package/dist/web/assets/index-DWi4DKrl.js +2 -0
  97. package/dist/web/assets/{info-A6RAGUB7-cbWtF3FJ.js → info-A6RAGUB7-CGg3Xa4X.js} +1 -1
  98. package/dist/web/assets/{infoDiagram-VRGFBTTK-C_kWn3Ai.js → infoDiagram-VRGFBTTK-vx9EUrdV.js} +1 -1
  99. package/dist/web/assets/init-app-CgDkvlRb.js +1 -0
  100. package/dist/web/assets/is-BB7rJzdU.js +1 -0
  101. package/dist/web/assets/{ishikawaDiagram-OU5B5YK6-Xb9cDRr7.js → ishikawaDiagram-OU5B5YK6-DL7T0J63.js} +1 -1
  102. package/dist/web/assets/{journeyDiagram-ZHPQQLJL-6eZcefap.js → journeyDiagram-ZHPQQLJL-D45T1Pj5.js} +1 -1
  103. package/dist/web/assets/{kanban-definition-PNTS6WVX-D_z_AsKR.js → kanban-definition-PNTS6WVX-C9ZnTE1l.js} +1 -1
  104. package/dist/web/assets/{line-v7JtZ-Za.js → line-CLBEcmiH.js} +1 -1
  105. package/dist/web/assets/{linear-hp1KWmiC.js → linear-B5bBEu0i.js} +1 -1
  106. package/dist/web/assets/{mermaid-parser.core-Cwwk5NqO.js → mermaid-parser.core-Dj-n65Ou.js} +3 -3
  107. package/dist/web/assets/{mermaid.core-BjPJhwW0.js → mermaid.core-CmPsESsw.js} +4 -4
  108. package/dist/web/assets/{mindmap-definition-NLK3R4M7-lQohhDXq.js → mindmap-definition-NLK3R4M7-ZX638ce_.js} +1 -1
  109. package/dist/web/assets/{packet-AYTQ26CC-DxiGe6nt.js → packet-AYTQ26CC-D-XZHUq_.js} +1 -1
  110. package/dist/web/assets/{pegDiagram-GJSIUBJH-XZRpQTLx.js → pegDiagram-GJSIUBJH-sE-EOKNL.js} +1 -1
  111. package/dist/web/assets/{pie-WAS4IAKB-fxN0O90k.js → pie-WAS4IAKB-DMkVggJA.js} +1 -1
  112. package/dist/web/assets/{pieDiagram-5QR66LMP-M_bBKE-n.js → pieDiagram-5QR66LMP-DSdIEQyS.js} +1 -1
  113. package/dist/web/assets/pinia-BMN9Drn3.js +1 -0
  114. package/dist/web/assets/pinia-DmTi2T44.js +1 -0
  115. package/dist/web/assets/{quadrantDiagram-O4NWA36T-BNxVPrq3.js → quadrantDiagram-O4NWA36T-BReXSVss.js} +1 -1
  116. package/dist/web/assets/{radar-RG4KPBEZ-Cd-5aokk.js → radar-RG4KPBEZ-CJzPwlj6.js} +1 -1
  117. package/dist/web/assets/{railroad-74A4TZTK-BvSKL8Ze.js → railroad-74A4TZTK-CkhRuR3N.js} +1 -1
  118. package/dist/web/assets/railroad-abnf-HS5TGJTU-CcS8fVgm.js +1 -0
  119. package/dist/web/assets/railroad-ebnf-LZEXJU2U-BlpYA103.js +1 -0
  120. package/dist/web/assets/railroad-peg-WCYAUIDC-C2EznudM.js +1 -0
  121. package/dist/web/assets/{railroadDiagram-XR7U4H2S-D664lPSL.js → railroadDiagram-XR7U4H2S-DCoF4AEN.js} +1 -1
  122. package/dist/web/assets/{requirementDiagram-PLB6GJNP-B_1Dl0-3.js → requirementDiagram-PLB6GJNP-B15aXZXI.js} +1 -1
  123. package/dist/web/assets/runtime-core.esm-bundler-CUpq88z7.js +1 -0
  124. package/dist/web/assets/{safe-storage-CWaD6Dq3.js → safe-storage-ak36tVRO.js} +1 -1
  125. package/dist/web/assets/{sankeyDiagram-IPEJSGJF-BvFmMC_F.js → sankeyDiagram-IPEJSGJF-DcWsnYYP.js} +1 -1
  126. package/dist/web/assets/{sequenceDiagram-PO4LG4MO-DMK1ey4g.js → sequenceDiagram-PO4LG4MO-CXWUNmSl.js} +1 -1
  127. package/dist/web/assets/{src-Cy0hT8a0.js → src-CB-shhJB.js} +1 -1
  128. package/dist/web/assets/{stateDiagram-v2-GCMORJYK-VVElU-_E.js → stateDiagram-v2-GCMORJYK-M9PkKdix.js} +1 -1
  129. package/dist/web/assets/{swimlanes-2SLR337P-DyfYJvHL.js → swimlanes-2SLR337P-B13LpAqh.js} +1 -1
  130. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BmCQPnJC.js +8 -0
  131. package/dist/web/assets/theme-BMaTNJdz.js +1 -0
  132. package/dist/web/assets/{timeline-definition-EJHVYXUP-DBoRb7uC.js → timeline-definition-EJHVYXUP-Dli0jMaX.js} +1 -1
  133. package/dist/web/assets/{treeView-Q6P3EWNA-D8AoZg1Y.js → treeView-Q6P3EWNA-BwBFpWIC.js} +1 -1
  134. package/dist/web/assets/{treemap-WGGIJYW6-DhcVtZsu.js → treemap-WGGIJYW6-DYGjPuTv.js} +1 -1
  135. package/dist/web/assets/use-app-theme-C9HTPdVu.js +1 -0
  136. package/dist/web/assets/use-panel-width-DGbjs_T8.js +1 -0
  137. package/dist/web/assets/use-virtual-scroll-BVHTKnoC.js +1 -0
  138. package/dist/web/assets/{usecaseDiagram-POWQR4AR-BCfvWOH4.js → usecaseDiagram-POWQR4AR-Chs4O-o-.js} +1 -1
  139. package/dist/web/assets/{vennDiagram-UO4OBE2U-BIk6-FLo.js → vennDiagram-UO4OBE2U-D3GVZcIr.js} +1 -1
  140. package/dist/web/assets/{vue-i18n.runtime-BQwnBdmC.js → vue-i18n.runtime-D0pkhc7g.js} +2 -2
  141. package/dist/web/assets/{wardley-WFR3VGLG-BMHH70A3.js → wardley-WFR3VGLG-B5YUEVo5.js} +1 -1
  142. package/dist/web/assets/{wardleyDiagram-VNRHLVJA-DB-cxlSq.js → wardleyDiagram-VNRHLVJA-liqH-4lm.js} +1 -1
  143. package/dist/web/assets/{xychartDiagram-PMCCYNJV-BejE-3gi.js → xychartDiagram-PMCCYNJV-C63N6ZvA.js} +1 -1
  144. package/dist/web/index.html +6 -6
  145. package/package.json +2 -2
  146. package/dist/web/assets/AppPage-CSb8txs5.js +0 -1
  147. package/dist/web/assets/CreateReviewFabButton-B18DLuH6.js +0 -1
  148. package/dist/web/assets/IndexPage-B-hkrnvt.css +0 -1
  149. package/dist/web/assets/IndexPage-Cm_ssBeC.js +0 -1
  150. package/dist/web/assets/MainLayout-ClBPF545.js +0 -7
  151. package/dist/web/assets/QBtn-E32fUOKx.js +0 -1
  152. package/dist/web/assets/QItemLabel-Co6oCEUr.js +0 -1
  153. package/dist/web/assets/QSelect-D1V4tbDr.js +0 -1
  154. package/dist/web/assets/ReviewDetailPage-BWa3EEkv.css +0 -1
  155. package/dist/web/assets/ReviewDetailPage-Ct0hXwZL.js +0 -1
  156. package/dist/web/assets/ReviewsPage-CbLAU6OM.js +0 -1
  157. package/dist/web/assets/StatusSwitcher-C0k4efwK.js +0 -4
  158. package/dist/web/assets/agent-icon-BKoq2a6T.js +0 -17
  159. package/dist/web/assets/agent-store-CEQ6MmTC.js +0 -1
  160. package/dist/web/assets/architecture-7GRP2DOG-CTkghkmX.js +0 -1
  161. package/dist/web/assets/channel-f9cE6NV9.js +0 -1
  162. package/dist/web/assets/create-B9ZqyTxV.js +0 -1
  163. package/dist/web/assets/eventmodeling-NTZA5JFV-BjGpd42n.js +0 -1
  164. package/dist/web/assets/flowDiagram-KWPJA3E3-a3N8MVB8.js +0 -1
  165. package/dist/web/assets/i18n-CyUPETur.js +0 -1
  166. package/dist/web/assets/index-C_B6Z2lc.js +0 -2
  167. package/dist/web/assets/init-app-DnfYD-0k.js +0 -1
  168. package/dist/web/assets/is-ChHvR1Z-.js +0 -1
  169. package/dist/web/assets/pinia-0B8sIOgD.js +0 -1
  170. package/dist/web/assets/pinia-DbmXdZbT.js +0 -1
  171. package/dist/web/assets/railroad-abnf-HS5TGJTU-CB32K25L.js +0 -1
  172. package/dist/web/assets/railroad-ebnf-LZEXJU2U-CqQR3zRl.js +0 -1
  173. package/dist/web/assets/railroad-peg-WCYAUIDC-DRSdB-oL.js +0 -1
  174. package/dist/web/assets/runtime-core.esm-bundler-Carm8LUR.js +0 -1
  175. package/dist/web/assets/swimlanesDiagram-TC7HE7FX-BQ0mK2BI.js +0 -8
  176. package/dist/web/assets/theme-BFceB3b9.js +0 -1
  177. package/dist/web/assets/use-app-theme-DxZW-eOR.js +0 -1
  178. package/dist/web/assets/use-panel-width-CY75Fomr.js +0 -1
@@ -1,23 +1,71 @@
1
1
  import { BadRequestError } from "../errors/http.js";
2
2
  import { Type } from '@sinclair/typebox';
3
- import { getStagedDiff, getUnstagedDiff, getBranchDiff, getCommitsDiff } from "../services/diff.service.js";
4
- const BranchQuery = Type.Object({
5
- base: Type.String({ minLength: 1 })
6
- });
7
- const CommitsQuery = Type.Object({
8
- from: Type.String({ minLength: 1 }),
9
- to: Type.String({ minLength: 1 })
10
- });
3
+ import { getBranchDiff, getCommitsDiff, getDiffSummaries, getFileDiff } from "../services/diff.service.js";
4
+ import { resolveWithinRepo } from "../services/git.service.js";
5
+ import { errorMessage } from "../../shared/utils/error-message.js";
6
+ import { BranchQuery, CommitsQuery, DiffFileSchema, DiffFileSummarySchema, FileQuery, SourceParams } from "../../shared/diff/schemas.js";
7
+ import { ERROR_RESPONSES } from "../../shared/http/schemas.js";
8
+ // OpenAPI group of these routes.
9
+ const TAGS = ['diff'];
11
10
  export async function diffRoutes(app, opts) {
12
11
  const { repositoryPath } = opts;
13
12
  const typedApp = app.withTypeProvider();
14
- typedApp.get('/api/diff/staged', async () => {
15
- return getStagedDiff(repositoryPath);
16
- });
17
- typedApp.get('/api/diff/unstaged', async () => {
18
- return getUnstagedDiff(repositoryPath);
13
+ // Files without hunks, then one file with hunks: the list stays small (ADR 0033).
14
+ typedApp.get('/api/diff/:source/files', {
15
+ schema: {
16
+ tags: TAGS,
17
+ summary: 'List changed files without hunks',
18
+ params: SourceParams,
19
+ response: {
20
+ 200: Type.Array(DiffFileSummarySchema, {
21
+ description: 'Changed files of the side.'
22
+ }),
23
+ ...ERROR_RESPONSES
24
+ }
25
+ }
26
+ }, async (request) => getDiffSummaries(repositoryPath, request.params.source));
27
+ typedApp.get('/api/diff/:source/file', {
28
+ schema: {
29
+ tags: TAGS,
30
+ summary: 'Get one changed file with hunks',
31
+ params: SourceParams,
32
+ querystring: FileQuery,
33
+ response: {
34
+ 200: { ...DiffFileSchema, description: 'The file with its hunks.' },
35
+ ...ERROR_RESPONSES
36
+ }
37
+ }
38
+ }, async (request) => {
39
+ const { path, oldPath, status } = request.query;
40
+ // Only a path outside the repository is a bad request. Other errors stay server errors.
41
+ try {
42
+ resolveWithinRepo(repositoryPath, path);
43
+ if (oldPath !== undefined)
44
+ resolveWithinRepo(repositoryPath, oldPath);
45
+ }
46
+ catch (err) {
47
+ throw new BadRequestError(errorMessage(err), { cause: err });
48
+ }
49
+ return getFileDiff(repositoryPath, request.params.source, {
50
+ oldPath: oldPath ?? path,
51
+ newPath: path,
52
+ status
53
+ });
19
54
  });
20
- typedApp.get('/api/diff/branch', { schema: { querystring: BranchQuery } }, async (request) => {
55
+ typedApp.get('/api/diff/branch', {
56
+ schema: {
57
+ tags: TAGS,
58
+ summary: 'Diff of HEAD against a ref',
59
+ description: 'No UI calls it yet. It is kept for branch reviews.',
60
+ querystring: BranchQuery,
61
+ response: {
62
+ 200: Type.Array(DiffFileSchema, {
63
+ description: 'Changed files with hunks.'
64
+ }),
65
+ ...ERROR_RESPONSES
66
+ }
67
+ }
68
+ }, async (request) => {
21
69
  try {
22
70
  return await getBranchDiff(repositoryPath, request.query.base);
23
71
  }
@@ -26,7 +74,20 @@ export async function diffRoutes(app, opts) {
26
74
  throw new BadRequestError(message, { cause: err });
27
75
  }
28
76
  });
29
- typedApp.get('/api/diff/commits', { schema: { querystring: CommitsQuery } }, async (request) => {
77
+ typedApp.get('/api/diff/commits', {
78
+ schema: {
79
+ tags: TAGS,
80
+ summary: 'Diff between two refs',
81
+ description: 'No UI calls it yet. It is kept for commit reviews.',
82
+ querystring: CommitsQuery,
83
+ response: {
84
+ 200: Type.Array(DiffFileSchema, {
85
+ description: 'Changed files with hunks.'
86
+ }),
87
+ ...ERROR_RESPONSES
88
+ }
89
+ }
90
+ }, async (request) => {
30
91
  try {
31
92
  return await getCommitsDiff(repositoryPath, request.query.from, request.query.to);
32
93
  }
@@ -1,8 +1,21 @@
1
1
  import { safeSend } from "../utils/sse.js";
2
+ import { EventStreamSchema } from "../../shared/events/schemas.js";
2
3
  export async function eventRoutes(app, opts) {
3
4
  const { eventBus, instanceId } = opts;
4
5
  const hello = { instanceId };
5
- app.get('/api/events', { sse: true }, async (request, reply) => {
6
+ app.get('/api/events', {
7
+ sse: true,
8
+ schema: {
9
+ tags: ['events'],
10
+ summary: 'Stream of review, comment and file changes',
11
+ response: {
12
+ 200: {
13
+ description: 'An SSE stream that stays open.',
14
+ content: { 'text/event-stream': { schema: EventStreamSchema } }
15
+ }
16
+ }
17
+ }
18
+ }, async (request, reply) => {
6
19
  reply.sse.keepAlive();
7
20
  await reply.sse.send({ event: 'connected', data: hello });
8
21
  // The client can leave during the await; a later `onClose` would never run.
@@ -1,13 +1,30 @@
1
1
  import { Type } from '@sinclair/typebox';
2
2
  import { exportAsJson, exportAsMarkdown } from "../services/export.service.js";
3
- const ExportParams = Type.Object({ id: Type.String() });
4
- const ExportQuery = Type.Object({
5
- format: Type.Union([Type.Literal('json'), Type.Literal('markdown')])
6
- });
3
+ import { ExportQuery, ReviewExportSchema, ReviewIdParams } from "../../shared/reviews/schemas.js";
4
+ import { ERROR_RESPONSES } from "../../shared/http/schemas.js";
7
5
  export async function exportRoutes(app, opts) {
8
6
  const { db, repositoryPath } = opts;
9
7
  const typedApp = app.withTypeProvider();
10
- typedApp.get('/api/reviews/:id/export', { schema: { params: ExportParams, querystring: ExportQuery } }, async (request, reply) => {
8
+ typedApp.get('/api/reviews/:id/export', {
9
+ schema: {
10
+ tags: ['reviews'],
11
+ summary: 'Export a review with its comments',
12
+ params: ReviewIdParams,
13
+ querystring: ExportQuery,
14
+ response: {
15
+ 200: {
16
+ description: 'The review as JSON or as a markdown report.',
17
+ content: {
18
+ 'application/json': { schema: ReviewExportSchema },
19
+ 'text/markdown': {
20
+ schema: Type.String({ description: 'Markdown report.' })
21
+ }
22
+ }
23
+ },
24
+ ...ERROR_RESPONSES
25
+ }
26
+ }
27
+ }, async (request, reply) => {
11
28
  if (request.query.format === 'json') {
12
29
  return exportAsJson(db, request.params.id);
13
30
  }
@@ -1,27 +1,29 @@
1
1
  import { BadRequestError } from "../errors/http.js";
2
- import { Type } from '@sinclair/typebox';
3
2
  import { getRepositoryInfo, stagePaths, unstagePaths, discardPaths, getFilesAtRef, getFileAtRef } from "../services/git.service.js";
4
- const TreeParams = Type.Object({
5
- ref: Type.String({ minLength: 1 })
6
- });
7
- const FileContentQuery = Type.Object({
8
- ref: Type.String({ minLength: 1 })
9
- });
10
- /** Body for `/stage` and `/unstage`: an omitted/empty `paths` means "all". */
11
- const OptionalPathsBody = Type.Object({
12
- paths: Type.Optional(Type.Array(Type.String()))
13
- });
14
- /** Body for `/discard`: `paths` is required — no "discard everything". */
15
- const DiscardBody = Type.Object({
16
- paths: Type.Array(Type.String(), { minItems: 1 })
17
- });
3
+ import { DiscardBody, FileContentQuery, FileContentResponseSchema, FileParams, FileTreeResponseSchema, OkSchema, OptionalPathsBody, RepositoryInfoSchema, TreeParams } from "../../shared/git/schemas.js";
4
+ import { ERROR_RESPONSES } from "../../shared/http/schemas.js";
5
+ // OpenAPI group of these routes.
6
+ const TAGS = ['git'];
18
7
  export async function gitRoutes(app, opts) {
19
8
  const { repositoryPath } = opts;
20
9
  const typedApp = app.withTypeProvider();
21
- app.get('/api/git/info', async () => {
10
+ typedApp.get('/api/git/info', {
11
+ schema: {
12
+ tags: TAGS,
13
+ summary: 'Repository facts',
14
+ response: { 200: RepositoryInfoSchema, ...ERROR_RESPONSES }
15
+ }
16
+ }, async () => {
22
17
  return getRepositoryInfo(repositoryPath);
23
18
  });
24
- typedApp.post('/api/git/stage', { schema: { body: OptionalPathsBody } }, async (request) => {
19
+ typedApp.post('/api/git/stage', {
20
+ schema: {
21
+ tags: TAGS,
22
+ summary: 'Stage paths',
23
+ body: OptionalPathsBody,
24
+ response: { 200: OkSchema, ...ERROR_RESPONSES }
25
+ }
26
+ }, async (request) => {
25
27
  try {
26
28
  await stagePaths(repositoryPath, request.body.paths ?? []);
27
29
  return { ok: true };
@@ -31,7 +33,14 @@ export async function gitRoutes(app, opts) {
31
33
  throw new BadRequestError(message, { cause: err });
32
34
  }
33
35
  });
34
- typedApp.post('/api/git/unstage', { schema: { body: OptionalPathsBody } }, async (request) => {
36
+ typedApp.post('/api/git/unstage', {
37
+ schema: {
38
+ tags: TAGS,
39
+ summary: 'Unstage paths',
40
+ body: OptionalPathsBody,
41
+ response: { 200: OkSchema, ...ERROR_RESPONSES }
42
+ }
43
+ }, async (request) => {
35
44
  try {
36
45
  await unstagePaths(repositoryPath, request.body.paths ?? []);
37
46
  return { ok: true };
@@ -41,7 +50,14 @@ export async function gitRoutes(app, opts) {
41
50
  throw new BadRequestError(message, { cause: err });
42
51
  }
43
52
  });
44
- typedApp.post('/api/git/discard', { schema: { body: DiscardBody } }, async (request) => {
53
+ typedApp.post('/api/git/discard', {
54
+ schema: {
55
+ tags: TAGS,
56
+ summary: 'Discard working-tree changes of paths',
57
+ body: DiscardBody,
58
+ response: { 200: OkSchema, ...ERROR_RESPONSES }
59
+ }
60
+ }, async (request) => {
45
61
  try {
46
62
  await discardPaths(repositoryPath, request.body.paths);
47
63
  return { ok: true };
@@ -51,7 +67,14 @@ export async function gitRoutes(app, opts) {
51
67
  throw new BadRequestError(message, { cause: err });
52
68
  }
53
69
  });
54
- typedApp.get('/api/git/tree/:ref', { schema: { params: TreeParams } }, async (request) => {
70
+ typedApp.get('/api/git/tree/:ref', {
71
+ schema: {
72
+ tags: TAGS,
73
+ summary: 'List files at a ref',
74
+ params: TreeParams,
75
+ response: { 200: FileTreeResponseSchema, ...ERROR_RESPONSES }
76
+ }
77
+ }, async (request) => {
55
78
  const { ref } = request.params;
56
79
  try {
57
80
  const files = await getFilesAtRef(repositoryPath, ref);
@@ -62,7 +85,15 @@ export async function gitRoutes(app, opts) {
62
85
  throw new BadRequestError(message, { cause: err });
63
86
  }
64
87
  });
65
- typedApp.get('/api/git/file/*', { schema: { querystring: FileContentQuery } }, async (request) => {
88
+ typedApp.get('/api/git/file/*', {
89
+ schema: {
90
+ tags: TAGS,
91
+ summary: 'Read one file at a ref',
92
+ params: FileParams,
93
+ querystring: FileContentQuery,
94
+ response: { 200: FileContentResponseSchema, ...ERROR_RESPONSES }
95
+ }
96
+ }, async (request) => {
66
97
  const filePath = request.params['*'];
67
98
  const { ref } = request.query;
68
99
  if (!filePath) {
@@ -1,5 +1,12 @@
1
+ import { HealthSchema } from "../../shared/app/schemas.js";
1
2
  export async function healthRoutes(app) {
2
- app.get('/health', async () => {
3
+ app.withTypeProvider().get('/health', {
4
+ schema: {
5
+ tags: ['app'],
6
+ summary: 'Liveness check',
7
+ response: { 200: HealthSchema }
8
+ }
9
+ }, async () => {
3
10
  return { status: 'ok' };
4
11
  });
5
12
  }
@@ -0,0 +1,4 @@
1
+ export async function openApiRoutes(app) {
2
+ // No authentication (ADR 0008): the spec tells no more than the API itself.
3
+ app.get('/api/openapi.json', { schema: { hide: true } }, async () => app.swagger());
4
+ }
@@ -2,43 +2,41 @@ import { NotFoundError, BadRequestError } from "../errors/http.js";
2
2
  import { Type } from '@sinclair/typebox';
3
3
  import { createReview, getReview, getReviews, setReviewStatus, removeReview } from "../services/review.service.js";
4
4
  import { getRepositoryInfo } from "../services/git.service.js";
5
- const CreateReviewBody = Type.Object({
6
- sourceType: Type.Optional(Type.Union([
7
- Type.Literal('local'),
8
- Type.Literal('staged'),
9
- Type.Literal('unstaged'),
10
- Type.Literal('branch'),
11
- Type.Literal('commits')
12
- ])),
13
- sourceRef: Type.Optional(Type.String()),
14
- baseRef: Type.Optional(Type.String()),
15
- name: Type.Optional(Type.String())
16
- });
17
- const ReviewsQuery = Type.Object({
18
- branch: Type.Optional(Type.String()),
19
- search: Type.Optional(Type.String()),
20
- createdFrom: Type.Optional(Type.String()),
21
- createdTo: Type.Optional(Type.String()),
22
- /** Comma-separated file paths — reviews matching any of them are returned. */
23
- files: Type.Optional(Type.String())
24
- });
25
- const UpdateReviewBody = Type.Object({
26
- status: Type.Optional(Type.Union([
27
- Type.Literal('in_progress'),
28
- Type.Literal('approved'),
29
- Type.Literal('changes_requested')
30
- ]))
31
- });
32
- const ReviewIdParams = Type.Object({ id: Type.String() });
5
+ import { CreateReviewBody, ReviewIdParams, ReviewSchema, ReviewSummarySchema, ReviewsQuery, UpdateReviewBody } from "../../shared/reviews/schemas.js";
6
+ import { ERROR_RESPONSES, NoContentSchema } from "../../shared/http/schemas.js";
7
+ // OpenAPI group of these routes.
8
+ const TAGS = ['reviews'];
33
9
  export async function reviewRoutes(app, opts) {
34
10
  const { repositoryPath, db, eventBus } = opts;
35
11
  const typedApp = app.withTypeProvider();
36
- typedApp.post('/api/reviews', { schema: { body: CreateReviewBody } }, async (request, reply) => {
12
+ typedApp.post('/api/reviews', {
13
+ schema: {
14
+ tags: TAGS,
15
+ summary: 'Create a review from the current diff',
16
+ body: CreateReviewBody,
17
+ response: {
18
+ 201: { ...ReviewSchema, description: 'The new review.' },
19
+ ...ERROR_RESPONSES
20
+ }
21
+ }
22
+ }, async (request, reply) => {
37
23
  const review = await createReview(db, repositoryPath, request.body, eventBus);
38
24
  reply.code(201);
39
25
  return review;
40
26
  });
41
- typedApp.get('/api/reviews', { schema: { querystring: ReviewsQuery } }, async (request) => {
27
+ typedApp.get('/api/reviews', {
28
+ schema: {
29
+ tags: TAGS,
30
+ summary: 'List reviews, newest first',
31
+ querystring: ReviewsQuery,
32
+ response: {
33
+ 200: Type.Array(ReviewSummarySchema, {
34
+ description: 'Reviews without snapshots.'
35
+ }),
36
+ ...ERROR_RESPONSES
37
+ }
38
+ }
39
+ }, async (request) => {
42
40
  const { branch, search, createdFrom, createdTo, files } = request.query;
43
41
  const resolvedBranch = branch ?? (await getRepositoryInfo(repositoryPath)).branch;
44
42
  return getReviews(db, {
@@ -51,14 +49,35 @@ export async function reviewRoutes(app, opts) {
51
49
  : undefined
52
50
  });
53
51
  });
54
- typedApp.get('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request) => {
52
+ typedApp.get('/api/reviews/:id', {
53
+ schema: {
54
+ tags: TAGS,
55
+ summary: 'Get a review with its snapshot',
56
+ params: ReviewIdParams,
57
+ response: { 200: ReviewSchema, ...ERROR_RESPONSES }
58
+ }
59
+ }, async (request) => {
55
60
  const review = getReview(db, request.params.id);
56
61
  if (!review) {
57
62
  throw new NotFoundError(`Review ${request.params.id} not found`);
58
63
  }
59
64
  return review;
60
65
  });
61
- typedApp.patch('/api/reviews/:id', { schema: { params: ReviewIdParams, body: UpdateReviewBody } }, async (request) => {
66
+ typedApp.patch('/api/reviews/:id', {
67
+ schema: {
68
+ tags: TAGS,
69
+ summary: 'Change the review status',
70
+ params: ReviewIdParams,
71
+ body: UpdateReviewBody,
72
+ response: {
73
+ 200: {
74
+ ...ReviewSummarySchema,
75
+ description: 'The changed review without its snapshot.'
76
+ },
77
+ ...ERROR_RESPONSES
78
+ }
79
+ }
80
+ }, async (request) => {
62
81
  if (!request.body.status) {
63
82
  throw new BadRequestError('"status" is required');
64
83
  }
@@ -68,7 +87,14 @@ export async function reviewRoutes(app, opts) {
68
87
  }
69
88
  return updated;
70
89
  });
71
- typedApp.delete('/api/reviews/:id', { schema: { params: ReviewIdParams } }, async (request, reply) => {
90
+ typedApp.delete('/api/reviews/:id', {
91
+ schema: {
92
+ tags: TAGS,
93
+ summary: 'Delete a review and its comments',
94
+ params: ReviewIdParams,
95
+ response: { 204: NoContentSchema, ...ERROR_RESPONSES }
96
+ }
97
+ }, async (request, reply) => {
72
98
  if (!getReview(db, request.params.id)) {
73
99
  throw new NotFoundError(`Review ${request.params.id} not found`);
74
100
  }
@@ -195,6 +195,7 @@ export class AgentSession {
195
195
  * Changes a session setting (model, mode, …) and returns the agent's new
196
196
  * full set of options. Validated against what the agent itself offered, so
197
197
  * an unknown id or value is rejected here rather than sent on.
198
+ * `value` is `unknown`: the route schema gives it no type (see ConfigBody).
198
199
  */
199
200
  async setConfig(configId, value) {
200
201
  const connection = this.#connection;
@@ -208,9 +209,10 @@ export class AgentSession {
208
209
  if (!option) {
209
210
  throw new AgentConfigError(`Unknown setting "${configId}"`);
210
211
  }
211
- if (option.type === 'boolean'
212
- ? typeof value !== 'boolean'
213
- : !option.options?.some(c => c.value === value)) {
212
+ if ((typeof value !== 'boolean' && typeof value !== 'string') ||
213
+ (option.type === 'boolean'
214
+ ? typeof value !== 'boolean'
215
+ : !option.options?.some(c => c.value === value))) {
214
216
  throw new AgentConfigError(`Invalid value for "${option.name}"`);
215
217
  }
216
218
  try {
@@ -1,7 +1,15 @@
1
1
  import { diffLines } from 'diff';
2
- import { assertSafeRef, getFileAtRef, getFilesAtRefBatch, listChangedPaths, listUntrackedPaths } from "./git.service.js";
2
+ import { stat } from 'node:fs/promises';
3
+ import { assertSafeRef, listChangedSummaries, resolveWithinRepo, getFileAtRef, getFilesAtRefBatch, listChangedPaths, listUntrackedPaths } from "./git.service.js";
3
4
  /** Unchanged lines kept around each change in a hunk, as in `git diff`. */
4
5
  const CONTEXT_LINES = 3;
6
+ /** Stamp of a worktree file that cannot be read. */
7
+ const GONE_STAMP = 'gone';
8
+ /**
9
+ * Line count and binary flag of untracked files, by repository and path. A refetch runs on
10
+ * every save, so an untracked file is read again only when its stamp changes.
11
+ */
12
+ const untrackedCounts = new Map();
5
13
  function splitLines(text) {
6
14
  if (text === '')
7
15
  return [];
@@ -161,3 +169,113 @@ export async function getCommitsDiff(repoPath, from, to) {
161
169
  assertSafeRef(to);
162
170
  return buildDiffFiles(repoPath, [from, to], from, to);
163
171
  }
172
+ /**
173
+ * `mtime:ctime:inode:size` of a file in the worktree. It costs no read. The inode catches
174
+ * a file replaced by rename. Known limit: an edit of the same size within one timestamp
175
+ * tick of the file system keeps the stamp.
176
+ */
177
+ async function worktreeStamp(repoPath, path) {
178
+ try {
179
+ const info = await stat(resolveWithinRepo(repoPath, path));
180
+ return `${info.mtimeMs}:${info.ctimeMs}:${info.ino}:${info.size}`;
181
+ }
182
+ catch {
183
+ return GONE_STAMP;
184
+ }
185
+ }
186
+ function countLines(content) {
187
+ return splitLines(content).length;
188
+ }
189
+ /**
190
+ * Builds a signature from both sides of the diff. Equal signatures must mean equal hunks,
191
+ * so the old side counts too: a change of only HEAD or only the index changes the hunks.
192
+ */
193
+ function signatureOf(change, oldSide, newSide) {
194
+ return [
195
+ change.status,
196
+ change.oldPath,
197
+ change.newPath,
198
+ change.additions,
199
+ change.deletions,
200
+ oldSide,
201
+ newSide
202
+ ].join('|');
203
+ }
204
+ /** Counts the lines of an untracked file. The file is read only when its stamp changed. */
205
+ async function untrackedSummary(repoPath, path) {
206
+ const stamp = await worktreeStamp(repoPath, path);
207
+ const key = `${repoPath}\0${path}`;
208
+ let counts = untrackedCounts.get(key);
209
+ if (!counts || counts.stamp !== stamp) {
210
+ const file = await getFileAtRef(repoPath, 'WORKTREE', path);
211
+ counts = {
212
+ stamp,
213
+ additions: file.isBinary ? 0 : countLines(file.content),
214
+ isBinary: file.isBinary
215
+ };
216
+ if (stamp === GONE_STAMP) {
217
+ untrackedCounts.delete(key);
218
+ }
219
+ else {
220
+ untrackedCounts.set(key, counts);
221
+ }
222
+ }
223
+ const change = {
224
+ oldPath: path,
225
+ newPath: path,
226
+ status: 'added',
227
+ additions: counts.additions,
228
+ deletions: 0,
229
+ isBinary: counts.isBinary
230
+ };
231
+ return { ...change, signature: signatureOf(change, '', stamp) };
232
+ }
233
+ /** Drops the cached counts of files that are no longer untracked in this repository. */
234
+ function pruneUntrackedCounts(repoPath, untrackedPaths) {
235
+ const prefix = `${repoPath}\0`;
236
+ const alive = new Set(untrackedPaths.map(path => prefix + path));
237
+ for (const key of untrackedCounts.keys()) {
238
+ if (key.startsWith(prefix) && !alive.has(key))
239
+ untrackedCounts.delete(key);
240
+ }
241
+ }
242
+ /**
243
+ * Lists the files of one diff side with counts and a signature, but no hunks (ADR 0033).
244
+ * Counts come from git, the hunks of `getFileDiff` from `diffLines`: they may differ by a few lines.
245
+ */
246
+ export async function getDiffSummaries(repoPath, source) {
247
+ if (source === 'staged') {
248
+ const changes = await listChangedSummaries(repoPath, ['--cached']);
249
+ return changes.map(({ oldOid, newOid, ...change }) => ({
250
+ ...change,
251
+ signature: signatureOf(change, oldOid, newOid)
252
+ }));
253
+ }
254
+ const [changes, untrackedPaths] = await Promise.all([
255
+ listChangedSummaries(repoPath, []),
256
+ listUntrackedPaths(repoPath)
257
+ ]);
258
+ // The new side is the worktree: git prints zeros for its blob id, so a stamp stands in.
259
+ const tracked = await Promise.all(changes.map(async ({ oldOid, newOid: _newOid, ...change }) => ({
260
+ ...change,
261
+ signature: signatureOf(change, oldOid, await worktreeStamp(repoPath, change.newPath))
262
+ })));
263
+ const untracked = await Promise.all(untrackedPaths.map(path => untrackedSummary(repoPath, path)));
264
+ pruneUntrackedCounts(repoPath, untrackedPaths);
265
+ return [...tracked, ...untracked];
266
+ }
267
+ /**
268
+ * Builds the diff of one file. The caller passes the summary's paths and status, so no
269
+ * whole-repository listing runs per file. Untracked files have no index entry: their old side is empty.
270
+ */
271
+ export async function getFileDiff(repoPath, source, meta) {
272
+ const [oldRef, newRef] = source === 'staged' ? ['HEAD', 'INDEX'] : ['INDEX', 'WORKTREE'];
273
+ const [oldFile, newFile] = await Promise.all([
274
+ getFileAtRef(repoPath, oldRef, meta.oldPath),
275
+ getFileAtRef(repoPath, newRef, meta.newPath)
276
+ ]);
277
+ return computeFileDiff(oldFile.content, newFile.content, {
278
+ ...meta,
279
+ isBinary: oldFile.isBinary || newFile.isBinary
280
+ });
281
+ }