Closes the silent no-op when _allTasks is null on first launch (the
onboarding bulkCreateTasks path). The function now upserts: builds an
empty kanban shell with the standard column names if needed and places
the task in its target column. Unknown column names append a new
column at the end so the task is always reachable.
Also drops the second branch that conditionally wrote to
_tasksByResidence — that cache is being deleted in Phase 3 and
updateTask should not maintain it any more.
The Phase 1 unit tests now pass; the Phase 2 force-refresh in the
next commit replaces the placeholder column metadata (display names,
colors, icons) with authoritative server values.
Single source of truth: `com.tt.honeyDue.testing.GalleryScreens` lists
every user-reachable screen with its category (DataCarrying / DataFree)
and per-platform reachability. Both platforms' test harnesses are
CI-gated against it — `GalleryManifestParityTest` on each side fails
if the surface list drifts from the manifest.
Variant matrix by category: DataCarrying captures 4 PNGs
(empty/populated × light/dark), DataFree captures 2 (light/dark only).
Empty variants for DataCarrying use `FixtureDataManager.empty(seedLookups = false)`
so form screens that only read DM lookups can diff against populated.
Detail-screen rendering fixed on both platforms. Root cause: VM
`stateIn(Eagerly, initialValue = …)` closures evaluated
`_selectedX.value` before screen-side `LaunchedEffect` / `.onAppear`
could set the id, leaving populated captures byte-identical to empty.
Kotlin: `ContractorViewModel` + `DocumentViewModel` accept
`initialSelectedX: Int? = null` so the id is set in the primary
constructor before `stateIn` computes its seed.
Swift: `ContractorViewModel`, `DocumentViewModelWrapper`,
`ResidenceViewModel`, `OnboardingTasksViewModel` gained pre-seed
init params. `ContractorDetailView`, `DocumentDetailView`,
`ResidenceDetailView`, `OnboardingFirstTaskContent` gained
test/preview init overloads that accept the pre-seeded VM.
Corresponding view bodies prefer cached success state over
loading/error — avoids a spinner flashing over already-visible
content during background refreshes (production benefit too).
Real production bug fixed along the way: `DataManager.clear()` was
missing `_contractorDetail`, `_documentDetail`, `_contractorsByResidence`,
`_taskCompletions`, `_notificationPreferences`. On logout these maps
leaked across user sessions; in the gallery they leaked the previous
surface's populated state into the next surface's empty capture.
`ImagePicker.android.kt` guards `rememberCameraPicker` with
`LocalInspectionMode` — `FileProvider.getUriForFile` can't resolve the
Robolectric test-cache path, so `add_document` / `edit_document`
previously failed the entire capture.
Honest reclassifications: `complete_task`, `manage_users`, and
`task_suggestions` moved to DataFree. Their first-paint visible state
is driven by static props or APILayer calls, not by anything on
`IDataManager` — populated would be byte-identical to empty without
a significant production rewire. The manifest comments call this out.
Manifest counts after all moves: 43 screens = 12 DataCarrying + 31
DataFree, 37 on both platforms + 3 Android-only (home, documents,
biometric_lock) + 3 iOS-only (documents_warranties, add_task,
profile_edit).
Test results after full record:
Android: 11/11 DataCarrying diff populated vs empty
iOS: 12/12 DataCarrying diff populated vs empty
Also in this change:
- `scripts/build_parity_gallery.py` parses the Kotlin manifest
directly, renders rows in product-flow order, shows explicit
`[missing — <platform>]` placeholders for expected-but-absent
captures and muted `not on <platform>` placeholders for
platform-specific screens. Docs regenerated.
- `scripts/cleanup_orphan_goldens.sh` safely removes PNGs from prior
test configurations (theme-named, compare artifacts, legacy
empty/populated pairs for what is now DataFree). Dry-run by default.
- `docs/parity-gallery.md` rewritten: canonical-manifest workflow,
adding-a-screen guide, variant matrix explained.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Four broken VMs refactored to derive read-state from IDataManager, three
gaps closed:
1. TaskViewModel: tasksState / tasksByResidenceState / taskCompletionsState
now derived via .map + .stateIn / combine. isLoading / loadError separated.
2. ResidenceViewModel: residencesState / myResidencesState / summaryState /
residenceTasksState / residenceContractorsState all derived. 8 mutation
states retained as independent (legit one-shot feedback).
3. ContractorViewModel: contractorsState / contractorDetailState derived.
4 mutation states retained.
4. DocumentViewModel: documentsState / documentDetailState derived. 6
mutation states retained.
5. AuthViewModel: currentUserState now derived from dataManager.currentUser.
10 other states stay independent (one-shot mutation feedback by design).
6. LookupsViewModel: accepts IDataManager ctor param for test injection
consistency. Direct-exposure pattern preserved. Legacy ApiResult-wrapped
states now derived from DataManager instead of manual _xxxState.value =.
7. NotificationPreferencesViewModel: preferencesState derived from new
IDataManager.notificationPreferences. APILayer writes through on both
getNotificationPreferences and updateNotificationPreferences.
IDataManager also grew notificationPreferences: StateFlow<NotificationPreference?>.
DataManager, InMemoryDataManager updated. No screen edits needed — screens
consume viewModel.xxxState the same way; the source just switched.
Architecture enforcement test comes in P3.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a narrow IDataManager contract covering the 5 DataManager members
referenced from ui/screens/** (currentUser, residences, totalSummary,
featureBenefits, subscription) and a staticCompositionLocalOf ambient
(LocalDataManager) that defaults to the DataManager singleton.
No screen call-sites change in this commit — screens migrate in P0.2.
ViewModels, APILayer, and PersistenceManager continue to depend on the
concrete DataManager singleton directly; the interface is deliberately
scoped to the screen surface the parity-gallery needs to substitute.
Includes IDataManagerTest (DataManager is IDataManager) and
LocalDataManagerTest (ambient val is exposed + default type-checks to the
real singleton). runComposeUiTest intentionally avoided — consistent with
ThemeSelectionScreenTest's convention, since commonTest composition
runtime is flaky on iosSimulator.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>