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 CHANGED
@@ -1 +1 @@
1
- v6.3.180
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, 10);
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
- 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
+ `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.180",
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": {
package/skills.lock.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "compilerVersion": "1.0.0",
4
- "generatedAt": "2026-05-12T19:46:23.268Z",
4
+ "generatedAt": "2026-05-12T20:34:49.636Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",