pumuki 6.3.121 → 6.3.123
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/CHANGELOG.md +16 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +40 -0
- package/core/facts/detectors/text/android.ts +68 -19
- package/core/facts/detectors/text/ios.test.ts +48 -0
- package/core/facts/detectors/text/ios.ts +134 -54
- package/core/facts/detectors/typescript/index.test.ts +66 -12
- package/core/facts/detectors/typescript/index.ts +24 -19
- 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/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/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.123] - 2026-04-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Detectores SOLID sin contadores arbitrarios:** `SRP`, `DIP`, `OCP` e `ISP` en iOS/Android/TypeScript dejan de depender de mínimos tipo `relatedNodes.length < N`, `typedCaseCount >= N` o cortes `slice(0, N)` y pasan a usar categorías semánticas explícitas.
|
|
14
|
+
- **ISP iOS/Android por familias de contrato:** los protocolos/interfaces anchos se detectan por mezcla real de `query`/`command` y uso de una sola familia por consumidor, no por número de miembros.
|
|
15
|
+
- **Cierre de `PUMUKI-INC-116`:** se añade regresión negativa para clases/protocolos grandes pero cohesionados y auditoría textual para impedir que los detectores estructurales vuelvan a degradarse a `N señales => bloqueo`.
|
|
16
|
+
|
|
17
|
+
## [6.3.122] - 2026-04-28
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **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.
|
|
22
|
+
- **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.
|
|
23
|
+
- **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.
|
|
24
|
+
|
|
9
25
|
## [6.3.121] - 2026-04-28
|
|
10
26
|
|
|
11
27
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.123
|
|
@@ -254,6 +254,46 @@ class PumukiIspAndroidCanaryUseCase(
|
|
|
254
254
|
assert.match(match.expected_fix, /interfaces pequeñas|puerto mínimo/i);
|
|
255
255
|
});
|
|
256
256
|
|
|
257
|
+
test('detectores SOLID Android no convierten tamaño o cardinalidad en violacion', () => {
|
|
258
|
+
const presentationWithoutMixedResponsibilities = `
|
|
259
|
+
class CatalogViewModel {
|
|
260
|
+
fun restoreSessionSnapshot() {}
|
|
261
|
+
fun refreshSessionToken() {}
|
|
262
|
+
fun resumeSessionIfNeeded() {}
|
|
263
|
+
fun signOut() {}
|
|
264
|
+
}
|
|
265
|
+
`;
|
|
266
|
+
const dipWithPortOnly = `
|
|
267
|
+
interface CatalogFetching {
|
|
268
|
+
suspend fun fetchCatalog(): List<String>
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
class CatalogUseCase(
|
|
272
|
+
private val catalog: CatalogFetching,
|
|
273
|
+
)
|
|
274
|
+
`;
|
|
275
|
+
const cohesiveInterface = `
|
|
276
|
+
interface CatalogReading {
|
|
277
|
+
suspend fun fetchCatalog(): List<String>
|
|
278
|
+
suspend fun loadCachedCatalog(): List<String>
|
|
279
|
+
suspend fun readCatalogVersion(): String
|
|
280
|
+
suspend fun getFeaturedCatalog(): List<String>
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
class CatalogUseCase(
|
|
284
|
+
private val catalog: CatalogReading,
|
|
285
|
+
) {
|
|
286
|
+
suspend fun execute() {
|
|
287
|
+
catalog.fetchCatalog()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
`;
|
|
291
|
+
|
|
292
|
+
assert.equal(findKotlinPresentationSrpMatch(presentationWithoutMixedResponsibilities), undefined);
|
|
293
|
+
assert.equal(findKotlinConcreteDependencyDipMatch(dipWithPortOnly), undefined);
|
|
294
|
+
assert.equal(findKotlinInterfaceSegregationMatch(cohesiveInterface), undefined);
|
|
295
|
+
});
|
|
296
|
+
|
|
257
297
|
test('findKotlinLiskovSubstitutionMatch devuelve payload semantico para LSP-Android en application', () => {
|
|
258
298
|
const source = `
|
|
259
299
|
interface PumukiLspAndroidCanaryDiscountPolicy {
|
|
@@ -70,6 +70,40 @@ type KotlinTypeDeclaration = {
|
|
|
70
70
|
bodyEndLine: number;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
|
+
type KotlinResponsibilityMatch = {
|
|
74
|
+
key: string;
|
|
75
|
+
node: KotlinSemanticNodeMatch;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const kotlinQueryMemberNamePattern = /^(get|find|list|fetch|read|load|restore|refresh|is|has|can)/i;
|
|
79
|
+
const kotlinCommandMemberNamePattern =
|
|
80
|
+
/^(create|update|delete|remove|save|insert|upsert|set|write|persist|clear|reset|sync|store)/i;
|
|
81
|
+
|
|
82
|
+
const registerKotlinResponsibility = (
|
|
83
|
+
nodes: KotlinResponsibilityMatch[],
|
|
84
|
+
key: string,
|
|
85
|
+
kind: KotlinSemanticNodeMatch['kind'],
|
|
86
|
+
name: string,
|
|
87
|
+
lines: readonly number[]
|
|
88
|
+
): void => {
|
|
89
|
+
if (lines.length === 0) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
nodes.push({ key, node: { kind, name, lines } });
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const hasKotlinResponsibilityKeys = (
|
|
96
|
+
nodes: readonly KotlinResponsibilityMatch[],
|
|
97
|
+
keys: readonly string[]
|
|
98
|
+
): boolean => {
|
|
99
|
+
const observedKeys = new Set(nodes.map((node) => node.key));
|
|
100
|
+
return keys.every((key) => observedKeys.has(key));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const isKotlinQueryMemberName = (name: string): boolean => kotlinQueryMemberNamePattern.test(name);
|
|
104
|
+
const isKotlinCommandMemberName = (name: string): boolean =>
|
|
105
|
+
kotlinCommandMemberNamePattern.test(name);
|
|
106
|
+
|
|
73
107
|
const stripKotlinLineForSemanticScan = (line: string): string => {
|
|
74
108
|
return line
|
|
75
109
|
.replace(/\/\/.*$/, '')
|
|
@@ -274,44 +308,46 @@ export const findKotlinPresentationSrpMatch = (
|
|
|
274
308
|
return undefined;
|
|
275
309
|
}
|
|
276
310
|
|
|
277
|
-
const
|
|
311
|
+
const responsibilities: KotlinResponsibilityMatch[] = [];
|
|
278
312
|
const registerNode = (
|
|
313
|
+
key: string,
|
|
279
314
|
kind: KotlinSemanticNodeMatch['kind'],
|
|
280
315
|
name: string,
|
|
281
316
|
regex: RegExp
|
|
282
317
|
): void => {
|
|
283
|
-
|
|
284
|
-
if (lines.length === 0) {
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
relatedNodes.push({ kind, name, lines });
|
|
318
|
+
registerKotlinResponsibility(responsibilities, key, kind, name, collectKotlinRegexLines(source, regex));
|
|
288
319
|
};
|
|
289
320
|
|
|
290
321
|
registerNode(
|
|
322
|
+
'session',
|
|
291
323
|
'member',
|
|
292
324
|
'session/auth flow',
|
|
293
325
|
/\b(?:restore|bootstrap|refresh|resume|signIn|signOut|authenticate|session)\w*\s*\(/
|
|
294
326
|
);
|
|
295
327
|
registerNode(
|
|
328
|
+
'networking',
|
|
296
329
|
'call',
|
|
297
330
|
'remote networking',
|
|
298
331
|
/\b(?:OkHttpClient\s*\(|Retrofit\.Builder\s*\(|HttpURLConnection\b)/
|
|
299
332
|
);
|
|
300
333
|
registerNode(
|
|
334
|
+
'persistence',
|
|
301
335
|
'call',
|
|
302
336
|
'local persistence',
|
|
303
337
|
/\b(?:SharedPreferences\b.*\)|PreferenceDataStoreFactory\.create|preferencesDataStore|DataStore<|RoomDatabase\b)/
|
|
304
338
|
);
|
|
305
339
|
registerNode(
|
|
340
|
+
'navigation',
|
|
306
341
|
'member',
|
|
307
342
|
'navigation flow',
|
|
308
343
|
/\b(?:findNavController\s*\(|\.\s*navigate\s*\()/
|
|
309
344
|
);
|
|
310
345
|
|
|
311
|
-
if (
|
|
346
|
+
if (!hasKotlinResponsibilityKeys(responsibilities, ['session', 'networking', 'persistence', 'navigation'])) {
|
|
312
347
|
return undefined;
|
|
313
348
|
}
|
|
314
349
|
|
|
350
|
+
const relatedNodes = responsibilities.map((entry) => entry.node);
|
|
315
351
|
const allLines = sortedUniqueLines([
|
|
316
352
|
...classLines,
|
|
317
353
|
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
@@ -396,7 +432,7 @@ export const findKotlinConcreteDependencyDipMatch = (
|
|
|
396
432
|
);
|
|
397
433
|
registerNode('call', 'Room.databaseBuilder', /\bRoom\.databaseBuilder\s*\(/);
|
|
398
434
|
|
|
399
|
-
if (relatedNodes.length
|
|
435
|
+
if (relatedNodes.length === 0) {
|
|
400
436
|
return undefined;
|
|
401
437
|
}
|
|
402
438
|
|
|
@@ -473,7 +509,8 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
473
509
|
}
|
|
474
510
|
}
|
|
475
511
|
|
|
476
|
-
|
|
512
|
+
const [firstBranchNode, secondBranchNode] = branchNodes;
|
|
513
|
+
if (!firstBranchNode || !secondBranchNode) {
|
|
477
514
|
continue;
|
|
478
515
|
}
|
|
479
516
|
|
|
@@ -483,7 +520,7 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
483
520
|
name: `discriminator switch: ${discriminatorName}`,
|
|
484
521
|
lines: [index + 1],
|
|
485
522
|
},
|
|
486
|
-
...branchNodes
|
|
523
|
+
...branchNodes,
|
|
487
524
|
];
|
|
488
525
|
|
|
489
526
|
const allLines = sortedUniqueLines([
|
|
@@ -492,7 +529,6 @@ export const findKotlinOpenClosedWhenMatch = (
|
|
|
492
529
|
...branchNodes.flatMap((node) => [...node.lines]),
|
|
493
530
|
]);
|
|
494
531
|
const branchSummary = branchNodes
|
|
495
|
-
.slice(0, 3)
|
|
496
532
|
.map((node) => node.name.replace(/^branch /, ''))
|
|
497
533
|
.join(', ');
|
|
498
534
|
|
|
@@ -541,7 +577,13 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
541
577
|
const sourceLines = source.split(/\r?\n/);
|
|
542
578
|
|
|
543
579
|
for (const interfaceDeclaration of interfaceDeclarations) {
|
|
544
|
-
|
|
580
|
+
const queryMembers = interfaceDeclaration.members.filter((member) =>
|
|
581
|
+
isKotlinQueryMemberName(member.name)
|
|
582
|
+
);
|
|
583
|
+
const commandMembers = interfaceDeclaration.members.filter((member) =>
|
|
584
|
+
isKotlinCommandMemberName(member.name)
|
|
585
|
+
);
|
|
586
|
+
if (queryMembers.length === 0 || commandMembers.length === 0) {
|
|
545
587
|
continue;
|
|
546
588
|
}
|
|
547
589
|
|
|
@@ -579,14 +621,21 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
579
621
|
}
|
|
580
622
|
});
|
|
581
623
|
|
|
582
|
-
|
|
624
|
+
const usedMemberNames = [...usedMembers.keys()];
|
|
625
|
+
if (usedMemberNames.length === 0) {
|
|
583
626
|
continue;
|
|
584
627
|
}
|
|
585
628
|
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
)
|
|
589
|
-
|
|
629
|
+
const usesQueryContract = usedMemberNames.some(isKotlinQueryMemberName);
|
|
630
|
+
const usesCommandContract = usedMemberNames.some(isKotlinCommandMemberName);
|
|
631
|
+
if (usesQueryContract === usesCommandContract) {
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const oppositeFamilyMembers = usesQueryContract ? commandMembers : queryMembers;
|
|
636
|
+
const unusedMembers = oppositeFamilyMembers.filter((member) => !usedMembers.has(member.name));
|
|
637
|
+
const firstUnusedMember = unusedMembers[0];
|
|
638
|
+
if (!firstUnusedMember) {
|
|
590
639
|
continue;
|
|
591
640
|
}
|
|
592
641
|
|
|
@@ -603,7 +652,7 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
603
652
|
lines: [interfaceDeclaration.line],
|
|
604
653
|
},
|
|
605
654
|
...usedDescriptors,
|
|
606
|
-
...unusedMembers.
|
|
655
|
+
...unusedMembers.map((member) => ({
|
|
607
656
|
kind: 'member' as const,
|
|
608
657
|
name: `unused contract member: ${member.name}`,
|
|
609
658
|
lines: [member.line],
|
|
@@ -611,7 +660,7 @@ export const findKotlinInterfaceSegregationMatch = (
|
|
|
611
660
|
];
|
|
612
661
|
|
|
613
662
|
const usedMemberSummary = usedDescriptors.map((member) => member.name.replace('used member: ', ''));
|
|
614
|
-
const unusedSummary = unusedMembers.
|
|
663
|
+
const unusedSummary = unusedMembers.map((member) => member.name);
|
|
615
664
|
const allLines = sortedUniqueLines([
|
|
616
665
|
...typeLines,
|
|
617
666
|
interfaceDeclaration.line,
|
|
@@ -807,6 +807,54 @@ final class PumukiIspIosCanaryUseCase {
|
|
|
807
807
|
assert.match(match.expected_fix, /protocolos pequeños|puerto mínimo/i);
|
|
808
808
|
});
|
|
809
809
|
|
|
810
|
+
test('detectores SOLID iOS no convierten tamaño o cardinalidad en violacion', () => {
|
|
811
|
+
const presentationWithoutMixedResponsibilities = `
|
|
812
|
+
final class CatalogViewModel {
|
|
813
|
+
func restoreSessionSnapshot() async {}
|
|
814
|
+
func refreshSessionToken() async {}
|
|
815
|
+
func resumeSessionIfNeeded() async {}
|
|
816
|
+
func signOut() async {}
|
|
817
|
+
}
|
|
818
|
+
`;
|
|
819
|
+
const dipWithPortOnly = `
|
|
820
|
+
protocol CatalogFetching {
|
|
821
|
+
func fetchCatalog() async throws -> [String]
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
final class CatalogUseCase {
|
|
825
|
+
private let catalog: CatalogFetching
|
|
826
|
+
|
|
827
|
+
init(catalog: CatalogFetching) {
|
|
828
|
+
self.catalog = catalog
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
`;
|
|
832
|
+
const cohesiveProtocol = `
|
|
833
|
+
protocol CatalogReading {
|
|
834
|
+
func fetchCatalog() async throws -> [String]
|
|
835
|
+
func loadCachedCatalog() async throws -> [String]
|
|
836
|
+
func readCatalogVersion() async throws -> String
|
|
837
|
+
func getFeaturedCatalog() async throws -> [String]
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
final class CatalogUseCase {
|
|
841
|
+
private let catalog: CatalogReading
|
|
842
|
+
|
|
843
|
+
init(catalog: CatalogReading) {
|
|
844
|
+
self.catalog = catalog
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
func execute() async throws {
|
|
848
|
+
_ = try await catalog.fetchCatalog()
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
`;
|
|
852
|
+
|
|
853
|
+
assert.equal(findSwiftPresentationSrpMatch(presentationWithoutMixedResponsibilities), undefined);
|
|
854
|
+
assert.equal(findSwiftConcreteDependencyDipMatch(dipWithPortOnly), undefined);
|
|
855
|
+
assert.equal(findSwiftInterfaceSegregationMatch(cohesiveProtocol), undefined);
|
|
856
|
+
});
|
|
857
|
+
|
|
810
858
|
test('findSwiftLiskovSubstitutionMatch devuelve payload semantico para LSP-iOS en application', () => {
|
|
811
859
|
const source = `
|
|
812
860
|
protocol PumukiLspIosCanaryDiscountApplying {
|
|
@@ -144,6 +144,39 @@ type SwiftTypeDeclaration = {
|
|
|
144
144
|
bodyEndLine: number;
|
|
145
145
|
};
|
|
146
146
|
|
|
147
|
+
type SwiftResponsibilityMatch = {
|
|
148
|
+
key: string;
|
|
149
|
+
node: SwiftSemanticNodeMatch;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const swiftQueryMemberNamePattern = /^(get|find|list|fetch|read|load|restore|refresh|is|has|can)/i;
|
|
153
|
+
const swiftCommandMemberNamePattern =
|
|
154
|
+
/^(create|update|delete|remove|save|insert|upsert|set|write|persist|clear|reset|sync|store)/i;
|
|
155
|
+
|
|
156
|
+
const registerSwiftResponsibility = (
|
|
157
|
+
nodes: SwiftResponsibilityMatch[],
|
|
158
|
+
key: string,
|
|
159
|
+
kind: SwiftSemanticNodeMatch['kind'],
|
|
160
|
+
name: string,
|
|
161
|
+
lines: readonly number[]
|
|
162
|
+
): void => {
|
|
163
|
+
if (lines.length === 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
nodes.push({ key, node: { kind, name, lines } });
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const hasSwiftResponsibilityKeys = (
|
|
170
|
+
nodes: readonly SwiftResponsibilityMatch[],
|
|
171
|
+
keys: readonly string[]
|
|
172
|
+
): boolean => {
|
|
173
|
+
const observedKeys = new Set(nodes.map((node) => node.key));
|
|
174
|
+
return keys.every((key) => observedKeys.has(key));
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const isSwiftQueryMemberName = (name: string): boolean => swiftQueryMemberNamePattern.test(name);
|
|
178
|
+
const isSwiftCommandMemberName = (name: string): boolean => swiftCommandMemberNamePattern.test(name);
|
|
179
|
+
|
|
147
180
|
const parseSwiftProtocolDeclarations = (source: string): readonly SwiftProtocolDeclaration[] => {
|
|
148
181
|
const lines = source.split(/\r?\n/);
|
|
149
182
|
const declarations: SwiftProtocolDeclaration[] = [];
|
|
@@ -874,31 +907,41 @@ export const findSwiftIOSCanary001Match = (source: string): SwiftIOSCanary001Mat
|
|
|
874
907
|
return undefined;
|
|
875
908
|
}
|
|
876
909
|
|
|
877
|
-
const
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
910
|
+
const explicitInfraResponsibilities: SwiftResponsibilityMatch[] = [];
|
|
911
|
+
registerSwiftResponsibility(
|
|
912
|
+
explicitInfraResponsibilities,
|
|
913
|
+
'shared-state',
|
|
914
|
+
'property',
|
|
915
|
+
'shared singleton',
|
|
916
|
+
collectSwiftRegexLines(source, /\bstatic\s+let\s+shared\b/)
|
|
917
|
+
);
|
|
918
|
+
registerSwiftResponsibility(
|
|
919
|
+
explicitInfraResponsibilities,
|
|
920
|
+
'networking',
|
|
921
|
+
'call',
|
|
922
|
+
'URLSession.shared',
|
|
923
|
+
collectSwiftRegexLines(source, /\bURLSession\.shared\b/)
|
|
924
|
+
);
|
|
925
|
+
registerSwiftResponsibility(
|
|
926
|
+
explicitInfraResponsibilities,
|
|
927
|
+
'persistence',
|
|
928
|
+
'call',
|
|
929
|
+
'FileManager.default',
|
|
930
|
+
collectSwiftRegexLines(source, /\bFileManager\.default\b/)
|
|
931
|
+
);
|
|
932
|
+
registerSwiftResponsibility(
|
|
933
|
+
explicitInfraResponsibilities,
|
|
934
|
+
'navigation',
|
|
896
935
|
'member',
|
|
897
936
|
'navigation flow',
|
|
898
|
-
|
|
937
|
+
collectSwiftRegexLines(
|
|
938
|
+
source,
|
|
939
|
+
/\b(?:router|route|coordinator|navigationPath|navigationDestination)\b|\b(?:navigate|dismiss|present)\s*\(/i
|
|
940
|
+
)
|
|
899
941
|
);
|
|
900
942
|
|
|
901
|
-
if (
|
|
943
|
+
if (hasSwiftResponsibilityKeys(explicitInfraResponsibilities, ['networking', 'persistence', 'navigation'])) {
|
|
944
|
+
const explicitInfraNodes = explicitInfraResponsibilities.map((entry) => entry.node);
|
|
902
945
|
const relatedNodeNames = explicitInfraNodes.map((node) => node.name).join(', ');
|
|
903
946
|
const allLines = sortedUniqueLines([
|
|
904
947
|
...classLines,
|
|
@@ -921,33 +964,55 @@ export const findSwiftIOSCanary001Match = (source: string): SwiftIOSCanary001Mat
|
|
|
921
964
|
};
|
|
922
965
|
}
|
|
923
966
|
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
|
|
967
|
+
const appShellResponsibilities: SwiftResponsibilityMatch[] = [];
|
|
968
|
+
registerSwiftResponsibility(
|
|
969
|
+
appShellResponsibilities,
|
|
970
|
+
'session',
|
|
927
971
|
'member',
|
|
928
972
|
'session bootstrap/restoration',
|
|
929
|
-
/\b(?:restorePersistedSessionIfNeeded|continueAsGuest|bootstrapAuthenticatedSession)\s*\(/
|
|
973
|
+
collectSwiftRegexLines(source, /\b(?:restorePersistedSessionIfNeeded|continueAsGuest|bootstrapAuthenticatedSession)\s*\(/)
|
|
930
974
|
);
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
975
|
+
registerSwiftResponsibility(
|
|
976
|
+
appShellResponsibilities,
|
|
977
|
+
'store',
|
|
978
|
+
'member',
|
|
979
|
+
'store selection orchestration',
|
|
980
|
+
collectSwiftRegexLines(source, /\bselectStore\s*\(/)
|
|
981
|
+
);
|
|
982
|
+
registerSwiftResponsibility(
|
|
983
|
+
appShellResponsibilities,
|
|
984
|
+
'shopping-list',
|
|
985
|
+
'member',
|
|
986
|
+
'shopping list synchronization',
|
|
987
|
+
collectSwiftRegexLines(source, /\bsyncShoppingList\s*\(/)
|
|
988
|
+
);
|
|
989
|
+
registerSwiftResponsibility(
|
|
990
|
+
appShellResponsibilities,
|
|
991
|
+
'route',
|
|
935
992
|
'member',
|
|
936
993
|
'route progression',
|
|
937
|
-
/\b(?:markNextStopCompleted|scanCheckpoint|rebuildRouteStatus)\s*\(/
|
|
994
|
+
collectSwiftRegexLines(source, /\b(?:markNextStopCompleted|scanCheckpoint|rebuildRouteStatus)\s*\(/)
|
|
938
995
|
);
|
|
939
|
-
|
|
940
|
-
|
|
996
|
+
registerSwiftResponsibility(
|
|
997
|
+
appShellResponsibilities,
|
|
998
|
+
'offline-queue',
|
|
941
999
|
'member',
|
|
942
1000
|
'offline queue coordination',
|
|
943
|
-
/\b(?:flushOfflineQueue|enqueueOfflineCheckpoint)\s*\(/
|
|
1001
|
+
collectSwiftRegexLines(source, /\b(?:flushOfflineQueue|enqueueOfflineCheckpoint)\s*\(/)
|
|
1002
|
+
);
|
|
1003
|
+
registerSwiftResponsibility(
|
|
1004
|
+
appShellResponsibilities,
|
|
1005
|
+
'navigation',
|
|
1006
|
+
'member',
|
|
1007
|
+
'deep link/navigation flow',
|
|
1008
|
+
collectSwiftRegexLines(source, /\bopenDeepLink\s*\(/)
|
|
944
1009
|
);
|
|
945
|
-
registerNode(appShellNodes, 'member', 'deep link/navigation flow', /\bopenDeepLink\s*\(/);
|
|
946
1010
|
|
|
947
|
-
if (
|
|
1011
|
+
if (!hasSwiftResponsibilityKeys(appShellResponsibilities, ['session', 'store', 'route', 'navigation'])) {
|
|
948
1012
|
return undefined;
|
|
949
1013
|
}
|
|
950
1014
|
|
|
1015
|
+
const appShellNodes = appShellResponsibilities.map((entry) => entry.node);
|
|
951
1016
|
const relatedNodeNames = appShellNodes.map((node) => node.name).join(', ');
|
|
952
1017
|
const allLines = sortedUniqueLines([
|
|
953
1018
|
...classLines,
|
|
@@ -985,44 +1050,46 @@ export const findSwiftPresentationSrpMatch = (
|
|
|
985
1050
|
return undefined;
|
|
986
1051
|
}
|
|
987
1052
|
|
|
988
|
-
const
|
|
1053
|
+
const responsibilities: SwiftResponsibilityMatch[] = [];
|
|
989
1054
|
const registerNode = (
|
|
1055
|
+
key: string,
|
|
990
1056
|
kind: SwiftSemanticNodeMatch['kind'],
|
|
991
1057
|
name: string,
|
|
992
1058
|
regex: RegExp
|
|
993
1059
|
): void => {
|
|
994
|
-
|
|
995
|
-
if (lines.length === 0) {
|
|
996
|
-
return;
|
|
997
|
-
}
|
|
998
|
-
relatedNodes.push({ kind, name, lines });
|
|
1060
|
+
registerSwiftResponsibility(responsibilities, key, kind, name, collectSwiftRegexLines(source, regex));
|
|
999
1061
|
};
|
|
1000
1062
|
|
|
1001
1063
|
registerNode(
|
|
1064
|
+
'session',
|
|
1002
1065
|
'member',
|
|
1003
1066
|
'session/auth flow',
|
|
1004
1067
|
/\b(?:restore|bootstrap|refresh|resume|signIn|signOut|authenticate|session)\w*\s*\(/
|
|
1005
1068
|
);
|
|
1006
1069
|
registerNode(
|
|
1070
|
+
'networking',
|
|
1007
1071
|
'call',
|
|
1008
1072
|
'remote networking',
|
|
1009
1073
|
/\b(?:URLSession\.shared|URLRequest\b|dataTask\s*\(|uploadTask\s*\(|downloadTask\s*\()/
|
|
1010
1074
|
);
|
|
1011
1075
|
registerNode(
|
|
1076
|
+
'persistence',
|
|
1012
1077
|
'call',
|
|
1013
1078
|
'local persistence',
|
|
1014
1079
|
/\b(?:UserDefaults\.standard|FileManager\.default|Keychain|NSPersistentContainer|CoreData)\b/
|
|
1015
1080
|
);
|
|
1016
1081
|
registerNode(
|
|
1082
|
+
'navigation',
|
|
1017
1083
|
'member',
|
|
1018
1084
|
'navigation flow',
|
|
1019
1085
|
/\b(?:navigationPath|navigationDestination)\b|(?:\.\s*(?:navigate|present|dismiss|push|open)\s*\()/
|
|
1020
1086
|
);
|
|
1021
1087
|
|
|
1022
|
-
if (
|
|
1088
|
+
if (!hasSwiftResponsibilityKeys(responsibilities, ['session', 'networking', 'persistence', 'navigation'])) {
|
|
1023
1089
|
return undefined;
|
|
1024
1090
|
}
|
|
1025
1091
|
|
|
1092
|
+
const relatedNodes = responsibilities.map((entry) => entry.node);
|
|
1026
1093
|
const allLines = sortedUniqueLines([
|
|
1027
1094
|
...classLines,
|
|
1028
1095
|
...relatedNodes.flatMap((node) => [...node.lines]),
|
|
@@ -1092,7 +1159,7 @@ export const findSwiftConcreteDependencyDipMatch = (
|
|
|
1092
1159
|
);
|
|
1093
1160
|
registerNode('call', 'FileManager.default', /\bFileManager\.default\b/);
|
|
1094
1161
|
|
|
1095
|
-
if (relatedNodes.length
|
|
1162
|
+
if (relatedNodes.length === 0) {
|
|
1096
1163
|
return undefined;
|
|
1097
1164
|
}
|
|
1098
1165
|
|
|
@@ -1178,7 +1245,8 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1178
1245
|
}
|
|
1179
1246
|
}
|
|
1180
1247
|
|
|
1181
|
-
|
|
1248
|
+
const [firstCaseNode, secondCaseNode] = caseNodes;
|
|
1249
|
+
if (!firstCaseNode || !secondCaseNode) {
|
|
1182
1250
|
continue;
|
|
1183
1251
|
}
|
|
1184
1252
|
|
|
@@ -1188,7 +1256,7 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1188
1256
|
name: `discriminator switch: ${discriminatorName}`,
|
|
1189
1257
|
lines: [index + 1],
|
|
1190
1258
|
},
|
|
1191
|
-
...caseNodes
|
|
1259
|
+
...caseNodes,
|
|
1192
1260
|
];
|
|
1193
1261
|
|
|
1194
1262
|
const allLines = sortedUniqueLines([
|
|
@@ -1197,7 +1265,6 @@ export const findSwiftOpenClosedSwitchMatch = (
|
|
|
1197
1265
|
...caseNodes.flatMap((node) => [...node.lines]),
|
|
1198
1266
|
]);
|
|
1199
1267
|
const caseSummary = caseNodes
|
|
1200
|
-
.slice(0, 3)
|
|
1201
1268
|
.map((node) => node.name.replace(/^case\s+/, ''))
|
|
1202
1269
|
.join(', ');
|
|
1203
1270
|
|
|
@@ -1246,7 +1313,13 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1246
1313
|
const sourceLines = source.split(/\r?\n/);
|
|
1247
1314
|
|
|
1248
1315
|
for (const protocolDeclaration of protocolDeclarations) {
|
|
1249
|
-
|
|
1316
|
+
const queryMembers = protocolDeclaration.members.filter((member) =>
|
|
1317
|
+
isSwiftQueryMemberName(member.name)
|
|
1318
|
+
);
|
|
1319
|
+
const commandMembers = protocolDeclaration.members.filter((member) =>
|
|
1320
|
+
isSwiftCommandMemberName(member.name)
|
|
1321
|
+
);
|
|
1322
|
+
if (queryMembers.length === 0 || commandMembers.length === 0) {
|
|
1250
1323
|
continue;
|
|
1251
1324
|
}
|
|
1252
1325
|
|
|
@@ -1283,14 +1356,21 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1283
1356
|
}
|
|
1284
1357
|
});
|
|
1285
1358
|
|
|
1286
|
-
|
|
1359
|
+
const usedMemberNames = [...usedMembers.keys()];
|
|
1360
|
+
if (usedMemberNames.length === 0) {
|
|
1287
1361
|
continue;
|
|
1288
1362
|
}
|
|
1289
1363
|
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1292
|
-
)
|
|
1293
|
-
|
|
1364
|
+
const usesQueryContract = usedMemberNames.some(isSwiftQueryMemberName);
|
|
1365
|
+
const usesCommandContract = usedMemberNames.some(isSwiftCommandMemberName);
|
|
1366
|
+
if (usesQueryContract === usesCommandContract) {
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
const oppositeFamilyMembers = usesQueryContract ? commandMembers : queryMembers;
|
|
1371
|
+
const unusedMembers = oppositeFamilyMembers.filter((member) => !usedMembers.has(member.name));
|
|
1372
|
+
const firstUnusedMember = unusedMembers[0];
|
|
1373
|
+
if (!firstUnusedMember) {
|
|
1294
1374
|
continue;
|
|
1295
1375
|
}
|
|
1296
1376
|
|
|
@@ -1311,7 +1391,7 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1311
1391
|
name: `used member: ${usedMemberName}`,
|
|
1312
1392
|
lines: sortedUniqueLines(usedMemberLines),
|
|
1313
1393
|
},
|
|
1314
|
-
...unusedMembers.
|
|
1394
|
+
...unusedMembers.map((member) => ({
|
|
1315
1395
|
kind: 'member' as const,
|
|
1316
1396
|
name: `unused contract member: ${member.name}`,
|
|
1317
1397
|
lines: [member.line],
|
|
@@ -1322,7 +1402,7 @@ export const findSwiftInterfaceSegregationMatch = (
|
|
|
1322
1402
|
...typeLines,
|
|
1323
1403
|
protocolDeclaration.line,
|
|
1324
1404
|
...usedMemberLines,
|
|
1325
|
-
...unusedMembers.
|
|
1405
|
+
...unusedMembers.map((member) => member.line),
|
|
1326
1406
|
]);
|
|
1327
1407
|
|
|
1328
1408
|
return {
|
|
@@ -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 = {
|
|
@@ -894,14 +893,15 @@ const findTypeDiscriminatorSwitchInfo = (
|
|
|
894
893
|
return false;
|
|
895
894
|
}
|
|
896
895
|
|
|
897
|
-
const
|
|
896
|
+
const typedCases = value.cases.filter((entry) => {
|
|
898
897
|
if (!isObject(entry) || entry.type !== 'SwitchCase' || !isObject(entry.test)) {
|
|
899
898
|
return false;
|
|
900
899
|
}
|
|
901
900
|
return typeof switchCaseLabelFromNode(entry.test) === 'string';
|
|
902
|
-
})
|
|
901
|
+
});
|
|
903
902
|
|
|
904
|
-
|
|
903
|
+
const [firstTypedCase, secondTypedCase] = typedCases;
|
|
904
|
+
return firstTypedCase !== undefined && secondTypedCase !== undefined;
|
|
905
905
|
});
|
|
906
906
|
|
|
907
907
|
if (!match) {
|
|
@@ -935,7 +935,8 @@ const findTypeDiscriminatorSwitchInfo = (
|
|
|
935
935
|
})
|
|
936
936
|
: [];
|
|
937
937
|
|
|
938
|
-
|
|
938
|
+
const [firstCaseNode, secondCaseNode] = caseNodes;
|
|
939
|
+
if (!firstCaseNode || !secondCaseNode) {
|
|
939
940
|
return undefined;
|
|
940
941
|
}
|
|
941
942
|
|
|
@@ -1658,7 +1659,7 @@ export const hasTypeDiscriminatorSwitch = (node: unknown): boolean => {
|
|
|
1658
1659
|
return false;
|
|
1659
1660
|
}
|
|
1660
1661
|
|
|
1661
|
-
const
|
|
1662
|
+
const typedCases = value.cases.filter((entry) => {
|
|
1662
1663
|
if (!isObject(entry) || entry.type !== 'SwitchCase' || !isObject(entry.test)) {
|
|
1663
1664
|
return false;
|
|
1664
1665
|
}
|
|
@@ -1668,9 +1669,10 @@ export const hasTypeDiscriminatorSwitch = (node: unknown): boolean => {
|
|
|
1668
1669
|
testNode.type === 'NumericLiteral' ||
|
|
1669
1670
|
testNode.type === 'BooleanLiteral'
|
|
1670
1671
|
);
|
|
1671
|
-
})
|
|
1672
|
+
});
|
|
1672
1673
|
|
|
1673
|
-
|
|
1674
|
+
const [firstTypedCase, secondTypedCase] = typedCases;
|
|
1675
|
+
return firstTypedCase !== undefined && secondTypedCase !== undefined;
|
|
1674
1676
|
});
|
|
1675
1677
|
};
|
|
1676
1678
|
|
|
@@ -1747,16 +1749,19 @@ export const findConcreteDependencyInstantiationMatch = (
|
|
|
1747
1749
|
return buildSolidDipMatch(node, 'concrete-instantiation');
|
|
1748
1750
|
};
|
|
1749
1751
|
|
|
1750
|
-
const
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1752
|
+
const hasSemanticGodClassResponsibilities = (classNode: AstNode): boolean => {
|
|
1753
|
+
const mixesCommandsAndQueries = typeof buildMixedCommandQueryClassMatch(classNode) !== 'undefined';
|
|
1754
|
+
const ownsConcreteInfrastructure = hasConcreteDependencyInstantiation(classNode);
|
|
1755
|
+
const ownsTypeBranching = hasTypeDiscriminatorSwitch(classNode);
|
|
1756
|
+
const weakensBaseContract = hasOverrideMethodThrowingNotImplemented(classNode);
|
|
1757
|
+
|
|
1758
|
+
return (
|
|
1759
|
+
(mixesCommandsAndQueries && ownsConcreteInfrastructure) ||
|
|
1760
|
+
(mixesCommandsAndQueries && ownsTypeBranching) ||
|
|
1761
|
+
(mixesCommandsAndQueries && weakensBaseContract) ||
|
|
1762
|
+
(ownsConcreteInfrastructure && ownsTypeBranching) ||
|
|
1763
|
+
(ownsConcreteInfrastructure && weakensBaseContract)
|
|
1764
|
+
);
|
|
1760
1765
|
};
|
|
1761
1766
|
|
|
1762
1767
|
export const hasLargeClassDeclaration = (node: unknown): boolean => {
|
|
@@ -1764,7 +1769,7 @@ export const hasLargeClassDeclaration = (node: unknown): boolean => {
|
|
|
1764
1769
|
if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
|
|
1765
1770
|
return false;
|
|
1766
1771
|
}
|
|
1767
|
-
return
|
|
1772
|
+
return hasSemanticGodClassResponsibilities(value);
|
|
1768
1773
|
});
|
|
1769
1774
|
};
|
|
1770
1775
|
|
|
@@ -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')) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.123",
|
|
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
|