@aiready/contract-enforcement 0.2.4 → 0.2.6
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 +6 -6
- package/.turbo/turbo-test.log +6 -6
- package/dist/index.js +68 -44
- package/dist/index.mjs +68 -44
- package/package.json +2 -2
- package/src/detector.ts +93 -60
- package/src/provider.ts +16 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @aiready/contract-enforcement@0.2.
|
|
2
|
+
> @aiready/contract-enforcement@0.2.6 build /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
3
3
|
> tsup src/index.ts --format cjs,esm --dts
|
|
4
4
|
|
|
5
5
|
CLI Building entry: src/index.ts
|
|
@@ -8,11 +8,11 @@ CLI tsup v8.5.1
|
|
|
8
8
|
CLI Target: es2020
|
|
9
9
|
CJS Build start
|
|
10
10
|
ESM Build start
|
|
11
|
-
ESM dist/index.mjs
|
|
12
|
-
ESM ⚡️ Build success in
|
|
13
|
-
CJS dist/index.js 18.
|
|
14
|
-
CJS ⚡️ Build success in
|
|
11
|
+
ESM dist/index.mjs 17.28 KB
|
|
12
|
+
ESM ⚡️ Build success in 37ms
|
|
13
|
+
CJS dist/index.js 18.72 KB
|
|
14
|
+
CJS ⚡️ Build success in 39ms
|
|
15
15
|
DTS Build start
|
|
16
|
-
DTS ⚡️ Build success in
|
|
16
|
+
DTS ⚡️ Build success in 4782ms
|
|
17
17
|
DTS dist/index.d.ts 2.45 KB
|
|
18
18
|
DTS dist/index.d.mts 2.45 KB
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
|
|
2
|
-
> @aiready/contract-enforcement@0.2.
|
|
2
|
+
> @aiready/contract-enforcement@0.2.5 test /Users/pengcao/projects/aiready/packages/contract-enforcement
|
|
3
3
|
> vitest run
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
[1m[46m RUN [49m[22m [36mv4.1.1 [39m[90m/Users/pengcao/projects/aiready/packages/contract-enforcement[39m
|
|
7
7
|
|
|
8
|
-
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m
|
|
9
|
-
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m
|
|
10
|
-
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m
|
|
8
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m 105[2mms[22m[39m
|
|
10
|
+
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 3[2mms[22m[39m
|
|
11
11
|
|
|
12
12
|
[2m Test Files [22m [1m[32m3 passed[39m[22m[90m (3)[39m
|
|
13
13
|
[2m Tests [22m [1m[32m26 passed[39m[22m[90m (26)[39m
|
|
14
|
-
[2m Start at [22m
|
|
15
|
-
[2m Duration [22m
|
|
14
|
+
[2m Start at [22m 20:27:25
|
|
15
|
+
[2m Duration [22m 4.35s[2m (transform 1.54s, setup 0ms, import 6.86s, tests 111ms, environment 0ms)[22m
|
|
16
16
|
|
package/dist/index.js
CHANGED
|
@@ -109,7 +109,6 @@ function countOptionalChainDepth(node) {
|
|
|
109
109
|
return depth;
|
|
110
110
|
}
|
|
111
111
|
function isLiteral(node) {
|
|
112
|
-
if (!node) return false;
|
|
113
112
|
if (node.type === "Literal") return true;
|
|
114
113
|
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
115
114
|
return true;
|
|
@@ -119,10 +118,9 @@ function isLiteral(node) {
|
|
|
119
118
|
return false;
|
|
120
119
|
}
|
|
121
120
|
function isProcessEnvAccess(node) {
|
|
122
|
-
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
121
|
+
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "Identifier" && node.object.object.name === "process" && node.object.property?.type === "Identifier" && node.object.property.name === "env";
|
|
123
122
|
}
|
|
124
123
|
function isSstResourceAccess(node) {
|
|
125
|
-
if (!node) return false;
|
|
126
124
|
let current = node;
|
|
127
125
|
if (current.type === "ChainExpression") {
|
|
128
126
|
current = current.expression;
|
|
@@ -145,9 +143,13 @@ function isSwallowedCatch(body, filePath) {
|
|
|
145
143
|
const stmt = body[0];
|
|
146
144
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
147
145
|
const callee = stmt.expression.callee;
|
|
148
|
-
if (callee?.object?.name === "console")
|
|
146
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console")
|
|
147
|
+
return true;
|
|
149
148
|
if (isUiComponent) {
|
|
150
|
-
|
|
149
|
+
let calleeName = "";
|
|
150
|
+
if (callee?.type === "Identifier") calleeName = callee.name;
|
|
151
|
+
else if (callee?.type === "MemberExpression" && callee.property.type === "Identifier")
|
|
152
|
+
calleeName = callee.property.name;
|
|
151
153
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
152
154
|
return false;
|
|
153
155
|
}
|
|
@@ -193,9 +195,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
193
195
|
import_core.Severity.Major,
|
|
194
196
|
"`as any` type assertion bypasses type safety",
|
|
195
197
|
filePath,
|
|
196
|
-
node.loc
|
|
197
|
-
node.loc
|
|
198
|
-
getLineContent(code, node.loc
|
|
198
|
+
node.loc.start.line,
|
|
199
|
+
node.loc.start.column,
|
|
200
|
+
getLineContent(code, node.loc.start.line)
|
|
199
201
|
)
|
|
200
202
|
);
|
|
201
203
|
}
|
|
@@ -209,9 +211,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
209
211
|
import_core.Severity.Major,
|
|
210
212
|
"`as unknown` double-cast bypasses type safety",
|
|
211
213
|
filePath,
|
|
212
|
-
node.loc
|
|
213
|
-
node.loc
|
|
214
|
-
getLineContent(code, node.loc
|
|
214
|
+
node.loc.start.line,
|
|
215
|
+
node.loc.start.column,
|
|
216
|
+
getLineContent(code, node.loc.start.line)
|
|
215
217
|
)
|
|
216
218
|
);
|
|
217
219
|
}
|
|
@@ -226,9 +228,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
226
228
|
import_core.Severity.Minor,
|
|
227
229
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
228
230
|
filePath,
|
|
229
|
-
node.loc
|
|
230
|
-
node.loc
|
|
231
|
-
getLineContent(code, node.loc
|
|
231
|
+
node.loc.start.line,
|
|
232
|
+
node.loc.start.column,
|
|
233
|
+
getLineContent(code, node.loc.start.line)
|
|
232
234
|
)
|
|
233
235
|
);
|
|
234
236
|
}
|
|
@@ -242,9 +244,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
242
244
|
import_core.Severity.Minor,
|
|
243
245
|
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
244
246
|
filePath,
|
|
245
|
-
node.loc
|
|
246
|
-
node.loc
|
|
247
|
-
getLineContent(code, node.loc
|
|
247
|
+
node.loc.start.line,
|
|
248
|
+
node.loc.start.column,
|
|
249
|
+
getLineContent(code, node.loc.start.line)
|
|
248
250
|
)
|
|
249
251
|
);
|
|
250
252
|
}
|
|
@@ -259,9 +261,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
259
261
|
import_core.Severity.Major,
|
|
260
262
|
"Error is swallowed in catch block \u2014 failures will be silent",
|
|
261
263
|
filePath,
|
|
262
|
-
node.handler.loc
|
|
263
|
-
node.handler.loc
|
|
264
|
-
getLineContent(code, node.handler.loc
|
|
264
|
+
node.handler.loc.start.line,
|
|
265
|
+
node.handler.loc.start.column,
|
|
266
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
265
267
|
)
|
|
266
268
|
);
|
|
267
269
|
}
|
|
@@ -274,9 +276,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
274
276
|
import_core.Severity.Minor,
|
|
275
277
|
"Environment variable with fallback \u2014 use a validated env schema instead",
|
|
276
278
|
filePath,
|
|
277
|
-
node.loc
|
|
278
|
-
node.loc
|
|
279
|
-
getLineContent(code, node.loc
|
|
279
|
+
node.loc.start.line,
|
|
280
|
+
node.loc.start.column,
|
|
281
|
+
getLineContent(code, node.loc.start.line)
|
|
280
282
|
)
|
|
281
283
|
);
|
|
282
284
|
}
|
|
@@ -296,16 +298,19 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
296
298
|
import_core.Severity.Info,
|
|
297
299
|
"Guard clause could be eliminated with non-nullable type at source",
|
|
298
300
|
filePath,
|
|
299
|
-
node.loc
|
|
300
|
-
node.loc
|
|
301
|
-
getLineContent(code, node.loc
|
|
301
|
+
node.loc.start.line,
|
|
302
|
+
node.loc.start.column,
|
|
303
|
+
getLineContent(code, node.loc.start.line)
|
|
302
304
|
)
|
|
303
305
|
);
|
|
304
306
|
}
|
|
305
307
|
}
|
|
306
308
|
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.params) {
|
|
307
309
|
for (const param of node.params) {
|
|
308
|
-
|
|
310
|
+
let typeAnno;
|
|
311
|
+
if ("typeAnnotation" in param && param.typeAnnotation) {
|
|
312
|
+
typeAnno = param.typeAnnotation.typeAnnotation;
|
|
313
|
+
}
|
|
309
314
|
if (typeAnno?.type === "TSAnyKeyword") {
|
|
310
315
|
counts["any-parameter"]++;
|
|
311
316
|
issues.push(
|
|
@@ -314,27 +319,33 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
314
319
|
import_core.Severity.Major,
|
|
315
320
|
"Parameter typed as `any` bypasses type safety",
|
|
316
321
|
filePath,
|
|
317
|
-
param.loc
|
|
318
|
-
param.loc
|
|
319
|
-
getLineContent(code, param.loc
|
|
322
|
+
param.loc.start.line,
|
|
323
|
+
param.loc.start.column,
|
|
324
|
+
getLineContent(code, param.loc.start.line)
|
|
320
325
|
)
|
|
321
326
|
);
|
|
322
327
|
}
|
|
323
328
|
}
|
|
324
|
-
|
|
329
|
+
let returnAnno;
|
|
330
|
+
if ("returnType" in node && node.returnType) {
|
|
331
|
+
returnAnno = node.returnType.typeAnnotation;
|
|
332
|
+
}
|
|
325
333
|
if (returnAnno?.type === "TSAnyKeyword") {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
334
|
+
const fnNode = node;
|
|
335
|
+
if (fnNode.returnType?.loc) {
|
|
336
|
+
counts["any-return"]++;
|
|
337
|
+
issues.push(
|
|
338
|
+
makeIssue(
|
|
339
|
+
"any-return",
|
|
340
|
+
import_core.Severity.Major,
|
|
341
|
+
"Return type is `any` \u2014 callers cannot rely on the result shape",
|
|
342
|
+
filePath,
|
|
343
|
+
fnNode.returnType.loc.start.line,
|
|
344
|
+
fnNode.returnType.loc.start.column,
|
|
345
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
346
|
+
)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
338
349
|
}
|
|
339
350
|
}
|
|
340
351
|
for (const key in node) {
|
|
@@ -495,11 +506,24 @@ var ContractEnforcementProvider = (0, import_core3.createProvider)({
|
|
|
495
506
|
},
|
|
496
507
|
score(output, _options) {
|
|
497
508
|
const rawData = output.metadata?.rawData ?? {};
|
|
498
|
-
|
|
509
|
+
const result = calculateContractEnforcementScore(
|
|
499
510
|
rawData,
|
|
500
511
|
rawData.totalLines ?? 1,
|
|
501
512
|
rawData.sourceFiles ?? 1
|
|
502
513
|
);
|
|
514
|
+
return {
|
|
515
|
+
toolName: import_core3.ToolName.ContractEnforcement,
|
|
516
|
+
score: result.score,
|
|
517
|
+
rating: result.rating,
|
|
518
|
+
recommendations: result.recommendations.map((action) => ({
|
|
519
|
+
action,
|
|
520
|
+
estimatedImpact: 10,
|
|
521
|
+
priority: "medium"
|
|
522
|
+
})),
|
|
523
|
+
dimensions: result.dimensions,
|
|
524
|
+
rawMetrics: rawData,
|
|
525
|
+
factors: []
|
|
526
|
+
};
|
|
503
527
|
}
|
|
504
528
|
});
|
|
505
529
|
|
package/dist/index.mjs
CHANGED
|
@@ -82,7 +82,6 @@ function countOptionalChainDepth(node) {
|
|
|
82
82
|
return depth;
|
|
83
83
|
}
|
|
84
84
|
function isLiteral(node) {
|
|
85
|
-
if (!node) return false;
|
|
86
85
|
if (node.type === "Literal") return true;
|
|
87
86
|
if (node.type === "TemplateLiteral" && node.expressions.length === 0)
|
|
88
87
|
return true;
|
|
@@ -92,10 +91,9 @@ function isLiteral(node) {
|
|
|
92
91
|
return false;
|
|
93
92
|
}
|
|
94
93
|
function isProcessEnvAccess(node) {
|
|
95
|
-
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
94
|
+
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.type === "Identifier" && node.object.object.name === "process" && node.object.property?.type === "Identifier" && node.object.property.name === "env";
|
|
96
95
|
}
|
|
97
96
|
function isSstResourceAccess(node) {
|
|
98
|
-
if (!node) return false;
|
|
99
97
|
let current = node;
|
|
100
98
|
if (current.type === "ChainExpression") {
|
|
101
99
|
current = current.expression;
|
|
@@ -118,9 +116,13 @@ function isSwallowedCatch(body, filePath) {
|
|
|
118
116
|
const stmt = body[0];
|
|
119
117
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
120
118
|
const callee = stmt.expression.callee;
|
|
121
|
-
if (callee?.object?.name === "console")
|
|
119
|
+
if (callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === "console")
|
|
120
|
+
return true;
|
|
122
121
|
if (isUiComponent) {
|
|
123
|
-
|
|
122
|
+
let calleeName = "";
|
|
123
|
+
if (callee?.type === "Identifier") calleeName = callee.name;
|
|
124
|
+
else if (callee?.type === "MemberExpression" && callee.property.type === "Identifier")
|
|
125
|
+
calleeName = callee.property.name;
|
|
124
126
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
125
127
|
return false;
|
|
126
128
|
}
|
|
@@ -166,9 +168,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
166
168
|
Severity.Major,
|
|
167
169
|
"`as any` type assertion bypasses type safety",
|
|
168
170
|
filePath,
|
|
169
|
-
node.loc
|
|
170
|
-
node.loc
|
|
171
|
-
getLineContent(code, node.loc
|
|
171
|
+
node.loc.start.line,
|
|
172
|
+
node.loc.start.column,
|
|
173
|
+
getLineContent(code, node.loc.start.line)
|
|
172
174
|
)
|
|
173
175
|
);
|
|
174
176
|
}
|
|
@@ -182,9 +184,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
182
184
|
Severity.Major,
|
|
183
185
|
"`as unknown` double-cast bypasses type safety",
|
|
184
186
|
filePath,
|
|
185
|
-
node.loc
|
|
186
|
-
node.loc
|
|
187
|
-
getLineContent(code, node.loc
|
|
187
|
+
node.loc.start.line,
|
|
188
|
+
node.loc.start.column,
|
|
189
|
+
getLineContent(code, node.loc.start.line)
|
|
188
190
|
)
|
|
189
191
|
);
|
|
190
192
|
}
|
|
@@ -199,9 +201,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
199
201
|
Severity.Minor,
|
|
200
202
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
201
203
|
filePath,
|
|
202
|
-
node.loc
|
|
203
|
-
node.loc
|
|
204
|
-
getLineContent(code, node.loc
|
|
204
|
+
node.loc.start.line,
|
|
205
|
+
node.loc.start.column,
|
|
206
|
+
getLineContent(code, node.loc.start.line)
|
|
205
207
|
)
|
|
206
208
|
);
|
|
207
209
|
}
|
|
@@ -215,9 +217,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
215
217
|
Severity.Minor,
|
|
216
218
|
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
217
219
|
filePath,
|
|
218
|
-
node.loc
|
|
219
|
-
node.loc
|
|
220
|
-
getLineContent(code, node.loc
|
|
220
|
+
node.loc.start.line,
|
|
221
|
+
node.loc.start.column,
|
|
222
|
+
getLineContent(code, node.loc.start.line)
|
|
221
223
|
)
|
|
222
224
|
);
|
|
223
225
|
}
|
|
@@ -232,9 +234,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
232
234
|
Severity.Major,
|
|
233
235
|
"Error is swallowed in catch block \u2014 failures will be silent",
|
|
234
236
|
filePath,
|
|
235
|
-
node.handler.loc
|
|
236
|
-
node.handler.loc
|
|
237
|
-
getLineContent(code, node.handler.loc
|
|
237
|
+
node.handler.loc.start.line,
|
|
238
|
+
node.handler.loc.start.column,
|
|
239
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
238
240
|
)
|
|
239
241
|
);
|
|
240
242
|
}
|
|
@@ -247,9 +249,9 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
247
249
|
Severity.Minor,
|
|
248
250
|
"Environment variable with fallback \u2014 use a validated env schema instead",
|
|
249
251
|
filePath,
|
|
250
|
-
node.loc
|
|
251
|
-
node.loc
|
|
252
|
-
getLineContent(code, node.loc
|
|
252
|
+
node.loc.start.line,
|
|
253
|
+
node.loc.start.column,
|
|
254
|
+
getLineContent(code, node.loc.start.line)
|
|
253
255
|
)
|
|
254
256
|
);
|
|
255
257
|
}
|
|
@@ -269,16 +271,19 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
269
271
|
Severity.Info,
|
|
270
272
|
"Guard clause could be eliminated with non-nullable type at source",
|
|
271
273
|
filePath,
|
|
272
|
-
node.loc
|
|
273
|
-
node.loc
|
|
274
|
-
getLineContent(code, node.loc
|
|
274
|
+
node.loc.start.line,
|
|
275
|
+
node.loc.start.column,
|
|
276
|
+
getLineContent(code, node.loc.start.line)
|
|
275
277
|
)
|
|
276
278
|
);
|
|
277
279
|
}
|
|
278
280
|
}
|
|
279
281
|
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.params) {
|
|
280
282
|
for (const param of node.params) {
|
|
281
|
-
|
|
283
|
+
let typeAnno;
|
|
284
|
+
if ("typeAnnotation" in param && param.typeAnnotation) {
|
|
285
|
+
typeAnno = param.typeAnnotation.typeAnnotation;
|
|
286
|
+
}
|
|
282
287
|
if (typeAnno?.type === "TSAnyKeyword") {
|
|
283
288
|
counts["any-parameter"]++;
|
|
284
289
|
issues.push(
|
|
@@ -287,27 +292,33 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
287
292
|
Severity.Major,
|
|
288
293
|
"Parameter typed as `any` bypasses type safety",
|
|
289
294
|
filePath,
|
|
290
|
-
param.loc
|
|
291
|
-
param.loc
|
|
292
|
-
getLineContent(code, param.loc
|
|
295
|
+
param.loc.start.line,
|
|
296
|
+
param.loc.start.column,
|
|
297
|
+
getLineContent(code, param.loc.start.line)
|
|
293
298
|
)
|
|
294
299
|
);
|
|
295
300
|
}
|
|
296
301
|
}
|
|
297
|
-
|
|
302
|
+
let returnAnno;
|
|
303
|
+
if ("returnType" in node && node.returnType) {
|
|
304
|
+
returnAnno = node.returnType.typeAnnotation;
|
|
305
|
+
}
|
|
298
306
|
if (returnAnno?.type === "TSAnyKeyword") {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
307
|
+
const fnNode = node;
|
|
308
|
+
if (fnNode.returnType?.loc) {
|
|
309
|
+
counts["any-return"]++;
|
|
310
|
+
issues.push(
|
|
311
|
+
makeIssue(
|
|
312
|
+
"any-return",
|
|
313
|
+
Severity.Major,
|
|
314
|
+
"Return type is `any` \u2014 callers cannot rely on the result shape",
|
|
315
|
+
filePath,
|
|
316
|
+
fnNode.returnType.loc.start.line,
|
|
317
|
+
fnNode.returnType.loc.start.column,
|
|
318
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
319
|
+
)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
311
322
|
}
|
|
312
323
|
}
|
|
313
324
|
for (const key in node) {
|
|
@@ -468,11 +479,24 @@ var ContractEnforcementProvider = createProvider({
|
|
|
468
479
|
},
|
|
469
480
|
score(output, _options) {
|
|
470
481
|
const rawData = output.metadata?.rawData ?? {};
|
|
471
|
-
|
|
482
|
+
const result = calculateContractEnforcementScore(
|
|
472
483
|
rawData,
|
|
473
484
|
rawData.totalLines ?? 1,
|
|
474
485
|
rawData.sourceFiles ?? 1
|
|
475
486
|
);
|
|
487
|
+
return {
|
|
488
|
+
toolName: ToolName.ContractEnforcement,
|
|
489
|
+
score: result.score,
|
|
490
|
+
rating: result.rating,
|
|
491
|
+
recommendations: result.recommendations.map((action) => ({
|
|
492
|
+
action,
|
|
493
|
+
estimatedImpact: 10,
|
|
494
|
+
priority: "medium"
|
|
495
|
+
})),
|
|
496
|
+
dimensions: result.dimensions,
|
|
497
|
+
rawMetrics: rawData,
|
|
498
|
+
factors: []
|
|
499
|
+
};
|
|
476
500
|
}
|
|
477
501
|
});
|
|
478
502
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/contract-enforcement",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Measures structural type safety and boundary validation to reduce fallback cascades for AI agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"homepage": "https://github.com/caopengau/aiready-contract-enforcement",
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
35
|
-
"@aiready/core": "0.24.
|
|
35
|
+
"@aiready/core": "0.24.6"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^24.0.0",
|
package/src/detector.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { parse } from '@typescript-eslint/typescript-estree';
|
|
1
|
+
import { parse, TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
2
|
import { Severity, IssueType } from '@aiready/core';
|
|
3
3
|
import type {
|
|
4
4
|
ContractEnforcementIssue,
|
|
@@ -56,9 +56,9 @@ function getLineContent(code: string, line: number): string {
|
|
|
56
56
|
return (lines[line - 1] || '').trim().slice(0, 120);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function countOptionalChainDepth(node:
|
|
59
|
+
function countOptionalChainDepth(node: TSESTree.Node): number {
|
|
60
60
|
let depth = 0;
|
|
61
|
-
let current = node;
|
|
61
|
+
let current: TSESTree.Node | undefined = node;
|
|
62
62
|
while (current) {
|
|
63
63
|
if (current.type === 'MemberExpression' && current.optional) {
|
|
64
64
|
depth++;
|
|
@@ -75,8 +75,7 @@ function countOptionalChainDepth(node: any): number {
|
|
|
75
75
|
return depth;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function isLiteral(node:
|
|
79
|
-
if (!node) return false;
|
|
78
|
+
function isLiteral(node: TSESTree.Node): boolean {
|
|
80
79
|
if (node.type === 'Literal') return true;
|
|
81
80
|
if (node.type === 'TemplateLiteral' && node.expressions.length === 0)
|
|
82
81
|
return true;
|
|
@@ -89,18 +88,19 @@ function isLiteral(node: any): boolean {
|
|
|
89
88
|
return false;
|
|
90
89
|
}
|
|
91
90
|
|
|
92
|
-
function isProcessEnvAccess(node:
|
|
91
|
+
function isProcessEnvAccess(node: TSESTree.Node | undefined): boolean {
|
|
93
92
|
return (
|
|
94
93
|
node?.type === 'MemberExpression' &&
|
|
95
94
|
node.object?.type === 'MemberExpression' &&
|
|
96
|
-
node.object.object?.
|
|
97
|
-
node.object.
|
|
95
|
+
node.object.object?.type === 'Identifier' &&
|
|
96
|
+
node.object.object.name === 'process' &&
|
|
97
|
+
node.object.property?.type === 'Identifier' &&
|
|
98
|
+
node.object.property.name === 'env'
|
|
98
99
|
);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
function isSstResourceAccess(node:
|
|
102
|
-
|
|
103
|
-
let current = node;
|
|
102
|
+
function isSstResourceAccess(node: TSESTree.Node): boolean {
|
|
103
|
+
let current: TSESTree.Node = node;
|
|
104
104
|
// Handle ChainExpression for optional chaining like Resource.MySecret?.value
|
|
105
105
|
if (current.type === 'ChainExpression') {
|
|
106
106
|
current = current.expression;
|
|
@@ -125,7 +125,10 @@ function isSstResourceAccess(node: any): boolean {
|
|
|
125
125
|
return false;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
function isSwallowedCatch(
|
|
128
|
+
function isSwallowedCatch(
|
|
129
|
+
body: TSESTree.Statement[],
|
|
130
|
+
filePath: string
|
|
131
|
+
): boolean {
|
|
129
132
|
if (body.length === 0) return true;
|
|
130
133
|
|
|
131
134
|
// UI components often have intentional silent catches for telemetry/analytics
|
|
@@ -139,11 +142,23 @@ function isSwallowedCatch(body: any[], filePath: string): boolean {
|
|
|
139
142
|
) {
|
|
140
143
|
const callee = stmt.expression.callee;
|
|
141
144
|
// console.log/warn/error is still considered "swallowed" but might be acceptable
|
|
142
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
callee?.type === 'MemberExpression' &&
|
|
147
|
+
callee.object?.type === 'Identifier' &&
|
|
148
|
+
callee.object.name === 'console'
|
|
149
|
+
)
|
|
150
|
+
return true;
|
|
143
151
|
|
|
144
152
|
// If it's a UI component and looks like telemetry, it's a false positive
|
|
145
153
|
if (isUiComponent) {
|
|
146
|
-
|
|
154
|
+
let calleeName = '';
|
|
155
|
+
if (callee?.type === 'Identifier') calleeName = callee.name;
|
|
156
|
+
else if (
|
|
157
|
+
callee?.type === 'MemberExpression' &&
|
|
158
|
+
callee.property.type === 'Identifier'
|
|
159
|
+
)
|
|
160
|
+
calleeName = callee.property.name;
|
|
161
|
+
|
|
147
162
|
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
148
163
|
return false; // Not "swallowed" in a bad way
|
|
149
164
|
}
|
|
@@ -164,7 +179,7 @@ export function detectDefensivePatterns(
|
|
|
164
179
|
const counts: PatternCounts = { ...ZERO_COUNTS };
|
|
165
180
|
const totalLines = code.split('\n').length;
|
|
166
181
|
|
|
167
|
-
let ast:
|
|
182
|
+
let ast: TSESTree.Program;
|
|
168
183
|
try {
|
|
169
184
|
ast = parse(code, {
|
|
170
185
|
filePath,
|
|
@@ -176,9 +191,9 @@ export function detectDefensivePatterns(
|
|
|
176
191
|
return { issues, counts, totalLines };
|
|
177
192
|
}
|
|
178
193
|
|
|
179
|
-
const nodesAtFunctionStart = new WeakSet<
|
|
194
|
+
const nodesAtFunctionStart = new WeakSet<TSESTree.Node>();
|
|
180
195
|
|
|
181
|
-
function markFunctionParamNodes(node:
|
|
196
|
+
function markFunctionParamNodes(node: TSESTree.Node) {
|
|
182
197
|
if (
|
|
183
198
|
node.type === 'FunctionDeclaration' ||
|
|
184
199
|
node.type === 'FunctionExpression' ||
|
|
@@ -191,7 +206,11 @@ export function detectDefensivePatterns(
|
|
|
191
206
|
}
|
|
192
207
|
}
|
|
193
208
|
|
|
194
|
-
function visit(
|
|
209
|
+
function visit(
|
|
210
|
+
node: TSESTree.Node | undefined,
|
|
211
|
+
_parent?: TSESTree.Node,
|
|
212
|
+
_keyInParent?: string
|
|
213
|
+
) {
|
|
195
214
|
if (!node || typeof node !== 'object') return;
|
|
196
215
|
|
|
197
216
|
markFunctionParamNodes(node);
|
|
@@ -213,9 +232,9 @@ export function detectDefensivePatterns(
|
|
|
213
232
|
Severity.Major,
|
|
214
233
|
'`as any` type assertion bypasses type safety',
|
|
215
234
|
filePath,
|
|
216
|
-
node.loc
|
|
217
|
-
node.loc
|
|
218
|
-
getLineContent(code, node.loc
|
|
235
|
+
node.loc.start.line,
|
|
236
|
+
node.loc.start.column,
|
|
237
|
+
getLineContent(code, node.loc.start.line)
|
|
219
238
|
)
|
|
220
239
|
);
|
|
221
240
|
}
|
|
@@ -238,9 +257,9 @@ export function detectDefensivePatterns(
|
|
|
238
257
|
Severity.Major,
|
|
239
258
|
'`as unknown` double-cast bypasses type safety',
|
|
240
259
|
filePath,
|
|
241
|
-
node.loc
|
|
242
|
-
node.loc
|
|
243
|
-
getLineContent(code, node.loc
|
|
260
|
+
node.loc.start.line,
|
|
261
|
+
node.loc.start.column,
|
|
262
|
+
getLineContent(code, node.loc.start.line)
|
|
244
263
|
)
|
|
245
264
|
);
|
|
246
265
|
}
|
|
@@ -257,9 +276,9 @@ export function detectDefensivePatterns(
|
|
|
257
276
|
Severity.Minor,
|
|
258
277
|
`Optional chain depth of ${depth} indicates missing structural guarantees`,
|
|
259
278
|
filePath,
|
|
260
|
-
node.loc
|
|
261
|
-
node.loc
|
|
262
|
-
getLineContent(code, node.loc
|
|
279
|
+
node.loc.start.line,
|
|
280
|
+
node.loc.start.column,
|
|
281
|
+
getLineContent(code, node.loc.start.line)
|
|
263
282
|
)
|
|
264
283
|
);
|
|
265
284
|
}
|
|
@@ -280,9 +299,9 @@ export function detectDefensivePatterns(
|
|
|
280
299
|
Severity.Minor,
|
|
281
300
|
'Nullish coalescing with literal default suggests missing upstream type guarantee',
|
|
282
301
|
filePath,
|
|
283
|
-
node.loc
|
|
284
|
-
node.loc
|
|
285
|
-
getLineContent(code, node.loc
|
|
302
|
+
node.loc.start.line,
|
|
303
|
+
node.loc.start.column,
|
|
304
|
+
getLineContent(code, node.loc.start.line)
|
|
286
305
|
)
|
|
287
306
|
);
|
|
288
307
|
}
|
|
@@ -299,9 +318,9 @@ export function detectDefensivePatterns(
|
|
|
299
318
|
Severity.Major,
|
|
300
319
|
'Error is swallowed in catch block — failures will be silent',
|
|
301
320
|
filePath,
|
|
302
|
-
node.handler.loc
|
|
303
|
-
node.handler.loc
|
|
304
|
-
getLineContent(code, node.handler.loc
|
|
321
|
+
node.handler.loc.start.line,
|
|
322
|
+
node.handler.loc.start.column,
|
|
323
|
+
getLineContent(code, node.handler.loc.start.line)
|
|
305
324
|
)
|
|
306
325
|
);
|
|
307
326
|
}
|
|
@@ -320,9 +339,9 @@ export function detectDefensivePatterns(
|
|
|
320
339
|
Severity.Minor,
|
|
321
340
|
'Environment variable with fallback — use a validated env schema instead',
|
|
322
341
|
filePath,
|
|
323
|
-
node.loc
|
|
324
|
-
node.loc
|
|
325
|
-
getLineContent(code, node.loc
|
|
342
|
+
node.loc.start.line,
|
|
343
|
+
node.loc.start.column,
|
|
344
|
+
getLineContent(code, node.loc.start.line)
|
|
326
345
|
)
|
|
327
346
|
);
|
|
328
347
|
}
|
|
@@ -352,9 +371,9 @@ export function detectDefensivePatterns(
|
|
|
352
371
|
Severity.Info,
|
|
353
372
|
'Guard clause could be eliminated with non-nullable type at source',
|
|
354
373
|
filePath,
|
|
355
|
-
node.loc
|
|
356
|
-
node.loc
|
|
357
|
-
getLineContent(code, node.loc
|
|
374
|
+
node.loc.start.line,
|
|
375
|
+
node.loc.start.column,
|
|
376
|
+
getLineContent(code, node.loc.start.line)
|
|
358
377
|
)
|
|
359
378
|
);
|
|
360
379
|
}
|
|
@@ -368,8 +387,14 @@ export function detectDefensivePatterns(
|
|
|
368
387
|
node.params
|
|
369
388
|
) {
|
|
370
389
|
for (const param of node.params) {
|
|
371
|
-
|
|
372
|
-
|
|
390
|
+
let typeAnno: TSESTree.TypeNode | undefined;
|
|
391
|
+
|
|
392
|
+
// Handle Identifier, AssignmentPattern, RestElement, etc.
|
|
393
|
+
if ('typeAnnotation' in param && param.typeAnnotation) {
|
|
394
|
+
typeAnno = (param.typeAnnotation as TSESTree.TSTypeAnnotation)
|
|
395
|
+
.typeAnnotation;
|
|
396
|
+
}
|
|
397
|
+
|
|
373
398
|
if (typeAnno?.type === 'TSAnyKeyword') {
|
|
374
399
|
counts['any-parameter']++;
|
|
375
400
|
issues.push(
|
|
@@ -378,40 +403,48 @@ export function detectDefensivePatterns(
|
|
|
378
403
|
Severity.Major,
|
|
379
404
|
'Parameter typed as `any` bypasses type safety',
|
|
380
405
|
filePath,
|
|
381
|
-
param.loc
|
|
382
|
-
param.loc
|
|
383
|
-
getLineContent(code, param.loc
|
|
406
|
+
param.loc.start.line,
|
|
407
|
+
param.loc.start.column,
|
|
408
|
+
getLineContent(code, param.loc.start.line)
|
|
384
409
|
)
|
|
385
410
|
);
|
|
386
411
|
}
|
|
387
412
|
}
|
|
388
413
|
|
|
389
414
|
// Pattern: any return type
|
|
390
|
-
|
|
415
|
+
let returnAnno: TSESTree.TypeNode | undefined;
|
|
416
|
+
if ('returnType' in node && node.returnType) {
|
|
417
|
+
returnAnno = (node.returnType as TSESTree.TSTypeAnnotation)
|
|
418
|
+
.typeAnnotation;
|
|
419
|
+
}
|
|
420
|
+
|
|
391
421
|
if (returnAnno?.type === 'TSAnyKeyword') {
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
422
|
+
const fnNode = node as TSESTree.FunctionDeclaration;
|
|
423
|
+
if (fnNode.returnType?.loc) {
|
|
424
|
+
counts['any-return']++;
|
|
425
|
+
issues.push(
|
|
426
|
+
makeIssue(
|
|
427
|
+
'any-return',
|
|
428
|
+
Severity.Major,
|
|
429
|
+
'Return type is `any` — callers cannot rely on the result shape',
|
|
430
|
+
filePath,
|
|
431
|
+
fnNode.returnType.loc.start.line,
|
|
432
|
+
fnNode.returnType.loc.start.column,
|
|
433
|
+
getLineContent(code, fnNode.returnType.loc.start.line)
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
}
|
|
404
437
|
}
|
|
405
438
|
}
|
|
406
439
|
|
|
407
440
|
// Recurse
|
|
408
441
|
for (const key in node) {
|
|
409
442
|
if (key === 'loc' || key === 'range' || key === 'parent') continue;
|
|
410
|
-
const child = node[key];
|
|
443
|
+
const child = (node as Record<string, any>)[key];
|
|
411
444
|
if (Array.isArray(child)) {
|
|
412
445
|
for (const item of child) {
|
|
413
446
|
if (item && typeof item.type === 'string') {
|
|
414
|
-
visit(item, node, key);
|
|
447
|
+
visit(item as TSESTree.Node, node, key);
|
|
415
448
|
}
|
|
416
449
|
}
|
|
417
450
|
} else if (
|
|
@@ -419,7 +452,7 @@ export function detectDefensivePatterns(
|
|
|
419
452
|
typeof child === 'object' &&
|
|
420
453
|
typeof child.type === 'string'
|
|
421
454
|
) {
|
|
422
|
-
visit(child, node, key);
|
|
455
|
+
visit(child as TSESTree.Node, node, key);
|
|
423
456
|
}
|
|
424
457
|
}
|
|
425
458
|
}
|
package/src/provider.ts
CHANGED
|
@@ -30,11 +30,24 @@ export const ContractEnforcementProvider = createProvider({
|
|
|
30
30
|
},
|
|
31
31
|
|
|
32
32
|
score(output: SpokeOutput, _options: ScanOptions) {
|
|
33
|
-
const rawData =
|
|
34
|
-
|
|
33
|
+
const rawData = output.metadata?.rawData ?? {};
|
|
34
|
+
const result = calculateContractEnforcementScore(
|
|
35
35
|
rawData,
|
|
36
36
|
rawData.totalLines ?? 1,
|
|
37
37
|
rawData.sourceFiles ?? 1
|
|
38
|
-
)
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
toolName: ToolName.ContractEnforcement,
|
|
41
|
+
score: result.score,
|
|
42
|
+
rating: result.rating,
|
|
43
|
+
recommendations: result.recommendations.map((action) => ({
|
|
44
|
+
action,
|
|
45
|
+
estimatedImpact: 10,
|
|
46
|
+
priority: 'medium' as const,
|
|
47
|
+
})),
|
|
48
|
+
dimensions: result.dimensions,
|
|
49
|
+
rawMetrics: rawData,
|
|
50
|
+
factors: [],
|
|
51
|
+
};
|
|
39
52
|
},
|
|
40
53
|
});
|