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