Skip to content

refactor: complete Phoenix rebrand with safe persisted-file migration - #711

Merged
9thLevelSoftware merged 10 commits into
mainfrom
codex/phoenix-persisted-file-migration
Aug 22, 2026
Merged

refactor: complete Phoenix rebrand with safe persisted-file migration#711
9thLevelSoftware merged 10 commits into
mainfrom
codex/phoenix-persisted-file-migration

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

Replaces closed PR #709 with the completed persisted-file compatibility implementation on a dedicated branch.

Legal rebrand plus an upgrade-safe persisted-file migration. Existing Android and iOS installations keep their sandbox and local data while the active database and Android preference stores move to Phoenix filenames.

This supersedes the earlier “keep legacy persisted filenames” workaround. Fresh installs and successful upgrades use:

  • Database: phoenix.db
  • Database recovery: phoenix-recovery.db
  • Database staging/lock: phoenix.db.migrating, phoenix-db-migration.lock
  • Android preferences: phoenix_preferences, phoenix_secure_preferences
  • Preference recoveries: phoenix_preferences_recovery, phoenix_secure_preferences_recovery

Rebrand changes

  • iOS: VitruvianPhoenix → PhoenixApp (directory, project, scheme, Swift type, entitlements)
  • Android: VitruvianApp → PhoenixApp, VitruvianTheme → PhoenixTheme
  • SQLDelight API: VitruvianDatabase → PhoenixDatabase
  • Build/UI/docs: Phoenix project, theme, icons, documentation, workflows, and scripts

Persisted-file compatibility

  • A common pre-database state machine checkpoints WAL, validates SQLite, creates and flushes a neutral recovery copy, and performs same-directory no-replacement atomic cutover.
  • Interrupted migration resumes only from a verified legacy or recovery source.
  • If legacy and target databases coexist, startup blocks with DB_DUAL_DATABASES; neither candidate is opened, changed, selected, or merged automatically.
  • SQLDelight schema migration and reconciliation run only after the physical file is safely selected.
  • Recovery survives the migrated launch and is removed only after validation on the next real launch. Same-process Retry cannot consume it early.
  • Android plaintext and encrypted stores migrate through SharedPreferences APIs. Phoenix values win conflicts; encrypted XML bytes are never copied.
  • Database/preference failures render a shared preserved-data screen with a stable diagnostic and Retry where safe.
  • Application ID, bundle ID, signing configuration, SQLDelight schema contents, and migration files are unchanged.

Compatibility lookup remains indefinitely for skipped-release upgrades. Legacy filename literals are intentionally limited to:

  • DatabaseFileNames.LEGACY
  • AndroidPreferenceFileNames.LEGACY_PLAINTEXT and LEGACY_ENCRYPTED
  • Android backup/data-extraction exclusions
  • Migration tests and the disposable-emulator upgrade harness

Verification completed

  • Common coordinator state/failure-order tests: pass
  • Android host and unit tests: pass
  • Six Android instrumentation migration tests: pass
  • :shared:verifyCommonMainPhoenixDatabaseMigration: pass
  • :shared:compileKotlinIosArm64: pass
  • Android lint and debug assembly: pass
  • Android release assembly: pass
  • Release cue-resource verifier: pass
  • QA-release-boundary verifier: pass
  • iOS schema shell validator: 47 tables / 571 columns
  • Legacy-literal and schema-diff review: pass
  • Changed-file Spotless review and git diff --check: pass

The disposable API 35 upgrade matrix passed all six scenarios: Fresh, v0.9.6 Upgrade, InterruptedStaging, CorruptSource, DualDatabase, and LowStorage. The upgrade retained 3 profiles, 10 workouts, 1 routine, plaintext preferences, and a real committed-WAL payload while advancing schema 43 → 47. Low storage reached 372 KiB free, blocked with DB_RECOVERY_COPY_FAILED, preserved the legacy source, and completed through the rendered Retry action after recovery. Successful scenarios retained neutral recovery on the migrated launch and removed it on the next launch.

