@lumenflow/core 2.2.2 → 2.3.2

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 (213) hide show
  1. package/dist/active-wu-detector.d.ts +1 -1
  2. package/dist/active-wu-detector.js +1 -1
  3. package/dist/arg-parser.js +51 -18
  4. package/dist/backlog-generator.d.ts +4 -4
  5. package/dist/backlog-generator.js +4 -4
  6. package/dist/backlog-sync-validator.js +1 -1
  7. package/dist/cleanup-lock.d.ts +9 -2
  8. package/dist/cleanup-lock.js +17 -7
  9. package/dist/code-path-validator.d.ts +3 -3
  10. package/dist/code-path-validator.js +3 -3
  11. package/dist/compliance-parser.d.ts +1 -1
  12. package/dist/compliance-parser.js +1 -1
  13. package/dist/constants/backlog-patterns.d.ts +1 -1
  14. package/dist/constants/backlog-patterns.js +1 -1
  15. package/dist/constants/dora-constants.d.ts +1 -1
  16. package/dist/constants/dora-constants.js +1 -1
  17. package/dist/constants/gate-constants.d.ts +1 -1
  18. package/dist/constants/gate-constants.js +1 -1
  19. package/dist/constants/linter-constants.d.ts +1 -1
  20. package/dist/constants/linter-constants.js +1 -1
  21. package/dist/constants/tokenizer-constants.d.ts +1 -1
  22. package/dist/constants/tokenizer-constants.js +1 -1
  23. package/dist/context/location-resolver.js +2 -1
  24. package/dist/context-validation-integration.d.ts +1 -0
  25. package/dist/core/scope-checker.d.ts +3 -3
  26. package/dist/core/scope-checker.js +3 -3
  27. package/dist/core/tool-runner.d.ts +5 -5
  28. package/dist/core/tool-runner.js +5 -5
  29. package/dist/core/tool.constants.d.ts +1 -1
  30. package/dist/core/tool.constants.js +1 -1
  31. package/dist/core/tool.schemas.d.ts +2 -2
  32. package/dist/core/tool.schemas.js +1 -1
  33. package/dist/core/worktree-guard.d.ts +1 -1
  34. package/dist/core/worktree-guard.js +1 -1
  35. package/dist/coverage-gate.d.ts +12 -3
  36. package/dist/coverage-gate.js +15 -8
  37. package/dist/date-utils.d.ts +4 -4
  38. package/dist/date-utils.js +4 -4
  39. package/dist/dependency-graph.d.ts +6 -0
  40. package/dist/dependency-graph.js +43 -2
  41. package/dist/dependency-guard.d.ts +2 -2
  42. package/dist/dependency-guard.js +3 -3
  43. package/dist/dependency-validator.d.ts +4 -4
  44. package/dist/dependency-validator.js +4 -7
  45. package/dist/domain/orchestration.constants.d.ts +31 -10
  46. package/dist/domain/orchestration.constants.js +45 -16
  47. package/dist/domain/orchestration.schemas.d.ts +54 -28
  48. package/dist/domain/orchestration.schemas.js +2 -2
  49. package/dist/domain/orchestration.types.d.ts +2 -2
  50. package/dist/domain/orchestration.types.js +2 -2
  51. package/dist/error-handler.d.ts +10 -10
  52. package/dist/error-handler.js +10 -10
  53. package/dist/file-classifiers.d.ts +6 -6
  54. package/dist/file-classifiers.js +6 -6
  55. package/dist/gates-config.d.ts +74 -0
  56. package/dist/gates-config.js +209 -2
  57. package/dist/git-adapter.d.ts +11 -11
  58. package/dist/git-adapter.js +11 -11
  59. package/dist/git-context-extractor.d.ts +112 -0
  60. package/dist/git-context-extractor.js +559 -0
  61. package/dist/hardcoded-strings.d.ts +1 -1
  62. package/dist/hardcoded-strings.js +1 -1
  63. package/dist/incremental-lint.d.ts +1 -1
  64. package/dist/incremental-lint.js +2 -2
  65. package/dist/incremental-test.d.ts +1 -1
  66. package/dist/incremental-test.js +1 -1
  67. package/dist/index.d.ts +13 -0
  68. package/dist/index.js +25 -0
  69. package/dist/invariants/check-automated-tests.d.ts +2 -2
  70. package/dist/invariants/check-automated-tests.js +3 -3
  71. package/dist/lane-checker.d.ts +28 -7
  72. package/dist/lane-checker.js +316 -159
  73. package/dist/lane-suggest-prompt.d.ts +108 -0
  74. package/dist/lane-suggest-prompt.js +359 -0
  75. package/dist/lane-validator.d.ts +3 -3
  76. package/dist/lane-validator.js +3 -3
  77. package/dist/logs-lib.d.ts +1 -1
  78. package/dist/logs-lib.js +1 -1
  79. package/dist/lumenflow-config-schema.d.ts +162 -0
  80. package/dist/lumenflow-config-schema.js +180 -0
  81. package/dist/manual-test-validator.d.ts +2 -2
  82. package/dist/manual-test-validator.js +3 -3
  83. package/dist/merge-lock.d.ts +8 -1
  84. package/dist/merge-lock.js +16 -7
  85. package/dist/micro-worktree.d.ts +81 -13
  86. package/dist/micro-worktree.js +98 -17
  87. package/dist/migration-deployer.d.ts +1 -1
  88. package/dist/migration-deployer.js +1 -1
  89. package/dist/orchestration-advisory-loader.d.ts +2 -2
  90. package/dist/orchestration-advisory-loader.js +10 -6
  91. package/dist/orchestration-advisory.d.ts +3 -3
  92. package/dist/orchestration-advisory.js +4 -4
  93. package/dist/orchestration-di.d.ts +4 -4
  94. package/dist/orchestration-di.js +4 -4
  95. package/dist/orchestration-rules.d.ts +4 -4
  96. package/dist/orchestration-rules.js +18 -10
  97. package/dist/orphan-detector.d.ts +3 -3
  98. package/dist/orphan-detector.js +3 -3
  99. package/dist/patrol-loop.d.ts +170 -0
  100. package/dist/patrol-loop.js +186 -0
  101. package/dist/process-detector.d.ts +5 -5
  102. package/dist/process-detector.js +5 -5
  103. package/dist/rebase-artifact-cleanup.d.ts +3 -3
  104. package/dist/rebase-artifact-cleanup.js +3 -3
  105. package/dist/resolve-policy.d.ts +195 -0
  106. package/dist/resolve-policy.js +203 -0
  107. package/dist/risk-detector.d.ts +2 -2
  108. package/dist/risk-detector.js +2 -2
  109. package/dist/rollback-utils.d.ts +1 -1
  110. package/dist/rollback-utils.js +1 -1
  111. package/dist/section-headings.d.ts +1 -1
  112. package/dist/section-headings.js +1 -1
  113. package/dist/spawn-escalation.d.ts +4 -4
  114. package/dist/spawn-escalation.js +3 -3
  115. package/dist/spawn-monitor.d.ts +4 -4
  116. package/dist/spawn-monitor.js +4 -4
  117. package/dist/spawn-recovery.d.ts +3 -3
  118. package/dist/spawn-recovery.js +3 -3
  119. package/dist/spawn-registry-schema.d.ts +2 -2
  120. package/dist/spawn-registry-schema.js +2 -2
  121. package/dist/spawn-registry-store.d.ts +2 -2
  122. package/dist/spawn-registry-store.js +2 -2
  123. package/dist/spawn-strategy.d.ts +17 -11
  124. package/dist/spawn-strategy.js +47 -44
  125. package/dist/spawn-tree.d.ts +3 -3
  126. package/dist/spawn-tree.js +3 -3
  127. package/dist/state-cleanup-core.d.ts +205 -0
  128. package/dist/state-cleanup-core.js +240 -0
  129. package/dist/state-doctor-core.d.ts +168 -0
  130. package/dist/state-doctor-core.js +251 -0
  131. package/dist/stream-error-handler.d.ts +67 -0
  132. package/dist/stream-error-handler.js +94 -0
  133. package/dist/telemetry.d.ts +1 -1
  134. package/dist/telemetry.js +1 -1
  135. package/dist/template-loader.d.ts +162 -0
  136. package/dist/template-loader.js +372 -0
  137. package/dist/test-baseline.d.ts +176 -0
  138. package/dist/test-baseline.js +282 -0
  139. package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
  140. package/dist/validation/command-registry.js +37 -0
  141. package/dist/validators/backlog-sync.js +4 -2
  142. package/dist/worktree-scanner.d.ts +1 -1
  143. package/dist/worktree-scanner.js +1 -1
  144. package/dist/worktree-symlink.d.ts +3 -3
  145. package/dist/worktree-symlink.js +3 -3
  146. package/dist/wu-backlog-updater.d.ts +1 -1
  147. package/dist/wu-backlog-updater.js +1 -1
  148. package/dist/wu-claim-helpers.d.ts +1 -1
  149. package/dist/wu-claim-helpers.js +1 -1
  150. package/dist/wu-claim-resume.d.ts +1 -1
  151. package/dist/wu-claim-resume.js +1 -1
  152. package/dist/wu-consistency-checker.d.ts +1 -1
  153. package/dist/wu-consistency-checker.js +17 -11
  154. package/dist/wu-constants.d.ts +73 -21
  155. package/dist/wu-constants.js +65 -22
  156. package/dist/wu-done-branch-only.d.ts +1 -1
  157. package/dist/wu-done-branch-only.js +1 -1
  158. package/dist/wu-done-docs-generate.d.ts +1 -1
  159. package/dist/wu-done-docs-generate.js +1 -1
  160. package/dist/wu-done-messages.d.ts +2 -2
  161. package/dist/wu-done-messages.js +2 -2
  162. package/dist/wu-done-metadata.d.ts +3 -3
  163. package/dist/wu-done-metadata.js +3 -3
  164. package/dist/wu-done-pr.d.ts +1 -1
  165. package/dist/wu-done-pr.js +4 -2
  166. package/dist/wu-done-preflight.d.ts +8 -0
  167. package/dist/wu-done-preflight.js +18 -2
  168. package/dist/wu-done-ui.d.ts +3 -3
  169. package/dist/wu-done-ui.js +3 -3
  170. package/dist/wu-done-validation.d.ts +30 -0
  171. package/dist/wu-done-validation.js +106 -1
  172. package/dist/wu-done-worktree.d.ts +1 -1
  173. package/dist/wu-done-worktree.js +11 -1
  174. package/dist/wu-events-cleanup.d.ts +148 -0
  175. package/dist/wu-events-cleanup.js +401 -0
  176. package/dist/wu-helpers.d.ts +2 -2
  177. package/dist/wu-helpers.js +2 -2
  178. package/dist/wu-id-generator.d.ts +58 -0
  179. package/dist/wu-id-generator.js +103 -0
  180. package/dist/wu-lint.js +1 -1
  181. package/dist/wu-preflight-validators.d.ts +13 -1
  182. package/dist/wu-preflight-validators.js +56 -1
  183. package/dist/wu-recovery.d.ts +2 -2
  184. package/dist/wu-recovery.js +4 -4
  185. package/dist/wu-repair-core.d.ts +5 -5
  186. package/dist/wu-repair-core.js +6 -6
  187. package/dist/wu-schema-normalization.d.ts +1 -1
  188. package/dist/wu-schema-normalization.js +1 -1
  189. package/dist/wu-schema.d.ts +7 -7
  190. package/dist/wu-schema.js +8 -8
  191. package/dist/wu-spawn-context.d.ts +87 -0
  192. package/dist/wu-spawn-context.js +175 -0
  193. package/dist/wu-spawn-helpers.d.ts +1 -1
  194. package/dist/wu-spawn-helpers.js +1 -1
  195. package/dist/wu-spawn.d.ts +177 -4
  196. package/dist/wu-spawn.js +694 -72
  197. package/dist/wu-state-schema.d.ts +1 -1
  198. package/dist/wu-state-schema.js +1 -1
  199. package/dist/wu-state-store.d.ts +3 -3
  200. package/dist/wu-state-store.js +3 -3
  201. package/dist/wu-status-transition.d.ts +1 -1
  202. package/dist/wu-status-transition.js +1 -1
  203. package/dist/wu-status-updater.d.ts +3 -3
  204. package/dist/wu-status-updater.js +3 -3
  205. package/dist/wu-validation-constants.d.ts +2 -2
  206. package/dist/wu-validation-constants.js +2 -2
  207. package/dist/wu-validation.d.ts +3 -3
  208. package/dist/wu-validation.js +3 -3
  209. package/dist/wu-yaml-fixer.d.ts +2 -2
  210. package/dist/wu-yaml-fixer.js +3 -3
  211. package/dist/wu-yaml.d.ts +23 -0
  212. package/dist/wu-yaml.js +76 -2
  213. package/package.json +5 -2
