Privacy-Period-Tracker/core/database/schemas/dev.privacyllc.period.core..../1.json

274 lines
8.5 KiB
JSON
Raw Permalink Normal View History

feat: Room database for cycle history, and the schema guard that actually works core/database holds the four entities from PRODUCT_PLAN.md §10 — period_records, spotting_records, prediction_records, not_yet_observations — with DAOs returning Flow, epoch-day/epoch-milli converters, and the schema exported to core/database/schemas and committed. Three constraints are structural rather than remembered: - startDate is UNIQUE and inserts ABORT rather than REPLACE. REPLACE would delete the original row with its createdAt and source; §14 says health history is never modified silently. - spotting has its own table, so no query for periods can reach it. §25: it must never start or reset a cycle. - a prediction snapshot can be scored but not rewritten — score() sets only actualStartDate and absoluteErrorDays. A snapshot editable after the fact can only ever report that the app was right, which would make §16's whole accuracy feature a lie. deleteEverything() is one transaction and the only bulk delete in the module: a partial wipe leaves the cycle reconstructible from the tables the user asked to be rid of. 14 tests, on the JVM under Robolectric — no emulator. THE SCHEMA GUARD, AND WHY IT IS A SCRIPT SchemaTest was written as a drift guard and proved not to be one. Room regenerates the schema export during compilation, so adding a column to PeriodRecordEntity without bumping VERSION leaves the suite green while the committed schema quietly changes underneath it. That was not reasoned about, it was run: the column was added, 1.json gained it, and every test passed. On a device that is "Room cannot verify the data integrity" — a crash on update, after shipping. scripts/schema-guard.sh asks git instead, which Room cannot overwrite. Proved both ways before being trusted: green on a clean tree, exit 1 on the injected drift. It runs in pre-commit when an entity or the schema directory is staged, and the hook treats its exit 2 as a refusal. SchemaTest keeps its four tests and now documents what it does not catch. Room's own MigrationTestHelper is not used: every constructor needs an Instrumentation and schema assets, and AGP 9's library source-set DSL throws DefaultAndroidLibrarySourceSet_Decorated cannot be cast to AndroidLibrarySourceSet when you add an asset directory. Recorded so the next person does not spend the afternoon on it. Docs updated in this commit, as their triggers required: the migration table now has its version 1 row and the trap that makes such tables go stale, TOOLS explains the seventh script, and the hooks README lists the new guard. closes #3
2026-08-18 02:32:07 -05:00
{
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "219d6bf3f5b796b3ee87fce8d4b5a41d",
"entities": [
{
"tableName": "period_records",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `startDate` INTEGER NOT NULL, `endDate` INTEGER, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, `source` TEXT NOT NULL, `isConfirmed` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "startDate",
"columnName": "startDate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "endDate",
"columnName": "endDate",
"affinity": "INTEGER"
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "updatedAt",
"columnName": "updatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "source",
"columnName": "source",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isConfirmed",
"columnName": "isConfirmed",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_period_records_startDate",
"unique": true,
"columnNames": [
"startDate"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_period_records_startDate` ON `${TABLE_NAME}` (`startDate`)"
}
]
},
{
"tableName": "spotting_records",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_spotting_records_date",
"unique": true,
"columnNames": [
"date"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_spotting_records_date` ON `${TABLE_NAME}` (`date`)"
}
]
},
{
"tableName": "prediction_records",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `generatedAt` INTEGER NOT NULL, `basedOnLastConfirmedPeriodId` INTEGER, `predictedStartDate` INTEGER NOT NULL, `predictedWindowStart` INTEGER NOT NULL, `predictedWindowEnd` INTEGER NOT NULL, `estimatedOvulationDate` INTEGER, `fertileWindowStart` INTEGER, `fertileWindowEnd` INTEGER, `confidenceScore` REAL NOT NULL, `confidenceLabel` TEXT NOT NULL, `modelVersion` TEXT NOT NULL, `actualStartDate` INTEGER, `absoluteErrorDays` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "generatedAt",
"columnName": "generatedAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "basedOnLastConfirmedPeriodId",
"columnName": "basedOnLastConfirmedPeriodId",
"affinity": "INTEGER"
},
{
"fieldPath": "predictedStartDate",
"columnName": "predictedStartDate",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "predictedWindowStart",
"columnName": "predictedWindowStart",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "predictedWindowEnd",
"columnName": "predictedWindowEnd",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "estimatedOvulationDate",
"columnName": "estimatedOvulationDate",
"affinity": "INTEGER"
},
{
"fieldPath": "fertileWindowStart",
"columnName": "fertileWindowStart",
"affinity": "INTEGER"
},
{
"fieldPath": "fertileWindowEnd",
"columnName": "fertileWindowEnd",
"affinity": "INTEGER"
},
{
"fieldPath": "confidenceScore",
"columnName": "confidenceScore",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "confidenceLabel",
"columnName": "confidenceLabel",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "modelVersion",
"columnName": "modelVersion",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "actualStartDate",
"columnName": "actualStartDate",
"affinity": "INTEGER"
},
{
"fieldPath": "absoluteErrorDays",
"columnName": "absoluteErrorDays",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_prediction_records_generatedAt",
"unique": false,
"columnNames": [
"generatedAt"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_prediction_records_generatedAt` ON `${TABLE_NAME}` (`generatedAt`)"
},
{
"name": "index_prediction_records_actualStartDate",
"unique": false,
"columnNames": [
"actualStartDate"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_prediction_records_actualStartDate` ON `${TABLE_NAME}` (`actualStartDate`)"
}
]
},
{
"tableName": "not_yet_observations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER NOT NULL, `predictionId` INTEGER, `createdAt` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "predictionId",
"columnName": "predictionId",
"affinity": "INTEGER"
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_not_yet_observations_date",
"unique": true,
"columnNames": [
"date"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_not_yet_observations_date` ON `${TABLE_NAME}` (`date`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '219d6bf3f5b796b3ee87fce8d4b5a41d')"
]
}
}