pumuki 6.3.269 → 6.3.271

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/VERSION +1 -1
  3. package/core/facts/detectors/text/android.test.ts +538 -0
  4. package/core/facts/detectors/text/android.ts +436 -0
  5. package/core/facts/detectors/text/ios.test.ts +328 -1
  6. package/core/facts/detectors/text/ios.ts +241 -0
  7. package/core/facts/detectors/typescript/index.test.ts +393 -0
  8. package/core/facts/detectors/typescript/index.ts +316 -0
  9. package/core/facts/extractHeuristicFacts.ts +70 -1
  10. package/core/rules/presets/heuristics/android.test.ts +91 -1
  11. package/core/rules/presets/heuristics/android.ts +360 -0
  12. package/core/rules/presets/heuristics/ios.test.ts +54 -1
  13. package/core/rules/presets/heuristics/ios.ts +243 -2
  14. package/core/rules/presets/heuristics/typescript.test.ts +50 -2
  15. package/core/rules/presets/heuristics/typescript.ts +162 -0
  16. package/docs/operations/RELEASE_NOTES.md +4 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +501 -0
  18. package/integrations/config/skillsRuleClassification.ts +127 -3
  19. package/integrations/context/contextGate.ts +192 -0
  20. package/integrations/git/runPlatformGate.ts +4 -1
  21. package/integrations/lifecycle/preWriteAutomation.ts +1 -0
  22. package/integrations/lifecycle/preWriteLease.ts +41 -4
  23. package/package.json +2 -1
  24. package/scripts/classify-skills-rules.ts +2 -2
  25. package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
  26. package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
  27. package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
  28. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
  29. package/scripts/framework-menu-gate-lib.ts +86 -1
  30. package/scripts/framework-menu-layout-data.ts +3 -3
  31. package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
  32. package/scripts/framework-menu.ts +10 -6
  33. package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
  34. package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
  35. package/scripts/package-manifest-lib.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.271] - 2026-05-18
4
+
5
+ - Lifecycle: PRE_WRITE can now materialize a `validated-diff` lease after SDD and AI Gate allow an already staged slice; PRE_COMMIT/PRE_PUSH accept it only while the current code paths match the validated diff, fixing RuralGo `PUMUKI-INC-142` without opening a bypass.
6
+
3
7
  ## [6.3.267] - 2026-05-14
4
8
 
5
9
  - iOS: `skills.ios.no-dispatchsemaphore` now emits actionable AST-style evidence for `DispatchSemaphore` usage, including exact lines, primary/related nodes and remediation toward `TaskGroup`, `AsyncStream` or explicit async boundaries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.269
1
+ v6.3.271
@@ -6,17 +6,36 @@ import {
6
6
  findKotlinLiskovSubstitutionMatch,
7
7
  findKotlinOpenClosedWhenMatch,
8
8
  findKotlinPresentationSrpMatch,
9
+ hasAndroidAsyncTaskUsage,
9
10
  hasKotlinCoroutineTryCatchUsage,
10
11
  hasKotlinDispatcherMainBoundaryLeakUsage,
11
12
  hasKotlinGlobalScopeUsage,
13
+ hasKotlinGodActivityUsage,
12
14
  hasKotlinHardcodedBackgroundDispatcherUsage,
15
+ hasKotlinIncompleteMaterialThemeUsage,
13
16
  hasKotlinJUnit4Usage,
17
+ hasKotlinHardcodedUiStringUsage,
18
+ collectKotlinHardcodedUiStringLines,
19
+ hasKotlinImperativeNavigationUsage,
20
+ hasKotlinLaunchedEffectBusyLoopUsage,
14
21
  hasKotlinLiveDataStateExposureUsage,
15
22
  hasKotlinLifecycleScopeUsage,
23
+ hasKotlinLegacyBottomNavigationUsage,
24
+ hasKotlinComposableObjectCreationWithoutRememberUsage,
25
+ hasKotlinComposableStateCreationWithoutRememberUsage,
26
+ hasKotlinForceUnwrapUsage,
27
+ hasKotlinFontScaleDisabledUsage,
16
28
  hasKotlinProductionMockUsage,
29
+ hasKotlinNonLazyScrollableCollectionUsage,
30
+ hasKotlinProductionLoggingUsage,
17
31
  hasKotlinSharedPreferencesUsage,
32
+ hasKotlinSharedFlowUsedAsStateUsage,
33
+ hasKotlinUnstableLaunchedEffectKeyUsage,
34
+ hasKotlinViewModelFlowWithoutStateInUsage,
18
35
  hasKotlinWithContextUsage,
19
36
  hasKotlinManualCoroutineScopeInViewModelUsage,
37
+ hasKotlinMissingContentDescriptionUsage,
38
+ hasKotlinModifierBackgroundBeforePaddingUsage,
20
39
  hasKotlinRunBlockingUsage,
21
40
  hasKotlinSupervisorScopeUsage,
22
41
  hasKotlinThreadSleepCall,
@@ -102,6 +121,446 @@ fun main() {
102
121
  assert.equal(hasKotlinRunBlockingUsage(partialSource), false);
103
122
  });
