pumuki 6.3.129 → 6.3.130

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 (77) hide show
  1. package/AGENTS.md +16 -1
  2. package/CHANGELOG.md +9 -0
  3. package/README.md +14 -10
  4. package/VERSION +1 -1
  5. package/core/facts/detectors/text/android.test.ts +2583 -24
  6. package/core/facts/detectors/text/android.ts +4633 -34
  7. package/core/facts/detectors/typescript/index.test.ts +3639 -73
  8. package/core/facts/detectors/typescript/index.ts +4819 -270
  9. package/core/facts/extractHeuristicFacts.ts +271 -6
  10. package/core/rules/presets/heuristics/android.test.ts +314 -1
  11. package/core/rules/presets/heuristics/android.ts +1220 -50
  12. package/core/rules/presets/heuristics/typescript.test.ts +158 -2
  13. package/core/rules/presets/heuristics/typescript.ts +508 -0
  14. package/docs/README.md +3 -3
  15. package/docs/operations/RELEASE_NOTES.md +7 -1
  16. package/docs/operations/framework-menu-consumer-walkthrough.md +18 -15
  17. package/docs/product/API_REFERENCE.md +1 -1
  18. package/docs/product/USAGE.md +1 -1
  19. package/docs/validation/README.md +3 -1
  20. package/integrations/config/skillsCompilerTemplates.test.ts +131 -0
  21. package/integrations/config/skillsCompilerTemplates.ts +953 -7
  22. package/integrations/config/skillsDetectorRegistry.ts +451 -8
  23. package/integrations/config/skillsMarkdownRules.ts +884 -2
  24. package/integrations/evidence/buildEvidence.ts +29 -1
  25. package/integrations/evidence/platformSummary.test.ts +73 -9
  26. package/integrations/evidence/platformSummary.ts +165 -7
  27. package/integrations/evidence/repoState.ts +3 -0
  28. package/integrations/evidence/schema.ts +18 -0
  29. package/integrations/evidence/trackingContract.ts +17 -0
  30. package/integrations/evidence/writeEvidence.test.ts +3 -0
  31. package/integrations/evidence/writeEvidence.ts +29 -1
  32. package/integrations/gate/evaluateAiGate.ts +251 -8
  33. package/integrations/gate/governanceActionCatalog.ts +275 -0
  34. package/integrations/gate/remediationCatalog.ts +8 -0
  35. package/integrations/git/runPlatformGate.ts +9 -1
  36. package/integrations/lifecycle/adapter.ts +24 -0
  37. package/integrations/lifecycle/bootstrapManifest.ts +248 -0
  38. package/integrations/lifecycle/cliGovernanceConsole.ts +69 -0
  39. package/integrations/lifecycle/governanceNextAction.ts +171 -0
  40. package/integrations/lifecycle/governanceObservationSnapshot.ts +369 -0
  41. package/integrations/lifecycle/packageInfo.ts +118 -1
  42. package/integrations/lifecycle/state.ts +8 -1
  43. package/integrations/lifecycle/trackingState.ts +403 -0
  44. package/integrations/lifecycle/watch.ts +1 -1
  45. package/integrations/mcp/aiGateCheck.ts +194 -10
  46. package/integrations/mcp/alignedPlatformGate.ts +232 -0
  47. package/integrations/mcp/enterpriseServer.ts +19 -3
  48. package/integrations/mcp/preFlightCheck.ts +66 -3
  49. package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
  50. package/package.json +1 -1
  51. package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
  52. package/scripts/check-tracking-single-active.sh +1 -1
  53. package/scripts/framework-menu-advanced-view-lib.ts +49 -0
  54. package/scripts/framework-menu-consumer-actions-lib.ts +32 -32
  55. package/scripts/framework-menu-consumer-preflight-render.ts +10 -0
  56. package/scripts/framework-menu-consumer-preflight-run.ts +23 -0
  57. package/scripts/framework-menu-consumer-preflight-types.ts +12 -0
  58. package/scripts/framework-menu-consumer-runtime-actions.ts +11 -5
  59. package/scripts/framework-menu-consumer-runtime-audit.ts +0 -28
  60. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +118 -42
  61. package/scripts/framework-menu-consumer-runtime-lib.ts +38 -0
  62. package/scripts/framework-menu-consumer-runtime-menu.ts +55 -15
  63. package/scripts/framework-menu-consumer-runtime-types.ts +4 -0
  64. package/scripts/framework-menu-evidence-summary-read.ts +17 -1
  65. package/scripts/framework-menu-evidence-summary-types.ts +3 -0
  66. package/scripts/framework-menu-layout-data.ts +3 -23
  67. package/scripts/framework-menu-system-notifications-macos-applescript-dialog.ts +1 -1
  68. package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +14 -2
  69. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +1 -1
  70. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +128 -4
  71. package/scripts/framework-menu-system-notifications-payloads.ts +8 -1
  72. package/scripts/framework-menu-system-notifications-text.ts +7 -1
  73. package/scripts/framework-menu.ts +37 -2
  74. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +10 -1
  75. package/scripts/package-install-smoke-consumer-npm-lib.ts +46 -9
  76. package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
  77. package/skills.lock.json +613 -698
