pumuki 6.3.10 → 6.3.12

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.
Files changed (74) hide show
  1. package/README.md +4 -1
  2. package/VERSION +1 -1
  3. package/core/facts/detectors/text/ios.test.ts +74 -0
  4. package/core/facts/detectors/text/ios.ts +145 -0
  5. package/core/facts/detectors/typescript/index.test.ts +147 -0
  6. package/core/facts/detectors/typescript/index.ts +288 -0
  7. package/core/facts/extractHeuristicFacts.ts +33 -0
  8. package/core/gate/Finding.ts +5 -0
  9. package/core/rules/presets/astHeuristicsRuleSet.test.ts +55 -0
  10. package/core/rules/presets/heuristics/android.test.ts +37 -0
  11. package/core/rules/presets/heuristics/browser.test.ts +37 -0
  12. package/core/rules/presets/heuristics/fsCallbacks.test.ts +27 -0
  13. package/core/rules/presets/heuristics/fsCallbacksFileOperationsRules.test.ts +56 -0
  14. package/core/rules/presets/heuristics/fsCallbacksMetadataRules.test.ts +57 -0
  15. package/core/rules/presets/heuristics/fsPromises.test.ts +27 -0
  16. package/core/rules/presets/heuristics/fsPromisesFileOperations.test.ts +44 -0
  17. package/core/rules/presets/heuristics/fsPromisesMetadataRules.test.ts +47 -0
  18. package/core/rules/presets/heuristics/fsSync.test.ts +34 -0
  19. package/core/rules/presets/heuristics/fsSyncAppendRules.test.ts +17 -0
  20. package/core/rules/presets/heuristics/fsSyncDescriptorRules.test.ts +52 -0
  21. package/core/rules/presets/heuristics/fsSyncFileOperationsRules.test.ts +55 -0
  22. package/core/rules/presets/heuristics/fsSyncPathRules.test.ts +8 -0
  23. package/core/rules/presets/heuristics/ios.test.ts +50 -0
  24. package/core/rules/presets/heuristics/ios.ts +198 -0
  25. package/core/rules/presets/heuristics/process.test.ts +51 -0
  26. package/core/rules/presets/heuristics/security.test.ts +35 -0
  27. package/core/rules/presets/heuristics/securityCredentialsRules.test.ts +42 -0
  28. package/core/rules/presets/heuristics/securityCryptoRules.test.ts +37 -0
  29. package/core/rules/presets/heuristics/securityJwtRules.test.ts +37 -0
  30. package/core/rules/presets/heuristics/securityTlsRules.test.ts +32 -0
  31. package/core/rules/presets/heuristics/typescript.test.ts +56 -0
  32. package/core/rules/presets/heuristics/typescript.ts +113 -0
  33. package/core/rules/presets/heuristics/vm.test.ts +17 -0
  34. package/core/rules/presets/iosEnterpriseRuleSet.test.ts +33 -0
  35. package/core/rules/presets/iosNonNegotiableRuleSet.test.ts +30 -0
  36. package/core/rules/presets/rulePackVersions.test.ts +1 -1
  37. package/core/rules/presets/rulePackVersions.ts +1 -1
  38. package/core/utils/stableStringify.test.ts +22 -0
  39. package/docs/INSTALLATION.md +1 -0
  40. package/docs/PUMUKI_FULL_VALIDATION_CHECKLIST.md +151 -0
  41. package/docs/REFRACTOR_PROGRESS.md +110 -2
  42. package/docs/USAGE.md +1 -0
  43. package/docs/evidence-v2.1.md +5 -0
  44. package/docs/rule-packs/README.md +14 -1
  45. package/docs/rule-packs/engineering-baseline.md +54 -0
  46. package/docs/rule-packs/heuristics.md +1 -1
  47. package/docs/rule-packs/ios.md +11 -0
  48. package/integrations/config/heuristics.test.ts +54 -0
  49. package/integrations/config/loadProjectRules.test.ts +148 -0
  50. package/integrations/config/loadProjectRules.ts +21 -3
  51. package/integrations/config/projectRules.test.ts +111 -0
  52. package/integrations/config/projectRulesSchema.test.ts +148 -0
  53. package/integrations/config/skillsCompilerTemplates.test.ts +67 -0
  54. package/integrations/config/skillsCompilerTemplates.ts +121 -0
  55. package/integrations/config/skillsLock.test.ts +154 -0
  56. package/integrations/config/skillsPolicy.test.ts +118 -0
  57. package/integrations/config/skillsRuleSet.ts +12 -0
  58. package/integrations/config/skillsSources.test.ts +119 -0
  59. package/integrations/evidence/buildEvidence.ts +32 -3
  60. package/integrations/evidence/generateEvidence.test.ts +123 -0
  61. package/integrations/evidence/readEvidence.test.ts +106 -0
  62. package/integrations/evidence/schema.test.ts +170 -0
  63. package/integrations/evidence/schema.ts +4 -0
  64. package/integrations/evidence/writeEvidence.test.ts +180 -0
  65. package/integrations/gate/stagePolicies.ts +18 -0
  66. package/integrations/git/findingTraceability.ts +313 -0
  67. package/integrations/git/runPlatformGateEvaluation.ts +55 -11
  68. package/integrations/git/runPlatformGateEvidence.ts +15 -2
  69. package/integrations/lifecycle/artifacts.ts +21 -1
  70. package/integrations/lifecycle/hookBlock.ts +6 -1
  71. package/integrations/lifecycle/remove.ts +162 -31
  72. package/package.json +1 -1
  73. package/skills.lock.json +149 -2
  74. package/skills.sources.json +15 -1
