pumuki 6.3.179 → 6.3.181

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 CHANGED
@@ -1 +1 @@
1
- v6.3.179
1
+ v6.3.181
@@ -6,8 +6,10 @@ import {
6
6
  findKotlinLiskovSubstitutionMatch,
7
7
  findKotlinOpenClosedWhenMatch,
8
8
  findKotlinPresentationSrpMatch,
9
+ hasKotlinDispatcherMainBoundaryLeakUsage,
9
10
  hasKotlinGlobalScopeUsage,
10
11
  hasKotlinLiveDataStateExposureUsage,
12
+ hasKotlinManualCoroutineScopeInViewModelUsage,
11
13
  hasKotlinRunBlockingUsage,
12
14
  hasKotlinThreadSleepCall,
13
15
  } from './android';
@@ -114,6 +116,53 @@ class OrdersViewModel : ViewModel() {
114
116
  assert.equal(hasKotlinLiveDataStateExposureUsage(source), false);
115
117
  });
116
118
 
119
+ test('hasKotlinManualCoroutineScopeInViewModelUsage detecta CoroutineScope manual dentro de ViewModel', () => {
120
+ const source = `
121
+ class OrdersViewModel : ViewModel() {
122
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
123
+ }
124
+ `;
125
+ assert.equal(hasKotlinManualCoroutineScopeInViewModelUsage(source), true);
126
+ });
127
+
128
+ test('hasKotlinManualCoroutineScopeInViewModelUsage ignora imports, comentarios y uso fuera de ViewModel', () => {
129
+ const source = `
130
+ import kotlinx.coroutines.CoroutineScope
131
+ // private val scope = CoroutineScope(SupervisorJob())
132
+ val sample = "CoroutineScope(SupervisorJob())"
133
+ class OrdersWorker {
134
+ private val scope = CoroutineScope(SupervisorJob())
135
+ }
136
+ class OrdersViewModel : ViewModel() {
137
+ fun load() {
138
+ viewModelScope.launch { }
139
+ }
140
+ }
141
+ `;
142
+ assert.equal(hasKotlinManualCoroutineScopeInViewModelUsage(source), false);
143
+ });
144
+
145
+ test('hasKotlinDispatcherMainBoundaryLeakUsage detecta Dispatchers.Main como dispatcher UI explícito', () => {
146
+ const source = `
147
+ class SyncOrdersUseCase {
148
+ suspend fun execute() = withContext(Dispatchers.Main) { }
149
+ }
150
+ `;
151
+ assert.equal(hasKotlinDispatcherMainBoundaryLeakUsage(source), true);
152
+ });
153
+
154
+ test('hasKotlinDispatcherMainBoundaryLeakUsage ignora imports, comentarios y strings', () => {
155
+ const source = `
156
+ import kotlinx.coroutines.Dispatchers
157
+ // withContext(Dispatchers.Main) { }
158
+ val sample = "Dispatchers.Main"
159
+ class SyncOrdersUseCase {
160
+ suspend fun execute() = withContext(Dispatchers.IO) { }
161
+ }
162
+ `;
163
+ assert.equal(hasKotlinDispatcherMainBoundaryLeakUsage(source), false);
164
+ });
165
+
117
166
  test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
