pumuki 6.3.180 → 6.3.182
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/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +50 -0
- package/core/facts/detectors/text/android.ts +8 -0
- package/core/facts/extractHeuristicFacts.ts +10 -0
- package/core/rules/presets/heuristics/android.test.ts +11 -1
- package/core/rules/presets/heuristics/android.ts +40 -0
- package/docs/codex-skills/android-enterprise-rules.md +2 -1
- package/integrations/config/skillsDetectorRegistry.ts +7 -0
- package/package.json +1 -1
- package/skills.lock.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.182
|
|
@@ -6,7 +6,9 @@ import {
|
|
|
6
6
|
findKotlinLiskovSubstitutionMatch,
|
|
7
7
|
findKotlinOpenClosedWhenMatch,
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
|
+
hasKotlinDispatcherMainBoundaryLeakUsage,
|
|
9
10
|
hasKotlinGlobalScopeUsage,
|
|
11
|
+
hasKotlinHardcodedBackgroundDispatcherUsage,
|
|
10
12
|
hasKotlinLiveDataStateExposureUsage,
|
|
11
13
|
hasKotlinManualCoroutineScopeInViewModelUsage,
|
|
12
14
|
hasKotlinRunBlockingUsage,
|
|
@@ -141,6 +143,54 @@ class OrdersViewModel : ViewModel() {
|
|
|
141
143
|
assert.equal(hasKotlinManualCoroutineScopeInViewModelUsage(source), false);
|
|
142
144
|
});
|
|
143
145
|
|
|
146
|
+
test('hasKotlinDispatcherMainBoundaryLeakUsage detecta Dispatchers.Main como dispatcher UI explícito', () => {
|
|
147
|
+
const source = `
|
|
148
|
+
class SyncOrdersUseCase {
|
|
149
|
+
suspend fun execute() = withContext(Dispatchers.Main) { }
|
|
150
|
+
}
|
|
151
|
+
`;
|
|
152
|
+
assert.equal(hasKotlinDispatcherMainBoundaryLeakUsage(source), true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('hasKotlinDispatcherMainBoundaryLeakUsage ignora imports, comentarios y strings', () => {
|
|
156
|
+
const source = `
|
|
157
|
+
import kotlinx.coroutines.Dispatchers
|
|
158
|
+
// withContext(Dispatchers.Main) { }
|
|
159
|
+
val sample = "Dispatchers.Main"
|
|
160
|
+
class SyncOrdersUseCase {
|
|
161
|
+
suspend fun execute() = withContext(Dispatchers.IO) { }
|
|
162
|
+
}
|
|
163
|
+
`;
|
|
164
|
+
assert.equal(hasKotlinDispatcherMainBoundaryLeakUsage(source), false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('hasKotlinHardcodedBackgroundDispatcherUsage detecta Dispatchers.IO y Dispatchers.Default', () => {
|
|
168
|
+
const ioSource = `
|
|
169
|
+
class SyncOrdersUseCase {
|
|
170
|
+
suspend fun execute() = withContext(Dispatchers.IO) { }
|
|
171
|
+
}
|
|
172
|
+
`;
|
|
173
|
+
const defaultSource = `
|
|
174
|
+
class BuildCatalogIndexUseCase {
|
|
175
|
+
suspend fun execute() = withContext(Dispatchers.Default) { }
|
|
176
|
+
}
|
|
177
|
+
`;
|
|
178
|
+
assert.equal(hasKotlinHardcodedBackgroundDispatcherUsage(ioSource), true);
|
|
179
|
+
assert.equal(hasKotlinHardcodedBackgroundDispatcherUsage(defaultSource), true);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('hasKotlinHardcodedBackgroundDispatcherUsage ignora imports, comentarios, strings y Main', () => {
|
|
183
|
+
const source = `
|
|
184
|
+
import kotlinx.coroutines.Dispatchers
|
|
185
|
+
// withContext(Dispatchers.IO) { }
|
|
186
|
+
val sample = "Dispatchers.Default"
|
|
187
|
+
class SyncOrdersUseCase {
|
|
188
|
+
suspend fun execute() = withContext(Dispatchers.Main) { }
|
|
189
|
+
}
|
|
190
|
+
`;
|
|
191
|
+
assert.equal(hasKotlinHardcodedBackgroundDispatcherUsage(source), false);
|
|
192
|
+
});
|
|
193
|
+
|
|
144
194
|
test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
|
|
145
195
|
const source = `
|
|
146
196
|
import android.content.SharedPreferences
|
|
@@ -255,6 +255,10 @@ const parseKotlinTypeDeclarations = (source: string): readonly KotlinTypeDeclara
|
|
|
255
255
|
return declarations;
|
|
256
256
|
};
|
|
257
257
|
|
|
258
|
+
export const hasKotlinHardcodedBackgroundDispatcherUsage = (source: string): boolean => {
|
|
259
|
+
return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*(?:IO|Default)\b/).length > 0;
|
|
260
|
+
};
|
|
261
|
+
|
|
258
262
|
export const hasKotlinThreadSleepCall = (source: string): boolean => {
|
|
259
263
|
return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
|
|
260
264
|
if (current !== 'T') {
|
|
@@ -331,6 +335,10 @@ export const hasKotlinManualCoroutineScopeInViewModelUsage = (source: string): b
|
|
|
331
335
|
return false;
|
|
332
336
|
};
|
|
333
337
|
|
|
338
|
+
export const hasKotlinDispatcherMainBoundaryLeakUsage = (source: string): boolean => {
|
|
339
|
+
return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*Main\b/).length > 0;
|
|
340
|
+
};
|
|
341
|
+
|
|
334
342
|
export const findKotlinPresentationSrpMatch = (
|
|
335
343
|
source: string
|
|
336
344
|
): KotlinPresentationSrpMatch | undefined => {
|
|
@@ -99,6 +99,14 @@ const isAndroidApplicationOrPresentationPath = (path: string): boolean => {
|
|
|
99
99
|
return isAndroidKotlinPath(path) && (path.includes('/application/') || path.includes('/presentation/'));
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
const isAndroidDomainOrApplicationPath = (path: string): boolean => {
|
|
103
|
+
return isAndroidKotlinPath(path) && (path.includes('/domain/') || path.includes('/application/'));
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const isAndroidNonPresentationKotlinPath = (path: string): boolean => {
|
|
107
|
+
return isAndroidKotlinPath(path) && !path.includes('/presentation/');
|
|
108
|
+
};
|
|
109
|
+
|
|
102
110
|
const isApprovedIOSBridgePath = (path: string): boolean => {
|
|
103
111
|
const normalized = path.toLowerCase();
|
|
104
112
|
return (
|
|
@@ -639,6 +647,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
639
647
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinRunBlockingUsage, ruleId: 'heuristics.android.run-blocking.ast', code: 'HEURISTICS_ANDROID_RUN_BLOCKING_AST', message: 'AST heuristic detected runBlocking usage in production Kotlin code.' },
|
|
640
648
|
{ platform: 'android', pathCheck: isAndroidPresentationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinLiveDataStateExposureUsage, ruleId: 'heuristics.android.flow.livedata-state-exposure.ast', code: 'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST', message: 'AST heuristic detected LiveData state exposure in Android presentation code where StateFlow or SharedFlow should be preferred.' },
|
|
641
649
|
{ platform: 'android', pathCheck: isAndroidPresentationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinManualCoroutineScopeInViewModelUsage, ruleId: 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast', code: 'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST', message: 'AST heuristic detected manual CoroutineScope inside an Android ViewModel where viewModelScope should be preferred.' },
|
|
650
|
+
{ platform: 'android', pathCheck: isAndroidNonPresentationKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinDispatcherMainBoundaryLeakUsage, ruleId: 'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast', code: 'HEURISTICS_ANDROID_COROUTINES_DISPATCHERS_MAIN_BOUNDARY_LEAK_AST', message: 'AST heuristic detected Dispatchers.Main outside Android presentation code.' },
|
|
651
|
+
{ platform: 'android', pathCheck: isAndroidDomainOrApplicationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinHardcodedBackgroundDispatcherUsage, ruleId: 'heuristics.android.coroutines.hardcoded-background-dispatcher.ast', code: 'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST', message: 'AST heuristic detected hard-coded Dispatchers.IO or Dispatchers.Default in Android domain/application code.' },
|
|
642
652
|
];
|
|
643
653
|
|
|
644
654
|
const extractWorkflowHeuristicFacts = (
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { androidRules } from './android';
|
|
4
4
|
|
|
5
5
|
test('androidRules define reglas heurísticas locked para plataforma android', () => {
|
|
6
|
-
assert.equal(androidRules.length,
|
|
6
|
+
assert.equal(androidRules.length, 12);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -12,6 +12,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
12
12
|
'heuristics.android.run-blocking.ast',
|
|
13
13
|
'heuristics.android.flow.livedata-state-exposure.ast',
|
|
14
14
|
'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
15
|
+
'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
16
|
+
'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
15
17
|
'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
16
18
|
'heuristics.android.solid.ocp.discriminator-branching.ast',
|
|
17
19
|
'heuristics.android.solid.dip.concrete-framework-dependency.ast',
|
|
@@ -50,6 +52,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
50
52
|
byId.get('heuristics.android.coroutines.manual-scope-in-viewmodel.ast')?.then.code,
|
|
51
53
|
'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST'
|
|
52
54
|
);
|
|
55
|
+
assert.equal(
|
|
56
|
+
byId.get('heuristics.android.coroutines.dispatchers-main-boundary-leak.ast')?.then.code,
|
|
57
|
+
'HEURISTICS_ANDROID_COROUTINES_DISPATCHERS_MAIN_BOUNDARY_LEAK_AST'
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
byId.get('heuristics.android.coroutines.hardcoded-background-dispatcher.ast')?.then.code,
|
|
61
|
+
'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST'
|
|
62
|
+
);
|
|
53
63
|
assert.equal(
|
|
54
64
|
byId.get('heuristics.android.solid.srp.presentation-mixed-responsibilities.ast')?.then.code,
|
|
55
65
|
'HEURISTICS_ANDROID_SOLID_SRP_PRESENTATION_MIXED_RESPONSIBILITIES_AST'
|
|
@@ -95,6 +95,46 @@ export const androidRules: RuleSet = [
|
|
|
95
95
|
code: 'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST',
|
|
96
96
|
},
|
|
97
97
|
},
|
|
98
|
+
{
|
|
99
|
+
id: 'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
100
|
+
description:
|
|
101
|
+
'Detects Dispatchers.Main usage outside Android presentation code.',
|
|
102
|
+
severity: 'WARN',
|
|
103
|
+
platform: 'android',
|
|
104
|
+
locked: true,
|
|
105
|
+
when: {
|
|
106
|
+
kind: 'Heuristic',
|
|
107
|
+
where: {
|
|
108
|
+
ruleId: 'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
then: {
|
|
112
|
+
kind: 'Finding',
|
|
113
|
+
message:
|
|
114
|
+
'AST heuristic detected Dispatchers.Main outside Android presentation code.',
|
|
115
|
+
code: 'HEURISTICS_ANDROID_COROUTINES_DISPATCHERS_MAIN_BOUNDARY_LEAK_AST',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
120
|
+
description:
|
|
121
|
+
'Detects hard-coded Dispatchers.IO or Dispatchers.Default usage in Android domain/application code.',
|
|
122
|
+
severity: 'WARN',
|
|
123
|
+
platform: 'android',
|
|
124
|
+
locked: true,
|
|
125
|
+
when: {
|
|
126
|
+
kind: 'Heuristic',
|
|
127
|
+
where: {
|
|
128
|
+
ruleId: 'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
then: {
|
|
132
|
+
kind: 'Finding',
|
|
133
|
+
message:
|
|
134
|
+
'AST heuristic detected hard-coded background dispatcher in Android domain/application code.',
|
|
135
|
+
code: 'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST',
|
|
136
|
+
},
|
|
137
|
+
},
|
|
98
138
|
{
|
|
99
139
|
id: 'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
100
140
|
description:
|
|
@@ -125,7 +125,8 @@ app/
|
|
|
125
125
|
### Enforcement AST inicial de coroutines Android
|
|
126
126
|
✅ `skills.android.guideline.android.coroutines-async-await-no-callbacks` debe mapear a señales ejecutables existentes de uso inseguro de coroutines, empezando por `GlobalScope` y `runBlocking`.
|
|
127
127
|
✅ `skills.android.guideline.android.viewmodelscope-scope-de-viewmodel-cancelado-automa-ticamente` debe mapear a señales ejecutables de scopes manuales dentro de `ViewModel`, empezando por `CoroutineScope(...)` construido en presentation.
|
|
128
|
-
✅
|
|
128
|
+
✅ `skills.android.guideline.android.dispatchers-main-ui-io-network-disk-default-cpu` debe mapear a señales ejecutables de dispatchers mal ubicados: `Dispatchers.Main` fuera de `presentation` y `Dispatchers.IO` / `Dispatchers.Default` hardcodeados en `domain` o `application` en lugar de entrar por frontera inyectable.
|
|
129
|
+
✅ El baseline inicial de coroutines es parcial: cubre APIs bloqueantes, scopes no estructurados, scopes manuales en `ViewModel`, filtraciones de `Dispatchers.Main` y hardcode de dispatchers de background en dominio/aplicación, pero no declara todavía cobertura completa de `Flow`, `supervisorScope`, dispatchers ni cancelación cooperativa.
|
|
129
130
|
✅ Si se amplía esta cobertura, cada nueva regla debe aterrizar como detector AST/textual semántico con test dirigido antes de declararse cubierta en el registry.
|
|
130
131
|
|
|
131
132
|
### Enforcement AST inicial de Flow/StateFlow Android
|
|
@@ -224,6 +224,13 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
224
224
|
'android.coroutines.viewmodel-scope',
|
|
225
225
|
['heuristics.android.coroutines.manual-scope-in-viewmodel.ast']
|
|
226
226
|
),
|
|
227
|
+
'skills.android.guideline.android.dispatchers-main-ui-io-network-disk-default-cpu': heuristicDetector(
|
|
228
|
+
'android.coroutines.dispatchers',
|
|
229
|
+
[
|
|
230
|
+
'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
231
|
+
'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
232
|
+
]
|
|
233
|
+
),
|
|
227
234
|
'skills.android.guideline.android.stateflow-estado-mutable-observable': heuristicDetector(
|
|
228
235
|
'android.flow.state-exposure',
|
|
229
236
|
['heuristics.android.flow.livedata-state-exposure.ast']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.182",
|
|
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": {
|