pumuki 6.3.60 → 6.3.61

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 (62) hide show
  1. package/VERSION +1 -1
  2. package/assets/rule-packs/ios-swiftui-modernization-v1.json +77 -0
  3. package/core/facts/detectors/text/ios.test.ts +141 -0
  4. package/core/facts/detectors/text/ios.ts +75 -0
  5. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.test.ts +31 -0
  6. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.ts +110 -0
  7. package/core/facts/extractHeuristicFacts.ts +14 -1
  8. package/core/gate/GateStage.test.ts +6 -4
  9. package/core/gate/GateStage.ts +6 -1
  10. package/core/rules/presets/heuristics/ios.test.ts +22 -1
  11. package/core/rules/presets/heuristics/ios.ts +162 -0
  12. package/docs/codex-skills/core-data-expert.md +23 -0
  13. package/docs/codex-skills/swift-testing-expert.md +25 -0
  14. package/docs/rule-packs/ios.md +2 -0
  15. package/integrations/config/skillsCompilerTemplates.ts +97 -0
  16. package/integrations/config/skillsCustomRules.ts +6 -4
  17. package/integrations/config/skillsDetectorRegistry.ts +30 -0
  18. package/integrations/config/skillsMarkdownRules.ts +56 -0
  19. package/integrations/config/skillsRuleSet.ts +1 -0
  20. package/integrations/evidence/buildEvidence.ts +18 -0
  21. package/integrations/git/brownfieldHotspots.ts +309 -0
  22. package/integrations/git/runPlatformGate.ts +17 -1
  23. package/integrations/policy/policyProfiles.ts +25 -0
  24. package/package.json +2 -1
  25. package/scripts/package-manifest-lib.ts +12 -0
  26. package/skills.lock.json +220 -87
  27. package/skills.sources.json +14 -0
  28. package/vendor/skills/MANIFEST.json +64 -0
  29. package/vendor/skills/android-enterprise-rules/SKILL.md +341 -0
  30. package/vendor/skills/backend-enterprise-rules/SKILL.md +262 -0
  31. package/vendor/skills/core-data-expert/SKILL.md +23 -0
  32. package/vendor/skills/enterprise-operating-system/SKILL.md +223 -0
  33. package/vendor/skills/frontend-enterprise-rules/SKILL.md +208 -0
  34. package/vendor/skills/ios-enterprise-rules/SKILL.md +916 -0
  35. package/vendor/skills/swift-concurrency/SKILL.md +246 -0
  36. package/vendor/skills/swift-concurrency/references/actors.md +640 -0
  37. package/vendor/skills/swift-concurrency/references/async-algorithms.md +819 -0
  38. package/vendor/skills/swift-concurrency/references/async-await-basics.md +249 -0
  39. package/vendor/skills/swift-concurrency/references/async-sequences.md +670 -0
  40. package/vendor/skills/swift-concurrency/references/core-data.md +533 -0
  41. package/vendor/skills/swift-concurrency/references/glossary.md +128 -0
  42. package/vendor/skills/swift-concurrency/references/linting.md +142 -0
  43. package/vendor/skills/swift-concurrency/references/memory-management.md +542 -0
  44. package/vendor/skills/swift-concurrency/references/migration.md +1073 -0
  45. package/vendor/skills/swift-concurrency/references/performance.md +574 -0
  46. package/vendor/skills/swift-concurrency/references/sendable.md +578 -0
  47. package/vendor/skills/swift-concurrency/references/tasks.md +604 -0
  48. package/vendor/skills/swift-concurrency/references/testing.md +565 -0
  49. package/vendor/skills/swift-concurrency/references/threading.md +452 -0
  50. package/vendor/skills/swift-testing-expert/SKILL.md +25 -0
  51. package/vendor/skills/swiftui-expert-skill/SKILL.md +263 -0
  52. package/vendor/skills/swiftui-expert-skill/references/image-optimization.md +286 -0
  53. package/vendor/skills/swiftui-expert-skill/references/layout-best-practices.md +312 -0
  54. package/vendor/skills/swiftui-expert-skill/references/liquid-glass.md +377 -0
  55. package/vendor/skills/swiftui-expert-skill/references/list-patterns.md +153 -0
  56. package/vendor/skills/swiftui-expert-skill/references/modern-apis.md +400 -0
  57. package/vendor/skills/swiftui-expert-skill/references/performance-patterns.md +377 -0
  58. package/vendor/skills/swiftui-expert-skill/references/scroll-patterns.md +305 -0
  59. package/vendor/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
  60. package/vendor/skills/swiftui-expert-skill/references/state-management.md +447 -0
  61. package/vendor/skills/swiftui-expert-skill/references/text-formatting.md +285 -0
  62. package/vendor/skills/swiftui-expert-skill/references/view-structure.md +276 -0
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.57
1
+ v6.3.61
@@ -0,0 +1,77 @@
1
+ {
2
+ "snapshotId": "ios-swiftui-modernization-v1",
3
+ "version": "1.0.0",
4
+ "generatedAt": "2026-03-31T00: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
+ ],
11
+ "entries": [
12
+ {
13
+ "id": "foreground-color",
14
+ "ruleId": "skills.ios.no-foreground-color",
15
+ "heuristicRuleId": "heuristics.ios.foreground-color.ast",
16
+ "category": "styling",
17
+ "legacyApi": ".foregroundColor(...)",
18
+ "modernApi": ".foregroundStyle(...)",
19
+ "rationale": "foregroundStyle supports hierarchical styles, gradients and materials.",
20
+ "confidence": "HIGH",
21
+ "minimumStage": "PRE_PUSH",
22
+ "minimumIos": "13.0",
23
+ "match": {
24
+ "kind": "regex",
25
+ "pattern": "\\.\\s*foregroundColor\\s*\\("
26
+ }
27
+ },
28
+ {
29
+ "id": "corner-radius",
30
+ "ruleId": "skills.ios.no-corner-radius",
31
+ "heuristicRuleId": "heuristics.ios.corner-radius.ast",
32
+ "category": "styling",
33
+ "legacyApi": ".cornerRadius(...)",
34
+ "modernApi": ".clipShape(.rect(cornerRadius: ...))",
35
+ "rationale": "cornerRadius is deprecated in modern SwiftUI and clipShape is explicit about the rendered shape.",
36
+ "confidence": "HIGH",
37
+ "minimumStage": "PRE_PUSH",
38
+ "minimumIos": "17.0",
39
+ "match": {
40
+ "kind": "regex",
41
+ "pattern": "\\.\\s*cornerRadius\\s*\\("
42
+ }
43
+ },
44
+ {
45
+ "id": "tab-item",
46
+ "ruleId": "skills.ios.no-tab-item",
47
+ "heuristicRuleId": "heuristics.ios.tab-item.ast",
48
+ "category": "tabs",
49
+ "legacyApi": ".tabItem { ... }",
50
+ "modernApi": "Tab { ... } label: { ... }",
51
+ "rationale": "The Tab API unlocks modern tab roles and avoids mixed syntax issues on iOS 18+.",
52
+ "confidence": "MEDIUM",
53
+ "minimumStage": "PRE_PUSH",
54
+ "minimumIos": "18.0",
55
+ "match": {
56
+ "kind": "regex",
57
+ "pattern": "\\.\\s*tabItem\\s*\\{"
58
+ }
59
+ },
60
+ {
61
+ "id": "scrollview-shows-indicators",
62
+ "ruleId": "skills.ios.no-scrollview-shows-indicators",
63
+ "heuristicRuleId": "heuristics.ios.scrollview-shows-indicators.ast",
64
+ "category": "scrolling",
65
+ "legacyApi": "ScrollView(..., showsIndicators: false)",
66
+ "modernApi": ".scrollIndicators(.hidden)",
67
+ "rationale": "The modifier-based API keeps the initializer clean and matches modern SwiftUI scrolling patterns.",
68
+ "confidence": "HIGH",
69
+ "minimumStage": "PRE_PUSH",
70
+ "minimumIos": "16.0",
71
+ "match": {
72
+ "kind": "regex",
73
+ "pattern": "\\bScrollView\\s*\\([\\s\\S]{0,80}showsIndicators\\s*:\\s*false\\b"
74
+ }
75
+ }
76
+ ]
77
+ }
@@ -8,19 +8,28 @@ import {
8
8
  findSwiftPresentationSrpMatch,
9
9
  hasSwiftAnyViewUsage,
10
10
  hasSwiftCallbackStyleSignature,
11
+ hasSwiftCornerRadiusUsage,
11
12
  hasSwiftDispatchGroupUsage,
12
13
  hasSwiftDispatchQueueUsage,
13
14
  hasSwiftDispatchSemaphoreUsage,
14
15
  hasSwiftForceCastUsage,
16
+ hasSwiftForegroundColorUsage,
15
17
  hasSwiftForceTryUsage,
16
18
  hasSwiftForceUnwrap,
19
+ hasSwiftLegacyXCTestImportUsage,
20
+ hasSwiftNSManagedObjectAsyncBoundaryUsage,
21
+ hasSwiftNSManagedObjectBoundaryUsage,
17
22
  hasSwiftNavigationViewUsage,
18
23
  hasSwiftObservableObjectUsage,
19
24
  hasSwiftOnTapGestureUsage,
20
25
  hasSwiftOperationQueueUsage,
26
+ hasSwiftScrollViewShowsIndicatorsUsage,
21
27
  hasSwiftStringFormatUsage,
28
+ hasSwiftTabItemUsage,
22
29
  hasSwiftTaskDetachedUsage,
23
30
  hasSwiftUIScreenMainBoundsUsage,
31
+ hasSwiftXCTestAssertionUsage,
32
+ hasSwiftXCTUnwrapUsage,
24
33
  hasSwiftUncheckedSendableUsage,
25
34
  } from './ios';
