Privacy-Period-Tracker/core/data/build.gradle.kts

50 lines
1.7 KiB
Plaintext
Raw Permalink Normal View History

feat: repository layer, and stop backfilled history fabricating accuracy figures core/data is the seam between storage and everything else. Reads return domain types, cycles are derived rather than stored, and the forecast is a function of the data instead of a field somebody has to remember to refresh — so §11's "recalculate after a confirmed start, after an edit, after a Not yet" is automatic rather than three call sites. Confirming a period is four writes in one transaction, because a partial result is a corrupt history rather than a failed action: write the record, score the forecast that was standing, clear the "not yet" observations it resolved, and snapshot a fresh forecast. THE DEFECT THIS FOUND A test expecting one scored prediction found three. The cause was not the test: every historical period entered during onboarding was scoring the current forecast against a date in the past, inventing an error for a prediction nobody had ever been shown. §16's "your predictions are getting better" would have been populated with figures the app made up about itself — plausible ones, which is what makes it expensive to notice. Two rules now, both pinned by tests: - exactly one unscored snapshot exists at a time. A forecast superseded before its outcome was known is not a wrong forecast, and counting it lets one cycle contribute several errors. - a confirmed start only scores a forecast made on or before it. Anything earlier is backfill and leaves the standing forecast alone. Accuracy also stays quiet below three scored predictions. One lucky forecast reading "average error: 0 days" is an overstatement, not a measurement. THE ROOM BOUNDARY, HELD THREE WAYS implementation rather than api on core:database; CycleRepository's constructor internal because it names a PeriodDatabase; reads mapped to domain types in Mappers.kt. Callers use CycleData.repository(context) and never learn Room exists. Verified rather than asserted: grep -rn "androidx.room" app/src domain is empty, and Room appears zero times in :app's debugCompileClasspath. No fallbackToDestructiveMigration: it turns a forgotten migration into a silent wipe of the user's entire cycle history on update. Also fixed: `domain/*` inside a KDoc silently opened a nested block comment — Kotlin block comments nest — which broke compilation in a way the error message pointed nowhere near. 58 tests across the project, all passing. closes #5
2026-08-18 02:41:05 -05:00
plugins {
alias(libs.plugins.android.library)
}
android {
namespace = "dev.privacyllc.period.core.data"
compileSdk = 37
defaultConfig {
minSdk = 26
feat: period CRUD end to end, and stop a double tap killing the app The Batch 01 vertical slice from PRODUCT_PLAN.md §58 now runs on a device: launch, log a period, it is stored, the forecast recalculates, edit or delete it and the forecast moves again. Hilt wiring, a TodayViewModel exposing one immutable state, and a working surface that says "Batch 01 · working surface" at the top so nobody mistakes it for the designed Today screen, which is Batch 03. THE DEFECT THIS FOUND, ON A DEVICE Tapping "Started today" twice on the same day killed the app: FATAL EXCEPTION: main android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: period_records.startDate Not a hypothetical — the crash was reproduced on emulator-5580, the fix applied, and the same two taps then produced "That day is already logged." with the process still alive and zero FATAL lines in logcat. The constraint is right: a duplicate must not overwrite the original row and lose its createdAt and source. The API around it was wrong. Repeating a tap when you are not sure the first one registered is an ordinary thing for a person to do, not a fault, and it must never be an exception. So the period writes return PeriodWriteResult — Added, AlreadyRecorded, Updated, Conflict, NotFound — and only genuine faults still throw. editPeriod had the same hole: moving a record onto a date another record holds. That is refused rather than merged, because merging would delete a period the user entered and only they can settle it. The ViewModel now installs a CoroutineExceptionHandler as a backstop. In a health app a crash mid-write is adjacent to losing what was just entered, and a message somebody can read beats a process that vanished. The message carries the exception type and never a record's contents (§45). Four regression tests pin all of it, plus two instrumented tests on a real file-backed database that close and reopen it — what a force-stop actually does, and something an in-memory database cannot fail. 70 unit tests and 2 instrumented tests, all passing. Release APK 1.2 MB. closes #6
2026-08-18 02:52:35 -05:00
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
feat: repository layer, and stop backfilled history fabricating accuracy figures core/data is the seam between storage and everything else. Reads return domain types, cycles are derived rather than stored, and the forecast is a function of the data instead of a field somebody has to remember to refresh — so §11's "recalculate after a confirmed start, after an edit, after a Not yet" is automatic rather than three call sites. Confirming a period is four writes in one transaction, because a partial result is a corrupt history rather than a failed action: write the record, score the forecast that was standing, clear the "not yet" observations it resolved, and snapshot a fresh forecast. THE DEFECT THIS FOUND A test expecting one scored prediction found three. The cause was not the test: every historical period entered during onboarding was scoring the current forecast against a date in the past, inventing an error for a prediction nobody had ever been shown. §16's "your predictions are getting better" would have been populated with figures the app made up about itself — plausible ones, which is what makes it expensive to notice. Two rules now, both pinned by tests: - exactly one unscored snapshot exists at a time. A forecast superseded before its outcome was known is not a wrong forecast, and counting it lets one cycle contribute several errors. - a confirmed start only scores a forecast made on or before it. Anything earlier is backfill and leaves the standing forecast alone. Accuracy also stays quiet below three scored predictions. One lucky forecast reading "average error: 0 days" is an overstatement, not a measurement. THE ROOM BOUNDARY, HELD THREE WAYS implementation rather than api on core:database; CycleRepository's constructor internal because it names a PeriodDatabase; reads mapped to domain types in Mappers.kt. Callers use CycleData.repository(context) and never learn Room exists. Verified rather than asserted: grep -rn "androidx.room" app/src domain is empty, and Room appears zero times in :app's debugCompileClasspath. No fallbackToDestructiveMigration: it turns a forgotten migration into a silent wipe of the user's entire cycle history on update. Also fixed: `domain/*` inside a KDoc silently opened a nested block comment — Kotlin block comments nest — which broke compilation in a way the error message pointed nowhere near. 58 tests across the project, all passing. closes #5
2026-08-18 02:41:05 -05:00
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
testOptions {
unitTests.isIncludeAndroidResources = true
}
}
dependencies {
// Room is an `implementation` dependency on purpose: it must NOT leak onto
// the compile classpath of anything above this module. That is half of what
// makes "no module above the repository imports Room" true rather than
// merely intended — the other half is the guard in issue #7.
implementation(project(":core:database"))
// Needed only to name RoomDatabase, PeriodDatabase's supertype, and to build
// the instance in CycleData. `implementation`, so it stops here.
implementation(libs.androidx.room.runtime)
api(project(":domain:cycle"))
api(project(":domain:prediction"))
implementation(libs.kotlinx.coroutines.core)
testImplementation(project(":core:database"))
testImplementation(libs.junit)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.androidx.room.runtime)
testImplementation(libs.robolectric)
testImplementation(libs.androidx.test.core)
feat: period CRUD end to end, and stop a double tap killing the app The Batch 01 vertical slice from PRODUCT_PLAN.md §58 now runs on a device: launch, log a period, it is stored, the forecast recalculates, edit or delete it and the forecast moves again. Hilt wiring, a TodayViewModel exposing one immutable state, and a working surface that says "Batch 01 · working surface" at the top so nobody mistakes it for the designed Today screen, which is Batch 03. THE DEFECT THIS FOUND, ON A DEVICE Tapping "Started today" twice on the same day killed the app: FATAL EXCEPTION: main android.database.sqlite.SQLiteConstraintException: UNIQUE constraint failed: period_records.startDate Not a hypothetical — the crash was reproduced on emulator-5580, the fix applied, and the same two taps then produced "That day is already logged." with the process still alive and zero FATAL lines in logcat. The constraint is right: a duplicate must not overwrite the original row and lose its createdAt and source. The API around it was wrong. Repeating a tap when you are not sure the first one registered is an ordinary thing for a person to do, not a fault, and it must never be an exception. So the period writes return PeriodWriteResult — Added, AlreadyRecorded, Updated, Conflict, NotFound — and only genuine faults still throw. editPeriod had the same hole: moving a record onto a date another record holds. That is refused rather than merged, because merging would delete a period the user entered and only they can settle it. The ViewModel now installs a CoroutineExceptionHandler as a backstop. In a health app a crash mid-write is adjacent to losing what was just entered, and a message somebody can read beats a process that vanished. The message carries the exception type and never a record's contents (§45). Four regression tests pin all of it, plus two instrumented tests on a real file-backed database that close and reopen it — what a force-stop actually does, and something an in-memory database cannot fail. 70 unit tests and 2 instrumented tests, all passing. Release APK 1.2 MB. closes #6
2026-08-18 02:52:35 -05:00
androidTestImplementation(project(":core:database"))
androidTestImplementation(libs.androidx.test.junit)
androidTestImplementation(libs.androidx.test.runner)
androidTestImplementation(libs.androidx.room.runtime)
androidTestImplementation(libs.kotlinx.coroutines.test)
feat: repository layer, and stop backfilled history fabricating accuracy figures core/data is the seam between storage and everything else. Reads return domain types, cycles are derived rather than stored, and the forecast is a function of the data instead of a field somebody has to remember to refresh — so §11's "recalculate after a confirmed start, after an edit, after a Not yet" is automatic rather than three call sites. Confirming a period is four writes in one transaction, because a partial result is a corrupt history rather than a failed action: write the record, score the forecast that was standing, clear the "not yet" observations it resolved, and snapshot a fresh forecast. THE DEFECT THIS FOUND A test expecting one scored prediction found three. The cause was not the test: every historical period entered during onboarding was scoring the current forecast against a date in the past, inventing an error for a prediction nobody had ever been shown. §16's "your predictions are getting better" would have been populated with figures the app made up about itself — plausible ones, which is what makes it expensive to notice. Two rules now, both pinned by tests: - exactly one unscored snapshot exists at a time. A forecast superseded before its outcome was known is not a wrong forecast, and counting it lets one cycle contribute several errors. - a confirmed start only scores a forecast made on or before it. Anything earlier is backfill and leaves the standing forecast alone. Accuracy also stays quiet below three scored predictions. One lucky forecast reading "average error: 0 days" is an overstatement, not a measurement. THE ROOM BOUNDARY, HELD THREE WAYS implementation rather than api on core:database; CycleRepository's constructor internal because it names a PeriodDatabase; reads mapped to domain types in Mappers.kt. Callers use CycleData.repository(context) and never learn Room exists. Verified rather than asserted: grep -rn "androidx.room" app/src domain is empty, and Room appears zero times in :app's debugCompileClasspath. No fallbackToDestructiveMigration: it turns a forgotten migration into a silent wipe of the user's entire cycle history on update. Also fixed: `domain/*` inside a KDoc silently opened a nested block comment — Kotlin block comments nest — which broke compilation in a way the error message pointed nowhere near. 58 tests across the project, all passing. closes #5
2026-08-18 02:41:05 -05:00
}