pumuki 6.3.120 → 6.3.122

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/AGENTS.md CHANGED
@@ -81,6 +81,17 @@ Antes de realizar cualquier accion:
81
81
  - explicar la regla exacta bloqueante, y
82
82
  - pedir decision explicita del usuario antes de continuar.
83
83
 
84
+ ## Protocolo condicionado de review paralela
85
+ - MUST: Cuando el usuario pida revisar una rama, PR, diff amplio o cambio transversal, usar subagentes paralelos si el alcance lo justifica.
86
+ - MUST: Separar la revision en tres focos independientes:
87
+ 1. riesgos de seguridad
88
+ 2. carencias de tests
89
+ 3. mantenibilidad, deuda y complejidad
90
+ - MUST: Integrar los hallazgos en una unica respuesta final priorizada por severidad, con referencias a archivo/linea y sin duplicar conclusiones.
91
+ - MUST: Mantener el formato de review: hallazgos primero, preguntas/assumptions despues, resumen al final.
92
+ - MUST: No activar este protocolo para implementaciones normales, fixes pequenos, tareas de tracking o cambios documentales simples salvo peticion explicita del usuario.
93
+ - MUST: No sustituir los gates del repo por opiniones de subagentes; los subagentes aportan focos de revision, pero la respuesta final debe contrastar sus hallazgos con codigo, tests y evidencia local.
94
+
84
95
  ## Contrato hard de GitFlow y ramas (no negociable)
85
96
  - El ciclo GitFlow del proyecto es obligatorio.
86
97
  - Es obligatorio respetar ramas nombradas segun la convencion acordada del repositorio.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.122] - 2026-04-28
10
+
11
+ ### Fixed
12
+
13
+ - **Skills estructurales sin umbrales hardcodeados:** `skills.backend.no-god-classes` / `skills.frontend.no-god-classes` dejan de depender de `GOD_CLASS_MAX_LINES` y pasan a detectar mezcla semántica de responsabilidades por nodos AST.
14
+ - **Auditoría transversal iOS/Android/backend/frontend:** las skills estructurales `God/Massive/SRP/Clean Architecture` ya no expresan límites implícitos de líneas; una regresión dedicada falla si vuelven a aparecer umbrales `> N líneas` en las skills de las cuatro plataformas.
15
+ - **Cierre de `PUMUKI-INC-115`:** Pumuki mantiene hotspots por `max_lines` solo cuando el consumer los declara explícitamente, pero las skills hard vuelven a depender de nodos AST inteligentes o reglas declarativas.
16
+
17
+ ## [6.3.121] - 2026-04-28
18
+
19
+ ### Fixed
20
+
21
+ - **Hotspots brownfield sin umbrales hardcodeados:** `BrownfieldHotspotGuard` deja de bloquear por tamaño implícito en carpetas `presentation`/`application`; el bloqueo por líneas solo se activa cuando el consumer declara explícitamente `max_lines` en `config/pumuki-hotspots.json`.
22
+ - **Cierre de `PUMUKI-INC-114`:** el guard mantiene bloqueo declarativo para hotspots marcados, pero SRP/god class vuelve a depender de reglas/skills AST inteligentes en vez de números internos `800/1200`.
23
+
9
24
  ## [6.3.120] - 2026-04-28
10
25
 
11
26
  ### Fixed
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.120
1
+ v6.3.122
@@ -856,32 +856,86 @@ test('findConcreteDependencyInstantiationMatch devuelve payload semantico para D
856
856
  assert.match(match.expected_fix, /adapter|puerto|abstracci/i);
857
857
  });
858
858
 
