# Hilt/KSP Cross-Package Injection: `error.NonExistentClass`

## Symptom

Kotlin compiles cleanly (no `^e:` errors in Gradle output), but KSP's `InjectProcessingStep` fails:

```
e: [ksp] InjectProcessingStep was unable to process 'ConsumerClass(..., 
error.NonExistentClass, ...)' because 'error.NonExistentClass' could not be resolved.
```

The `error.NonExistentClass` appears at the position of a newly-added `@Inject @Singleton` class in the consumer's constructor. There are **no Kotlin compilation errors** — the class compiles fine, but KSP's symbol database cannot find it.

## Root Cause

KSP processes symbols in a dependency-aware order. When a consumer in package `A` references a newly-created class in package `B`, and KSP encounters the consumer **before** the new class has been compiled into its symbol database, the class shows as `error.NonExistentClass`.

This is most common when:
- The new class is in a DIFFERENT package from the consumer
- The new file was just created (no prior build to establish order)
- Configuration cache holds stale state

## Fix Priority (most reliable first)

### 1. Same-package co-location (best)

Move the new class into the **same package** as the consumer:

```kotlin
// Before: consumer in com.app.data.addon, new class in com.app.data.source
// → KSP error

// After: both in com.app.data.addon
// → builds cleanly
```

**Real example (UNSPOOLED, May 2026):** `DebridEnricher` was created in `com.nexstream.app.data.source` but consumed by `AddonRepository` in `com.nexstream.app.data.addon`. Moving `DebridEnricher` to `com.nexstream.app.data.addon` resolved the error immediately — no other changes needed.

### 2. `dagger.Lazy<>` wrapper

Wrapping the injected dependency in `Lazy` defers the symbol resolution:

```kotlin
// Before (fails):
@Singleton
class AddonRepository @Inject constructor(
    private val enricher: DebridEnricher,  // NonExistentClass
)

// After (often works):
@Singleton
class AddonRepository @Inject constructor(
    private val enricher: dagger.Lazy<DebridEnricher>,
)

// Usage: enricher.get().enrich(...)
```

This helped in some cases but was NOT sufficient for the UNSPOOLED cross-package case — same-package move was still needed.

### 3. Module `@Provides`

Remove `@Inject` from the constructor and provide via a Hilt module:

```kotlin
// Delete @Inject from DebridEnricher
class DebridEnricher(private val debridService: DebridService) { ... }

// Add module:
@Module @InstallIn(SingletonComponent::class)
object DebridEnricherModule {
    @Provides @Singleton
    fun provide(debridService: DebridService): DebridEnricher = DebridEnricher(debridService)
}
```

Warning: having BOTH `@Inject constructor` AND `@Provides` creates duplicate bindings.

### 4. Fresh build (least reliable)

```bash
./gradlew --stop
rm -rf app/build .gradle/configuration-cache
./gradlew :app:compileDebugKotlin --no-configuration-cache
```

Rarely works because the root issue is symbol visibility, not stale caches.

## Detection

If you add a new `@Inject` class and get KSP `NonExistentClass` with zero Kotlin errors:
1. Check if the new class is in a different package than the consumer
2. Count 1 → move to same package
3. Count 0 → check import paths, rebuild fresh

## Prevention

When adding a new injectable class that will be consumed in another package, place it in the **consumer's package** initially. If it grows enough to justify its own package, extract it later — once KSP has processed it across builds, the cross-package reference stabilizes.
