@chemx/create-chemx 26.9.26-253 → 26.9.28-1234
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/cli/audit/ast-visitors.js +7 -1
- package/cli/audit/csharp-analyzer.js +360 -7
- package/cli/audit/csharp-analyzer.spec.js +216 -0
- package/cli/audit/extended-text-patterns.js +6 -3
- package/cli/audit/metrics.js +5 -3
- package/cli/audit/reporter-markdown.js +1 -1
- package/cli/audit/reporter-sections.js +1 -1
- package/cli/audit/rules-helpers.js +7 -1
- package/cli/audit/rules-registry.js +10 -0
- package/cli/audit/rules.js +2 -2
- package/cli/audit.js +16 -8
- package/cli/commands/cmd-audit.js +6 -4
- package/cli/config/loader.js +1 -0
- package/cli/config/profiles.js +3 -0
- package/cli/friction-fixes.spec.js +96 -0
- package/cli/installer-templates.js +37 -6
- package/cli/mcp/manifests.js +7 -2
- package/cli/mcp/tools-patch.js +4 -3
- package/cli/mcp/tools-read.js +3 -1
- package/cli/mcp/tools-team-locks.js +10 -3
- package/cli/mcp/tools-team-tasks.js +5 -2
- package/cli/mcp/tools-write.js +2 -1
- package/cli/mcp/tools.js +44 -1
- package/cli/patcher.js +3 -2
- package/cli/path-scope.js +113 -0
- package/cli/path-traversal.spec.js +242 -0
- package/cli/reader.js +3 -1
- package/cli/search.d.ts +2 -1
- package/cli/search.js +1 -21
- package/cli/search.spec.js +12 -0
- package/cli/team/team-commands-lock.js +17 -5
- package/cli/team/team-commands.js +2 -2
- package/cli/team/team-db-locks.js +50 -15
- package/package.json +1 -1
- package/scripts/pre-commit.sh +28 -6
|
@@ -23,12 +23,18 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
|
|
|
23
23
|
const isCustomHook = isCustomHookFunction(astPath);
|
|
24
24
|
|
|
25
25
|
// Pillar 3: Hook Saturation
|
|
26
|
+
const CONTROLLER_WRAPPER_EXCLUSIONS = new Set([
|
|
27
|
+
'useSharedStatusController',
|
|
28
|
+
'useSharedController',
|
|
29
|
+
'useController'
|
|
30
|
+
]);
|
|
26
31
|
let hookCount = 0;
|
|
27
32
|
astPath.traverse({
|
|
28
33
|
CallExpression(callPath) {
|
|
29
34
|
const callee = callPath.node.callee;
|
|
30
35
|
if (t.isIdentifier(callee) && /^use[A-Z0-9]/.test(callee.name)) {
|
|
31
|
-
|
|
36
|
+
const isExcluded = CONTROLLER_WRAPPER_EXCLUSIONS.has(callee.name);
|
|
37
|
+
if (!isExcluded && callPath.getFunctionParent() === astPath) {
|
|
32
38
|
hookCount += 1;
|
|
33
39
|
}
|
|
34
40
|
}
|
|
@@ -1,18 +1,334 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chemical X C# / .NET Structural Analyzer
|
|
3
|
-
* Evaluates .NET Clean Architecture, MediatR handlers, EF Core usage,
|
|
3
|
+
* Evaluates .NET Clean Architecture, MediatR handlers, EF Core usage,
|
|
4
|
+
* cyclomatic complexity, control flow nesting depth, and AI slop.
|
|
4
5
|
*/
|
|
5
6
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
6
7
|
|
|
7
|
-
const SHALLOW_CATCH_PATTERN = /catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\/[^\n]*)?\s*\}/;
|
|
8
8
|
const IN_MEMORY_DB_PATTERN = /\bUseInMemoryDatabase\s*\(/;
|
|
9
|
+
const SQLITE_PROVIDER_PATTERN = /\b(?:UseSqlite|SqliteConnection|DataSource\s*=\s*:memory:)\b/;
|
|
9
10
|
const SIMULATED_DELAY_PATTERN = /\bTask\.Delay\s*\(\s*\d+\s*\)/;
|
|
10
11
|
const HARDCODED_LIST_MOCK_PATTERN = /new\s+List<[A-Za-z0-9_]+>\s*\{\s*new\s+[A-Za-z0-9_]+\s*\{/;
|
|
12
|
+
const SIMULATION_SWITCH_PATTERN = /(?:public|private|protected|internal)?\s*(?:static\s+)?(?:readonly\s+)?(?:const\s+)?bool\s+(?:Simulate|UseSimulation|Mock|Fake|Stub)[A-Za-z0-9_]*\s*(?:\{\s*get;\s*(?:set|init)?;\s*\}\s*)?=\s*true\b/i;
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const isVerbatimStringPrefix = (code, i) => {
|
|
15
|
+
const ch = code[i];
|
|
16
|
+
const next = code[i + 1];
|
|
17
|
+
const isVerbatim = ch === '@' && next === '"';
|
|
18
|
+
const isInterpVerbatim = (ch === '$' && next === '@') || (ch === '@' && next === '$');
|
|
19
|
+
const hasQuote = code[i + 2] === '"';
|
|
20
|
+
return isVerbatim || (isInterpVerbatim && hasQuote);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const maskCommentsAndStrings = (code) => {
|
|
24
|
+
let out = '';
|
|
25
|
+
let i = 0;
|
|
26
|
+
const len = code.length;
|
|
27
|
+
while (i < len) {
|
|
28
|
+
const ch = code[i];
|
|
29
|
+
const next = code[i + 1];
|
|
30
|
+
|
|
31
|
+
if (ch === '/' && next === '/') {
|
|
32
|
+
out += ' ';
|
|
33
|
+
i += 2;
|
|
34
|
+
while (i < len && code[i] !== '\n') {
|
|
35
|
+
out += ' ';
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
} else if (ch === '/' && next === '*') {
|
|
39
|
+
out += ' ';
|
|
40
|
+
i += 2;
|
|
41
|
+
while (i < len && !(code[i] === '*' && code[i + 1] === '/')) {
|
|
42
|
+
out += code[i] === '\n' ? '\n' : ' ';
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
if (i < len) {
|
|
46
|
+
out += ' ';
|
|
47
|
+
i += 2;
|
|
48
|
+
}
|
|
49
|
+
} else if (isVerbatimStringPrefix(code, i)) {
|
|
50
|
+
const isVerbatim = ch === '@' && next === '"';
|
|
51
|
+
const skip = isVerbatim ? 2 : 3;
|
|
52
|
+
out += ' '.repeat(skip);
|
|
53
|
+
i += skip;
|
|
54
|
+
while (i < len) {
|
|
55
|
+
if (code[i] === '"' && code[i + 1] === '"') {
|
|
56
|
+
out += ' ';
|
|
57
|
+
i += 2;
|
|
58
|
+
} else if (code[i] === '"') {
|
|
59
|
+
out += ' ';
|
|
60
|
+
i++;
|
|
61
|
+
break;
|
|
62
|
+
} else {
|
|
63
|
+
out += code[i] === '\n' ? '\n' : ' ';
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else if (ch === '"') {
|
|
68
|
+
out += ' ';
|
|
69
|
+
i++;
|
|
70
|
+
while (i < len && code[i] !== '"' && code[i] !== '\n') {
|
|
71
|
+
if (code[i] === '\\') {
|
|
72
|
+
out += ' ';
|
|
73
|
+
i += 2;
|
|
74
|
+
} else {
|
|
75
|
+
out += ' ';
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (i < len && code[i] === '"') {
|
|
80
|
+
out += ' ';
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
} else if (ch === "'") {
|
|
84
|
+
out += ' ';
|
|
85
|
+
i++;
|
|
86
|
+
while (i < len && code[i] !== "'" && code[i] !== '\n') {
|
|
87
|
+
if (code[i] === '\\') {
|
|
88
|
+
out += ' ';
|
|
89
|
+
i += 2;
|
|
90
|
+
} else {
|
|
91
|
+
out += ' ';
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (i < len && code[i] === "'") {
|
|
96
|
+
out += ' ';
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
out += ch;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const extractMethods = (maskedContent) => {
|
|
108
|
+
const methods = [];
|
|
109
|
+
const methodRegex = /(?:public|internal|protected|private|static|async|override|virtual|sealed|partial)\s+[\w<>\[\],\s?]+?\s+([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)\s*(?:where[^{]+)?\s*\{/g;
|
|
110
|
+
let match;
|
|
111
|
+
|
|
112
|
+
while ((match = methodRegex.exec(maskedContent)) !== null) {
|
|
113
|
+
const methodName = match[1];
|
|
114
|
+
const signature = match[0];
|
|
115
|
+
const startIdx = match.index + signature.length - 1;
|
|
116
|
+
let depth = 0;
|
|
117
|
+
let endIdx = -1;
|
|
118
|
+
|
|
119
|
+
for (let i = startIdx; i < maskedContent.length; i++) {
|
|
120
|
+
if (maskedContent[i] === '{') depth++;
|
|
121
|
+
else if (maskedContent[i] === '}') {
|
|
122
|
+
depth--;
|
|
123
|
+
if (depth === 0) {
|
|
124
|
+
endIdx = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (endIdx !== -1) {
|
|
131
|
+
const startLine = maskedContent.slice(0, match.index).split('\n').length;
|
|
132
|
+
methods.push({
|
|
133
|
+
name: methodName,
|
|
134
|
+
startLine,
|
|
135
|
+
body: maskedContent.slice(startIdx, endIdx + 1),
|
|
136
|
+
startIdx,
|
|
137
|
+
endIdx
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return methods;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const evaluateMethodComplexityAndNesting = (method, relativePath, violations, config = {}) => {
|
|
146
|
+
const maxComplexity = config.maxCyclomaticComplexity || 12;
|
|
147
|
+
const maxNestingLimit = 4;
|
|
148
|
+
const body = method.body;
|
|
149
|
+
|
|
150
|
+
let cyclomaticComplexity = 1;
|
|
151
|
+
const ifMatches = body.match(/\bif\s*\(/g);
|
|
152
|
+
if (ifMatches) cyclomaticComplexity += ifMatches.length;
|
|
153
|
+
const caseMatches = body.match(/\bcase\b[^:]+:/g);
|
|
154
|
+
if (caseMatches) cyclomaticComplexity += caseMatches.length;
|
|
155
|
+
const whileMatches = body.match(/\bwhile\s*\(/g);
|
|
156
|
+
if (whileMatches) cyclomaticComplexity += whileMatches.length;
|
|
157
|
+
const forMatches = body.match(/\bfor\s*\(/g);
|
|
158
|
+
if (forMatches) cyclomaticComplexity += forMatches.length;
|
|
159
|
+
const foreachMatches = body.match(/\bforeach\s*\(/g);
|
|
160
|
+
if (foreachMatches) cyclomaticComplexity += foreachMatches.length;
|
|
161
|
+
const catchMatches = body.match(/\bcatch\b/g);
|
|
162
|
+
if (catchMatches) cyclomaticComplexity += catchMatches.length;
|
|
163
|
+
const andMatches = body.match(/&&/g);
|
|
164
|
+
if (andMatches) cyclomaticComplexity += andMatches.length;
|
|
165
|
+
const orMatches = body.match(/\|\|/g);
|
|
166
|
+
if (orMatches) cyclomaticComplexity += orMatches.length;
|
|
167
|
+
const nullCoalMatches = body.match(/\?\?/g);
|
|
168
|
+
if (nullCoalMatches) cyclomaticComplexity += nullCoalMatches.length;
|
|
169
|
+
|
|
170
|
+
if (cyclomaticComplexity > maxComplexity) {
|
|
171
|
+
const meta = RULE_REGISTRY.COMPLEXITY_CYCLOMATIC_HIGH;
|
|
172
|
+
violations.push({
|
|
173
|
+
filePath: relativePath,
|
|
174
|
+
line: method.startLine,
|
|
175
|
+
column: 1,
|
|
176
|
+
hazard: `High cyclomatic complexity in method ${method.name} (${cyclomaticComplexity} > ${maxComplexity} threshold)`,
|
|
177
|
+
rule: 'COMPLEXITY_CYCLOMATIC_HIGH',
|
|
178
|
+
severity: meta.severity,
|
|
179
|
+
pillar: meta.pillar,
|
|
180
|
+
directive: meta.directive
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let braceLevel = 0;
|
|
185
|
+
const controlStack = [];
|
|
186
|
+
let maxControlDepth = 0;
|
|
187
|
+
let maxDepthLine = method.startLine;
|
|
188
|
+
const bodyTokens = body.split(/(\bif\s*\(|\belse\s*if\s*\(|\belse\b|\bwhile\s*\(|\bfor\s*\(|\bforeach\s*\(|\bswitch\s*\(|\bcatch\b|\{|\})/g);
|
|
189
|
+
let curLine = method.startLine;
|
|
190
|
+
let pendingControl = null;
|
|
191
|
+
|
|
192
|
+
for (const token of bodyTokens) {
|
|
193
|
+
curLine += (token.match(/\n/g) || []).length;
|
|
194
|
+
if (/\b(?:if|else|while|for|foreach|switch|catch)\b/.test(token)) {
|
|
195
|
+
pendingControl = { line: curLine };
|
|
196
|
+
} else if (token === '{') {
|
|
197
|
+
braceLevel++;
|
|
198
|
+
if (pendingControl) {
|
|
199
|
+
controlStack.push({ braceLevel, line: pendingControl.line });
|
|
200
|
+
pendingControl = null;
|
|
201
|
+
if (controlStack.length > maxControlDepth) {
|
|
202
|
+
maxControlDepth = controlStack.length;
|
|
203
|
+
maxDepthLine = curLine;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else if (token === '}') {
|
|
207
|
+
if (controlStack.length > 0 && controlStack[controlStack.length - 1].braceLevel === braceLevel) {
|
|
208
|
+
controlStack.pop();
|
|
209
|
+
}
|
|
210
|
+
braceLevel--;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (maxControlDepth >= maxNestingLimit) {
|
|
215
|
+
const meta = RULE_REGISTRY.STRUCTURAL_WEIGHT_EXCEEDED;
|
|
216
|
+
violations.push({
|
|
217
|
+
filePath: relativePath,
|
|
218
|
+
line: maxDepthLine,
|
|
219
|
+
column: 1,
|
|
220
|
+
hazard: `Excessive control flow nesting depth (${maxControlDepth} levels >= ${maxNestingLimit}) in method ${method.name}`,
|
|
221
|
+
rule: 'STRUCTURAL_WEIGHT_EXCEEDED',
|
|
222
|
+
severity: meta.severity,
|
|
223
|
+
pillar: meta.pillar,
|
|
224
|
+
directive: 'Flatten control flow using early-return guard clauses or decomposed methods'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const condRegex = /\b(?:if|while)\s*\(([\s\S]*?)\)/g;
|
|
229
|
+
let condMatch;
|
|
230
|
+
while ((condMatch = condRegex.exec(body)) !== null) {
|
|
231
|
+
const expr = condMatch[1];
|
|
232
|
+
const logicOps = (expr.match(/&&|\|\|/g) || []).length;
|
|
233
|
+
if (logicOps >= 3) {
|
|
234
|
+
const lineNum = method.startLine + body.slice(0, condMatch.index).split('\n').length - 1;
|
|
235
|
+
const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
|
|
236
|
+
violations.push({
|
|
237
|
+
filePath: relativePath,
|
|
238
|
+
line: lineNum,
|
|
239
|
+
column: 1,
|
|
240
|
+
hazard: `Complex inline boolean condition detected in method ${method.name} (${logicOps} logical operators)`,
|
|
241
|
+
rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
|
|
242
|
+
severity: meta.severity,
|
|
243
|
+
pillar: meta.pillar,
|
|
244
|
+
directive: meta.directive
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const checkCleanArchitectureAndCoupling = (content, relativePath, violations) => {
|
|
252
|
+
const isController =
|
|
253
|
+
relativePath.includes('/Controllers/') ||
|
|
254
|
+
relativePath.endsWith('Controller.cs') ||
|
|
255
|
+
/:\s*(?:ControllerBase|Controller)\b/.test(content) ||
|
|
256
|
+
/\[ApiController\]/.test(content);
|
|
257
|
+
|
|
258
|
+
if (!isController) return;
|
|
259
|
+
|
|
260
|
+
const ctorRegex = /(?:public|internal)\s+(?:class\s+)?([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)\s*(?::\s*(?:base|this)\s*\([^)]*\)\s*)?\{/g;
|
|
261
|
+
let ctorMatch;
|
|
262
|
+
|
|
263
|
+
while ((ctorMatch = ctorRegex.exec(content)) !== null) {
|
|
264
|
+
const paramsText = ctorMatch[2].trim();
|
|
265
|
+
if (!paramsText) continue;
|
|
266
|
+
|
|
267
|
+
const lineNum = content.slice(0, ctorMatch.index).split('\n').length;
|
|
268
|
+
const rawParams = paramsText.split(',').map((p) => p.trim()).filter(Boolean);
|
|
269
|
+
|
|
270
|
+
if (rawParams.length > 5) {
|
|
271
|
+
const meta = RULE_REGISTRY.COUPLING_EXCESSIVE_INJECTION;
|
|
272
|
+
violations.push({
|
|
273
|
+
filePath: relativePath,
|
|
274
|
+
line: lineNum,
|
|
275
|
+
column: 1,
|
|
276
|
+
hazard: `Excessive constructor dependency injection (${rawParams.length} dependencies > 5 threshold) in controller`,
|
|
277
|
+
rule: 'COUPLING_EXCESSIVE_INJECTION',
|
|
278
|
+
severity: meta.severity,
|
|
279
|
+
pillar: meta.pillar,
|
|
280
|
+
directive: meta.directive
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const hasDbContext = rawParams.some((p) => {
|
|
285
|
+
const isDb = /\b(?:[A-Za-z0-9_]*DbContext|DbContext|[A-Za-z0-9_]*Context)\b/.test(p);
|
|
286
|
+
const isHttp = /\b(?:HttpContext|ActionContext|ControllerContext)\b/.test(p);
|
|
287
|
+
return isDb && !isHttp;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const hasService = rawParams.some((p) => /\b(?:[A-Za-z0-9_]*Service|[A-Za-z0-9_]*Repository)\b/.test(p));
|
|
291
|
+
|
|
292
|
+
if (hasDbContext) {
|
|
293
|
+
const meta = RULE_REGISTRY.LAYER_VIOLATION_CONTROLLER;
|
|
294
|
+
const hazardDesc = hasService
|
|
295
|
+
? 'Clean Architecture layer violation: Controller directly injects DbContext and service dependencies, bypassing MediatR CQRS'
|
|
296
|
+
: 'Clean Architecture layer violation: Controller directly injects DbContext, bypassing application/MediatR layer';
|
|
297
|
+
|
|
298
|
+
violations.push({
|
|
299
|
+
filePath: relativePath,
|
|
300
|
+
line: lineNum,
|
|
301
|
+
column: 1,
|
|
302
|
+
hazard: hazardDesc,
|
|
303
|
+
rule: 'LAYER_VIOLATION_CONTROLLER',
|
|
304
|
+
severity: meta.severity,
|
|
305
|
+
pillar: meta.pillar,
|
|
306
|
+
directive: meta.directive
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const fromServicesDbMatches = content.matchAll(/\[FromServices\]\s*(?:[A-Za-z0-9_]*DbContext|DbContext|[A-Za-z0-9_]*Context)\b/g);
|
|
312
|
+
for (const m of fromServicesDbMatches) {
|
|
313
|
+
const lineNum = content.slice(0, m.index).split('\n').length;
|
|
314
|
+
const meta = RULE_REGISTRY.LAYER_VIOLATION_CONTROLLER;
|
|
315
|
+
violations.push({
|
|
316
|
+
filePath: relativePath,
|
|
317
|
+
line: lineNum,
|
|
318
|
+
column: 1,
|
|
319
|
+
hazard: 'Clean Architecture layer violation: Action method directly injects DbContext via [FromServices]',
|
|
320
|
+
rule: 'LAYER_VIOLATION_CONTROLLER',
|
|
321
|
+
severity: meta.severity,
|
|
322
|
+
pillar: meta.pillar,
|
|
323
|
+
directive: meta.directive
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const analyzeCSharpCode = (content, relativePath, violations, config = {}) => {
|
|
13
329
|
const lines = content.split('\n');
|
|
330
|
+
const maskedContent = maskCommentsAndStrings(content);
|
|
14
331
|
|
|
15
|
-
// Multi-line and single-line shallow catch detection
|
|
16
332
|
const catchMatches = content.matchAll(/catch\s*(?:\([^)]*\))?\s*\{(?:\s*|\s*\/\/[^\n]*\s*)\}/g);
|
|
17
333
|
for (const m of catchMatches) {
|
|
18
334
|
const lineNum = content.slice(0, m.index).split('\n').length;
|
|
@@ -25,14 +341,29 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
25
341
|
rule: 'AI_SLOP_SHALLOW_CATCH',
|
|
26
342
|
severity: meta.severity,
|
|
27
343
|
pillar: meta.pillar,
|
|
28
|
-
directive: meta.directive
|
|
344
|
+
directive: meta.directive,
|
|
345
|
+
isAiSlop: true
|
|
29
346
|
});
|
|
30
347
|
}
|
|
31
348
|
|
|
32
349
|
lines.forEach((lineText, idx) => {
|
|
33
350
|
const lineNum = idx + 1;
|
|
34
351
|
|
|
35
|
-
|
|
352
|
+
if (SIMULATION_SWITCH_PATTERN.test(lineText)) {
|
|
353
|
+
const meta = RULE_REGISTRY.AI_SLOP_LAZY_PLACEHOLDER;
|
|
354
|
+
violations.push({
|
|
355
|
+
filePath: relativePath,
|
|
356
|
+
line: lineNum,
|
|
357
|
+
column: 1,
|
|
358
|
+
hazard: `Global simulation switch detected: "${lineText.trim()}" (unverified synthetic component mode)`,
|
|
359
|
+
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
360
|
+
severity: 'CRITICAL',
|
|
361
|
+
pillar: meta.pillar,
|
|
362
|
+
directive: meta.directive,
|
|
363
|
+
isAiSlop: true
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
36
367
|
if (IN_MEMORY_DB_PATTERN.test(lineText)) {
|
|
37
368
|
const meta = RULE_REGISTRY.SYNTHETIC_MOCK_DATA;
|
|
38
369
|
violations.push({
|
|
@@ -47,6 +378,20 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
47
378
|
});
|
|
48
379
|
}
|
|
49
380
|
|
|
381
|
+
if (SQLITE_PROVIDER_PATTERN.test(lineText)) {
|
|
382
|
+
const meta = RULE_REGISTRY.SYNTHETIC_MOCK_DATA;
|
|
383
|
+
violations.push({
|
|
384
|
+
filePath: relativePath,
|
|
385
|
+
line: lineNum,
|
|
386
|
+
column: 1,
|
|
387
|
+
hazard: 'SQLite provider detected in configuration or test setup (potential production database parity mismatch)',
|
|
388
|
+
rule: 'SYNTHETIC_MOCK_DATA',
|
|
389
|
+
severity: 'HIGH',
|
|
390
|
+
pillar: meta.pillar,
|
|
391
|
+
directive: 'Use Testcontainers or production-identical database engine for integration tests'
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
50
395
|
if (SIMULATED_DELAY_PATTERN.test(lineText)) {
|
|
51
396
|
const meta = RULE_REGISTRY.AI_SLOP_LAZY_PLACEHOLDER;
|
|
52
397
|
violations.push({
|
|
@@ -57,7 +402,8 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
57
402
|
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
58
403
|
severity: meta.severity,
|
|
59
404
|
pillar: meta.pillar,
|
|
60
|
-
directive: meta.directive
|
|
405
|
+
directive: meta.directive,
|
|
406
|
+
isAiSlop: true
|
|
61
407
|
});
|
|
62
408
|
}
|
|
63
409
|
|
|
@@ -76,5 +422,12 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
76
422
|
}
|
|
77
423
|
});
|
|
78
424
|
|
|
425
|
+
const methods = extractMethods(maskedContent);
|
|
426
|
+
for (const method of methods) {
|
|
427
|
+
evaluateMethodComplexityAndNesting(method, relativePath, violations, config);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
checkCleanArchitectureAndCoupling(content, relativePath, violations);
|
|
431
|
+
|
|
79
432
|
return violations;
|
|
80
433
|
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { analyzeCSharpCode, maskCommentsAndStrings } from "./csharp-analyzer.js";
|
|
4
|
+
import { calculateAiSlopScore, calculateMolecularHealthScore } from "./metrics.js";
|
|
5
|
+
|
|
6
|
+
test("csharp-analyzer: maskCommentsAndStrings preserves layout and line count", () => {
|
|
7
|
+
const code = "// comment\n/* block */\nstring s = \"hello\";\n";
|
|
8
|
+
const masked = maskCommentsAndStrings(code);
|
|
9
|
+
assert.equal(masked.split("\n").length, code.split("\n").length);
|
|
10
|
+
assert.equal(masked.includes("hello"), false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("csharp-analyzer: flags global simulation switches with isAiSlop: true", () => {
|
|
14
|
+
const code = `
|
|
15
|
+
namespace Nadena.Domain;
|
|
16
|
+
|
|
17
|
+
public class MarketSimulator
|
|
18
|
+
{
|
|
19
|
+
public static bool SimulateBuyers = true;
|
|
20
|
+
public static bool SimulateOrders { get; set; } = true;
|
|
21
|
+
private static bool UseSimulation = true;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const violations = [];
|
|
26
|
+
analyzeCSharpCode(code, "Domain/MarketSimulator.cs", violations);
|
|
27
|
+
|
|
28
|
+
const slopMatches = violations.filter((v) => v.rule === "AI_SLOP_LAZY_PLACEHOLDER" && v.isAiSlop);
|
|
29
|
+
assert.ok(slopMatches.length >= 3, "Must flag all 3 simulation switches");
|
|
30
|
+
assert.equal(slopMatches[0].severity, "CRITICAL");
|
|
31
|
+
assert.ok(slopMatches[0].hazard.includes("SimulateBuyers"));
|
|
32
|
+
|
|
33
|
+
// Verify AI Slop scoring calculation is degraded
|
|
34
|
+
const slopScore = calculateAiSlopScore(violations, 1);
|
|
35
|
+
assert.ok(slopScore.score < 90, "AI Slop score must drop from 100");
|
|
36
|
+
assert.notEqual(slopScore.grade, "A+");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("csharp-analyzer: detects deep control flow nesting (>= 4 levels) in methods", () => {
|
|
40
|
+
const code = `
|
|
41
|
+
namespace Nadena.Services;
|
|
42
|
+
|
|
43
|
+
public class OrderProcessor
|
|
44
|
+
{
|
|
45
|
+
public async Task ProcessAsync(Order order)
|
|
46
|
+
{
|
|
47
|
+
if (order != null)
|
|
48
|
+
{
|
|
49
|
+
if (order.IsValid)
|
|
50
|
+
{
|
|
51
|
+
if (order.HasItems)
|
|
52
|
+
{
|
|
53
|
+
if (order.Total > 0)
|
|
54
|
+
{
|
|
55
|
+
if (order.IsPaid)
|
|
56
|
+
{
|
|
57
|
+
await SaveAsync(order);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const violations = [];
|
|
68
|
+
analyzeCSharpCode(code, "Services/OrderProcessor.cs", violations);
|
|
69
|
+
|
|
70
|
+
const nestingViolation = violations.find((v) => v.rule === "STRUCTURAL_WEIGHT_EXCEEDED");
|
|
71
|
+
assert.ok(nestingViolation, "Must flag excessive control flow nesting depth");
|
|
72
|
+
assert.ok(nestingViolation.hazard.includes("nesting depth"));
|
|
73
|
+
assert.equal(nestingViolation.severity, "HIGH");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("csharp-analyzer: detects high cyclomatic complexity (> 12) in methods", () => {
|
|
77
|
+
const code = `
|
|
78
|
+
namespace Nadena.Services;
|
|
79
|
+
|
|
80
|
+
public class ComplexEvaluator
|
|
81
|
+
{
|
|
82
|
+
public int EvaluateScore(int a, int b, int c, int d, int e, int f)
|
|
83
|
+
{
|
|
84
|
+
int score = 0;
|
|
85
|
+
if (a > 1) score++;
|
|
86
|
+
if (b > 2) score++;
|
|
87
|
+
if (c > 3) score++;
|
|
88
|
+
if (d > 4) score++;
|
|
89
|
+
if (e > 5) score++;
|
|
90
|
+
if (f > 6) score++;
|
|
91
|
+
if (a == b) score++;
|
|
92
|
+
if (b == c) score++;
|
|
93
|
+
if (c == d) score++;
|
|
94
|
+
if (d == e) score++;
|
|
95
|
+
if (e == f) score++;
|
|
96
|
+
if (a + b > 10) score++;
|
|
97
|
+
if (c + d > 10) score++;
|
|
98
|
+
return score;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
analyzeCSharpCode(code, "Services/ComplexEvaluator.cs", violations);
|
|
105
|
+
|
|
106
|
+
const complexityViolation = violations.find((v) => v.rule === "COMPLEXITY_CYCLOMATIC_HIGH");
|
|
107
|
+
assert.ok(complexityViolation, "Must flag cyclomatic complexity > 12");
|
|
108
|
+
assert.ok(complexityViolation.hazard.includes("EvaluateScore"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("csharp-analyzer: detects complex inline boolean conditions in control flow", () => {
|
|
112
|
+
const code = `
|
|
113
|
+
namespace Nadena.Services;
|
|
114
|
+
|
|
115
|
+
public class Validator
|
|
116
|
+
{
|
|
117
|
+
public bool Check(Item item)
|
|
118
|
+
{
|
|
119
|
+
if (item.A && item.B && item.C || item.D)
|
|
120
|
+
{
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const violations = [];
|
|
129
|
+
analyzeCSharpCode(code, "Services/Validator.cs", violations);
|
|
130
|
+
|
|
131
|
+
const inlineBool = violations.find((v) => v.rule === "CONTROL_FLOW_INLINE_BOOLEAN");
|
|
132
|
+
assert.ok(inlineBool, "Must flag complex inline boolean condition with >= 3 operators");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("csharp-analyzer: flags Clean Architecture layer violations in controllers", () => {
|
|
136
|
+
const controllerCode = `
|
|
137
|
+
namespace Nadena.Controllers;
|
|
138
|
+
|
|
139
|
+
[ApiController]
|
|
140
|
+
[Route("api/[controller]")]
|
|
141
|
+
public class OrdersController : ControllerBase
|
|
142
|
+
{
|
|
143
|
+
private readonly AppDbContext _context;
|
|
144
|
+
private readonly IOrderService _orderService;
|
|
145
|
+
|
|
146
|
+
public OrdersController(AppDbContext context, IOrderService orderService)
|
|
147
|
+
{
|
|
148
|
+
_context = context;
|
|
149
|
+
_orderService = orderService;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[HttpGet]
|
|
153
|
+
public IActionResult GetDirect([FromServices] ApplicationDbContext inlineDb)
|
|
154
|
+
{
|
|
155
|
+
return Ok();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const violations = [];
|
|
161
|
+
analyzeCSharpCode(controllerCode, "Controllers/OrdersController.cs", violations);
|
|
162
|
+
|
|
163
|
+
const layerViolations = violations.filter((v) => v.rule === "LAYER_VIOLATION_CONTROLLER");
|
|
164
|
+
assert.ok(layerViolations.length >= 2, "Must flag constructor DbContext and [FromServices] DbContext");
|
|
165
|
+
assert.equal(layerViolations[0].severity, "CRITICAL");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("csharp-analyzer: flags excessive controller constructor injection (> 5 dependencies)", () => {
|
|
169
|
+
const controllerCode = `
|
|
170
|
+
namespace Nadena.Controllers;
|
|
171
|
+
|
|
172
|
+
public class HeavyController : ControllerBase
|
|
173
|
+
{
|
|
174
|
+
public HeavyController(
|
|
175
|
+
IAuthService auth,
|
|
176
|
+
IUserService user,
|
|
177
|
+
IOrderService order,
|
|
178
|
+
IEmailService email,
|
|
179
|
+
IPaymentService payment,
|
|
180
|
+
ILogger<HeavyController> logger)
|
|
181
|
+
{
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
const violations = [];
|
|
187
|
+
analyzeCSharpCode(controllerCode, "Controllers/HeavyController.cs", violations);
|
|
188
|
+
|
|
189
|
+
const coupling = violations.find((v) => v.rule === "COUPLING_EXCESSIVE_INJECTION");
|
|
190
|
+
assert.ok(coupling, "Must flag constructor with 6 dependencies");
|
|
191
|
+
assert.equal(coupling.severity, "HIGH");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("csharp-analyzer: flags SQLite test provider in tests/configuration", () => {
|
|
195
|
+
const testCode = `
|
|
196
|
+
using Microsoft.EntityFrameworkCore;
|
|
197
|
+
using Xunit;
|
|
198
|
+
|
|
199
|
+
public class OrderTests
|
|
200
|
+
{
|
|
201
|
+
[Fact]
|
|
202
|
+
public void TestDatabase()
|
|
203
|
+
{
|
|
204
|
+
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
205
|
+
.UseSqlite("DataSource=:memory:")
|
|
206
|
+
.Options;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
const violations = [];
|
|
212
|
+
analyzeCSharpCode(testCode, "Tests/OrderTests.cs", violations);
|
|
213
|
+
|
|
214
|
+
const sqliteViolation = violations.find((v) => v.rule === "SYNTHETIC_MOCK_DATA" && v.hazard.includes("SQLite"));
|
|
215
|
+
assert.ok(sqliteViolation, "Must flag UseSqlite/DataSource=:memory: provider");
|
|
216
|
+
});
|
|
@@ -32,7 +32,10 @@ const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*
|
|
|
32
32
|
/**
|
|
33
33
|
* Pillar 10: Check for co-located test files for molecule capsules.
|
|
34
34
|
*/
|
|
35
|
-
export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
|
|
35
|
+
export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations, config = {}) => {
|
|
36
|
+
const enforceColocated = config.enforceColocatedTests ?? (config.profile === 'atomic-strict');
|
|
37
|
+
if (!enforceColocated) return;
|
|
38
|
+
|
|
36
39
|
const baseName = path.basename(filePath);
|
|
37
40
|
const ext = path.extname(filePath);
|
|
38
41
|
|
|
@@ -194,8 +197,8 @@ export const checkControllerViewContract = (filePath, relativePath, content, vio
|
|
|
194
197
|
}
|
|
195
198
|
};
|
|
196
199
|
|
|
197
|
-
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
|
|
198
|
-
checkMoleculeCoLocatedTest(filePath, relativePath, violations);
|
|
200
|
+
export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations, config = {}) => {
|
|
201
|
+
checkMoleculeCoLocatedTest(filePath, relativePath, violations, config);
|
|
199
202
|
checkControllerViewContract(filePath, relativePath, content, violations);
|
|
200
203
|
|
|
201
204
|
const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath)
|