pumuki 6.3.148 → 6.3.150
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 +64 -0
- package/core/facts/detectors/text/ios.ts +97 -1
- package/core/facts/extractHeuristicFacts.ts +25 -5
- package/docs/operations/RELEASE_NOTES.md +6 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +2 -2
- package/scripts/framework-menu-system-notifications-remediation.ts +1 -1
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findSwiftOpenClosedSwitchMatch,
|
|
7
7
|
findSwiftConcreteDependencyDipMatch,
|
|
8
8
|
findSwiftPresentationSrpMatch,
|
|
9
|
+
findSwiftXCTestSrpMatch,
|
|
9
10
|
hasSwiftAnyViewUsage,
|
|
10
11
|
hasSwiftCallbackStyleSignature,
|
|
11
12
|
hasSwiftCornerRadiusUsage,
|
|
@@ -888,6 +889,69 @@ final class PumukiOcpIosCanaryUseCase {
|
|
|
888
889
|
assert.match(match.expected_fix, /estrategia|protocolo|registry/i);
|
|
889
890
|
});
|
|
890
891
|
|
|
892
|
+
test('findSwiftOpenClosedSwitchMatch detecta switch sobre outcome en Coordinator iOS', () => {
|
|
893
|
+
const source = `public final class LaunchFlowCoordinator {
|
|
894
|
+
public func bootstrap() async {
|
|
895
|
+
let outcome = await appConfigurationUseCase.execute()
|
|
896
|
+
switch outcome {
|
|
897
|
+
case .mandatoryUpdate:
|
|
898
|
+
route = .updateRequired
|
|
899
|
+
case .maintenance:
|
|
900
|
+
route = .maintenance
|
|
901
|
+
case .proceed:
|
|
902
|
+
route = .home
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
`;
|
|
907
|
+
|
|
908
|
+
const match = findSwiftOpenClosedSwitchMatch(source);
|
|
909
|
+
|
|
910
|
+
assert.ok(match);
|
|
911
|
+
assert.equal(match.primary_node.name, 'LaunchFlowCoordinator');
|
|
912
|
+
assert.deepEqual(match.related_nodes, [
|
|
913
|
+
{ kind: 'member', name: 'discriminator switch: outcome', lines: [4] },
|
|
914
|
+
{ kind: 'member', name: 'case .mandatoryUpdate', lines: [5] },
|
|
915
|
+
{ kind: 'member', name: 'case .maintenance', lines: [7] },
|
|
916
|
+
{ kind: 'member', name: 'case .proceed', lines: [9] },
|
|
917
|
+
]);
|
|
918
|
+
assert.match(match.why, /OCP/);
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
test('findSwiftXCTestSrpMatch detecta XCTestCase con responsabilidades mezcladas', () => {
|
|
922
|
+
const source = `import XCTest
|
|
923
|
+
|
|
924
|
+
final class LaunchFlowCoordinatorConfigTests: XCTestCase {
|
|
925
|
+
func test_bootstrap_whenMandatoryUpdate_routesToUpdateRequired() async {}
|
|
926
|
+
func test_bootstrap_whenSessionIsValid_routesHome() async {}
|
|
927
|
+
func test_completeOnboarding_marksProgressAndRoutesToLogin() async {}
|
|
928
|
+
}
|
|
929
|
+
`;
|
|
930
|
+
|
|
931
|
+
const match = findSwiftXCTestSrpMatch(source);
|
|
932
|
+
|
|
933
|
+
assert.ok(match);
|
|
934
|
+
assert.equal(match.primary_node.name, 'LaunchFlowCoordinatorConfigTests');
|
|
935
|
+
assert.deepEqual(match.related_nodes, [
|
|
936
|
+
{ kind: 'member', name: 'session routing tests', lines: [5] },
|
|
937
|
+
{ kind: 'member', name: 'onboarding progress tests', lines: [6] },
|
|
938
|
+
]);
|
|
939
|
+
assert.match(match.why, /XCTestCase|SRP/);
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test('findSwiftXCTestSrpMatch permite XCTestCase enfocado en una responsabilidad', () => {
|
|
943
|
+
const source = `import XCTest
|
|
944
|
+
|
|
945
|
+
final class LaunchFlowCoordinatorNonBlockingConfigTests: XCTestCase {
|
|
946
|
+
func test_bootstrap_whenConfigFetchFailsWithCache_usesCachedConfigAndContinues() async {}
|
|
947
|
+
func test_bootstrap_whenOptionalUpdate_allowsAccessAndContinues() async {}
|
|
948
|
+
func test_bootstrap_whenProceed_continuesToSessionValidation() async {}
|
|
949
|
+
}
|
|
950
|
+
`;
|
|
951
|
+
|
|
952
|
+
assert.equal(findSwiftXCTestSrpMatch(source), undefined);
|
|
953
|
+
});
|
|
954
|
+
|
|
891
955
|
test('findSwiftInterfaceSegregationMatch devuelve payload semantico para ISP-iOS en application', () => {
|
|
892
956
|
const source = `
|
|
893
957
|
protocol PumukiIspIosCanarySessionManaging {
|
|
@@ -32,6 +32,8 @@ export type SwiftPresentationSrpMatch = {
|
|
|
32
32
|
lines: readonly number[];
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
export type SwiftXCTestSrpMatch = SwiftPresentationSrpMatch;
|
|
36
|
+
|
|
35
37
|
export type SwiftConcreteDependencyDipMatch = {
|
|
36
38
|
primary_node: SwiftSemanticNodeMatch;
|
|
37
39
|
related_nodes: readonly SwiftSemanticNodeMatch[];
|
|
@@ -1185,6 +1187,100 @@ export const findSwiftPresentationSrpMatch = (
|
|
|
1185
1187
|
};
|
|
1186
1188
|
};
|
|
1187
1189
|
|
|
1190
|
+
export const findSwiftXCTestSrpMatch = (source: string): SwiftXCTestSrpMatch | undefined => {
|
|
1191
|
+
if (!hasSwiftXCTestCaseSubclassUsage(source)) {
|
|
1192
|
+
return undefined;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
const classPattern = /\b(?:final\s+)?class\s+([A-Za-z0-9_]*Tests?)\s*:\s*XCTestCase\b/;
|
|
1196
|
+
const classLines = collectSwiftRegexLines(source, classPattern);
|
|
1197
|
+
if (classLines.length === 0) {
|
|
1198
|
+
return undefined;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
const classLine = source.split(/\r?\n/)[classLines[0] - 1] ?? '';
|
|
1202
|
+
const className = classLine.match(classPattern)?.[1];
|
|
1203
|
+
if (!className) {
|
|
1204
|
+
return undefined;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const sourceLines = source.split(/\r?\n/);
|
|
1208
|
+
const familyPatterns: ReadonlyArray<{
|
|
1209
|
+
key: string;
|
|
1210
|
+
name: string;
|
|
1211
|
+
tokens: readonly string[];
|
|
1212
|
+
}> = [
|
|
1213
|
+
{ key: 'configuration', name: 'configuration outcome tests', tokens: ['config', 'configuration', 'update', 'cache'] },
|
|
1214
|
+
{ key: 'session', name: 'session routing tests', tokens: ['session', 'login', 'auth', 'valid', 'invalid'] },
|
|
1215
|
+
{ key: 'onboarding', name: 'onboarding progress tests', tokens: ['onboarding', 'progress', 'completeonboarding'] },
|
|
1216
|
+
{ key: 'permissions', name: 'permissions routing tests', tokens: ['permission', 'camera', 'location', 'notification'] },
|
|
1217
|
+
{ key: 'tutorial', name: 'tutorial or feature discovery tests', tokens: ['tutorial', 'featurediscovery'] },
|
|
1218
|
+
{ key: 'splash', name: 'splash delay tests', tokens: ['splash', 'delay', 'start'] },
|
|
1219
|
+
];
|
|
1220
|
+
const matchesFamily = (value: string, tokens: readonly string[]): boolean => {
|
|
1221
|
+
const normalizedValue = value.toLowerCase();
|
|
1222
|
+
return tokens.some((token) => normalizedValue.includes(token));
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
const classFamilyKeys = new Set(
|
|
1226
|
+
familyPatterns
|
|
1227
|
+
.filter((entry) => matchesFamily(className, entry.tokens))
|
|
1228
|
+
.map((entry) => entry.key)
|
|
1229
|
+
);
|
|
1230
|
+
const matchedFamilies = new Map<string, SwiftSemanticNodeMatch>();
|
|
1231
|
+
|
|
1232
|
+
sourceLines.forEach((line, index) => {
|
|
1233
|
+
const sanitizedLine = stripSwiftLineForSemanticScan(line);
|
|
1234
|
+
const methodMatch = sanitizedLine.match(/\bfunc\s+(test[A-Za-z0-9_]+)\s*\(/);
|
|
1235
|
+
const methodName = methodMatch?.[1];
|
|
1236
|
+
if (!methodName) {
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
for (const family of familyPatterns) {
|
|
1241
|
+
if (!matchesFamily(methodName, family.tokens)) {
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
1244
|
+
if (classFamilyKeys.has(family.key)) {
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (!matchedFamilies.has(family.key)) {
|
|
1248
|
+
matchedFamilies.set(family.key, {
|
|
1249
|
+
kind: 'member',
|
|
1250
|
+
name: family.name,
|
|
1251
|
+
lines: [index + 1],
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
const relatedNodes = [...matchedFamilies.values()];
|
|
1258
|
+
if (relatedNodes.length < 2) {
|
|
1259
|
+
return undefined;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
const relatedNodeNames = relatedNodes.map((node) => node.name).join(', ');
|
|
1263
|
+
const allLines = sortedUniqueLines([
|
|
1264
|
+
...classLines,
|
|
1265
|
+
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
1266
|
+
]);
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
primary_node: {
|
|
1270
|
+
kind: 'class',
|
|
1271
|
+
name: className,
|
|
1272
|
+
lines: classLines,
|
|
1273
|
+
},
|
|
1274
|
+
related_nodes: relatedNodes,
|
|
1275
|
+
why: `${className} mezcla ${relatedNodeNames} dentro del mismo XCTestCase, rompiendo SRP en la suite de tests.`,
|
|
1276
|
+
impact:
|
|
1277
|
+
'La suite acumula múltiples razones de cambio, oculta regresiones por responsabilidad y hace más difícil aislar el baseline afectado antes de PRE_WRITE.',
|
|
1278
|
+
expected_fix:
|
|
1279
|
+
'Divide la suite por responsabilidad observable: configuración, sesión, onboarding, permisos, tutorial o splash deben vivir en XCTestCase separados.',
|
|
1280
|
+
lines: allLines,
|
|
1281
|
+
};
|
|
1282
|
+
};
|
|
1283
|
+
|
|
1188
1284
|
export const findSwiftConcreteDependencyDipMatch = (
|
|
1189
1285
|
source: string
|
|
1190
1286
|
): SwiftConcreteDependencyDipMatch | undefined => {
|
|
@@ -1275,7 +1371,7 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1275
1371
|
}
|
|
1276
1372
|
|
|
1277
1373
|
const lines = source.split(/\r?\n/);
|
|
1278
|
-
const discriminatorPattern = /\b(?:kind|type|mode|channel|variant|provider|route|flow|source|experience)\b/i;
|
|
1374
|
+
const discriminatorPattern = /\b(?:kind|type|mode|channel|variant|provider|route|flow|source|experience|outcome|state|status|configuration|config)\b/i;
|
|
1279
1375
|
const switchPattern = /\bswitch\s+([A-Za-z_][A-Za-z0-9_\.]*)\s*\{/;
|
|
1280
1376
|
|
|
1281
1377
|
for (let index = 0; index < lines.length; index += 1) {
|
|
@@ -901,11 +901,30 @@ export const extractHeuristicFacts = (
|
|
|
901
901
|
}
|
|
902
902
|
}
|
|
903
903
|
|
|
904
|
-
if (
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
904
|
+
if (params.detectedPlatforms.ios?.detected && isIOSSwiftPath(fileFact.path)) {
|
|
905
|
+
if (isSwiftTestPath(fileFact.path)) {
|
|
906
|
+
const semanticTestSrpMatch = TextIOS.findSwiftXCTestSrpMatch(fileFact.content);
|
|
907
|
+
if (semanticTestSrpMatch) {
|
|
908
|
+
heuristicFacts.push(
|
|
909
|
+
createHeuristicFact({
|
|
910
|
+
ruleId: 'heuristics.ios.solid.srp.presentation-mixed-responsibilities.ast',
|
|
911
|
+
code: 'HEURISTICS_IOS_SOLID_SRP_XCTEST_MIXED_RESPONSIBILITIES_AST',
|
|
912
|
+
message:
|
|
913
|
+
'Semantic iOS SRP heuristic detected an XCTestCase suite mixing multiple responsibilities.',
|
|
914
|
+
filePath: fileFact.path,
|
|
915
|
+
lines: semanticTestSrpMatch.lines,
|
|
916
|
+
severity: 'CRITICAL',
|
|
917
|
+
primary_node: semanticTestSrpMatch.primary_node,
|
|
918
|
+
related_nodes: semanticTestSrpMatch.related_nodes,
|
|
919
|
+
why: semanticTestSrpMatch.why,
|
|
920
|
+
impact: semanticTestSrpMatch.impact,
|
|
921
|
+
expected_fix: semanticTestSrpMatch.expected_fix,
|
|
922
|
+
})
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (!isSwiftTestPath(fileFact.path)) {
|
|
909
928
|
if (isIOSApplicationOrPresentationPath(fileFact.path)) {
|
|
910
929
|
const semanticOcpMatch = TextIOS.findSwiftOpenClosedSwitchMatch(fileFact.content);
|
|
911
930
|
if (semanticOcpMatch) {
|
|
@@ -1027,6 +1046,7 @@ export const extractHeuristicFacts = (
|
|
|
1027
1046
|
})
|
|
1028
1047
|
);
|
|
1029
1048
|
}
|
|
1049
|
+
}
|
|
1030
1050
|
}
|
|
1031
1051
|
|
|
1032
1052
|
if (
|
|
@@ -4,6 +4,12 @@ This file tracks the active deterministic framework line used in this repository
|
|
|
4
4
|
Canonical release chronology lives in `CHANGELOG.md`.
|
|
5
5
|
This file keeps only the operational highlights and rollout notes that matter while running the framework.
|
|
6
6
|
|
|
7
|
+
### 2026-05-05 (v6.3.150)
|
|
8
|
+
|
|
9
|
+
- **RuralGo PUMUKI-INC-061:** las notificaciones de bloqueo ya recomiendan `policy reconcile --strict --apply --json` cuando la remediación real requiere converger policy-as-code.
|
|
10
|
+
- **UX sin loop falso:** `EVIDENCE_GATE_BLOCKED` y gaps de skills/policy dejan de mostrar un comando dry-run que no puede desbloquear el repo por sí solo.
|
|
11
|
+
- **Rollout:** publicar `pumuki@6.3.150`, repinear primero RuralGo y revalidar un bloqueo de governance verificando el comando visible.
|
|
12
|
+
|
|
7
13
|
### 2026-05-05 (v6.3.145)
|
|
8
14
|
|
|
9
15
|
- **RuralGo PUMUKI-INC-124:** `skills.ios.critical-test-quality` deja de bloquear tests XCTest de UI automation/performance cuando usan `XCUIApplication`, `XCTMetric` o `measure`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.150",
|
|
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": {
|
|
@@ -64,7 +64,7 @@ export const resolveBlockedCommand = (params: {
|
|
|
64
64
|
case 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE':
|
|
65
65
|
return `${buildPinnedPumukiCommand({
|
|
66
66
|
repoRoot: params.repoRoot,
|
|
67
|
-
executableAndArgs: 'pumuki policy reconcile --strict --json',
|
|
67
|
+
executableAndArgs: 'pumuki policy reconcile --strict --apply --json',
|
|
68
68
|
})} && ${buildPinnedPumukiCommand({
|
|
69
69
|
repoRoot: params.repoRoot,
|
|
70
70
|
executableAndArgs: `pumuki sdd validate --stage=${params.event.stage} --json`,
|
|
@@ -79,7 +79,7 @@ export const resolveBlockedCommand = (params: {
|
|
|
79
79
|
case 'EVIDENCE_GATE_BLOCKED':
|
|
80
80
|
return `${buildPinnedPumukiCommand({
|
|
81
81
|
repoRoot: params.repoRoot,
|
|
82
|
-
executableAndArgs: 'pumuki policy reconcile --strict --json',
|
|
82
|
+
executableAndArgs: 'pumuki policy reconcile --strict --apply --json',
|
|
83
83
|
})} && ${buildPinnedPumukiCommand({
|
|
84
84
|
repoRoot: params.repoRoot,
|
|
85
85
|
executableAndArgs: `pumuki sdd validate --stage=${params.event.stage} --json`,
|
|
@@ -41,7 +41,7 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
41
41
|
TRACKING_CANONICAL_IN_PROGRESS_INVALID: TRACKING_BLOCKED_REMEDIATION,
|
|
42
42
|
TRACKING_CANONICAL_SOURCE_CONFLICT: TRACKING_BLOCKED_REMEDIATION,
|
|
43
43
|
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
44
|
-
'Ejecuta `pumuki policy reconcile --strict --json` y revalida antes de continuar.',
|
|
44
|
+
'Ejecuta `pumuki policy reconcile --strict --apply --json` y revalida antes de continuar.',
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly<Record<BlockedRemediationVariant, number>> = {
|