pumuki 6.3.61 → 6.3.63

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 (43) hide show
  1. package/README.md +2 -0
  2. package/assets/rule-packs/ios-swiftui-modernization-v2.json +110 -0
  3. package/core/facts/detectors/text/ios.test.ts +333 -12
  4. package/core/facts/detectors/text/ios.ts +322 -3
  5. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.test.ts +16 -3
  6. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.ts +12 -3
  7. package/core/facts/extractHeuristicFacts.ts +21 -0
  8. package/core/rules/presets/heuristics/ios.test.ts +85 -3
  9. package/core/rules/presets/heuristics/ios.ts +310 -0
  10. package/docs/README.md +6 -5
  11. package/docs/codex-skills/core-data-expert.md +2 -1
  12. package/docs/codex-skills/swift-concurrency.md +3 -0
  13. package/docs/codex-skills/swift-testing-expert.md +6 -0
  14. package/docs/codex-skills/swiftui-expert-skill.md +1 -0
  15. package/docs/operations/RELEASE_NOTES.md +15 -1
  16. package/docs/rule-packs/ios.md +18 -1
  17. package/docs/validation/README.md +3 -32
  18. package/docs/validation/ios-avdlee-parity-matrix.md +48 -0
  19. package/integrations/config/skillsCompilerTemplates.test.ts +2 -0
  20. package/integrations/config/skillsCompilerTemplates.ts +157 -1
  21. package/integrations/config/skillsDetectorRegistry.ts +52 -0
  22. package/integrations/config/skillsLock.test.ts +3 -3
  23. package/integrations/config/skillsLock.ts +1 -1
  24. package/integrations/config/skillsMarkdownRules.ts +133 -2
  25. package/integrations/config/skillsPolicy.ts +2 -1
  26. package/integrations/config/skillsSources.test.ts +4 -4
  27. package/integrations/evidence/buildEvidence.ts +32 -0
  28. package/integrations/gate/stagePolicies.ts +591 -50
  29. package/integrations/lifecycle/cli.ts +37 -537
  30. package/integrations/lifecycle/cliOutputs.ts +14 -0
  31. package/integrations/lifecycle/cliSdd.ts +505 -0
  32. package/integrations/lifecycle/doctor.ts +13 -63
  33. package/integrations/lifecycle/install.ts +43 -8
  34. package/integrations/lifecycle/policyReconcile.ts +227 -217
  35. package/integrations/lifecycle/policyValidationSnapshot.ts +1 -7
  36. package/integrations/telemetry/gateTelemetry.ts +0 -4
  37. package/package.json +3 -1
  38. package/scripts/consumer-postinstall.cjs +59 -0
  39. package/scripts/framework-menu-system-notifications-config-choice.ts +2 -2
  40. package/scripts/framework-menu-system-notifications-config-state.ts +2 -2
  41. package/scripts/framework-menu-system-notifications-macos-dialog-enabled.ts +1 -1
  42. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +8 -1
  43. package/skills.lock.json +308 -172
package/README.md CHANGED
@@ -37,6 +37,8 @@ npx --yes pumuki status
37
37
  npx --yes pumuki doctor --json
38
38
  ```
39
39
 
40
+ Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta `pumuki install` automáticamente (hooks `pre-commit` / `pre-push`). No configura MCP del IDE por sí solo: usa `pumuki install --with-mcp` o el adaptador. Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`).
41
+
40
42
  Fallback (equivalent in pasos separados):
41
43
 