26
35
 
@@ -159,15 +168,29 @@ test('detectores SwiftUI modernos detectan patrones legacy relevantes', () => {
159
168
  const source = `
160
169
  final class LegacyViewModel: ObservableObject {}
161
170
  NavigationView { Text("x") }
171
+ Text("Primary").foregroundColor(.blue)
172
+ Image("hero").cornerRadius(12)
173
+ TabView {
174
+ HomeView().tabItem {
175
+ Label("Home", systemImage: "house")
176
+ }
177
+ }
162
178
  Text("Tap").onTapGesture { }
163
179
  let value = String(format: "%d", 1)
164
180
  let width = UIScreen.main.bounds.width
181
+ ScrollView(.horizontal, showsIndicators: false) {
182
+ Text("feed")
183
+ }
165
184
  `;
166
185
  assert.equal(hasSwiftObservableObjectUsage(source), true);
167
186
  assert.equal(hasSwiftNavigationViewUsage(source), true);
187
+ assert.equal(hasSwiftForegroundColorUsage(source), true);
188
+ assert.equal(hasSwiftCornerRadiusUsage(source), true);
189
+ assert.equal(hasSwiftTabItemUsage(source), true);
168
190
  assert.equal(hasSwiftOnTapGestureUsage(source), true);
169
191
  assert.equal(hasSwiftStringFormatUsage(source), true);
170
192
  assert.equal(hasSwiftUIScreenMainBoundsUsage(source), true);
193
+ assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), true);
171
194
  });