@@ -2,11 +2,49 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import {
4
4
  findEmptyCatchClauseLines,
5
+ findEmptyCatchClauseMatch,
5
6
  findConcreteDependencyInstantiationMatch,
7
+ findConsoleLogCallMatch,
8
+ findSensitiveLogCallMatch,
9
+ findErrorLoggingFullContextMatch,
10
+ findCorrelationIdsMatch,
11
+ findCorsConfiguredMatch,
12
+ findValidationPipeGlobalMatch,
13
+ findValidationConfigMatch,
14
+ findApiVersioningMatch,
15
+ findInputValidationMatch,
16
+ findNestedValidationMatch,
17
+ findClassValidatorDecoratorsMatch,
18
+ findClassTransformerDecoratorsMatch,
19
+ findDtoBoundaryMatch,
20
+ findSeparatedDtoMatch,
21
+ findBackendReturnDtosExposureLines,
22
+ findBackendCriticalTransactionsLines,
23
+ findBackendCriticalTransactionsMatch,
24
+ findBackendMultiTableTransactionsLines,
25
+ findBackendMultiTableTransactionsMatch,
26
+ findPrometheusMetricsMatch,
27
+ findPasswordHashingPatternMatch,
28
+ findRateLimitingThrottlerMatch,
29
+ findWinstonStructuredLoggerMatch,
30
+ findExplicitAnyTypeMatch,
31
+ findCleanArchitectureMatch,
32
+ findExceptionFilterClassMatch,
33
+ findGuardUseGuardsJwtAuthGuardMatch,
34
+ findUseInterceptorsLoggingTransformMatch,
6
35
  findFrameworkDependencyImportMatch,
7
36
  findMixedCommandQueryClassMatch,
8
37
  findMixedCommandQueryInterfaceMatch,
9
38
  findOverrideMethodThrowingNotImplementedMatch,
39
+ findProductionMockCallMatch,
40
+ findCallbackHellPatternMatch,
41
+ findHardcodedValuePatternMatch,
42
+ findEnvDefaultFallbackPatternMatch,
43
+ findMagicNumberPatternMatch,
44
+ findLargeClassDeclarationMatch,
45
+ findReactClassComponentLines,
46
+ findReactClassComponentMatch,
47
+ findSingletonPatternMatch,
10
48
  findTypeDiscriminatorSwitchMatch,
11
49
  findNetworkCallWithoutErrorHandlingLines,
12
50
  findRecordStringUnknownTypeLines,
@@ -17,6 +55,27 @@ import {
17
55
  hasConcreteDependencyInstantiation,
18
56
  hasConsoleErrorCall,
19
57
  hasConsoleLogCall,
58
+ hasSensitiveLogCall,
59
+ hasErrorLoggingFullContextPattern,
60
+ hasCorrelationIdsPattern,
61
+ hasCorsConfiguredPattern,
62
+ hasValidationPipeGlobalPattern,
63
+ hasValidationConfigPattern,
64
+ hasApiVersioningPattern,
65
+ hasInputValidationPattern,
66
+ hasNestedValidationPattern,
67
+ hasClassValidatorDecoratorsPattern,
68
+ hasClassTransformerDecoratorsPattern,
69
+ hasDtoBoundaryPattern,
70
+ hasSeparatedDtoPattern,
71
+ hasBackendReturnDtosExposureUsage,
72
+ hasBackendCriticalTransactionsUsage,
73
+ hasBackendMultiTableTransactionsUsage,
74
+ hasPrometheusMetricsPattern,
75
+ hasPasswordHashingPattern,
76
+ hasRateLimitingThrottlerPattern,
77
+ hasWinstonStructuredLoggerPattern,
78
+ hasCallbackHellPattern,
20
79
  hasDebuggerStatement,
21
80
  hasDeleteOperator,
22
81
  hasEmptyCatchClause,
@@ -24,13 +83,22 @@ import {
24
83
  hasExplicitAnyType,
25
84
  hasFrameworkDependencyImport,
26
85
  hasFunctionConstructorUsage,
86
+ hasHardcodedValuePattern,
87
+ hasEnvDefaultFallbackPattern,
27
88
  hasNetworkCallWithoutErrorHandling,
28
89
  hasMixedCommandQueryClass,
29
90
  hasMixedCommandQueryInterface,
91
+ hasMagicNumberPattern,
30
92
  hasUnknownWithoutGuard,
31
93
  hasRecordStringUnknownType,
32
94
  hasOverrideMethodThrowingNotImplemented,
33
95
  hasLargeClassDeclaration,
96
+ hasReactClassComponentUsage,
97
+ hasExceptionFilterClass,
98
+ hasGuardUseGuardsJwtAuthGuard,
99
+ hasUseInterceptorsLoggingTransform,
100
+ hasProductionMockCall,
101
+ hasSingletonPattern,
34
102
  hasSetIntervalStringCallback,
35
103
  hasSetTimeoutStringCallback,
36
104
  hasTypeDiscriminatorSwitch,
@@ -76,6 +144,50 @@ test('findEmptyCatchClauseLines devuelve lineas ancla para catch vacio', () => {
76
144
  assert.deepEqual(findEmptyCatchClauseLines(ast), [7]);
77
145
  });
78
146
 
147
+ test('findEmptyCatchClauseMatch devuelve payload semantico para catch vacio', () => {
148
+ const ast = {
149
+ type: 'Program',
150
+ body: [
151
+ {
152
+ type: 'FunctionDeclaration',
153
+ id: { type: 'Identifier', name: 'processOrder' },
154
+ loc: { start: { line: 2 }, end: { line: 8 } },
155
+ body: {
156
+ type: 'BlockStatement',
157
+ body: [
158
+ {
159
+ type: 'TryStatement',
160
+ loc: { start: { line: 4 }, end: { line: 7 } },
161
+ block: { type: 'BlockStatement', body: [] },
162
+ handler: {
163
+ type: 'CatchClause',
164
+ loc: { start: { line: 6 }, end: { line: 6 } },
165
+ body: { type: 'BlockStatement', body: [] },
166
+ },
167
+ },
168
+ ],
169
+ },
170
+ },
171
+ ],
172
+ };
173
+
174
+ const match = findEmptyCatchClauseMatch(ast);
175
+
176
+ assert.ok(match);
177
+ assert.deepEqual(match?.primary_node, {
178
+ kind: 'member',
179
+ name: 'processOrder',
180
+ lines: [2],
181
+ });
182
+ assert.deepEqual(match?.related_nodes, [
183
+ { kind: 'member', name: 'empty catch', lines: [6] },
184
+ ]);
185
+ assert.deepEqual(match?.lines, [2, 6]);
186
+ assert.match(match?.why ?? '', /catch vac[ií]o/i);
187
+ assert.match(match?.impact ?? '', /producci[oó]n|observabilidad/i);
188
+ assert.match(match?.expected_fix ?? '', /registra|propaga|documenta/i);
189
+ });
190
+
79
191
  test('hasExplicitAnyType detecta TSAnyKeyword', () => {
80
192
  const anyAst = {
81
193
  type: 'TSTypeAnnotation',
@@ -90,30 +202,2758 @@ test('hasExplicitAnyType detecta TSAnyKeyword', () => {
90
202
  assert.equal(hasExplicitAnyType(unknownAst), false);
91
203
  });
92
204
 
93
- test('hasConsoleLogCall detecta console.log y descarta variantes no soportadas', () => {
94
- const logAst = {
205
+ test('findExplicitAnyTypeMatch devuelve payload semantico para any explicito', () => {
206
+ const anyAst = {
207
+ type: 'Program',
208
+ body: [
209
+ {
210
+ type: 'FunctionDeclaration',
211
+ id: { type: 'Identifier', name: 'parsePayload' },
212
+ loc: { start: { line: 2 }, end: { line: 8 } },
213
+ params: [
214
+ {
215
+ type: 'Identifier',
216
+ name: 'payload',
217
+ typeAnnotation: {
218
+ type: 'TSTypeAnnotation',
219
+ loc: { start: { line: 3 }, end: { line: 3 } },
220
+ typeAnnotation: { type: 'TSAnyKeyword', loc: { start: { line: 3 }, end: { line: 3 } } },
221
+ },
222
+ },
223
+ ],
224
+ body: { type: 'BlockStatement', body: [] },
225
+ },
226
+ ],
227
+ };
228
+
229
+ const match = findExplicitAnyTypeMatch(anyAst);
230
+
231
+ assert.ok(match);
232
+ assert.deepEqual(match?.primary_node, {
233
+ kind: 'member',
234
+ name: 'parsePayload',
235
+ lines: [2],
236
+ });
237
+ assert.deepEqual(match?.related_nodes, [
238
+ { kind: 'member', name: 'explicit any', lines: [3] },
239
+ ]);
240
+ assert.deepEqual(match?.lines, [2, 3]);
241
+ assert.match(match?.why ?? '', /any/i);
242
+ assert.match(match?.impact ?? '', /regresiones|tipad/i);
243
+ assert.match(match?.expected_fix ?? '', /unknown|gen[eé]rico/i);
244
+ });
245
+
246
+ test('findCleanArchitectureMatch devuelve payload semantico para clean architecture', () => {
247
+ const cleanArchitectureAst = {
248
+ type: 'Program',
249
+ body: [
250
+ {
251
+ type: 'ImportDeclaration',
252
+ loc: { start: { line: 1 }, end: { line: 1 } },
253
+ source: { type: 'StringLiteral', value: '@prisma/client' },
254
+ },
255
+ {
256
+ type: 'ClassDeclaration',
257
+ id: { type: 'Identifier', name: 'OrderApplicationService' },
258
+ loc: { start: { line: 3 }, end: { line: 12 } },
259
+ body: {
260
+ type: 'ClassBody',
261
+ body: [
262
+ {
263
+ type: 'ClassProperty',
264
+ key: { type: 'Identifier', name: 'client' },
265
+ loc: { start: { line: 4 }, end: { line: 4 } },
266
+ value: {
267
+ type: 'NewExpression',
268
+ loc: { start: { line: 4 }, end: { line: 4 } },
269
+ callee: { type: 'Identifier', name: 'PrismaClient' },
270
+ arguments: [],
271
+ },
272
+ },
273
+ ],
274
+ },
275
+ },
276
+ ],
277
+ };
278
+
279
+ const match = findCleanArchitectureMatch(cleanArchitectureAst);
280
+
281
+ assert.ok(match);
282
+ assert.deepEqual(match?.primary_node, {
283
+ kind: 'class',
284
+ name: 'OrderApplicationService',
285
+ lines: [3],
286
+ });
287
+ assert.deepEqual(match?.related_nodes, [
288
+ { kind: 'member', name: 'import:@prisma/client', lines: [1] },
289
+ { kind: 'call', name: 'new PrismaClient', lines: [4] },
290
+ ]);
291
+ assert.deepEqual(match?.lines, [1, 3, 4]);
292
+ assert.match(match?.why ?? '', /Clean Architecture/i);
293
+ assert.match(match?.impact ?? '', /direcci[oó]n de dependencias|acopl/i);
294
+ assert.match(match?.expected_fix ?? '', /puerto|abstracci/i);
295
+ });
296
+
297
+ test('hasProductionMockCall detecta jest.mock y descarta llamadas no mock', () => {
298
+ const productionMockAst = {
95
299
  type: 'CallExpression',
300
+ loc: { start: { line: 14 }, end: { line: 14 } },
96
301
  callee: {
97
302
  type: 'MemberExpression',
98
303
  computed: false,
99
- object: { type: 'Identifier', name: 'console' },
100
- property: { type: 'Identifier', name: 'log' },
304
+ object: { type: 'Identifier', name: 'jest' },
305
+ property: { type: 'Identifier', name: 'mock' },
101
306
  },
102
- arguments: [{ type: 'StringLiteral', value: 'x' }],
307
+ arguments: [{ type: 'StringLiteral', value: '@prisma/client' }],
103
308
  };
104
- const computedAst = {
309
+ const nonMockAst = {
105
310
  type: 'CallExpression',
311
+ loc: { start: { line: 18 }, end: { line: 18 } },
106
312
  callee: {
107
313
  type: 'MemberExpression',
108
- computed: true,
109
- object: { type: 'Identifier', name: 'console' },
110
- property: { type: 'StringLiteral', value: 'log' },
314
+ computed: false,
315
+ object: { type: 'Identifier', name: 'jest' },
316
+ property: { type: 'Identifier', name: 'fn' },
111
317
  },
112
318
  arguments: [],
113
319
  };
114
320
 
115
- assert.equal(hasConsoleLogCall(logAst), true);
116
- assert.equal(hasConsoleLogCall(computedAst), false);
321
+ assert.equal(hasProductionMockCall(productionMockAst), true);
322
+ assert.equal(hasProductionMockCall(nonMockAst), false);
323
+ });
324
+
325
+ test('findProductionMockCallMatch devuelve payload semantico para mocks en produccion', () => {
326
+ const ast = {
327
+ type: 'Program',
328
+ body: [
329
+ {
330
+ type: 'FunctionDeclaration',
331
+ id: { type: 'Identifier', name: 'bootstrap' },
332
+ loc: { start: { line: 2 }, end: { line: 10 } },
333
+ body: {
334
+ type: 'BlockStatement',
335
+ body: [
336
+ {
337
+ type: 'ExpressionStatement',
338
+ loc: { start: { line: 6 }, end: { line: 6 } },
339
+ expression: {
340
+ type: 'CallExpression',
341
+ loc: { start: { line: 6 }, end: { line: 6 } },
342
+ callee: {
343
+ type: 'MemberExpression',
344
+ computed: false,
345
+ object: { type: 'Identifier', name: 'vi' },
346
+ property: { type: 'Identifier', name: 'mock' },
347
+ },
348
+ arguments: [{ type: 'StringLiteral', value: '@/services/order' }],
349
+ },
350
+ },
351
+ ],
352
+ },
353
+ },
354
+ ],
355
+ };
356
+
357
+ const match = findProductionMockCallMatch(ast);
358
+
359
+ assert.ok(match);
360
+ assert.deepEqual(match?.primary_node, {
361
+ kind: 'member',
362
+ name: 'bootstrap',
363
+ lines: [2],
364
+ });
365
+ assert.deepEqual(match?.related_nodes, [
366
+ { kind: 'call', name: 'vi.mock', lines: [6] },
367
+ ]);
368
+ assert.deepEqual(match?.lines, [2, 6]);
369
+ assert.match(match?.why ?? '', /mock/i);
370
+ assert.match(match?.impact ?? '', /producci[oó]n|observabilidad|integraci[oó]n/i);
371
+ assert.match(match?.expected_fix ?? '', /tests|fixtures|dobles|reales/i);
372
+ });
373
+
374
+ test('hasExceptionFilterClass detecta filtros globales de excepciones y descarta clases comunes', () => {
375
+ const exceptionFilterAst = {
376
+ type: 'ClassDeclaration',
377
+ id: { type: 'Identifier', name: 'HttpExceptionFilter' },
378
+ decorators: [
379
+ {
380
+ type: 'Decorator',
381
+ expression: {
382
+ type: 'CallExpression',
383
+ callee: { type: 'Identifier', name: 'Catch' },
384
+ arguments: [],
385
+ },
386
+ },
387
+ ],
388
+ implements: [
389
+ {
390
+ type: 'TSExpressionWithTypeArguments',
391
+ expression: { type: 'Identifier', name: 'ExceptionFilter' },
392
+ },
393
+ ],
394
+ body: {
395
+ type: 'ClassBody',
396
+ body: [
397
+ {
398
+ type: 'ClassMethod',
399
+ key: { type: 'Identifier', name: 'catch' },
400
+ params: [
401
+ { type: 'Identifier', name: 'exception' },
402
+ { type: 'Identifier', name: 'host' },
403
+ ],
404
+ body: { type: 'BlockStatement', body: [] },
405
+ },
406
+ ],
407
+ },
408
+ };
409
+ const commonClassAst = {
410
+ type: 'ClassDeclaration',
411
+ id: { type: 'Identifier', name: 'OrderService' },
412
+ body: {
413
+ type: 'ClassBody',
414
+ body: [],
415
+ },
416
+ };
417
+
418
+ assert.equal(hasExceptionFilterClass(exceptionFilterAst), true);
419
+ assert.equal(hasExceptionFilterClass(commonClassAst), false);
420
+ });
421
+
422
+ test('findExceptionFilterClassMatch devuelve payload semantico para filtros globales de excepciones', () => {
423
+ const ast = {
424
+ type: 'Program',
425
+ body: [
426
+ {
427
+ type: 'ClassDeclaration',
428
+ id: { type: 'Identifier', name: 'AllExceptionsFilter' },
429
+ loc: { start: { line: 3 }, end: { line: 26 } },
430
+ decorators: [
431
+ {
432
+ type: 'Decorator',
433
+ loc: { start: { line: 3 }, end: { line: 3 } },
434
+ expression: {
435
+ type: 'CallExpression',
436
+ callee: { type: 'Identifier', name: 'Catch' },
437
+ arguments: [],
438
+ },
439
+ },
440
+ ],
441
+ implements: [
442
+ {
443
+ type: 'TSExpressionWithTypeArguments',
444
+ expression: { type: 'Identifier', name: 'ExceptionFilter' },
445
+ },
446
+ ],
447
+ body: {
448
+ type: 'ClassBody',
449
+ body: [
450
+ {
451
+ type: 'ClassMethod',
452
+ key: { type: 'Identifier', name: 'catch' },
453
+ loc: { start: { line: 10 }, end: { line: 22 } },
454
+ params: [
455
+ { type: 'Identifier', name: 'exception' },
456
+ { type: 'Identifier', name: 'host' },
457
+ ],
458
+ body: { type: 'BlockStatement', body: [] },
459
+ },
460
+ ],
461
+ },
462
+ },
463
+ ],
464
+ };
465
+
466
+ const match = findExceptionFilterClassMatch(ast);
467
+
468
+ assert.ok(match);
469
+ assert.deepEqual(match?.primary_node, {
470
+ kind: 'class',
471
+ name: 'AllExceptionsFilter',
472
+ lines: [3],
473
+ });
474
+ assert.deepEqual(match?.related_nodes, [
475
+ { kind: 'member', name: 'catch', lines: [10] },
476
+ { kind: 'member', name: '@Catch', lines: [3] },
477
+ ]);
478
+ assert.deepEqual(match?.lines, [3, 10]);
479
+ assert.match(match?.why ?? '', /filtro global|excepciones/i);
480
+ assert.match(match?.impact ?? '', /trazabilidad|respuestas/i);
481
+ assert.match(match?.expected_fix ?? '', /@Catch|catch/i);
482
+ });
483
+
484
+ test('hasGuardUseGuardsJwtAuthGuard detecta UseGuards(JwtAuthGuard) y descarta guards distintos', () => {
485
+ const guardAst = {
486
+ type: 'ClassDeclaration',
487
+ id: { type: 'Identifier', name: 'OrdersController' },
488
+ decorators: [
489
+ {
490
+ type: 'Decorator',
491
+ expression: {
492
+ type: 'CallExpression',
493
+ callee: { type: 'Identifier', name: 'UseGuards' },
494
+ arguments: [{ type: 'Identifier', name: 'JwtAuthGuard' }],
495
+ },
496
+ },
497
+ ],
498
+ body: {
499
+ type: 'ClassBody',
500
+ body: [],
501
+ },
502
+ };
503
+ const otherGuardAst = {
504
+ type: 'ClassDeclaration',
505
+ id: { type: 'Identifier', name: 'OrdersController' },
506
+ decorators: [
507
+ {
508
+ type: 'Decorator',
509
+ expression: {
510
+ type: 'CallExpression',
511
+ callee: { type: 'Identifier', name: 'UseGuards' },
512
+ arguments: [{ type: 'Identifier', name: 'RolesGuard' }],
513
+ },
514
+ },
515
+ ],
516
+ body: {
517
+ type: 'ClassBody',
518
+ body: [],
519
+ },
520
+ };
521
+
522
+ assert.equal(hasGuardUseGuardsJwtAuthGuard(guardAst), true);
523
+ assert.equal(hasGuardUseGuardsJwtAuthGuard(otherGuardAst), false);
524
+ });
525
+
526
+ test('findGuardUseGuardsJwtAuthGuardMatch devuelve payload semantico para UseGuards(JwtAuthGuard)', () => {
527
+ const ast = {
528
+ type: 'Program',
529
+ body: [
530
+ {
531
+ type: 'ClassDeclaration',
532
+ id: { type: 'Identifier', name: 'OrdersController' },
533
+ loc: { start: { line: 3 }, end: { line: 18 } },
534
+ decorators: [
535
+ {
536
+ type: 'Decorator',
537
+ loc: { start: { line: 3 }, end: { line: 3 } },
538
+ expression: {
539
+ type: 'CallExpression',
540
+ callee: { type: 'Identifier', name: 'UseGuards' },
541
+ arguments: [{ type: 'Identifier', name: 'JwtAuthGuard' }],
542
+ },
543
+ },
544
+ ],
545
+ body: {
546
+ type: 'ClassBody',
547
+ body: [],
548
+ },
549
+ },
550
+ ],
551
+ };
552
+
553
+ const match = findGuardUseGuardsJwtAuthGuardMatch(ast);
554
+
555
+ assert.ok(match);
556
+ assert.deepEqual(match?.primary_node, {
557
+ kind: 'class',
558
+ name: 'OrdersController',
559
+ lines: [3],
560
+ });
561
+ assert.deepEqual(match?.related_nodes, [
562
+ { kind: 'call', name: 'UseGuards', lines: [3] },
563
+ { kind: 'member', name: 'JwtAuthGuard', lines: [3] },
564
+ ]);
565
+ assert.deepEqual(match?.lines, [3]);
566
+ assert.match(match?.why ?? '', /UseGuards/i);
567
+ assert.match(match?.impact ?? '', /per[ií]metro|guard/i);
568
+ assert.match(match?.expected_fix ?? '', /JwtAuthGuard|guard/i);
569
+ });
570
+
571
+ test('hasUseInterceptorsLoggingTransform detecta UseInterceptors y descarta decoradores no relevantes', () => {
572
+ const interceptorAst = {
573
+ type: 'ClassDeclaration',
574
+ id: { type: 'Identifier', name: 'OrdersController' },
575
+ decorators: [
576
+ {
577
+ type: 'Decorator',
578
+ expression: {
579
+ type: 'CallExpression',
580
+ callee: { type: 'Identifier', name: 'UseInterceptors' },
581
+ arguments: [{ type: 'Identifier', name: 'LoggingInterceptor' }],
582
+ },
583
+ },
584
+ ],
585
+ body: {
586
+ type: 'ClassBody',
587
+ body: [],
588
+ },
589
+ };
590
+ const otherDecoratorAst = {
591
+ type: 'ClassDeclaration',
592
+ id: { type: 'Identifier', name: 'OrdersController' },
593
+ decorators: [
594
+ {
595
+ type: 'Decorator',
596
+ expression: {
597
+ type: 'CallExpression',
598
+ callee: { type: 'Identifier', name: 'UseGuards' },
599
+ arguments: [{ type: 'Identifier', name: 'JwtAuthGuard' }],
600
+ },
601
+ },
602
+ ],
603
+ body: {
604
+ type: 'ClassBody',
605
+ body: [],
606
+ },
607
+ };
608
+
609
+ assert.equal(hasUseInterceptorsLoggingTransform(interceptorAst), true);
610
+ assert.equal(hasUseInterceptorsLoggingTransform(otherDecoratorAst), false);
611
+ });
612
+
613
+ test('findUseInterceptorsLoggingTransformMatch devuelve payload semantico para UseInterceptors', () => {
614
+ const ast = {
615
+ type: 'Program',
616
+ body: [
617
+ {
618
+ type: 'ClassDeclaration',
619
+ id: { type: 'Identifier', name: 'OrdersController' },
620
+ loc: { start: { line: 4 }, end: { line: 24 } },
621
+ decorators: [
622
+ {
623
+ type: 'Decorator',
624
+ loc: { start: { line: 4 }, end: { line: 4 } },
625
+ expression: {
626
+ type: 'CallExpression',
627
+ callee: { type: 'Identifier', name: 'UseInterceptors' },
628
+ arguments: [{ type: 'Identifier', name: 'ClassSerializerInterceptor' }],
629
+ },
630
+ },
631
+ ],
632
+ body: {
633
+ type: 'ClassBody',
634
+ body: [],
635
+ },
636
+ },
637
+ ],
638
+ };
639
+
640
+ const match = findUseInterceptorsLoggingTransformMatch(ast);
641
+
642
+ assert.ok(match);
643
+ assert.deepEqual(match?.primary_node, {
644
+ kind: 'class',
645
+ name: 'OrdersController',
646
+ lines: [4],
647
+ });
648
+ assert.deepEqual(match?.related_nodes, [
649
+ { kind: 'call', name: 'UseInterceptors', lines: [4] },
650
+ { kind: 'member', name: 'ClassSerializerInterceptor', lines: [4] },
651
+ ]);
652
+ assert.deepEqual(match?.lines, [4]);
653
+ assert.match(match?.why ?? '', /UseInterceptors/i);
654
+ assert.match(match?.impact ?? '', /logging|transform/i);
655
+ assert.match(match?.expected_fix ?? '', /interceptor|reutilizable/i);
656
+ });
657
+
658
+ test('hasConsoleLogCall detecta console.log y descarta variantes no soportadas', () => {
659
+ const logAst = {
660
+ type: 'CallExpression',
661
+ callee: {
662
+ type: 'MemberExpression',
663
+ computed: false,
664
+ object: { type: 'Identifier', name: 'console' },
665
+ property: { type: 'Identifier', name: 'log' },
666
+ },
667
+ arguments: [{ type: 'StringLiteral', value: 'x' }],
668
+ };
669
+ const computedAst = {
670
+ type: 'CallExpression',
671
+ callee: {
672
+ type: 'MemberExpression',
673
+ computed: true,
674
+ object: { type: 'Identifier', name: 'console' },
675
+ property: { type: 'StringLiteral', value: 'log' },
676
+ },
677
+ arguments: [],
678
+ };
679
+
680
+ assert.equal(hasConsoleLogCall(logAst), true);
681
+ assert.equal(hasConsoleLogCall(computedAst), false);
682
+ });
683
+
684
+ test('findConsoleLogCallMatch devuelve payload semantico para console.log', () => {
685
+ const logAst = {
686
+ type: 'Program',
687
+ body: [
688
+ {
689
+ type: 'FunctionDeclaration',
690
+ id: { type: 'Identifier', name: 'renderAuditTrail' },
691
+ loc: { start: { line: 2 }, end: { line: 8 } },
692
+ body: {
693
+ type: 'BlockStatement',
694
+ body: [
695
+ {
696
+ type: 'ExpressionStatement',
697
+ loc: { start: { line: 4 }, end: { line: 4 } },
698
+ expression: {
699
+ type: 'CallExpression',
700
+ loc: { start: { line: 4 }, end: { line: 4 } },
701
+ callee: {
702
+ type: 'MemberExpression',
703
+ computed: false,
704
+ object: { type: 'Identifier', name: 'console' },
705
+ property: { type: 'Identifier', name: 'log' },
706
+ },
707
+ arguments: [{ type: 'Identifier', name: 'value' }],
708
+ },
709
+ },
710
+ ],
711
+ },
712
+ },
713
+ ],
714
+ };
715
+
716
+ const match = findConsoleLogCallMatch(logAst);
717
+
718
+ assert.ok(match);
719
+ assert.deepEqual(match?.primary_node, {
720
+ kind: 'member',
721
+ name: 'renderAuditTrail',
722
+ lines: [2],
723
+ });
724
+ assert.deepEqual(match?.related_nodes, [
725
+ { kind: 'call', name: 'console.log', lines: [4] },
726
+ ]);
727
+ assert.deepEqual(match?.lines, [2, 4]);
728
+ assert.match(match?.why ?? '', /console\.log/i);
729
+ assert.match(match?.impact ?? '', /logs de producci/i);
730
+ assert.match(match?.expected_fix ?? '', /logger|traza/i);
731
+ });
732
+
733
+ test('hasSensitiveLogCall detecta logger con datos sensibles y descarta logs inocuos', () => {
734
+ const sensitiveLogAst = {
735
+ type: 'CallExpression',
736
+ loc: { start: { line: 14 }, end: { line: 14 } },
737
+ callee: {
738
+ type: 'MemberExpression',
739
+ computed: false,
740
+ object: { type: 'Identifier', name: 'logger' },
741
+ property: { type: 'Identifier', name: 'info' },
742
+ },
743
+ arguments: [
744
+ {
745
+ type: 'ObjectExpression',
746
+ properties: [
747
+ {
748
+ type: 'ObjectProperty',
749
+ loc: { start: { line: 14 }, end: { line: 14 } },
750
+ key: { type: 'Identifier', name: 'password' },
751
+ value: { type: 'Identifier', name: 'rawPassword' },
752
+ },
753
+ ],
754
+ },
755
+ ],
756
+ };
757
+ const safeLogAst = {
758
+ type: 'CallExpression',
759
+ loc: { start: { line: 18 }, end: { line: 18 } },
760
+ callee: {
761
+ type: 'MemberExpression',
762
+ computed: false,
763
+ object: { type: 'Identifier', name: 'logger' },
764
+ property: { type: 'Identifier', name: 'info' },
765
+ },
766
+ arguments: [
767
+ {
768
+ type: 'ObjectExpression',
769
+ properties: [
770
+ {
771
+ type: 'ObjectProperty',
772
+ loc: { start: { line: 18 }, end: { line: 18 } },
773
+ key: { type: 'Identifier', name: 'status' },
774
+ value: { type: 'Identifier', name: 'state' },
775
+ },
776
+ ],
777
+ },
778
+ ],
779
+ };
780
+
781
+ assert.equal(hasSensitiveLogCall(sensitiveLogAst), true);
782
+ assert.equal(hasSensitiveLogCall(safeLogAst), false);
783
+ });
784
+
785
+ test('findSensitiveLogCallMatch devuelve payload semantico para log sensible', () => {
786
+ const ast = {
787
+ type: 'Program',
788
+ body: [
789
+ {
790
+ type: 'FunctionDeclaration',
791
+ id: { type: 'Identifier', name: 'createSession' },
792
+ loc: { start: { line: 3 }, end: { line: 14 } },
793
+ body: {
794
+ type: 'BlockStatement',
795
+ body: [
796
+ {
797
+ type: 'ExpressionStatement',
798
+ loc: { start: { line: 9 }, end: { line: 9 } },
799
+ expression: {
800
+ type: 'CallExpression',
801
+ loc: { start: { line: 9 }, end: { line: 9 } },
802
+ callee: {
803
+ type: 'MemberExpression',
804
+ computed: false,
805
+ object: { type: 'Identifier', name: 'logger' },
806
+ property: { type: 'Identifier', name: 'warn' },
807
+ },
808
+ arguments: [
809
+ {
810
+ type: 'StringLiteral',
811
+ value: 'session created',
812
+ },
813
+ {
814
+ type: 'Identifier',
815
+ loc: { start: { line: 9 }, end: { line: 9 } },
816
+ name: 'accessToken',
817
+ },
818
+ ],
819
+ },
820
+ },
821
+ ],
822
+ },
823
+ },
824
+ ],
825
+ };
826
+
827
+ const match = findSensitiveLogCallMatch(ast);
828
+
829
+ assert.ok(match);
830
+ assert.deepEqual(match?.primary_node, {
831
+ kind: 'member',
832
+ name: 'createSession',
833
+ lines: [3],
834
+ });
835
+ assert.deepEqual(match?.related_nodes, [
836
+ { kind: 'call', name: 'logging', lines: [9] },
837
+ { kind: 'member', name: 'accessToken', lines: [9] },
838
+ ]);
839
+ assert.deepEqual(match?.lines, [3, 9]);
840
+ assert.match(match?.why ?? '', /datos sensibles|accessToken/i);
841
+ assert.match(match?.impact ?? '', /PII|tokens|Passwords/i);
842
+ assert.match(match?.expected_fix ?? '', /red[aá]ctalo|mascarado|elimina/i);
843
+ });
844
+
845
+ test('hasErrorLoggingFullContextPattern detecta logger.error sin contexto y descarta logs completos', () => {
846
+ const incompleteErrorLogAst = {
847
+ type: 'CallExpression',
848
+ callee: {
849
+ type: 'MemberExpression',
850
+ computed: false,
851
+ object: { type: 'Identifier', name: 'logger' },
852
+ property: { type: 'Identifier', name: 'error' },
853
+ },
854
+ arguments: [{ type: 'Identifier', name: 'error' }],
855
+ };
856
+ const completeErrorLogAst = {
857
+ type: 'CallExpression',
858
+ callee: {
859
+ type: 'MemberExpression',
860
+ computed: false,
861
+ object: { type: 'Identifier', name: 'logger' },
862
+ property: { type: 'Identifier', name: 'error' },
863
+ },
864
+ arguments: [
865
+ { type: 'StringLiteral', value: 'Order failed' },
866
+ {
867
+ type: 'ObjectExpression',
868
+ properties: [
869
+ {
870
+ type: 'ObjectProperty',
871
+ key: { type: 'Identifier', name: 'context' },
872
+ value: { type: 'StringLiteral', value: 'checkout' },
873
+ },
874
+ ],
875
+ },
876
+ ],
877
+ };
878
+
879
+ assert.equal(hasErrorLoggingFullContextPattern(incompleteErrorLogAst), true);
880
+ assert.equal(hasErrorLoggingFullContextPattern(completeErrorLogAst), false);
881
+ });
882
+
883
+ test('findErrorLoggingFullContextMatch devuelve payload semantico para logger.error sin contexto', () => {
884
+ const ast = {
885
+ type: 'Program',
886
+ body: [
887
+ {
888
+ type: 'FunctionDeclaration',
889
+ id: { type: 'Identifier', name: 'processPayment' },
890
+ loc: { start: { line: 2 }, end: { line: 8 } },
891
+ body: {
892
+ type: 'BlockStatement',
893
+ body: [
894
+ {
895
+ type: 'ExpressionStatement',
896
+ loc: { start: { line: 4 }, end: { line: 4 } },
897
+ expression: {
898
+ type: 'CallExpression',
899
+ loc: { start: { line: 4 }, end: { line: 4 } },
900
+ callee: {
901
+ type: 'MemberExpression',
902
+ computed: false,
903
+ object: { type: 'Identifier', name: 'logger' },
904
+ property: { type: 'Identifier', name: 'error' },
905
+ },
906
+ arguments: [{ type: 'Identifier', name: 'error' }],
907
+ },
908
+ },
909
+ ],
910
+ },
911
+ },
912
+ ],
913
+ };
914
+
915
+ const match = findErrorLoggingFullContextMatch(ast);
916
+
917
+ assert.ok(match);
918
+ assert.deepEqual(match?.primary_node, {
919
+ kind: 'member',
920
+ name: 'processPayment',
921
+ lines: [2],
922
+ });
923
+ assert.deepEqual(match?.related_nodes, [
924
+ { kind: 'call', name: 'logger.error', lines: [4] },
925
+ { kind: 'member', name: 'missing context', lines: [4] },
926
+ ]);
927
+ assert.deepEqual(match?.lines, [2, 4]);
928
+ assert.match(match?.why ?? '', /contexto completo/i);
929
+ assert.match(match?.impact ?? '', /requestId|traceId|userId/i);
930
+ assert.match(match?.expected_fix ?? '', /contexto|logger\.error/i);
931
+ });
932
+
933
+ test('hasCorrelationIdsPattern detecta logger con correlation ids y descarta logs sin contexto', () => {
934
+ const correlationAst = {
935
+ type: 'CallExpression',
936
+ callee: {
937
+ type: 'MemberExpression',
938
+ computed: false,
939
+ object: { type: 'Identifier', name: 'logger' },
940
+ property: { type: 'Identifier', name: 'info' },
941
+ },
942
+ arguments: [
943
+ {
944
+ type: 'ObjectExpression',
945
+ properties: [
946
+ {
947
+ type: 'ObjectProperty',
948
+ key: { type: 'Identifier', name: 'requestId' },
949
+ value: { type: 'Identifier', name: 'requestId' },
950
+ },
951
+ ],
952
+ },
953
+ ],
954
+ };
955
+ const plainAst = {
956
+ type: 'CallExpression',
957
+ callee: {
958
+ type: 'MemberExpression',
959
+ computed: false,
960
+ object: { type: 'Identifier', name: 'logger' },
961
+ property: { type: 'Identifier', name: 'info' },
962
+ },
963
+ arguments: [{ type: 'StringLiteral', value: 'ok' }],
964
+ };
965
+
966
+ assert.equal(hasCorrelationIdsPattern(correlationAst), true);
967
+ assert.equal(hasCorrelationIdsPattern(plainAst), false);
968
+ });
969
+
970
+ test('findCorrelationIdsMatch devuelve payload semantico para correlation ids', () => {
971
+ const ast = {
972
+ type: 'Program',
973
+ body: [
974
+ {
975
+ type: 'FunctionDeclaration',
976
+ id: { type: 'Identifier', name: 'trackRequest' },
977
+ loc: { start: { line: 2 }, end: { line: 8 } },
978
+ body: {
979
+ type: 'BlockStatement',
980
+ body: [
981
+ {
982
+ type: 'ExpressionStatement',
983
+ loc: { start: { line: 4 }, end: { line: 4 } },
984
+ expression: {
985
+ type: 'CallExpression',
986
+ loc: { start: { line: 4 }, end: { line: 4 } },
987
+ callee: {
988
+ type: 'MemberExpression',
989
+ computed: false,
990
+ object: { type: 'Identifier', name: 'logger' },
991
+ property: { type: 'Identifier', name: 'info' },
992
+ },
993
+ arguments: [
994
+ {
995
+ type: 'ObjectExpression',
996
+ properties: [
997
+ {
998
+ type: 'ObjectProperty',
999
+ key: { type: 'Identifier', name: 'requestId' },
1000
+ value: { type: 'Identifier', name: 'requestId' },
1001
+ },
1002
+ {
1003
+ type: 'ObjectProperty',
1004
+ key: { type: 'Identifier', name: 'traceId' },
1005
+ value: { type: 'Identifier', name: 'traceId' },
1006
+ },
1007
+ ],
1008
+ },
1009
+ ],
1010
+ },
1011
+ },
1012
+ ],
1013
+ },
1014
+ },
1015
+ ],
1016
+ };
1017
+
1018
+ const match = findCorrelationIdsMatch(ast);
1019
+
1020
+ assert.ok(match);
1021
+ assert.deepEqual(match?.primary_node, {
1022
+ kind: 'member',
1023
+ name: 'trackRequest',
1024
+ lines: [2],
1025
+ });
1026
+ assert.deepEqual(match?.related_nodes, [
1027
+ { kind: 'call', name: 'logger.info', lines: [4] },
1028
+ { kind: 'member', name: 'correlation context', lines: [4] },
1029
+ ]);
1030
+ assert.deepEqual(match?.lines, [2, 4]);
1031
+ assert.match(match?.why ?? '', /correlation IDs/i);
1032
+ assert.match(match?.impact ?? '', /requestId|traceId|correlationId/i);
1033
+ assert.match(match?.expected_fix ?? '', /requestId|traceId|correlationId/i);
1034
+ });
1035
+
1036
+ test('hasCorsConfiguredPattern detecta enableCors con orígenes permitidos y descarta comodines', () => {
1037
+ const configuredAst = {
1038
+ type: 'CallExpression',
1039
+ callee: {
1040
+ type: 'MemberExpression',
1041
+ computed: false,
1042
+ object: { type: 'Identifier', name: 'app' },
1043
+ property: { type: 'Identifier', name: 'enableCors' },
1044
+ },
1045
+ arguments: [
1046
+ {
1047
+ type: 'ObjectExpression',
1048
+ properties: [
1049
+ {
1050
+ type: 'ObjectProperty',
1051
+ key: { type: 'Identifier', name: 'origin' },
1052
+ value: {
1053
+ type: 'ArrayExpression',
1054
+ elements: [
1055
+ { type: 'StringLiteral', value: 'https://app.example.com' },
1056
+ { type: 'StringLiteral', value: 'https://admin.example.com' },
1057
+ ],
1058
+ },
1059
+ },
1060
+ ],
1061
+ },
1062
+ ],
1063
+ };
1064
+ const wildcardAst = {
1065
+ type: 'CallExpression',
1066
+ callee: {
1067
+ type: 'MemberExpression',
1068
+ computed: false,
1069
+ object: { type: 'Identifier', name: 'app' },
1070
+ property: { type: 'Identifier', name: 'enableCors' },
1071
+ },
1072
+ arguments: [
1073
+ {
1074
+ type: 'ObjectExpression',
1075
+ properties: [
1076
+ {
1077
+ type: 'ObjectProperty',
1078
+ key: { type: 'Identifier', name: 'origin' },
1079
+ value: { type: 'StringLiteral', value: '*' },
1080
+ },
1081
+ ],
1082
+ },
1083
+ ],
1084
+ };
1085
+
1086
+ assert.equal(hasCorsConfiguredPattern(configuredAst), true);
1087
+ assert.equal(hasCorsConfiguredPattern(wildcardAst), false);
1088
+ });
1089
+
1090
+ test('findCorsConfiguredMatch devuelve payload semantico para CORS configurado', () => {
1091
+ const ast = {
1092
+ type: 'Program',
1093
+ body: [
1094
+ {
1095
+ type: 'FunctionDeclaration',
1096
+ id: { type: 'Identifier', name: 'bootstrap' },
1097
+ loc: { start: { line: 2 }, end: { line: 9 } },
1098
+ body: {
1099
+ type: 'BlockStatement',
1100
+ body: [
1101
+ {
1102
+ type: 'ExpressionStatement',
1103
+ loc: { start: { line: 4 }, end: { line: 4 } },
1104
+ expression: {
1105
+ type: 'CallExpression',
1106
+ loc: { start: { line: 4 }, end: { line: 4 } },
1107
+ callee: {
1108
+ type: 'MemberExpression',
1109
+ computed: false,
1110
+ object: { type: 'Identifier', name: 'app' },
1111
+ property: { type: 'Identifier', name: 'enableCors' },
1112
+ },
1113
+ arguments: [
1114
+ {
1115
+ type: 'ObjectExpression',
1116
+ properties: [
1117
+ {
1118
+ type: 'ObjectProperty',
1119
+ key: { type: 'Identifier', name: 'origin' },
1120
+ value: {
1121
+ type: 'ArrayExpression',
1122
+ elements: [
1123
+ { type: 'StringLiteral', value: 'https://app.example.com' },
1124
+ { type: 'StringLiteral', value: 'https://admin.example.com' },
1125
+ ],
1126
+ },
1127
+ },
1128
+ ],
1129
+ },
1130
+ ],
1131
+ },
1132
+ },
1133
+ ],
1134
+ },
1135
+ },
1136
+ ],
1137
+ };
1138
+
1139
+ const match = findCorsConfiguredMatch(ast);
1140
+
1141
+ assert.ok(match);
1142
+ assert.deepEqual(match?.primary_node, {
1143
+ kind: 'member',
1144
+ name: 'bootstrap',
1145
+ lines: [2],
1146
+ });
1147
+ assert.deepEqual(match?.related_nodes, [
1148
+ { kind: 'call', name: 'app.enableCors', lines: [4] },
1149
+ { kind: 'member', name: 'allowed origins', lines: [4] },
1150
+ ]);
1151
+ assert.deepEqual(match?.lines, [2, 4]);
1152
+ assert.match(match?.why ?? '', /CORS/i);
1153
+ assert.match(match?.impact ?? '', /cross-origin|orígenes permitidos/i);
1154
+ assert.match(match?.expected_fix ?? '', /enableCors|origin/i);
1155
+ });
1156
+
1157
+ test('hasValidationPipeGlobalPattern detecta useGlobalPipes con whitelist y descarta configuraciones incompletas', () => {
1158
+ const configuredAst = {
1159
+ type: 'CallExpression',
1160
+ callee: {
1161
+ type: 'MemberExpression',
1162
+ computed: false,
1163
+ object: { type: 'Identifier', name: 'app' },
1164
+ property: { type: 'Identifier', name: 'useGlobalPipes' },
1165
+ },
1166
+ arguments: [
1167
+ {
1168
+ type: 'NewExpression',
1169
+ callee: { type: 'Identifier', name: 'ValidationPipe' },
1170
+ arguments: [
1171
+ {
1172
+ type: 'ObjectExpression',
1173
+ properties: [
1174
+ {
1175
+ type: 'ObjectProperty',
1176
+ key: { type: 'Identifier', name: 'whitelist' },
1177
+ value: { type: 'BooleanLiteral', value: true },
1178
+ },
1179
+ ],
1180
+ },
1181
+ ],
1182
+ },
1183
+ ],
1184
+ };
1185
+ const incompleteAst = {
1186
+ type: 'CallExpression',
1187
+ callee: {
1188
+ type: 'MemberExpression',
1189
+ computed: false,
1190
+ object: { type: 'Identifier', name: 'app' },
1191
+ property: { type: 'Identifier', name: 'useGlobalPipes' },
1192
+ },
1193
+ arguments: [
1194
+ {
1195
+ type: 'NewExpression',
1196
+ callee: { type: 'Identifier', name: 'ValidationPipe' },
1197
+ arguments: [{ type: 'ObjectExpression', properties: [] }],
1198
+ },
1199
+ ],
1200
+ };
1201
+
1202
+ assert.equal(hasValidationPipeGlobalPattern(configuredAst), true);
1203
+ assert.equal(hasValidationPipeGlobalPattern(incompleteAst), false);
1204
+ });
1205
+
1206
+ test('findValidationPipeGlobalMatch devuelve payload semantico para ValidationPipe global', () => {
1207
+ const ast = {
1208
+ type: 'Program',
1209
+ body: [
1210
+ {
1211
+ type: 'FunctionDeclaration',
1212
+ id: { type: 'Identifier', name: 'bootstrap' },
1213
+ loc: { start: { line: 2 }, end: { line: 9 } },
1214
+ body: {
1215
+ type: 'BlockStatement',
1216
+ body: [
1217
+ {
1218
+ type: 'ExpressionStatement',
1219
+ loc: { start: { line: 4 }, end: { line: 4 } },
1220
+ expression: {
1221
+ type: 'CallExpression',
1222
+ loc: { start: { line: 4 }, end: { line: 4 } },
1223
+ callee: {
1224
+ type: 'MemberExpression',
1225
+ computed: false,
1226
+ object: { type: 'Identifier', name: 'app' },
1227
+ property: { type: 'Identifier', name: 'useGlobalPipes' },
1228
+ },
1229
+ arguments: [
1230
+ {
1231
+ type: 'NewExpression',
1232
+ callee: { type: 'Identifier', name: 'ValidationPipe' },
1233
+ arguments: [
1234
+ {
1235
+ type: 'ObjectExpression',
1236
+ properties: [
1237
+ {
1238
+ type: 'ObjectProperty',
1239
+ key: { type: 'Identifier', name: 'whitelist' },
1240
+ value: { type: 'BooleanLiteral', value: true },
1241
+ },
1242
+ ],
1243
+ },
1244
+ ],
1245
+ },
1246
+ ],
1247
+ },
1248
+ },
1249
+ ],
1250
+ },
1251
+ },
1252
+ ],
1253
+ };
1254
+
1255
+ const match = findValidationPipeGlobalMatch(ast);
1256
+
1257
+ assert.ok(match);
1258
+ assert.deepEqual(match?.primary_node, {
1259
+ kind: 'member',
1260
+ name: 'bootstrap',
1261
+ lines: [2],
1262
+ });
1263
+ assert.deepEqual(match?.related_nodes, [
1264
+ { kind: 'call', name: 'app.useGlobalPipes', lines: [4] },
1265
+ { kind: 'member', name: 'whitelist: true', lines: [4] },
1266
+ ]);
1267
+ assert.deepEqual(match?.lines, [2, 4]);
1268
+ assert.match(match?.why ?? '', /ValidationPipe/i);
1269
+ assert.match(match?.impact ?? '', /whitelist/i);
1270
+ assert.match(match?.expected_fix ?? '', /ValidationPipe|whitelist/i);
1271
+ });
1272
+
1273
+ test('hasValidationConfigPattern detecta ConfigModule con validacion de env y descarta forRoot incompletos', () => {
1274
+ const configuredAst = {
1275
+ type: 'CallExpression',
1276
+ callee: {
1277
+ type: 'MemberExpression',
1278
+ computed: false,
1279
+ object: { type: 'Identifier', name: 'ConfigModule' },
1280
+ property: { type: 'Identifier', name: 'forRoot' },
1281
+ },
1282
+ arguments: [
1283
+ {
1284
+ type: 'ObjectExpression',
1285
+ properties: [
1286
+ {
1287
+ type: 'ObjectProperty',
1288
+ key: { type: 'Identifier', name: 'validationSchema' },
1289
+ value: {
1290
+ type: 'CallExpression',
1291
+ callee: {
1292
+ type: 'MemberExpression',
1293
+ computed: false,
1294
+ object: { type: 'Identifier', name: 'Joi' },
1295
+ property: { type: 'Identifier', name: 'object' },
1296
+ },
1297
+ arguments: [],
1298
+ },
1299
+ },
1300
+ ],
1301
+ },
1302
+ ],
1303
+ };
1304
+ const incompleteAst = {
1305
+ type: 'CallExpression',
1306
+ callee: {
1307
+ type: 'MemberExpression',
1308
+ computed: false,
1309
+ object: { type: 'Identifier', name: 'ConfigModule' },
1310
+ property: { type: 'Identifier', name: 'forRoot' },
1311
+ },
1312
+ arguments: [{ type: 'ObjectExpression', properties: [] }],
1313
+ };
1314
+
1315
+ assert.equal(hasValidationConfigPattern(configuredAst), true);
1316
+ assert.equal(hasValidationConfigPattern(incompleteAst), false);
1317
+ });
1318
+
1319
+ test('findValidationConfigMatch devuelve payload semantico para ConfigModule validation', () => {
1320
+ const ast = {
1321
+ type: 'Program',
1322
+ body: [
1323
+ {
1324
+ type: 'FunctionDeclaration',
1325
+ id: { type: 'Identifier', name: 'bootstrap' },
1326
+ loc: { start: { line: 1 }, end: { line: 10 } },
1327
+ body: {
1328
+ type: 'BlockStatement',
1329
+ body: [
1330
+ {
1331
+ type: 'ExpressionStatement',
1332
+ loc: { start: { line: 3 }, end: { line: 3 } },
1333
+ expression: {
1334
+ type: 'CallExpression',
1335
+ loc: { start: { line: 3 }, end: { line: 3 } },
1336
+ callee: {
1337
+ type: 'MemberExpression',
1338
+ computed: false,
1339
+ object: { type: 'Identifier', name: 'ConfigModule' },
1340
+ property: { type: 'Identifier', name: 'forRoot' },
1341
+ },
1342
+ arguments: [
1343
+ {
1344
+ type: 'ObjectExpression',
1345
+ properties: [
1346
+ {
1347
+ type: 'ObjectProperty',
1348
+ key: { type: 'Identifier', name: 'validationSchema' },
1349
+ value: {
1350
+ type: 'CallExpression',
1351
+ callee: {
1352
+ type: 'MemberExpression',
1353
+ computed: false,
1354
+ object: { type: 'Identifier', name: 'Joi' },
1355
+ property: { type: 'Identifier', name: 'object' },
1356
+ },
1357
+ arguments: [],
1358
+ },
1359
+ },
1360
+ ],
1361
+ },
1362
+ ],
1363
+ },
1364
+ },
1365
+ ],
1366
+ },
1367
+ },
1368
+ ],
1369
+ };
1370
+
1371
+ const match = findValidationConfigMatch(ast);
1372
+
1373
+ assert.ok(match);
1374
+ assert.deepEqual(match?.primary_node, {
1375
+ kind: 'member',
1376
+ name: 'bootstrap',
1377
+ lines: [1],
1378
+ });
1379
+ assert.deepEqual(match?.related_nodes, [
1380
+ { kind: 'call', name: 'ConfigModule.forRoot', lines: [3] },
1381
+ { kind: 'member', name: 'validationSchema / validate', lines: [3] },
1382
+ ]);
1383
+ assert.deepEqual(match?.lines, [1, 3]);
1384
+ assert.match(match?.why ?? '', /ConfigModule/i);
1385
+ assert.match(match?.impact ?? '', /variables inválidas|faltantes/i);
1386
+ assert.match(match?.expected_fix ?? '', /validationSchema|validate/i);
1387
+ });
1388
+
1389
+ test('hasClassValidatorDecoratorsPattern detecta decoradores class-validator y descarta decoradores no relacionados', () => {
1390
+ const dtoAst = {
1391
+ type: 'ClassDeclaration',
1392
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
1393
+ body: {
1394
+ type: 'ClassBody',
1395
+ body: [
1396
+ {
1397
+ type: 'ClassProperty',
1398
+ key: { type: 'Identifier', name: 'userEmail' },
1399
+ decorators: [
1400
+ {
1401
+ type: 'Decorator',
1402
+ expression: {
1403
+ type: 'CallExpression',
1404
+ callee: { type: 'Identifier', name: 'IsEmail' },
1405
+ arguments: [],
1406
+ },
1407
+ },
1408
+ ],
1409
+ },
1410
+ ],
1411
+ },
1412
+ };
1413
+ const unrelatedAst = {
1414
+ type: 'ClassDeclaration',
1415
+ id: { type: 'Identifier', name: 'OrderService' },
1416
+ body: {
1417
+ type: 'ClassBody',
1418
+ body: [
1419
+ {
1420
+ type: 'ClassMethod',
1421
+ key: { type: 'Identifier', name: 'save' },
1422
+ decorators: [
1423
+ {
1424
+ type: 'Decorator',
1425
+ expression: {
1426
+ type: 'CallExpression',
1427
+ callee: { type: 'Identifier', name: 'Injectable' },
1428
+ arguments: [],
1429
+ },
1430
+ },
1431
+ ],
1432
+ },
1433
+ ],
1434
+ },
1435
+ };
1436
+
1437
+ assert.equal(hasClassValidatorDecoratorsPattern(dtoAst), true);
1438
+ assert.equal(hasClassValidatorDecoratorsPattern(unrelatedAst), false);
1439
+ });
1440
+
1441
+ test('findClassValidatorDecoratorsMatch devuelve payload semantico para DTOs con class-validator', () => {
1442
+ const ast = {
1443
+ type: 'Program',
1444
+ body: [
1445
+ {
1446
+ type: 'ClassDeclaration',
1447
+ id: { type: 'Identifier', name: 'CreateUserDto' },
1448
+ loc: { start: { line: 3 }, end: { line: 16 } },
1449
+ body: {
1450
+ type: 'ClassBody',
1451
+ body: [
1452
+ {
1453
+ type: 'ClassProperty',
1454
+ key: { type: 'Identifier', name: 'email' },
1455
+ loc: { start: { line: 6 }, end: { line: 6 } },
1456
+ decorators: [
1457
+ {
1458
+ type: 'Decorator',
1459
+ loc: { start: { line: 6 }, end: { line: 6 } },
1460
+ expression: {
1461
+ type: 'CallExpression',
1462
+ callee: { type: 'Identifier', name: 'IsEmail' },
1463
+ arguments: [],
1464
+ },
1465
+ },
1466
+ ],
1467
+ },
1468
+ ],
1469
+ },
1470
+ },
1471
+ ],
1472
+ };
1473
+
1474
+ const match = findClassValidatorDecoratorsMatch(ast);
1475
+
1476
+ assert.ok(match);
1477
+ assert.deepEqual(match?.primary_node, {
1478
+ kind: 'class',
1479
+ name: 'CreateUserDto',
1480
+ lines: [3],
1481
+ });
1482
+ assert.deepEqual(match?.related_nodes, [
1483
+ { kind: 'member', name: 'email', lines: [6] },
1484
+ { kind: 'member', name: '@IsEmail', lines: [6] },
1485
+ ]);
1486
+ assert.deepEqual(match?.lines, [3, 6]);
1487
+ assert.match(match?.why ?? '', /class-validator/i);
1488
+ assert.match(match?.impact ?? '', /inv[aá]lidos|runtime/i);
1489
+ assert.match(match?.expected_fix ?? '', /IsString|IsEmail|Min|Max/i);
1490
+ });
1491
+
1492
+ test('hasClassTransformerDecoratorsPattern detecta decoradores class-transformer y descarta decoradores no relacionados', () => {
1493
+ const dtoAst = {
1494
+ type: 'ClassDeclaration',
1495
+ id: { type: 'Identifier', name: 'UserResponseDto' },
1496
+ body: {
1497
+ type: 'ClassBody',
1498
+ body: [
1499
+ {
1500
+ type: 'ClassProperty',
1501
+ key: { type: 'Identifier', name: 'password' },
1502
+ decorators: [
1503
+ {
1504
+ type: 'Decorator',
1505
+ expression: {
1506
+ type: 'CallExpression',
1507
+ callee: { type: 'Identifier', name: 'Exclude' },
1508
+ arguments: [],
1509
+ },
1510
+ },
1511
+ ],
1512
+ },
1513
+ ],
1514
+ },
1515
+ };
1516
+ const unrelatedAst = {
1517
+ type: 'ClassDeclaration',
1518
+ id: { type: 'Identifier', name: 'UserResponseDto' },
1519
+ body: {
1520
+ type: 'ClassBody',
1521
+ body: [
1522
+ {
1523
+ type: 'ClassProperty',
1524
+ key: { type: 'Identifier', name: 'password' },
1525
+ decorators: [
1526
+ {
1527
+ type: 'Decorator',
1528
+ expression: {
1529
+ type: 'CallExpression',
1530
+ callee: { type: 'Identifier', name: 'IsString' },
1531
+ arguments: [],
1532
+ },
1533
+ },
1534
+ ],
1535
+ },
1536
+ ],
1537
+ },
1538
+ };
1539
+
1540
+ assert.equal(hasClassTransformerDecoratorsPattern(dtoAst), true);
1541
+ assert.equal(hasClassTransformerDecoratorsPattern(unrelatedAst), false);
1542
+ });
1543
+
1544
+ test('findClassTransformerDecoratorsMatch devuelve payload semantico para DTOs con class-transformer', () => {
1545
+ const ast = {
1546
+ type: 'Program',
1547
+ body: [
1548
+ {
1549
+ type: 'ClassDeclaration',
1550
+ id: { type: 'Identifier', name: 'UserResponseDto' },
1551
+ loc: { start: { line: 4 }, end: { line: 14 } },
1552
+ body: {
1553
+ type: 'ClassBody',
1554
+ body: [
1555
+ {
1556
+ type: 'ClassProperty',
1557
+ key: { type: 'Identifier', name: 'internalNotes' },
1558
+ loc: { start: { line: 8 }, end: { line: 8 } },
1559
+ decorators: [
1560
+ {
1561
+ type: 'Decorator',
1562
+ loc: { start: { line: 8 }, end: { line: 8 } },
1563
+ expression: {
1564
+ type: 'CallExpression',
1565
+ callee: { type: 'Identifier', name: 'Exclude' },
1566
+ arguments: [],
1567
+ },
1568
+ },
1569
+ ],
1570
+ },
1571
+ ],
1572
+ },
1573
+ },
1574
+ ],
1575
+ };
1576
+
1577
+ const match = findClassTransformerDecoratorsMatch(ast);
1578
+
1579
+ assert.ok(match);
1580
+ assert.deepEqual(match?.primary_node, {
1581
+ kind: 'class',
1582
+ name: 'UserResponseDto',
1583
+ lines: [4],
1584
+ });
1585
+ assert.deepEqual(match?.related_nodes, [
1586
+ { kind: 'member', name: 'internalNotes', lines: [8] },
1587
+ { kind: 'member', name: '@Exclude', lines: [8] },
1588
+ ]);
1589
+ assert.deepEqual(match?.lines, [4, 8]);
1590
+ assert.match(match?.why ?? '', /class-transformer/i);
1591
+ assert.match(match?.impact ?? '', /transformaci[oó]n|exposici[oó]n|filtrar/i);
1592
+ assert.match(match?.expected_fix ?? '', /Transform|Exclude|Expose/i);
1593
+ });
1594
+
1595
+ test('hasDtoBoundaryPattern detecta DTOs en boundaries y descarta DTOs decorados', () => {
1596
+ const dtoAst = {
1597
+ type: 'ClassDeclaration',
1598
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
1599
+ body: {
1600
+ type: 'ClassBody',
1601
+ body: [
1602
+ {
1603
+ type: 'ClassProperty',
1604
+ key: { type: 'Identifier', name: 'userId' },
1605
+ },
1606
+ ],
1607
+ },
1608
+ };
1609
+ const decoratedDtoAst = {
1610
+ type: 'ClassDeclaration',
1611
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
1612
+ body: {
1613
+ type: 'ClassBody',
1614
+ body: [
1615
+ {
1616
+ type: 'ClassProperty',
1617
+ key: { type: 'Identifier', name: 'userId' },
1618
+ decorators: [
1619
+ {
1620
+ type: 'Decorator',
1621
+ expression: {
1622
+ type: 'CallExpression',
1623
+ callee: { type: 'Identifier', name: 'IsString' },
1624
+ arguments: [],
1625
+ },
1626
+ },
1627
+ ],
1628
+ },
1629
+ ],
1630
+ },
1631
+ };
1632
+
1633
+ assert.equal(hasDtoBoundaryPattern(dtoAst), true);
1634
+ assert.equal(hasDtoBoundaryPattern(decoratedDtoAst), false);
1635
+ });
1636
+
1637
+ test('findDtoBoundaryMatch devuelve payload semantico para DTOs en boundaries', () => {
1638
+ const ast = {
1639
+ type: 'Program',
1640
+ body: [
1641
+ {
1642
+ type: 'ClassDeclaration',
1643
+ id: { type: 'Identifier', name: 'CreateUserDto' },
1644
+ loc: { start: { line: 3 }, end: { line: 9 } },
1645
+ body: {
1646
+ type: 'ClassBody',
1647
+ body: [
1648
+ {
1649
+ type: 'ClassProperty',
1650
+ key: { type: 'Identifier', name: 'email' },
1651
+ loc: { start: { line: 5 }, end: { line: 5 } },
1652
+ },
1653
+ {
1654
+ type: 'ClassProperty',
1655
+ key: { type: 'Identifier', name: 'name' },
1656
+ loc: { start: { line: 6 }, end: { line: 6 } },
1657
+ },
1658
+ ],
1659
+ },
1660
+ },
1661
+ ],
1662
+ };
1663
+
1664
+ const match = findDtoBoundaryMatch(ast);
1665
+
1666
+ assert.ok(match);
1667
+ assert.deepEqual(match?.primary_node, {
1668
+ kind: 'class',
1669
+ name: 'CreateUserDto',
1670
+ lines: [3],
1671
+ });
1672
+ assert.deepEqual(match?.related_nodes, [
1673
+ { kind: 'member', name: 'email', lines: [5] },
1674
+ { kind: 'member', name: 'name', lines: [6] },
1675
+ ]);
1676
+ assert.deepEqual(match?.lines, [3, 5, 6]);
1677
+ assert.match(match?.why ?? '', /boundary|DTO/i);
1678
+ assert.match(match?.impact ?? '', /contratos ambiguos|serializaci[oó]n/i);
1679
+ assert.match(match?.expected_fix ?? '', /entrada|salida|DTO/i);
1680
+ });
1681
+
1682
+ test('hasBackendCriticalTransactionsUsage detecta transacciones criticas y descarta llamadas sin transaccion', () => {
1683
+ const criticalTransactionAst = {
1684
+ type: 'Program',
1685
+ body: [
1686
+ {
1687
+ type: 'ExpressionStatement',
1688
+ loc: { start: { line: 4 }, end: { line: 8 } },
1689
+ expression: {
1690
+ type: 'CallExpression',
1691
+ loc: { start: { line: 4 }, end: { line: 8 } },
1692
+ callee: {
1693
+ type: 'MemberExpression',
1694
+ object: { type: 'Identifier', name: 'dataSource' },
1695
+ property: { type: 'Identifier', name: 'transaction' },
1696
+ },
1697
+ arguments: [
1698
+ {
1699
+ type: 'ArrowFunctionExpression',
1700
+ loc: { start: { line: 4 }, end: { line: 8 } },
1701
+ body: { type: 'BlockStatement', body: [] },
1702
+ },
1703
+ ],
1704
+ },
1705
+ },
1706
+ ],
1707
+ };
1708
+ const noTransactionAst = {
1709
+ type: 'Program',
1710
+ body: [
1711
+ {
1712
+ type: 'ExpressionStatement',
1713
+ loc: { start: { line: 4 }, end: { line: 4 } },
1714
+ expression: {
1715
+ type: 'CallExpression',
1716
+ loc: { start: { line: 4 }, end: { line: 4 } },
1717
+ callee: {
1718
+ type: 'MemberExpression',
1719
+ object: { type: 'Identifier', name: 'dataSource' },
1720
+ property: { type: 'Identifier', name: 'query' },
1721
+ },
1722
+ arguments: [],
1723
+ },
1724
+ },
1725
+ ],
1726
+ };
1727
+
1728
+ assert.equal(hasBackendCriticalTransactionsUsage(criticalTransactionAst), true);
1729
+ assert.equal(hasBackendCriticalTransactionsUsage(noTransactionAst), false);
1730
+ });
1731
+
1732
+ test('findBackendCriticalTransactionsLines devuelve la linea de la transaccion critica', () => {
1733
+ const ast = {
1734
+ type: 'Program',
1735
+ body: [
1736
+ {
1737
+ type: 'ExpressionStatement',
1738
+ loc: { start: { line: 4 }, end: { line: 8 } },
1739
+ expression: {
1740
+ type: 'CallExpression',
1741
+ loc: { start: { line: 4 }, end: { line: 8 } },
1742
+ callee: {
1743
+ type: 'MemberExpression',
1744
+ object: { type: 'Identifier', name: 'dataSource' },
1745
+ property: { type: 'Identifier', name: 'transaction' },
1746
+ },
1747
+ arguments: [
1748
+ {
1749
+ type: 'ArrowFunctionExpression',
1750
+ loc: { start: { line: 4 }, end: { line: 8 } },
1751
+ body: { type: 'BlockStatement', body: [] },
1752
+ },
1753
+ ],
1754
+ },
1755
+ },
1756
+ ],
1757
+ };
1758
+
1759
+ assert.deepEqual(findBackendCriticalTransactionsLines(ast), [4]);
1760
+ assert.ok(findBackendCriticalTransactionsMatch(ast));
1761
+ });
1762
+
1763
+ test('hasBackendMultiTableTransactionsUsage detecta transacciones multi-tabla y descarta una sola escritura', () => {
1764
+ const multiTableTransactionAst = {
1765
+ type: 'Program',
1766
+ body: [
1767
+ {
1768
+ type: 'ExpressionStatement',
1769
+ loc: { start: { line: 4 }, end: { line: 9 } },
1770
+ expression: {
1771
+ type: 'CallExpression',
1772
+ loc: { start: { line: 4 }, end: { line: 9 } },
1773
+ callee: {
1774
+ type: 'MemberExpression',
1775
+ object: { type: 'Identifier', name: 'dataSource' },
1776
+ property: { type: 'Identifier', name: 'transaction' },
1777
+ },
1778
+ arguments: [
1779
+ {
1780
+ type: 'ArrowFunctionExpression',
1781
+ loc: { start: { line: 4 }, end: { line: 9 } },
1782
+ body: {
1783
+ type: 'BlockStatement',
1784
+ body: [
1785
+ {
1786
+ type: 'ExpressionStatement',
1787
+ loc: { start: { line: 5 }, end: { line: 5 } },
1788
+ expression: {
1789
+ type: 'CallExpression',
1790
+ loc: { start: { line: 5 }, end: { line: 5 } },
1791
+ callee: {
1792
+ type: 'MemberExpression',
1793
+ object: { type: 'Identifier', name: 'manager' },
1794
+ property: { type: 'Identifier', name: 'save' },
1795
+ },
1796
+ arguments: [],
1797
+ },
1798
+ },
1799
+ {
1800
+ type: 'ExpressionStatement',
1801
+ loc: { start: { line: 6 }, end: { line: 6 } },
1802
+ expression: {
1803
+ type: 'CallExpression',
1804
+ loc: { start: { line: 6 }, end: { line: 6 } },
1805
+ callee: {
1806
+ type: 'MemberExpression',
1807
+ object: { type: 'Identifier', name: 'manager' },
1808
+ property: { type: 'Identifier', name: 'update' },
1809
+ },
1810
+ arguments: [],
1811
+ },
1812
+ },
1813
+ ],
1814
+ },
1815
+ },
1816
+ ],
1817
+ },
1818
+ },
1819
+ ],
1820
+ };
1821
+ const singleWriteTransactionAst = {
1822
+ type: 'Program',
1823
+ body: [
1824
+ {
1825
+ type: 'ExpressionStatement',
1826
+ loc: { start: { line: 4 }, end: { line: 8 } },
1827
+ expression: {
1828
+ type: 'CallExpression',
1829
+ loc: { start: { line: 4 }, end: { line: 8 } },
1830
+ callee: {
1831
+ type: 'MemberExpression',
1832
+ object: { type: 'Identifier', name: 'dataSource' },
1833
+ property: { type: 'Identifier', name: 'transaction' },
1834
+ },
1835
+ arguments: [
1836
+ {
1837
+ type: 'ArrowFunctionExpression',
1838
+ loc: { start: { line: 4 }, end: { line: 8 } },
1839
+ body: {
1840
+ type: 'BlockStatement',
1841
+ body: [
1842
+ {
1843
+ type: 'ExpressionStatement',
1844
+ loc: { start: { line: 5 }, end: { line: 5 } },
1845
+ expression: {
1846
+ type: 'CallExpression',
1847
+ loc: { start: { line: 5 }, end: { line: 5 } },
1848
+ callee: {
1849
+ type: 'MemberExpression',
1850
+ object: { type: 'Identifier', name: 'manager' },
1851
+ property: { type: 'Identifier', name: 'save' },
1852
+ },
1853
+ arguments: [],
1854
+ },
1855
+ },
1856
+ ],
1857
+ },
1858
+ },
1859
+ ],
1860
+ },
1861
+ },
1862
+ ],
1863
+ };
1864
+
1865
+ assert.equal(hasBackendMultiTableTransactionsUsage(multiTableTransactionAst), true);
1866
+ assert.equal(hasBackendMultiTableTransactionsUsage(singleWriteTransactionAst), false);
1867
+ });
1868
+
1869
+ test('findBackendMultiTableTransactionsLines devuelve la transaccion y las escrituras relacionadas', () => {
1870
+ const ast = {
1871
+ type: 'Program',
1872
+ body: [
1873
+ {
1874
+ type: 'ExpressionStatement',
1875
+ loc: { start: { line: 4 }, end: { line: 9 } },
1876
+ expression: {
1877
+ type: 'CallExpression',
1878
+ loc: { start: { line: 4 }, end: { line: 9 } },
1879
+ callee: {
1880
+ type: 'MemberExpression',
1881
+ object: { type: 'Identifier', name: 'dataSource' },
1882
+ property: { type: 'Identifier', name: 'transaction' },
1883
+ },
1884
+ arguments: [
1885
+ {
1886
+ type: 'ArrowFunctionExpression',
1887
+ loc: { start: { line: 4 }, end: { line: 9 } },
1888
+ body: {
1889
+ type: 'BlockStatement',
1890
+ body: [
1891
+ {
1892
+ type: 'ExpressionStatement',
1893
+ loc: { start: { line: 5 }, end: { line: 5 } },
1894
+ expression: {
1895
+ type: 'CallExpression',
1896
+ loc: { start: { line: 5 }, end: { line: 5 } },
1897
+ callee: {
1898
+ type: 'MemberExpression',
1899
+ object: { type: 'Identifier', name: 'manager' },
1900
+ property: { type: 'Identifier', name: 'save' },
1901
+ },
1902
+ arguments: [],
1903
+ },
1904
+ },
1905
+ {
1906
+ type: 'ExpressionStatement',
1907
+ loc: { start: { line: 6 }, end: { line: 6 } },
1908
+ expression: {
1909
+ type: 'CallExpression',
1910
+ loc: { start: { line: 6 }, end: { line: 6 } },
1911
+ callee: {
1912
+ type: 'MemberExpression',
1913
+ object: { type: 'Identifier', name: 'manager' },
1914
+ property: { type: 'Identifier', name: 'update' },
1915
+ },
1916
+ arguments: [],
1917
+ },
1918
+ },
1919
+ ],
1920
+ },
1921
+ },
1922
+ ],
1923
+ },
1924
+ },
1925
+ ],
1926
+ };
1927
+
1928
+ assert.deepEqual(findBackendMultiTableTransactionsLines(ast), [4, 5, 6]);
1929
+ assert.ok(findBackendMultiTableTransactionsMatch(ast));
1930
+ });
1931
+
1932
+ test('hasApiVersioningPattern detecta controllers versionados y descarta controllers sin version', () => {
1933
+ const versionedAst = {
1934
+ type: 'Program',
1935
+ body: [
1936
+ {
1937
+ type: 'ClassDeclaration',
1938
+ id: { type: 'Identifier', name: 'OrdersController' },
1939
+ decorators: [
1940
+ {
1941
+ type: 'Decorator',
1942
+ expression: {
1943
+ type: 'CallExpression',
1944
+ callee: { type: 'Identifier', name: 'Controller' },
1945
+ arguments: [
1946
+ {
1947
+ type: 'ObjectExpression',
1948
+ properties: [
1949
+ {
1950
+ type: 'ObjectProperty',
1951
+ key: { type: 'Identifier', name: 'path' },
1952
+ value: { type: 'StringLiteral', value: 'orders' },
1953
+ },
1954
+ {
1955
+ type: 'ObjectProperty',
1956
+ key: { type: 'Identifier', name: 'version' },
1957
+ value: { type: 'StringLiteral', value: '1' },
1958
+ },
1959
+ ],
1960
+ },
1961
+ ],
1962
+ },
1963
+ },
1964
+ ],
1965
+ body: {
1966
+ type: 'ClassBody',
1967
+ body: [
1968
+ {
1969
+ type: 'ClassMethod',
1970
+ key: { type: 'Identifier', name: 'list' },
1971
+ body: { type: 'BlockStatement', body: [] },
1972
+ },
1973
+ ],
1974
+ },
1975
+ },
1976
+ ],
1977
+ };
1978
+ const unversionedAst = {
1979
+ type: 'Program',
1980
+ body: [
1981
+ {
1982
+ type: 'ClassDeclaration',
1983
+ id: { type: 'Identifier', name: 'OrdersController' },
1984
+ decorators: [
1985
+ {
1986
+ type: 'Decorator',
1987
+ expression: {
1988
+ type: 'CallExpression',
1989
+ callee: { type: 'Identifier', name: 'Controller' },
1990
+ arguments: [{ type: 'StringLiteral', value: 'orders' }],
1991
+ },
1992
+ },
1993
+ ],
1994
+ body: {
1995
+ type: 'ClassBody',
1996
+ body: [
1997
+ {
1998
+ type: 'ClassMethod',
1999
+ key: { type: 'Identifier', name: 'list' },
2000
+ body: { type: 'BlockStatement', body: [] },
2001
+ },
2002
+ ],
2003
+ },
2004
+ },
2005
+ ],
2006
+ };
2007
+
2008
+ assert.equal(hasApiVersioningPattern(versionedAst), true);
2009
+ assert.equal(hasApiVersioningPattern(unversionedAst), false);
2010
+ });
2011
+
2012
+ test('findApiVersioningMatch devuelve payload semantico para controllers versionados', () => {
2013
+ const ast = {
2014
+ type: 'Program',
2015
+ body: [
2016
+ {
2017
+ type: 'ClassDeclaration',
2018
+ id: { type: 'Identifier', name: 'OrdersController' },
2019
+ loc: { start: { line: 3 }, end: { line: 12 } },
2020
+ body: {
2021
+ type: 'ClassBody',
2022
+ body: [
2023
+ {
2024
+ type: 'ClassMethod',
2025
+ key: { type: 'Identifier', name: 'list' },
2026
+ loc: { start: { line: 5 }, end: { line: 8 } },
2027
+ decorators: [
2028
+ {
2029
+ type: 'Decorator',
2030
+ loc: { start: { line: 5 }, end: { line: 5 } },
2031
+ expression: {
2032
+ type: 'CallExpression',
2033
+ callee: { type: 'Identifier', name: 'Version' },
2034
+ arguments: [{ type: 'StringLiteral', value: '2' }],
2035
+ },
2036
+ },
2037
+ ],
2038
+ body: { type: 'BlockStatement', body: [] },
2039
+ },
2040
+ ],
2041
+ },
2042
+ },
2043
+ ],
2044
+ };
2045
+
2046
+ const match = findApiVersioningMatch(ast);
2047
+
2048
+ assert.ok(match);
2049
+ assert.deepEqual(match?.primary_node, {
2050
+ kind: 'class',
2051
+ name: 'OrdersController',
2052
+ lines: [3],
2053
+ });
2054
+ assert.deepEqual(match?.related_nodes, [
2055
+ { kind: 'member', name: 'list', lines: [5] },
2056
+ { kind: 'member', name: '@Version', lines: [5] },
2057
+ { kind: 'member', name: 'v2', lines: [5] },
2058
+ ]);
2059
+ assert.deepEqual(match?.lines, [3, 5]);
2060
+ assert.match(match?.why ?? '', /versionadas|v1|v2/i);
2061
+ assert.match(match?.impact ?? '', /contratos de API|consumidores/i);
2062
+ assert.match(match?.expected_fix ?? '', /@Version|version:|\/api\/v1|\/api\/v2/i);
2063
+ });
2064
+
2065
+ test('hasInputValidationPattern detecta DTOs de entrada en controllers y descarta parametros primitivos', () => {
2066
+ const dtoAst = {
2067
+ type: 'Program',
2068
+ body: [
2069
+ {
2070
+ type: 'ClassDeclaration',
2071
+ id: { type: 'Identifier', name: 'OrdersController' },
2072
+ body: {
2073
+ type: 'ClassBody',
2074
+ body: [
2075
+ {
2076
+ type: 'ClassMethod',
2077
+ key: { type: 'Identifier', name: 'create' },
2078
+ params: [
2079
+ {
2080
+ type: 'Identifier',
2081
+ name: 'createOrderDto',
2082
+ decorators: [
2083
+ {
2084
+ type: 'Decorator',
2085
+ expression: {
2086
+ type: 'CallExpression',
2087
+ callee: { type: 'Identifier', name: 'Body' },
2088
+ arguments: [],
2089
+ },
2090
+ },
2091
+ ],
2092
+ typeAnnotation: {
2093
+ type: 'TSTypeAnnotation',
2094
+ typeAnnotation: {
2095
+ type: 'TSTypeReference',
2096
+ typeName: { type: 'Identifier', name: 'CreateOrderDto' },
2097
+ },
2098
+ },
2099
+ },
2100
+ ],
2101
+ body: { type: 'BlockStatement', body: [] },
2102
+ },
2103
+ ],
2104
+ },
2105
+ },
2106
+ ],
2107
+ };
2108
+ const primitiveAst = {
2109
+ type: 'Program',
2110
+ body: [
2111
+ {
2112
+ type: 'ClassDeclaration',
2113
+ id: { type: 'Identifier', name: 'OrdersController' },
2114
+ body: {
2115
+ type: 'ClassBody',
2116
+ body: [
2117
+ {
2118
+ type: 'ClassMethod',
2119
+ key: { type: 'Identifier', name: 'findById' },
2120
+ params: [
2121
+ {
2122
+ type: 'Identifier',
2123
+ name: 'id',
2124
+ decorators: [
2125
+ {
2126
+ type: 'Decorator',
2127
+ expression: {
2128
+ type: 'CallExpression',
2129
+ callee: { type: 'Identifier', name: 'Param' },
2130
+ arguments: [],
2131
+ },
2132
+ },
2133
+ ],
2134
+ typeAnnotation: {
2135
+ type: 'TSTypeAnnotation',
2136
+ typeAnnotation: { type: 'TSStringKeyword' },
2137
+ },
2138
+ },
2139
+ ],
2140
+ body: { type: 'BlockStatement', body: [] },
2141
+ },
2142
+ ],
2143
+ },
2144
+ },
2145
+ ],
2146
+ };
2147
+
2148
+ assert.equal(hasInputValidationPattern(dtoAst), true);
2149
+ assert.equal(hasInputValidationPattern(primitiveAst), false);
2150
+ });
2151
+
2152
+ test('findInputValidationMatch devuelve payload semantico para DTOs de entrada en controllers', () => {
2153
+ const ast = {
2154
+ type: 'Program',
2155
+ body: [
2156
+ {
2157
+ type: 'ClassDeclaration',
2158
+ id: { type: 'Identifier', name: 'OrdersController' },
2159
+ loc: { start: { line: 3 }, end: { line: 16 } },
2160
+ body: {
2161
+ type: 'ClassBody',
2162
+ body: [
2163
+ {
2164
+ type: 'ClassMethod',
2165
+ key: { type: 'Identifier', name: 'create' },
2166
+ loc: { start: { line: 5 }, end: { line: 11 } },
2167
+ params: [
2168
+ {
2169
+ type: 'Identifier',
2170
+ name: 'createOrderDto',
2171
+ loc: { start: { line: 6 }, end: { line: 6 } },
2172
+ decorators: [
2173
+ {
2174
+ type: 'Decorator',
2175
+ loc: { start: { line: 6 }, end: { line: 6 } },
2176
+ expression: {
2177
+ type: 'CallExpression',
2178
+ callee: { type: 'Identifier', name: 'Body' },
2179
+ arguments: [],
2180
+ },
2181
+ },
2182
+ ],
2183
+ typeAnnotation: {
2184
+ type: 'TSTypeAnnotation',
2185
+ typeAnnotation: {
2186
+ type: 'TSTypeReference',
2187
+ typeName: { type: 'Identifier', name: 'CreateOrderDto' },
2188
+ },
2189
+ },
2190
+ },
2191
+ ],
2192
+ body: { type: 'BlockStatement', body: [] },
2193
+ },
2194
+ ],
2195
+ },
2196
+ },
2197
+ ],
2198
+ };
2199
+
2200
+ const match = findInputValidationMatch(ast);
2201
+
2202
+ assert.ok(match);
2203
+ assert.deepEqual(match?.primary_node, {
2204
+ kind: 'class',
2205
+ name: 'OrdersController',
2206
+ lines: [3],
2207
+ });
2208
+ assert.deepEqual(match?.related_nodes, [
2209
+ { kind: 'member', name: 'create', lines: [5] },
2210
+ { kind: 'member', name: 'createOrderDto', lines: [6] },
2211
+ { kind: 'member', name: '@Body', lines: [6] },
2212
+ { kind: 'member', name: 'CreateOrderDto', lines: [6] },
2213
+ ]);
2214
+ assert.deepEqual(match?.lines, [3, 5, 6]);
2215
+ assert.match(match?.why ?? '', /controller/i);
2216
+ assert.match(match?.impact ?? '', /payloads crudos|validaci[oó]n/i);
2217
+ assert.match(match?.expected_fix ?? '', /ValidationPipe|class-validator|DTO/i);
2218
+ });
2219
+
2220
+ test('hasNestedValidationPattern detecta @ValidateNested() y @Type() en DTOs anidados', () => {
2221
+ const nestedDtoAst = {
2222
+ type: 'Program',
2223
+ body: [
2224
+ {
2225
+ type: 'ClassDeclaration',
2226
+ id: { type: 'Identifier', name: 'AddressDto' },
2227
+ body: {
2228
+ type: 'ClassBody',
2229
+ body: [
2230
+ {
2231
+ type: 'ClassProperty',
2232
+ key: { type: 'Identifier', name: 'street' },
2233
+ decorators: [
2234
+ {
2235
+ type: 'Decorator',
2236
+ expression: {
2237
+ type: 'CallExpression',
2238
+ callee: { type: 'Identifier', name: 'IsString' },
2239
+ arguments: [],
2240
+ },
2241
+ },
2242
+ ],
2243
+ typeAnnotation: {
2244
+ type: 'TSTypeAnnotation',
2245
+ typeAnnotation: { type: 'TSStringKeyword' },
2246
+ },
2247
+ },
2248
+ ],
2249
+ },
2250
+ },
2251
+ {
2252
+ type: 'ClassDeclaration',
2253
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
2254
+ body: {
2255
+ type: 'ClassBody',
2256
+ body: [
2257
+ {
2258
+ type: 'ClassProperty',
2259
+ key: { type: 'Identifier', name: 'address' },
2260
+ decorators: [
2261
+ {
2262
+ type: 'Decorator',
2263
+ expression: {
2264
+ type: 'CallExpression',
2265
+ callee: { type: 'Identifier', name: 'ValidateNested' },
2266
+ arguments: [],
2267
+ },
2268
+ },
2269
+ {
2270
+ type: 'Decorator',
2271
+ expression: {
2272
+ type: 'CallExpression',
2273
+ callee: { type: 'Identifier', name: 'Type' },
2274
+ arguments: [
2275
+ {
2276
+ type: 'ArrowFunctionExpression',
2277
+ body: { type: 'Identifier', name: 'AddressDto' },
2278
+ },
2279
+ ],
2280
+ },
2281
+ },
2282
+ ],
2283
+ typeAnnotation: {
2284
+ type: 'TSTypeAnnotation',
2285
+ typeAnnotation: {
2286
+ type: 'TSTypeReference',
2287
+ typeName: { type: 'Identifier', name: 'AddressDto' },
2288
+ },
2289
+ },
2290
+ },
2291
+ ],
2292
+ },
2293
+ },
2294
+ ],
2295
+ };
2296
+ const incompleteAst = {
2297
+ type: 'Program',
2298
+ body: [
2299
+ {
2300
+ type: 'ClassDeclaration',
2301
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
2302
+ body: {
2303
+ type: 'ClassBody',
2304
+ body: [
2305
+ {
2306
+ type: 'ClassProperty',
2307
+ key: { type: 'Identifier', name: 'address' },
2308
+ decorators: [
2309
+ {
2310
+ type: 'Decorator',
2311
+ expression: {
2312
+ type: 'CallExpression',
2313
+ callee: { type: 'Identifier', name: 'Type' },
2314
+ arguments: [],
2315
+ },
2316
+ },
2317
+ ],
2318
+ typeAnnotation: {
2319
+ type: 'TSTypeAnnotation',
2320
+ typeAnnotation: {
2321
+ type: 'TSTypeReference',
2322
+ typeName: { type: 'Identifier', name: 'AddressDto' },
2323
+ },
2324
+ },
2325
+ },
2326
+ ],
2327
+ },
2328
+ },
2329
+ ],
2330
+ };
2331
+
2332
+ assert.equal(hasNestedValidationPattern(nestedDtoAst), true);
2333
+ assert.equal(hasNestedValidationPattern(incompleteAst), false);
2334
+ });
2335
+
2336
+ test('findNestedValidationMatch devuelve payload semantico para DTOs anidados', () => {
2337
+ const ast = {
2338
+ type: 'Program',
2339
+ body: [
2340
+ {
2341
+ type: 'ClassDeclaration',
2342
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
2343
+ loc: { start: { line: 3 }, end: { line: 11 } },
2344
+ body: {
2345
+ type: 'ClassBody',
2346
+ body: [
2347
+ {
2348
+ type: 'ClassProperty',
2349
+ key: { type: 'Identifier', name: 'address' },
2350
+ loc: { start: { line: 5 }, end: { line: 10 } },
2351
+ decorators: [
2352
+ {
2353
+ type: 'Decorator',
2354
+ loc: { start: { line: 5 }, end: { line: 5 } },
2355
+ expression: {
2356
+ type: 'CallExpression',
2357
+ callee: { type: 'Identifier', name: 'ValidateNested' },
2358
+ arguments: [],
2359
+ },
2360
+ },
2361
+ {
2362
+ type: 'Decorator',
2363
+ loc: { start: { line: 6 }, end: { line: 6 } },
2364
+ expression: {
2365
+ type: 'CallExpression',
2366
+ callee: { type: 'Identifier', name: 'Type' },
2367
+ arguments: [
2368
+ {
2369
+ type: 'ArrowFunctionExpression',
2370
+ body: { type: 'Identifier', name: 'AddressDto' },
2371
+ },
2372
+ ],
2373
+ },
2374
+ },
2375
+ ],
2376
+ typeAnnotation: {
2377
+ type: 'TSTypeAnnotation',
2378
+ typeAnnotation: {
2379
+ type: 'TSTypeReference',
2380
+ typeName: { type: 'Identifier', name: 'AddressDto' },
2381
+ },
2382
+ },
2383
+ },
2384
+ ],
2385
+ },
2386
+ },
2387
+ ],
2388
+ };
2389
+
2390
+ const match = findNestedValidationMatch(ast);
2391
+
2392
+ assert.ok(match);
2393
+ assert.deepEqual(match?.primary_node, {
2394
+ kind: 'class',
2395
+ name: 'CreateOrderDto',
2396
+ lines: [3],
2397
+ });
2398
+ assert.deepEqual(match?.related_nodes, [
2399
+ { kind: 'member', name: 'address', lines: [5] },
2400
+ { kind: 'member', name: '@ValidateNested', lines: [5] },
2401
+ { kind: 'member', name: '@Type', lines: [6] },
2402
+ { kind: 'member', name: 'AddressDto', lines: [5] },
2403
+ ]);
2404
+ assert.deepEqual(match?.lines, [3, 5, 6]);
2405
+ assert.match(match?.why ?? '', /@ValidateNested|@Type|DTO/i);
2406
+ assert.match(match?.impact ?? '', /anidada|payloads inv[aá]lidos/i);
2407
+ assert.match(match?.expected_fix ?? '', /@ValidateNested|@Type|ChildDto/i);
2408
+ });
2409
+
2410
+ test('hasSeparatedDtoPattern detecta DTOs separados y descarta variantes incompletas', () => {
2411
+ const dtoAst = {
2412
+ type: 'Program',
2413
+ body: [
2414
+ { type: 'ClassDeclaration', id: { type: 'Identifier', name: 'CreateOrderDto' }, body: { type: 'ClassBody', body: [] } },
2415
+ { type: 'ClassDeclaration', id: { type: 'Identifier', name: 'UpdateOrderDto' }, body: { type: 'ClassBody', body: [] } },
2416
+ { type: 'ClassDeclaration', id: { type: 'Identifier', name: 'OrderResponseDto' }, body: { type: 'ClassBody', body: [] } },
2417
+ ],
2418
+ };
2419
+ const incompleteAst = {
2420
+ type: 'Program',
2421
+ body: [
2422
+ { type: 'ClassDeclaration', id: { type: 'Identifier', name: 'CreateOrderDto' }, body: { type: 'ClassBody', body: [] } },
2423
+ { type: 'ClassDeclaration', id: { type: 'Identifier', name: 'OrderResponseDto' }, body: { type: 'ClassBody', body: [] } },
2424
+ ],
2425
+ };
2426
+
2427
+ assert.equal(hasSeparatedDtoPattern(dtoAst), true);
2428
+ assert.equal(hasSeparatedDtoPattern(incompleteAst), false);
2429
+ });
2430
+
2431
+ test('findSeparatedDtoMatch devuelve payload semantico para DTOs separados', () => {
2432
+ const ast = {
2433
+ type: 'Program',
2434
+ body: [
2435
+ {
2436
+ type: 'ClassDeclaration',
2437
+ id: { type: 'Identifier', name: 'CreateOrderDto' },
2438
+ loc: { start: { line: 3 }, end: { line: 6 } },
2439
+ body: { type: 'ClassBody', body: [] },
2440
+ },
2441
+ {
2442
+ type: 'ClassDeclaration',
2443
+ id: { type: 'Identifier', name: 'UpdateOrderDto' },
2444
+ loc: { start: { line: 8 }, end: { line: 11 } },
2445
+ body: { type: 'ClassBody', body: [] },
2446
+ },
2447
+ {
2448
+ type: 'ClassDeclaration',
2449
+ id: { type: 'Identifier', name: 'OrderResponseDto' },
2450
+ loc: { start: { line: 13 }, end: { line: 16 } },
2451
+ body: { type: 'ClassBody', body: [] },
2452
+ },
2453
+ ],
2454
+ };
2455
+
2456
+ const match = findSeparatedDtoMatch(ast);
2457
+
2458
+ assert.ok(match);
2459
+ assert.deepEqual(match?.primary_node, {
2460
+ kind: 'class',
2461
+ name: 'CreateOrderDto',
2462
+ lines: [3],
2463
+ });
2464
+ assert.deepEqual(match?.related_nodes, [
2465
+ { kind: 'class', name: 'UpdateOrderDto', lines: [8] },
2466
+ { kind: 'class', name: 'OrderResponseDto', lines: [13] },
2467
+ ]);
2468
+ assert.deepEqual(match?.lines, [3, 8, 13]);
2469
+ assert.match(match?.why ?? '', /separan creación, actualización y respuesta/i);
2470
+ assert.match(match?.impact ?? '', /mezcla contratos|validaci[oó]n y serializaci[oó]n/i);
2471
+ assert.match(match?.expected_fix ?? '', /Create, Update y Response/i);
2472
+ });
2473
+
2474
+ test('hasBackendReturnDtosExposureUsage detecta retornos directos de entidades y descarta DTOs', () => {
2475
+ const entityReturnAst = {
2476
+ type: 'ClassMethod',
2477
+ key: { type: 'Identifier', name: 'getOrder' },
2478
+ returnType: {
2479
+ type: 'TSTypeAnnotation',
2480
+ typeAnnotation: {
2481
+ type: 'TSTypeReference',
2482
+ typeName: { type: 'Identifier', name: 'Promise' },
2483
+ typeParameters: {
2484
+ params: [
2485
+ {
2486
+ type: 'TSTypeReference',
2487
+ typeName: { type: 'Identifier', name: 'OrderEntity' },
2488
+ },
2489
+ ],
2490
+ },
2491
+ },
2492
+ },
2493
+ body: { type: 'BlockStatement', body: [] },
2494
+ };
2495
+ const dtoReturnAst = {
2496
+ type: 'ClassMethod',
2497
+ key: { type: 'Identifier', name: 'getOrder' },
2498
+ returnType: {
2499
+ type: 'TSTypeAnnotation',
2500
+ typeAnnotation: {
2501
+ type: 'TSTypeReference',
2502
+ typeName: { type: 'Identifier', name: 'Promise' },
2503
+ typeParameters: {
2504
+ params: [
2505
+ {
2506
+ type: 'TSTypeReference',
2507
+ typeName: { type: 'Identifier', name: 'OrderResponseDto' },
2508
+ },
2509
+ ],
2510
+ },
2511
+ },
2512
+ },
2513
+ body: { type: 'BlockStatement', body: [] },
2514
+ };
2515
+ const returnStatementAst = {
2516
+ type: 'ReturnStatement',
2517
+ argument: { type: 'Identifier', name: 'orderEntity' },
2518
+ };
2519
+ const safeReturnStatementAst = {
2520
+ type: 'ReturnStatement',
2521
+ argument: { type: 'Identifier', name: 'orderResponseDto' },
2522
+ };
2523
+
2524
+ assert.equal(hasBackendReturnDtosExposureUsage(entityReturnAst), true);
2525
+ assert.equal(hasBackendReturnDtosExposureUsage(returnStatementAst), true);
2526
+ assert.equal(hasBackendReturnDtosExposureUsage(dtoReturnAst), false);
2527
+ assert.equal(hasBackendReturnDtosExposureUsage(safeReturnStatementAst), false);
2528
+ });
2529
+
2530
+ test('findBackendReturnDtosExposureLines devuelve lineas de retornos directos de entidades', () => {
2531
+ const ast = {
2532
+ type: 'Program',
2533
+ body: [
2534
+ {
2535
+ type: 'ClassMethod',
2536
+ key: { type: 'Identifier', name: 'getOrder' },
2537
+ loc: { start: { line: 4 }, end: { line: 10 } },
2538
+ returnType: {
2539
+ type: 'TSTypeAnnotation',
2540
+ typeAnnotation: {
2541
+ type: 'TSTypeReference',
2542
+ typeName: { type: 'Identifier', name: 'Promise' },
2543
+ typeParameters: {
2544
+ params: [
2545
+ {
2546
+ type: 'TSTypeReference',
2547
+ typeName: { type: 'Identifier', name: 'OrderEntity' },
2548
+ },
2549
+ ],
2550
+ },
2551
+ },
2552
+ },
2553
+ body: {
2554
+ type: 'BlockStatement',
2555
+ body: [
2556
+ {
2557
+ type: 'ReturnStatement',
2558
+ loc: { start: { line: 7 }, end: { line: 7 } },
2559
+ argument: { type: 'Identifier', name: 'orderEntity' },
2560
+ },
2561
+ ],
2562
+ },
2563
+ },
2564
+ ],
2565
+ };
2566
+
2567
+ assert.deepEqual(findBackendReturnDtosExposureLines(ast), [4, 7]);
2568
+ });
2569
+
2570
+ test('hasPrometheusMetricsPattern detecta prom-client y descarta dependencias no metrics', () => {
2571
+ const importAst = {
2572
+ type: 'ImportDeclaration',
2573
+ source: { type: 'StringLiteral', value: 'prom-client' },
2574
+ };
2575
+ const requireAst = {
2576
+ type: 'CallExpression',
2577
+ callee: { type: 'Identifier', name: 'require' },
2578
+ arguments: [{ type: 'StringLiteral', value: 'prom-client' }],
2579
+ };
2580
+ const localAst = {
2581
+ type: 'ImportDeclaration',
2582
+ source: { type: 'StringLiteral', value: './metrics' },
2583
+ };
2584
+
2585
+ assert.equal(hasPrometheusMetricsPattern(importAst), true);
2586
+ assert.equal(hasPrometheusMetricsPattern(requireAst), true);
2587
+ assert.equal(hasPrometheusMetricsPattern(localAst), false);
2588
+ });
2589
+
2590
+ test('findPrometheusMetricsMatch devuelve payload semantico para prom-client', () => {
2591
+ const ast = {
2592
+ type: 'Program',
2593
+ body: [
2594
+ {
2595
+ type: 'ClassDeclaration',
2596
+ id: { type: 'Identifier', name: 'MetricsController' },
2597
+ loc: { start: { line: 2 }, end: { line: 8 } },
2598
+ body: {
2599
+ type: 'ClassBody',
2600
+ body: [
2601
+ {
2602
+ type: 'ClassProperty',
2603
+ loc: { start: { line: 3 }, end: { line: 3 } },
2604
+ key: { type: 'Identifier', name: 'metrics' },
2605
+ value: {
2606
+ type: 'CallExpression',
2607
+ loc: { start: { line: 3 }, end: { line: 3 } },
2608
+ callee: { type: 'Identifier', name: 'require' },
2609
+ arguments: [{ type: 'StringLiteral', value: 'prom-client' }],
2610
+ },
2611
+ },
2612
+ ],
2613
+ },
2614
+ },
2615
+ ],
2616
+ };
2617
+
2618
+ const match = findPrometheusMetricsMatch(ast);
2619
+
2620
+ assert.ok(match);
2621
+ assert.deepEqual(match?.primary_node, {
2622
+ kind: 'class',
2623
+ name: 'MetricsController',
2624
+ lines: [2],
2625
+ });
2626
+ assert.deepEqual(match?.related_nodes, [
2627
+ { kind: 'member', name: 'import:prom-client', lines: [3] },
2628
+ ]);
2629
+ assert.deepEqual(match?.lines, [2, 3]);
2630
+ assert.match(match?.why ?? '', /Prometheus/i);
2631
+ assert.match(match?.impact ?? '', /m[eé]tricas/i);
2632
+ assert.match(match?.expected_fix ?? '', /prom-client/i);
2633
+ });
2634
+
2635
+ test('hasPasswordHashingPattern detecta bcrypt con salt rounds inseguros y descarta rounds seguros', () => {
2636
+ const insecureHashAst = {
2637
+ type: 'CallExpression',
2638
+ callee: {
2639
+ type: 'MemberExpression',
2640
+ computed: false,
2641
+ object: { type: 'Identifier', name: 'bcrypt' },
2642
+ property: { type: 'Identifier', name: 'hashSync' },
2643
+ },
2644
+ arguments: [
2645
+ { type: 'Identifier', name: 'password' },
2646
+ { type: 'NumericLiteral', value: 8, loc: { start: { line: 4 }, end: { line: 4 } } },
2647
+ ],
2648
+ };
2649
+ const insecureSaltAst = {
2650
+ type: 'CallExpression',
2651
+ callee: {
2652
+ type: 'MemberExpression',
2653
+ computed: false,
2654
+ object: { type: 'Identifier', name: 'bcryptjs' },
2655
+ property: { type: 'Identifier', name: 'genSaltSync' },
2656
+ },
2657
+ arguments: [
2658
+ { type: 'NumericLiteral', value: 6, loc: { start: { line: 8 }, end: { line: 8 } } },
2659
+ ],
2660
+ };
2661
+ const secureHashAst = {
2662
+ type: 'CallExpression',
2663
+ callee: {
2664
+ type: 'MemberExpression',
2665
+ computed: false,
2666
+ object: { type: 'Identifier', name: 'bcrypt' },
2667
+ property: { type: 'Identifier', name: 'hashSync' },
2668
+ },
2669
+ arguments: [
2670
+ { type: 'Identifier', name: 'password' },
2671
+ { type: 'NumericLiteral', value: 12, loc: { start: { line: 12 }, end: { line: 12 } } },
2672
+ ],
2673
+ };
2674
+
2675
+ assert.equal(hasPasswordHashingPattern(insecureHashAst), true);
2676
+ assert.equal(hasPasswordHashingPattern(insecureSaltAst), true);
2677
+ assert.equal(hasPasswordHashingPattern(secureHashAst), false);
2678
+ });
2679
+
2680
+ test('findPasswordHashingPatternMatch devuelve payload semantico para bcrypt', () => {
2681
+ const ast = {
2682
+ type: 'Program',
2683
+ body: [
2684
+ {
2685
+ type: 'FunctionDeclaration',
2686
+ id: { type: 'Identifier', name: 'createPasswordHash' },
2687
+ loc: { start: { line: 2 }, end: { line: 8 } },
2688
+ body: {
2689
+ type: 'BlockStatement',
2690
+ body: [
2691
+ {
2692
+ type: 'ExpressionStatement',
2693
+ loc: { start: { line: 4 }, end: { line: 4 } },
2694
+ expression: {
2695
+ type: 'CallExpression',
2696
+ loc: { start: { line: 4 }, end: { line: 4 } },
2697
+ callee: {
2698
+ type: 'MemberExpression',
2699
+ computed: false,
2700
+ object: { type: 'Identifier', name: 'bcrypt' },
2701
+ property: { type: 'Identifier', name: 'hashSync' },
2702
+ },
2703
+ arguments: [
2704
+ { type: 'Identifier', name: 'password' },
2705
+ { type: 'NumericLiteral', value: 8, loc: { start: { line: 4 }, end: { line: 4 } } },
2706
+ ],
2707
+ },
2708
+ },
2709
+ ],
2710
+ },
2711
+ },
2712
+ ],
2713
+ };
2714
+
2715
+ const match = findPasswordHashingPatternMatch(ast);
2716
+
2717
+ assert.ok(match);
2718
+ assert.deepEqual(match?.primary_node, {
2719
+ kind: 'member',
2720
+ name: 'createPasswordHash',
2721
+ lines: [2],
2722
+ });
2723
+ assert.deepEqual(match?.related_nodes, [
2724
+ { kind: 'call', name: 'bcrypt.hashSync', lines: [4] },
2725
+ { kind: 'member', name: 'salt rounds: 8', lines: [4] },
2726
+ ]);
2727
+ assert.deepEqual(match?.lines, [2, 4]);
2728
+ assert.match(match?.why ?? '', /salt rounds/i);
2729
+ assert.match(match?.impact ?? '', /fuerza bruta|offline/i);
2730
+ assert.match(match?.expected_fix ?? '', /10 o m[aá]s|constante/i);
2731
+ });
2732
+
2733
+ test('hasRateLimitingThrottlerPattern detecta ThrottlerModule y decoradores de throttling y descarta variantes ajenas', () => {
2734
+ const throttlerSetupAst = {
2735
+ type: 'CallExpression',
2736
+ callee: {
2737
+ type: 'MemberExpression',
2738
+ computed: false,
2739
+ object: { type: 'Identifier', name: 'ThrottlerModule' },
2740
+ property: { type: 'Identifier', name: 'forRoot' },
2741
+ },
2742
+ arguments: [],
2743
+ };
2744
+ const throttleDecoratorAst = {
2745
+ type: 'Decorator',
2746
+ expression: {
2747
+ type: 'CallExpression',
2748
+ callee: { type: 'Identifier', name: 'Throttle' },
2749
+ arguments: [
2750
+ { type: 'NumericLiteral', value: 5 },
2751
+ { type: 'NumericLiteral', value: 60 },
2752
+ ],
2753
+ },
2754
+ };
2755
+ const throttlerGuardDecoratorAst = {
2756
+ type: 'Decorator',
2757
+ expression: {
2758
+ type: 'CallExpression',
2759
+ callee: { type: 'Identifier', name: 'UseGuards' },
2760
+ arguments: [{ type: 'Identifier', name: 'ThrottlerGuard' }],
2761
+ },
2762
+ };
2763
+ const unrelatedAst = {
2764
+ type: 'CallExpression',
2765
+ callee: {
2766
+ type: 'MemberExpression',
2767
+ computed: false,
2768
+ object: { type: 'Identifier', name: 'RateLimitModule' },
2769
+ property: { type: 'Identifier', name: 'forRoot' },
2770
+ },
2771
+ arguments: [],
2772
+ };
2773
+
2774
+ assert.equal(hasRateLimitingThrottlerPattern(throttlerSetupAst), true);
2775
+ assert.equal(hasRateLimitingThrottlerPattern(throttleDecoratorAst), true);
2776
+ assert.equal(hasRateLimitingThrottlerPattern(throttlerGuardDecoratorAst), true);
2777
+ assert.equal(hasRateLimitingThrottlerPattern(unrelatedAst), false);
2778
+ });
2779
+
2780
+ test('findRateLimitingThrottlerMatch devuelve payload semantico para @UseGuards(ThrottlerGuard)', () => {
2781
+ const ast = {
2782
+ type: 'Program',
2783
+ body: [
2784
+ {
2785
+ type: 'ClassDeclaration',
2786
+ id: { type: 'Identifier', name: 'AuthController' },
2787
+ loc: { start: { line: 3 }, end: { line: 18 } },
2788
+ decorators: [
2789
+ {
2790
+ type: 'Decorator',
2791
+ loc: { start: { line: 3 }, end: { line: 3 } },
2792
+ expression: {
2793
+ type: 'CallExpression',
2794
+ callee: { type: 'Identifier', name: 'UseGuards' },
2795
+ arguments: [{ type: 'Identifier', name: 'ThrottlerGuard' }],
2796
+ },
2797
+ },
2798
+ ],
2799
+ body: {
2800
+ type: 'ClassBody',
2801
+ body: [],
2802
+ },
2803
+ },
2804
+ ],
2805
+ };
2806
+
2807
+ const match = findRateLimitingThrottlerMatch(ast);
2808
+
2809
+ assert.ok(match);
2810
+ assert.deepEqual(match?.primary_node, {
2811
+ kind: 'class',
2812
+ name: 'AuthController',
2813
+ lines: [3],
2814
+ });
2815
+ assert.deepEqual(match?.related_nodes, [
2816
+ { kind: 'call', name: 'UseGuards', lines: [3] },
2817
+ { kind: 'member', name: 'ThrottlerGuard', lines: [3] },
2818
+ ]);
2819
+ assert.deepEqual(match?.lines, [3]);
2820
+ assert.match(match?.why ?? '', /rate limiting/i);
2821
+ assert.match(match?.impact ?? '', /brute force|abuso/i);
2822
+ assert.match(match?.expected_fix ?? '', /ThrottlerModule|Throttle|ThrottlerGuard/i);
2823
+ });
2824
+
2825
+ test('hasWinstonStructuredLoggerPattern detecta createLogger con JSON logs y descarta configuraciones planas', () => {
2826
+ const structuredLoggerAst = {
2827
+ type: 'CallExpression',
2828
+ callee: {
2829
+ type: 'MemberExpression',
2830
+ computed: false,
2831
+ object: { type: 'Identifier', name: 'winston' },
2832
+ property: { type: 'Identifier', name: 'createLogger' },
2833
+ },
2834
+ arguments: [
2835
+ {
2836
+ type: 'ObjectExpression',
2837
+ properties: [
2838
+ {
2839
+ type: 'ObjectProperty',
2840
+ key: { type: 'Identifier', name: 'format' },
2841
+ value: {
2842
+ type: 'CallExpression',
2843
+ callee: {
2844
+ type: 'MemberExpression',
2845
+ computed: false,
2846
+ object: { type: 'Identifier', name: 'format' },
2847
+ property: { type: 'Identifier', name: 'json' },
2848
+ },
2849
+ arguments: [],
2850
+ },
2851
+ },
2852
+ ],
2853
+ },
2854
+ ],
2855
+ };
2856
+ const flatLoggerAst = {
2857
+ type: 'CallExpression',
2858
+ callee: {
2859
+ type: 'MemberExpression',
2860
+ computed: false,
2861
+ object: { type: 'Identifier', name: 'winston' },
2862
+ property: { type: 'Identifier', name: 'createLogger' },
2863
+ },
2864
+ arguments: [
2865
+ {
2866
+ type: 'ObjectExpression',
2867
+ properties: [
2868
+ {
2869
+ type: 'ObjectProperty',
2870
+ key: { type: 'Identifier', name: 'level' },
2871
+ value: { type: 'StringLiteral', value: 'info' },
2872
+ },
2873
+ ],
2874
+ },
2875
+ ],
2876
+ };
2877
+
2878
+ assert.equal(hasWinstonStructuredLoggerPattern(structuredLoggerAst), true);
2879
+ assert.equal(hasWinstonStructuredLoggerPattern(flatLoggerAst), false);
2880
+ });
2881
+
2882
+ test('findWinstonStructuredLoggerMatch devuelve payload semantico para Winston con JSON logs', () => {
2883
+ const ast = {
2884
+ type: 'Program',
2885
+ body: [
2886
+ {
2887
+ type: 'FunctionDeclaration',
2888
+ id: { type: 'Identifier', name: 'buildLogger' },
2889
+ loc: { start: { line: 2 }, end: { line: 10 } },
2890
+ body: {
2891
+ type: 'BlockStatement',
2892
+ body: [
2893
+ {
2894
+ type: 'VariableDeclaration',
2895
+ loc: { start: { line: 4 }, end: { line: 4 } },
2896
+ declarations: [
2897
+ {
2898
+ type: 'VariableDeclarator',
2899
+ id: { type: 'Identifier', name: 'logger' },
2900
+ init: {
2901
+ type: 'CallExpression',
2902
+ loc: { start: { line: 4 }, end: { line: 4 } },
2903
+ callee: {
2904
+ type: 'MemberExpression',
2905
+ computed: false,
2906
+ object: { type: 'Identifier', name: 'winston' },
2907
+ property: { type: 'Identifier', name: 'createLogger' },
2908
+ },
2909
+ arguments: [
2910
+ {
2911
+ type: 'ObjectExpression',
2912
+ properties: [
2913
+ {
2914
+ type: 'ObjectProperty',
2915
+ key: { type: 'Identifier', name: 'format' },
2916
+ value: {
2917
+ type: 'CallExpression',
2918
+ loc: { start: { line: 4 }, end: { line: 4 } },
2919
+ callee: {
2920
+ type: 'MemberExpression',
2921
+ computed: false,
2922
+ object: { type: 'Identifier', name: 'format' },
2923
+ property: { type: 'Identifier', name: 'json' },
2924
+ },
2925
+ arguments: [],
2926
+ },
2927
+ },
2928
+ ],
2929
+ },
2930
+ ],
2931
+ },
2932
+ },
2933
+ ],
2934
+ },
2935
+ ],
2936
+ },
2937
+ },
2938
+ ],
2939
+ };
2940
+
2941
+ const match = findWinstonStructuredLoggerMatch(ast);
2942
+
2943
+ assert.ok(match);
2944
+ assert.deepEqual(match?.primary_node, {
2945
+ kind: 'member',
2946
+ name: 'logger',
2947
+ lines: undefined,
2948
+ });
2949
+ assert.deepEqual(match?.related_nodes, [
2950
+ { kind: 'call', name: 'winston.createLogger', lines: [4] },
2951
+ { kind: 'member', name: 'format.json', lines: [4] },
2952
+ ]);
2953
+ assert.deepEqual(match?.lines, [4]);
2954
+ assert.match(match?.why ?? '', /Winston/i);
2955
+ assert.match(match?.impact ?? '', /JSON|agregadores|correlation/i);
2956
+ assert.match(match?.expected_fix ?? '', /createLogger|format\.json/i);
117
2957
  });
118
2958
 
119
2959
  test('hasConsoleErrorCall detecta console.error y descarta metodos distintos', () => {
@@ -239,6 +3079,128 @@ test('hasAsyncPromiseExecutor detecta ejecutor async en new Promise', () => {
239
3079
  assert.equal(hasAsyncPromiseExecutor(syncExecutorAst), false);
240
3080
  });
241
3081
 
3082
+ test('hasCallbackHellPattern detecta callbacks anidados y descarta encadenado plano', () => {
3083
+ const callbackHellAst = {
3084
+ type: 'Program',
3085
+ body: [
3086
+ {
3087
+ type: 'ClassDeclaration',
3088
+ id: { type: 'Identifier', name: 'AsyncFlowService' },
3089
+ loc: { start: { line: 1 }, end: { line: 14 } },
3090
+ body: {
3091
+ type: 'ClassBody',
3092
+ body: [
3093
+ {
3094
+ type: 'ClassMethod',
3095
+ key: { type: 'Identifier', name: 'load' },
3096
+ loc: { start: { line: 2 }, end: { line: 13 } },
3097
+ body: {
3098
+ type: 'BlockStatement',
3099
+ body: [
3100
+ {
3101
+ type: 'ExpressionStatement',
3102
+ loc: { start: { line: 3 }, end: { line: 12 } },
3103
+ expression: {
3104
+ type: 'CallExpression',
3105
+ loc: { start: { line: 3 }, end: { line: 12 } },
3106
+ callee: {
3107
+ type: 'MemberExpression',
3108
+ computed: false,
3109
+ object: {
3110
+ type: 'CallExpression',
3111
+ loc: { start: { line: 3 }, end: { line: 3 } },
3112
+ callee: { type: 'Identifier', name: 'fetchData' },
3113
+ arguments: [],
3114
+ },
3115
+ property: { type: 'Identifier', name: 'then' },
3116
+ },
3117
+ arguments: [
3118
+ {
3119
+ type: 'ArrowFunctionExpression',
3120
+ loc: { start: { line: 4 }, end: { line: 11 } },
3121
+ body: {
3122
+ type: 'BlockStatement',
3123
+ body: [
3124
+ {
3125
+ type: 'ExpressionStatement',
3126
+ loc: { start: { line: 5 }, end: { line: 10 } },
3127
+ expression: {
3128
+ type: 'CallExpression',
3129
+ loc: { start: { line: 5 }, end: { line: 10 } },
3130
+ callee: {
3131
+ type: 'MemberExpression',
3132
+ computed: false,
3133
+ object: {
3134
+ type: 'CallExpression',
3135
+ loc: { start: { line: 5 }, end: { line: 5 } },
3136
+ callee: { type: 'Identifier', name: 'persist' },
3137
+ arguments: [],
3138
+ },
3139
+ property: { type: 'Identifier', name: 'then' },
3140
+ },
3141
+ arguments: [
3142
+ {
3143
+ type: 'ArrowFunctionExpression',
3144
+ loc: { start: { line: 6 }, end: { line: 9 } },
3145
+ body: { type: 'BlockStatement', body: [] },
3146
+ },
3147
+ ],
3148
+ },
3149
+ },
3150
+ ],
3151
+ },
3152
+ },
3153
+ ],
3154
+ },
3155
+ },
3156
+ ],
3157
+ },
3158
+ },
3159
+ ],
3160
+ },
3161
+ },
3162
+ ],
3163
+ };
3164
+ const flatChainAst = {
3165
+ type: 'CallExpression',
3166
+ callee: {
3167
+ type: 'MemberExpression',
3168
+ computed: false,
3169
+ object: {
3170
+ type: 'CallExpression',
3171
+ callee: { type: 'Identifier', name: 'fetchData' },
3172
+ arguments: [],
3173
+ },
3174
+ property: { type: 'Identifier', name: 'then' },
3175
+ },
3176
+ arguments: [
3177
+ {
3178
+ type: 'ArrowFunctionExpression',
3179
+ body: { type: 'BlockStatement', body: [] },
3180
+ },
3181
+ ],
3182
+ };
3183
+
3184
+ assert.equal(hasCallbackHellPattern(callbackHellAst), true);
3185
+ assert.equal(hasCallbackHellPattern(flatChainAst), false);
3186
+
3187
+ const match = findCallbackHellPatternMatch(callbackHellAst);
3188
+ assert.ok(match);
3189
+ assert.deepEqual(match.primary_node, {
3190
+ kind: 'member',
3191
+ name: 'load',
3192
+ lines: [2],
3193
+ });
3194
+ assert.deepEqual(match.related_nodes, [
3195
+ { kind: 'call', name: 'then callback', lines: [3] },
3196
+ { kind: 'call', name: 'then nested callback', lines: [5] },
3197
+ ]);
3198
+ assert.deepEqual(match.lines, [2, 3, 4, 5]);
3199
+ assert.match(match.why, /async\/await/i);
3200
+ assert.match(match.impact, /difícil de leer/i);
3201
+ assert.match(match.expected_fix, /async\/await/i);
3202
+ });
3203
+
242
3204
  test('hasWithStatement detecta with y hasDeleteOperator detecta operador delete', () => {
243
3205
  const withAst = {
244
3206
  type: 'Program',
@@ -801,36 +3763,275 @@ test('hasConcreteDependencyInstantiation detecta instanciacion directa de depend
801
3763
  arguments: [],
802
3764
  };
803
3765
 
804
- assert.equal(hasConcreteDependencyInstantiation(concreteAst), true);
805
- assert.equal(hasConcreteDependencyInstantiation(localAst), false);
3766
+ assert.equal(hasConcreteDependencyInstantiation(concreteAst), true);
3767
+ assert.equal(hasConcreteDependencyInstantiation(localAst), false);
3768
+ });
3769
+
3770
+ test('findConcreteDependencyInstantiationMatch devuelve payload semantico para DIP', () => {
3771
+ const concreteAst = {
3772
+ type: 'Program',
3773
+ body: [
3774
+ {
3775
+ type: 'ImportDeclaration',
3776
+ loc: { start: { line: 1 }, end: { line: 1 } },
3777
+ source: { type: 'StringLiteral', value: '@prisma/client' },
3778
+ },
3779
+ {
3780
+ type: 'ClassDeclaration',
3781
+ id: { type: 'Identifier', name: 'PumukiDipConcreteInstantiationCanary' },
3782
+ loc: { start: { line: 3 }, end: { line: 12 } },
3783
+ body: {
3784
+ type: 'ClassBody',
3785
+ body: [
3786
+ {
3787
+ type: 'ClassProperty',
3788
+ key: { type: 'Identifier', name: 'client' },
3789
+ loc: { start: { line: 4 }, end: { line: 4 } },
3790
+ value: {
3791
+ type: 'NewExpression',
3792
+ loc: { start: { line: 4 }, end: { line: 4 } },
3793
+ callee: { type: 'Identifier', name: 'PrismaClient' },
3794
+ arguments: [],
3795
+ },
3796
+ },
3797
+ ],
3798
+ },
3799
+ },
3800
+ ],
3801
+ };
3802
+
3803
+ const match = findConcreteDependencyInstantiationMatch(concreteAst);
3804
+
3805
+ assert.ok(match);
3806
+ assert.deepEqual(match.primary_node, {
3807
+ kind: 'class',
3808
+ name: 'PumukiDipConcreteInstantiationCanary',
3809
+ lines: [3],
3810
+ });
3811
+ assert.deepEqual(match.related_nodes, [
3812
+ { kind: 'member', name: 'import:@prisma/client', lines: [1] },
3813
+ { kind: 'call', name: 'new PrismaClient', lines: [4] },
3814
+ ]);
3815
+ assert.deepEqual(match.lines, [1, 3, 4]);
3816
+ assert.match(match.why, /DIP/i);
3817
+ assert.match(match.impact, /tests aislados/i);
3818
+ assert.match(match.expected_fix, /adapter|puerto|abstracci/i);
3819
+ });
3820
+
3821
+ test('hasLargeClassDeclaration detecta god class por mezcla semantica de responsabilidades', () => {
3822
+ const godClassAst = {
3823
+ type: 'ClassDeclaration',
3824
+ loc: {
3825
+ start: { line: 1 },
3826
+ end: { line: 80 },
3827
+ },
3828
+ body: {
3829
+ type: 'ClassBody',
3830
+ body: [
3831
+ {
3832
+ type: 'ClassProperty',
3833
+ loc: { start: { line: 15 }, end: { line: 15 } },
3834
+ key: { type: 'Identifier', name: 'client' },
3835
+ value: {
3836
+ type: 'NewExpression',
3837
+ callee: { type: 'Identifier', name: 'PrismaClient' },
3838
+ arguments: [],
3839
+ },
3840
+ },
3841
+ {
3842
+ type: 'ClassMethod',
3843
+ key: { type: 'Identifier', name: 'getOrder' },
3844
+ loc: { start: { line: 20 }, end: { line: 24 } },
3845
+ body: { type: 'BlockStatement', body: [] },
3846
+ },
3847
+ {
3848
+ type: 'ClassMethod',
3849
+ key: { type: 'Identifier', name: 'saveOrder' },
3850
+ loc: { start: { line: 30 }, end: { line: 40 } },
3851
+ body: { type: 'BlockStatement', body: [] },
3852
+ },
3853
+ ],
3854
+ },
3855
+ };
3856
+ const oversizedButSingleResponsibilityAst = {
3857
+ type: 'ClassDeclaration',
3858
+ loc: {
3859
+ start: { line: 1 },
3860
+ end: { line: 1_000 },
3861
+ },
3862
+ body: {
3863
+ type: 'ClassBody',
3864
+ body: [
3865
+ {
3866
+ type: 'ClassMethod',
3867
+ key: { type: 'Identifier', name: 'getOrder' },
3868
+ loc: { start: { line: 20 }, end: { line: 24 } },
3869
+ body: { type: 'BlockStatement', body: [] },
3870
+ },
3871
+ ],
3872
+ },
3873
+ };
3874
+ const srpOnlyAst = {
3875
+ type: 'ClassDeclaration',
3876
+ loc: {
3877
+ start: { line: 1 },
3878
+ end: { line: 80 },
3879
+ },
3880
+ body: {
3881
+ type: 'ClassBody',
3882
+ body: [
3883
+ {
3884
+ type: 'ClassMethod',
3885
+ key: { type: 'Identifier', name: 'getOrder' },
3886
+ loc: { start: { line: 20 }, end: { line: 24 } },
3887
+ body: { type: 'BlockStatement', body: [] },
3888
+ },
3889
+ {
3890
+ type: 'ClassMethod',
3891
+ key: { type: 'Identifier', name: 'saveOrder' },
3892
+ loc: { start: { line: 30 }, end: { line: 40 } },
3893
+ body: { type: 'BlockStatement', body: [] },
3894
+ },
3895
+ ],
3896
+ },
3897
+ };
3898
+
3899
+ assert.equal(hasLargeClassDeclaration(godClassAst), true);
3900
+ assert.equal(hasLargeClassDeclaration(oversizedButSingleResponsibilityAst), false);
3901
+ assert.equal(hasLargeClassDeclaration(srpOnlyAst), false);
3902
+
3903
+ const match = findLargeClassDeclarationMatch(godClassAst);
3904
+ assert.ok(match);
3905
+ assert.deepEqual(match?.primary_node, {
3906
+ kind: 'class',
3907
+ name: 'AnonymousClass',
3908
+ lines: [1],
3909
+ });
3910
+ assert.deepEqual(match?.related_nodes, [
3911
+ { kind: 'member', name: 'query:getOrder', lines: [20] },
3912
+ { kind: 'member', name: 'command:saveOrder', lines: [30] },
3913
+ { kind: 'call', name: 'new PrismaClient', lines: [15] },
3914
+ ]);
3915
+ assert.match(match?.why ?? '', /God Class/i);
3916
+ });
3917
+
3918
+ test('hasReactClassComponentUsage detecta class components de React e ignora clases no React', () => {
3919
+ const reactClassAst = {
3920
+ type: 'Program',
3921
+ body: [
3922
+ {
3923
+ type: 'ImportDeclaration',
3924
+ loc: { start: { line: 1 }, end: { line: 1 } },
3925
+ source: { type: 'StringLiteral', value: 'react' },
3926
+ specifiers: [
3927
+ {
3928
+ type: 'ImportDefaultSpecifier',
3929
+ local: { type: 'Identifier', name: 'React' },
3930
+ },
3931
+ ],
3932
+ },
3933
+ {
3934
+ type: 'ClassDeclaration',
3935
+ id: { type: 'Identifier', name: 'LegacyCounter' },
3936
+ superClass: {
3937
+ type: 'MemberExpression',
3938
+ computed: false,
3939
+ object: { type: 'Identifier', name: 'React' },
3940
+ property: { type: 'Identifier', name: 'Component' },
3941
+ loc: { start: { line: 3 }, end: { line: 3 } },
3942
+ },
3943
+ loc: { start: { line: 4 }, end: { line: 18 } },
3944
+ body: {
3945
+ type: 'ClassBody',
3946
+ body: [
3947
+ {
3948
+ type: 'ClassMethod',
3949
+ key: { type: 'Identifier', name: 'render' },
3950
+ loc: { start: { line: 8 }, end: { line: 14 } },
3951
+ body: { type: 'BlockStatement', body: [] },
3952
+ },
3953
+ ],
3954
+ },
3955
+ },
3956
+ ],
3957
+ };
3958
+ const nonReactClassAst = {
3959
+ type: 'Program',
3960
+ body: [
3961
+ {
3962
+ type: 'ImportDeclaration',
3963
+ loc: { start: { line: 1 }, end: { line: 1 } },
3964
+ source: { type: 'StringLiteral', value: 'somewhere-else' },
3965
+ specifiers: [
3966
+ {
3967
+ type: 'ImportDefaultSpecifier',
3968
+ local: { type: 'Identifier', name: 'Component' },
3969
+ },
3970
+ ],
3971
+ },
3972
+ {
3973
+ type: 'ClassDeclaration',
3974
+ id: { type: 'Identifier', name: 'PlainClass' },
3975
+ superClass: { type: 'Identifier', name: 'Component' },
3976
+ loc: { start: { line: 4 }, end: { line: 9 } },
3977
+ body: {
3978
+ type: 'ClassBody',
3979
+ body: [
3980
+ {
3981
+ type: 'ClassMethod',
3982
+ key: { type: 'Identifier', name: 'render' },
3983
+ loc: { start: { line: 6 }, end: { line: 8 } },
3984
+ body: { type: 'BlockStatement', body: [] },
3985
+ },
3986
+ ],
3987
+ },
3988
+ },
3989
+ ],
3990
+ };
3991
+
3992
+ assert.equal(hasReactClassComponentUsage(reactClassAst), true);
3993
+ assert.equal(hasReactClassComponentUsage(nonReactClassAst), false);
806
3994
  });
807
3995
 
808
- test('findConcreteDependencyInstantiationMatch devuelve payload semantico para DIP', () => {
809
- const concreteAst = {
3996
+ test('findReactClassComponentMatch devuelve payload semantico para class components de React', () => {
3997
+ const ast = {
810
3998
  type: 'Program',
811
3999
  body: [
812
4000
  {
813
4001
  type: 'ImportDeclaration',
814
4002
  loc: { start: { line: 1 }, end: { line: 1 } },
815
- source: { type: 'StringLiteral', value: '@prisma/client' },
4003
+ source: { type: 'StringLiteral', value: 'react' },
4004
+ specifiers: [
4005
+ {
4006
+ type: 'ImportDefaultSpecifier',
4007
+ local: { type: 'Identifier', name: 'React' },
4008
+ },
4009
+ ],
816
4010
  },
817
4011
  {
818
4012
  type: 'ClassDeclaration',
819
- id: { type: 'Identifier', name: 'PumukiDipConcreteInstantiationCanary' },
820
- loc: { start: { line: 3 }, end: { line: 12 } },
4013
+ id: { type: 'Identifier', name: 'LegacyCounter' },
4014
+ superClass: {
4015
+ type: 'MemberExpression',
4016
+ computed: false,
4017
+ object: { type: 'Identifier', name: 'React' },
4018
+ property: { type: 'Identifier', name: 'Component' },
4019
+ loc: { start: { line: 3 }, end: { line: 3 } },
4020
+ },
4021
+ loc: { start: { line: 4 }, end: { line: 18 } },
821
4022
  body: {
822
4023
  type: 'ClassBody',
823
4024
  body: [
824
4025
  {
825
4026
  type: 'ClassProperty',
826
- key: { type: 'Identifier', name: 'client' },
827
- loc: { start: { line: 4 }, end: { line: 4 } },
828
- value: {
829
- type: 'NewExpression',
830
- loc: { start: { line: 4 }, end: { line: 4 } },
831
- callee: { type: 'Identifier', name: 'PrismaClient' },
832
- arguments: [],
833
- },
4027
+ key: { type: 'Identifier', name: 'state' },
4028
+ loc: { start: { line: 5 }, end: { line: 5 } },
4029
+ },
4030
+ {
4031
+ type: 'ClassMethod',
4032
+ key: { type: 'Identifier', name: 'render' },
4033
+ loc: { start: { line: 8 }, end: { line: 14 } },
4034
+ body: { type: 'BlockStatement', body: [] },
834
4035
  },
835
4036
  ],
836
4037
  },
@@ -838,104 +4039,469 @@ test('findConcreteDependencyInstantiationMatch devuelve payload semantico para D
838
4039
  ],
839
4040
  };
840
4041
 
841
- const match = findConcreteDependencyInstantiationMatch(concreteAst);
4042
+ const match = findReactClassComponentMatch(ast);
842
4043
 
843
4044
  assert.ok(match);
844
- assert.deepEqual(match.primary_node, {
4045
+ assert.deepEqual(match?.primary_node, {
845
4046
  kind: 'class',
846
- name: 'PumukiDipConcreteInstantiationCanary',
847
- lines: [3],
4047
+ name: 'LegacyCounter',
4048
+ lines: [4],
848
4049
  });
849
- assert.deepEqual(match.related_nodes, [
850
- { kind: 'member', name: 'import:@prisma/client', lines: [1] },
851
- { kind: 'call', name: 'new PrismaClient', lines: [4] },
4050
+ assert.deepEqual(match?.related_nodes, [
4051
+ { kind: 'member', name: 'extends React.Component', lines: [3] },
4052
+ { kind: 'member', name: 'import from react', lines: [1] },
852
4053
  ]);
853
- assert.deepEqual(match.lines, [1, 3, 4]);
854
- assert.match(match.why, /DIP/i);
855
- assert.match(match.impact, /tests aislados/i);
856
- assert.match(match.expected_fix, /adapter|puerto|abstracci/i);
4054
+ assert.deepEqual(match?.lines, [1, 3, 4]);
4055
+ assert.match(match?.why ?? '', /componente funcional/i);
4056
+ assert.match(match?.impact ?? '', /hooks/i);
4057
+ assert.match(match?.expected_fix ?? '', /custom hooks/i);
857
4058
  });
858
4059
 
859
- test('hasLargeClassDeclaration detecta god class por mezcla semantica de responsabilidades', () => {
860
- const godClassAst = {
4060
+ test('findReactClassComponentLines devuelve lineas de class components de React', () => {
4061
+ const ast = {
4062
+ type: 'Program',
4063
+ body: [
4064
+ {
4065
+ type: 'ImportDeclaration',
4066
+ loc: { start: { line: 1 }, end: { line: 1 } },
4067
+ source: { type: 'StringLiteral', value: 'react' },
4068
+ specifiers: [
4069
+ {
4070
+ type: 'ImportDefaultSpecifier',
4071
+ local: { type: 'Identifier', name: 'React' },
4072
+ },
4073
+ ],
4074
+ },
4075
+ {
4076
+ type: 'ClassDeclaration',
4077
+ id: { type: 'Identifier', name: 'LegacyCounter' },
4078
+ superClass: {
4079
+ type: 'MemberExpression',
4080
+ computed: false,
4081
+ object: { type: 'Identifier', name: 'React' },
4082
+ property: { type: 'Identifier', name: 'Component' },
4083
+ },
4084
+ loc: { start: { line: 4 }, end: { line: 18 } },
4085
+ body: { type: 'ClassBody', body: [] },
4086
+ },
4087
+ ],
4088
+ };
4089
+
4090
+ assert.deepEqual(findReactClassComponentLines(ast), [4]);
4091
+ });
4092
+
4093
+ test('hasSingletonPattern detecta constructor privado y singleton estatico', () => {
4094
+ const singletonAst = {
861
4095
  type: 'ClassDeclaration',
862
4096
  loc: {
863
4097
  start: { line: 1 },
864
- end: { line: 80 },
4098
+ end: { line: 20 },
865
4099
  },
4100
+ id: { type: 'Identifier', name: 'SingletonService' },
866
4101
  body: {
867
4102
  type: 'ClassBody',
868
4103
  body: [
4104
+ {
4105
+ type: 'ClassMethod',
4106
+ kind: 'constructor',
4107
+ accessibility: 'private',
4108
+ key: { type: 'Identifier', name: 'constructor' },
4109
+ loc: { start: { line: 2 }, end: { line: 4 } },
4110
+ body: { type: 'BlockStatement', body: [] },
4111
+ },
869
4112
  {
870
4113
  type: 'ClassProperty',
871
- key: { type: 'Identifier', name: 'client' },
4114
+ static: true,
4115
+ key: { type: 'Identifier', name: 'instance' },
4116
+ loc: { start: { line: 5 }, end: { line: 5 } },
872
4117
  value: {
873
4118
  type: 'NewExpression',
874
- callee: { type: 'Identifier', name: 'PrismaClient' },
4119
+ callee: { type: 'Identifier', name: 'SingletonService' },
875
4120
  arguments: [],
876
4121
  },
877
4122
  },
4123
+ ],
4124
+ },
4125
+ };
4126
+ const utilityAst = {
4127
+ type: 'ClassDeclaration',
4128
+ loc: {
4129
+ start: { line: 1 },
4130
+ end: { line: 18 },
4131
+ },
4132
+ id: { type: 'Identifier', name: 'UtilityService' },
4133
+ body: {
4134
+ type: 'ClassBody',
4135
+ body: [
878
4136
  {
879
4137
  type: 'ClassMethod',
880
- key: { type: 'Identifier', name: 'getOrder' },
881
- loc: { start: { line: 20 }, end: { line: 24 } },
4138
+ kind: 'constructor',
4139
+ accessibility: 'private',
4140
+ key: { type: 'Identifier', name: 'constructor' },
4141
+ loc: { start: { line: 2 }, end: { line: 4 } },
882
4142
  body: { type: 'BlockStatement', body: [] },
883
4143
  },
884
4144
  {
885
4145
  type: 'ClassMethod',
886
- key: { type: 'Identifier', name: 'saveOrder' },
887
- loc: { start: { line: 30 }, end: { line: 40 } },
4146
+ static: true,
4147
+ key: { type: 'Identifier', name: 'format' },
4148
+ loc: { start: { line: 6 }, end: { line: 8 } },
888
4149
  body: { type: 'BlockStatement', body: [] },
889
4150
  },
890
4151
  ],
891
4152
  },
892
4153
  };
893
- const oversizedButSingleResponsibilityAst = {
894
- type: 'ClassDeclaration',
895
- loc: {
896
- start: { line: 1 },
897
- end: { line: 1_000 },
4154
+
4155
+ assert.equal(hasSingletonPattern(singletonAst), true);
4156
+ const match = findSingletonPatternMatch(singletonAst);
4157
+ assert.ok(match);
4158
+ assert.deepEqual(match.primary_node, {
4159
+ kind: 'class',
4160
+ name: 'SingletonService',
4161
+ lines: [1],
4162
+ });
4163
+ assert.deepEqual(match.related_nodes, [
4164
+ { kind: 'member', name: 'private constructor', lines: [2] },
4165
+ { kind: 'member', name: 'static instance', lines: [5] },
4166
+ ]);
4167
+ assert.equal(hasSingletonPattern(utilityAst), false);
4168
+ });
4169
+
4170
+ test('hasMagicNumberPattern detecta literales numericos en runtime y omite enums o tipos', () => {
4171
+ const magicNumberAst = {
4172
+ type: 'FunctionDeclaration',
4173
+ id: { type: 'Identifier', name: 'buildRetryPolicy' },
4174
+ loc: { start: { line: 1 }, end: { line: 8 } },
4175
+ body: {
4176
+ type: 'BlockStatement',
4177
+ body: [
4178
+ {
4179
+ type: 'ReturnStatement',
4180
+ loc: { start: { line: 4 }, end: { line: 4 } },
4181
+ argument: {
4182
+ type: 'NumericLiteral',
4183
+ value: 3,
4184
+ loc: { start: { line: 4 }, end: { line: 4 } },
4185
+ },
4186
+ },
4187
+ ],
4188
+ },
4189
+ };
4190
+ const enumAst = {
4191
+ type: 'TSEnumMember',
4192
+ id: { type: 'Identifier', name: 'RetryPolicy' },
4193
+ initializer: {
4194
+ type: 'NumericLiteral',
4195
+ value: 3,
4196
+ loc: { start: { line: 12 }, end: { line: 12 } },
4197
+ },
4198
+ };
4199
+
4200
+ assert.equal(hasMagicNumberPattern(magicNumberAst), true);
4201
+ const match = findMagicNumberPatternMatch(magicNumberAst);
4202
+ assert.ok(match);
4203
+ assert.deepEqual(match.primary_node, {
4204
+ kind: 'member',
4205
+ name: 'buildRetryPolicy',
4206
+ lines: [1],
4207
+ });
4208
+ assert.deepEqual(match.related_nodes, [
4209
+ { kind: 'member', name: 'numeric literal: 3', lines: [4] },
4210
+ ]);
4211
+ assert.equal(hasMagicNumberPattern(enumAst), false);
4212
+ assert.equal(findMagicNumberPatternMatch(enumAst), undefined);
4213
+ });
4214
+
4215
+ test('hasHardcodedValuePattern detecta literals de configuracion y omite valores neutros', () => {
4216
+ const hardcodedConfigAst = {
4217
+ type: 'VariableDeclarator',
4218
+ id: { type: 'Identifier', name: 'apiBaseUrl' },
4219
+ init: {
4220
+ type: 'StringLiteral',
4221
+ value: 'https://api.example.com',
4222
+ loc: { start: { line: 3 }, end: { line: 3 } },
4223
+ },
4224
+ loc: { start: { line: 3 }, end: { line: 3 } },
4225
+ };
4226
+ const neutralLiteralAst = {
4227
+ type: 'VariableDeclarator',
4228
+ id: { type: 'Identifier', name: 'count' },
4229
+ init: {
4230
+ type: 'NumericLiteral',
4231
+ value: 3,
4232
+ loc: { start: { line: 9 }, end: { line: 9 } },
898
4233
  },
4234
+ loc: { start: { line: 9 }, end: { line: 9 } },
4235
+ };
4236
+
4237
+ assert.equal(hasHardcodedValuePattern(hardcodedConfigAst), true);
4238
+ const match = findHardcodedValuePatternMatch(hardcodedConfigAst);
4239
+ assert.ok(match);
4240
+ assert.deepEqual(match.primary_node, {
4241
+ kind: 'member',
4242
+ name: 'apiBaseUrl',
4243
+ lines: [3],
4244
+ });
4245
+ assert.deepEqual(match.related_nodes, [
4246
+ { kind: 'member', name: 'hardcoded value: https://api.example.com', lines: [3] },
4247
+ ]);
4248
+ assert.equal(hasHardcodedValuePattern(neutralLiteralAst), false);
4249
+ });
4250
+
4251
+ test('hasHardcodedValuePattern usa tokens exactos y no subcadenas accidentales', () => {
4252
+ const reportFunctionAst = {
4253
+ type: 'FunctionDeclaration',
4254
+ id: { type: 'Identifier', name: 'formatVintageEvidenceReportLines' },
4255
+ loc: { start: { line: 1 }, end: { line: 5 } },
899
4256
  body: {
900
- type: 'ClassBody',
4257
+ type: 'BlockStatement',
901
4258
  body: [
902
4259
  {
903
- type: 'ClassMethod',
904
- key: { type: 'Identifier', name: 'getOrder' },
905
- loc: { start: { line: 20 }, end: { line: 24 } },
906
- body: { type: 'BlockStatement', body: [] },
4260
+ type: 'ReturnStatement',
4261
+ argument: {
4262
+ type: 'StringLiteral',
4263
+ value: 'Advanced Project Audit',
4264
+ loc: { start: { line: 3 }, end: { line: 3 } },
4265
+ },
907
4266
  },
908
4267
  ],
909
4268
  },
910
4269
  };
911
- const srpOnlyAst = {
912
- type: 'ClassDeclaration',
913
- loc: {
914
- start: { line: 1 },
915
- end: { line: 80 },
4270
+ const uiKeyAst = {
4271
+ type: 'ObjectProperty',
4272
+ key: { type: 'Identifier', name: 'key' },
4273
+ value: {
4274
+ type: 'StringLiteral',
4275
+ value: 'ast-intelligence-legacy',
4276
+ loc: { start: { line: 8 }, end: { line: 8 } },
4277
+ },
4278
+ loc: { start: { line: 8 }, end: { line: 8 } },
4279
+ };
4280
+ const configModuleDetectorAst = {
4281
+ type: 'FunctionDeclaration',
4282
+ id: { type: 'Identifier', name: 'isValidationConfigModuleCallExpression' },
4283
+ body: {
4284
+ type: 'BlockStatement',
4285
+ body: [
4286
+ {
4287
+ type: 'ReturnStatement',
4288
+ argument: {
4289
+ type: 'LogicalExpression',
4290
+ left: {
4291
+ type: 'BinaryExpression',
4292
+ left: { type: 'Identifier', name: 'objectName' },
4293
+ operator: '===',
4294
+ right: { type: 'StringLiteral', value: 'ConfigModule', loc: { start: { line: 16 }, end: { line: 16 } } },
4295
+ },
4296
+ operator: '&&',
4297
+ right: {
4298
+ type: 'BinaryExpression',
4299
+ left: { type: 'Identifier', name: 'propertyName' },
4300
+ operator: '===',
4301
+ right: { type: 'StringLiteral', value: 'forRoot', loc: { start: { line: 16 }, end: { line: 16 } } },
4302
+ },
4303
+ },
4304
+ },
4305
+ ],
916
4306
  },
4307
+ };
4308
+ const apiVersionDetectorAst = {
4309
+ type: 'VariableDeclaration',
4310
+ declarations: [
4311
+ {
4312
+ type: 'VariableDeclarator',
4313
+ id: { type: 'Identifier', name: 'apiVersionDecoratorNames' },
4314
+ init: {
4315
+ type: 'NewExpression',
4316
+ callee: { type: 'Identifier', name: 'Set' },
4317
+ arguments: [
4318
+ {
4319
+ type: 'ArrayExpression',
4320
+ elements: [
4321
+ { type: 'StringLiteral', value: 'controller', loc: { start: { line: 24 }, end: { line: 24 } } },
4322
+ { type: 'StringLiteral', value: 'version', loc: { start: { line: 24 }, end: { line: 24 } } },
4323
+ ],
4324
+ },
4325
+ ],
4326
+ },
4327
+ },
4328
+ ],
4329
+ };
4330
+ const passwordDetectorAst = {
4331
+ type: 'FunctionDeclaration',
4332
+ id: { type: 'Identifier', name: 'isPasswordHashingCallExpression' },
917
4333
  body: {
918
- type: 'ClassBody',
4334
+ type: 'BlockStatement',
919
4335
  body: [
920
4336
  {
921
- type: 'ClassMethod',
922
- key: { type: 'Identifier', name: 'getOrder' },
923
- loc: { start: { line: 20 }, end: { line: 24 } },
924
- body: { type: 'BlockStatement', body: [] },
4337
+ type: 'IfStatement',
4338
+ test: {
4339
+ type: 'BinaryExpression',
4340
+ left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'node' }, property: { type: 'Identifier', name: 'type' } },
4341
+ operator: '!==',
4342
+ right: { type: 'StringLiteral', value: 'CallExpression', loc: { start: { line: 31 }, end: { line: 31 } } },
4343
+ },
925
4344
  },
926
4345
  {
927
- type: 'ClassMethod',
928
- key: { type: 'Identifier', name: 'saveOrder' },
929
- loc: { start: { line: 30 }, end: { line: 40 } },
930
- body: { type: 'BlockStatement', body: [] },
4346
+ type: 'ReturnStatement',
4347
+ argument: {
4348
+ type: 'BinaryExpression',
4349
+ left: { type: 'UnaryExpression', operator: 'typeof', argument: { type: 'Identifier', name: 'objectName' } },
4350
+ operator: '===',
4351
+ right: { type: 'StringLiteral', value: 'string', loc: { start: { line: 34 }, end: { line: 34 } } },
4352
+ },
4353
+ },
4354
+ {
4355
+ type: 'IfStatement',
4356
+ test: {
4357
+ type: 'BinaryExpression',
4358
+ left: {
4359
+ type: 'MemberExpression',
4360
+ object: { type: 'Identifier', name: 'arguments' },
4361
+ property: { type: 'Identifier', name: 'length' },
4362
+ },
4363
+ operator: '===',
4364
+ right: { type: 'NumericLiteral', value: 0, loc: { start: { line: 37 }, end: { line: 37 } } },
4365
+ },
4366
+ },
4367
+ ],
4368
+ },
4369
+ };
4370
+ const timeoutDetectorAst = {
4371
+ type: 'FunctionDeclaration',
4372
+ id: { type: 'Identifier', name: 'hasSetTimeoutStringCallback' },
4373
+ body: {
4374
+ type: 'BlockStatement',
4375
+ body: [
4376
+ {
4377
+ type: 'ReturnStatement',
4378
+ argument: {
4379
+ type: 'BinaryExpression',
4380
+ left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'callee' }, property: { type: 'Identifier', name: 'name' } },
4381
+ operator: '!==',
4382
+ right: { type: 'StringLiteral', value: 'setTimeout', loc: { start: { line: 45 }, end: { line: 45 } } },
4383
+ },
4384
+ },
4385
+ {
4386
+ type: 'ReturnStatement',
4387
+ argument: {
4388
+ type: 'BinaryExpression',
4389
+ left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'firstArg' }, property: { type: 'Identifier', name: 'type' } },
4390
+ operator: '===',
4391
+ right: { type: 'StringLiteral', value: 'TemplateLiteral', loc: { start: { line: 48 }, end: { line: 48 } } },
4392
+ },
4393
+ },
4394
+ ],
4395
+ },
4396
+ };
4397
+ const hardcodedDetectorAst = {
4398
+ type: 'FunctionDeclaration',
4399
+ id: { type: 'Identifier', name: 'hasHardcodedConfigNameToken' },
4400
+ body: {
4401
+ type: 'BlockStatement',
4402
+ body: [
4403
+ {
4404
+ type: 'ReturnStatement',
4405
+ argument: {
4406
+ type: 'CallExpression',
4407
+ callee: {
4408
+ type: 'MemberExpression',
4409
+ object: { type: 'Identifier', name: 'tokenSet' },
4410
+ property: { type: 'Identifier', name: 'has' },
4411
+ },
4412
+ arguments: [{ type: 'StringLiteral', value: 'key', loc: { start: { line: 54 }, end: { line: 54 } } }],
4413
+ },
4414
+ },
4415
+ ],
4416
+ },
4417
+ };
4418
+ const processEnvDetectorAst = {
4419
+ type: 'FunctionDeclaration',
4420
+ id: { type: 'Identifier', name: 'isProcessEnvBaseAccess' },
4421
+ body: {
4422
+ type: 'BlockStatement',
4423
+ body: [
4424
+ {
4425
+ type: 'ReturnStatement',
4426
+ argument: {
4427
+ type: 'LogicalExpression',
4428
+ left: {
4429
+ type: 'BinaryExpression',
4430
+ left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'object' }, property: { type: 'Identifier', name: 'name' } },
4431
+ operator: '===',
4432
+ right: { type: 'StringLiteral', value: 'process', loc: { start: { line: 60 }, end: { line: 60 } } },
4433
+ },
4434
+ operator: '&&',
4435
+ right: {
4436
+ type: 'BinaryExpression',
4437
+ left: { type: 'MemberExpression', object: { type: 'Identifier', name: 'property' }, property: { type: 'Identifier', name: 'name' } },
4438
+ operator: '===',
4439
+ right: { type: 'StringLiteral', value: 'env', loc: { start: { line: 61 }, end: { line: 61 } } },
4440
+ },
4441
+ },
931
4442
  },
932
4443
  ],
933
4444
  },
934
4445
  };
935
4446
 
936
- assert.equal(hasLargeClassDeclaration(godClassAst), true);
937
- assert.equal(hasLargeClassDeclaration(oversizedButSingleResponsibilityAst), false);
938
- assert.equal(hasLargeClassDeclaration(srpOnlyAst), false);
4447
+ assert.equal(hasHardcodedValuePattern(reportFunctionAst), false);
4448
+ assert.equal(hasHardcodedValuePattern(uiKeyAst), false);
4449
+ assert.equal(hasHardcodedValuePattern(configModuleDetectorAst), false);
4450
+ assert.equal(hasHardcodedValuePattern(apiVersionDetectorAst), false);
4451
+ assert.equal(hasHardcodedValuePattern(passwordDetectorAst), false);
4452
+ assert.equal(hasHardcodedValuePattern(timeoutDetectorAst), false);
4453
+ assert.equal(hasHardcodedValuePattern(hardcodedDetectorAst), false);
4454
+ assert.equal(hasHardcodedValuePattern(processEnvDetectorAst), false);
4455
+ });
4456
+
4457
+ test('hasEnvDefaultFallbackPattern detecta defaults implícitos sobre process.env y descarta accesos directos', () => {
4458
+ const envDefaultAst = {
4459
+ type: 'LogicalExpression',
4460
+ operator: '||',
4461
+ left: {
4462
+ type: 'MemberExpression',
4463
+ computed: false,
4464
+ object: {
4465
+ type: 'MemberExpression',
4466
+ computed: false,
4467
+ object: { type: 'Identifier', name: 'process' },
4468
+ property: { type: 'Identifier', name: 'env' },
4469
+ },
4470
+ property: { type: 'Identifier', name: 'API_URL' },
4471
+ loc: { start: { line: 6 }, end: { line: 6 } },
4472
+ },
4473
+ right: {
4474
+ type: 'StringLiteral',
4475
+ value: 'http://localhost:3000',
4476
+ loc: { start: { line: 6 }, end: { line: 6 } },
4477
+ },
4478
+ loc: { start: { line: 6 }, end: { line: 6 } },
4479
+ };
4480
+ const directEnvAccessAst = {
4481
+ type: 'MemberExpression',
4482
+ computed: false,
4483
+ object: {
4484
+ type: 'MemberExpression',
4485
+ computed: false,
4486
+ object: { type: 'Identifier', name: 'process' },
4487
+ property: { type: 'Identifier', name: 'env' },
4488
+ },
4489
+ property: { type: 'Identifier', name: 'API_URL' },
4490
+ loc: { start: { line: 9 }, end: { line: 9 } },
4491
+ };
4492
+
4493
+ assert.equal(hasEnvDefaultFallbackPattern(envDefaultAst), true);
4494
+ const match = findEnvDefaultFallbackPatternMatch(envDefaultAst);
4495
+ assert.ok(match);
4496
+ assert.deepEqual(match.primary_node, {
4497
+ kind: 'member',
4498
+ name: 'API_URL',
4499
+ lines: [6],
4500
+ });
4501
+ assert.deepEqual(match.related_nodes, [
4502
+ { kind: 'member', name: 'fallback value: http://localhost:3000', lines: [6] },
4503
+ ]);
4504
+ assert.equal(hasEnvDefaultFallbackPattern(directEnvAccessAst), false);
939
4505
  });
940
4506
 
941
4507
  test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {