pumuki 6.3.184 → 6.3.186
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 +54 -0
- package/core/facts/detectors/text/android.ts +8 -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 +3 -1
- package/integrations/config/skillsDetectorRegistry.ts +8 -0
- package/package.json +1 -1
- package/skills.lock.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.186
|
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
hasKotlinGlobalScopeUsage,
|
|
12
12
|
hasKotlinHardcodedBackgroundDispatcherUsage,
|
|
13
13
|
hasKotlinLiveDataStateExposureUsage,
|
|
14
|
+
hasKotlinLifecycleScopeUsage,
|
|
15
|
+
hasKotlinWithContextUsage,
|
|
14
16
|
hasKotlinManualCoroutineScopeInViewModelUsage,
|
|
15
17
|
hasKotlinRunBlockingUsage,
|
|
16
18
|
hasKotlinSupervisorScopeUsage,
|
|
@@ -193,6 +195,58 @@ class SyncOrdersUseCase {
|
|
|
193
195
|
assert.equal(hasKotlinHardcodedBackgroundDispatcherUsage(source), false);
|
|
194
196
|
});
|
|
195
197
|
|
|
198
|
+
test('hasKotlinWithContextUsage detecta withContext con dispatcher y con generics', () => {
|
|
199
|
+
const dispatcherSource = `
|
|
200
|
+
class SyncOrdersUseCase {
|
|
201
|
+
suspend fun execute() = withContext(Dispatchers.IO) { syncRemote() }
|
|
202
|
+
}
|
|
203
|
+
`;
|
|
204
|
+
const genericSource = `
|
|
205
|
+
class SyncOrdersUseCase {
|
|
206
|
+
suspend fun execute() = withContext<Unit>(dispatcher) { syncRemote() }
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
assert.equal(hasKotlinWithContextUsage(dispatcherSource), true);
|
|
210
|
+
assert.equal(hasKotlinWithContextUsage(genericSource), true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('hasKotlinWithContextUsage ignora imports, comentarios, strings y nombres parciales', () => {
|
|
214
|
+
const source = `
|
|
215
|
+
import kotlinx.coroutines.withContext
|
|
216
|
+
// withContext(Dispatchers.IO) { }
|
|
217
|
+
val sample = "withContext(Dispatchers.Default)"
|
|
218
|
+
class SyncOrdersUseCase {
|
|
219
|
+
suspend fun execute() = customWithContext(dispatcher) { syncRemote() }
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
assert.equal(hasKotlinWithContextUsage(source), false);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('hasKotlinLifecycleScopeUsage detecta lifecycleScope con llamadas encadenadas', () => {
|
|
226
|
+
const source = `
|
|
227
|
+
class SyncOrdersUseCase {
|
|
228
|
+
fun execute() {
|
|
229
|
+
lifecycleScope.launch { syncRemote() }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
`;
|
|
233
|
+
assert.equal(hasKotlinLifecycleScopeUsage(source), true);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('hasKotlinLifecycleScopeUsage ignora imports, comentarios, strings y nombres parciales', () => {
|
|
237
|
+
const source = `
|
|
238
|
+
import androidx.lifecycle.lifecycleScope
|
|
239
|
+
// lifecycleScope.launch { syncRemote() }
|
|
240
|
+
val sample = "lifecycleScope.launch { syncRemote() }"
|
|
241
|
+
class SyncOrdersUseCase {
|
|
242
|
+
fun execute() {
|
|
243
|
+
customLifecycleScope.launch { syncRemote() }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
assert.equal(hasKotlinLifecycleScopeUsage(source), false);
|
|
248
|
+
});
|
|
249
|
+
|
|
196
250
|
test('hasKotlinSupervisorScopeUsage detecta supervisorScope con parentesis y llaves', () => {
|
|
197
251
|
const parenthesesSource = `
|
|
198
252
|
class SyncOrdersUseCase {
|
|
@@ -259,6 +259,14 @@ export const hasKotlinHardcodedBackgroundDispatcherUsage = (source: string): boo
|
|
|
259
259
|
return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*(?:IO|Default)\b/).length > 0;
|
|
260
260
|
};
|
|
261
261
|
|
|
262
|
+
export const hasKotlinWithContextUsage = (source: string): boolean => {
|
|
263
|
+
return collectKotlinRegexLines(source, /\bwithContext\s*(?:<[^>\n]+>\s*)?\(/).length > 0;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
export const hasKotlinLifecycleScopeUsage = (source: string): boolean => {
|
|
267
|
+
return collectKotlinRegexLines(source, /\blifecycleScope\s*\./).length > 0;
|
|
268
|
+
};
|
|
269
|
+
|
|
262
270
|
export const hasKotlinSupervisorScopeUsage = (source: string): boolean => {
|
|
263
271
|
return collectKotlinRegexLines(source, /\bsupervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{)/).length > 0;
|
|
264
272
|
};
|
|
@@ -649,6 +649,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
649
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
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
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.' },
|
|
652
|
+
{ platform: 'android', pathCheck: isAndroidDomainOrApplicationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinWithContextUsage, ruleId: 'heuristics.android.coroutines.with-context.ast', code: 'HEURISTICS_ANDROID_COROUTINES_WITH_CONTEXT_AST', message: 'AST heuristic detected withContext usage in Android domain/application code.' },
|
|
653
|
+
{ platform: 'android', pathCheck: isAndroidDomainOrApplicationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinLifecycleScopeUsage, ruleId: 'heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast', code: 'HEURISTICS_ANDROID_COROUTINES_LIFECYCLE_SCOPE_BOUNDARY_LEAK_AST', message: 'AST heuristic detected lifecycleScope usage in Android domain/application code.' },
|
|
652
654
|
{ platform: 'android', pathCheck: isAndroidDomainOrApplicationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinSupervisorScopeUsage, ruleId: 'heuristics.android.coroutines.supervisor-scope.ast', code: 'HEURISTICS_ANDROID_COROUTINES_SUPERVISOR_SCOPE_AST', message: 'AST heuristic detected supervisorScope usage in Android domain/application code.' },
|
|
653
655
|
{ platform: 'android', pathCheck: isAndroidDomainOrApplicationPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinCoroutineTryCatchUsage, ruleId: 'heuristics.android.coroutines.try-catch.ast', code: 'HEURISTICS_ANDROID_COROUTINES_TRY_CATCH_AST', message: 'AST heuristic detected try-catch handling in Android coroutine code.' },
|
|
654
656
|
];
|
|
@@ -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, 16);
|
|
7
7
|
|
|
8
8
|
const ids = androidRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -14,6 +14,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
14
14
|
'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
|
|
15
15
|
'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
|
|
16
16
|
'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
17
|
+
'heuristics.android.coroutines.with-context.ast',
|
|
18
|
+
'heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast',
|
|
17
19
|
'heuristics.android.coroutines.supervisor-scope.ast',
|
|
18
20
|
'heuristics.android.coroutines.try-catch.ast',
|
|
19
21
|
'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
@@ -62,6 +64,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
|
|
|
62
64
|
byId.get('heuristics.android.coroutines.hardcoded-background-dispatcher.ast')?.then.code,
|
|
63
65
|
'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST'
|
|
64
66
|
);
|
|
67
|
+
assert.equal(
|
|
68
|
+
byId.get('heuristics.android.coroutines.with-context.ast')?.then.code,
|
|
69
|
+
'HEURISTICS_ANDROID_COROUTINES_WITH_CONTEXT_AST'
|
|
70
|
+
);
|
|
71
|
+
assert.equal(
|
|
72
|
+
byId.get('heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast')?.then.code,
|
|
73
|
+
'HEURISTICS_ANDROID_COROUTINES_LIFECYCLE_SCOPE_BOUNDARY_LEAK_AST'
|
|
74
|
+
);
|
|
65
75
|
assert.equal(
|
|
66
76
|
byId.get('heuristics.android.coroutines.supervisor-scope.ast')?.then.code,
|
|
67
77
|
'HEURISTICS_ANDROID_COROUTINES_SUPERVISOR_SCOPE_AST'
|
|
@@ -135,6 +135,46 @@ export const androidRules: RuleSet = [
|
|
|
135
135
|
code: 'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST',
|
|
136
136
|
},
|
|
137
137
|
},
|
|
138
|
+
{
|
|
139
|
+
id: 'heuristics.android.coroutines.with-context.ast',
|
|
140
|
+
description:
|
|
141
|
+
'Detects withContext usage in Android domain/application code.',
|
|
142
|
+
severity: 'WARN',
|
|
143
|
+
platform: 'android',
|
|
144
|
+
locked: true,
|
|
145
|
+
when: {
|
|
146
|
+
kind: 'Heuristic',
|
|
147
|
+
where: {
|
|
148
|
+
ruleId: 'heuristics.android.coroutines.with-context.ast',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
then: {
|
|
152
|
+
kind: 'Finding',
|
|
153
|
+
message:
|
|
154
|
+
'AST heuristic detected withContext usage in Android domain/application code.',
|
|
155
|
+
code: 'HEURISTICS_ANDROID_COROUTINES_WITH_CONTEXT_AST',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast',
|
|
160
|
+
description:
|
|
161
|
+
'Detects lifecycleScope usage in Android domain/application code where lifecycle ownership belongs to UI lifecycle owners.',
|
|
162
|
+
severity: 'WARN',
|
|
163
|
+
platform: 'android',
|
|
164
|
+
locked: true,
|
|
165
|
+
when: {
|
|
166
|
+
kind: 'Heuristic',
|
|
167
|
+
where: {
|
|
168
|
+
ruleId: 'heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast',
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
then: {
|
|
172
|
+
kind: 'Finding',
|
|
173
|
+
message:
|
|
174
|
+
'AST heuristic detected lifecycleScope usage in Android domain/application code.',
|
|
175
|
+
code: 'HEURISTICS_ANDROID_COROUTINES_LIFECYCLE_SCOPE_BOUNDARY_LEAK_AST',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
138
178
|
{
|
|
139
179
|
id: 'heuristics.android.coroutines.supervisor-scope.ast',
|
|
140
180
|
description:
|
|
@@ -126,9 +126,11 @@ app/
|
|
|
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
|
+
✅ `skills.android.guideline.android.withcontext-cambiar-dispatcher` debe mapear a señal ejecutable de `withContext` en `domain` o `application`.
|
|
130
|
+
✅ `skills.android.guideline.android.lifecyclescope-scope-de-activity-fragment` debe mapear a señal ejecutable de `lifecycleScope` fuera de la capa UI, empezando por `domain` o `application`.
|
|
129
131
|
✅ `skills.android.guideline.android.supervisorscope-errores-no-cancelan-otros-jobs` debe mapear a señal ejecutable de `supervisorScope` en `domain` o `application`.
|
|
130
132
|
✅ `skills.android.guideline.android.try-catch-manejo-de-errores-en-coroutines` debe mapear a señal ejecutable de `try/catch` dentro de código coroutine en `domain` o `application`.
|
|
131
|
-
✅ El baseline inicial de coroutines es parcial: cubre APIs bloqueantes, scopes no estructurados, scopes manuales en `ViewModel`, filtraciones de `Dispatchers.Main`, hardcode de dispatchers de background en dominio/aplicación, `supervisorScope` y `try/catch` en coroutines, pero no declara todavía cobertura completa de `Flow`, dispatchers ni cancelación cooperativa.
|
|
133
|
+
✅ El baseline inicial de coroutines es parcial: cubre APIs bloqueantes, scopes no estructurados, scopes manuales en `ViewModel`, filtraciones de `Dispatchers.Main`, hardcode de dispatchers de background en dominio/aplicación, `withContext`, fuga de `lifecycleScope`, `supervisorScope` y `try/catch` en coroutines, pero no declara todavía cobertura completa de `Flow`, dispatchers ni cancelación cooperativa.
|
|
132
134
|
✅ 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.
|
|
133
135
|
|
|
134
136
|
### Enforcement AST inicial de Flow/StateFlow Android
|
|
@@ -231,6 +231,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
231
231
|
'heuristics.android.coroutines.hardcoded-background-dispatcher.ast',
|
|
232
232
|
]
|
|
233
233
|
),
|
|
234
|
+
'skills.android.guideline.android.withcontext-cambiar-dispatcher': heuristicDetector(
|
|
235
|
+
'android.coroutines.with-context',
|
|
236
|
+
['heuristics.android.coroutines.with-context.ast']
|
|
237
|
+
),
|
|
238
|
+
'skills.android.guideline.android.lifecyclescope-scope-de-activity-fragment': heuristicDetector(
|
|
239
|
+
'android.coroutines.lifecycle-scope',
|
|
240
|
+
['heuristics.android.coroutines.lifecycle-scope-boundary-leak.ast']
|
|
241
|
+
),
|
|
234
242
|
'skills.android.guideline.android.supervisorscope-errores-no-cancelan-otros-jobs': heuristicDetector(
|
|
235
243
|
'android.coroutines.supervisor-scope',
|
|
236
244
|
['heuristics.android.coroutines.supervisor-scope.ast']
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.186",
|
|
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": {
|