pumuki 6.3.153 → 6.3.155

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.
@@ -13,6 +13,7 @@ import {
13
13
  hasSwiftDispatchGroupUsage,
14
14
  hasSwiftDispatchQueueUsage,
15
15
  hasSwiftDispatchSemaphoreUsage,
16
+ hasSwiftExplicitColorStaticMemberUsage,
16
17
  hasSwiftForEachIndicesUsage,
17
18
  hasSwiftForceCastUsage,
18
19
  hasSwiftFontWeightBoldUsage,
@@ -20,6 +21,7 @@ import {
20
21
  hasSwiftForceTryUsage,
21
22
  hasSwiftForceUnwrap,
22
23
  hasSwiftGeometryReaderUsage,
24
+ hasSwiftInlineFilteringInForEachUsage,
23
25
  hasSwiftLegacyOnChangeUsage,
24
26
  hasSwiftLegacyExpectationDescriptionUsage,
25
27
  hasSwiftLegacySwiftUiObservableWrapperUsage,
@@ -243,7 +245,9 @@ MainActor.assumeIsolated { reload() }
243
245
  assert.equal(hasSwiftNonisolatedUnsafeUsage(source), true);
244
246
  assert.equal(hasSwiftAssumeIsolatedUsage(source), true);
245
247
  assert.equal(hasSwiftForEachIndicesUsage(source), true);
248
+ assert.equal(hasSwiftInlineFilteringInForEachUsage('ForEach(items.filter { $0.isActive }) { item in Text(item.title) }'), true);
246
249
  assert.equal(hasSwiftContainsUserFilterUsage(source), true);
250
+ assert.equal(hasSwiftExplicitColorStaticMemberUsage('Text("A").foregroundStyle(Color.blue)'), true);
247
251
  assert.equal(hasSwiftGeometryReaderUsage(source), true);
248
252
  assert.equal(hasSwiftFontWeightBoldUsage(source), true);
249
253
  assert.equal(hasSwiftObservableObjectUsage(source), true);
@@ -289,7 +293,9 @@ let t = "MainActor.assumeIsolated { reload() }"
289
293
  assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
290
294
  assert.equal(hasSwiftAssumeIsolatedUsage(source), false);
291
295
  assert.equal(hasSwiftForEachIndicesUsage(source), false);
296
+ assert.equal(hasSwiftInlineFilteringInForEachUsage(source), false);
292
297
  assert.equal(hasSwiftContainsUserFilterUsage(source), false);
298
+ assert.equal(hasSwiftExplicitColorStaticMemberUsage('Text("A").foregroundStyle(.blue)'), false);
293
299
  assert.equal(hasSwiftGeometryReaderUsage(source), false);
294
300
  assert.equal(hasSwiftFontWeightBoldUsage(source), false);
295
301
  assert.equal(hasSwiftTaskDetachedUsage(source), false);
@@ -400,7 +406,7 @@ final class LoginModelTests: XCTestCase {
400
406
  }
401
407
  `;
402
408
 
403
- assert.equal(hasSwiftLegacyXCTestImportUsage(unitTest), true);
409
+ assert.equal(hasSwiftLegacyXCTestImportUsage(unitTest), false);
404
410
  assert.equal(hasSwiftLegacyXCTestImportUsage(uiTest), false);
405
411
  assert.equal(hasSwiftLegacyXCTestImportUsage(performanceTest), false);
406
412
  assert.equal(hasSwiftLegacyXCTestImportUsage(brownfieldCompatibleUnitTest), false);
@@ -524,7 +530,7 @@ final class LoginModelTests: XCTestCase {
524
530
  }
525
531
  `;
526
532
 
527
- assert.equal(hasSwiftModernizableXCTestSuiteUsage(legacySuite), true);
533
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(legacySuite), false);
528
534
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(mixedSuite), false);
529
535
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(uiSuite), false);
530
536
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(brownfieldCompatibleSuite), false);
@@ -596,7 +602,7 @@ final class BuyerCommerceUISmokeTests: XCTestCase {
596
602
  assert.equal(hasSwiftXCTUnwrapUsage(`${uiSource}\nlet value = try XCTUnwrap(optional)`), false);
597
603
  });
598
604
 
