pumuki 6.3.246 → 6.3.248

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/CHANGELOG.md CHANGED
@@ -6,6 +6,20 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.248] - 2026-05-14
10
+
11
+ ### Fixed
12
+
13
+ - **PRE_WRITE iOS critical skills parity:** `skills.ios.critical-test-quality` vuelve a materializarse como cobertura crítica de `PRE_WRITE`; el modo advisory informa sin bloquear y `PUMUKI_SKILLS_ENFORCEMENT=strict` bloquea cuando la regla crítica falta.
14
+ - **Notification opt-out parity:** `PUMUKI_SYSTEM_NOTIFICATIONS=0` y `PUMUKI_NOTIFICATIONS=0` desactivan también el canal de notificaciones del sistema, evitando diálogos macOS en smokes/CI no interactivos.
15
+ - **Release validation alignment:** package smoke, metadata, framework-menu, policy y `rules_coverage.contract=AUTO_RUNTIME_RULES_FOR_STAGE` quedan alineados con la política zero-violation actual.
16
+
17
+ ## [6.3.247] - 2026-05-14
18
+
19
+ ### Added
20
+
21
+ - **SwiftUI sheet action parity:** `skills.ios.guideline.ios-swiftui-expert.sheets-should-own-their-actions-and-call-dismiss-internally` now maps to a scoped AUTO heuristic that detects sheet content receiving parent-owned save/cancel/dismiss callbacks, preserving sheets that own their actions internally.
22
+
9
23
  ## [6.3.246] - 2026-05-14
10
24
 
11
25
  ### Added