42
44
  ```bash
@@ -0,0 +1,110 @@
1
+ {
2
+ "snapshotId": "ios-swiftui-modernization-v2",
3
+ "version": "2.0.0",
4
+ "generatedAt": "2026-04-04T00:00:00.000Z",
5
+ "sourceSkill": "swiftui-expert-skill",
6
+ "sourceReferences": [
7
+ "vendor/skills/swiftui-expert-skill/references/modern-apis.md",
8
+ "vendor/skills/swiftui-expert-skill/references/text-formatting.md",
9
+ "vendor/skills/swiftui-expert-skill/references/scroll-patterns.md",
10
+ "vendor/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md"
11
+ ],
12
+ "entries": [
13
+ {
14
+ "id": "foreground-color",
15
+ "ruleId": "skills.ios.no-foreground-color",
16
+ "heuristicRuleId": "heuristics.ios.foreground-color.ast",
17
+ "category": "styling",
18
+ "legacyApi": ".foregroundColor(...)",
19
+ "modernApi": ".foregroundStyle(...)",
20
+ "rationale": "foregroundStyle supports hierarchical styles, gradients and materials.",
21
+ "confidence": "HIGH",
22
+ "minimumStage": "PRE_PUSH",
23
+ "minimumIos": "13.0",
24
+ "match": {
25
+ "kind": "regex",
26
+ "pattern": "\\.\\s*foregroundColor\\s*\\("
27
+ }
28
+ },
29
+ {
30
+ "id": "corner-radius",
31
+ "ruleId": "skills.ios.no-corner-radius",
32
+ "heuristicRuleId": "heuristics.ios.corner-radius.ast",
33
+ "category": "styling",
34
+ "legacyApi": ".cornerRadius(...)",
35
+ "modernApi": ".clipShape(.rect(cornerRadius: ...))",
36
+ "rationale": "cornerRadius is deprecated in modern SwiftUI and clipShape is explicit about the rendered shape.",
37
+ "confidence": "HIGH",
38
+ "minimumStage": "PRE_PUSH",
39
+ "minimumIos": "17.0",
40
+ "match": {
41
+ "kind": "regex",
42
+ "pattern": "\\.\\s*cornerRadius\\s*\\("
43
+ }
44
+ },
45
+ {
46
+ "id": "tab-item",
47
+ "ruleId": "skills.ios.no-tab-item",
48
+ "heuristicRuleId": "heuristics.ios.tab-item.ast",
49
+ "category": "tabs",
50
+ "legacyApi": ".tabItem { ... }",
51
+ "modernApi": "Tab { ... } label: { ... }",
52
+ "rationale": "The Tab API unlocks modern tab roles and avoids mixed syntax issues on iOS 18+.",
53
+ "confidence": "MEDIUM",
54
+ "minimumStage": "PRE_PUSH",
55
+ "minimumIos": "18.0",
56
+ "match": {
57
+ "kind": "regex",
58
+ "pattern": "\\.\\s*tabItem\\s*\\{"
59
+ }
60
+ },
61
+ {
62
+ "id": "scrollview-shows-indicators",
63
+ "ruleId": "skills.ios.no-scrollview-shows-indicators",
64
+ "heuristicRuleId": "heuristics.ios.scrollview-shows-indicators.ast",
65
+ "category": "scrolling",
66
+ "legacyApi": "ScrollView(..., showsIndicators: false)",
67
+ "modernApi": ".scrollIndicators(.hidden)",
68
+ "rationale": "The modifier-based API keeps the initializer clean and matches modern SwiftUI scrolling patterns.",
69
+ "confidence": "HIGH",
70
+ "minimumStage": "PRE_PUSH",
71
+ "minimumIos": "16.0",
72
+ "match": {
73
+ "kind": "regex",
74
+ "pattern": "\\bScrollView\\s*\\([\\s\\S]{0,80}showsIndicators\\s*:\\s*false\\b"
75
+ }
76
+ },
77
+ {
78
+ "id": "sheet-is-presented",
79
+ "ruleId": "skills.ios.no-sheet-is-presented",
80
+ "heuristicRuleId": "heuristics.ios.sheet-is-presented.ast",
81
+ "category": "presentation",
82
+ "legacyApi": ".sheet(isPresented: ...)",
83
+ "modernApi": ".sheet(item: ...)",
84
+ "rationale": "Item-based sheet presentation keeps model and presentation state aligned and avoids boolean drift in SwiftUI flows.",
85
+ "confidence": "MEDIUM",
86
+ "minimumStage": "PRE_PUSH",
87
+ "minimumIos": "13.0",
88
+ "match": {
89
+ "kind": "regex",
90
+ "pattern": "\\.\\s*sheet\\s*\\(\\s*isPresented\\s*:"
91
+ }
92
+ },
93
+ {
94
+ "id": "legacy-onchange",
95
+ "ruleId": "skills.ios.no-legacy-onchange",
96
+ "heuristicRuleId": "heuristics.ios.legacy-onchange.ast",
97
+ "category": "state",
98
+ "legacyApi": ".onChange(of: ...) { value in ... }",
99
+ "modernApi": ".onChange(of: ...) { oldValue, newValue in ... } or .onChange(of: ...) { ... }",
100
+ "rationale": "Modern onChange variants make value transitions explicit and remove the need for legacy single-value closure patterns.",
101
+ "confidence": "HIGH",
102
+ "minimumStage": "PRE_PUSH",
103
+ "minimumIos": "17.0",
104
+ "match": {
105
+ "kind": "regex",
106
+ "pattern": "\\.\\s*onChange\\s*\\(\\s*of\\s*:[\\s\\S]{0,160}?(?:,\\s*perform\\s*:\\s*\\{\\s*(?:\\[[^\\]]+\\]\\s*)?(?:\\(\\s*[A-Za-z_][A-Za-z0-9_]*(?:\\s*:\\s*[^,)]+)?\\s*\\)|[A-Za-z_][A-Za-z0-9_]*)\\s+in\\b|\\)\\s*\\{\\s*(?:\\[[^\\]]+\\]\\s*)?(?:\\(\\s*[A-Za-z_][A-Za-z0-9_]*(?:\\s*:\\s*[^,)]+)?\\s*\\)|[A-Za-z_][A-Za-z0-9_]*)\\s+in\\b)"
107
+ }
108
+ }
109
+ ]
110
+ }
@@ -12,21 +12,38 @@ import {
12
12
  hasSwiftDispatchGroupUsage,
13
13
  hasSwiftDispatchQueueUsage,
14
14
  hasSwiftDispatchSemaphoreUsage,
15
+ hasSwiftForEachIndicesUsage,
15
16
  hasSwiftForceCastUsage,
17
+ hasSwiftFontWeightBoldUsage,
16
18
  hasSwiftForegroundColorUsage,
17
19
  hasSwiftForceTryUsage,
18
20
  hasSwiftForceUnwrap,
21
+ hasSwiftGeometryReaderUsage,
22
+ hasSwiftLegacyOnChangeUsage,
23
+ hasSwiftLegacyExpectationDescriptionUsage,
24
+ hasSwiftLegacySwiftUiObservableWrapperUsage,
25
+ hasSwiftMixedTestingFrameworksUsage,
19
26
  hasSwiftLegacyXCTestImportUsage,
27
+ hasSwiftModernizableXCTestSuiteUsage,
28
+ hasSwiftAssumeIsolatedUsage,
29
+ hasSwiftCoreDataLayerLeakUsage,
30
+ hasSwiftNonisolatedUnsafeUsage,
20
31
  hasSwiftNSManagedObjectAsyncBoundaryUsage,
21
32
  hasSwiftNSManagedObjectBoundaryUsage,
33
+ hasSwiftNSManagedObjectStateLeakUsage,
22
34
  hasSwiftNavigationViewUsage,
23
35
  hasSwiftObservableObjectUsage,
24
36
  hasSwiftOnTapGestureUsage,
25
37
  hasSwiftOperationQueueUsage,
38
+ hasSwiftContainsUserFilterUsage,
39
+ hasSwiftPassedValueStateWrapperUsage,
40
+ hasSwiftPreconcurrencyUsage,
41
+ hasSwiftSheetIsPresentedUsage,
26
42
  hasSwiftScrollViewShowsIndicatorsUsage,
27
43
  hasSwiftStringFormatUsage,
28
44
  hasSwiftTabItemUsage,
29
45
  hasSwiftTaskDetachedUsage,
46
+ hasSwiftWaitForExpectationsUsage,
30
47
  hasSwiftUIScreenMainBoundsUsage,
31
48
  hasSwiftXCTestAssertionUsage,
32
49
  hasSwiftXCTUnwrapUsage,
@@ -120,16 +137,6 @@ test('hasSwiftCallbackStyleSignature ignora usos fuera de firmas callback', () =
120
137
  assert.equal(hasSwiftCallbackStyleSignature(source), false);
121
138
  });
122
139
 
123
- test('hasSwiftCallbackStyleSignature ignora closures async modernos con @Sendable', () => {
124
- const source = `
125
- public init(publish: @escaping @Sendable ([AppRoute]) async -> Void) {
126
- self.publish = publish
127
- }
128
- `;
129
-
130
- assert.equal(hasSwiftCallbackStyleSignature(source), false);
131
- });
132
-
133
140
  test('detecta primitivas GCD y OperationQueue en codigo ejecutable', () => {
134
141
  const source = `
