pumuki 6.3.191 → 6.3.193

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.
@@ -13,6 +13,7 @@ import {
13
13
  hasSwiftDispatchQueueUsage,
14
14
  hasSwiftDispatchSemaphoreUsage,
15
15
  hasSwiftAdHocLoggingUsage,
16
+ hasSwiftAlamofireUsage,
16
17
  hasSwiftForEachIndicesUsage,
17
18
  hasSwiftForceCastUsage,
18
19
  hasSwiftFontWeightBoldUsage,
@@ -43,6 +44,7 @@ import {
43
44
  hasSwiftSheetIsPresentedUsage,
44
45
  hasSwiftScrollViewShowsIndicatorsUsage,
45
46
  hasSwiftSensitiveLoggingUsage,
47
+ hasSwiftJSONSerializationUsage,
46
48
  hasSwiftStringFormatUsage,
47
49
  hasSwiftTabItemUsage,
48
50
  hasSwiftTaskDetachedUsage,
@@ -191,6 +193,36 @@ logger.error("Refresh failed \\(refreshToken)")
191
193
  assert.equal(hasSwiftSensitiveLoggingUsage(structuredSafe), false);
192
194
  });
193
195
 
196
+ test('detectores iOS de networking y JSON detectan Alamofire y JSONSerialization sin leer comentarios ni strings', () => {
197
+ const source = `
198
+ import Alamofire
199
+
200
+ final class APIClient {
201
+ func load() {
202
+ AF.request("https://example.com")
203
+ let object = try? JSONSerialization.jsonObject(with: Data())
204
+ }
205
+ }
206
+ `;
207
+ const ignored = `
208
+ import Foundation
209
+
210
+ final class APIClient {
211
+ func load() async throws {
212
+ let url = URL(string: "https://example.com")
213
+ let dto = try JSONDecoder().decode(UserDTO.self, from: Data())
214
+ let text = "JSONSerialization.jsonObject"
215
+ // AF.request("debug")
216
+ }
217
+ }
218
+ `;
219
+
220
+ assert.equal(hasSwiftAlamofireUsage(source), true);
221
+ assert.equal(hasSwiftJSONSerializationUsage(source), true);
222
+ assert.equal(hasSwiftAlamofireUsage(ignored), false);
223
+ assert.equal(hasSwiftJSONSerializationUsage(ignored), false);
224
+ });
225
+
194
226
  test('hasSwiftUncheckedSendableUsage detecta @unchecked Sendable', () => {
195
227
  const source = `
196
228
  final class LegacyBox: @unchecked Sendable {}
@@ -464,6 +464,17 @@ export const hasSwiftSensitiveLoggingUsage = (source: string): boolean => {
464
464
  });
465
465
  };
466
466
 
467
+ export const hasSwiftAlamofireUsage = (source: string): boolean => {
468
+ return (
469
+ collectSwiftRegexLines(source, /^\s*import\s+Alamofire\b/).length > 0 ||
470
+ collectSwiftRegexLines(source, /\b(?:AF|Alamofire)\s*\.\s*request\b/).length > 0
471
+ );
472
+ };
473
+
474
+ export const hasSwiftJSONSerializationUsage = (source: string): boolean => {
475
+ return collectSwiftRegexLines(source, /\bJSONSerialization\s*\./).length > 0;
476
+ };
477
+
467
478
  export const hasSwiftUncheckedSendableUsage = (source: string): boolean => {
468
479
  return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
469
480
  if (current !== '@' || !swiftSource.startsWith('@unchecked', index)) {
@@ -620,6 +620,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
620
620
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
621
621
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAdHocLoggingUsage, ruleId: 'heuristics.ios.logging.adhoc-print.ast', code: 'HEURISTICS_IOS_LOGGING_ADHOC_PRINT_AST', message: 'AST heuristic detected print/debugPrint/dump/NSLog/os_log usage in iOS production code.' },
622
622
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftSensitiveLoggingUsage, ruleId: 'heuristics.ios.logging.sensitive-data.ast', code: 'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST', message: 'AST heuristic detected sensitive data in an iOS logging call.' },
623
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAlamofireUsage, ruleId: 'heuristics.ios.networking.alamofire.ast', code: 'HEURISTICS_IOS_NETWORKING_ALAMOFIRE_AST', message: 'AST heuristic detected Alamofire usage in iOS production code; URLSession remains the preferred baseline for new code.' },
624
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftJSONSerializationUsage, ruleId: 'heuristics.ios.json.jsonserialization.ast', code: 'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST', message: 'AST heuristic detected JSONSerialization usage in iOS production code; Codable remains the preferred baseline for new code.' },
623
625
  { 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.' },
624
626
  { 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.' },
625
627
  { 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, 45);
6
+ assert.equal(iosRules.length, 47);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -19,6 +19,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
19
19
  'heuristics.ios.task-detached.ast',
20
20
  'heuristics.ios.logging.adhoc-print.ast',
21
21
  'heuristics.ios.logging.sensitive-data.ast',
22
+ 'heuristics.ios.networking.alamofire.ast',
23
+ 'heuristics.ios.json.jsonserialization.ast',
22
24
  'heuristics.ios.unchecked-sendable.ast',
23
25
  'heuristics.ios.preconcurrency.ast',
24
26
  'heuristics.ios.nonisolated-unsafe.ast',
@@ -71,6 +73,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
71
73
  byId.get('heuristics.ios.logging.sensitive-data.ast')?.then.code,
72
74
  'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST'
73
75
  );
76
+ assert.equal(
77
+ byId.get('heuristics.ios.networking.alamofire.ast')?.then.code,
78
+ 'HEURISTICS_IOS_NETWORKING_ALAMOFIRE_AST'
79
+ );
80
+ assert.equal(
81
+ byId.get('heuristics.ios.json.jsonserialization.ast')?.then.code,
82
+ 'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST'
83
+ );
74
84
  assert.equal(
75
85
  byId.get('heuristics.ios.preconcurrency.ast')?.then.code,
76
86
  'HEURISTICS_IOS_PRECONCURRENCY_AST'
@@ -217,6 +217,42 @@ export const iosRules: RuleSet = [
217
217
  code: 'HEURISTICS_IOS_LOGGING_SENSITIVE_DATA_AST',
218
218
  },
219
219
  },
220
+ {
221
+ id: 'heuristics.ios.networking.alamofire.ast',
222
+ description: 'Detects Alamofire usage in iOS production code; URLSession is the preferred baseline for new code.',
223
+ severity: 'WARN',
224
+ platform: 'ios',
225
+ locked: true,
226
+ when: {
227
+ kind: 'Heuristic',
228
+ where: {
229
+ ruleId: 'heuristics.ios.networking.alamofire.ast',
230
+ },
231
+ },
232
+ then: {
233
+ kind: 'Finding',
234
+ message: 'AST heuristic detected Alamofire usage in iOS production code; URLSession remains the preferred baseline for new code.',
235
+ code: 'HEURISTICS_IOS_NETWORKING_ALAMOFIRE_AST',
236
+ },
237
+ },
238
+ {
239
+ id: 'heuristics.ios.json.jsonserialization.ast',
240
+ description: 'Detects JSONSerialization usage in iOS production code; Codable is the preferred baseline for new code.',
241
+ severity: 'WARN',
242
+ platform: 'ios',
243
+ locked: true,
244
+ when: {
245
+ kind: 'Heuristic',
246
+ where: {
247
+ ruleId: 'heuristics.ios.json.jsonserialization.ast',
248
+ },
249
+ },
250
+ then: {
251
+ kind: 'Finding',
252
+ message: 'AST heuristic detected JSONSerialization usage in iOS production code; Codable remains the preferred baseline for new code.',
253
+ code: 'HEURISTICS_IOS_JSON_JSONSERIALIZATION_AST',
254
+ },
255
+ },
220
256
  {
221
257
  id: 'heuristics.ios.unchecked-sendable.ast',
222
258
  description: 'Detects @unchecked Sendable usage in iOS production code.',
@@ -499,8 +499,15 @@ struct UseCaseFactory {
499
499
  ✅ **Request/Response interceptors** - Logging, auth tokens
500
500
  ✅ **SSL pinning** - Para apps con alta seguridad
501
501
  ✅ **Network reachability** - Detectar conectividad
502
- **Alamofire** - Prohibido, usar URLSession nativo
503
- **JSONSerialization** - Prohibido, usar Codable
502
+ ⚠️ **Alamofire** - No introducir en código nuevo; en brownfield existente se trata como señal de migración gradual. URLSession nativo es la línea base preferente.
503
+ ⚠️ **JSONSerialization** - No introducir en código nuevo; en brownfield existente se trata como señal de migración gradual. Codable es la línea base preferente.
504
+
505
+ ### Enforcement AST inicial de networking y JSON iOS
506
+
507
+ - `skills.ios.guideline.ios.alamofire-prohibido-usar-urlsession-nativo` se mapea a `heuristics.ios.networking.alamofire.ast`.
508
+ - `skills.ios.guideline.ios.codable-decodificacio-n-automa-tica-de-json-nunca-jsonserialization` se mapea a `heuristics.ios.json.jsonserialization.ast`.
509
+ - `skills.ios.guideline.ios.codable-para-serializacio-n-json-nunca-jsonserialization` se mapea a `heuristics.ios.json.jsonserialization.ast`.
510
+ - En `PROJECT MODE: brownfield`, estos hallazgos son señal de baseline/adopción y deben evitar drift nuevo sin bloquear deuda histórica salvo promoción explícita de policy.
504
511
 
505
512
  ```swift
506
513
  // ✅ Ejemplo: APIClient con URLSession y async/await
@@ -55,6 +55,18 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
55
55
  'ios.logging.sensitive-data',
56
56
  ['heuristics.ios.logging.sensitive-data.ast']
57
57
  ),
58
+ 'skills.ios.guideline.ios.alamofire-prohibido-usar-urlsession-nativo': heuristicDetector(
59
+ 'ios.networking.alamofire',
60
+ ['heuristics.ios.networking.alamofire.ast']
61
+ ),
62
+ 'skills.ios.guideline.ios.codable-decodificacio-n-automa-tica-de-json-nunca-jsonserialization': heuristicDetector(
63
+ 'ios.json.jsonserialization',
64
+ ['heuristics.ios.json.jsonserialization.ast']
65
+ ),
66
+ 'skills.ios.guideline.ios.codable-para-serializacio-n-json-nunca-jsonserialization': heuristicDetector(
67
+ 'ios.json.jsonserialization',
68
+ ['heuristics.ios.json.jsonserialization.ast']
69
+ ),
58
70
  'skills.ios.no-unchecked-sendable': heuristicDetector('ios.unchecked-sendable', [
59
71
  'heuristics.ios.unchecked-sendable.ast',
60
72
  ]),
@@ -366,11 +366,15 @@ const resolveRuleSeverity = (params: {
366
366
  stage: Exclude<GateStage, 'STAGED'>;
367
367
  }): Severity => {
368
368
  const promotedRuleIds = params.bundlePolicy?.promoteToErrorRuleIds ?? [];
369
- const shouldPromoteBySolidContract =
369
+ const shouldPromote = promotedRuleIds.includes(params.rule.id);
370
+
371
+ if (
370
372
  params.rule.id.endsWith('.no-solid-violations') &&
371
- (params.stage === 'PRE_PUSH' || params.stage === 'CI');
372
- const shouldPromote =
373
- shouldPromoteBySolidContract || promotedRuleIds.includes(params.rule.id);
373
+ (params.stage === 'PRE_PUSH' || params.stage === 'CI') &&
374
+ !shouldPromote
375
+ ) {
376
+ return 'WARN';
377
+ }
374
378
 
375
379
  if (!shouldPromote) {
376
380
  return params.rule.severity;
@@ -111,6 +111,22 @@ const toLifecycleAuditFinding = (finding: SnapshotFinding): LifecycleAuditFindin
111
111
  blocking: isFindingBlocking(finding),
112
112
  });
113
113
 
114
+ const toGateAllowedAuditAdvisoryFinding = (
115
+ finding: LifecycleAuditFinding
116
+ ): LifecycleAuditFinding => {
117
+ if (!finding.blocking) {
118
+ return finding;
119
+ }
120
+ return {
121
+ ...finding,
122
+ severity: 'WARN',
123
+ blocking: false,
124
+ message:
125
+ `${finding.message} ` +
126
+ '(Advisory: current audit gate exited 0, so this finding is not blocking for this run.)',
127
+ };
128
+ };
129
+
114
130
  const buildBlockedWithoutFindingsFallback = (params: {
115
131
  stage: LifecycleAuditStage;
116
132
  gateExitCode: number;
@@ -282,10 +298,15 @@ export const runLifecycleAudit = async (params: {
282
298
  scope,
283
299
  findings,
284
300
  });
301
+ const gateAllowed = originalGateExitCode === 0;
285
302
  const effectiveFindings = scopedGlobalEnforcementOnly
286
303
  ? findings.map(toScopedAuditAdvisoryFinding)
287
- : findings;
304
+ : gateAllowed
305
+ ? findings.map(toGateAllowedAuditAdvisoryFinding)
306
+ : findings;
288
307
  const gateExitCode = scopedGlobalEnforcementOnly ? 0 : originalGateExitCode;
308
+ const effectiveSnapshotOutcome =
309
+ gateExitCode === 0 && snapshotOutcome === 'BLOCK' ? 'PASS' : snapshotOutcome;
289
310
 
290
311
  return {
291
312
  command: 'pumuki audit',
@@ -299,7 +320,7 @@ export const runLifecycleAudit = async (params: {
299
320
  gate_exit_code: gateExitCode,
300
321
  files_scanned: filesScanned,
301
322
  untracked_matching_extensions_count: untrackedMatchingExtensionsCount,
302
- snapshot_outcome: snapshotOutcome,
323
+ snapshot_outcome: effectiveSnapshotOutcome,
303
324
  findings_count: effectiveFindings.length,
304
325
  blocking_findings_count: effectiveFindings.filter((finding) => finding.blocking).length,
305
326
  rules_coverage: evidence?.snapshot.rules_coverage ?? null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.191",
3
+ "version": "6.3.193",
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": {