pumuki 6.3.202 → 6.3.204

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.
@@ -23,10 +23,12 @@ import {
23
23
  hasSwiftForceUnwrap,
24
24
  hasSwiftGeometryReaderUsage,
25
25
  hasSwiftHardcodedUiStringUsage,
26
+ hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage,
26
27
  hasSwiftLooseAssetResourceUsage,
27
28
  hasSwiftLegacyOnChangeUsage,
28
29
  hasSwiftLegacyExpectationDescriptionUsage,
29
30
  hasSwiftLegacySwiftUiObservableWrapperUsage,
31
+ hasSwiftMainThreadBlockingSleepUsage,
30
32
  hasSwiftMixedTestingFrameworksUsage,
31
33
  hasSwiftLegacyXCTestImportUsage,
32
34
  hasSwiftModernizableXCTestSuiteUsage,
@@ -354,6 +356,60 @@ let sample = "TextAlignment.right"
354
356
  assert.equal(hasSwiftPhysicalTextAlignmentUsage(ignored), false);
355
357
  });
356
358
 
359
+ test('detector iOS de performance detecta sleeps bloqueantes sin confundir Task.sleep', () => {
360
+ const source = `
361
+ final class SplashDelay {
362
+ func wait() {
363
+ Thread.sleep(forTimeInterval: 0.25)
364
+ sleep(1)
365
+ usleep(100)
366
+ }
367
+ }
368
+ `;
369
+ const ignored = `
370
+ func wait() async throws {
371
+ try await Task.sleep(for: .seconds(1))
372
+ let text = "Thread.sleep(forTimeInterval: 1)"
373
+ // sleep(1)
374
+ }
375
+ `;
376
+
377
+ assert.equal(hasSwiftMainThreadBlockingSleepUsage(source), true);
378
+ assert.equal(hasSwiftMainThreadBlockingSleepUsage(ignored), false);
379
+ });
380
+
381
+ test('detector iOS de accesibilidad detecta botones icon-only sin label explicita', () => {
382
+ const source = `
383
+ struct ToolbarView: View {
384
+ var body: some View {
385
+ Button {
386
+ delete()
387
+ } label: {
388
+ Image(systemName: "trash")
389
+ }
390
+ }
391
+ }
392
+ `;
393
+ const ignored = `
394
+ struct ToolbarView: View {
395
+ var body: some View {
396
+ Button {
397
+ delete()
398
+ } label: {
399
+ Image(systemName: "trash")
400
+ }
401
+ .accessibilityLabel(Text("Delete item"))
402
+ Button("Delete") { delete() }
403
+ let text = "Button { Image(systemName: \\"trash\\") }"
404
+ // Button { Image(systemName: "trash") }
405
+ }
406
+ }
407
+ `;
408
+
409
+ assert.equal(hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage(source), true);
410
+ assert.equal(hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage(ignored), false);
411
+ });
412
+
357
413
  test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
