# CoreApp

Kotlin Multiplatform / Compose Multiplatform app targeting Android and iOS. This is the **public** repository; an **internal** repository mirrors the same module layout at the root (no `public/` prefix). Both build from the root `./gradlew`.

## Overview

- App supports modern Pebble/Core watches over BLE, exposing features like notifications, health data, watchfaces, and more.
- 'Index 01' AKA 'Ring' is a new ring device to record notes and ingest into the 'Index AI'. The 'experimental' module is dedicated to ring device features.
- The ring is not 'always-on' like a watch so is scanned for continuously.

## Critical Rules

- **NEVER use `emulator -wipe-data`**. This destroys all user data (Google account, installed apps, app logins) with no recovery. If you need a fresh emulator, create a new AVD instead.

- **Iterating on iOS — swap the framework binary, don't rebuild Pebble.app.** A full xcodebuild from scratch takes ~30 min. After the first successful build of `Pebble.app`, every subsequent Kotlin source change only needs:
  1. `./gradlew :composeApp:linkPodDebugFrameworkIosSimulatorArm64` — relinks the K/N framework into `public/composeApp/build/bin/iosSimulatorArm64/podDebugFramework/ComposeApp.framework/`
  2. `cp` the new `ComposeApp` binary over `public/iosApp/build/Build/Products/Debug-iphonesimulator/Pebble.app/Frameworks/ComposeApp.framework/ComposeApp`
  3. `codesign --force --sign - --preserve-metadata=identifier,entitlements,flags --timestamp=none` on the framework, then on `Pebble.app` itself
  4. `xcrun simctl uninstall` + `install` + `launch` — no xcodebuild needed
  Only run a full `xcodebuild` again when you change Swift code, Info.plist, resources, or pod dependencies.

## Platform Rules

- **All features must work on both Android and iOS.** Write shared code in `commonMain` whenever possible. If a feature cannot be implemented on one platform, alert the user before proceeding.
- **Do NOT modify the Ring recording processing pipeline.** The Ring Bluetooth audio capture, preprocessing, transcription, and agent processing flow works correctly. New audio input sources (e.g., phone mic) must massage their output into the format the existing pipeline expects (16kHz, PCM_16BIT, mono, raw) and feed into `queueLocalAudioProcessing(fileId)`.

## Repository layout

Most source lives under `public/`. The root `settings.gradle.kts` includes each module and remaps its `projectDir` into `public/<module>`:

- `:composeApp` — main Android/iOS app entry point (Compose UI, Firebase, Cocoapods, Koin DI). Android `applicationId` is `coredevices.coreapp`.
- `:libpebble3` — KMP library for talking to Pebble/Core watches (BLE, protocol, services, endpoint managers). Mirrored from a standalone repo via `scripts/pull-from-public-libpebble-local.sh` / `push-public-to-libpebble-local.sh`.
- `:pebble` — Pebble-related shared code used by the app.
- `:experimental` — newer/experimental device features (e.g. ring); see `coredevices.ring`.
- `:util` — shared utilities (logging, IO, etc.).
- `:mcp`, `:index-ai`, `:libindex` — AI/MCP-related modules.
- `:cactus`, `:resampler`, `:krisp-stubs` — audio/ML support modules. Real Krisp models live in `krispmodels/` (gated by `ENABLE_KRISP` in `gradle.properties`).
- `:blobannotations`, `:blobdbgen` — KSP annotations + code generator for Pebble blobdb.
- `:desktopAITool` — root-level standalone tool (not under `public/`).

iOS app project: `public/iosApp/iosApp.xcworkspace` (always open the `.xcworkspace`, not `.xcodeproj`).

## Build basics

- Gradle wrapper at the root: `./gradlew` (the `public/gradlew` exists for the standalone public build, but the canonical entry point in this checkout is the root one).
- JDK 17 required. JVM target is 17 across modules.
- Version catalog: `public/gradle/libs.versions.toml`, exposed as both `libs` and `corelibs` (see `defaultLibrariesExtensionName` in `settings.gradle.kts`).
- Android: `./gradlew :composeApp:assembleDebug` / `assembleRelease`. Needs `composeApp/src/google-services.json` (a dummy is committed alongside).
- iOS: `./gradlew podInstall`, then build from Xcode against `public/iosApp/iosApp.xcworkspace`. The root `build.gradle.kts` copies `GoogleService-Info.plist` into `public/iosApp/iosApp/` before configuration.
- The iOS framework is named `ComposeApp` and is wired up via the Kotlin Cocoapods plugin in `public/composeApp/build.gradle.kts`.

## Configuration / secrets

- `local.properties` at the repo root holds `github.username` / `github.token` (used to pull `coredevices/krisp-kmp` from GitHub Packages) and optional flags like `LOCAL_RELEASE_BUILD=true` and `USE_MAVEN_LOCAL=true`.
- Tokens for Mixpanel, Memfault, Google sign-in, Cactus, etc. live in `gradle.properties`.
- Release signing reads `RELEASE_KEYSTORE_PASSWORD` / `RELEASE_KEYSTORE_ALIAS` / `RELEASE_KEY_PASSWORD` from the environment (skipped when `LOCAL_RELEASE_BUILD=true`).

## General editing info

