@a5c-ai/adapters-gateway 5.1.1-staging.00ceebd28cf2

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 (202) hide show
  1. package/README.md +20 -0
  2. package/dist/auth/bootstrap.d.ts +89 -0
  3. package/dist/auth/bootstrap.d.ts.map +1 -0
  4. package/dist/auth/bootstrap.js +222 -0
  5. package/dist/auth/bootstrap.js.map +1 -0
  6. package/dist/auth/hashing.d.ts +4 -0
  7. package/dist/auth/hashing.d.ts.map +1 -0
  8. package/dist/auth/hashing.js +27 -0
  9. package/dist/auth/hashing.js.map +1 -0
  10. package/dist/auth/middleware.d.ts +3 -0
  11. package/dist/auth/middleware.d.ts.map +1 -0
  12. package/dist/auth/middleware.js +17 -0
  13. package/dist/auth/middleware.js.map +1 -0
  14. package/dist/auth/tokens.d.ts +45 -0
  15. package/dist/auth/tokens.d.ts.map +1 -0
  16. package/dist/auth/tokens.js +186 -0
  17. package/dist/auth/tokens.js.map +1 -0
  18. package/dist/builtin-adapters.d.ts +17 -0
  19. package/dist/builtin-adapters.d.ts.map +1 -0
  20. package/dist/builtin-adapters.js +119 -0
  21. package/dist/builtin-adapters.js.map +1 -0
  22. package/dist/config.d.ts +37 -0
  23. package/dist/config.d.ts.map +1 -0
  24. package/dist/config.js +97 -0
  25. package/dist/config.js.map +1 -0
  26. package/dist/fanout/client-conn.d.ts +20 -0
  27. package/dist/fanout/client-conn.d.ts.map +1 -0
  28. package/dist/fanout/client-conn.js +53 -0
  29. package/dist/fanout/client-conn.js.map +1 -0
  30. package/dist/fanout/subscriber.d.ts +12 -0
  31. package/dist/fanout/subscriber.d.ts.map +1 -0
  32. package/dist/fanout/subscriber.js +40 -0
  33. package/dist/fanout/subscriber.js.map +1 -0
  34. package/dist/index.d.ts +30 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +52 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/kanban/lib/config-loader.d.ts +29 -0
  39. package/dist/kanban/lib/config-loader.d.ts.map +1 -0
  40. package/dist/kanban/lib/config-loader.js +166 -0
  41. package/dist/kanban/lib/config-loader.js.map +1 -0
  42. package/dist/kanban/lib/config.d.ts +3 -0
  43. package/dist/kanban/lib/config.d.ts.map +1 -0
  44. package/dist/kanban/lib/config.js +6 -0
  45. package/dist/kanban/lib/config.js.map +1 -0
  46. package/dist/kanban/lib/create-global-registry.d.ts +28 -0
  47. package/dist/kanban/lib/create-global-registry.d.ts.map +1 -0
  48. package/dist/kanban/lib/create-global-registry.js +53 -0
  49. package/dist/kanban/lib/create-global-registry.js.map +1 -0
  50. package/dist/kanban/lib/dispatch-context-audit.d.ts +12 -0
  51. package/dist/kanban/lib/dispatch-context-audit.d.ts.map +1 -0
  52. package/dist/kanban/lib/dispatch-context-audit.js +44 -0
  53. package/dist/kanban/lib/dispatch-context-audit.js.map +1 -0
  54. package/dist/kanban/lib/error-handler.d.ts +28 -0
  55. package/dist/kanban/lib/error-handler.d.ts.map +1 -0
  56. package/dist/kanban/lib/error-handler.js +61 -0
  57. package/dist/kanban/lib/error-handler.js.map +1 -0
  58. package/dist/kanban/lib/global-registry.d.ts +49 -0
  59. package/dist/kanban/lib/global-registry.d.ts.map +1 -0
  60. package/dist/kanban/lib/global-registry.js +18 -0
  61. package/dist/kanban/lib/global-registry.js.map +1 -0
  62. package/dist/kanban/lib/parser.d.ts +36 -0
  63. package/dist/kanban/lib/parser.d.ts.map +1 -0
  64. package/dist/kanban/lib/parser.js +585 -0
  65. package/dist/kanban/lib/parser.js.map +1 -0
  66. package/dist/kanban/lib/path-resolver.d.ts +2 -0
  67. package/dist/kanban/lib/path-resolver.d.ts.map +1 -0
  68. package/dist/kanban/lib/path-resolver.js +16 -0
  69. package/dist/kanban/lib/path-resolver.js.map +1 -0
  70. package/dist/kanban/lib/review-service.d.ts +63 -0
  71. package/dist/kanban/lib/review-service.d.ts.map +1 -0
  72. package/dist/kanban/lib/review-service.js +571 -0
  73. package/dist/kanban/lib/review-service.js.map +1 -0
  74. package/dist/kanban/lib/run-cache.d.ts +36 -0
  75. package/dist/kanban/lib/run-cache.d.ts.map +1 -0
  76. package/dist/kanban/lib/run-cache.js +313 -0
  77. package/dist/kanban/lib/run-cache.js.map +1 -0
  78. package/dist/kanban/lib/server-init.d.ts +26 -0
  79. package/dist/kanban/lib/server-init.d.ts.map +1 -0
  80. package/dist/kanban/lib/server-init.js +179 -0
  81. package/dist/kanban/lib/server-init.js.map +1 -0
  82. package/dist/kanban/lib/services/automation-rule-service.d.ts +97 -0
  83. package/dist/kanban/lib/services/automation-rule-service.d.ts.map +1 -0
  84. package/dist/kanban/lib/services/automation-rule-service.js +806 -0
  85. package/dist/kanban/lib/services/automation-rule-service.js.map +1 -0
  86. package/dist/kanban/lib/services/automation-webhook-service.d.ts +44 -0
  87. package/dist/kanban/lib/services/automation-webhook-service.d.ts.map +1 -0
  88. package/dist/kanban/lib/services/automation-webhook-service.js +405 -0
  89. package/dist/kanban/lib/services/automation-webhook-service.js.map +1 -0
  90. package/dist/kanban/lib/services/backlog-query-service.d.ts +130 -0
  91. package/dist/kanban/lib/services/backlog-query-service.d.ts.map +1 -0
  92. package/dist/kanban/lib/services/backlog-query-service.js +1972 -0
  93. package/dist/kanban/lib/services/backlog-query-service.js.map +1 -0
  94. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts +39 -0
  95. package/dist/kanban/lib/services/dispatch-context-label-service.d.ts.map +1 -0
  96. package/dist/kanban/lib/services/dispatch-context-label-service.js +160 -0
  97. package/dist/kanban/lib/services/dispatch-context-label-service.js.map +1 -0
  98. package/dist/kanban/lib/services/kanban-storage.d.ts +36 -0
  99. package/dist/kanban/lib/services/kanban-storage.d.ts.map +1 -0
  100. package/dist/kanban/lib/services/kanban-storage.js +26 -0
  101. package/dist/kanban/lib/services/kanban-storage.js.map +1 -0
  102. package/dist/kanban/lib/services/run-query-service.d.ts +79 -0
  103. package/dist/kanban/lib/services/run-query-service.d.ts.map +1 -0
  104. package/dist/kanban/lib/services/run-query-service.js +202 -0
  105. package/dist/kanban/lib/services/run-query-service.js.map +1 -0
  106. package/dist/kanban/lib/services/task-tag-service.d.ts +39 -0
  107. package/dist/kanban/lib/services/task-tag-service.d.ts.map +1 -0
  108. package/dist/kanban/lib/services/task-tag-service.js +145 -0
  109. package/dist/kanban/lib/services/task-tag-service.js.map +1 -0
  110. package/dist/kanban/lib/settings-section-storage.d.ts +13 -0
  111. package/dist/kanban/lib/settings-section-storage.d.ts.map +1 -0
  112. package/dist/kanban/lib/settings-section-storage.js +38 -0
  113. package/dist/kanban/lib/settings-section-storage.js.map +1 -0
  114. package/dist/kanban/lib/source-discovery.d.ts +10 -0
  115. package/dist/kanban/lib/source-discovery.d.ts.map +1 -0
  116. package/dist/kanban/lib/source-discovery.js +201 -0
  117. package/dist/kanban/lib/source-discovery.js.map +1 -0
  118. package/dist/kanban/lib/utils.d.ts +8 -0
  119. package/dist/kanban/lib/utils.d.ts.map +1 -0
  120. package/dist/kanban/lib/utils.js +116 -0
  121. package/dist/kanban/lib/utils.js.map +1 -0
  122. package/dist/kanban/lib/watcher.d.ts +14 -0
  123. package/dist/kanban/lib/watcher.d.ts.map +1 -0
  124. package/dist/kanban/lib/watcher.js +221 -0
  125. package/dist/kanban/lib/watcher.js.map +1 -0
  126. package/dist/kanban/lib/workspace-lifecycle.d.ts +68 -0
  127. package/dist/kanban/lib/workspace-lifecycle.d.ts.map +1 -0
  128. package/dist/kanban/lib/workspace-lifecycle.js +1085 -0
  129. package/dist/kanban/lib/workspace-lifecycle.js.map +1 -0
  130. package/dist/kanban/routes.d.ts +2 -0
  131. package/dist/kanban/routes.d.ts.map +1 -0
  132. package/dist/kanban/routes.js +1358 -0
  133. package/dist/kanban/routes.js.map +1 -0
  134. package/dist/kanban/types/breakpoint.d.ts +13 -0
  135. package/dist/kanban/types/breakpoint.d.ts.map +1 -0
  136. package/dist/kanban/types/breakpoint.js +3 -0
  137. package/dist/kanban/types/breakpoint.js.map +1 -0
  138. package/dist/kanban/types/index.d.ts +173 -0
  139. package/dist/kanban/types/index.d.ts.map +1 -0
  140. package/dist/kanban/types/index.js +3 -0
  141. package/dist/kanban/types/index.js.map +1 -0
  142. package/dist/logging.d.ts +7 -0
  143. package/dist/logging.d.ts.map +1 -0
  144. package/dist/logging.js +22 -0
  145. package/dist/logging.js.map +1 -0
  146. package/dist/notifications/types.d.ts +18 -0
  147. package/dist/notifications/types.d.ts.map +1 -0
  148. package/dist/notifications/types.js +2 -0
  149. package/dist/notifications/types.js.map +1 -0
  150. package/dist/notifications/webhook-out.d.ts +3 -0
  151. package/dist/notifications/webhook-out.d.ts.map +1 -0
  152. package/dist/notifications/webhook-out.js +55 -0
  153. package/dist/notifications/webhook-out.js.map +1 -0
  154. package/dist/pairing/short-code.d.ts +20 -0
  155. package/dist/pairing/short-code.d.ts.map +1 -0
  156. package/dist/pairing/short-code.js +50 -0
  157. package/dist/pairing/short-code.js.map +1 -0
  158. package/dist/protocol/errors.d.ts +10 -0
  159. package/dist/protocol/errors.d.ts.map +1 -0
  160. package/dist/protocol/errors.js +15 -0
  161. package/dist/protocol/errors.js.map +1 -0
  162. package/dist/protocol/frames.d.ts +107 -0
  163. package/dist/protocol/frames.d.ts.map +1 -0
  164. package/dist/protocol/frames.js +146 -0
  165. package/dist/protocol/frames.js.map +1 -0
  166. package/dist/protocol/v1.d.ts +111 -0
  167. package/dist/protocol/v1.d.ts.map +1 -0
  168. package/dist/protocol/v1.js +2 -0
  169. package/dist/protocol/v1.js.map +1 -0
  170. package/dist/runs/event-log-index.d.ts +29 -0
  171. package/dist/runs/event-log-index.d.ts.map +1 -0
  172. package/dist/runs/event-log-index.js +210 -0
  173. package/dist/runs/event-log-index.js.map +1 -0
  174. package/dist/runs/event-log.d.ts +25 -0
  175. package/dist/runs/event-log.d.ts.map +1 -0
  176. package/dist/runs/event-log.js +104 -0
  177. package/dist/runs/event-log.js.map +1 -0
  178. package/dist/runs/hook-broker.d.ts +18 -0
  179. package/dist/runs/hook-broker.d.ts.map +1 -0
  180. package/dist/runs/hook-broker.js +110 -0
  181. package/dist/runs/hook-broker.js.map +1 -0
  182. package/dist/runs/manager.d.ts +57 -0
  183. package/dist/runs/manager.d.ts.map +1 -0
  184. package/dist/runs/manager.js +753 -0
  185. package/dist/runs/manager.js.map +1 -0
  186. package/dist/runs/session-runtime.d.ts +8 -0
  187. package/dist/runs/session-runtime.d.ts.map +1 -0
  188. package/dist/runs/session-runtime.js +291 -0
  189. package/dist/runs/session-runtime.js.map +1 -0
  190. package/dist/runs/types.d.ts +55 -0
  191. package/dist/runs/types.d.ts.map +1 -0
  192. package/dist/runs/types.js +2 -0
  193. package/dist/runs/types.js.map +1 -0
  194. package/dist/server.d.ts +15 -0
  195. package/dist/server.d.ts.map +1 -0
  196. package/dist/server.js +702 -0
  197. package/dist/server.js.map +1 -0
  198. package/dist/static/webui-server.d.ts +2 -0
  199. package/dist/static/webui-server.d.ts.map +1 -0
  200. package/dist/static/webui-server.js +97 -0
  201. package/dist/static/webui-server.js.map +1 -0
  202. package/package.json +68 -0