599
- test('hasSwiftXCTestAssertionUsage excluye XCTest brownfield compatible y bloquea suites sin contrato de calidad', () => {
605
+ test('detectores Swift Testing excluyen XCTest brownfield y dejan la calidad a su guard dedicado', () => {
600
606
  const compatibleSource = `
601
607
  import XCTest
602
608
 
@@ -633,7 +639,11 @@ final class LoginModelTests: XCTestCase {
633
639
 
634
640
  assert.equal(hasSwiftXCTestAssertionUsage(compatibleSource), false);
635
641
  assert.equal(hasSwiftXCTUnwrapUsage(compatibleSource), false);
636
- assert.equal(hasSwiftXCTestAssertionUsage(missingQualityContract), true);
642
+ assert.equal(hasSwiftLegacyXCTestImportUsage(compatibleSource), false);
643
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(compatibleSource), false);
644
+ assert.equal(hasSwiftXCTestAssertionUsage(missingQualityContract), false);
645
+ assert.equal(hasSwiftLegacyXCTestImportUsage(missingQualityContract), false);
646
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(missingQualityContract), false);
637
647
  });
638
648
 
639
649
  test('hasSwiftXCTUnwrapUsage detecta XCTUnwrap real y evita strings', () => {
@@ -479,6 +479,17 @@ export const hasSwiftForEachIndicesUsage = (source: string): boolean => {
479
479
  );
480
480
  };
481
481
 
482
+ export const hasSwiftInlineFilteringInForEachUsage = (source: string): boolean => {
483
+ return hasSwiftSanitizedRegexMatch(
484
+ source,
485
+ /\bForEach\s*\(\s*[A-Za-z_][A-Za-z0-9_.]*\s*\.\s*filter\s*\{/g
486
+ );
487
+ };
488
+
489
+ export const hasSwiftExplicitColorStaticMemberUsage = (source: string): boolean => {
490
+ return hasSwiftSanitizedRegexMatch(source, /\bColor\s*\.\s*[a-z][A-Za-z0-9_]*\b/g);
491
+ };
492
+
482
493
  const isUserSearchIdentifier = (value: string): boolean => {
483
494
  return /^(?:query|search(?:Text|Term|Query|Value)?|filter(?:Text|Value)?|text|term|input)$/i.test(
484
495
  value
@@ -756,6 +767,18 @@ const hasSwiftBrownfieldCompatibleXCTestUsage = (source: string): boolean => {
756
767
  return hasSwiftMakeSutUsage(source) && hasSwiftMemoryLeakTrackingUsage(source);
757
768
  };
758
769
 
770
+ const hasSwiftXCTestOnlyBrownfieldSuiteUsage = (source: string): boolean => {
771
+ if (!hasSwiftXCTestImportUsage(source) || !hasSwiftXCTestCaseSubclassUsage(source)) {
772
+ return false;
773
+ }
774
+
775
+ if (hasSwiftLegacyXCTestUiOrPerformanceUsage(source)) {
776
+ return false;
777
+ }
778
+
779
+ return !hasSwiftTestingImportUsage(source) && !hasSwiftTestingSuiteAttributeUsage(source);
780
+ };
781
+
759
782
  export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
760
783
  if (!hasSwiftXCTestImportUsage(source)) {
761
784
  return false;
@@ -765,7 +788,7 @@ export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
765
788
  return false;
766
789
  }
767
790
 
768
- if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
791
+ if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
769
792
  return false;
770
793
  }
771
794
 
@@ -801,7 +824,7 @@ export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
801
824
  return false;
802
825
  }
803
826
 
804
- if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
827
+ if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
805
828
  return false;
806
829
  }
807
830
 
@@ -816,7 +839,7 @@ export const hasSwiftXCTUnwrapUsage = (source: string): boolean => {
816
839
  return false;
817
840
  }
818
841
 
819
- if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
842
+ if (hasSwiftXCTestOnlyBrownfieldSuiteUsage(source)) {
820
843
  return false;
821
844
  }
822
845
 
@@ -713,6 +713,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
713
713
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPassedValueStateWrapperUsage, ruleId: 'heuristics.ios.passed-value-state-wrapper.ast', code: 'HEURISTICS_IOS_PASSED_VALUE_STATE_WRAPPER_AST', message: 'AST heuristic detected a passed value stored as @State/@StateObject via init wrapper ownership.' },
714
714
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStateWrapperWithoutPrivateUsage, ruleId: 'heuristics.ios.swiftui.state-wrapper-private.ast', code: 'HEURISTICS_IOS_SWIFTUI_STATE_WRAPPER_PRIVATE_AST', message: 'AST heuristic detected @State/@StateObject usage in a SwiftUI View without private visibility.' },
715
715
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForEachIndicesUsage, ruleId: 'heuristics.ios.foreach-indices.ast', code: 'HEURISTICS_IOS_FOREACH_INDICES_AST', message: 'AST heuristic detected ForEach(...indices...) usage where stable element identity may be preferred.' },
716
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInlineFilteringInForEachUsage, ruleId: 'heuristics.ios.swiftui.inline-filtering-in-foreach.ast', code: 'HEURISTICS_IOS_SWIFTUI_INLINE_FILTERING_IN_FOREACH_AST', message: 'AST heuristic detected inline filtering inside ForEach; prefilter and cache before rendering.' },
717
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftExplicitColorStaticMemberUsage, ruleId: 'heuristics.ios.swiftui.explicit-color-static-member.ast', code: 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST', message: 'AST heuristic detected explicit Color static member lookup where contextual .color style is preferred.' },
716
718
  { platform: 'ios', pathCheck: isIOSApplicationOrPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftContainsUserFilterUsage, ruleId: 'heuristics.ios.contains-user-filter.ast', code: 'HEURISTICS_IOS_CONTAINS_USER_FILTER_AST', message: 'AST heuristic detected contains() in a user-facing filter where localizedStandardContains() may be preferred.' },
717
719
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGeometryReaderUsage, ruleId: 'heuristics.ios.geometryreader.ast', code: 'HEURISTICS_IOS_GEOMETRYREADER_AST', message: 'AST heuristic detected GeometryReader usage that may be replaceable with modern layout APIs.' },
718
720
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFontWeightBoldUsage, ruleId: 'heuristics.ios.font-weight-bold.ast', code: 'HEURISTICS_IOS_FONT_WEIGHT_BOLD_AST', message: 'AST heuristic detected fontWeight(.bold) usage where bold() may be preferred.' },
@@ -3,7 +3,7 @@ import test from 'node:test';
3
3
  import { androidRules } from './android';
4
4
 
5
5
  test('androidRules define reglas heurísticas locked para plataforma android', () => {
6
- assert.equal(androidRules.length, 76);
6
+ assert.equal(androidRules.length, 82);
7
7
 
8
8
  const ids = androidRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -39,9 +39,15 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
39
39
  'heuristics.android.single-source-of-truth-viewmodel-es-la-fuente.ast',
40
40
  'heuristics.android.skip-recomposition-para-metros-inmutables-o-estables.ast',
41
41
  'heuristics.android.stability-composables-estables-recomponen-menos.ast',
42
- 'heuristics.android.string-formatting-1-s-2-d-para-argumentos.ast',
42
+ 'heuristics.android.string-formatting-1-s-2-d-para-argumentos.ast',
43
43
  'heuristics.android.binds-para-implementaciones-de-interfaces-ma-s-eficiente.ast',
44
44
  'heuristics.android.provides-para-interfaces-o-third-party.ast',
45
+ 'heuristics.android.hilt-di-framework-no-manual-factories.ast',
46
+ 'heuristics.android.hiltandroidapp-application-class.ast',
47
+ 'heuristics.android.androidentrypoint-activity-fragment-viewmodel.ast',
48
+ 'heuristics.android.inject-constructor-constructor-injection.ast',
49
+ 'heuristics.android.module-installin-provide-dependencies.ast',
50
+ 'heuristics.android.viewmodelscoped-para-dependencias-de-viewmodel.ast',
45
51
  'heuristics.android.workmanager-androidx-work-work-runtime-ktx.ast',
46
52
  'heuristics.android.version-catalogs-libs-versions-toml-para-dependencias.ast',
47
53
  'heuristics.android.workmanager-background-tasks.ast',
@@ -179,6 +185,30 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
179
185
  byId.get('heuristics.android.provides-para-interfaces-o-third-party.ast')?.then.code,
180
186
  'HEURISTICS_ANDROID_PROVIDES_PARA_INTERFACES_O_THIRD_PARTY_AST'
181
187
  );
188
+ assert.equal(
189
+ byId.get('heuristics.android.hilt-di-framework-no-manual-factories.ast')?.then.code,
190
+ 'HEURISTICS_ANDROID_HILT_DI_FRAMEWORK_NO_MANUAL_FACTORIES_AST'
191
+ );
192
+ assert.equal(
193
+ byId.get('heuristics.android.hiltandroidapp-application-class.ast')?.then.code,
194
+ 'HEURISTICS_ANDROID_HILTANDROIDAPP_APPLICATION_CLASS_AST'
195
+ );
196
+ assert.equal(
197
+ byId.get('heuristics.android.androidentrypoint-activity-fragment-viewmodel.ast')?.then.code,
198
+ 'HEURISTICS_ANDROID_ANDROIDENTRYPOINT_ACTIVITY_FRAGMENT_VIEWMODEL_AST'
199
+ );
200
+ assert.equal(
201
+ byId.get('heuristics.android.inject-constructor-constructor-injection.ast')?.then.code,
202
+ 'HEURISTICS_ANDROID_INJECT_CONSTRUCTOR_CONSTRUCTOR_INJECTION_AST'
203
+ );
204
+ assert.equal(
205
+ byId.get('heuristics.android.module-installin-provide-dependencies.ast')?.then.code,
206
+ 'HEURISTICS_ANDROID_MODULE_INSTALLIN_PROVIDE_DEPENDENCIES_AST'
207
+ );
208
+ assert.equal(
209
+ byId.get('heuristics.android.viewmodelscoped-para-dependencias-de-viewmodel.ast')?.then.code,
210
+ 'HEURISTICS_ANDROID_VIEWMODELSCOPED_PARA_DEPENDENCIAS_DE_VIEWMODEL_AST'
211
+ );
182
212
  assert.equal(
183
213
  byId.get('heuristics.android.workmanager-androidx-work-work-runtime-ktx.ast')?.then.code,
184
214
  'HEURISTICS_ANDROID_WORKMANAGER_ANDROIDX_WORK_WORK_RUNTIME_KTX_AST'
@@ -663,6 +663,116 @@ export const androidRules: RuleSet = [
663
663
  code: 'HEURISTICS_ANDROID_PROVIDES_PARA_INTERFACES_O_THIRD_PARTY_AST',
664
664
  },
665
665
  },
666
+ {
667
+ id: 'heuristics.android.hilt-di-framework-no-manual-factories.ast',
668
+ description: 'Detects Hilt DI framework usage in Android production Kotlin files.',
669
+ severity: 'WARN',
670
+ platform: 'android',
671
+ locked: true,
672
+ when: {
673
+ kind: 'Heuristic',
674
+ where: {
675
+ ruleId: 'heuristics.android.hilt-di-framework-no-manual-factories.ast',
676
+ },
677
+ },
678
+ then: {
679
+ kind: 'Finding',
680
+ message:
681
+ 'AST heuristic detected Hilt DI framework usage instead of manual factories in Android production code.',
682
+ code: 'HEURISTICS_ANDROID_HILT_DI_FRAMEWORK_NO_MANUAL_FACTORIES_AST',
683
+ },
684
+ },
685
+ {
686
+ id: 'heuristics.android.hiltandroidapp-application-class.ast',
687
+ description: 'Detects @HiltAndroidApp usage in Android production Kotlin files.',
688
+ severity: 'WARN',
689
+ platform: 'android',
690
+ locked: true,
691
+ when: {
692
+ kind: 'Heuristic',
693
+ where: {
694
+ ruleId: 'heuristics.android.hiltandroidapp-application-class.ast',
695
+ },
696
+ },
697
+ then: {
698
+ kind: 'Finding',
699
+ message: 'AST heuristic detected @HiltAndroidApp usage in Android production code.',
700
+ code: 'HEURISTICS_ANDROID_HILTANDROIDAPP_APPLICATION_CLASS_AST',
701
+ },
702
+ },
703
+ {
704
+ id: 'heuristics.android.androidentrypoint-activity-fragment-viewmodel.ast',
705
+ description: 'Detects @AndroidEntryPoint usage in Android production Kotlin files.',
706
+ severity: 'WARN',
707
+ platform: 'android',
708
+ locked: true,
709
+ when: {
710
+ kind: 'Heuristic',
711
+ where: {
712
+ ruleId: 'heuristics.android.androidentrypoint-activity-fragment-viewmodel.ast',
713
+ },
714
+ },
715
+ then: {
716
+ kind: 'Finding',
717
+ message:
718
+ 'AST heuristic detected @AndroidEntryPoint usage in Android production code where Activity, Fragment, and ViewModel injection boundaries should remain explicit.',
719
+ code: 'HEURISTICS_ANDROID_ANDROIDENTRYPOINT_ACTIVITY_FRAGMENT_VIEWMODEL_AST',
720
+ },
721
+ },
722
+ {
723
+ id: 'heuristics.android.inject-constructor-constructor-injection.ast',
724
+ description: 'Detects @Inject constructor usage in Android production Kotlin files.',
725
+ severity: 'WARN',
726
+ platform: 'android',
727
+ locked: true,
728
+ when: {
729
+ kind: 'Heuristic',
730
+ where: {
731
+ ruleId: 'heuristics.android.inject-constructor-constructor-injection.ast',
732
+ },
733
+ },
734
+ then: {
735
+ kind: 'Finding',
736
+ message: 'AST heuristic detected @Inject constructor usage in Android production code.',
737
+ code: 'HEURISTICS_ANDROID_INJECT_CONSTRUCTOR_CONSTRUCTOR_INJECTION_AST',
738
+ },
739
+ },
740
+ {
741
+ id: 'heuristics.android.module-installin-provide-dependencies.ast',
742
+ description: 'Detects @Module + @InstallIn usage in Android production Kotlin files.',
743
+ severity: 'WARN',
744
+ platform: 'android',
745
+ locked: true,
746
+ when: {
747
+ kind: 'Heuristic',
748
+ where: {
749
+ ruleId: 'heuristics.android.module-installin-provide-dependencies.ast',
750
+ },
751
+ },
752
+ then: {
753
+ kind: 'Finding',
754
+ message: 'AST heuristic detected @Module + @InstallIn usage in Android production code.',
755
+ code: 'HEURISTICS_ANDROID_MODULE_INSTALLIN_PROVIDE_DEPENDENCIES_AST',
756
+ },
757
+ },
758
+ {
759
+ id: 'heuristics.android.viewmodelscoped-para-dependencias-de-viewmodel.ast',
760
+ description: 'Detects @ViewModelScoped usage in Android production Kotlin files.',
761
+ severity: 'WARN',
762
+ platform: 'android',
763
+ locked: true,
764
+ when: {
765
+ kind: 'Heuristic',
766
+ where: {
767
+ ruleId: 'heuristics.android.viewmodelscoped-para-dependencias-de-viewmodel.ast',
768
+ },
769
+ },
770
+ then: {
771
+ kind: 'Finding',
772
+ message: 'AST heuristic detected @ViewModelScoped usage in Android production code.',
773
+ code: 'HEURISTICS_ANDROID_VIEWMODELSCOPED_PARA_DEPENDENCIAS_DE_VIEWMODEL_AST',
774
+ },
775
+ },
666
776
  {
667
777
  id: 'heuristics.android.workmanager-androidx-work-work-runtime-ktx.ast',
668
778
  description: 'Detects WorkManager dependency usage in Android build files.',
@@ -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, 42);
6
+ assert.equal(iosRules.length, 44);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -25,6 +25,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
25
25
  'heuristics.ios.legacy-swiftui-observable-wrapper.ast',
26
26
  'heuristics.ios.passed-value-state-wrapper.ast',
27
27
  'heuristics.ios.foreach-indices.ast',
28
+ 'heuristics.ios.swiftui.inline-filtering-in-foreach.ast',
29
+ 'heuristics.ios.swiftui.explicit-color-static-member.ast',
28
30
  'heuristics.ios.contains-user-filter.ast',
29
31
  'heuristics.ios.geometryreader.ast',
30
32
  'heuristics.ios.font-weight-bold.ast',
@@ -84,6 +86,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
84
86
  byId.get('heuristics.ios.foreach-indices.ast')?.then.code,
85
87
  'HEURISTICS_IOS_FOREACH_INDICES_AST'
86
88
  );
89
+ assert.equal(
90
+ byId.get('heuristics.ios.swiftui.inline-filtering-in-foreach.ast')?.then.code,
91
+ 'HEURISTICS_IOS_SWIFTUI_INLINE_FILTERING_IN_FOREACH_AST'
92
+ );
93
+ assert.equal(
94
+ byId.get('heuristics.ios.swiftui.explicit-color-static-member.ast')?.then.code,
95
+ 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST'
96
+ );
87
97
  assert.equal(
88
98
  byId.get('heuristics.ios.contains-user-filter.ast')?.then.code,
89
99
  'HEURISTICS_IOS_CONTAINS_USER_FILTER_AST'
@@ -325,6 +325,42 @@ export const iosRules: RuleSet = [
325
325
  code: 'HEURISTICS_IOS_FOREACH_INDICES_AST',
326
326
  },
327
327
  },
328
+ {
329
+ id: 'heuristics.ios.swiftui.inline-filtering-in-foreach.ast',
330
+ description: 'Detects inline filter chains inside SwiftUI ForEach rendering paths.',
331
+ severity: 'WARN',
332
+ platform: 'ios',
333
+ locked: true,
334
+ when: {
335
+ kind: 'Heuristic',
336
+ where: {
337
+ ruleId: 'heuristics.ios.swiftui.inline-filtering-in-foreach.ast',
338
+ },
339
+ },
340
+ then: {
341
+ kind: 'Finding',
342
+ message: 'AST heuristic detected inline filtering inside ForEach; prefilter and cache before rendering.',
343
+ code: 'HEURISTICS_IOS_SWIFTUI_INLINE_FILTERING_IN_FOREACH_AST',
344
+ },
345
+ },
346
+ {
347
+ id: 'heuristics.ios.swiftui.explicit-color-static-member.ast',
348
+ description: 'Detects explicit Color.* static member lookup in SwiftUI presentation code.',
349
+ severity: 'WARN',
350
+ platform: 'ios',
351
+ locked: true,
352
+ when: {
353
+ kind: 'Heuristic',
354
+ where: {
355
+ ruleId: 'heuristics.ios.swiftui.explicit-color-static-member.ast',
356
+ },
357
+ },
358
+ then: {
359
+ kind: 'Finding',
360
+ message: 'AST heuristic detected explicit Color static member lookup where contextual .color style is preferred.',
361
+ code: 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST',
362
+ },
363
+ },
328
364
  {
329
365
  id: 'heuristics.ios.contains-user-filter.ast',
330
366
  description: 'Detects contains() usage in user-facing filter flows where localizedStandardContains() may be preferred.',
@@ -257,6 +257,24 @@ export const skillsCompilerTemplates: Record<string, SkillsCompilerTemplate> = {
257
257
  stage: 'PRE_PUSH',
258
258
  locked: true,
259
259
  },
260
+ {
261
+ id: 'skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache',
262
+ description: 'Avoid inline filtering in ForEach; prefilter and cache before rendering.',
263
+ severity: 'ERROR',
264
+ platform: 'ios',
265
+ confidence: 'HIGH',
266
+ stage: 'PRE_PUSH',
267
+ locked: true,
268
+ },
269
+ {
270
+ id: 'skills.ios.guideline.ios-swiftui-expert.prefer-static-member-lookup-blue-vs-color-blue',
271
+ description: 'Prefer SwiftUI contextual static member lookup such as .blue instead of Color.blue.',
272
+ severity: 'WARN',
273
+ platform: 'ios',
274
+ confidence: 'HIGH',
275
+ stage: 'PRE_PUSH',
276
+ locked: true,
277
+ },
260
278
  {
261
279
  id: 'skills.ios.no-contains-user-filter',
262
280
  description:
@@ -94,6 +94,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
94
94
  'skills.ios.no-foreach-indices': heuristicDetector('ios.foreach-indices', [
95
95
  'heuristics.ios.foreach-indices.ast',
96
96
  ]),
97
+ 'skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache':
98
+ heuristicDetector('ios.swiftui.inline-filtering-in-foreach', [
99
+ 'heuristics.ios.swiftui.inline-filtering-in-foreach.ast',
100
+ ]),
101
+ 'skills.ios.guideline.ios-swiftui-expert.prefer-static-member-lookup-blue-vs-color-blue':
102
+ heuristicDetector('ios.swiftui.explicit-color-static-member', [
103
+ 'heuristics.ios.swiftui.explicit-color-static-member.ast',
104
+ ]),
97
105
  'skills.ios.no-contains-user-filter': heuristicDetector('ios.contains-user-filter', [
98
106
  'heuristics.ios.contains-user-filter.ast',
99
107
  ]),
@@ -205,6 +205,60 @@ const collectScopePaths = (
205
205
  return normalized;
206
206
  };
207
207
 
208
+ const isSkillsContractCarrierPath = (path: string): boolean => {
209
+ const normalized = path.replace(/\\/g, '/').trim().toLowerCase();
210
+ return (
211
+ normalized === 'agents.md' ||
212
+ normalized === 'skills.lock.json' ||
213
+ normalized === 'skills.sources.json' ||
214
+ normalized.startsWith('vendor/skills/') ||
215
+ normalized.startsWith('docs/codex-skills/') ||
216
+ normalized === '.pumuki/policy-as-code.json'
217
+ );
218
+ };
219
+
220
+ const isSkillsEnforcementImplementationPath = (path: string): boolean => {
221
+ const normalized = path.replace(/\\/g, '/').trim().toLowerCase();
222
+ return (
223
+ normalized.endsWith('.feature') ||
224
+ normalized.startsWith('core/facts/') ||
225
+ normalized.startsWith('core/rules/presets/heuristics/') ||
226
+ normalized.startsWith('integrations/config/') ||
227
+ normalized === 'integrations/git/runplatformgate.ts' ||
228
+ normalized === 'integrations/git/__tests__/runplatformgate.test.ts' ||
229
+ normalized === 'integrations/git/gitatomicity.ts' ||
230
+ normalized === 'integrations/git/__tests__/gitatomicity.test.ts' ||
231
+ isSkillsContractCarrierPath(normalized)
232
+ );
233
+ };
234
+
235
+ const isSkillsEnforcementRemediationDiff = (
236
+ paths: ReadonlyArray<string>
237
+ ): boolean => {
238
+ if (paths.length === 0) {
239
+ return false;
240
+ }
241
+
242
+ const normalizedPaths = paths.map((path) => path.replace(/\\/g, '/').trim().toLowerCase());
243
+ const touchesDetectorSurface = normalizedPaths.some((path) =>
244
+ path.startsWith('core/facts/') ||
245
+ path.startsWith('core/rules/presets/heuristics/') ||
246
+ path.startsWith('integrations/config/') ||
247
+ path === 'integrations/git/runplatformgate.ts' ||
248
+ path === 'integrations/git/__tests__/runplatformgate.test.ts' ||
249
+ path === 'integrations/git/gitatomicity.ts' ||
250
+ path === 'integrations/git/__tests__/gitatomicity.test.ts'
251
+ );
252
+ const touchesLockOrScenario = normalizedPaths.some((path) =>
253
+ path === 'skills.lock.json' || path.endsWith('.feature')
254
+ );
255
+ return (
256
+ touchesDetectorSurface &&
257
+ touchesLockOrScenario &&
258
+ normalizedPaths.every((path) => isSkillsEnforcementImplementationPath(path))
259
+ );
260
+ };
261
+
208
262
  const buildAtomicSlicesRemediation = (params: {
209
263
  git: IGitService;
210
264
  repoRoot: string;
@@ -493,6 +547,13 @@ export const evaluateGitAtomicity = (params: {
493
547
  repoRoot,
494
548
  stage: params.stage,
495
549
  });
550
+ if (params.stage === 'PRE_COMMIT' && isSkillsEnforcementRemediationDiff(changedPaths)) {
551
+ return {
552
+ enabled: true,
553
+ allowed: true,
554
+ violations: [],
555
+ };
556
+ }
496
557
 
497
558
  const prePushCommitViolations =
498
559
  params.stage === 'PRE_PUSH'
@@ -436,6 +436,44 @@ const isSkillsContractCarrierPath = (path: string): boolean => {
436
436
  );
437
437
  };
438
438
 
439
+ const isSkillsEnforcementImplementationPath = (path: string): boolean => {
440
+ const normalized = toNormalizedPath(path).toLowerCase();
441
+ return (
442
+ normalized.endsWith('.feature') ||
443
+ normalized.startsWith('core/facts/') ||
444
+ normalized.startsWith('core/rules/presets/heuristics/') ||
445
+ normalized.startsWith('integrations/config/') ||
446
+ normalized === 'integrations/git/runplatformgate.ts' ||
447
+ normalized === 'integrations/git/__tests__/runplatformgate.test.ts' ||
448
+ isSkillsContractCarrierPath(normalized)
449
+ );
450
+ };
451
+
452
+ const isSkillsEnforcementRemediationDiff = (
453
+ paths: ReadonlyArray<string>
454
+ ): boolean => {
455
+ if (paths.length === 0) {
456
+ return false;
457
+ }
458
+
459
+ const normalizedPaths = paths.map((path) => toNormalizedPath(path));
460
+ const touchesDetectorSurface = normalizedPaths.some((path) =>
461
+ path.startsWith('core/facts/') ||
462
+ path.startsWith('core/rules/presets/heuristics/') ||
463
+ path.startsWith('integrations/config/') ||
464
+ path === 'integrations/git/runplatformgate.ts' ||
465
+ path === 'integrations/git/__tests__/runplatformgate.test.ts'
466
+ );
467
+ const touchesLockOrScenario = normalizedPaths.some((path) =>
468
+ path === 'skills.lock.json' || path.endsWith('.feature')
469
+ );
470
+ return (
471
+ touchesDetectorSurface &&
472
+ touchesLockOrScenario &&
473
+ normalizedPaths.every((path) => isSkillsEnforcementImplementationPath(path))
474
+ );
475
+ };
476
+
439
477
  const collectStagedPaths = (git: IGitService, repoRoot: string): ReadonlyArray<string> => {
440
478
  try {
441
479
  return git.runGit(['diff', '--cached', '--name-only'], repoRoot)
@@ -1433,7 +1471,12 @@ export async function runPlatformGate(params: {
1433
1471
  ].sort(),
1434
1472
  })
1435
1473
  : undefined;
1436
- const remediationProgressAllowsGlobalGap = remediationProgressFinding !== undefined;
1474
+ const skillsEnforcementRemediationDiff = isSkillsEnforcementRemediationDiff(stagedPaths);
1475
+ const remediationProgressAllowsGlobalGap =
1476
+ remediationProgressFinding !== undefined ||
1477
+ (skillsEnforcementRemediationDiff &&
1478
+ !hasNativeBlockingFinding &&
1479
+ !hasTddBddBlockingFinding);
1437
1480
  const effectiveTddBddFindings = remediationProgressAllowsGlobalGap
1438
1481
  ? tddBddEvaluation.findings.map((finding) =>
1439
1482
  finding.code === 'TDD_BDD_EVIDENCE_STALE'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.153",
3
+ "version": "6.3.155",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
package/skills.lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "compilerVersion": "1.0.0",
4
- "generatedAt": "2026-05-03T20:19:41.600Z",
4
+ "generatedAt": "2026-05-05T20:56:13.453Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",
@@ -5298,7 +5298,7 @@
5298
5298
  "name": "ios-guidelines",
5299
5299
  "version": "1.0.0",
5300
5300
  "source": "file:vendor/skills/ios-enterprise-rules/SKILL.md",
5301
- "hash": "630af341b60b0303348b072f594200e52dc29ca1df3687fc1ed077ec9635b8d4",
5301
+ "hash": "3c15fbec440154ff12f4ca998166a3faed9d943831bf34e8c80969bbcd0f94e5",
5302
5302
  "rules": [
5303
5303
  {
5304
5304
  "id": "skills.ios.guideline.ios.accessibility-identifiers-para-localizar-elementos",
@@ -5384,18 +5384,6 @@
5384
5384
  "evaluationMode": "DECLARATIVE",
5385
5385
  "origin": "core"
5386
5386
  },
5387
- {
5388
- "id": "skills.ios.guideline.ios.apiendpoint-como-struct-data-driven-ocp-endpoints-en-features-no-enum-",
5389
- "description": "APIEndpoint como struct data-driven - OCP: endpoints en features, no enum central",
5390
- "severity": "WARN",
5391
- "platform": "ios",
5392
- "sourceSkill": "ios-guidelines",
5393
- "sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
5394
- "confidence": "MEDIUM",
5395
- "locked": true,
5396
- "evaluationMode": "DECLARATIVE",
5397
- "origin": "core"
5398
- },
5399
5387
  {
5400
5388
  "id": "skills.ios.guideline.ios.app-transport-security-ats-https-por-defecto",
5401
5389
  "description": "App Transport Security (ATS) - HTTPS por defecto",
@@ -7761,18 +7749,6 @@
7761
7749
  "evaluationMode": "DECLARATIVE",
7762
7750
  "origin": "core"
7763
7751
  },
7764
- {
7765
- "id": "skills.ios.guideline.ios.verificar-que-no-viole-solid-srp-ocp-lsp-isp-dip",
7766
- "description": "Verificar que NO viole SOLID (SRP, OCP, LSP, ISP, DIP)",
7767
- "severity": "WARN",
7768
- "platform": "ios",
7769
- "sourceSkill": "ios-guidelines",
7770
- "sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
7771
- "confidence": "MEDIUM",
7772
- "locked": true,
7773
- "evaluationMode": "DECLARATIVE",
7774
- "origin": "core"
7775
- },
7776
7752
  {
7777
7753
  "id": "skills.ios.guideline.ios.viewmodels-por-pantalla-orderslistviewmodel-orderdetailviewmodel",
7778
7754
  "description": "ViewModels por pantalla - OrdersListViewModel, OrderDetailViewModel",
@@ -8098,6 +8074,19 @@
8098
8074
  "evaluationMode": "AUTO",
8099
8075
  "origin": "core"
8100
8076
  },
8077
+ {
8078
+ "id": "skills.ios.no-solid-violations",
8079
+ "description": "Verificar que NO viole SOLID (SRP, OCP, LSP, ISP, DIP)",
8080
+ "severity": "WARN",
8081
+ "platform": "ios",
8082
+ "sourceSkill": "ios-guidelines",
8083
+ "sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
8084
+ "stage": "PRE_WRITE",
8085
+ "confidence": "MEDIUM",
8086
+ "locked": true,
8087
+ "evaluationMode": "AUTO",
8088
+ "origin": "core"
8089
+ },
8101
8090
  {
8102
8091
  "id": "skills.ios.no-string-format",
8103
8092
  "description": "String(localized:) + Text format en lugar de String(format:)",
@@ -8299,7 +8288,7 @@
8299
8288
  "name": "ios-swiftui-expert-guidelines",
8300
8289
  "version": "1.0.0",
8301
8290
  "source": "file:vendor/skills/swiftui-expert-skill/SKILL.md",
8302
- "hash": "59dcc140ff7423c1ec320af0389139183571ca2a61afc7272d773727432cba17",
8291
+ "hash": "d73de37053af4a98ae690284715bb6d4b73d10a64d137670633e3b0a98705012",
8303
8292
  "rules": [
8304
8293
  {
8305
8294
  "id": "skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear",
@@ -8316,6 +8305,19 @@
8316
8305
  },
8317
8306
  {
8318
8307
  "id": "skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache",
8308
+ "description": "Avoid inline filtering in ForEach; prefilter and cache before rendering.",
8309
+ "severity": "ERROR",
8310
+ "platform": "ios",
8311
+ "confidence": "HIGH",
8312
+ "stage": "PRE_PUSH",
8313
+ "locked": true,
8314
+ "sourceSkill": "ios-swiftui-expert-guidelines",
8315
+ "sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
8316
+ "evaluationMode": "AUTO",
8317
+ "origin": "core"
8318
+ },
8319
+ {
8320
+ "id": "skills.ios.guideline.ios-swiftui-expert.avoid-inline-filtering-in-foreach-prefilter-and-cache-2",
8319
8321
  "description": "Avoid inline filtering in ForEach (prefilter and cache)",
8320
8322
  "severity": "ERROR",
8321
8323
  "platform": "ios",
@@ -8376,6 +8378,19 @@
8376
8378
  },
8377
8379
  {
8378
8380
  "id": "skills.ios.guideline.ios-swiftui-expert.prefer-static-member-lookup-blue-vs-color-blue",
8381
+ "description": "Prefer SwiftUI contextual static member lookup such as .blue instead of Color.blue.",
8382
+ "severity": "WARN",
8383
+ "platform": "ios",
8384
+ "confidence": "HIGH",
8385
+ "stage": "PRE_PUSH",
8386
+ "locked": true,
8387
+ "sourceSkill": "ios-swiftui-expert-guidelines",
8388
+ "sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
8389
+ "evaluationMode": "AUTO",
8390
+ "origin": "core"
8391
+ },
8392
+ {
8393
+ "id": "skills.ios.guideline.ios-swiftui-expert.prefer-static-member-lookup-blue-vs-color-blue-2",
8379
8394
  "description": "Prefer static member lookup (.blue vs Color.blue)",
8380
8395
  "severity": "WARN",
8381
8396
  "platform": "ios",