@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @aiready/contract-enforcement@0.2.0 build /Users/pengcao/projects/aiready/packages/contract-enforcement
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 15.46 KB
12
- ESM ⚡️ Build success in 76ms
13
- CJS dist/index.js 16.88 KB
14
- CJS ⚡️ Build success in 86ms
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 4708ms
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
@@ -1,16 +1,16 @@
1
1
 
2
- > @aiready/contract-enforcement@0.1.0 test /Users/pengcao/projects/aiready/packages/contract-enforcement
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
   RUN  v4.1.1 /Users/pengcao/projects/aiready/packages/contract-enforcement
7
7
 
8
- ✓ src/__tests__/scoring.test.ts (7 tests) 3ms
9
- ✓ src/__tests__/detector.test.ts (14 tests) 23ms
8
+ ✓ src/__tests__/scoring.test.ts (7 tests) 62ms
9
+ ✓ src/__tests__/detector.test.ts (14 tests) 90ms
10
10
  ✓ src/__tests__/provider.test.ts (5 tests) 2ms
11
11
 
12
12
   Test Files  3 passed (3)
13
13
   Tests  26 passed (26)
14
-  Start at  15:17:54
15
-  Duration  2.10s (transform 511ms, setup 0ms, import 3.00s, tests 28ms, environment 0ms)
14
+  Start at  14:03:18
15
+  Duration  3.32s (transform 1.63s, setup 0ms, import 5.02s, tests 154ms, environment 0ms)
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
+ [![npm version](https://img.shields.io/npm/v/@aiready/contract-enforcement.svg)](https://npmjs.com/package/@aiready/contract-enforcement)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 isSwallowedCatch(body) {
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
- counts["as-any"]++;
165
- issues.push(
166
- makeIssue(
167
- "as-any",
168
- import_core.Severity.Major,
169
- "`as any` type assertion bypasses type safety",
170
- filePath,
171
- node.loc?.start.line ?? 0,
172
- node.loc?.start.column ?? 0,
173
- getLineContent(code, node.loc?.start.line ?? 0)
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
- counts["as-unknown"]++;
179
- issues.push(
180
- makeIssue(
181
- "as-unknown",
182
- import_core.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
- );
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
- counts["nullish-literal-default"]++;
210
- issues.push(
211
- makeIssue(
212
- "nullish-literal-default",
213
- import_core.Severity.Minor,
214
- "Nullish coalescing with literal default suggests missing upstream type guarantee",
215
- filePath,
216
- node.loc?.start.line ?? 0,
217
- node.loc?.start.column ?? 0,
218
- getLineContent(code, node.loc?.start.line ?? 0)
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 * 15, 0, 100);
349
- const fallbackCascadeScore = clamp(100 - fallbackDensity * 12, 0, 100);
350
- const errorTransparencyScore = clamp(100 - errorDensity * 25, 0, 100);
351
- const boundaryValidationScore = clamp(100 - boundaryDensity * 10, 0, 100);
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 isSwallowedCatch(body) {
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
- counts["as-any"]++;
138
- issues.push(
139
- makeIssue(
140
- "as-any",
141
- Severity.Major,
142
- "`as any` type assertion bypasses type safety",
143
- filePath,
144
- node.loc?.start.line ?? 0,
145
- node.loc?.start.column ?? 0,
146
- getLineContent(code, node.loc?.start.line ?? 0)
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
- counts["as-unknown"]++;
152
- issues.push(
153
- makeIssue(
154
- "as-unknown",
155
- Severity.Major,
156
- "`as unknown` double-cast bypasses type safety",
157
- filePath,
158
- node.loc?.start.line ?? 0,
159
- node.loc?.start.column ?? 0,
160
- getLineContent(code, node.loc?.start.line ?? 0)
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
- counts["nullish-literal-default"]++;
183
- issues.push(
184
- makeIssue(
185
- "nullish-literal-default",
186
- Severity.Minor,
187
- "Nullish coalescing with literal default suggests missing upstream type guarantee",
188
- filePath,
189
- node.loc?.start.line ?? 0,
190
- node.loc?.start.column ?? 0,
191
- getLineContent(code, node.loc?.start.line ?? 0)
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 * 15, 0, 100);
322
- const fallbackCascadeScore = clamp(100 - fallbackDensity * 12, 0, 100);
323
- const errorTransparencyScore = clamp(100 - errorDensity * 25, 0, 100);
324
- const boundaryValidationScore = clamp(100 - boundaryDensity * 10, 0, 100);
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.0",
4
- "description": "Measures structural contract enforcement to reduce defensive coding cascades",
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.0"
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 isSwallowedCatch(body: any[]): boolean {
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
- counts['as-any']++;
164
- issues.push(
165
- makeIssue(
166
- 'as-any',
167
- Severity.Major,
168
- '`as any` type assertion bypasses type safety',
169
- filePath,
170
- node.loc?.start.line ?? 0,
171
- node.loc?.start.column ?? 0,
172
- getLineContent(code, node.loc?.start.line ?? 0)
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
- counts['as-unknown']++;
183
- issues.push(
184
- makeIssue(
185
- 'as-unknown',
186
- Severity.Major,
187
- '`as unknown` double-cast bypasses type safety',
188
- filePath,
189
- node.loc?.start.line ?? 0,
190
- node.loc?.start.column ?? 0,
191
- getLineContent(code, node.loc?.start.line ?? 0)
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
- counts['nullish-literal-default']++;
222
- issues.push(
223
- makeIssue(
224
- 'nullish-literal-default',
225
- Severity.Minor,
226
- 'Nullish coalescing with literal default suggests missing upstream type guarantee',
227
- filePath,
228
- node.loc?.start.line ?? 0,
229
- node.loc?.start.column ?? 0,
230
- getLineContent(code, node.loc?.start.line ?? 0)
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
- const typeEscapeHatchScore = clamp(100 - typeDensity * 15, 0, 100);
39
- const fallbackCascadeScore = clamp(100 - fallbackDensity * 12, 0, 100);
40
- const errorTransparencyScore = clamp(100 - errorDensity * 25, 0, 100);
41
- const boundaryValidationScore = clamp(100 - boundaryDensity * 10, 0, 100);
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 +