@aiready/ai-signal-clarity 0.11.21 → 0.12.1
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/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-test.log +8 -8
- package/dist/chunk-2SCIL2EA.mjs +557 -0
- package/dist/chunk-5WOEEOWG.mjs +526 -0
- package/dist/cli.js +74 -11
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +74 -11
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
- package/src/__tests__/scanner.test.ts +44 -9
- package/src/__tests__/scoring.test.ts +7 -3
- package/src/analyzer.ts +3 -0
- package/src/scanner.ts +103 -8
- package/src/scoring.ts +1 -0
- package/src/types.ts +5 -1
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/ai-signal-clarity@0.
|
|
3
|
+
> @aiready/ai-signal-clarity@0.12.1 build /Users/pengcao/projects/aiready/packages/ai-signal-clarity
|
|
4
4
|
> tsup src/index.ts src/cli.ts --format cjs,esm --dts
|
|
5
5
|
|
|
6
6
|
[34mCLI[39m Building entry: src/cli.ts, src/index.ts
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
[34mCLI[39m Target: es2020
|
|
10
10
|
[34mCJS[39m Build start
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/chunk-4RIAQ4QQ.mjs [22m[32m16.98 KB[39m
|
|
13
|
-
[32mESM[39m [1mdist/cli.mjs [22m[32m6.83 KB[39m
|
|
14
12
|
[32mESM[39m [1mdist/index.mjs [22m[32m1.40 KB[39m
|
|
15
|
-
[32mESM[39m
|
|
16
|
-
[
|
|
17
|
-
[
|
|
18
|
-
[32mCJS[39m
|
|
13
|
+
[32mESM[39m [1mdist/chunk-2SCIL2EA.mjs [22m[32m19.32 KB[39m
|
|
14
|
+
[32mESM[39m [1mdist/cli.mjs [22m[32m6.83 KB[39m
|
|
15
|
+
[32mESM[39m ⚡️ Build success in 66ms
|
|
16
|
+
[32mCJS[39m [1mdist/cli.js [22m[32m28.05 KB[39m
|
|
17
|
+
[32mCJS[39m [1mdist/index.js [22m[32m22.15 KB[39m
|
|
18
|
+
[32mCJS[39m ⚡️ Build success in 66ms
|
|
19
19
|
DTS Build start
|
|
20
|
-
DTS ⚡️ Build success in
|
|
20
|
+
DTS ⚡️ Build success in 1087ms
|
|
21
21
|
DTS dist/cli.d.ts 20.00 B
|
|
22
|
-
DTS dist/index.d.ts 2.
|
|
22
|
+
DTS dist/index.d.ts 2.90 KB
|
|
23
23
|
DTS dist/cli.d.mts 20.00 B
|
|
24
|
-
DTS dist/index.d.mts 2.
|
|
24
|
+
DTS dist/index.d.mts 2.90 KB
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
> @aiready/ai-signal-clarity@0.
|
|
3
|
+
> @aiready/ai-signal-clarity@0.12.0 test /Users/pengcao/projects/aiready/packages/ai-signal-clarity
|
|
4
4
|
> vitest run
|
|
5
5
|
|
|
6
6
|
[?25l
|
|
7
7
|
[1m[46m RUN [49m[22m [36mv4.0.18 [39m[90m/Users/pengcao/projects/aiready/packages/ai-signal-clarity[39m
|
|
8
8
|
|
|
9
|
+
[32m✓[39m src/__tests__/scanner-advanced.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 19[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/scanner.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 26[2mms[22m[39m
|
|
9
11
|
[32m✓[39m src/__tests__/analyzer.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
10
|
-
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
11
|
-
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m
|
|
12
|
-
[32m✓[39m src/__tests__/contract.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m
|
|
13
|
-
[32m✓[39m src/__tests__/scanner-advanced.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 35[2mms[22m[39m
|
|
14
|
-
[32m✓[39m src/__tests__/scanner.test.ts [2m([22m[2m6 tests[22m[2m)[22m[32m 29[2mms[22m[39m
|
|
12
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
13
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m2 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
14
|
+
[32m✓[39m src/__tests__/contract.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 4[2mms[22m[39m
|
|
15
15
|
|
|
16
16
|
[2m Test Files [22m [1m[32m6 passed[39m[22m[90m (6)[39m
|
|
17
17
|
[2m Tests [22m [1m[32m15 passed[39m[22m[90m (15)[39m
|
|
18
|
-
[2m Start at [22m
|
|
19
|
-
[2m Duration [22m
|
|
18
|
+
[2m Start at [22m 00:58:49
|
|
19
|
+
[2m Duration [22m 2.68s[2m (transform 1.64s, setup 0ms, import 9.94s, tests 60ms, environment 1ms)[22m
|
|
20
20
|
|
|
21
21
|
[?25h
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
// src/scanner.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { getParser, Severity, IssueType } from "@aiready/core";
|
|
4
|
+
var AMBIGUOUS_NAME_PATTERNS = [
|
|
5
|
+
/^[a-z]$/,
|
|
6
|
+
// single letter: a, b, x, y
|
|
7
|
+
/^(tmp|temp|data|obj|val|res|ret|result|item|elem|thing|stuff|info|misc|util|helper|handler|cb|fn|func)$/i,
|
|
8
|
+
/^[a-z]\d+$/
|
|
9
|
+
// x1, x2, n3
|
|
10
|
+
];
|
|
11
|
+
var MAGIC_LITERAL_IGNORE = /* @__PURE__ */ new Set([0, 1, -1, 2, 10, 100, 1e3, 1024]);
|
|
12
|
+
var MAGIC_STRING_IGNORE = /* @__PURE__ */ new Set([
|
|
13
|
+
"",
|
|
14
|
+
" ",
|
|
15
|
+
"\n",
|
|
16
|
+
" ",
|
|
17
|
+
"utf8",
|
|
18
|
+
"utf-8",
|
|
19
|
+
"hex",
|
|
20
|
+
"base64",
|
|
21
|
+
"true",
|
|
22
|
+
"false",
|
|
23
|
+
"null",
|
|
24
|
+
"undefined",
|
|
25
|
+
"node",
|
|
26
|
+
"production",
|
|
27
|
+
"development",
|
|
28
|
+
"test",
|
|
29
|
+
"error",
|
|
30
|
+
"warn",
|
|
31
|
+
"info",
|
|
32
|
+
"debug",
|
|
33
|
+
"main",
|
|
34
|
+
"module",
|
|
35
|
+
"types",
|
|
36
|
+
"scripts",
|
|
37
|
+
"dependencies",
|
|
38
|
+
"devDependencies",
|
|
39
|
+
"peerDependencies",
|
|
40
|
+
"remove",
|
|
41
|
+
"delete",
|
|
42
|
+
"update",
|
|
43
|
+
"create"
|
|
44
|
+
]);
|
|
45
|
+
var TAILWIND_PATTERN = /^[a-z0-9:-]+(\/[0-9]+)?$/;
|
|
46
|
+
function isAmbiguousName(name) {
|
|
47
|
+
return AMBIGUOUS_NAME_PATTERNS.some((p) => p.test(name));
|
|
48
|
+
}
|
|
49
|
+
function isMagicNumber(value) {
|
|
50
|
+
return !MAGIC_LITERAL_IGNORE.has(value);
|
|
51
|
+
}
|
|
52
|
+
function isMagicString(value) {
|
|
53
|
+
if (value.length === 0) return false;
|
|
54
|
+
if (value.length > 20) return false;
|
|
55
|
+
if (MAGIC_STRING_IGNORE.has(value)) return false;
|
|
56
|
+
if (TAILWIND_PATTERN.test(value) && value.includes("-")) return false;
|
|
57
|
+
if (value === value.toUpperCase() && value.length > 3) return false;
|
|
58
|
+
if (/[/.]/.test(value)) return false;
|
|
59
|
+
if (/^#[0-9a-fA-F]{3,6}$/.test(value)) return false;
|
|
60
|
+
return !/^\s+$/.test(value);
|
|
61
|
+
}
|
|
62
|
+
async function scanFile(filePath, options = { rootDir: "." }) {
|
|
63
|
+
let code;
|
|
64
|
+
try {
|
|
65
|
+
code = readFileSync(filePath, "utf-8");
|
|
66
|
+
} catch {
|
|
67
|
+
return emptyResult(filePath);
|
|
68
|
+
}
|
|
69
|
+
const parser = getParser(filePath);
|
|
70
|
+
if (!parser) return emptyResult(filePath);
|
|
71
|
+
try {
|
|
72
|
+
await parser.initialize();
|
|
73
|
+
const result = parser.parse(code, filePath);
|
|
74
|
+
const ast = await parser.getAST(code, filePath);
|
|
75
|
+
const issues = [];
|
|
76
|
+
const lineCount = code.split("\n").length;
|
|
77
|
+
const signals = {
|
|
78
|
+
magicLiterals: 0,
|
|
79
|
+
booleanTraps: 0,
|
|
80
|
+
ambiguousNames: 0,
|
|
81
|
+
undocumentedExports: 0,
|
|
82
|
+
implicitSideEffects: 0,
|
|
83
|
+
deepCallbacks: 0,
|
|
84
|
+
overloadedSymbols: 0,
|
|
85
|
+
largeFiles: 0,
|
|
86
|
+
totalSymbols: result.exports.length + result.imports.length,
|
|
87
|
+
totalExports: result.exports.length,
|
|
88
|
+
totalLines: lineCount
|
|
89
|
+
};
|
|
90
|
+
const symbolCounts = /* @__PURE__ */ new Map();
|
|
91
|
+
if (options.checkLargeFiles !== false) {
|
|
92
|
+
if (lineCount > 750) {
|
|
93
|
+
signals.largeFiles++;
|
|
94
|
+
issues.push({
|
|
95
|
+
type: IssueType.AiSignalClarity,
|
|
96
|
+
category: "large-file",
|
|
97
|
+
severity: Severity.Critical,
|
|
98
|
+
message: `Extreme file length (${lineCount} lines) \u2014 AI context window will overflow or "Lose the Middle" critical details.`,
|
|
99
|
+
location: { file: filePath, line: 1 },
|
|
100
|
+
suggestion: "Split into smaller, single-responsibility modules (< 500 lines)."
|
|
101
|
+
});
|
|
102
|
+
} else if (lineCount > 500) {
|
|
103
|
+
signals.largeFiles++;
|
|
104
|
+
issues.push({
|
|
105
|
+
type: IssueType.AiSignalClarity,
|
|
106
|
+
category: "large-file",
|
|
107
|
+
severity: Severity.Major,
|
|
108
|
+
message: `Large file (${lineCount} lines) \u2014 pushing the limits of effective AI reasoning.`,
|
|
109
|
+
location: { file: filePath, line: 1 },
|
|
110
|
+
suggestion: "Consider refactoring and extracting logic to new files."
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (const exp of result.exports) {
|
|
115
|
+
symbolCounts.set(exp.name, (symbolCounts.get(exp.name) || 0) + 1);
|
|
116
|
+
if (options.checkUndocumentedExports !== false) {
|
|
117
|
+
if (!exp.documentation || !exp.documentation.content) {
|
|
118
|
+
signals.undocumentedExports++;
|
|
119
|
+
issues.push({
|
|
120
|
+
type: IssueType.AiSignalClarity,
|
|
121
|
+
category: "undocumented-export",
|
|
122
|
+
severity: Severity.Minor,
|
|
123
|
+
message: `Public export "${exp.name}" has no documentation \u2014 AI fabricates behavior from the name alone.`,
|
|
124
|
+
location: {
|
|
125
|
+
file: filePath,
|
|
126
|
+
line: exp.loc?.start.line || 1,
|
|
127
|
+
column: exp.loc?.start.column
|
|
128
|
+
},
|
|
129
|
+
suggestion: "Add a docstring or comment describing parameters, return value, and side effects."
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (options.checkImplicitSideEffects !== false) {
|
|
134
|
+
if (exp.hasSideEffects && !exp.isPure && exp.type === "function") {
|
|
135
|
+
const lowerName = exp.name.toLowerCase();
|
|
136
|
+
const looksPure = !/(set|update|save|delete|create|write|send|post|sync)/.test(
|
|
137
|
+
lowerName
|
|
138
|
+
);
|
|
139
|
+
if (looksPure) {
|
|
140
|
+
signals.implicitSideEffects++;
|
|
141
|
+
issues.push({
|
|
142
|
+
type: IssueType.AiSignalClarity,
|
|
143
|
+
category: "implicit-side-effect",
|
|
144
|
+
severity: Severity.Major,
|
|
145
|
+
message: `Function "${exp.name}" mutates external state but name doesn't reflect it \u2014 AI misses this contract.`,
|
|
146
|
+
location: {
|
|
147
|
+
file: filePath,
|
|
148
|
+
line: exp.loc?.start.line || 1
|
|
149
|
+
},
|
|
150
|
+
suggestion: "Make side-effects explicit in function name (e.g., updateX) or return a result."
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (options.checkAmbiguousNames !== false && isAmbiguousName(exp.name)) {
|
|
156
|
+
signals.ambiguousNames++;
|
|
157
|
+
issues.push({
|
|
158
|
+
type: IssueType.AmbiguousApi,
|
|
159
|
+
category: "ambiguous-name",
|
|
160
|
+
severity: Severity.Info,
|
|
161
|
+
message: `Ambiguous public export "${exp.name}" \u2014 AI cannot infer intent and will guess incorrectly.`,
|
|
162
|
+
location: {
|
|
163
|
+
file: filePath,
|
|
164
|
+
line: exp.loc?.start.line || 1
|
|
165
|
+
},
|
|
166
|
+
suggestion: "Use a domain-descriptive name instead."
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (options.checkOverloadedSymbols !== false) {
|
|
171
|
+
for (const [name, count] of symbolCounts.entries()) {
|
|
172
|
+
if (count > 1 && name !== "default" && name !== "anonymous") {
|
|
173
|
+
signals.overloadedSymbols++;
|
|
174
|
+
issues.push({
|
|
175
|
+
type: IssueType.AiSignalClarity,
|
|
176
|
+
category: "overloaded-symbol",
|
|
177
|
+
severity: Severity.Critical,
|
|
178
|
+
message: `Symbol "${name}" has ${count} overloaded signatures \u2014 AI often picks the wrong one or gets confused by conflicting contracts.`,
|
|
179
|
+
location: {
|
|
180
|
+
file: filePath,
|
|
181
|
+
line: 1
|
|
182
|
+
},
|
|
183
|
+
suggestion: `Rename overloads to unique, descriptive names if possible.`
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (ast) {
|
|
189
|
+
let callbackDepth = 0;
|
|
190
|
+
let maxCallbackDepth = 0;
|
|
191
|
+
const visitNode = (node, parent, keyInParent) => {
|
|
192
|
+
if (!node) return;
|
|
193
|
+
if (options.checkMagicLiterals !== false) {
|
|
194
|
+
if (node.type === "number") {
|
|
195
|
+
const val = parseFloat(node.text);
|
|
196
|
+
if (!isNaN(val) && isMagicNumber(val)) {
|
|
197
|
+
signals.magicLiterals++;
|
|
198
|
+
issues.push({
|
|
199
|
+
type: IssueType.MagicLiteral,
|
|
200
|
+
category: "magic-literal",
|
|
201
|
+
severity: Severity.Minor,
|
|
202
|
+
message: `Magic number ${node.text} \u2014 AI will invent wrong semantics. Extract to a named constant.`,
|
|
203
|
+
location: {
|
|
204
|
+
file: filePath,
|
|
205
|
+
line: node.startPosition.row + 1,
|
|
206
|
+
column: node.startPosition.column
|
|
207
|
+
},
|
|
208
|
+
suggestion: `const MEANINGFUL_NAME = ${node.text};`
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
} else if (node.type === "string" || node.type === "string_literal") {
|
|
212
|
+
const val = node.text.replace(/['"]/g, "");
|
|
213
|
+
const isKey = node.parent?.type?.includes("pair") || node.parent?.type === "assignment_expression";
|
|
214
|
+
if (!isKey && isMagicString(val)) {
|
|
215
|
+
signals.magicLiterals++;
|
|
216
|
+
issues.push({
|
|
217
|
+
type: IssueType.MagicLiteral,
|
|
218
|
+
category: "magic-literal",
|
|
219
|
+
severity: Severity.Info,
|
|
220
|
+
message: `Magic string "${val}" \u2014 intent is ambiguous to AI. Consider a named constant.`,
|
|
221
|
+
location: {
|
|
222
|
+
file: filePath,
|
|
223
|
+
line: node.startPosition.row + 1
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
} else if (node.type === "Literal") {
|
|
228
|
+
const isNamedConstant = parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z0-9_]{3,}$/.test(parent.id.name);
|
|
229
|
+
const isObjectKey = parent?.type === "Property" && keyInParent === "key";
|
|
230
|
+
const isJSXClassName = parent?.type === "JSXAttribute" && parent.name?.name === "className";
|
|
231
|
+
if (isNamedConstant || isObjectKey || isJSXClassName) {
|
|
232
|
+
} else if (typeof node.value === "number" && isMagicNumber(node.value)) {
|
|
233
|
+
signals.magicLiterals++;
|
|
234
|
+
issues.push({
|
|
235
|
+
type: IssueType.MagicLiteral,
|
|
236
|
+
category: "magic-literal",
|
|
237
|
+
severity: Severity.Minor,
|
|
238
|
+
message: `Magic number ${node.value} \u2014 AI will invent wrong semantics. Extract to a named constant.`,
|
|
239
|
+
location: {
|
|
240
|
+
file: filePath,
|
|
241
|
+
line: node.loc?.start.line || 1,
|
|
242
|
+
column: node.loc?.start.column
|
|
243
|
+
},
|
|
244
|
+
suggestion: `const MEANINGFUL_NAME = ${node.value};`
|
|
245
|
+
});
|
|
246
|
+
} else if (typeof node.value === "string" && isMagicString(node.value)) {
|
|
247
|
+
signals.magicLiterals++;
|
|
248
|
+
issues.push({
|
|
249
|
+
type: IssueType.MagicLiteral,
|
|
250
|
+
category: "magic-literal",
|
|
251
|
+
severity: Severity.Info,
|
|
252
|
+
message: `Magic string "${node.value}" \u2014 intent is ambiguous to AI. Consider a named constant.`,
|
|
253
|
+
location: {
|
|
254
|
+
file: filePath,
|
|
255
|
+
line: node.loc?.start.line || 1
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (options.checkBooleanTraps !== false) {
|
|
262
|
+
if (node.type === "argument_list") {
|
|
263
|
+
const hasBool = node.namedChildren?.some(
|
|
264
|
+
(c) => c.type === "true" || c.type === "false" || c.type === "boolean" && (c.text === "true" || c.text === "false")
|
|
265
|
+
);
|
|
266
|
+
if (hasBool) {
|
|
267
|
+
signals.booleanTraps++;
|
|
268
|
+
issues.push({
|
|
269
|
+
type: IssueType.BooleanTrap,
|
|
270
|
+
category: "boolean-trap",
|
|
271
|
+
severity: Severity.Major,
|
|
272
|
+
message: `Boolean trap: positional boolean argument at call site. AI inverts intent ~30% of the time.`,
|
|
273
|
+
location: {
|
|
274
|
+
file: filePath,
|
|
275
|
+
line: (node.startPosition?.row || 0) + 1
|
|
276
|
+
},
|
|
277
|
+
suggestion: "Replace boolean arg with a named options object or separate functions."
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} else if (node.type === "CallExpression") {
|
|
281
|
+
const hasBool = node.arguments.some(
|
|
282
|
+
(arg) => arg.type === "Literal" && typeof arg.value === "boolean"
|
|
283
|
+
);
|
|
284
|
+
if (hasBool) {
|
|
285
|
+
signals.booleanTraps++;
|
|
286
|
+
issues.push({
|
|
287
|
+
type: IssueType.BooleanTrap,
|
|
288
|
+
category: "boolean-trap",
|
|
289
|
+
severity: Severity.Major,
|
|
290
|
+
message: `Boolean trap: positional boolean argument at call site. AI inverts intent ~30% of the time.`,
|
|
291
|
+
location: {
|
|
292
|
+
file: filePath,
|
|
293
|
+
line: node.loc?.start.line || 1
|
|
294
|
+
},
|
|
295
|
+
suggestion: "Replace boolean arg with a named options object or separate functions."
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (options.checkAmbiguousNames !== false) {
|
|
301
|
+
if (node.type === "variable_declarator") {
|
|
302
|
+
const nameNode = node.childForFieldName("name");
|
|
303
|
+
if (nameNode && isAmbiguousName(nameNode.text)) {
|
|
304
|
+
signals.ambiguousNames++;
|
|
305
|
+
issues.push({
|
|
306
|
+
type: IssueType.AmbiguousApi,
|
|
307
|
+
category: "ambiguous-name",
|
|
308
|
+
severity: Severity.Info,
|
|
309
|
+
message: `Ambiguous variable name "${nameNode.text}" \u2014 AI intent is unclear.`,
|
|
310
|
+
location: {
|
|
311
|
+
file: filePath,
|
|
312
|
+
line: node.startPosition.row + 1
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
} else if (node.type === "VariableDeclarator" && node.id.type === "Identifier") {
|
|
317
|
+
if (isAmbiguousName(node.id.name)) {
|
|
318
|
+
signals.ambiguousNames++;
|
|
319
|
+
issues.push({
|
|
320
|
+
type: IssueType.AmbiguousApi,
|
|
321
|
+
category: "ambiguous-name",
|
|
322
|
+
severity: Severity.Info,
|
|
323
|
+
message: `Ambiguous variable name "${node.id.name}" \u2014 AI intent is unclear.`,
|
|
324
|
+
location: {
|
|
325
|
+
file: filePath,
|
|
326
|
+
line: node.loc?.start.line || 1
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const nodeType = (node.type || "").toLowerCase();
|
|
333
|
+
const isFunction = nodeType.includes("function") || nodeType.includes("arrow") || nodeType.includes("lambda") || nodeType === "method_declaration";
|
|
334
|
+
if (isFunction) {
|
|
335
|
+
callbackDepth++;
|
|
336
|
+
maxCallbackDepth = Math.max(maxCallbackDepth, callbackDepth);
|
|
337
|
+
}
|
|
338
|
+
if (node.namedChildren) {
|
|
339
|
+
for (const child of node.namedChildren) {
|
|
340
|
+
visitNode(child, node);
|
|
341
|
+
}
|
|
342
|
+
} else {
|
|
343
|
+
for (const key in node) {
|
|
344
|
+
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
345
|
+
const child = node[key];
|
|
346
|
+
if (child && typeof child === "object") {
|
|
347
|
+
if (Array.isArray(child)) {
|
|
348
|
+
child.forEach(
|
|
349
|
+
(c) => c && typeof c.type === "string" && visitNode(c, node, key)
|
|
350
|
+
);
|
|
351
|
+
} else if (typeof child.type === "string") {
|
|
352
|
+
visitNode(child, node, key);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (isFunction) {
|
|
358
|
+
callbackDepth--;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
if (ast.rootNode) {
|
|
362
|
+
visitNode(ast.rootNode);
|
|
363
|
+
} else {
|
|
364
|
+
visitNode(ast);
|
|
365
|
+
}
|
|
366
|
+
if (options.checkDeepCallbacks !== false && maxCallbackDepth >= 3) {
|
|
367
|
+
signals.deepCallbacks = maxCallbackDepth - 2;
|
|
368
|
+
issues.push({
|
|
369
|
+
type: IssueType.AiSignalClarity,
|
|
370
|
+
category: "deep-callback",
|
|
371
|
+
severity: Severity.Major,
|
|
372
|
+
message: `Deeply nested logic (depth ${maxCallbackDepth}) \u2014 AI loses control flow context beyond 3 levels.`,
|
|
373
|
+
location: {
|
|
374
|
+
file: filePath,
|
|
375
|
+
line: 1
|
|
376
|
+
},
|
|
377
|
+
suggestion: "Extract nested logic into named functions or flatten the structure."
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
filePath,
|
|
383
|
+
issues,
|
|
384
|
+
signals,
|
|
385
|
+
fileName: filePath,
|
|
386
|
+
metrics: {
|
|
387
|
+
totalSymbols: signals.totalSymbols,
|
|
388
|
+
totalExports: signals.totalExports
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error(`AI Signal Clarity: Failed to scan ${filePath}: ${error}`);
|
|
393
|
+
return emptyResult(filePath);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function emptyResult(filePath) {
|
|
397
|
+
return {
|
|
398
|
+
filePath,
|
|
399
|
+
issues: [],
|
|
400
|
+
signals: {
|
|
401
|
+
magicLiterals: 0,
|
|
402
|
+
booleanTraps: 0,
|
|
403
|
+
ambiguousNames: 0,
|
|
404
|
+
undocumentedExports: 0,
|
|
405
|
+
implicitSideEffects: 0,
|
|
406
|
+
deepCallbacks: 0,
|
|
407
|
+
overloadedSymbols: 0,
|
|
408
|
+
largeFiles: 0,
|
|
409
|
+
totalSymbols: 0,
|
|
410
|
+
totalExports: 0,
|
|
411
|
+
totalLines: 0
|
|
412
|
+
},
|
|
413
|
+
fileName: filePath,
|
|
414
|
+
metrics: {
|
|
415
|
+
totalSymbols: 0,
|
|
416
|
+
totalExports: 0
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/analyzer.ts
|
|
422
|
+
import {
|
|
423
|
+
scanFiles,
|
|
424
|
+
calculateAiSignalClarity,
|
|
425
|
+
Severity as Severity2,
|
|
426
|
+
emitProgress
|
|
427
|
+
} from "@aiready/core";
|
|
428
|
+
async function analyzeAiSignalClarity(options) {
|
|
429
|
+
const files = await scanFiles(options);
|
|
430
|
+
const results = [];
|
|
431
|
+
const aggregate = {
|
|
432
|
+
magicLiterals: 0,
|
|
433
|
+
booleanTraps: 0,
|
|
434
|
+
ambiguousNames: 0,
|
|
435
|
+
undocumentedExports: 0,
|
|
436
|
+
implicitSideEffects: 0,
|
|
437
|
+
deepCallbacks: 0,
|
|
438
|
+
overloadedSymbols: 0,
|
|
439
|
+
largeFiles: 0,
|
|
440
|
+
totalSymbols: 0,
|
|
441
|
+
totalExports: 0,
|
|
442
|
+
totalLines: 0
|
|
443
|
+
};
|
|
444
|
+
let processed = 0;
|
|
445
|
+
for (const filePath of files) {
|
|
446
|
+
processed++;
|
|
447
|
+
emitProgress(
|
|
448
|
+
processed,
|
|
449
|
+
files.length,
|
|
450
|
+
"ai-signal-clarity",
|
|
451
|
+
"analyzing files",
|
|
452
|
+
options.onProgress
|
|
453
|
+
);
|
|
454
|
+
const result = await scanFile(filePath, options);
|
|
455
|
+
results.push(result);
|
|
456
|
+
for (const key of Object.keys(aggregate)) {
|
|
457
|
+
aggregate[key] += result.signals[key] ?? 0;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const riskResult = calculateAiSignalClarity({
|
|
461
|
+
overloadedSymbols: aggregate.overloadedSymbols,
|
|
462
|
+
magicLiterals: aggregate.magicLiterals,
|
|
463
|
+
booleanTraps: aggregate.booleanTraps,
|
|
464
|
+
implicitSideEffects: aggregate.implicitSideEffects,
|
|
465
|
+
deepCallbacks: aggregate.deepCallbacks,
|
|
466
|
+
ambiguousNames: aggregate.ambiguousNames,
|
|
467
|
+
undocumentedExports: aggregate.undocumentedExports,
|
|
468
|
+
largeFiles: aggregate.largeFiles,
|
|
469
|
+
totalSymbols: Math.max(1, aggregate.totalSymbols),
|
|
470
|
+
totalExports: Math.max(1, aggregate.totalExports)
|
|
471
|
+
});
|
|
472
|
+
const getLevel = (s) => {
|
|
473
|
+
if (s === Severity2.Critical || s === "critical") return 4;
|
|
474
|
+
if (s === Severity2.Major || s === "major") return 3;
|
|
475
|
+
if (s === Severity2.Minor || s === "minor") return 2;
|
|
476
|
+
if (s === Severity2.Info || s === "info") return 1;
|
|
477
|
+
return 0;
|
|
478
|
+
};
|
|
479
|
+
const allIssues = results.flatMap((r) => r.issues);
|
|
480
|
+
const criticalSignals = allIssues.filter(
|
|
481
|
+
(i) => getLevel(i.severity) === 4
|
|
482
|
+
).length;
|
|
483
|
+
const majorSignals = allIssues.filter(
|
|
484
|
+
(i) => getLevel(i.severity) === 3
|
|
485
|
+
).length;
|
|
486
|
+
const minorSignals = allIssues.filter(
|
|
487
|
+
(i) => getLevel(i.severity) === 2
|
|
488
|
+
).length;
|
|
489
|
+
const minSev = options.minSeverity ?? Severity2.Info;
|
|
490
|
+
const filteredResults = results.map((r) => ({
|
|
491
|
+
...r,
|
|
492
|
+
issues: r.issues.filter((i) => getLevel(i.severity) >= getLevel(minSev))
|
|
493
|
+
}));
|
|
494
|
+
return {
|
|
495
|
+
summary: {
|
|
496
|
+
filesAnalyzed: files.length,
|
|
497
|
+
totalSignals: allIssues.length,
|
|
498
|
+
criticalSignals,
|
|
499
|
+
majorSignals,
|
|
500
|
+
minorSignals,
|
|
501
|
+
topRisk: riskResult.topRisk,
|
|
502
|
+
rating: riskResult.rating
|
|
503
|
+
},
|
|
504
|
+
results: filteredResults,
|
|
505
|
+
aggregateSignals: aggregate,
|
|
506
|
+
recommendations: riskResult.recommendations
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/scoring.ts
|
|
511
|
+
import { calculateAiSignalClarity as calculateAiSignalClarity2, ToolName } from "@aiready/core";
|
|
512
|
+
function calculateAiSignalClarityScore(report) {
|
|
513
|
+
const { aggregateSignals } = report;
|
|
514
|
+
const riskResult = calculateAiSignalClarity2({
|
|
515
|
+
overloadedSymbols: aggregateSignals.overloadedSymbols,
|
|
516
|
+
magicLiterals: aggregateSignals.magicLiterals,
|
|
517
|
+
booleanTraps: aggregateSignals.booleanTraps,
|
|
518
|
+
implicitSideEffects: aggregateSignals.implicitSideEffects,
|
|
519
|
+
deepCallbacks: aggregateSignals.deepCallbacks,
|
|
520
|
+
ambiguousNames: aggregateSignals.ambiguousNames,
|
|
521
|
+
undocumentedExports: aggregateSignals.undocumentedExports,
|
|
522
|
+
largeFiles: aggregateSignals.largeFiles,
|
|
523
|
+
totalSymbols: Math.max(1, aggregateSignals.totalSymbols),
|
|
524
|
+
totalExports: Math.max(1, aggregateSignals.totalExports)
|
|
525
|
+
});
|
|
526
|
+
const score = Math.max(0, 100 - riskResult.score);
|
|
527
|
+
const factors = riskResult.signals.map(
|
|
528
|
+
(sig) => ({
|
|
529
|
+
name: sig.name,
|
|
530
|
+
impact: -sig.riskContribution,
|
|
531
|
+
description: sig.description
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
const recommendations = riskResult.recommendations.map((rec) => ({
|
|
535
|
+
action: rec,
|
|
536
|
+
estimatedImpact: 8,
|
|
537
|
+
priority: riskResult.score > 50 ? "high" : "medium"
|
|
538
|
+
}));
|
|
539
|
+
return {
|
|
540
|
+
toolName: ToolName.AiSignalClarity,
|
|
541
|
+
score,
|
|
542
|
+
rawMetrics: {
|
|
543
|
+
riskScore: riskResult.score,
|
|
544
|
+
rating: riskResult.rating,
|
|
545
|
+
topRisk: riskResult.topRisk,
|
|
546
|
+
...aggregateSignals
|
|
547
|
+
},
|
|
548
|
+
factors,
|
|
549
|
+
recommendations
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export {
|
|
554
|
+
scanFile,
|
|
555
|
+
analyzeAiSignalClarity,
|
|
556
|
+
calculateAiSignalClarityScore
|
|
557
|
+
};
|