@@ -7,6 +7,7 @@ import {
7
7
  findSwiftConcreteDependencyDipMatch,
8
8
  findSwiftPresentationSrpMatch,
9
9
  hasSwiftAnyViewUsage,
10
+ hasSwiftAsyncWithoutAwaitUsage,
10
11
  hasSwiftCallbackStyleSignature,
11
12
  hasSwiftCornerRadiusUsage,
12
13
  hasSwiftDispatchGroupUsage,
@@ -14,6 +15,7 @@ import {
14
15
  hasSwiftDispatchSemaphoreUsage,
15
16
  hasSwiftAdHocLoggingUsage,
16
17
  hasSwiftAlamofireUsage,
18
+ hasSwiftEmptyCatchUsage,
17
19
  hasSwiftForEachIndicesUsage,
18
20
  hasSwiftForEachSelfIdentityUsage,
19
21
  hasSwiftForceCastUsage,
@@ -75,6 +77,7 @@ import {
75
77
  hasSwiftClosureBasedViewBuilderContentUsage,
76
78
  hasSwiftLargeConfigContextViewPropertyUsage,
77
79
  hasSwiftUiConditionalSameViewIdentityUsage,
80
+ hasSwiftUiParentOwnedSheetActionUsage,
78
81
  hasSwiftRedundantReactiveStateAssignmentUsage,
79
82
  hasSwiftInlineForEachTransformUsage,
80
83
  hasSwiftStringFormatUsage,
@@ -135,6 +138,27 @@ test('hasSwiftAnyViewUsage ignora comentarios, strings y coincidencias parciales
135
138
  assert.equal(hasSwiftAnyViewUsage(source), false);
136
139
  });
137
140
 
141
+ test('hasSwiftEmptyCatchUsage detecta catch vacio e ignora comentarios y strings', () => {
142
+ const source = `
143
+ do {
144
+ try repository.save()
145
+ } catch {
146
+ // TODO: report error
147
+ }
148
+ `;
149
+ const safe = `
150
+ let sample = "catch {}"
151
+ do {
152
+ try repository.save()
153
+ } catch {
154
+ logger.error("Save failed")
155
+ }
156
+ `;
157
+
158
+ assert.equal(hasSwiftEmptyCatchUsage(source), true);
159
+ assert.equal(hasSwiftEmptyCatchUsage(safe), false);
160
+ });
161
+
138
162
  test('hasSwiftNonLazyScrollForEachUsage detecta ScrollView con stack no lazy y preserva LazyVStack', () => {
139
163
  const source = `
140
164
  struct FeedView: View {
@@ -1018,6 +1042,34 @@ func wait() async throws {
1018
1042
  assert.equal(hasSwiftMainThreadBlockingSleepUsage(ignored), false);
1019
1043
  });
1020
1044
 
1045
+ test('detector iOS de concurrencia detecta async privado sin await y evita boundaries publicos', () => {
1046
+ const source = `
1047
+ final class ProfileLoader {
1048
+ private func buildSnapshot() async throws -> ProfileSnapshot {
1049
+ ProfileSnapshot.empty
1050
+ }
1051
+ }
1052
+ `;
1053
+ const ignored = `
1054
+ protocol RemoteLoader {
1055
+ func load() async throws -> ProfileSnapshot
1056
+ }
1057
+
1058
+ final class ProfileLoader: RemoteLoader {
1059
+ func load() async throws -> ProfileSnapshot {
1060
+ ProfileSnapshot.empty
1061
+ }
1062
+
1063
+ private func refresh() async throws -> ProfileSnapshot {
1064
+ try await api.load()
1065
+ }
1066
+ }
1067
+ `;
1068
+
1069
+ assert.equal(hasSwiftAsyncWithoutAwaitUsage(source), true);
1070
+ assert.equal(hasSwiftAsyncWithoutAwaitUsage(ignored), false);
1071
+ });
1072
+
1021
1073
  test('detector iOS de accesibilidad detecta botones icon-only sin label explicita', () => {
1022
1074
  const source = `
1023
1075
  struct ToolbarView: View {
@@ -2112,3 +2164,63 @@ struct StatusBadge: View {
2112
2164
  assert.equal(hasSwiftUiConditionalSameViewIdentityUsage(source), true);
2113
2165
  assert.equal(hasSwiftUiConditionalSameViewIdentityUsage(safe), false);
2114
2166
  });
2167
+
2168
+ test('hasSwiftUiParentOwnedSheetActionUsage detecta sheets que reciben callbacks de accion del padre', () => {
2169
+ const source = `
2170
+ struct ParentView: View {
2171
+ @State private var selectedItem: Item?
2172
+
2173
+ var body: some View {
2174
+ List(items) { item in
2175
+ Button(item.name) {
2176
+ selectedItem = item
2177
+ }
2178
+ }
2179
+ .sheet(item: $selectedItem) { item in
2180
+ EditItemSheet(
2181
+ item: item,
2182
+ onSave: { newName in
2183
+ save(item, newName)
2184
+ },
2185
+ onCancel: {
2186
+ selectedItem = nil
2187
+ }
2188
+ )
2189
+ }
2190
+ }
2191
+ }
2192
+ `;
2193
+ const safe = `
2194
+ struct ParentView: View {
2195
+ @State private var selectedItem: Item?
2196
+
2197
+ var body: some View {
2198
+ List(items) { item in
2199
+ Button(item.name) {
2200
+ selectedItem = item
2201
+ }
2202
+ }
2203
+ .sheet(item: $selectedItem) { item in
2204
+ EditItemSheet(item: item)
2205
+ }
2206
+
2207
+ let sample = ".sheet(item: $selectedItem) { EditItemSheet(onCancel: {}) }"
2208
+ // .sheet(item: $selectedItem) { EditItemSheet(onSave: {}) }
2209
+ }
2210
+ }
2211
+
2212
+ struct EditItemSheet: View {
2213
+ @Environment(\\.dismiss) private var dismiss
2214
+ let item: Item
2215
+
2216
+ var body: some View {
2217
+ Button("Cancel") {
2218
+ dismiss()
2219
+ }
2220
+ }
2221
+ }
2222
+ `;
2223
+
2224
+ assert.equal(hasSwiftUiParentOwnedSheetActionUsage(source), true);
2225
+ assert.equal(hasSwiftUiParentOwnedSheetActionUsage(safe), false);
2226
+ });
@@ -476,6 +476,53 @@ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
476
476
  });
477
477
  };
478
478
 
479
+ const findMatchingSwiftBrace = (source: string, openBraceIndex: number): number => {
480
+ let depth = 0;
481
+ for (let index = openBraceIndex; index < source.length; index += 1) {
482
+ const current = source[index];
483
+ if (current === '{') {
484
+ depth += 1;
485
+ } else if (current === '}') {
486
+ depth -= 1;
487
+ if (depth === 0) {
488
+ return index;
489
+ }
490
+ }
491
+ }
492
+ return -1;
493
+ };
494
+
495
+ export const hasSwiftAsyncWithoutAwaitUsage = (source: string): boolean => {
496
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
497
+ const privateAsyncFunctionPattern =
498
+ /\bprivate\s+(?:static\s+|class\s+)?func\s+[A-Za-z_][A-Za-z0-9_]*[^{};]*\basync\b[^{};]*\{/g;
499
+
500
+ for (const match of sanitized.matchAll(privateAsyncFunctionPattern)) {
501
+ const header = match[0] ?? '';
502
+ if (/\boverride\b|\bprotocol\b/.test(header)) {
503
+ continue;
504
+ }
505
+
506
+ const openBraceIndex = (match.index ?? 0) + header.length - 1;
507
+ const closeBraceIndex = findMatchingSwiftBrace(sanitized, openBraceIndex);
508
+ if (closeBraceIndex < 0) {
509
+ continue;
510
+ }
511
+
512
+ const body = sanitized.slice(openBraceIndex + 1, closeBraceIndex);
513
+ if (!/\bawait\b/.test(body)) {
514
+ return true;
515
+ }
516
+ }
517
+
518
+ return false;
519
+ };
520
+
521
+ export const hasSwiftEmptyCatchUsage = (source: string): boolean => {
522
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
523
+ return /\bcatch(?:\s+(?:let|var)\s+[A-Za-z_][A-Za-z0-9_]*)?\s*\{\s*\}/.test(sanitized);
524
+ };
525
+
479
526
  export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
480
527
  const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
481
528
  return /\.onAppear\s*\{[\s\S]{0,500}?\bTask\s*(?:\([^)]*\))?\s*\{/.test(sanitized);
@@ -1019,6 +1066,21 @@ export const hasSwiftUiConditionalSameViewIdentityUsage = (source: string): bool
1019
1066
  return false;
1020
1067
  };
1021
1068
 
1069
+ export const hasSwiftUiParentOwnedSheetActionUsage = (source: string): boolean => {
1070
+ const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1071
+ const swiftUIViewBodyPattern =
1072
+ /\bstruct\s+[A-Za-z_][A-Za-z0-9_]*\s*:\s*View\s*\{[\s\S]{0,2200}?\bvar\s+body\s*:\s*some\s+View\s*\{/m;
1073
+
1074
+ if (!swiftUIViewBodyPattern.test(sanitized)) {
1075
+ return false;
1076
+ }
1077
+
1078
+ const sheetWithCallbackPattern =
1079
+ /\.(?:sheet|fullScreenCover)\s*\([^)]*\)\s*\{[\s\S]{0,1200}?\b[A-Za-z_][A-Za-z0-9_]*\s*\([\s\S]{0,900}?\b(?:onSave|onCancel|onDismiss|onClose|onDone|onDelete|onConfirm)\s*:\s*\{/g;
1080
+
1081
+ return sheetWithCallbackPattern.test(sanitized);
1082
+ };
1083
+
1022
1084
  export const hasSwiftRedundantReactiveStateAssignmentUsage = (source: string): boolean => {
1023
1085
  const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
1024
1086
  const reactiveAssignmentPattern =
@@ -646,6 +646,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
646
646
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftDispatchSemaphoreUsage, ruleId: 'heuristics.ios.dispatchsemaphore.ast', code: 'HEURISTICS_IOS_DISPATCHSEMAPHORE_AST', message: 'AST heuristic detected DispatchSemaphore usage.' },
647
647
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOperationQueueUsage, ruleId: 'heuristics.ios.operation-queue.ast', code: 'HEURISTICS_IOS_OPERATION_QUEUE_AST', message: 'AST heuristic detected OperationQueue usage.' },
648
648
  { 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.' },
649
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAsyncWithoutAwaitUsage, ruleId: 'heuristics.ios.concurrency.async-without-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST', message: 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.' },
650
+ { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEmptyCatchUsage, ruleId: 'heuristics.ios.error.empty-catch.ast', code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST', message: 'AST heuristic detected an empty Swift catch block; handle, log, or propagate the error.' },
649
651
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnAppearTaskUsage, ruleId: 'heuristics.ios.swiftui.onappear-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.' },
650
652
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeTaskUsage, ruleId: 'heuristics.ios.swiftui.onchange-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onChange; .task(id:) provides lifecycle-aware cancellation for value-dependent async work.' },
651
653
  { platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongDelegateReferenceUsage, ruleId: 'heuristics.ios.memory.strong-delegate.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST', message: 'AST heuristic detected a strong delegate/dataSource reference; weak delegates remain the preferred baseline to avoid retain cycles.' },
@@ -691,6 +693,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
691
693
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftClosureBasedViewBuilderContentUsage, ruleId: 'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast', code: 'HEURISTICS_IOS_SWIFTUI_CLOSURE_BASED_VIEWBUILDER_CONTENT_AST', message: 'AST heuristic detected closure-based content storage; @ViewBuilder let content: Content remains the preferred SwiftUI container baseline.' },
692
694
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftLargeConfigContextViewPropertyUsage, ruleId: 'heuristics.ios.swiftui.large-config-context-prop.ast', code: 'HEURISTICS_IOS_SWIFTUI_LARGE_CONFIG_CONTEXT_PROP_AST', message: 'AST heuristic detected a SwiftUI View storing a broad config/context object; pass only needed values to reduce update fan-out.' },
693
695
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiConditionalSameViewIdentityUsage, ruleId: 'heuristics.ios.swiftui.conditional-same-view-identity.ast', code: 'HEURISTICS_IOS_SWIFTUI_CONDITIONAL_SAME_VIEW_IDENTITY_AST', message: 'AST heuristic detected conditional branches rebuilding the same SwiftUI View type; prefer conditional modifiers or values to preserve view identity.' },
696
+ { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftUiParentOwnedSheetActionUsage, ruleId: 'heuristics.ios.swiftui.parent-owned-sheet-action.ast', code: 'HEURISTICS_IOS_SWIFTUI_PARENT_OWNED_SHEET_ACTION_AST', message: 'AST heuristic detected a SwiftUI sheet receiving parent-owned action callbacks; sheets should own save/cancel actions and call dismiss() internally.' },
694
697
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftRedundantReactiveStateAssignmentUsage, ruleId: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast', code: 'HEURISTICS_IOS_SWIFTUI_REDUNDANT_REACTIVE_STATE_ASSIGNMENT_AST', message: 'AST heuristic detected reactive state assignment without a value-change guard; check for value changes before assigning state in hot paths.' },
695
698
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNonLazyScrollForEachUsage, ruleId: 'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast', code: 'HEURISTICS_IOS_SWIFTUI_NON_LAZY_SCROLL_FOREACH_AST', message: 'AST heuristic detected ScrollView with a non-lazy stack feeding ForEach; LazyVStack/LazyHStack remain the preferred baseline for large scrollable collections.' },
696
699
  { platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftViewBodyObjectCreationUsage, ruleId: 'heuristics.ios.swiftui.body-object-creation.ast', code: 'HEURISTICS_IOS_SWIFTUI_BODY_OBJECT_CREATION_AST', message: 'AST heuristic detected formatter object creation inside SwiftUI body; keep body simple and move expensive objects out of render paths.' },
@@ -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, 85);
6
+ assert.equal(iosRules.length, 88);
7
7
 