358
414
  const source = `
359
415
  final class LegacyBox: @unchecked Sendable {}
@@ -593,6 +593,35 @@ export const hasSwiftPhysicalTextAlignmentUsage = (source: string): boolean => {
593
593
  });
594
594
  };
595
595
 
596
+ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean => {
597
+ const withoutBlockComments = source.replace(/\/\*[\s\S]*?\*\//g, '\n');
598
+ return withoutBlockComments.split(/\r?\n/).some((line) => {
599
+ if (/^\s*\/\//.test(line)) {
600
+ return false;
601
+ }
602
+ const sanitized = stripSwiftLineForSemanticScan(line);
603
+ return (
604
+ /\bThread\s*\.\s*sleep\s*\(/.test(sanitized) ||
605
+ /(^|[^\w.])sleep\s*\(/.test(sanitized) ||
606
+ /(^|[^\w.])usleep\s*\(/.test(sanitized)
607
+ );
608
+ });
609
+ };
610
+
611
+ export const hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage = (source: string): boolean => {
612
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
613
+ const iconOnlyButtonPattern =
614
+ /\bButton\s*(?:\([^)]*\))?\s*\{[\s\S]{0,240}?\bImage\s*\(\s*systemName\s*:\s*""\s*\)[\s\S]{0,240}?\}/g;
615
+ for (const match of sanitized.matchAll(iconOnlyButtonPattern)) {
616
+ const segment = match[0] ?? '';
617
+ const following = sanitized.slice(match.index ?? 0, (match.index ?? 0) + segment.length + 160);
618
+ if (!/\.\s*accessibilityLabel\s*\(/.test(following)) {
619
+ return true;
620
+ }
621
+ }
622
+ return false;
623
+ };
624
+
596
625
  export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
597
626
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
598
627
  if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
@@ -658,6 +658,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
658
658
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLooseAssetResourceUsage, ruleId: 'heuristics.ios.assets.loose-resource.ast', code: 'HEURISTICS_IOS_ASSETS_LOOSE_RESOURCE_AST', message: 'AST heuristic detected loose image resource loading in iOS production code; Asset Catalogs remain the preferred baseline.' },
659
659
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFixedFontSizeUsage, ruleId: 'heuristics.ios.accessibility.fixed-font-size.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_FIXED_FONT_SIZE_AST', message: 'AST heuristic detected fixed font sizing in iOS production code; Dynamic Type semantic text styles remain the preferred baseline.' },
660
660
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPhysicalTextAlignmentUsage, ruleId: 'heuristics.ios.localization.physical-text-alignment.ast', code: 'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST', message: 'AST heuristic detected physical left/right text alignment in iOS production code; leading/trailing remain the preferred RTL-safe baseline.' },
661
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftMainThreadBlockingSleepUsage, ruleId: 'heuristics.ios.performance.blocking-sleep.ast', code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST', message: 'AST heuristic detected blocking sleep usage in iOS production code; async clocks, suspension or cancellable scheduling remain the preferred baseline.' },
662
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage, ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast', code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST', message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.' },
661
663
  { 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.' },
662
664
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftPreconcurrencyUsage, ruleId: 'heuristics.ios.preconcurrency.ast', code: 'HEURISTICS_IOS_PRECONCURRENCY_AST', message: 'AST heuristic detected @preconcurrency usage.' },
663
665
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonisolatedUnsafeUsage, ruleId: 'heuristics.ios.nonisolated-unsafe.ast', code: 'HEURISTICS_IOS_NONISOLATED_UNSAFE_AST', message: 'AST heuristic detected nonisolated(unsafe) usage.' },
@@ -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, 56);
6
+ assert.equal(iosRules.length, 58);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -30,6 +30,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
30
30
  'heuristics.ios.assets.loose-resource.ast',
31
31
  'heuristics.ios.accessibility.fixed-font-size.ast',
32
32
  'heuristics.ios.localization.physical-text-alignment.ast',
33
+ 'heuristics.ios.performance.blocking-sleep.ast',
34
+ 'heuristics.ios.accessibility.icon-only-control-label.ast',
33
35
  'heuristics.ios.unchecked-sendable.ast',
34
36
  'heuristics.ios.preconcurrency.ast',
35
37
  'heuristics.ios.nonisolated-unsafe.ast',
@@ -126,6 +128,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
126
128
  byId.get('heuristics.ios.localization.physical-text-alignment.ast')?.then.code,
127
129
  'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST'
128
130
  );
131
+ assert.equal(
132
+ byId.get('heuristics.ios.performance.blocking-sleep.ast')?.then.code,
133
+ 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST'
134
+ );
135
+ assert.equal(
136
+ byId.get('heuristics.ios.accessibility.icon-only-control-label.ast')?.then.code,
137
+ 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST'
138
+ );
129
139
  assert.equal(
130
140
  byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
131
141
  'HEURISTICS_IOS_PRECONCURRENCY_AST'
@@ -415,6 +415,42 @@ export const iosRules: RuleSet = [
415
415
  code: 'HEURISTICS_IOS_LOCALIZATION_PHYSICAL_TEXT_ALIGNMENT_AST',
416
416
  },
417
417
  },
418
+ {
419
+ id: 'heuristics.ios.performance.blocking-sleep.ast',
420
+ description: 'Detects blocking sleep calls where cancellable async scheduling is the preferred iOS baseline.',
421
+ severity: 'WARN',
422
+ platform: 'ios',
423
+ locked: true,
424
+ when: {
425
+ kind: 'Heuristic',
426
+ where: {
427
+ ruleId: 'heuristics.ios.performance.blocking-sleep.ast',
428
+ },
429
+ },
430
+ then: {
431
+ kind: 'Finding',
432
+ message: 'AST heuristic detected blocking sleep usage in iOS production code; async clocks, suspension or cancellable scheduling remain the preferred baseline.',
433
+ code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST',
434
+ },
435
+ },
436
+ {
437
+ id: 'heuristics.ios.accessibility.icon-only-control-label.ast',
438
+ description: 'Detects icon-only SwiftUI controls without explicit accessibility labels.',
439
+ severity: 'WARN',
440
+ platform: 'ios',
441
+ locked: true,
442
+ when: {
443
+ kind: 'Heuristic',
444
+ where: {
445
+ ruleId: 'heuristics.ios.accessibility.icon-only-control-label.ast',
446
+ },
447
+ },
448
+ then: {
449
+ kind: 'Finding',
450
+ message: 'AST heuristic detected an icon-only SwiftUI control without accessibilityLabel; explicit accessible labels remain the preferred baseline.',
451
+ code: 'HEURISTICS_IOS_ACCESSIBILITY_ICON_ONLY_CONTROL_LABEL_AST',
452
+ },
453
+ },
418
454
  {
419
455
  id: 'heuristics.ios.unchecked-sendable.ast',
420
456
  description: 'Detects @unchecked Sendable usage in iOS production code.',
@@ -626,13 +626,20 @@ struct APIEndpoint: Sendable {
626
626
 
627
627
  - `skills.ios.guideline.ios.dynamic-type-font-scaling-automa-tico` se mapea a `heuristics.ios.accessibility.fixed-font-size.ast`.
628
628
  - `skills.ios.guideline.ios.dynamic-type-fuentes-escalables-y-layouts-adaptativos` se mapea a `heuristics.ios.accessibility.fixed-font-size.ast`.
629
+ - `skills.ios.guideline.ios.accessibility-labels-accessibilitylabel` se mapea a `heuristics.ios.accessibility.icon-only-control-label.ast`.
629
630
  - En `PROJECT MODE: brownfield`, este hallazgo detecta tamaños de fuente fijos (`.font(.system(size:))`, `Font.system(size:)`, `UIFont.systemFont(ofSize:)`) como señal de adopción hacia Dynamic Type y estilos semánticos. No marca `.font(.headline)`, `.font(.body)` ni otros estilos semánticos.
631
+ - En `PROJECT MODE: brownfield`, este hallazgo detecta controles SwiftUI icon-only con `Button` + `Image(systemName:)` sin `.accessibilityLabel` cercano como señal de adopción hacia labels accesibles explícitas. No intenta inferir todos los casos de accesibilidad ni marca botones con texto visible.
630
632
 
631
633
  ### Enforcement AST inicial de RTL iOS
632
634
 
633
635
  - `skills.ios.guideline.ios.rtl-support-right-to-left-para-a-rabe-hebreo` se mapea a `heuristics.ios.localization.physical-text-alignment.ast`.
634
636
  - En `PROJECT MODE: brownfield`, este hallazgo detecta alineaciones físicas `.left`/`.right` en texto y frames (`multilineTextAlignment`, `frame(alignment:)`, `TextAlignment`, `NSTextAlignment`) como señal de adopción hacia `.leading`/`.trailing`. No marca `.leading` ni `.trailing`.
635
637
 
638
+ ### Enforcement AST inicial de bloqueo de thread iOS
639
+
640
+ - `skills.ios.guideline.ios.background-threads-no-bloquear-main-thread` se mapea a `heuristics.ios.performance.blocking-sleep.ast`.
641
+ - En `PROJECT MODE: brownfield`, este hallazgo detecta sleeps bloqueantes (`Thread.sleep`, `sleep`, `usleep`) en Swift production como señal de adopción hacia scheduling cancellable, clocks o suspensión asíncrona. No marca `Task.sleep`.
642
+
636
643
  ### Combine (Reactive):
637
644
  ✅ **Publishers** - AsyncSequence para async, Combine para streams complejos
638
645
  ✅ **@Published** - En ViewModels para binding con Views
@@ -127,6 +127,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
127
127
  'ios.localization.physical-text-alignment',
128
128
  ['heuristics.ios.localization.physical-text-alignment.ast']
129
129
  ),
130
+ 'skills.ios.guideline.ios.background-threads-no-bloquear-main-thread': heuristicDetector(
131
+ 'ios.performance.blocking-sleep',
132
+ ['heuristics.ios.performance.blocking-sleep.ast']
133
+ ),
134
+ 'skills.ios.guideline.ios.accessibility-labels-accessibilitylabel': heuristicDetector(
135
+ 'ios.accessibility.icon-only-control-label',
136
+ ['heuristics.ios.accessibility.icon-only-control-label.ast']
137
+ ),
130
138
  'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
131
139
  'heuristics.ios.unchecked-sendable.ast',
132
140
  ]),
@@ -407,6 +407,22 @@ const normalizeKnownRuleTarget = (
407
407
  if (includes('rtl support') || includes('right to left')) {
408
408
  return 'skills.ios.guideline.ios.rtl-support-right-to-left-para-a-rabe-hebreo';
409
409
  }
410
+ if (
411
+ includes('background threads') ||
412
+ includes('bloquear main thread') ||
413
+ includes('no bloquear main thread') ||
414
+ includes('thread.sleep') ||
415
+ includes('blocking sleep')
416
+ ) {
417
+ return 'skills.ios.guideline.ios.background-threads-no-bloquear-main-thread';
418
+ }
419
+ if (
420
+ includes('accessibility labels') ||
421
+ includes('accessibilitylabel') ||
422
+ includes('accessibility label')
423
+ ) {
424
+ return 'skills.ios.guideline.ios.accessibility-labels-accessibilitylabel';
425
+ }
410
426
  if (
411
427
  includes('mixing legacy xctest style') ||
412
428
  includes('mixed xctest and swift testing') ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.202",
3
+ "version": "6.3.204",
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-13T11:47:21.054Z",
4
+ "generatedAt": "2026-05-13T12:03:23.736Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",
@@ -5764,7 +5764,7 @@
5764
5764
  "name": "ios-guidelines",
5765
5765
  "version": "1.0.0",
5766
5766
  "source": "file:vendor/skills/ios-enterprise-rules/SKILL.md",
5767
- "hash": "4689c3cc0d9f813517c298e0cddac9fffa9c029951fbc1c6f58d95994938f2d9",
5767
+ "hash": "41ef368c744a0b772fa408e83fe7edf4621fe2766ba3ea47d6753339690dbbd2",
5768
5768
  "rules": [
5769
5769
  {
5770
5770
  "id": "skills.ios.guideline.ios.accessibility-identifiers-para-localizar-elementos",
@@ -5787,7 +5787,7 @@
5787
5787
  "sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
5788
5788
  "confidence": "MEDIUM",
5789
5789
  "locked": true,
5790
- "evaluationMode": "DECLARATIVE",
5790
+ "evaluationMode": "AUTO",
5791
5791
  "origin": "core"
5792
5792
  },
5793
5793
  {
@@ -5967,7 +5967,7 @@
5967
5967
  "sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
5968
5968
  "confidence": "MEDIUM",
5969
5969
  "locked": true,
5970
- "evaluationMode": "DECLARATIVE",
5970
+ "evaluationMode": "AUTO",
5971
5971
  "origin": "core"
5972
5972
  },
5973
5973
  {