Detailed procedure and evidence contract: docs/qa/persisted-file-upgrade.md.

Remaining release gates — do not merge yet

  • Run a production-signed Android upgrade with the same signing certificate and a higher-version candidate APK. A local debug key cannot supply this evidence.
  • Run a true skipped-version upgrade from the oldest supported production build.
  • Run the upgrade and two-launch cleanup checks on a physical iOS device; this Windows host has no iOS device/simulator target and skips framework linking.
  • compileTestKotlinIosArm64 is still blocked by three pre-existing errors in unchanged tests (RoutineSetWeightResolverTest and WorkoutExecutionGuardRestoredRuntimeTest).
  • Repository-wide spotlessCheck still reports 168 unrelated pre-existing Kotlin formatting violations; all files touched here are clean.

Published implementation branch: codex/phoenix-persisted-file-migration at b98aa9f8.

This PR is intentionally a draft. Mark it ready only after the production-signed Android, true skipped-version, and physical-iOS device gates are attached.

Devil and others added 10 commits August 21, 2026 12:18
- Rename iOS project: VitruvianPhoenix → PhoenixApp (directory, xcodeproj, scheme, swift, entitlements)
- Rename Android: VitruvianApp → PhoenixApp, VitruvianTheme → PhoenixTheme
- Rename SQLDelight: VitruvianDatabase.sq → PhoenixDatabase.sq
- Update settings.gradle.kts root project name: ProjectPhoenix
- Update AndroidManifest: Theme.PhoenixProject, .PhoenixApp
- Replace all icon assets with new Phoenix logo
- Scrub all Vitruvian/vitphoe references from source, tests, docs, CI workflows
- Verified: grep -ri vitruvian = 0, grep -ri vitphoe = 0
- Builds verified: SQLDelight interface gen, Kotlin Android/iOS, Xcode device build
- Revert DATABASE_NAME to 'vitruvian.db' (Android + iOS) to preserve existing user data on upgrade
- Revert Android preference filenames to 'vitruvian_secure_preferences' / 'vitruvian_preferences'
- Update SQLDelight fault injection statement IDs to match regenerated PhoenixDatabase identifiers
- Add opaque black background to iOS AppIcon (Apple requires no transparent corners)
- Remove accidentally committed HTML file (failed Google Drive download)
…ed-file-migration

# Conflicts:
#	.github/ISSUE_TEMPLATE/bug_report.yml
#	ANDROID_INSTALL.md
#	androidApp/src/main/res/xml/backup_rules.xml
#	androidApp/src/main/res/xml/data_extraction_rules.xml
#	docs/index.html
#	docs/superpowers/specs/2026-06-16-per-cable-weight-handling-design.md
#	iOS_INSTALL.md
#	iosApp/README.md
#	iosApp/TESTFLIGHT_NOTES.txt
#	shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/repository/SqlDelightSyncRepositoryTest.kt
#	shared/src/androidMain/kotlin/com/devil/phoenixproject/presentation/screen/WorkoutTabPreviews.kt
#	shared/src/androidMain/kotlin/com/devil/phoenixproject/util/CsvExporter.android.kt
#	shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.android.kt
#	shared/src/androidMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.android.kt
#	shared/src/commonMain/composeResources/values-de/strings.xml
#	shared/src/commonMain/composeResources/values-es/strings.xml
#	shared/src/commonMain/composeResources/values-fr/strings.xml
#	shared/src/commonMain/composeResources/values-nl/strings.xml
#	shared/src/commonMain/composeResources/values/strings.xml
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleCompatibilityMode.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/BleExceptions.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoder.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/KableBleConnectionManager.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/ble/MetricPollingEngine.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/LegacyCatalogueIdMap.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/migration/MigrationManager.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/data/repository/ConnectionLogRepository.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/components/ConnectingOverlay.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SetSummaryCard.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/screen/SplashScreen.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/DiagnosticsViewModel.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BleConstants.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/util/BlePacketFactory.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/util/DataBackupManager.kt
#	shared/src/commonMain/kotlin/com/devil/phoenixproject/util/HardwareDetection.kt
#	shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/DiagnosticFaultDecoderTest.kt
#	shared/src/commonTest/kotlin/com/devil/phoenixproject/data/ble/ProtocolParserTest.kt
#	shared/src/commonTest/kotlin/com/devil/phoenixproject/data/integration/HealthDataMappingTest.kt
#	shared/src/iosMain/kotlin/com/devil/phoenixproject/util/DeviceInfo.ios.kt
#	third_party/kable-core-android-patched/README.md
#	third_party/kable-core-android-patched/src/androidMain/BluetoothGatt.kt
@9thLevelSoftware
9thLevelSoftware marked this pull request as ready for review August 22, 2026 03:39
Copilot AI lite review requested due to automatic review settings August 22, 2026 03:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd3a7fc6b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


