pumuki 6.3.359 → 6.3.361

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.
@@ -7,6 +7,8 @@ import {
7
7
  findKotlinOpenClosedWhenMatch,
8
8
  findKotlinPresentationSrpMatch,
9
9
  hasAndroidAsyncTaskUsage,
10
+ hasAndroidCustomSingletonObjectUsage,
11
+ hasAndroidLegacyFingerprintApiUsage,
10
12
  hasKotlinCoroutineTryCatchUsage,
11
13
  hasKotlinDispatcherMainBoundaryLeakUsage,
12
14
  hasKotlinGlobalScopeUsage,
@@ -1243,3 +1245,64 @@ class PumukiLspAndroidCanaryPremiumDiscountPolicy : PumukiLspAndroidCanaryDiscou
1243
1245
  assert.match(match.impact, /sustituci|regresion|crash/i);
1244
1246
  assert.match(match.expected_fix, /contrato base|estrategia|subtipo/i);
1245
1247
  });
1248
+
1249
+ test('hasAndroidLegacyFingerprintApiUsage detecta APIs biométricas legacy y preserva BiometricPrompt', () => {
1250
+ const legacyManager = `
1251
+ import android.hardware.fingerprint.FingerprintManager
1252
+
1253
+ class LegacyBiometricAuth(private val fingerprintManager: FingerprintManager) {
1254
+ fun authenticate(callback: FingerprintManager.AuthenticationCallback) {
1255
+ fingerprintManager.authenticate(null, null, 0, callback, null)
1256
+ }
1257
+ }
1258
+ `;
1259
+ const legacyCompat = `
1260
+ class LegacyCompatAuth(private val fingerprintManager: FingerprintManagerCompat) {
1261
+ fun authenticate(callback: FingerprintManagerCompat.AuthenticationCallback) {
1262
+ fingerprintManager.authenticate(null, 0, null, callback, null)
1263
+ }
1264
+ }
1265
+ `;
1266
+ const modern = `
1267
+ import androidx.biometric.BiometricPrompt
1268
+
1269
+ class ModernBiometricAuth(private val biometricPrompt: BiometricPrompt) {
1270
+ fun authenticate(promptInfo: BiometricPrompt.PromptInfo) {
1271
+ biometricPrompt.authenticate(promptInfo)
1272
+ }
1273
+ }
1274
+ // FingerprintManager should not match inside comments
1275
+ val sample = "FingerprintManagerCompat"
1276
+ `;
1277
+
1278
+ assert.equal(hasAndroidLegacyFingerprintApiUsage(legacyManager), true);
1279
+ assert.equal(hasAndroidLegacyFingerprintApiUsage(legacyCompat), true);
1280
+ assert.equal(hasAndroidLegacyFingerprintApiUsage(modern), false);
1281
+ });
1282
+
1283
+ test('hasAndroidCustomSingletonObjectUsage detecta singletons Kotlin propios y preserva objetos seguros', () => {
1284
+ const singletonSource = `
1285
+ object CheckoutRepository {
1286
+ fun loadCheckout() = Unit
1287
+ }
1288
+
1289
+ object SessionManager {
1290
+ fun clear() = Unit
1291
+ }
1292
+ `;
1293
+ const safeSource = `
1294
+ object CheckoutRoute {
1295
+ const val PATH = "checkout"
1296
+ }
1297
+
1298
+ @Module
1299
+ @InstallIn(SingletonComponent::class)
1300
+ object CheckoutModule {
1301
+ @Provides
1302
+ fun provideRepository(): CheckoutRepository = CheckoutRepository()
1303
+ }
1304
+ `;
1305
+
1306
+ assert.equal(hasAndroidCustomSingletonObjectUsage(singletonSource), true);
1307
+ assert.equal(hasAndroidCustomSingletonObjectUsage(safeSource), false);
1308
+ });
@@ -130,6 +130,51 @@ const sortedUniqueLines = (lines: ReadonlyArray<number>): readonly number[] => {
130
130
  .sort((left, right) => left - right);
131
131
  };
