Clean up canonical project documentation

This commit is contained in:
fy59 2026-09-06 19:36:21 +02:00
parent 18958c5001
commit e68b6cdc14
12 changed files with 1874 additions and 3946 deletions

357
AGENTS.md
View file

@ -2,186 +2,257 @@
## 1. Scope ## 1. Scope
Trainlog is composed of two applications sharing a versioned exchange format: Trainlog consists of:
- a lightweight Android application for fast workout data entry; - a native Android application for fast workout and body-data capture;
- a Unix/Linux TUI for storage, review, analysis, and visualization. - a Unix/Linux C17 ncursesw TUI for durable history, correction, analysis,
visualization, and manual synchronization;
- a small user-session PC agent, `trainlog-syncd`, for Android-triggered
synchronization;
- versioned JSON synchronization artifacts exchanged over direct MTP.
The TUI SQLite database is the canonical long-term history. The desktop SQLite database is the canonical long-term history.
JSON files are the exchange contract between Android and the TUI. Android has an independent local SQLite store for offline capture. SQLite
database files are never synchronized directly.
## 2. General development rules ## 2. Frozen compatibility boundary
Every change must respect the following rules: `TRAINLOG_FORMAT_V1` is frozen.
1. behavior is defined before implementation;
2. code must be readable and deterministic;
3. errors must be handled explicitly;
4. user data must never be silently discarded;
5. persistent formats must be versioned;
6. importing the same data repeatedly must not create duplicates;
7. every new feature must be documented;
8. affected tests must be added or updated;
9. compiler warnings are treated as defects unless explicitly justified;
10. an undocumented or untested feature is not considered complete.
## 3. TUI
The TUI is implemented in C17.
Planned dependencies:
- ncursesw;
- SQLite3;
- a deliberately selected JSON library;
- Meson;
- Ninja.
The business logic, persistence layer, and ncurses rendering layer must remain separated.
SQLite calls must not be scattered through rendering code.
Important business rules must not depend directly on ncurses.
## 4. Android
The Android application is a data-entry client.
It must remain intentionally simple and must not become the primary analytics or historical store.
It must support:
- starting a workout session;
- automatic recording of the start time;
- selecting or creating an exercise;
- entering planned sets and repetitions;
- entering actual sets and repetitions;
- entering load;
- entering planned rest time;
- entering body weight and supported measurements;
- automatic recording of the end time;
- exporting a valid Trainlog JSON file.
## 5. Documentation
Documentation is mandatory.
Primary documents:
- `README.md`: user-facing project overview;
- `docs/architecture.md`: architecture and component boundaries;
- `docs/coding_style.md`: coding and commenting conventions;
- `docs/exchange_format.md`: JSON exchange contract;
- `docs/database.md`: SQLite schema and migration policy;
- `docs/tui.md`: TUI behavior and visual rules;
- `docs/android.md`: Android behavior and scope;
- `docs/tests.md`: validation strategy and commands;
- `docs/roadmap.md`: implementation order and gates.
A behavior change must update the relevant documentation in the same change.
## 6. Code comments
Comments are mandatory when code expresses:
- an invariant;
- a format constraint;
- an architectural decision;
- non-obvious logic;
- special error handling;
- a public API;
- an important data structure;
- an assumption required for correctness.
Comments must not merely restate obvious code.
Prefer explaining why a decision exists when the reason is not obvious from the code.
## 7. Exchange format
The Trainlog format is versioned.
Each workout export must contain:
- a format identifier;
- a schema version;
- a unique session identifier;
- ISO 8601 timestamps including an explicit UTC offset.
Exercise identifiers are stable and permanent.
An exercise display name may change without changing its identifier.
Imports must be idempotent.
A published format version must never receive an incompatible semantic change. A published format version must never receive an incompatible semantic change.
## 8. TUI visual rules New synchronization or domain needs use separate, explicitly versioned
artifacts. Do not overload frozen v1 through notes, fake sets, or silent data
loss.
The TUI uses color when it improves understanding. ## 3. Exercise model
Color must never be the sole carrier of information. Exercise behavior is metadata-driven:
Important states must remain understandable in monochrome terminals. ```text
recording_mode = SETS | CONTINUOUS
tracking_mode = REPS | DURATION
data_fields = bounded supplemental field mask
```
Colors must be centralized in a dedicated theme module. Valid model-v1 combinations are:
The TUI must use `ncursesw` and handle UTF-8 correctly. ```text
SETS + REPS
SETS + DURATION
CONTINUOUS + DURATION
```
Raw ANSI escape sequences are forbidden in ncurses rendering code unless explicitly documented and justified. `CONTINUOUS + REPS` is invalid.
## 9. Database Continuous activity must not be represented as a fake performed set.
SQLite is the canonical TUI store. Actual set values are independent records. Heterogeneous repetitions are valid.
The database schema must be versioned. ## 4. Identity
Incompatible schema evolution requires an explicit migration. Stable identities use UUIDv4-based creator IDs:
Integrity constraints must be used where appropriate, including: ```text
ex_<uuid-v4> exercise
se_<uuid-v4> session
bo_<uuid-v4> body observation
sy_<uuid-v4> synchronization run
```
- foreign keys; Display names are not identities.
- unique identifiers;
- anti-duplication constraints.
## 10. Validation Import and synchronization paths must remain idempotent by stable IDs.
## 5. Desktop implementation
The desktop core is C17.
Current primary dependencies:
- ncursesw;
- SQLite3;
- utf8proc;
- libuuid;
- libudev;
- libmtp;
- Meson;
- Ninja.
Business logic, persistence, transport, and rendering remain separated.
SQLite operations must not be scattered through rendering code.
Important business rules must not depend directly on ncurses.
Strict warning policy must not be weakened to make a change compile.
## 6. Android implementation
Android is a Kotlin/Jetpack Compose capture client.
It owns local data entry and local persistence for:
- exercise catalog entries;
- sessions;
- performed sets;
- continuous activities;
- body observations.
It is not the canonical analytics store.
The Android UI is driven by exercise metadata, never by exercise-name
heuristics.
## 7. Synchronization architecture
Desktop access to Android uses physical-device discovery with `libudev` and
direct object access with `libmtp`.
Do not introduce a mandatory GVFS/FUSE mount.
Canonical exchange folder:
```text
Download/Trainlog
```
The shared desktop synchronization engine is:
```text
trainlog_sync_run()
```
Both the TUI and `trainlog-syncd` use this engine.
Android-triggered synchronization uses:
```text
trainlog-sync-request-v1.json
trainlog-sync-receipt-v1.json
```
Android -> PC data uses:
```text
trainlog-mobile-export-v1.json
```
PC -> Android catalog data uses:
```text
trainlog-pc-catalog-v1.json
```
## 8. Persistence
Desktop SQLite schema is versioned with:
```sql
PRAGMA user_version;
```
The current desktop schema is v5.
Every incompatible schema evolution requires an explicit migration and
regression coverage.
Foreign keys must be enabled.
Multi-row mutations that represent one user operation must be transactional.
## 9. Error handling
Trainlog prefers explicit failure over silent corruption.
Examples:
- malformed exchange JSON -> reject;
- unsupported version -> reject;
- duplicate stable ID -> idempotent skip or explicit conflict as defined;
- incompatible exercise profile -> reject;
- incomplete session -> preserve explicitly;
- failed persisted-session replacement -> rollback;
- synchronization failure -> record a meaningful diagnostic.
User-facing code must not intentionally return placeholders such as
`error=unknown` when a specific failure can be reported.
## 10. Documentation
Canonical documents:
- `README.md`;
- `docs/current_state.md`;
- `docs/architecture.md`;
- `docs/coding_style.md`;
- `docs/exchange_format.md`;
- `docs/exercise_data_model.md`;
- `docs/database.md`;
- `docs/tui.md`;
- `docs/android.md`;
- `docs/sync_exchange.md`;
- `docs/tests.md`;
- `docs/roadmap.md`;
- `CHANGELOG.md`.
Checkpoint history belongs in Git history and `docs/reviews`; canonical
documents describe the current state rather than accumulating obsolete
`NEXT` sections.
## 11. Validation
Before every meaningful push: Before every meaningful push:
- build; ```bash
- run tests; meson compile -C build
- verify formatting; meson test -C build --print-errorlogs
- verify compiler warnings;
- validate JSON examples against the schema;
- verify documentation impacted by the change.
The repository must not knowingly be pushed in a broken state. python tools/validate_json.py
python tools/validate_import_contract.py
## 11. Git workflow git diff --check
git status --short
```
Forgejo is the primary repository. When Android code changes:
Primary remote: ```bash
cd android
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug
```
`ssh://git@git.labfytools.com:2223/fy59/trainlog.git` Run ASan/UBSan at meaningful C implementation checkpoints.
Hardware-dependent MTP tests remain explicit manual validations and are not
required to run in CI without a connected unlocked Android device.
## 12. Git workflow
Forgejo is primary:
```text
ssh://git@git.labfytools.com:2223/fy59/trainlog.git
```
GitHub is a mirror: GitHub is a mirror:
`git@github.com:labfytools/trainlog.git` ```text
git@github.com:labfytools/trainlog.git
Normal development must push to Forgejo. ```
Do not develop directly against the GitHub mirror. Do not develop directly against the GitHub mirror.
## 12. Definition of Done ## 13. Definition of Done
A task is complete only when: A task is complete only when:
- the expected behavior is implemented; - behavior is implemented;
- the code builds without accepted warnings; - the affected code builds without accepted warnings;
- relevant tests pass; - relevant tests pass;
- new error paths are handled; - new error paths are explicit;
- documentation is current; - persistent-format changes have migrations;
- examples and schemas are updated when required; - synchronization remains idempotent where applicable;
- documentation describes the resulting state;
- no known regression is intentionally left behind. - no known regression is intentionally left behind.

View file