8
8
  const ids = iosRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -17,6 +17,8 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
17
17
  'heuristics.ios.dispatchsemaphore.ast',
18
18
  'heuristics.ios.operation-queue.ast',
19
19
  'heuristics.ios.task-detached.ast',
20
+ 'heuristics.ios.concurrency.async-without-await.ast',
21
+ 'heuristics.ios.error.empty-catch.ast',
20
22
  'heuristics.ios.swiftui.onappear-task.ast',
21
23
  'heuristics.ios.swiftui.onchange-task.ast',
22
24
  'heuristics.ios.memory.strong-delegate.ast',
@@ -63,6 +65,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
63
65
  'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast',
64
66
  'heuristics.ios.swiftui.large-config-context-prop.ast',
65
67
  'heuristics.ios.swiftui.conditional-same-view-identity.ast',
68
+ 'heuristics.ios.swiftui.parent-owned-sheet-action.ast',
66
69
  'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
67
70
  'heuristics.ios.swiftui.non-lazy-scroll-foreach.ast',
68
71
  'heuristics.ios.swiftui.body-object-creation.ast',
@@ -103,6 +106,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
103
106
  byId.get('heuristics.ios.task-detached.ast')?.then.code,
104
107
  'HEURISTICS_IOS_TASK_DETACHED_AST'
