Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 82 additions & 21 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,91 @@
1. Code Style
## 1. Code Style

* Code follows general Kotlin code style recommendations.
* Use Android Jetpack ViewModel for managing UI-related data and use viewModelScope for coroutines.
* Use LiveData for observing and updating UI state. Use Custom event wrappers `LiveDataEventWithContent` to handle one-time events.
* Use delegates like `FragmentViewBindingDelegate` to simplify view binding in fragments.
* Use Android Jetpack ViewModel for managing UI-related data and use `viewModelScope` for coroutines.
* Use `LiveData` for observing and updating UI state. Use custom event wrappers `LiveDataEventWithContent` and `LiveDataEvent` (in `infra/core`) to handle one-time events.
* Use `FragmentViewBindingDelegate` (`by viewBinding(...)`) to simplify view binding in fragments.
* All UI is View-based (XML layouts + ViewBinding).
* Use `internal` visibility for module-internal classes (fragments, view models, helpers) to enforce module boundaries. Only `Contract` objects and public API types should be `public`.
* Use `Simber` (in `infra/logging`) for logging instead of `Log` or `println`.

2. Architecture Layers:
## 2. Architecture Layers

* Feature Modules: Manages UI components and ViewModels as well as UI-dependant features. Implemented in `:feature` subfolder.
* Infra Modules: Contains use cases and core business logic. Implemented in `:infra` subfolder.
* Modality Modules: Contain business subsets of feature and infra modules related to a specific biometric modality. Implemented in `:face`
and `:fingerprint` subfolder.
* Each layer is isolated. Feature modules depend on other features and infra modules, infra modules only depend on other infra modules.
* **Feature Modules** (`:feature:*`): UI components, Fragments, ViewModels, and UI-specific use cases. Applied via `simprints.feature` convention plugin.
* **Infra Modules** (`:infra:*`): Core business logic, repositories, use cases, data layer. Applied via `simprints.infra` convention plugin.
* **Modality Modules** (`:face:*`, `:fingerprint:*`): Biometric modality-specific subsets of feature and infra modules.
* **App Module** (`:id`): Application entry point with Firebase, Crashlytics, and Play Store deployment config.
* Each layer is isolated. Feature modules depend on other features and infra modules; infra modules only depend on other infra modules.

3. Dependency Injection:
## 3. Module Communication & Navigation

* Use Dagger Hilt for dependency injection with the `@Inject` constructor as the preferred method of injection.
* Use Singleton scope (`@Singleton`) for critical dependencies.
* ViewModels are injected using Hilt (`@HiltViewModel`).
* Each feature module exposes a public `Contract` object (e.g., `ConsentContract`, `LoginContract`) defining:
* `DESTINATION` — the navigation destination ID.
* `getParams(...)` — a factory method to create type-safe navigation arguments.
* Each feature module has its own navigation graph in `src/main/res/navigation/graph_*.xml`.
* Use Jetpack Navigation with SafeArgs (auto-applied by `simprints.feature` plugin).
* Fragment-level result handling supports cross-navigation-graph communication via `SavedStateHandle` with `FragmentResultListener` fallback for process death resilience.
* The orchestrator (`feature/orchestrator`) acts as a central coordinator, dynamically navigating to feature graphs and handling results based on a step-based state machine.
* Use `navigateSafely()` for navigation, `finishWithResult()` to return results, and `handleResult<T>()` to receive results. All defined in `infra/ui-base/navigation/NavigationResultExt.kt`.

4. Unit testing rules:
## 4. Build System

* Use JUnit 4 for unit testing, MockK for mocking, Google Truth for assertions, AndroidX Test for Android-specific testing.
* Convention plugins are defined in `build-logic/convention/` and applied via plugin IDs like `simprints.feature`, `simprints.infra`, `simprints.library.room`, `simprints.library.hilt`, etc.
* Version catalog is in `gradle/libs.versions.toml`.
* Build types: `debug`, `staging` (minified), `release` (minified, `@StopShip` lint fatal).
* Room modules use SQLCipher encryption controlled by `DB_ENCRYPTION` build config.

