@aiready/contract-enforcement 0.2.0 → 0.2.2
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 +5 -5
- package/README.md +68 -0
- package/dist/index.js +72 -42
- package/dist/index.mjs +72 -42
- package/package.json +23 -4
- package/src/detector.ts +94 -38
- package/src/scoring.ts +5 -4
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @aiready/contract-enforcement@0.2.
|
|
2
|
+
> @aiready/contract-enforcement@0.2.2 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
|
|
14
|
-
CJS ⚡️ Build success in
|
|
11
|
+
ESM dist/index.mjs 16.58 KB
|
|
12
|
+
ESM ⚡️ Build success in 159ms
|
|
13
|
+
CJS dist/index.js 18.01 KB
|
|
14
|
+
CJS ⚡️ Build success in 159ms
|
|
15
15
|
DTS Build start
|
|
16
|
-
DTS ⚡️ Build success in
|
|
16
|
+
DTS ⚡️ Build success in 7234ms
|
|
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.1
|
|
2
|
+
> @aiready/contract-enforcement@0.2.1 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
|
|
8
|
+
[32m✓[39m src/__tests__/scoring.test.ts [2m([22m[2m7 tests[22m[2m)[22m[32m 62[2mms[22m[39m
|
|
9
|
+
[32m✓[39m src/__tests__/detector.test.ts [2m([22m[2m14 tests[22m[2m)[22m[32m 90[2mms[22m[39m
|
|
10
10
|
[32m✓[39m src/__tests__/provider.test.ts [2m([22m[2m5 tests[22m[2m)[22m[32m 2[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 14:03:18
|
|
15
|
+
[2m Duration [22m 3.32s[2m (transform 1.63s, setup 0ms, import 5.02s, tests 154ms, environment 0ms)[22m
|
|
16
16
|
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @aiready/contract-enforcement
|
|
2
|
+
|
|
3
|
+
> AIReady Spoke: Measures structural type safety and contract enforcement to reduce downstream fallback cascades for AI agents.
|
|
4
|
+
|
|
5
|
+
[](https://npmjs.com/package/@aiready/contract-enforcement)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
AI agents rely on strong structural contracts to navigate codebases safely. When type safety is bypassed or errors are swallowed, agents lose context and may hallucinate or fail silently. The **Contract Enforcement** analyzer detects defensive coding anti-patterns and scores how well the codebase enforces structural contracts.
|
|
11
|
+
|
|
12
|
+
## 🏛️ Architecture
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
🎯 USER
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
🎛️ @aiready/cli (orchestrator)
|
|
19
|
+
│ │ │ │ │ │ │ │ │ │
|
|
20
|
+
▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼
|
|
21
|
+
[PAT] [CTX] [CON] [AMP] [DEP] [DOC] [SIG] [AGT] [TST] [CTR]
|
|
22
|
+
│ │ │ │ │ │ │ │ │ │
|
|
23
|
+
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
|
24
|
+
│
|
|
25
|
+
▼
|
|
26
|
+
🏢 @aiready/core
|
|
27
|
+
|
|
28
|
+
Legend:
|
|
29
|
+
PAT = pattern-detect CTX = context-analyzer
|
|
30
|
+
CON = consistency AMP = change-amplification
|
|
31
|
+
DEP = deps-health DOC = doc-drift
|
|
32
|
+
SIG = ai-signal-clarity AGT = agent-grounding
|
|
33
|
+
TST = testability CTR = contract-enforcement ★
|
|
34
|
+
★ = YOU ARE HERE
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Type Escape Hatches**: Detects `as any`, `as unknown`, and `any` parameter/return types that bypass type safety.
|
|
40
|
+
- **Fallback Cascades**: Identifies deep optional chaining (`?.?.?`) and nullish coalescing with literal defaults that suggest missing upstream guarantees.
|
|
41
|
+
- **Error Transparency**: Flags swallowed catch blocks where errors are silenced, hiding failures from agents.
|
|
42
|
+
- **Boundary Validation**: Detects unvalidated environment variable access and unnecessary guard clauses that could be handled via stronger types at the source.
|
|
43
|
+
|
|
44
|
+
## Scoring Dimensions
|
|
45
|
+
|
|
46
|
+
- **Type Escape Hatches (35%)**: Measures the density of `any` and `unknown` bypasses.
|
|
47
|
+
- **Fallback Cascades (25%)**: Evaluates reliance on inline defaults vs. structural guarantees.
|
|
48
|
+
- **Error Transparency (20%)**: penalizes silent failures and swallowed exceptions.
|
|
49
|
+
- **Boundary Validation (20%)**: Assesses the use of validated schemas vs. ad-hoc guards.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pnpm add @aiready/contract-enforcement
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Usage
|
|
58
|
+
|
|
59
|
+
This tool is designed to be run through the unified AIReady CLI.
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# Scan for contract enforcement quality
|
|
63
|
+
aiready scan . --tools contract-enforcement
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.js
CHANGED
|
@@ -121,13 +121,37 @@ function isLiteral(node) {
|
|
|
121
121
|
function isProcessEnvAccess(node) {
|
|
122
122
|
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
123
123
|
}
|
|
124
|
-
function
|
|
124
|
+
function isSstResourceAccess(node) {
|
|
125
|
+
if (!node) return false;
|
|
126
|
+
let current = node;
|
|
127
|
+
if (current.type === "ChainExpression") {
|
|
128
|
+
current = current.expression;
|
|
129
|
+
}
|
|
130
|
+
while (current && current.type === "MemberExpression") {
|
|
131
|
+
if (current.object?.type === "Identifier" && current.object.name === "Resource") {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
current = current.object;
|
|
135
|
+
}
|
|
136
|
+
if (current?.type === "Identifier" && current.name === "Resource") {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
function isSwallowedCatch(body, filePath) {
|
|
125
142
|
if (body.length === 0) return true;
|
|
143
|
+
const isUiComponent = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
126
144
|
if (body.length === 1) {
|
|
127
145
|
const stmt = body[0];
|
|
128
146
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
129
147
|
const callee = stmt.expression.callee;
|
|
130
148
|
if (callee?.object?.name === "console") return true;
|
|
149
|
+
if (isUiComponent) {
|
|
150
|
+
const calleeName = callee?.name || callee?.property?.name || "";
|
|
151
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
}
|
|
132
156
|
if (stmt.type === "ThrowStatement") return false;
|
|
133
157
|
}
|
|
@@ -161,32 +185,36 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
161
185
|
if (!node || typeof node !== "object") return;
|
|
162
186
|
markFunctionParamNodes(node);
|
|
163
187
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSAnyKeyword") {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
188
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
189
|
+
counts["as-any"]++;
|
|
190
|
+
issues.push(
|
|
191
|
+
makeIssue(
|
|
192
|
+
"as-any",
|
|
193
|
+
import_core.Severity.Major,
|
|
194
|
+
"`as any` type assertion bypasses type safety",
|
|
195
|
+
filePath,
|
|
196
|
+
node.loc?.start.line ?? 0,
|
|
197
|
+
node.loc?.start.column ?? 0,
|
|
198
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
199
|
+
)
|
|
200
|
+
);
|
|
201
|
+
}
|
|
176
202
|
}
|
|
177
203
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSUnknownKeyword") {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
204
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
205
|
+
counts["as-unknown"]++;
|
|
206
|
+
issues.push(
|
|
207
|
+
makeIssue(
|
|
208
|
+
"as-unknown",
|
|
209
|
+
import_core.Severity.Major,
|
|
210
|
+
"`as unknown` double-cast bypasses type safety",
|
|
211
|
+
filePath,
|
|
212
|
+
node.loc?.start.line ?? 0,
|
|
213
|
+
node.loc?.start.column ?? 0,
|
|
214
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
215
|
+
)
|
|
216
|
+
);
|
|
217
|
+
}
|
|
190
218
|
}
|
|
191
219
|
if (node.type === "ChainExpression") {
|
|
192
220
|
const depth = countOptionalChainDepth(node);
|
|
@@ -206,22 +234,24 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
206
234
|
}
|
|
207
235
|
}
|
|
208
236
|
if (node.type === "LogicalExpression" && node.operator === "??" && isLiteral(node.right)) {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
237
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
238
|
+
counts["nullish-literal-default"]++;
|
|
239
|
+
issues.push(
|
|
240
|
+
makeIssue(
|
|
241
|
+
"nullish-literal-default",
|
|
242
|
+
import_core.Severity.Minor,
|
|
243
|
+
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
244
|
+
filePath,
|
|
245
|
+
node.loc?.start.line ?? 0,
|
|
246
|
+
node.loc?.start.column ?? 0,
|
|
247
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
}
|
|
221
251
|
}
|
|
222
252
|
if (node.type === "TryStatement" && node.handler) {
|
|
223
253
|
const catchBody = node.handler.body?.body;
|
|
224
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
254
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
225
255
|
counts["swallowed-error"]++;
|
|
226
256
|
issues.push(
|
|
227
257
|
makeIssue(
|
|
@@ -345,10 +375,10 @@ function calculateContractEnforcementScore(counts, totalLines, _fileCount) {
|
|
|
345
375
|
const fallbackDensity = fallbackCount / loc * 1e3;
|
|
346
376
|
const errorDensity = errorCount / loc * 1e3;
|
|
347
377
|
const boundaryDensity = boundaryCount / loc * 1e3;
|
|
348
|
-
const typeEscapeHatchScore = clamp(100 - typeDensity *
|
|
349
|
-
const fallbackCascadeScore = clamp(100 - fallbackDensity *
|
|
350
|
-
const errorTransparencyScore = clamp(100 - errorDensity *
|
|
351
|
-
const boundaryValidationScore = clamp(100 - boundaryDensity *
|
|
378
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
379
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
380
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
381
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
352
382
|
const score = Math.round(
|
|
353
383
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch + fallbackCascadeScore * DIMENSION_WEIGHTS.fallbackCascade + errorTransparencyScore * DIMENSION_WEIGHTS.errorTransparency + boundaryValidationScore * DIMENSION_WEIGHTS.boundaryValidation
|
|
354
384
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -94,13 +94,37 @@ function isLiteral(node) {
|
|
|
94
94
|
function isProcessEnvAccess(node) {
|
|
95
95
|
return node?.type === "MemberExpression" && node.object?.type === "MemberExpression" && node.object.object?.name === "process" && node.object.property?.name === "env";
|
|
96
96
|
}
|
|
97
|
-
function
|
|
97
|
+
function isSstResourceAccess(node) {
|
|
98
|
+
if (!node) return false;
|
|
99
|
+
let current = node;
|
|
100
|
+
if (current.type === "ChainExpression") {
|
|
101
|
+
current = current.expression;
|
|
102
|
+
}
|
|
103
|
+
while (current && current.type === "MemberExpression") {
|
|
104
|
+
if (current.object?.type === "Identifier" && current.object.name === "Resource") {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
current = current.object;
|
|
108
|
+
}
|
|
109
|
+
if (current?.type === "Identifier" && current.name === "Resource") {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function isSwallowedCatch(body, filePath) {
|
|
98
115
|
if (body.length === 0) return true;
|
|
116
|
+
const isUiComponent = filePath.endsWith(".tsx") || filePath.endsWith(".jsx");
|
|
99
117
|
if (body.length === 1) {
|
|
100
118
|
const stmt = body[0];
|
|
101
119
|
if (stmt.type === "ExpressionStatement" && stmt.expression?.type === "CallExpression") {
|
|
102
120
|
const callee = stmt.expression.callee;
|
|
103
121
|
if (callee?.object?.name === "console") return true;
|
|
122
|
+
if (isUiComponent) {
|
|
123
|
+
const calleeName = callee?.name || callee?.property?.name || "";
|
|
124
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
104
128
|
}
|
|
105
129
|
if (stmt.type === "ThrowStatement") return false;
|
|
106
130
|
}
|
|
@@ -134,32 +158,36 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
134
158
|
if (!node || typeof node !== "object") return;
|
|
135
159
|
markFunctionParamNodes(node);
|
|
136
160
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSAnyKeyword") {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
162
|
+
counts["as-any"]++;
|
|
163
|
+
issues.push(
|
|
164
|
+
makeIssue(
|
|
165
|
+
"as-any",
|
|
166
|
+
Severity.Major,
|
|
167
|
+
"`as any` type assertion bypasses type safety",
|
|
168
|
+
filePath,
|
|
169
|
+
node.loc?.start.line ?? 0,
|
|
170
|
+
node.loc?.start.column ?? 0,
|
|
171
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
}
|
|
149
175
|
}
|
|
150
176
|
if (node.type === "TSAsExpression" && node.typeAnnotation?.type === "TSUnknownKeyword") {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
if (!isSstResourceAccess(node.expression) && !isProcessEnvAccess(node.expression)) {
|
|
178
|
+
counts["as-unknown"]++;
|
|
179
|
+
issues.push(
|
|
180
|
+
makeIssue(
|
|
181
|
+
"as-unknown",
|
|
182
|
+
Severity.Major,
|
|
183
|
+
"`as unknown` double-cast bypasses type safety",
|
|
184
|
+
filePath,
|
|
185
|
+
node.loc?.start.line ?? 0,
|
|
186
|
+
node.loc?.start.column ?? 0,
|
|
187
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
188
|
+
)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
163
191
|
}
|
|
164
192
|
if (node.type === "ChainExpression") {
|
|
165
193
|
const depth = countOptionalChainDepth(node);
|
|
@@ -179,22 +207,24 @@ function detectDefensivePatterns(filePath, code, minChainDepth = 3) {
|
|
|
179
207
|
}
|
|
180
208
|
}
|
|
181
209
|
if (node.type === "LogicalExpression" && node.operator === "??" && isLiteral(node.right)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
210
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
211
|
+
counts["nullish-literal-default"]++;
|
|
212
|
+
issues.push(
|
|
213
|
+
makeIssue(
|
|
214
|
+
"nullish-literal-default",
|
|
215
|
+
Severity.Minor,
|
|
216
|
+
"Nullish coalescing with literal default suggests missing upstream type guarantee",
|
|
217
|
+
filePath,
|
|
218
|
+
node.loc?.start.line ?? 0,
|
|
219
|
+
node.loc?.start.column ?? 0,
|
|
220
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
}
|
|
194
224
|
}
|
|
195
225
|
if (node.type === "TryStatement" && node.handler) {
|
|
196
226
|
const catchBody = node.handler.body?.body;
|
|
197
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
227
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
198
228
|
counts["swallowed-error"]++;
|
|
199
229
|
issues.push(
|
|
200
230
|
makeIssue(
|
|
@@ -318,10 +348,10 @@ function calculateContractEnforcementScore(counts, totalLines, _fileCount) {
|
|
|
318
348
|
const fallbackDensity = fallbackCount / loc * 1e3;
|
|
319
349
|
const errorDensity = errorCount / loc * 1e3;
|
|
320
350
|
const boundaryDensity = boundaryCount / loc * 1e3;
|
|
321
|
-
const typeEscapeHatchScore = clamp(100 - typeDensity *
|
|
322
|
-
const fallbackCascadeScore = clamp(100 - fallbackDensity *
|
|
323
|
-
const errorTransparencyScore = clamp(100 - errorDensity *
|
|
324
|
-
const boundaryValidationScore = clamp(100 - boundaryDensity *
|
|
351
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
352
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
353
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
354
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
325
355
|
const score = Math.round(
|
|
326
356
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch + fallbackCascadeScore * DIMENSION_WEIGHTS.fallbackCascade + errorTransparencyScore * DIMENSION_WEIGHTS.errorTransparency + boundaryValidationScore * DIMENSION_WEIGHTS.boundaryValidation
|
|
327
357
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiready/contract-enforcement",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Measures structural
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -12,9 +12,27 @@
|
|
|
12
12
|
"import": "./dist/index.js"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"aiready",
|
|
17
|
+
"contract-enforcement",
|
|
18
|
+
"type-safety",
|
|
19
|
+
"static-analysis",
|
|
20
|
+
"ai-ready",
|
|
21
|
+
"typescript",
|
|
22
|
+
"structural-contracts",
|
|
23
|
+
"defensive-coding",
|
|
24
|
+
"error-handling"
|
|
25
|
+
],
|
|
26
|
+
"author": "AIReady Team",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/caopengau/aiready-contract-enforcement.git"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/caopengau/aiready-contract-enforcement",
|
|
15
33
|
"dependencies": {
|
|
16
34
|
"@typescript-eslint/typescript-estree": "^8.53.0",
|
|
17
|
-
"@aiready/core": "0.24.
|
|
35
|
+
"@aiready/core": "0.24.2"
|
|
18
36
|
},
|
|
19
37
|
"devDependencies": {
|
|
20
38
|
"@types/node": "^24.0.0",
|
|
@@ -30,6 +48,7 @@
|
|
|
30
48
|
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
31
49
|
"test": "vitest run",
|
|
32
50
|
"lint": "eslint src",
|
|
33
|
-
"clean": "rm -rf dist"
|
|
51
|
+
"clean": "rm -rf dist",
|
|
52
|
+
"release": "pnpm build && pnpm publish --no-git-checks"
|
|
34
53
|
}
|
|
35
54
|
}
|
package/src/detector.ts
CHANGED
|
@@ -98,8 +98,39 @@ function isProcessEnvAccess(node: any): boolean {
|
|
|
98
98
|
);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
function
|
|
101
|
+
function isSstResourceAccess(node: any): boolean {
|
|
102
|
+
if (!node) return false;
|
|
103
|
+
let current = node;
|
|
104
|
+
// Handle ChainExpression for optional chaining like Resource.MySecret?.value
|
|
105
|
+
if (current.type === 'ChainExpression') {
|
|
106
|
+
current = current.expression;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Recursively check for 'Resource' identifier at the base of the member chain
|
|
110
|
+
while (current && current.type === 'MemberExpression') {
|
|
111
|
+
if (
|
|
112
|
+
current.object?.type === 'Identifier' &&
|
|
113
|
+
current.object.name === 'Resource'
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
current = current.object;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Also check if the node itself is 'Resource' (rare but possible in some contexts)
|
|
121
|
+
if (current?.type === 'Identifier' && current.name === 'Resource') {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isSwallowedCatch(body: any[], filePath: string): boolean {
|
|
102
129
|
if (body.length === 0) return true;
|
|
130
|
+
|
|
131
|
+
// UI components often have intentional silent catches for telemetry/analytics
|
|
132
|
+
const isUiComponent = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
|
|
133
|
+
|
|
103
134
|
if (body.length === 1) {
|
|
104
135
|
const stmt = body[0];
|
|
105
136
|
if (
|
|
@@ -107,10 +138,20 @@ function isSwallowedCatch(body: any[]): boolean {
|
|
|
107
138
|
stmt.expression?.type === 'CallExpression'
|
|
108
139
|
) {
|
|
109
140
|
const callee = stmt.expression.callee;
|
|
141
|
+
// console.log/warn/error is still considered "swallowed" but might be acceptable
|
|
110
142
|
if (callee?.object?.name === 'console') return true;
|
|
143
|
+
|
|
144
|
+
// If it's a UI component and looks like telemetry, it's a false positive
|
|
145
|
+
if (isUiComponent) {
|
|
146
|
+
const calleeName = callee?.name || callee?.property?.name || '';
|
|
147
|
+
if (/telemetry|analytics|track|logEvent/i.test(calleeName)) {
|
|
148
|
+
return false; // Not "swallowed" in a bad way
|
|
149
|
+
}
|
|
150
|
+
}
|
|
111
151
|
}
|
|
112
152
|
if (stmt.type === 'ThrowStatement') return false;
|
|
113
153
|
}
|
|
154
|
+
|
|
114
155
|
return false;
|
|
115
156
|
}
|
|
116
157
|
|
|
@@ -160,18 +201,24 @@ export function detectDefensivePatterns(
|
|
|
160
201
|
node.type === 'TSAsExpression' &&
|
|
161
202
|
node.typeAnnotation?.type === 'TSAnyKeyword'
|
|
162
203
|
) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
204
|
+
// Ignore if it's acting on an SST resource or process.env, common "necessary escape hatches"
|
|
205
|
+
if (
|
|
206
|
+
!isSstResourceAccess(node.expression) &&
|
|
207
|
+
!isProcessEnvAccess(node.expression)
|
|
208
|
+
) {
|
|
209
|
+
counts['as-any']++;
|
|
210
|
+
issues.push(
|
|
211
|
+
makeIssue(
|
|
212
|
+
'as-any',
|
|
213
|
+
Severity.Major,
|
|
214
|
+
'`as any` type assertion bypasses type safety',
|
|
215
|
+
filePath,
|
|
216
|
+
node.loc?.start.line ?? 0,
|
|
217
|
+
node.loc?.start.column ?? 0,
|
|
218
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
219
|
+
)
|
|
220
|
+
);
|
|
221
|
+
}
|
|
175
222
|
}
|
|
176
223
|
|
|
177
224
|
// Pattern: as unknown
|
|
@@ -179,18 +226,24 @@ export function detectDefensivePatterns(
|
|
|
179
226
|
node.type === 'TSAsExpression' &&
|
|
180
227
|
node.typeAnnotation?.type === 'TSUnknownKeyword'
|
|
181
228
|
) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
229
|
+
// Ignore if it's acting on an SST resource or process.env
|
|
230
|
+
if (
|
|
231
|
+
!isSstResourceAccess(node.expression) &&
|
|
232
|
+
!isProcessEnvAccess(node.expression)
|
|
233
|
+
) {
|
|
234
|
+
counts['as-unknown']++;
|
|
235
|
+
issues.push(
|
|
236
|
+
makeIssue(
|
|
237
|
+
'as-unknown',
|
|
238
|
+
Severity.Major,
|
|
239
|
+
'`as unknown` double-cast bypasses type safety',
|
|
240
|
+
filePath,
|
|
241
|
+
node.loc?.start.line ?? 0,
|
|
242
|
+
node.loc?.start.column ?? 0,
|
|
243
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
194
247
|
}
|
|
195
248
|
|
|
196
249
|
// Pattern: deep optional chaining
|
|
@@ -218,24 +271,27 @@ export function detectDefensivePatterns(
|
|
|
218
271
|
node.operator === '??' &&
|
|
219
272
|
isLiteral(node.right)
|
|
220
273
|
) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
274
|
+
// Ignore if it's an SST Resource or process.env - the standard way to handle fallbacks
|
|
275
|
+
if (!isSstResourceAccess(node.left) && !isProcessEnvAccess(node.left)) {
|
|
276
|
+
counts['nullish-literal-default']++;
|
|
277
|
+
issues.push(
|
|
278
|
+
makeIssue(
|
|
279
|
+
'nullish-literal-default',
|
|
280
|
+
Severity.Minor,
|
|
281
|
+
'Nullish coalescing with literal default suggests missing upstream type guarantee',
|
|
282
|
+
filePath,
|
|
283
|
+
node.loc?.start.line ?? 0,
|
|
284
|
+
node.loc?.start.column ?? 0,
|
|
285
|
+
getLineContent(code, node.loc?.start.line ?? 0)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
}
|
|
233
289
|
}
|
|
234
290
|
|
|
235
291
|
// Pattern: swallowed error
|
|
236
292
|
if (node.type === 'TryStatement' && node.handler) {
|
|
237
293
|
const catchBody = node.handler.body?.body;
|
|
238
|
-
if (catchBody && isSwallowedCatch(catchBody)) {
|
|
294
|
+
if (catchBody && isSwallowedCatch(catchBody, filePath)) {
|
|
239
295
|
counts['swallowed-error']++;
|
|
240
296
|
issues.push(
|
|
241
297
|
makeIssue(
|
package/src/scoring.ts
CHANGED
|
@@ -35,10 +35,11 @@ export function calculateContractEnforcementScore(
|
|
|
35
35
|
const boundaryDensity = (boundaryCount / loc) * 1000;
|
|
36
36
|
|
|
37
37
|
// Dimension scores: 100 = no patterns, 0 = very high density
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
38
|
+
// Adjusted to be more lenient - many defensive patterns are valid practices
|
|
39
|
+
const typeEscapeHatchScore = clamp(100 - typeDensity * 10, 0, 100);
|
|
40
|
+
const fallbackCascadeScore = clamp(100 - fallbackDensity * 8, 0, 100);
|
|
41
|
+
const errorTransparencyScore = clamp(100 - errorDensity * 15, 0, 100);
|
|
42
|
+
const boundaryValidationScore = clamp(100 - boundaryDensity * 7, 0, 100);
|
|
42
43
|
|
|
43
44
|
const score = Math.round(
|
|
44
45
|
typeEscapeHatchScore * DIMENSION_WEIGHTS.typeEscapeHatch +
|