package/README.md CHANGED
@@ -104,6 +104,7 @@ npx --yes pumuki remove
104
104
  ```
105
105
 
106
106
  `pumuki remove` performs managed uninstall + artifact purge in one command.
107
+ `npm uninstall pumuki` only removes the dependency from `package.json`; it does not remove managed hooks or lifecycle state.
107
108
 
108
109
  ## Lifecycle Commands
109
110
 
@@ -118,7 +119,8 @@ The `pumuki` binary provides repository lifecycle operations:
118
119
  | `pumuki doctor` | Safety checks (hook drift, tracked `node_modules`, lifecycle state) |
119
120
  | `pumuki status` | Current lifecycle snapshot |
120
121
 
121
- `pumuki remove` is dependency-safe by design: it never deletes non-Pumuki third-party dependencies.
122
+ `pumuki remove` is dependency-safe by design: it never deletes non-Pumuki third-party dependencies and preserves pre-existing third-party empty directories.
123
+ Use `pumuki remove` (or `pumuki uninstall --purge-artifacts` + `npm uninstall pumuki`) for complete teardown.
122
124
 
123
125
  ## Gate Commands
124
126
 
@@ -140,6 +142,7 @@ Dedicated gate binaries are available:
140
142
  - Rules are evaluated with platform-aware packs and project overrides.
141
143
  - Gate applies stage policy thresholds.
142
144
  - Evidence is generated as deterministic, machine-readable contract.
145
+ - Findings carry deterministic traceability when available (`file`, `lines`, `matchedBy`, `source`).
143
146
 
144
147
  ### Default stage policies
145
148
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.10
1
+ v6.3.11
@@ -3,9 +3,20 @@ import test from 'node:test';
3
3
  import {
4
4
  hasSwiftAnyViewUsage,
5
5
  hasSwiftCallbackStyleSignature,
6
+ hasSwiftDispatchGroupUsage,
7
+ hasSwiftDispatchQueueUsage,
8
+ hasSwiftDispatchSemaphoreUsage,
6
9
  hasSwiftForceCastUsage,
7
10
  hasSwiftForceTryUsage,
8
11
  hasSwiftForceUnwrap,
12
+ hasSwiftNavigationViewUsage,
13
+ hasSwiftObservableObjectUsage,
14
+ hasSwiftOnTapGestureUsage,
15
+ hasSwiftOperationQueueUsage,
16
+ hasSwiftStringFormatUsage,
17
+ hasSwiftTaskDetachedUsage,
18
+ hasSwiftUIScreenMainBoundsUsage,
19
+ hasSwiftUncheckedSendableUsage,
9
20
  } from './ios';
10
21
 
11
22
  test('hasSwiftForceUnwrap detecta force unwrap postfix en expresiones', () => {
@@ -81,3 +92,66 @@ test('hasSwiftCallbackStyleSignature ignora usos fuera de firmas callback', () =
81
92
  const source = `\n// @escaping completion: @escaping () -> Void\nlet text = "@escaping completion: @escaping () -> Void"\n`;
