KMP phase 6E: core:data:exercise becomes a KMP module — with zero androidMain - #241
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
1f1297a to
278b99e
Compare
d6bd37e to
78714e5
Compare
278b99e to
7459530
Compare
78714e5 to
351708c
Compare
|
@claude review |
|
@codex review |
|
Claude finished @stslex's task in 2m 18s —— View job Review complete
I read all six commits ( 1. The catch fix (
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 706f30856e
ℹ️ 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".
7459530 to
a00c91f
Compare
|
Review classification before the next push:
|
706f308 to
1a35bf4
Compare
|
Implemented the correct Markdown spacing nit in |
|
@claude review |
|
@codex review |
|
Claude finished @stslex's task in 2m 4s —— View job Review complete
I independently re-verified the load-bearing claims rather than taking the PR body or the earlier 1. Catch fix + device test — confirmed correct
2.
|
|
Review classification before the next push:
|
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@claude review |
|
@codex review |
|
Claude finished @stslex's task in 2m 0s —— View job Review complete
I re-verified independently against the current head ( Both outstanding Codex findings are now fixed, correctly
Spot-checked the core claims once more, all hold
No new issues foundNothing new surfaced from the fix commits themselves — both are narrowly-scoped doc/path corrections with no code-behavior surface. This PR is in a clean, mergeable state as far as this review can tell: the one real behavior fix is correct and proven on-device, the mechanical conversion is complete, and all review feedback from both Codex and prior Claude passes has been addressed and re-verified. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a9761c485
ℹ️ 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".
a00c91f to
5362cc7
Compare
…lly throws The old catch named android.database.sqlite.SQLiteConstraintException — the framework driver's type. Under BundledSQLiteDriver (production since the flip) a UNIQUE violation arrives as the driver's own throw, and the catch silently stopped matching — the behaviour change with no compile error the phase-6 spec's §5 predicted. Observed live before fixing: a new on-device duplicate-name test (the module's first device test, @regression, in-memory DB via the flipped InMemoryDatabaseProvider) escaped with "android.database.SQLException: Error code: 2067, message: UNIQUE constraint failed: exercise_table.name". The fix catches androidx.sqlite.SQLiteException — on Android a typealias to android.database.SQLException, so the framework subtype and the bundled throw both land in the one clause, and the same import will compile from commonMain when the module converts — and gates on the one string both engines emit, because the common type carries no code field and the framework subtype is not nameable from common code. Non-matching SQLite errors keep escaping exactly as before. Proven in all four directions: device test red on the old catch (the escape above), green on the new; Robolectric twin (framework driver) green throughout; mutation M-DUP1 (never-matching string) reds BOTH legs — 54 host tests 1 failed, device 1/1 failed — reverted. Full module suites after revert: 212 host + 1 device, 0 failures. detekt separate, 56/56. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
…leaves the JVM System.currentTimeMillis() has no commonMain home; its eight sites across the three repositories become kotlin.time Clock.System.now() .toEpochMilliseconds() — the same wall clock through the stdlib's multiplatform face, zero behaviour change, zero new dependency. Split out of the conversion commit so that one stays pure moves. Module suite 212/0; detekt separate, 57/57 (the 57th is the suite gate over the module's new device test, previously NO-SOURCE). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
…econd platform core:data:exercise converts to convention.kmpLibrary with ZERO androidMain files — the repo's first pure-common data module. All 42 production files move to commonMain: the paging surface rides paging-common (replacing the Android-only paging-runtime-ktx), and after the duplicate-name catch took the common androidx.sqlite.SQLiteException one commit earlier, no platform type remained. The dead compose-bom/compose-runtime/room3-runtime declarations (zero usages, spec §5) do not return. A new androidx-sqlite base alias supplies the driver-agnostic exception type, which the database module's implementation deps rightly do not leak. 19 host-test classes + the seed helper move to androidHostTest (Robolectric trio + mockk + paging-testing + database-test, the established per-module set), the device test to src/androidDeviceTest/kotlin. One lint catch on the way: a file-private helper called from inside the class needs a synthetic accessor, and SyntheticAccessor rightly objected — the constraint check is now a private member of the repository. The second conversion rode the first's rails end to end: device-test component, both CI aliases, desugaring, detekt device globs, suite gate — nothing new was needed. Gates (--rerun-tasks --no-build-cache --no-configuration-cache): full forced gate 3265/3265, 0 failed; detekt separate 57/57; unit tests 2018/211 (testDebugUnitTest scope) + 377/52 (testAndroidHostTest scope) = 2395, 0 failures — exercise's 212 moved scope, not existence; device 1/1 green via the connectedDebugAndroidTest alias under the Regression filter; exercise iOS klib + kspKotlinIosSimulatorArm64 in the repo-wide graph; goldens 456/0; schemas untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
The script copied .githooks/pre-commit into .git/hooks — a snapshot that goes stale the moment the tracked hook changes. Two live consequences: a clone that never ran it had NO hook at all (this session's clone — caught when a red detekt run and a passing commit coexisted; the arc's separate forced detekt invocations were the real gate all along), and a clone that ran it before #235 still carries the rename-blind ACM filter that PR proved skips detekt on move-only commits. Now it sets core.hooksPath = .githooks — the tracked directory, so hook fixes deploy with git pull — which is what CLAUDE.md has described the script as doing all along. Proven both directions through the new wiring: a staged probe file with an ImportOrdering violation is rejected by the hook (probe removed, never committed); this commit itself passes through it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
… the riders, and the road not taken The typealias fact that dissolved §5's seam, the first zero-androidMain module, the second conversion riding the first's rails at the budgeted per-module cost, the setup-hooks copy-vs-configure finding, the §7 riders closed with numbers (java.time discharged; File→okio re-priced cheaper and less urgent), and the deliberate skip of the verifyPaparazzi side-track — pinned to phase 7's first golden conversion by the convention's own KDoc. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
8a9761c to
409159e
Compare
|
@codex review |
|
@claude review |
|
Claude finished @stslex's task in 1m 30s —— View job Review complete
I re-verified independently against the current head ( Everything previously flagged and marked "fixed" checks out
One open finding — unaddressed, and backed by the repo's own rule
// Every production file is commonMain — the module has NO androidMain: the one platform type it
// carried (the framework SQLiteConstraintException in ExerciseRepositoryImpl's duplicate-name
// catch) was replaced by the common androidx.sqlite.SQLiteException before the conversion, and the
// paging surface (Pager/PagingData/PagingSource) lives in paging-common. The compose-bom/runtime
// and room3-runtime declarations the Android build script carried had ZERO usages (phase-6 spec §5)
// and do not return.is HISTORY under AGENTS.md's own three-category comment rule ( The adjacent two-line comment at SummaryThe four commits do what the PR body claims, and every previously-reported issue that was marked fixed is in fact fixed in the committed diff. The one open item is the |
|
Review classification before the next push:
|
|
@claude review |
|
@codex review |
|
Claude finished @stslex's task in 1m 0s —— View job Review complete
I independently re-verified against the current head ( Everything previously flagged is fixed, and I re-confirmed each fix directly rather than trusting the classification comments
Core claims re-verified from scratch
No new issues foundThis PR is in a clean, mergeable state. The one real behavior fix (duplicate-name catch under the bundled driver) is correct and proven on-device; the mechanical conversion (wall-clock swap, commonMain move) is complete with no stragglers; every review finding from both Codex and prior Claude passes — including the most recent build-script comment-category finding — has been fixed and I've re-confirmed the fix independently rather than trusting the classification comment. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1e9a27d10b
ℹ️ 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".
|
Review classification before the next push:
|
|
Follow-up self-review classification before the same push:
|
|
@claude review |
|
@codex review |
|
Claude finished @stslex's task in 1m 13s —— View job Review complete
I independently re-verified against the current head ( All prior findings (Codex + earlier Claude passes) are fixed, and I re-confirmed each directly
Core claims re-verified from scratch at
|
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
) **KMP phase 6, the driver flip: Android moves to `BundledSQLiteDriver`. Stacked on #239** — review that first; this is one commit on top, deliberately separate so a SQLite-behaviour regression and a source-set regression stay distinguishable under bisect, and so this half reverts alone. #241 (core:data:exercise → KMP) stacks on top of this PR. ## What flips, what doesn't, and why the line is where it is | Surface | Driver after this PR | Why | |---|---|---| | Production (`buildAppDatabase`) | **Bundled** (SQLite 3.50.x) | The point: one SQLite build on every device instead of the per-OEM system one | | The 5 device-test classes (migration, rollback, invalidation, round-trip, recently-trained) | **Bundled** | The migration suite is the flip's gate, so it must run the flipped driver | | `InMemoryDatabaseProvider` (app:app androidTest via `MetroTestRule`) | **Bundled** | Device-side; the whole instrumented app suite now exercises the production driver | | `RepositoryTestEnv` + host tests (`BaseDatabaseTest`, snapshot test) | **Framework, pinned with a comment** | **Measured, not assumed**: the bundled android variant ships Android-ABI natives only — under Robolectric on a desktop JVM it dies with `UnsatisfiedLinkError`. Robolectric is not an admissible driver oracle in this repo anyway (`AtomicRollbackDeviceTest` KDoc: two false negatives) | The Room convention's KMP branch now declares `sqlite-bundled` for androidMain; the database module pins `sqlite-framework` on its host tests explicitly, next to the reason. ## The gate — the migration suite on a real device, under the flipped driver - **28/28 device tests green under `BundledSQLiteDriver`** on Pixel_6_API_34 (arm64), collected count printed (`Starting 28 tests`), via the `connectedDebugAndroidTest` alias under the exact ui_tests.yml Regression filter. That includes all 7 `AppDatabaseMigrationTest` cases — a real v5 file seeded on disk, `Migration6` applied, schema validated against `6.json` — plus the atomicity-rollback oracle (both production transaction shapes), invalidation across connections, and the on-disk round-trip. - **The same 28 ran green under `AndroidSQLiteDriver` in #239** — the spec's "under both drivers" requirement, satisfied across the two PRs by construction. - **41/41 app:app device Regression tests green** with the flipped in-memory provider (41 = the 35 pinned in tech-debt plus #232's six DataStore singleton tests). ## Risk register (from the pre-flip SQL audit of all 105 `@Query` sites + every raw-SQL site) - **Zero double-quoted string literals** repo-wide (the classic `SQLITE_DQS=0` breakage): every SQL literal is single-quoted. Zero version-sensitive constructs in use — no window functions, CTEs, upserts (`@Insert(REPLACE/IGNORE)` compile to the pre-upsert forms, frozen since 3.6.19), date/time/json/math functions, `RETURNING`, or `IIF`. - **One query with version-floating output, pre-existing**: `SessionDao.getRecentSessionsForExercise` projects a bare column under `GROUP BY` with no aggregate — legal and arbitrary in *both* SQLite builds; a planner change may pick a different in-group row when one session holds the same exercise twice. Not a flip regression; noted for the record. - **Cross-library file interop is deliberate and stays**: the `snapshot/` package opens the live DB via framework SQLite (peek + pre-migration checkpoint) while Room holds bundled connections. Main-db and WAL formats are frozen, so reads are safe by format; the concurrent-open path leans on POSIX locking shared by both libraries. This is the residual risk the device-restore pass (Ilya's gate, per the recovery spec) covers end-to-end; flagged here rather than silently carried. - Improvements that come free: host-variable limit 999 → 32766, and `ROW_NUMBER()` becomes available — `SessionDao`'s PR-batch comment now records the rewrite as *available but not taken* (this PR is behaviour-neutral by design). ## Gates | Gate | Result | |---|---| | device suite under bundled | ✅ 28/28, `Starting 28 tests on Pixel_6_API_34(AVD)` | | app:app device Regression | ✅ 41/41 | | host tests (framework pin) | ✅ database 128/26, exercise 212/19, 0 failures | | full forced gate (assembleDebug testDebugUnitTest verifyPaparazziDebug lintDebug assembleDebugAndroidTest) | ✅ `3248 actionable tasks: 3248 executed`, 0 failed | | detekt (separate invocation) | ✅ `56 actionable tasks: 56 executed` | | unit tests | ✅ 2230/230 + 165/33 = 2395, 0 failures — identical to #239, as a behaviour-neutral flip must be | | goldens | ✅ 456, 0 touched | 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH
KMP phase 6, increment E:
core:data:exercisebecomes a KMP module — with zero androidMain. Stacked on #240 → #239. Review in that order.The last code increment of phase 6. Four commits: the one genuine behaviour change (red-first, on device), the wall-clock swap, the pure-mechanics conversion, and the hook-deployment fix the conversion's own near-miss surfaced.
1. The behaviour change the spec predicted, observed live before it was fixed
Spec §5 called
catch (SQLiteConstraintException)inExerciseRepositoryImpl.saveItemcontrol flow, not a type name: under a Room 3 driver the constraint violation arrives as a different type, the catch silently stops matching, and the exception escapes — with no compile error. That is exactly what happened. A new on-device duplicate-name test (this module's first device test,@Regression, in-memory DB under the flippedBundledSQLiteDriver) ran red against the unfixed repository, escaping with:The fix is smaller than the spec's own sketch, because of a fact worth keeping: on Android,
androidx.sqlite.SQLiteExceptionisactual typealias … = android.database.SQLException(read from the 2.7.0 sources jar). So one common catch covers the framework driver'sSQLiteConstraintException(a subtype), the bundled driver's own throw (measured above), and iOS's native actual — no expect/actual seam needed at all. The constraint check gates on the one string both engines emit (UNIQUE constraint failed), because the common type has no code field and the framework subtype is not nameable from common code.Proven in all four directions: device red on the old catch → green on the new; the Robolectric twin (framework driver) green throughout; mutation M-DUP1 (never-matching string) reds both legs — 1 of 54 host tests, 1 of 1 device — reverted.
2. Mechanics
System.currentTimeMillis()sites →kotlin.time.Clock.System.now().toEpochMilliseconds()(stdlib, same wall clock; its own commit so the conversion stays pure moves).paging-common(verifiediosSimulatorArm64published). 19 test classes + seed helper →androidHostTest, the device test →src/androidDeviceTest/kotlin. The deadcompose-bom/compose-runtime/room3-runtimedeclarations (zero usages, spec §5) do not return;paging-runtime-ktx→paging-common; the newandroidx-sqlite(base) alias supplies the exception type the database module'simplementationdeps rightly don't leak.3. A process finding, in the open
setup-hooks.shcopies.githooks/pre-commitinto.git/hooks/— it does not setcore.hooksPath, though CLAUDE.md says it does. Consequences, both real: a clone that never ran the script has no hook at all (this session's clone — caught when a red detekt run and a passing commit coexisted; the separate forced detekt invocations in this arc's process were the actual gate all along), and a clone that ran it before #235 still carries the ACM filter the fixture-move PR proved blind to renames. Fixed here: the script now setscore.hooksPathto the tracked.githooks/, so hook fixes deploy withgit pullinstead of with a ritual — which is what CLAUDE.md already claimed. Reported rather than silently corrected.4. The §7 riders, re-priced with numbers (closing the phase brief's ask)
java.time: productioncore/datais now clean —WorkoutExportMapper's line was taken in KMP phase 6D: core:data:database becomes a KMP module, on AndroidSQLiteDriver #239. What remains: 1 test file (DatabaseJsonExporterImplTest, deliberately kept as a cross-library oracle) and 14feature:exercise-chartfiles, which are Phase 7's by the spec's own split. This rider is discharged for phase 6.java.io.File→okio.Path: the dependency half is now free (okio rides the graph viadatastore-preferences-coresince KMP phase 6B: core:data:dataStore becomes a KMP module #233; using it in signatures needs only a toml alias). The remaining cost is unchanged in kind: a signature ripple across 7 modules (backup:api, google-drive, worker, database, recovery, settings, app:app). And its urgency dropped with this phase: after KMP phase 6D: core:data:database becomes a KMP module, on AndroidSQLiteDriver #239 everyFile-typed surface sits in an androidMain of a KMP module — nothing common is blocked by it. Still correctly deferred until a real iOS backup implementation makes the right shape observable;android.net.Uristill has no common analogue (SAF).Gates
All
--rerun-tasks --no-build-cache --no-configuration-cache.connectedDebugAndroidTestalias under the ui_tests.yml Regression filtertestAndroidHostTestscope), feature:exercise 113/9, exercise-chart 66/8, 0 failures3265 actionable tasks: 3265 executed, 0 failed57 actionable tasks: 57 executed— the 57th is the suite gate over this module's new device test, previously NO-SOURCEtestDebugUnitTestscope) + 377 / 52 (testAndroidHostTestscope) = 2395, 0 failures, 0 skipped — identical total to #239/#240; exercise's 212 moved scope, not existencekspKotlinIosSimulatorArm64in the repo-wideassembleDebuggraph🤖 Generated with Claude Code
https://claude.ai/code/session_011kNU4XxzXp6NpCnVUQA6yH