@ -1,395 +1,86 @@
# Changelog # Changelog
All notable changes to Trainlog will be documented in this file. All notable changes to Trainlog are documented here.
Detailed implementation chronology remains available in Git history and
`docs/reviews`. This file summarizes the current unreleased product baseline.
## Unreleased ## Unreleased
### Added ### Added
- Frozen Trainlog JSON v1 contract. - native Kotlin/Compose Android capture client;
- SQLite persistence foundation. - Android local exercise, session, continuous-activity, and body persistence;
- Direct workout entry. - C17/ncursesw desktop TUI with direct session entry and durable SQLite history;
- Exercise catalog. - profile-aware exercise model using recording mode, tracking mode, and
- Body tracking. supplemental fields;
- Colored ncursesw dashboard. - continuous activity persistence without synthetic sets;
- Body-weight graph. - variable repetition-set input including `5x10`, explicit lists, and pyramid
- Arrow-key and F1-F4 navigation. shorthand such as `4..10..4`;
- Highlighted list selections. - persisted desktop session editing and exercise removal;
- Navigable session history. - Android current-session draft exercise removal;
- body-observation history, editing, graphs, and normalized overlays;
- exercise performance history;
- direct physical Android USB/MTP discovery with libudev/libmtp;
- direct MTP read/write/list/delete support without filesystem mounts;
- Android full mobile snapshot export;
- strict idempotent desktop mobile importer;
- PC canonical exercise-catalog publication to Android;
- automatic Android snapshot maintenance;
- Android `Synchroniser maintenant` request flow;
- shared C bidirectional synchronization engine;
- `trainlog-syncd` user-session synchronization agent;
- request/receipt synchronization protocol;
- stable `sy_<uuid-v4>` synchronization identities;
- structured synchronization history with selectable TUI list/detail views.
### Changed ### Changed
- Exercise selection now uses interactive keyboard navigation. - desktop SQLite schema evolved to v5;
- Body tracking is now a persistent history screen instead of entry-only. - schema v5 permits targetless set-session rows for actual-only mobile data;
- TUI polish is prioritized before Android development. - heterogeneous performed sets are preserved without inventing a uniform target;
- Android/desktop synchronization uses dedicated versioned artifacts instead of
modifying frozen Trainlog JSON v1;
- PC and TUI synchronization paths now share one engine;
- Android PC-created artifact access uses a persistent SAF grant for
`Download/Trainlog`;
- the Android data package is no longer accidentally hidden by a broad
repository `data/` ignore rule;
- user-facing session history timestamps use `DD/MM/YYYY HH:MM` while canonical
storage remains RFC3339.
<!-- TRAINLOG_TUI_V02_CHANGELOG --> ### Fixed
### TUI v0.2 checkpoint
Added: - stale schema-v4 importer call after desktop schema v5 migration;
- stale schema-v4 guard in the PC catalog exporter;
- missing `sy` prefix support in the UUID creator;
- synchronization failures that previously surfaced only as `error=unknown`;
- Android folder-selection UX so a wrong SAF folder can be changed without
clearing application data;
- libmtp terminal output leaking into ncurses rendering.
- bordered colorful dashboard; ### Validation
- arrow-key and F-key navigation;
- weight history visualization;
- flat-series weight graph handling;
- navigable session history;
- complete read-only workout details.
Planned next: Current validated baseline:
- human duration input such as `1:30`, `1m30`, `2m`, and `45s` (implemented);
- automatic minute/second display formatting (implemented);
- history graphs for all body measurements (implemented);
- left/right asymmetry summaries (implemented).
<!-- TRAINLOG_TUI_V02_CHANGELOG _END -->
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_CHANGELOG -->
### Current TUI checkpoint
Implemented:
- human duration input;
- human minute/second display;
- graphs for every persisted body metric;
- recent values for every body metric;
- left/right asymmetry summaries.
Next:
- normalized global body overlay graph (implemented);
- metric legend using both colors and symbols;
- percentage evolution summary;
- richer dashboard weight graph (implemented);
- dashboard previous-weight delta and min/max (implemented);
- compact body/asymmetry dashboard summary (implemented).
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_CHANGELOG _END -->
### Dashboard graph-only refinement
- removed redundant weight/min/max/asymmetry summary lines;
- dashboard now uses a global body evolution graph;
- dates are shown on X;
- Y is percentage evolution from each metric baseline;
- legend shows metric name, unit, and latest percentage change.
### Dashboard rolling 12 months
- added a fixed 12-month rolling X axis;
- empty months remain visible and empty;
- no zero fill or interpolation across missing months;
- multiple readings in one month use the last monthly value.
### Exercise performance history
- added Enter-to-open exercise performance detail;
- added mode-aware representative best-set history;
- added terminal performance graph;
- assistance explicitly treats lower assistance as better;
- best recorded set remains distinct from measured max.
### Session type schema v2
- added local `training` / `max_test` session classification;
- added transactional SQLite schema migration v1 -> v2;
- preserved migrated sessions as `training`;
- kept Trainlog JSON v1 frozen and unchanged.
### Transaction-safe session editing foundation
- added exact bounded loading of persisted exercise/set values;
- added atomic replacement of recorded session exercise/set rows;
- preserved the parent session row and linked body observations;
- added rollback coverage for failed replacements.
### Body observation history and correction
- redesigned `F4 Corps` around recorded measurement dates;
- added a framed trend graph above the newest-first record list;
- added PageUp/PageDown and a visual ncurses scrollbar;
- added two-page detail views with `e Modifier` on every page;
- added correction of existing observations without changing their timestamp or
session link;
- added `-` to clear one erroneous measurement and Escape to cancel the edit.
<!-- TRAINLOG_EDITABILITY_NAV_CHANGELOG -->
### TUI editability and navigation checkpoint
- added local `training` / `max_test` selection to the session workflow;
- added safe persisted-session editing while preserving parent session identity;
- added newest-first body-observation history, detail pages, and editing;
- added immediate Escape cancellation to text/numeric entry paths;
- added a shared top navigation bar with `0 Accueil`, `1-4`, and `F1-F4`;
- added Tab/Shift+Tab focus on multi-zone screens;
- focused frames use a yellow border/title without recoloring content;
- added consistent ASCII banners and ncurses frames to secondary detail views;
- added direct exercise creation from the in-session exercise chooser;
- kept the final rolling 12-month `MM/YY` dashboard label inside its frame.
<!-- TRAINLOG_EDITABILITY_NAV_CHANGELOG _END -->
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG -->
### Direct Android USB/MTP transport foundation
- added `libudev` discovery of physical MTP devices;
- filtered MTP USB interface children to avoid duplicate phones;
- added exact bus/device matching into `libmtp`;
- added MTP storage enumeration;
- added root-folder discovery/creation;
- added direct MTP text-file upload;
- added direct folder-child listing;
- added direct MTP file download;
- split cached and uncached libmtp open paths where required;
- validated a real Android internal-storage write/list/read roundtrip;
- kept Android access mount-free: no GVFS/FUSE mount lifecycle is required.
<!-- TRAINLOG_MTP_TRANSPORT_CHANGELOG _END -->
<!-- TRAINLOG_SYNC_FINAL_CHANGELOG -->
### Sync TUI page
- added `5 Sync / F5` to primary navigation;
- added dedicated Sync ASCII banner and framed page;
- added live direct-MTP device and storage status;
- suppressed libmtp terminal output during ncurses rendering;
- added focused-frame navigation with Tab/Shift+Tab;
- added clean list navigation and scrollbar behavior;
- exposed Android -> PC session/exercise/body synchronization directions;
- exposed PC -> Android canonical exercise-catalog direction;
- kept frozen Trainlog session JSON v1 unchanged.
<!-- TRAINLOG_SYNC_FINAL_CHANGELOG _END -->
<!-- TRAINLOG_PROFILE_AWARE_CHANGELOG -->
### Profile-aware and continuous exercise tracking
- added set-based versus continuous recording organization;
- added speed and distance supplemental-field metadata;
- added profiled catalog creation API;
- added profile-aware exercise creation in the TUI;
- added inline profiled exercise creation while recording a session;
- migrated SQLite persistence to schema v4;
- added one-to-one continuous activity persistence;
- kept continuous activities out of `performed_sets`;
- added profile-aware session detail loading;
- added continuous duration/speed/distance history display;
- made continuous TUI duration entry explicitly minute-based;
- preserved frozen session JSON v1 unchanged.
<!-- TRAINLOG_PROFILE_AWARE_CHANGELOG _END -->
<!-- TRAINLOG_ANDROID_LOCAL_CHANGELOG -->
### Android local client
- added native Kotlin/Jetpack Compose Android client;
- matched Trainlog TUI visual language;
- added minimal themed `T` launcher icon;
- added exercise catalog and profile-aware exercise creation;
- added inline exercise creation from session recording;
- added persistent local session recording;
- preserved SETS versus CONTINUOUS persistence semantics;
- added local session history and detail views;
- added persistent body measurement recording;
- validated the application on a real Samsung device through ADB.
<!-- TRAINLOG_ANDROID_LOCAL_CHANGELOG _END -->
<!-- TRAINLOG_SYNC_FOUNDATION_CHANGELOG -->
### Bidirectional synchronization foundation
- validated Android-to-PC domain snapshot transfer through direct MTP;
- added strict transactional and idempotent desktop mobile import;
- added canonical PC exercise-catalog export;
- validated PC-to-Android catalog publication through direct MTP;
- added Android Storage Access Framework access for PC-created catalog files;
- made the Android synchronization folder selection recoverable/changeable;
- kept synchronization artifacts separate from frozen Trainlog session JSON v1;
- documented the next synchronization architecture: structured history,
detailed sync inspection, common sync engine and PC-side `trainlog-syncd`.
<!-- TRAINLOG_SYNC_FOUNDATION_CHANGELOG _END -->
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 -->
## Variable repetition sets
Trainlog preserves each performed set independently.
Accepted repetition input:
```text ```text
5x10 TRAINLOG_FORMAT_V1=FROZEN
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
`4..10..4` expands to:
```text
4,5,6,7,8,9,10,9,8,7,6,5,4
```
Desktop schema v5 permits targetless `SETS` rows for actual-only mobile
observations. Synchronization therefore does not invent a uniform target when
performed sets are heterogeneous.
`performed_sets` remains the source of truth for actual per-set values.
Existing planned desktop sessions may still carry explicit target sets/reps or
target durations.
`trainlog-mobile-export` v1 keeps ordered heterogeneous `sets[]`.
Frozen `TRAINLOG_FORMAT_V1` is unchanged.
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 _END -->
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL -->
## Variable sets and session exercise removal checkpoint
Validated functionality in this checkpoint:
```text
VARIABLE_REPETITION_SETS=PASS
REPETITION_SHORTHAND_5x10=PASS
REPETITION_EXPLICIT_LIST=PASS
REPETITION_PYRAMID=PASS
DESKTOP_SCHEMA_V5=PASS DESKTOP_SCHEMA_V5=PASS
V4_TO_V5_MIGRATION_REGRESSION=PASS DESKTOP_TESTS=19/19 PASS
MOBILE_HETEROGENEOUS_SET_IMPORT=PASS
MOBILE_IMPORT_IDEMPOTENCE=PASS
NO_FAKE_UNIFORM_TARGET=PASS
ANDROID_SESSION_DRAFT_EXERCISE_REMOVE=PASS ANDROID_BUILD=PASS
DESKTOP_SESSION_EXERCISE_REMOVE=PASS ANDROID_LOCAL_WORKFLOWS=PASS
```
Accepted repetition examples: USB_MTP_DETECTION=PASS
MTP_ROUNDTRIP=PASS
```text ANDROID_TO_PC_MTP=PASS
5x10 PC_TO_ANDROID_MTP_PUBLISH=PASS
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
A heterogeneous mobile session is persisted as ordered `performed_sets`.
The desktop does not invent `target_sets`, `target_reps` or
`target_duration_seconds` for actual-only mobile observations.
On Android, an exercise already added to the current session can be removed
before saving the session.
On the desktop TUI, session editing already supports:
```text
d supprimer
```
for removing the selected exercise from a current or persisted session draft.
The database replacement remains transactional.
`TRAINLOG_FORMAT_V1` remains frozen and unchanged.
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL _END -->
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 -->
## Shared bidirectional synchronization v1
Validated architecture:
```text
Android local write
-> automatic mobile snapshot
Android "Synchroniser maintenant"
-> trainlog-sync-request-v1.json
trainlog-syncd
-> shared C synchronization engine
-> Android → PC mobile import
-> PC → Android catalog publish
-> trainlog-sync-receipt-v1.json
Android
-> receipt matched by request_id
-> PC catalog applied locally
-> final result displayed
```
The ncurses TUI and `trainlog-syncd` call the same
`trainlog_sync_run()` implementation.
Direct libmtp remains mandatory. No filesystem mount and no SQLite-file
synchronization are introduced.
### Concurrency
The shared engine owns:
```text
$XDG_DATA_HOME/trainlog/sync.lock
```
A TUI-triggered transaction waits for the lock. Daemon request polling is
non-blocking and retries later.
### Sync history
Every actual synchronization transaction creates:
```text
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.json
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.txt
```
and appends a compact entry to:
```text
$XDG_DATA_HOME/trainlog/sync_history.log
```
The TUI behaves like:
```text
git log
↑/↓ select synchronization
git show
Enter opens structured detail
```
Legacy three-field history entries remain readable but have no structured
detail file.
### Android request and receipt
Request:
```text
format = trainlog-sync-request
version = 1
```
Receipt:
```text
format = trainlog-sync-receipt
version = 1
```
The receipt carries the originating `request_id`, a generated `sync_id`,
status, summary and synchronization counts. Android ignores a receipt for a
different request ID.
### User service
Install/refresh the user service with:
```text
bash tools/install_syncd_user.sh
```
No root privilege is required.
### Status
```text
COMMON_SYNC_ENGINE=PASS COMMON_SYNC_ENGINE=PASS
TUI_SYNC_LOG_SHOW=PASS
TRAINLOG_SYNCD=PASS TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS ANDROID_SYNC_RECEIPT=PASS
TUI_SYNC_LOG_SHOW=PASS
BIDIRECTIONAL_SYNC_V1=PASS BIDIRECTIONAL_SYNC_V1=PASS
``` ```
Frozen `TRAINLOG_FORMAT_V1` remains unchanged.
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 _END -->

206
README.md
View file

