@neurcode-ai/cli 0.9.49 → 0.9.59
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/commands/fix.d.ts +1 -0
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +710 -29
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/generate.d.ts.map +1 -1
- package/dist/commands/generate.js +16 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/patch-apply.d.ts +7 -0
- package/dist/commands/patch-apply.d.ts.map +1 -0
- package/dist/commands/patch-apply.js +85 -0
- package/dist/commands/patch-apply.js.map +1 -0
- package/dist/commands/start-intent.d.ts.map +1 -1
- package/dist/commands/start-intent.js +17 -2
- package/dist/commands/start-intent.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +372 -71
- package/dist/commands/verify.js.map +1 -1
- package/dist/context-engine/graph.d.ts +6 -0
- package/dist/context-engine/graph.d.ts.map +1 -0
- package/dist/context-engine/graph.js +55 -0
- package/dist/context-engine/graph.js.map +1 -0
- package/dist/context-engine/index.d.ts +14 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +26 -0
- package/dist/context-engine/index.js.map +1 -0
- package/dist/context-engine/scanner.d.ts +6 -0
- package/dist/context-engine/scanner.d.ts.map +1 -0
- package/dist/context-engine/scanner.js +62 -0
- package/dist/context-engine/scanner.js.map +1 -0
- package/dist/context-engine/scorer.d.ts +9 -0
- package/dist/context-engine/scorer.d.ts.map +1 -0
- package/dist/context-engine/scorer.js +112 -0
- package/dist/context-engine/scorer.js.map +1 -0
- package/dist/context-engine/suggestions.d.ts +12 -0
- package/dist/context-engine/suggestions.d.ts.map +1 -0
- package/dist/context-engine/suggestions.js +22 -0
- package/dist/context-engine/suggestions.js.map +1 -0
- package/dist/daemon/server.d.ts +23 -0
- package/dist/daemon/server.d.ts.map +1 -0
- package/dist/daemon/server.js +222 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/intent-engine/coverage.d.ts +69 -0
- package/dist/intent-engine/coverage.d.ts.map +1 -0
- package/dist/intent-engine/coverage.js +140 -0
- package/dist/intent-engine/coverage.js.map +1 -0
- package/dist/intent-engine/flow-rules.d.ts +21 -0
- package/dist/intent-engine/flow-rules.d.ts.map +1 -0
- package/dist/intent-engine/flow-rules.js +83 -0
- package/dist/intent-engine/flow-rules.js.map +1 -0
- package/dist/intent-engine/flow-validator.d.ts +29 -0
- package/dist/intent-engine/flow-validator.d.ts.map +1 -0
- package/dist/intent-engine/flow-validator.js +202 -0
- package/dist/intent-engine/flow-validator.js.map +1 -0
- package/dist/intent-engine/graph.d.ts +33 -0
- package/dist/intent-engine/graph.d.ts.map +1 -0
- package/dist/intent-engine/graph.js +67 -0
- package/dist/intent-engine/graph.js.map +1 -0
- package/dist/intent-engine/index.d.ts +35 -0
- package/dist/intent-engine/index.d.ts.map +1 -0
- package/dist/intent-engine/index.js +94 -0
- package/dist/intent-engine/index.js.map +1 -0
- package/dist/intent-engine/indexer.d.ts +18 -0
- package/dist/intent-engine/indexer.d.ts.map +1 -0
- package/dist/intent-engine/indexer.js +100 -0
- package/dist/intent-engine/indexer.js.map +1 -0
- package/dist/intent-engine/matcher.d.ts +35 -0
- package/dist/intent-engine/matcher.d.ts.map +1 -0
- package/dist/intent-engine/matcher.js +522 -0
- package/dist/intent-engine/matcher.js.map +1 -0
- package/dist/intent-engine/parser.d.ts +12 -0
- package/dist/intent-engine/parser.d.ts.map +1 -0
- package/dist/intent-engine/parser.js +93 -0
- package/dist/intent-engine/parser.js.map +1 -0
- package/dist/intent-engine/regression.d.ts +32 -0
- package/dist/intent-engine/regression.d.ts.map +1 -0
- package/dist/intent-engine/regression.js +166 -0
- package/dist/intent-engine/regression.js.map +1 -0
- package/dist/intent-engine/requirements.d.ts +22 -0
- package/dist/intent-engine/requirements.d.ts.map +1 -0
- package/dist/intent-engine/requirements.js +147 -0
- package/dist/intent-engine/requirements.js.map +1 -0
- package/dist/intent-engine/state.d.ts +44 -0
- package/dist/intent-engine/state.d.ts.map +1 -0
- package/dist/intent-engine/state.js +83 -0
- package/dist/intent-engine/state.js.map +1 -0
- package/dist/patch-engine/diff.d.ts +12 -0
- package/dist/patch-engine/diff.d.ts.map +1 -0
- package/dist/patch-engine/diff.js +74 -0
- package/dist/patch-engine/diff.js.map +1 -0
- package/dist/patch-engine/generator.d.ts +13 -0
- package/dist/patch-engine/generator.d.ts.map +1 -0
- package/dist/patch-engine/generator.js +51 -0
- package/dist/patch-engine/generator.js.map +1 -0
- package/dist/patch-engine/index.d.ts +47 -0
- package/dist/patch-engine/index.d.ts.map +1 -0
- package/dist/patch-engine/index.js +182 -0
- package/dist/patch-engine/index.js.map +1 -0
- package/dist/patch-engine/patterns.d.ts +4 -0
- package/dist/patch-engine/patterns.d.ts.map +1 -0
- package/dist/patch-engine/patterns.js +99 -0
- package/dist/patch-engine/patterns.js.map +1 -0
- package/dist/utils/ai-debt-budget.d.ts +3 -2
- package/dist/utils/ai-debt-budget.d.ts.map +1 -1
- package/dist/utils/ai-debt-budget.js +83 -2
- package/dist/utils/ai-debt-budget.js.map +1 -1
- package/package.json +8 -7
- package/LICENSE +0 -201
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PatternKind } from './patterns';
|
|
2
|
+
export type PatchInput = {
|
|
3
|
+
filePath: string;
|
|
4
|
+
issue: string;
|
|
5
|
+
policy: string;
|
|
6
|
+
fileContent: string;
|
|
7
|
+
patternKind: PatternKind;
|
|
8
|
+
};
|
|
9
|
+
export type PatchResult = {
|
|
10
|
+
updatedContent: string;
|
|
11
|
+
} | null;
|
|
12
|
+
export declare function generatePatch(input: PatchInput): PatchResult;
|
|
13
|
+
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../src/patch-engine/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAE7D,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,WAAW,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB,GAAG,IAAI,CAAC;AAgCT,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAqB5D"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.generatePatch = generatePatch;
|
|
4
|
+
const patterns_1 = require("./patterns");
|
|
5
|
+
function leadingWhitespace(line) {
|
|
6
|
+
return line.match(/^(\s*)/)?.[1] ?? '';
|
|
7
|
+
}
|
|
8
|
+
// Replace the matched DB call line with a service-layer redirect comment.
|
|
9
|
+
function applyDbAccessFix(lines, lineIndex) {
|
|
10
|
+
const indent = leadingWhitespace(lines[lineIndex]);
|
|
11
|
+
const updated = [...lines];
|
|
12
|
+
updated[lineIndex] = `${indent}// [NEURCODE] Move to service layer — replace direct DB call with a service method`;
|
|
13
|
+
return updated;
|
|
14
|
+
}
|
|
15
|
+
// Insert a validation reminder line immediately before the req.body/params/query access.
|
|
16
|
+
function applyValidationFix(lines, lineIndex) {
|
|
17
|
+
const indent = leadingWhitespace(lines[lineIndex]);
|
|
18
|
+
const comment = `${indent}// [NEURCODE] Add validation — e.g. const { error } = schema.validate(req.body); ` +
|
|
19
|
+
`if (error) return res.status(400).json({ error: error.message });`;
|
|
20
|
+
const updated = [...lines];
|
|
21
|
+
updated.splice(lineIndex, 0, comment);
|
|
22
|
+
return updated;
|
|
23
|
+
}
|
|
24
|
+
// Remove the TODO/FIXME comment line entirely.
|
|
25
|
+
function applyTodoRemoval(lines, lineIndex) {
|
|
26
|
+
const updated = [...lines];
|
|
27
|
+
updated.splice(lineIndex, 1);
|
|
28
|
+
return updated;
|
|
29
|
+
}
|
|
30
|
+
function generatePatch(input) {
|
|
31
|
+
const lines = input.fileContent.split('\n');
|
|
32
|
+
const lineIndex = (0, patterns_1.detectPattern)(input.fileContent, input.patternKind);
|
|
33
|
+
if (lineIndex === null)
|
|
34
|
+
return null;
|
|
35
|
+
let updatedLines;
|
|
36
|
+
switch (input.patternKind) {
|
|
37
|
+
case 'db_in_ui':
|
|
38
|
+
updatedLines = applyDbAccessFix(lines, lineIndex);
|
|
39
|
+
break;
|
|
40
|
+
case 'missing_validation':
|
|
41
|
+
updatedLines = applyValidationFix(lines, lineIndex);
|
|
42
|
+
break;
|
|
43
|
+
case 'todo_fixme':
|
|
44
|
+
updatedLines = applyTodoRemoval(lines, lineIndex);
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
return { updatedContent: updatedLines.join('\n') };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generator.js","sourceRoot":"","sources":["../../src/patch-engine/generator.ts"],"names":[],"mappings":";;AA4CA,sCAqBC;AAjED,yCAA6D;AAc7D,SAAS,iBAAiB,CAAC,IAAY;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;AACzC,CAAC;AAED,0EAA0E;AAC1E,SAAS,gBAAgB,CAAC,KAAe,EAAE,SAAiB;IAC1D,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,SAAS,CAAC,GAAG,GAAG,MAAM,oFAAoF,CAAC;IACnH,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,yFAAyF;AACzF,SAAS,kBAAkB,CAAC,KAAe,EAAE,SAAiB;IAC5D,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;IACnD,MAAM,OAAO,GACX,GAAG,MAAM,mFAAmF;QAC5F,mEAAmE,CAAC;IACtE,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACtC,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,+CAA+C;AAC/C,SAAS,gBAAgB,CAAC,KAAe,EAAE,SAAiB;IAC1D,MAAM,OAAO,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IAC3B,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC7B,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,SAAgB,aAAa,CAAC,KAAiB;IAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,IAAA,wBAAa,EAAC,KAAK,CAAC,WAAW,EAAE,KAAK,CAAC,WAAW,CAAC,CAAC;IACtE,IAAI,SAAS,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAEpC,IAAI,YAAsB,CAAC;IAC3B,QAAQ,KAAK,CAAC,WAAW,EAAE,CAAC;QAC1B,KAAK,UAAU;YACb,YAAY,GAAG,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAClD,MAAM;QACR,KAAK,oBAAoB;YACvB,YAAY,GAAG,kBAAkB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YACpD,MAAM;QACR,KAAK,YAAY;YACf,YAAY,GAAG,gBAAgB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;YAClD,MAAM;QACR;YACE,OAAO,IAAI,CAAC;IAChB,CAAC;IAED,OAAO,EAAE,cAAc,EAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AACrD,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type PatternKind } from './patterns';
|
|
2
|
+
export type { PatternKind };
|
|
3
|
+
export type PatchConfidence = 'high' | 'medium' | 'low';
|
|
4
|
+
export type SuggestionPatch = {
|
|
5
|
+
file: string;
|
|
6
|
+
diff: string;
|
|
7
|
+
patchConfidence: PatchConfidence;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Apply a unified diff (as produced by generateUnifiedDiff) to fileContent.
|
|
11
|
+
*
|
|
12
|
+
* Parses the single-hunk diff format, verifies every context and removal line
|
|
13
|
+
* matches the current file, then reconstructs the updated content.
|
|
14
|
+
*
|
|
15
|
+
* Returns null when:
|
|
16
|
+
* - no hunk header found
|
|
17
|
+
* - a context or removal line does not match current file content (file changed)
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyUnifiedDiff(fileContent: string, diff: string): string | null;
|
|
20
|
+
/**
|
|
21
|
+
* Detect the first matching patchable pattern in fileContent and return the
|
|
22
|
+
* updated content. Tries patterns in priority order: db_in_ui → missing_validation
|
|
23
|
+
* → todo_fixme. Validates safety before returning.
|
|
24
|
+
*
|
|
25
|
+
* Used by `neurcode patch --file` to apply a patch without needing suggestion metadata.
|
|
26
|
+
*/
|
|
27
|
+
export declare function applyFirstMatchingPatch(filePath: string, fileContent: string): {
|
|
28
|
+
updatedContent: string;
|
|
29
|
+
patternKind: PatternKind;
|
|
30
|
+
patchConfidence: PatchConfidence;
|
|
31
|
+
} | null;
|
|
32
|
+
/**
|
|
33
|
+
* Given a fix suggestion and the current content of suggestion.file,
|
|
34
|
+
* attempts to generate a deterministic, safety-validated code patch.
|
|
35
|
+
*
|
|
36
|
+
* Returns null when:
|
|
37
|
+
* - the violation type has no patchable pattern
|
|
38
|
+
* - the pattern is not found in the file content
|
|
39
|
+
* - the generated patch produces no diff
|
|
40
|
+
* - the patch fails the safety gate (isPatchSafe)
|
|
41
|
+
*/
|
|
42
|
+
export declare function generatePatchForSuggestion(suggestion: {
|
|
43
|
+
file: string;
|
|
44
|
+
issue: string;
|
|
45
|
+
policy: string;
|
|
46
|
+
}, fileContent: string): SuggestionPatch | null;
|
|
47
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/patch-engine/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAIjE,YAAY,EAAE,WAAW,EAAE,CAAC;AAE5B,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;AAExD,MAAM,MAAM,eAAe,GAAG;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,eAAe,CAAC;CAClC,CAAC;AAqCF;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiEjF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAClB;IAAE,cAAc,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,WAAW,CAAC;IAAC,eAAe,EAAE,eAAe,CAAA;CAAE,GAAG,IAAI,CA0B/F;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,UAAU,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAC3D,WAAW,EAAE,MAAM,GAClB,eAAe,GAAG,IAAI,CAuBxB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyUnifiedDiff = applyUnifiedDiff;
|
|
4
|
+
exports.applyFirstMatchingPatch = applyFirstMatchingPatch;
|
|
5
|
+
exports.generatePatchForSuggestion = generatePatchForSuggestion;
|
|
6
|
+
const patterns_1 = require("./patterns");
|
|
7
|
+
const generator_1 = require("./generator");
|
|
8
|
+
const diff_1 = require("./diff");
|
|
9
|
+
// Patterns that must appear in original content for a patch to be considered safe.
|
|
10
|
+
const PATCHABLE_PATTERN_RE = /db\.(query|execute|run|find[A-Za-z]*)\b|prisma\.\w+\.\w+\b|new\s+Pool\s*\(|knex\s*\(|TODO|FIXME|\bvalidat/i;
|
|
11
|
+
/**
|
|
12
|
+
* A patch is safe when:
|
|
13
|
+
* - updated content is non-empty
|
|
14
|
+
* - the diff is non-empty (something actually changed)
|
|
15
|
+
* - total added + removed lines ≤ 5 (not a full-file rewrite)
|
|
16
|
+
* - the original file contains at least one recognizable patchable pattern
|
|
17
|
+
*/
|
|
18
|
+
function isPatchSafe(original, updated) {
|
|
19
|
+
if (!updated || !updated.trim())
|
|
20
|
+
return false;
|
|
21
|
+
const diff = (0, diff_1.generateUnifiedDiff)('', original, updated);
|
|
22
|
+
if (!diff)
|
|
23
|
+
return false;
|
|
24
|
+
let changed = 0;
|
|
25
|
+
for (const line of diff.split('\n')) {
|
|
26
|
+
if (line.startsWith('-') && !line.startsWith('---'))
|
|
27
|
+
changed++;
|
|
28
|
+
if (line.startsWith('+') && !line.startsWith('+++'))
|
|
29
|
+
changed++;
|
|
30
|
+
}
|
|
31
|
+
if (changed > 5)
|
|
32
|
+
return false;
|
|
33
|
+
if (!PATCHABLE_PATTERN_RE.test(original))
|
|
34
|
+
return false;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
function scorePatchConfidence(kind) {
|
|
38
|
+
if (kind === 'db_in_ui')
|
|
39
|
+
return 'high';
|
|
40
|
+
if (kind === 'missing_validation')
|
|
41
|
+
return 'medium';
|
|
42
|
+
return 'low'; // todo_fixme — simple removal, lowest confidence
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Apply a unified diff (as produced by generateUnifiedDiff) to fileContent.
|
|
46
|
+
*
|
|
47
|
+
* Parses the single-hunk diff format, verifies every context and removal line
|
|
48
|
+
* matches the current file, then reconstructs the updated content.
|
|
49
|
+
*
|
|
50
|
+
* Returns null when:
|
|
51
|
+
* - no hunk header found
|
|
52
|
+
* - a context or removal line does not match current file content (file changed)
|
|
53
|
+
*/
|
|
54
|
+
function applyUnifiedDiff(fileContent, diff) {
|
|
55
|
+
if (!diff)
|
|
56
|
+
return null;
|
|
57
|
+
const diffLines = diff.split('\n');
|
|
58
|
+
// Locate the hunk header (skip --- / +++ file headers)
|
|
59
|
+
let hunkIdx = -1;
|
|
60
|
+
for (let i = 0; i < diffLines.length; i++) {
|
|
61
|
+
if (diffLines[i].startsWith('@@')) {
|
|
62
|
+
hunkIdx = i;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (hunkIdx === -1)
|
|
67
|
+
return null;
|
|
68
|
+
// Parse @@ -oldStart[,oldCount] +newStart[,newCount] @@
|
|
69
|
+
const match = diffLines[hunkIdx].match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
70
|
+
if (!match)
|
|
71
|
+
return null;
|
|
72
|
+
// Diff uses 1-indexed lines; convert to 0-indexed
|
|
73
|
+
const origStart = parseInt(match[1], 10) - 1;
|
|
74
|
+
const origLines = fileContent.split('\n');
|
|
75
|
+
const output = [];
|
|
76
|
+
// Lines before the hunk are copied unchanged
|
|
77
|
+
for (let i = 0; i < origStart; i++) {
|
|
78
|
+
output.push(origLines[i] ?? '');
|
|
79
|
+
}
|
|
80
|
+
let origIdx = origStart;
|
|
81
|
+
for (let i = hunkIdx + 1; i < diffLines.length; i++) {
|
|
82
|
+
const line = diffLines[i];
|
|
83
|
+
// A trailing empty string from split('\n') signals end of diff
|
|
84
|
+
if (line.length === 0 && i === diffLines.length - 1)
|
|
85
|
+
break;
|
|
86
|
+
const prefix = line[0];
|
|
87
|
+
const content = line.slice(1);
|
|
88
|
+
if (prefix === ' ') {
|
|
89
|
+
// Context: must match current file — abort on mismatch (file changed)
|
|
90
|
+
if (origIdx >= origLines.length || origLines[origIdx] !== content)
|
|
91
|
+
return null;
|
|
92
|
+
output.push(content);
|
|
93
|
+
origIdx++;
|
|
94
|
+
}
|
|
95
|
+
else if (prefix === '-') {
|
|
96
|
+
// Removal: must match current file — abort on mismatch
|
|
97
|
+
if (origIdx >= origLines.length || origLines[origIdx] !== content)
|
|
98
|
+
return null;
|
|
99
|
+
origIdx++; // consume original line without adding to output
|
|
100
|
+
}
|
|
101
|
+
else if (prefix === '+') {
|
|
102
|
+
// Addition: inject into output without consuming original
|
|
103
|
+
output.push(content);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
break; // unexpected prefix — stop hunk processing
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Copy remaining original lines after the hunk
|
|
110
|
+
while (origIdx < origLines.length) {
|
|
111
|
+
output.push(origLines[origIdx]);
|
|
112
|
+
origIdx++;
|
|
113
|
+
}
|
|
114
|
+
return output.join('\n');
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Detect the first matching patchable pattern in fileContent and return the
|
|
118
|
+
* updated content. Tries patterns in priority order: db_in_ui → missing_validation
|
|
119
|
+
* → todo_fixme. Validates safety before returning.
|
|
120
|
+
*
|
|
121
|
+
* Used by `neurcode patch --file` to apply a patch without needing suggestion metadata.
|
|
122
|
+
*/
|
|
123
|
+
function applyFirstMatchingPatch(filePath, fileContent) {
|
|
124
|
+
const kinds = ['db_in_ui', 'missing_validation', 'todo_fixme'];
|
|
125
|
+
for (const kind of kinds) {
|
|
126
|
+
const result = (0, generator_1.generatePatch)({
|
|
127
|
+
filePath,
|
|
128
|
+
issue: '',
|
|
129
|
+
policy: '',
|
|
130
|
+
fileContent,
|
|
131
|
+
patternKind: kind,
|
|
132
|
+
});
|
|
133
|
+
if (!result)
|
|
134
|
+
continue;
|
|
135
|
+
const diff = (0, diff_1.generateUnifiedDiff)(filePath, fileContent, result.updatedContent);
|
|
136
|
+
if (!diff)
|
|
137
|
+
continue;
|
|
138
|
+
if (!isPatchSafe(fileContent, result.updatedContent))
|
|
139
|
+
continue;
|
|
140
|
+
return {
|
|
141
|
+
updatedContent: result.updatedContent,
|
|
142
|
+
patternKind: kind,
|
|
143
|
+
patchConfidence: scorePatchConfidence(kind),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Given a fix suggestion and the current content of suggestion.file,
|
|
150
|
+
* attempts to generate a deterministic, safety-validated code patch.
|
|
151
|
+
*
|
|
152
|
+
* Returns null when:
|
|
153
|
+
* - the violation type has no patchable pattern
|
|
154
|
+
* - the pattern is not found in the file content
|
|
155
|
+
* - the generated patch produces no diff
|
|
156
|
+
* - the patch fails the safety gate (isPatchSafe)
|
|
157
|
+
*/
|
|
158
|
+
function generatePatchForSuggestion(suggestion, fileContent) {
|
|
159
|
+
const kind = (0, patterns_1.classifyViolation)(suggestion.issue, suggestion.policy);
|
|
160
|
+
if (!kind)
|
|
161
|
+
return null;
|
|
162
|
+
const result = (0, generator_1.generatePatch)({
|
|
163
|
+
filePath: suggestion.file,
|
|
164
|
+
issue: suggestion.issue,
|
|
165
|
+
policy: suggestion.policy,
|
|
166
|
+
fileContent,
|
|
167
|
+
patternKind: kind,
|
|
168
|
+
});
|
|
169
|
+
if (!result)
|
|
170
|
+
return null;
|
|
171
|
+
const diff = (0, diff_1.generateUnifiedDiff)(suggestion.file, fileContent, result.updatedContent);
|
|
172
|
+
if (!diff)
|
|
173
|
+
return null;
|
|
174
|
+
if (!isPatchSafe(fileContent, result.updatedContent))
|
|
175
|
+
return null;
|
|
176
|
+
return {
|
|
177
|
+
file: suggestion.file,
|
|
178
|
+
diff,
|
|
179
|
+
patchConfidence: scorePatchConfidence(kind),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/patch-engine/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export type PatternKind = 'db_in_ui' | 'missing_validation' | 'todo_fixme';
|
|
2
|
+
export declare function classifyViolation(issue: string, policy: string): PatternKind | null;
|
|
3
|
+
export declare function detectPattern(content: string, kind: PatternKind): number | null;
|
|
4
|
+
//# sourceMappingURL=patterns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.d.ts","sourceRoot":"","sources":["../../src/patch-engine/patterns.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,oBAAoB,GAAG,YAAY,CAAC;AAM3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAsBnF;AA2ED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAO/E"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Detection rules for deterministic patch generation.
|
|
3
|
+
// Each function returns the 0-based line index of the first match, or null.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.classifyViolation = classifyViolation;
|
|
6
|
+
exports.detectPattern = detectPattern;
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Classifiy a fix suggestion into a patchable pattern kind
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function classifyViolation(issue, policy) {
|
|
11
|
+
const combined = `${issue} ${policy}`.toLowerCase();
|
|
12
|
+
if (combined.includes('todo') || combined.includes('fixme'))
|
|
13
|
+
return 'todo_fixme';
|
|
14
|
+
if (combined.includes('db') || combined.includes('database') ||
|
|
15
|
+
combined.includes('query') || combined.includes('data access') ||
|
|
16
|
+
combined.includes('direct access') ||
|
|
17
|
+
policy.includes('layer') || policy.includes('layering') || policy.includes('db')) {
|
|
18
|
+
return 'db_in_ui';
|
|
19
|
+
}
|
|
20
|
+
if (combined.includes('validation') || combined.includes('validate') ||
|
|
21
|
+
combined.includes('input') || policy.includes('validation')) {
|
|
22
|
+
return 'missing_validation';
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Pattern detectors (work on pre-split line arrays)
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const DB_ACCESS_PATTERNS = [
|
|
30
|
+
/\bdb\s*\.\s*query\s*\(/,
|
|
31
|
+
/\bdb\s*\.\s*execute\s*\(/,
|
|
32
|
+
/\bdb\s*\.\s*run\s*\(/,
|
|
33
|
+
/\bdb\s*\.\s*find\b/,
|
|
34
|
+
/\bdb\s*\.\s*findOne\s*\(/,
|
|
35
|
+
/\bprisma\s*\.\s*\w+\s*\.\s*find/,
|
|
36
|
+
/\bprisma\s*\.\s*\w+\s*\.\s*create\s*\(/,
|
|
37
|
+
/\bprisma\s*\.\s*\w+\s*\.\s*update\s*\(/,
|
|
38
|
+
/\bprisma\s*\.\s*\w+\s*\.\s*delete\s*\(/,
|
|
39
|
+
/\bnew\s+Pool\s*\(/,
|
|
40
|
+
/\bknex\s*\(/,
|
|
41
|
+
];
|
|
42
|
+
const VALIDATION_PATTERNS = [
|
|
43
|
+
/\.validate\s*\(/,
|
|
44
|
+
/schema\.parse\s*\(/,
|
|
45
|
+
/\bJoi\s*\./,
|
|
46
|
+
/\byup\s*\./,
|
|
47
|
+
/\bzod\s*\./,
|
|
48
|
+
/\bajv\s*\.\s*compile/,
|
|
49
|
+
];
|
|
50
|
+
// Matches the request/response parameter pair in a handler signature
|
|
51
|
+
const REQ_HANDLER_RE = /\b(?:req|request)\s*,\s*(?:res|response|reply)\b/;
|
|
52
|
+
// Matches direct access to incoming data without a prior validation call
|
|
53
|
+
const REQ_INPUT_RE = /\b(?:req|request)\.(?:body|params|query)\b/;
|
|
54
|
+
const TODO_FIXME_RE = /\/\/\s*(?:TODO|FIXME)\b/;
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
function findDbAccessLine(lines) {
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const trimmed = lines[i].trimStart();
|
|
59
|
+
// Skip lines that are themselves comments
|
|
60
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
61
|
+
continue;
|
|
62
|
+
if (DB_ACCESS_PATTERNS.some((re) => re.test(lines[i])))
|
|
63
|
+
return i;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function findMissingValidationLine(lines) {
|
|
68
|
+
let handlerStartIndex = -1;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (REQ_HANDLER_RE.test(lines[i])) {
|
|
71
|
+
handlerStartIndex = i;
|
|
72
|
+
}
|
|
73
|
+
if (handlerStartIndex !== -1 && REQ_INPUT_RE.test(lines[i])) {
|
|
74
|
+
// Look backward from this line (within the handler) for a validation call
|
|
75
|
+
const searchFrom = Math.max(handlerStartIndex, i - 30);
|
|
76
|
+
const priorLines = lines.slice(searchFrom, i);
|
|
77
|
+
const hasValidation = priorLines.some((l) => VALIDATION_PATTERNS.some((re) => re.test(l)));
|
|
78
|
+
if (!hasValidation)
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function findTodoLine(lines) {
|
|
85
|
+
for (let i = 0; i < lines.length; i++) {
|
|
86
|
+
if (TODO_FIXME_RE.test(lines[i]))
|
|
87
|
+
return i;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function detectPattern(content, kind) {
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
switch (kind) {
|
|
94
|
+
case 'db_in_ui': return findDbAccessLine(lines);
|
|
95
|
+
case 'missing_validation': return findMissingValidationLine(lines);
|
|
96
|
+
case 'todo_fixme': return findTodoLine(lines);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patterns.js","sourceRoot":"","sources":["../../src/patch-engine/patterns.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,11 +16,12 @@ export interface AiDebtMetrics {
|
|
|
16
16
|
bloatFiles: number;
|
|
17
17
|
}
|
|
18
18
|
export interface AiDebtViolation {
|
|
19
|
-
code: 'added_todo_fixme' | 'added_console_logs' | 'added_any_types' | 'large_files_touched' | 'bloat_files';
|
|
20
|
-
metric: keyof AiDebtMetrics;
|
|
19
|
+
code: 'added_todo_fixme' | 'added_console_logs' | 'added_any_types' | 'large_files_touched' | 'bloat_files' | 'db_in_ui' | 'missing_validation';
|
|
20
|
+
metric: keyof AiDebtMetrics | 'architectural';
|
|
21
21
|
observed: number;
|
|
22
22
|
budget: number;
|
|
23
23
|
message: string;
|
|
24
|
+
files?: string[];
|
|
24
25
|
}
|
|
25
26
|
export interface AiDebtBudgetConfig {
|
|
26
27
|
mode: AiDebtMode;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-debt-budget.d.ts","sourceRoot":"","sources":["../../src/utils/ai-debt-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIzD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,qBAAqB,GACrB,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"ai-debt-budget.d.ts","sourceRoot":"","sources":["../../src/utils/ai-debt-budget.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAIzD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,UAAU,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,gBAAgB;IAC/B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,gBAAgB,EAAE,MAAM,CAAC;IACzB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EACA,kBAAkB,GAClB,oBAAoB,GACpB,iBAAiB,GACjB,qBAAqB,GACrB,aAAa,GACb,UAAU,GACV,oBAAoB,CAAC;IACzB,MAAM,EAAE,MAAM,aAAa,GAAG,eAAe,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,MAAM,EAAE,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,UAAU,CAAC;CAClD;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,aAAa,CAAC;IACvB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,MAAM,EAAE,kBAAkB,CAAC,QAAQ,CAAC,CAAC;CACtC;AA4BD,UAAU,mBAAmB;IAC3B,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAgFD,wBAAgB,yBAAyB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IACvE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,GAAG,kBAAkB,CAsDrB;AAqMD,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,mBAAmB,GAAG,gBAAgB,CAwBjF"}
|
|
@@ -7,6 +7,13 @@ const path_1 = require("path");
|
|
|
7
7
|
const TODO_FIXME_PATTERN = /\b(?:TODO|FIXME)\b/i;
|
|
8
8
|
const CONSOLE_LOG_PATTERN = /\bconsole\.log\s*\(/;
|
|
9
9
|
const ANY_TYPE_PATTERN = /(:\s*any\b|<\s*any\s*>|\bArray<any>\b|\bPromise<any>\b)/i;
|
|
10
|
+
// Architectural pattern detection
|
|
11
|
+
const DB_IMPORT_PATTERN = /import\s+.*from\s+['"][^'"]*\/(db|database|prisma|knex|sequelize|drizzle)[^'"]*['"]/i;
|
|
12
|
+
const DB_CALL_PATTERN = /\b(db|prisma|knex|sequelize|drizzle|pool|client)\s*\.\s*(query|execute|findMany|findOne|findFirst|findUnique|create|update|delete|upsert|raw|select|insert)\s*\(/;
|
|
13
|
+
const UI_PATH_PATTERN = /(\/|^)(components?|pages?|views?|screens?|containers?|app|ui)[/\\]|\.tsx$/i;
|
|
14
|
+
const VALIDATION_PATTERN = /\b(zod|joi|yup|valibot|ajv|class-validator|express-validator|validate|sanitize|schema\.parse|\.validate\()\b/i;
|
|
15
|
+
const REQ_BODY_PATTERN = /\breq\.body\b/;
|
|
16
|
+
const API_PATH_PATTERN = /(\/|^)(routes?|controllers?|handlers?|api|endpoints?)[/\\]/i;
|
|
10
17
|
const DEFAULT_THRESHOLDS = {
|
|
11
18
|
maxAddedTodoFixme: 0,
|
|
12
19
|
maxAddedConsoleLogs: 0,
|
|
@@ -125,11 +132,20 @@ function buildMetrics(diffFiles, bloatCount, thresholds) {
|
|
|
125
132
|
let addedConsoleLogs = 0;
|
|
126
133
|
let addedAnyTypes = 0;
|
|
127
134
|
let largeFilesTouched = 0;
|
|
135
|
+
const todoFixmeFiles = [];
|
|
136
|
+
const consoleLogFiles = [];
|
|
137
|
+
const anyTypeFiles = [];
|
|
138
|
+
const largeFiles = [];
|
|
128
139
|
for (const file of diffFiles) {
|
|
140
|
+
const filePath = file.path || 'unknown';
|
|
129
141
|
const fileDelta = Math.max(0, Number(file.addedLines || 0)) + Math.max(0, Number(file.removedLines || 0));
|
|
130
142
|
if (fileDelta >= thresholds.largeFileDeltaLines) {
|
|
131
143
|
largeFilesTouched += 1;
|
|
144
|
+
largeFiles.push(filePath);
|
|
132
145
|
}
|
|
146
|
+
let fileTodo = false;
|
|
147
|
+
let fileConsole = false;
|
|
148
|
+
let fileAny = false;
|
|
133
149
|
for (const hunk of file.hunks || []) {
|
|
134
150
|
for (const line of hunk.lines || []) {
|
|
135
151
|
if (line.type !== 'added') {
|
|
@@ -138,15 +154,24 @@ function buildMetrics(diffFiles, bloatCount, thresholds) {
|
|
|
138
154
|
const content = String(line.content || '');
|
|
139
155
|
if (TODO_FIXME_PATTERN.test(content)) {
|
|
140
156
|
addedTodoFixme += 1;
|
|
157
|
+
fileTodo = true;
|
|
141
158
|
}
|
|
142
159
|
if (CONSOLE_LOG_PATTERN.test(content)) {
|
|
143
160
|
addedConsoleLogs += 1;
|
|
161
|
+
fileConsole = true;
|
|
144
162
|
}
|
|
145
163
|
if (ANY_TYPE_PATTERN.test(content)) {
|
|
146
164
|
addedAnyTypes += 1;
|
|
165
|
+
fileAny = true;
|
|
147
166
|
}
|
|
148
167
|
}
|
|
149
168
|
}
|
|
169
|
+
if (fileTodo)
|
|
170
|
+
todoFixmeFiles.push(filePath);
|
|
171
|
+
if (fileConsole)
|
|
172
|
+
consoleLogFiles.push(filePath);
|
|
173
|
+
if (fileAny)
|
|
174
|
+
anyTypeFiles.push(filePath);
|
|
150
175
|
}
|
|
151
176
|
return {
|
|
152
177
|
addedTodoFixme,
|
|
@@ -154,6 +179,10 @@ function buildMetrics(diffFiles, bloatCount, thresholds) {
|
|
|
154
179
|
addedAnyTypes,
|
|
155
180
|
largeFilesTouched,
|
|
156
181
|
bloatFiles: Math.max(0, Math.floor(bloatCount)),
|
|
182
|
+
todoFixmeFiles,
|
|
183
|
+
consoleLogFiles,
|
|
184
|
+
anyTypeFiles,
|
|
185
|
+
largeFiles,
|
|
157
186
|
};
|
|
158
187
|
}
|
|
159
188
|
function buildViolations(metrics, thresholds) {
|
|
@@ -165,6 +194,7 @@ function buildViolations(metrics, thresholds) {
|
|
|
165
194
|
observed: metrics.addedTodoFixme,
|
|
166
195
|
budget: thresholds.maxAddedTodoFixme,
|
|
167
196
|
message: `Added TODO/FIXME markers (${metrics.addedTodoFixme}) exceed budget (${thresholds.maxAddedTodoFixme}).`,
|
|
197
|
+
files: metrics.todoFixmeFiles,
|
|
168
198
|
});
|
|
169
199
|
}
|
|
170
200
|
if (metrics.addedConsoleLogs > thresholds.maxAddedConsoleLogs) {
|
|
@@ -174,6 +204,7 @@ function buildViolations(metrics, thresholds) {
|
|
|
174
204
|
observed: metrics.addedConsoleLogs,
|
|
175
205
|
budget: thresholds.maxAddedConsoleLogs,
|
|
176
206
|
message: `Added console.log statements (${metrics.addedConsoleLogs}) exceed budget (${thresholds.maxAddedConsoleLogs}).`,
|
|
207
|
+
files: metrics.consoleLogFiles,
|
|
177
208
|
});
|
|
178
209
|
}
|
|
179
210
|
if (metrics.addedAnyTypes > thresholds.maxAddedAnyTypes) {
|
|
@@ -183,6 +214,7 @@ function buildViolations(metrics, thresholds) {
|
|
|
183
214
|
observed: metrics.addedAnyTypes,
|
|
184
215
|
budget: thresholds.maxAddedAnyTypes,
|
|
185
216
|
message: `Added TypeScript any usages (${metrics.addedAnyTypes}) exceed budget (${thresholds.maxAddedAnyTypes}).`,
|
|
217
|
+
files: metrics.anyTypeFiles,
|
|
186
218
|
});
|
|
187
219
|
}
|
|
188
220
|
if (metrics.largeFilesTouched > thresholds.maxLargeFilesTouched) {
|
|
@@ -193,6 +225,7 @@ function buildViolations(metrics, thresholds) {
|
|
|
193
225
|
budget: thresholds.maxLargeFilesTouched,
|
|
194
226
|
message: `Large file churn (${metrics.largeFilesTouched} file(s) >= ${thresholds.largeFileDeltaLines} lines) ` +
|
|
195
227
|
`exceeds budget (${thresholds.maxLargeFilesTouched}).`,
|
|
228
|
+
files: metrics.largeFiles,
|
|
196
229
|
});
|
|
197
230
|
}
|
|
198
231
|
if (metrics.bloatFiles > thresholds.maxBloatFiles) {
|
|
@@ -219,22 +252,70 @@ function computeScore(metrics, thresholds) {
|
|
|
219
252
|
overBloat * 18;
|
|
220
253
|
return Math.max(0, Math.min(100, 100 - weightedPenalty));
|
|
221
254
|
}
|
|
255
|
+
function detectArchitecturalViolations(diffFiles) {
|
|
256
|
+
const dbInUiFiles = [];
|
|
257
|
+
const missingValidationFiles = [];
|
|
258
|
+
for (const file of diffFiles) {
|
|
259
|
+
const filePath = String(file.path || '');
|
|
260
|
+
const addedLines = (file.hunks || [])
|
|
261
|
+
.flatMap((h) => h.lines || [])
|
|
262
|
+
.filter((l) => l.type === 'added')
|
|
263
|
+
.map((l) => String(l.content || ''));
|
|
264
|
+
const addedText = addedLines.join('\n');
|
|
265
|
+
// db_in_ui: UI component file that imports from or directly calls DB
|
|
266
|
+
if (UI_PATH_PATTERN.test(filePath)) {
|
|
267
|
+
if (DB_IMPORT_PATTERN.test(addedText) || DB_CALL_PATTERN.test(addedText)) {
|
|
268
|
+
dbInUiFiles.push(filePath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// missing_validation: API handler file that reads req.body without any validation library
|
|
272
|
+
if (API_PATH_PATTERN.test(filePath) || filePath.endsWith('.ts') || filePath.endsWith('.js')) {
|
|
273
|
+
if (REQ_BODY_PATTERN.test(addedText) && !VALIDATION_PATTERN.test(addedText)) {
|
|
274
|
+
missingValidationFiles.push(filePath);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const violations = [];
|
|
279
|
+
if (dbInUiFiles.length > 0) {
|
|
280
|
+
violations.push({
|
|
281
|
+
code: 'db_in_ui',
|
|
282
|
+
metric: 'architectural',
|
|
283
|
+
observed: dbInUiFiles.length,
|
|
284
|
+
budget: 0,
|
|
285
|
+
message: `Direct database access in UI component (${dbInUiFiles.length} file${dbInUiFiles.length > 1 ? 's' : ''}). DB calls belong in the service/repository layer.`,
|
|
286
|
+
files: [...new Set(dbInUiFiles)],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (missingValidationFiles.length > 0) {
|
|
290
|
+
violations.push({
|
|
291
|
+
code: 'missing_validation',
|
|
292
|
+
metric: 'architectural',
|
|
293
|
+
observed: missingValidationFiles.length,
|
|
294
|
+
budget: 0,
|
|
295
|
+
message: `API handler reads req.body without input validation (${missingValidationFiles.length} file${missingValidationFiles.length > 1 ? 's' : ''}). All inputs must be validated at the API boundary.`,
|
|
296
|
+
files: [...new Set(missingValidationFiles)],
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
return violations;
|
|
300
|
+
}
|
|
222
301
|
function evaluateAiDebtBudget(input) {
|
|
223
302
|
const metrics = buildMetrics(input.diffFiles, input.bloatCount, input.config.thresholds);
|
|
224
303
|
const violations = buildViolations(metrics, input.config.thresholds);
|
|
304
|
+
const archViolations = detectArchitecturalViolations(input.diffFiles);
|
|
225
305
|
const score = computeScore(metrics, input.config.thresholds);
|
|
306
|
+
const allViolations = [...violations, ...archViolations];
|
|
226
307
|
const pass = input.config.mode === 'off'
|
|
227
308
|
? true
|
|
228
309
|
: input.config.mode === 'advisory'
|
|
229
310
|
? true
|
|
230
|
-
:
|
|
311
|
+
: allViolations.length === 0;
|
|
231
312
|
return {
|
|
232
313
|
mode: input.config.mode,
|
|
233
314
|
pass,
|
|
234
315
|
score,
|
|
235
316
|
metrics,
|
|
236
317
|
thresholds: input.config.thresholds,
|
|
237
|
-
violations,
|
|
318
|
+
violations: allViolations,
|
|
238
319
|
source: input.config.source,
|
|
239
320
|
};
|
|
240
321
|
}
|