82
93
  assert.equal(hasSwiftCallbackStyleSignature(source), false);
83
94
  });
95
+
96
+ test('detecta primitivas GCD y OperationQueue en codigo ejecutable', () => {
97
+ const source = `
98
+ DispatchQueue.main.async { }
99
+ DispatchGroup()
100
+ DispatchSemaphore(value: 1)
101
+ OperationQueue()
102
+ `;
103
+
104
+ assert.equal(hasSwiftDispatchQueueUsage(source), true);
105
+ assert.equal(hasSwiftDispatchGroupUsage(source), true);
106
+ assert.equal(hasSwiftDispatchSemaphoreUsage(source), true);
107
+ assert.equal(hasSwiftOperationQueueUsage(source), true);
108
+ });
109
+
110
+ test('hasSwiftTaskDetachedUsage detecta Task.detached y evita Task normal', () => {
111
+ const positive = `
112
+ Task.detached(priority: .background) { }
113
+ `;
114
+ const negative = `
115
+ Task {
116
+ await work()
117
+ }
118
+ `;
119
+ assert.equal(hasSwiftTaskDetachedUsage(positive), true);
120
+ assert.equal(hasSwiftTaskDetachedUsage(negative), false);
121
+ });
122
+
123
+ test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
124
+ const source = `
125
+ final class LegacyBox: @unchecked Sendable {}
126
+ `;
127
+ assert.equal(hasSwiftUncheckedSendableUsage(source), true);
128
+ });
129
+
130
+ test('detectores SwiftUI modernos detectan patrones legacy relevantes', () => {
131
+ const source = `
132
+ final class LegacyViewModel: ObservableObject {}
133
+ NavigationView { Text("x") }
134
+ Text("Tap").onTapGesture { }
135
+ let value = String(format: "%d", 1)
136
+ let width = UIScreen.main.bounds.width
137
+ `;
138
+ assert.equal(hasSwiftObservableObjectUsage(source), true);
139
+ assert.equal(hasSwiftNavigationViewUsage(source), true);
140
+ assert.equal(hasSwiftOnTapGestureUsage(source), true);
141
+ assert.equal(hasSwiftStringFormatUsage(source), true);
142
+ assert.equal(hasSwiftUIScreenMainBoundsUsage(source), true);
143
+ });
144
+
145
+ test('detectores legacy ignoran strings y comentarios', () => {
146
+ const source = `
147
+ // Task.detached { }
148
+ let a = "Task.detached { }"
149
+ let b = "NavigationView { }"
150
+ let c = "String(format: \\\"%d\\\", 1)"
151
+ let d = "UIScreen.main.bounds.width"
152
+ `;
153
+ assert.equal(hasSwiftTaskDetachedUsage(source), false);
154
+ assert.equal(hasSwiftNavigationViewUsage(source), false);
155
+ assert.equal(hasSwiftStringFormatUsage(source), false);
156
+ assert.equal(hasSwiftUIScreenMainBoundsUsage(source), false);
157
+ });
@@ -67,6 +67,151 @@ export const hasSwiftAnyViewUsage = (source: string): boolean => {
67
67
  });
68
68
  };
69
69
 