104
123
 
124
+ test('hasAndroidAsyncTaskUsage detecta AsyncTask legacy en Kotlin y Java', () => {
125
+ const kotlinSource = `
126
+ class LegacySyncTask : AsyncTask<Unit, Unit, Unit>() {
127
+ override fun doInBackground(vararg params: Unit) = Unit
128
+ }
129
+ `;
130
+ const javaSource = `
131
+ class LegacySyncTask extends android.os.AsyncTask<Void, Void, Void> {
132
+ }
133
+ `;
134
+ const executorSource = `
135
+ fun executeLegacy(task: Runnable) {
136
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(task)
137
+ }
138
+ `;
139
+ assert.equal(hasAndroidAsyncTaskUsage(kotlinSource), true);
140
+ assert.equal(hasAndroidAsyncTaskUsage(javaSource), true);
141
+ assert.equal(hasAndroidAsyncTaskUsage(executorSource), true);
142
+ });
143
+
144
+ test('hasAndroidAsyncTaskUsage ignora imports, comentarios y strings', () => {
145
+ const source = `
146
+ import android.os.AsyncTask
147
+ // class LegacySyncTask : AsyncTask<Unit, Unit, Unit>()
148
+ val sample = "AsyncTask.THREAD_POOL_EXECUTOR"
149
+ class ModernSyncTask {
150
+ suspend fun execute() = coroutineScope { }
151
+ }
152
+ `;
153
+ assert.equal(hasAndroidAsyncTaskUsage(source), false);
154
+ });
155
+
156
+ test('hasKotlinGodActivityUsage detecta Activity con UI, red y persistencia mezcladas', () => {
157
+ const source = `
158
+ class CheckoutActivity : ComponentActivity() {
159
+ override fun onCreate(savedInstanceState: Bundle?) {
160
+ super.onCreate(savedInstanceState)
161
+ setContent { CheckoutScreen() }
162
+ }
163
+
164
+ fun loadRemoteCheckout() {
165
+ OkHttpClient().newCall(request).execute()
166
+ }
167
+
168
+ fun cacheCheckoutState() {
169
+ getSharedPreferences("checkout", MODE_PRIVATE).edit().apply()
170
+ }
171
+ }
172
+ `;
173
+ assert.equal(hasKotlinGodActivityUsage(source), true);
174
+ });
175
+
176
+ test('hasKotlinGodActivityUsage permite Activity entrypoint delgada', () => {
177
+ const source = `
178
+ class MainActivity : ComponentActivity() {
179
+ override fun onCreate(savedInstanceState: Bundle?) {
180
+ super.onCreate(savedInstanceState)
181
+ setContent { AppRoot() }
182
+ }
183
+ }
184
+ `;
185
+ assert.equal(hasKotlinGodActivityUsage(source), false);
186
+ });
187
+
188
+ test('hasKotlinNonLazyScrollableCollectionUsage detecta Column scrollable con iteracion de coleccion', () => {
189
+ const source = `
190
+ @Composable
191
+ fun OrdersScreen(orders: List<Order>) {
192
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
193
+ orders.forEach { order ->
194
+ Text(order.name)
195
+ }
196
+ }
197
+ }
198
+ `;
199
+ assert.equal(hasKotlinNonLazyScrollableCollectionUsage(source), true);
200
+ });
201
+
202
+ test('hasKotlinNonLazyScrollableCollectionUsage permite LazyColumn y Column estatica', () => {
203
+ const lazySource = `
204
+ @Composable
205
+ fun OrdersScreen(orders: List<Order>) {
206
+ LazyColumn {
207
+ items(orders) { order -> Text(order.name) }
208
+ }
209
+ }
210
+ `;
211
+ const staticSource = `
212
+ @Composable
213
+ fun EmptyState() {
214
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
215
+ Text("One")
216
+ Text("Two")
217
+ }
218
+ }
219
+ `;
220
+ assert.equal(hasKotlinNonLazyScrollableCollectionUsage(lazySource), false);
221
+ assert.equal(hasKotlinNonLazyScrollableCollectionUsage(staticSource), false);
222
+ });
223
+
224
+ test('hasKotlinUnstableLaunchedEffectKeyUsage detecta claves constantes o ausentes', () => {
225
+ const unitSource = `
226
+ @Composable
227
+ fun OrdersScreen(viewModel: OrdersViewModel) {
228
+ LaunchedEffect(Unit) { viewModel.load() }
229
+ }
230
+ `;
231
+ const trueSource = `
232
+ @Composable
233
+ fun OrdersScreen(viewModel: OrdersViewModel) {
234
+ LaunchedEffect(true) { viewModel.load() }
235
+ }
236
+ `;
237
+ const emptySource = `
238
+ @Composable
239
+ fun OrdersScreen(viewModel: OrdersViewModel) {
240
+ LaunchedEffect() { viewModel.load() }
241
+ }
242
+ `;
243
+ assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(unitSource), true);
244
+ assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(trueSource), true);
245
+ assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(emptySource), true);
246
+ });
247
+
248
+ test('hasKotlinUnstableLaunchedEffectKeyUsage permite claves de estado estables e ignora comentarios', () => {
249
+ const source = `
250
+ // LaunchedEffect(Unit) { debug() }
251
+ val sample = "LaunchedEffect(true) { debug() }"
252
+ @Composable
253
+ fun OrdersScreen(orderId: String, viewModel: OrdersViewModel) {
254
+ LaunchedEffect(orderId) { viewModel.load(orderId) }
255
+ }
256
+ `;
257
+ assert.equal(hasKotlinUnstableLaunchedEffectKeyUsage(source), false);
258
+ });
259
+
260
+ test('hasKotlinLaunchedEffectBusyLoopUsage detecta bucle no cooperativo dentro de LaunchedEffect', () => {
261
+ const source = `
262
+ @Composable
263
+ fun SyncPulse(viewModel: SyncViewModel) {
264
+ LaunchedEffect(viewModel.sessionId) {
265
+ while (true) {
266
+ viewModel.poll()
267
+ }
268
+ }
269
+ }
270
+ `;
271
+ assert.equal(hasKotlinLaunchedEffectBusyLoopUsage(source), true);
272
+ });
273
+
274
+ test('hasKotlinLaunchedEffectBusyLoopUsage permite bucles cooperativos e ignora comentarios y strings', () => {
275
+ const source = `
276
+ // LaunchedEffect(Unit) { while (true) { poll() } }
277
+ val sample = "LaunchedEffect(Unit) { while (isActive) { poll() } }"
278
+ @Composable
279
+ fun SyncPulse(viewModel: SyncViewModel) {
280
+ LaunchedEffect(viewModel.sessionId) {
281
+ while (isActive) {
282
+ viewModel.poll()
283
+ delay(1000)
284
+ }
285
+ }
286
+ }
287
+ `;
288
+ assert.equal(hasKotlinLaunchedEffectBusyLoopUsage(source), false);
289
+ });
290
+
291
+ test('hasKotlinProductionLoggingUsage detecta logs directos de produccion', () => {
292
+ const source = `
293
+ class CheckoutRepository {
294
+ fun load() {
295
+ println("loading")
296
+ System.out.println("debug")
297
+ Log.d("Checkout", "loading")
298
+ Timber.e(error)
299
+ }
300
+ }
301
+ `;
302
+ assert.equal(hasKotlinProductionLoggingUsage(source), true);
303
+ });
304
+
305
+ test('hasKotlinProductionLoggingUsage permite imports, comentarios, strings y logs guardados por debug', () => {
306
+ const source = `
307
+ import android.util.Log
308
+ // Log.d("Checkout", "debug")
309
+ val sample = "println(\\"debug\\")"
310
+ class CheckoutRepository {
311
+ fun load() {
312
+ if (BuildConfig.DEBUG) Timber.d("loading")
313
+ }
314
+ }
315
+ `;
316
+ assert.equal(hasKotlinProductionLoggingUsage(source), false);
317
+ });
318
+
319
+ test('hasKotlinModifierBackgroundBeforePaddingUsage detecta background antes de padding', () => {
320
+ const source = `
321
+ @Composable
322
+ fun CheckoutCard() {
323
+ Box(
324
+ modifier = Modifier
325
+ .fillMaxWidth()
326
+ .background(Color.Red)
327
+ .padding(16.dp)
328
+ )
329
+ }
330
+ `;
331
+ assert.equal(hasKotlinModifierBackgroundBeforePaddingUsage(source), true);
332
+ });
333
+
334
+ test('hasKotlinModifierBackgroundBeforePaddingUsage permite padding antes de background e ignora comentarios', () => {
335
+ const source = `
336
+ // Modifier.background(Color.Red).padding(16.dp)
337
+ val sample = "Modifier.background(Color.Red).padding(16.dp)"
338
+ @Composable
339
+ fun CheckoutCard() {
340
+ Box(
341
+ modifier = Modifier
342
+ .padding(16.dp)
343
+ .background(Color.Red)
344
+ )
345
+ }
346
+ `;
347
+ assert.equal(hasKotlinModifierBackgroundBeforePaddingUsage(source), false);
348
+ });
349
+
350
+ test('hasKotlinMissingContentDescriptionUsage detecta Image e Icon sin contentDescription', () => {
351
+ const source = `
352
+ @Composable
353
+ fun CheckoutIcon() {
354
+ Image(painter = painterResource(R.drawable.checkout), modifier = Modifier.size(24.dp))
355
+ Icon(Icons.Default.Warning, tint = Color.Red)
356
+ }
357
+ `;
358
+ assert.equal(hasKotlinMissingContentDescriptionUsage(source), true);
359
+ });
360
+
361
+ test('hasKotlinMissingContentDescriptionUsage permite contentDescription explicita e ignora comentarios', () => {
362
+ const source = `
363
+ // Image(painter = painterResource(R.drawable.checkout))
364
+ val sample = "Icon(Icons.Default.Warning)"
365
+ @Composable
366
+ fun CheckoutIcon() {
367
+ Image(
368
+ painter = painterResource(R.drawable.checkout),
369
+ contentDescription = stringResource(R.string.checkout_icon)
370
+ )
371
+ Icon(
372
+ imageVector = Icons.Default.Warning,
373
+ contentDescription = null
374
+ )
375
+ }
376
+ `;
377
+ assert.equal(hasKotlinMissingContentDescriptionUsage(source), false);
378
+ });
379
+
380
+ test('hasKotlinFontScaleDisabledUsage detecta desactivacion del fontScale del sistema', () => {
381
+ const source = `
382
+ @Composable
383
+ fun FixedTextScale(content: @Composable () -> Unit) {
384
+ val density = LocalDensity.current
385
+ CompositionLocalProvider(LocalDensity provides Density(density.density, fontScale = 1f)) {
386
+ content()
387
+ }
388
+ }
389
+ `;
390
+ assert.equal(hasKotlinFontScaleDisabledUsage(source), true);
391
+ });
392
+
393
+ test('hasKotlinFontScaleDisabledUsage permite LocalDensity sin forzar fontScale e ignora comentarios', () => {
394
+ const source = `
395
+ // Density(density.density, fontScale = 1f)
396
+ val sample = "fontScale = 1f"
397
+ @Composable
398
+ fun ScalableText(content: @Composable () -> Unit) {
399
+ val density = LocalDensity.current
400
+ CompositionLocalProvider(LocalDensity provides density) {
401
+ content()
402
+ }
403
+ }
404
+ `;
405
+ assert.equal(hasKotlinFontScaleDisabledUsage(source), false);
406
+ });
407
+
408
+ test('hasKotlinIncompleteMaterialThemeUsage detecta MaterialTheme incompleto', () => {
409
+ const source = `
410
+ @Composable
411
+ fun AppTheme(content: @Composable () -> Unit) {
412
+ MaterialTheme(
413
+ colorScheme = AppColorScheme,
414
+ content = content
415
+ )
416
+ }
417
+ `;
418
+ assert.equal(hasKotlinIncompleteMaterialThemeUsage(source), true);
419
+ });
420
+
421
+ test('hasKotlinIncompleteMaterialThemeUsage permite MaterialTheme con colorScheme typography y shapes', () => {
422
+ const source = `
423
+ // MaterialTheme(colorScheme = AppColorScheme)
424
+ val sample = "MaterialTheme(colorScheme = AppColorScheme)"
425
+ @Composable
426
+ fun AppTheme(content: @Composable () -> Unit) {
427
+ MaterialTheme(
428
+ colorScheme = AppColorScheme,
429
+ typography = AppTypography,
430
+ shapes = AppShapes,
431
+ content = content
432
+ )
433
+ }
434
+ `;
435
+ assert.equal(hasKotlinIncompleteMaterialThemeUsage(source), false);
436
+ });
437
+
438
+ test('hasKotlinLegacyBottomNavigationUsage detecta BottomNavigation legacy de Material 2', () => {
439
+ const source = `
440
+ @Composable
441
+ fun LegacyBottomBar() {
442
+ BottomNavigation {
443
+ BottomNavigationItem(selected = true, onClick = {}, icon = { Text("Home") })
444
+ }
445
+ }
446
+ `;
447
+ assert.equal(hasKotlinLegacyBottomNavigationUsage(source), true);
448
+ });
449
+
450
+ test('hasKotlinLegacyBottomNavigationUsage permite NavigationBar Material 3 e ignora comentarios', () => {
451
+ const source = `
452
+ // BottomNavigation { }
453
+ val sample = "BottomNavigationItem(selected = true)"
454
+ @Composable
455
+ fun ModernBottomBar() {
456
+ NavigationBar {
457
+ NavigationBarItem(selected = true, onClick = {}, icon = { Text("Home") })
458
+ }
459
+ }
460
+ `;
461
+ assert.equal(hasKotlinLegacyBottomNavigationUsage(source), false);
462
+ });
463
+
464
+ test('hasKotlinImperativeNavigationUsage detecta navegacion imperative legacy', () => {
465
+ const source = `
466
+ class CheckoutActivity : ComponentActivity() {
467
+ fun openOrder() {
468
+ startActivity(Intent(this, OrderActivity::class.java))
469
+ supportFragmentManager.beginTransaction().replace(R.id.container, OrderFragment()).commit()
470
+ }
471
+ }
472
+ `;
473
+ assert.equal(hasKotlinImperativeNavigationUsage(source), true);
474
+ });
475
+
476
+ test('hasKotlinImperativeNavigationUsage permite Navigation Compose e ignora comentarios y strings', () => {
477
+ const source = `
478
+ // startActivity(Intent(this, LegacyActivity::class.java))
479
+ val sample = "supportFragmentManager.beginTransaction()"
480
+ @Composable
481
+ fun AppNav() {
482
+ val navController = rememberNavController()
483
+ NavHost(navController = navController, startDestination = "home") { }
484
+ }
485
+ `;
486
+ assert.equal(hasKotlinImperativeNavigationUsage(source), false);
487
+ });
488
+
489
+ test('hasKotlinComposableObjectCreationWithoutRememberUsage detecta objetos recreados en Composable', () => {
490
+ const source = `
491
+ @Composable
492
+ fun PriceLabel(value: BigDecimal) {
493
+ val formatter = DecimalFormat("#.00")
494
+ Text(formatter.format(value))
495
+ }
496
+ `;
497
+ assert.equal(hasKotlinComposableObjectCreationWithoutRememberUsage(source), true);
498
+ });
499
+
500
+ test('hasKotlinComposableObjectCreationWithoutRememberUsage permite remember e ignora codigo no Composable', () => {
501
+ const source = `
502
+ fun buildFormatter() {
503
+ val formatter = DecimalFormat("#.00")
504
+ }
505
+
506
+ @Composable
507
+ fun PriceLabel(value: BigDecimal) {
508
+ val formatter = remember { DecimalFormat("#.00") }
509
+ Text(formatter.format(value))
510
+ }
511
+ `;
512
+ assert.equal(hasKotlinComposableObjectCreationWithoutRememberUsage(source), false);
513
+ });
514
+
515
+ test('hasKotlinComposableStateCreationWithoutRememberUsage detecta estado creado sin remember', () => {
516
+ const source = `
517
+ @Composable
518
+ fun SearchBox() {
519
+ val query = mutableStateOf("")
520
+ val filtered = derivedStateOf { query.value.trim() }
521
+ Text(filtered.value)
522
+ }
523
+ `;
524
+ assert.equal(hasKotlinComposableStateCreationWithoutRememberUsage(source), true);
525
+ });
526
+
527
+ test('hasKotlinComposableStateCreationWithoutRememberUsage permite remember e ignora codigo no Composable', () => {
528
+ const source = `
529
+ fun buildState() {
530
+ val query = mutableStateOf("")
531
+ }
532
+
533
+ @Composable
534
+ fun SearchBox() {
535
+ val query = remember { mutableStateOf("") }
536
+ val filtered = remember { derivedStateOf { query.value.trim() } }
537
+ Text(filtered.value)
538
+ }
539
+ `;
540
+ assert.equal(hasKotlinComposableStateCreationWithoutRememberUsage(source), false);
541
+ });
542
+
543
+ test('hasKotlinForceUnwrapUsage detecta force unwrap Kotlin', () => {
544
+ const source = `
545
+ class CheckoutState {
546
+ fun title(value: String?) = value!!.trim()
547
+ }
548
+ `;
549
+ assert.equal(hasKotlinForceUnwrapUsage(source), true);
550
+ });
551
+
552
+ test('hasKotlinForceUnwrapUsage ignora comentarios, strings y operadores no unwrap', () => {
553
+ const source = `
554
+ // value!!.trim()
555
+ val sample = "value!!.trim()"
556
+ class CheckoutState {
557
+ fun same(left: String, right: String) = left !== right
558
+ fun different(left: String, right: String) = left != right
559
+ }
560
+ `;
561
+ assert.equal(hasKotlinForceUnwrapUsage(source), false);
562
+ });
563
+
105
564
  test('hasKotlinLiveDataStateExposureUsage detecta LiveData y MutableLiveData como estado observable legacy', () => {
106
565
  const source = `
107
566
  class OrdersViewModel : ViewModel() {
@@ -124,6 +583,52 @@ class OrdersViewModel : ViewModel() {
124
583
  assert.equal(hasKotlinLiveDataStateExposureUsage(source), false);
125
584
  });
126
585
 
586
+ test('hasKotlinViewModelFlowWithoutStateInUsage detecta Flow expuesto desde ViewModel sin stateIn', () => {
587
+ const source = `
588
+ class OrdersViewModel : ViewModel() {
589
+ val state: Flow<OrdersUiState> = repository.observeOrders().map { OrdersUiState(it) }
590
+ }
591
+ `;
592
+ assert.equal(hasKotlinViewModelFlowWithoutStateInUsage(source), true);
593
+ });
594
+
595
+ test('hasKotlinViewModelFlowWithoutStateInUsage permite stateIn e ignora imports, comentarios y strings', () => {
596
+ const source = `
597
+ import kotlinx.coroutines.flow.Flow
598
+ // val state: Flow<OrdersUiState> = repository.observeOrders()
599
+ val sample = "val state: Flow<OrdersUiState>"
600
+ class OrdersViewModel : ViewModel() {
601
+ val state: StateFlow<OrdersUiState> = repository.observeOrders()
602
+ .map { OrdersUiState(it) }
603
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), OrdersUiState())
604
+ }
605
+ `;
606
+ assert.equal(hasKotlinViewModelFlowWithoutStateInUsage(source), false);
607
+ });
608
+
609
+ test('hasKotlinSharedFlowUsedAsStateUsage detecta SharedFlow usado como estado de ViewModel', () => {
610
+ const source = `
611
+ class OrdersViewModel : ViewModel() {
612
+ val uiState: SharedFlow<OrdersUiState> = MutableSharedFlow()
613
+ private val _screenState: MutableSharedFlow<CheckoutState> = MutableSharedFlow()
614
+ }
615
+ `;
616
+ assert.equal(hasKotlinSharedFlowUsedAsStateUsage(source), true);
617
+ });
618
+
619
+ test('hasKotlinSharedFlowUsedAsStateUsage permite SharedFlow de eventos y StateFlow de estado', () => {
620
+ const source = `
621
+ import kotlinx.coroutines.flow.SharedFlow
622
+ // val uiState: SharedFlow<OrdersUiState> = MutableSharedFlow()
623
+ val sample = "val state: SharedFlow<OrdersUiState>"
624
+ class OrdersViewModel : ViewModel() {
625
+ val uiState: StateFlow<OrdersUiState> = MutableStateFlow(OrdersUiState())
626
+ val events: SharedFlow<OrdersEvent> = MutableSharedFlow()
627
+ }
628
+ `;
629
+ assert.equal(hasKotlinSharedFlowUsedAsStateUsage(source), false);
630
+ });
631
+
127
632
  test('hasKotlinManualCoroutineScopeInViewModelUsage detecta CoroutineScope manual dentro de ViewModel', () => {
128
633
  const source = `
