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.
- package/core/facts/detectors/text/ios.test.ts +56 -0
- package/core/facts/detectors/text/ios.ts +29 -0
- package/core/facts/extractHeuristicFacts.ts +2 -0
- package/core/rules/presets/heuristics/ios.test.ts +11 -1
- package/core/rules/presets/heuristics/ios.ts +36 -0
- package/docs/codex-skills/ios-enterprise-rules.md +7 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsMarkdownRules.ts +16 -0
- package/package.json +1 -1
- package/skills.lock.json +4 -4
|
@@ -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,
|
|
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.
|
|
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-
|
|
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": "
|
|
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": "
|
|
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": "
|
|
5970
|
+
"evaluationMode": "AUTO",
|
|
5971
5971
|
"origin": "core"
|
|
5972
5972
|
},
|
|
5973
5973
|
{
|