pumuki 6.3.297 → 6.3.299

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -113,12 +113,67 @@ Antes de realizar cualquier accion:
113
113
  - `release/*`: debe salir de `develop` y solo contener cambios de estabilizacion/release.
114
114
  - `hotfix/*`: debe salir de `main` para fixes urgentes de produccion.
115
115
  - Esta prohibido commitear en `main` y `develop` sin instruccion explicita del usuario.
116
+ - Ciclo de cierre de ramas obligatorio:
117
+ - una rama `feature/*`, `bugfix/*`, `chore/*`, `refactor/*` o `docs/*` terminada debe integrarse en `develop` mediante el flujo acordado;
118
+ - una rama `release/*` terminada debe integrarse en `main` y `develop`;
119
+ - una rama `hotfix/*` terminada debe integrarse en `main` y propagarse a `develop`;
120
+ - tras confirmar que la rama esta mergeada en sus destinos obligatorios, debe eliminarse la rama local y remota si existe;
121
+ - las ramas cerradas no pueden quedar acumuladas como backlog, marcador historico o pseudo-tracking.
122
+ - Criterio de borrado seguro de ramas:
123
+ - antes de borrar cualquier rama, comprobar si esta mergeada con `git branch --merged <destino>`;
124
+ - si no aparece como mergeada, no borrarla sin revisar commits no integrados;
125
+ - si contiene commits utiles no integrados, integrarlos, crear backup branch explicita o stash/patch documentado antes de eliminar;
126
+ - nunca usar borrado forzado de ramas (`git branch -D` o delete remoto forzado) sin instruccion explicita y evidencia de preservacion.
127
+ - La limpieza de ramas forma parte del cierre de cada slice:
128
+ - no se considera cerrada una task si deja worktrees temporales o ramas de trabajo ya mergeadas sin limpiar;
129
+ - si no se puede limpiar por riesgo, reportar `STATUS: BLOCKED` o dejar `NEXT instruction` explicita con la razon.
116
130
  - Si la rama actual no cumple naming o flujo:
117
131
  - detener implementacion,
118
132
  - declarar `STATUS: BLOCKED`,
119
133
  - explicar el conflicto de rama/flujo, y
120
134
  - pedir al usuario el cambio o confirmacion de rama antes de continuar.
121
135
 
136
+ ## Contrato hard de commits atomicos y limite de worktree (no negociable)
137
+ - Todo cambio debe cerrarse en commits atomicos por slice funcional, bug o contrato tecnico.
138
+ - Esta prohibido acumular cambios de varios subsistemas sin commit intermedio.
139
+ - Un commit atomico debe tener una unica intencion verificable, por ejemplo:
140
+ - un detector AST/nodal concreto y sus tests,
141
+ - una correccion de gate/lifecycle concreta y sus tests,
142
+ - una mejora de notificacion concreta y sus tests,
143
+ - una actualizacion de tracking/docs estrictamente asociada a esa slice,
144
+ - una preparacion de release/versionado separada del codigo funcional.
145
+ - Esta prohibido mezclar en el mismo commit:
146
+ - detectores AST con versionado/release,
147
+ - cambios de lifecycle/gate con cambios de menu/runtime no relacionados,
148
+ - cambios de consumers con cambios internos de Pumuki,
149
+ - tracking amplio con implementacion funcional no relacionada,
150
+ - limpieza de ramas/worktrees con codigo de producto.
151
+ - Umbral hard de higiene:
152
+ - si `git status --short` muestra mas de 24 rutas pendientes, no se permite seguir implementando;
153
+ - si muestra mas de 12 rutas pendientes, se debe evaluar si la slice ya debe cerrarse;
154
+ - si existen archivos `??` fuera del alcance de la slice, deben clasificarse antes de continuar.
155
+ - Si el worktree supera el umbral hard:
156
+ - declarar `STATUS: BLOCKED`,
157
+ - detener nuevas ediciones funcionales,
158
+ - listar cambios por subsistema,
159
+ - crear commits atomicos separados o stashes con nombre explicito antes de continuar,
160
+ - no usar un commit masivo para "limpiar" el estado.
161
+ - Antes de cada commit:
162
+ - revisar `git diff --cached --name-status`,
163
+ - confirmar que todos los archivos staged pertenecen a la misma intencion,
164
+ - dejar fuera del stage cualquier archivo no relacionado,
165
+ - documentar en la respuesta la relacion `escenario -> tests -> evidencia -> task`.
166
+ - Si los hooks bloquean por deuda ajena al staged:
167
+ - no ampliar el commit para "aprovechar",
168
+ - reportar la causa exacta,
169
+ - corregir Pumuki si es bug del gate,
170
+ - o aislar la slice si es deuda del worktree.
171
+ - Las ramas locales y worktrees temporales deben revisarse con inventario antes de borrarse.
172
+ - Esta prohibido borrar ramas, worktrees, stashes o directorios temporales con cambios sin:
173
+ - comprobar estado,
174
+ - preservar cambios en commit o stash con nombre explicito,
175
+ - y reportar la evidencia.
176
+
122
177
  ## Gate operativo obligatorio (antes de editar codigo)
123
178
  - Declarar internamente las skills aplicables y tratarlas como activas durante TODO el turno.
124
179
  - Verificar cumplimiento minimo previo:
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.298] - 2026-05-19
4
+
5
+ ## [6.3.299] - 2026-05-19
6
+
7
+ - Release line for INC145 AST/skills actionable next actions. Block summaries now emit rule, file, line and expected fix for any unmapped AST/skills finding instead of falling back to a generic primary-blocker message, while preserving specific mappings such as console.log.
8
+
9
+ - Release line for the merged notification/evidence/context cleanup on develop. Keeps the published package unique after 6.3.297 was already present on npm, preserving the actionable notification payloads, expanded AST detector coverage, PRE_WRITE lease/evidence cause propagation, lifecycle hook alignment, local context gate scaffold and atomic Git hygiene rules.
10
+
3
11
  ## [6.3.297] - 2026-05-19
4
12
 
5
13
  - `PUMUKI-INC-157`: staged no-code/documentation/repin scopes no longer fail the skills contract with `EVIDENCE_SKILLS_PLATFORMS_UNDETECTED`. When the effective staged scope has no iOS, Android, backend or frontend code paths, platform skills are reported as `NOT_APPLICABLE` instead of blocking, while code-bearing slices keep the hard skills gate.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.297