companion object {
const val CHANNEL_ID = "vitruvian_workout_channel"
const val CHANNEL_ID = "phoenix_workout_channel"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the released notification channel ID

On Android upgrades, changing CHANNEL_ID creates a new notification channel rather than renaming the existing one. Users' per-channel choices—especially disabling workout notifications, importance, sound, and vibration—remain attached to vitruvian_workout_channel, while foreground-service notifications use the newly enabled default channel. Retain the released ID and change only the channel's display name, or explicitly migrate the old channel behavior.

Useful? React with 👍 / 👎.

return
when (resolution) {
is StartupDependencyResolution.Failed -> {
Logger.e { "iOS app dependency resolution blocked: ${resolution.diagnosticCode}" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Re-run Koin initialization from the iOS retry action

When doInitKoin() throws in the Swift entry point, the exception is swallowed and no usable global Koin instance exists. This retry callback only increments Compose state, so every retry calls KoinPlatform.getKoin() again without re-running initialization and leaves the user in a permanent STARTUP_INITIALIZATION_FAILED loop. The retry path needs to invoke the Koin initializer or the initialization failure must be handled separately as non-retryable.

Useful? React with 👍 / 👎.

@@ -73,7 +73,7 @@ class ThemeViewModel(private val settings: Settings) : ViewModel() {

companion object {
const val THEME_MODE_KEY = "theme_mode"
const val THEME_PREFS_FILE = "vitruvian_preferences"
const val THEME_PREFS_FILE = "phoenix_preferences"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Fall back to legacy preferences before startup migration

On the first Android launch after upgrading, PhoenixApp.attachBaseContext() calls applyPersistedApplicationNightMode() before Koin runs AndroidPreferenceFileMigrator, but this constant now points only to the not-yet-created Phoenix preference file. On API 31+, users with an explicit legacy LIGHT or DARK setting therefore get a SYSTEM-themed starting window until Compose loads, causing a visible incorrect-theme flash. The early reader should use the legacy file when the target file has not yet been migrated.

Useful? React with 👍 / 👎.

@kilo-code-bot

kilo-code-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: 0 Critical Issues | Recommendation: Address before merge

Overview

Severity Count
🚨 critical 0
⚠️ warning 0
💡 suggestion 0
🤏 nitpick 0

Correctness / Safety Findings

No correctness or safety findings.

The persisted-file migration coordinator (DatabaseFileMigrationCoordinator) is well-designed: the dual-database guard, the staging-then-verify-then-promote dance, the same-process retry guard (recoveryCreatedThisProcess), and the post-cutover recovery-cleanup deferral are all sound. The preference migrator (AndroidPreferenceFileMigrator) is idempotent across partial failures — a thrown prepare() leaves prepared = false so Koin's singleton retry re-runs the work, and writeMissing ensures Phoenix target values win on key conflicts. The PersistedFileStartupPrerequisite wiring in AppModule correctly orders PhoenixDatabaseSettingsSecureSettingsQualifier so a DB failure masks nothing and a pref failure doesn't corrupt the DB. The rebrand is COMPLETE — every remaining vitruvian reference is one of the intentional legacy literals called out in the PR description (database filenames, preference filenames, backup exclusions, migration tests, upgrade harness, trademark disclaimer).

Ponytail Review

  • shared/src/commonTest/kotlin/com/devil/phoenixproject/data/local/DatabaseFileMigrationCoordinatorTest.kt:L75,L120,L178,L219: test-shrink — assertEquals("delete:STAGING", operations.calls[2]) uses a fragile positional index that breaks if any operation is added before the staging delete. The same file already uses the safer indexOf("delete:STAGING") < indexOf("checkpoint:LEGACY") pattern elsewhere (lines 76, 121, 179, 201). Apply that pattern to all four call sites. Saves ~8 lines of magic-index assumptions and makes future coordinator changes safe.

  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigratorTest.kt:L276: test-shrink — cleanRestartCanDeleteRecoveryWithoutReopeningEncryptedRecoveryData sets failedRead = RECOVERY_ENCRYPTED, but on the (target+recovery, no-source) path the production code never calls read(recovery) — only readStore(pair.target) at AndroidPreferenceFileMigrator.kt:L102. The injection is dead weight. Either remove the failedRead line or add a read counter to the fake and assert read(RECOVERY_ENCRYPTED) was never called, which is the actual behavior under test.

  • shared/src/commonMain/kotlin/com/devil/phoenixproject/StartupDependencyResolution.kt:L38-L46: shrink — manual cause-chain walk with a depth counter:

var current: Throwable? = this; var depth = 0
while (current != null && depth < 16) { ... depth++ }

Replace with generateSequence(this as Throwable?) { it.cause }.take(16).filterIsInstance<StartupDiagnosticFailure>().firstOrNull(). Same semantics, one expression, no mutable state. Saves ~6 lines.

  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/migration/MigrationManagerTest.kt:L72-L83: test-shrink — four scenarios asserted with assertNotEquals(StartupSurface.MAIN, …) pass silently if the function returns the wrong non-MAIN surface (e.g., MIGRATION_RETRY or EULA). Replace each assertNotEquals with the specific assertEquals(StartupSurface.MIGRATION_RETRY, …) or assertEquals(StartupSurface.EULA, …) that the test name promises. Tightens regression detection at zero cost.

Ponytail net: -20 lines.

Suggested Minimal Patch

No critical patch needed. Optional cleanup (all independent, pick what you want):

  1. In DatabaseFileMigrationCoordinatorTest.kt, replace calls[2] with calls.indexOf("delete:STAGING") at the four sites listed.
  2. In StartupDependencyResolution.kt, collapse the depth-walking while into a generateSequence(...).take(16).filterIsInstance<...>().firstOrNull().
  3. In MigrationManagerTest.kt, swap assertNotEquals(StartupSurface.MAIN, …) for the specific expected surface at each of the four sites.
  4. In AndroidPreferenceFileMigratorTest.kt, either drop the failedRead = RECOVERY_ENCRYPTED line at L276 or instrument the fake to count reads and assert read(RECOVERY_ENCRYPTED) was never called.

Final Merge Guidance

Can merge; Ponytail suggestions are optional cleanup.

The rebrand is clean, the migration coordinator is correct, the preference migrator is idempotent, the DI wiring orders dependencies safely, and the test coverage is thorough (all 7 DatabaseMigrationFailureCode paths exercised, all failure modes of the preference migrator's source/recovery/target lifecycle covered). The PR description's three remaining release gates (production-signed Android upgrade, true skipped-version upgrade, physical iOS device two-launch cleanup) are process gates, not code-quality gates — this review finds nothing that should block those gates from being pursued.

🏆 Best part: The recoveryCreatedThisProcess flag at DatabaseFileMigration.kt:L88 — a single mutable boolean that prevents same-process Koin retries from prematurely deleting the recovery file after a successful physical cutover. Small, targeted, and exactly the kind of state-tracking that earns its keep.

💀 Worst part: The failedRead = RECOVERY_ENCRYPTED injection in AndroidPreferenceFileMigratorTest.kt:L276 — it looks like it's testing encrypted-recovery safety, but the code path under test never reads encrypted recovery. A test that lies about what it's testing is worse than no test, because it gives false confidence during refactors.

📊 Overall: Like a well-organized moving day — the boxes are labeled, the fragile stuff is wrapped, and you actually read the instructions before touching the antique vase. The 463-file diff is intimidating, but 95% of it is a clean Vitruvian→Phoenix string substitution; the remaining 5% (the migration coordinator + tests + wiring) is the real work, and it's done carefully.

Files Reviewed (463 files, focused on ~20 new/changed)

Core migration logic:

  • shared/src/commonMain/kotlin/com/devil/phoenixproject/data/local/DatabaseFileMigration.kt (new)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/data/local/AndroidDatabaseFileOperations.kt (new)
  • shared/src/iosMain/kotlin/com/devil/phoenixproject/data/local/IosDatabaseFileOperations.kt (new)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigrator.kt (new)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/data/local/DriverFactory.android.kt (modified)
  • shared/src/iosMain/kotlin/com/devil/phoenixproject/data/local/DriverFactory.ios.kt (modified)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/di/AppModule.kt (modified — new PersistedFileStartupPrerequisite)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/StartupDependencyResolution.kt (new)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/App.kt (modified — failure screen)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/AndroidAppHost.kt (modified)
  • shared/src/iosMain/kotlin/com/devil/phoenixproject/IosAppHost.kt (modified)
  • shared/src/androidMain/kotlin/com/devil/phoenixproject/di/PlatformModule.android.kt (modified)

Tests:

  • shared/src/commonTest/kotlin/com/devil/phoenixproject/data/local/DatabaseFileMigrationCoordinatorTest.kt (new)
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigratorTest.kt (new)
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/data/migration/MigrationManagerTest.kt (modified — rename only)
  • shared/src/commonTest/kotlin/com/devil/phoenixproject/StartupDependencyResolutionTest.kt (new)
  • androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/local/AndroidDatabaseFileMigrationTest.kt (new)
  • androidApp/src/androidTest/kotlin/com/devil/phoenixproject/data/preferences/AndroidPreferenceFileMigrationTest.kt (new)
  • androidApp/src/test/kotlin/com/devil/phoenixproject/qa/PersistedFileUpgradeHarnessContractTest.kt (new)

Rebrand completeness: verified across androidApp/build.gradle.kts, shared/build.gradle.kts, settings.gradle.kts, androidApp/src/main/AndroidManifest.xml, androidApp/src/main/res/values/themes.xml, androidApp/src/main/res/values-night/themes.xml, iosApp/PhoenixApp/PhoenixApp.xcodeproj/project.pbxproj, iosApp/PhoenixApp/PhoenixApp/Info.plist, shared/src/commonMain/sqldelight/com/devil/phoenixproject/database/PhoenixDatabase.sq, all .github/workflows/*.yml, README.md, CLAUDE.md, AGENTS.md, gradle.properties. Zero unintentional leftovers.


Reviewed by minimax-m3 · Input: 113.3K · Output: 26.4K · Cached: 1.4M

Review guidance: REVIEW.md from base branch main

@9thLevelSoftware
9thLevelSoftware merged commit c8d60ca into main Aug 22, 2026
9 of 10 checks passed
@9thLevelSoftware
9thLevelSoftware deleted the codex/phoenix-persisted-file-migration branch August 22, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants