@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,559 @@
1
+ /**
2
+ * Git Context Extractor Module (WU-1190)
3
+ *
4
+ * Extracts git history insights for LLM context enrichment:
5
+ * - Co-occurrence: files frequently changed together
6
+ * - Ownership: primary contributors to file/directory
7
+ * - Churn: change frequency metrics (hotspots)
8
+ *
9
+ * These signals help the LLM understand codebase relationships
10
+ * without algorithmic clustering - the LLM interprets the patterns.
11
+ */
12
+ import { execSync } from 'node:child_process';
13
+ // Constants
14
+ const DEFAULT_MAX_COMMITS = 500;
15
+ const DEFAULT_MAX_RESULTS = 20;
16
+ const CHARS_PER_TOKEN = 4; // Rough approximation
17
+ const DEFAULT_EXCLUDE_PATTERNS = ['*.lock', '*.yaml', '*.yml', '*.json', '*.md', 'pnpm-lock.yaml'];
18
+ // Pre-compiled regex patterns for performance
19
+ // Note: Using atomic groups or possessive quantifiers isn't supported in JS,
20
+ // so some patterns are implemented as functions to avoid backtracking
21
+ const NUMSTAT_LINE_REGEX = /^(\d+|-)\t(\d+|-)\t(.+)$/;
22
+ const DANGEROUS_CHARS_REGEX = /[;&|`$]/;
23
+ /**
24
+ * Parse shortlog line: " 10\tName <email>"
25
+ * Manual parsing to avoid slow regex backtracking
26
+ */
27
+ function parseShortlogFormat(line) {
28
+ // Skip leading whitespace
29
+ let i = 0;
30
+ while (i < line.length && (line[i] === ' ' || line[i] === '\t')) {
31
+ i++;
32
+ }
33
+ // Parse digits
34
+ const digitStart = i;
35
+ while (i < line.length && line[i] >= '0' && line[i] <= '9') {
36
+ i++;
37
+ }
38
+ if (i === digitStart)
39
+ return null; // No digits found
40
+ const count = parseInt(line.slice(digitStart, i), 10);
41
+ // Skip whitespace after digits
42
+ while (i < line.length && (line[i] === ' ' || line[i] === '\t')) {
43
+ i++;
44
+ }
45
+ // Rest is the name
46
+ const name = line.slice(i).trim();
47
+ if (!name)
48
+ return null;
49
+ return { count, name };
50
+ }
51
+ /**
52
+ * Check if a string is a git commit hash (40 hex chars)
53
+ * Uses character-by-character check to avoid slow regex backtracking
54
+ */
55
+ function isCommitHash(str) {
56
+ if (str.length !== 40)
57
+ return false;
58
+ for (const char of str) {
59
+ if (!((char >= '0' && char <= '9') || (char >= 'a' && char <= 'f'))) {
60
+ return false;
61
+ }
62
+ }
63
+ return true;
64
+ }
65
+ const SOURCE_FILE_EXCLUDE_PATTERNS = [
66
+ /\.lock$/,
67
+ /lock\.ya?ml$/,
68
+ /package-lock\.json$/,
69
+ /yarn\.lock$/,
70
+ /\.gitignore$/,
71
+ /\.env/,
72
+ /node_modules/,
73
+ /dist\//,
74
+ /build\//,
75
+ /\.min\./,
76
+ ];
77
+ /**
78
+ * Execute a git command safely, returning empty string on error.
79
+ *
80
+ * SECURITY: Commands are constructed from static arguments (no user input) to prevent injection.
81
+ * The args array is joined into a command string for execSync.
82
+ * All callers pass only internally-constructed arguments.
83
+ */
84
+ function safeGitExec(args, cwd) {
85
+ try {
86
+ // Join args into a command string
87
+ // SECURITY: all args are constructed internally (no user input)
88
+ const cmd = ['git', ...args].join(' ');
89
+ // SECURITY: execSync is safe here because:
90
+ // 1. 'git' is a fixed command from PATH (trusted)
91
+ // 2. args are internally constructed, not from user input
92
+ // 3. cwd is validated by the caller (projectRoot from CLI)
93
+ // eslint-disable-next-line sonarjs/os-command -- safe: git is trusted, args are static
94
+ const result = execSync(cmd, {
95
+ cwd,
96
+ encoding: 'utf-8',
97
+ maxBuffer: 10 * 1024 * 1024, // 10MB
98
+ timeout: 30000, // 30 seconds
99
+ });
100
+ return result;
101
+ }
102
+ catch {
103
+ return '';
104
+ }
105
+ }
106
+ /**
107
+ * Check if commit count indicates limited history
108
+ */
109
+ function hasLimitedCommitCount(projectRoot) {
110
+ const commitCountStr = safeGitExec(['rev-list', '--count', 'HEAD'], projectRoot).trim();
111
+ const count = parseInt(commitCountStr, 10) || 0;
112
+ return { limited: count < 10, count };
113
+ }
114
+ /**
115
+ * Extract complete git context from a repository
116
+ */
117
+ export function extractGitContext(projectRoot, options = {}) {
118
+ const result = {
119
+ coOccurrences: [],
120
+ ownership: [],
121
+ churn: [],
122
+ hasLimitedHistory: false,
123
+ };
124
+ try {
125
+ // Check if this is a git repo using execSync directly
126
+ // SECURITY: This is a static command with no user input
127
+ // eslint-disable-next-line sonarjs/no-os-command-from-path -- safe: static command
128
+ execSync('git rev-parse --is-inside-work-tree', {
129
+ cwd: projectRoot,
130
+ encoding: 'utf-8',
131
+ });
132
+ const { limited, count } = hasLimitedCommitCount(projectRoot);
133
+ if (limited) {
134
+ result.hasLimitedHistory = true;
135
+ result.error = `Repository has fewer than 10 commits (found ${count})`;
136
+ return result;
137
+ }
138
+ // Extract co-occurrences
139
+ result.coOccurrences = getFileCoOccurrence(projectRoot, {
140
+ maxCommits: options.maxCommits ?? DEFAULT_MAX_COMMITS,
141
+ since: options.since,
142
+ });
143
+ // Get top-level directories for ownership analysis
144
+ const topDirs = getTopLevelDirs(projectRoot);
145
+ result.ownership = getOwnershipSignals(projectRoot, topDirs);
146
+ // Extract churn metrics
147
+ result.churn = getChurnMetrics(projectRoot, {
148
+ maxCommits: options.maxCommits ?? DEFAULT_MAX_COMMITS,
149
+ since: options.since,
150
+ });
151
+ }
152
+ catch (error) {
153
+ handleExtractionError(result, error);
154
+ }
155
+ return result;
156
+ }
157
+ /**
158
+ * Handle extraction errors and set appropriate error messages
159
+ */
160
+ function handleExtractionError(result, error) {
161
+ result.hasLimitedHistory = true;
162
+ if (error instanceof Error) {
163
+ if (error.message.includes('not a git repository')) {
164
+ result.error = 'not a git repository';
165
+ }
166
+ else if (error.message.includes('does not have any commits')) {
167
+ result.error = 'no commits in repository';
168
+ }
169
+ else {
170
+ result.error = error.message;
171
+ }
172
+ }
173
+ }
174
+ /**
175
+ * Get top-level directories for ownership analysis
176
+ */
177
+ function getTopLevelDirs(projectRoot) {
178
+ const output = safeGitExec(['ls-tree', '-d', '--name-only', 'HEAD'], projectRoot);
179
+ return output
180
+ .split('\n')
181
+ .filter((d) => d.trim() && !d.startsWith('.'))
182
+ .slice(0, 20); // Limit to first 20 directories
183
+ }
184
+ /**
185
+ * Parse git log output into commits with their files
186
+ */
187
+ function parseCommitsFromLog(output) {
188
+ const commits = [];
189
+ let currentFiles = [];
190
+ for (const line of output.split('\n')) {
191
+ const trimmed = line.trim();
192
+ if (!trimmed) {
193
+ if (currentFiles.length > 0) {
194
+ commits.push([...currentFiles]);
195
+ currentFiles = [];
196
+ }
197
+ continue;
198
+ }
199
+ // Skip commit hashes (40 hex chars)
200
+ if (isCommitHash(trimmed)) {
201
+ if (currentFiles.length > 0) {
202
+ commits.push([...currentFiles]);
203
+ currentFiles = [];
204
+ }
205
+ continue;
206
+ }
207
+ // Only track source files
208
+ if (isSourceFile(trimmed)) {
209
+ currentFiles.push(trimmed);
210
+ }
211
+ }
212
+ if (currentFiles.length > 0) {
213
+ commits.push(currentFiles);
214
+ }
215
+ return commits;
216
+ }
217
+ /**
218
+ * Count file pair co-occurrences from commits
219
+ */
220
+ function countFilePairs(commits) {
221
+ const pairCounts = new Map();
222
+ for (const files of commits) {
223
+ if (files.length < 2)
224
+ continue;
225
+ // Generate all pairs
226
+ for (let i = 0; i < files.length; i++) {
227
+ for (let j = i + 1; j < files.length; j++) {
228
+ const pair = [files[i], files[j]]
229
+ .slice()
230
+ .sort((a, b) => a.localeCompare(b))
231
+ .join('::');
232
+ pairCounts.set(pair, (pairCounts.get(pair) ?? 0) + 1);
233
+ }
234
+ }
235
+ }
236
+ return pairCounts;
237
+ }
238
+ /**
239
+ * Extract file co-occurrence patterns from git history
240
+ */
241
+ export function getFileCoOccurrence(projectRoot, options = {}) {
242
+ const maxCommits = options.maxCommits ?? DEFAULT_MAX_COMMITS;
243
+ const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
244
+ // Build git log args array (safe - no user input in paths)
245
+ const args = ['log', `-n`, String(maxCommits)];
246
+ if (options.since) {
247
+ args.push(`--since=${options.since}`);
248
+ }
249
+ args.push('--name-only', '--pretty=format:%H', '--diff-filter=ACMRT');
250
+ const output = safeGitExec(args, projectRoot);
251
+ if (!output.trim()) {
252
+ return [];
253
+ }
254
+ const commits = parseCommitsFromLog(output);
255
+ const pairCounts = countFilePairs(commits);
256
+ // Convert to array and filter
257
+ const coOccurrences = [];
258
+ for (const [pair, count] of pairCounts.entries()) {
259
+ if (count >= 2) {
260
+ // Only include pairs that co-occurred at least twice
261
+ const [file1, file2] = pair.split('::');
262
+ coOccurrences.push({ file1, file2, count });
263
+ }
264
+ }
265
+ // Sort by count descending and limit results (use toSorted to avoid mutation)
266
+ return coOccurrences
267
+ .slice()
268
+ .sort((a, b) => b.count - a.count)
269
+ .slice(0, maxResults);
270
+ }
271
+ /**
272
+ * Check if a file is a source file (not config, lock, etc.)
273
+ */
274
+ function isSourceFile(filePath) {
275
+ return !SOURCE_FILE_EXCLUDE_PATTERNS.some((pattern) => pattern.test(filePath));
276
+ }
277
+ /**
278
+ * Parse a single contributor line from shortlog output
279
+ */
280
+ function parseShortlogLine(line) {
281
+ const result = parseShortlogFormat(line);
282
+ if (!result)
283
+ return null;
284
+ return {
285
+ count: result.count,
286
+ name: result.name,
287
+ };
288
+ }
289
+ /**
290
+ * Extract ownership signal for a single path
291
+ */
292
+ function extractOwnershipForPath(projectRoot, targetPath) {
293
+ // Validate path doesn't contain dangerous characters
294
+ if (DANGEROUS_CHARS_REGEX.test(targetPath)) {
295
+ return {
296
+ path: targetPath,
297
+ primaryOwner: null,
298
+ contributors: [],
299
+ commitCount: 0,
300
+ };
301
+ }
302
+ // Use array form for safety
303
+ const args = ['shortlog', '-sne', '--all', '--', targetPath];
304
+ const output = safeGitExec(args, projectRoot);
305
+ if (!output.trim()) {
306
+ return {
307
+ path: targetPath,
308
+ primaryOwner: null,
309
+ contributors: [],
310
+ commitCount: 0,
311
+ };
312
+ }
313
+ // Parse shortlog output
314
+ const contributors = [];
315
+ let totalCommits = 0;
316
+ for (const line of output.split('\n')) {
317
+ const parsed = parseShortlogLine(line);
318
+ if (parsed) {
319
+ contributors.push(parsed);
320
+ totalCommits += parsed.count;
321
+ }
322
+ }
323
+ // Sort by commit count descending (use toSorted to avoid mutation)
324
+ const sorted = contributors.slice().sort((a, b) => b.count - a.count);
325
+ return {
326
+ path: targetPath,
327
+ primaryOwner: sorted[0]?.name ?? null,
328
+ contributors: sorted.map((c) => c.name),
329
+ commitCount: totalCommits,
330
+ };
331
+ }
332
+ /**
333
+ * Extract ownership signals for specified paths.
334
+ * Note: paths are validated internally and not derived from user input.
335
+ */
336
+ export function getOwnershipSignals(projectRoot, paths) {
337
+ return paths.map((path) => extractOwnershipForPath(projectRoot, path));
338
+ }
339
+ /**
340
+ * Parse a single numstat line from git log output
341
+ */
342
+ function parseNumstatLine(line) {
343
+ const match = NUMSTAT_LINE_REGEX.exec(line);
344
+ if (!match)
345
+ return null;
346
+ return {
347
+ additions: match[1] === '-' ? 0 : parseInt(match[1], 10),
348
+ deletions: match[2] === '-' ? 0 : parseInt(match[2], 10),
349
+ filePath: match[3],
350
+ };
351
+ }
352
+ /**
353
+ * Check if a file matches any exclude pattern using simple glob matching
354
+ */
355
+ function matchesExcludePattern(filePath, patterns) {
356
+ return patterns.some((pattern) => {
357
+ // Simple glob matching using a state machine approach
358
+ // Avoids control characters and dynamic regex construction
359
+ return globMatch(filePath, pattern);
360
+ });
361
+ }
362
+ /**
363
+ * Simple glob matching without regex
364
+ * Supports * (match any) and ? (match single char)
365
+ */
366
+ function globMatch(str, pattern) {
367
+ let si = 0; // string index
368
+ let pi = 0; // pattern index
369
+ let starIdx = -1;
370
+ let matchIdx = 0;
371
+ while (si < str.length) {
372
+ if (pi < pattern.length && (pattern[pi] === '?' || pattern[pi] === str[si])) {
373
+ // Character match or ? wildcard
374
+ si++;
375
+ pi++;
376
+ }
377
+ else if (pi < pattern.length && pattern[pi] === '*') {
378
+ // Star found, mark position
379
+ starIdx = pi;
380
+ matchIdx = si;
381
+ pi++;
382
+ }
383
+ else if (starIdx !== -1) {
384
+ // Mismatch after star, backtrack
385
+ pi = starIdx + 1;
386
+ matchIdx++;
387
+ si = matchIdx;
388
+ }
389
+ else {
390
+ // Mismatch without star
391
+ return false;
392
+ }
393
+ }
394
+ // Skip trailing stars
395
+ while (pi < pattern.length && pattern[pi] === '*') {
396
+ pi++;
397
+ }
398
+ return pi === pattern.length;
399
+ }
400
+ /**
401
+ * Aggregate file statistics from numstat output
402
+ */
403
+ function aggregateFileStats(output, excludePatterns) {
404
+ const fileStats = new Map();
405
+ for (const line of output.split('\n')) {
406
+ const trimmed = line.trim();
407
+ if (!trimmed)
408
+ continue;
409
+ const parsed = parseNumstatLine(trimmed);
410
+ if (!parsed)
411
+ continue;
412
+ // Skip excluded patterns
413
+ if (matchesExcludePattern(parsed.filePath, excludePatterns))
414
+ continue;
415
+ // Skip non-source files
416
+ if (!isSourceFile(parsed.filePath))
417
+ continue;
418
+ const existing = fileStats.get(parsed.filePath) ?? { additions: 0, deletions: 0, commits: 0 };
419
+ fileStats.set(parsed.filePath, {
420
+ additions: existing.additions + parsed.additions,
421
+ deletions: existing.deletions + parsed.deletions,
422
+ commits: existing.commits + 1,
423
+ });
424
+ }
425
+ return fileStats;
426
+ }
427
+ /**
428
+ * Extract churn metrics for the repository
429
+ */
430
+ export function getChurnMetrics(projectRoot, options = {}) {
431
+ const maxCommits = options.maxCommits ?? DEFAULT_MAX_COMMITS;
432
+ const maxResults = options.maxResults ?? DEFAULT_MAX_RESULTS;
433
+ const excludePatterns = options.excludePatterns ?? DEFAULT_EXCLUDE_PATTERNS;
434
+ // Build git log args array
435
+ const args = ['log', `-n`, String(maxCommits)];
436
+ if (options.since) {
437
+ args.push(`--since=${options.since}`);
438
+ }
439
+ args.push('--numstat', '--pretty=format:');
440
+ let output;
441
+ try {
442
+ /// SECURITY: all args are constructed internally (no user input)
443
+ const cmd = ['git', ...args].join(' ');
444
+ // eslint-disable-next-line sonarjs/os-command -- safe: git is trusted, args are static
445
+ output = execSync(cmd, {
446
+ cwd: projectRoot,
447
+ encoding: 'utf-8',
448
+ maxBuffer: 10 * 1024 * 1024,
449
+ timeout: 30000,
450
+ });
451
+ }
452
+ catch {
453
+ return [];
454
+ }
455
+ if (!output.trim()) {
456
+ return [];
457
+ }
458
+ const fileStats = aggregateFileStats(output, excludePatterns);
459
+ // Convert to array with churn scores
460
+ const metrics = [];
461
+ for (const [filePath, stats] of fileStats.entries()) {
462
+ metrics.push({
463
+ filePath,
464
+ additions: stats.additions,
465
+ deletions: stats.deletions,
466
+ churnScore: stats.additions + stats.deletions,
467
+ commitCount: stats.commits,
468
+ });
469
+ }
470
+ // Sort by churn score descending and limit results (use toSorted to avoid mutation)
471
+ return metrics
472
+ .slice()
473
+ .sort((a, b) => b.churnScore - a.churnScore)
474
+ .slice(0, maxResults);
475
+ }
476
+ /**
477
+ * Build co-occurrence section for summary
478
+ */
479
+ function buildCoOccurrenceSection(coOccurrences) {
480
+ const lines = ['## Co-occurrence Patterns', 'Files frequently changed together:'];
481
+ for (const co of coOccurrences.slice(0, 10)) {
482
+ lines.push(`- ${co.file1} <-> ${co.file2} (${co.count} commits)`);
483
+ }
484
+ return lines.join('\n');
485
+ }
486
+ /**
487
+ * Build ownership section for summary
488
+ */
489
+ function buildOwnershipSection(ownership) {
490
+ const lines = ['## Ownership Signals', 'Primary contributors by area:'];
491
+ for (const own of ownership.filter((o) => o.primaryOwner)) {
492
+ const ownerName = own.primaryOwner?.split(' <')[0] ?? 'unknown';
493
+ lines.push(`- ${own.path}: ${ownerName} (${own.commitCount} commits)`);
494
+ }
495
+ return lines.join('\n');
496
+ }
497
+ /**
498
+ * Build churn section for summary
499
+ */
500
+ function buildChurnSection(churn) {
501
+ const lines = ['## Churn Hotspots', 'High-change files (potential complexity):'];
502
+ for (const ch of churn.slice(0, 10)) {
503
+ lines.push(`- ${ch.filePath}: ${ch.churnScore} lines changed across ${ch.commitCount} commits`);
504
+ }
505
+ return lines.join('\n');
506
+ }
507
+ /**
508
+ * Truncate a section to fit within character limit
509
+ */
510
+ function truncateSection(section, maxChars) {
511
+ if (section.length <= maxChars)
512
+ return section;
513
+ const lines = section.split('\n');
514
+ const header = lines.slice(0, 2).join('\n');
515
+ const items = lines.slice(2);
516
+ const availableChars = maxChars - header.length - 20;
517
+ const truncatedItems = [];
518
+ let currentChars = 0;
519
+ for (const item of items) {
520
+ if (currentChars + item.length > availableChars)
521
+ break;
522
+ truncatedItems.push(item);
523
+ currentChars += item.length + 1;
524
+ }
525
+ return [header, ...truncatedItems, '(truncated)'].join('\n');
526
+ }
527
+ /**
528
+ * Summarize git context for LLM prompt inclusion
529
+ *
530
+ * Produces a token-efficient summary that fits within specified limits.
531
+ */
532
+ export function summarizeGitContext(context, options = {}) {
533
+ const maxTokens = options.maxTokens ?? 500;
534
+ const maxChars = maxTokens * CHARS_PER_TOKEN;
535
+ // Handle limited history case
536
+ if (context.hasLimitedHistory) {
537
+ return context.error
538
+ ? `Git history analysis limited: ${context.error}`
539
+ : 'Git history analysis limited due to sparse commit history.';
540
+ }
541
+ const sections = [];
542
+ if (context.coOccurrences.length > 0) {
543
+ sections.push(buildCoOccurrenceSection(context.coOccurrences));
544
+ }
545
+ if (context.ownership.length > 0) {
546
+ sections.push(buildOwnershipSection(context.ownership));
547
+ }
548
+ if (context.churn.length > 0) {
549
+ sections.push(buildChurnSection(context.churn));
550
+ }
551
+ let result = sections.join('\n\n');
552
+ // Truncate if needed
553
+ if (result.length > maxChars && sections.length > 0) {
554
+ const charsPerSection = Math.floor(maxChars / sections.length) - 20;
555
+ const truncatedSections = sections.map((s) => truncateSection(s, charsPerSection));
556
+ result = truncatedSections.join('\n\n');
557
+ }
558
+ return result || 'Git history analysis limited due to sparse commit history.';
559
+ }
@@ -50,7 +50,7 @@ export interface FindHardcodedPathViolationsOptions {
50
50
  */
51
51
  export declare function findHardcodedPathViolations(line: any, options?: FindHardcodedPathViolationsOptions): any[];
52
52
  /**
53
- * Legacy function for backwards compatibility with gates-pre-commit.mjs
53
+ * Legacy function for backwards compatibility with gates-pre-commit.ts
54
54
  * Detects all hardcoded string patterns (not just paths)
55
55
  *
56
56
  * @deprecated Use findHardcodedPathViolations for path-specific detection
@@ -249,7 +249,7 @@ export function findHardcodedPathViolations(line, options = {}) {
249
249
  return violations;
250
250
  }
251
251
  /**
252
- * Legacy function for backwards compatibility with gates-pre-commit.mjs
252
+ * Legacy function for backwards compatibility with gates-pre-commit.ts
253
253
  * Detects all hardcoded string patterns (not just paths)
254
254
  *
255
255
  * @deprecated Use findHardcodedPathViolations for path-specific detection
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file incremental-lint.mjs
2
+ * @file incremental-lint.ts
3
3
  * @description Incremental linting utilities for gates
4
4
  * WU-1304: Optimise ESLint gates performance
5
5
  *
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file incremental-lint.mjs
2
+ * @file incremental-lint.ts
3
3
  * @description Incremental linting utilities for gates
4
4
  * WU-1304: Optimise ESLint gates performance
5
5
  *
@@ -15,7 +15,7 @@ import { STRING_LITERALS } from './wu-constants.js';
15
15
  export const LINTABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.js'];
16
16
  /**
17
17
  * Directory patterns that should be ignored
18
- * Matches ESLint ignores in apps/web/eslint.config.mjs
18
+ * Matches ESLint ignores in apps/web/eslint.config.ts
19
19
  * @type {string[]}
20
20
  */
21
21
  const IGNORED_DIRECTORIES = [
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file incremental-test.mjs
2
+ * @file incremental-test.ts
3
3
  * @description Helpers for Vitest --changed execution
4
4
  *
5
5
  * WU-1920: Add incremental test execution to gates
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @file incremental-test.mjs
2
+ * @file incremental-test.ts
3
3
  * @description Helpers for Vitest --changed execution
4
4
  *
5
5
  * WU-1920: Add incremental test execution to gates
package/dist/index.d.ts CHANGED
@@ -44,12 +44,17 @@ export * from './dependency-guard.js';
44
44
  export * from './stamp-utils.js';
45
45
  export * from './lumenflow-config.js';
46
46
  export * from './lumenflow-config-schema.js';
47
+ export * from './wu-events-cleanup.js';
48
+ export * from './state-cleanup-core.js';
49
+ export * from './state-doctor-core.js';
47
50
  export * from './gates-config.js';
48
51
  export * from './branch-check.js';
49
52
  export * from './agent-patterns-registry.js';
50
53
  export * from './lumenflow-home.js';
51
54
  export * from './force-bypass-audit.js';
52
55
  export { LUMENFLOW_PATHS, BEACON_PATHS } from './wu-constants.js';
56
+ export { STREAM_ERRORS, EXIT_CODES } from './wu-constants.js';
57
+ export * from './stream-error-handler.js';
53
58
  export * from './color-support.js';
54
59
  export * from './context/index.js';
55
60
  export * from './validation/index.js';
@@ -69,4 +74,12 @@ export { VALIDATION_ERROR_CODE_VALUES, ValidationErrorCodeSchema, PREDICATE_SEVE
69
74
  export { RECOVERY_ISSUE_CODE_VALUES, RecoveryIssueCodeSchema, RECOVERY_ACTION_TYPE_VALUES, RecoveryActionTypeSchema, RecoveryIssueSchema, RecoveryActionSchema, RecoveryAnalysisSchema, } from './domain/recovery.schemas.js';
70
75
  export { SimpleGitLocationAdapter, SimpleGitStateAdapter, FileSystemWuStateAdapter, CommandRegistryAdapter, RecoveryAnalyzerAdapter, } from './adapters/index.js';
71
76
  export { ComputeContextUseCase, type ComputeContextOptions, ValidateCommandUseCase, AnalyzeRecoveryUseCase, } from './usecases/index.js';
77
+ export * from './lane-suggest-prompt.js';
78
+ export * from './git-context-extractor.js';
72
79
  export { createContextAdapters, createValidationAdapters, createRecoveryAdapters, createComputeContextUseCase, createValidateCommandUseCase, createAnalyzeRecoveryUseCase, computeWuContext, validateCommand, analyzeRecoveryIssues, type ContextAdapters, type ValidationAdapters, type RecoveryAdapters, type CreateComputeContextOptions, type CreateValidateCommandOptions, type CreateAnalyzeRecoveryOptions, } from './context-di.js';
80
+ export * from './wu-id-generator.js';
81
+ export * from './test-baseline.js';
82
+ export { loadManifest, loadTemplate, loadTemplatesWithOverrides, assembleTemplates, replaceTokens, evaluateCondition, type TemplateFrontmatter, type LoadedTemplate, type ManifestEntry, type TemplateManifest, type TemplateContext, } from './template-loader.js';
83
+ export { tryAssembleSpawnTemplates, buildTemplateContext } from './wu-spawn.js';
84
+ export * from './patrol-loop.js';
85
+ export { resolvePolicy, getDefaultPolicy, MethodologyConfigSchema, MethodologyOverridesSchema, TestingMethodologySchema, ArchitectureMethodologySchema, CoverageModeSchema, TESTING_METHODOLOGY, ARCHITECTURE_METHODOLOGY, COVERAGE_MODE, type ResolvedPolicy, type ResolvePolicyOptions, type MethodologyConfig, type MethodologyOverrides, type TestingMethodology, type ArchitectureMethodology, type CoverageMode, } from './resolve-policy.js';