pumuki 6.3.59 → 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 (185) hide show
  1. package/VERSION +1 -1
  2. package/assets/rule-packs/ios-swiftui-modernization-v1.json +77 -0
  3. package/bin/_run-ts-entry.js +4 -1
  4. package/core/facts/detectors/text/ios.test.ts +151 -0
  5. package/core/facts/detectors/text/ios.ts +75 -0
  6. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.test.ts +31 -0
  7. package/core/facts/detectors/text/iosSwiftUiModernizationSnapshot.ts +110 -0
  8. package/core/facts/extractHeuristicFacts.ts +14 -1
  9. package/core/gate/GateStage.test.ts +6 -4
  10. package/core/gate/GateStage.ts +6 -1
  11. package/core/rules/presets/heuristics/ios.test.ts +22 -1
  12. package/core/rules/presets/heuristics/ios.ts +162 -0
  13. package/core/rules/presets/iosEnterpriseRuleSet.test.ts +49 -1
  14. package/core/rules/presets/iosEnterpriseRuleSet.ts +4 -11
  15. package/docs/README.md +5 -10
  16. package/docs/codex-skills/core-data-expert.md +23 -0
  17. package/docs/codex-skills/swift-testing-expert.md +25 -0
  18. package/docs/operations/RELEASE_NOTES.md +28 -33
  19. package/docs/product/API_REFERENCE.md +35 -16
  20. package/docs/product/CONFIGURATION.md +8 -1
  21. package/docs/product/HOW_IT_WORKS.md +9 -9
  22. package/docs/product/TESTING.md +28 -0
  23. package/docs/product/USAGE.md +60 -18
  24. package/docs/rule-packs/ios.md +2 -0
  25. package/docs/validation/README.md +31 -7
  26. package/integrations/config/compileSkillsLock.ts +1 -1
  27. package/integrations/config/coreSkillsLock.ts +20 -10
  28. package/integrations/config/skillsCompilerTemplates.ts +97 -0
  29. package/integrations/config/skillsCustomRules.ts +6 -4
  30. package/integrations/config/skillsDetectorRegistry.ts +30 -0
  31. package/integrations/config/skillsMarkdownRules.ts +56 -0
  32. package/integrations/config/skillsRuleSet.ts +1 -0
  33. package/integrations/evidence/buildEvidence.ts +18 -0
  34. package/integrations/evidence/repoState.ts +8 -20
  35. package/integrations/gate/stagePolicies.ts +50 -550
  36. package/integrations/git/brownfieldHotspots.ts +309 -0
  37. package/integrations/git/runPlatformGate.ts +81 -19
  38. package/integrations/lifecycle/cli.ts +293 -21
  39. package/integrations/lifecycle/doctor.ts +51 -10
  40. package/integrations/lifecycle/experimentalFeaturesSnapshot.ts +61 -0
  41. package/integrations/lifecycle/packageInfo.ts +22 -5
  42. package/integrations/lifecycle/policyValidationSnapshot.ts +6 -0
  43. package/integrations/lifecycle/saasIngestionContract.ts +1 -1
  44. package/integrations/lifecycle/status.ts +6 -0
  45. package/integrations/mcp/aiGateCheck.ts +7 -3
  46. package/integrations/mcp/autoExecuteAiStart.ts +7 -3
  47. package/integrations/mcp/enterpriseServer.ts +49 -2
  48. package/integrations/mcp/preFlightCheck.ts +7 -3
  49. package/integrations/policy/experimentalFeatures.ts +189 -0
  50. package/integrations/policy/heuristicsEnforcement.ts +65 -0
  51. package/integrations/policy/policyAsCode.ts +250 -0
  52. package/integrations/policy/policyProfiles.ts +435 -0
  53. package/integrations/policy/preWriteEnforcement.ts +19 -31
  54. package/integrations/policy/sddCompletenessEnforcement.ts +57 -0
  55. package/integrations/policy/skillsEnforcement.ts +1 -10
  56. package/integrations/policy/tddBddEnforcement.ts +103 -0
  57. package/integrations/sdd/policy.ts +125 -39
  58. package/integrations/sdd/types.ts +2 -0
  59. package/integrations/tdd/types.ts +1 -1
  60. package/integrations/telemetry/gateTelemetry.ts +4 -0
  61. package/package.json +38 -3
  62. package/scripts/adapter-readiness-markdown-next-actions-lib.ts +1 -1
  63. package/scripts/adapter-real-session-analysis-evaluation-lib.ts +4 -0
  64. package/scripts/adapter-real-session-analysis-messages-lib.ts +13 -0
  65. package/scripts/adapter-real-session-contract.ts +3 -0
  66. package/scripts/adapter-real-session-markdown-sections-context-lib.ts +8 -2
  67. package/scripts/adapter-real-session-status-parser-lib.ts +29 -1
  68. package/scripts/adapter-session-status-capabilities-lib.ts +77 -0
  69. package/scripts/adapter-session-status-command-lib.ts +18 -6
  70. package/scripts/adapter-session-status-contract.ts +20 -5
  71. package/scripts/adapter-session-status-markdown-lib.ts +30 -7
  72. package/scripts/adapter-session-status-verdict-lib.ts +15 -4
  73. package/scripts/build-adapter-session-status.ts +5 -3
  74. package/scripts/build-consumer-menu-matrix-baseline.ts +5 -0
  75. package/scripts/build-consumer-startup-triage-runner-lib.ts +2 -4
  76. package/scripts/check-tracking-single-active.sh +12 -12
  77. package/scripts/compile-skills-lock.ts +9 -2
  78. package/scripts/consumer-menu-matrix-baseline-builder-lib.ts +114 -0
  79. package/scripts/consumer-menu-matrix-baseline-cli-lib.ts +117 -0
  80. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +263 -0
  81. package/scripts/consumer-startup-triage-command-builders-core-required-lib.ts +9 -2
  82. package/scripts/consumer-startup-triage-command-builders-support-bundle-lib.ts +5 -1
  83. package/scripts/consumer-startup-triage-command-builders-support-followup-lib.ts +9 -2
  84. package/scripts/consumer-startup-triage-command-builders-workflow-lint-lib.ts +9 -6
  85. package/scripts/consumer-startup-triage-contract.ts +2 -1
  86. package/scripts/consumer-startup-triage-markdown-lib.ts +1 -1
  87. package/scripts/consumer-startup-triage-script-paths-lib.ts +17 -0
  88. package/scripts/consumer-support-bundle-markdown-sections-support-payload-lib.ts +57 -3
  89. package/scripts/consumer-support-ticket-draft-lib.ts +2 -1
  90. package/scripts/consumer-support-ticket-draft-primary-sections-lib.ts +49 -4
  91. package/scripts/consumer-workflow-lint-markdown-lib.ts +8 -1
  92. package/scripts/enterprise-contract-suite-contract.ts +2 -2
  93. package/scripts/framework-menu-actions-diagnostics-adapter-lib.ts +17 -3
  94. package/scripts/framework-menu-actions-diagnostics-maintenance-lib.ts +1 -1
  95. package/scripts/framework-menu-actions-diagnostics-support-core-lib.ts +3 -3
  96. package/scripts/framework-menu-actions-diagnostics-support-triage-lib.ts +2 -2
  97. package/scripts/framework-menu-actions-gates-stage-lib.ts +3 -3
  98. package/scripts/framework-menu-actions-phase5-exec-lib.ts +2 -2
  99. package/scripts/framework-menu-actions-phase5-reports-lib.ts +4 -4
  100. package/scripts/framework-menu-advanced-view-help.ts +15 -15
  101. package/scripts/framework-menu-builders-maintenance.ts +10 -4
  102. package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
  103. package/scripts/framework-menu-consumer-runtime-actions.ts +27 -7
  104. package/scripts/framework-menu-consumer-runtime-audit.ts +163 -17
  105. package/scripts/framework-menu-consumer-runtime-lib.ts +10 -0
  106. package/scripts/framework-menu-consumer-runtime-menu.ts +17 -8
  107. package/scripts/framework-menu-consumer-runtime-types.ts +20 -6
  108. package/scripts/framework-menu-evidence-summary-normalize.ts +7 -0
  109. package/scripts/framework-menu-evidence-summary-read.ts +28 -0
  110. package/scripts/framework-menu-evidence-summary-severity.ts +129 -0
  111. package/scripts/framework-menu-evidence-summary-types.ts +16 -0
  112. package/scripts/framework-menu-gate-lib.ts +44 -7
  113. package/scripts/framework-menu-layout-data.ts +15 -10
  114. package/scripts/framework-menu-legacy-audit-markdown-document.ts +6 -2
  115. package/scripts/framework-menu-legacy-audit-render-panel.ts +3 -2
  116. package/scripts/framework-menu-legacy-audit-render-report.ts +20 -58
  117. package/scripts/framework-menu-legacy-audit-render-sections.ts +5 -7
  118. package/scripts/framework-menu-runner-constants.ts +2 -1
  119. package/scripts/framework-menu-runners-adapter-real-session-lib.ts +35 -12
  120. package/scripts/framework-menu-runners-adapter-session-lib.ts +35 -12
  121. package/scripts/framework-menu-runners-validation-custom-rules-lib.ts +10 -2
  122. package/scripts/framework-menu-runners-validation-skills-lib.ts +10 -2
  123. package/scripts/framework-menu-skills-lib.ts +8 -1
  124. package/scripts/framework-menu-system-notifications-gate.ts +12 -0
  125. package/scripts/framework-menu-system-notifications-lib.ts +1 -0
  126. package/scripts/framework-menu.ts +23 -14
  127. package/scripts/lint-consumer-workflows.ts +21 -12
  128. package/scripts/package-install-smoke-consumer-npm-lib.ts +41 -5
  129. package/scripts/package-install-smoke-workspace-factory-lib.ts +1 -1
  130. package/scripts/package-manifest-lib.ts +14 -0
  131. package/scripts/phase5-blockers-markdown-next-actions-blocked-lib.ts +3 -3
  132. package/scripts/phase5-blockers-markdown-next-actions-ready-lib.ts +1 -1
  133. package/scripts/phase5-execution-closure-plan-consumer-triage-lib.ts +4 -5
  134. package/scripts/phase5-execution-closure-status-markdown-next-actions-lib.ts +1 -1
  135. package/scripts/phase5-external-handoff-markdown-next-actions-lib.ts +1 -1
  136. package/scripts/prepare-phase5-escalation-submission.sh +1 -1
  137. package/scripts/refresh-phase5-latest-escalation.sh +1 -1
  138. package/scripts/run-phase5-post-support-refresh.sh +1 -1
  139. package/scripts/run-phase8-after-billing-reactivation.sh +2 -2
  140. package/scripts/run-phase8-autopilot.sh +3 -3
  141. package/scripts/run-phase8-close-ready.sh +2 -4
  142. package/scripts/run-phase8-doctor.sh +3 -3
  143. package/scripts/run-phase8-next-step.sh +3 -3
  144. package/scripts/run-phase8-status-pack.sh +3 -3
  145. package/scripts/run-phase8-tick.sh +5 -5
  146. package/skills.lock.json +220 -87
  147. package/skills.sources.json +14 -0
  148. package/vendor/skills/MANIFEST.json +64 -0
  149. package/vendor/skills/android-enterprise-rules/SKILL.md +341 -0
  150. package/vendor/skills/backend-enterprise-rules/SKILL.md +262 -0
  151. package/vendor/skills/core-data-expert/SKILL.md +23 -0
  152. package/vendor/skills/enterprise-operating-system/SKILL.md +223 -0
  153. package/vendor/skills/frontend-enterprise-rules/SKILL.md +208 -0
  154. package/vendor/skills/ios-enterprise-rules/SKILL.md +916 -0
  155. package/vendor/skills/swift-concurrency/SKILL.md +246 -0
  156. package/vendor/skills/swift-concurrency/references/actors.md +640 -0
  157. package/vendor/skills/swift-concurrency/references/async-algorithms.md +819 -0
  158. package/vendor/skills/swift-concurrency/references/async-await-basics.md +249 -0
  159. package/vendor/skills/swift-concurrency/references/async-sequences.md +670 -0
  160. package/vendor/skills/swift-concurrency/references/core-data.md +533 -0
  161. package/vendor/skills/swift-concurrency/references/glossary.md +128 -0
  162. package/vendor/skills/swift-concurrency/references/linting.md +142 -0
  163. package/vendor/skills/swift-concurrency/references/memory-management.md +542 -0
  164. package/vendor/skills/swift-concurrency/references/migration.md +1073 -0
  165. package/vendor/skills/swift-concurrency/references/performance.md +574 -0
  166. package/vendor/skills/swift-concurrency/references/sendable.md +578 -0
  167. package/vendor/skills/swift-concurrency/references/tasks.md +604 -0
  168. package/vendor/skills/swift-concurrency/references/testing.md +565 -0
  169. package/vendor/skills/swift-concurrency/references/threading.md +452 -0
  170. package/vendor/skills/swift-testing-expert/SKILL.md +25 -0
  171. package/vendor/skills/swiftui-expert-skill/SKILL.md +263 -0
  172. package/vendor/skills/swiftui-expert-skill/references/image-optimization.md +286 -0
  173. package/vendor/skills/swiftui-expert-skill/references/layout-best-practices.md +312 -0
  174. package/vendor/skills/swiftui-expert-skill/references/liquid-glass.md +377 -0
  175. package/vendor/skills/swiftui-expert-skill/references/list-patterns.md +153 -0
  176. package/vendor/skills/swiftui-expert-skill/references/modern-apis.md +400 -0
  177. package/vendor/skills/swiftui-expert-skill/references/performance-patterns.md +377 -0
  178. package/vendor/skills/swiftui-expert-skill/references/scroll-patterns.md +305 -0
  179. package/vendor/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +292 -0
  180. package/vendor/skills/swiftui-expert-skill/references/state-management.md +447 -0
  181. package/vendor/skills/swiftui-expert-skill/references/text-formatting.md +285 -0
  182. package/vendor/skills/swiftui-expert-skill/references/view-structure.md +276 -0
  183. package/scripts/build-phase8-ready-handoff-summary.sh +0 -66
  184. package/scripts/check-refactor-progress-single-active.sh +0 -25
  185. package/scripts/close-phase5-escalation-submission.sh +0 -81
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.59
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
+ }
@@ -22,7 +22,10 @@ function runTsEntry(relativeEntry, forwardedArgs = []) {
22
22
  [tsxCli, entryFile, ...forwardedArgs],
23
23
  {
24
24
  stdio: 'inherit',
25
- env: process.env,
25
+ env: {
26
+ ...process.env,
27
+ PUMUKI_RUNTIME_EXECUTION_SOURCE: 'source-bin',
28
+ },
26
29
  }
27
30
  );