105
108
  );
109
+ assert.equal(
110
+ byId.get('heuristics.ios.concurrency.async-without-await.ast')?.then.code,
111
+ 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST'
112
+ );
113
+ assert.equal(
114
+ byId.get('heuristics.ios.error.empty-catch.ast')?.then.code,
115
+ 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST'
116
+ );
106
117
  assert.equal(
107
118
  byId.get('heuristics.ios.swiftui.onchange-task.ast')?.then.code,
108
119
  'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST'
@@ -215,6 +226,10 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
215
226
  byId.get('heuristics.ios.swiftui.conditional-same-view-identity.ast')?.then.code,
216
227
  'HEURISTICS_IOS_SWIFTUI_CONDITIONAL_SAME_VIEW_IDENTITY_AST'
217
228
  );
229
+ assert.equal(
230
+ byId.get('heuristics.ios.swiftui.parent-owned-sheet-action.ast')?.then.code,
231
+ 'HEURISTICS_IOS_SWIFTUI_PARENT_OWNED_SHEET_ACTION_AST'
232
+ );
218
233
  assert.equal(
219
234
  byId.get('heuristics.ios.uiscreen-main-bounds.ast')?.then.code,
220
235
  'HEURISTICS_IOS_UISCREEN_MAIN_BOUNDS_AST'
@@ -181,6 +181,43 @@ export const iosRules: RuleSet = [
181
181
  code: 'HEURISTICS_IOS_TASK_DETACHED_AST',
182
182
  },
183
183
  },