70
+ export const hasSwiftDispatchQueueUsage = (source: string): boolean => {
71
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
72
+ if (current !== 'D' || !hasIdentifierAt(swiftSource, index, 'DispatchQueue')) {
73
+ return false;
74
+ }
75
+
76
+ const dotIndex = nextNonWhitespaceIndex(swiftSource, index + 'DispatchQueue'.length);
77
+ return dotIndex >= 0 && swiftSource[dotIndex] === '.';
78
+ });
79
+ };
80
+
81
+ export const hasSwiftDispatchGroupUsage = (source: string): boolean => {
82
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
83
+ if (current !== 'D') {
84
+ return false;
85
+ }
86
+
87
+ return hasIdentifierAt(swiftSource, index, 'DispatchGroup');
88
+ });
89
+ };
90
+
91
+ export const hasSwiftDispatchSemaphoreUsage = (source: string): boolean => {
92
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
93
+ if (current !== 'D') {
94
+ return false;
95
+ }
96
+
97
+ return hasIdentifierAt(swiftSource, index, 'DispatchSemaphore');
98
+ });
99
+ };
100
+
101
+ export const hasSwiftOperationQueueUsage = (source: string): boolean => {
102
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
103
+ if (current !== 'O') {
104
+ return false;
105
+ }
106
+
107
+ return hasIdentifierAt(swiftSource, index, 'OperationQueue');
108
+ });
109
+ };
110
+
111
+ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
112
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
113
+ if (current !== 'T' || !hasIdentifierAt(swiftSource, index, 'Task')) {
114
+ return false;
115
+ }
116
+
117
+ const dotIndex = nextNonWhitespaceIndex(swiftSource, index + 'Task'.length);
118
+ if (dotIndex < 0 || swiftSource[dotIndex] !== '.') {
119
+ return false;
120
+ }
121
+
122
+ const detachedIndex = nextNonWhitespaceIndex(swiftSource, dotIndex + 1);
123
+ return detachedIndex >= 0 && hasIdentifierAt(swiftSource, detachedIndex, 'detached');
124
+ });
125
+ };
126
+
127
+ export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
128
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
129
+ if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
130
+ return false;
131
+ }
132
+
133
+ const sendableIndex = nextNonWhitespaceIndex(swiftSource, index + '@unchecked'.length);
134
+ return sendableIndex >= 0 && hasIdentifierAt(swiftSource, sendableIndex, 'Sendable');
135
+ });
136
+ };
137
+
138
+ export const hasSwiftObservableObjectUsage = (source: string): boolean => {
139
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
140
+ if (current !== 'O') {
141
+ return false;
142
+ }
143
+
144
+ return hasIdentifierAt(swiftSource, index, 'ObservableObject');
145
+ });
146
+ };
147
+
148
+ export const hasSwiftNavigationViewUsage = (source: string): boolean => {
149
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
150
+ if (current !== 'N') {
151
+ return false;
152
+ }
153
+
154
+ return hasIdentifierAt(swiftSource, index, 'NavigationView');
155
+ });
156
+ };
157
+
158
+ export const hasSwiftOnTapGestureUsage = (source: string): boolean => {
159
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
160
+ if (current !== 'o') {
161
+ return false;
162
+ }
163
+
164
+ return hasIdentifierAt(swiftSource, index, 'onTapGesture');
165
+ });
166
+ };
167
+
168
+ export const hasSwiftStringFormatUsage = (source: string): boolean => {
169
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
170
+ if (current !== 'S' || !hasIdentifierAt(swiftSource, index, 'String')) {
171
+ return false;
172
+ }
173
+
174
+ const openingParenIndex = nextNonWhitespaceIndex(swiftSource, index + 'String'.length);
175
+ if (openingParenIndex < 0 || swiftSource[openingParenIndex] !== '(') {
176
+ return false;
177
+ }
178
+
179
+ const formatIndex = nextNonWhitespaceIndex(swiftSource, openingParenIndex + 1);
180
+ if (formatIndex < 0 || !hasIdentifierAt(swiftSource, formatIndex, 'format')) {
181
+ return false;
182
+ }
183
+
184
+ const colonIndex = nextNonWhitespaceIndex(swiftSource, formatIndex + 'format'.length);
185
+ return colonIndex >= 0 && swiftSource[colonIndex] === ':';
186
+ });
187
+ };
188
+
189
+ export const hasSwiftUIScreenMainBoundsUsage = (source: string): boolean => {
190
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
191
+ if (current !== 'U' || !hasIdentifierAt(swiftSource, index, 'UIScreen')) {
192
+ return false;
193
+ }
194
+
195
+ const dotMainIndex = nextNonWhitespaceIndex(swiftSource, index + 'UIScreen'.length);
196
+ if (dotMainIndex < 0 || swiftSource[dotMainIndex] !== '.') {
197
+ return false;
198
+ }
199
+
200
+ const mainIndex = nextNonWhitespaceIndex(swiftSource, dotMainIndex + 1);
201
+ if (mainIndex < 0 || !hasIdentifierAt(swiftSource, mainIndex, 'main')) {
202
+ return false;
203
+ }
204
+
205
+ const dotBoundsIndex = nextNonWhitespaceIndex(swiftSource, mainIndex + 'main'.length);
206
+ if (dotBoundsIndex < 0 || swiftSource[dotBoundsIndex] !== '.') {
207
+ return false;
208
+ }
209
+
210
+ const boundsIndex = nextNonWhitespaceIndex(swiftSource, dotBoundsIndex + 1);
211
+ return boundsIndex >= 0 && hasIdentifierAt(swiftSource, boundsIndex, 'bounds');
212
+ });
213
+ };
214
+
70
215
  export const hasSwiftForceTryUsage = (source: string): boolean => {
71
216
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
72
217
  if (current !== 't' || !hasIdentifierAt(swiftSource, index, 'try')) {
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import {
4
4
  hasAsyncPromiseExecutor,
5
+ hasConcreteDependencyInstantiation,
5
6
  hasConsoleErrorCall,
6
7
  hasConsoleLogCall,
7
8
  hasDebuggerStatement,
@@ -9,9 +10,14 @@ import {
9
10
  hasEmptyCatchClause,
10
11
  hasEvalCall,
11
12
  hasExplicitAnyType,
13
+ hasFrameworkDependencyImport,
12
14
  hasFunctionConstructorUsage,
15
+ hasMixedCommandQueryClass,
16
+ hasMixedCommandQueryInterface,
17
+ hasOverrideMethodThrowingNotImplemented,
13
18
  hasSetIntervalStringCallback,
14
19
  hasSetTimeoutStringCallback,
20
+ hasTypeDiscriminatorSwitch,
15
21
  hasWithStatement,
16
22
  } from './index';
17
23
 
@@ -229,3 +235,144 @@ test('hasDebuggerStatement detecta nodos debugger', () => {
229
235
  assert.equal(hasDebuggerStatement(debuggerAst), true);
230
236
  assert.equal(hasDebuggerStatement(noDebuggerAst), false);
231
237
  });
238
+
239
+ test('hasMixedCommandQueryClass detecta mezcla command/query en la misma clase', () => {
240
+ const mixedClassAst = {
241
+ type: 'ClassDeclaration',
242
+ body: {
243
+ type: 'ClassBody',
244
+ body: [
245
+ { type: 'ClassMethod', key: { type: 'Identifier', name: 'getById' } },
246
+ { type: 'ClassMethod', key: { type: 'Identifier', name: 'save' } },
247
+ ],
248
+ },
249
+ };
250
+ const queryOnlyClassAst = {
251
+ type: 'ClassDeclaration',
252
+ body: {
253
+ type: 'ClassBody',
254
+ body: [{ type: 'ClassMethod', key: { type: 'Identifier', name: 'getById' } }],
255
+ },
256
+ };
257
+
258
+ assert.equal(hasMixedCommandQueryClass(mixedClassAst), true);
259
+ assert.equal(hasMixedCommandQueryClass(queryOnlyClassAst), false);
260
+ });
261
+
262
+ test('hasMixedCommandQueryInterface detecta mezcla command/query en la misma interfaz', () => {
263
+ const mixedInterfaceAst = {
264
+ type: 'TSInterfaceDeclaration',
265
+ body: {
266
+ type: 'TSInterfaceBody',
267
+ body: [
268
+ { type: 'TSMethodSignature', key: { type: 'Identifier', name: 'findById' } },
269
+ { type: 'TSMethodSignature', key: { type: 'Identifier', name: 'create' } },
270
+ ],
271
+ },
272
+ };
273
+ const queryOnlyInterfaceAst = {
274
+ type: 'TSInterfaceDeclaration',
275
+ body: {
276
+ type: 'TSInterfaceBody',
277
+ body: [{ type: 'TSMethodSignature', key: { type: 'Identifier', name: 'findById' } }],
278
+ },
279
+ };
280
+
281
+ assert.equal(hasMixedCommandQueryInterface(mixedInterfaceAst), true);
282
+ assert.equal(hasMixedCommandQueryInterface(queryOnlyInterfaceAst), false);
283
+ });
284
+
285
+ test('hasTypeDiscriminatorSwitch detecta switch por tipo/kind con multiples cases', () => {
286
+ const switchAst = {
287
+ type: 'SwitchStatement',
288
+ discriminant: {
289
+ type: 'MemberExpression',
290
+ computed: false,
291
+ object: { type: 'Identifier', name: 'event' },
292
+ property: { type: 'Identifier', name: 'kind' },
293
+ },
294
+ cases: [
295
+ { type: 'SwitchCase', test: { type: 'StringLiteral', value: 'a' } },
296
+ { type: 'SwitchCase', test: { type: 'StringLiteral', value: 'b' } },
297
+ ],
298
+ };
299
+ const nonDiscriminatorAst = {
300
+ type: 'SwitchStatement',
301
+ discriminant: { type: 'Identifier', name: 'index' },
302
+ cases: [
303
+ { type: 'SwitchCase', test: { type: 'NumericLiteral', value: 1 } },
304
+ { type: 'SwitchCase', test: { type: 'NumericLiteral', value: 2 } },
305
+ ],
306
+ };
307
+
308
+ assert.equal(hasTypeDiscriminatorSwitch(switchAst), true);
309
+ assert.equal(hasTypeDiscriminatorSwitch(nonDiscriminatorAst), false);
310
+ });
311
+
312
+ test('hasOverrideMethodThrowingNotImplemented detecta override con throw not implemented', () => {
313
+ const overrideThrowAst = {
314
+ type: 'ClassMethod',
315
+ override: true,
316
+ body: {
317
+ type: 'BlockStatement',
318
+ body: [
319
+ {
320
+ type: 'ThrowStatement',
321
+ argument: {
322
+ type: 'NewExpression',
323
+ callee: { type: 'Identifier', name: 'Error' },
324
+ arguments: [{ type: 'StringLiteral', value: 'Not implemented' }],
325
+ },
326
+ },
327
+ ],
328
+ },
329
+ };
330
+ const overrideValidAst = {
331
+ type: 'ClassMethod',
332
+ override: true,
333
+ body: {
334
+ type: 'BlockStatement',
335
+ body: [{ type: 'ReturnStatement', argument: { type: 'NumericLiteral', value: 1 } }],
336
+ },
337
+ };
338
+
339
+ assert.equal(hasOverrideMethodThrowingNotImplemented(overrideThrowAst), true);
340
+ assert.equal(hasOverrideMethodThrowingNotImplemented(overrideValidAst), false);
341
+ });
342
+
343
+ test('hasFrameworkDependencyImport detecta import/require de frameworks concretos', () => {
344
+ const importAst = {
345
+ type: 'ImportDeclaration',
346
+ source: { type: 'StringLiteral', value: '@nestjs/common' },
347
+ };
348
+ const requireAst = {
349
+ type: 'CallExpression',
350
+ callee: { type: 'Identifier', name: 'require' },
351
+ arguments: [{ type: 'StringLiteral', value: '@prisma/client' }],
352
+ };
353
+ const localRequireAst = {
354
+ type: 'CallExpression',
355
+ callee: { type: 'Identifier', name: 'require' },
356
+ arguments: [{ type: 'StringLiteral', value: './local' }],
357
+ };
358
+
359
+ assert.equal(hasFrameworkDependencyImport(importAst), true);
360
+ assert.equal(hasFrameworkDependencyImport(requireAst), true);
361
+ assert.equal(hasFrameworkDependencyImport(localRequireAst), false);
362
+ });
363
+
364
+ test('hasConcreteDependencyInstantiation detecta instanciacion directa de dependencias concretas', () => {
365
+ const concreteAst = {
366
+ type: 'NewExpression',
367
+ callee: { type: 'Identifier', name: 'PrismaClient' },
368
+ arguments: [],
369
+ };
370
+ const localAst = {
371
+ type: 'NewExpression',
372
+ callee: { type: 'Identifier', name: 'LocalBuilder' },
373
+ arguments: [],
374
+ };
375
+
376
+ assert.equal(hasConcreteDependencyInstantiation(concreteAst), true);
377
+ assert.equal(hasConcreteDependencyInstantiation(localAst), false);
378
+ });