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.
@@ -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, 52);
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.198",
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:18:53.521Z",
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": "30c5afdca8ca553960a3063adf318d314f3d60489bf88465b670e82e6e157d51",
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": "Cero strings hardcodeadas en UI",
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": "DECLARATIVE",
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",