- When adding a module, include it in the root `settings.gradle.kts` and remap `projectDir` to `file("public/<name>")` so it builds from this repo. The internal repo's settings file references the same names without the `public/` prefix.
- Source layout per module follows standard KMP: `src/commonMain/kotlin`, `src/androidMain/kotlin`, `src/iosMain/kotlin`, plus `commonTest` / `androidUnitTest` / `androidInstrumentedTest`.
- Compose resources are generated under the package `coreapp.composeapp.generated.resources`.
- `versionCode` is derived from git commit count (`versioning.getVersionCode()`); `versionName` requires a git tag (e.g. `git tag 1.0.0`) or falls back to `"unknown"`.
- DI is Koin (`koin-core`, `koin-compose`, `koin-compose-viewmodel`); navigation uses `androidx.navigation.compose`; logging uses Kermit; HTTP is Ktor (OkHttp on Android, Darwin on iOS).
- Some dependencies are internally developed and published. You can still ask for the source code of these dependencies to be included in the session if you need more context but don't try looking for it in the filesystem.

## Guidance

- Project follows DRY principles; attempt to use shared code and split out to platform-specific when required or more performant. The `util` module can be used for potentially reusable generic utilities, even if it isn't reused now.
- Changes should reuse existing code where possible and loosely follow clean coding principles, this is a big project which requires production-level maintainable code and good separation of concerns to stay manageable.
- Write tests for new logic, and consider test coverage when modifying existing code. Try to stick to unit tests where possible.

## Code Review Guidelines (from crc-32 feedback)

These rules come from real review feedback on PRs #17–#32. Follow them to avoid repeated mistakes.

### 1. Minimal fixes only — no scope creep

Make the smallest change that fixes the problem. Do not bundle extra "improvements," retry logic, or defensive code alongside a root-cause fix. If a bug is a one-line fix, the PR should be a one-line fix. Reviewers will extract the minimal fix and discard the rest.

- **Bad:** Fixing a stale token bug (`authStateChanged` → `idTokenChanged`) + adding retry loops + adding token refresh in UI layer. Reviewer merged only the one-line fix.
- **Bad:** Fixing a dispatcher issue (`Dispatchers.IO` → `Dispatchers.Default`) + restructuring file writes to be async fire-and-forget. Reviewer took only the dispatcher change.

### 2. Use existing abstractions at the correct scope — don't reinvent

Before writing new code, search for existing services, injectables, and utilities that already do what you need. This codebase uses dependency injection extensively. If something feels like it should exist, it probably does. When adding state, put it at the correct DI scope — per-watch state belongs in per-watch services, not in singletons.

- **Bad:** Manually querying DAOs and iterating trace sessions to find the right one to append to. The injectable `RingTraceSession` already tracks the current session.
- **Bad:** Writing temp files for audio playback. In-memory `MediaDataSource` (Android) avoids temp file management entirely.
- **Bad:** Putting per-watch timezone tracking state in `LibPebble3` (singleton). It belongs in `SystemService` (per-watch). Reviewer moved it.

### 3. Understand root causes before fixing

Do not apply brute-force workarounds to symptoms. Understand why something is happening before changing it.

- **Bad:** Removing `LookaheadScope` to "fix" an animation. The actual bug was a Compose recomposition where bounds changed from 0 to full width on first compose. The "fix" just made the UI blink jarringly instead.
- **Bad:** Setting `disableNextTransitionAnimation = true` by default. This disabled ALL page transitions, not just the problematic one.

### 4. Distinguish transient vs. deterministic failures

Do not mark deterministic failures as recoverable/retryable. If a transcription model can't handle audio, retrying won't change the outcome — it just holds up the processing queue for other recordings.

- **Rule:** Network errors = transient (retry). Model/processing errors = deterministic (fail permanently).

### 5. Never blur state boundaries for marginal performance

If the system defines clear states (transferring → complete), do not introduce async operations that make a recording appear "complete" while files are still being written. "It'll be fast enough" is not a correctness argument.

- **Bad:** Fire-and-forget async file write after marking recording complete. A user could see a transcription-failed file with no playable audio, or a bug report could be missing its audio attachment.

### 6. Use the existing permission-nag UI — don't add manual permission requests

Most code runs in background services where requesting permissions isn't possible. The app already has a UI that nags users about missing permissions. Don't add manual `requestPermission()` calls in feature code — rely on the existing permission flow and handle the denied case gracefully.

### 7. Verify iOS builds compile before submitting

Do not submit PRs with iOS code that hasn't been verified to compile. Multiple iterative "fix iOS compilation" commits signal the code was written blind. Validate cinterop imports, NSData handling, and platform-specific APIs against the actual iOS build.

### 8. Keep PRs single-purpose

Each PR should address one concern. Do not bundle animation fixes + trace timeline + retry UI + error handling changes into one PR. Multi-concern PRs are harder to review and get closed as "rewritten" when any part is wrong.

### 9. Include tests for new logic

Non-trivial new logic (parsers, encoders, state machines) must have unit tests. When Alice rewrote the M4A feature, she included 324 lines of tests for the M4A parser. The original PR had zero.

### 10. Prefer simple architectures over clever ones

When adding a new format/encoding, prefer keeping the local storage format unchanged and converting at the boundary (upload/download). Don't introduce MIME-type dispatch, dual playback paths, or multi-step file replacement schemes when you can encode on upload and decode on download.

## GitHub Self-Hosted Runner

See [GITHUB_RUNNER.md](GITHUB_RUNNER.md) for the full runner management guide.

## Useful references

- Public-facing setup steps (iOS prerequisites, Firebase, signing): `public/README.md`.
- Contribution / licensing: `public/CONTRIBUTING.md`, `public/LICENSE`, `public/LICENSE-COMMERCIAL`.
- CI helpers: `ci/`, `scripts/`, `GITHUB_RUNNER.md`, `GITHUBRUNNER.md`.
