diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c6326..dca5197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,11 +75,12 @@ - Added one end-to-end tomorrow-session readiness regression. A synchronized Program session retains its stable session and Program identities through - edits, two explicit exercise intervals, pre-exercise/inter-exercise/active - heart-rate ownership, finalization, full-generation publication, desktop - import, replay and correlated ACK. Raw observation timestamps, BPM, RR, - nullable stable exercise ownership and capture/session bounds are compared - on both platforms without inferring ownership from time overlap. + edits, two ordered Program-derived occurrence intervals, + pre-exercise/inter-exercise/active heart-rate ownership, finalization, + full-generation publication, desktop import, replay and correlated ACK. Raw + observation timestamps, BPM, RR, nullable stable exercise ownership and + capture/session bounds are compared on both platforms without inferring + ownership from time overlap. - Added factual synchronized heart-rate timelines to the local Web. One bounded C17 read model and loopback GET endpoint now project raw BPM/RR measurements with canonical workout exercise intervals or Sleep Diary/medication markers. Cardio history additionally presents resolved phase targets, instruction changes and calibration peak/recovery facts. History and Sleep reuse one responsive SVG component; full-sample min/max/mean statistics remain exact while only very large chart projections retain bounded bucket extrema. No sleep stage, HRV, readiness or medical inference is added. diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/ProgramSessionCardioReadinessTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/ProgramSessionCardioReadinessTest.kt index 948e684..0d94908 100644 --- a/android/app/src/test/java/com/labfytools/trainlog/data/ProgramSessionCardioReadinessTest.kt +++ b/android/app/src/test/java/com/labfytools/trainlog/data/ProgramSessionCardioReadinessTest.kt @@ -4,11 +4,7 @@ import android.content.Context import android.database.sqlite.SQLiteDatabase import androidx.test.core.app.ApplicationProvider import com.labfytools.trainlog.model.HeartRateContextKind -import com.labfytools.trainlog.model.NewExerciseProfile -import com.labfytools.trainlog.model.RecordingMode -import com.labfytools.trainlog.model.SessionExerciseDraft import com.labfytools.trainlog.model.SessionSetDraft -import com.labfytools.trainlog.model.TrackingMode import java.nio.file.Files import java.time.OffsetDateTime import java.util.UUID @@ -67,21 +63,31 @@ class ProgramSessionCardioReadinessTest { try { val programId = "pg_aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" val programSessionId = "pgs_bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" - val programExerciseId = "ex_dddddddd-dddd-4ddd-8ddd-dddddddddddd" + val programExerciseAId = "ex_dddddddd-dddd-4ddd-8ddd-dddddddddddd" + val programExerciseBId = "ex_11111111-1111-4111-8111-111111111111" val catalog = JSONObject() .put("format", "trainlog-pc-catalog") .put("version", 1) .put( "exercises", - JSONArray().put( - JSONObject() - .put("exercise_id", programExerciseId) - .put("name", "Program exercise fixture") - .put("recording_mode", "sets") - .put("tracking_mode", "reps") - .put("data_fields", 0), - ), + JSONArray() + .put( + JSONObject() + .put("exercise_id", programExerciseAId) + .put("name", "Program exercise fixture A") + .put("recording_mode", "sets") + .put("tracking_mode", "reps") + .put("data_fields", 0), + ) + .put( + JSONObject() + .put("exercise_id", programExerciseBId) + .put("name", "Program exercise fixture B") + .put("recording_mode", "sets") + .put("tracking_mode", "reps") + .put("data_fields", 0), + ), ) .toString() assertTrue(android.applyPcCatalogJson(catalog) is PcCatalogImportResult.Applied) @@ -110,22 +116,39 @@ class ProgramSessionCardioReadinessTest { .put("note", JSONObject.NULL) .put( "occurrences", - JSONArray().put( - JSONObject() - .put( - "entry_id", - "pge_cccccccc-cccc-4ccc-8ccc-cccccccccccc", - ) - .put("exercise_id", programExerciseId) - .put("equipment_id", JSONObject.NULL) - .put("load_mode", "external") - .put("rest_seconds", 90) - .put("target_sets", 1) - .put("target_reps", 8) - .put("target_duration_seconds", JSONObject.NULL) - .put("target_weight_kg", 40.0) - .put("notes", JSONObject.NULL), - ), + JSONArray() + .put( + JSONObject() + .put( + "entry_id", + "pge_cccccccc-cccc-4ccc-8ccc-cccccccccccc", + ) + .put("exercise_id", programExerciseAId) + .put("equipment_id", JSONObject.NULL) + .put("load_mode", "external") + .put("rest_seconds", 90) + .put("target_sets", 1) + .put("target_reps", 8) + .put("target_duration_seconds", JSONObject.NULL) + .put("target_weight_kg", 40.0) + .put("notes", JSONObject.NULL), + ) + .put( + JSONObject() + .put( + "entry_id", + "pge_22222222-2222-4222-8222-222222222222", + ) + .put("exercise_id", programExerciseBId) + .put("equipment_id", JSONObject.NULL) + .put("load_mode", "external") + .put("rest_seconds", 60) + .put("target_sets", 1) + .put("target_reps", 10) + .put("target_duration_seconds", JSONObject.NULL) + .put("target_weight_kg", 25.0) + .put("notes", JSONObject.NULL), + ), ), ), ) @@ -141,15 +164,6 @@ class ProgramSessionCardioReadinessTest { ProgramsImportResult.Applied(1, 0, 0), android.applyProgramsV1Json(programs), ) - val exerciseB = - (android.createExercise( - NewExerciseProfile( - "Readiness exercise B", - RecordingMode.SETS, - TrackingMode.REPS, - 0, - ), - ) as CreateExerciseResult.Created).exercise assertEquals( StartProgramSessionResult.Started, @@ -160,12 +174,12 @@ class ProgramSessionCardioReadinessTest { val sessionStartedAt = checkNotNull(opened.draft.startedAt) assertEquals(programId, opened.draft.sourceProgramId) assertEquals(programSessionId, opened.draft.sourceProgramSessionId) - val exerciseA = opened.draft.exercises.single() - val exerciseBEntry = - SessionExerciseDraft( - exercise = exerciseB, - sets = listOf(SessionSetDraft(reps = 10, weightKg = 25.0)), - ) + assertEquals(2, opened.draft.exercises.size) + val exerciseA = opened.draft.exercises[0] + val exerciseBEntry = opened.draft.exercises[1] + assertEquals(programExerciseAId, exerciseA.exercise.exerciseId) + assertEquals(programExerciseBId, exerciseBEntry.exercise.exerciseId) + assertTrue(exerciseA.entryId != exerciseBEntry.entryId) assertEquals( ActiveDraftMutationResult.Saved, android.saveActiveSessionDraft( @@ -175,7 +189,9 @@ class ProgramSessionCardioReadinessTest { exerciseA.copy( sets = listOf(SessionSetDraft(reps = 8, weightKg = 40.0)), ), - exerciseBEntry, + exerciseBEntry.copy( + sets = listOf(SessionSetDraft(reps = 10, weightKg = 25.0)), + ), ), ), ), diff --git a/docs/current_state.md b/docs/current_state.md index 1869863..af34f99 100644 --- a/docs/current_state.md +++ b/docs/current_state.md @@ -157,7 +157,7 @@ parallel implementation of its rules. | Cardio BPM guidance | `TRAINLOG_CARDIO_BPM_GUIDANCE_V1=PASS`: absolute/calibration-derived immutable BPM targets, 2 BPM hysteresis, 3 s confirmation, stale-signal suspension, durable phase/condition history, Android live guidance UI and strict Android→desktop `trainlog-cardio-guidance` V1 sync | | Cardio Web visualization | `TRAINLOG_CARDIO_WEB_VISUALIZATION_V1=PASS`: one bounded factual heart-rate timeline read model/HTTP endpoint powers Web History and Sleep with raw BPM/RR, real session/exercise/Sleep markers, guided targets/instructions and calibration facts; no sleep-stage or medical inference | | 0.1.5 validation | `TRAINLOG_0_1_5_VALIDATION_V1=SOFTWARE_PASS / DEVICE_VISUAL_PASS`: native 111/111; Android 304 tests (299 pass + 5 historical skips) and debug APK build; Web 183/183 + typecheck/build baseline. Private vc46 preserves the live HR capture and samples across Training → Cardio, drives measured guidance, and renders Sommeil and Calibration cardio without mutation. No stable release is authorized by this status. | -| Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves Program provenance, factual two-exercise timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK | +| Tomorrow session readiness | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: the private vc46 is clean on schema v31 with fresh CYCPLUS BPM and the next Program session is unambiguous; one combined regression proves two ordered occurrences loaded from the synchronized Program, Program provenance, factual timeline/HR ownership, finalization, full generation, desktop import, replay and correlated ACK | | Current operational cursor | `TRAINLOG_TOMORROW_SESSION_CARDIO_READINESS_V1=PASS`: tomorrow's recording path is ready; any stable 0.1.5 tag or release remains a separate explicit decision | | Complete synchronization gap contract | Frozen dependency contract; operational USB/Drive slices required by v0.1.2 are delivered | | Isolated synchronization test environment | `TRAINLOG_SYNC_TEST_ENV_V1=PASS/FROZEN` | diff --git a/docs/roadmap.md b/docs/roadmap.md index 73e5da8..c821091 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -86,9 +86,10 @@ Tomorrow's next synchronized Program session is unambiguously private phone remains on Android schema v31 with integrity/FK checks clean and no active draft, timeline, HR capture, guidance phase or calibration. A single combined regression now proves Program start and provenance through two -explicit exercise intervals, four HR ownership positions, finalization, full -generation, desktop import, idempotent replay and correlated ACK. This -readiness result does not authorize a stable 0.1.5 tag or release. +ordered occurrences originating in the synchronized Program payload, four HR +ownership positions, finalization, full generation, desktop import, idempotent +replay and correlated ACK. This readiness result does not authorize a stable +0.1.5 tag or release. The focused `TRAINLOG_SYNC_CAUSAL_DELETE_V1_CLOSEOUT` is complete before this cursor: complete predecessor state, imported built-in authorization,