172
195
 
173
196
  test('detectores legacy ignoran strings y comentarios', () => {
@@ -177,11 +200,129 @@ let a = "Task.detached { }"
177
200
  let b = "NavigationView { }"
178
201
  let c = "String(format: \\\"%d\\\", 1)"
179
202
  let d = "UIScreen.main.bounds.width"
203
+ let e = ".foregroundColor(.blue)"
204
+ let f = ".cornerRadius(12)"
205
+ let g = ".tabItem { Label(\\\"Home\\\", systemImage: \\\"house\\\") }"
206
+ let h = "ScrollView(showsIndicators: false) { }"
180
207
  `;
181
208
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
182
209
  assert.equal(hasSwiftNavigationViewUsage(source), false);
210
+ assert.equal(hasSwiftForegroundColorUsage(source), false);
211
+ assert.equal(hasSwiftCornerRadiusUsage(source), false);
212
+ assert.equal(hasSwiftTabItemUsage(source), false);
183
213
  assert.equal(hasSwiftStringFormatUsage(source), false);
184
214
  assert.equal(hasSwiftUIScreenMainBoundsUsage(source), false);
215
+ assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
216
+ });
217
+
218
+ test('detectores snapshot SwiftUI ignoran reemplazos modernos', () => {
219
+ const source = `
220
+ Text("Primary").foregroundStyle(.blue)
221
+ Image("hero").clipShape(.rect(cornerRadius: 12))
222
+ TabView {
223
+ Tab("Home", systemImage: "house") {
224
+ HomeView()
225
+ }
226
+ }
227
+ ScrollView {
228
+ Text("feed")
229
+ }
230
+ .scrollIndicators(.hidden)
231
+ `;
232
+ assert.equal(hasSwiftForegroundColorUsage(source), false);
233
+ assert.equal(hasSwiftCornerRadiusUsage(source), false);
234
+ assert.equal(hasSwiftTabItemUsage(source), false);
235
+ assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), false);
236
+ });
237
+
238
+ test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
239
+ const unitTest = `
240
+ import XCTest
241
+
242
+ final class LoginTests: XCTestCase {}
243
+ `;
244
+ const uiTest = `
245
+ import XCTest
246
+
247
+ final class LoginUITests: XCTestCase {
248
+ func testLoginFlow() {
249
+ let app = XCUIApplication()
250
+ app.launch()
251
+ }
252
+ }
253
+ `;
254
+ const performanceTest = `
255
+ import XCTest
256
+
257
+ final class SyncTests: XCTestCase {
258
+ func testPerformance() {
259
+ measure {
260
+ runSync()
261
+ }
262
+ }
263
+ }
264
+ `;
265
+
266
+ assert.equal(hasSwiftLegacyXCTestImportUsage(unitTest), true);
267
+ assert.equal(hasSwiftLegacyXCTestImportUsage(uiTest), false);
268
+ assert.equal(hasSwiftLegacyXCTestImportUsage(performanceTest), false);
269
+ });
270
+
271
+ test('hasSwiftXCTestAssertionUsage detecta XCTAssert y XCTFail reales', () => {
272
+ const source = `
273
+ XCTAssertEqual(value, expected)
274
+ XCTFail("boom")
275
+ `;
276
+ const ignored = `
277
+ // XCTAssertEqual(value, expected)
278
+ let text = "XCTAssertEqual(value, expected)"
279
+ `;
280
+
281
+ assert.equal(hasSwiftXCTestAssertionUsage(source), true);
282
+ assert.equal(hasSwiftXCTestAssertionUsage(ignored), false);
283
+ });
284
+
285
+ test('hasSwiftXCTUnwrapUsage detecta XCTUnwrap real y evita strings', () => {
286
+ const source = `
287
+ let value = try XCTUnwrap(optionalValue)
288
+ `;
289
+ const ignored = `
290
+ let text = "XCTUnwrap(optionalValue)"
291
+ `;
292
+
293
+ assert.equal(hasSwiftXCTUnwrapUsage(source), true);
294
+ assert.equal(hasSwiftXCTUnwrapUsage(ignored), false);
295
+ });
296
+
297
+ test('hasSwiftNSManagedObjectBoundaryUsage detecta boundaries con NSManagedObject y excluye IDs o subclases', () => {
298
+ const source = `
299
+ func persist(_ entity: NSManagedObject) {}
300
+ var selectedEntity: NSManagedObject?
301
+ `;
302
+ const ignored = `
303
+ final class TodoEntity: NSManagedObject {}
304
+ var selectedID: NSManagedObjectID?
305
+ let context: NSManagedObjectContext
306
+ `;
307
+
308
+ assert.equal(hasSwiftNSManagedObjectBoundaryUsage(source), true);
309
+ assert.equal(hasSwiftNSManagedObjectBoundaryUsage(ignored), false);
310
+ });
311
+
312
+ test('hasSwiftNSManagedObjectAsyncBoundaryUsage detecta async APIs con NSManagedObject', () => {
313
+ const source = `
314
+ func fetchEntity() async throws -> NSManagedObject {
315
+ fatalError()
316
+ }
317
+ `;
318
+ const ignored = `
319
+ func fetchEntityID() async throws -> NSManagedObjectID {
320
+ fatalError()
321
+ }
322
+ `;
323
+
324
+ assert.equal(hasSwiftNSManagedObjectAsyncBoundaryUsage(source), true);
325
+ assert.equal(hasSwiftNSManagedObjectAsyncBoundaryUsage(ignored), false);
185
326
  });
186
327
 
187
328
  test('findSwiftPresentationSrpMatch devuelve payload semantico para SRP-iOS en presentation', () => {
@@ -6,6 +6,7 @@ import {
6
6
  readIdentifierBackward,
7
7
  scanCodeLikeSource,
8
8
  } from './utils';
9
+ import { getIosSwiftUiModernizationEntry } from './iosSwiftUiModernizationSnapshot';
9
10
 
10
11
  export type SwiftSemanticNodeMatch = {
11
12
  kind: 'class' | 'property' | 'call' | 'member';
@@ -85,6 +86,26 @@ const collectSwiftRegexLines = (source: string, regex: RegExp): readonly number[
85
86
  return matches;
86
87
  };
87
88
 
89
+ const sanitizeSwiftSourceForMultilineRegex = (source: string): string => {
90
+ return source
91
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
92
+ .replace(/\/\/.*$/gm, '')
93
+ .replace(/"(?:\\.|[^"\\])*"/g, '""');
94
+ };
95
+
96
+ const hasSwiftSanitizedRegexMatch = (source: string, regex: RegExp): boolean => {
97
+ regex.lastIndex = 0;
98
+ return regex.test(sanitizeSwiftSourceForMultilineRegex(source));
99
+ };
100
+
101
+ const hasSwiftUiModernizationSnapshotMatch = (source: string, entryId: string): boolean => {
102
+ const entry = getIosSwiftUiModernizationEntry(entryId);
103
+ if (!entry) {
104
+ return false;
105
+ }
106
+ return hasSwiftSanitizedRegexMatch(source, new RegExp(entry.match.pattern, 'g'));
107
+ };
108
+
88
109
  const sortedUniqueLines = (lines: ReadonlyArray<number>): readonly number[] => {
89
110
  return Array.from(new Set(lines.filter((line) => Number.isFinite(line)).map((line) => Math.trunc(line))))
90
111
  .sort((left, right) => left - right);
@@ -370,6 +391,18 @@ export const hasSwiftNavigationViewUsage = (source: string): boolean => {
370
391
  });
371
392
  };
372
393
 
394
+ export const hasSwiftForegroundColorUsage = (source: string): boolean => {
395
+ return hasSwiftUiModernizationSnapshotMatch(source, 'foreground-color');
396
+ };
397
+
398
+ export const hasSwiftCornerRadiusUsage = (source: string): boolean => {
399
+ return hasSwiftUiModernizationSnapshotMatch(source, 'corner-radius');
400
+ };
401
+
402
+ export const hasSwiftTabItemUsage = (source: string): boolean => {
403
+ return hasSwiftUiModernizationSnapshotMatch(source, 'tab-item');
404
+ };
405
+
373
406
  export const hasSwiftOnTapGestureUsage = (source: string): boolean => {
374
407
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
375
408
  if (current !== 'o') {
@@ -427,6 +460,48 @@ export const hasSwiftUIScreenMainBoundsUsage = (source: string): boolean => {
427
460
  });
428
461
  };
429
462
 
463
+ export const hasSwiftScrollViewShowsIndicatorsUsage = (source: string): boolean => {
464
+ return hasSwiftUiModernizationSnapshotMatch(source, 'scrollview-shows-indicators');
465
+ };
466
+
467
+ export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
468
+ const hasXCTestImport = collectSwiftRegexLines(source, /^\s*import\s+XCTest\b/).length > 0;
469
+ if (!hasXCTestImport) {
470
+ return false;
471
+ }
472
+
473
+ if (hasSwiftSanitizedRegexMatch(source, /\bXCUIApplication\b|\bXCTMetric\b|\bmeasure\s*(?:\(|\{)/)) {
474
+ return false;
475
+ }
476
+
477
+ return true;
478
+ };
479
+
480
+ export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
481
+ return (
482
+ collectSwiftRegexLines(source, /\bXCTAssert[A-Za-z0-9_]*\s*\(/).length > 0 ||
483
+ collectSwiftRegexLines(source, /\bXCTFail\s*\(/).length > 0
484
+ );
485
+ };
486
+
487
+ export const hasSwiftXCTUnwrapUsage = (source: string): boolean => {
488
+ return collectSwiftRegexLines(source, /\bXCTUnwrap\s*\(/).length > 0;
489
+ };
490
+
491
+ export const hasSwiftNSManagedObjectBoundaryUsage = (source: string): boolean => {
492
+ return hasSwiftSanitizedRegexMatch(
493
+ source,
494
+ /\bfunc\b[\s\S]{0,240}\([^)]*\bNSManagedObject\b(?!ID\b|Context\b)[^)]*\)|\b(?:var|let)\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*(?:\[[^\]]*NSManagedObject\b(?!ID\b|Context\b)[^\]]*\]|NSManagedObject\b(?!ID\b|Context\b))/g
495
+ );
496
+ };
497
+
498
+ export const hasSwiftNSManagedObjectAsyncBoundaryUsage = (source: string): boolean => {
499
+ return hasSwiftSanitizedRegexMatch(
500
+ source,
501
+ /\bfunc\b[\s\S]{0,240}\basync\b[\s\S]{0,200}(?:\([^)]*\bNSManagedObject\b(?!ID\b|Context\b)[^)]*\)|->\s*(?:\[[^\]]*NSManagedObject\b(?!ID\b|Context\b)[^\]]*\]|NSManagedObject\b(?!ID\b|Context\b)))/g
502
+ );
503
+ };
504
+
430
505
  export const hasSwiftForceTryUsage = (source: string): boolean => {
431
506
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
432
507
  if (current !== 't' || !hasIdentifierAt(swiftSource, index, 'try')) {
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ getIosSwiftUiModernizationEntry,
5
+ IOS_SWIFTUI_MODERNIZATION_SNAPSHOT,
6
+ listIosSwiftUiModernizationEntries,
7
+ } from './iosSwiftUiModernizationSnapshot';
8
+
9
+ test('iosSwiftUiModernizationSnapshot expone snapshot versionado y determinista', () => {
10
+ assert.equal(IOS_SWIFTUI_MODERNIZATION_SNAPSHOT.snapshotId, 'ios-swiftui-modernization-v1');
11
+ assert.equal(IOS_SWIFTUI_MODERNIZATION_SNAPSHOT.version, '1.0.0');
12
+ assert.equal(IOS_SWIFTUI_MODERNIZATION_SNAPSHOT.sourceSkill, 'swiftui-expert-skill');
13
+ assert.deepEqual(
14
+ listIosSwiftUiModernizationEntries().map((entry) => entry.id),
15
+ ['foreground-color', 'corner-radius', 'tab-item', 'scrollview-shows-indicators']
16
+ );
17
+ });
18
+
19
+ test('iosSwiftUiModernizationSnapshot resuelve entradas canonicas con ruleIds y heuristics alineados', () => {
20
+ const foregroundColor = getIosSwiftUiModernizationEntry('foreground-color');
21
+ const tabItem = getIosSwiftUiModernizationEntry('tab-item');
22
+
23
+ assert.ok(foregroundColor);
24
+ assert.equal(foregroundColor.ruleId, 'skills.ios.no-foreground-color');
25
+ assert.equal(foregroundColor.heuristicRuleId, 'heuristics.ios.foreground-color.ast');
26
+
27
+ assert.ok(tabItem);
28
+ assert.equal(tabItem.ruleId, 'skills.ios.no-tab-item');
29
+ assert.equal(tabItem.heuristicRuleId, 'heuristics.ios.tab-item.ast');
30
+ assert.equal(tabItem.minimumIos, '18.0');
31
+ });
@@ -0,0 +1,110 @@
1
+ import rawSnapshot from '../../../../assets/rule-packs/ios-swiftui-modernization-v1.json';
2
+
3
+ export type IosSwiftUiModernizationConfidence = 'HIGH' | 'MEDIUM';
4
+ export type IosSwiftUiModernizationStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
5
+ export type IosSwiftUiModernizationCategory = 'styling' | 'tabs' | 'scrolling';
6
+
7
+ export type IosSwiftUiModernizationMatch = {
8
+ kind: 'regex';
9
+ pattern: string;
10
+ };
11
+
12
+ export type IosSwiftUiModernizationEntry = {
13
+ id: string;
14
+ ruleId: string;
15
+ heuristicRuleId: string;
16
+ category: IosSwiftUiModernizationCategory;
17
+ legacyApi: string;
18
+ modernApi: string;
19
+ rationale: string;
20
+ confidence: IosSwiftUiModernizationConfidence;
21
+ minimumStage: IosSwiftUiModernizationStage;
22
+ minimumIos: string;
23
+ match: IosSwiftUiModernizationMatch;
24
+ };
25
+
26
+ export type IosSwiftUiModernizationSnapshot = {
27
+ snapshotId: string;
28
+ version: string;
29
+ generatedAt: string;
30
+ sourceSkill: string;
31
+ sourceReferences: readonly string[];
32
+ entries: readonly IosSwiftUiModernizationEntry[];
33
+ };
34
+
35
+ const isObject = (value: unknown): value is Record<string, unknown> =>
36
+ typeof value === 'object' && value !== null;
37
+
38
+ const isNonEmptyString = (value: unknown): value is string =>
39
+ typeof value === 'string' && value.trim().length > 0;
40
+
41
+ const isStage = (value: unknown): value is IosSwiftUiModernizationStage =>
42
+ value === 'PRE_COMMIT' || value === 'PRE_PUSH' || value === 'CI';
43
+
44
+ const isConfidence = (value: unknown): value is IosSwiftUiModernizationConfidence =>
45
+ value === 'HIGH' || value === 'MEDIUM';
46
+
47
+ const isCategory = (value: unknown): value is IosSwiftUiModernizationCategory =>
48
+ value === 'styling' || value === 'tabs' || value === 'scrolling';
49
+
50
+ const isMatch = (value: unknown): value is IosSwiftUiModernizationMatch => {
51
+ return (
52
+ isObject(value) &&
53
+ value.kind === 'regex' &&
54
+ isNonEmptyString(value.pattern)
55
+ );
56
+ };
57
+
58
+ const isEntry = (value: unknown): value is IosSwiftUiModernizationEntry => {
59
+ return (
60
+ isObject(value) &&
61
+ isNonEmptyString(value.id) &&
62
+ isNonEmptyString(value.ruleId) &&
63
+ isNonEmptyString(value.heuristicRuleId) &&
64
+ isCategory(value.category) &&
65
+ isNonEmptyString(value.legacyApi) &&
66
+ isNonEmptyString(value.modernApi) &&
67
+ isNonEmptyString(value.rationale) &&
68
+ isConfidence(value.confidence) &&
69
+ isStage(value.minimumStage) &&
70
+ isNonEmptyString(value.minimumIos) &&
71
+ isMatch(value.match)
72
+ );
73
+ };
74
+
75
+ const isSnapshot = (value: unknown): value is IosSwiftUiModernizationSnapshot => {
76
+ return (
77
+ isObject(value) &&
78
+ isNonEmptyString(value.snapshotId) &&
79
+ isNonEmptyString(value.version) &&
80
+ isNonEmptyString(value.generatedAt) &&
81
+ isNonEmptyString(value.sourceSkill) &&
82
+ Array.isArray(value.sourceReferences) &&
83
+ value.sourceReferences.every((item) => isNonEmptyString(item)) &&
84
+ Array.isArray(value.entries) &&
85
+ value.entries.every((entry) => isEntry(entry))
86
+ );
87
+ };
88
+
89
+ const parseSnapshot = (value: unknown): IosSwiftUiModernizationSnapshot => {
90
+ if (!isSnapshot(value)) {
91
+ throw new Error('Invalid iOS SwiftUI modernization snapshot.');
92
+ }
93
+ return value;
94
+ };
95
+
96
+ export const IOS_SWIFTUI_MODERNIZATION_SNAPSHOT = parseSnapshot(rawSnapshot);
97
+
98
+ const entryById = new Map(
99
+ IOS_SWIFTUI_MODERNIZATION_SNAPSHOT.entries.map((entry) => [entry.id, entry] as const)
100
+ );
101
+
102
+ export const listIosSwiftUiModernizationEntries = (): readonly IosSwiftUiModernizationEntry[] => {
103
+ return IOS_SWIFTUI_MODERNIZATION_SNAPSHOT.entries;
104
+ };
105
+
106
+ export const getIosSwiftUiModernizationEntry = (
107
+ entryId: string
108
+ ): IosSwiftUiModernizationEntry | undefined => {
109
+ return entryById.get(entryId);
110
+ };
@@ -128,6 +128,10 @@ const isSwiftTestPath = (path: string): boolean => {
128
128
  );
129
129
  };
130
130
 
131
+ const isIOSSwiftTestPath = (path: string): boolean => {
132
+ return isIOSSwiftPath(path) && isSwiftTestPath(path);
133
+ };
134
+
131
135
  const isKotlinTestPath = (path: string): boolean => {
132
136
  const normalized = path.toLowerCase();
133
137
  return (
@@ -594,9 +598,18 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
594
598
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUncheckedSendableUsage, ruleId: 'heuristics.ios.unchecked-sendable.ast', code: 'HEURISTICS_IOS_UNCHECKED_SENDABLE_AST', message: 'AST heuristic detected @unchecked Sendable usage.' },
595
599
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftObservableObjectUsage, ruleId: 'heuristics.ios.observable-object.ast', code: 'HEURISTICS_IOS_OBSERVABLE_OBJECT_AST', message: 'AST heuristic detected ObservableObject usage.' },
596
600
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationViewUsage, ruleId: 'heuristics.ios.navigation-view.ast', code: 'HEURISTICS_IOS_NAVIGATION_VIEW_AST', message: 'AST heuristic detected NavigationView usage.' },
601
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForegroundColorUsage, ruleId: 'heuristics.ios.foreground-color.ast', code: 'HEURISTICS_IOS_FOREGROUND_COLOR_AST', message: 'AST heuristic detected foregroundColor usage.' },
602
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCornerRadiusUsage, ruleId: 'heuristics.ios.corner-radius.ast', code: 'HEURISTICS_IOS_CORNER_RADIUS_AST', message: 'AST heuristic detected cornerRadius usage.' },
603
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTabItemUsage, ruleId: 'heuristics.ios.tab-item.ast', code: 'HEURISTICS_IOS_TAB_ITEM_AST', message: 'AST heuristic detected tabItem usage.' },
597
604
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnTapGestureUsage, ruleId: 'heuristics.ios.on-tap-gesture.ast', code: 'HEURISTICS_IOS_ON_TAP_GESTURE_AST', message: 'AST heuristic detected onTapGesture usage where Button may be preferred.' },
598
605
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStringFormatUsage, ruleId: 'heuristics.ios.string-format.ast', code: 'HEURISTICS_IOS_STRING_FORMAT_AST', message: 'AST heuristic detected String(format:) usage.' },
606
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftScrollViewShowsIndicatorsUsage, ruleId: 'heuristics.ios.scrollview-shows-indicators.ast', code: 'HEURISTICS_IOS_SCROLLVIEW_SHOWS_INDICATORS_AST', message: 'AST heuristic detected ScrollView(showsIndicators: false) usage.' },
599
607
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUIScreenMainBoundsUsage, ruleId: 'heuristics.ios.uiscreen-main-bounds.ast', code: 'HEURISTICS_IOS_UISCREEN_MAIN_BOUNDS_AST', message: 'AST heuristic detected UIScreen.main.bounds usage.' },
608
+ { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftLegacyXCTestImportUsage, ruleId: 'heuristics.ios.testing.xctest-import.ast', code: 'HEURISTICS_IOS_TESTING_XCTEST_IMPORT_AST', message: 'AST heuristic detected XCTest-only test usage where Swift Testing may be preferred.' },
609
+ { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftXCTestAssertionUsage, ruleId: 'heuristics.ios.testing.xctassert.ast', code: 'HEURISTICS_IOS_TESTING_XCTASSERT_AST', message: 'AST heuristic detected XCTest assertion usage where #expect may be preferred.' },
610
+ { platform: 'ios', pathCheck: isIOSSwiftTestPath, excludePaths: [], detect: TextIOS.hasSwiftXCTUnwrapUsage, ruleId: 'heuristics.ios.testing.xctunwrap.ast', code: 'HEURISTICS_IOS_TESTING_XCTUNWRAP_AST', message: 'AST heuristic detected XCTUnwrap usage where #require may be preferred.' },
611
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in a shared boundary.' },
612
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSManagedObjectAsyncBoundaryUsage, ruleId: 'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast', code: 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_ASYNC_BOUNDARY_AST', message: 'AST heuristic detected NSManagedObject in an async boundary.' },
600
613
 
601
614
  // Android
602
615
  { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinThreadSleepCall, ruleId: 'heuristics.android.thread-sleep.ast', code: 'HEURISTICS_ANDROID_THREAD_SLEEP_AST', message: 'AST heuristic detected Thread.sleep usage in production Kotlin code.' },
@@ -671,7 +684,7 @@ export const extractHeuristicFacts = (
671
684
  if (
672
685
  platformDetected &&
673
686
  entry.pathCheck(fileFact.path) &&
674
- entry.excludePaths.every((exclude) => !exclude(fileFact.path)) &&
687
+ (entry.excludePaths ?? []).every((exclude) => !exclude(fileFact.path)) &&
675
688
  entry.detect(fileFact.content)
676
689
  ) {
677
690
  heuristicFacts.push(
@@ -2,21 +2,23 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import type { GateStage } from './GateStage';
4
4
 
5
- test('GateStage soporta STAGED, PRE_COMMIT, PRE_PUSH y CI', () => {
5
+ test('GateStage soporta STAGED, PRE_WRITE, PRE_COMMIT, PRE_PUSH y CI', () => {
6
6
  const staged: GateStage = 'STAGED';
7
+ const preWrite: GateStage = 'PRE_WRITE';
7
8
  const preCommit: GateStage = 'PRE_COMMIT';
8
9
  const prePush: GateStage = 'PRE_PUSH';
9
10
  const ci: GateStage = 'CI';
10
11
 
11
12
  assert.equal(staged, 'STAGED');
13
+ assert.equal(preWrite, 'PRE_WRITE');
12
14
  assert.equal(preCommit, 'PRE_COMMIT');
13
15
  assert.equal(prePush, 'PRE_PUSH');
14
16
  assert.equal(ci, 'CI');
15
17
  });
16
18
 
17
19
  test('GateStage permite colecciones tipadas de stages', () => {
18
- const stages: GateStage[] = ['STAGED', 'PRE_COMMIT', 'PRE_PUSH', 'CI'];
20
+ const stages: GateStage[] = ['STAGED', 'PRE_WRITE', 'PRE_COMMIT', 'PRE_PUSH', 'CI'];
19
21
 
20
- assert.equal(stages.length, 4);
21
- assert.deepEqual(stages, ['STAGED', 'PRE_COMMIT', 'PRE_PUSH', 'CI']);
22
+ assert.equal(stages.length, 5);
23
+ assert.deepEqual(stages, ['STAGED', 'PRE_WRITE', 'PRE_COMMIT', 'PRE_PUSH', 'CI']);
22
24
  });
@@ -1 +1,6 @@
1
- export type GateStage = 'STAGED' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
1
+ export type GateStage =
2
+ | 'STAGED'
3
+ | 'PRE_WRITE'
4
+ | 'PRE_COMMIT'
5
+ | 'PRE_PUSH'
6
+ | 'CI';
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { iosRules } from './ios';
4
4
 
5
5
  test('iosRules define reglas heurísticas locked para plataforma ios', () => {
6
- assert.equal(iosRules.length, 16);
6
+ assert.equal(iosRules.length, 25);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -20,9 +20,18 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
20
20
  'heuristics.ios.unchecked-sendable.ast',
21
21
  'heuristics.ios.observable-object.ast',
22
22
  'heuristics.ios.navigation-view.ast',
23
+ 'heuristics.ios.foreground-color.ast',
24
+ 'heuristics.ios.corner-radius.ast',
25
+ 'heuristics.ios.tab-item.ast',
23
26
  'heuristics.ios.on-tap-gesture.ast',
24
27
  'heuristics.ios.string-format.ast',
28
+ 'heuristics.ios.scrollview-shows-indicators.ast',
25
29
  'heuristics.ios.uiscreen-main-bounds.ast',
30
+ 'heuristics.ios.testing.xctest-import.ast',
31
+ 'heuristics.ios.testing.xctassert.ast',
32
+ 'heuristics.ios.testing.xctunwrap.ast',
33
+ 'heuristics.ios.core-data.nsmanagedobject-boundary.ast',
34
+ 'heuristics.ios.core-data.nsmanagedobject-async-boundary.ast',
26
35
  ]);
27
36
 
28
37
  const byId = new Map(iosRules.map((rule) => [rule.id, rule]));
@@ -38,6 +47,18 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
38
47
  byId.get('heuristics.ios.uiscreen-main-bounds.ast')?.then.code,
39
48
  'HEURISTICS_IOS_UISCREEN_MAIN_BOUNDS_AST'
40
49
  );
50
+ assert.equal(
51
+ byId.get('heuristics.ios.foreground-color.ast')?.then.code,
52
+ 'HEURISTICS_IOS_FOREGROUND_COLOR_AST'
53
+ );
54
+ assert.equal(
55
+ byId.get('heuristics.ios.testing.xctassert.ast')?.then.code,
56
+ 'HEURISTICS_IOS_TESTING_XCTASSERT_AST'
57
+ );
58
+ assert.equal(
59
+ byId.get('heuristics.ios.core-data.nsmanagedobject-async-boundary.ast')?.then.code,
60
+ 'HEURISTICS_IOS_CORE_DATA_NSMANAGEDOBJECT_ASYNC_BOUNDARY_AST'
61
+ );
41
62
 
42
63
  for (const rule of iosRules) {
43
64
  assert.equal(rule.platform, 'ios');