@@ -0,0 +1,401 @@
1
+ /**
2
+ * WU Events Cleanup (WU-1207)
3
+ *
4
+ * Archive old WU events to prevent unbounded growth of wu-events.jsonl.
5
+ * Moves completed WU events older than 90d to .lumenflow/archive/wu-events-YYYY-MM.jsonl.
6
+ * Keeps active WU events intact.
7
+ *
8
+ * Features:
9
+ * - Configurable archiveAfter threshold (default: 90 days)
10
+ * - Groups events by WU ID for atomic archival
11
+ * - Monthly archive file rollup
12
+ * - Active WU protection (in_progress/blocked/waiting never archived)
13
+ * - Dry-run mode for preview
14
+ *
15
+ * Reuses atomic write patterns from wu-state-store.ts.
16
+ *
17
+ * @see {@link packages/@lumenflow/core/src/__tests__/wu-events-cleanup.test.ts} - Tests
18
+ */
19
+ import fs from 'node:fs/promises';
20
+ import { writeFileSync, mkdirSync, openSync, closeSync, fsyncSync, renameSync, unlinkSync, } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { createRequire } from 'node:module';
23
+ const require = createRequire(import.meta.url);
24
+ const ms = require('ms');
25
+ import { validateWUEvent } from './wu-state-schema.js';
26
+ import { WUStateStore, WU_EVENTS_FILE_NAME } from './wu-state-store.js';
27
+ /**
28
+ * One day in milliseconds
29
+ */
30
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000;
31
+ /**
32
+ * LumenFlow paths for state and archive
33
+ */
34
+ const LUMENFLOW_PATHS = {
35
+ STATE_DIR: '.lumenflow/state',
36
+ ARCHIVE_DIR: '.lumenflow/archive',
37
+ };
38
+ /**
39
+ * Default event archival configuration
40
+ */
41
+ export const DEFAULT_EVENT_ARCHIVAL_CONFIG = {
42
+ archiveAfter: 90 * ONE_DAY_MS,
43
+ keepArchives: true,
44
+ };
45
+ /**
46
+ * Parse an archiveAfter duration string into milliseconds.
47
+ *
48
+ * Uses the `ms` package to parse human-readable duration strings.
49
+ *
50
+ * @param archiveAfterString - Duration string (e.g., '90d', '30d', '24h')
51
+ * @returns Duration in milliseconds
52
+ * @throws If format is invalid
53
+ *
54
+ * @example
55
+ * parseArchiveAfter('90d'); // 7776000000 (90 days in ms)
56
+ * parseArchiveAfter('30d'); // 2592000000 (30 days in ms)
57
+ */
58
+ export function parseArchiveAfter(archiveAfterString) {
59
+ if (!archiveAfterString || typeof archiveAfterString !== 'string') {
60
+ throw new Error('Invalid archiveAfter format: duration string is required');
61
+ }
62
+ const trimmed = archiveAfterString.trim();
63
+ if (!trimmed) {
64
+ throw new Error('Invalid archiveAfter format: duration string is required');
65
+ }
66
+ const result = ms(trimmed);
67
+ if (result === undefined || result <= 0) {
68
+ throw new Error(`Invalid archiveAfter format: "${archiveAfterString}" is not a valid duration`);
69
+ }
70
+ return result;
71
+ }
72
+ /**
73
+ * Get the archive file path for a given event timestamp.
74
+ *
75
+ * Groups events by month into files like:
76
+ * .lumenflow/archive/wu-events-2026-01.jsonl
77
+ *
78
+ * @param timestamp - ISO 8601 timestamp string
79
+ * @returns Relative path to archive file
80
+ *
81
+ * @example
82
+ * getArchiveFilePath('2026-01-15T10:30:00.000Z');
83
+ * // Returns: '.lumenflow/archive/wu-events-2026-01.jsonl'
84
+ */
85
+ export function getArchiveFilePath(timestamp) {
86
+ const date = new Date(timestamp);
87
+ const year = date.getUTCFullYear();
88
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
89
+ return `${LUMENFLOW_PATHS.ARCHIVE_DIR}/wu-events-${year}-${month}.jsonl`;
90
+ }
91
+ /**
92
+ * Check if an event's WU is older than the archive threshold.
93
+ *
94
+ * Uses the complete event timestamp (if WU is done) to determine age.
95
+ *
96
+ * @param timestamp - Event timestamp
97
+ * @param archiveAfterMs - Threshold in milliseconds
98
+ * @param now - Current timestamp
99
+ * @returns True if event is older than threshold
100
+ */
101
+ function isOlderThanThreshold(timestamp, archiveAfterMs, now) {
102
+ const eventTime = new Date(timestamp).getTime();
103
+ if (Number.isNaN(eventTime)) {
104
+ return false; // Invalid timestamp - safer to retain
105
+ }
106
+ const age = now - eventTime;
107
+ return age > archiveAfterMs;
108
+ }
109
+ /**
110
+ * Determine if an event should be archived based on WU status and age.
111
+ *
112
+ * Policy rules (checked in order):
113
+ * 1. Active WU events (in_progress/blocked/waiting) are never archived
114
+ * 2. Completed WU events older than threshold are archived
115
+ * 3. Otherwise, event is retained
116
+ *
117
+ * @param event - WU event to check
118
+ * @param config - Archival configuration
119
+ * @param context - Archive context (now timestamp, active WU IDs)
120
+ * @returns Archive decision with reason
121
+ */
122
+ export function shouldArchiveEvent(event, config, context) {
123
+ const { now, activeWuIds } = context;
124
+ // Active WU protection: events linked to in_progress/blocked/waiting WUs are always retained
125
+ if (activeWuIds.has(event.wuId)) {
126
+ return { archive: false, reason: 'active-wu-protected' };
127
+ }
128
+ // Check age threshold
129
+ if (isOlderThanThreshold(event.timestamp, config.archiveAfter, now)) {
130
+ return { archive: true, reason: 'completed-older-than-threshold' };
131
+ }
132
+ // Default: retain
133
+ return { archive: false, reason: 'within-retention-period' };
134
+ }
135
+ /**
136
+ * Load all events from wu-events.jsonl file.
137
+ *
138
+ * @param baseDir - Project base directory
139
+ * @returns Array of all WU events
140
+ */
141
+ async function loadAllEvents(baseDir) {
142
+ const eventsPath = path.join(baseDir, LUMENFLOW_PATHS.STATE_DIR, WU_EVENTS_FILE_NAME);
143
+ try {
144
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
145
+ const content = await fs.readFile(eventsPath, { encoding: 'utf-8' });
146
+ const lines = content.split('\n').filter((line) => line.trim());
147
+ return lines.map((line) => {
148
+ const parsed = JSON.parse(line);
149
+ const validation = validateWUEvent(parsed);
150
+ if (!validation.success) {
151
+ // Return as-is if validation fails (repair later)
152
+ return parsed;
153
+ }
154
+ return validation.data;
155
+ });
156
+ }
157
+ catch (err) {
158
+ const error = err;
159
+ if (error.code === 'ENOENT') {
160
+ return [];
161
+ }
162
+ throw error;
163
+ }
164
+ }
165
+ /**
166
+ * Get active WU IDs by replaying events to determine current state.
167
+ *
168
+ * A WU is active if its last status-changing event leaves it in:
169
+ * - in_progress (claim without complete)
170
+ * - blocked (block without unblock)
171
+ * - waiting (could be future state)
172
+ *
173
+ * @param baseDir - Project base directory
174
+ * @returns Set of active WU IDs
175
+ */
176
+ async function getActiveWuIdsFromStore(baseDir) {
177
+ const store = new WUStateStore(path.join(baseDir, LUMENFLOW_PATHS.STATE_DIR));
178
+ await store.load();
179
+ const activeStatuses = ['in_progress', 'blocked', 'waiting'];
180
+ const activeIds = new Set();
181
+ for (const status of activeStatuses) {
182
+ const wuIds = store.getByStatus(status);
183
+ for (const wuId of wuIds) {
184
+ activeIds.add(wuId);
185
+ }
186
+ }
187
+ return activeIds;
188
+ }
189
+ /**
190
+ * Build archival configuration from options.
191
+ *
192
+ * @param options - Archive options
193
+ * @returns Effective archival configuration
194
+ */
195
+ function buildArchivalConfig(options) {
196
+ const { archiveAfter, archiveAfterMs: providedArchiveAfterMs } = options;
197
+ let archiveAfterMs = providedArchiveAfterMs ?? DEFAULT_EVENT_ARCHIVAL_CONFIG.archiveAfter;
198
+ if (archiveAfter && !providedArchiveAfterMs) {
199
+ archiveAfterMs = parseArchiveAfter(archiveAfter);
200
+ }
201
+ return {
202
+ archiveAfter: archiveAfterMs,
203
+ keepArchives: DEFAULT_EVENT_ARCHIVAL_CONFIG.keepArchives,
204
+ };
205
+ }
206
+ /**
207
+ * Group events by WU ID for atomic archival.
208
+ *
209
+ * @param events - Array of WU events
210
+ * @returns Map of WU ID to events array
211
+ */
212
+ function groupEventsByWuId(events) {
213
+ const grouped = new Map();
214
+ for (const event of events) {
215
+ const existing = grouped.get(event.wuId) ?? [];
216
+ existing.push(event);
217
+ grouped.set(event.wuId, existing);
218
+ }
219
+ return grouped;
220
+ }
221
+ /**
222
+ * Get the most recent event timestamp for a WU.
223
+ *
224
+ * @param events - Events for a single WU
225
+ * @returns Most recent timestamp
226
+ */
227
+ function getMostRecentTimestamp(events) {
228
+ let mostRecent = events[0].timestamp;
229
+ for (const event of events) {
230
+ if (new Date(event.timestamp) > new Date(mostRecent)) {
231
+ mostRecent = event.timestamp;
232
+ }
233
+ }
234
+ return mostRecent;
235
+ }
236
+ /**
237
+ * Group events by archive file path (monthly buckets).
238
+ *
239
+ * @param events - Events to group
240
+ * @returns Map of archive file path to events
241
+ */
242
+ function groupEventsByArchivePath(events) {
243
+ const grouped = new Map();
244
+ for (const event of events) {
245
+ const archivePath = getArchiveFilePath(event.timestamp);
246
+ const existing = grouped.get(archivePath) ?? [];
247
+ existing.push(event);
248
+ grouped.set(archivePath, existing);
249
+ }
250
+ return grouped;
251
+ }
252
+ /**
253
+ * Calculate byte size of events when serialized.
254
+ *
255
+ * @param events - Events to measure
256
+ * @returns Approximate byte size
257
+ */
258
+ function estimateEventsBytes(events) {
259
+ return events.reduce((total, event) => total + JSON.stringify(event).length + 1, 0);
260
+ }
261
+ /**
262
+ * Write retained events back to wu-events.jsonl.
263
+ *
264
+ * Uses atomic write pattern: temp file + fsync + rename.
265
+ *
266
+ * @param baseDir - Project base directory
267
+ * @param events - Events to write
268
+ */
269
+ async function writeRetainedEvents(baseDir, events) {
270
+ const eventsPath = path.join(baseDir, LUMENFLOW_PATHS.STATE_DIR, WU_EVENTS_FILE_NAME);
271
+ const tempPath = `${eventsPath}.tmp.${process.pid}`;
272
+ const content = events.map((e) => JSON.stringify(e)).join('\n') + (events.length > 0 ? '\n' : '');
273
+ try {
274
+ const fd = openSync(tempPath, 'w');
275
+ writeFileSync(fd, content, 'utf-8');
276
+ fsyncSync(fd);
277
+ closeSync(fd);
278
+ // Atomic rename
279
+ renameSync(tempPath, eventsPath);
280
+ // Fsync directory
281
+ const dirPath = path.dirname(eventsPath);
282
+ const dirFd = openSync(dirPath, 'r');
283
+ fsyncSync(dirFd);
284
+ closeSync(dirFd);
285
+ }
286
+ catch (error) {
287
+ // Cleanup temp file on failure
288
+ try {
289
+ unlinkSync(tempPath);
290
+ }
291
+ catch {
292
+ // Ignore cleanup errors
293
+ }
294
+ throw error;
295
+ }
296
+ }
297
+ /**
298
+ * Append events to archive file.
299
+ *
300
+ * Creates archive directory and file if they don't exist.
301
+ * Appends to existing archive file for monthly rollup.
302
+ *
303
+ * @param baseDir - Project base directory
304
+ * @param archivePath - Relative path to archive file
305
+ * @param events - Events to append
306
+ */
307
+ async function appendToArchive(baseDir, archivePath, events) {
308
+ const fullPath = path.join(baseDir, archivePath);
309
+ const dirPath = path.dirname(fullPath);
310
+ // Ensure archive directory exists
311
+ mkdirSync(dirPath, { recursive: true });
312
+ const content = events.map((e) => JSON.stringify(e)).join('\n') + '\n';
313
+ // Append to archive file (creates if doesn't exist)
314
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
315
+ await fs.appendFile(fullPath, content, 'utf-8');
316
+ }
317
+ /**
318
+ * Archive old WU events to monthly archive files.
319
+ *
320
+ * Moves completed WU events older than threshold to archive files.
321
+ * Events are grouped by WU ID so all events for a WU are archived together.
322
+ * Active WU events (in_progress/blocked/waiting) are never archived.
323
+ *
324
+ * @param baseDir - Project base directory
325
+ * @param options - Archive options
326
+ * @returns Archive result with statistics
327
+ *
328
+ * @example
329
+ * // Preview archival
330
+ * const preview = await archiveWuEvents(baseDir, { dryRun: true });
331
+ * console.log(`Would archive ${preview.archivedWuIds.length} WUs`);
332
+ *
333
+ * @example
334
+ * // Archive with custom threshold
335
+ * const result = await archiveWuEvents(baseDir, { archiveAfter: '30d' });
336
+ */
337
+ export async function archiveWuEvents(baseDir, options = {}) {
338
+ const { dryRun = false, now = Date.now(), getActiveWuIds = getActiveWuIdsFromStore } = options;
339
+ const config = buildArchivalConfig(options);
340
+ const events = await loadAllEvents(baseDir);
341
+ const activeWuIds = await getActiveWuIds(baseDir);
342
+ // Group events by WU ID for atomic archival decisions
343
+ const eventsByWu = groupEventsByWuId(events);
344
+ // Determine which WUs to archive
345
+ const archivedWuIds = [];
346
+ const retainedWuIds = [];
347
+ const eventsToArchive = [];
348
+ const eventsToRetain = [];
349
+ const breakdown = {
350
+ archivedOlderThanThreshold: 0,
351
+ retainedActiveWu: 0,
352
+ retainedWithinThreshold: 0,
353
+ };
354
+ for (const [wuId, wuEvents] of eventsByWu) {
355
+ // Check if WU is active - if so, retain all events
356
+ if (activeWuIds.has(wuId)) {
357
+ retainedWuIds.push(wuId);
358
+ eventsToRetain.push(...wuEvents);
359
+ breakdown.retainedActiveWu++;
360
+ continue;
361
+ }
362
+ // Check age based on most recent event
363
+ const mostRecentTimestamp = getMostRecentTimestamp(wuEvents);
364
+ const decision = shouldArchiveEvent({ wuId, timestamp: mostRecentTimestamp }, config, { now, activeWuIds });
365
+ if (decision.archive) {
366
+ archivedWuIds.push(wuId);
367
+ eventsToArchive.push(...wuEvents);
368
+ breakdown.archivedOlderThanThreshold++;
369
+ }
370
+ else {
371
+ retainedWuIds.push(wuId);
372
+ eventsToRetain.push(...wuEvents);
373
+ breakdown.retainedWithinThreshold++;
374
+ }
375
+ }
376
+ const bytesArchived = estimateEventsBytes(eventsToArchive);
377
+ const baseResult = {
378
+ success: true,
379
+ archivedWuIds,
380
+ retainedWuIds,
381
+ archivedEventCount: eventsToArchive.length,
382
+ retainedEventCount: eventsToRetain.length,
383
+ bytesArchived,
384
+ breakdown,
385
+ };
386
+ if (dryRun) {
387
+ return { ...baseResult, dryRun: true };
388
+ }
389
+ // Actually archive events
390
+ if (eventsToArchive.length > 0) {
391
+ // Group by archive file path (monthly)
392
+ const archiveGroups = groupEventsByArchivePath(eventsToArchive);
393
+ // Write to archive files
394
+ for (const [archivePath, archiveEvents] of archiveGroups) {
395
+ await appendToArchive(baseDir, archivePath, archiveEvents);
396
+ }
397
+ // Write retained events back to main file
398
+ await writeRetainedEvents(baseDir, eventsToRetain);
399
+ }
400
+ return baseResult;
401
+ }
@@ -10,8 +10,8 @@
10
10
  /**
11
11
  * Validate WU ID format
12
12
  *
13
- * WU-1593: Extracted from duplicate implementations in wu-create.mjs and wu-edit.mjs (DRY).
14
- * Uses centralized PATTERNS.WU_ID regex from wu-constants.mjs.
13
+ * WU-1593: Extracted from duplicate implementations in wu-create.ts and wu-edit.ts (DRY).
14
+ * Uses centralized PATTERNS.WU_ID regex from wu-constants.ts.
15
15
  *
16
16
  * @param {string} id - WU ID to validate (e.g., 'WU-123')
17
17
  * @throws {Error} If ID format is invalid
@@ -17,8 +17,8 @@ import { isAgentBranchWithDetails } from './branch-check.js';
17
17
  /**
18
18
  * Validate WU ID format
19
19
  *
20
- * WU-1593: Extracted from duplicate implementations in wu-create.mjs and wu-edit.mjs (DRY).
21
- * Uses centralized PATTERNS.WU_ID regex from wu-constants.mjs.
20
+ * WU-1593: Extracted from duplicate implementations in wu-create.ts and wu-edit.ts (DRY).
21
+ * Uses centralized PATTERNS.WU_ID regex from wu-constants.ts.
22
22
  *
23
23
  * @param {string} id - WU ID to validate (e.g., 'WU-123')
24
24
  * @throws {Error} If ID format is invalid
@@ -0,0 +1,58 @@
1
+ /**
2
+ * WU ID Generator (WU-1246)
3
+ *
4
+ * Auto-generates sequential WU IDs by scanning existing WU YAML files.
5
+ * Provides race-condition handling via retry mechanism.
6
+ *
7
+ * @module wu-id-generator
8
+ */
9
+ /** WU ID prefix constant */
10
+ export declare const WU_ID_PREFIX = "WU-";
11
+ /**
12
+ * Parse the numeric part from a WU ID string.
13
+ *
14
+ * Supports formats:
15
+ * - WU-123 (direct ID)
16
+ * - WU-123.yaml (filename)
17
+ *
18
+ * @param wuIdOrFilename - WU ID or filename to parse
19
+ * @returns Numeric ID or null if invalid format
20
+ */
21
+ export declare function parseWuIdNumber(wuIdOrFilename: string): number | null;
22
+ /**
23
+ * Get the highest WU ID number from the WU directory.
24
+ *
25
+ * Scans all WU-*.yaml files and returns the highest numeric ID found.
26
+ * Returns 0 if directory doesn't exist or contains no valid WU files.
27
+ *
28
+ * @returns Highest WU ID number or 0 if none found
29
+ */
30
+ export declare function getHighestWuId(): number;
31
+ /**
32
+ * Get the next available WU ID.
33
+ *
34
+ * Returns the next sequential ID after the highest existing WU.
35
+ * Does not fill gaps - always returns highest + 1.
36
+ *
37
+ * @returns Next WU ID in format "WU-{number}"
38
+ */
39
+ export declare function getNextWuId(): string;
40
+ /** Options for generateWuIdWithRetry */
41
+ interface GenerateOptions {
42
+ /** Maximum number of retry attempts (default: 5) */
43
+ maxRetries?: number;
44
+ }
45
+ /**
46
+ * Generate a unique WU ID with retry handling for race conditions.
47
+ *
48
+ * This function handles concurrent wu:create calls by:
49
+ * 1. Generating the next sequential ID
50
+ * 2. Checking if the file already exists (race condition detection)
51
+ * 3. Retrying with exponential backoff if conflict detected
52
+ *
53
+ * @param options - Generation options
54
+ * @returns Promise resolving to unique WU ID
55
+ * @throws Error if max retries exceeded
56
+ */
57
+ export declare function generateWuIdWithRetry(options?: GenerateOptions): Promise<string>;
58
+ export {};
@@ -0,0 +1,103 @@
1
+ /**
2
+ * WU ID Generator (WU-1246)
3
+ *
4
+ * Auto-generates sequential WU IDs by scanning existing WU YAML files.
5
+ * Provides race-condition handling via retry mechanism.
6
+ *
7
+ * @module wu-id-generator
8
+ */
9
+ import { existsSync, readdirSync } from 'node:fs';
10
+ import { WU_PATHS } from './wu-paths.js';
11
+ /** WU ID prefix constant */
12
+ export const WU_ID_PREFIX = 'WU-';
13
+ /** Default maximum retry attempts for race condition handling */
14
+ const DEFAULT_MAX_RETRIES = 5;
15
+ /** Retry delay in milliseconds (exponential backoff base) */
16
+ const RETRY_DELAY_BASE_MS = 50;
17
+ /**
18
+ * Parse the numeric part from a WU ID string.
19
+ *
20
+ * Supports formats:
21
+ * - WU-123 (direct ID)
22
+ * - WU-123.yaml (filename)
23
+ *
24
+ * @param wuIdOrFilename - WU ID or filename to parse
25
+ * @returns Numeric ID or null if invalid format
26
+ */
27
+ export function parseWuIdNumber(wuIdOrFilename) {
28
+ if (!wuIdOrFilename || typeof wuIdOrFilename !== 'string') {
29
+ return null;
30
+ }
31
+ // Remove .yaml extension if present
32
+ const wuId = wuIdOrFilename.replace(/\.yaml$/, '');
33
+ // Match WU-{number} pattern using RegExp method per sonarjs/prefer-regexp-exec
34
+ const WU_ID_PATTERN = /^WU-(\d+)$/;
35
+ const regexResult = WU_ID_PATTERN.exec(wuId);
36
+ if (!regexResult) {
37
+ return null;
38
+ }
39
+ const num = parseInt(regexResult[1], 10);
40
+ return isNaN(num) ? null : num;
41
+ }
42
+ /**
43
+ * Get the highest WU ID number from the WU directory.
44
+ *
45
+ * Scans all WU-*.yaml files and returns the highest numeric ID found.
46
+ * Returns 0 if directory doesn't exist or contains no valid WU files.
47
+ *
48
+ * @returns Highest WU ID number or 0 if none found
49
+ */
50
+ export function getHighestWuId() {
51
+ const wuDir = WU_PATHS.WU_DIR();
52
+ if (!existsSync(wuDir)) {
53
+ return 0;
54
+ }
55
+ const files = readdirSync(wuDir);
56
+ let highest = 0;
57
+ for (const file of files) {
58
+ const num = parseWuIdNumber(file);
59
+ if (num !== null && num > highest) {
60
+ highest = num;
61
+ }
62
+ }
63
+ return highest;
64
+ }
65
+ /**
66
+ * Get the next available WU ID.
67
+ *
68
+ * Returns the next sequential ID after the highest existing WU.
69
+ * Does not fill gaps - always returns highest + 1.
70
+ *
71
+ * @returns Next WU ID in format "WU-{number}"
72
+ */
73
+ export function getNextWuId() {
74
+ const highest = getHighestWuId();
75
+ return `${WU_ID_PREFIX}${highest + 1}`;
76
+ }
77
+ /**
78
+ * Generate a unique WU ID with retry handling for race conditions.
79
+ *
80
+ * This function handles concurrent wu:create calls by:
81
+ * 1. Generating the next sequential ID
82
+ * 2. Checking if the file already exists (race condition detection)
83
+ * 3. Retrying with exponential backoff if conflict detected
84
+ *
85
+ * @param options - Generation options
86
+ * @returns Promise resolving to unique WU ID
87
+ * @throws Error if max retries exceeded
88
+ */
89
+ export async function generateWuIdWithRetry(options = {}) {
90
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
91
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
92
+ const nextId = getNextWuId();
93
+ const wuPath = WU_PATHS.WU(nextId);
94
+ // Check if file already exists (race condition)
95
+ if (!existsSync(wuPath)) {
96
+ return nextId;
97
+ }
98
+ // Exponential backoff before retry
99
+ const delay = RETRY_DELAY_BASE_MS * Math.pow(2, attempt);
100
+ await new Promise((resolve) => setTimeout(resolve, delay));
101
+ }
102
+ throw new Error(`Failed to generate unique WU ID after ${maxRetries} attempts`);
103
+ }
package/dist/wu-lint.js CHANGED
@@ -22,7 +22,7 @@ export const WU_LINT_ERROR_TYPES = {
22
22
  };
