reviewflow 3.4.0 → 3.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 (205) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli/parseCliArgs.d.ts +6 -1
  3. package/dist/cli/parseCliArgs.d.ts.map +1 -1
  4. package/dist/cli/parseCliArgs.js +11 -1
  5. package/dist/cli/parseCliArgs.js.map +1 -1
  6. package/dist/config/projectConfig.d.ts +6 -0
  7. package/dist/config/projectConfig.d.ts.map +1 -1
  8. package/dist/config/projectConfig.js +13 -0
  9. package/dist/config/projectConfig.js.map +1 -1
  10. package/dist/entities/language/language.schema.d.ts +7 -0
  11. package/dist/entities/language/language.schema.d.ts.map +1 -0
  12. package/dist/entities/language/language.schema.js +3 -0
  13. package/dist/entities/language/language.schema.js.map +1 -0
  14. package/dist/frameworks/claude/claudeInvoker.d.ts +1 -1
  15. package/dist/frameworks/claude/claudeInvoker.d.ts.map +1 -1
  16. package/dist/frameworks/claude/claudeInvoker.js +6 -3
  17. package/dist/frameworks/claude/claudeInvoker.js.map +1 -1
  18. package/dist/frameworks/claude/languageDirective.d.ts +3 -0
  19. package/dist/frameworks/claude/languageDirective.d.ts.map +1 -0
  20. package/dist/frameworks/claude/languageDirective.js +9 -0
  21. package/dist/frameworks/claude/languageDirective.js.map +1 -0
  22. package/dist/frameworks/queue/pQueueAdapter.d.ts +2 -0
  23. package/dist/frameworks/queue/pQueueAdapter.d.ts.map +1 -1
  24. package/dist/frameworks/queue/pQueueAdapter.js +1 -1
  25. package/dist/frameworks/queue/pQueueAdapter.js.map +1 -1
  26. package/dist/frameworks/settings/runtimeSettings.d.ts +4 -0
  27. package/dist/frameworks/settings/runtimeSettings.d.ts.map +1 -1
  28. package/dist/frameworks/settings/runtimeSettings.js +8 -1
  29. package/dist/frameworks/settings/runtimeSettings.js.map +1 -1
  30. package/dist/interface-adapters/controllers/http/settings.routes.d.ts.map +1 -1
  31. package/dist/interface-adapters/controllers/http/settings.routes.js +12 -1
  32. package/dist/interface-adapters/controllers/http/settings.routes.js.map +1 -1
  33. package/dist/interface-adapters/controllers/webhook/github.controller.d.ts.map +1 -1
  34. package/dist/interface-adapters/controllers/webhook/github.controller.js +2 -1
  35. package/dist/interface-adapters/controllers/webhook/github.controller.js.map +1 -1
  36. package/dist/interface-adapters/controllers/webhook/gitlab.controller.d.ts.map +1 -1
  37. package/dist/interface-adapters/controllers/webhook/gitlab.controller.js +2 -1
  38. package/dist/interface-adapters/controllers/webhook/gitlab.controller.js.map +1 -1
  39. package/dist/interface-adapters/views/dashboard/index.html +974 -393
  40. package/dist/interface-adapters/views/dashboard/modules/assignee.d.ts +7 -0
  41. package/dist/interface-adapters/views/dashboard/modules/assignee.d.ts.map +1 -0
  42. package/dist/interface-adapters/views/dashboard/modules/assignee.js +47 -0
  43. package/dist/interface-adapters/views/dashboard/modules/assignee.js.map +1 -0
  44. package/dist/interface-adapters/views/dashboard/modules/constants.d.ts +7 -0
  45. package/dist/interface-adapters/views/dashboard/modules/constants.d.ts.map +1 -0
  46. package/dist/interface-adapters/views/dashboard/modules/constants.js +6 -0
  47. package/dist/interface-adapters/views/dashboard/modules/constants.js.map +1 -0
  48. package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.d.ts +23 -0
  49. package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.d.ts.map +1 -0
  50. package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.js +37 -0
  51. package/dist/interface-adapters/views/dashboard/modules/desktopNotifications.js.map +1 -0
  52. package/dist/interface-adapters/views/dashboard/modules/formatting.d.ts +23 -0
  53. package/dist/interface-adapters/views/dashboard/modules/formatting.d.ts.map +1 -0
  54. package/dist/interface-adapters/views/dashboard/modules/formatting.js +57 -0
  55. package/dist/interface-adapters/views/dashboard/modules/formatting.js.map +1 -0
  56. package/dist/interface-adapters/views/dashboard/modules/html.d.ts +16 -0
  57. package/dist/interface-adapters/views/dashboard/modules/html.d.ts.map +1 -0
  58. package/dist/interface-adapters/views/dashboard/modules/html.js +68 -0
  59. package/dist/interface-adapters/views/dashboard/modules/html.js.map +1 -0
  60. package/dist/interface-adapters/views/dashboard/modules/i18n.d.ts +11 -0
  61. package/dist/interface-adapters/views/dashboard/modules/i18n.d.ts.map +1 -0
  62. package/dist/interface-adapters/views/dashboard/modules/i18n.js +500 -0
  63. package/dist/interface-adapters/views/dashboard/modules/i18n.js.map +1 -0
  64. package/dist/interface-adapters/views/dashboard/modules/icons.d.ts +13 -0
  65. package/dist/interface-adapters/views/dashboard/modules/icons.d.ts.map +1 -0
  66. package/dist/interface-adapters/views/dashboard/modules/icons.js +29 -0
  67. package/dist/interface-adapters/views/dashboard/modules/icons.js.map +1 -0
  68. package/dist/interface-adapters/views/dashboard/modules/loading.d.ts +30 -0
  69. package/dist/interface-adapters/views/dashboard/modules/loading.d.ts.map +1 -0
  70. package/dist/interface-adapters/views/dashboard/modules/loading.js +48 -0
  71. package/dist/interface-adapters/views/dashboard/modules/loading.js.map +1 -0
  72. package/dist/interface-adapters/views/dashboard/modules/notifications.d.ts +39 -0
  73. package/dist/interface-adapters/views/dashboard/modules/notifications.d.ts.map +1 -0
  74. package/dist/interface-adapters/views/dashboard/modules/notifications.js +131 -0
  75. package/dist/interface-adapters/views/dashboard/modules/notifications.js.map +1 -0
  76. package/dist/interface-adapters/views/dashboard/modules/priority.d.ts +10 -0
  77. package/dist/interface-adapters/views/dashboard/modules/priority.d.ts.map +1 -0
  78. package/dist/interface-adapters/views/dashboard/modules/priority.js +86 -0
  79. package/dist/interface-adapters/views/dashboard/modules/priority.js.map +1 -0
  80. package/dist/interface-adapters/views/dashboard/modules/quality.d.ts +30 -0
  81. package/dist/interface-adapters/views/dashboard/modules/quality.d.ts.map +1 -0
  82. package/dist/interface-adapters/views/dashboard/modules/quality.js +83 -0
  83. package/dist/interface-adapters/views/dashboard/modules/quality.js.map +1 -0
  84. package/dist/interface-adapters/views/dashboard/modules/queueLanes.d.ts +22 -0
  85. package/dist/interface-adapters/views/dashboard/modules/queueLanes.d.ts.map +1 -0
  86. package/dist/interface-adapters/views/dashboard/modules/queueLanes.js +27 -0
  87. package/dist/interface-adapters/views/dashboard/modules/queueLanes.js.map +1 -0
  88. package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.d.ts +54 -0
  89. package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.d.ts.map +1 -0
  90. package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.js +120 -0
  91. package/dist/interface-adapters/views/dashboard/modules/sessionMetrics.js.map +1 -0
  92. package/dist/interface-adapters/views/dashboard/styles.css +1031 -93
  93. package/dist/main/cli.d.ts +41 -1
  94. package/dist/main/cli.d.ts.map +1 -1
  95. package/dist/main/cli.js +228 -88
  96. package/dist/main/cli.js.map +1 -1
  97. package/dist/tests/factories/reviewJob.factory.d.ts.map +1 -1
  98. package/dist/tests/factories/reviewJob.factory.js +1 -0
  99. package/dist/tests/factories/reviewJob.factory.js.map +1 -1
  100. package/dist/tests/units/cli/parseCliArgs.test.js +14 -0
  101. package/dist/tests/units/cli/parseCliArgs.test.js.map +1 -1
  102. package/dist/tests/units/config/projectConfig.test.d.ts +2 -0
  103. package/dist/tests/units/config/projectConfig.test.d.ts.map +1 -0
  104. package/dist/tests/units/config/projectConfig.test.js +69 -0
  105. package/dist/tests/units/config/projectConfig.test.js.map +1 -0
  106. package/dist/tests/units/entities/language/language.schema.test.d.ts +2 -0
  107. package/dist/tests/units/entities/language/language.schema.test.d.ts.map +1 -0
  108. package/dist/tests/units/entities/language/language.schema.test.js +17 -0
  109. package/dist/tests/units/entities/language/language.schema.test.js.map +1 -0
  110. package/dist/tests/units/frameworks/claude/languageDirective.test.d.ts +2 -0
  111. package/dist/tests/units/frameworks/claude/languageDirective.test.d.ts.map +1 -0
  112. package/dist/tests/units/frameworks/claude/languageDirective.test.js +13 -0
  113. package/dist/tests/units/frameworks/claude/languageDirective.test.js.map +1 -0
  114. package/dist/tests/units/frameworks/settings/runtimeSettings.test.d.ts +2 -0
  115. package/dist/tests/units/frameworks/settings/runtimeSettings.test.d.ts.map +1 -0
  116. package/dist/tests/units/frameworks/settings/runtimeSettings.test.js +20 -0
  117. package/dist/tests/units/frameworks/settings/runtimeSettings.test.js.map +1 -0
  118. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js +1 -0
  119. package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js.map +1 -1
  120. package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.d.ts +2 -0
  121. package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.d.ts.map +1 -0
  122. package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.js +35 -0
  123. package/dist/tests/units/interface-adapters/views/dashboard/modules/assignee.test.js.map +1 -0
  124. package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.d.ts +2 -0
  125. package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.d.ts.map +1 -0
  126. package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.js +17 -0
  127. package/dist/tests/units/interface-adapters/views/dashboard/modules/constants.test.js.map +1 -0
  128. package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.d.ts +2 -0
  129. package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.d.ts.map +1 -0
  130. package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.js +54 -0
  131. package/dist/tests/units/interface-adapters/views/dashboard/modules/desktopNotifications.test.js.map +1 -0
  132. package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.d.ts +2 -0
  133. package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.d.ts.map +1 -0
  134. package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.js +95 -0
  135. package/dist/tests/units/interface-adapters/views/dashboard/modules/formatting.test.js.map +1 -0
  136. package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.d.ts +2 -0
  137. package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.d.ts.map +1 -0
  138. package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.js +55 -0
  139. package/dist/tests/units/interface-adapters/views/dashboard/modules/html.test.js.map +1 -0
  140. package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.d.ts +2 -0
  141. package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.d.ts.map +1 -0
  142. package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.js +98 -0
  143. package/dist/tests/units/interface-adapters/views/dashboard/modules/i18n.test.js.map +1 -0
  144. package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.d.ts +2 -0
  145. package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.d.ts.map +1 -0
  146. package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.js +28 -0
  147. package/dist/tests/units/interface-adapters/views/dashboard/modules/icons.test.js.map +1 -0
  148. package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.d.ts +2 -0
  149. package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.d.ts.map +1 -0
  150. package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.js +51 -0
  151. package/dist/tests/units/interface-adapters/views/dashboard/modules/loading.test.js.map +1 -0
  152. package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.d.ts +2 -0
  153. package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.d.ts.map +1 -0
  154. package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.js +43 -0
  155. package/dist/tests/units/interface-adapters/views/dashboard/modules/notifications.test.js.map +1 -0
  156. package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.d.ts +2 -0
  157. package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.d.ts.map +1 -0
  158. package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.js +78 -0
  159. package/dist/tests/units/interface-adapters/views/dashboard/modules/priority.test.js.map +1 -0
  160. package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.d.ts +2 -0
  161. package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.d.ts.map +1 -0
  162. package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.js +87 -0
  163. package/dist/tests/units/interface-adapters/views/dashboard/modules/quality.test.js.map +1 -0
  164. package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.d.ts +2 -0
  165. package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.d.ts.map +1 -0
  166. package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.js +29 -0
  167. package/dist/tests/units/interface-adapters/views/dashboard/modules/queueLanes.test.js.map +1 -0
  168. package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.d.ts +2 -0
  169. package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.d.ts.map +1 -0
  170. package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.js +60 -0
  171. package/dist/tests/units/interface-adapters/views/dashboard/modules/sessionMetrics.test.js.map +1 -0
  172. package/dist/tests/units/main/executeDiscover.test.d.ts +2 -0
  173. package/dist/tests/units/main/executeDiscover.test.d.ts.map +1 -0
  174. package/dist/tests/units/main/executeDiscover.test.js +106 -0
  175. package/dist/tests/units/main/executeDiscover.test.js.map +1 -0
  176. package/dist/tests/units/main/executeInit.test.d.ts +2 -0
  177. package/dist/tests/units/main/executeInit.test.d.ts.map +1 -0
  178. package/dist/tests/units/main/executeInit.test.js +290 -0
  179. package/dist/tests/units/main/executeInit.test.js.map +1 -0
  180. package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.d.ts +2 -0
  181. package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.d.ts.map +1 -0
  182. package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.js +127 -0
  183. package/dist/tests/units/usecases/cli/addRepositoriesToConfig.usecase.test.js.map +1 -0
  184. package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.d.ts +2 -0
  185. package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.d.ts.map +1 -0
  186. package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.js +57 -0
  187. package/dist/tests/units/usecases/cli/checkInitPrerequisites.test.js.map +1 -0
  188. package/dist/usecases/cli/addRepositoriesToConfig.usecase.d.ts +27 -0
  189. package/dist/usecases/cli/addRepositoriesToConfig.usecase.d.ts.map +1 -0
  190. package/dist/usecases/cli/addRepositoriesToConfig.usecase.js +34 -0
  191. package/dist/usecases/cli/addRepositoriesToConfig.usecase.js.map +1 -0
  192. package/dist/usecases/cli/checkInitPrerequisites.d.ts +16 -0
  193. package/dist/usecases/cli/checkInitPrerequisites.d.ts.map +1 -0
  194. package/dist/usecases/cli/checkInitPrerequisites.js +23 -0
  195. package/dist/usecases/cli/checkInitPrerequisites.js.map +1 -0
  196. package/dist/usecases/cli/configureMcp.usecase.d.ts +1 -1
  197. package/dist/usecases/cli/configureMcp.usecase.d.ts.map +1 -1
  198. package/dist/usecases/cli/validateConfig.usecase.d.ts +1 -1
  199. package/dist/usecases/cli/validateConfig.usecase.d.ts.map +1 -1
  200. package/dist/usecases/triggerReview.usecase.d.ts +2 -0
  201. package/dist/usecases/triggerReview.usecase.d.ts.map +1 -1
  202. package/dist/usecases/triggerReview.usecase.js +1 -0
  203. package/dist/usecases/triggerReview.usecase.js.map +1 -1
  204. package/package.json +1 -1
  205. package/templates/SETUP.md +23 -8
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @returns {{ initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }}
3
+ */
4
+ export function createReviewNotificationState(): {
5
+ initialized: boolean;
6
+ activeStatusById: Record<string, {
7
+ status: string;
8
+ jobType: string | null;
9
+ }>;
10
+ seenRecentKeys: string[];
11
+ };
12
+ /**
13
+ * @param {{ initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }} state
14
+ * @param {Record<string, unknown>[]} activeReviews
15
+ * @param {Record<string, unknown>[]} recentReviews
16
+ * @returns {{ nextState: { initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }, notifications: Array<{ kind: string, review: Record<string, unknown> }> }}
17
+ */
18
+ export function collectReviewNotifications(state: {
19
+ initialized: boolean;
20
+ activeStatusById: Record<string, {
21
+ status: string;
22
+ jobType: string | null;
23
+ }>;
24
+ seenRecentKeys: string[];
25
+ }, activeReviews: Record<string, unknown>[], recentReviews: Record<string, unknown>[]): {
26
+ nextState: {
27
+ initialized: boolean;
28
+ activeStatusById: Record<string, {
29
+ status: string;
30
+ jobType: string | null;
31
+ }>;
32
+ seenRecentKeys: string[];
33
+ };
34
+ notifications: Array<{
35
+ kind: string;
36
+ review: Record<string, unknown>;
37
+ }>;
38
+ };
39
+ //# sourceMappingURL=notifications.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/notifications.js"],"names":[],"mappings":"AAEA;;GAEG;AACH,iDAFa;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAQ5I;AAmDD;;;;;GAKG;AACH,kDALW;IAAE,WAAW,EAAE,OAAO,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,iBAChI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,iBACzB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GACvB;IAAE,SAAS,EAAE;QAAE,WAAW,EAAE,OAAO,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,CAAC,CAAC;QAAC,cAAc,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAAC,aAAa,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAA;CAAE,CAgEpO"}
@@ -0,0 +1,131 @@
1
+ const MAX_RECENT_KEYS = 500;
2
+
3
+ /**
4
+ * @returns {{ initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }}
5
+ */
6
+ export function createReviewNotificationState() {
7
+ return {
8
+ initialized: false,
9
+ activeStatusById: {},
10
+ seenRecentKeys: [],
11
+ };
12
+ }
13
+
14
+ /**
15
+ * @param {Record<string, unknown>} review
16
+ * @returns {string}
17
+ */
18
+ function getReviewIdentifier(review) {
19
+ if (typeof review.id === 'string' && review.id.length > 0) return review.id;
20
+ if (typeof review.filename === 'string' && review.filename.length > 0) return review.filename;
21
+ if (typeof review.mrNumber === 'number') return String(review.mrNumber);
22
+ return '';
23
+ }
24
+
25
+ /**
26
+ * @param {Record<string, unknown>} review
27
+ * @returns {string}
28
+ */
29
+ function getRecentReviewKey(review) {
30
+ const identifier = getReviewIdentifier(review);
31
+ const timeMarker = typeof review.completedAt === 'string'
32
+ ? review.completedAt
33
+ : typeof review.date === 'string'
34
+ ? review.date
35
+ : typeof review.timestamp === 'string'
36
+ ? review.timestamp
37
+ : '';
38
+ const typeMarker = typeof review.type === 'string'
39
+ ? review.type
40
+ : typeof review.jobType === 'string'
41
+ ? review.jobType
42
+ : '';
43
+ return `${identifier}::${timeMarker}::${typeMarker}`;
44
+ }
45
+
46
+ /**
47
+ * @param {Record<string, unknown>[]} activeReviews
48
+ * @returns {Record<string, { status: string, jobType: string | null }>}
49
+ */
50
+ function buildActiveStatusMap(activeReviews) {
51
+ const activeStatusById = {};
52
+ activeReviews.forEach((review) => {
53
+ const identifier = getReviewIdentifier(review);
54
+ if (!identifier) return;
55
+ activeStatusById[identifier] = {
56
+ status: typeof review.status === 'string' ? review.status : '',
57
+ jobType: typeof review.jobType === 'string' ? review.jobType : null,
58
+ };
59
+ });
60
+ return activeStatusById;
61
+ }
62
+
63
+ /**
64
+ * @param {{ initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }} state
65
+ * @param {Record<string, unknown>[]} activeReviews
66
+ * @param {Record<string, unknown>[]} recentReviews
67
+ * @returns {{ nextState: { initialized: boolean, activeStatusById: Record<string, { status: string, jobType: string | null }>, seenRecentKeys: string[] }, notifications: Array<{ kind: string, review: Record<string, unknown> }> }}
68
+ */
69
+ export function collectReviewNotifications(state, activeReviews, recentReviews) {
70
+ const nextActiveStatusById = buildActiveStatusMap(activeReviews);
71
+ const nextSeenRecentKeysSet = new Set(state.seenRecentKeys);
72
+ const notifications = [];
73
+
74
+ if (!state.initialized) {
75
+ recentReviews.forEach((review) => nextSeenRecentKeysSet.add(getRecentReviewKey(review)));
76
+ return {
77
+ nextState: {
78
+ initialized: true,
79
+ activeStatusById: nextActiveStatusById,
80
+ seenRecentKeys: Array.from(nextSeenRecentKeysSet).slice(-MAX_RECENT_KEYS),
81
+ },
82
+ notifications,
83
+ };
84
+ }
85
+
86
+ activeReviews.forEach((review) => {
87
+ const identifier = getReviewIdentifier(review);
88
+ if (!identifier) return;
89
+ const previousStatus = state.activeStatusById[identifier]?.status ?? '';
90
+ const currentStatus = typeof review.status === 'string' ? review.status : '';
91
+ const isRunningTransition = currentStatus === 'running' && previousStatus !== 'running';
92
+ if (!isRunningTransition) return;
93
+
94
+ const jobType = typeof review.jobType === 'string' ? review.jobType : '';
95
+ notifications.push({
96
+ kind: jobType === 'followup' ? 'followupStarted' : 'reviewStarted',
97
+ review,
98
+ });
99
+ });
100
+
101
+ recentReviews.forEach((review) => {
102
+ const reviewKey = getRecentReviewKey(review);
103
+ if (nextSeenRecentKeysSet.has(reviewKey)) return;
104
+ nextSeenRecentKeysSet.add(reviewKey);
105
+
106
+ const status = typeof review.status === 'string' ? review.status : '';
107
+ if (status === 'failed') {
108
+ notifications.push({ kind: 'reviewFailed', review });
109
+ return;
110
+ }
111
+
112
+ const reviewType = typeof review.type === 'string'
113
+ ? review.type
114
+ : typeof review.jobType === 'string'
115
+ ? review.jobType
116
+ : '';
117
+ notifications.push({
118
+ kind: reviewType === 'followup' ? 'followupCompleted' : 'reviewCompleted',
119
+ review,
120
+ });
121
+ });
122
+
123
+ return {
124
+ nextState: {
125
+ initialized: true,
126
+ activeStatusById: nextActiveStatusById,
127
+ seenRecentKeys: Array.from(nextSeenRecentKeysSet).slice(-MAX_RECENT_KEYS),
128
+ },
129
+ notifications,
130
+ };
131
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/notifications.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @param {Array<Record<string, unknown>>} pendingFix
3
+ * @param {{ nowIsoDate?: string, qualityTarget?: number }} [options]
4
+ * @returns {Array<Record<string, unknown>>}
5
+ */
6
+ export function rankPendingFixForNowLane(pendingFix: Array<Record<string, unknown>>, options?: {
7
+ nowIsoDate?: string;
8
+ qualityTarget?: number;
9
+ }): Array<Record<string, unknown>>;
10
+ //# sourceMappingURL=priority.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"priority.d.ts","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/priority.js"],"names":[],"mappings":"AAiDA;;;;GAIG;AACH,qDAJW,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,YAC9B;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAiC1C"}
@@ -0,0 +1,86 @@
1
+ import { QUALITY_TARGET_SCORE } from './constants.js';
2
+
3
+ const HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
4
+
5
+ /**
6
+ * @param {unknown} value
7
+ * @returns {number | null}
8
+ */
9
+ function toNullableNumber(value) {
10
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
11
+ }
12
+
13
+ /**
14
+ * @param {Record<string, unknown>} mergeRequest
15
+ * @returns {number}
16
+ */
17
+ function getReferenceTimestamp(mergeRequest) {
18
+ const preferredDate = typeof mergeRequest.lastReviewAt === 'string'
19
+ ? mergeRequest.lastReviewAt
20
+ : typeof mergeRequest.createdAt === 'string'
21
+ ? mergeRequest.createdAt
22
+ : '';
23
+
24
+ const parsedTimestamp = preferredDate ? Date.parse(preferredDate) : Number.NaN;
25
+ return Number.isFinite(parsedTimestamp) ? parsedTimestamp : Number.MAX_SAFE_INTEGER;
26
+ }
27
+
28
+ /**
29
+ * @param {Record<string, unknown>} mergeRequest
30
+ * @param {number} qualityTarget
31
+ * @param {number} nowTimestamp
32
+ * @returns {number}
33
+ */
34
+ function computeUrgencyScore(mergeRequest, qualityTarget, nowTimestamp) {
35
+ const openThreads = toNullableNumber(mergeRequest.openThreads) ?? 0;
36
+ const latestScore = toNullableNumber(mergeRequest.latestScore);
37
+ const referenceTimestamp = getReferenceTimestamp(mergeRequest);
38
+ const ageHours = referenceTimestamp === Number.MAX_SAFE_INTEGER
39
+ ? 0
40
+ : Math.max(0, (nowTimestamp - referenceTimestamp) / HOUR_IN_MILLISECONDS);
41
+
42
+ const threadScore = openThreads * 12;
43
+ const qualityGap = latestScore === null ? 2 : Math.max(0, qualityTarget - latestScore);
44
+ const qualityScore = qualityGap * 6;
45
+ const ageScore = Math.min(12, ageHours * 0.2);
46
+
47
+ return threadScore + qualityScore + ageScore;
48
+ }
49
+
50
+ /**
51
+ * @param {Array<Record<string, unknown>>} pendingFix
52
+ * @param {{ nowIsoDate?: string, qualityTarget?: number }} [options]
53
+ * @returns {Array<Record<string, unknown>>}
54
+ */
55
+ export function rankPendingFixForNowLane(pendingFix, options = {}) {
56
+ const nowTimestamp = options.nowIsoDate ? Date.parse(options.nowIsoDate) : Date.now();
57
+ const normalizedNowTimestamp = Number.isFinite(nowTimestamp) ? nowTimestamp : Date.now();
58
+ const qualityTarget = typeof options.qualityTarget === 'number' && Number.isFinite(options.qualityTarget)
59
+ ? options.qualityTarget
60
+ : QUALITY_TARGET_SCORE;
61
+
62
+ return [...pendingFix].sort((leftMergeRequest, rightMergeRequest) => {
63
+ const leftScore = computeUrgencyScore(leftMergeRequest, qualityTarget, normalizedNowTimestamp);
64
+ const rightScore = computeUrgencyScore(rightMergeRequest, qualityTarget, normalizedNowTimestamp);
65
+
66
+ if (leftScore !== rightScore) {
67
+ return rightScore - leftScore;
68
+ }
69
+
70
+ const leftTimestamp = getReferenceTimestamp(leftMergeRequest);
71
+ const rightTimestamp = getReferenceTimestamp(rightMergeRequest);
72
+ if (leftTimestamp !== rightTimestamp) {
73
+ return leftTimestamp - rightTimestamp;
74
+ }
75
+
76
+ const leftNumber = toNullableNumber(leftMergeRequest.mrNumber) ?? Number.MAX_SAFE_INTEGER;
77
+ const rightNumber = toNullableNumber(rightMergeRequest.mrNumber) ?? Number.MAX_SAFE_INTEGER;
78
+ if (leftNumber !== rightNumber) {
79
+ return leftNumber - rightNumber;
80
+ }
81
+
82
+ const leftId = String(leftMergeRequest.id ?? '');
83
+ const rightId = String(rightMergeRequest.id ?? '');
84
+ return leftId.localeCompare(rightId);
85
+ });
86
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"priority.js","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/priority.js"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @param {number | null} qualityScore
3
+ * @param {number} [qualityTarget]
4
+ * @returns {{
5
+ * qualityScore: number | null,
6
+ * qualityTarget: number,
7
+ * progressPercent: number | null,
8
+ * clampedProgressPercent: number,
9
+ * targetDelta: number | null,
10
+ * targetDeltaLabel: string
11
+ * }}
12
+ */
13
+ export function getQualityProgress(qualityScore: number | null, qualityTarget?: number): {
14
+ qualityScore: number | null;
15
+ qualityTarget: number;
16
+ progressPercent: number | null;
17
+ clampedProgressPercent: number;
18
+ targetDelta: number | null;
19
+ targetDeltaLabel: string;
20
+ };
21
+ /**
22
+ * @param {Record<string, unknown>} mergeRequest
23
+ * @returns {{ direction: 'up' | 'down' | 'flat' | 'unknown', delta: number | null, label: string }}
24
+ */
25
+ export function getQualityTrend(mergeRequest: Record<string, unknown>): {
26
+ direction: "up" | "down" | "flat" | "unknown";
27
+ delta: number | null;
28
+ label: string;
29
+ };
30
+ //# sourceMappingURL=quality.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.d.ts","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/quality.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;GAWG;AACH,iDAXW,MAAM,GAAG,IAAI,kBACb,MAAM,GACJ;IACR,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gBAAgB,EAAE,MAAM,CAAA;CACzB,CA0BH;AAED;;;GAGG;AACH,8CAHW,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACrB;IAAE,SAAS,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAuBlG"}
@@ -0,0 +1,83 @@
1
+ import { QUALITY_TARGET_SCORE } from './constants.js';
2
+
3
+ /**
4
+ * @param {number} value
5
+ * @returns {number}
6
+ */
7
+ function roundOneDecimal(value) {
8
+ return Number(value.toFixed(1));
9
+ }
10
+
11
+ /**
12
+ * @param {number} value
13
+ * @returns {string}
14
+ */
15
+ function formatSignedLabel(value) {
16
+ if (value > 0) return `+${value.toFixed(1)}`;
17
+ return value.toFixed(1);
18
+ }
19
+
20
+ /**
21
+ * @param {number | null} qualityScore
22
+ * @param {number} [qualityTarget]
23
+ * @returns {{
24
+ * qualityScore: number | null,
25
+ * qualityTarget: number,
26
+ * progressPercent: number | null,
27
+ * clampedProgressPercent: number,
28
+ * targetDelta: number | null,
29
+ * targetDeltaLabel: string
30
+ * }}
31
+ */
32
+ export function getQualityProgress(qualityScore, qualityTarget = QUALITY_TARGET_SCORE) {
33
+ if (typeof qualityScore !== 'number' || !Number.isFinite(qualityScore)) {
34
+ return {
35
+ qualityScore: null,
36
+ qualityTarget,
37
+ progressPercent: null,
38
+ clampedProgressPercent: 0,
39
+ targetDelta: null,
40
+ targetDeltaLabel: '',
41
+ };
42
+ }
43
+
44
+ const progressPercent = (qualityScore / qualityTarget) * 100;
45
+ const clampedProgressPercent = Math.max(0, Math.min(100, progressPercent));
46
+ const targetDelta = qualityScore - qualityTarget;
47
+
48
+ return {
49
+ qualityScore,
50
+ qualityTarget,
51
+ progressPercent,
52
+ clampedProgressPercent,
53
+ targetDelta: roundOneDecimal(targetDelta),
54
+ targetDeltaLabel: formatSignedLabel(roundOneDecimal(targetDelta)),
55
+ };
56
+ }
57
+
58
+ /**
59
+ * @param {Record<string, unknown>} mergeRequest
60
+ * @returns {{ direction: 'up' | 'down' | 'flat' | 'unknown', delta: number | null, label: string }}
61
+ */
62
+ export function getQualityTrend(mergeRequest) {
63
+ const reviews = Array.isArray(mergeRequest.reviews) ? mergeRequest.reviews : [];
64
+ const scoredReviews = reviews
65
+ .filter((review) => typeof review.score === 'number' && Number.isFinite(review.score))
66
+ .slice(-2);
67
+
68
+ if (scoredReviews.length < 2) {
69
+ return { direction: 'unknown', delta: null, label: '' };
70
+ }
71
+
72
+ const previousReview = scoredReviews[0];
73
+ const latestReview = scoredReviews[1];
74
+ const delta = roundOneDecimal(latestReview.score - previousReview.score);
75
+
76
+ if (delta > 0) {
77
+ return { direction: 'up', delta, label: formatSignedLabel(delta) };
78
+ }
79
+ if (delta < 0) {
80
+ return { direction: 'down', delta, label: formatSignedLabel(delta) };
81
+ }
82
+ return { direction: 'flat', delta, label: delta.toFixed(1) };
83
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quality.js","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/quality.js"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEtD;;;GAGG;AACH,SAAS,eAAe,CAAC,KAAK;IAC5B,OAAO,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,SAAS,iBAAiB,CAAC,KAAK;IAC9B,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7C,OAAO,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAC1B,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAAC,YAAY,EAAE,aAAa,GAAG,oBAAoB;IACnF,IAAI,OAAO,YAAY,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,EAAE,CAAC;QACvE,OAAO;YACL,YAAY,EAAE,IAAI;YAClB,aAAa;YACb,eAAe,EAAE,IAAI;YACrB,sBAAsB,EAAE,CAAC;YACzB,WAAW,EAAE,IAAI;YACjB,gBAAgB,EAAE,EAAE;SACrB,CAAC;IACJ,CAAC;IAED,MAAM,eAAe,GAAG,CAAC,YAAY,GAAG,aAAa,CAAC,GAAG,GAAG,CAAC;IAC7D,MAAM,sBAAsB,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,eAAe,CAAC,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,YAAY,GAAG,aAAa,CAAC;IAEjD,OAAO;QACL,YAAY;QACZ,aAAa;QACb,eAAe;QACf,sBAAsB;QACtB,WAAW,EAAE,eAAe,CAAC,WAAW,CAAC;QACzC,gBAAgB,EAAE,iBAAiB,CAAC,eAAe,CAAC,WAAW,CAAC,CAAC;KAClE,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,YAAY;IAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAChF,MAAM,aAAa,GAAG,OAAO;SAC1B,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SACrF,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAEb,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;IAC1D,CAAC;IAED,MAAM,cAAc,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IACxC,MAAM,YAAY,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC;IACtC,MAAM,KAAK,GAAG,eAAe,CAAC,YAAY,CAAC,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAEzE,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;IACrE,CAAC;IACD,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;IACvE,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;AAC/D,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @template MergeRequest
3
+ * @param {MergeRequest[]} rankedPendingFix
4
+ * @param {MergeRequest[]} pendingApproval
5
+ * @returns {{
6
+ * nowLaneItem: MergeRequest | null,
7
+ * needsFixItems: MergeRequest[],
8
+ * readyToApproveItems: MergeRequest[],
9
+ * nowLaneCount: number,
10
+ * needsFixCount: number,
11
+ * readyToApproveCount: number
12
+ * }}
13
+ */
14
+ export function buildQueueLanesModel<MergeRequest>(rankedPendingFix: MergeRequest[], pendingApproval: MergeRequest[]): {
15
+ nowLaneItem: MergeRequest | null;
16
+ needsFixItems: MergeRequest[];
17
+ readyToApproveItems: MergeRequest[];
18
+ nowLaneCount: number;
19
+ needsFixCount: number;
20
+ readyToApproveCount: number;
21
+ };
22
+ //# sourceMappingURL=queueLanes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queueLanes.d.ts","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/queueLanes.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,qCAZa,YAAY,oBACd,YAAY,EAAE,mBACd,YAAY,EAAE,GACZ;IACR,WAAW,EAAE,YAAY,GAAG,IAAI,CAAC;IACjC,aAAa,EAAE,YAAY,EAAE,CAAC;IAC9B,mBAAmB,EAAE,YAAY,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,mBAAmB,EAAE,MAAM,CAAA;CAC5B,CAeH"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @template MergeRequest
3
+ * @param {MergeRequest[]} rankedPendingFix
4
+ * @param {MergeRequest[]} pendingApproval
5
+ * @returns {{
6
+ * nowLaneItem: MergeRequest | null,
7
+ * needsFixItems: MergeRequest[],
8
+ * readyToApproveItems: MergeRequest[],
9
+ * nowLaneCount: number,
10
+ * needsFixCount: number,
11
+ * readyToApproveCount: number
12
+ * }}
13
+ */
14
+ export function buildQueueLanesModel(rankedPendingFix, pendingApproval) {
15
+ const nowLaneItem = rankedPendingFix[0] ?? null;
16
+ const needsFixItems = rankedPendingFix.slice(1);
17
+ const readyToApproveItems = [...pendingApproval];
18
+
19
+ return {
20
+ nowLaneItem,
21
+ needsFixItems,
22
+ readyToApproveItems,
23
+ nowLaneCount: nowLaneItem ? 1 : 0,
24
+ needsFixCount: needsFixItems.length,
25
+ readyToApproveCount: readyToApproveItems.length,
26
+ };
27
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queueLanes.js","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/queueLanes.js"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,gBAAgB,EAAE,eAAe;IACpE,MAAM,WAAW,GAAG,gBAAgB,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAChD,MAAM,aAAa,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,mBAAmB,GAAG,CAAC,GAAG,eAAe,CAAC,CAAC;IAEjD,OAAO;QACL,WAAW;QACX,aAAa;QACb,mBAAmB;QACnB,YAAY,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,aAAa,EAAE,aAAa,CAAC,MAAM;QACnC,mBAAmB,EAAE,mBAAmB,CAAC,MAAM;KAChD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @param {number} [nowMs]
3
+ * @returns {{
4
+ * sessionStartedAt: number,
5
+ * firstUsefulActionDelayMs: number | null,
6
+ * actionCount: number,
7
+ * actionBreakdown: Record<string, number>,
8
+ * currentPriorityItemId: string | null,
9
+ * currentPriorityItemStartedAt: number | null,
10
+ * resolvedPriorityDurationsMs: number[],
11
+ * }}
12
+ */
13
+ export function createSessionMetricsState(nowMs?: number): {
14
+ sessionStartedAt: number;
15
+ firstUsefulActionDelayMs: number | null;
16
+ actionCount: number;
17
+ actionBreakdown: Record<string, number>;
18
+ currentPriorityItemId: string | null;
19
+ currentPriorityItemStartedAt: number | null;
20
+ resolvedPriorityDurationsMs: number[];
21
+ };
22
+ /**
23
+ * @param {ReturnType<typeof createSessionMetricsState>} state
24
+ * @param {'followup' | 'open' | 'approve' | 'cancelReview' | 'syncThreads' | 'other' | string} actionType
25
+ * @param {number} [nowMs]
26
+ * @returns {ReturnType<typeof createSessionMetricsState>}
27
+ */
28
+ export function trackSessionAction(state: ReturnType<typeof createSessionMetricsState>, actionType: "followup" | "open" | "approve" | "cancelReview" | "syncThreads" | "other" | string, nowMs?: number): ReturnType<typeof createSessionMetricsState>;
29
+ /**
30
+ * @param {ReturnType<typeof createSessionMetricsState>} state
31
+ * @param {{ nowLaneItemId: string | null, pendingFixIds: string[], nowMs?: number }} input
32
+ * @returns {ReturnType<typeof createSessionMetricsState>}
33
+ */
34
+ export function updatePriorityItemTracking(state: ReturnType<typeof createSessionMetricsState>, input: {
35
+ nowLaneItemId: string | null;
36
+ pendingFixIds: string[];
37
+ nowMs?: number;
38
+ }): ReturnType<typeof createSessionMetricsState>;
39
+ /**
40
+ * @param {ReturnType<typeof createSessionMetricsState>} state
41
+ * @returns {{
42
+ * firstUsefulActionDelayMs: number | null,
43
+ * actionCount: number,
44
+ * actionBreakdown: Record<string, number>,
45
+ * averagePriorityResolutionMs: number | null,
46
+ * }}
47
+ */
48
+ export function getSessionMetricsSnapshot(state: ReturnType<typeof createSessionMetricsState>): {
49
+ firstUsefulActionDelayMs: number | null;
50
+ actionCount: number;
51
+ actionBreakdown: Record<string, number>;
52
+ averagePriorityResolutionMs: number | null;
53
+ };
54
+ //# sourceMappingURL=sessionMetrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionMetrics.d.ts","sourceRoot":"","sources":["../../../../../src/interface-adapters/views/dashboard/modules/sessionMetrics.js"],"names":[],"mappings":"AASA;;;;;;;;;;;GAWG;AACH,kDAXW,MAAM,GACJ;IACR,gBAAgB,EAAE,MAAM,CAAC;IACzB,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,qBAAqB,EAAE,MAAM,GAAG,IAAI,CAAC;IACrC,4BAA4B,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C,2BAA2B,EAAE,MAAM,EAAE,CAAC;CACvC,CAYH;AAED;;;;;GAKG;AACH,0CALW,UAAU,CAAC,OAAO,yBAAyB,CAAC,cAC5C,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,aAAa,GAAG,OAAO,GAAG,MAAM,UACnF,MAAM,GACJ,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAiBxD;AAED;;;;GAIG;AACH,kDAJW,UAAU,CAAC,OAAO,yBAAyB,CAAC,SAC5C;IAAE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAsCxD;AAED;;;;;;;;GAQG;AACH,iDARW,UAAU,CAAC,OAAO,yBAAyB,CAAC,GAC1C;IACR,wBAAwB,EAAE,MAAM,GAAG,IAAI,CAAC;IACxC,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,2BAA2B,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5C,CAaH"}
@@ -0,0 +1,120 @@
1
+ const DEFAULT_ACTION_BREAKDOWN = {
2
+ followup: 0,
3
+ open: 0,
4
+ approve: 0,
5
+ cancelReview: 0,
6
+ syncThreads: 0,
7
+ other: 0,
8
+ };
9
+
10
+ /**
11
+ * @param {number} [nowMs]
12
+ * @returns {{
13
+ * sessionStartedAt: number,
14
+ * firstUsefulActionDelayMs: number | null,
15
+ * actionCount: number,
16
+ * actionBreakdown: Record<string, number>,
17
+ * currentPriorityItemId: string | null,
18
+ * currentPriorityItemStartedAt: number | null,
19
+ * resolvedPriorityDurationsMs: number[],
20
+ * }}
21
+ */
22
+ export function createSessionMetricsState(nowMs = Date.now()) {
23
+ return {
24
+ sessionStartedAt: nowMs,
25
+ firstUsefulActionDelayMs: null,
26
+ actionCount: 0,
27
+ actionBreakdown: { ...DEFAULT_ACTION_BREAKDOWN },
28
+ currentPriorityItemId: null,
29
+ currentPriorityItemStartedAt: null,
30
+ resolvedPriorityDurationsMs: [],
31
+ };
32
+ }
33
+
34
+ /**
35
+ * @param {ReturnType<typeof createSessionMetricsState>} state
36
+ * @param {'followup' | 'open' | 'approve' | 'cancelReview' | 'syncThreads' | 'other' | string} actionType
37
+ * @param {number} [nowMs]
38
+ * @returns {ReturnType<typeof createSessionMetricsState>}
39
+ */
40
+ export function trackSessionAction(state, actionType, nowMs = Date.now()) {
41
+ const normalizedActionType = actionType in state.actionBreakdown ? actionType : 'other';
42
+ const firstUsefulActionDelayMs = state.firstUsefulActionDelayMs === null
43
+ ? Math.max(0, nowMs - state.sessionStartedAt)
44
+ : state.firstUsefulActionDelayMs;
45
+
46
+ return {
47
+ ...state,
48
+ firstUsefulActionDelayMs,
49
+ actionCount: state.actionCount + 1,
50
+ actionBreakdown: {
51
+ ...state.actionBreakdown,
52
+ [normalizedActionType]: state.actionBreakdown[normalizedActionType] + 1,
53
+ },
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {ReturnType<typeof createSessionMetricsState>} state
59
+ * @param {{ nowLaneItemId: string | null, pendingFixIds: string[], nowMs?: number }} input
60
+ * @returns {ReturnType<typeof createSessionMetricsState>}
61
+ */
62
+ export function updatePriorityItemTracking(state, input) {
63
+ const nowMs = input.nowMs ?? Date.now();
64
+ let nextState = { ...state };
65
+
66
+ const trackedItemId = state.currentPriorityItemId;
67
+ const trackedStartAt = state.currentPriorityItemStartedAt;
68
+ const trackedItemResolved = trackedItemId !== null && trackedStartAt !== null && !input.pendingFixIds.includes(trackedItemId);
69
+ if (trackedItemResolved) {
70
+ nextState = {
71
+ ...nextState,
72
+ resolvedPriorityDurationsMs: [
73
+ ...nextState.resolvedPriorityDurationsMs,
74
+ Math.max(0, nowMs - trackedStartAt),
75
+ ],
76
+ currentPriorityItemId: null,
77
+ currentPriorityItemStartedAt: null,
78
+ };
79
+ }
80
+
81
+ if (input.nowLaneItemId === null) {
82
+ return {
83
+ ...nextState,
84
+ currentPriorityItemId: null,
85
+ currentPriorityItemStartedAt: null,
86
+ };
87
+ }
88
+
89
+ if (nextState.currentPriorityItemId !== input.nowLaneItemId) {
90
+ return {
91
+ ...nextState,
92
+ currentPriorityItemId: input.nowLaneItemId,
93
+ currentPriorityItemStartedAt: nowMs,
94
+ };
95
+ }
96
+
97
+ return nextState;
98
+ }
99
+
100
+ /**
101
+ * @param {ReturnType<typeof createSessionMetricsState>} state
102
+ * @returns {{
103
+ * firstUsefulActionDelayMs: number | null,
104
+ * actionCount: number,
105
+ * actionBreakdown: Record<string, number>,
106
+ * averagePriorityResolutionMs: number | null,
107
+ * }}
108
+ */
109
+ export function getSessionMetricsSnapshot(state) {
110
+ const averagePriorityResolutionMs = state.resolvedPriorityDurationsMs.length === 0
111
+ ? null
112
+ : state.resolvedPriorityDurationsMs.reduce((total, duration) => total + duration, 0) / state.resolvedPriorityDurationsMs.length;
113
+
114
+ return {
115
+ firstUsefulActionDelayMs: state.firstUsefulActionDelayMs,
116
+ actionCount: state.actionCount,
117
+ actionBreakdown: { ...state.actionBreakdown },
118
+ averagePriorityResolutionMs,
119
+ };
120
+ }