@renseiai/agentfactory 0.8.7 → 0.8.9

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 (276) hide show
  1. package/dist/src/config/index.d.ts +1 -1
  2. package/dist/src/config/index.d.ts.map +1 -1
  3. package/dist/src/config/index.js +1 -1
  4. package/dist/src/config/repository-config.d.ts +37 -0
  5. package/dist/src/config/repository-config.d.ts.map +1 -1
  6. package/dist/src/config/repository-config.js +47 -0
  7. package/dist/src/config/repository-config.test.js +140 -1
  8. package/dist/src/governor/decision-engine.d.ts +3 -0
  9. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  10. package/dist/src/governor/decision-engine.js +11 -0
  11. package/dist/src/governor/decision-engine.test.js +33 -0
  12. package/dist/src/governor/event-types.d.ts +18 -1
  13. package/dist/src/governor/event-types.d.ts.map +1 -1
  14. package/dist/src/governor/event-types.js +4 -0
  15. package/dist/src/governor/governor-types.d.ts +1 -1
  16. package/dist/src/governor/governor-types.d.ts.map +1 -1
  17. package/dist/src/governor/governor.d.ts +17 -1
  18. package/dist/src/governor/governor.d.ts.map +1 -1
  19. package/dist/src/governor/governor.js +112 -1
  20. package/dist/src/governor/governor.test.js +155 -0
  21. package/dist/src/index.d.ts +1 -0
  22. package/dist/src/index.d.ts.map +1 -1
  23. package/dist/src/index.js +1 -0
  24. package/dist/src/merge-queue/adapters/github-native.d.ts +22 -0
  25. package/dist/src/merge-queue/adapters/github-native.d.ts.map +1 -0
  26. package/dist/src/merge-queue/adapters/github-native.js +243 -0
  27. package/dist/src/merge-queue/adapters/github-native.test.d.ts +2 -0
  28. package/dist/src/merge-queue/adapters/github-native.test.d.ts.map +1 -0
  29. package/dist/src/merge-queue/adapters/github-native.test.js +384 -0
  30. package/dist/src/merge-queue/index.d.ts +18 -0
  31. package/dist/src/merge-queue/index.d.ts.map +1 -0
  32. package/dist/src/merge-queue/index.js +28 -0
  33. package/dist/src/merge-queue/merge-queue.integration.test.d.ts +2 -0
  34. package/dist/src/merge-queue/merge-queue.integration.test.d.ts.map +1 -0
  35. package/dist/src/merge-queue/merge-queue.integration.test.js +128 -0
  36. package/dist/src/merge-queue/types.d.ts +48 -0
  37. package/dist/src/merge-queue/types.d.ts.map +1 -0
  38. package/dist/src/merge-queue/types.js +8 -0
  39. package/dist/src/orchestrator/artifact-tracker.d.ts +93 -0
  40. package/dist/src/orchestrator/artifact-tracker.d.ts.map +1 -0
  41. package/dist/src/orchestrator/artifact-tracker.js +235 -0
  42. package/dist/src/orchestrator/artifact-tracker.test.d.ts +2 -0
  43. package/dist/src/orchestrator/artifact-tracker.test.d.ts.map +1 -0
  44. package/dist/src/orchestrator/artifact-tracker.test.js +189 -0
  45. package/dist/src/orchestrator/context-manager.d.ts +72 -0
  46. package/dist/src/orchestrator/context-manager.d.ts.map +1 -0
  47. package/dist/src/orchestrator/context-manager.js +120 -0
  48. package/dist/src/orchestrator/context-manager.test.d.ts +2 -0
  49. package/dist/src/orchestrator/context-manager.test.d.ts.map +1 -0
  50. package/dist/src/orchestrator/context-manager.test.js +137 -0
  51. package/dist/src/orchestrator/index.d.ts +8 -2
  52. package/dist/src/orchestrator/index.d.ts.map +1 -1
  53. package/dist/src/orchestrator/index.js +8 -1
  54. package/dist/src/orchestrator/issue-tracker-client.d.ts +4 -0
  55. package/dist/src/orchestrator/issue-tracker-client.d.ts.map +1 -1
  56. package/dist/src/orchestrator/orchestrator.d.ts +12 -0
  57. package/dist/src/orchestrator/orchestrator.d.ts.map +1 -1
  58. package/dist/src/orchestrator/orchestrator.js +282 -2
  59. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  60. package/dist/src/orchestrator/parse-work-result.js +6 -0
  61. package/dist/src/orchestrator/parse-work-result.test.js +19 -0
  62. package/dist/src/orchestrator/state-recovery.d.ts +21 -2
  63. package/dist/src/orchestrator/state-recovery.d.ts.map +1 -1
  64. package/dist/src/orchestrator/state-recovery.js +54 -2
  65. package/dist/src/orchestrator/state-recovery.test.js +106 -2
  66. package/dist/src/orchestrator/state-types.d.ts +62 -0
  67. package/dist/src/orchestrator/state-types.d.ts.map +1 -1
  68. package/dist/src/orchestrator/state-types.js +5 -1
  69. package/dist/src/orchestrator/summary-builder.d.ts +47 -0
  70. package/dist/src/orchestrator/summary-builder.d.ts.map +1 -0
  71. package/dist/src/orchestrator/summary-builder.js +240 -0
  72. package/dist/src/orchestrator/summary-builder.test.d.ts +2 -0
  73. package/dist/src/orchestrator/summary-builder.test.d.ts.map +1 -0
  74. package/dist/src/orchestrator/summary-builder.test.js +236 -0
  75. package/dist/src/orchestrator/types.d.ts +2 -0
  76. package/dist/src/orchestrator/types.d.ts.map +1 -1
  77. package/dist/src/orchestrator/work-types.d.ts +1 -1
  78. package/dist/src/orchestrator/work-types.d.ts.map +1 -1
  79. package/dist/src/providers/index.d.ts +64 -1
  80. package/dist/src/providers/index.d.ts.map +1 -1
  81. package/dist/src/providers/index.js +132 -1
  82. package/dist/src/providers/index.test.js +340 -2
  83. package/dist/src/routing/index.d.ts +7 -0
  84. package/dist/src/routing/index.d.ts.map +1 -0
  85. package/dist/src/routing/index.js +6 -0
  86. package/dist/src/routing/observation-recorder.d.ts +19 -0
  87. package/dist/src/routing/observation-recorder.d.ts.map +1 -0
  88. package/dist/src/routing/observation-recorder.js +73 -0
  89. package/dist/src/routing/observation-recorder.test.d.ts +2 -0
  90. package/dist/src/routing/observation-recorder.test.d.ts.map +1 -0
  91. package/dist/src/routing/observation-recorder.test.js +322 -0
  92. package/dist/src/routing/observation-store.d.ts +40 -0
  93. package/dist/src/routing/observation-store.d.ts.map +1 -0
  94. package/dist/src/routing/observation-store.js +1 -0
  95. package/dist/src/routing/observation-store.test.d.ts +2 -0
  96. package/dist/src/routing/observation-store.test.d.ts.map +1 -0
  97. package/dist/src/routing/observation-store.test.js +138 -0
  98. package/dist/src/routing/posterior-store.d.ts +12 -0
  99. package/dist/src/routing/posterior-store.d.ts.map +1 -0
  100. package/dist/src/routing/posterior-store.js +13 -0
  101. package/dist/src/routing/posterior-store.test.d.ts +2 -0
  102. package/dist/src/routing/posterior-store.test.d.ts.map +1 -0
  103. package/dist/src/routing/posterior-store.test.js +37 -0
  104. package/dist/src/routing/reward.d.ts +16 -0
  105. package/dist/src/routing/reward.d.ts.map +1 -0
  106. package/dist/src/routing/reward.js +29 -0
  107. package/dist/src/routing/reward.test.d.ts +2 -0
  108. package/dist/src/routing/reward.test.d.ts.map +1 -0
  109. package/dist/src/routing/reward.test.js +210 -0
  110. package/dist/src/routing/routing-engine.d.ts +20 -0
  111. package/dist/src/routing/routing-engine.d.ts.map +1 -0
  112. package/dist/src/routing/routing-engine.js +113 -0
  113. package/dist/src/routing/routing-engine.test.d.ts +2 -0
  114. package/dist/src/routing/routing-engine.test.d.ts.map +1 -0
  115. package/dist/src/routing/routing-engine.test.js +310 -0
  116. package/dist/src/routing/types.d.ts +157 -0
  117. package/dist/src/routing/types.d.ts.map +1 -0
  118. package/dist/src/routing/types.js +68 -0
  119. package/dist/src/routing/types.test.d.ts +2 -0
  120. package/dist/src/routing/types.test.d.ts.map +1 -0
  121. package/dist/src/routing/types.test.js +184 -0
  122. package/dist/src/templates/registry.test.js +2 -2
  123. package/dist/src/templates/types.d.ts +5 -0
  124. package/dist/src/templates/types.d.ts.map +1 -1
  125. package/dist/src/templates/types.js +3 -0
  126. package/dist/src/workflow/agent-cancellation.d.ts +37 -0
  127. package/dist/src/workflow/agent-cancellation.d.ts.map +1 -0
  128. package/dist/src/workflow/agent-cancellation.js +41 -0
  129. package/dist/src/workflow/agent-cancellation.test.d.ts +2 -0
  130. package/dist/src/workflow/agent-cancellation.test.d.ts.map +1 -0
  131. package/dist/src/workflow/agent-cancellation.test.js +86 -0
  132. package/dist/src/workflow/branching-router.d.ts +38 -0
  133. package/dist/src/workflow/branching-router.d.ts.map +1 -0
  134. package/dist/src/workflow/branching-router.js +52 -0
  135. package/dist/src/workflow/branching-router.test.d.ts +2 -0
  136. package/dist/src/workflow/branching-router.test.d.ts.map +1 -0
  137. package/dist/src/workflow/branching-router.test.js +209 -0
  138. package/dist/src/workflow/concurrency-semaphore.d.ts +21 -0
  139. package/dist/src/workflow/concurrency-semaphore.d.ts.map +1 -0
  140. package/dist/src/workflow/concurrency-semaphore.js +46 -0
  141. package/dist/src/workflow/concurrency-semaphore.test.d.ts +2 -0
  142. package/dist/src/workflow/concurrency-semaphore.test.d.ts.map +1 -0
  143. package/dist/src/workflow/concurrency-semaphore.test.js +183 -0
  144. package/dist/src/workflow/duration.d.ts +28 -0
  145. package/dist/src/workflow/duration.d.ts.map +1 -0
  146. package/dist/src/workflow/duration.js +57 -0
  147. package/dist/src/workflow/duration.test.d.ts +2 -0
  148. package/dist/src/workflow/duration.test.d.ts.map +1 -0
  149. package/dist/src/workflow/duration.test.js +74 -0
  150. package/dist/src/workflow/expression/ast.d.ts +53 -0
  151. package/dist/src/workflow/expression/ast.d.ts.map +1 -0
  152. package/dist/src/workflow/expression/ast.js +8 -0
  153. package/dist/src/workflow/expression/context.d.ts +40 -0
  154. package/dist/src/workflow/expression/context.d.ts.map +1 -0
  155. package/dist/src/workflow/expression/context.js +37 -0
  156. package/dist/src/workflow/expression/evaluator.d.ts +28 -0
  157. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -0
  158. package/dist/src/workflow/expression/evaluator.js +165 -0
  159. package/dist/src/workflow/expression/evaluator.test.d.ts +2 -0
  160. package/dist/src/workflow/expression/evaluator.test.d.ts.map +1 -0
  161. package/dist/src/workflow/expression/evaluator.test.js +792 -0
  162. package/dist/src/workflow/expression/expression.test.d.ts +2 -0
  163. package/dist/src/workflow/expression/expression.test.d.ts.map +1 -0
  164. package/dist/src/workflow/expression/expression.test.js +516 -0
  165. package/dist/src/workflow/expression/helpers.d.ts +21 -0
  166. package/dist/src/workflow/expression/helpers.d.ts.map +1 -0
  167. package/dist/src/workflow/expression/helpers.js +56 -0
  168. package/dist/src/workflow/expression/index.d.ts +55 -0
  169. package/dist/src/workflow/expression/index.d.ts.map +1 -0
  170. package/dist/src/workflow/expression/index.js +71 -0
  171. package/dist/src/workflow/expression/lexer.d.ts +37 -0
  172. package/dist/src/workflow/expression/lexer.d.ts.map +1 -0
  173. package/dist/src/workflow/expression/lexer.js +166 -0
  174. package/dist/src/workflow/expression/parser.d.ts +23 -0
  175. package/dist/src/workflow/expression/parser.d.ts.map +1 -0
  176. package/dist/src/workflow/expression/parser.js +181 -0
  177. package/dist/src/workflow/gate-state.d.ts +115 -0
  178. package/dist/src/workflow/gate-state.d.ts.map +1 -0
  179. package/dist/src/workflow/gate-state.js +185 -0
  180. package/dist/src/workflow/gate-state.test.d.ts +2 -0
  181. package/dist/src/workflow/gate-state.test.d.ts.map +1 -0
  182. package/dist/src/workflow/gate-state.test.js +251 -0
  183. package/dist/src/workflow/gates/gate-evaluator.d.ts +119 -0
  184. package/dist/src/workflow/gates/gate-evaluator.d.ts.map +1 -0
  185. package/dist/src/workflow/gates/gate-evaluator.js +243 -0
  186. package/dist/src/workflow/gates/gate-evaluator.test.d.ts +2 -0
  187. package/dist/src/workflow/gates/gate-evaluator.test.d.ts.map +1 -0
  188. package/dist/src/workflow/gates/gate-evaluator.test.js +240 -0
  189. package/dist/src/workflow/gates/signal-gate.d.ts +114 -0
  190. package/dist/src/workflow/gates/signal-gate.d.ts.map +1 -0
  191. package/dist/src/workflow/gates/signal-gate.js +216 -0
  192. package/dist/src/workflow/gates/signal-gate.test.d.ts +2 -0
  193. package/dist/src/workflow/gates/signal-gate.test.d.ts.map +1 -0
  194. package/dist/src/workflow/gates/signal-gate.test.js +199 -0
  195. package/dist/src/workflow/gates/timeout-engine.d.ts +96 -0
  196. package/dist/src/workflow/gates/timeout-engine.d.ts.map +1 -0
  197. package/dist/src/workflow/gates/timeout-engine.js +162 -0
  198. package/dist/src/workflow/gates/timeout-engine.test.d.ts +2 -0
  199. package/dist/src/workflow/gates/timeout-engine.test.d.ts.map +1 -0
  200. package/dist/src/workflow/gates/timeout-engine.test.js +186 -0
  201. package/dist/src/workflow/gates/timer-gate.d.ts +125 -0
  202. package/dist/src/workflow/gates/timer-gate.d.ts.map +1 -0
  203. package/dist/src/workflow/gates/timer-gate.js +381 -0
  204. package/dist/src/workflow/gates/timer-gate.test.d.ts +2 -0
  205. package/dist/src/workflow/gates/timer-gate.test.d.ts.map +1 -0
  206. package/dist/src/workflow/gates/timer-gate.test.js +211 -0
  207. package/dist/src/workflow/gates/webhook-gate.d.ts +132 -0
  208. package/dist/src/workflow/gates/webhook-gate.d.ts.map +1 -0
  209. package/dist/src/workflow/gates/webhook-gate.js +216 -0
  210. package/dist/src/workflow/gates/webhook-gate.test.d.ts +2 -0
  211. package/dist/src/workflow/gates/webhook-gate.test.d.ts.map +1 -0
  212. package/dist/src/workflow/gates/webhook-gate.test.js +182 -0
  213. package/dist/src/workflow/index.d.ts +31 -3
  214. package/dist/src/workflow/index.d.ts.map +1 -1
  215. package/dist/src/workflow/index.js +20 -1
  216. package/dist/src/workflow/parallelism-executor.d.ts +25 -0
  217. package/dist/src/workflow/parallelism-executor.d.ts.map +1 -0
  218. package/dist/src/workflow/parallelism-executor.js +53 -0
  219. package/dist/src/workflow/parallelism-executor.test.d.ts +2 -0
  220. package/dist/src/workflow/parallelism-executor.test.d.ts.map +1 -0
  221. package/dist/src/workflow/parallelism-executor.test.js +191 -0
  222. package/dist/src/workflow/parallelism-types.d.ts +80 -0
  223. package/dist/src/workflow/parallelism-types.d.ts.map +1 -0
  224. package/dist/src/workflow/parallelism-types.js +8 -0
  225. package/dist/src/workflow/phase-context-injector.d.ts +29 -0
  226. package/dist/src/workflow/phase-context-injector.d.ts.map +1 -0
  227. package/dist/src/workflow/phase-context-injector.js +43 -0
  228. package/dist/src/workflow/phase-context-injector.test.d.ts +2 -0
  229. package/dist/src/workflow/phase-context-injector.test.d.ts.map +1 -0
  230. package/dist/src/workflow/phase-context-injector.test.js +123 -0
  231. package/dist/src/workflow/phase-output-collector.d.ts +39 -0
  232. package/dist/src/workflow/phase-output-collector.d.ts.map +1 -0
  233. package/dist/src/workflow/phase-output-collector.js +141 -0
  234. package/dist/src/workflow/phase-output-collector.test.d.ts +2 -0
  235. package/dist/src/workflow/phase-output-collector.test.d.ts.map +1 -0
  236. package/dist/src/workflow/phase-output-collector.test.js +179 -0
  237. package/dist/src/workflow/retry-resolver.d.ts +51 -0
  238. package/dist/src/workflow/retry-resolver.d.ts.map +1 -0
  239. package/dist/src/workflow/retry-resolver.js +70 -0
  240. package/dist/src/workflow/retry-resolver.test.d.ts +2 -0
  241. package/dist/src/workflow/retry-resolver.test.d.ts.map +1 -0
  242. package/dist/src/workflow/retry-resolver.test.js +149 -0
  243. package/dist/src/workflow/strategies/fan-in-strategy.d.ts +21 -0
  244. package/dist/src/workflow/strategies/fan-in-strategy.d.ts.map +1 -0
  245. package/dist/src/workflow/strategies/fan-in-strategy.js +92 -0
  246. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts +2 -0
  247. package/dist/src/workflow/strategies/fan-in-strategy.test.d.ts.map +1 -0
  248. package/dist/src/workflow/strategies/fan-in-strategy.test.js +182 -0
  249. package/dist/src/workflow/strategies/fan-out-strategy.d.ts +16 -0
  250. package/dist/src/workflow/strategies/fan-out-strategy.d.ts.map +1 -0
  251. package/dist/src/workflow/strategies/fan-out-strategy.js +47 -0
  252. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts +2 -0
  253. package/dist/src/workflow/strategies/fan-out-strategy.test.d.ts.map +1 -0
  254. package/dist/src/workflow/strategies/fan-out-strategy.test.js +97 -0
  255. package/dist/src/workflow/strategies/index.d.ts +4 -0
  256. package/dist/src/workflow/strategies/index.d.ts.map +1 -0
  257. package/dist/src/workflow/strategies/index.js +3 -0
  258. package/dist/src/workflow/strategies/race-strategy.d.ts +19 -0
  259. package/dist/src/workflow/strategies/race-strategy.d.ts.map +1 -0
  260. package/dist/src/workflow/strategies/race-strategy.js +92 -0
  261. package/dist/src/workflow/strategies/race-strategy.test.d.ts +2 -0
  262. package/dist/src/workflow/strategies/race-strategy.test.d.ts.map +1 -0
  263. package/dist/src/workflow/strategies/race-strategy.test.js +318 -0
  264. package/dist/src/workflow/transition-engine.d.ts +3 -1
  265. package/dist/src/workflow/transition-engine.d.ts.map +1 -1
  266. package/dist/src/workflow/transition-engine.js +26 -7
  267. package/dist/src/workflow/transition-engine.test.js +215 -11
  268. package/dist/src/workflow/workflow-registry.d.ts +46 -1
  269. package/dist/src/workflow/workflow-registry.d.ts.map +1 -1
  270. package/dist/src/workflow/workflow-registry.js +74 -0
  271. package/dist/src/workflow/workflow-registry.test.js +54 -0
  272. package/dist/src/workflow/workflow-types.d.ts +330 -12
  273. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  274. package/dist/src/workflow/workflow-types.js +100 -5
  275. package/dist/src/workflow/workflow-types.test.js +293 -2
  276. package/package.json +2 -2
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Timer Gate Executor
3
+ *
4
+ * Pure-function executor for cron-based timer gates. Evaluates whether a
5
+ * timer gate's cron schedule has fired, and computes the next fire time.
6
+ *
7
+ * Implements a from-scratch 5-field cron parser supporting:
8
+ * - Exact values: 5, 10
9
+ * - Wildcards: *
10
+ * - Ranges: 1-5
11
+ * - Step values: *\/15, 1-30/5
12
+ * - Lists: 1,3,5
13
+ *
14
+ * No external dependencies are used.
15
+ */
16
+ // ---------------------------------------------------------------------------
17
+ // Cron Field Parser
18
+ // ---------------------------------------------------------------------------
19
+ /**
20
+ * Parse a single cron field into a set of valid integer values.
21
+ *
22
+ * Supported syntax:
23
+ * - `*` — all values in [min, max]
24
+ * - `5` — exact value
25
+ * - `1-5` — inclusive range
26
+ * - `*\/15` — step from min
27
+ * - `1-30/5` — step within a range
28
+ * - `1,3,5` — list of values (each element can be a range or step)
29
+ *
30
+ * @param field - The raw cron field string
31
+ * @param min - Minimum valid value for this field (inclusive)
32
+ * @param max - Maximum valid value for this field (inclusive)
33
+ * @returns A sorted array of unique integers that the field expands to
34
+ * @throws Error if the field contains invalid syntax
35
+ */
36
+ export function parseCronField(field, min, max) {
37
+ const result = new Set();
38
+ const parts = field.split(',');
39
+ for (const part of parts) {
40
+ const trimmed = part.trim();
41
+ if (trimmed === '') {
42
+ throw new Error(`Invalid cron field: empty segment in "${field}"`);
43
+ }
44
+ // Check for step value (e.g., */15 or 1-30/5)
45
+ const stepParts = trimmed.split('/');
46
+ if (stepParts.length > 2) {
47
+ throw new Error(`Invalid cron field: multiple '/' in "${trimmed}"`);
48
+ }
49
+ let rangeStart;
50
+ let rangeEnd;
51
+ let step = 1;
52
+ if (stepParts.length === 2) {
53
+ step = parseInt(stepParts[1], 10);
54
+ if (isNaN(step) || step <= 0) {
55
+ throw new Error(`Invalid cron step value: "${stepParts[1]}" in "${trimmed}"`);
56
+ }
57
+ }
58
+ const base = stepParts[0];
59
+ if (base === '*') {
60
+ rangeStart = min;
61
+ rangeEnd = max;
62
+ }
63
+ else if (base.includes('-')) {
64
+ const rangeParts = base.split('-');
65
+ if (rangeParts.length !== 2) {
66
+ throw new Error(`Invalid cron range: "${base}" in "${trimmed}"`);
67
+ }
68
+ rangeStart = parseInt(rangeParts[0], 10);
69
+ rangeEnd = parseInt(rangeParts[1], 10);
70
+ if (isNaN(rangeStart) || isNaN(rangeEnd)) {
71
+ throw new Error(`Invalid cron range values: "${base}" in "${trimmed}"`);
72
+ }
73
+ if (rangeStart < min || rangeEnd > max || rangeStart > rangeEnd) {
74
+ throw new Error(`Cron range out of bounds: ${rangeStart}-${rangeEnd} (valid: ${min}-${max})`);
75
+ }
76
+ }
77
+ else {
78
+ const value = parseInt(base, 10);
79
+ if (isNaN(value)) {
80
+ throw new Error(`Invalid cron value: "${base}" in "${trimmed}"`);
81
+ }
82
+ if (value < min || value > max) {
83
+ throw new Error(`Cron value out of bounds: ${value} (valid: ${min}-${max})`);
84
+ }
85
+ rangeStart = value;
86
+ rangeEnd = value;
87
+ }
88
+ for (let i = rangeStart; i <= rangeEnd; i += step) {
89
+ result.add(i);
90
+ }
91
+ }
92
+ return Array.from(result).sort((a, b) => a - b);
93
+ }
94
+ /**
95
+ * Parse a 5-field cron expression string into structured arrays of valid values.
96
+ *
97
+ * @param cronExpression - Standard 5-field cron string (e.g., "0 9 * * 1-5")
98
+ * @returns Parsed cron with expanded field values
99
+ * @throws Error if the expression doesn't have exactly 5 fields
100
+ */
101
+ export function parseCronExpression(cronExpression) {
102
+ const fields = cronExpression.trim().split(/\s+/);
103
+ if (fields.length !== 5) {
104
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${fields.length} in "${cronExpression}"`);
105
+ }
106
+ return {
107
+ minutes: parseCronField(fields[0], 0, 59),
108
+ hours: parseCronField(fields[1], 0, 23),
109
+ daysOfMonth: parseCronField(fields[2], 1, 31),
110
+ months: parseCronField(fields[3], 1, 12),
111
+ // Day of week: 0-7 where both 0 and 7 mean Sunday
112
+ daysOfWeek: normalizeDaysOfWeek(parseCronField(fields[4], 0, 7)),
113
+ };
114
+ }
115
+ /**
116
+ * Normalize day-of-week values so that 7 (Sunday) maps to 0.
117
+ * Returns a deduplicated sorted array.
118
+ */
119
+ function normalizeDaysOfWeek(days) {
120
+ const normalized = new Set();
121
+ for (const d of days) {
122
+ normalized.add(d === 7 ? 0 : d);
123
+ }
124
+ return Array.from(normalized).sort((a, b) => a - b);
125
+ }
126
+ // ---------------------------------------------------------------------------
127
+ // Next Cron Fire Time Computation
128
+ // ---------------------------------------------------------------------------
129
+ /**
130
+ * Compute the next cron fire time strictly after a given timestamp.
131
+ *
132
+ * Algorithm:
133
+ * 1. Start from the minute after the `after` timestamp
134
+ * 2. Check month, day-of-month, day-of-week, hour, minute in sequence
135
+ * 3. If a field doesn't match, advance to the next valid value and reset
136
+ * all lower-order fields
137
+ * 4. Guard against infinite loops with a maximum iteration count
138
+ *
139
+ * @param cronExpression - Standard 5-field cron expression
140
+ * @param after - Epoch milliseconds; the next fire time is strictly after this
141
+ * @returns Epoch milliseconds of the next matching cron time
142
+ * @throws Error if no valid fire time is found within the search window
143
+ */
144
+ export function computeNextCronFireTime(cronExpression, after) {
145
+ const cron = parseCronExpression(cronExpression);
146
+ // Start from one minute after the given timestamp, zeroing out seconds/ms
147
+ const start = new Date(after);
148
+ start.setSeconds(0, 0);
149
+ start.setMinutes(start.getMinutes() + 1);
150
+ let year = start.getFullYear();
151
+ let month = start.getMonth() + 1; // 1-based
152
+ let day = start.getDate();
153
+ let hour = start.getHours();
154
+ let minute = start.getMinutes();
155
+ // Safety limit to prevent infinite loops (4 years of minutes should be plenty)
156
+ const MAX_ITERATIONS = 4 * 366 * 24 * 60;
157
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
158
+ // --- Month ---
159
+ if (!cron.months.includes(month)) {
160
+ const nextMonth = findNextValue(cron.months, month);
161
+ if (nextMonth === null || nextMonth < month) {
162
+ // Wrap to next year
163
+ year++;
164
+ month = cron.months[0];
165
+ }
166
+ else {
167
+ month = nextMonth;
168
+ }
169
+ day = 1;
170
+ hour = 0;
171
+ minute = 0;
172
+ }
173
+ // --- Day of month ---
174
+ const maxDay = daysInMonth(year, month);
175
+ // Filter valid days for the actual month length
176
+ const validDays = cron.daysOfMonth.filter(d => d <= maxDay);
177
+ if (validDays.length === 0) {
178
+ // No valid day in this month; advance to next month
179
+ month++;
180
+ if (month > 12) {
181
+ month = 1;
182
+ year++;
183
+ }
184
+ day = 1;
185
+ hour = 0;
186
+ minute = 0;
187
+ continue;
188
+ }
189
+ if (!validDays.includes(day)) {
190
+ const nextDay = findNextValue(validDays, day);
191
+ if (nextDay === null || nextDay < day) {
192
+ // Wrap to next month
193
+ month++;
194
+ if (month > 12) {
195
+ month = 1;
196
+ year++;
197
+ }
198
+ day = 1;
199
+ hour = 0;
200
+ minute = 0;
201
+ continue;
202
+ }
203
+ day = nextDay;
204
+ hour = 0;
205
+ minute = 0;
206
+ }
207
+ // --- Day of week ---
208
+ const candidateDate = new Date(year, month - 1, day);
209
+ const dow = candidateDate.getDay(); // 0=Sunday
210
+ if (!cron.daysOfWeek.includes(dow)) {
211
+ // Advance to the next day
212
+ day++;
213
+ if (day > maxDay) {
214
+ month++;
215
+ if (month > 12) {
216
+ month = 1;
217
+ year++;
218
+ }
219
+ day = 1;
220
+ }
221
+ hour = 0;
222
+ minute = 0;
223
+ continue;
224
+ }
225
+ // --- Hour ---
226
+ if (!cron.hours.includes(hour)) {
227
+ const nextHour = findNextValue(cron.hours, hour);
228
+ if (nextHour === null || nextHour < hour) {
229
+ // Wrap to next day
230
+ day++;
231
+ if (day > maxDay) {
232
+ month++;
233
+ if (month > 12) {
234
+ month = 1;
235
+ year++;
236
+ }
237
+ day = 1;
238
+ }
239
+ hour = 0;
240
+ minute = 0;
241
+ continue;
242
+ }
243
+ hour = nextHour;
244
+ minute = 0;
245
+ }
246
+ // --- Minute ---
247
+ if (!cron.minutes.includes(minute)) {
248
+ const nextMinute = findNextValue(cron.minutes, minute);
249
+ if (nextMinute === null || nextMinute < minute) {
250
+ // Wrap to next hour
251
+ hour++;
252
+ if (hour > 23) {
253
+ day++;
254
+ if (day > maxDay) {
255
+ month++;
256
+ if (month > 12) {
257
+ month = 1;
258
+ year++;
259
+ }
260
+ day = 1;
261
+ }
262
+ hour = 0;
263
+ }
264
+ minute = 0;
265
+ continue;
266
+ }
267
+ minute = nextMinute;
268
+ }
269
+ // All fields match! Build the result date.
270
+ const result = new Date(year, month - 1, day, hour, minute, 0, 0);
271
+ return result.getTime();
272
+ }
273
+ throw new Error(`Could not find next cron fire time for "${cronExpression}" after ${new Date(after).toISOString()} within search window`);
274
+ }
275
+ /**
276
+ * Find the next value >= target in a sorted array of integers.
277
+ * Returns null if no such value exists.
278
+ */
279
+ function findNextValue(sortedValues, target) {
280
+ for (const v of sortedValues) {
281
+ if (v >= target)
282
+ return v;
283
+ }
284
+ return null;
285
+ }
286
+ /**
287
+ * Get the number of days in a given month (1-based) for a given year.
288
+ * Accounts for leap years.
289
+ */
290
+ function daysInMonth(year, month) {
291
+ // Day 0 of the next month gives the last day of the current month
292
+ return new Date(year, month, 0).getDate();
293
+ }
294
+ // ---------------------------------------------------------------------------
295
+ // Type Guard
296
+ // ---------------------------------------------------------------------------
297
+ /**
298
+ * Type guard that validates whether a trigger object has the shape
299
+ * expected for a timer gate (i.e., contains a `cron` string property).
300
+ *
301
+ * @param trigger - The trigger record to validate
302
+ * @returns True if the trigger is a valid TimerGateTrigger
303
+ */
304
+ export function isTimerGateTrigger(trigger) {
305
+ return (typeof trigger === 'object' &&
306
+ trigger !== null &&
307
+ 'cron' in trigger &&
308
+ typeof trigger.cron === 'string' &&
309
+ trigger.cron.trim().length > 0);
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Gate Evaluation
313
+ // ---------------------------------------------------------------------------
314
+ /**
315
+ * Evaluate whether a cron-based timer gate should fire.
316
+ *
317
+ * This is a pure function (no I/O). It parses the gate's `trigger.cron`
318
+ * expression, computes the next fire time from the gate's activation time
319
+ * (or from epoch 0 if no activation context), and checks whether the
320
+ * current time has reached or passed that fire time.
321
+ *
322
+ * @param gate - The gate definition with type "timer" and a trigger containing a cron field
323
+ * @param now - Current time in epoch milliseconds (defaults to Date.now() for testability)
324
+ * @returns TimerGateResult with fired status and next fire time
325
+ * @throws Error if the gate is not a timer gate or has an invalid trigger
326
+ */
327
+ export function evaluateTimerGate(gate, now) {
328
+ const currentTime = now ?? Date.now();
329
+ if (gate.type !== 'timer') {
330
+ throw new Error(`evaluateTimerGate called with non-timer gate: type="${gate.type}"`);
331
+ }
332
+ if (!isTimerGateTrigger(gate.trigger)) {
333
+ throw new Error(`Timer gate "${gate.name}" has invalid trigger: missing or empty "cron" field`);
334
+ }
335
+ const cronExpression = gate.trigger.cron;
336
+ // Compute the next fire time relative to one "cycle" before now,
337
+ // so we can detect whether we're currently in a fire window.
338
+ // We look for the next fire time after (now - 60 seconds) to catch
339
+ // the current minute's match, and compare against the current time.
340
+ const lookbackTime = currentTime - 60_000;
341
+ const nextFireTime = computeNextCronFireTime(cronExpression, lookbackTime);
342
+ // The gate has fired if the next fire time (computed from the lookback)
343
+ // falls at or before the current time
344
+ const fired = nextFireTime <= currentTime;
345
+ // Compute the actual next fire time from the current moment for the result
346
+ const upcomingFireTime = fired
347
+ ? computeNextCronFireTime(cronExpression, currentTime)
348
+ : nextFireTime;
349
+ return {
350
+ fired,
351
+ nextFireTime: upcomingFireTime,
352
+ };
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // Gate Filtering
356
+ // ---------------------------------------------------------------------------
357
+ /**
358
+ * Get all timer gates from a workflow definition that apply to a given phase.
359
+ *
360
+ * A gate applies to a phase if:
361
+ * - The gate's `appliesTo` array includes the phase name, OR
362
+ * - The gate has no `appliesTo` array (applies to all phases)
363
+ *
364
+ * Only gates with `type: "timer"` are returned.
365
+ *
366
+ * @param workflow - The workflow definition to search
367
+ * @param phase - The phase name to filter by
368
+ * @returns Array of GateDefinition objects for matching timer gates
369
+ */
370
+ export function getApplicableTimerGates(workflow, phase) {
371
+ if (!workflow.gates) {
372
+ return [];
373
+ }
374
+ return workflow.gates.filter(gate => {
375
+ if (gate.type !== 'timer')
376
+ return false;
377
+ if (!gate.appliesTo || gate.appliesTo.length === 0)
378
+ return true;
379
+ return gate.appliesTo.includes(phase);
380
+ });
381
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=timer-gate.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timer-gate.test.d.ts","sourceRoot":"","sources":["../../../../src/workflow/gates/timer-gate.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,211 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { parseCronField, parseCronExpression, computeNextCronFireTime, evaluateTimerGate, getApplicableTimerGates, } from './timer-gate.js';
3
+ // ---------------------------------------------------------------------------
4
+ // Helpers
5
+ // ---------------------------------------------------------------------------
6
+ function makeGateDefinition(overrides = {}) {
7
+ return {
8
+ name: 'test-gate',
9
+ type: 'timer',
10
+ trigger: { cron: '0 9 * * *' },
11
+ ...overrides,
12
+ };
13
+ }
14
+ function makeWorkflow(gates = []) {
15
+ return {
16
+ apiVersion: 'v1.1',
17
+ kind: 'WorkflowDefinition',
18
+ metadata: { name: 'test-workflow' },
19
+ phases: [{ name: 'development', template: 'dev' }],
20
+ transitions: [{ from: 'Backlog', to: 'development' }],
21
+ gates,
22
+ };
23
+ }
24
+ // ---------------------------------------------------------------------------
25
+ // parseCronField
26
+ // ---------------------------------------------------------------------------
27
+ describe('parseCronField', () => {
28
+ it('parses wildcard (*)', () => {
29
+ const result = parseCronField('*', 0, 5);
30
+ expect(result).toEqual([0, 1, 2, 3, 4, 5]);
31
+ });
32
+ it('parses exact value', () => {
33
+ const result = parseCronField('5', 0, 59);
34
+ expect(result).toEqual([5]);
35
+ });
36
+ it('parses range (1-5)', () => {
37
+ const result = parseCronField('1-5', 0, 59);
38
+ expect(result).toEqual([1, 2, 3, 4, 5]);
39
+ });
40
+ it('parses step value (*/15)', () => {
41
+ const result = parseCronField('*/15', 0, 59);
42
+ expect(result).toEqual([0, 15, 30, 45]);
43
+ });
44
+ it('parses step within range (1-30/5)', () => {
45
+ const result = parseCronField('1-30/5', 0, 59);
46
+ expect(result).toEqual([1, 6, 11, 16, 21, 26]);
47
+ });
48
+ it('parses list (1,3,5)', () => {
49
+ const result = parseCronField('1,3,5', 0, 59);
50
+ expect(result).toEqual([1, 3, 5]);
51
+ });
52
+ it('throws for out-of-bounds value', () => {
53
+ expect(() => parseCronField('60', 0, 59)).toThrow('out of bounds');
54
+ });
55
+ it('throws for out-of-bounds range', () => {
56
+ expect(() => parseCronField('0-60', 0, 59)).toThrow('out of bounds');
57
+ });
58
+ it('throws for invalid value', () => {
59
+ expect(() => parseCronField('abc', 0, 59)).toThrow('Invalid cron value');
60
+ });
61
+ it('throws for empty segment in list', () => {
62
+ expect(() => parseCronField('1,,3', 0, 59)).toThrow('empty segment');
63
+ });
64
+ it('throws for multiple slashes', () => {
65
+ expect(() => parseCronField('*/5/2', 0, 59)).toThrow("multiple '/'");
66
+ });
67
+ });
68
+ // ---------------------------------------------------------------------------
69
+ // parseCronExpression
70
+ // ---------------------------------------------------------------------------
71
+ describe('parseCronExpression', () => {
72
+ it('parses valid 5-field cron expression', () => {
73
+ const result = parseCronExpression('0 9 * * *');
74
+ expect(result.minutes).toEqual([0]);
75
+ expect(result.hours).toEqual([9]);
76
+ expect(result.daysOfMonth).toHaveLength(31);
77
+ expect(result.months).toHaveLength(12);
78
+ expect(result.daysOfWeek).toHaveLength(7);
79
+ });
80
+ it('throws for invalid field count (3 fields)', () => {
81
+ expect(() => parseCronExpression('0 9 *')).toThrow('expected 5 fields');
82
+ });
83
+ it('throws for invalid field count (6 fields)', () => {
84
+ expect(() => parseCronExpression('0 9 * * * *')).toThrow('expected 5 fields');
85
+ });
86
+ it('normalizes day-of-week 7 to 0 (Sunday)', () => {
87
+ const result = parseCronExpression('0 0 * * 7');
88
+ expect(result.daysOfWeek).toContain(0);
89
+ expect(result.daysOfWeek).not.toContain(7);
90
+ });
91
+ it('deduplicates when both 0 and 7 are present', () => {
92
+ const result = parseCronExpression('0 0 * * 0,7');
93
+ expect(result.daysOfWeek).toEqual([0]);
94
+ });
95
+ });
96
+ // ---------------------------------------------------------------------------
97
+ // computeNextCronFireTime
98
+ // ---------------------------------------------------------------------------
99
+ describe('computeNextCronFireTime', () => {
100
+ beforeEach(() => {
101
+ vi.useFakeTimers();
102
+ });
103
+ afterEach(() => {
104
+ vi.useRealTimers();
105
+ });
106
+ it('computes next fire time for "0 9 * * *" (daily at 9am)', () => {
107
+ // Set time to June 1, 2025, 08:00 UTC
108
+ const baseTime = new Date('2025-06-01T08:00:00Z').getTime();
109
+ const nextFire = computeNextCronFireTime('0 9 * * *', baseTime);
110
+ const fireDate = new Date(nextFire);
111
+ // Should fire at 9:00 on the same day (local time)
112
+ expect(fireDate.getHours()).toBe(9);
113
+ expect(fireDate.getMinutes()).toBe(0);
114
+ });
115
+ it('computes next fire time for weekday-only "0 9 * * 1-5"', () => {
116
+ // June 1, 2025 is a Sunday (day 0)
117
+ // Set to Sunday 10:00 — next weekday 9:00 should be Monday
118
+ const sunday = new Date(2025, 5, 1, 10, 0, 0, 0); // June 1, 2025, Sunday
119
+ vi.setSystemTime(sunday);
120
+ const nextFire = computeNextCronFireTime('0 9 * * 1-5', sunday.getTime());
121
+ const fireDate = new Date(nextFire);
122
+ // Next valid fire should be Monday (day 1) at 9:00
123
+ expect(fireDate.getDay()).toBeGreaterThanOrEqual(1);
124
+ expect(fireDate.getDay()).toBeLessThanOrEqual(5);
125
+ expect(fireDate.getHours()).toBe(9);
126
+ expect(fireDate.getMinutes()).toBe(0);
127
+ });
128
+ it('computes next fire time for "*/15 * * * *" (every 15 minutes)', () => {
129
+ const baseTime = new Date(2025, 5, 1, 10, 3, 0, 0).getTime(); // 10:03
130
+ const nextFire = computeNextCronFireTime('*/15 * * * *', baseTime);
131
+ const fireDate = new Date(nextFire);
132
+ // Next 15-minute mark after 10:03 should be 10:15
133
+ expect(fireDate.getMinutes()).toBe(15);
134
+ expect(fireDate.getHours()).toBe(10);
135
+ });
136
+ it('returns a time strictly after the given timestamp', () => {
137
+ const baseTime = new Date(2025, 5, 1, 9, 0, 0, 0).getTime(); // exactly 9:00
138
+ const nextFire = computeNextCronFireTime('0 9 * * *', baseTime);
139
+ expect(nextFire).toBeGreaterThan(baseTime);
140
+ });
141
+ });
142
+ // ---------------------------------------------------------------------------
143
+ // evaluateTimerGate
144
+ // ---------------------------------------------------------------------------
145
+ describe('evaluateTimerGate', () => {
146
+ it('fires when the current time matches the cron schedule', () => {
147
+ // Set now to 9:00, and use a cron that fires at 9:00 every day
148
+ const now = new Date(2025, 5, 1, 9, 0, 30, 0).getTime(); // 9:00:30
149
+ const gate = makeGateDefinition({ trigger: { cron: '0 9 * * *' } });
150
+ const result = evaluateTimerGate(gate, now);
151
+ expect(result.fired).toBe(true);
152
+ });
153
+ it('does not fire before the scheduled time', () => {
154
+ const now = new Date(2025, 5, 1, 8, 30, 0, 0).getTime(); // 8:30
155
+ const gate = makeGateDefinition({ trigger: { cron: '0 9 * * *' } });
156
+ const result = evaluateTimerGate(gate, now);
157
+ expect(result.fired).toBe(false);
158
+ });
159
+ it('throws for non-timer gate', () => {
160
+ const gate = makeGateDefinition({ type: 'signal' });
161
+ expect(() => evaluateTimerGate(gate)).toThrow('non-timer gate');
162
+ });
163
+ it('throws for timer gate with missing cron field', () => {
164
+ const gate = makeGateDefinition({ trigger: {} });
165
+ expect(() => evaluateTimerGate(gate)).toThrow('missing or empty "cron" field');
166
+ });
167
+ it('returns a nextFireTime property', () => {
168
+ const now = new Date(2025, 5, 1, 8, 30, 0, 0).getTime();
169
+ const gate = makeGateDefinition({ trigger: { cron: '0 9 * * *' } });
170
+ const result = evaluateTimerGate(gate, now);
171
+ expect(result.nextFireTime).toBeGreaterThan(0);
172
+ });
173
+ });
174
+ // ---------------------------------------------------------------------------
175
+ // getApplicableTimerGates
176
+ // ---------------------------------------------------------------------------
177
+ describe('getApplicableTimerGates', () => {
178
+ it('filters by type=timer and appliesTo', () => {
179
+ const gates = [
180
+ makeGateDefinition({ name: 'timer-1', type: 'timer', appliesTo: ['development'] }),
181
+ makeGateDefinition({ name: 'signal-1', type: 'signal', appliesTo: ['development'] }),
182
+ makeGateDefinition({ name: 'timer-2', type: 'timer', appliesTo: ['qa'] }),
183
+ ];
184
+ const workflow = makeWorkflow(gates);
185
+ const result = getApplicableTimerGates(workflow, 'development');
186
+ expect(result).toHaveLength(1);
187
+ expect(result[0].name).toBe('timer-1');
188
+ });
189
+ it('returns gates with no appliesTo restriction', () => {
190
+ const gates = [
191
+ makeGateDefinition({ name: 'global-timer', type: 'timer' }),
192
+ ];
193
+ const workflow = makeWorkflow(gates);
194
+ const result = getApplicableTimerGates(workflow, 'any-phase');
195
+ expect(result).toHaveLength(1);
196
+ });
197
+ it('returns empty array when no gates defined', () => {
198
+ const workflow = makeWorkflow();
199
+ delete workflow.gates;
200
+ const result = getApplicableTimerGates(workflow, 'development');
201
+ expect(result).toEqual([]);
202
+ });
203
+ it('returns empty array when no timer gates match phase', () => {
204
+ const gates = [
205
+ makeGateDefinition({ name: 'timer-qa', type: 'timer', appliesTo: ['qa'] }),
206
+ ];
207
+ const workflow = makeWorkflow(gates);
208
+ const result = getApplicableTimerGates(workflow, 'development');
209
+ expect(result).toEqual([]);
210
+ });
211
+ });