184
+ {
185
+ id: 'heuristics.ios.concurrency.async-without-await.ast',
186
+ description: 'Detects private async functions that do not await in iOS production code.',
187
+ severity: 'WARN',
188
+ platform: 'ios',
189
+ locked: true,
190
+ when: {
191
+ kind: 'Heuristic',
192
+ where: {
193
+ ruleId: 'heuristics.ios.concurrency.async-without-await.ast',
194
+ },
195
+ },
196
+ then: {
197
+ kind: 'Finding',
198
+ message:
199
+ 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.',
200
+ code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST',
201
+ },
202
+ },
203
+ {
204
+ id: 'heuristics.ios.error.empty-catch.ast',
205
+ description: 'Detects Swift catch blocks that silently swallow errors.',
206
+ severity: 'WARN',
207
+ platform: 'ios',
208
+ locked: true,
209
+ when: {
210
+ kind: 'Heuristic',
211
+ where: {
212
+ ruleId: 'heuristics.ios.error.empty-catch.ast',
213
+ },
214
+ },
215
+ then: {
216
+ kind: 'Finding',
217
+ message: 'AST heuristic detected an empty Swift catch block.',
218
+ code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST',
219
+ },
220
+ },
184
221
  {
185
222
  id: 'heuristics.ios.swiftui.onappear-task.ast',
186
223
  description: 'Detects Task launches from SwiftUI onAppear where .task can provide lifecycle cancellation.',
@@ -1028,6 +1065,26 @@ export const iosRules: RuleSet = [
1028
1065
  code: 'HEURISTICS_IOS_SWIFTUI_CONDITIONAL_SAME_VIEW_IDENTITY_AST',
1029
1066
  },
1030
1067
  },