132
132
 
133
+ const androidCustomSingletonObjectNamePattern =
134
+ /(?:Repository|Service|Manager|Client|Store|DataSource|Gateway|Controller|Coordinator)$/;
135
+
136
+ const androidSafeObjectNamePattern =
137
+ /(?:Route|Routes|Screen|Screens|Destination|Destinations|Module|Modules|Constants|Config|Keys|Theme|Colors|Typography)$/;
138
+
139
+ const isAndroidHiltModuleObjectContext = (
140
+ lines: readonly string[],
141
+ index: number
142
+ ): boolean => {
143
+ const context = lines
144
+ .slice(Math.max(0, index - 4), index)
145
+ .map((line) => stripKotlinLineForSemanticScan(line))
146
+ .join('\n');
147
+ return /@(?:Module|InstallIn|Provides|Binds)\b/.test(context);
148
+ };
149
+
150
+ export const collectAndroidCustomSingletonObjectLines = (source: string): readonly number[] => {
151
+ const lines = source.split(/\r?\n/);
152
+ const matches: number[] = [];
153
+
154
+ lines.forEach((line, index) => {
155
+ const sanitized = stripKotlinLineForSemanticScan(line);
156
+ if (sanitized.trimStart().startsWith('import ')) {
157
+ return;
158
+ }
159
+ const objectMatch = sanitized.match(/^\s*object\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
160
+ const objectName = objectMatch?.[1];
161
+ if (!objectName) {
162
+ return;
163
+ }
164
+ if (androidSafeObjectNamePattern.test(objectName) || isAndroidHiltModuleObjectContext(lines, index)) {
165
+ return;
166
+ }
167
+ if (androidCustomSingletonObjectNamePattern.test(objectName)) {
168
+ matches.push(index + 1);
169
+ }
170
+ });
171
+
172
+ return sortedUniqueLines(matches);
173
+ };
174
+
175
+ export const hasAndroidCustomSingletonObjectUsage = (source: string): boolean =>
176
+ collectAndroidCustomSingletonObjectLines(source).length > 0;
177
+
133
178
  const countTokenOccurrences = (line: string, token: string): number => {
134
179
  return line.split(token).length - 1;
135
180
  };
@@ -1407,3 +1452,25 @@ export const findKotlinLiskovSubstitutionMatch = (
1407
1452
 
1408
1453
  return undefined;
1409
1454
  };
1455
+
1456
+ const legacyFingerprintApiPattern =
1457
+ /\b(?:android\.hardware\.fingerprint\.)?FingerprintManager(?:Compat)?\b/;
1458
+
1459
+ export const hasAndroidLegacyFingerprintApiUsage = (source: string): boolean =>
1460
+ scanCodeLikeSource(source, ({ source: scannedSource, index }) => {
1461
+ const match = scannedSource.slice(index).match(legacyFingerprintApiPattern);
1462
+ return match?.index === 0;
1463
+ });
1464
+
1465
+ export const collectAndroidLegacyFingerprintApiLines = (
1466
+ source: string
1467
+ ): readonly number[] => {
1468
+ const lines: number[] = [];
1469
+ source.split(/\r?\n/).forEach((line, index) => {
1470
+ const sanitized = stripKotlinLineForSemanticScan(line);
1471
+ if (legacyFingerprintApiPattern.test(sanitized)) {
1472
+ lines.push(index + 1);
1473
+ }
1474
+ });
1475
+ return sortedUniqueLines(lines);
1476
+ };
@@ -893,6 +893,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
893
893
  { 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.' },
894
894
  { 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.' },
895
895
  { platform: 'android', pathCheck: isAndroidSourcePath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidAsyncTaskUsage, ruleId: 'heuristics.android.concurrency.asynctask.ast', code: 'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST', message: 'AST heuristic detected deprecated AsyncTask usage in Android production code; use coroutines.' },
896
+ { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidLegacyFingerprintApiUsage, locateLines: TextAndroid.collectAndroidLegacyFingerprintApiLines, ruleId: 'heuristics.android.security.legacy-fingerprint-api.ast', code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST', message: 'AST heuristic detected legacy FingerprintManager API usage; use androidx.biometric.BiometricPrompt.' },
897
+ { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasAndroidCustomSingletonObjectUsage, locateLines: TextAndroid.collectAndroidCustomSingletonObjectLines, primaryNode: (lines) => ({ kind: 'class', name: 'Kotlin object singleton', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: Hilt/Dagger dependency injection boundary', lines }], why: 'Kotlin object singletons create global mutable architecture boundaries that bypass the Android DI graph.', impact: 'Global repositories, services or managers make feature slices harder to test, override and isolate in brownfield remediation.', expected_fix: 'Replace custom Kotlin object singletons with constructor-injected classes provided by Hilt/Dagger. Keep object declarations only for constants, routes, UI metadata or DI modules.', ruleId: 'heuristics.android.architecture.custom-singleton-object.ast', code: 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST', message: 'AST heuristic detected a custom Kotlin object singleton in Android production code; use Hilt/Dagger dependency injection.' },
896
898
  { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinGodActivityUsage, ruleId: 'heuristics.android.architecture.god-activity.ast', code: 'HEURISTICS_ANDROID_ARCHITECTURE_GOD_ACTIVITY_AST', message: 'AST heuristic detected an Android Activity mixing UI entrypoint with product responsibilities; keep Activity thin and move features to composables/ViewModels/use cases.' },
897
899
  { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinApplicationOnCreateHeavyInitializationUsage, ruleId: 'heuristics.android.startup.application-oncreate-heavy-init.ast', code: 'HEURISTICS_ANDROID_STARTUP_APPLICATION_ONCREATE_HEAVY_INIT_AST', message: 'AST heuristic detected heavy library initialization in Application.onCreate; move lazy startup work to AndroidX Startup Initializer or defer it behind the feature boundary.' },
898
900
  { platform: 'android', pathCheck: isAndroidKotlinPath, excludePaths: [isKotlinTestPath], detect: TextAndroid.hasKotlinNonLazyScrollableCollectionUsage, ruleId: 'heuristics.android.compose.non-lazy-scrollable-collection.ast', code: 'HEURISTICS_ANDROID_COMPOSE_NON_LAZY_SCROLLABLE_COLLECTION_AST', message: 'AST heuristic detected a scrollable Column/Row rendering a collection; use LazyColumn/LazyRow for virtualized lists.' },
@@ -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, 39);
6
+ assert.equal(androidRules.length, 41);
7
7
 
8
8
  const ids = androidRules.map((rule) => rule.id);
9
9
  assert.deepEqual(ids, [
@@ -13,6 +13,8 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
13
13
  'heuristics.android.flow.livedata-state-exposure.ast',
14
14
  'heuristics.android.flow.viewmodel-flow-without-statein.ast',
15
15
  'heuristics.android.flow.sharedflow-used-as-state.ast',
16
+ 'heuristics.android.security.legacy-fingerprint-api.ast',
17
+ 'heuristics.android.architecture.custom-singleton-object.ast',
16
18
  'heuristics.android.concurrency.asynctask.ast',
17
19
  'heuristics.android.architecture.god-activity.ast',
18
20
  'heuristics.android.startup.application-oncreate-heavy-init.ast',
@@ -83,6 +85,14 @@ test('androidRules define reglas heurísticas locked para plataforma android', (
83
85
  byId.get('heuristics.android.flow.sharedflow-used-as-state.ast')?.then.code,
84
86
  'HEURISTICS_ANDROID_FLOW_SHAREDFLOW_USED_AS_STATE_AST'
85
87
  );
88
+ assert.equal(
89
+ byId.get('heuristics.android.security.legacy-fingerprint-api.ast')?.then.code,
90
+ 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST'
91
+ );
92
+ assert.equal(
93
+ byId.get('heuristics.android.architecture.custom-singleton-object.ast')?.then.code,
94
+ 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST'
95
+ );
86
96
  assert.equal(
87
97
  byId.get('heuristics.android.concurrency.asynctask.ast')?.then.code,
88
98
  'HEURISTICS_ANDROID_CONCURRENCY_ASYNCTASK_AST'
@@ -115,6 +115,46 @@ export const androidRules: RuleSet = [
115
115
  code: 'HEURISTICS_ANDROID_FLOW_SHAREDFLOW_USED_AS_STATE_AST',
116
116
  },
117
117
  },
118
+ {
119
+ id: 'heuristics.android.security.legacy-fingerprint-api.ast',
120
+ description:
121
+ 'Detects legacy FingerprintManager API usage where androidx.biometric.BiometricPrompt is the supported baseline.',
122
+ severity: 'WARN',
123
+ platform: 'android',
124
+ locked: true,
125
+ when: {
126
+ kind: 'Heuristic',
127
+ where: {
128
+ ruleId: 'heuristics.android.security.legacy-fingerprint-api.ast',
129
+ },
130
+ },
131
+ then: {
132
+ kind: 'Finding',
133
+ message:
134
+ 'AST heuristic detected legacy FingerprintManager API usage; use androidx.biometric.BiometricPrompt.',
135
+ code: 'HEURISTICS_ANDROID_SECURITY_LEGACY_FINGERPRINT_API_AST',
136
+ },
137
+ },
138
+ {
139
+ id: 'heuristics.android.architecture.custom-singleton-object.ast',
140
+ description:
141
+ 'Detects custom Kotlin object singletons in Android production code where Hilt/Dagger dependency injection is expected.',
142
+ severity: 'WARN',
143
+ platform: 'android',
144
+ locked: true,
145
+ when: {
146
+ kind: 'Heuristic',
147
+ where: {
148
+ ruleId: 'heuristics.android.architecture.custom-singleton-object.ast',
149
+ },
150
+ },
151
+ then: {
152
+ kind: 'Finding',
153
+ message:
154
+ 'AST heuristic detected a custom Kotlin object singleton in Android production code; use Hilt/Dagger dependency injection.',
155
+ code: 'HEURISTICS_ANDROID_ARCHITECTURE_CUSTOM_SINGLETON_OBJECT_AST',
156
+ },
157
+ },
118
158
  {
119
159
  id: 'heuristics.android.concurrency.asynctask.ast',
120
160
  description:
@@ -1431,6 +1431,18 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
1431
1431
  'android.flow.state-exposure',
1432
1432
  ['heuristics.android.flow.livedata-state-exposure.ast']
1433
1433
  ),
1434
+ 'skills.android.guideline.android.biometric-auth-biometricprompt-api': heuristicDetector(
1435
+ 'android.security.biometric-prompt',
1436
+ ['heuristics.android.security.legacy-fingerprint-api.ast']
1437
+ ),
1438
+ 'skills.android.guideline.android.no-singleton-usar-inyeccio-n-de-dependencias-hilt-dagger':
1439
+ heuristicDetector('android.architecture.custom-singleton-object', [
1440
+ 'heuristics.android.architecture.custom-singleton-object.ast',
1441
+ ]),
1442
+ 'skills.android.guideline.android.singletons-everywhere-usar-hilt-di': heuristicDetector(
1443
+ 'android.architecture.custom-singleton-object',
1444
+ ['heuristics.android.architecture.custom-singleton-object.ast']
1445
+ ),
1434
1446
  'skills.android.guideline.android.statein-convertir-cold-flow-a-hot-stateflow': heuristicDetector(
1435
1447
  'android.flow.viewmodel-flow-without-statein',
1436
1448
  ['heuristics.android.flow.viewmodel-flow-without-statein.ast']
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.359",
3
+ "version": "6.3.361",
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-19T21:32:21.704Z",
4
+ "generatedAt": "2026-05-24T21:16:11.060Z",
5
5
  "bundles": [
6
6
  {
7
7
  "name": "android-guidelines",