pumuki 6.3.296 → 6.3.298

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
+ - 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.
6
+
7
+ ## [6.3.297] - 2026-05-19
8
+
9
+ - `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.
10
+
3
11
  ## [6.3.296] - 2026-05-19
4
12
 
5
13
  - `PUMUKI-INC-156`: follow-up after RuralGo replay. Broad SwiftUI findings are now considered non-actionable even when they include a `primary_node` if that node spans many lines. Only a bounded AST/node range can hard-block a staged diff; file-level brownfield SwiftUI debt remains advisory for the atomic slice.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 6.3.294
1
+ 6.3.298
@@ -8,6 +8,8 @@ import {
8
8
  findSwiftPresentationSrpMatch,
9
9
  collectSwiftModernizableXCTestSuiteLines,
10
10
  collectSwiftLegacyExpectationDescriptionLines,
11
+ collectSwiftDirectSUTInstantiationWithoutMakeSUTLines,
12
+ collectSwiftMakeSUTWithoutMemoryTrackingLines,
11
13
  collectSwiftMixedTestingFrameworkLines,
12
14
  collectSwiftQuickNimbleLines,
13
15
  collectSwiftWaitForExpectationsLines,
@@ -35,6 +37,10 @@ import {
35
37
  hasSwiftNSErrorThrowUsage,
36
38
  collectSwiftPackageBranchDependencyLines,
37
39
  hasSwiftPackageBranchDependencyUsage,
40
+ collectSwiftPackageToolsVersionBelow62Lines,
41
+ hasSwiftPackageToolsVersionBelow62Usage,
42
+ collectSwiftStrictConcurrencyBelowCompleteLines,
43
+ hasSwiftStrictConcurrencyBelowCompleteUsage,
38
44
  hasSwiftForEachIndicesUsage,
39
45
  hasSwiftForEachSelfIdentityUsage,
40
46
  collectSwiftForceCastLines,
@@ -60,7 +66,9 @@ import {
60
66
  collectSwiftHardcodedUiStringLines,
61
67
  collectSwiftNonLazyScrollForEachLines,
62
68
  collectSwiftUiForEachConditionalViewCountLines,
69
+ collectSwiftAnimationWithoutReduceMotionLines,
63
70
  collectSwiftUiInlineActionLogicLines,
71
+ collectSwiftOnTapGestureWithoutButtonTraitLines,
64
72
  collectSwiftForEachIndicesLines,
65
73
  collectSwiftForEachSelfIdentityLines,
66
74
  collectSwiftInlineForEachTransformLines,
@@ -78,6 +86,8 @@ import {
78
86
  hasSwiftEnvironmentObjectUsage,
79
87
  hasSwiftLowContrastStaticColorPairUsage,
80
88
  hasSwiftMainThreadBlockingSleepUsage,
89
+ hasSwiftDirectSUTInstantiationWithoutMakeSUTUsage,
90
+ hasSwiftMakeSUTWithoutMemoryTrackingUsage,
81
91
  hasSwiftMassiveViewControllerResponsibilityUsage,
82
92
  hasSwiftMagicNumberLayoutUsage,
83
93
  hasSwiftMixedTestingFrameworksUsage,
@@ -86,8 +96,13 @@ import {
86
96
  hasSwiftNestedIfPyramidUsage,
87
97
  hasSwiftNonLazyScrollForEachUsage,
88
98
  hasSwiftUiForEachConditionalViewCountUsage,
99
+ hasSwiftAnimationWithoutReduceMotionUsage,
89
100
  hasSwiftViewBodyObjectCreationUsage,
90
101
  hasSwiftUiImageDataDecodingUsage,
102
+ collectSwiftUiManualRenderingWithoutImageRendererLines,
103
+ hasSwiftUiManualRenderingWithoutImageRendererUsage,
104
+ collectSwiftLargeViewBuilderFunctionLines,
105
+ hasSwiftLargeViewBuilderFunctionUsage,
91
106
  hasSwiftUiInlineActionLogicUsage,
92
107
  hasSwiftAssumeIsolatedUsage,
93
108
  hasSwiftCoreDataLayerLeakUsage,
@@ -108,6 +123,7 @@ import {
108
123
  hasSwiftOnAppearTaskUsage,
109
124
  collectSwiftOnAppearTaskLines,
110
125
  hasSwiftOnTapGestureUsage,
126
+ hasSwiftOnTapGestureWithoutButtonTraitUsage,
111
127
  hasSwiftOperationQueueUsage,
112
128
  hasSwiftContainsUserFilterUsage,
113
129
  hasSwiftCustomSingletonUsage,
@@ -128,6 +144,7 @@ import {
128
144
  hasSwiftExplicitColorStaticMemberUsage,
129
145
  hasSwiftClosureBasedViewBuilderContentUsage,
130
146
  collectSwiftForceUnwrapLines,
147
+ collectSwiftUIKitManualFrameLayoutLines,
131
148
  collectSwiftWarningSuppressionLines,
132
149
  hasSwiftLargeConfigContextViewPropertyUsage,
133
150
  hasSwiftUiConditionalSameViewIdentityUsage,
@@ -141,12 +158,19 @@ import {
141
158
  hasSwiftTabItemUsage,
142
159
  hasSwiftTaskDetachedUsage,
143
160
  hasSwiftUnownedSelfCaptureUsage,
161
+ collectSwiftManualMemoryManagementLines,
162
+ hasSwiftManualMemoryManagementUsage,
163
+ collectSwiftNonPascalCaseTypeDeclarationLines,
164
+ hasSwiftNonPascalCaseTypeDeclarationUsage,
165
+ collectSwiftCellCreationWithoutReuseLines,
166
+ hasSwiftCellCreationWithoutReuseUsage,
144
167
  hasSwiftWaitForExpectationsUsage,
145
168
  hasSwiftWarningSuppressionUsage,
146
169
  hasSwiftUIScreenMainBoundsUsage,
147
170
  hasSwiftXCTestAssertionUsage,
148
171
  hasSwiftXCTUnwrapUsage,
149
172
  hasSwiftUncheckedSendableUsage,
173
+ hasSwiftUIKitManualFrameLayoutUsage,
150
174
  } from './ios';
151
175
 
152
176
  test('hasSwiftForceUnwrap detecta force unwrap postfix en expresiones', () => {
@@ -230,6 +254,87 @@ let ignored = "[unowned self]"
230
254
  assert.equal(hasSwiftUnownedSelfCaptureUsage(safe), false);
231
255
  });
232
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
+
233
338
  test('hasSwiftNestedIfPyramidUsage detecta pyramid of doom y preserva guard clauses', () => {
234
339
  const source = `