1068
+ {
1069
+ id: 'heuristics.ios.swiftui.parent-owned-sheet-action.ast',
1070
+ description:
1071
+ 'Detects SwiftUI sheets that receive save/cancel/dismiss callbacks from the parent view.',
1072
+ severity: 'WARN',
1073
+ platform: 'ios',
1074
+ locked: true,
1075
+ when: {
1076
+ kind: 'Heuristic',
1077
+ where: {
1078
+ ruleId: 'heuristics.ios.swiftui.parent-owned-sheet-action.ast',
1079
+ },
1080
+ },
1081
+ then: {
1082
+ kind: 'Finding',
1083
+ message:
1084
+ 'AST heuristic detected a SwiftUI sheet receiving parent-owned action callbacks; sheets should own save/cancel actions and call dismiss() internally.',
1085
+ code: 'HEURISTICS_IOS_SWIFTUI_PARENT_OWNED_SHEET_ACTION_AST',
1086
+ },
1087
+ },
1031
1088
  {
1032
1089
  id: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
1033
1090
  description: 'Detects onChange/onReceive state assignments without a value-change guard.',
@@ -6,6 +6,13 @@ This file keeps only the operational highlights and rollout notes that matter wh
6
6
 
7
7
  ## 2026-04 (CLI stability and macOS notifications)
8
8
 
9
+ ### 2026-05-14 (v6.3.248)
10
+
11
+ - **Normalización iOS mode-aware:** la línea activa conserva reglas iOS automatizables con evidencia concreta y deja como declarativas las reglas greenfield/brownfield que requieren contexto de adopción, baseline o migración.
12
+ - **Package smoke estable para fixtures Git:** los commits y pushes internos de preparación del consumer smoke no disparan hooks del paquete bajo prueba; el gate real sigue validándose en los pasos explícitos del smoke.
13
+ - **Smokes no interactivos sin diálogos macOS:** `PUMUKI_SYSTEM_NOTIFICATIONS=0` y `PUMUKI_NOTIFICATIONS=0` vuelven a apagar el canal de sistema, evitando bloqueos por Swift dialog en validaciones de release.
14
+ - **Rollout recomendado:** publicar `pumuki@6.3.248` tras el test suite global verde; `validation:package-smoke`, metadata local y `PRE_WRITE` strict/advisory quedan alineados para esta versión.
15
+
9
16
  ### 2026-04-25 (v6.3.116)
10
17
 
11
18
  - **Inventario local real de dependencias:** `status` y `doctor` conservan `trackedNodeModules*` como señal estricta de seguridad Git y añaden `dependencyInventory` como fuente de verdad de instalación local.
@@ -108,6 +108,16 @@ export const skillsCompilerTemplates: Record<string, SkillsCompilerTemplate> = {
108
108
  stage: 'PRE_PUSH',
109
109
  locked: true,
110
110
  },
111
+ {
112
+ id: 'skills.ios.no-async-without-await',
113
+ description:
114
+ 'Avoid private async functions without await; remove async unless a protocol or override boundary requires it.',
115
+ severity: 'WARN',
116
+ platform: 'ios',
117
+ confidence: 'MEDIUM',
118
+ stage: 'PRE_PUSH',
119
+ locked: true,
120
+ },
111
121
  {
112
122
  id: 'skills.ios.no-unchecked-sendable',
113
123
  description: 'Avoid @unchecked Sendable in production iOS code without strict safety invariant.',
@@ -47,6 +47,9 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
47
47
  'skills.ios.no-task-detached': heuristicDetector('ios.task-detached', [
48
48
  'heuristics.ios.task-detached.ast',
49
49
  ]),
50
+ 'skills.ios.no-async-without-await': heuristicDetector('ios.concurrency.async-without-await', [
51
+ 'heuristics.ios.concurrency.async-without-await.ast',
52
+ ]),
50
53
  'skills.ios.guideline.ios.delegation-pattern-weak-delegates-para-evitar-retain-cycles': heuristicDetector(
51
54
  'ios.memory.strong-delegate',
52
55
  ['heuristics.ios.memory.strong-delegate.ast']
@@ -58,6 +61,13 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
58
61
  'heuristics.ios.memory.strong-self-escaping-closure.ast',
59
62
  ]
60
63
  ),
64
+ 'skills.ios.guideline.ios.retain-cycles-memory-leaks': heuristicDetector(
65
+ 'ios.memory.retain-cycles',
66
+ [
67
+ 'heuristics.ios.memory.strong-delegate.ast',
68
+ 'heuristics.ios.memory.strong-self-escaping-closure.ast',
69
+ ]
70
+ ),
61
71
  'skills.ios.guideline.ios.no-singleton-usar-inyeccio-n-de-dependencias-no-compartir-instancias-g': heuristicDetector(
62
72
  'ios.architecture.custom-singleton',
63
73
  ['heuristics.ios.architecture.custom-singleton.ast']
@@ -98,6 +108,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
98
108
  'ios.logging.adhoc-print',
99
109
  ['heuristics.ios.logging.adhoc-print.ast']
100
110
  ),
111
+ 'skills.ios.guideline.ios.catch-vaci-os-prohibido-silenciar-errores-ast-common-error-emptycatch': heuristicDetector(
112
+ 'ios.error.empty-catch',
113
+ ['heuristics.ios.error.empty-catch.ast']
114
+ ),
101
115
  'skills.ios.guideline.ios.no-loggear-pii-tokens-emails-ids-sensibles': heuristicDetector(
102
116
  'ios.logging.sensitive-data',
103
117
  ['heuristics.ios.logging.sensitive-data.ast']
@@ -110,10 +124,6 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
110
124
  'ios.localization.unlocalized-dateformatter',
111
125
  ['heuristics.ios.localization.unlocalized-dateformatter.ast']
112
126
  ),
113
- 'skills.ios.guideline.ios.alamofire-prohibido-usar-urlsession-nativo': heuristicDetector(
114
- 'ios.networking.alamofire',
115
- ['heuristics.ios.networking.alamofire.ast']
116
- ),
117
127
  'skills.ios.guideline.ios.codable-decodificacio-n-automa-tica-de-json-nunca-jsonserialization': heuristicDetector(
118
128
  'ios.json.jsonserialization',
119
129
  ['heuristics.ios.json.jsonserialization.ast']
@@ -122,14 +132,6 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
122
132
  'ios.json.jsonserialization',
123
133
  ['heuristics.ios.json.jsonserialization.ast']
124
134
  ),
125
- 'skills.ios.guideline.ios.cocoapods-prohibido': heuristicDetector(
126
- 'ios.dependencies.cocoapods',
127
- ['heuristics.ios.dependencies.cocoapods.ast']
128
- ),
129
- 'skills.ios.guideline.ios.carthage-prohibido': heuristicDetector(
130
- 'ios.dependencies.carthage',
131
- ['heuristics.ios.dependencies.carthage.ast']
132
- ),
133
135
  'skills.ios.guideline.ios.keychain-passwords-tokens-no-userdefaults': heuristicDetector(
134
136
  'ios.security.userdefaults-sensitive-data',
135
137
  ['heuristics.ios.security.userdefaults-sensitive-data.ast']
@@ -146,18 +148,6 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
146
148
  'ios.security.insecure-transport',
147
149
  ['heuristics.ios.security.insecure-transport.ast']
148
150
  ),
149
- 'skills.ios.guideline.ios.localizable-strings-deprecado-usar-string-catalogs': heuristicDetector(
150
- 'ios.localization.localizable-strings',
151
- ['heuristics.ios.localization.localizable-strings.ast']
152
- ),
153
- 'skills.ios.guideline.ios.string-catalogs-xcstrings': heuristicDetector(
154
- 'ios.localization.localizable-strings',
155
- ['heuristics.ios.localization.localizable-strings.ast']
156
- ),
157
- 'skills.ios.guideline.ios.string-catalogs-xcstrings-sistema-moderno-de-localizacio-n-xcode-15': heuristicDetector(
158
- 'ios.localization.localizable-strings',
159
- ['heuristics.ios.localization.localizable-strings.ast']
160
- ),
161
151
  'skills.ios.guideline.ios.cero-strings-hardcodeadas-en-ui': heuristicDetector(
162
152
  'ios.localization.hardcoded-ui-string',
163
153
  ['heuristics.ios.localization.hardcoded-ui-string.ast']
@@ -288,6 +278,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
288
278
  heuristicDetector('ios.swiftui.conditional-same-view-identity', [
289
279
  'heuristics.ios.swiftui.conditional-same-view-identity.ast',
290
280
  ]),
281
+ 'skills.ios.guideline.ios-swiftui-expert.sheets-should-own-their-actions-and-call-dismiss-internally':
282
+ heuristicDetector('ios.swiftui.parent-owned-sheet-action', [
283
+ 'heuristics.ios.swiftui.parent-owned-sheet-action.ast',
284
+ ]),
291
285
  'skills.ios.guideline.ios-swiftui-expert.avoid-redundant-state-updates-in-onreceive-onchange-scroll-handlers':
292
286
  heuristicDetector('ios.swiftui.redundant-reactive-state-assignment', [
293
287
  'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',