129
634
  class OrdersViewModel : ViewModel() {
@@ -352,6 +857,39 @@ class OrdersRepositoryFactory {
352
857
  assert.equal(hasKotlinProductionMockUsage(source), false);
353
858
  });
354
859
 
860
+ test('hasKotlinHardcodedUiStringUsage detecta textos UI hardcodeados en Compose', () => {
861
+ const source = `
862
+ @Composable
863
+ fun CheckoutHeader() {
864
+ Text("Comprar ahora")
865
+ Icon(
866
+ imageVector = Icons.Default.Warning,
867
+ contentDescription = "Aviso"
868
+ )
869
+ }
870
+ `;
871
+ assert.equal(hasKotlinHardcodedUiStringUsage(source), true);
872
+ assert.deepEqual(collectKotlinHardcodedUiStringLines(source), [4, 7]);
873
+ });
874
+
875
+ test('hasKotlinHardcodedUiStringUsage ignora imports, comentarios y strings localizados', () => {
876
+ const source = `
877
+ import androidx.compose.material3.Text
878
+ // Text("Debug")
879
+ val sample = "Text(\\"Debug\\")"
880
+ @Composable
881
+ fun CheckoutHeader() {
882
+ Text(stringResource(R.string.checkout_title))
883
+ Icon(
884
+ imageVector = Icons.Default.Warning,
885
+ contentDescription = stringResource(R.string.warning)
886
+ )
887
+ }
888
+ `;
889
+ assert.equal(hasKotlinHardcodedUiStringUsage(source), false);
890
+ assert.deepEqual(collectKotlinHardcodedUiStringLines(source), []);
891
+ });
892
+
355
893
  test('hasKotlinSupervisorScopeUsage detecta supervisorScope con parentesis y llaves', () => {
356
894
  const parenthesesSource = `
357
895
  class SyncOrdersUseCase {