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 +14 -0
- package/core/facts/detectors/text/ios.test.ts +112 -0
- package/core/facts/detectors/text/ios.ts +62 -0
- package/core/facts/extractHeuristicFacts.ts +3 -0
- package/core/rules/presets/heuristics/ios.test.ts +16 -1
- package/core/rules/presets/heuristics/ios.ts +57 -0
- package/docs/operations/RELEASE_NOTES.md +7 -0
- package/integrations/config/skillsCompilerTemplates.ts +10 -0
- package/integrations/config/skillsDetectorRegistry.ts +18 -24
- package/integrations/config/skillsMarkdownRules.ts +143 -9
- package/integrations/gate/evaluateAiGate.ts +3 -2
- package/package.json +1 -1
- package/scripts/framework-menu-gate-lib.ts +5 -0
- package/scripts/framework-menu-system-notifications-gate.ts +13 -1
- package/scripts/package-install-smoke-consumer-git-payload-lib.ts +6 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -1
- package/skills.lock.json +112 -195
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,
|
|
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',
|