@ -1,75 +1,189 @@
# Trainlog # Trainlog
Trainlog is a local-first workout log composed of: Trainlog is a local-first workout and body-tracking system with two user
interfaces:
- a lightweight Android data-entry application; - a native Android application optimized for fast data entry during training;
- a colorful Unix/Linux TUI for history, statistics, graphs, and progress tracking. - a C17/ncursesw TUI used for durable history, editing, visualization,
statistics, and synchronization.
## Project goals The desktop SQLite database is the canonical long-term history. Android keeps
its own local SQLite database so recording remains usable independently from the
desktop.
Android is optimized for fast use during a workout: ## Current status
- start and end time; ```text
- exercise selection; TRAINLOG_FORMAT_V1=FROZEN
- new exercise creation;
- planned sets and repetitions;
- actual sets and repetitions;
- load;
- rest time;
- body weight;
- body measurements;
- JSON export.
The TUI is the main application: DESKTOP_SCHEMA_V5=PASS
ANDROID_LOCAL_WORKFLOWS=PASS
- import Android exports; VARIABLE_REPETITION_SETS=PASS
- create sessions directly from the terminal; CONTINUOUS_ACTIVITY_TRACKING=PASS
- maintain the canonical exercise catalog;
- maintain SQLite history; DIRECT_MTP_TRANSPORT=PASS
- display workout history; COMMON_SYNC_ENGINE=PASS
- track body weight; TRAINLOG_SYNCD=PASS
- track body measurements; ANDROID_TRIGGERED_SYNC=PASS
- track performance; ANDROID_SYNC_RECEIPT=PASS
- display colorful terminal graphs; BIDIRECTIONAL_SYNC_V1=PASS
- export data for external use.
DESKTOP_TESTS=19/19 PASS
ANDROID_BUILD=PASS
```
## Architecture ## Architecture
```text ```text
Android Android application
local SQLite store
| |
| Trainlog JSON automatic mobile snapshot
v
Trainlog TUI
| |
v v
SQLite Download/Trainlog on Android storage
|
| direct MTP / libmtp
v
trainlog_sync_run()
/ \
/ \
Android -> PC PC -> Android
snapshot import catalog publish
\ /
\ /
sync receipt
|
v
Android
Desktop TUI --------------------+
| |
+-- same sync engine -------+
|
v
desktop SQLite
canonical long-term history
``` ```
The TUI SQLite database is the canonical long-term store. No SQLite database file is copied between devices. The desktop does not require
a GVFS/FUSE mount of the phone.
The JSON exchange format is versioned and designed for idempotent imports. ## Exercise model
Trainlog does not infer behavior from exercise names.
```text
recording_mode = SETS | CONTINUOUS
tracking_mode = REPS | DURATION
data_fields = SPEED_KMH | DISTANCE_KM
```
Valid model-v1 combinations are:
```text
SETS + REPS
SETS + DURATION
CONTINUOUS + DURATION
```
Actual repetition sets are stored independently. Compact input supports:
```text
5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
## Repository layout ## Repository layout
```text ```text
android/ Android application android/ native Kotlin/Compose Android client
tui/ C17 ncursesw application tui/ C17 ncursesw desktop application and core
docs/ Canonical project documentation docs/ canonical project documentation
format/ JSON schema and exchange-format material format/ frozen Trainlog JSON v1 schema material
examples/ Valid exchange examples examples/ valid frozen-format examples
tools/ Development and validation tools tests/ fixtures and cross-component tests
tools/ validators, import/export helpers, sync daemon tooling
``` ```
## Desktop build and validation
```bash
meson setup --reconfigure build
meson compile -C build
meson test -C build --print-errorlogs
python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check
```
## Android build
The local Android SDK is intentionally not committed. Configure it with either
`ANDROID_HOME` or `android/local.properties`.
Example:
```bash
cd android
printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.properties
JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
./gradlew assembleDebug
```
## Android-triggered synchronization
Build the desktop first, then install the user service:
```bash
bash tools/install_syncd_user.sh
```
Check it with:
```bash
systemctl --user is-active trainlog-syncd.service
tail -f ~/.local/state/trainlog/syncd.log
```
On Android:
```text
Sync
-> Synchroniser maintenant
```
The request is consumed by `trainlog-syncd`, the shared bidirectional engine
runs, a receipt is returned to Android, and the PC catalog is applied locally.
## Documentation
- `docs/current_state.md`: compact canonical implementation snapshot;
- `docs/architecture.md`: component and ownership boundaries;
- `docs/exercise_data_model.md`: exercise semantics;
- `docs/database.md`: desktop SQLite schema and migrations;
- `docs/android.md`: Android behavior;
- `docs/tui.md`: desktop TUI behavior;
- `docs/sync_exchange.md`: MTP synchronization artifacts and protocol;
- `docs/exchange_format.md`: frozen Trainlog JSON v1 contract;
- `docs/tests.md`: validation strategy;
- `docs/roadmap.md`: completed gates and future cursor;
- `AGENTS.md`: development contract.
## Development principles ## Development principles
- local-first; - local-first;
- no mandatory cloud account; - no mandatory cloud account;
- user-owned data; - user-owned data;
- versioned persistent formats; - versioned persistent and exchange formats;
- stable exercise identifiers; - stable identities;
- idempotent imports; - idempotent synchronization;
- documentation and tests are part of every feature; - no fake data representation to force incompatible models together;
- clear separation between UI, business logic, and persistence. - strict compiler warnings;
- documentation and tests are part of feature completion.
See `AGENTS.md` for the development contract.

View file

@ -1,556 +1,119 @@
# Android Application # Android application
## 1. Purpose ## 1. Purpose
The Android application is a lightweight training-session recorder. The Android application is Trainlog's low-friction capture client.
Its design priority is low-friction data entry during a workout. It is a native Kotlin/Jetpack Compose application with local SQLite persistence.
It is not the canonical history or analytics application. The desktop remains the canonical long-term history and analytics store.
## 2. Session flow ## 2. Implemented navigation
```text
Start session
|
v
record started_at
|
v
select/create exercise
|
v
enter target + planned rest
|
v
record actual sets
|
v
optional body data / notes
|
v
Finish session
|
v
record ended_at
|
v
validate + export Trainlog JSON
```
## 3. Exercise catalog
The Android application keeps a local exercise catalog so names are not retyped every session.
Creating an exercise requires:
- display name;
- tracking mode: repetitions or duration.
The application generates a stable `exercise_id`.
A new exercise used in an exported session is included in the top-level session export metadata and is therefore importable by the TUI.
The Android application must prevent accidental duplicate normalized names according to the Trainlog v1 contract.
## 4. Fast exercise form
For a repetition exercise, the basic form is conceptually:
```text
Exercise Presse à cuisses
Load mode External
Load 80 kg
Sets 4
Repetitions 5
Rest 60 s
```
For a timed exercise:
```text
Exercise Gainage ventral
Load mode None
Sets 3
Duration 45 s
Rest 60 s
```
The application should remember practical defaults from the previous use of an exercise when that reduces typing, but remembered UI defaults are not part of the exchange-format contract.
## 5. Load modes
The user chooses only when relevant:
- none;
- external;
- assistance.
`external` covers free weights and machine-displayed load.
`assistance` stores a positive assistance value.
The UI should label assistance explicitly so it cannot be confused with added resistance.
## 6. Actual work
The application should pre-populate actual sets from the target.
The user edits only what differs.
Example target:
```text
5 / 5 / 5 / 5
```
Actual:
```text
5 / 5 / 5 / 3
```
The export preserves both target and actual values.
Zero actual repetitions are valid for a real failed attempt.
A planned exercise may also have zero actual sets if it was never started.
## 7. Rest
`rest_seconds` is the planned rest duration for the exercise.
v1 does not require a running rest timer and does not serialize measured per-set rest.
A timer can be added later as UI behavior without changing the v1 format.
## 8. Timestamps
`started_at` is recorded automatically when the session starts.
`ended_at` is recorded automatically when the user finishes the session.
An active/interrupted local session may exist without `ended_at`.
The application must never invent an end timestamp merely to make export validation pass.
## 9. Body data
Optional session-associated data:
- body weight;
- neck;
- shoulders;
- chest;
- waist;
- hips;
- left/right arm;
- left/right forearm;
- left/right thigh;
- left/right calf.
The Android UI does not need to force these fields during every workout.
## 10. Notes
Session and exercise notes are optional.
The initial Android UI may omit note controls without violating v1, because the fields are optional.
## 11. Export
Before export, Android must enforce both:
- JSON structural validity;
- Trainlog v1 semantic validity.
A malformed or semantically inconsistent file must not be exported as a completed Trainlog document.
## 12. Non-goals
Initial Android versions do not need:
- analytics;
- complex graphs;
- cloud accounts;
- remote databases;
- social features;
- muscle classification;
- distance/cardio metrics;
- per-set rest measurement.
## 13. Identifier generation
When Android creates a new exercise, it generates:
```text
ex_<random UUID v4>
```
When Android creates a new session, it generates:
```text
se_<random UUID v4>
```
Display-name slugs must not be used as persistent identifiers.
The visible exercise name remains independent from identity.
## 14. Catalog conflict behavior
Android must prevent duplicate normalized names inside its own local catalog.
A valid Android export can still conflict with an independently edited TUI catalog.
The TUI owns final reconciliation.
Android must not assume that a matching display name means two different IDs may be silently merged.
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT -->
## 15. USB file-transfer transport
The initial Android/Linux integration uses standard Android **file transfer
(MTP)** mode.
The Linux TUI/core accesses the device directly with `libudev` + `libmtp`.
Trainlog does not require a mounted Android filesystem.
Validated Linux-side capabilities:
```text
USB_MTP_DETECTION=PASS
MTP_STORAGE_ACCESS=PASS
MTP_WRITE=PASS
MTP_READ=PASS
MTP_ROUNDTRIP=PASS
```
The transport foundation currently uses a root `Trainlog` folder for physical
validation.
The final JSON exchange subdirectory/naming convention is defined in the next
slice before the Android recorder depends on it.
The Android application itself will only need to produce a valid frozen
Trainlog JSON v1 document and place it in the agreed exchange area.
<!-- TRAINLOG_ANDROID_MTP_TRANSPORT _END -->
<!-- TRAINLOG_ANDROID_NEXT_SLICE -->
## 16. Current Android implementation cursor
The Linux transport and Sync TUI foundations are complete enough to begin the
Android client.
Initial Android development uses fictitious data.
First Android slice:
```text
1. application scaffold
2. local exercise catalog
3. create/select exercise
4. session form
5. actual set entry
6. optional body measurements
7. fictitious completed session
```
Transport/export is added only after the local recorder workflow is comfortable.
The Android client must preserve stable exercise IDs so a catalog snapshot from
the PC can normalize exercise selection on both sides.
<!-- TRAINLOG_ANDROID_NEXT_SLICE _END -->
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY -->
## Profile-aware Android forms
Android uses the same exercise metadata as the TUI:
```text
recording_mode
tracking_mode
data_fields
```
A continuous `Marche` configured with speed displays only:
```text
Durée
Vitesse
```
and no set count.
Creating an exercise directly inside session entry must configure this metadata
before adding it to the catalog/session.
<!-- TRAINLOG_ANDROID_PROFILE_AWARE_ENTRY _END -->
<!-- TRAINLOG_ANDROID_PROFILE_CURSOR -->
## Android implementation cursor
Android is the next implementation area.
The application must share Trainlog's visual language with the TUI.
Theme direction:
```text
dark background
cyan/teal Trainlog accent
yellow active/focus role
green success
red error
```
Launcher icon:
```text
T
```
Only the letter `T`, using Trainlog theme colors.
Required recording sections:
```text
Séance
Exercice
Mensurations
```
Session recording must allow creating a new exercise inline without leaving the
session flow.
Android forms are driven by the same profile metadata as desktop:
```text
recording_mode
tracking_mode
data_fields
```
Examples:
```text
SETS + REPS
sets / reps / load / rest
SETS + DURATION
sets / duration / optional load / rest
CONTINUOUS + DURATION + SPEED_KMH
duration / speed
```
The app must never infer an input form from an exercise display name.
Initial development uses fictitious records. Test/development data is removed
before normal production use starts.
<!-- TRAINLOG_ANDROID_PROFILE_CURSOR _END -->
<!-- TRAINLOG_ANDROID_SCAFFOLD_IMPLEMENTED -->
## Android scaffold implementation
The Android project now lives in:
```text
android/
```
It is a native Kotlin + Jetpack Compose application with a custom Trainlog
visual layer rather than default Material presentation.
Implemented scaffold navigation:
```text ```text
Accueil Accueil
├── Enregistrer une séance ├── Enregistrer une séance
│ ├── Ajouter depuis le catalogue
│ └── Créer un nouvel exercice
│ └── returns to session flow
├── Enregistrer un exercice ├── Enregistrer un exercice
└── Enregistrer des mensurations ├── Enregistrer des mensurations
├── Historique des séances
└── Synchroniser avec le PC
``` ```
The launcher icon is a minimal themed `T`. ## 3. Local persistence
The UI reuses the Trainlog visual roles from the TUI: Android local database version:
```text ```text
background 3
accent/cyan
success/green
warning/yellow
error/red
muted/blue
graph/magenta
``` ```
Android forms will be driven by: Domain tables cover:
```text
recording_mode
tracking_mode
data_fields
```
The scaffold intentionally does not implement persistence or MTP yet.
Next:
```text
ANDROID_LOCAL_MODEL_AND_PERSISTENCE=NEXT
ANDROID_SESSION_FORM=AFTER
ANDROID_MTP_SYNC=AFTER_LOCAL_WORKFLOW
```
<!-- TRAINLOG_ANDROID_SCAFFOLD_IMPLEMENTED _END -->
<!-- TRAINLOG_ANDROID_LOCAL_CATALOG -->
## Android local catalog checkpoint
The Android application now has a persistent local exercise catalog.
Implemented:
```text
Android SQLite exercise database
profile-aware exercise model
standalone exercise creation
inline exercise creation from session flow
catalog survives application restart
session screen refreshes after inline creation
```
Android uses the same semantic axes as desktop:
```text
recording_mode
tracking_mode
data_fields
```
Known supplemental fields remain:
```text
SPEED_KMH
DISTANCE_KM
```
Continuous creation forces duration tracking. Set-based creation keeps
supplemental continuous fields disabled.
The local Android schema is intentionally independent from the desktop SQLite
schema. Synchronization later exchanges versioned domain data rather than
copying SQLite database files.
Next:
```text
ANDROID_SESSION_RECORDING=NEXT
ANDROID_BODY_PERSISTENCE=AFTER
MTP_SYNC=AFTER_LOCAL_WORKFLOWS
```
<!-- TRAINLOG_ANDROID_LOCAL_CATALOG _END -->
<!-- TRAINLOG_ANDROID_SESSION_RECORDING -->
## Android session recording checkpoint
Android can now build and persist real local sessions.
Flow:
```text
Session
→ choose catalog exercise
→ profile-aware entry form
→ add exercise to session draft
→ repeat for additional exercises
→ save session
```
Profile-aware forms:
```text
SETS + REPS
set count
repetitions per set
SETS + DURATION
set count
duration per set
CONTINUOUS + DURATION
duration minutes
configured speed/distance fields
```
Persistence mirrors the domain split:
```text ```text
exercises
sessions sessions
session_exercises session_exercises
performed_sets performed_sets
continuous_activity continuous_activity
body_observations
``` ```
Continuous exercises do not create fake performed sets. This database is Android-local. It is not copied to the PC.
The Android local database version is now 2. ## 4. Exercise catalog
Next: Exercise creation records:
```text ```text
ANDROID_SESSION_HISTORY=NEXT name
ANDROID_BODY_PERSISTENCE=AFTER recording_mode
MTP_SYNC=AFTER_LOCAL_WORKFLOWS tracking_mode
data_fields
``` ```
<!-- TRAINLOG_ANDROID_SESSION_RECORDING _END -->
<!-- TRAINLOG_ANDROID_SESSION_HISTORY --> Stable identity:
## Android session history checkpoint
Android now exposes persisted local sessions through:
```text ```text
Accueil ex_<uuid-v4>
→ Consultation
→ Historique des séances
→ Détail séance
``` ```
Detail rendering remains profile-aware: The UI rejects invalid profile combinations and local normalized-name
collisions.
An exercise may be created standalone or inline while building a session.
## 5. Session recording
Stable session identity:
```text ```text
SETS + REPS se_<uuid-v4>
one line per performed set with reps
SETS + DURATION
one line per performed set with duration
CONTINUOUS
duration
configured speed
configured distance
``` ```
The history reader uses the persisted session snapshot metadata rather than Session entry is profile-aware.
inferring behavior from exercise names.
Next: ### Sets + repetitions
Actual set values may be heterogeneous.
Compact entry supports:
```text ```text
ANDROID_BODY_PERSISTENCE=NEXT 5x10
ANDROID_LOCAL_WORKFLOWS_THEN_MTP 4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
``` ```
<!-- TRAINLOG_ANDROID_SESSION_HISTORY _END -->
<!-- TRAINLOG_ANDROID_BODY_PERSISTENCE --> ### Sets + duration
## Android body measurement checkpoint
The Android body workflow is now persistent and uses the same measurement set Each performed set stores its own duration.
as the TUI.
Fields: ### Continuous + duration
The form asks for duration and only the configured supplemental fields such as
speed or distance.
Continuous work does not create fake sets.
## 6. Session draft editing
Before a session is saved, an exercise already added to the draft can be
removed.
Removing one exercise does not alter the exercise catalog entry itself.
## 7. Session history
Android exposes persisted local session history and profile-aware detail.
Set-based history renders ordered performed sets.
Continuous history renders its one activity record with configured supplemental
values.
## 8. Body measurements
Supported metrics:
```text ```text
weight weight
@ -568,304 +131,114 @@ left/right calf
Rules: Rules:
```text ```text
empty field = measurement not taken empty field = not measured
at least one positive metric required at least one positive metric required
comma or dot accepted for decimal entry comma or dot accepted for decimal entry
``` ```
Android SQLite schema version: Stable identity:
```text ```text
3 bo_<uuid-v4>
``` ```
The body screen also shows the five most recent observations. ## 9. Automatic mobile snapshot
At this point the three primary Android recording workflows are locally Android maintains:
functional:
```text
session recording
exercise creation
body measurement recording
```
Next:
```text
ANDROID_LOCAL_POLISH_AND_VALIDATION=NEXT
MTP_SYNC=AFTER_LOCAL_CHECKPOINT
```
<!-- TRAINLOG_ANDROID_BODY_PERSISTENCE _END -->
<!-- TRAINLOG_ANDROID_LOCAL_WORKFLOWS_PASS -->
## Local Android workflows — validated
```text
ANDROID_SCAFFOLD=PASS
ANDROID_THEME_PARITY=PASS
ANDROID_EXERCISE_CREATE=PASS
ANDROID_INLINE_EXERCISE_CREATE=PASS
ANDROID_SESSION_RECORDING=PASS
ANDROID_SESSION_HISTORY=PASS
ANDROID_BODY_RECORDING=PASS
ANDROID_LOCAL_WORKFLOWS=PASS
```
The application is now locally usable for its three primary recording flows:
```text
session
exercise
body measurements
```
Session and history rendering are profile-aware.
The Android-local SQLite database is not a synchronization format.
Next:
```text
ANDROID_MTP_SYNC=NEXT
```
<!-- TRAINLOG_ANDROID_LOCAL_WORKFLOWS_PASS _END -->
<!-- TRAINLOG_MOBILE_EXPORT_V1 -->
## MTP mobile export v1
Android now prepares a versioned full mobile snapshot at:
```text ```text
Download/Trainlog/trainlog-mobile-export-v1.json Download/Trainlog/trainlog-mobile-export-v1.json
``` ```
The file contains: The snapshot is refreshed after relevant local changes, including exercise,
session, body-observation, and PC-catalog updates.
```text The user does not need a separate manual export step before synchronization.
exercise profiles
sessions
body observations
```
It is explicitly separate from frozen `TRAINLOG_FORMAT_V1`. ## 10. PC catalog access
Desktop direct-MTP validation is available through: PC-created files are accessed through a persistent Storage Access Framework
grant.
```text The selected folder must be:
./build/tui/trainlog-mtp-mobile-export-probe
```
The probe traverses:
```text
internal storage
→ Download
→ Trainlog
→ trainlog-mobile-export-v1.json
```
and downloads it directly through libmtp without a mount.
Next after hardware PASS:
```text
DESKTOP_MOBILE_EXPORT_IMPORT=NEXT
PC_TO_ANDROID_CATALOG=AFTER
```
<!-- TRAINLOG_MOBILE_EXPORT_V1 _END -->
<!-- TRAINLOG_ANDROID_SYNC_FOLDER_CHECKPOINT -->
## Android synchronization folder
PC-created synchronization artifacts are consumed through a persistent Storage
Access Framework grant.
Canonical selected folder:
```text ```text
Download/Trainlog Download/Trainlog
``` ```
The Sync screen always exposes the folder-selection action. The Sync screen always permits changing the stored folder selection.
When a folder is already authorized, the action becomes: No application-data reset is required to fix a wrong folder choice.
## 11. Android-triggered synchronization
The Sync screen exposes:
```text ```text
Changer le dossier Trainlog Synchroniser maintenant
``` ```
This is required so a wrong persisted folder selection can be corrected without Android writes:
clearing the Android application database.
Validated PC catalog publication:
```text ```text
trainlog-pc-catalog-v1.json trainlog-sync-request-v1.json
``` ```
The final Android synchronization workflow must evolve toward a single and waits for a matching:
`Synchroniser maintenant` action backed by a PC-side synchronization agent,
rather than manual export/import steps.
<!-- TRAINLOG_ANDROID_SYNC_FOLDER_CHECKPOINT _END -->
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL -->
## Variable sets and session exercise removal checkpoint
Validated functionality in this checkpoint:
```text ```text
VARIABLE_REPETITION_SETS=PASS trainlog-sync-receipt-v1.json
REPETITION_SHORTHAND_5x10=PASS
REPETITION_EXPLICIT_LIST=PASS
REPETITION_PYRAMID=PASS
DESKTOP_SCHEMA_V5=PASS
V4_TO_V5_MIGRATION_REGRESSION=PASS
MOBILE_HETEROGENEOUS_SET_IMPORT=PASS
MOBILE_IMPORT_IDEMPOTENCE=PASS
NO_FAKE_UNIFORM_TARGET=PASS
ANDROID_SESSION_DRAFT_EXERCISE_REMOVE=PASS
DESKTOP_SESSION_EXERCISE_REMOVE=PASS
``` ```
Accepted repetition examples: The receipt is matched by `request_id`.
On success Android then applies the latest PC catalog and displays the final
result.
A receipt belonging to another request is ignored as pending rather than
misreported as the current result.
## 12. Synchronization ownership
Android does not initiate raw MTP operations itself.
MTP is host-initiated:
```text ```text
5x10 Android request
4,5,6,7,8,9,10,9,8,7,6,5,4 -> PC trainlog-syncd
4..10..4 -> shared desktop sync engine
-> receipt
``` ```
A heterogeneous mobile session is persisted as ordered `performed_sets`. ## 13. Build
The desktop does not invent `target_sets`, `target_reps` or
`target_duration_seconds` for actual-only mobile observations.
On Android, an exercise already added to the current session can be removed Example local configuration:
before saving the session.
On the desktop TUI, session editing already supports: ```bash
cd android
```text printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.properties
d supprimer
JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
./gradlew assembleDebug
``` ```
for removing the selected exercise from a current or persisted session draft. Install to a connected test device:
The database replacement remains transactional.
`TRAINLOG_FORMAT_V1` remains frozen and unchanged. ```bash
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL _END --> adb install -r app/build/outputs/apk/debug/app-debug.apk
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 -->
## Shared bidirectional synchronization v1
Validated architecture:
```text
Android local write
-> automatic mobile snapshot
Android "Synchroniser maintenant"
-> trainlog-sync-request-v1.json
trainlog-syncd
-> shared C synchronization engine
-> Android → PC mobile import
-> PC → Android catalog publish
-> trainlog-sync-receipt-v1.json
Android
-> receipt matched by request_id
-> PC catalog applied locally
-> final result displayed
``` ```
The ncurses TUI and `trainlog-syncd` call the same `local.properties` is local machine configuration and must not be committed.
`trainlog_sync_run()` implementation.
Direct libmtp remains mandatory. No filesystem mount and no SQLite-file ## 14. Non-goals
synchronization are introduced.
### Concurrency Android is not intended to own:
The shared engine owns: - canonical long-term analytics;
- complex body/performance graphs;
```text - cloud accounts;
$XDG_DATA_HOME/trainlog/sync.lock - direct SQLite-file synchronization;
``` - exercise-name heuristics;
- a mounted-filesystem dependency.
A TUI-triggered transaction waits for the lock. Daemon request polling is
non-blocking and retries later.
### Sync history
Every actual synchronization transaction creates:
```text
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.json
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.txt
```
and appends a compact entry to:
```text
$XDG_DATA_HOME/trainlog/sync_history.log
```
The TUI behaves like:
```text
git log
↑/↓ select synchronization
git show
Enter opens structured detail
```
Legacy three-field history entries remain readable but have no structured
detail file.
### Android request and receipt
Request:
```text
format = trainlog-sync-request
version = 1
```
Receipt:
```text
format = trainlog-sync-receipt
version = 1
```
The receipt carries the originating `request_id`, a generated `sync_id`,
status, summary and synchronization counts. Android ignores a receipt for a
different request ID.
### User service
Install/refresh the user service with:
```text
bash tools/install_syncd_user.sh
```
No root privilege is required.
### Status
```text
COMMON_SYNC_ENGINE=PASS
TUI_SYNC_LOG_SHOW=PASS
TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS
BIDIRECTIONAL_SYNC_V1=PASS
```
Frozen `TRAINLOG_FORMAT_V1` remains unchanged.
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 _END -->

View file

@ -1,199 +1,287 @@
# Architecture # Architecture
## 1. Purpose ## 1. System boundary
Trainlog separates capture from analysis. Trainlog separates capture, durable history, transport, and presentation.
The Android application is optimized for fast data entry during training. ```text
Android capture client
|
| local SQLite
|
+-- automatic mobile snapshot
|
v
Android shared storage
Download/Trainlog
|
| direct MTP
v
shared desktop sync engine
/ \
/ \
desktop SQLite PC catalog artifact
| |
v v
desktop TUI Android
```
The TUI is optimized for durable storage, inspection, statistics, and visualization. The desktop SQLite database is the canonical long-term history.
Android local SQLite is a capture store, not a synchronization format.
## 2. Components ## 2. Components
### Android client ### Android application
Responsibilities: Responsibilities:
- start a session; - exercise catalog entry;
- record the session start timestamp; - workout-session recording;
- select an existing exercise; - performed set entry;
- create a new exercise; - continuous-activity entry;
- record workout targets; - body measurement entry;
- record actual performed sets; - local history/detail;
- record rest duration; - automatic mobile snapshot generation;
- record body data; - PC catalog application;
- record the session end timestamp; - synchronization request creation;
- export one valid Trainlog JSON document. - synchronization receipt display.
Non-responsibilities: Android is not responsible for canonical long-term analytics.
- long-term analytics; ### Desktop core
- canonical history;
- complex graphing;
- cloud synchronization.
### Exchange format The C17 core owns:
The exchange format is the compatibility boundary between Android and the TUI. - desktop SQLite persistence;
- exercise/catalog rules;
It is: - profile-aware session data;
- body data;
- JSON; - ID and time helpers;
- UTF-8; - USB discovery;
- versioned; - direct MTP operations;
- self-contained enough to import newly created exercises; - the shared bidirectional synchronization engine.
- designed for idempotent import.
### TUI ### TUI
Responsibilities: The ncursesw layer owns interaction and rendering.
- import Trainlog JSON; It consumes core services for:
- reject malformed or incompatible input cleanly;
- deduplicate sessions;
- maintain the canonical exercise catalog;
- create workouts directly from the terminal;
- maintain SQLite history;
- calculate progress metrics;
- render graphs and summaries;
- export data when needed.
### SQLite store - session entry/editing;
- history;
- exercise performance;
- body tracking;
- graphs;
- manual synchronization;
- synchronization log/detail display.
SQLite is the canonical local history. ### `trainlog-syncd`
The database must use: `trainlog-syncd` is a small user-session agent.
- foreign keys; It polls for a new Android request and invokes the same shared C synchronization
- uniqueness constraints; engine used by the TUI.
- schema versioning;
- explicit migration rules.
## 3. Data flow It does not implement a second synchronization algorithm.
## 3. Exercise model
Trainlog is metadata-driven:
```text ```text
Android recording_mode = SETS | CONTINUOUS
| tracking_mode = REPS | DURATION
| export data_fields = SPEED_KMH | DISTANCE_KM
v
Trainlog JSON
|
| import + validation
v
TUI application
|
| persistence
v
SQLite
``` ```
## 4. Identity rules Valid model-v1 combinations:
Exercises have:
- a stable machine identifier: `exercise_id`;
- a mutable display name: `name`.
The display name is not the identity.
Sessions have:
- a globally unique `session_id`.
A second import of the same `session_id` must not duplicate the session.
## 5. Separation rules for the TUI
The C17 TUI will be split into layers:
```text ```text
ncursesw rendering SETS + REPS
| SETS + DURATION
v CONTINUOUS + DURATION
TUI state / navigation
|
v
application services
|
+---- exchange-format parser
|
+---- analytics
|
v
SQLite persistence
``` ```
The rendering layer must not own business rules. Load mode is session-specific:
The persistence layer must not depend on ncurses.
## 6. Error philosophy
Trainlog must prefer explicit failure over silent corruption.
Examples:
- malformed JSON: reject import with a precise error;
- unsupported format version: reject import;
- duplicate session: report already imported, do not duplicate;
- unknown exercise: import it when valid catalog data is present;
- incomplete active session: preserve it explicitly rather than silently inventing an end time.
<!-- TRAINLOG_DIRECT_MTP_ARCHITECTURE -->
## 7. Direct Android USB/MTP transport
The Linux side does not require the Android device to be mounted as a normal
filesystem.
Transport layering is:
```text ```text
Android USB file-transfer mode none
| external
v assistance
libudev physical-device discovery
|
| bus number + device number
v
libmtp exact raw-device open
|
v
Android internal MTP storage
``` ```
This avoids GVFS/FUSE mount state and manual mount/unmount lifecycle management. Continuous work is persisted separately from performed sets.
`libudev` owns physical-device discovery. `libmtp` owns storage and object ## 4. Persistence ownership
operations. The JSON exchange layer remains above both and stays independent
from USB/MTP backend details.
Current transport foundation supports folder creation, file upload, folder ### Desktop
listing, file download, and verified byte-for-byte roundtrip.
<!-- TRAINLOG_DIRECT_MTP_ARCHITECTURE _END -->
<!-- TRAINLOG_PROFILE_AWARE_ARCHITECTURE --> Desktop SQLite schema v5 is canonical long-term history.
## Profile-aware activity architecture
Trainlog has two distinct actual-work persistence paths: Main tables:
```text ```text
SET-based exercise exercises
session_exercises sessions
| session_exercises
+--> performed_sets [0..N] performed_sets
continuous_activity
CONTINUOUS exercise body_observations
session_exercises
|
+--> continuous_activity [exactly 1]
``` ```
The two paths must remain semantically distinct. ### Android
Catalog metadata determines future entry forms. Android has an independent local SQLite schema.
Session-exercise snapshot metadata determines historical rendering/editing. It mirrors domain concepts needed for capture, but its schema version is not
coupled to the desktop schema.
The Android client must consume the same catalog profile metadata rather than Synchronization exchanges domain artifacts rather than database files.
maintaining an independent exercise-type system.
<!-- TRAINLOG_PROFILE_AWARE_ARCHITECTURE _END --> ## 5. Compatibility boundaries
### Frozen Trainlog JSON v1
`TRAINLOG_FORMAT_V1` is frozen and remains a compatibility boundary for its
existing set-based session contract.
### Synchronization artifacts
Synchronization uses separate formats:
```text
trainlog-mobile-export v1
trainlog-pc-catalog v1
trainlog-sync-request v1
trainlog-sync-receipt v1
```
A new domain requirement must not be forced into frozen v1 by using notes,
synthetic sets, or data loss.
## 6. Direct MTP transport
Linux transport:
```text
physical Android USB device
|
v
libudev discovery
|
| bus + device number
v
libmtp exact raw-device access
|
v
Android internal storage
```
No GVFS/FUSE mount is required.
Canonical exchange directory:
```text
Download/Trainlog
```
## 7. Shared synchronization engine
Both user-trigger paths call:
```text
trainlog_sync_run()
```
Manual path:
```text
TUI -> trainlog_sync_run()
```
Android-triggered path:
```text
Android request
-> trainlog-syncd
-> trainlog_sync_run()
-> receipt
```
The engine performs:
```text
1. direct-MTP device/storage discovery
2. exchange-folder resolution
3. mobile snapshot download
4. strict transactional Android -> PC import
5. PC catalog export
6. direct-MTP PC catalog publication
7. optional request receipt publication
8. structured run-history recording
```
## 8. Synchronization concurrency
The shared engine serializes synchronization with:
```text
$XDG_DATA_HOME/trainlog/sync.lock
```
The TUI waits for an active transaction.
Daemon polling uses non-blocking acquisition and retries later.
A request ID already successfully consumed is not processed as a new request.
## 9. Synchronization history
Every actual run gets a stable:
```text
sy_<uuid-v4>
```
Structured history is stored under:
```text
$XDG_DATA_HOME/trainlog/sync_runs/
```
The TUI exposes list/detail semantics comparable to:
```text
git log
git show
```
## 10. Error philosophy
Trainlog prefers explicit failure over silent corruption.
Hard validation or persistence failure aborts the relevant transaction.
The synchronization layer records useful failure detail rather than masking
known errors with generic placeholders.
## 11. Layering rule
```text
ncurses / Compose rendering
|
v
application workflow
|
+-- domain model
+-- synchronization
+-- validation
|
v
persistence / MTP transport
```
Rendering does not own persistence rules.
Persistence and MTP code do not depend on ncurses rendering.

146
docs/current_state.md Normal file
View file

@ -0,0 +1,146 @@
# Current implementation state
Canonical snapshot: 2026-09-06.
This document is the compact source of truth for the implemented Trainlog
baseline. Detailed behavior belongs in the topic-specific documents.
## Status
```text
GATE_0_PROJECT_CONTRACT=PASS
GATE_1_TRAINLOG_FORMAT_V1=PASS/FROZEN
GATE_2_PERSISTENCE_AND_USABLE_TUI=PASS
TRAINLOG_FORMAT_V1=FROZEN
DESKTOP_SCHEMA_V5=PASS
ANDROID_LOCAL_DATABASE_V3=PASS
PROFILE_AWARE_EXERCISES=PASS
CONTINUOUS_ACTIVITY=PASS
VARIABLE_REPETITION_SETS=PASS
DIRECT_MTP_TRANSPORT=PASS
ANDROID_TO_PC_IMPORT=PASS
PC_TO_ANDROID_CATALOG=PASS
COMMON_SYNC_ENGINE=PASS
TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS
TUI_SYNC_LOG_SHOW=PASS
BIDIRECTIONAL_SYNC_V1=PASS
DESKTOP_TESTS=19/19 PASS
ANDROID_BUILD=PASS
HARDWARE_SYNC_VALIDATION=PASS
```
## Desktop
Implemented:
- C17/ncursesw TUI;
- SQLite schema v5;
- direct session entry;
- persisted session detail and editing;
- exercise removal from a session through transactional child replacement;
- exercise catalog;
- profile-aware set and continuous activities;
- heterogeneous repetition sets;
- body-observation creation/history/editing;
- body graphs and normalized overlays;
- exercise performance history;
- direct USB/MTP device access;
- manual bidirectional synchronization;
- structured synchronization history and detail.
Primary navigation:
```text
0 Accueil
1 Séance
2 Historique
3 Exercices
4 Corps
5 Sync
```
## Android
Implemented:
- native Kotlin/Compose application;
- local SQLite database v3;
- exercise creation;
- inline exercise creation during session entry;
- profile-aware session recording;
- heterogeneous repetition-set entry;
- exercise removal from the current session draft;
- continuous activity recording;
- local session history/detail;
- body measurements;
- automatic mobile snapshot maintenance;
- PC catalog application through a persistent SAF folder grant;
- Android-triggered synchronization request;
- synchronization receipt handling.
## Synchronization
Canonical exchange directory:
```text
Download/Trainlog
```
Artifacts:
```text
Android -> PC
trainlog-mobile-export-v1.json
PC -> Android
trainlog-pc-catalog-v1.json
Android -> PC agent
trainlog-sync-request-v1.json
PC agent -> Android
trainlog-sync-receipt-v1.json
```
The desktop TUI and `trainlog-syncd` share `trainlog_sync_run()`.
No SQLite file is copied.
No mounted Android filesystem is required.
## Validation checkpoint
Desktop:
```text
19/19 Meson tests PASS
frozen JSON validator PASS
import-contract validator PASS
git diff --check PASS
```
Android:
```text
assembleDebug PASS
real Samsung request -> daemon -> bidirectional sync -> receipt PASS
multiple distinct request IDs consumed once each PASS
```
## Current implementation cursor
No new feature is frozen by this documentation cleanup.
```text
NEXT_FEATURE=UNFROZEN
```
Future work must start from this validated baseline rather than from obsolete
historical `NEXT` notes.

View file

@ -1,122 +1,90 @@
# Database # Desktop database
## 1. Status ## 1. Status
```text ```text
GATE_2=IN_PROGRESS TRAINLOG_DATABASE_SCHEMA_VERSION=5
DATABASE_SCHEMA_V2=IMPLEMENTED DATABASE_SCHEMA_V5=PASS
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
SESSION_EDIT_PERSISTENCE=IMPLEMENTED
BODY_OBSERVATION_EDIT=IMPLEMENTED
TRAINLOG_FORMAT_V1=FROZEN TRAINLOG_FORMAT_V1=FROZEN
``` ```
Gate 2 review #1 establishes the persistence foundation. The desktop SQLite database is the canonical long-term Trainlog history.
The Trainlog exchange format v1 is already frozen and is not modified by this gate. Its schema evolves independently from all JSON exchange-format versions.
## 2. Purpose ## 2. Versioning
SQLite is the canonical long-term store used by the TUI. Schema version uses:
The SQLite database is an internal persistence format and is versioned independently from the Trainlog JSON exchange format.
## 3. Schema versioning
Trainlog database schema version uses SQLite:
```sql ```sql
PRAGMA user_version; PRAGMA user_version;
``` ```
Current schema: Current value:
```text ```text
DATABASE_SCHEMA_V2=2 5
``` ```
A new database starts with `user_version = 0` and is initialized atomically to Supported historical databases are migrated explicitly through the implemented
the current schema. migration chain. A database newer than the running binary understands is
rejected.
The implemented historical path is: A schema fixture must represent the real historical structure. Rewriting only
`user_version` is not an acceptable migration test.
```text ## 3. Connection invariants
0 -> 2 fresh initialization
1 -> 2 transactional migration
```
Schema v2 adds local session classification while leaving the frozen Trainlog Every connection enables:
JSON v1 exchange contract unchanged.
A database newer than the running binary understands is rejected.
Every future schema change requires an explicit migration and dedicated
coverage; metadata-only version rewriting is not an accepted migration.
## 4. Connection rules
Every Trainlog SQLite connection must enable:
```sql ```sql
PRAGMA foreign_keys = ON; PRAGMA foreign_keys = ON;
``` ```
The core also configures a bounded SQLite busy timeout. A bounded SQLite busy timeout is configured by the core.
Foreign-key activation is verified by tests. ## 4. Tables
## 5. Tables ### `exercises`
### 5.1 `exercises` Canonical desktop exercise catalog.
Canonical exercise catalog.
Fields:
```text
id internal INTEGER primary key
exercise_id stable Trainlog identity, UNIQUE
name display name
normalized_name v1 comparison form, UNIQUE
tracking_mode reps | duration
```
The database does not compute Unicode normalization in review #1.
The application/catalog layer will compute the frozen v1 normalized name in Gate 2 review #2.
SQLite owns the final uniqueness barrier.
### 5.2 `sessions`
Canonical workout session header.
Fields:
```text ```text
id id
session_id UNIQUE exercise_id UNIQUE stable identity
name
normalized_name UNIQUE normalized display form
tracking_mode reps | duration
recording_mode sets | continuous
data_fields bounded bit mask
```
Rules include:
- continuous implies duration tracking;
- unknown supplemental field bits are rejected;
- normalized names remain unique.
### `sessions`
```text
id
session_id UNIQUE stable identity
started_at started_at
ended_at nullable ended_at nullable
session_type training | max_test session_type training | max_test
notes nullable notes nullable
``` ```
`session_type` is a local SQLite concern in schema v2. Existing schema-v1 rows ### `session_exercises`
migrate to `training`; no historical workout is retroactively inferred to be a
max test.
Body data is stored separately so standalone body observations can use the same representation. Ordered exercise occurrence inside one session.
### 5.3 `session_exercises`
One ordered exercise within a session.
Fields include:
```text ```text
session_row_id session_row_id
exercise_row_id exercise_row_id
recording_mode
data_fields
position position
load_mode load_mode
rest_seconds rest_seconds
@ -127,41 +95,77 @@ target_weight_kg
notes notes
``` ```
Constraints enforce: Current desktop history snapshots `recording_mode` and `data_fields` in the
session row. `tracking_mode` remains associated with the referenced exercise
catalog identity.
- one exercise identity at most once per session; `SETS` rows support two target shapes in schema v5:
- one row at each session position;
- exactly one target metric: repetitions or duration;
- target load presence consistent with `load_mode`.
### 5.4 `performed_sets` ```text
explicit planned target
target_sets + exactly one target metric
Ordered actual sets. actual-only mobile observation
target_sets = NULL
target_reps = NULL
target_duration_seconds = NULL
```
Fields: This v5 rule is what permits heterogeneous mobile performed sets without
inventing a fake uniform target.
`CONTINUOUS` rows are targetless and require:
```text
load_mode = none
rest_seconds = 0
target_weight_kg = NULL
```
### `performed_sets`
Ordered actual set records.
```text ```text
session_exercise_row_id session_exercise_row_id
position position
reps reps nullable
duration_seconds duration_seconds nullable
weight_kg weight_kg nullable
``` ```
Exactly one of repetitions or duration is present. Exactly one primary actual metric is present:
Cross-table rules such as actual-set load consistency with the owning session exercise remain application/import invariants and will be tested at the import layer.
### 5.5 `body_observations`
Body history is a first-class database concept and may exist with or without a workout session.
Fields include:
```text ```text
observation_id reps
or
duration_seconds
```
Actual repetitions may be zero.
Each row is independent; heterogeneous repetition sequences are first-class
data.
### `continuous_activity`
One-to-one actual record for a continuous session exercise.
```text
session_exercise_row_id UNIQUE
duration_seconds
speed_kmh nullable
distance_km nullable
```
Continuous activity never creates a fake performed set.
### `body_observations`
```text
observation_id UNIQUE
observed_at observed_at
session_row_id optional and UNIQUE session_row_id optional UNIQUE link
body_weight_kg body_weight_kg
neck_cm neck_cm
shoulders_cm shoulders_cm
@ -181,180 +185,94 @@ notes
At least one body metric must be present. At least one body metric must be present.
Imported session-associated body data will create one linked observation. ## 5. Identifier generation
Standalone TUI measurements use the same table without `session_row_id`. Official desktop creator prefixes:
## 6. UUID generation
Official Trainlog creators generate UUID version 4 identifiers.
The C core provides generated IDs for:
```text ```text
ex_<uuid-v4> ex_ exercise
se_<uuid-v4> se_ session
bo_<uuid-v4> bo_ body observation
sy_ synchronization run
``` ```
This is creation policy. All use random UUIDv4 values.
The frozen exchange parser remains able to accept other schema-valid opaque v1 identifiers. Exchange parsers may accept other schema-valid opaque identities where their
contract explicitly permits it.
## 7. Transactions ## 6. Transactions
Multi-row operations are atomic. Multi-row user operations are atomic.
Gate 2 provides explicit: Persisted session correction replaces session child rows transactionally while
preserving the parent:
```text ```text
BEGIN IMMEDIATE session_id
COMMIT started_at
ROLLBACK ended_at
session_type
session notes
linked body observation
``` ```
primitives. Removing an exercise from a persisted session is therefore a transactional
replacement of the remaining child set.
The JSON import service must perform catalog reconciliation and all session inserts inside one transaction. A failed replacement rolls back to the previously persisted session.
Persisted session correction also uses an explicit transaction. Editing a Body-observation editing preserves its stable identity, timestamp, and optional
session replaces only its `session_exercises` / `performed_sets` children and session link.
preserves the parent session row, stable `session_id`, timestamps,
`session_type`, session notes, and any linked body observation.
Body-observation correction preserves observation identity, timestamp, and ## 7. Mobile import semantics
optional session link.
A hard conflict or validation failure leaves the database unchanged. `tools/import_mobile_export.py` validates the complete mobile snapshot before
committing database changes.
Properties:
```text
schema-v5 aware
transactional
idempotent by stable IDs
profile-aware catalog reconciliation
heterogeneous performed sets preserved
no fake target generated
continuous activity kept separate
```
## 8. Units ## 8. Units
Canonical persistent units remain: Canonical desktop persistence:
- weight/load: kilograms; ```text
- body circumference: centimeters; weight/load kg
- duration/rest: seconds. body circumference cm
duration/rest seconds
speed km/h
distance km
```
## 9. Current Gate 2 persistence boundary ## 9. Android database
Implemented persistence includes: The Android SQLite database is independent.
- SQLite schema v2; Desktop and Android schema versions are not required to match.
- transactional schema migration v1 -> v2;
- Unicode-aware canonical exercise catalog support;
- complete session insertion;
- session detail loading;
- exact bounded editable-session loading;
- transactional replacement of session exercise/set children;
- body-observation creation, listing, exact lookup, and update;
- stable local identifiers for exercises, sessions, and body observations.
The database remains independent from ncurses rendering. Do not synchronize SQLite database files.
The frozen Trainlog JSON v1 format remains a separate compatibility boundary
and is not version-coupled to SQLite schema v2.
## 10. Validation ## 10. Validation
Normal build:
```bash ```bash
CC=clang meson setup build
meson compile -C build meson compile -C build
meson test -C build --print-errorlogs meson test -C build --print-errorlogs
``` ```
Sanitizer build: Migration-specific regression coverage includes:
```bash
CC=clang meson setup build-asan \
-Db_sanitize=address,undefined \
-Db_lundef=false
meson compile -C build-asan
meson test -C build-asan --print-errorlogs
```
Repository-level format validators remain mandatory:
```bash
python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check
```
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 -->
## Exercise recording metadata — schema v3 direction
The next SQLite migration adds:
```text ```text
recording_mode = sets | continuous schema_v5_migration
data_fields = bounded bit mask
``` ```
Existing `tracking_mode = reps | duration` remains stable. The current normal suite contains 19 tests.
Every `session_exercises` row snapshots this metadata so later catalog changes
do not reinterpret historical sessions.
Migration v2 -> v3 defaults all existing rows to `sets` with `data_fields = 0`.
No name-based migration is allowed.
Continuous actual activity data is stored separately from `performed_sets`;
Trainlog will not manufacture a fake one-set workout.
<!-- TRAINLOG_EXERCISE_DATA_MODEL_V1 _END -->
<!-- TRAINLOG_SCHEMA_V4_CONTINUOUS -->
## Schema v4 — continuous exercise persistence
Current database schema:
```text
TRAINLOG_DATABASE_SCHEMA_VERSION = 4
PRAGMA user_version = 4
```
Relevant profile metadata is stored in both:
```text
exercises
session_exercises
```
`session_exercises` snapshots:
```text
recording_mode
data_fields
```
Continuous actual activity data is stored one-to-one in:
```text
continuous_activity
```
Columns:
```text
session_exercise_row_id
duration_seconds
speed_kmh nullable
distance_km nullable
```
For a valid continuous activity:
```text
target_sets NULL
target_reps NULL
target_duration_seconds NULL
load_mode none
rest_seconds 0
target_weight_kg NULL
performed_sets none
```
Schema migration never infers profile information from exercise names.
<!-- TRAINLOG_SCHEMA_V4_CONTINUOUS _END -->

View file

@ -3,17 +3,17 @@
## Status ## Status
```text ```text
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION EXERCISE_DATA_MODEL_V1=PASS
DATABASE_SCHEMA_V3=NEXT PROFILE_AWARE_DESKTOP=PASS
TUI_PROFILE_AWARE_ENTRY=AFTER_SCHEMA_V3 PROFILE_AWARE_ANDROID=PASS
ANDROID_PROFILE_AWARE_ENTRY=AFTER_TUI CONTINUOUS_ACTIVITY=PASS
VARIABLE_REPETITION_SETS=PASS
TRAINLOG_FORMAT_V1=FROZEN TRAINLOG_FORMAT_V1=FROZEN
SESSION_EXCHANGE_V2=DESIGN_REQUIRED_LATER
``` ```
Trainlog must not use one universal exercise form. ## 1. Metadata axes
An exercise is defined by three independent pieces of metadata: An exercise is described by independent metadata:
```text ```text
recording_mode = SETS | CONTINUOUS recording_mode = SETS | CONTINUOUS
@ -21,196 +21,13 @@ tracking_mode = REPS | DURATION
data_fields = supplemental field bit mask data_fields = supplemental field bit mask
``` ```
Initial valid combinations: Known supplemental fields:
```text
SETS + REPS
SETS + DURATION
CONTINUOUS + DURATION
```
`CONTINUOUS + REPS` is invalid in model v1.
Initial supplemental fields:
```text ```text
SPEED_KMH SPEED_KMH
DISTANCE_KM DISTANCE_KM
``` ```
Unknown field bits are invalid.
Examples:
```text
Presse à cuisses
SETS + REPS
Gainage
SETS + DURATION
Marche
CONTINUOUS + DURATION
SPEED_KMH
Course
CONTINUOUS + DURATION
SPEED_KMH
Vélo
CONTINUOUS + DURATION
SPEED_KMH | DISTANCE_KM
Rameur
CONTINUOUS + DURATION
DISTANCE_KM
```
Load semantics remain separate and session-specific:
```text
none
external
assistance
```
A continuous exercise does not ask for:
```text
number of sets
repetitions
per-set rest
```
For example:
```text
Marche
Durée 45 min
Vitesse 5.8 km/h
```
Creating/editing an exercise asks for:
```text
Name
Organization: Sets | Continuous
Primary metric: Repetitions | Duration
Supplemental fields: Speed | Distance
```
Rules:
- continuous forces duration in model v1;
- sets accepts reps or duration;
- UI fields are driven by metadata, never exercise-name heuristics;
- changing catalog metadata affects future entry only.
Historical stability:
Every session exercise stores a snapshot of:
```text
recording_mode
tracking_mode
data_fields
```
So changing `Marche` from an old set-based duration exercise to continuous
duration + speed does not reinterpret old sessions.
## SQLite schema v3 direction
Schema v3 adds to `exercises`:
```text
recording_mode
data_fields
```
and snapshots the same values in `session_exercises`.
Migration v2 -> v3 is conservative:
```text
all existing exercises -> SETS
all existing session exercises -> SETS
data_fields -> 0
```
No migration guesses by exercise name.
Continuous actual activity data gets its own one-to-one record:
```text
duration_seconds
speed_kmh nullable
distance_km nullable
```
Continuous activities have no `performed_sets` rows and no fake one-set
representation.
## Frozen JSON v1
Trainlog JSON session v1 remains frozen.
It represents the existing set-based exchange model.
Continuous data that cannot be represented in v1 must not be:
- hidden in notes;
- converted into a fake set;
- silently discarded.
A future explicit session exchange v2 will carry profile-aware exercise data
while v1 import remains supported.
## Android/TUI parity
Both interfaces consume identical exercise metadata.
The future PC -> Android catalog snapshot must contain:
```text
exercise_id
name
recording_mode
tracking_mode
data_fields
```
## Implementation order
```text
1. SQLite schema v3 + migration tests
2. C model/API additions
3. catalog create/edit support
4. TUI profile-aware session entry
5. manually convert Marche to CONTINUOUS + SPEED_KMH
6. profile-aware detail/history
7. Android uses the same model
8. design session exchange v2
```
<!-- TRAINLOG_PROFILE_AWARE_IMPLEMENTED -->
## Implemented checkpoint
The profile-aware exercise model is now implemented in the C model, SQLite
persistence and TUI.
Current canonical rules:
```text
recording_mode = SETS | CONTINUOUS
tracking_mode = REPS | DURATION
known data_fields:
SPEED_KMH
DISTANCE_KM
```
Valid model-v1 combinations: Valid model-v1 combinations:
```text ```text
@ -219,25 +36,166 @@ SETS + DURATION
CONTINUOUS + DURATION CONTINUOUS + DURATION
``` ```
Continuous exercise actual data is persisted as one `continuous_activity` Invalid:
record rather than a performed-set list.
A continuous activity never manufactures a one-set representation. ```text
CONTINUOUS + REPS
```
The TUI asks continuous duration in **minutes**, converts to seconds, and stores UI behavior must never be inferred from an exercise display name.
seconds internally.
## 2. Examples
```text
Presse à cuisses
SETS + REPS
Gainage
SETS + DURATION
Marche
CONTINUOUS + DURATION + SPEED_KMH
Course
CONTINUOUS + DURATION + SPEED_KMH
Vélo
CONTINUOUS + DURATION + SPEED_KMH + DISTANCE_KM
Rameur
CONTINUOUS + DURATION + DISTANCE_KM
```
## 3. Load semantics
Load mode is session-specific:
```text
none
external
assistance
```
`external` is added resistance.
`assistance` is help. Lower assistance represents less help and is therefore
better when comparing otherwise equivalent performance.
Machine-displayed kilograms are stored faithfully without claiming mechanical
equivalence across different machines.
## 4. Set-based work
`SETS + REPS` stores one performed-set row per actual set.
Actual repetitions can differ across sets.
Accepted compact repetition input includes:
```text
5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
The pyramid shorthand:
```text
4..10..4
```
expands to:
```text
4,5,6,7,8,9,10,9,8,7,6,5,4
```
Each performed row is the source of truth for actual work.
`SETS + DURATION` likewise stores one actual duration per performed set.
## 5. Planned versus actual
Desktop-created set sessions may carry explicit planned targets.
Android mobile snapshots can represent actual-only heterogeneous work.
Desktop schema v5 therefore allows an imported set session to have no synthetic
uniform target:
```text
target_sets = NULL
target_reps = NULL
target_duration_seconds = NULL
```
Do not derive a fake target from heterogeneous actual sets.
## 6. Continuous work
Continuous activity does not ask for:
```text
set count
repetitions
per-set rest
load
```
Example: Example:
```text ```text
Marche Marche
CONTINUOUS + DURATION + SPEED_KMH
TUI entry: Durée 45 min
Durée (minutes) Vitesse 5.8 km/h
Vitesse km/h
``` ```
Historical session rows snapshot recording metadata and are not reinterpreted Actual persistence uses one `continuous_activity` record containing duration
when catalog metadata later changes. and configured supplemental values.
<!-- TRAINLOG_PROFILE_AWARE_IMPLEMENTED _END -->
No fake performed set is created.
## 7. Historical interpretation
Desktop `session_exercises` snapshot:
```text
recording_mode
data_fields
```
This prevents later catalog changes to those fields from rewriting the meaning
of historical rows.
Current desktop tracking mode remains tied to the referenced exercise identity.
Any future change to that historical contract requires an explicit schema
decision rather than an implicit name-based migration.
## 8. Android/TUI parity
Both interfaces use the same metadata axes.
The PC -> Android catalog synchronization artifact carries:
```text
exercise_id
name
recording_mode
tracking_mode
data_fields
```
Creating an exercise inline on Android or desktop follows the same model rules.
## 9. Exchange boundaries
Frozen Trainlog session JSON v1 remains unchanged.
Synchronization data that exceeds frozen v1 uses separate versioned artifacts.
Continuous activity must never be:
- hidden in notes;
- converted into a fake set;
- silently discarded.

