@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.
- package/dist/active-wu-detector.d.ts +1 -1
- package/dist/active-wu-detector.js +1 -1
- package/dist/arg-parser.js +51 -18
- package/dist/backlog-generator.d.ts +4 -4
- package/dist/backlog-generator.js +4 -4
- package/dist/backlog-sync-validator.js +1 -1
- package/dist/cleanup-lock.d.ts +9 -2
- package/dist/cleanup-lock.js +17 -7
- package/dist/code-path-validator.d.ts +3 -3
- package/dist/code-path-validator.js +3 -3
- package/dist/compliance-parser.d.ts +1 -1
- package/dist/compliance-parser.js +1 -1
- package/dist/constants/backlog-patterns.d.ts +1 -1
- package/dist/constants/backlog-patterns.js +1 -1
- package/dist/constants/dora-constants.d.ts +1 -1
- package/dist/constants/dora-constants.js +1 -1
- package/dist/constants/gate-constants.d.ts +1 -1
- package/dist/constants/gate-constants.js +1 -1
- package/dist/constants/linter-constants.d.ts +1 -1
- package/dist/constants/linter-constants.js +1 -1
- package/dist/constants/tokenizer-constants.d.ts +1 -1
- package/dist/constants/tokenizer-constants.js +1 -1
- package/dist/context/location-resolver.js +2 -1
- package/dist/context-validation-integration.d.ts +1 -0
- package/dist/core/scope-checker.d.ts +3 -3
- package/dist/core/scope-checker.js +3 -3
- package/dist/core/tool-runner.d.ts +5 -5
- package/dist/core/tool-runner.js +5 -5
- package/dist/core/tool.constants.d.ts +1 -1
- package/dist/core/tool.constants.js +1 -1
- package/dist/core/tool.schemas.d.ts +2 -2
- package/dist/core/tool.schemas.js +1 -1
- package/dist/core/worktree-guard.d.ts +1 -1
- package/dist/core/worktree-guard.js +1 -1
- package/dist/coverage-gate.d.ts +12 -3
- package/dist/coverage-gate.js +15 -8
- package/dist/date-utils.d.ts +4 -4
- package/dist/date-utils.js +4 -4
- package/dist/dependency-graph.d.ts +6 -0
- package/dist/dependency-graph.js +43 -2
- package/dist/dependency-guard.d.ts +2 -2
- package/dist/dependency-guard.js +3 -3
- package/dist/dependency-validator.d.ts +4 -4
- package/dist/dependency-validator.js +4 -7
- package/dist/domain/orchestration.constants.d.ts +31 -10
- package/dist/domain/orchestration.constants.js +45 -16
- package/dist/domain/orchestration.schemas.d.ts +54 -28
- package/dist/domain/orchestration.schemas.js +2 -2
- package/dist/domain/orchestration.types.d.ts +2 -2
- package/dist/domain/orchestration.types.js +2 -2
- package/dist/error-handler.d.ts +10 -10
- package/dist/error-handler.js +10 -10
- package/dist/file-classifiers.d.ts +6 -6
- package/dist/file-classifiers.js +6 -6
- package/dist/gates-config.d.ts +74 -0
- package/dist/gates-config.js +209 -2
- package/dist/git-adapter.d.ts +11 -11
- package/dist/git-adapter.js +11 -11
- package/dist/git-context-extractor.d.ts +112 -0
- package/dist/git-context-extractor.js +559 -0
- package/dist/hardcoded-strings.d.ts +1 -1
- package/dist/hardcoded-strings.js +1 -1
- package/dist/incremental-lint.d.ts +1 -1
- package/dist/incremental-lint.js +2 -2
- package/dist/incremental-test.d.ts +1 -1
- package/dist/incremental-test.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +25 -0
- package/dist/invariants/check-automated-tests.d.ts +2 -2
- package/dist/invariants/check-automated-tests.js +3 -3
- package/dist/lane-checker.d.ts +28 -7
- package/dist/lane-checker.js +316 -159
- package/dist/lane-suggest-prompt.d.ts +108 -0
- package/dist/lane-suggest-prompt.js +359 -0
- package/dist/lane-validator.d.ts +3 -3
- package/dist/lane-validator.js +3 -3
- package/dist/logs-lib.d.ts +1 -1
- package/dist/logs-lib.js +1 -1
- package/dist/lumenflow-config-schema.d.ts +162 -0
- package/dist/lumenflow-config-schema.js +180 -0
- package/dist/manual-test-validator.d.ts +2 -2
- package/dist/manual-test-validator.js +3 -3
- package/dist/merge-lock.d.ts +8 -1
- package/dist/merge-lock.js +16 -7
- package/dist/micro-worktree.d.ts +81 -13
- package/dist/micro-worktree.js +98 -17
- package/dist/migration-deployer.d.ts +1 -1
- package/dist/migration-deployer.js +1 -1
- package/dist/orchestration-advisory-loader.d.ts +2 -2
- package/dist/orchestration-advisory-loader.js +10 -6
- package/dist/orchestration-advisory.d.ts +3 -3
- package/dist/orchestration-advisory.js +4 -4
- package/dist/orchestration-di.d.ts +4 -4
- package/dist/orchestration-di.js +4 -4
- package/dist/orchestration-rules.d.ts +4 -4
- package/dist/orchestration-rules.js +18 -10
- package/dist/orphan-detector.d.ts +3 -3
- package/dist/orphan-detector.js +3 -3
- package/dist/patrol-loop.d.ts +170 -0
- package/dist/patrol-loop.js +186 -0
- package/dist/process-detector.d.ts +5 -5
- package/dist/process-detector.js +5 -5
- package/dist/rebase-artifact-cleanup.d.ts +3 -3
- package/dist/rebase-artifact-cleanup.js +3 -3
- package/dist/resolve-policy.d.ts +195 -0
- package/dist/resolve-policy.js +203 -0
- package/dist/risk-detector.d.ts +2 -2
- package/dist/risk-detector.js +2 -2
- package/dist/rollback-utils.d.ts +1 -1
- package/dist/rollback-utils.js +1 -1
- package/dist/section-headings.d.ts +1 -1
- package/dist/section-headings.js +1 -1
- package/dist/spawn-escalation.d.ts +4 -4
- package/dist/spawn-escalation.js +3 -3
- package/dist/spawn-monitor.d.ts +4 -4
- package/dist/spawn-monitor.js +4 -4
- package/dist/spawn-recovery.d.ts +3 -3
- package/dist/spawn-recovery.js +3 -3
- package/dist/spawn-registry-schema.d.ts +2 -2
- package/dist/spawn-registry-schema.js +2 -2
- package/dist/spawn-registry-store.d.ts +2 -2
- package/dist/spawn-registry-store.js +2 -2
- package/dist/spawn-strategy.d.ts +17 -11
- package/dist/spawn-strategy.js +47 -44
- package/dist/spawn-tree.d.ts +3 -3
- package/dist/spawn-tree.js +3 -3
- package/dist/state-cleanup-core.d.ts +205 -0
- package/dist/state-cleanup-core.js +240 -0
- package/dist/state-doctor-core.d.ts +168 -0
- package/dist/state-doctor-core.js +251 -0
- package/dist/stream-error-handler.d.ts +67 -0
- package/dist/stream-error-handler.js +94 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/template-loader.d.ts +162 -0
- package/dist/template-loader.js +372 -0
- package/dist/test-baseline.d.ts +176 -0
- package/dist/test-baseline.js +282 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
- package/dist/validation/command-registry.js +37 -0
- package/dist/validators/backlog-sync.js +4 -2
- package/dist/worktree-scanner.d.ts +1 -1
- package/dist/worktree-scanner.js +1 -1
- package/dist/worktree-symlink.d.ts +3 -3
- package/dist/worktree-symlink.js +3 -3
- package/dist/wu-backlog-updater.d.ts +1 -1
- package/dist/wu-backlog-updater.js +1 -1
- package/dist/wu-claim-helpers.d.ts +1 -1
- package/dist/wu-claim-helpers.js +1 -1
- package/dist/wu-claim-resume.d.ts +1 -1
- package/dist/wu-claim-resume.js +1 -1
- package/dist/wu-consistency-checker.d.ts +1 -1
- package/dist/wu-consistency-checker.js +17 -11
- package/dist/wu-constants.d.ts +73 -21
- package/dist/wu-constants.js +65 -22
- package/dist/wu-done-branch-only.d.ts +1 -1
- package/dist/wu-done-branch-only.js +1 -1
- package/dist/wu-done-docs-generate.d.ts +1 -1
- package/dist/wu-done-docs-generate.js +1 -1
- package/dist/wu-done-messages.d.ts +2 -2
- package/dist/wu-done-messages.js +2 -2
- package/dist/wu-done-metadata.d.ts +3 -3
- package/dist/wu-done-metadata.js +3 -3
- package/dist/wu-done-pr.d.ts +1 -1
- package/dist/wu-done-pr.js +4 -2
- package/dist/wu-done-preflight.d.ts +8 -0
- package/dist/wu-done-preflight.js +18 -2
- package/dist/wu-done-ui.d.ts +3 -3
- package/dist/wu-done-ui.js +3 -3
- package/dist/wu-done-validation.d.ts +30 -0
- package/dist/wu-done-validation.js +106 -1
- package/dist/wu-done-worktree.d.ts +1 -1
- package/dist/wu-done-worktree.js +11 -1
- package/dist/wu-events-cleanup.d.ts +148 -0
- package/dist/wu-events-cleanup.js +401 -0
- package/dist/wu-helpers.d.ts +2 -2
- package/dist/wu-helpers.js +2 -2
- package/dist/wu-id-generator.d.ts +58 -0
- package/dist/wu-id-generator.js +103 -0
- package/dist/wu-lint.js +1 -1
- package/dist/wu-preflight-validators.d.ts +13 -1
- package/dist/wu-preflight-validators.js +56 -1
- package/dist/wu-recovery.d.ts +2 -2
- package/dist/wu-recovery.js +4 -4
- package/dist/wu-repair-core.d.ts +5 -5
- package/dist/wu-repair-core.js +6 -6
- package/dist/wu-schema-normalization.d.ts +1 -1
- package/dist/wu-schema-normalization.js +1 -1
- package/dist/wu-schema.d.ts +7 -7
- package/dist/wu-schema.js +8 -8
- package/dist/wu-spawn-context.d.ts +87 -0
- package/dist/wu-spawn-context.js +175 -0
- package/dist/wu-spawn-helpers.d.ts +1 -1
- package/dist/wu-spawn-helpers.js +1 -1
- package/dist/wu-spawn.d.ts +177 -4
- package/dist/wu-spawn.js +694 -72
- package/dist/wu-state-schema.d.ts +1 -1
- package/dist/wu-state-schema.js +1 -1
- package/dist/wu-state-store.d.ts +3 -3
- package/dist/wu-state-store.js +3 -3
- package/dist/wu-status-transition.d.ts +1 -1
- package/dist/wu-status-transition.js +1 -1
- package/dist/wu-status-updater.d.ts +3 -3
- package/dist/wu-status-updater.js +3 -3
- package/dist/wu-validation-constants.d.ts +2 -2
- package/dist/wu-validation-constants.js +2 -2
- package/dist/wu-validation.d.ts +3 -3
- package/dist/wu-validation.js +3 -3
- package/dist/wu-yaml-fixer.d.ts +2 -2
- package/dist/wu-yaml-fixer.js +3 -3
- package/dist/wu-yaml.d.ts +23 -0
- package/dist/wu-yaml.js +76 -2
- 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
|
+
}
|
package/dist/wu-helpers.d.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
/**
|
|
11
11
|
* Validate WU ID format
|
|
12
12
|
*
|
|
13
|
-
* WU-1593: Extracted from duplicate implementations in wu-create.
|
|
14
|
-
* Uses centralized PATTERNS.WU_ID regex from wu-constants.
|
|
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
|
package/dist/wu-helpers.js
CHANGED
|
@@ -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.
|
|
21
|
-
* Uses centralized PATTERNS.WU_ID regex from wu-constants.
|
|
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.
|
|
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<{}>;
|