pumuki 6.3.178 → 6.3.180
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 +39 -0
- package/core/facts/extractHeuristicFacts.ts +2 -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 +7 -1
- package/integrations/config/skillsDetectorRegistry.ts +16 -0
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.180
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
findKotlinOpenClosedWhenMatch,
|
|
8
8
|
findKotlinPresentationSrpMatch,
|
|
9
9
|
hasKotlinGlobalScopeUsage,
|
|
10
|
+
hasKotlinLiveDataStateExposureUsage,
|
|
11
|
+
hasKotlinManualCoroutineScopeInViewModelUsage,
|
|
10
12
|
hasKotlinRunBlockingUsage,
|
|
11
13
|
hasKotlinThreadSleepCall,
|
|
12
14
|
} from './android';
|
|
@@ -91,6 +93,54 @@ fun main() {
|
|
|
91
93
|
assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
|
|
92
94
|
});
|
|
93
95
|
|
|
96
|
+
test('hasKotlinLiveDataStateExposureUsage detecta LiveData y MutableLiveData como estado observable legacy', () => {
|
|
97
|
+
const source = `
|
|
98
|
+
class OrdersViewModel : ViewModel() {
|
|
99
|
+
private val mutableState = MutableLiveData<OrdersUiState>()
|
|
100
|
+
val state: LiveData<OrdersUiState> = mutableState
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
assert.equal(hasKotlinLiveDataStateExposureUsage(source), true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('hasKotlinLiveDataStateExposureUsage ignora imports, comentarios y strings', () => {
|
|
107
|
+
const source = `
|
|
108
|
+
import androidx.lifecycle.LiveData
|
|
109
|
+
// val state = MutableLiveData<OrdersUiState>()
|
|
110
|
+
val sample = "LiveData<OrdersUiState>"
|
|
111
|
+
class OrdersViewModel : ViewModel() {
|
|
112
|
+
val state: StateFlow<OrdersUiState> = MutableStateFlow(OrdersUiState())
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
115
|
+
assert.equal(hasKotlinLiveDataStateExposureUsage(source), false);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('hasKotlinManualCoroutineScopeInViewModelUsage detecta CoroutineScope manual dentro de ViewModel', () => {
|
|
119
|
+
const source = `
|
|
120
|
+
class OrdersViewModel : ViewModel() {
|
|
121
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
assert.equal(hasKotlinManualCoroutineScopeInViewModelUsage(source), true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('hasKotlinManualCoroutineScopeInViewModelUsage ignora imports, comentarios y uso fuera de ViewModel', () => {
|
|
128
|
+
const source = `
|
|
129
|
+
import kotlinx.coroutines.CoroutineScope
|
|
130
|
+
// private val scope = CoroutineScope(SupervisorJob())
|
|
131
|
+
val sample = "CoroutineScope(SupervisorJob())"
|
|
132
|
+
class OrdersWorker {
|
|
133
|
+
private val scope = CoroutineScope(SupervisorJob())
|
|
134
|
+
}
|
|
135
|
+
class OrdersViewModel : ViewModel() {
|
|
136
|
+
fun load() {
|
|
137
|
+
viewModelScope.launch { }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
assert.equal(hasKotlinManualCoroutineScopeInViewModelUsage(source), false);
|
|
142
|
+
});
|
|
143
|
+
|
|
94
144
|
test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
|
|
95
145
|
const source = `
|
|
96
146
|
import android.content.SharedPreferences
|
|
@@ -292,6 +292,45 @@ export const hasKotlinRunBlockingUsage = (source: string): boolean => {
|
|
|
292
292
|
});
|
|
293
293
|
};
|
|
294
294
|
|
|
295
|
+
export const hasKotlinLiveDataStateExposureUsage = (source: string): boolean => {
|
|
296
|
+
return collectKotlinRegexLines(
|
|
297
|
+
source,
|
|
298
|
+
/\b(?:MutableLiveData|LiveData)\s*(?:<|\(|\.)/
|
|
299
|
+
).length > 0;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export const hasKotlinManualCoroutineScopeInViewModelUsage = (source: string): boolean => {
|
|
303
|
+
const lines = source.split(/\r?\n/);
|
|
304
|
+
let insideViewModel = false;
|
|
305
|
+
let braceDepth = 0;
|
|
306
|
+
|
|
307
|
+
for (const rawLine of lines) {
|
|
308
|
+
const sanitized = stripKotlinLineForSemanticScan(rawLine);
|
|
309
|
+
if (sanitized.trimStart().startsWith('import ')) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!insideViewModel && /\bclass\s+\w*ViewModel\b/.test(sanitized)) {
|
|
314
|
+
insideViewModel = true;
|
|
315
|
+
braceDepth =
|
|
316
|
+
countTokenOccurrences(sanitized, '{') - countTokenOccurrences(sanitized, '}');
|
|
317
|
+
} else if (insideViewModel) {
|
|
318
|
+
braceDepth += countTokenOccurrences(sanitized, '{');
|
|
319
|
+
braceDepth -= countTokenOccurrences(sanitized, '}');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (insideViewModel && /\bCoroutineScope\s*\(/.test(sanitized)) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (insideViewModel && braceDepth <= 0 && sanitized.includes('}')) {
|
|
327
|
+
insideViewModel = false;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return false;
|
|
332
|
+
};
|
|
333
|
+
|
|
295
334
|
export const findKotlinPresentationSrpMatch = (
|
|
296
335
|
source: string
|
|
297
336
|
): KotlinPresentationSrpMatch | undefined => {
|
|
@@ -637,6 +637,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
637
637
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinThreadSleepCall, ruleId: 'heuristics.android.thread-sleep.ast', code: 'HEURISTICS_ANDROID_THREAD_SLEEP_AST', message: 'AST heuristic detected Thread.sleep usage in production Kotlin code.' },
|
|
638
638
|
{ platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGlobalScopeUsage, ruleId: 'heuristics.android.globalscope.ast', code: 'HEURISTICS_ANDROID_GLOBAL_SCOPE_AST', message: 'AST heuristic detected GlobalScope coroutine usage in production Kotlin code.' },
|
|
639
639
|
{ 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
|
+
{ 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
|
+
{ 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.' },
|
|
640
642
|
];
|
|
641
643
|
|
|
642
644
|
const extractWorkflowHeuristicFacts = (
|
|
@@ -3,13 +3,15 @@ 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, 10);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
10
10
|
'heuristics.android.thread-sleep.ast',
|
|
11
11
|
'heuristics.android.globalscope.ast',
|
|
12
12
|
'heuristics.android.run-blocking.ast',
|
|
13
|
+
'heuristics.android.flow.livedata-state-exposure.ast',
|
|
14
|
+
'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
13
15
|
'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
14
16
|
'heuristics.android.solid.ocp.discriminator-branching.ast',
|
|
15
17
|
'heuristics.android.solid.dip.concrete-framework-dependency.ast',
|
|
@@ -40,6 +42,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
40
42
|
byId.get('heuristics.android.run-blocking.ast')?.then.code,
|
|
41
43
|
'HEURISTICS_ANDROID_RUN_BLOCKING_AST'
|
|
42
44
|
);
|
|
45
|
+
assert.equal(
|
|
46
|
+
byId.get('heuristics.android.flow.livedata-state-exposure.ast')?.then.code,
|
|
47
|
+
'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST'
|
|
48
|
+
);
|
|
49
|
+
assert.equal(
|
|
50
|
+
byId.get('heuristics.android.coroutines.manual-scope-in-viewmodel.ast')?.then.code,
|
|
51
|
+
'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST'
|
|
52
|
+
);
|
|
43
53
|
assert.equal(
|
|
44
54
|
byId.get('heuristics.android.solid.srp.presentation-mixed-responsibilities.ast')?.then.code,
|
|
45
55
|
'HEURISTICS_ANDROID_SOLID_SRP_PRESENTATION_MIXED_RESPONSIBILITIES_AST'
|
|
@@ -55,6 +55,46 @@ export const androidRules: RuleSet = [
|
|
|
55
55
|
code: 'HEURISTICS_ANDROID_RUN_BLOCKING_AST',
|
|
56
56
|
},
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
id: 'heuristics.android.flow.livedata-state-exposure.ast',
|
|
60
|
+
description:
|
|
61
|
+
'Detects LiveData state exposure in Android presentation code where StateFlow or SharedFlow should be preferred.',
|
|
62
|
+
severity: 'WARN',
|
|
63
|
+
platform: 'android',
|
|
64
|
+
locked: true,
|
|
65
|
+
when: {
|
|
66
|
+
kind: 'Heuristic',
|
|
67
|
+
where: {
|
|
68
|
+
ruleId: 'heuristics.android.flow.livedata-state-exposure.ast',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
then: {
|
|
72
|
+
kind: 'Finding',
|
|
73
|
+
message:
|
|
74
|
+
'AST heuristic detected LiveData state exposure in Android presentation code.',
|
|
75
|
+
code: 'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
80
|
+
description:
|
|
81
|
+
'Detects manual CoroutineScope construction inside Android ViewModel classes where viewModelScope should be preferred.',
|
|
82
|
+
severity: 'WARN',
|
|
83
|
+
platform: 'android',
|
|
84
|
+
locked: true,
|
|
85
|
+
when: {
|
|
86
|
+
kind: 'Heuristic',
|
|
87
|
+
where: {
|
|
88
|
+
ruleId: 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
then: {
|
|
92
|
+
kind: 'Finding',
|
|
93
|
+
message:
|
|
94
|
+
'AST heuristic detected manual CoroutineScope inside an Android ViewModel.',
|
|
95
|
+
code: 'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST',
|
|
96
|
+
},
|
|
97
|
+
},
|
|
58
98
|
{
|
|
59
99
|
id: 'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
60
100
|
description:
|
|
@@ -124,9 +124,15 @@ app/
|
|
|
124
124
|
|
|
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
|
+
✅ El baseline inicial de coroutines es parcial: cubre APIs bloqueantes, scopes no estructurados y scopes manuales en `ViewModel`, pero no declara todavía cobertura completa de `Flow`, `supervisorScope`, dispatchers ni cancelación cooperativa.
|
|
128
129
|
✅ 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.
|
|
129
130
|
|
|
131
|
+
### Enforcement AST inicial de Flow/StateFlow Android
|
|
132
|
+
✅ Las reglas de `StateFlow` / `StateFlow-SharedFlow` deben mapear a señales ejecutables de estado observable legacy, empezando por exposición de `LiveData` / `MutableLiveData` en presentation.
|
|
133
|
+
✅ Este baseline no obliga a que todo ViewModel use Flow; solo detecta una alternativa legacy concreta cuando aparece en código de presentación Android.
|
|
134
|
+
✅ `Flow`, `collectAsState`, `stateIn`, Room observable queries y SharedFlow events quedan fuera hasta que tengan detectores propios y regresiones dirigidas.
|
|
135
|
+
|
|
130
136
|
### Dependency Injection (Hilt):
|
|
131
137
|
✅ **Hilt** - DI framework (NO manual factories)
|
|
132
138
|
✅ **@HiltAndroidApp** - Application class
|
|
@@ -220,6 +220,22 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
220
220
|
'heuristics.android.run-blocking.ast',
|
|
221
221
|
]
|
|
222
222
|
),
|
|
223
|
+
'skills.android.guideline.android.viewmodelscope-scope-de-viewmodel-cancelado-automa-ticamente': heuristicDetector(
|
|
224
|
+
'android.coroutines.viewmodel-scope',
|
|
225
|
+
['heuristics.android.coroutines.manual-scope-in-viewmodel.ast']
|
|
226
|
+
),
|
|
227
|
+
'skills.android.guideline.android.stateflow-estado-mutable-observable': heuristicDetector(
|
|
228
|
+
'android.flow.state-exposure',
|
|
229
|
+
['heuristics.android.flow.livedata-state-exposure.ast']
|
|
230
|
+
),
|
|
231
|
+
'skills.android.guideline.android.stateflow-hot-stream-siempre-tiene-valor-para-estado': heuristicDetector(
|
|
232
|
+
'android.flow.state-exposure',
|
|
233
|
+
['heuristics.android.flow.livedata-state-exposure.ast']
|
|
234
|
+
),
|
|
235
|
+
'skills.android.guideline.android.stateflow-sharedflow-para-exponer-estado-del-viewmodel': heuristicDetector(
|
|
236
|
+
'android.flow.state-exposure',
|
|
237
|
+
['heuristics.android.flow.livedata-state-exposure.ast']
|
|
238
|
+
),
|
|
223
239
|
'skills.android.no-solid-violations': heuristicDetector('android.solid', [
|
|
224
240
|
'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
225
241
|
'heuristics.android.solid.ocp.discriminator-branching.ast',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.180",
|
|
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": {
|