## 5. Dependency Injection

* Use Dagger Hilt for dependency injection with `@Inject` constructor as the preferred method.
* All Hilt modules use `@InstallIn(SingletonComponent::class)`. Use `@Singleton` for critical dependencies.
* ViewModels are annotated with `@HiltViewModel` and injected via `@Inject constructor`.
* Use `@Binds` (abstract module) for interface-to-implementation bindings. Use `@Provides` (object module) for factory methods.
* Custom dispatcher qualifiers: `@DispatcherIO`, `@DispatcherMain`, `@DispatcherBG` for coroutine scopes.

## 6. Serialization

* Use `kotlinx.serialization` with the global `SimJson` instance (in `infra/serialization`).
* Domain models use `@Serializable` annotations with compile-time polymorphic serialization.
* Use `@SerialName` for custom JSON field names. When referencing companion object constants in `@SerialName`, use direct reference (e.g., `BiometricReferenceType.FACE_REFERENCE_KEY`) without `.Companion`.

## 7. Data Persistence

* **Room**: Used for events, logs, image metadata, and enrolment records. Modules apply `simprints.library.room` plugin.
* **Realm**: Deprecated database for enrolment record storage (`infra/enrolment-records/realm-store`). Will be removed in a future release.
* **DataStore/Protobuf**: Used for config and auth stores via `simprints.library.protobuf` plugin.

## 8. Network

* Retrofit with `kotlinx.serialization` converter, OkHttp client with retry logic (5 attempts default, exponential backoff).
* API calls return `ApiResult<T>` (sealed class with `Success`/`Failure`) from `infra/backend-api`.
* All network calls are `suspend` functions. Use `withContext(Dispatchers.IO)` for network operations.
* `SimCoroutineWorker` (in `infra/core`) is the base class for WorkManager workers, providing structured `retry()`, `fail()`, `success()` result helpers and foreground notification support.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is in the wrong section, but not sure which one would be more appropriate.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No other suitable one so will leave it here - models will get it anyway.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe under architecture?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's strictly about layers and the bigger picture.


## 9. Error Handling

* Use `ApiResult<T>` sealed class for network error handling with `getOrThrow()` and `getOrMapFailure()`.
* Specific exceptions: `BackendMaintenanceException`, `NetworkConnectionException`, `RetryableCloudException`.
* Network-related exceptions (timeout, SSL, unknown host) are downgraded from error to info severity in `Simber` to reduce Crashlytics noise.

## 10. Coroutines

* Use `viewModelScope` for ViewModel coroutines. Use `runTest { ... }` for coroutine tests.
* Functions returning `Flow` should **not** be marked `suspend` — expose them as plain functions returning `Flow`.
* Use `StateFlow` and `SharedFlow` for reactive state in ViewModels.

## 11. Unit Testing

* Use JUnit 4, MockK for mocking, Google Truth for assertions, AndroidX Test and Robolectric for Android-specific testing.
* Test classes use the `*Test` suffix. Test methods use descriptive names with backticks.
* Setup logic is in `@Before` methods. Tests use `@RunWith(AndroidJUnit4::class)` where appropriate. Coroutine tests use `runTest { ... }`.
* Use Given-When-Then structure. Keep tests concise and focused. Use relaxed mocks to reduce boilerplate.
* Use helper methods for mock objects or test data.
* Use lifecycle-aware and coroutine testing where appropriate.
* Isolate Android framework dependencies unless required.
* Setup logic is in `@Before` methods. Tests use `@RunWith(AndroidJUnit4::class)` where appropriate.
* Use Given-When-Then structure. Keep tests concise and focused. Use `@RelaxedMockK` / relaxed mocks to reduce boilerplate.
* Use `TestCoroutineRule` (in `infra/test-tools`) — sets `UnconfinedTestDispatcher` as `Dispatchers.Main`.
* Use `InstantTaskExecutorRule` for synchronous LiveData execution in tests.
* LiveData assertions: `assertEventReceived()`, `assertEventReceivedWithContent()`, `getOrAwaitValue()` from `infra/test-tools`.
* Use `@TestInstallIn` with fake Hilt modules for DI replacement in tests.
* Fragments, Activities, and Hilt Modules are excluded from SonarCloud coverage via project-level settings. Use `@ExcludedFromGeneratedTestCoverageReports("UI class")` to exclude other UI-adjacent classes (adapters, view helpers, etc.).