@@ -0,0 +1,806 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { AppError } from '../error-handler.js';
3
+ import { BacklogQueryService } from './backlog-query-service.js';
4
+ import { KANBAN_BACKLOG_FILE_PATH, defaultKanbanStorageDeps, readKanbanStorageFile, writeKanbanStorageFile, } from './kanban-storage.js';
5
+ const createId = () => randomUUID();
6
+ export const AUTOMATION_RULE_STATES = [
7
+ 'draft',
8
+ 'active',
9
+ 'paused',
10
+ 'disabled',
11
+ 'archived',
12
+ ];
13
+ export const AUTOMATION_TRIGGER_TYPES = ['timer', 'webhook'];
14
+ const MUTABLE_RULE_STATES = new Set([
15
+ 'draft',
16
+ 'active',
17
+ 'paused',
18
+ 'disabled',
19
+ ]);
20
+ const AUTOMATION_SOURCE_KINDS = new Set(['manual', 'config-file', 'api', 'external-system']);
21
+ const KANBAN_PRIORITIES = new Set(['critical', 'high', 'medium', 'low']);
22
+ const TEMPLATE_STATUSES = new Set(['backlog', 'ready']);
23
+ const ISSUE_SOURCE_KINDS = new Set(['seed', 'file', 'run-derived']);
24
+ const DECOMPOSITION_KINDS = new Set([
25
+ 'research',
26
+ 'implementation',
27
+ 'validation',
28
+ 'coordination',
29
+ ]);
30
+ const DECOMPOSITION_STATUSES = new Set(['todo', 'ready', 'done']);
31
+ const defaultDeps = {
32
+ ...defaultKanbanStorageDeps,
33
+ backlogQueryService: new BacklogQueryService(),
34
+ backlogFilePath: KANBAN_BACKLOG_FILE_PATH,
35
+ now: () => new Date().toISOString(),
36
+ };
37
+ function isRecord(value) {
38
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
39
+ }
40
+ function readRequiredString(value, fieldName) {
41
+ if (typeof value !== 'string' || !value.trim()) {
42
+ throw new AppError(`${fieldName} is required.`, 'BAD_REQUEST', 400);
43
+ }
44
+ return value.trim();
45
+ }
46
+ function readOptionalString(value, fieldName) {
47
+ if (value === undefined) {
48
+ return undefined;
49
+ }
50
+ if (typeof value !== 'string' || !value.trim()) {
51
+ throw new AppError(`${fieldName} must be a non-empty string.`, 'BAD_REQUEST', 400);
52
+ }
53
+ return value.trim();
54
+ }
55
+ function readOptionalStringArray(value, fieldName) {
56
+ if (value === undefined) {
57
+ return undefined;
58
+ }
59
+ if (!Array.isArray(value)) {
60
+ throw new AppError(`${fieldName} must be an array of strings.`, 'BAD_REQUEST', 400);
61
+ }
62
+ return value.map((entry, index) => readRequiredString(entry, `${fieldName}[${index}]`));
63
+ }
64
+ function readOptionalMetadata(value, fieldName) {
65
+ if (value === undefined) {
66
+ return undefined;
67
+ }
68
+ if (!isRecord(value)) {
69
+ throw new AppError(`${fieldName} must be an object.`, 'BAD_REQUEST', 400);
70
+ }
71
+ return value;
72
+ }
73
+ function readState(value, fieldName) {
74
+ const state = readRequiredString(value, fieldName);
75
+ if (!isAutomationRuleState(state)) {
76
+ throw new AppError(`${fieldName} is invalid.`, 'BAD_REQUEST', 400);
77
+ }
78
+ return state;
79
+ }
80
+ export function isAutomationRuleState(value) {
81
+ return AUTOMATION_RULE_STATES.includes(value);
82
+ }
83
+ export function isAutomationTriggerType(value) {
84
+ return AUTOMATION_TRIGGER_TYPES.includes(value);
85
+ }
86
+ function readTrigger(value) {
87
+ if (!isRecord(value)) {
88
+ throw new AppError('trigger is required.', 'BAD_REQUEST', 400);
89
+ }
90
+ if (value.type === 'timer') {
91
+ return {
92
+ type: 'timer',
93
+ cron: readRequiredString(value.cron, 'trigger.cron'),
94
+ timezone: readOptionalString(value.timezone, 'trigger.timezone'),
95
+ };
96
+ }
97
+ if (value.type === 'webhook') {
98
+ if (typeof value.port !== 'number' || !Number.isInteger(value.port) || value.port <= 0) {
99
+ throw new AppError('trigger.port must be a positive integer.', 'BAD_REQUEST', 400);
100
+ }
101
+ let auth;
102
+ if (value.auth !== undefined) {
103
+ if (!isRecord(value.auth) || typeof value.auth.type !== 'string') {
104
+ throw new AppError('trigger.auth is invalid.', 'BAD_REQUEST', 400);
105
+ }
106
+ if (value.auth.type === 'none') {
107
+ auth = { type: 'none' };
108
+ }
109
+ else if (value.auth.type === 'bearer') {
110
+ auth = { type: 'bearer', token: readRequiredString(value.auth.token, 'trigger.auth.token') };
111
+ }
112
+ else {
113
+ throw new AppError('trigger.auth.type is invalid.', 'BAD_REQUEST', 400);
114
+ }
115
+ }
116
+ if (value.method !== undefined && value.method !== 'POST') {
117
+ throw new AppError('trigger.method must be POST when provided.', 'BAD_REQUEST', 400);
118
+ }
119
+ return {
120
+ type: 'webhook',
121
+ port: value.port,
122
+ path: readOptionalString(value.path, 'trigger.path'),
123
+ method: value.method,
124
+ auth,
125
+ sourceEvent: readOptionalString(value.sourceEvent, 'trigger.sourceEvent'),
126
+ };
127
+ }
128
+ throw new AppError('trigger.type must be timer or webhook.', 'BAD_REQUEST', 400);
129
+ }
130
+ function readTarget(value) {
131
+ if (!isRecord(value)) {
132
+ throw new AppError('target is required.', 'BAD_REQUEST', 400);
133
+ }
134
+ return {
135
+ projectId: readRequiredString(value.projectId, 'target.projectId'),
136
+ boardProjectId: readRequiredString(value.boardProjectId, 'target.boardProjectId'),
137
+ };
138
+ }
139
+ function readIssueRoute(value) {
140
+ if (!isRecord(value) || value.action !== 'canonical-issue-create') {
141
+ throw new AppError('routing.issue.action must be canonical-issue-create.', 'BAD_REQUEST', 400);
142
+ }
143
+ return {
144
+ action: 'canonical-issue-create',
145
+ projectId: readRequiredString(value.projectId, 'routing.issue.projectId'),
146
+ };
147
+ }
148
+ function readBoardRoute(value) {
149
+ if (!isRecord(value) || value.action !== 'shared-board-derive') {
150
+ throw new AppError('routing.board.action must be shared-board-derive.', 'BAD_REQUEST', 400);
151
+ }
152
+ return {
153
+ action: 'shared-board-derive',
154
+ boardProjectId: readRequiredString(value.boardProjectId, 'routing.board.boardProjectId'),
155
+ };
156
+ }
157
+ function readRouting(value) {
158
+ if (!isRecord(value)) {
159
+ throw new AppError('routing is required.', 'BAD_REQUEST', 400);
160
+ }
161
+ if (value.mutateBoardDirectly !== false) {
162
+ throw new AppError('routing.mutateBoardDirectly must be false.', 'BAD_REQUEST', 400);
163
+ }
164
+ return {
165
+ issue: readIssueRoute(value.issue),
166
+ board: readBoardRoute(value.board),
167
+ mutateBoardDirectly: false,
168
+ };
169
+ }
170
+ function readTemplate(value) {
171
+ if (!isRecord(value)) {
172
+ throw new AppError('template is required.', 'BAD_REQUEST', 400);
173
+ }
174
+ const status = value.status === undefined ? undefined : readRequiredString(value.status, 'template.status');
175
+ if (status !== undefined && !TEMPLATE_STATUSES.has(status)) {
176
+ throw new AppError('template.status must be backlog or ready.', 'BAD_REQUEST', 400);
177
+ }
178
+ const priority = value.priority === undefined ? undefined : readRequiredString(value.priority, 'template.priority');
179
+ if (priority !== undefined && !KANBAN_PRIORITIES.has(priority)) {
180
+ throw new AppError('template.priority is invalid.', 'BAD_REQUEST', 400);
181
+ }
182
+ const issueSource = value.issueSource;
183
+ let normalizedIssueSource;
184
+ if (issueSource !== undefined) {
185
+ if (!isRecord(issueSource)) {
186
+ throw new AppError('template.issueSource is invalid.', 'BAD_REQUEST', 400);
187
+ }
188
+ const kind = readRequiredString(issueSource.kind, 'template.issueSource.kind');
189
+ if (!ISSUE_SOURCE_KINDS.has(kind)) {
190
+ throw new AppError('template.issueSource.kind is invalid.', 'BAD_REQUEST', 400);
191
+ }
192
+ normalizedIssueSource = {
193
+ kind: kind,
194
+ path: readOptionalString(issueSource.path, 'template.issueSource.path'),
195
+ externalId: readOptionalString(issueSource.externalId, 'template.issueSource.externalId'),
196
+ metadata: readOptionalMetadata(issueSource.metadata, 'template.issueSource.metadata'),
197
+ };
198
+ }
199
+ let decomposition;
200
+ if (value.decomposition !== undefined) {
201
+ if (!Array.isArray(value.decomposition)) {
202
+ throw new AppError('template.decomposition must be an array.', 'BAD_REQUEST', 400);
203
+ }
204
+ decomposition = value.decomposition.map((entry, index) => {
205
+ if (!isRecord(entry)) {
206
+ throw new AppError(`template.decomposition[${index}] is invalid.`, 'BAD_REQUEST', 400);
207
+ }
208
+ const kind = readRequiredString(entry.kind, `template.decomposition[${index}].kind`);
209
+ const itemStatus = readRequiredString(entry.status, `template.decomposition[${index}].status`);
210
+ if (!DECOMPOSITION_KINDS.has(kind)) {
211
+ throw new AppError(`template.decomposition[${index}].kind is invalid.`, 'BAD_REQUEST', 400);
212
+ }
213
+ if (!DECOMPOSITION_STATUSES.has(itemStatus)) {
214
+ throw new AppError(`template.decomposition[${index}].status is invalid.`, 'BAD_REQUEST', 400);
215
+ }
216
+ return {
217
+ title: readRequiredString(entry.title, `template.decomposition[${index}].title`),
218
+ kind: kind,
219
+ status: itemStatus,
220
+ };
221
+ });
222
+ }
223
+ return {
224
+ title: readRequiredString(value.title, 'template.title'),
225
+ summary: readOptionalString(value.summary, 'template.summary'),
226
+ description: readOptionalString(value.description, 'template.description'),
227
+ status: status,
228
+ priority: priority,
229
+ labelIds: readOptionalStringArray(value.labelIds, 'template.labelIds'),
230
+ assigneeIds: readOptionalStringArray(value.assigneeIds, 'template.assigneeIds'),
231
+ acceptanceCriteria: readOptionalStringArray(value.acceptanceCriteria, 'template.acceptanceCriteria'),
232
+ decomposition,
233
+ issueSource: normalizedIssueSource,
234
+ metadata: readOptionalMetadata(value.metadata, 'template.metadata'),
235
+ };
236
+ }
237
+ function readSource(value, fallbackKind = 'api') {
238
+ if (value === undefined) {
239
+ return { kind: fallbackKind };
240
+ }
241
+ if (!isRecord(value)) {
242
+ throw new AppError('source must be an object.', 'BAD_REQUEST', 400);
243
+ }
244
+ const kind = readRequiredString(value.kind, 'source.kind');
245
+ if (!AUTOMATION_SOURCE_KINDS.has(kind)) {
246
+ throw new AppError('source.kind is invalid.', 'BAD_REQUEST', 400);
247
+ }
248
+ const metadata = value.metadata;
249
+ if (metadata !== undefined && !isRecord(metadata)) {
250
+ throw new AppError('source.metadata must be an object.', 'BAD_REQUEST', 400);
251
+ }
252
+ return {
253
+ kind: kind,
254
+ path: readOptionalString(value.path, 'source.path'),
255
+ provider: readOptionalString(value.provider, 'source.provider'),
256
+ externalId: readOptionalString(value.externalId, 'source.externalId'),
257
+ metadata: metadata,
258
+ };
259
+ }
260
+ function readTriggerEvent(value) {
261
+ if (!isRecord(value)) {
262
+ throw new AppError('triggerEvent is required.', 'BAD_REQUEST', 400);
263
+ }
264
+ return {
265
+ id: readRequiredString(value.id, 'triggerEvent.id'),
266
+ summary: readOptionalString(value.summary, 'triggerEvent.summary'),
267
+ sourceEvent: readOptionalString(value.sourceEvent, 'triggerEvent.sourceEvent'),
268
+ receivedAt: readOptionalString(value.receivedAt, 'triggerEvent.receivedAt'),
269
+ payload: readOptionalMetadata(value.payload, 'triggerEvent.payload'),
270
+ metadata: readOptionalMetadata(value.metadata, 'triggerEvent.metadata'),
271
+ };
272
+ }
273
+ function buildAutomationRule(input) {
274
+ if (input.trigger.type === 'timer') {
275
+ return {
276
+ ...input,
277
+ trigger: input.trigger,
278
+ };
279
+ }
280
+ return {
281
+ ...input,
282
+ trigger: input.trigger,
283
+ };
284
+ }
285
+ function readAllowedActions(state) {
286
+ switch (state) {
287
+ case 'draft':
288
+ return ['enable', 'disable', 'delete'];
289
+ case 'active':
290
+ return ['pause', 'disable', 'delete'];
291
+ case 'paused':
292
+ return ['resume', 'disable', 'delete'];
293
+ case 'disabled':
294
+ return ['enable', 'delete'];
295
+ case 'archived':
296
+ return ['delete'];
297
+ }
298
+ return ['delete'];
299
+ }
300
+ function summarizeRules(allRules, visibleRules, executionIndex) {
301
+ const stateCounts = {
302
+ draft: 0,
303
+ active: 0,
304
+ paused: 0,
305
+ disabled: 0,
306
+ archived: 0,
307
+ };
308
+ const triggerCounts = {
309
+ timer: 0,
310
+ webhook: 0,
311
+ };
312
+ for (const rule of allRules) {
313
+ stateCounts[rule.state] += 1;
314
+ triggerCounts[rule.trigger.type] += 1;
315
+ }
316
+ let executionCount = 0;
317
+ let failureCount = 0;
318
+ let failingCount = 0;
319
+ for (const rule of allRules) {
320
+ const summary = summarizeExecutions(rule, executionIndex.get(rule.id) ?? []);
321
+ executionCount += summary.totalCount;
322
+ failureCount += summary.rejectedCount;
323
+ if (summary.isFailing) {
324
+ failingCount += 1;
325
+ }
326
+ }
327
+ return {
328
+ totalCount: allRules.length,
329
+ visibleCount: visibleRules.length,
330
+ stateCounts,
331
+ triggerCounts,
332
+ executionCount,
333
+ failureCount,
334
+ failingCount,
335
+ };
336
+ }
337
+ function ruleMatchesQuery(rule, query) {
338
+ if (!query.includeArchived && rule.state === 'archived') {
339
+ return false;
340
+ }
341
+ if (query.state?.length && !query.state.includes(rule.state)) {
342
+ return false;
343
+ }
344
+ if (query.triggerType?.length && !query.triggerType.includes(rule.trigger.type)) {
345
+ return false;
346
+ }
347
+ if (query.projectId && rule.target.projectId !== query.projectId) {
348
+ return false;
349
+ }
350
+ if (query.boardProjectId && rule.target.boardProjectId !== query.boardProjectId) {
351
+ return false;
352
+ }
353
+ if (query.search) {
354
+ const haystack = [
355
+ rule.id,
356
+ rule.name,
357
+ rule.template.title,
358
+ rule.template.summary,
359
+ rule.template.description,
360
+ rule.source.provider,
361
+ rule.source.externalId,
362
+ ]
363
+ .filter((value) => Boolean(value))
364
+ .join('\n')
365
+ .toLowerCase();
366
+ if (!haystack.includes(query.search.toLowerCase())) {
367
+ return false;
368
+ }
369
+ }
370
+ return true;
371
+ }
372
+ function compareExecutionDescending(left, right) {
373
+ return (Date.parse(right.triggeredAt) - Date.parse(left.triggeredAt) ||
374
+ right.id.localeCompare(left.id));
375
+ }
376
+ function buildExecutionIndex(executions) {
377
+ const index = new Map();
378
+ for (const execution of executions ?? []) {
379
+ const records = index.get(execution.ruleId) ?? [];
380
+ records.push(execution);
381
+ index.set(execution.ruleId, records);
382
+ }
383
+ for (const records of index.values()) {
384
+ records.sort(compareExecutionDescending);
385
+ }
386
+ return index;
387
+ }
388
+ function summarizeExecutions(rule, executions) {
389
+ const latestExecution = executions[0];
390
+ const lastFailure = executions.find((execution) => execution.status === 'rejected');
391
+ const createdCount = executions.filter((execution) => execution.status === 'created').length;
392
+ const coalescedCount = executions.filter((execution) => execution.status === 'coalesced').length;
393
+ const rejectedCount = executions.filter((execution) => execution.status === 'rejected').length;
394
+ return {
395
+ totalCount: executions.length,
396
+ createdCount,
397
+ coalescedCount,
398
+ rejectedCount,
399
+ latestStatus: latestExecution?.status,
400
+ lastTriggeredAt: latestExecution?.triggeredAt,
401
+ lastFailureAt: lastFailure?.triggeredAt,
402
+ isFailing: rule.state === 'active' && latestExecution?.status === 'rejected',
403
+ };
404
+ }
405
+ function buildExecutionRecord(input) {
406
+ return {
407
+ id: input.id,
408
+ ruleId: input.rule.id,
409
+ ruleName: input.rule.name,
410
+ triggerType: input.rule.trigger.type,
411
+ status: input.status,
412
+ triggeredAt: input.triggeredAt,
413
+ triggeredBy: input.triggeredBy,
414
+ source: input.rule.source,
415
+ projectId: input.rule.routing.issue.projectId,
416
+ boardProjectId: input.rule.routing.board.boardProjectId,
417
+ issueId: input.issue?.id,
418
+ issueKey: input.issue?.key,
419
+ issueSource: input.issue?.source,
420
+ stateAtExecution: input.rule.state,
421
+ reason: input.reason,
422
+ deliveryId: input.deliveryId,
423
+ inputs: input.inputs,
424
+ metadata: input.metadata,
425
+ };
426
+ }
427
+ function toRuleRecord(rule, executions = []) {
428
+ return {
429
+ ...rule,
430
+ allowedActions: readAllowedActions(rule.state),
431
+ isEnabled: rule.state === 'active',
432
+ triggerType: rule.trigger.type,
433
+ executionSummary: summarizeExecutions(rule, executions),
434
+ recentExecutions: executions.slice(0, 5),
435
+ };
436
+ }
437
+ function toTargetOption(project) {
438
+ return {
439
+ projectId: project.id,
440
+ boardProjectId: project.id,
441
+ key: project.key,
442
+ name: project.name,
443
+ linkedRunProjectName: project.linkedRunProjectName,
444
+ };
445
+ }
446
+ function assertMutableState(rule) {
447
+ if (!MUTABLE_RULE_STATES.has(rule.state)) {
448
+ throw new AppError(`Rule ${rule.id} is ${rule.state} and can no longer be modified.`, 'AUTOMATION_RULE_IMMUTABLE', 409);
449
+ }
450
+ }
451
+ function assertRoutingMatchesTarget(target, routing) {
452
+ if (routing.issue.projectId !== target.projectId) {
453
+ throw new AppError('routing.issue.projectId must match target.projectId.', 'BAD_REQUEST', 400);
454
+ }
455
+ if (routing.board.boardProjectId !== target.boardProjectId) {
456
+ throw new AppError('routing.board.boardProjectId must match target.boardProjectId.', 'BAD_REQUEST', 400);
457
+ }
458
+ }
459
+ function validateUpdateBody(body) {
460
+ if ('id' in body || 'state' in body || 'audit' in body) {
461
+ throw new AppError('id, state, and audit cannot be updated directly. Use lifecycle endpoints for state changes.', 'BAD_REQUEST', 400);
462
+ }
463
+ }
464
+ export class AutomationRuleService {
465
+ deps;
466
+ constructor(overrides = {}) {
467
+ this.deps = { ...defaultDeps, ...overrides };
468
+ }
469
+ async readStorage() {
470
+ return (await readKanbanStorageFile(this.deps)) ?? {};
471
+ }
472
+ async listTargetOptions() {
473
+ const overview = await this.deps.backlogQueryService.getOverview();
474
+ return overview.snapshot.projects.map(toTargetOption);
475
+ }
476
+ async assertTargetExists(target) {
477
+ const targets = await this.listTargetOptions();
478
+ const projectExists = targets.some((candidate) => candidate.projectId === target.projectId);
479
+ if (!projectExists) {
480
+ throw new AppError(`Project ${target.projectId} not found.`, 'NOT_FOUND', 404);
481
+ }
482
+ const boardExists = targets.some((candidate) => candidate.boardProjectId === target.boardProjectId);
483
+ if (!boardExists) {
484
+ throw new AppError(`Board project ${target.boardProjectId} not found.`, 'NOT_FOUND', 404);
485
+ }
486
+ }
487
+ async assertMaterializationTargetExists(target) {
488
+ try {
489
+ await this.assertTargetExists(target);
490
+ }
491
+ catch (error) {
492
+ if (error instanceof AppError && error.code === 'NOT_FOUND') {
493
+ throw new AppError(`Automation routing failed: ${error.message}`, 'AUTOMATION_ROUTING_FAILED', 409);
494
+ }
495
+ throw error;
496
+ }
497
+ }
498
+ async persistRules(storage, rules) {
499
+ await writeKanbanStorageFile(this.deps, {
500
+ ...storage,
501
+ automationRules: rules,
502
+ });
503
+ }
504
+ async persistExecution(storage, rule, execution, triggeredAt, triggeredBy) {
505
+ const audit = {
506
+ ...rule.audit,
507
+ lastTriggeredAt: triggeredAt,
508
+ lastTriggeredBy: triggeredBy,
509
+ updatedAt: this.deps.now(),
510
+ updatedBy: triggeredBy,
511
+ };
512
+ await writeKanbanStorageFile(this.deps, {
513
+ ...storage,
514
+ automationRules: (storage.automationRules ?? []).map((candidate) => candidate.id === rule.id
515
+ ? {
516
+ ...candidate,
517
+ audit,
518
+ }
519
+ : candidate),
520
+ automationExecutions: [...(storage.automationExecutions ?? []), execution],
521
+ });
522
+ }
523
+ readExistingRule(rules, ruleId) {
524
+ const rule = rules.find((candidate) => candidate.id === ruleId);
525
+ if (!rule) {
526
+ throw new AppError(`Automation rule ${ruleId} not found.`, 'NOT_FOUND', 404);
527
+ }
528
+ return rule;
529
+ }
530
+ async listRules(query = {}) {
531
+ const storage = await this.readStorage();
532
+ const allRules = [...(storage.automationRules ?? [])];
533
+ const visibleRules = allRules.filter((rule) => ruleMatchesQuery(rule, query));
534
+ const executionIndex = buildExecutionIndex(storage.automationExecutions);
535
+ const targetOptions = await this.listTargetOptions();
536
+ return {
537
+ generatedAt: this.deps.now(),
538
+ rules: visibleRules.map((rule) => toRuleRecord(rule, executionIndex.get(rule.id) ?? [])),
539
+ summary: summarizeRules(allRules, visibleRules, executionIndex),
540
+ availableStates: AUTOMATION_RULE_STATES,
541
+ availableTriggerTypes: AUTOMATION_TRIGGER_TYPES,
542
+ targetOptions,
543
+ };
544
+ }
545
+ async getRule(ruleId) {
546
+ const storage = await this.readStorage();
547
+ const rule = this.readExistingRule(storage.automationRules ?? [], ruleId);
548
+ const executionIndex = buildExecutionIndex(storage.automationExecutions);
549
+ return {
550
+ generatedAt: this.deps.now(),
551
+ rule: toRuleRecord(rule, executionIndex.get(rule.id) ?? []),
552
+ targetOptions: await this.listTargetOptions(),
553
+ };
554
+ }
555
+ async createRule(body) {
556
+ const now = this.deps.now();
557
+ const state = body.state === undefined ? 'draft' : readState(body.state, 'state');
558
+ const trigger = readTrigger(body.trigger);
559
+ const target = readTarget(body.target);
560
+ const routing = readRouting(body.routing);
561
+ assertRoutingMatchesTarget(target, routing);
562
+ await this.assertTargetExists(target);
563
+ const rule = buildAutomationRule({
564
+ id: `automation-${createId().toLowerCase()}`,
565
+ name: readRequiredString(body.name, 'name'),
566
+ state,
567
+ trigger,
568
+ target,
569
+ template: readTemplate(body.template),
570
+ routing,
571
+ source: readSource(body.source),
572
+ audit: {
573
+ createdAt: now,
574
+ createdBy: readOptionalString(body.createdBy, 'createdBy'),
575
+ },
576
+ });
577
+ const storage = await this.readStorage();
578
+ const rules = [...(storage.automationRules ?? []), rule];
579
+ await this.persistRules(storage, rules);
580
+ return this.getRule(rule.id);
581
+ }
582
+ async updateRule(ruleId, body) {
583
+ validateUpdateBody(body);
584
+ const storage = await this.readStorage();
585
+ const existingRule = this.readExistingRule(storage.automationRules ?? [], ruleId);
586
+ assertMutableState(existingRule);
587
+ const nextTarget = body.target === undefined ? existingRule.target : readTarget(body.target);
588
+ const nextRouting = body.routing === undefined ? existingRule.routing : readRouting(body.routing);
589
+ assertRoutingMatchesTarget(nextTarget, nextRouting);
590
+ await this.assertTargetExists(nextTarget);
591
+ const nextRule = buildAutomationRule({
592
+ ...existingRule,
593
+ name: body.name === undefined ? existingRule.name : readRequiredString(body.name, 'name'),
594
+ trigger: body.trigger === undefined ? existingRule.trigger : readTrigger(body.trigger),
595
+ target: nextTarget,
596
+ template: body.template === undefined ? existingRule.template : readTemplate(body.template),
597
+ routing: nextRouting,
598
+ source: body.source === undefined ? existingRule.source : readSource(body.source, existingRule.source.kind),
599
+ audit: {
600
+ ...existingRule.audit,
601
+ updatedAt: this.deps.now(),
602
+ updatedBy: readOptionalString(body.updatedBy, 'updatedBy'),
603
+ },
604
+ });
605
+ const rules = (storage.automationRules ?? []).map((candidate) => candidate.id === ruleId ? nextRule : candidate);
606
+ await this.persistRules(storage, rules);
607
+ return this.getRule(ruleId);
608
+ }
609
+ async transitionRule(ruleId, action, updatedBy) {
610
+ const storage = await this.readStorage();
611
+ const existingRule = this.readExistingRule(storage.automationRules ?? [], ruleId);
612
+ const nextState = (() => {
613
+ switch (action) {
614
+ case 'enable':
615
+ if (existingRule.state === 'draft' || existingRule.state === 'disabled') {
616
+ return 'active';
617
+ }
618
+ break;
619
+ case 'pause':
620
+ if (existingRule.state === 'active') {
621
+ return 'paused';
622
+ }
623
+ break;
624
+ case 'resume':
625
+ if (existingRule.state === 'paused') {
626
+ return 'active';
627
+ }
628
+ break;
629
+ case 'disable':
630
+ if (existingRule.state === 'draft' || existingRule.state === 'active' || existingRule.state === 'paused') {
631
+ return 'disabled';
632
+ }
633
+ break;
634
+ }
635
+ throw new AppError(`Cannot ${action} a rule in ${existingRule.state} state.`, 'AUTOMATION_RULE_INVALID_TRANSITION', 409);
636
+ })();
637
+ const nextRule = {
638
+ ...existingRule,
639
+ state: nextState,
640
+ audit: {
641
+ ...existingRule.audit,
642
+ updatedAt: this.deps.now(),
643
+ updatedBy,
644
+ },
645
+ };
646
+ const rules = (storage.automationRules ?? []).map((candidate) => candidate.id === ruleId ? nextRule : candidate);
647
+ await this.persistRules(storage, rules);
648
+ return this.getRule(ruleId);
649
+ }
650
+ async deleteRule(ruleId) {
651
+ const storage = await this.readStorage();
652
+ this.readExistingRule(storage.automationRules ?? [], ruleId);
653
+ const nextRules = (storage.automationRules ?? []).filter((candidate) => candidate.id !== ruleId);
654
+ await this.persistRules(storage, nextRules);
655
+ return {
656
+ deletedRuleId: ruleId,
657
+ deletedAt: this.deps.now(),
658
+ };
659
+ }
660
+ async materializeEvent(ruleId, body) {
661
+ const storage = await this.readStorage();
662
+ const existingRule = this.readExistingRule(storage.automationRules ?? [], ruleId);
663
+ const triggeredAt = body.triggeredAt === undefined
664
+ ? this.deps.now()
665
+ : readRequiredString(body.triggeredAt, 'triggeredAt');
666
+ const triggeredBy = body.triggeredBy === undefined
667
+ ? 'automation'
668
+ : readRequiredString(body.triggeredBy, 'triggeredBy');
669
+ const triggerEvent = readTriggerEvent(body.triggerEvent);
670
+ const executionMetadata = readOptionalMetadata(body.metadata, 'metadata');
671
+ const executionId = `automation-execution-${createId().toLowerCase()}`;
672
+ const triggerEventSource = triggerEvent.sourceEvent ??
673
+ (existingRule.trigger.type === 'webhook' ? existingRule.trigger.sourceEvent : undefined);
674
+ const baseExecutionMetadata = {
675
+ ...(executionMetadata ?? {}),
676
+ triggerEventId: triggerEvent.id,
677
+ triggerEventSummary: triggerEvent.summary,
678
+ triggerEventSource,
679
+ triggerEventReceivedAt: triggerEvent.receivedAt,
680
+ triggerEventMetadata: triggerEvent.metadata,
681
+ };
682
+ const rejectExecution = async (message, code, status = 409) => {
683
+ const latestStorage = await this.readStorage();
684
+ const latestRule = this.readExistingRule(latestStorage.automationRules ?? [], ruleId);
685
+ const execution = buildExecutionRecord({
686
+ id: executionId,
687
+ rule: latestRule,
688
+ status: 'rejected',
689
+ triggeredAt,
690
+ triggeredBy,
691
+ reason: message,
692
+ inputs: triggerEvent.payload,
693
+ metadata: baseExecutionMetadata,
694
+ deliveryId: triggerEvent.id,
695
+ });
696
+ await this.persistExecution(latestStorage, latestRule, execution, triggeredAt, triggeredBy);
697
+ throw new AppError(message, code, status);
698
+ };
699
+ if (existingRule.state !== 'active') {
700
+ await rejectExecution(`Automation rule ${ruleId} is ${existingRule.state} and cannot materialize work.`, 'AUTOMATION_RULE_NOT_ACTIVE');
701
+ }
702
+ try {
703
+ assertRoutingMatchesTarget(existingRule.target, existingRule.routing);
704
+ await this.assertMaterializationTargetExists(existingRule.target);
705
+ }
706
+ catch (error) {
707
+ if (error instanceof AppError) {
708
+ await rejectExecution(error.message, error.code, error.status);
709
+ }
710
+ throw error;
711
+ }
712
+ const source = {
713
+ kind: existingRule.template.issueSource?.kind ?? 'run-derived',
714
+ path: existingRule.template.issueSource?.path,
715
+ externalId: existingRule.template.issueSource?.externalId ?? triggerEvent.id,
716
+ metadata: {
717
+ ...(existingRule.template.issueSource?.metadata ?? {}),
718
+ automationRuleId: existingRule.id,
719
+ automationRuleName: existingRule.name,
720
+ automationExecutionId: executionId,
721
+ triggerType: existingRule.trigger.type,
722
+ triggerEventId: triggerEvent.id,
723
+ triggerEventSummary: triggerEvent.summary,
724
+ triggerEventSource,
725
+ triggerEventReceivedAt: triggerEvent.receivedAt,
726
+ triggeredAt,
727
+ triggeredBy,
728
+ routeProjectId: existingRule.routing.issue.projectId,
729
+ routeBoardProjectId: existingRule.routing.board.boardProjectId,
730
+ },
731
+ };
732
+ let issue;
733
+ try {
734
+ ({ issue } = await this.deps.backlogQueryService.createIssue({
735
+ projectId: existingRule.routing.issue.projectId,
736
+ title: existingRule.template.title,
737
+ summary: existingRule.template.summary,
738
+ description: existingRule.template.description,
739
+ status: existingRule.template.status ?? 'backlog',
740
+ priority: existingRule.template.priority,
741
+ labelIds: existingRule.template.labelIds,
742
+ assigneeIds: existingRule.template.assigneeIds,
743
+ acceptanceCriteria: existingRule.template.acceptanceCriteria?.map((title) => ({
744
+ title,
745
+ })),
746
+ decomposition: existingRule.template.decomposition,
747
+ source,
748
+ metadata: existingRule.template.metadata,
749
+ }));
750
+ }
751
+ catch (error) {
752
+ if (error instanceof AppError) {
753
+ const reason = error.code === 'NOT_FOUND'
754
+ ? `Automation routing failed: ${error.message}`
755
+ : error.message;
756
+ await rejectExecution(reason, error.code === 'NOT_FOUND' ? 'AUTOMATION_ROUTING_FAILED' : error.code, error.code === 'NOT_FOUND' ? 409 : error.status);
757
+ }
758
+ throw error;
759
+ }
760
+ const refreshedStorage = await this.readStorage();
761
+ const latestRule = this.readExistingRule(refreshedStorage.automationRules ?? [], ruleId);
762
+ const execution = buildExecutionRecord({
763
+ id: executionId,
764
+ rule: latestRule,
765
+ status: 'created',
766
+ triggeredAt,
767
+ triggeredBy,
768
+ issue,
769
+ inputs: triggerEvent.payload,
770
+ metadata: baseExecutionMetadata,
771
+ deliveryId: triggerEvent.id,
772
+ });
773
+ const audit = {
774
+ ...latestRule.audit,
775
+ lastTriggeredAt: triggeredAt,
776
+ lastTriggeredBy: triggeredBy,
777
+ updatedAt: this.deps.now(),
778
+ updatedBy: triggeredBy,
779
+ };
780
+ const nextRules = (refreshedStorage.automationRules ?? []).map((candidate) => candidate.id === latestRule.id
781
+ ? {
782
+ ...candidate,
783
+ audit,
784
+ }
785
+ : candidate);
786
+ await writeKanbanStorageFile(this.deps, {
787
+ ...refreshedStorage,
788
+ automationRules: nextRules,
789
+ automationExecutions: [...(refreshedStorage.automationExecutions ?? []), execution],
790
+ });
791
+ const executionIndex = buildExecutionIndex([
792
+ ...(refreshedStorage.automationExecutions ?? []),
793
+ execution,
794
+ ]);
795
+ return {
796
+ generatedAt: this.deps.now(),
797
+ rule: toRuleRecord({
798
+ ...latestRule,
799
+ audit,
800
+ }, executionIndex.get(latestRule.id) ?? [execution]),
801
+ execution,
802
+ issue,
803
+ };
804
+ }
805
+ }
806
+ //# sourceMappingURL=automation-rule-service.js.map