1
+ 6.3.299
@@ -8,6 +8,7 @@ import {
8
8
  findSwiftPresentationSrpMatch,
9
9
  collectSwiftModernizableXCTestSuiteLines,
10
10
  collectSwiftLegacyExpectationDescriptionLines,
11
+ collectSwiftDirectSUTInstantiationWithoutMakeSUTLines,
11
12
  collectSwiftMakeSUTWithoutMemoryTrackingLines,
12
13
  collectSwiftMixedTestingFrameworkLines,
13
14
  collectSwiftQuickNimbleLines,
@@ -36,6 +37,10 @@ import {
36
37
  hasSwiftNSErrorThrowUsage,
37
38
  collectSwiftPackageBranchDependencyLines,
38
39
  hasSwiftPackageBranchDependencyUsage,
40
+ collectSwiftPackageToolsVersionBelow62Lines,
41
+ hasSwiftPackageToolsVersionBelow62Usage,
42
+ collectSwiftStrictConcurrencyBelowCompleteLines,
43
+ hasSwiftStrictConcurrencyBelowCompleteUsage,
39
44
  hasSwiftForEachIndicesUsage,
40
45
  hasSwiftForEachSelfIdentityUsage,
41
46
  collectSwiftForceCastLines,
@@ -61,7 +66,9 @@ import {
61
66
  collectSwiftHardcodedUiStringLines,
62
67
  collectSwiftNonLazyScrollForEachLines,
63
68
  collectSwiftUiForEachConditionalViewCountLines,
69
+ collectSwiftAnimationWithoutReduceMotionLines,
64
70
  collectSwiftUiInlineActionLogicLines,
71
+ collectSwiftOnTapGestureWithoutButtonTraitLines,
65
72
  collectSwiftForEachIndicesLines,
66
73
  collectSwiftForEachSelfIdentityLines,
67
74
  collectSwiftInlineForEachTransformLines,
@@ -79,6 +86,7 @@ import {
79
86
  hasSwiftEnvironmentObjectUsage,
80
87
  hasSwiftLowContrastStaticColorPairUsage,
81
88
  hasSwiftMainThreadBlockingSleepUsage,
89
+ hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage,
82
90
  hasSwiftMakeSUTWithoutMemoryTrackingUsage,
83
91
  hasSwiftMassiveViewControllerResponsibilityUsage,
84
92
  hasSwiftMagicNumberLayoutUsage,
@@ -88,8 +96,13 @@ import {
88
96
  hasSwiftNestedIfPyramidUsage,
89
97
  hasSwiftNonLazyScrollForEachUsage,
90
98
  hasSwiftUiForEachConditionalViewCountUsage,
99
+ hasSwiftAnimationWithoutReduceMotionUsage,
91
100
  hasSwiftViewBodyObjectCreationUsage,
92
101
  hasSwiftUiImageDataDecodingUsage,
102
+ collectSwiftUiManualRenderingWithoutImageRendererLines,
103
+ hasSwiftUiManualRenderingWithoutImageRendererUsage,
104
+ collectSwiftLargeViewBuilderFunctionLines,
105
+ hasSwiftLargeViewBuilderFunctionUsage,
93
106
  hasSwiftUiInlineActionLogicUsage,
94
107
  hasSwiftAssumeIsolatedUsage,
95
108
  hasSwiftCoreDataLayerLeakUsage,
@@ -110,6 +123,7 @@ import {
110
123
  hasSwiftOnAppearTaskUsage,
111
124
  collectSwiftOnAppearTaskLines,
112
125
  hasSwiftOnTapGestureUsage,
126
+ hasSwiftOnTapGestureWithoutButtonTraitUsage,
113
127
  hasSwiftOperationQueueUsage,
114
128
  hasSwiftContainsUserFilterUsage,
115
129
  hasSwiftCustomSingletonUsage,
@@ -144,6 +158,12 @@ import {
144
158
  hasSwiftTabItemUsage,
145
159
  hasSwiftTaskDetachedUsage,
146
160
  hasSwiftUnownedSelfCaptureUsage,
161
+ collectSwiftManualMemoryManagementLines,
162
+ hasSwiftManualMemoryManagementUsage,
163
+ collectSwiftNonPascalCaseTypeDeclarationLines,
164
+ hasSwiftNonPascalCaseTypeDeclarationUsage,
165
+ collectSwiftCellCreationWithoutReuseLines,
166
+ hasSwiftCellCreationWithoutReuseUsage,
147
167
  hasSwiftWaitForExpectationsUsage,
148
168
  hasSwiftWarningSuppressionUsage,
149
169
  hasSwiftUIScreenMainBoundsUsage,
@@ -234,6 +254,87 @@ let ignored = "[unowned self]"
234
254
  assert.equal(hasSwiftUnownedSelfCaptureUsage(safe), false);
235
255
  });
236
256
 
257
+ test('hasSwiftManualMemoryManagementUsage detecta bypass manual de ARC', () => {
258
+ const source = `
259
+ final class LegacyBridge {
260
+ func load(_ pointer: UnsafeRawPointer) {
261
+ let value = Unmanaged<NSObject>.fromOpaque(pointer).takeRetainedValue()
262
+ CFRetain(value)
263
+ CFRelease(value)
264
+ }
265
+ }
266
+ `;
267
+ const safe = `
268
+ final class NativeBridge {
269
+ private let value = NSObject()
270
+ func load() -> NSObject {
271
+ let sample = "Unmanaged<NSObject>.fromOpaque(pointer)"
272
+ // CFRelease(value)
273
+ return value
274
+ }
275
+ }
276
+ `;
277
+
278
+ assert.equal(hasSwiftManualMemoryManagementUsage(source), true);
279
+ assert.deepEqual(collectSwiftManualMemoryManagementLines(source), [4, 5, 6]);
280
+ assert.equal(hasSwiftManualMemoryManagementUsage(safe), false);
281
+ assert.deepEqual(collectSwiftManualMemoryManagementLines(safe), []);
282
+ });
283
+
284
+ test('hasSwiftNonPascalCaseTypeDeclarationUsage detecta tipos Swift sin PascalCase', () => {
285
+ const source = `
286
+ struct user_profile {}
287
+ final class orderView {}
288
+ protocol _service {}
289
+ enum CheckoutState {}
290
+ actor CartActor {}
291
+ `;
292
+ const safe = `
293
+ struct UserProfile {}
294
+ final class OrderView {}
295
+ protocol PaymentService {}
296
+ enum CheckoutState {}
297
+ actor CartActor {}
298
+ let sample = "struct user_profile {}"
299
+ // class orderView {}
300
+ `;
301
+
302
+ assert.equal(hasSwiftNonPascalCaseTypeDeclarationUsage(source), true);
303
+ assert.deepEqual(collectSwiftNonPascalCaseTypeDeclarationLines(source), [2, 3, 4]);
304
+ assert.equal(hasSwiftNonPascalCaseTypeDeclarationUsage(safe), false);
305
+ assert.deepEqual(collectSwiftNonPascalCaseTypeDeclarationLines(safe), []);
306
+ });
307
+
308
+ test('hasSwiftCellCreationWithoutReuseUsage detecta celdas UIKit sin reutilizacion', () => {
309
+ const source = `
310
+ final class OrdersDataSource: NSObject, UITableViewDataSource {
311
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
312
+ let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "OrderCell")
313
+ return cell
314
+ }
315
+
316
+ func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
317
+ return UICollectionViewCell(frame: .zero)
318
+ }
319
+ }
320
+ `;
321
+ const safe = `
322
+ final class OrdersDataSource: NSObject, UITableViewDataSource {
323
+ func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
324
+ let cell = tableView.dequeueReusableCell(withIdentifier: "OrderCell", for: indexPath)
325
+ return cell
326
+ }
327
+ }
328
+ let sample = "UITableViewCell(style: .subtitle, reuseIdentifier: nil)"
329
+ // return UICollectionViewCell(frame: .zero)
330
+ `;
331
+
332
+ assert.equal(hasSwiftCellCreationWithoutReuseUsage(source), true);
333
+ assert.deepEqual(collectSwiftCellCreationWithoutReuseLines(source), [4, 9]);
334
+ assert.equal(hasSwiftCellCreationWithoutReuseUsage(safe), false);
335
+ assert.deepEqual(collectSwiftCellCreationWithoutReuseLines(safe), []);
336
+ });
337
+
237
338
  test('hasSwiftNestedIfPyramidUsage detecta pyramid of doom y preserva guard clauses', () => {
238
339
  const source = `
239
340
  func submit() {
@@ -447,6 +548,52 @@ let sample = ".package(url: branch:)"
447
548
  assert.deepEqual(collectSwiftPackageBranchDependencyLines(safe), []);
448
549
  });
449
550
 
551
+ test('hasSwiftPackageToolsVersionBelow62Usage detecta Package.swift iOS bajo Swift 6.2', () => {
552
+ const source = `
553
+ // swift-tools-version: 5.9
554
+ import PackageDescription
555
+ `;
556
+ const sourceSixOne = `
557
+ // swift-tools-version:6.1
558
+ import PackageDescription
559
+ `;
560
+ const safe = `
561
+ // swift-tools-version: 6.2
562
+ import PackageDescription
563
+ // ordinary comment swift-tools-version: 5.9
564
+ let sample = "// swift-tools-version: 5.9"
565
+ `;
566
+
567
+ assert.equal(hasSwiftPackageToolsVersionBelow62Usage(source), true);
568
+ assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(source), [2]);
569
+ assert.equal(hasSwiftPackageToolsVersionBelow62Usage(sourceSixOne), true);
570
+ assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(sourceSixOne), [2]);
571
+ assert.equal(hasSwiftPackageToolsVersionBelow62Usage(safe), false);
572
+ assert.deepEqual(collectSwiftPackageToolsVersionBelow62Lines(safe), []);
573
+ });
574
+
575
+ test('hasSwiftStrictConcurrencyBelowCompleteUsage detecta Xcode strict concurrency inferior a complete', () => {
576
+ const source = `
577
+ buildSettings = {
578
+ SWIFT_STRICT_CONCURRENCY = targeted;
579
+ OTHER_SWIFT_FLAGS = "$(inherited)";
580
+ SWIFT_STRICT_CONCURRENCY = minimal;
581
+ };
582
+ `;
583
+ const safe = `
584
+ buildSettings = {
585
+ SWIFT_STRICT_CONCURRENCY = complete;
586
+ let sample = "SWIFT_STRICT_CONCURRENCY = targeted;"
587
+ // SWIFT_STRICT_CONCURRENCY = minimal;
588
+ };
589
+ `;
590
+
591
+ assert.equal(hasSwiftStrictConcurrencyBelowCompleteUsage(source), true);
592
+ assert.deepEqual(collectSwiftStrictConcurrencyBelowCompleteLines(source), [3, 5]);
593
+ assert.equal(hasSwiftStrictConcurrencyBelowCompleteUsage(safe), false);
594
+ assert.deepEqual(collectSwiftStrictConcurrencyBelowCompleteLines(safe), []);
595
+ });
596
+
450
597
  test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
451
598
  const source = `
452
599
  struct FeedView: View {
@@ -575,6 +722,74 @@ struct AvatarView: View {
575
722
  assert.equal(hasSwiftUiImageDataDecodingUsage(safe), false);
576
723
  });
577
724
 
725
+ test('hasSwiftUiManualRenderingWithoutImageRendererUsage detecta render manual de SwiftUI sin ImageRenderer', () => {
726
+ const source = `
727
+ struct ReceiptExporter {
728
+ func image(from receipt: ReceiptView) -> UIImage {
729
+ let controller = UIHostingController(rootView: receipt)
730
+ let renderer = UIGraphicsImageRenderer(size: controller.view.bounds.size)
731
+ return renderer.image { _ in
732
+ controller.view.drawHierarchy(in: controller.view.bounds, afterScreenUpdates: true)
733
+ }
734
+ }
735
+ }
736
+ `;
737
+ const safe = `
738
+ struct ReceiptExporter {
739
+ func image(from receipt: ReceiptView) -> UIImage? {
740
+ let renderer = ImageRenderer(content: receipt)
741
+ let sample = "UIHostingController(rootView: receipt)"
742
+ // UIGraphicsImageRenderer(size: .zero)
743
+ return renderer.uiImage
744
+ }
745
+ }
746
+ `;
747
+
748
+ assert.equal(hasSwiftUiManualRenderingWithoutImageRendererUsage(source), true);
749
+ assert.deepEqual(collectSwiftUiManualRenderingWithoutImageRendererLines(source), [4, 5, 7]);
750
+ assert.equal(hasSwiftUiManualRenderingWithoutImageRendererUsage(safe), false);
751
+ assert.deepEqual(collectSwiftUiManualRenderingWithoutImageRendererLines(safe), []);
752
+ });
753
+
754
+ test('hasSwiftLargeViewBuilderFunctionUsage detecta @ViewBuilder functions grandes y preserva secciones pequenas', () => {
755
+ const source = `
756
+ struct CheckoutView: View {
757
+ @ViewBuilder
758
+ private func checkoutSections() -> some View {
759
+ HeaderView()
760
+ SummaryView()
761
+ AddressView()
762
+ PaymentView()
763
+ CouponView()
764
+ LoyaltyView()
765
+ DeliveryView()
766
+ NotesView()
767
+ LegalView()
768
+ FooterView()
769
+ SupportView()
770
+ TrackingView()
771
+ ConfirmationView()
772
+ }
773
+ }
774
+ `;
775
+ const safe = `
776
+ struct CheckoutView: View {
777
+ @ViewBuilder
778
+ private func header() -> some View {
779
+ HeaderView()
780
+ SubtitleView()
781
+ let sample = "@ViewBuilder func huge() { A() B() C() }"
782
+ // @ViewBuilder func huge() { A() B() C() }
783
+ }
784
+ }
785
+ `;
786
+
787
+ assert.equal(hasSwiftLargeViewBuilderFunctionUsage(source), true);
788
+ assert.deepEqual(collectSwiftLargeViewBuilderFunctionLines(source), [3, 4]);
789
+ assert.equal(hasSwiftLargeViewBuilderFunctionUsage(safe), false);
790
+ assert.deepEqual(collectSwiftLargeViewBuilderFunctionLines(safe), []);
791
+ });
792
+
578
793
  test('hasSwiftUiInlineActionLogicUsage detecta lógica inline en Button y preserva método referenciado', () => {
579
794
  const source = `
580
795
  struct CheckoutView: View {
@@ -612,6 +827,42 @@ struct CheckoutView: View {
612
827
  assert.deepEqual(collectSwiftUiInlineActionLogicLines(safe), []);
613
828
  });
614
829
 
830
+ test('hasSwiftAnimationWithoutReduceMotionUsage detecta animaciones sin preferencia reduce motion', () => {
831
+ const source = `
832
+ struct CheckoutView: View {
833
+ @State private var expanded = false
834
+
835
+ var body: some View {
836
+ Text("Checkout")
837
+ .animation(.spring(), value: expanded)
838
+ .onTapGesture {
839
+ withAnimation {
840
+ expanded.toggle()
841
+ }
842
+ }
843
+ }
844
+ }
845
+ `;
846
+ const safe = `
847
+ struct CheckoutView: View {
848
+ @Environment(\\.accessibilityReduceMotion) private var reduceMotion
849
+ @State private var expanded = false
850
+
851
+ var body: some View {
852
+ Text("Checkout")
853
+ .animation(reduceMotion ? nil : .spring(), value: expanded)
854
+ let sample = "withAnimation { expanded.toggle() }"
855
+ // .animation(.spring(), value: expanded)
856
+ }
857
+ }
858
+ `;
859
+
860
+ assert.equal(hasSwiftAnimationWithoutReduceMotionUsage(source), true);
861
+ assert.deepEqual(collectSwiftAnimationWithoutReduceMotionLines(source), [7, 9]);
862
+ assert.equal(hasSwiftAnimationWithoutReduceMotionUsage(safe), false);
863
+ assert.deepEqual(collectSwiftAnimationWithoutReduceMotionLines(safe), []);
864
+ });
865
+
615
866
  test('hasSwiftForEachSelfIdentityUsage detecta id self y preserva ids estables', () => {
616
867
  const source = `
617
868
  struct FeedView: View {
@@ -1671,6 +1922,38 @@ MainActor.assumeIsolated { reload() }
1671
1922
  assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
1672
1923
  });
1673
1924
 
1925
+ test('hasSwiftOnTapGestureWithoutButtonTraitUsage detecta tap no semantico sin trait de boton', () => {
1926
+ const source = `
1927
+ struct RowView: View {
1928
+ var body: some View {
1929
+ Image(systemName: "cart")
1930
+ .onTapGesture {
1931
+ addToCart()
1932
+ }
1933
+ }
1934
+ }
1935
+ `;
1936
+ const safe = `
1937
+ struct RowView: View {
1938
+ var body: some View {
1939
+ Image(systemName: "cart")
1940
+ .onTapGesture {
1941
+ addToCart()
1942
+ }
1943
+ .accessibilityAddTraits(.isButton)
1944
+
1945
+ let sample = ".onTapGesture { addToCart() }"
1946
+ // .onTapGesture { addToCart() }
1947
+ }
1948
+ }
1949
+ `;
1950
+
1951
+ assert.equal(hasSwiftOnTapGestureWithoutButtonTraitUsage(source), true);
1952
+ assert.deepEqual(collectSwiftOnTapGestureWithoutButtonTraitLines(source), [5]);
1953
+ assert.equal(hasSwiftOnTapGestureWithoutButtonTraitUsage(safe), false);
1954
+ assert.deepEqual(collectSwiftOnTapGestureWithoutButtonTraitLines(safe), []);
1955
+ });
1956
+
1674
1957
  test('detectores legacy ignoran strings y comentarios', () => {
1675
1958
  const source = `
1676
1959
  // Task.detached { }
@@ -2300,6 +2583,47 @@ let sample = "makeSUT() without trackForMemoryLeaks"
2300
2583
  assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(safe), []);
2301
2584
  });
2302
2585
 
2586
+ test('hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage detecta sut inline sin factory', () => {
2587
+ const source = `
2588
+ import XCTest
2589
+
2590
+ final class BuyerAuthScreenTests: XCTestCase {
2591
+ func testRendersTitle() {
2592
+ let sut = BuyerAuthScreen()
2593
+ XCTAssertNotNil(sut)
2594
+ }
2595
+
2596
+ func testRendersSubtitle() {
2597
+ var sut = BuyerAuthScreen(viewModel: BuyerAuthViewModel())
2598
+ XCTAssertNotNil(sut)
2599
+ }
2600
+ }
2601
+ `;
2602
+ const safe = `
2603
+ import XCTest
2604
+
2605
+ final class BuyerAuthScreenTests: XCTestCase {
2606
+ func testRendersTitle() {
2607
+ let sut = makeSUT()
2608
+ XCTAssertNotNil(sut)
2609
+ }
2610
+
2611
+ private func makeSUT() -> BuyerAuthScreen {
2612
+ BuyerAuthScreen()
2613
+ }
2614
+ }
2615
+
2616
+ let ignored = "let sut = BuyerAuthScreen()"
2617
+ // let sut = BuyerAuthScreen()
2618
+ let notSut = BuyerAuthScreen()
2619
+ `;
2620
+
2621
+ assert.equal(hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage(source), true);
2622
+ assert.deepEqual(collectSwiftDirectSUTInstantiationWithoutMakeSUTLines(source), [6, 11]);
2623
+ assert.equal(hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage(safe), false);
2624
+ assert.deepEqual(collectSwiftDirectSUTInstantiationWithoutMakeSUTLines(safe), []);
2625
+ });
2626
+
2303
2627
  test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
2304
2628
  const mixedSuite = `
2305
2629
  import XCTest