## 12. Event System

* The project uses a custom event system (`infra/events`) to track user actions, biometric captures, matches, and workflow steps as structured, auditable events.
See [event-system.md](.github/event-system.md) for details, including event hierarchy, scopes, persistence, sync, and tokenization.
41 changes: 41 additions & 0 deletions .github/event-system.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
## Event System

### Event Hierarchy

* `Event` is a **sealed class** with 60+ concrete subtypes (e.g., `AuthenticationEvent`, `FaceCaptureEvent`, `EnrolmentEventV4`, `OneToOneMatchEvent`).
* Each `Event` has: `id`, `type: EventType` (enum discriminator), `payload: EventPayload`, `scopeId`, and `projectId`.
* `EventPayload` is a sealed base class with: `type`, `eventVersion: Int` (for schema versioning), `createdAt`, `endedAt`, and `toSafeString()` for safe logging.
* Each concrete event defines its own nested `Payload` data class extending `EventPayload`.
* Serialization uses **manual polymorphic dispatch** via `concreteSerializer` instead of Kotlinx's built-in discriminator, to avoid collision with the business `type` field.

### Event Scopes

* Events are grouped into **scopes** via `EventScope` — a container with metadata about the session (app version, device info, language, modalities, project config).
* `EventScopeType` enum: `SESSION` (user workflow), `UP_SYNC`, `DOWN_SYNC`, `SAMPLE_UP_SYNC`.
* Scopes have a lifecycle: created with `createEventScope()`, ended with `closeEventScope()` which sets `endedAt` and `endCause` (`WORKFLOW_ENDED` or `NEW_SESSION`).
* Events reference their scope via `scopeId`.

### Adding Events

* Use `SessionEventRepository.addOrUpdateEvent(event)` to add an event to the current session (auto-scopes).
* Use `EventRepository.addOrUpdateEvent(scope, event)` for explicit scope association.
* Events are validated before saving (e.g., duplicate GUID checks).
* Use @SessionCoroutineScope for adding or updating events and sessions
* Feature modules usually create events in use cases (e.g., `AddCaptureEventsUseCase`) and call the repository to persist them.

### Local Storage

* Events are stored in Room as `DbEvent` with the full event serialized as JSON in `eventJson`.
* Scopes are stored as `DbEventScope` with metadata in `payloadJson`.

### Event Sync

* **Upload**: `EventUpSyncTask` loads closed scopes + events, tokenizes sensitive fields, maps domain models to API models (`ApiEvent`, `ApiEventScope`), and uploads in batch.
* **Download**: `SimprintsEventDownSyncTask` fetches remote events and creates local enrolment records.
* API models (`ApiEvent`, `ApiEventScope`, `ApiEventPayloadType`) are separate from domain models; mapping is done via `MapDomainEventScopeToApiUseCase` and `MapDomainEventToApiUseCase`.

### Tokenization (Sensitive Data)

* Sensitive fields (e.g., `attendantId`, `moduleId`, `userId`) use `TokenizableString` — a sealed class with `Raw` and `Tokenized` subtypes.
* Events declare tokenizable fields via `getTokenizableFields()` and accept encrypted values via `setTokenizedFields()`.
* `TokenizeEventPayloadFieldsUseCase` encrypts raw fields before upload. The `tokenizedFields` list on `ApiEvent` documents which JSON paths contain encrypted data.