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.
@@ -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
- params.detectedPlatforms.ios?.detected &&
906
- isIOSSwiftPath(fileFact.path) &&
907
- !isSwiftTestPath(fileFact.path)
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.148",
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>> = {