135
142
  DispatchQueue.main.async { }
@@ -164,10 +171,41 @@ final class LegacyBox: @unchecked Sendable {}
164
171
  assert.equal(hasSwiftUncheckedSendableUsage(source), true);
165
172
  });
166
173
 
174
+ test('detectores de hardening de concurrencia detectan escapes inseguros', () => {
175
+ const source = `
176
+ @preconcurrency import LegacyFramework
177
+
178
+ struct APIProvider: Sendable {
179
+ nonisolated(unsafe) static private(set) var shared: APIProvider!
180
+ }
181
+
182
+ func renderFromLegacyCallback() {
183
+ MainActor.assumeIsolated {
184
+ updateUI()
185
+ }
186
+ }
187
+ `;
188
+
189
+ assert.equal(hasSwiftPreconcurrencyUsage(source), true);
190
+ assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
191
+ assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
192
+ });
193
+
167
194
  test('detectores SwiftUI modernos detectan patrones legacy relevantes', () => {
168
195
  const source = `
196
+ @preconcurrency import LegacyFramework
169
197
  final class LegacyViewModel: ObservableObject {}
198
+ @StateObject private var ownedViewModel = LegacyViewModel()
199
+ @ObservedObject var injectedViewModel: LegacyViewModel
170
200
  NavigationView { Text("x") }
201
+ GeometryReader { proxy in
202
+ Text("x").frame(width: proxy.size.width)
203
+ }
204
+ Text("Headline").fontWeight(.bold)
205
+ let filtered = items.filter { $0.title.contains(searchText) }
206
+ ForEach(items.indices, id: \\.self) { index in
207
+ Text(items[index].title)
208
+ }
171
209
  Text("Primary").foregroundColor(.blue)
172
210
  Image("hero").cornerRadius(12)
173
211
  TabView {
@@ -181,8 +219,33 @@ let width = UIScreen.main.bounds.width
181
219
  ScrollView(.horizontal, showsIndicators: false) {
182
220
  Text("feed")
183
221
  }
184
- `;
222
+ .sheet(isPresented: $showDetails) {
223
+ DetailView()
224
+ }
225
+ .onChange(of: query) { newValue in
226
+ print(newValue)
227
+ }
228
+ struct DetailView: View {
229
+ @State private var filter: String
230
+ @StateObject private var detailViewModel: LegacyViewModel
231
+
232
+ init(filter: String, detailViewModel: LegacyViewModel) {
233
+ _filter = State(initialValue: filter)
234
+ _detailViewModel = StateObject(wrappedValue: detailViewModel)
235
+ }
236
+ }
237
+ nonisolated(unsafe) static var sharedBridge: LegacyViewModel?
238
+ MainActor.assumeIsolated { reload() }
239
+ `;
240
+ assert.equal(hasSwiftPreconcurrencyUsage(source), true);
241
+ assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
242
+ assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
243
+ assert.equal(hasSwiftForEachIndicesUsage(source), true);
244
+ assert.equal(hasSwiftContainsUserFilterUsage(source), true);
245
+ assert.equal(hasSwiftGeometryReaderUsage(source), true);
246
+ assert.equal(hasSwiftFontWeightBoldUsage(source), true);
185
247
  assert.equal(hasSwiftObservableObjectUsage(source), true);
248
+ assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), true);
186
249
  assert.equal(hasSwiftNavigationViewUsage(source), true);
187
250
  assert.equal(hasSwiftForegroundColorUsage(source), true);
188
251
  assert.equal(hasSwiftCornerRadiusUsage(source), true);
@@ -191,6 +254,9 @@ ScrollView(.horizontal, showsIndicators: false) {
191
254
  assert.equal(hasSwiftStringFormatUsage(source), true);
192
255
  assert.equal(hasSwiftUIScreenMainBoundsUsage(source), true);
193
256
  assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), true);
257
+ assert.equal(hasSwiftSheetIsPresentedUsage(source), true);
258
+ assert.equal(hasSwiftLegacyOnChangeUsage(source), true);
259
+ assert.equal(hasSwiftPassedValueStateWrapperUsage(source), true);
194
260
  });
195
261
 
196
262
  test('detectores legacy ignoran strings y comentarios', () => {
@@ -204,7 +270,26 @@ let e = ".foregroundColor(.blue)"
204
270
  let f = ".cornerRadius(12)"
205
271
  let g = ".tabItem { Label(\\\"Home\\\", systemImage: \\\"house\\\") }"
206
272
  let h = "ScrollView(showsIndicators: false) { }"
207
- `;
273
+ let i = ".sheet(isPresented: $showDetails) { DetailView() }"
274
+ let j = ".onChange(of: query) { newValue in }"
275
+ let k = "@StateObject private var ownedViewModel = LegacyViewModel()"
276
+ let l = "@ObservedObject var injectedViewModel: LegacyViewModel"
277
+ let m = "_filter = State(initialValue: filter)"
278
+ let n = "ForEach(items.indices, id: \\.self) { index in }"
279
+ let o = "items.filter { $0.title.contains(searchText) }"
280
+ let p = "GeometryReader { proxy in }"
281
+ let q = ".fontWeight(.bold)"
282
+ let r = "@preconcurrency import LegacyFramework"
283
+ let s = "nonisolated(unsafe) static var sharedBridge: Model?"
284
+ let t = "MainActor.assumeIsolated { reload() }"
285
+ `;
286
+ assert.equal(hasSwiftPreconcurrencyUsage(source), false);
287
+ assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
288
+ assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
289
+ assert.equal(hasSwiftForEachIndicesUsage(source), false);
290
+ assert.equal(hasSwiftContainsUserFilterUsage(source), false);
291
+ assert.equal(hasSwiftGeometryReaderUsage(source), false);
292
+ assert.equal(hasSwiftFontWeightBoldUsage(source), false);
208
293
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
209
294
  assert.equal(hasSwiftNavigationViewUsage(source), false);
210
295
  assert.equal(hasSwiftForegroundColorUsage(source), false);
@@ -213,26 +298,54 @@ let h = "ScrollView(showsIndicators: false) { }"
213
298
  assert.equal(hasSwiftStringFormatUsage(source), false);
214
299
  assert.equal(hasSwiftUIScreenMainBoundsUsage(source), false);
215
300
  assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
301
+ assert.equal(hasSwiftSheetIsPresentedUsage(source), false);
302
+ assert.equal(hasSwiftLegacyOnChangeUsage(source), false);
303
+ assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), false);
304
+ assert.equal(hasSwiftPassedValueStateWrapperUsage(source), false);
216
305
  });
217
306
 
218
307
  test('detectores snapshot SwiftUI ignoran reemplazos modernos', () => {
219
308
  const source = `
220
309
  Text("Primary").foregroundStyle(.blue)
221
310
  Image("hero").clipShape(.rect(cornerRadius: 12))
311
+ Text("Headline").bold()
222
312
  TabView {
223
313
  Tab("Home", systemImage: "house") {
224
314
  HomeView()
225
315
  }
226
316
  }
317
+ let filtered = items.filter { $0.title.localizedStandardContains(searchText) }
318
+ ForEach(items) { item in
319
+ Text(item.title)
320
+ }
321
+ containerRelativeFrame(.horizontal)
227
322
  ScrollView {
228
323
  Text("feed")
229
324
  }
230
325
  .scrollIndicators(.hidden)
326
+ .sheet(item: $selectedItem) { item in
327
+ DetailView(item: item)
328
+ }
329
+ .onChange(of: query) { oldValue, newValue in
330
+ print(oldValue, newValue)
331
+ }
332
+ .onChange(of: selection) {
333
+ reloadSelection()
334
+ }
231
335
  `;
336
+ assert.equal(hasSwiftPreconcurrencyUsage(source), false);
337
+ assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
338
+ assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
339
+ assert.equal(hasSwiftForEachIndicesUsage(source), false);
340
+ assert.equal(hasSwiftContainsUserFilterUsage(source), false);
341
+ assert.equal(hasSwiftGeometryReaderUsage(source), false);
342
+ assert.equal(hasSwiftFontWeightBoldUsage(source), false);
232
343
  assert.equal(hasSwiftForegroundColorUsage(source), false);
233
344
  assert.equal(hasSwiftCornerRadiusUsage(source), false);
234
345
  assert.equal(hasSwiftTabItemUsage(source), false);
235
346
  assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
347
+ assert.equal(hasSwiftSheetIsPresentedUsage(source), false);
348
+ assert.equal(hasSwiftLegacyOnChangeUsage(source), false);
236
349
  });
237
350
 
238
351
  test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
@@ -268,6 +381,124 @@ final class SyncTests: XCTestCase {
268
381
  assert.equal(hasSwiftLegacyXCTestImportUsage(performanceTest), false);
269
382
  });
270
383
 
384
+ test('hasSwiftLegacySwiftUiObservableWrapperUsage detecta @StateObject/@ObservedObject legacy', () => {
385
+ const legacyWrapper = `
386
+ @StateObject private var viewModel = LegacyViewModel()
387
+ @ObservedObject var sessionViewModel: SessionViewModel
388
+ `;
389
+ const modernWrapper = `
390
+ @Observable
391
+ final class SessionViewModel {}
392
+
393
+ struct ContentView: View {
394
+ @State private var viewModel = SessionViewModel()
395
+ }
396
+ `;
397
+
398
+ assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(legacyWrapper), true);
399
+ assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(modernWrapper), false);
400
+ });
401
+
402
+ test('hasSwiftPassedValueStateWrapperUsage detecta valores inyectados guardados como @State o @StateObject', () => {
403
+ const invalidOwnership = `
404
+ struct DetailView: View {
405
+ @State private var filter: String
406
+ @StateObject private var viewModel: DetailViewModel
407
+
408
+ init(filter: String, viewModel: DetailViewModel) {
409
+ _filter = State(initialValue: filter)
410
+ _viewModel = StateObject(wrappedValue: viewModel)
411
+ }
412
+ }
413
+ `;
414
+ const validOwnership = `
415
+ @Observable
416
+ final class DetailViewModel {}
417
+
418
+ struct DetailView: View {
419
+ let filter: String
420
+ @State private var viewModel = DetailViewModel()
421
+ }
422
+ `;
423
+
424
+ assert.equal(hasSwiftPassedValueStateWrapperUsage(invalidOwnership), true);
425
+ assert.equal(hasSwiftPassedValueStateWrapperUsage(validOwnership), false);
426
+ });
427
+
428
+ test('hasSwiftModernizableXCTestSuiteUsage detecta suites legacy y excluye mixed/UI', () => {
429
+ const legacySuite = `
430
+ import XCTest
431
+
432
+ final class LoginTests: XCTestCase {
433
+ func testLogin() async throws {
434
+ XCTAssertEqual(result, expected)
435
+ }
436
+ }
437
+ `;
438
+ const mixedSuite = `
439
+ import XCTest
440
+ import Testing
441
+
442
+ final class LoginTests: XCTestCase {
443
+ func testLegacyLogin() {}
444
+ }
445
+
446
+ @Suite
447
+ struct LoginModernTests {
448
+ @Test func login() async {}
449
+ }
450
+ `;
451
+ const uiSuite = `
452
+ import XCTest
453
+
454
+ final class LoginUITests: XCTestCase {
455
+ func testLoginFlow() {
456
+ let app = XCUIApplication()
457
+ app.launch()
458
+ }
459
+ }
460
+ `;
461
+
462
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(legacySuite), true);
463
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(mixedSuite), false);
464
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(uiSuite), false);
465
+ });
466
+
467
+ test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
468
+ const mixedSuite = `
469
+ import XCTest
470
+ import Testing
471
+
472
+ final class LoginTests: XCTestCase {
473
+ func testLegacyLogin() {}
474
+ }
475
+
476
+ @Suite
477
+ struct LoginModernTests {
478
+ @Test func login() async {}
479
+ }
480
+ `;
481
+ const legacyOnly = `
482
+ import XCTest
483
+
484
+ final class LoginTests: XCTestCase {
485
+ func testLegacyLogin() {}
486
+ }
487
+ `;
488
+ const modernOnly = `
489
+ import Testing
490
+
491
+ @Suite
492
+ struct LoginModernTests {
493
+ @Test func login() async {}
494
+ }
495
+ `;
496
+
497
+ assert.equal(hasSwiftMixedTestingFrameworksUsage(mixedSuite), true);
498
+ assert.equal(hasSwiftMixedTestingFrameworksUsage(legacyOnly), false);
499
+ assert.equal(hasSwiftMixedTestingFrameworksUsage(modernOnly), false);
500
+ });
501
+
271
502
  test('hasSwiftXCTestAssertionUsage detecta XCTAssert y XCTFail reales', () => {
272
503
  const source = `
273
504
  XCTAssertEqual(value, expected)
@@ -294,6 +525,43 @@ let text = "XCTUnwrap(optionalValue)"
294
525
  assert.equal(hasSwiftXCTUnwrapUsage(ignored), false);
295
526
  });