859
- test('hasLargeClassDeclaration detecta clases con 300 lineas o mas', () => {
860
- const oversizedClassAst = {
859
+ test('hasLargeClassDeclaration detecta god class por mezcla semantica de responsabilidades', () => {
860
+ const godClassAst = {
861
861
  type: 'ClassDeclaration',
862
862
  loc: {
863
- start: { line: 10 },
864
- end: { line: 320 },
863
+ start: { line: 1 },
864
+ end: { line: 80 },
865
+ },
866
+ body: {
867
+ type: 'ClassBody',
868
+ body: [
869
+ {
870
+ type: 'ClassProperty',
871
+ key: { type: 'Identifier', name: 'client' },
872
+ value: {
873
+ type: 'NewExpression',
874
+ callee: { type: 'Identifier', name: 'PrismaClient' },
875
+ arguments: [],
876
+ },
877
+ },
878
+ {
879
+ type: 'ClassMethod',
880
+ key: { type: 'Identifier', name: 'getOrder' },
881
+ loc: { start: { line: 20 }, end: { line: 24 } },
882
+ body: { type: 'BlockStatement', body: [] },
883
+ },
884
+ {
885
+ type: 'ClassMethod',
886
+ key: { type: 'Identifier', name: 'saveOrder' },
887
+ loc: { start: { line: 30 }, end: { line: 40 } },
888
+ body: { type: 'BlockStatement', body: [] },
889
+ },
890
+ ],
865
891
  },
866
892
  };
867
- const thresholdClassAst = {
893
+ const oversizedButSingleResponsibilityAst = {
868
894
  type: 'ClassDeclaration',
869
895
  loc: {
870
- start: { line: 10 },
871
- end: { line: 309 },
896
+ start: { line: 1 },
897
+ end: { line: 1_000 },
898
+ },
899
+ body: {
900
+ type: 'ClassBody',
901
+ body: [
902
+ {
903
+ type: 'ClassMethod',
904
+ key: { type: 'Identifier', name: 'getOrder' },
905
+ loc: { start: { line: 20 }, end: { line: 24 } },
906
+ body: { type: 'BlockStatement', body: [] },
907
+ },
908
+ ],
872
909
  },
873
910
  };
874
- const compactClassAst = {
911
+ const srpOnlyAst = {
875
912
  type: 'ClassDeclaration',
876
913
  loc: {
877
- start: { line: 10 },
914
+ start: { line: 1 },
878
915
  end: { line: 80 },
879
916
  },
917
+ body: {
918
+ type: 'ClassBody',
919
+ body: [
920
+ {
921
+ type: 'ClassMethod',
922
+ key: { type: 'Identifier', name: 'getOrder' },
923
+ loc: { start: { line: 20 }, end: { line: 24 } },
924
+ body: { type: 'BlockStatement', body: [] },
925
+ },
926
+ {
927
+ type: 'ClassMethod',
928
+ key: { type: 'Identifier', name: 'saveOrder' },
929
+ loc: { start: { line: 30 }, end: { line: 40 } },
930
+ body: { type: 'BlockStatement', body: [] },
931
+ },
932
+ ],
933
+ },
880
934
  };
881
935
 
882
- assert.equal(hasLargeClassDeclaration(oversizedClassAst), true);
883
- assert.equal(hasLargeClassDeclaration(thresholdClassAst), true);
884
- assert.equal(hasLargeClassDeclaration(compactClassAst), false);
936
+ assert.equal(hasLargeClassDeclaration(godClassAst), true);
937
+ assert.equal(hasLargeClassDeclaration(oversizedButSingleResponsibilityAst), false);
938
+ assert.equal(hasLargeClassDeclaration(srpOnlyAst), false);
885
939
  });
886
940
 
887
941
  test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {
@@ -19,7 +19,6 @@ const concreteDependencyNames = new Set<string>([
19
19
  'ApolloClient',
20
20
  'Axios',
21
21
  ]);
22
- const GOD_CLASS_MAX_LINES = 300;
23
22
  const networkCallCalleePattern = /^(fetch|axios|get|post|put|patch|delete|request)$/i;
24
23
  type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
25
24
  type TypeScriptSemanticNode = {
@@ -1747,16 +1746,19 @@ export const findConcreteDependencyInstantiationMatch = (
1747
1746
  return buildSolidDipMatch(node, 'concrete-instantiation');
1748
1747
  };
1749
1748
 
1750
- const nodeLineSpan = (node: unknown): number => {
1751
- if (!isObject(node) || !isObject(node.loc)) {
1752
- return 0;
1753
- }
1754
- const start = isObject(node.loc.start) ? node.loc.start.line : undefined;
1755
- const end = isObject(node.loc.end) ? node.loc.end.line : undefined;
1756
- if (typeof start !== 'number' || typeof end !== 'number') {
1757
- return 0;
1758
- }
1759
- return Math.max(0, end - start + 1);
1749
+ const hasSemanticGodClassResponsibilities = (classNode: AstNode): boolean => {
1750
+ const mixesCommandsAndQueries = typeof buildMixedCommandQueryClassMatch(classNode) !== 'undefined';
1751
+ const ownsConcreteInfrastructure = hasConcreteDependencyInstantiation(classNode);
1752
+ const ownsTypeBranching = hasTypeDiscriminatorSwitch(classNode);
1753
+ const weakensBaseContract = hasOverrideMethodThrowingNotImplemented(classNode);
1754
+
1755
+ return (
1756
+ (mixesCommandsAndQueries && ownsConcreteInfrastructure) ||
1757
+ (mixesCommandsAndQueries && ownsTypeBranching) ||
1758
+ (mixesCommandsAndQueries && weakensBaseContract) ||
1759
+ (ownsConcreteInfrastructure && ownsTypeBranching) ||
1760
+ (ownsConcreteInfrastructure && weakensBaseContract)
1761
+ );
1760
1762
  };
1761
1763
 
1762
1764
  export const hasLargeClassDeclaration = (node: unknown): boolean => {
@@ -1764,7 +1766,7 @@ export const hasLargeClassDeclaration = (node: unknown): boolean => {
1764
1766
  if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
1765
1767
  return false;
1766
1768
  }
1767
- return nodeLineSpan(value) >= GOD_CLASS_MAX_LINES;
1769
+ return hasSemanticGodClassResponsibilities(value);
1768
1770
  });
1769
1771
  };
1770
1772
 
@@ -410,7 +410,7 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
410
410
  { detect: TS.hasOverrideMethodThrowingNotImplemented, ruleId: 'heuristics.ts.solid.lsp.override-not-implemented.ast', code: 'HEURISTICS_SOLID_LSP_OVERRIDE_NOT_IMPLEMENTED_AST', message: 'AST heuristic detected LSP risk: override throws not-implemented/unsupported.' },
411
411
  { detect: TS.hasFrameworkDependencyImport, ruleId: 'heuristics.ts.solid.dip.framework-import.ast', code: 'HEURISTICS_SOLID_DIP_FRAMEWORK_IMPORT_AST', message: 'AST heuristic detected DIP risk: framework dependency imported in domain/application code.', pathCheck: isTypeScriptDomainOrApplicationPath },
412
412
  { detect: TS.hasConcreteDependencyInstantiation, ruleId: 'heuristics.ts.solid.dip.concrete-instantiation.ast', code: 'HEURISTICS_SOLID_DIP_CONCRETE_INSTANTIATION_AST', message: 'AST heuristic detected DIP risk: direct instantiation of concrete framework dependency.', pathCheck: isTypeScriptDomainOrApplicationPath },
413
- { detect: TS.hasLargeClassDeclaration, ruleId: 'heuristics.ts.god-class-large-class.ast', code: 'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST', message: 'AST heuristic detected God Class candidate (>=300 lines in a single class declaration).' },
413
+ { detect: TS.hasLargeClassDeclaration, ruleId: 'heuristics.ts.god-class-large-class.ast', code: 'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST', message: 'AST heuristic detected God Class candidate by mixed responsibility nodes in a single class declaration.' },
414
414
  { detect: TS.hasRecordStringUnknownType, locateLines: TS.findRecordStringUnknownTypeLines, ruleId: 'common.types.record_unknown_requires_type', code: 'COMMON_TYPES_RECORD_UNKNOWN_REQUIRES_TYPE_AST', message: 'AST heuristic detected Record<string, unknown> without explicit value union.' },
415
415
  { detect: TS.hasUnknownWithoutGuard, locateLines: TS.findUnknownWithoutGuardLines, ruleId: 'common.types.unknown_without_guard', code: 'COMMON_TYPES_UNKNOWN_WITHOUT_GUARD_AST', message: 'AST heuristic detected unknown usage without explicit guard evidence.', pathCheck: isTypeScriptDomainOrApplicationPath },
416
416
  { detect: TS.hasUndefinedInBaseTypeUnion, locateLines: TS.findUndefinedInBaseTypeUnionLines, ruleId: 'common.types.undefined_in_base_type', code: 'COMMON_TYPES_UNDEFINED_IN_BASE_TYPE_AST', message: 'AST heuristic detected undefined inside base-type unions.' },
@@ -332,7 +332,7 @@ export const typescriptRules: RuleSet = [
332
332
  },
333
333
  {
334
334
  id: 'heuristics.ts.god-class-large-class.ast',
335
- description: 'Detects God Class candidates when a class declaration exceeds 500 lines.',
335
+ description: 'Detects God Class candidates when one class mixes multiple responsibility nodes.',
336
336
  severity: 'ERROR',
337
337
  platform: 'generic',
338
338
  locked: true,
@@ -344,7 +344,7 @@ export const typescriptRules: RuleSet = [
344
344
  },
345
345
  then: {
346
346
  kind: 'Finding',
347
- message: 'AST heuristic detected God Class candidate (>500 lines in one class declaration).',
347
+ message: 'AST heuristic detected God Class candidate by mixed responsibility nodes in one class declaration.',
348
348
  code: 'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST',
349
349
  },
350
350
  },
@@ -249,7 +249,7 @@ const { data } = await supabase
249
249
  ✅ **i18n en error messages** - Mensajes traducibles
250
250
 
251
251
  ### Anti-patterns a EVITAR:
252
- ❌ **God classes** - Servicios con >500 líneas
252
+ ❌ **God classes** - Servicios que mezclan responsabilidades de dominio, aplicación, infraestructura, branching de tipos o contratos en una misma clase
253
253
  ❌ **Anemic domain models** - Entidades solo con getters/setters
254
254
  ❌ **Magic numbers** - Usar constantes con nombres descriptivos
255
255
  ❌ **Callback hell** - Usar async/await
@@ -806,7 +806,7 @@ final class APIClientSpy: @unchecked Sendable, APIClientProtocol {
806
806
  - Usar `@preconcurrency` solo como medida temporal de migración
807
807
 
808
808
  ### Anti-patterns a EVITAR:
809
- ❌ **Massive View Controllers** - ViewControllers >300 líneas
809
+ ❌ **Massive View Controllers** - ViewControllers que mezclan presentación, navegación, estado, acceso a datos o coordinación de infraestructura
810
810
  ❌ **Force unwrapping (!)** - Salvo IBOutlets y casos justificados
811
811
  ❌ **Singletons** - Dificultan testing
812
812
  ❌ **Storyboards grandes** - Merge conflicts, lentitud
@@ -446,13 +446,7 @@ const normalizeKnownRuleTarget = (
446
446
  if (includes('clean architecture')) {
447
447
  return `${prefix}.enforce-clean-architecture`;
448
448
  }
449
- if (
450
- includes('god classes') ||
451
- includes('god class') ||
452
- includes('500 lineas') ||
453
- includes('500 li neas') ||
454
- includes('500 lines')
455
- ) {
449
+ if (includes('god classes') || includes('god class')) {
456
450
  return `${prefix}.no-god-classes`;
457
451
  }
458
452
  if (includes('empty catch')) {
@@ -24,9 +24,6 @@ type ObservedFile = {
24
24
  };
25
25
 
26
26
  const DEFAULT_HOTSPOTS_CONFIG_PATH = 'config/pumuki-hotspots.json';
27
- const PRESENTATION_OR_APPLICATION_SEGMENTS = ['/presentation/', '/application/'];
28
- const HIGH_LINE_THRESHOLD = 800;
29
- const CRITICAL_LINE_THRESHOLD = 1200;
30
27
 
31
28
  const isObject = (value: unknown): value is Record<string, unknown> =>
32
29
  typeof value === 'object' && value !== null;
@@ -102,11 +99,6 @@ const readBrownfieldHotspotConfig = (repoRoot: string): BrownfieldHotspotConfig
102
99
  }
103
100
  };
104
101
 
105
- const isSensitiveLayerPath = (path: string): boolean => {
106
- const normalized = `/${toNormalizedPath(path).toLowerCase()}`;
107
- return PRESENTATION_OR_APPLICATION_SEGMENTS.some((segment) => normalized.includes(segment));
108
- };
109
-
110
102
  const hasRequiredArtifacts = (
111
103
  repoRoot: string,
112
104
  candidates: ReadonlyArray<string> | undefined
@@ -245,28 +237,15 @@ export const evaluateBrownfieldHotspotFindings = (params: {
245
237
 
246
238
  for (const file of observedFiles) {
247
239
  const configEntry = configByPath.get(file.path);
248
- const isSensitiveLayer = isSensitiveLayerPath(file.path);
249
- if (typeof file.lineCount === 'number' && (isSensitiveLayer || configEntry?.max_lines)) {
250
- const highThreshold = configEntry?.max_lines ?? HIGH_LINE_THRESHOLD;
251
- const criticalThreshold = Math.max(highThreshold, CRITICAL_LINE_THRESHOLD);
252
- if (file.lineCount > criticalThreshold) {
253
- findings.push(
254
- toHotspotSizeFinding({
255
- stage: params.stage,
256
- path: file.path,
257
- lineCount: file.lineCount,
258
- threshold: criticalThreshold,
259
- severity: 'CRITICAL',
260
- reason: configEntry?.reason,
261
- })
262
- );
263
- } else if (file.lineCount > highThreshold) {
240
+ if (typeof file.lineCount === 'number' && configEntry?.max_lines) {
241
+ const threshold = configEntry.max_lines;
242
+ if (file.lineCount > threshold) {
264
243
  findings.push(
265
244
  toHotspotSizeFinding({
266
245
  stage: params.stage,
267
246
  path: file.path,
268
247
  lineCount: file.lineCount,
269
- threshold: highThreshold,
248
+ threshold,
270
249
  severity: 'ERROR',
271
250
  reason: configEntry?.reason,
272
251
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.120",
3
+ "version": "6.3.122",
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": {
@@ -249,7 +249,7 @@ const { data } = await supabase
249
249
  ✅ **i18n en error messages** - Mensajes traducibles
250
250
 
251
251
  ### Anti-patterns a EVITAR:
252
- ❌ **God classes** - Servicios con >500 líneas
252
+ ❌ **God classes** - Servicios que mezclan responsabilidades de dominio, aplicación, infraestructura, branching de tipos o contratos en una misma clase
253
253
  ❌ **Anemic domain models** - Entidades solo con getters/setters
254
254
  ❌ **Magic numbers** - Usar constantes con nombres descriptivos
255
255
  ❌ **Callback hell** - Usar async/await
@@ -806,7 +806,7 @@ final class APIClientSpy: @unchecked Sendable, APIClientProtocol {
806
806
  - Usar `@preconcurrency` solo como medida temporal de migración
807
807
 
808
808
  ### Anti-patterns a EVITAR:
809
- ❌ **Massive View Controllers** - ViewControllers >300 líneas
809
+ ❌ **Massive View Controllers** - ViewControllers que mezclan presentación, navegación, estado, acceso a datos o coordinación de infraestructura
810
810
  ❌ **Force unwrapping (!)** - Salvo IBOutlets y casos justificados
811
811
  ❌ **Singletons** - Dificultan testing
812
812
  ❌ **Storyboards grandes** - Merge conflicts, lentitud