View file

@ -1,546 +1,64 @@
# Roadmap # Roadmap
This file describes the current project state and the next implementation
cursor. Historical checkpoint detail belongs in Git history and `docs/reviews`.
## Gate 0 — Project contract ## Gate 0 — Project contract
Status: PASS ```text
GATE_0=PASS
```
## Gate 1 — Exchange format v1 freeze Development rules, ownership boundaries, identity rules, validation discipline,
and Git workflow are established.
Status: PASS ## Gate 1 — Trainlog exchange format v1
```text ```text
GATE_1=PASS
TRAINLOG_FORMAT_V1=FROZEN TRAINLOG_FORMAT_V1=FROZEN
``` ```
## Gate 2 — Persistence + usable TUI The published Trainlog JSON v1 compatibility boundary remains unchanged.
Status: IN PROGRESS ## Gate 2 — Persistence and usable desktop TUI
```text ```text
GATE_2=PASS
DESKTOP_SCHEMA_V5=PASS
FIRST_USABLE_TUI=PASS FIRST_USABLE_TUI=PASS
TUI_V0_2_POLISH=IMPLEMENTED
GATE_2=IN_PROGRESS
``` ```
Current TUI capabilities: Completed baseline includes:
- direct workout entry; - SQLite persistence and explicit migrations;
- direct session entry;
- session history/detail/editing;
- exercise catalog; - exercise catalog;
- body tracking; - profile-aware set and continuous work;
- weight graph; - body history/editing/visualization;
- colored dashboard; - exercise performance history;
- arrow/F-key navigation; - strict validation and rollback behavior.
- navigable history;
- Unicode anti-duplicate exercise names.
Current remaining Gate 2 direction: ## Android local client
1. detect Android over USB/ADB;
2. build the minimal Android recorder;
3. transfer/export one frozen Trainlog JSON v1 document over USB;
4. import it transactionally into the canonical SQLite store;
5. validate the complete Android -> JSON -> TUI -> SQLite path.
Measured-max semantics remain a later independent analytics contract.
<!-- TRAINLOG_TUI_V02_ROADMAP -->
## TUI v0.2 checkpoint
Current state:
```text
FIRST_USABLE_TUI=PASS
TUI_V0_2_POLISH=IMPLEMENTED
TUI_SESSION_DETAILS=IMPLEMENTED
TUI_DURATION_HUMAN_INPUT=IMPLEMENTED
TUI_BODY_METRIC_GRAPHS=IMPLEMENTED
GATE_2=IN_PROGRESS
TRAINLOG_FORMAT_V1=FROZEN
```
Completed before this checkpoint:
- C17/Meson persistence core;
- SQLite schema v1 foundation;
- UUIDv4 generation;
- Unicode catalog normalization;
- direct workout recording;
- direct body observation recording;
- colored ncursesw dashboard;
- keyboard navigation;
- body-weight graph;
- flat-series graph rendering;
- navigable workout history;
- full read-only session detail.
Next implementation slice:
1. shared duration parser accepting seconds and minute-oriented syntax;
2. shared human duration formatter;
3. generic body-metric history query;
4. F4 metric selector;
5. graphs for weight and every body measurement;
6. left/right asymmetry presentation.
No incompatible change to frozen Trainlog JSON v1 is required.
<!-- TRAINLOG_TUI_V02_ROADMAP _END -->
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_ROADMAP -->
## Next TUI visualization slice
Canonical state after the current checkpoint:
```text
FIRST_USABLE_TUI=PASS
TUI_V0_2_POLISH=IMPLEMENTED
TUI_SESSION_DETAILS=IMPLEMENTED
TUI_DURATION_HUMAN_INPUT=IMPLEMENTED
TUI_BODY_METRIC_GRAPHS=IMPLEMENTED
TUI_GLOBAL_BODY_OVERLAY=IMPLEMENTED
DASHBOARD_GRAPH_ONLY=IMPLEMENTED
GATE_2=IN_PROGRESS
TRAINLOG_FORMAT_V1=FROZEN
```
Next deliverables:
1. normalized global body graph in `F4`;
2. color + symbol identity for every overlaid metric;
3. global percent-change summary;
4. richer home weight graph;
5. previous-measurement delta on dashboard;
6. dashboard min/max weight;
7. latest waist summary when available;
8. compact asymmetry warning when relevant.
No database schema migration is expected.
No Trainlog JSON v1 change is expected.
<!-- TRAINLOG_GLOBAL_BODY_GRAPH_ROADMAP _END -->
```text
DASHBOARD_12_MONTHS=IMPLEMENTED
```
```text
TUI_EXERCISE_PERFORMANCE=IMPLEMENTED
TUI_SESSION_EDIT=IMPLEMENTED
TUI_BODY_OBSERVATION_EDIT=IMPLEMENTED
ANDROID_USB_DETECTION=NEXT
MEASURED_MAX_TRACKING=LATER
```
## Session type / measured max foundation
```text
DATABASE_SCHEMA_V2=IMPLEMENTED
SESSION_TYPE_PERSISTENCE=IMPLEMENTED
SESSION_TYPE_TUI=IMPLEMENTED
TUI_SESSION_EDIT=IMPLEMENTED
TUI_BODY_OBSERVATION_EDIT=IMPLEMENTED
TUI_PRIMARY_NAVIGATION=IMPLEMENTED
TUI_SECONDARY_VIEW_POLISH=IMPLEMENTED
ANDROID_USB_DETECTION=NEXT
MEASURED_MAX_TRACKING=LATER
TRAINLOG_FORMAT_V1=FROZEN
```
SQLite schema v2 adds `sessions.session_type` with `training` and `max_test`.
Existing v1 rows migrate to `training`. No existing session is retroactively
classified as a max test.
## Editable session data
```text
SESSION_EDIT_PERSISTENCE=IMPLEMENTED
SESSION_EDIT_TUI=IMPLEMENTED
BODY_OBSERVATION_EDIT=IMPLEMENTED
USB_PHONE_DETECTION=NEXT
```
Recorded session exercise/set correction uses atomic replacement of child rows
while preserving the parent session row and linked body observations.
The TUI exposes correction both while reviewing an in-progress draft and after
persistence. Escape cancels without committing partial edits.
## Body observation record workflow
```text
BODY_OBSERVATION_HISTORY_UI=IMPLEMENTED
BODY_OBSERVATION_EDIT=IMPLEMENTED
BODY_OBSERVATION_SCROLLBAR=IMPLEMENTED
USB_PHONE_DETECTION=NEXT
```
`F4 Corps` now uses newest-first observation records with detail/edit views
instead of making individual metrics the primary navigation model.
<!-- TRAINLOG_EDITABILITY_ROADMAP -->
## Editability/navigation checkpoint
Completed:
- local schema v2 migration with `training` / `max_test`;
- session-type selection and persistence;
- persisted session editing with stable parent identity;
- body-observation history, detail, and editing;
- Escape-safe prompt cancellation;
- framed ASCII-banner primary and secondary views;
- top `Accueil / Séance / Historique / Exercices / Corps` navigation;
- Tab focus with yellow border-only focus indication;
- direct exercise creation while building a session;
- 12-month dashboard axis kept inside its frame.
Next implementation cursor:
```text
ANDROID_USB_DETECTION=NEXT
ANDROID_MINIMAL_RECORDER=AFTER
JSON_V1_USB_IMPORT_EXPORT=AFTER
```
<!-- TRAINLOG_EDITABILITY_ROADMAP _END -->
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP -->
## Direct MTP transport checkpoint
```text
USB_MTP_DETECTION=PASS
MTP_STORAGE_ACCESS=PASS
MTP_ROOT_FOLDER_ACCESS=PASS
MTP_WRITE=PASS
MTP_LIST_FOLDER=PASS
MTP_READ=PASS
MTP_ROUNDTRIP=PASS
MTP_TRANSPORT_FOUNDATION=PASS
JSON_V1_MTP_TRANSFER=NEXT
ANDROID_MINIMAL_RECORDER=AFTER
TRAINLOG_FORMAT_V1=FROZEN
```
The Linux side now detects one physical MTP phone without counting USB
interface children, opens the exact device with libmtp, accesses internal
storage, and performs a verified write/list/read roundtrip without mounting the
phone.
Next implementation slice:
```text
1. freeze the MTP exchange directory/file convention
2. roundtrip a real examples/session-v1.json
3. validate/import the downloaded JSON
4. begin the minimal Android recorder
```
<!-- TRAINLOG_MTP_TRANSPORT_ROADMAP _END -->
<!-- TRAINLOG_SYNC_PAGE_ROADMAP -->
## Sync TUI integration
```text
MTP_TRANSPORT_FOUNDATION=PASS
TUI_SYNC_PAGE=IMPLEMENTED
TUI_SYNC_DEVICE_STATUS=IMPLEMENTED
TUI_SYNC_REMOTE_JSON_CANDIDATES=IMPLEMENTED
TUI_SYNC_LOCAL_CATALOG_COUNT=IMPLEMENTED
JSON_V1_MTP_CLASSIFICATION_IMPORT=NEXT
CATALOG_SNAPSHOT_SYNC=AFTER
ANDROID_MINIMAL_RECORDER=AFTER
TRAINLOG_FORMAT_V1=FROZEN
```
Sync direction:
```text
Android -> PC
sessions
new exercises embedded in sessions -> automatic reconciliation
body data embedded in sessions -> automatic import
PC -> Android
canonical exercise catalog snapshot
```
Standalone body-observation exchange, if required by the Android recorder, gets
its own explicit versioned contract rather than changing session JSON v1.
<!-- TRAINLOG_SYNC_PAGE_ROADMAP _END -->
<!-- TRAINLOG_SYNC_FINAL_ROADMAP -->
## Sync UI checkpoint complete
```text
MTP_TRANSPORT_FOUNDATION=PASS
TUI_SYNC_PAGE=IMPLEMENTED
TUI_SYNC_DEVICE_STATUS=IMPLEMENTED
TUI_SYNC_FOCUS_NAVIGATION=IMPLEMENTED
TUI_SYNC_REMOTE_JSON_CANDIDATES=IMPLEMENTED
TUI_SYNC_LOCAL_CATALOG_COUNT=IMPLEMENTED
ANDROID_APP_SCAFFOLD=NEXT
ANDROID_FAKE_DATA_FLOW=AFTER
JSON_V1_ANDROID_EXPORT=AFTER
CATALOG_SNAPSHOT_SYNC=AFTER
TRAINLOG_FORMAT_V1=FROZEN
```
Development now moves to the Android client.
Fictitious sessions, exercises, and body observations are used during Android
development and synchronization testing. The development database will be
purged before normal production use begins.
<!-- TRAINLOG_SYNC_FINAL_ROADMAP _END -->
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP -->
## Exercise data-model checkpoint
```text
EXERCISE_DATA_MODEL_V1=FROZEN_FOR_IMPLEMENTATION
DATABASE_SCHEMA_V3=NEXT
PROFILE_AWARE_C_MODEL=AFTER_SCHEMA
PROFILE_AWARE_TUI=AFTER
ANDROID_PROFILE_AWARE_UI=AFTER
SESSION_EXCHANGE_V2=DESIGN_LATER
TRAINLOG_FORMAT_V1=FROZEN
```
The model separates:
```text
recording organization: SETS | CONTINUOUS
primary metric: REPS | DURATION
supplemental fields: SPEED_KMH | DISTANCE_KM
```
Existing schema-v2 data migrates conservatively to `SETS`.
<!-- TRAINLOG_EXERCISE_DATA_MODEL_ROADMAP _END -->
<!-- TRAINLOG_PROFILE_AWARE_ROADMAP_FINAL -->
## Current implementation cursor
```text
MTP_TRANSPORT_FOUNDATION=PASS
TUI_SYNC_PAGE=PASS
EXERCISE_DATA_MODEL_V1=PASS
DATABASE_SCHEMA_V4=PASS
PROFILED_CATALOG_API=PASS
PROFILE_AWARE_EXERCISE_CREATION=PASS
CONTINUOUS_ACTIVITY_PERSISTENCE=PASS
CONTINUOUS_ACTIVITY_DETAIL_DISPLAY=PASS
CONTINUOUS_DURATION_MINUTES_UI=PASS
ANDROID_APP=NEXT
TRAINLOG_FORMAT_V1=FROZEN
PROFILE_AWARE_SESSION_EXCHANGE=DESIGN_LATER
```
Next Android slice:
```text
1. Android project scaffold
2. shared Trainlog visual identity
3. launcher icon = themed T
4. home/navigation
5. session recording
6. inline exercise creation
7. standalone exercise creation
8. standalone body measurement recording
9. fictitious local records
10. only then connect exchange/sync
```
Do not revert continuous activities to performed sets.
Do not modify JSON v1 to accommodate continuous metrics.
<!-- TRAINLOG_PROFILE_AWARE_ROADMAP_FINAL _END -->
<!-- TRAINLOG_ANDROID_LOCAL_CHECKPOINT -->
## Android local checkpoint
```text
ANDROID_PROJECT=PASS
ANDROID_THEME=PASS
ANDROID_EXERCISE_CATALOG=PASS
ANDROID_SESSION_RECORDING=PASS
ANDROID_SESSION_HISTORY=PASS
ANDROID_BODY_RECORDING=PASS
ANDROID_MTP_SYNC=NEXT
```
The next implementation cursor is synchronization between the Android client
and the desktop TUI over the existing direct-MTP transport architecture.
Constraints remain:
```text
no GVFS/FUSE dependency
no SQLite-file synchronization
TRAINLOG_FORMAT_V1 remains frozen
profile-aware data must not be forced into v1
```
<!-- TRAINLOG_ANDROID_LOCAL_CHECKPOINT _END -->
<!-- TRAINLOG_MOBILE_EXPORT_V1 -->
## MTP mobile export v1
Android now prepares a versioned full mobile snapshot at:
```text
Download/Trainlog/trainlog-mobile-export-v1.json
```
The file contains:
```text
exercise profiles
sessions
body observations
```
It is explicitly separate from frozen `TRAINLOG_FORMAT_V1`.
Desktop direct-MTP validation is available through:
```text
./build/tui/trainlog-mtp-mobile-export-probe
```
The probe traverses:
```text
internal storage
→ Download
→ Trainlog
→ trainlog-mobile-export-v1.json
```
and downloads it directly through libmtp without a mount.
Next after hardware PASS:
```text
DESKTOP_MOBILE_EXPORT_IMPORT=NEXT
PC_TO_ANDROID_CATALOG=AFTER
```
<!-- TRAINLOG_MOBILE_EXPORT_V1 _END -->
<!-- TRAINLOG_DESKTOP_MOBILE_IMPORT_CURSOR -->
## Mobile import cursor
```text
MOBILE_EXPORT_MTP=PASS
DESKTOP_MOBILE_IMPORT_V1=IMPLEMENTED
TUI_SYNC_ACTION=NEXT
PC_TO_ANDROID_CATALOG=AFTER
```
The CLI importer is the reference import engine for the next TUI Sync action.
<!-- TRAINLOG_DESKTOP_MOBILE_IMPORT_CURSOR _END -->
<!-- TRAINLOG_TUI_MOBILE_SYNC_CURSOR -->
## Sync implementation cursor
```text
MOBILE_EXPORT_MTP=PASS
DESKTOP_MOBILE_IMPORT_V1=PASS
TUI_ANDROID_TO_PC_SYNC_ACTION=IMPLEMENTED
TUI_ANDROID_TO_PC_SYNC_HARDWARE_VALIDATION=NEXT
PC_TO_ANDROID_CATALOG=AFTER
```
<!-- TRAINLOG_TUI_MOBILE_SYNC_CURSOR _END -->
<!-- TRAINLOG_BIDIRECTIONAL_SYNC_CURSOR -->
## Bidirectional sync cursor
```text
ANDROID_TO_PC_MTP=PASS
DESKTOP_MOBILE_IMPORT=PASS
PC_TO_ANDROID_CATALOG=IMPLEMENTED
SYNC_HISTORY_UI=IMPLEMENTED
BIDIRECTIONAL_HARDWARE_VALIDATION=NEXT
```
<!-- TRAINLOG_BIDIRECTIONAL_SYNC_CURSOR _END -->
<!-- TRAINLOG_SYNC_AGENT_CURSOR -->
## Sync agent cursor
```text
ANDROID_AUTO_OUTBOX=IMPLEMENTED
ANDROID_SYNC_REQUEST=IMPLEMENTED
TRAINLOG_SYNCD=NEXT
ANDROID_SYNC_RECEIPT=AFTER
TUI_SYNC_LOG_SHOW=AFTER_AGENT_FOUNDATION
```
<!-- TRAINLOG_SYNC_AGENT_CURSOR _END -->
<!-- TRAINLOG_SYNC_VALIDATED_ROADMAP -->
## Synchronization cursor
```text ```text
ANDROID_LOCAL_WORKFLOWS=PASS ANDROID_LOCAL_WORKFLOWS=PASS
ANDROID_LOCAL_DATABASE_V3=PASS
ANDROID_TO_PC_MTP=PASS
DESKTOP_MOBILE_IMPORT_V1=PASS
DESKTOP_MOBILE_IMPORT_IDEMPOTENT=PASS
PC_CATALOG_EXPORT_V1=PASS
PC_TO_ANDROID_MTP_PUBLISH=PASS
ANDROID_SAF_FOLDER_CHANGE=PASS
SYNC_HISTORY_GIT_LIKE=NEXT
COMMON_SYNC_ENGINE=NEXT
TRAINLOG_SYNCD=NEXT
ANDROID_REQUEST_RECEIPT=AFTER
AUTO_OUTBOX=AFTER_AGENT_FOUNDATION
``` ```
Do not regress to: Completed:
```text - exercise creation;
SQLite file synchronization - inline exercise creation;
filesystem mounts - profile-aware session entry;
exercise-name heuristics - heterogeneous repetition sets;
manual fake sets for continuous activities - continuous activity;
overloading frozen Trainlog JSON v1 - session history/detail;
``` - body measurements;
<!-- TRAINLOG_SYNC_VALIDATED_ROADMAP _END --> - exercise removal from the current session draft.
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 --> ## Variable set checkpoint
## Variable repetition sets
Trainlog preserves each performed set independently.
Accepted repetition input:
```text
5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
`4..10..4` expands to:
```text
4,5,6,7,8,9,10,9,8,7,6,5,4
```
Desktop schema v5 permits targetless `SETS` rows for actual-only mobile
observations. Synchronization therefore does not invent a uniform target when
performed sets are heterogeneous.
`performed_sets` remains the source of truth for actual per-set values.
Existing planned desktop sessions may still carry explicit target sets/reps or
target durations.
`trainlog-mobile-export` v1 keeps ordered heterogeneous `sets[]`.
Frozen `TRAINLOG_FORMAT_V1` is unchanged.
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 _END -->
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL -->
## Variable sets and session exercise removal checkpoint
Validated functionality in this checkpoint:
```text ```text
VARIABLE_REPETITION_SETS=PASS VARIABLE_REPETITION_SETS=PASS
@ -551,149 +69,95 @@ REPETITION_PYRAMID=PASS
DESKTOP_SCHEMA_V5=PASS DESKTOP_SCHEMA_V5=PASS
V4_TO_V5_MIGRATION_REGRESSION=PASS V4_TO_V5_MIGRATION_REGRESSION=PASS
MOBILE_HETEROGENEOUS_SET_IMPORT=PASS MOBILE_HETEROGENEOUS_SET_IMPORT=PASS
MOBILE_IMPORT_IDEMPOTENCE=PASS
NO_FAKE_UNIFORM_TARGET=PASS NO_FAKE_UNIFORM_TARGET=PASS
ANDROID_SESSION_DRAFT_EXERCISE_REMOVE=PASS
DESKTOP_SESSION_EXERCISE_REMOVE=PASS
``` ```
Accepted repetition examples: ## Direct MTP transport
```text ```text
5x10 USB_MTP_DETECTION=PASS
4,5,6,7,8,9,10,9,8,7,6,5,4 MTP_STORAGE_ACCESS=PASS
4..10..4 MTP_WRITE=PASS
MTP_LIST_FOLDER=PASS
MTP_READ=PASS
MTP_ROUNDTRIP=PASS
DIRECT_MTP_TRANSPORT=PASS
``` ```
A heterogeneous mobile session is persisted as ordered `performed_sets`. No mounted-filesystem dependency is required.
The desktop does not invent `target_sets`, `target_reps` or
`target_duration_seconds` for actual-only mobile observations.
On Android, an exercise already added to the current session can be removed ## Bidirectional synchronization v1
before saving the session.
On the desktop TUI, session editing already supports:
```text ```text
d supprimer ANDROID_TO_PC_MTP=PASS
DESKTOP_MOBILE_IMPORT_V1=PASS
DESKTOP_MOBILE_IMPORT_IDEMPOTENT=PASS
PC_CATALOG_EXPORT_V1=PASS
PC_TO_ANDROID_MTP_PUBLISH=PASS
COMMON_SYNC_ENGINE=PASS
TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS
TUI_SYNC_LOG_SHOW=PASS
BIDIRECTIONAL_SYNC_V1=PASS
``` ```
for removing the selected exercise from a current or persisted session draft.
The database replacement remains transactional.
`TRAINLOG_FORMAT_V1` remains frozen and unchanged.
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL _END -->
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 -->
## Shared bidirectional synchronization v1
Validated architecture: Validated architecture:
```text ```text
Android local write Android local change
-> automatic mobile snapshot -> automatic mobile snapshot
Android "Synchroniser maintenant" Android "Synchroniser maintenant"
-> trainlog-sync-request-v1.json -> request
trainlog-syncd trainlog-syncd
-> shared C synchronization engine -> shared C sync engine
-> Android → PC mobile import -> Android -> PC import
-> PC → Android catalog publish -> PC -> Android catalog
-> trainlog-sync-receipt-v1.json -> receipt
Android Android
-> receipt matched by request_id -> matching receipt
-> PC catalog applied locally -> catalog apply
-> final result displayed -> final result
``` ```
The ncurses TUI and `trainlog-syncd` call the same The TUI invokes the same engine manually.
`trainlog_sync_run()` implementation.
Direct libmtp remains mandatory. No filesystem mount and no SQLite-file ## Current quality baseline
synchronization are introduced.
### Concurrency
The shared engine owns:
```text ```text
$XDG_DATA_HOME/trainlog/sync.lock DESKTOP_TESTS=19/19 PASS
ANDROID_BUILD=PASS
HARDWARE_SYNC_VALIDATION=PASS
TRAINLOG_FORMAT_V1=FROZEN
``` ```
A TUI-triggered transaction waits for the lock. Daemon request polling is ## Current implementation cursor
non-blocking and retries later.
### Sync history No next product feature is frozen by this documentation checkpoint.
Every actual synchronization transaction creates:
```text ```text
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.json NEXT_FEATURE=UNFROZEN
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.txt
``` ```
and appends a compact entry to: Candidate future areas may include richer analytics, measured-max semantics,
additional editing workflows, synchronization hardening, or other product work,
but none is canonical until explicitly selected.
## Permanent constraints
Do not regress to:
```text ```text
$XDG_DATA_HOME/trainlog/sync_history.log SQLite file synchronization
mandatory mounted Android filesystem
exercise-name identity heuristics
fake performed sets for continuous activity
fake uniform targets for heterogeneous actual sets
incompatible changes to Trainlog JSON v1
``` ```
The TUI behaves like:
```text
git log
↑/↓ select synchronization
git show
Enter opens structured detail
```
Legacy three-field history entries remain readable but have no structured
detail file.
### Android request and receipt
Request:
```text
format = trainlog-sync-request
version = 1
```
Receipt:
```text
format = trainlog-sync-receipt
version = 1
```
The receipt carries the originating `request_id`, a generated `sync_id`,
status, summary and synchronization counts. Android ignores a receipt for a
different request ID.
### User service
Install/refresh the user service with:
```text
bash tools/install_syncd_user.sh
```
No root privilege is required.
### Status
```text
COMMON_SYNC_ENGINE=PASS
TUI_SYNC_LOG_SHOW=PASS
TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS
BIDIRECTIONAL_SYNC_V1=PASS
```
Frozen `TRAINLOG_FORMAT_V1` remains unchanged.
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 _END -->

View file

@ -1,24 +1,51 @@
# Trainlog synchronization exchange # Synchronization exchange
## Status ## 1. Status
```text ```text
MOBILE_EXPORT_V1=FROZEN_FOR_IMPLEMENTATION DIRECT_MTP_TRANSPORT=PASS
ANDROID_TO_PC_IMPORT=PASS
PC_TO_ANDROID_CATALOG=PASS
COMMON_SYNC_ENGINE=PASS
TRAINLOG_SYNCD=PASS
ANDROID_TRIGGERED_SYNC=PASS
ANDROID_SYNC_RECEIPT=PASS
TUI_SYNC_LOG_SHOW=PASS
BIDIRECTIONAL_SYNC_V1=PASS
TRAINLOG_FORMAT_V1=FROZEN_UNCHANGED TRAINLOG_FORMAT_V1=FROZEN_UNCHANGED
``` ```
This document defines a synchronization artifact. It is not the frozen Synchronization artifacts are separate from the frozen Trainlog session JSON
Trainlog session JSON v1 format. v1 format.
## Android → PC artifact ## 2. Exchange directory
Shared-storage path: Canonical Android shared-storage directory:
```text ```text
Download/Trainlog/trainlog-mobile-export-v1.json Download/Trainlog
``` ```
Format header: Desktop accesses this directory through direct MTP.
Android accesses PC-created artifacts through a persistent Storage Access
Framework folder grant.
## 3. Artifact table
| Direction | File | Format |
| --- | --- | --- |
| Android -> PC | `trainlog-mobile-export-v1.json` | `trainlog-mobile-export` v1 |
| PC -> Android | `trainlog-pc-catalog-v1.json` | `trainlog-pc-catalog` v1 |
| Android -> PC agent | `trainlog-sync-request-v1.json` | `trainlog-sync-request` v1 |
| PC agent -> Android | `trainlog-sync-receipt-v1.json` | `trainlog-sync-receipt` v1 |
No SQLite file is transferred.
## 4. Android -> PC mobile snapshot
Header:
```json ```json
{ {
@ -35,9 +62,7 @@ sessions
body_observations body_observations
``` ```
### Exercises Exercise profile fields:
Each exercise contains:
```text ```text
exercise_id exercise_id
@ -47,39 +72,27 @@ tracking_mode
data_fields data_fields
``` ```
### Sessions Set-based session exercise actuals use ordered:
Each session contains:
```text
session_id
started_at
session_type
exercises
```
Each session exercise snapshots:
```text
exercise_id
name
recording_mode
tracking_mode
data_fields
load_mode
rest_seconds
```
For `SETS`:
```text ```text
sets[] sets[]
reps
or
duration_seconds
``` ```
For `CONTINUOUS`: with either:
```text
reps
```
or:
```text
duration_seconds
```
Heterogeneous repetition values are valid.
Continuous actuals use:
```text ```text
continuous continuous
@ -88,391 +101,224 @@ continuous
distance_km optional distance_km optional
``` ```
A continuous exercise has no synthetic set. No synthetic set is created for continuous work.
### Body observations ## 5. Desktop mobile importer
Each body observation contains: Reference importer:
```text
observation_id
observed_at
only the metrics actually measured
```
## Transport
Android writes its own export into shared Downloads storage.
Desktop reads the artifact through direct libmtp transport.
No filesystem mount is required.
No SQLite database file is transferred.
## PC → Android
A separate canonical catalog artifact will be defined and implemented after
Android → PC export is validated on physical hardware.
The PC → Android path must not overload frozen Trainlog JSON v1.
<!-- TRAINLOG_DESKTOP_MOBILE_IMPORT_V1 -->
## Desktop import of mobile export v1
The desktop importer is:
```text ```text
tools/import_mobile_export.py tools/import_mobile_export.py
``` ```
It validates the complete mobile snapshot before opening a write transaction.
Properties: Properties:
```text ```text
transactional strict full-snapshot validation
idempotent by stable IDs transactional import
exercise reconciliation by normalized name stable-ID idempotence
profile conflicts rejected catalog reconciliation
unknown JSON fields rejected profile conflict rejection
no SQLite file copying heterogeneous performed-set preservation
targetless schema-v5 import when no true target exists
continuous activity kept separate
``` ```
For mobile `SETS` v1, the Android form records one uniform set metric. The The importer never invents a uniform target merely to fit desktop persistence.
desktop importer derives:
## 6. PC -> Android catalog
The desktop publishes:
```text ```text
target_sets = number of logged sets trainlog-pc-catalog-v1.json
target_reps or target_duration = uniform logged value
``` ```
and preserves all performed sets separately. It is a canonical exercise catalog snapshot containing stable profile metadata.
A v1 mobile session with heterogeneous set metrics or `0 reps` is rejected Android reconciles the received catalog into its local exercise catalog.
rather than inventing a desktop target.
Continuous activities remain target-less and are imported only into This direction does not overload frozen Trainlog session JSON v1.
`continuous_activity`.
Recommended validation sequence: ## 7. Android sync request
```bash Android writes:
python tools/import_mobile_export.py /tmp/trainlog-mobile-export-v1.json --dry-run
python tools/import_mobile_export.py /tmp/trainlog-mobile-export-v1.json
```
Running the real import a second time must import nothing new and report the
existing IDs as skipped.
<!-- TRAINLOG_DESKTOP_MOBILE_IMPORT_V1 _END -->
<!-- TRAINLOG_BIDIRECTIONAL_SYNC_V1 -->
## Bidirectional synchronization v1
One desktop Sync action now performs both directions:
```text ```text
Android → PC trainlog-sync-request-v1.json
direct-MTP download
strict transactional import
PC → Android
canonical PC exercise catalog export
direct-MTP publication
``` ```
The Android app obtains one persistent Storage Access Framework grant for: Header:
```json
{
"format": "trainlog-sync-request",
"version": 1
}
```
Required synchronization identity:
```text ```text
Download/Trainlog request_id = sr_<uuid-v4>
``` ```
After this one-time grant, Android can import the PC-created catalog without The artifact also carries the request timestamp.
broad storage permissions.
The Sync page displays persistent synchronization history instead of remote A new `request_id` represents a new synchronization request.
snapshot counts. A snapshot remaining present is not a pending queue item and
must not be shown as a "candidate".
User-facing session history timestamps are displayed as: ## 8. PC sync receipt
After processing an Android request, the PC publishes:
```text ```text
DD/MM/YYYY HH:MM trainlog-sync-receipt-v1.json
``` ```
Canonical RFC3339 storage remains unchanged. Header:
<!-- TRAINLOG_BIDIRECTIONAL_SYNC_V1 _END -->
<!-- TRAINLOG_ANDROID_AUTO_OUTBOX_REQUEST --> ```json
## Automatic Android outbox and sync request {
"format": "trainlog-sync-receipt",
"version": 1
}
```
Android no longer requires a manual export action. The receipt contains:
The mobile snapshot is refreshed automatically on:
```text ```text
application start request_id
exercise save sync_id
session save status
body observation save summary
PC catalog apply Android -> PC counts
PC -> Android catalog count
``` ```
The Android Sync screen exposes: Android accepts a receipt only when its `request_id` matches the pending
request.
## 9. Shared desktop engine
Canonical implementation:
```text ```text
Synchroniser maintenant trainlog_sync_run()
``` ```
This writes: TUI path:
```text ```text
Download/Trainlog/trainlog-sync-request-v1.json TUI
-> shared engine
``` ```
with a stable request ID and timestamp. Android-triggered path:
The next PC-agent slice consumes this request and writes a sync receipt.
<!-- TRAINLOG_ANDROID_AUTO_OUTBOX_REQUEST _END -->
<!-- TRAINLOG_BIDIRECTIONAL_VALIDATED_CHECKPOINT -->
## Validated bidirectional transport checkpoint
Validated on the physical Samsung device:
```text ```text
ANDROID_TO_PC_MTP=PASS Android request
DESKTOP_MOBILE_IMPORT_V1=PASS -> trainlog-syncd
DESKTOP_MOBILE_IMPORT_IDEMPOTENT=PASS -> shared engine
-> receipt
PC_CATALOG_EXPORT_V1=PASS
PC_TO_ANDROID_MTP_PUBLISH=PASS
``` ```
Artifacts: One synchronization transaction performs:
```text ```text
Android → PC mobile snapshot download
Download/Trainlog/trainlog-mobile-export-v1.json -> mobile import
-> PC catalog export
PC → Android -> PC catalog MTP publication
Download/Trainlog/trainlog-pc-catalog-v1.json -> optional receipt publication
-> structured run history
``` ```
Both are synchronization artifacts and remain separate from frozen ## 10. Concurrency and request consumption
`TRAINLOG_FORMAT_V1`.
The Android Storage Access Framework folder grant must target: Synchronization owns:
```text
Download/Trainlog
```
and the UI must permit changing the stored folder selection.
Remaining synchronization work:
```text
persistent structured sync history
selectable sync detail
common sync engine
trainlog-syncd
Android-triggered request/receipt workflow
automatic mobile snapshot maintenance
```
<!-- TRAINLOG_BIDIRECTIONAL_VALIDATED_CHECKPOINT _END -->
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 -->
## Variable repetition sets
Trainlog preserves each performed set independently.
Accepted repetition input:
```text
5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
`4..10..4` expands to:
```text
4,5,6,7,8,9,10,9,8,7,6,5,4
```
Desktop schema v5 permits targetless `SETS` rows for actual-only mobile
observations. Synchronization therefore does not invent a uniform target when
performed sets are heterogeneous.
`performed_sets` remains the source of truth for actual per-set values.
Existing planned desktop sessions may still carry explicit target sets/reps or
target durations.
`trainlog-mobile-export` v1 keeps ordered heterogeneous `sets[]`.
Frozen `TRAINLOG_FORMAT_V1` is unchanged.
<!-- TRAINLOG_VARIABLE_SET_REPS_V1 _END -->
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL -->
## Variable sets and session exercise removal checkpoint
Validated functionality in this checkpoint:
```text
VARIABLE_REPETITION_SETS=PASS
REPETITION_SHORTHAND_5x10=PASS
REPETITION_EXPLICIT_LIST=PASS
REPETITION_PYRAMID=PASS
DESKTOP_SCHEMA_V5=PASS
V4_TO_V5_MIGRATION_REGRESSION=PASS
MOBILE_HETEROGENEOUS_SET_IMPORT=PASS
MOBILE_IMPORT_IDEMPOTENCE=PASS
NO_FAKE_UNIFORM_TARGET=PASS
ANDROID_SESSION_DRAFT_EXERCISE_REMOVE=PASS
DESKTOP_SESSION_EXERCISE_REMOVE=PASS
```
Accepted repetition examples:
```text
5x10
4,5,6,7,8,9,10,9,8,7,6,5,4
4..10..4
```
A heterogeneous mobile session is persisted as ordered `performed_sets`.
The desktop does not invent `target_sets`, `target_reps` or
`target_duration_seconds` for actual-only mobile observations.
On Android, an exercise already added to the current session can be removed
before saving the session.
On the desktop TUI, session editing already supports:
```text
d supprimer
```
for removing the selected exercise from a current or persisted session draft.
The database replacement remains transactional.
`TRAINLOG_FORMAT_V1` remains frozen and unchanged.
<!-- TRAINLOG_VARIABLE_SETS_CHECKPOINT_FINAL _END -->
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 -->
## Shared bidirectional synchronization v1
Validated architecture:
```text
Android local write
-> automatic mobile snapshot
Android "Synchroniser maintenant"
-> trainlog-sync-request-v1.json
trainlog-syncd
-> shared C synchronization engine
-> Android → PC mobile import
-> PC → Android catalog publish
-> trainlog-sync-receipt-v1.json
Android
-> receipt matched by request_id
-> PC catalog applied locally
-> final result displayed
```
The ncurses TUI and `trainlog-syncd` call the same
`trainlog_sync_run()` implementation.
Direct libmtp remains mandatory. No filesystem mount and no SQLite-file
synchronization are introduced.
### Concurrency
The shared engine owns:
```text ```text
$XDG_DATA_HOME/trainlog/sync.lock $XDG_DATA_HOME/trainlog/sync.lock
``` ```
A TUI-triggered transaction waits for the lock. Daemon request polling is The daemon uses non-blocking acquisition while polling.
non-blocking and retries later.
### Sync history The TUI manual action waits for the active synchronization lock.
Every actual synchronization transaction creates: After a request is completed and its receipt is published, the request ID is
recorded locally so the same request is not processed as a new request again.
## 11. Structured sync history
Every real run has:
```text
sy_<uuid-v4>
```
Artifacts:
```text ```text
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.json $XDG_DATA_HOME/trainlog/sync_runs/sy_*.json
$XDG_DATA_HOME/trainlog/sync_runs/sy_*.txt $XDG_DATA_HOME/trainlog/sync_runs/sy_*.txt
```
and appends a compact entry to:
```text
$XDG_DATA_HOME/trainlog/sync_history.log $XDG_DATA_HOME/trainlog/sync_history.log
``` ```
The TUI behaves like: The TUI presents newest runs in a selectable list and opens the detail file with
`Enter`.
```text Legacy history rows without a `sync_id` remain readable as list entries but
git log cannot have structured detail.
↑/↓ select synchronization
git show ## 12. PC user service
Enter opens structured detail
```
Legacy three-field history entries remain readable but have no structured Install or refresh:
detail file.
### Android request and receipt ```bash
Request:
```text
format = trainlog-sync-request
version = 1
```
Receipt:
```text
format = trainlog-sync-receipt
version = 1
```
The receipt carries the originating `request_id`, a generated `sync_id`,
status, summary and synchronization counts. Android ignores a receipt for a
different request ID.
### User service
Install/refresh the user service with:
```text
bash tools/install_syncd_user.sh bash tools/install_syncd_user.sh
``` ```
Check:
```bash
systemctl --user is-active trainlog-syncd.service
systemctl --user --no-pager --full status trainlog-syncd.service
```
Daemon log:
```bash
tail -f ~/.local/state/trainlog/syncd.log
```
No root privilege is required. No root privilege is required.
### Status ## 13. Transport invariants
Do not regress to:
```text ```text
COMMON_SYNC_ENGINE=PASS SQLite database copying
TUI_SYNC_LOG_SHOW=PASS mandatory GVFS/FUSE mounts
TRAINLOG_SYNCD=PASS exercise-name identity heuristics
ANDROID_TRIGGERED_SYNC=PASS fake sets for continuous activity
ANDROID_SYNC_RECEIPT=PASS fake uniform targets for heterogeneous actual sets
BIDIRECTIONAL_SYNC_V1=PASS overloading frozen Trainlog JSON v1
``` ```
Frozen `TRAINLOG_FORMAT_V1` remains unchanged. ## 14. Hardware validation
<!-- TRAINLOG_SHARED_SYNC_ENGINE_V1 _END -->
Validated on the physical Android device:
```text
MTP device discovery PASS
storage access PASS
read/write/list/delete PASS
Android mobile snapshot download PASS
desktop idempotent import PASS
PC catalog publication PASS
Android request detection PASS
trainlog-syncd processing PASS
receipt publication/readback PASS
multiple distinct Android request IDs PASS
```

View file

@ -1,12 +1,13 @@
# Tests and Validation # Tests and validation
## 1. Principle ## 1. Principle
A feature is not complete without relevant validation. A Trainlog feature is not complete without relevant validation.
The exchange validator is executable specification during the format gates. Frozen formats, persistence migrations, synchronization semantics, and user-data
mutations require executable coverage where practical.
## 2. Canonical exchange validation ## 2. Frozen Trainlog JSON v1
Run: Run:
@ -14,123 +15,30 @@ Run:
python tools/validate_json.py python tools/validate_json.py
``` ```
The command validates: It validates:
- `examples/session-v1.json`; - `examples/session-v1.json`;
- all `tests/fixtures/valid/*.json` as valid; - all positive fixtures;
- all `tests/fixtures/invalid/*.json` as invalid. - all negative fixtures.
A negative fixture passes only when Trainlog rejects it. A negative fixture passes only when Trainlog rejects it.
## 3. Validation layers Semantic validation includes:
A v1 document must pass:
1. JSON Schema validation;
2. Trainlog semantic validation.
Schema handles shape, enumerations, and primitive ranges.
Semantic validation handles cross-object and normalized rules.
## 4. Gate 1 semantic coverage
The canonical validator checks:
- unique `exercise_id`;
- normalized exercise-name uniqueness;
- explicit timestamp offsets;
- end time later than start time;
- exact catalog/reference set equality;
- one workout entry per exercise;
- catalog tracking mode matching target;
- catalog tracking mode matching actual sets;
- load-mode/weight consistency;
- non-blank notes.
## 5. Positive fixture coverage
Gate 1 includes:
- mixed loaded repetition + timed session;
- active session without `ended_at`;
- planned exercise with zero actual sets;
- bodyweight exercise with zero-repetition failed attempt;
- assistance load;
- completely interrupted session with zero exercises.
## 6. Negative fixture coverage
Gate 1 includes rejection of:
- duplicate exercise IDs;
- duplicate normalized exercise names;
- duplicate workout exercise entries;
- end timestamp before start;
- missing timestamp offset;
- target/actual tracking mismatch;
- target containing both repetitions and duration;
- unknown exercise reference;
- unknown JSON field;
- catalog tracking-mode mismatch;
- `load_mode=none` carrying weight;
- loaded target missing weight;
- loaded actual set missing weight;
- unreferenced catalog entries;
- blank session notes;
- blank exercise notes;
- negative actual repetitions.
## 7. Gate 2 compiled validation
The normal Meson suite currently covers:
```text ```text
database stable-ID uniqueness
catalog normalized exercise-name uniqueness
session_detail timestamp offsets
duration end > start
body_metrics catalog/reference equality
bodyviz tracking-mode consistency
exercise_performance load-mode/weight consistency
session_type_schema unknown-field rejection
session_edit non-blank notes
body_observation_edit zero actual repetitions allowed
``` ```
The session-edit test verifies transactional child replacement without changing ## 3. Import reconciliation contract
the parent session identity. The body-observation edit test verifies stable
observation identity while metric values and notes are updated.
Schema validation includes the v1 -> v2 `session_type` migration.
## 8. C validation
Current pre-push validation includes:
- normal strict-warning build;
- the complete Meson test suite;
- `git diff --check`;
- frozen JSON v1 validators.
ASan/UBSan is run for meaningful implementation checkpoints before declaring a
gate complete.
## 9. Pre-push checklist
Before every meaningful push:
1. run `python tools/validate_json.py`;
2. run `python tools/validate_import_contract.py`;
3. run `meson compile -C build`;
4. run `meson test -C build --print-errorlogs`;
5. run sanitizers when relevant;
6. run `git diff --check`;
7. inspect `git status --short`;
8. review documentation changes.
## 10. Catalog reconciliation contract
Before the C17 importer exists, Gate 1 defines local catalog merge behavior through an executable Python specification.
Run: Run:
@ -138,87 +46,171 @@ Run:
python tools/validate_import_contract.py python tools/validate_import_contract.py
``` ```
Canonical cases cover: Coverage includes:
- exact existing exercise reuse;
- same identity with renamed display text;
- same identity with incompatible tracking mode;
- different identities with equivalent normalized names;
- new unique exercise creation.
The full Gate 1 validation command is:
```bash
python tools/validate_json.py
python tools/validate_import_contract.py
git diff --check
```
<!-- TRAINLOG_MTP_VALIDATION -->
## 11. USB/MTP transport validation
The compiled suite now contains:
```text ```text
usb same ID + same name/profile -> reuse
mtp same ID + renamed display text -> controlled reuse/warning
same ID + incompatible mode -> reject
different ID + equivalent normalized name -> reject
new unique identity -> create
``` ```
The `usb` test covers API validation, physical-device enumeration invariants, ## 4. Desktop Meson suite
and rejection of duplicated USB interface children.
The `mtp` test covers bounded API argument validation for storage, folder, Current normal suite:
upload, listing, and download entry points.
Hardware probes additionally validate the real device path: ```text
1 database
2 catalog
3 session_detail
4 duration
5 body_metrics
6 bodyviz
7 exercise_performance
8 session_type_schema
9 session_edit
10 body_observation_edit
11 mtp
12 continuous_session
13 continuous_detail
14 reps
15 exercise_profile_schema
16 usb
17 variable_sets
18 schema_v5_migration
19 mobile_import_variable_sets
```
Validated checkpoint:
```text
19/19 PASS
```
Notable regression coverage:
- transactional persisted-session replacement;
- exercise removal from a session;
- body-observation stable-identity editing;
- profile-aware exercise constraints;
- continuous activity without fake sets;
- repetition shorthand/list/pyramid parsing;
- direct v4 -> v5 database migration;
- heterogeneous mobile-set import;
- targetless mobile SETS persistence;
- mobile-import idempotence.
## 5. Build
```bash
meson setup --reconfigure build
meson compile -C build
meson test -C build --print-errorlogs
```
Strict warning flags remain active. Do not weaken warnings to make a change pass.
## 6. Android build
When Android code changes:
```bash
cd android
printf 'sdk.dir=%s\n' "$HOME/Android/Sdk" > local.properties
JAVA_HOME=/usr/lib/jvm/java-17-openjdk \
./gradlew assembleDebug
```
Install to the connected device when hardware behavior changes:
```bash
adb install -r app/build/outputs/apk/debug/app-debug.apk
```
## 7. Hardware MTP validation
Hardware probes and real synchronization are separate from the normal automated
suite because a test runner cannot assume an unlocked MTP phone.
Available probe binaries include:
```text ```text
trainlog-usb-probe trainlog-usb-probe
trainlog-mtp-probe trainlog-mtp-probe
trainlog-mtp-exchange-probe trainlog-mtp-exchange-probe
trainlog-mtp-roundtrip-probe trainlog-mtp-roundtrip-probe
trainlog-mtp-mobile-export-probe
``` ```
Physical checkpoint result: Current physical baseline:
```text ```text
MTP devices: 1 USB_MTP_DETECTION=PASS
ROUNDTRIP=PASS Trainlog/trainlog-probe.txt MTP_STORAGE_ACCESS=PASS
MTP_WRITE=PASS
MTP_LIST_FOLDER=PASS
MTP_READ=PASS
MTP_ROUNDTRIP=PASS
``` ```
The hardware probe is intentionally separate from the normal automated test ## 8. Bidirectional synchronization validation
suite because CI is not expected to have a connected unlocked Android MTP
device.
<!-- TRAINLOG_MTP_VALIDATION _END -->
<!-- TRAINLOG_CONTINUOUS_TEST_CHECKPOINT --> Validated workflow:
## Profile-aware / continuous validation
Expected normal test suite after this checkpoint:
```text ```text
15 tests Android
-> Synchroniser maintenant
-> unique sr_ request
-> trainlog-syncd
-> shared engine
-> Android -> PC import
-> PC -> Android catalog
-> sy_ structured run
-> matching receipt
-> Android final status
``` ```
Coverage added around: Multiple distinct Android request IDs were processed successfully without
reprocessing one request as a new one.
```text The TUI also invokes the same engine manually and exposes structured run
exercise profile schema details.
profiled catalog creation
schema migration ## 9. Sanitizers
continuous session persistence
continuous session detail loading For meaningful C checkpoints:
```bash
CC=clang meson setup build-asan \
-Db_sanitize=address,undefined \
-Db_lundef=false
meson compile -C build-asan
meson test -C build-asan --print-errorlogs
``` ```
Manual TUI validation includes: ## 10. Pre-push checklist
```text ```bash
Marche configured CONTINUOUS + DURATION + SPEED_KMH meson compile -C build
entry asks duration minutes + speed meson test -C build --print-errorlogs
no sets/rest/load prompts
continuous_activity row persisted python tools/validate_json.py
performed_sets count remains zero python tools/validate_import_contract.py
history reopens as continuous
duration and speed render correctly git diff --check
git status --short
``` ```
<!-- TRAINLOG_CONTINUOUS_TEST_CHECKPOINT _END -->
When Android changed, add:
```bash
cd android
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./gradlew assembleDebug
```
Documentation must describe the resulting state, not retain contradictory old
`NEXT` checkpoints.

File diff suppressed because it is too large Load diff