pumuki 6.3.203 → 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 +33 -0
- package/core/facts/detectors/text/ios.ts +14 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/ios.test.ts +6 -1
- package/core/rules/presets/heuristics/ios.ts +18 -0
- package/docs/codex-skills/ios-enterprise-rules.md +2 -0
- package/integrations/config/skillsDetectorRegistry.ts +4 -0
- package/integrations/config/skillsMarkdownRules.ts +7 -0
- package/package.json +1 -1
- package/skills.lock.json +3 -3
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
hasSwiftForceUnwrap,
|
|
24
24
|
hasSwiftGeometryReaderUsage,
|
|
25
25
|
hasSwiftHardcodedUiStringUsage,
|
|
26
|
+
hasSwiftIconOnlyControlWithoutAccessibilityLabelUsage,
|
|
26
27
|
hasSwiftLooseAssetResourceUsage,
|
|
27
28
|
hasSwiftLegacyOnChangeUsage,
|
|
28
29
|
hasSwiftLegacyExpectationDescriptionUsage,
|
|
@@ -377,6 +378,38 @@ func wait() async throws {
|
|
|
377
378
|
assert.equal(hasSwiftMainThreadBlockingSleepUsage(ignored), false);
|
|
378
379
|
});
|
|
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
|
+
|
|
380
413
|
test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
|
|
381
414
|
const source = `
|
|
382
415
|
final class LegacyBox: @unchecked Sendable {}
|
|
@@ -608,6 +608,20 @@ export const hasSwiftMainThreadBlockingSleepUsage = (source: string): boolean =>
|
|
|
608
608
|
});
|
|
609
609
|
};
|
|
610
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
|
+
|
|
611
625
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
612
626
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
613
627
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -659,6 +659,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
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
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.' },
|
|
662
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.' },
|
|
663
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.' },
|
|
664
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, [
|
|
@@ -31,6 +31,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
31
31
|
'heuristics.ios.accessibility.fixed-font-size.ast',
|
|
32
32
|
'heuristics.ios.localization.physical-text-alignment.ast',
|
|
33
33
|
'heuristics.ios.performance.blocking-sleep.ast',
|
|
34
|
+
'heuristics.ios.accessibility.icon-only-control-label.ast',
|
|
34
35
|
'heuristics.ios.unchecked-sendable.ast',
|
|
35
36
|
'heuristics.ios.preconcurrency.ast',
|
|
36
37
|
'heuristics.ios.nonisolated-unsafe.ast',
|
|
@@ -131,6 +132,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
131
132
|
byId.get('heuristics.ios.performance.blocking-sleep.ast')?.then.code,
|
|
132
133
|
'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST'
|
|
133
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
|
+
);
|
|
134
139
|
assert.equal(
|
|
135
140
|
byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
|
|
136
141
|
'HEURISTICS_IOS_PRECONCURRENCY_AST'
|
|
@@ -433,6 +433,24 @@ export const iosRules: RuleSet = [
|
|
|
433
433
|
code: 'HEURISTICS_IOS_PERFORMANCE_BLOCKING_SLEEP_AST',
|
|
434
434
|
},
|
|
435
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
|
+
},
|
|
436
454
|
{
|
|
437
455
|
id: 'heuristics.ios.unchecked-sendable.ast',
|
|
438
456
|
description: 'Detects @unchecked Sendable usage in iOS production code.',
|
|
@@ -626,7 +626,9 @@ 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
|
|
|
@@ -131,6 +131,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
131
131
|
'ios.performance.blocking-sleep',
|
|
132
132
|
['heuristics.ios.performance.blocking-sleep.ast']
|
|
133
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
|
+
),
|
|
134
138
|
'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
|
|
135
139
|
'heuristics.ios.unchecked-sendable.ast',
|
|
136
140
|
]),
|
|
@@ -416,6 +416,13 @@ const normalizeKnownRuleTarget = (
|
|
|
416
416
|
) {
|
|
417
417
|
return 'skills.ios.guideline.ios.background-threads-no-bloquear-main-thread';
|
|
418
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
|
+
}
|
|
419
426
|
if (
|
|
420
427
|
includes('mixing legacy xctest style') ||
|
|
421
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
|
{
|