pumuki 6.3.198 → 6.3.199
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 +31 -0
- package/core/facts/detectors/text/ios.ts +32 -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 +10 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsMarkdownRules.ts +3 -0
- package/package.json +1 -1
- package/skills.lock.json +4 -28
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
hasSwiftForceTryUsage,
|
|
22
22
|
hasSwiftForceUnwrap,
|
|
23
23
|
hasSwiftGeometryReaderUsage,
|
|
24
|
+
hasSwiftHardcodedUiStringUsage,
|
|
24
25
|
hasSwiftLegacyOnChangeUsage,
|
|
25
26
|
hasSwiftLegacyExpectationDescriptionUsage,
|
|
26
27
|
hasSwiftLegacySwiftUiObservableWrapperUsage,
|
|
@@ -268,6 +269,36 @@ let text = "https://example.com/catalog.json"
|
|
|
268
269
|
assert.equal(hasSwiftInsecureTransportUsage(ignored), false);
|
|
269
270
|
});
|
|
270
271
|
|
|
272
|
+
test('detector iOS de localización detecta strings UI hardcodeadas sin confundir keys ni comentarios', () => {
|
|
273
|
+
const source = `
|
|
274
|
+
struct PaywallView: View {
|
|
275
|
+
var body: some View {
|
|
276
|
+
VStack {
|
|
277
|
+
Text("Start premium trial")
|
|
278
|
+
Button("Continue") {}
|
|
279
|
+
Label("Your orders", systemImage: "cart")
|
|
280
|
+
TextField("Search products", text: $query)
|
|
281
|
+
EmptyView().navigationTitle("Account details")
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
`;
|
|
286
|
+
const ignored = `
|
|
287
|
+
struct OrdersView: View {
|
|
288
|
+
var body: some View {
|
|
289
|
+
Text(String(localized: "orders.title"))
|
|
290
|
+
Text("orders.title")
|
|
291
|
+
Button(String(localized: "orders.checkout")) {}
|
|
292
|
+
let sample = "Text(\\"Start premium trial\\")"
|
|
293
|
+
// Text("Debug")
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
`;
|
|
297
|
+
|
|
298
|
+
assert.equal(hasSwiftHardcodedUiStringUsage(source), true);
|
|
299
|
+
assert.equal(hasSwiftHardcodedUiStringUsage(ignored), false);
|
|
300
|
+
});
|
|
301
|
+
|
|
271
302
|
test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
|
|
272
303
|
const source = `
|
|
273
304
|
final class LegacyBox: @unchecked Sendable {}
|
|
@@ -511,6 +511,38 @@ export const hasSwiftInsecureTransportUsage = (source: string): boolean => {
|
|
|
511
511
|
);
|
|
512
512
|
};
|
|
513
513
|
|
|
514
|
+
const swiftUiLiteralTextPatterns = [
|
|
515
|
+
/\b(?:Text|Button|Label|TextField|SecureField)\s*\(\s*"((?:\\.|[^"\\])*)"/,
|
|
516
|
+
/\.navigationTitle\s*\(\s*"((?:\\.|[^"\\])*)"/,
|
|
517
|
+
/\.navigationSubtitle\s*\(\s*"((?:\\.|[^"\\])*)"/,
|
|
518
|
+
/\.accessibilityLabel\s*\(\s*"((?:\\.|[^"\\])*)"/,
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
const looksLikeLocalizationKey = (value: string): boolean => {
|
|
522
|
+
return /^[A-Za-z0-9_]+(?:[.-][A-Za-z0-9_]+)+$/.test(value);
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
export const hasSwiftHardcodedUiStringUsage = (source: string): boolean => {
|
|
526
|
+
const withoutBlockComments = source.replace(/\/\*[\s\S]*?\*\//g, '\n');
|
|
527
|
+
return withoutBlockComments.split(/\r?\n/).some((line) => {
|
|
528
|
+
if (/^\s*\/\//.test(line)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const withoutInlineComment = line.replace(/\/\/.*$/, '');
|
|
532
|
+
return swiftUiLiteralTextPatterns.some((pattern) => {
|
|
533
|
+
const match = withoutInlineComment.match(pattern);
|
|
534
|
+
if (!match) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
const literal = match[1]?.trim() ?? '';
|
|
538
|
+
if (literal.length === 0) {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
return !looksLikeLocalizationKey(literal);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
514
546
|
export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
|
|
515
547
|
return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
|
|
516
548
|
if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
|
|
@@ -654,6 +654,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
654
654
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftInsecureTransportUsage, ruleId: 'heuristics.ios.security.insecure-transport.ast', code: 'HEURISTICS_IOS_SECURITY_INSECURE_TRANSPORT_AST', message: 'AST heuristic detected insecure HTTP transport in iOS production code; HTTPS and ATS remain the preferred baseline.' },
|
|
655
655
|
{ platform: 'ios', pathCheck: isIOSInfoPlistPath, excludePaths: [], detect: TextIOS.hasSwiftInsecureTransportUsage, ruleId: 'heuristics.ios.security.insecure-transport.ast', code: 'HEURISTICS_IOS_SECURITY_INSECURE_TRANSPORT_AST', message: 'AST heuristic detected permissive App Transport Security configuration; HTTPS and ATS remain the preferred baseline.' },
|
|
656
656
|
{ platform: 'ios', pathCheck: isIOSLocalizableStringsPath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.localization.localizable-strings.ast', code: 'HEURISTICS_IOS_LOCALIZATION_LOCALIZABLE_STRINGS_AST', message: 'AST heuristic detected Localizable.strings usage; String Catalogs (.xcstrings) remain the preferred baseline for new localization work.' },
|
|
657
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftHardcodedUiStringUsage, ruleId: 'heuristics.ios.localization.hardcoded-ui-string.ast', code: 'HEURISTICS_IOS_LOCALIZATION_HARDCODED_UI_STRING_AST', message: 'AST heuristic detected hardcoded user-facing SwiftUI text; String(localized:) and String Catalogs remain the preferred baseline.' },
|
|
657
658
|
{ 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.' },
|
|
658
659
|
{ 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.' },
|
|
659
660
|
{ 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, 53);
|
|
7
7
|
|
|
8
8
|
const ids = iosRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -26,6 +26,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
26
26
|
'heuristics.ios.security.userdefaults-sensitive-data.ast',
|
|
27
27
|
'heuristics.ios.security.insecure-transport.ast',
|
|
28
28
|
'heuristics.ios.localization.localizable-strings.ast',
|
|
29
|
+
'heuristics.ios.localization.hardcoded-ui-string.ast',
|
|
29
30
|
'heuristics.ios.unchecked-sendable.ast',
|
|
30
31
|
'heuristics.ios.preconcurrency.ast',
|
|
31
32
|
'heuristics.ios.nonisolated-unsafe.ast',
|
|
@@ -106,6 +107,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
106
107
|
byId.get('heuristics.ios.localization.localizable-strings.ast')?.then.code,
|
|
107
108
|
'HEURISTICS_IOS_LOCALIZATION_LOCALIZABLE_STRINGS_AST'
|
|
108
109
|
);
|
|
110
|
+
assert.equal(
|
|
111
|
+
byId.get('heuristics.ios.localization.hardcoded-ui-string.ast')?.then.code,
|
|
112
|
+
'HEURISTICS_IOS_LOCALIZATION_HARDCODED_UI_STRING_AST'
|
|
113
|
+
);
|
|
109
114
|
assert.equal(
|
|
110
115
|
byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
|
|
111
116
|
'HEURISTICS_IOS_PRECONCURRENCY_AST'
|
|
@@ -343,6 +343,24 @@ export const iosRules: RuleSet = [
|
|
|
343
343
|
code: 'HEURISTICS_IOS_LOCALIZATION_LOCALIZABLE_STRINGS_AST',
|
|
344
344
|
},
|
|
345
345
|
},
|
|
346
|
+
{
|
|
347
|
+
id: 'heuristics.ios.localization.hardcoded-ui-string.ast',
|
|
348
|
+
description: 'Detects hardcoded user-facing SwiftUI text where String(localized:) and String Catalogs are preferred.',
|
|
349
|
+
severity: 'WARN',
|
|
350
|
+
platform: 'ios',
|
|
351
|
+
locked: true,
|
|
352
|
+
when: {
|
|
353
|
+
kind: 'Heuristic',
|
|
354
|
+
where: {
|
|
355
|
+
ruleId: 'heuristics.ios.localization.hardcoded-ui-string.ast',
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
then: {
|
|
359
|
+
kind: 'Finding',
|
|
360
|
+
message: 'AST heuristic detected hardcoded user-facing SwiftUI text; String(localized:) and String Catalogs remain the preferred baseline.',
|
|
361
|
+
code: 'HEURISTICS_IOS_LOCALIZATION_HARDCODED_UI_STRING_AST',
|
|
362
|
+
},
|
|
363
|
+
},
|
|
346
364
|
{
|
|
347
365
|
id: 'heuristics.ios.unchecked-sendable.ast',
|
|
348
366
|
description: 'Detects @unchecked Sendable usage in iOS production code.',
|
|
@@ -607,6 +607,16 @@ struct APIEndpoint: Sendable {
|
|
|
607
607
|
- `skills.ios.guideline.ios.app-transport-security-ats-https-por-defecto` se mapea a `heuristics.ios.security.insecure-transport.ast`.
|
|
608
608
|
- En `PROJECT MODE: brownfield`, este hallazgo detecta `http://` en Swift production y `NSAllowsArbitraryLoads=true` en `Info.plist` como señal de baseline/adopción sin bloquear deuda histórica salvo promoción explícita de policy. HTTPS y ATS permanecen como baseline preferente.
|
|
609
609
|
|
|
610
|
+
### Enforcement AST inicial de localización iOS
|
|
611
|
+
|
|
612
|
+
- `skills.ios.guideline.ios.localizable-strings-deprecado-usar-string-catalogs` se mapea a `heuristics.ios.localization.localizable-strings.ast`.
|
|
613
|
+
- `skills.ios.guideline.ios.string-catalogs-xcstrings` se mapea a `heuristics.ios.localization.localizable-strings.ast`.
|
|
614
|
+
- `skills.ios.guideline.ios.string-catalogs-xcstrings-sistema-moderno-de-localizacio-n-xcode-15` se mapea a `heuristics.ios.localization.localizable-strings.ast`.
|
|
615
|
+
- `skills.ios.guideline.ios.cero-strings-hardcodeadas-en-ui` se mapea a `heuristics.ios.localization.hardcoded-ui-string.ast`.
|
|
616
|
+
- `skills.ios.guideline.ios.string-localized-api-moderna-para-strings-traducibles` se mapea a `heuristics.ios.localization.hardcoded-ui-string.ast`.
|
|
617
|
+
- En `PROJECT MODE: brownfield`, este hallazgo detecta `Localizable.strings` bajo `apps/ios/**` como señal de baseline/adopción sin bloquear deuda histórica salvo promoción explícita de policy. String Catalogs (`.xcstrings`) permanece como baseline preferente.
|
|
618
|
+
- También detecta literales de texto visibles en SwiftUI (`Text`, `Button`, `Label`, `TextField`, `SecureField`, `navigationTitle`, `accessibilityLabel`) como señal de adopción hacia `String(localized:)` y String Catalogs, ignorando keys como `orders.title`.
|
|
619
|
+
|
|
610
620
|
### Combine (Reactive):
|
|
611
621
|
✅ **Publishers** - AsyncSequence para async, Combine para streams complejos
|
|
612
622
|
✅ **@Published** - En ViewModels para binding con Views
|
|
@@ -103,6 +103,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
103
103
|
'ios.localization.localizable-strings',
|
|
104
104
|
['heuristics.ios.localization.localizable-strings.ast']
|
|
105
105
|
),
|
|
106
|
+
'skills.ios.guideline.ios.cero-strings-hardcodeadas-en-ui': heuristicDetector(
|
|
107
|
+
'ios.localization.hardcoded-ui-string',
|
|
108
|
+
['heuristics.ios.localization.hardcoded-ui-string.ast']
|
|
109
|
+
),
|
|
110
|
+
'skills.ios.guideline.ios.string-localized-api-moderna-para-strings-traducibles': heuristicDetector(
|
|
111
|
+
'ios.localization.hardcoded-ui-string',
|
|
112
|
+
['heuristics.ios.localization.hardcoded-ui-string.ast']
|
|
113
|
+
),
|
|
106
114
|
'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
|
|
107
115
|
'heuristics.ios.unchecked-sendable.ast',
|
|
108
116
|
]),
|
|
@@ -395,6 +395,9 @@ const normalizeKnownRuleTarget = (
|
|
|
395
395
|
) {
|
|
396
396
|
return 'skills.ios.guideline.ios.localizable-strings-deprecado-usar-string-catalogs';
|
|
397
397
|
}
|
|
398
|
+
if (includes('strings hardcodeadas') || includes('string localized')) {
|
|
399
|
+
return 'skills.ios.guideline.ios.cero-strings-hardcodeadas-en-ui';
|
|
400
|
+
}
|
|
398
401
|
if (
|
|
399
402
|
includes('mixing legacy xctest style') ||
|
|
400
403
|
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.199",
|
|
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:
|
|
4
|
+
"generatedAt": "2026-05-13T11:29:26.365Z",
|
|
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": "6b68665f29f3894ce007abafdd6a766e63ec02f576fc7299e9d7cf17430786d4",
|
|
5768
5768
|
"rules": [
|
|
5769
5769
|
{
|
|
5770
5770
|
"id": "skills.ios.guideline.ios.accessibility-identifiers-para-localizar-elementos",
|
|
@@ -6092,14 +6092,14 @@
|
|
|
6092
6092
|
},
|
|
6093
6093
|
{
|
|
6094
6094
|
"id": "skills.ios.guideline.ios.cero-strings-hardcodeadas-en-ui",
|
|
6095
|
-
"description": "
|
|
6095
|
+
"description": "String(localized:) y formateadores (Date/Number)",
|
|
6096
6096
|
"severity": "WARN",
|
|
6097
6097
|
"platform": "ios",
|
|
6098
6098
|
"sourceSkill": "ios-guidelines",
|
|
6099
6099
|
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
6100
6100
|
"confidence": "MEDIUM",
|
|
6101
6101
|
"locked": true,
|
|
6102
|
-
"evaluationMode": "
|
|
6102
|
+
"evaluationMode": "AUTO",
|
|
6103
6103
|
"origin": "core"
|
|
6104
6104
|
},
|
|
6105
6105
|
{
|
|
@@ -7879,30 +7879,6 @@
|
|
|
7879
7879
|
"evaluationMode": "DECLARATIVE",
|
|
7880
7880
|
"origin": "core"
|
|
7881
7881
|
},
|
|
7882
|
-
{
|
|
7883
|
-
"id": "skills.ios.guideline.ios.string-localized-api-moderna-para-strings-traducibles",
|
|
7884
|
-
"description": "String(localized:) - API moderna para strings traducibles",
|
|
7885
|
-
"severity": "WARN",
|
|
7886
|
-
"platform": "ios",
|
|
7887
|
-
"sourceSkill": "ios-guidelines",
|
|
7888
|
-
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
7889
|
-
"confidence": "MEDIUM",
|
|
7890
|
-
"locked": true,
|
|
7891
|
-
"evaluationMode": "DECLARATIVE",
|
|
7892
|
-
"origin": "core"
|
|
7893
|
-
},
|
|
7894
|
-
{
|
|
7895
|
-
"id": "skills.ios.guideline.ios.string-localized-y-formateadores-date-number",
|
|
7896
|
-
"description": "String(localized:) y formateadores (Date/Number)",
|
|
7897
|
-
"severity": "WARN",
|
|
7898
|
-
"platform": "ios",
|
|
7899
|
-
"sourceSkill": "ios-guidelines",
|
|
7900
|
-
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
7901
|
-
"confidence": "MEDIUM",
|
|
7902
|
-
"locked": true,
|
|
7903
|
-
"evaluationMode": "DECLARATIVE",
|
|
7904
|
-
"origin": "core"
|
|
7905
|
-
},
|
|
7906
7882
|
{
|
|
7907
7883
|
"id": "skills.ios.guideline.ios.struct-por-defecto-class-solo-cuando-necesites-identity-o-herencia",
|
|
7908
7884
|
"description": "struct por defecto - class solo cuando necesites identity o herencia",
|