296
527
 
528
+ test('hasSwiftWaitForExpectationsUsage detecta waits legacy y excluye await fulfillment', () => {
529
+ const legacyWait = `
530
+ let expectation = expectation(description: "Done")
531
+ wait(for: [expectation], timeout: 1)
532
+ waitForExpectations(timeout: 1)
533
+ `;
534
+ const modernWait = `
535
+ let expectation = expectation(description: "Done")
536
+ await fulfillment(of: [expectation], timeout: 1)
537
+ `;
538
+
539
+ assert.equal(hasSwiftWaitForExpectationsUsage(legacyWait), true);
540
+ assert.equal(hasSwiftWaitForExpectationsUsage(modernWait), false);
541
+ });
542
+
543
+ test('hasSwiftLegacyExpectationDescriptionUsage detecta expectation(description:) sin flujo moderno', () => {
544
+ const legacyExpectation = `
545
+ let expectation = expectation(description: "Done")
546
+ doWork { expectation.fulfill() }
547
+ waitForExpectations(timeout: 1)
548
+ `;
549
+ const modernExpectation = `
550
+ let expectation = expectation(description: "Done")
551
+ doWork { expectation.fulfill() }
552
+ await fulfillment(of: [expectation], timeout: 1)
553
+ `;
554
+ const confirmationOnly = `
555
+ await confirmation("Done") { confirm in
556
+ await doWork { confirm() }
557
+ }
558
+ `;
559
+
560
+ assert.equal(hasSwiftLegacyExpectationDescriptionUsage(legacyExpectation), true);
561
+ assert.equal(hasSwiftLegacyExpectationDescriptionUsage(modernExpectation), false);
562
+ assert.equal(hasSwiftLegacyExpectationDescriptionUsage(confirmationOnly), false);
563
+ });
564
+
297
565
  test('hasSwiftNSManagedObjectBoundaryUsage detecta boundaries con NSManagedObject y excluye IDs o subclases', () => {
298
566
  const source = `
299
567
  func persist(_ entity: NSManagedObject) {}
@@ -325,6 +593,59 @@ func fetchEntityID() async throws -> NSManagedObjectID {
325
593
  assert.equal(hasSwiftNSManagedObjectAsyncBoundaryUsage(ignored), false);
326
594
  });
327
595
 
596
+ test('hasSwiftCoreDataLayerLeakUsage detecta Core Data fuera de infraestructura', () => {
597
+ const source = `
598
+ import CoreData
599
+
600
+ struct DetailView: View {
601
+ @Environment(\\.managedObjectContext) private var context
602
+ @FetchRequest(sortDescriptors: []) private var items: FetchedResults<TodoEntity>
603
+ }
604
+
605
+ final class DetailUseCase {
606
+ private let container: NSPersistentContainer
607
+ }
608
+ `;
609
+ const ignored = `
610
+ import Foundation
611
+
612
+ struct DetailView: View {
613
+ let selectedID: NSManagedObjectID?
614
+ }
615
+ `;
616
+
617
+ assert.equal(hasSwiftCoreDataLayerLeakUsage(source), true);
618
+ assert.equal(hasSwiftCoreDataLayerLeakUsage(ignored), false);
619
+ });
620
+
621
+ test('hasSwiftNSManagedObjectStateLeakUsage detecta fugas a SwiftUI state y ViewModels', () => {
622
+ const source = `
623
+ final class TodoEntity: NSManagedObject {}
624
+
625
+ struct DetailView: View {
626
+ @State private var selectedEntity: TodoEntity?
627
+ }
628
+
629
+ final class DetailViewModel: ObservableObject {
630
+ @Published var entity: TodoEntity?
631
+ }
632
+ `;
633
+ const ignored = `
634
+ final class TodoEntity: NSManagedObject {}
635
+
636
+ struct DetailView: View {
637
+ let selectedID: NSManagedObjectID?
638
+ }
639
+
640
+ final class DetailViewModel: ObservableObject {
641
+ @Published var entityID: NSManagedObjectID?
642
+ }
643
+ `;
644
+
645
+ assert.equal(hasSwiftNSManagedObjectStateLeakUsage(source), true);
646
+ assert.equal(hasSwiftNSManagedObjectStateLeakUsage(ignored), false);
647
+ });
648
+
328
649
  test('findSwiftPresentationSrpMatch devuelve payload semantico para SRP-iOS en presentation', () => {
329
650
  const source = `
330
651
  @MainActor