pumuki 6.3.183 → 6.3.185

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.183
1
+ v6.3.185
@@ -6,10 +6,12 @@ import {
6
6
  findKotlinLiskovSubstitutionMatch,
7
7
  findKotlinOpenClosedWhenMatch,
8
8
  findKotlinPresentationSrpMatch,
9
+ hasKotlinCoroutineTryCatchUsage,
9
10
  hasKotlinDispatcherMainBoundaryLeakUsage,
10
11
  hasKotlinGlobalScopeUsage,
11
12
  hasKotlinHardcodedBackgroundDispatcherUsage,
12
13
  hasKotlinLiveDataStateExposureUsage,
14
+ hasKotlinWithContextUsage,
13
15
  hasKotlinManualCoroutineScopeInViewModelUsage,
14
16
  hasKotlinRunBlockingUsage,
15
17
  hasKotlinSupervisorScopeUsage,
@@ -192,6 +194,33 @@ class SyncOrdersUseCase {
192
194
  assert.equal(hasKotlinHardcodedBackgroundDispatcherUsage(source), false);
193
195
  });
194
196
 
197
+ test('hasKotlinWithContextUsage detecta withContext con dispatcher y con generics', () => {
198
+ const dispatcherSource = `
199
+ class SyncOrdersUseCase {
200
+ suspend fun execute() = withContext(Dispatchers.IO) { syncRemote() }
201
+ }
202
+ `;
203
+ const genericSource = `
204
+ class SyncOrdersUseCase {
205
+ suspend fun execute() = withContext<Unit>(dispatcher) { syncRemote() }
206
+ }
207
+ `;
208
+ assert.equal(hasKotlinWithContextUsage(dispatcherSource), true);
209
+ assert.equal(hasKotlinWithContextUsage(genericSource), true);
210
+ });
211
+
212
+ test('hasKotlinWithContextUsage ignora imports, comentarios, strings y nombres parciales', () => {
213
+ const source = `
214
+ import kotlinx.coroutines.withContext
215
+ // withContext(Dispatchers.IO) { }
216
+ val sample = "withContext(Dispatchers.Default)"
217
+ class SyncOrdersUseCase {
218
+ suspend fun execute() = customWithContext(dispatcher) { syncRemote() }
219
+ }
220
+ `;
221
+ assert.equal(hasKotlinWithContextUsage(source), false);
222
+ });
223
+
195
224
  test('hasKotlinSupervisorScopeUsage detecta supervisorScope con parentesis y llaves', () => {
196
225
  const parenthesesSource = `
197
226
  class SyncOrdersUseCase {
@@ -223,6 +252,53 @@ class SyncOrdersUseCase {
223
252
  assert.equal(hasKotlinSupervisorScopeUsage(source), false);
224
253
  });
225
254
 
255
+ test('hasKotlinCoroutineTryCatchUsage detecta try-catch dentro de contexto coroutine', () => {
256
+ const suspendSource = `
257
+ class SyncOrdersUseCase {
258
+ suspend fun execute() {
259
+ try {
260
+ syncRemote()
261
+ } catch (error: IOException) {
262
+ recover(error)
263
+ }
264
+ }
265
+ }
266
+ `;
267
+ const launchSource = `
268
+ class SyncOrdersUseCase {
269
+ fun execute() {
270
+ launch {
271
+ try {
272
+ syncRemote()
273
+ } catch (error: IOException) {
274
+ recover(error)
275
+ }
276
+ }
277
+ }
278
+ }
279
+ `;
280
+ assert.equal(hasKotlinCoroutineTryCatchUsage(suspendSource), true);
281
+ assert.equal(hasKotlinCoroutineTryCatchUsage(launchSource), true);
282
+ });
283
+
284
+ test('hasKotlinCoroutineTryCatchUsage ignora imports, comentarios, strings y try-catch no coroutine', () => {
285
+ const source = `
286
+ import kotlin.runCatching
287
+ // suspend fun execute() { try { syncRemote() } catch (error: IOException) { recover(error) } }
288
+ val sample = "try { syncRemote() } catch (error: IOException) { recover(error) }"
289
+ class SyncOrdersUseCase {
290
+ fun execute() {
291
+ try {
292
+ syncRemote()
293
+ } catch (error: IOException) {
294
+ recover(error)
295
+ }
296
+ }
297
+ }
298
+ `;
299
+ assert.equal(hasKotlinCoroutineTryCatchUsage(source), false);
300
+ });
301
+
226
302
  test('findKotlinPresentationSrpMatch devuelve payload semantico para SRP-Android en presentation', () => {
227
303
  const source = `
228
304
  import android.content.SharedPreferences
@@ -259,10 +259,27 @@ 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
+
262
266
  export const hasKotlinSupervisorScopeUsage = (source: string): boolean => {
263
267
  return collectKotlinRegexLines(source, /\bsupervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{)/).length > 0;
264
268
  };
265
269
 
270
+ export const hasKotlinCoroutineTryCatchUsage = (source: string): boolean => {
271
+ const sanitizedSource = source
272
+ .split(/\r?\n/)
273
+ .map((line) => stripKotlinLineForSemanticScan(line))
274
+ .filter((line) => !line.trimStart().startsWith('import '))
275
+ .join('\n');
276
+
277
+ return (
278
+ /\btry\s*\{[\s\S]*\bcatch\s*\(/.test(sanitizedSource) &&
279
+ /\b(?:suspend\s+fun|launch\s*\{|async\s*\{|withContext\s*\(|supervisorScope\s*(?:<[^>\n]+>\s*)?(?:\(|\{))/.test(sanitizedSource)
280
+ );
281
+ };
282
+
266
283
  export const hasKotlinThreadSleepCall = (source: string): boolean => {
267
284
  return scanCodeLikeSource(source, ({ source: kotlinSource, index, current }) => {
268
285
  if (current !== 'T') {
@@ -649,7 +649,9 @@ 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.' },
652
653
  { 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.' },
654
+ { 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.' },
653
655
  ];
654
656
 
655
657
  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, 13);
6
+ assert.equal(androidRules.length, 15);
7
7
 
8
8
  const ids = androidRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -14,7 +14,9 @@ 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',
17
18
  'heuristics.android.coroutines.supervisor-scope.ast',
19
+ 'heuristics.android.coroutines.try-catch.ast',
18
20
  'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
19
21
  'heuristics.android.solid.ocp.discriminator-branching.ast',
20
22
  'heuristics.android.solid.dip.concrete-framework-dependency.ast',
@@ -61,10 +63,18 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
61
63
  byId.get('heuristics.android.coroutines.hardcoded-background-dispatcher.ast')?.then.code,
62
64
  'HEURISTICS_ANDROID_COROUTINES_HARDCODED_BACKGROUND_DISPATCHER_AST'
63
65
  );
66
+ assert.equal(
67
+ byId.get('heuristics.android.coroutines.with-context.ast')?.then.code,
68
+ 'HEURISTICS_ANDROID_COROUTINES_WITH_CONTEXT_AST'
69
+ );
64
70
  assert.equal(
65
71
  byId.get('heuristics.android.coroutines.supervisor-scope.ast')?.then.code,
66
72
  'HEURISTICS_ANDROID_COROUTINES_SUPERVISOR_SCOPE_AST'
67
73
  );
74
+ assert.equal(
75
+ byId.get('heuristics.android.coroutines.try-catch.ast')?.then.code,
76
+ 'HEURISTICS_ANDROID_COROUTINES_TRY_CATCH_AST'
77
+ );
68
78
  assert.equal(
69
79
  byId.get('heuristics.android.solid.srp.presentation-mixed-responsibilities.ast')?.then.code,
70
80
  'HEURISTICS_ANDROID_SOLID_SRP_PRESENTATION_MIXED_RESPONSIBILITIES_AST'
@@ -135,6 +135,26 @@ 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
+ },
138
158
  {
139
159
  id: 'heuristics.android.coroutines.supervisor-scope.ast',
140
160
  description:
@@ -155,6 +175,26 @@ export const androidRules: RuleSet = [
155
175
  code: 'HEURISTICS_ANDROID_COROUTINES_SUPERVISOR_SCOPE_AST',
156
176
  },
157
177
  },
178
+ {
179
+ id: 'heuristics.android.coroutines.try-catch.ast',
180
+ description:
181
+ 'Detects try-catch error handling in Android coroutine code.',
182
+ severity: 'WARN',
183
+ platform: 'android',
184
+ locked: true,
185
+ when: {
186
+ kind: 'Heuristic',
187
+ where: {
188
+ ruleId: 'heuristics.android.coroutines.try-catch.ast',
189
+ },
190
+ },
191
+ then: {
192
+ kind: 'Finding',
193
+ message:
194
+ 'AST heuristic detected try-catch handling in Android coroutine code.',
195
+ code: 'HEURISTICS_ANDROID_COROUTINES_TRY_CATCH_AST',
196
+ },
197
+ },
158
198
  {
159
199
  id: 'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
160
200
  description:
@@ -126,8 +126,10 @@ 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`.
129
130
  ✅ `skills.android.guideline.android.supervisorscope-errores-no-cancelan-otros-jobs` debe mapear a señal ejecutable de `supervisorScope` en `domain` o `application`.
130
- 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 y `supervisorScope`, pero no declara todavía cobertura completa de `Flow`, dispatchers, try-catch ni cancelación cooperativa.
131
+ `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`.
132
+ ✅ 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`, `supervisorScope` y `try/catch` en coroutines, pero no declara todavía cobertura completa de `Flow`, dispatchers ni cancelación cooperativa.
131
133
  ✅ 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.
132
134
 
133
135
  ### Enforcement AST inicial de Flow/StateFlow Android
@@ -231,10 +231,18 @@ 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
+ ),
234
238
  'skills.android.guideline.android.supervisorscope-errores-no-cancelan-otros-jobs': heuristicDetector(
235
239
  'android.coroutines.supervisor-scope',
236
240
  ['heuristics.android.coroutines.supervisor-scope.ast']
237
241
  ),
242
+ 'skills.android.guideline.android.try-catch-manejo-de-errores-en-coroutines': heuristicDetector(
243
+ 'android.coroutines.try-catch',
244
+ ['heuristics.android.coroutines.try-catch.ast']
245
+ ),
238
246
  'skills.android.guideline.android.stateflow-estado-mutable-observable': heuristicDetector(
239
247
  'android.flow.state-exposure',
240
248
  ['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.183",
3
+ "version": "6.3.185",
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-12T20:42:36.407Z",
4
+ "generatedAt": "2026-05-12T20:54:35.698Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",