@lumenflow/core 1.0.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 (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,283 @@
1
+ /**
2
+ * WU-1747: Retry Strategy Module
3
+ *
4
+ * Provides exponential backoff retry mechanism with configurable parameters
5
+ * for wu:done concurrent load resilience.
6
+ *
7
+ * Features:
8
+ * - Configurable max attempts, base delay, multiplier
9
+ * - Exponential backoff with optional jitter
10
+ * - Presets for common scenarios (wu_done, recovery)
11
+ * - Callback hooks for retry events
12
+ * - Conditional retry based on error type
13
+ *
14
+ * @module retry-strategy
15
+ */
16
+ import { LOG_PREFIX, EMOJI } from './wu-constants.js';
17
+ /**
18
+ * Error message patterns that are considered retryable for wu:done operations
19
+ * Exported for test consistency
20
+ */
21
+ export const RETRYABLE_ERROR_PATTERNS = Object.freeze({
22
+ FAST_FORWARD: 'fast-forward',
23
+ NOT_POSSIBLE: 'not possible',
24
+ CANNOT_LOCK_REF: 'Cannot lock ref',
25
+ FETCH: 'fetch',
26
+ PUSH: 'push',
27
+ ETIMEDOUT: 'ETIMEDOUT',
28
+ ECONNRESET: 'ECONNRESET',
29
+ });
30
+ /**
31
+ * @typedef {Object} RetryConfig
32
+ * @property {number} maxAttempts - Maximum number of attempts (default: 5)
33
+ * @property {number} baseDelayMs - Base delay in milliseconds (default: 1000)
34
+ * @property {number} maxDelayMs - Maximum delay cap in milliseconds (default: 30000)
35
+ * @property {number} multiplier - Exponential multiplier (default: 2)
36
+ * @property {number} jitter - Jitter factor 0-1 (default: 0.1 for 10%)
37
+ * @property {function} [shouldRetry] - Optional function(error) => boolean to determine if error is retryable
38
+ * @property {function} [onRetry] - Optional callback(attempt, error, delay) before each retry
39
+ */
40
+ /**
41
+ * Default retry configuration
42
+ * Balanced for typical wu:done operations
43
+ */
44
+ export const DEFAULT_RETRY_CONFIG = Object.freeze({
45
+ maxAttempts: 5,
46
+ baseDelayMs: 1000,
47
+ maxDelayMs: 30000,
48
+ multiplier: 2,
49
+ jitter: 0.1,
50
+ shouldRetry: (_error) => true,
51
+ onRetry: null,
52
+ });
53
+ /**
54
+ * Pre-configured retry presets for common scenarios
55
+ */
56
+ export const RETRY_PRESETS = Object.freeze({
57
+ /**
58
+ * Preset for wu:done merge operations
59
+ * Higher attempts and longer delays for handling concurrent load
60
+ */
61
+ wu_done: Object.freeze({
62
+ maxAttempts: 6,
63
+ baseDelayMs: 2000,
64
+ maxDelayMs: 60000,
65
+ multiplier: 2,
66
+ jitter: 0.15, // 15% jitter to spread concurrent retries
67
+ shouldRetry: (error) => {
68
+ // Retry fast-forward failures and network errors using defined patterns
69
+ const message = error.message || '';
70
+ return Object.values(RETRYABLE_ERROR_PATTERNS).some((pattern) => message.includes(pattern));
71
+ },
72
+ onRetry: null,
73
+ }),
74
+ /**
75
+ * Preset for zombie state recovery
76
+ * More attempts with shorter delays
77
+ */
78
+ recovery: Object.freeze({
79
+ maxAttempts: 4,
80
+ baseDelayMs: 500,
81
+ maxDelayMs: 10000,
82
+ multiplier: 2,
83
+ jitter: 0.1,
84
+ shouldRetry: () => true,
85
+ onRetry: null,
86
+ }),
87
+ /**
88
+ * Preset for quick operations (file I/O, local git)
89
+ * Fast retries for transient errors
90
+ */
91
+ quick: Object.freeze({
92
+ maxAttempts: 3,
93
+ baseDelayMs: 100,
94
+ maxDelayMs: 2000,
95
+ multiplier: 2,
96
+ jitter: 0.05,
97
+ shouldRetry: () => true,
98
+ onRetry: null,
99
+ }),
100
+ });
101
+ /**
102
+ * Create a retry configuration by merging defaults with custom options
103
+ *
104
+ * @param {string|Object} [presetOrOptions] - Preset name or custom options
105
+ * @param {Object} [options] - Additional options to merge (when first arg is preset name)
106
+ * @returns {RetryConfig} Complete retry configuration
107
+ *
108
+ * @example
109
+ * // Use defaults
110
+ * const config = createRetryConfig();
111
+ *
112
+ * @example
113
+ * // Customize defaults
114
+ * const config = createRetryConfig({ maxAttempts: 10 });
115
+ *
116
+ * @example
117
+ * // Use preset
118
+ * const config = createRetryConfig('wu_done');
119
+ *
120
+ * @example
121
+ * // Customize preset
122
+ * const config = createRetryConfig('wu_done', { maxAttempts: 10 });
123
+ */
124
+ export function createRetryConfig(presetOrOptions, options) {
125
+ // Determine base config
126
+ let baseConfig;
127
+ let customOptions;
128
+ if (typeof presetOrOptions === 'string') {
129
+ // First arg is preset name
130
+ if (!(presetOrOptions in RETRY_PRESETS)) {
131
+ throw new Error(`Unknown retry preset: ${presetOrOptions}`);
132
+ }
133
+ baseConfig = RETRY_PRESETS[presetOrOptions];
134
+ customOptions = options || {};
135
+ }
136
+ else {
137
+ // First arg is options (or undefined)
138
+ baseConfig = DEFAULT_RETRY_CONFIG;
139
+ customOptions = presetOrOptions || {};
140
+ }
141
+ // Merge and return
142
+ // Avoid overriding preset/default values with `undefined` (WU-1756):
143
+ // callers often pass option keys conditionally, and spreading `undefined`
144
+ // clobbers required defaults (e.g., maxAttempts) leading to zero-attempt retries.
145
+ const definedOptions = Object.fromEntries(Object.entries(customOptions).filter(([, value]) => value !== undefined));
146
+ return {
147
+ ...baseConfig,
148
+ ...definedOptions,
149
+ };
150
+ }
151
+ /**
152
+ * Calculate the backoff delay for a given attempt number
153
+ *
154
+ * Uses exponential backoff formula: baseDelay * (multiplier ^ attempt)
155
+ * with optional jitter to spread concurrent retry attempts
156
+ *
157
+ * @param {number} attempt - Zero-based attempt number (0 = first attempt)
158
+ * @param {RetryConfig} config - Retry configuration
159
+ * @returns {number} Delay in milliseconds
160
+ */
161
+ export function calculateBackoffDelay(attempt, config) {
162
+ const { baseDelayMs, multiplier, maxDelayMs, jitter } = config;
163
+ // Exponential backoff: base * multiplier^attempt
164
+ let delay = baseDelayMs * Math.pow(multiplier, attempt);
165
+ // Cap at max delay
166
+ delay = Math.min(delay, maxDelayMs);
167
+ // Add jitter if enabled (spreads concurrent retries)
168
+ if (jitter > 0) {
169
+ // Jitter adds/subtracts random percentage
170
+ const jitterRange = delay * jitter;
171
+ const jitterOffset = (Math.random() * 2 - 1) * jitterRange;
172
+ delay = delay + jitterOffset;
173
+ }
174
+ return Math.round(delay);
175
+ }
176
+ /**
177
+ * Sleep for specified milliseconds
178
+ *
179
+ * @param {number} ms - Milliseconds to sleep
180
+ * @returns {Promise<void>}
181
+ */
182
+ function sleep(ms) {
183
+ return new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+ /**
186
+ * Execute a function with retry logic and exponential backoff
187
+ *
188
+ * @template T
189
+ * @param {function(): Promise<T>} fn - Async function to execute
190
+ * @param {RetryConfig} [config] - Retry configuration (uses defaults if not provided)
191
+ * @returns {Promise<T>} Result of successful execution
192
+ * @throws {Error} Last error if all attempts fail
193
+ *
194
+ * @example
195
+ * const result = await withRetry(
196
+ * async () => await mergeBranch(),
197
+ * createRetryConfig('wu_done')
198
+ * );
199
+ */
200
+ export async function withRetry(fn, config = DEFAULT_RETRY_CONFIG) {
201
+ const { maxAttempts, shouldRetry, onRetry } = config;
202
+ let lastError;
203
+ let attempt = 0;
204
+ while (attempt < maxAttempts) {
205
+ try {
206
+ return await fn();
207
+ }
208
+ catch (error) {
209
+ lastError = error;
210
+ attempt++;
211
+ // Check if we should retry
212
+ if (attempt >= maxAttempts) {
213
+ break; // Max attempts reached
214
+ }
215
+ if (!shouldRetry(error)) {
216
+ break; // Error not retryable
217
+ }
218
+ // Calculate delay for next retry
219
+ const delay = calculateBackoffDelay(attempt - 1, config);
220
+ // Call onRetry callback if provided
221
+ if (typeof onRetry === 'function') {
222
+ onRetry(attempt, error, delay);
223
+ }
224
+ // Log retry info
225
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Attempt ${attempt}/${maxAttempts} failed: ${error.message}`);
226
+ console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} Retrying in ${delay}ms...`);
227
+ // Wait before retry
228
+ await sleep(delay);
229
+ }
230
+ }
231
+ // All attempts failed
232
+ // Defensive: if a caller passes an invalid config, ensure we throw a useful error.
233
+ if (!lastError) {
234
+ throw new Error(`Operation failed: invalid retry configuration (maxAttempts=${maxAttempts})`);
235
+ }
236
+ throw new Error(`Operation failed after ${attempt} attempt(s): ${lastError.message}\n` +
237
+ `Original error: ${lastError.stack || lastError.message}`);
238
+ }
239
+ /**
240
+ * Higher-order function to wrap a function with retry logic
241
+ *
242
+ * @template T
243
+ * @param {function(...args): Promise<T>} fn - Function to wrap
244
+ * @param {RetryConfig} [config] - Retry configuration
245
+ * @returns {function(...args): Promise<T>} Wrapped function with retry logic
246
+ *
247
+ * @example
248
+ * const retryableMerge = withRetryWrapper(mergeBranch, createRetryConfig('wu_done'));
249
+ * await retryableMerge(branch);
250
+ */
251
+ export function withRetryWrapper(fn, config = DEFAULT_RETRY_CONFIG) {
252
+ return async (...args) => {
253
+ return withRetry(() => fn(...args), config);
254
+ };
255
+ }
256
+ /**
257
+ * Determine if an error is a git conflict error (non-retryable)
258
+ *
259
+ * @param {Error} error - Error to check
260
+ * @returns {boolean} True if conflict error
261
+ */
262
+ export function isConflictError(error) {
263
+ const message = error.message || '';
264
+ return (message.includes('conflict') ||
265
+ message.includes('CONFLICT') ||
266
+ message.includes('<<<<<<<') ||
267
+ message.includes('not possible to fast-forward'));
268
+ }
269
+ /**
270
+ * Determine if an error is a network/transient error (retryable)
271
+ *
272
+ * @param {Error} error - Error to check
273
+ * @returns {boolean} True if likely transient
274
+ */
275
+ export function isTransientError(error) {
276
+ const message = error.message || '';
277
+ return (message.includes('ETIMEDOUT') ||
278
+ message.includes('ECONNRESET') ||
279
+ message.includes('ECONNREFUSED') ||
280
+ message.includes('fetch failed') ||
281
+ message.includes('network') ||
282
+ message.includes('timeout'));
283
+ }
@@ -0,0 +1,108 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Risk Detector
4
+ *
5
+ * WU-2062: Implement tiered test execution for faster wu:done
6
+ *
7
+ * Detects risk tier based on changed files to enable tiered test execution:
8
+ * - docs-only: Skip tests, run lint/typecheck only
9
+ * - standard: Run changed tests
10
+ * - high-risk: Run integration tests in addition to unit tests
11
+ *
12
+ * Safety-critical tests (red-flag, PHI, escalation) always run regardless of tier.
13
+ *
14
+ * Note: This is project-specific risk detection logic for PatientPath's
15
+ * healthcare domain (PHI, RLS, auth). No standard library exists for this
16
+ * domain-specific classification.
17
+ *
18
+ * @see {@link tools/gates.mjs} - Consumer of risk detection
19
+ * @see {@link tools/lib/file-classifiers.mjs} - File classification utilities
20
+ */
21
+ /**
22
+ * Risk tier constants
23
+ * @readonly
24
+ * @enum {string}
25
+ */
26
+ export declare const RISK_TIERS: Readonly<{
27
+ /** Documentation-only changes - skip tests, run lint/typecheck only */
28
+ DOCS_ONLY: "docs-only";
29
+ /** Standard code changes - run incremental tests */
30
+ STANDARD: "standard";
31
+ /** Safety-critical tests need to run - always include safety test patterns */
32
+ SAFETY_CRITICAL: "safety-critical";
33
+ /** High-risk changes (auth, PHI, RLS, migrations) - run integration tests */
34
+ HIGH_RISK: "high-risk";
35
+ }>;
36
+ /**
37
+ * Test patterns that should ALWAYS run regardless of which files changed.
38
+ * These are safety-critical tests that verify red-flag detection, PHI protection,
39
+ * escalation triggers, and constitutional enforcement.
40
+ *
41
+ * @type {string[]}
42
+ */
43
+ export declare const SAFETY_CRITICAL_TEST_PATTERNS: readonly string[];
44
+ /**
45
+ * Path patterns that indicate high-risk code changes.
46
+ * Changes to these paths should trigger integration tests.
47
+ *
48
+ * WU-2242: Added authentication/ pattern
49
+ *
50
+ * @type {string[]}
51
+ */
52
+ export declare const HIGH_RISK_PATH_PATTERNS: readonly string[];
53
+ /**
54
+ * Check if a test file path matches safety-critical patterns.
55
+ *
56
+ * @param {string} testPath - Path to test file
57
+ * @returns {boolean} True if test is safety-critical
58
+ */
59
+ export declare function isSafetyCriticalTest(testPath: any): boolean;
60
+ /**
61
+ * Check if a migration file contains RLS or policy changes.
62
+ *
63
+ * @param {string} filePath - Migration file path
64
+ * @returns {boolean} True if migration is high-risk
65
+ */
66
+ export declare function isHighRiskMigration(filePath: any): boolean;
67
+ /**
68
+ * Check if a file path represents a high-risk code change.
69
+ *
70
+ * @param {string} filePath - Path to check
71
+ * @returns {boolean} True if path is high-risk
72
+ */
73
+ export declare function isHighRiskPath(filePath: any): boolean;
74
+ /**
75
+ * Detect the risk tier for a set of changed files.
76
+ *
77
+ * Returns a result object containing:
78
+ * - tier: The detected risk tier
79
+ * - safetyCriticalPatterns: Patterns to filter safety-critical tests
80
+ * - highRiskPaths: List of high-risk paths found in changes
81
+ * - isDocsOnly: Whether this is a docs-only change
82
+ * - shouldRunIntegration: Whether integration tests should run
83
+ *
84
+ * @param {object} options - Detection options
85
+ * @param {string[]} [options.changedFiles=[]] - List of changed file paths
86
+ * @returns {object} Risk detection result
87
+ *
88
+ * @example
89
+ * const result = detectRiskTier({ changedFiles: ['src/lib/auth/getUser.ts'] });
90
+ * // result.tier === 'high-risk'
91
+ * // result.shouldRunIntegration === true
92
+ *
93
+ * @example
94
+ * const result = detectRiskTier({ changedFiles: ['docs/guide.md'] });
95
+ * // result.tier === 'docs-only'
96
+ * // result.isDocsOnly === true
97
+ */
98
+ export interface DetectRiskTierOptions {
99
+ /** Array of changed file paths to analyze */
100
+ changedFiles?: string[];
101
+ }
102
+ export declare function detectRiskTier(options?: DetectRiskTierOptions): {
103
+ tier: any;
104
+ safetyCriticalPatterns: string[];
105
+ highRiskPaths: any;
106
+ isDocsOnly: any;
107
+ shouldRunIntegration: boolean;
108
+ };
@@ -0,0 +1,252 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Risk Detector
4
+ *
5
+ * WU-2062: Implement tiered test execution for faster wu:done
6
+ *
7
+ * Detects risk tier based on changed files to enable tiered test execution:
8
+ * - docs-only: Skip tests, run lint/typecheck only
9
+ * - standard: Run changed tests
10
+ * - high-risk: Run integration tests in addition to unit tests
11
+ *
12
+ * Safety-critical tests (red-flag, PHI, escalation) always run regardless of tier.
13
+ *
14
+ * Note: This is project-specific risk detection logic for PatientPath's
15
+ * healthcare domain (PHI, RLS, auth). No standard library exists for this
16
+ * domain-specific classification.
17
+ *
18
+ * @see {@link tools/gates.mjs} - Consumer of risk detection
19
+ * @see {@link tools/lib/file-classifiers.mjs} - File classification utilities
20
+ */
21
+ import path from 'node:path';
22
+ import { isDocumentationPath } from './file-classifiers.js';
23
+ /**
24
+ * Risk tier constants
25
+ * @readonly
26
+ * @enum {string}
27
+ */
28
+ export const RISK_TIERS = Object.freeze({
29
+ /** Documentation-only changes - skip tests, run lint/typecheck only */
30
+ DOCS_ONLY: 'docs-only',
31
+ /** Standard code changes - run incremental tests */
32
+ STANDARD: 'standard',
33
+ /** Safety-critical tests need to run - always include safety test patterns */
34
+ SAFETY_CRITICAL: 'safety-critical',
35
+ /** High-risk changes (auth, PHI, RLS, migrations) - run integration tests */
36
+ HIGH_RISK: 'high-risk',
37
+ });
38
+ /**
39
+ * Test patterns that should ALWAYS run regardless of which files changed.
40
+ * These are safety-critical tests that verify red-flag detection, PHI protection,
41
+ * escalation triggers, and constitutional enforcement.
42
+ *
43
+ * @type {string[]}
44
+ */
45
+ export const SAFETY_CRITICAL_TEST_PATTERNS = Object.freeze([
46
+ // Red-flag detection tests
47
+ 'red-flag',
48
+ 'redflag',
49
+ 'RedFlag',
50
+ // PHI protection tests
51
+ 'phi',
52
+ 'PHI',
53
+ 'PHIGuard',
54
+ // Escalation trigger tests
55
+ 'escalation',
56
+ 'Escalation',
57
+ // Privacy detection tests
58
+ 'privacy',
59
+ 'Privacy',
60
+ 'privacyDetector',
61
+ // Constitutional enforcement tests
62
+ 'constitutional',
63
+ 'Constitutional',
64
+ // Safe prompt wrapper tests
65
+ 'safePrompt',
66
+ 'SafePrompt',
67
+ // Crisis/emergency handling tests
68
+ 'crisis',
69
+ 'Crisis',
70
+ // Policy enforcement tests
71
+ 'policyReferee',
72
+ 'PolicyReferee',
73
+ ]);
74
+ /**
75
+ * Path patterns that indicate high-risk code changes.
76
+ * Changes to these paths should trigger integration tests.
77
+ *
78
+ * WU-2242: Added authentication/ pattern
79
+ *
80
+ * @type {string[]}
81
+ */
82
+ // constants: HIGH_RISK_PATH_PATTERNS (skip hardcoded string detection)
83
+ export const HIGH_RISK_PATH_PATTERNS = Object.freeze([
84
+ // Authentication (WU-2242: include both auth/ and authentication/)
85
+ '/auth/', // constants
86
+ '/auth.', // constants
87
+ '/authentication/', // constants
88
+ '/authentication.', // constants
89
+ // PHI (Protected Health Information)
90
+ '/phi/',
91
+ '/phi.',
92
+ // Row Level Security
93
+ '/rls/',
94
+ '/rls.',
95
+ 'rls.sql',
96
+ // Security policies
97
+ '/policy/',
98
+ '/policy.',
99
+ // Supabase configuration
100
+ 'supabase/config',
101
+ // API routes (potential attack surface)
102
+ '/api/',
103
+ ]);
104
+ /**
105
+ * Supabase migrations directory (relative to repo root).
106
+ * @type {string}
107
+ */
108
+ const SUPABASE_MIGRATIONS_DIR = 'supabase/supabase/migrations/';
109
+ /**
110
+ * Filename patterns that indicate RLS or policy changes in migrations.
111
+ * @type {RegExp[]}
112
+ */
113
+ const RLS_MIGRATION_PATTERNS = Object.freeze([
114
+ /policy/i,
115
+ /rls/i,
116
+ /row[_-]?level[_-]?security/i,
117
+ /enable[_-]?rls/i,
118
+ ]);
119
+ /**
120
+ * Check if a test file path matches safety-critical patterns.
121
+ *
122
+ * @param {string} testPath - Path to test file
123
+ * @returns {boolean} True if test is safety-critical
124
+ */
125
+ export function isSafetyCriticalTest(testPath) {
126
+ if (!testPath || typeof testPath !== 'string') {
127
+ return false;
128
+ }
129
+ // Normalise Windows paths
130
+ const normalized = testPath.replace(/\\/g, '/');
131
+ // Check if any safety-critical pattern matches
132
+ for (const pattern of SAFETY_CRITICAL_TEST_PATTERNS) {
133
+ if (normalized.includes(pattern)) {
134
+ return true;
135
+ }
136
+ }
137
+ return false;
138
+ }
139
+ /**
140
+ * Check whether a path refers to a Supabase migration file.
141
+ *
142
+ * @param {string} filePath - Path to check
143
+ * @returns {boolean} True if file is a migration SQL
144
+ */
145
+ function isMigrationPath(filePath) {
146
+ if (!filePath || typeof filePath !== 'string') {
147
+ return false;
148
+ }
149
+ const normalized = filePath.replace(/\\/g, '/');
150
+ return normalized.includes(SUPABASE_MIGRATIONS_DIR) && normalized.endsWith('.sql');
151
+ }
152
+ /**
153
+ * Check if a migration file contains RLS or policy changes.
154
+ *
155
+ * @param {string} filePath - Migration file path
156
+ * @returns {boolean} True if migration is high-risk
157
+ */
158
+ export function isHighRiskMigration(filePath) {
159
+ if (!isMigrationPath(filePath)) {
160
+ return false;
161
+ }
162
+ const normalized = filePath.replace(/\\/g, '/');
163
+ const filename = path.basename(normalized);
164
+ return RLS_MIGRATION_PATTERNS.some((pattern) => pattern.test(filename));
165
+ }
166
+ /**
167
+ * Check if a file path represents a high-risk code change.
168
+ *
169
+ * @param {string} filePath - Path to check
170
+ * @returns {boolean} True if path is high-risk
171
+ */
172
+ export function isHighRiskPath(filePath) {
173
+ if (!filePath || typeof filePath !== 'string') {
174
+ return false;
175
+ }
176
+ if (isMigrationPath(filePath)) {
177
+ return false;
178
+ }
179
+ // Normalise Windows paths
180
+ const normalized = filePath.replace(/\\/g, '/');
181
+ // Check if any high-risk pattern matches
182
+ for (const pattern of HIGH_RISK_PATH_PATTERNS) {
183
+ if (normalized.includes(pattern)) {
184
+ return true;
185
+ }
186
+ }
187
+ return false;
188
+ }
189
+ /**
190
+ * Check if all changed files are documentation-only.
191
+ *
192
+ * @param {string[]} changedFiles - List of changed file paths
193
+ * @returns {boolean} True if all files are documentation
194
+ */
195
+ function areAllDocsOnly(changedFiles) {
196
+ if (!changedFiles || changedFiles.length === 0) {
197
+ return true;
198
+ }
199
+ return changedFiles.every((file) => {
200
+ // Normalise Windows paths
201
+ const normalized = file.replace(/\\/g, '/');
202
+ return isDocumentationPath(normalized);
203
+ });
204
+ }
205
+ /**
206
+ * Find high-risk paths in changed files.
207
+ *
208
+ * @param {string[]} changedFiles - List of changed file paths
209
+ * @returns {string[]} List of high-risk paths found
210
+ */
211
+ function findHighRiskPaths(changedFiles) {
212
+ if (!changedFiles || changedFiles.length === 0) {
213
+ return [];
214
+ }
215
+ return changedFiles.filter((file) => {
216
+ // Normalise Windows paths
217
+ const normalized = file.replace(/\\/g, '/');
218
+ if (isMigrationPath(normalized)) {
219
+ return isHighRiskMigration(normalized);
220
+ }
221
+ return isHighRiskPath(normalized);
222
+ });
223
+ }
224
+ export function detectRiskTier(options = {}) {
225
+ const { changedFiles = [] } = options;
226
+ // Normalise all paths
227
+ const normalizedFiles = changedFiles.map((f) => (f ? f.replace(/\\/g, '/') : ''));
228
+ // Check if all files are documentation-only
229
+ const isDocsOnly = areAllDocsOnly(normalizedFiles);
230
+ // Find any high-risk paths
231
+ const highRiskPaths = findHighRiskPaths(normalizedFiles);
232
+ // Determine tier
233
+ let tier;
234
+ if (isDocsOnly) {
235
+ tier = RISK_TIERS.DOCS_ONLY;
236
+ }
237
+ else if (highRiskPaths.length > 0) {
238
+ tier = RISK_TIERS.HIGH_RISK;
239
+ }
240
+ else {
241
+ tier = RISK_TIERS.STANDARD;
242
+ }
243
+ // Safety-critical patterns always apply (for test filtering)
244
+ const safetyCriticalPatterns = [...SAFETY_CRITICAL_TEST_PATTERNS];
245
+ return {
246
+ tier,
247
+ safetyCriticalPatterns,
248
+ highRiskPaths,
249
+ isDocsOnly,
250
+ shouldRunIntegration: tier === RISK_TIERS.HIGH_RISK,
251
+ };
252
+ }