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 +11 -0
- package/CHANGELOG.md +15 -0
- package/VERSION +1 -1
- package/core/facts/detectors/typescript/index.test.ts +66 -12
- package/core/facts/detectors/typescript/index.ts +14 -12
- package/core/facts/extractHeuristicFacts.ts +1 -1
- package/core/rules/presets/heuristics/typescript.ts +2 -2
- package/docs/codex-skills/backend-enterprise-rules.md +1 -1
- package/docs/codex-skills/ios-enterprise-rules.md +1 -1
- package/integrations/config/skillsMarkdownRules.ts +1 -7
- package/integrations/git/brownfieldHotspots.ts +4 -25
- package/package.json +1 -1
- package/vendor/skills/backend-enterprise-rules/SKILL.md +1 -1
- package/vendor/skills/ios-enterprise-rules/SKILL.md +1 -1
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.
|
|
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
|
|
860
|
-
const
|
|
859
|
+
test('hasLargeClassDeclaration detecta god class por mezcla semantica de responsabilidades', () => {
|
|
860
|
+
const godClassAst = {
|
|
861
861
|
type: 'ClassDeclaration',
|
|
862
862
|
loc: {
|
|
863
|
-
start: { line:
|
|
864
|
-
end: { line:
|
|
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
|
|
893
|
+
const oversizedButSingleResponsibilityAst = {
|
|
868
894
|
type: 'ClassDeclaration',
|
|
869
895
|
loc: {
|
|
870
|
-
start: { line:
|
|
871
|
-
end: { line:
|
|
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
|
|
911
|
+
const srpOnlyAst = {
|
|
875
912
|
type: 'ClassDeclaration',
|
|
876
913
|
loc: {
|
|
877
|
-
start: { line:
|
|
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(
|
|
883
|
-
assert.equal(hasLargeClassDeclaration(
|
|
884
|
-
assert.equal(hasLargeClassDeclaration(
|
|
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
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|