From 32f1b5f964aa4823d626d2f2ec12135b47bef942 Mon Sep 17 00:00:00 2001 From: Jeevanjot19 Date: Sat, 16 May 2026 01:29:57 +0530 Subject: [PATCH 1/2] docs: Add CLAUDE.md for AI agent context (addresses PSMRI/AMRIT#131) --- CLAUDE.md | 358 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..a99262b23 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,358 @@ +# CLAUDE.md — FLW Mobile App + +This file gives AI coding agents (Claude Code, Cursor, Copilot) instant +contextual knowledge of the FLW-Mobile-App codebase so they can assist +without requiring manual context pasting. + +--- + +## What this app is + +The FLW (Field-Level Worker) Mobile App is an Android application built +on the AMRIT platform. It digitises the daily work of ASHA community +health workers across India — household registration, pregnancy and +delivery tracking, child health records, NCD screening, and immunization. + +**Critical constraint:** The app is offline-first. ASHA workers operate +in low-connectivity rural environments. Every feature must work without +internet and sync when connectivity returns. + +--- + +## Tech stack + +| Layer | Technology | +|---|---| +| Language | Kotlin | +| Min SDK | 25 (Android 7.1) | +| Target SDK | 35 | +| Architecture | MVVM + Repository | +| DI | Hilt | +| Database | Room 2.6.1 + SQLCipher (encrypted) | +| Background sync | WorkManager 2.10.2 | +| Notifications | Firebase Cloud Messaging | +| Analytics | Firebase Analytics | +| Concurrency | Kotlin Coroutines + Flow | +| Languages | English, Hindi (values-hi/), Assamese (values-as/) | + +--- + +## Package structure + +``` +org.piramalswasthya.sakhi +├── database/ +│ ├── converters/ # Room TypeConverters +│ ├── room/ +│ │ ├── dao/ # All DAO interfaces +│ │ │ └── dynamicSchemaDao/ +│ │ └── InAppDb.kt # @Database class — single source of truth +│ └── shared_preferences/ +│ └── PreferenceDao.kt # All SharedPreferences access +├── di/ # Hilt modules +├── gamification/ # Gamification engine (sealed events, engine, catalog) +├── helpers/ # Utility classes +├── model/ # All Room @Entity data classes +├── network/ # Retrofit API services +├── repositories/ # Data layer — one repo per domain +├── ui/ +│ ├── home_activity/ # Feature fragments (one folder per feature) +│ │ └── / +│ │ ├── Fragment.kt +│ │ └── ViewModel.kt +│ └── ... +├── work/ # WorkManager workers +└── SakhiApplication.kt +``` + +--- + +## Critical conventions + +### 1. Database (Room) + +**DB class:** `InAppDb` — NOT `AppDatabase`. Located at +`database/room/InAppDb.kt`. Current version: **58**. + +**Never** bump the version without adding a migration. + +**Entity pattern:** +```kotlin +@Entity(tableName = "MY_TABLE") +data class MyCache( + @PrimaryKey + @ColumnInfo(name = "id") + val id: Long, + + @ColumnInfo(name = "myField") + val myField: String, + + // Always use Int for sync state, never Boolean + @ColumnInfo(name = "syncState") + val syncState: Int = 0 // 0=UNSYNCED, 1=SYNCING, 2=SYNCED +) +``` + +**syncState convention:** Always `Int`, never `Boolean isSynced`. +- 0 = UNSYNCED (dirty, needs sync) +- 1 = SYNCING +- 2 = SYNCED + +**Migration pattern — always use guards:** +```kotlin +val MIGRATION_X_Y = object : Migration(X, Y) { + override fun migrate(database: SupportSQLiteDatabase) { + if (!tableExists(database, "MY_TABLE")) { + database.execSQL("CREATE TABLE `MY_TABLE` (...)") + } + if (!columnExists(database, "MY_TABLE", "myColumn")) { + database.execSQL("ALTER TABLE MY_TABLE ADD COLUMN myColumn TEXT") + } + } +} +``` + +`tableExists()` and `columnExists()` are companion object functions on +`InAppDb`. Always use them — never assume a table or column exists. + +**Adding a new entity checklist:** +1. Create `model/MyCache.kt` with `@Entity` +2. Create `database/room/dao/MyDao.kt` with `@Dao` +3. Add `MyCache::class` to entities list in `@Database` annotation in `InAppDb` +4. Add `abstract val myDao: MyDao` to `InAppDb` +5. Bump version by 1 +6. Add `MIGRATION_X_Y` with `tableExists` guard +7. Register migration in `builder.addMigrations(...)` +8. Add Hilt provider in appropriate `di/` module + +--- + +### 2. DAO pattern + +```kotlin +@Dao +interface MyDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsert(entity: MyCache) + + @Query("SELECT * FROM MY_TABLE WHERE id = :id") + suspend fun getById(id: Long): MyCache? + + // Use Flow for observation — never LiveData + @Query("SELECT * FROM MY_TABLE WHERE syncState = 0") + fun observeUnsynced(): Flow> + + @Query("UPDATE MY_TABLE SET syncState = 2 WHERE id = :id") + suspend fun markSynced(id: Long) +} +``` + +--- + +### 3. Repository pattern + +```kotlin +@Singleton +class MyRepo @Inject constructor( + private val myDao: MyDao, + private val apiService: AmritApiService, + private val prefDao: PreferenceDao +) { + // All DB calls — no context switch needed, Room handles threading + suspend fun getById(id: Long) = myDao.getById(id) + + // All network calls — always switch to IO dispatcher + suspend fun syncToServer(): NetworkResult { + return withContext(Dispatchers.IO) { + try { + val response = apiService.myEndpoint(...) + if (response.isSuccessful) NetworkResult.Success(...) + else NetworkResult.Error(response.code(), ...) + } catch (e: IOException) { + NetworkResult.Error(-1, "Unable to connect to Internet!") + } catch (e: SocketTimeoutException) { + NetworkResult.Error(-3, "Request Timed out! Please try again!") + } + } + } +} +``` + +--- + +### 4. ViewModel pattern + +```kotlin +@HiltViewModel +class MyViewModel @Inject constructor( + private val myRepo: MyRepo +) : ViewModel() { + + // Use StateFlow, never LiveData + val myData: StateFlow = myRepo + .observeMyData() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), null) + + fun doSomething() { + viewModelScope.launch { + myRepo.doSomething() + } + } +} +``` + +--- + +### 5. Fragment pattern + +```kotlin +@AndroidEntryPoint +class MyFragment : Fragment() { + + private val viewModel: MyViewModel by viewModels() + private var _binding: FragmentMyBinding? = null + private val binding get() = _binding!! + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?): View { + _binding = FragmentMyBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.myData.collect { data -> render(data) } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null // Always null binding to prevent memory leaks + } +} +``` + +--- + +### 6. WorkManager worker pattern + +```kotlin +@HiltWorker +class MySyncWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted params: WorkerParameters, + private val myRepo: MyRepo +) : CoroutineWorker(context, params) { + + override suspend fun doWork(): Result { + return try { + // do sync + Result.success() + } catch (e: CancellationException) { + throw e // Always rethrow CancellationException + } catch (e: Exception) { + if (runAttemptCount < 3) Result.retry() else Result.failure() + } + } + + companion object { + fun schedule(workManager: WorkManager) { + workManager.enqueueUniquePeriodicWork( + "MySyncWorker", + ExistingPeriodicWorkPolicy.KEEP, + PeriodicWorkRequestBuilder(6, TimeUnit.HOURS) + .setConstraints(Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build()) + .build() + ) + } + } +} +``` + +--- + +### 7. Getting the logged-in user + +```kotlin +// Inject PreferenceDao and call: +val user = preferenceDao.getLoggedInUser() // returns User? +val userId = user?.userId // Int +val userName = user?.userName // String +``` + +Never access SharedPreferences directly — always use `PreferenceDao`. + +--- + +### 8. API 25 compatibility rules + +- **Never use** `java.time.*` — requires API 26. Use `java.util.Calendar` + `SimpleDateFormat` +- **Never use** `paddingHorizontal`/`paddingVertical` XML attributes — use `paddingStart`/`paddingEnd`/`paddingTop`/`paddingBottom` +- **Never use** `ThreadLocal.withInitial {}` — use `object : ThreadLocal() { override fun initialValue() = ... }` + +--- + +### 9. Localization + +All user-facing strings must exist in three files: +- `res/values/strings.xml` — English +- `res/values-hi/strings.xml` — Hindi +- `res/values-as/strings.xml` — Assamese + +Never hardcode user-visible text in Kotlin or XML layout files. + +--- + +### 10. What NOT to do + +- Do not use `LiveData` — the project uses `Flow` + `StateFlow` +- Do not use `kotlin-android-extensions` (deprecated) — use ViewBinding +- Do not bump DB version without a migration +- Do not mark records as `syncState = 2` (SYNCED) before confirmed API success +- Do not access SharedPreferences directly — use `PreferenceDao` +- Do not use `java.time.*` — minSdk 25 + +--- + +## Health domain context + +The app tracks: +- **Beneficiaries** — registered via `BenRegCache`, identified by `beneficiaryId: Long` +- **Households** — registered via `HouseholdCache`, identified by `householdId: Long` +- **Pregnant women** — `PregnantWomanRegistrationCache`, ANC via `PregnantWomanAncCache` +- **Delivery outcomes** — `DeliveryOutcomeCache` +- **Children** — `ChildRegCache`, `InfantRegCache` +- **Immunization** — `ImmunizationCache` +- **HRP (High Risk Pregnancy)** — `HRPPregnantAssessCache`, `HRPPregnantTrackCache` +- **NCD screening** — via dynamic forms +- **CBAC** — Community Based Assessment Checklist + +The `BenBasicCache` is a Room `@DatabaseView` that joins most of these +into a single queryable view used by the main beneficiary list screen. + +--- + +## Gamification module (added in v58) + +Located at `gamification/`. Consists of: +- `GamificationEvent` — sealed class of health worker actions +- `GamificationEngine` — processes events, awards XP, updates streaks, unlocks badges +- `BadgeCatalog` — trilingual badge definitions + +To trigger gamification from a health form after a successful save: +```kotlin +gamificationViewModel.onHealthEvent( + userId, + GamificationEvent.AncVisitCompleted(benId.toString()) +) +``` + +Available events: `HouseholdRegistered`, `BeneficiaryRegistered`, +`AncVisitCompleted`, `DeliveryOutcomeRecorded`, `PncVisitCompleted`, +`ImmunizationRecorded`, `HrpCaseIdentified`, `NcdScreeningCompleted`, +`CbacFormFilled`, `DailyLogin`. From cfa056717ba749b626f6831bb6daa8a68c0b7213 Mon Sep 17 00:00:00 2001 From: Jeevanjot19 Date: Sat, 16 May 2026 20:02:28 +0530 Subject: [PATCH 2/2] docs: Add CLAUDE.md for AI agent context (addresses PSMRI/AMRIT#131) --- CLAUDE.md | Bin 10992 -> 22702 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index a99262b236fac3bdeee9da72d576f52e52c49c00..cfd875caae4d44b55f225ca6f226b747ab61b899 100644 GIT binary patch literal 22702 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 literal 10992 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