235
340
  func submit() {
@@ -443,6 +548,52 @@ let sample = ".package(url: branch:)"
443
548
  assert.deepEqual(collectSwiftPackageBranchDependencyLines(safe), []);
444
549
  });
445
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
+
446
597
  test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
447
598
  const source = `
448
599
  struct FeedView: View {
@@ -571,6 +722,74 @@ struct AvatarView: View {
571
722
  assert.equal(hasSwiftUiImageDataDecodingUsage(safe), false);
572
723
  });
573
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
+
574
793
  test('hasSwiftUiInlineActionLogicUsage detecta lógica inline en Button y preserva método referenciado', () => {
575
794
  const source = `
576
795
  struct CheckoutView: View {
@@ -608,6 +827,42 @@ struct CheckoutView: View {
608
827
  assert.deepEqual(collectSwiftUiInlineActionLogicLines(safe), []);
609
828
  });
610
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
+
611
866
  test('hasSwiftForEachSelfIdentityUsage detecta id self y preserva ids estables', () => {
612
867
  const source = `
613
868
  struct FeedView: View {
@@ -1129,6 +1384,44 @@ struct ProfileView: View {
1129
1384
  assert.deepEqual(collectSwiftMagicNumberLayoutLines(constants), []);
1130
1385
  });
1131
1386
 
1387
+ test('hasSwiftUIKitManualFrameLayoutUsage detecta layout manual UIKit y preserva Auto Layout y SwiftUI frame', () => {
1388
+ const source = `
1389
+ final class CheckoutView: UIView {
1390
+ private let titleLabel = UILabel(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
1391
+
1392
+ func layoutBadge() {
1393
+ badgeView.frame = CGRect(x: 16, y: 16, width: 80, height: 32)
1394
+ }
1395
+ }
1396
+ `;
1397
+ const safe = `
1398
+ final class CheckoutView: UIView {
1399
+ private let titleLabel = UILabel()
1400
+
1401
+ func installConstraints() {
1402
+ titleLabel.translatesAutoresizingMaskIntoConstraints = false
1403
+ NSLayoutConstraint.activate([
1404
+ titleLabel.leadingAnchor.constraint(equalTo: leadingAnchor)
1405
+ ])
1406
+ }
1407
+ }
1408
+
1409
+ struct CheckoutSwiftUIView: View {
1410
+ var body: some View {
1411
+ Text("Checkout").frame(maxWidth: .infinity)
1412
+ }
1413
+ }
1414
+
1415
+ let sample = "UILabel(frame: CGRect(x: 0, y: 0, width: 10, height: 10))"
1416
+ // badgeView.frame = CGRect(x: 0, y: 0, width: 10, height: 10)
1417
+ `;
1418
+
1419
+ assert.equal(hasSwiftUIKitManualFrameLayoutUsage(source), true);
1420
+ assert.deepEqual(collectSwiftUIKitManualFrameLayoutLines(source), [3, 6]);
1421
+ assert.equal(hasSwiftUIKitManualFrameLayoutUsage(safe), false);
1422
+ assert.deepEqual(collectSwiftUIKitManualFrameLayoutLines(safe), []);
1423
+ });
1424
+
1132
1425
  test('detectores de logging iOS detectan logs ad-hoc y PII en produccion', () => {
1133
1426
  const adHoc = `
1134
1427
  print(user.id)
@@ -1629,6 +1922,38 @@ MainActor.assumeIsolated { reload() }
1629
1922
  assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
1630
1923
  });
1631
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
+
1632
1957
  test('detectores legacy ignoran strings y comentarios', () => {
1633
1958
  const source = `
1634
1959
  // Task.detached { }
@@ -2217,6 +2542,88 @@ final class BuyerOnboardingStringsTests: XCTestCase {
2217
2542
  assert.deepEqual(collectSwiftXCTUnwrapLines(brownfieldSpec), []);
2218
2543
  });
2219
2544
 
2545
+ test('hasSwiftMakeSUTWithoutMemoryTrackingUsage detecta specs con makeSUT sin trackForMemoryLeaks', () => {
2546
+ const source = `
2547
+ import XCTest
2548
+
2549
+ final class BuyerAuthScreenTests: XCTestCase {
2550
+ func testRendersTitle() {
2551
+ let sut = makeSUT()
2552
+ XCTAssertNotNil(sut)
2553
+ }
2554
+
2555
+ private func makeSUT() -> BuyerAuthScreen {
2556
+ BuyerAuthScreen()
2557
+ }
2558
+ }
2559
+ `;
2560
+ const safe = `
2561
+ import XCTest
2562
+
2563
+ final class BuyerAuthScreenTests: XCTestCase {
2564
+ func testRendersTitle() {
2565
+ let sut = makeSUT()
2566
+ XCTAssertNotNil(sut)
2567
+ }
2568
+
2569
+ private func makeSUT() -> BuyerAuthScreen {
2570
+ let sut = BuyerAuthScreen()
2571
+ trackForMemoryLeaks(sut)
2572
+ return sut
2573
+ }
2574
+ }
2575
+
2576
+ let sample = "makeSUT() without trackForMemoryLeaks"
2577
+ // private func makeSUT() -> Sample { Sample() }
2578
+ `;
2579
+
2580
+ assert.equal(hasSwiftMakeSUTWithoutMemoryTrackingUsage(source), true);
2581
+ assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(source), [6, 10]);
2582
+ assert.equal(hasSwiftMakeSUTWithoutMemoryTrackingUsage(safe), false);
2583
+ assert.deepEqual(collectSwiftMakeSUTWithoutMemoryTrackingLines(safe), []);
2584
+ });
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
+
2220
2627
  test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
2221
2628
  const mixedSuite = `
2222
2629
  import XCTest