28
31
 
@@ -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
 
@@ -111,6 +120,16 @@ test('hasSwiftCallbackStyleSignature ignora usos fuera de firmas callback', () =
111
120
  assert.equal(hasSwiftCallbackStyleSignature(source), false);
112
121
  });
113
122
 
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
+
114
133
  test('detecta primitivas GCD y OperationQueue en codigo ejecutable', () => {
115
134
  const source = `
116
135
  DispatchQueue.main.async { }
@@ -149,15 +168,29 @@ test('detectores SwiftUI modernos detectan patrones legacy relevantes', () => {
149
168
  const source = `
150
169
  final class LegacyViewModel: ObservableObject {}
151
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
+ }
152
178
  Text("Tap").onTapGesture { }
153
179
  let value = String(format: "%d", 1)
154
180
  let width = UIScreen.main.bounds.width
181
+ ScrollView(.horizontal, showsIndicators: false) {
182
+ Text("feed")
183
+ }
155
184
  `;
156
185
  assert.equal(hasSwiftObservableObjectUsage(source), true);
157
186
  assert.equal(hasSwiftNavigationViewUsage(source), true);
187
+ assert.equal(hasSwiftForegroundColorUsage(source), true);
188
+ assert.equal(hasSwiftCornerRadiusUsage(source), true);
189
+ assert.equal(hasSwiftTabItemUsage(source), true);
158
190
  assert.equal(hasSwiftOnTapGestureUsage(source), true);
159
191
  assert.equal(hasSwiftStringFormatUsage(source), true);
160
192
  assert.equal(hasSwiftUIScreenMainBoundsUsage(source), true);
193
+ assert.equal(hasSwiftScrollViewShowsIndicatorsUsage(source), true);
161
194
  });
162
195
 
163
196
  test('detectores legacy ignoran strings y comentarios', () => {
@@ -167,11 +200,129 @@ let a = "Task.detached { }"
167
200
  let b = "NavigationView { }"
168
201
  let c = "String(format: \\\"%d\\\", 1)"
169
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) { }"
170
207
  `;
171
208
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
172
209
  assert.equal(hasSwiftNavigationViewUsage(source), false);
210
+ assert.equal(hasSwiftForegroundColorUsage(source), false);
211
+ assert.equal(hasSwiftCornerRadiusUsage(source), false);
212
+ assert.equal(hasSwiftTabItemUsage(source), false);
173
213
  assert.equal(hasSwiftStringFormatUsage(source), false);
174
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);
175
326
  });
176
327
 
177
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');