118
167
  const source = `
119
168
  import android.content.SharedPreferences
@@ -299,6 +299,42 @@ export const hasKotlinLiveDataStateExposureUsage = (source: string): boolean =>
299
299
  ).length > 0;
300
300
  };
301
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
+
334
+ export const hasKotlinDispatcherMainBoundaryLeakUsage = (source: string): boolean => {
335
+ return collectKotlinRegexLines(source, /\bDispatchers\s*\.\s*Main\b/).length > 0;
336
+ };
337
+
302
338
  export const findKotlinPresentationSrpMatch = (
303
339
  source: string
304
340
  ): KotlinPresentationSrpMatch | undefined => {
@@ -99,6 +99,10 @@ const isAndroidApplicationOrPresentationPath = (path: string): boolean => {
99
99
  return isAndroidKotlinPath(path) && (path.includes('/application/') || path.includes('/presentation/'));
100
100
  };
101
101
 
102
+ const isAndroidNonPresentationKotlinPath = (path: string): boolean => {
103
+ return isAndroidKotlinPath(path) && !path.includes('/presentation/');
104
+ };
105
+
102
106
  const isApprovedIOSBridgePath = (path: string): boolean => {
103
107
  const normalized = path.toLowerCase();
104
108
  return (
@@ -638,6 +642,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
638
642
  { 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
643
  { 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
644
  { 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.' },
645
+ { 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.' },
646
+ { 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.' },
641
647
  ];
642
648
 
643
649
  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, 9);
6
+ assert.equal(androidRules.length, 11);
7
7
 
8
8
  const ids = androidRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -11,6 +11,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
11
11
  'heuristics.android.globalscope.ast',
12
12
  'heuristics.android.run-blocking.ast',
13
13
  'heuristics.android.flow.livedata-state-exposure.ast',
14
+ 'heuristics.android.coroutines.manual-scope-in-viewmodel.ast',
15
+ 'heuristics.android.coroutines.dispatchers-main-boundary-leak.ast',
14
16
  'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
15
17
  'heuristics.android.solid.ocp.discriminator-branching.ast',
16
18
  'heuristics.android.solid.dip.concrete-framework-dependency.ast',
@@ -45,6 +47,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
45
47
  byId.get('heuristics.android.flow.livedata-state-exposure.ast')?.then.code,
46
48
  'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST'
47
49
  );
50
+ assert.equal(
51
+ byId.get('heuristics.android.coroutines.manual-scope-in-viewmodel.ast')?.then.code,
52
+ 'HEURISTICS_ANDROID_COROUTINES_MANUAL_SCOPE_IN_VIEWMODEL_AST'
53
+ );
54
+ assert.equal(
55
+ byId.get('heuristics.android.coroutines.dispatchers-main-boundary-leak.ast')?.then.code,
56
+ 'HEURISTICS_ANDROID_COROUTINES_DISPATCHERS_MAIN_BOUNDARY_LEAK_AST'
57
+ );
48
58
  assert.equal(
49
59
  byId.get('heuristics.android.solid.srp.presentation-mixed-responsibilities.ast')?.then.code,
50
60
  'HEURISTICS_ANDROID_SOLID_SRP_PRESENTATION_MIXED_RESPONSIBILITIES_AST'
@@ -75,6 +75,46 @@ export const androidRules: RuleSet = [
75
75
  code: 'HEURISTICS_ANDROID_FLOW_LIVEDATA_STATE_EXPOSURE_AST',
76
76
  },
77
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
+ },
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
+ },
78
118
  {
79
119
  id: 'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
80
120
  description:
@@ -124,7 +124,9 @@ 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
- El baseline inicial de coroutines es parcial: cubre APIs bloqueantes o scopes no estructurados, pero no declara todavía cobertura completa de `Flow`, `viewModelScope`, `supervisorScope`, dispatchers ni cancelación cooperativa.
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
+ ✅ `skills.android.guideline.android.dispatchers-main-ui-io-network-disk-default-cpu` debe mapear a señales ejecutables de dispatcher UI filtrado fuera de presentation, empezando por `Dispatchers.Main` en application/data.
129
+ ✅ El baseline inicial de coroutines es parcial: cubre APIs bloqueantes, scopes no estructurados, scopes manuales en `ViewModel` y filtraciones de `Dispatchers.Main`, pero no declara todavía cobertura completa de `Flow`, `supervisorScope`, dispatchers ni cancelación cooperativa.
128
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.
129
131
 
130
132
  ### Enforcement AST inicial de Flow/StateFlow Android
@@ -220,6 +220,14 @@ 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.dispatchers-main-ui-io-network-disk-default-cpu': heuristicDetector(
228
+ 'android.coroutines.dispatchers',
229
+ ['heuristics.android.coroutines.dispatchers-main-boundary-leak.ast']
230
+ ),
223
231
  'skills.android.guideline.android.stateflow-estado-mutable-observable': heuristicDetector(
224
232
  'android.flow.state-exposure',
225
233
  ['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.179",
3
+ "version": "6.3.181",
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": {