Repository navigation
Expand file tree
/
Copy pathprd.json
More file actions
46 lines (46 loc) · 8.66 KB
/
Copy pathprd.json
File metadata and controls
46 lines (46 loc) · 8.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"project": "inventory-android",
"branchName": "main",
"description": "Deeper follow-up audit (round 2) after the 2026-07-19 pass that fixed BUG-1..3 (logcat credential leak, backup crash-loop, repository cache race). Scope: quality-tier (P3-P4) and integration/edge-case categories the first pass did not exhaustively sweep - coroutine/ViewModel scope leaks, deprecated APIs, further edge cases in state handling. Each story is find-and-fix within one category, not a fixed bug list: audit that category first, write a failing test per confirmed defect (JVM unit test unless the defect is only observable instrumented), fix minimally, then mark the story passing. If a category turns up nothing after genuine audit effort, mark passes true with notes explaining what was checked and why nothing qualified - do not fabricate a fix.",
"userStories": [
{
"id": "US-001",
"title": "Coroutine scope leaks and unhandled cancellation in ViewModels/repositories",
"description": "As a user navigating rapidly between screens, in-flight coroutines from a destroyed ViewModel must not leak, crash, or silently corrupt state in a still-alive singleton (e.g. HierarchyStore, the repository caches touched by BUG-3).",
"acceptanceCriteria": [
"Audit every ViewModel's viewModelScope usage and HierarchyStore's coroutine launches for missing CancellationException rethrow or scope confinement",
"Any confirmed leak/race gets a regression test and a minimal fix (scope confinement, proper cancellation propagation)",
"Full JVM unit test suite, ktlintCheck, and detekt stay clean"
],
"priority": 1,
"passes": true,
"notes": "Audited every ViewModel viewModelScope.launch and HierarchyStore/singleton coroutine launches. HierarchyStore itself is safe: own SupervisorJob+Dispatchers.IO scope, single tracked activeJob cancelled per new refresh(), and already rethrows CancellationException before writing state. Found real bugs in the runCatching{}.onFailure/.fold idiom used throughout ViewModels: several call sites caught the Result without checking for CancellationException first, so a coroutine cancelled via viewModelScope (ViewModel torn down / superseded request) would still run its onFailure branch and write stale error state after cancellation - violating structured concurrency. Fixed by adding the same result.exceptionOrNull()?.let { if (it is CancellationException) throw it } guard already used correctly elsewhere (DrawerViewModel, ShelvesViewModel, StorageOverviewViewModel, ProductsViewModel, HouseholdsViewModel, MembersViewModel) to: ProductDetailViewModel (load/save/increment/decrement/uploadImage/delete), SearchViewModel (doSearch), JoinHouseholdViewModel (join), AuthViewModel (loginWithGoogle/loginWithGoogleCode/submit), ForgotPasswordViewModel (submit), InviteViewModel (load). Added a regression test (SearchViewModelTest.cancelling_the_view_model_scope_mid_search_never_writes_an_error) that cancels the ViewModel scope via ViewModelStore.clear() while a search is suspended mid-flight; verified red on the pre-fix code (errorRes got stamped with error_search_failed after cancellation) and green after the fix. Full JVM test suite, ktlintCheck and detekt all pass."
},
{
"id": "US-002",
"title": "Deprecated Android/Compose/AndroidX API usage",
"description": "As a maintainer targeting compileSdk/targetSdk 36, the app should not call deprecated APIs that will break or warn loudly on the next AGP/Compose upgrade.",
"acceptanceCriteria": [
"Search for @Deprecated-annotated calls in app/src/main (Compose, AndroidX, OkHttp, Retrofit) and confirm each with the library's current recommended replacement",
"Replace confirmed deprecated calls with their supported equivalent, preserving behavior exactly",
"Full JVM unit test suite, ktlintCheck, and detekt stay clean"
],
"priority": 2,
"passes": true,
"notes": "Ran compileDebugKotlin with full warning output to enumerate every deprecation warning in app/src/main (Compose, AndroidX, OkHttp/Retrofit surfaced none). Fixed the ones with a confirmed drop-in replacement that preserves behavior exactly: (1) hiltViewModel() moved from androidx.hilt.navigation.compose to androidx.hilt.lifecycle.viewmodel.compose - same artifact version (hilt-navigation-compose 1.4.0) already ships the new package, so this was an import-only change across 16 files; (2) LocalLifecycleOwner in ScannerScreen.kt moved from androidx.compose.ui.platform to androidx.lifecycle.compose - also import-only, dependency already present transitively; (3) KeyboardOptions(autoCorrect = false) renamed to autoCorrectEnabled across 7 files - straight parameter rename, same semantics; (4) java.util.Locale(String) constructor in MainActivity.attachBaseContext replaced with Locale.Builder().setLanguage(tag).build() - safe because AppLanguage.tag is always a plain 2-letter code (en/nl), never a full BCP-47 tag that setLanguage() would reject; (5) Icons.Filled.ViewList in LocationDetailScreen.kt replaced with Icons.AutoMirrored.Filled.ViewList. Left three deprecations unfixed because no behavior-preserving drop-in exists: EncryptedSharedPreferences/MasterKey in EncryptedTokenStore.kt (androidx.security-crypto has no direct successor API yet, only a Tink rewrite), LocalClipboardManager in InviteScreen.kt (LocalClipboard's replacement API is suspend-based and would require restructuring the composable, not a like-for-like swap), ScrollableTabRow in LocationDetailScreen.kt and rememberSwipeToDismissBoxState's confirmValueChange in ProductsPane.kt (Compose's own migration guidance says these have no direct equivalent - PrimaryScrollableTabRow/SecondaryScrollableTabRow changes visual behavior, and confirmValueChange is deprecated without any replacement per its own KDoc). This is a find-and-fix story per the PRD description, not a fixed bug list, so these are left as-is with this note explaining why. No regression tests needed since these are compile-time API surface changes with identical runtime behavior, not logic changes. Full JVM test suite, ktlintCheck (with ktlintFormat to fix a resulting line-wrap), and detekt all pass."
},
{
"id": "US-003",
"title": "Null/empty/boundary edge cases in UI state and DTO mapping",
"description": "As a user, screens driven by server data (product/shelf/location lists, search results, dashboard stats) must render sane empty/zero/boundary states instead of crashing or showing garbage when the API returns edge-case shapes.",
"acceptanceCriteria": [
"Audit ViewModels and DTO-to-UI-state mappers for unguarded !! , unsafe casts, or division/index operations on collections that could be empty",
"Any confirmed gap gets a regression test and a minimal null-safe/empty-safe fix",
"Full JVM unit test suite, ktlintCheck, and detekt stay clean"
],
"priority": 3,
"passes": true,
"notes": "Genuine audit found no confirmed defects in this category. Searched all of app/src/main/java (136 .kt files, prod source only, androidTest excluded) for: (1) `!!` non-null assertions - zero occurrences anywhere in production code; (2) unsafe `as X` casts on DTO/API-sourced data - only real cast found is ErrorLoggerImpl.kt:50 `context.getSystemService(...) as ConnectivityManager`, a standard Android system-service cast guaranteed non-null by the framework for CONNECTIVITY_SERVICE, not DTO data; (3) division where the divisor is a collection size that could be 0 - the only such calc, DashboardLocationChart.kt:158 `stat.productCount.toFloat() / maxProductCount`, is already guarded with `if (maxProductCount > 0) ... else 0f`; (4) index/first/last/random on API-sourced collections without an empty check - only hit is MainActivity.kt:770 `entries.first().id`, gated immediately by a preceding `entries.size == 1 ->` branch (this is the exact fix already documented in this repo's CLAUDE.md for a prior firstOrNull() bug). Roughly a dozen other `as`/`/`/`.size` hits were ruled out as false positives (doc comments, Compose Modifier.size() calls, import aliases, display-only .size usage with no arithmetic). No DTO-to-UI-state mapper files exist as a separate layer (no class/object matching *Mapper) - mapping happens inline in ViewModels, which were included in the same sweep. This codebase already went through a hardening pass (see CLAUDE.md's 'Editing the hierarchy' entries.firstOrNull() history and the dashboard chart's explicit zero-guard) that closed off this bug class before this story ran. No fix or regression test was added since no defect was found - fabricating one would violate the PRD's explicit instruction not to fabricate a fix. Full JVM test suite, ktlintCheck, and detekt all pass unchanged."
}
]
}