minovative-mind-cli 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -25
- package/dist/commands/chat.js +1 -1
- package/dist/services/agent/inputHandler.d.ts +9 -0
- package/dist/services/agent/inputHandler.js +34 -0
- package/dist/services/agent/slashCommands.js +158 -37
- package/dist/services/agent/syntaxAgent.d.ts +40 -0
- package/dist/services/agent/syntaxAgent.js +237 -23
- package/dist/services/agent/toolLoop.js +10 -1
- package/dist/services/agent/types.d.ts +1 -0
- package/dist/services/agent-tools.d.ts +156 -1
- package/dist/services/agent-tools.js +259 -67
- package/dist/services/agent.d.ts +74 -0
- package/dist/services/agent.js +192 -30
- package/dist/services/ai.d.ts +5 -0
- package/dist/services/ai.js +80 -87
- package/dist/services/chatHistoryService.d.ts +11 -0
- package/dist/services/chatHistoryService.js +20 -1
- package/dist/services/contextAgent.d.ts +1 -1
- package/dist/services/contextAgent.js +9 -29
- package/dist/services/orchestration/investigationAgent.d.ts +2 -1
- package/dist/services/orchestration/investigationAgent.js +7 -2
- package/dist/services/orchestration/investigationOrchestrator.d.ts +1 -1
- package/dist/services/orchestration/investigationOrchestrator.js +13 -2
- package/dist/services/orchestration/orchestrator.js +21 -4
- package/dist/services/orchestration/subAgent.d.ts +2 -1
- package/dist/services/orchestration/subAgent.js +12 -6
- package/dist/services/workspaceRegistry.d.ts +7 -0
- package/dist/services/workspaceRegistry.js +22 -0
- package/dist/utils/analysisRunner.d.ts +27 -4
- package/dist/utils/analysisRunner.js +100 -20
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +2 -0
- package/dist/utils/fuzzyMatch.d.ts +32 -0
- package/dist/utils/fuzzyMatch.js +215 -27
- package/dist/utils/localSyntaxValidator.d.ts +2 -2
- package/dist/utils/localSyntaxValidator.js +280 -81
- package/dist/utils/performanceAuditor.d.ts +2 -7
- package/dist/utils/performanceAuditor.js +541 -89
- package/dist/utils/projectStorage.js +9 -0
- package/dist/utils/systemPrompts.d.ts +3 -2
- package/dist/utils/systemPrompts.js +29 -5
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
package/dist/utils/fuzzyMatch.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { distance } from 'fastest-levenshtein';
|
|
2
|
+
import { localValidate } from './localSyntaxValidator.js';
|
|
2
3
|
/**
|
|
3
4
|
* Trims each line, collapses multiple spaces/tabs to single space,
|
|
4
5
|
* and normalizes line endings.
|
|
@@ -24,32 +25,50 @@ export function levenshteinSimilarity(a, b) {
|
|
|
24
25
|
/**
|
|
25
26
|
* Finds the best match for `searchContent` inside `fileContent`.
|
|
26
27
|
* Uses a pipeline of strategies: Exact -> Whitespace-normalized -> Levenshtein.
|
|
28
|
+
* Enforces uniqueness and higher confidence thresholds for non-unique search snippets across repetitive files.
|
|
27
29
|
*/
|
|
28
30
|
export function findBestMatch(fileContent, searchContent) {
|
|
29
|
-
if (
|
|
31
|
+
if (fileContent === undefined || searchContent === undefined)
|
|
30
32
|
return null;
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
if (exactIndex !== -1) {
|
|
33
|
+
// Special case: empty search content
|
|
34
|
+
if (searchContent === '') {
|
|
34
35
|
return {
|
|
35
|
-
start:
|
|
36
|
-
end:
|
|
36
|
+
start: 0,
|
|
37
|
+
end: 0,
|
|
37
38
|
strategy: 'Exact Match',
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
|
-
|
|
41
|
+
if (!fileContent)
|
|
42
|
+
return null;
|
|
43
|
+
// Strategy 1: Exact Match (with Uniqueness check)
|
|
44
|
+
let firstExactIndex = -1;
|
|
45
|
+
let exactMatchCount = 0;
|
|
46
|
+
let pos = fileContent.indexOf(searchContent);
|
|
47
|
+
while (pos !== -1) {
|
|
48
|
+
exactMatchCount++;
|
|
49
|
+
if (firstExactIndex === -1)
|
|
50
|
+
firstExactIndex = pos;
|
|
51
|
+
pos = fileContent.indexOf(searchContent, pos + 1);
|
|
52
|
+
}
|
|
53
|
+
if (exactMatchCount === 1) {
|
|
54
|
+
return {
|
|
55
|
+
start: firstExactIndex,
|
|
56
|
+
end: firstExactIndex + searchContent.length,
|
|
57
|
+
strategy: 'Exact Match',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
else if (exactMatchCount > 1) {
|
|
61
|
+
// Non-unique exact match across repetitive file content!
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// Strategy 2: Whitespace-normalized Match (with Uniqueness check)
|
|
41
65
|
const normFileLines = fileContent.split(/\r?\n/);
|
|
42
|
-
const normSearchContent = normalizeWhitespace(searchContent);
|
|
43
|
-
// Fast check: does the normalized file content contain the normalized search content at all?
|
|
44
|
-
// Since newlines are preserved by normalizeWhitespace (it only trims lines), we can do this:
|
|
45
|
-
const normFileContent = normalizeWhitespace(fileContent);
|
|
46
|
-
// To map back to original lines, we try matching line by line.
|
|
47
66
|
const searchLines = searchContent.split(/\r?\n/);
|
|
48
67
|
const numSearchLines = searchLines.length;
|
|
68
|
+
const normalizedMatches = [];
|
|
49
69
|
if (numSearchLines > 0 && numSearchLines <= normFileLines.length) {
|
|
50
70
|
for (let i = 0; i <= normFileLines.length - numSearchLines; i++) {
|
|
51
71
|
let isMatch = true;
|
|
52
|
-
let charMatchCount = 0;
|
|
53
72
|
for (let j = 0; j < numSearchLines; j++) {
|
|
54
73
|
const fileLineNorm = normalizeWhitespace(normFileLines[i + j]);
|
|
55
74
|
const searchLineNorm = normalizeWhitespace(searchLines[j]);
|
|
@@ -64,45 +83,100 @@ export function findBestMatch(fileContent, searchContent) {
|
|
|
64
83
|
const endLineOffset = getByteOffsetOfLine(fileContent, i + numSearchLines - 1);
|
|
65
84
|
const originalEndLine = normFileLines[i + numSearchLines - 1];
|
|
66
85
|
const end = endLineOffset + originalEndLine.length;
|
|
67
|
-
|
|
86
|
+
normalizedMatches.push({
|
|
68
87
|
start,
|
|
69
88
|
end,
|
|
70
89
|
strategy: 'Whitespace-Normalized Match',
|
|
71
|
-
};
|
|
90
|
+
});
|
|
72
91
|
}
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
94
|
+
if (normalizedMatches.length === 1) {
|
|
95
|
+
return normalizedMatches[0];
|
|
96
|
+
}
|
|
97
|
+
else if (normalizedMatches.length > 1) {
|
|
98
|
+
// Non-unique whitespace-normalized match across repetitive code blocks!
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
// Strategy 3: Levenshtein Similarity (with Confidence Threshold & Region Uniqueness)
|
|
102
|
+
// We use higher confidence thresholds and check for competing candidate regions in repetitive files.
|
|
103
|
+
const trimmedSearchLen = searchContent.trim().length;
|
|
104
|
+
// Require higher threshold (0.92) for short search content or single-line snippets, and 0.88 for longer content.
|
|
105
|
+
const confidenceThreshold = (numSearchLines < 3 || trimmedSearchLen < 40) ? 0.92 : 0.88;
|
|
106
|
+
const rawCandidates = [];
|
|
80
107
|
if (numSearchLines > 0) {
|
|
81
108
|
for (let i = 0; i < normFileLines.length; i++) {
|
|
82
109
|
for (const windowSize of [numSearchLines, numSearchLines + 1, numSearchLines - 1]) {
|
|
83
110
|
if (windowSize <= 0 || i + windowSize > normFileLines.length)
|
|
84
111
|
continue;
|
|
85
112
|
const windowLines = normFileLines.slice(i, i + windowSize);
|
|
86
|
-
// Normalize both strings before comparing to ignore indentation/whitespace differences
|
|
87
113
|
const windowStr = normalizeWhitespace(windowLines.join('\n'));
|
|
88
114
|
const targetStr = normalizeWhitespace(searchLines.join('\n'));
|
|
89
115
|
const sim = levenshteinSimilarity(windowStr, targetStr);
|
|
90
|
-
if (sim >=
|
|
91
|
-
bestSim = sim;
|
|
116
|
+
if (sim >= confidenceThreshold) {
|
|
92
117
|
const start = getByteOffsetOfLine(fileContent, i);
|
|
93
118
|
const endLineOffset = getByteOffsetOfLine(fileContent, i + windowSize - 1);
|
|
94
119
|
const originalEndLine = normFileLines[i + windowSize - 1];
|
|
95
120
|
const end = endLineOffset + originalEndLine.length;
|
|
96
|
-
|
|
121
|
+
rawCandidates.push({
|
|
122
|
+
lineIndex: i,
|
|
123
|
+
windowSize,
|
|
124
|
+
sim,
|
|
97
125
|
start,
|
|
98
126
|
end,
|
|
99
|
-
|
|
100
|
-
};
|
|
127
|
+
});
|
|
101
128
|
}
|
|
102
129
|
}
|
|
103
130
|
}
|
|
104
131
|
}
|
|
105
|
-
|
|
132
|
+
if (rawCandidates.length === 0)
|
|
133
|
+
return null;
|
|
134
|
+
// Group raw candidate windows into distinct file regions
|
|
135
|
+
// Candidates within `minRegionGap` lines of each other belong to the same region.
|
|
136
|
+
const minRegionGap = Math.max(2, Math.floor(numSearchLines / 2));
|
|
137
|
+
// Sort candidates by lineIndex
|
|
138
|
+
rawCandidates.sort((a, b) => a.lineIndex - b.lineIndex);
|
|
139
|
+
const distinctRegions = [];
|
|
140
|
+
for (const cand of rawCandidates) {
|
|
141
|
+
if (distinctRegions.length === 0) {
|
|
142
|
+
distinctRegions.push(cand);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const lastRegion = distinctRegions[distinctRegions.length - 1];
|
|
146
|
+
if (Math.abs(cand.lineIndex - lastRegion.lineIndex) <= minRegionGap) {
|
|
147
|
+
// Same region: keep the candidate with higher similarity score
|
|
148
|
+
if (cand.sim > lastRegion.sim) {
|
|
149
|
+
distinctRegions[distinctRegions.length - 1] = cand;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
distinctRegions.push(cand);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Sort distinct regions by similarity score descending
|
|
158
|
+
distinctRegions.sort((a, b) => b.sim - a.sim);
|
|
159
|
+
if (distinctRegions.length === 1) {
|
|
160
|
+
const best = distinctRegions[0];
|
|
161
|
+
return {
|
|
162
|
+
start: best.start,
|
|
163
|
+
end: best.end,
|
|
164
|
+
strategy: `Levenshtein Match (${(best.sim * 100).toFixed(1)}%) [Window: ${best.windowSize}]`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// Multiple distinct regions matched with high similarity across repetitive files.
|
|
168
|
+
// Check if top match significantly outperforms second best match.
|
|
169
|
+
const topCandidate = distinctRegions[0];
|
|
170
|
+
const secondCandidate = distinctRegions[1];
|
|
171
|
+
if (topCandidate.sim - secondCandidate.sim >= 0.08 && secondCandidate.sim < 0.85) {
|
|
172
|
+
return {
|
|
173
|
+
start: topCandidate.start,
|
|
174
|
+
end: topCandidate.end,
|
|
175
|
+
strategy: `Levenshtein Match (${(topCandidate.sim * 100).toFixed(1)}%) [Window: ${topCandidate.windowSize}]`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
// Search snippet is non-unique / ambiguous across repetitive code regions in the file.
|
|
179
|
+
return null;
|
|
106
180
|
}
|
|
107
181
|
export function applyMatch(fileContent, match, replaceContent) {
|
|
108
182
|
return fileContent.slice(0, match.start) + replaceContent + fileContent.slice(match.end);
|
|
@@ -119,3 +193,117 @@ function getByteOffsetOfLine(text, lineIndex) {
|
|
|
119
193
|
}
|
|
120
194
|
return offset;
|
|
121
195
|
}
|
|
196
|
+
/**
|
|
197
|
+
* Parses differential patch block string into structured PatchBlock objects.
|
|
198
|
+
* Supports blocks formatted as:
|
|
199
|
+
* <<<<< SEARCH
|
|
200
|
+
* search content
|
|
201
|
+
* =====
|
|
202
|
+
* replace content
|
|
203
|
+
* >>>>> REPLACE
|
|
204
|
+
*/
|
|
205
|
+
export function parsePatchBlocks(patchText) {
|
|
206
|
+
if (!patchText || typeof patchText !== 'string')
|
|
207
|
+
return [];
|
|
208
|
+
const blocks = [];
|
|
209
|
+
const lines = patchText.split(/\r?\n/);
|
|
210
|
+
let state = 'IDLE';
|
|
211
|
+
let currentSearchLines = [];
|
|
212
|
+
let currentReplaceLines = [];
|
|
213
|
+
const searchMarkerRegex = /^<{5,}\s*SEARCH\b/i;
|
|
214
|
+
const dividerMarkerRegex = /^={5,}\s*$/;
|
|
215
|
+
const replaceMarkerRegex = /^>{5,}\s*REPLACE\b/i;
|
|
216
|
+
for (let i = 0; i < lines.length; i++) {
|
|
217
|
+
const line = lines[i];
|
|
218
|
+
if (state === 'IDLE') {
|
|
219
|
+
if (searchMarkerRegex.test(line)) {
|
|
220
|
+
state = 'SEARCH';
|
|
221
|
+
currentSearchLines = [];
|
|
222
|
+
currentReplaceLines = [];
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else if (state === 'SEARCH') {
|
|
226
|
+
if (dividerMarkerRegex.test(line)) {
|
|
227
|
+
state = 'REPLACE';
|
|
228
|
+
}
|
|
229
|
+
else if (searchMarkerRegex.test(line)) {
|
|
230
|
+
currentSearchLines = [];
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
currentSearchLines.push(line);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
else if (state === 'REPLACE') {
|
|
237
|
+
if (replaceMarkerRegex.test(line)) {
|
|
238
|
+
blocks.push({
|
|
239
|
+
search: currentSearchLines.join('\n'),
|
|
240
|
+
replace: currentReplaceLines.join('\n'),
|
|
241
|
+
});
|
|
242
|
+
state = 'IDLE';
|
|
243
|
+
currentSearchLines = [];
|
|
244
|
+
currentReplaceLines = [];
|
|
245
|
+
}
|
|
246
|
+
else if (searchMarkerRegex.test(line)) {
|
|
247
|
+
state = 'SEARCH';
|
|
248
|
+
currentSearchLines = [];
|
|
249
|
+
currentReplaceLines = [];
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
currentReplaceLines.push(line);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return blocks;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Applies a sequence of search/replace patch blocks to file content
|
|
260
|
+
* using fuzzy matching, and validates local syntax on the result.
|
|
261
|
+
*/
|
|
262
|
+
export function applyPatch(fileContent, patchTextOrBlocks, filePath, options) {
|
|
263
|
+
const blocks = typeof patchTextOrBlocks === 'string'
|
|
264
|
+
? parsePatchBlocks(patchTextOrBlocks)
|
|
265
|
+
: patchTextOrBlocks;
|
|
266
|
+
if (!blocks || blocks.length === 0) {
|
|
267
|
+
return {
|
|
268
|
+
success: false,
|
|
269
|
+
content: fileContent,
|
|
270
|
+
appliedBlocks: 0,
|
|
271
|
+
failedBlocks: 0,
|
|
272
|
+
errors: ['No patch blocks found in input'],
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
let currentContent = fileContent;
|
|
276
|
+
let appliedBlocks = 0;
|
|
277
|
+
let failedBlocks = 0;
|
|
278
|
+
const errors = [];
|
|
279
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
280
|
+
const block = blocks[i];
|
|
281
|
+
const match = findBestMatch(currentContent, block.search);
|
|
282
|
+
if (match) {
|
|
283
|
+
currentContent = applyMatch(currentContent, match, block.replace);
|
|
284
|
+
appliedBlocks++;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
failedBlocks++;
|
|
288
|
+
const preview = block.search.length > 80 ? block.search.slice(0, 80) + '...' : block.search;
|
|
289
|
+
errors.push(`Failed to locate match for search block ${i + 1}: "${preview}"`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
let syntaxValidation;
|
|
293
|
+
const shouldValidate = options?.validateSyntax !== false;
|
|
294
|
+
if (shouldValidate && filePath) {
|
|
295
|
+
syntaxValidation = localValidate(filePath, currentContent);
|
|
296
|
+
if (!syntaxValidation.isValid) {
|
|
297
|
+
errors.push(`Syntax validation failed: ${syntaxValidation.error}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const success = failedBlocks === 0 && appliedBlocks > 0 && (!syntaxValidation || syntaxValidation.isValid);
|
|
301
|
+
return {
|
|
302
|
+
success,
|
|
303
|
+
content: currentContent,
|
|
304
|
+
appliedBlocks,
|
|
305
|
+
failedBlocks,
|
|
306
|
+
errors,
|
|
307
|
+
syntaxValidation,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Performs fast, local syntax validation using
|
|
3
|
-
*
|
|
2
|
+
* Performs fast, local syntax validation using bracket-matching state machine and native parsers.
|
|
3
|
+
* Acts as a first-pass filter before AI-based validation.
|
|
4
4
|
*/
|
|
5
5
|
export interface ValidationResult {
|
|
6
6
|
isValid: boolean;
|