@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,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.
|
|
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.
|
|
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
|
package/dist/incremental-lint.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @file incremental-lint.
|
|
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.
|
|
18
|
+
* Matches ESLint ignores in apps/web/eslint.config.ts
|
|
19
19
|
* @type {string[]}
|
|
20
20
|
*/
|
|
21
21
|
const IGNORED_DIRECTORIES = [
|
package/dist/incremental-test.js
CHANGED
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';
|