23
23
  /**
24
24
  * Regex to detect file paths in acceptance criteria text
25
- * Matches patterns like: apps/web/src/file.ts, tools/lib/helper.mjs
25
+ * Matches patterns like: apps/web/src/file.ts, tools/lib/helper.ts
26
26
  * Uses explicit character sets to avoid regex backtracking issues
27
27
  */
28
28
  const FILE_PATH_PATTERN = /(?:^|[\s'"`])([a-zA-Z0-9_-]+\/[a-zA-Z0-9_./-]+\.[a-zA-Z0-9]+)/g;
@@ -34,18 +34,21 @@
34
34
  * @param {string[]} [params.errors=[]] - Error messages
35
35
  * @param {string[]} [params.missingCodePaths=[]] - Missing code paths
36
36
  * @param {string[]} [params.missingTestPaths=[]] - Missing test paths
37
+ * @param {Record<string, string[]>} [params.suggestedTestPaths={}] - Suggested test paths
37
38
  * @returns {PreflightResult}
38
39
  */
39
- export declare function createPreflightResult({ valid, errors, missingCodePaths, missingTestPaths, }: {
40
+ export declare function createPreflightResult({ valid, errors, missingCodePaths, missingTestPaths, suggestedTestPaths, }: {
40
41
  valid: any;
41
42
  errors?: any[];
42
43
  missingCodePaths?: any[];
43
44
  missingTestPaths?: any[];
45
+ suggestedTestPaths?: {};
44
46
  }): {
45
47
  valid: any;
46
48
  errors: any[];
47
49
  missingCodePaths: any[];
48
50
  missingTestPaths: any[];
51
+ suggestedTestPaths: {};
49
52
  };
50
53
  /**
51
54
  * Run preflight validation for a WU
@@ -74,6 +77,7 @@ export declare function validatePreflight(id: any, options?: ValidatePreflightOp
74
77
  errors: any[];
75
78
  missingCodePaths: any[];
76
79
  missingTestPaths: any[];
80
+ suggestedTestPaths: {};
77
81
  }>;
78
82
  /**
79
83
  * Format preflight result as user-friendly message
@@ -84,3 +88,11 @@ export declare function validatePreflight(id: any, options?: ValidatePreflightOp
84
88
  */
85
89
  export declare function formatPreflightResult(id: any, result: any): string;
86
90
  export declare const PreflightResult: {};
91
+ /**
92
+ * Find suggested paths for missing test files
93
+ *
94
+ * @param {string[]} missingPaths - List of missing test paths
95
+ * @param {string} rootDir - Root directory to search in
96
+ * @returns {Promise<Record<string, string[]>>} Map of missing path -> suggestions
97
+ */
98
+ export declare function findSuggestedTestPaths(missingPaths: any, rootDir: any): Promise<{}>;