# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Git Workflow

**Never commit or push changes unless explicitly asked.** Stage and review only. The only exception is the Dependabot resolution process in `docs/dependabot-workflow.md`, which pre-authorizes mechanical auto-fix commits/pushes and allows manual fix commits/pushes after the documented review path is followed.

## Key Commands

```bash
# Build
./gradlew assembleDebug

# JVM tests (fast — use these for most development)
./gradlew test
./gradlew testDebugUnitTest --tests "com.yorvana.path.ClassName.testMethodName"

# Screenshot tests
./gradlew recordRoborazziDebug   # update baselines
./gradlew verifyRoborazziDebug   # verify against baselines

# Instrumented tests (requires KVM; Pixel 2 API 33 via GMD)
./gradlew cleanManagedDevices    # always run before changing GPU mode
./gradlew pixel2api33Check

# Full check with coverage
./gradlew verifyWithCoverage

# Lint + code style
./gradlew lintDebug ktlintCheck detekt
```

**GPU mode for instrumented tests** (set in `gradle.properties`):
- `host` — fastest, requires KVM + GPU passthrough
- `angle_indirect` — current local default; works on kernel ≥ 6.17 (Fedora 43)
- `swiftshader_indirect` — CI default; crashes on kernel ≥ 6.17

## Architecture

MVVM + Repository. Single-activity (`MainActivity`), Jetpack Compose UI, **no DI framework** — all dependencies are wired manually in `YorvanaApplication`.

```
com.yorvana/
├── YorvanaApplication.kt   # manual DI; all deps created here as public properties
├── data/
│   ├── model/                  # plain Kotlin data classes (Vehicle, ServiceRecord, etc.)
│   ├── storage/                # VaultStorageImpl — SAF/DocumentFile file I/O
│   └── repository/             # interface + *Impl for Vehicle, Record, Category
├── domain/                     # cross-layer models: VehicleWithMeta, RecordWithCategory
└── ui/
    ├── Screen.kt / AppNavGraph.kt   # Navigation 3 routing
    └── {feature}/               # Composables + ViewModel per feature
```

**Vault layout** (user's filesystem, not app-private):
```
Yorvana/
├── categories.json
└── vehicles/{slug}/
    ├── vehicle.json            # includes denormalized recordCount + lastServiceDate
    └── records/{uuid}.json
```

Vehicle list loads are O(N vehicles) by design — record counts are denormalized into `vehicle.json` (see ADR-008).

## Test Conventions

| Suffix | Location | Runner | Use for |
|--------|----------|--------|---------|
| `*Test.kt` | `src/test` | JVM (Robolectric) | Unit + UI tests |
| `*IT.kt` | `src/test` | JVM (Robolectric) | Multi-component integration |
| `*ScreenshotTest.kt` | `src/test` | JVM (Roborazzi) | Visual regression |
| `*IT.kt` / `*Test.kt` | `src/androidTest` | Real emulator (GMD) | SAF, UiAutomator, real Activity |

All Robolectric tests inherit `NATIVE` graphics mode globally (set in `app/build.gradle.kts`). Don't add `@GraphicsMode` unless overriding.

A test **must** stay in `src/androidTest` if it uses `ActivityScenario`, `createAndroidComposeRule`, a real `ContentResolver`/`DocumentFile`, or `InstrumentationRegistry`. If it only *mocks* those, move it to `src/test`.

**ViewModel state** is driven by events (use `onEvent()`); `StateFlow` has no public setter. In VMs using `WhileSubscribed` or `combine` (like `SettingsViewModel`), internal logic MUST read state via `_state.value` because the public `state.value` may return the initial value if there are no active collectors (e.g., in a unit test before calling `collect`).

## Key Documentation

- `GEMINI.md` — project overview and tech stack
- `TESTING_SETUP.md` — detailed GMD/GPU setup and Robolectric migration checklist
- `docs/dependabot-workflow.md` — Dependabot resolution process for agents
- `docs/adr.md` — architectural decisions (ADR-001 through ADR-013)
- `docs/specs.md` — functional requirements and acceptance criteria
