pumuki 6.3.221 → 6.3.223
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 +78 -0
- package/core/facts/detectors/text/ios.ts +29 -0
- package/core/facts/extractHeuristicFacts.ts +2 -0
- package/core/rules/presets/heuristics/ios.ts +36 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -0
- package/package.json +1 -1
- package/skills.lock.json +4 -4
|
@@ -62,6 +62,8 @@ import {
|
|
|
62
62
|
hasSwiftInsecureTransportUsage,
|
|
63
63
|
hasSwiftJSONSerializationUsage,
|
|
64
64
|
hasSwiftExplicitColorStaticMemberUsage,
|
|
65
|
+
hasSwiftClosureBasedViewBuilderContentUsage,
|
|
66
|
+
hasSwiftRedundantReactiveStateAssignmentUsage,
|
|
65
67
|
hasSwiftInlineForEachTransformUsage,
|
|
66
68
|
hasSwiftStringFormatUsage,
|
|
67
69
|
hasSwiftStrongDelegateReferenceUsage,
|
|
@@ -735,6 +737,10 @@ GeometryReader { proxy in
|
|
|
735
737
|
}
|
|
736
738
|
Text("Headline").fontWeight(.bold)
|
|
737
739
|
Text("State").foregroundStyle(Color.green)
|
|
740
|
+
let content: () -> Content
|
|
741
|
+
.onChange(of: query) { newValue in
|
|
742
|
+
query = newValue
|
|
743
|
+
}
|
|
738
744
|
let filtered = items.filter { $0.title.contains(searchText) }
|
|
739
745
|
ForEach(items.indices, id: \\.self) { index in
|
|
740
746
|
Text(items[index].title)
|
|
@@ -782,6 +788,8 @@ MainActor.assumeIsolated { reload() }
|
|
|
782
788
|
assert.equal(hasSwiftGeometryReaderUsage(source), true);
|
|
783
789
|
assert.equal(hasSwiftFontWeightBoldUsage(source), true);
|
|
784
790
|
assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), true);
|
|
791
|
+
assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), true);
|
|
792
|
+
assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
|
|
785
793
|
assert.equal(hasSwiftObservableObjectUsage(source), true);
|
|
786
794
|
assert.equal(hasSwiftLegacySwiftUiObservableWrapperUsage(source), true);
|
|
787
795
|
assert.equal(hasSwiftNavigationViewUsage(source), true);
|
|
@@ -822,6 +830,8 @@ let s = "nonisolated(unsafe) static var sharedBridge: Model?"
|
|
|
822
830
|
let t = "MainActor.assumeIsolated { reload() }"
|
|
823
831
|
let u = "ForEach(items.filter { $0.isVisible }) { item in }"
|
|
824
832
|
let v = "Color.green"
|
|
833
|
+
let w = "let content: () -> Content"
|
|
834
|
+
let x = ".onChange(of: query) { newValue in query = newValue }"
|
|
825
835
|
`;
|
|
826
836
|
assert.equal(hasSwiftPreconcurrencyUsage(source), false);
|
|
827
837
|
assert.equal(hasSwiftNonisolatedUnsafeUsage(source), false);
|
|
@@ -832,6 +842,8 @@ let v = "Color.green"
|
|
|
832
842
|
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
833
843
|
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
834
844
|
assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
|
|
845
|
+
assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
|
|
846
|
+
assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
|
|
835
847
|
assert.equal(hasSwiftTaskDetachedUsage(source), false);
|
|
836
848
|
assert.equal(hasSwiftNavigationViewUsage(source), false);
|
|
837
849
|
assert.equal(hasSwiftForegroundColorUsage(source), false);
|
|
@@ -850,6 +862,7 @@ test('detectores snapshot SwiftUI ignoran reemplazos modernos', () => {
|
|
|
850
862
|
const source = `
|
|
851
863
|
Text("Primary").foregroundStyle(.blue)
|
|
852
864
|
Text("State").foregroundStyle(.green)
|
|
865
|
+
@ViewBuilder let content: Content
|
|
853
866
|
Image("hero").clipShape(.rect(cornerRadius: 12))
|
|
854
867
|
Text("Headline").bold()
|
|
855
868
|
TabView {
|
|
@@ -884,6 +897,8 @@ ScrollView {
|
|
|
884
897
|
assert.equal(hasSwiftGeometryReaderUsage(source), false);
|
|
885
898
|
assert.equal(hasSwiftFontWeightBoldUsage(source), false);
|
|
886
899
|
assert.equal(hasSwiftExplicitColorStaticMemberUsage(source), false);
|
|
900
|
+
assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), false);
|
|
901
|
+
assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), false);
|
|
887
902
|
assert.equal(hasSwiftForegroundColorUsage(source), false);
|
|
888
903
|
assert.equal(hasSwiftCornerRadiusUsage(source), false);
|
|
889
904
|
assert.equal(hasSwiftTabItemUsage(source), false);
|
|
@@ -917,6 +932,69 @@ let ignored = "Color.green"
|
|
|
917
932
|
assert.equal(hasSwiftExplicitColorStaticMemberUsage(safe), false);
|
|
918
933
|
});
|
|
919
934
|
|
|
935
|
+
test('hasSwiftClosureBasedViewBuilderContentUsage detecta content closure y preserva @ViewBuilder let content', () => {
|
|
936
|
+
const source = `
|
|
937
|
+
struct Card<Content: View>: View {
|
|
938
|
+
private let content: () -> Content
|
|
939
|
+
|
|
940
|
+
init(@ViewBuilder content: @escaping () -> Content) {
|
|
941
|
+
self.content = content
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
`;
|
|
945
|
+
const safe = `
|
|
946
|
+
struct Card<Content: View>: View {
|
|
947
|
+
@ViewBuilder let content: Content
|
|
948
|
+
}
|
|
949
|
+
let ignored = "let content: () -> Content"
|
|
950
|
+
// let content: () -> Content
|
|
951
|
+
`;
|
|
952
|
+
|
|
953
|
+
assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(source), true);
|
|
954
|
+
assert.equal(hasSwiftClosureBasedViewBuilderContentUsage(safe), false);
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
test('hasSwiftRedundantReactiveStateAssignmentUsage detecta asignaciones reactivas redundantes y preserva guard de cambio', () => {
|
|
958
|
+
const source = `
|
|
959
|
+
struct SearchView: View {
|
|
960
|
+
@State private var query = ""
|
|
961
|
+
|
|
962
|
+
var body: some View {
|
|
963
|
+
TextField("Search", text: $query)
|
|
964
|
+
.onChange(of: query) { newValue in
|
|
965
|
+
query = newValue
|
|
966
|
+
}
|
|
967
|
+
.onReceive(model.$value) { value in
|
|
968
|
+
self.query = value
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
`;
|
|
973
|
+
const safe = `
|
|
974
|
+
struct SearchView: View {
|
|
975
|
+
@State private var query = ""
|
|
976
|
+
|
|
977
|
+
var body: some View {
|
|
978
|
+
TextField("Search", text: $query)
|
|
979
|
+
.onChange(of: query) { newValue in
|
|
980
|
+
if query != newValue {
|
|
981
|
+
query = newValue
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
.onReceive(model.$value) { value in
|
|
985
|
+
guard self.query != value else { return }
|
|
986
|
+
self.query = value
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
let ignored = ".onChange(of: query) { newValue in query = newValue }"
|
|
991
|
+
// .onReceive(model.$value) { value in query = value }
|
|
992
|
+
`;
|
|
993
|
+
|
|
994
|
+
assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(source), true);
|
|
995
|
+
assert.equal(hasSwiftRedundantReactiveStateAssignmentUsage(safe), false);
|
|
996
|
+
});
|
|
997
|
+
|
|
920
998
|
test('hasSwiftLegacyXCTestImportUsage detecta XCTest unitario y excluye UI/performance', () => {
|
|
921
999
|
const unitTest = `
|
|
922
1000
|
import XCTest
|
|
@@ -870,6 +870,35 @@ export const hasSwiftExplicitColorStaticMemberUsage = (source: string): boolean
|
|
|
870
870
|
);
|
|
871
871
|
};
|
|
872
872
|
|
|
873
|
+
export const hasSwiftClosureBasedViewBuilderContentUsage = (source: string): boolean => {
|
|
874
|
+
return hasSwiftSanitizedRegexMatch(
|
|
875
|
+
source,
|
|
876
|
+
/\b(?:let|var)\s+content\s*:\s*(?:\(\s*\)\s*->|@\s*escaping\s*\(\s*\)\s*->)\s*(?:some\s+View|Content)\b/g
|
|
877
|
+
);
|
|
878
|
+
};
|
|
879
|
+
|
|
880
|
+
export const hasSwiftRedundantReactiveStateAssignmentUsage = (source: string): boolean => {
|
|
881
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
882
|
+
const reactiveAssignmentPattern =
|
|
883
|
+
/\.(?:onChange|onReceive)\s*\([^)]*\)\s*\{[\s\S]{0,500}?\b(?:self\s*\.\s*)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:newValue|value|output|receivedValue)\b/g;
|
|
884
|
+
|
|
885
|
+
for (const match of sanitized.matchAll(reactiveAssignmentPattern)) {
|
|
886
|
+
const target = match[1];
|
|
887
|
+
const segment = match[0] ?? '';
|
|
888
|
+
if (!target) {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const guardedAssignmentPattern = new RegExp(
|
|
892
|
+
`\\b(?:if|guard)\\s+(?:self\\s*\\.\\s*)?${target}\\s*!=\\s*(?:newValue|value|output|receivedValue)\\b`
|
|
893
|
+
);
|
|
894
|
+
if (!guardedAssignmentPattern.test(segment)) {
|
|
895
|
+
return true;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
return false;
|
|
900
|
+
};
|
|
901
|
+
|
|
873
902
|
export const hasSwiftLegacySwiftUiObservableWrapperUsage = (source: string): boolean => {
|
|
874
903
|
return hasSwiftSanitizedRegexMatch(source, /@\s*(?:StateObject|ObservedObject)\b/);
|
|
875
904
|
};
|
|
@@ -683,6 +683,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
683
683
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftGeometryReaderUsage, ruleId: 'heuristics.ios.geometryreader.ast', code: 'HEURISTICS_IOS_GEOMETRYREADER_AST', message: 'AST heuristic detected GeometryReader usage that may be replaceable with modern layout APIs.' },
|
|
684
684
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftFontWeightBoldUsage, ruleId: 'heuristics.ios.font-weight-bold.ast', code: 'HEURISTICS_IOS_FONT_WEIGHT_BOLD_AST', message: 'AST heuristic detected fontWeight(.bold) usage where bold() may be preferred.' },
|
|
685
685
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftExplicitColorStaticMemberUsage, ruleId: 'heuristics.ios.swiftui.explicit-color-static-member.ast', code: 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST', message: 'AST heuristic detected Color.* static member usage where SwiftUI static member lookup may be preferred.' },
|
|
686
|
+
{ 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.' },
|
|
687
|
+
{ 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.' },
|
|
686
688
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNavigationViewUsage, ruleId: 'heuristics.ios.navigation-view.ast', code: 'HEURISTICS_IOS_NAVIGATION_VIEW_AST', message: 'AST heuristic detected NavigationView usage.' },
|
|
687
689
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForegroundColorUsage, ruleId: 'heuristics.ios.foreground-color.ast', code: 'HEURISTICS_IOS_FOREGROUND_COLOR_AST', message: 'AST heuristic detected foregroundColor usage.' },
|
|
688
690
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCornerRadiusUsage, ruleId: 'heuristics.ios.corner-radius.ast', code: 'HEURISTICS_IOS_CORNER_RADIUS_AST', message: 'AST heuristic detected cornerRadius usage.' },
|
|
@@ -875,6 +875,42 @@ export const iosRules: RuleSet = [
|
|
|
875
875
|
code: 'HEURISTICS_IOS_SWIFTUI_EXPLICIT_COLOR_STATIC_MEMBER_AST',
|
|
876
876
|
},
|
|
877
877
|
},
|
|
878
|
+
{
|
|
879
|
+
id: 'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast',
|
|
880
|
+
description: 'Detects closure-based SwiftUI content properties where @ViewBuilder let content: Content is preferred.',
|
|
881
|
+
severity: 'WARN',
|
|
882
|
+
platform: 'ios',
|
|
883
|
+
locked: true,
|
|
884
|
+
when: {
|
|
885
|
+
kind: 'Heuristic',
|
|
886
|
+
where: {
|
|
887
|
+
ruleId: 'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast',
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
then: {
|
|
891
|
+
kind: 'Finding',
|
|
892
|
+
message: 'AST heuristic detected closure-based content storage; @ViewBuilder let content: Content remains the preferred SwiftUI container baseline.',
|
|
893
|
+
code: 'HEURISTICS_IOS_SWIFTUI_CLOSURE_BASED_VIEWBUILDER_CONTENT_AST',
|
|
894
|
+
},
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
id: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
|
|
898
|
+
description: 'Detects onChange/onReceive state assignments without a value-change guard.',
|
|
899
|
+
severity: 'WARN',
|
|
900
|
+
platform: 'ios',
|
|
901
|
+
locked: true,
|
|
902
|
+
when: {
|
|
903
|
+
kind: 'Heuristic',
|
|
904
|
+
where: {
|
|
905
|
+
ruleId: 'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
then: {
|
|
909
|
+
kind: 'Finding',
|
|
910
|
+
message: 'AST heuristic detected reactive state assignment without a value-change guard; check for value changes before assigning state in hot paths.',
|
|
911
|
+
code: 'HEURISTICS_IOS_SWIFTUI_REDUNDANT_REACTIVE_STATE_ASSIGNMENT_AST',
|
|
912
|
+
},
|
|
913
|
+
},
|
|
878
914
|
{
|
|
879
915
|
id: 'heuristics.ios.navigation-view.ast',
|
|
880
916
|
description: 'Detects NavigationView usage in iOS production code.',
|
|
@@ -246,6 +246,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
246
246
|
heuristicDetector('ios.swiftui.explicit-color-static-member', [
|
|
247
247
|
'heuristics.ios.swiftui.explicit-color-static-member.ast',
|
|
248
248
|
]),
|
|
249
|
+
'skills.ios.guideline.ios-swiftui-expert.prefer-viewbuilder-let-content-content-over-closure-based-content-prop':
|
|
250
|
+
heuristicDetector('ios.swiftui.closure-based-viewbuilder-content', [
|
|
251
|
+
'heuristics.ios.swiftui.closure-based-viewbuilder-content.ast',
|
|
252
|
+
]),
|
|
253
|
+
'skills.ios.guideline.ios-swiftui-expert.avoid-redundant-state-updates-in-onreceive-onchange-scroll-handlers':
|
|
254
|
+
heuristicDetector('ios.swiftui.redundant-reactive-state-assignment', [
|
|
255
|
+
'heuristics.ios.swiftui.redundant-reactive-state-assignment.ast',
|
|
256
|
+
]),
|
|
249
257
|
'skills.ios.no-scrollview-shows-indicators': heuristicDetector(
|
|
250
258
|
'ios.scrollview-shows-indicators',
|
|
251
259
|
['heuristics.ios.scrollview-shows-indicators.ast']
|
|
@@ -346,6 +346,20 @@ const normalizeKnownRuleTarget = (
|
|
|
346
346
|
) {
|
|
347
347
|
return 'skills.ios.guideline.ios-swiftui-expert.prefer-static-member-lookup-blue-vs-color-blue';
|
|
348
348
|
}
|
|
349
|
+
if (
|
|
350
|
+
includes('viewbuilder let content') ||
|
|
351
|
+
(includes('closure-based content') && includes('content')) ||
|
|
352
|
+
(includes('content: content') && includes('viewbuilder'))
|
|
353
|
+
) {
|
|
354
|
+
return 'skills.ios.guideline.ios-swiftui-expert.prefer-viewbuilder-let-content-content-over-closure-based-content-prop';
|
|
355
|
+
}
|
|
356
|
+
if (
|
|
357
|
+
includes('redundant state updates') ||
|
|
358
|
+
(includes('onreceive') && includes('onchange') && includes('state updates')) ||
|
|
359
|
+
(includes('check for value changes') && includes('assigning state'))
|
|
360
|
+
) {
|
|
361
|
+
return 'skills.ios.guideline.ios-swiftui-expert.avoid-redundant-state-updates-in-onreceive-onchange-scroll-handlers';
|
|
362
|
+
}
|
|
349
363
|
if (
|
|
350
364
|
includes('scrollindicators hidden') ||
|
|
351
365
|
includes('scroll indicators hidden') ||
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.223",
|
|
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-13T16:
|
|
4
|
+
"generatedAt": "2026-05-13T16:08:30.839Z",
|
|
5
5
|
"bundles": [
|
|
6
6
|
{
|
|
7
7
|
"name": "android-guidelines",
|
|
@@ -8632,7 +8632,7 @@
|
|
|
8632
8632
|
"name": "ios-swiftui-expert-guidelines",
|
|
8633
8633
|
"version": "1.0.0",
|
|
8634
8634
|
"source": "file:vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8635
|
-
"hash": "
|
|
8635
|
+
"hash": "6570119e6399677bda8b010b801f6397859f7a066eeecf1a33fcfe9ca97348fe",
|
|
8636
8636
|
"rules": [
|
|
8637
8637
|
{
|
|
8638
8638
|
"id": "skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear",
|
|
@@ -8667,7 +8667,7 @@
|
|
|
8667
8667
|
"sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8668
8668
|
"confidence": "MEDIUM",
|
|
8669
8669
|
"locked": true,
|
|
8670
|
-
"evaluationMode": "
|
|
8670
|
+
"evaluationMode": "AUTO",
|
|
8671
8671
|
"origin": "core"
|
|
8672
8672
|
},
|
|
8673
8673
|
{
|
|
@@ -8727,7 +8727,7 @@
|
|
|
8727
8727
|
"sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8728
8728
|
"confidence": "MEDIUM",
|
|
8729
8729
|
"locked": true,
|
|
8730
|
-
"evaluationMode": "
|
|
8730
|
+
"evaluationMode": "AUTO",
|
|
8731
8731
|
"origin": "core"
|
|
8732
8732
|
},
|
|
8733
8733
|
{
|