pumuki 6.3.216 → 6.3.218
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 +38 -0
- package/core/facts/detectors/text/ios.ts +5 -0
- package/core/facts/extractHeuristicFacts.ts +1 -0
- package/core/rules/presets/heuristics/ios.ts +19 -0
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/integrations/config/skillsMarkdownRules.ts +9 -0
- package/integrations/gate/evaluateAiGate.ts +110 -1
- package/package.json +1 -1
- package/skills.lock.json +16 -16
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
hasSwiftNavigationViewUsage,
|
|
46
46
|
hasSwiftNonIBOutletImplicitlyUnwrappedOptionalUsage,
|
|
47
47
|
hasSwiftObservableObjectUsage,
|
|
48
|
+
hasSwiftOnAppearTaskUsage,
|
|
48
49
|
hasSwiftOnTapGestureUsage,
|
|
49
50
|
hasSwiftOperationQueueUsage,
|
|
50
51
|
hasSwiftContainsUserFilterUsage,
|
|
@@ -186,6 +187,43 @@ Task {
|
|
|
186
187
|
assert.equal(hasSwiftTaskDetachedUsage(negative), false);
|
|
187
188
|
});
|
|
188
189
|
|
|
190
|
+
test('hasSwiftOnAppearTaskUsage detecta Task dentro de onAppear y preserva task modifier', () => {
|
|
191
|
+
const source = `
|
|
192
|
+
struct FeedView: View {
|
|
193
|
+
var body: some View {
|
|
194
|
+
List(items) { item in
|
|
195
|
+
Text(item.title)
|
|
196
|
+
}
|
|
197
|
+
.onAppear {
|
|
198
|
+
Task {
|
|
199
|
+
await viewModel.load()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
`;
|
|
205
|
+
const safe = `
|
|
206
|
+
struct FeedView: View {
|
|
207
|
+
var body: some View {
|
|
208
|
+
List(items) { item in
|
|
209
|
+
Text(item.title)
|
|
210
|
+
}
|
|
211
|
+
.task {
|
|
212
|
+
await viewModel.load()
|
|
213
|
+
}
|
|
214
|
+
.onAppear {
|
|
215
|
+
analytics.trackScreen()
|
|
216
|
+
}
|
|
217
|
+
let text = ".onAppear { Task { await load() } }"
|
|
218
|
+
// .onAppear { Task { await load() } }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
assert.equal(hasSwiftOnAppearTaskUsage(source), true);
|
|
224
|
+
assert.equal(hasSwiftOnAppearTaskUsage(safe), false);
|
|
225
|
+
});
|
|
226
|
+
|
|
189
227
|
test('hasSwiftStrongDelegateReferenceUsage detecta delegates fuertes y preserva weak delegates', () => {
|
|
190
228
|
const positive = `
|
|
191
229
|
final class CheckoutCoordinator {
|
|
@@ -437,6 +437,11 @@ export const hasSwiftTaskDetachedUsage = (source: string): boolean => {
|
|
|
437
437
|
});
|
|
438
438
|
};
|
|
439
439
|
|
|
440
|
+
export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
|
|
441
|
+
const sanitized = sanitizeSwiftSourceForMultilineRegex(source);
|
|
442
|
+
return /\.onAppear\s*\{[\s\S]{0,500}?\bTask\s*(?:\([^)]*\))?\s*\{/.test(sanitized);
|
|
443
|
+
};
|
|
444
|
+
|
|
440
445
|
export const hasSwiftStrongDelegateReferenceUsage = (source: string): boolean => {
|
|
441
446
|
const delegatePropertyPattern =
|
|
442
447
|
/\b(?:var|let)\s+(?:[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)|delegate|dataSource)\s*:\s*(?:any\s+)?[A-Za-z_][A-Za-z0-9_]*(?:Delegate|DataSource)\b/;
|
|
@@ -646,6 +646,7 @@ 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: 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.' },
|
|
649
650
|
{ 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.' },
|
|
650
651
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
|
|
651
652
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftCustomSingletonUsage, ruleId: 'heuristics.ios.architecture.custom-singleton.ast', code: 'HEURISTICS_IOS_ARCHITECTURE_CUSTOM_SINGLETON_AST', message: 'AST heuristic detected a custom static shared singleton in iOS production code; dependency injection remains the preferred baseline for app-owned services.' },
|
|
@@ -181,6 +181,25 @@ export const iosRules: RuleSet = [
|
|
|
181
181
|
code: 'HEURISTICS_IOS_TASK_DETACHED_AST',
|
|
182
182
|
},
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
id: 'heuristics.ios.swiftui.onappear-task.ast',
|
|
186
|
+
description: 'Detects Task launches from SwiftUI onAppear where .task can provide lifecycle cancellation.',
|
|
187
|
+
severity: 'WARN',
|
|
188
|
+
platform: 'ios',
|
|
189
|
+
locked: true,
|
|
190
|
+
when: {
|
|
191
|
+
kind: 'Heuristic',
|
|
192
|
+
where: {
|
|
193
|
+
ruleId: 'heuristics.ios.swiftui.onappear-task.ast',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
then: {
|
|
197
|
+
kind: 'Finding',
|
|
198
|
+
message:
|
|
199
|
+
'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.',
|
|
200
|
+
code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
184
203
|
{
|
|
185
204
|
id: 'heuristics.ios.memory.strong-delegate.ast',
|
|
186
205
|
description: 'Detects strong delegate/dataSource references in iOS production code.',
|
|
@@ -244,6 +244,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
244
244
|
'skills.ios.no-legacy-onchange': heuristicDetector('ios.legacy-onchange', [
|
|
245
245
|
'heuristics.ios.legacy-onchange.ast',
|
|
246
246
|
]),
|
|
247
|
+
'skills.ios.guideline.ios.task-task-id-trabajos-async-con-cancelacio-n-automa-tica': heuristicDetector(
|
|
248
|
+
'ios.swiftui.onappear-task',
|
|
249
|
+
['heuristics.ios.swiftui.onappear-task.ast']
|
|
250
|
+
),
|
|
251
|
+
'skills.ios.guideline.ios-swiftui-expert.use-task-modifier-for-automatic-cancellation-of-async-work': heuristicDetector(
|
|
252
|
+
'ios.swiftui.onappear-task',
|
|
253
|
+
['heuristics.ios.swiftui.onappear-task.ast']
|
|
254
|
+
),
|
|
247
255
|
'skills.ios.no-uiscreen-main-bounds': heuristicDetector('ios.uiscreen-main-bounds', [
|
|
248
256
|
'heuristics.ios.uiscreen-main-bounds.ast',
|
|
249
257
|
]),
|
|
@@ -360,6 +360,15 @@ const normalizeKnownRuleTarget = (
|
|
|
360
360
|
if (includes('uiscreen main bounds') || includes('uiscreen.main.bounds')) {
|
|
361
361
|
return 'skills.ios.no-uiscreen-main-bounds';
|
|
362
362
|
}
|
|
363
|
+
if (
|
|
364
|
+
includes('task/.task(id') ||
|
|
365
|
+
includes('trabajos async con cancelacion automatica') ||
|
|
366
|
+
includes('trabajos async con cancelacio n automa tica') ||
|
|
367
|
+
includes('task modifier for automatic cancellation') ||
|
|
368
|
+
includes('automatic cancellation of async work')
|
|
369
|
+
) {
|
|
370
|
+
return 'skills.ios.guideline.ios-swiftui-expert.use-task-modifier-for-automatic-cancellation-of-async-work';
|
|
371
|
+
}
|
|
363
372
|
if (
|
|
364
373
|
includes('swift testing over xctest') ||
|
|
365
374
|
includes('prefer import testing') ||
|
|
@@ -1144,7 +1144,15 @@ const collectEvidenceViolations = (
|
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
if (result.evidence.ai_gate.status === 'BLOCKED') {
|
|
1147
|
-
|
|
1147
|
+
const gateBlockedMessage = 'Evidence AI gate status is BLOCKED.';
|
|
1148
|
+
violations.push(
|
|
1149
|
+
isAdvisoryDocumentationRenderSlice(repoRoot, ageSeconds, maxAgeSeconds)
|
|
1150
|
+
? toWarnViolation(
|
|
1151
|
+
'EVIDENCE_GATE_BLOCKED',
|
|
1152
|
+
`${gateBlockedMessage} Advisory because the current slice only contains documentation/render/tooling artifacts and evidence is fresh.`
|
|
1153
|
+
)
|
|
1154
|
+
: toErrorViolation('EVIDENCE_GATE_BLOCKED', gateBlockedMessage)
|
|
1155
|
+
);
|
|
1148
1156
|
}
|
|
1149
1157
|
|
|
1150
1158
|
if (stage === 'PRE_WRITE') {
|
|
@@ -1164,6 +1172,107 @@ const collectEvidenceViolations = (
|
|
|
1164
1172
|
return { violations, ageSeconds };
|
|
1165
1173
|
};
|
|
1166
1174
|
|
|
1175
|
+
const SUPPORTED_FUNCTIONAL_EXTENSIONS = new Set([
|
|
1176
|
+
'.swift',
|
|
1177
|
+
'.ts',
|
|
1178
|
+
'.tsx',
|
|
1179
|
+
'.js',
|
|
1180
|
+
'.jsx',
|
|
1181
|
+
'.kt',
|
|
1182
|
+
'.kts',
|
|
1183
|
+
]);
|
|
1184
|
+
|
|
1185
|
+
const DOCUMENTATION_RENDER_TOOLING_EXTENSIONS = new Set([
|
|
1186
|
+
'.css',
|
|
1187
|
+
'.html',
|
|
1188
|
+
'.json',
|
|
1189
|
+
'.md',
|
|
1190
|
+
'.mdx',
|
|
1191
|
+
'.txt',
|
|
1192
|
+
]);
|
|
1193
|
+
|
|
1194
|
+
const normalizeGitPath = (value: string): string => value.replace(/\\/g, '/').replace(/^"|"$/g, '');
|
|
1195
|
+
|
|
1196
|
+
const extractGitStatusPath = (line: string): string | null => {
|
|
1197
|
+
const payload = line.slice(3).trim();
|
|
1198
|
+
if (payload.length === 0) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
const renameSeparator = ' -> ';
|
|
1202
|
+
if (payload.includes(renameSeparator)) {
|
|
1203
|
+
return normalizeGitPath(payload.slice(payload.lastIndexOf(renameSeparator) + renameSeparator.length));
|
|
1204
|
+
}
|
|
1205
|
+
return normalizeGitPath(payload);
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1208
|
+
const collectGitStatusPaths = (repoRoot: string): readonly string[] => {
|
|
1209
|
+
if (!existsSync(resolve(repoRoot, '.git'))) {
|
|
1210
|
+
return [];
|
|
1211
|
+
}
|
|
1212
|
+
try {
|
|
1213
|
+
return execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], {
|
|
1214
|
+
cwd: repoRoot,
|
|
1215
|
+
encoding: 'utf8',
|
|
1216
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
1217
|
+
})
|
|
1218
|
+
.split(/\r?\n/)
|
|
1219
|
+
.map(extractGitStatusPath)
|
|
1220
|
+
.filter((path): path is string => path !== null);
|
|
1221
|
+
} catch {
|
|
1222
|
+
return [];
|
|
1223
|
+
}
|
|
1224
|
+
};
|
|
1225
|
+
|
|
1226
|
+
const pathExtension = (path: string): string => {
|
|
1227
|
+
const basename = path.split('/').pop() ?? '';
|
|
1228
|
+
const dotIndex = basename.lastIndexOf('.');
|
|
1229
|
+
return dotIndex >= 0 ? basename.slice(dotIndex).toLowerCase() : '';
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
const isSupportedFunctionalPath = (path: string): boolean =>
|
|
1233
|
+
SUPPORTED_FUNCTIONAL_EXTENSIONS.has(pathExtension(path));
|
|
1234
|
+
|
|
1235
|
+
const isDocumentationRenderToolingPath = (path: string): boolean => {
|
|
1236
|
+
const normalized = path.toLowerCase();
|
|
1237
|
+
if (normalized.startsWith('docs/') && DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized))) {
|
|
1238
|
+
return true;
|
|
1239
|
+
}
|
|
1240
|
+
if (normalized.endsWith('.md') || normalized.endsWith('.mdx')) {
|
|
1241
|
+
return true;
|
|
1242
|
+
}
|
|
1243
|
+
if (
|
|
1244
|
+
normalized.startsWith('stack-my-architecture-pumuki/dist/') ||
|
|
1245
|
+
normalized.startsWith('stack-my-architecture-hub/pumuki/')
|
|
1246
|
+
) {
|
|
1247
|
+
return DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized));
|
|
1248
|
+
}
|
|
1249
|
+
if (normalized === 'stack-my-architecture-pumuki/scripts/build-html.py') {
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
if (normalized === 'package.json' || normalized === 'package-lock.json') {
|
|
1253
|
+
return true;
|
|
1254
|
+
}
|
|
1255
|
+
return false;
|
|
1256
|
+
};
|
|
1257
|
+
|
|
1258
|
+
const isAdvisoryDocumentationRenderSlice = (
|
|
1259
|
+
repoRoot: string,
|
|
1260
|
+
ageSeconds: number,
|
|
1261
|
+
maxAgeSeconds: number
|
|
1262
|
+
): boolean => {
|
|
1263
|
+
if (ageSeconds > maxAgeSeconds) {
|
|
1264
|
+
return false;
|
|
1265
|
+
}
|
|
1266
|
+
const paths = collectGitStatusPaths(repoRoot);
|
|
1267
|
+
if (paths.length === 0) {
|
|
1268
|
+
return false;
|
|
1269
|
+
}
|
|
1270
|
+
return (
|
|
1271
|
+
paths.every(isDocumentationRenderToolingPath) &&
|
|
1272
|
+
!paths.some(isSupportedFunctionalPath)
|
|
1273
|
+
);
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1167
1276
|
const toEvidenceSourceDescriptor = (
|
|
1168
1277
|
result: EvidenceReadResult,
|
|
1169
1278
|
repoRoot: string
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.218",
|
|
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-13T13:
|
|
4
|
+
"generatedAt": "2026-05-13T13:40:06.121Z",
|
|
5
5
|
"bundles": [
|
|
6
6
|
{
|
|
7
7
|
"name": "android-guidelines",
|
|
@@ -5764,8 +5764,20 @@
|
|
|
5764
5764
|
"name": "ios-guidelines",
|
|
5765
5765
|
"version": "1.0.0",
|
|
5766
5766
|
"source": "file:vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
5767
|
-
"hash": "
|
|
5767
|
+
"hash": "2d56094eff6a0b688d9f20ef6970de5945dfa74aaf3a3973dfee2091b9e98542",
|
|
5768
5768
|
"rules": [
|
|
5769
|
+
{
|
|
5770
|
+
"id": "skills.ios.guideline.ios-swiftui-expert.use-task-modifier-for-automatic-cancellation-of-async-work",
|
|
5771
|
+
"description": ".task/.task(id:) - Trabajos async con cancelación automática",
|
|
5772
|
+
"severity": "WARN",
|
|
5773
|
+
"platform": "ios",
|
|
5774
|
+
"sourceSkill": "ios-guidelines",
|
|
5775
|
+
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
5776
|
+
"confidence": "MEDIUM",
|
|
5777
|
+
"locked": true,
|
|
5778
|
+
"evaluationMode": "AUTO",
|
|
5779
|
+
"origin": "core"
|
|
5780
|
+
},
|
|
5769
5781
|
{
|
|
5770
5782
|
"id": "skills.ios.guideline.ios.accessibility-identifiers-para-localizar-elementos",
|
|
5771
5783
|
"description": "Accessibility identifiers - Para localizar elementos",
|
|
@@ -7927,18 +7939,6 @@
|
|
|
7927
7939
|
"evaluationMode": "DECLARATIVE",
|
|
7928
7940
|
"origin": "core"
|
|
7929
7941
|
},
|
|
7930
|
-
{
|
|
7931
|
-
"id": "skills.ios.guideline.ios.task-task-id-trabajos-async-con-cancelacio-n-automa-tica",
|
|
7932
|
-
"description": ".task/.task(id:) - Trabajos async con cancelación automática",
|
|
7933
|
-
"severity": "WARN",
|
|
7934
|
-
"platform": "ios",
|
|
7935
|
-
"sourceSkill": "ios-guidelines",
|
|
7936
|
-
"sourcePath": "vendor/skills/ios-enterprise-rules/SKILL.md",
|
|
7937
|
-
"confidence": "MEDIUM",
|
|
7938
|
-
"locked": true,
|
|
7939
|
-
"evaluationMode": "DECLARATIVE",
|
|
7940
|
-
"origin": "core"
|
|
7941
|
-
},
|
|
7942
7942
|
{
|
|
7943
7943
|
"id": "skills.ios.guideline.ios.taskgroup-para-operaciones-paralelas",
|
|
7944
7944
|
"description": "TaskGroup - Para operaciones paralelas",
|
|
@@ -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": "05e71c648612ed794d4e8122105fc3f508e09b7f92db94a24bd3daa45335d6b7",
|
|
8636
8636
|
"rules": [
|
|
8637
8637
|
{
|
|
8638
8638
|
"id": "skills.ios.guideline.ios-swiftui-expert.always-mark-state-and-stateobject-as-private-makes-dependencies-clear",
|
|
@@ -8907,7 +8907,7 @@
|
|
|
8907
8907
|
"sourcePath": "vendor/skills/swiftui-expert-skill/SKILL.md",
|
|
8908
8908
|
"confidence": "MEDIUM",
|
|
8909
8909
|
"locked": true,
|
|
8910
|
-
"evaluationMode": "
|
|
8910
|
+
"evaluationMode": "AUTO",
|
|
8911
8911
|
"origin": "core"
|
|
8912
8912
|
},
|
|
8913
8913
|
{
|