24 KiB
Trainlog Exchange Format v1
Manual session preparations
trainlog-session-preparations V2 has a strict root format, version,
generated_at, bounded deliveries, and bounded withdrawals. Delivery
entries retain the V1 shape. Each withdrawal contains its stable
spw_<uuid-v4> identity, the exact preparation and revision, request timestamp,
and the bounded exact delivery/revision/reserved-execution identities that the
Android consumer may affect. Only a matching local pending delivery becomes
cancelled; a matching started delivery and its execution are preserved.
The durable Android withdrawal row prevents an older V1 delivery or generation
from recreating a startable preparation.
The readable legacy trainlog-session-preparations V1 root has a strict
format, version, generated_at, and bounded deliveries array. Each
delivery contains stable
delivery, preparation, revision, reserved execution and occurrence identities,
immutable metadata, and at most 64 ordered target-only occurrences. Null target
values remain distinct from zero. Performed sets, continuous results, MAX and
feedback are excluded. This format does not change TRAINLOG_FORMAT_V1 or any
mobile snapshot version.
TRAINLOG_AI_SESSION_DRAFT_V1
TRAINLOG_AI_SESSION_DRAFT_V1 is a separately versioned AI-proposal contract,
not an extension of frozen TRAINLOG_FORMAT_V1. Its one-object Drive source is
TrainLog Gdrive:Trainlog/AI/inbox/trainlog_ai_session_draft_v1.json with exact root
{"format":"TRAINLOG_AI_SESSION_DRAFT","version":1,"draft":...}.
The source draft_id is an aid_<uuid-v4> identity, created_at is canonical
UTC, session_type is exactly training, and all object keys are exact.
Text is optional only where stated and, when present, nonempty and trimmed:
title is limited to 120 characters and draft notes to 2,000. Entries have no
notes field; it is rejected as an unknown key. A source has 1..64 contiguous
ordered entries. Each entry resolves a
known exercise (or explicit alias) and is a target-only SETS plan: 1..99
sets, 1..999 reps for REPS tracking or a 1..86,400-second duration for DURATION
tracking, optional positive weight no greater than 10,000, and 0..86,400 rest.
Continuous exercises and a target metric inconsistent with the resolved
profile are rejected. This contract cannot encode performed sets.
Desktop records the source digest with its aid_ identity. The exact same
payload is an idempotent skip; changed content with that identity is a conflict.
After a successful import commit only, the exact validated local snapshot is
copied to TrainLog Gdrive:Trainlog/AI/archive/<aid>.json. The mutable inbox
source is not moved or deleted; logical replay makes archive copy and local
status failures retryable without undoing the import. The outbound
trainlog-ai-session-drafts V1 companion is a distinct strict object for
Android, with at most 256 unpublished drafts per publication. Only after that
artifact is published successfully does desktop transactionally mark its exact
IDs published; a failed publication resends the same deterministic batch, and
later normal syncs deliver the remainder. The permanent source import ledger is
not pruned by publication. Android's durable started and deleted
tombstones prevent an unchanged companion replay from recreating a proposal.
The required real Drive/Android-triggered smoke validation is still pending.
TRAINLOG_AI_EXPORT_V1
TRAINLOG_AI_EXPORT_V1 is a local, user-controlled JSON export of raw desktop
history for manual external analysis. It is not a synchronization format and
contains no recommendation, progression, or inferred attribution.
Create trainlog_ai_export_v1.json in the current directory with:
python tools/export_ai_history.py --database ~/.local/share/trainlog/trainlog.db
An explicit destination may be supplied as the final argument. The exporter
opens SQLite with mode=ro and PRAGMA query_only=ON; it performs no migration
and no persistent write. Format TRAINLOG_AI_EXPORT, version 1, uses
generated_at as an RFC 3339 UTC instant at whole-second precision, rendered
as YYYY-MM-DDTHH:MM:SSZ. All arrays have explicit deterministic ordering;
only generated_at varies between runs.
Immediate raw feedback is nested under the exact occurrence identified by
session_id (the owning session), entry_id, and exercise_id. Session
follow-ups remain in session_followups at session scope. The exporter never
infers that an occurrence caused a next-day observation. The current schema
stores revisioned free text, not structured soreness, intensity, body-zone, or
suspected-exercise fields, so those absent facts are not invented in V1.
1. Status
GATE_0=PASS
GATE_1_REVIEW_01=PASS
GATE_1_REVIEW_02=PASS
GATE_1=PASS
TRAINLOG_FORMAT_V1=FROZEN
This document defines the frozen Trainlog v1 exchange contract. Gate 1 validation and review are complete; incompatible new semantics require a new, separately versioned format.
The active completed-session synchronization artifact is separately versioned
trainlog-mobile-export V3 and is documented in Synchronization
exchange. It does not redefine this frozen V1 schema.
An explicit staged trainlog-mobile-export V4 codec carries nullable
ended_at, causal note values/revisions and observation session_id. A
separate strict trainlog-execution-drafts V1 codec carries confirmed business
state, stable draft/session and occurrence identities, ancestry and pending
state; it never carries raw UI form strings. Neither staged codec changes
TRAINLOG_FORMAT_V1 or is selected by the active synchronization transport.
2. Design goal
Trainlog v1 must represent the training patterns required by the initial applications without turning the Android recorder into a complex training platform.
The format supports:
- repetition-based exercises;
- time-based exercises;
- bodyweight work;
- free-weight and machine load;
- assistance load;
- planned versus performed work;
- planned rest;
- body weight;
- body measurements;
- active or interrupted sessions;
- optional notes.
Distance, velocity, heart rate, per-set measured rest, supersets, and arbitrary custom metrics are outside v1.
3. Encoding and strictness
A Trainlog v1 document is:
- JSON;
- UTF-8;
- one top-level object;
- structurally strict.
Unknown fields are rejected.
This is intentional. A misspelled field must fail validation instead of being silently discarded.
4. Top-level object
Required fields:
{
"format": "trainlog",
"version": 1,
"exercises": [],
"session": {}
}
format must equal trainlog.
version must equal integer 1.
The top-level exercises array contains metadata for exactly the exercises referenced by the session.
It is not a full catalog synchronization document.
5. Exercise identity
Each catalog entry contains:
{
"exercise_id": "leg_press",
"name": "Presse à cuisses",
"tracking_mode": "reps"
}
5.1 exercise_id
exercise_id is the permanent machine identity.
Rules:
- 1 to 128 characters;
- ASCII lowercase;
- first character:
a-zor0-9; - remaining characters:
a-z,0-9,_,-; - unique inside the document;
- unchanged when the visible name changes.
5.2 name
name is the human-readable display name.
The display name is not the identity.
Two exercises in one document must not have equivalent normalized names.
Normalization for comparison is:
- Unicode NFC normalization;
- trim leading and trailing Unicode whitespace;
- collapse each internal whitespace run to one ASCII space;
- Unicode case folding.
The serialized name is never rewritten by this normalization rule.
The future C implementation must use a Unicode implementation capable of reproducing this contract exactly; utf8proc or an equivalent tested implementation is acceptable.
5.3 tracking_mode
Every exercise has one stable tracking mode:
reps
duration
reps is used for repetition-counted exercises.
duration is used for time-counted exercises such as planks.
The tracking mode determines the Android input control and the interpretation of all sets for that exercise.
Changing the fundamental tracking mode of an existing exercise should normally create a new exercise identity rather than silently changing historical semantics.
6. Session identity
A session contains a unique opaque session_id.
Example:
20260905-183412-a84c
Rules:
- 1 to 128 characters;
- starts with an ASCII alphanumeric character;
- remaining characters are ASCII alphanumeric,
_, or-.
The generation algorithm remains implementation-defined.
The database uniqueness constraint is the final anti-duplication barrier.
Importing an already-known session_id is idempotent.
7. Session timestamps
started_at is required.
ended_at is optional.
Both use RFC 3339 / ISO 8601 date-time syntax with an explicit UTC offset.
Examples:
2026-09-05T18:34:12+02:00
2026-09-05T16:34:12Z
Offset-less timestamps are invalid.
If ended_at exists, it must represent an instant strictly later than started_at.
An absent ended_at means the session is still active, interrupted, or otherwise not formally completed.
Trainlog must not invent an end time.
8. Session exercise order
The order of session.exercises is meaningful.
It records the exercise order entered by the user.
The order of sets is also meaningful and defines performed set order.
No separate set number is serialized.
9. Planned work and performed work
Each session exercise contains:
{
"exercise_id": "leg_press",
"load_mode": "external",
"rest_seconds": 60,
"target": {
"sets": 4,
"reps": 5,
"weight_kg": 80
},
"sets": [
{ "reps": 5, "weight_kg": 80 },
{ "reps": 5, "weight_kg": 80 },
{ "reps": 5, "weight_kg": 80 },
{ "reps": 3, "weight_kg": 80 }
]
}
target describes intended work.
sets describes actual performed work.
They must remain separate.
target.sets is the intended number of sets.
The length of sets is the actual number of recorded sets and may be:
- smaller than the target;
- equal to the target;
- greater than the target;
- zero.
An empty actual-set array is valid for a planned exercise that was not performed.
10. Repetition mode
For a tracking_mode of reps:
target.repsis required;target.duration_secondsis forbidden;- each actual set contains
reps; - each actual set forbids
duration_seconds.
Target repetitions must be at least 1.
Actual repetitions may be 0.
A zero-repetition set represents a real attempted set with no completed repetition.
A skipped set should normally be omitted instead.
11. Duration mode
For a tracking_mode of duration:
target.duration_secondsis required;target.repsis forbidden;- each actual set contains
duration_seconds; - each actual set forbids
reps.
Duration values are positive integer seconds.
12. Load model
Each session exercise has exactly one load_mode:
none
external
assistance
12.1 none
Use for exercises where no separate load value is recorded.
Examples:
- bodyweight squat;
- unweighted plank;
- push-up;
- pull-up without added or assisted load.
When load_mode is none, weight_kg is forbidden in the target and actual sets.
12.2 external
Use for a positive externally applied or machine-displayed load.
Examples:
- barbell;
- dumbbell;
- cable machine;
- leg press;
- weighted pull-up.
When load_mode is external:
target.weight_kgis required;- every actual set requires its own
weight_kg.
Actual set load is stored per set so load changes remain representable.
12.3 assistance
Use when the numeric load represents assistance that reduces the effective difficulty of a bodyweight movement.
Example:
Assisted pull-up: 20 kg assistance
When load_mode is assistance:
target.weight_kgis required;- every actual set requires its own
weight_kg.
Assistance remains a positive value.
Analytics must not treat increasing assistance as increasing strength.
12.4 Unit and physical meaning
All serialized loads use kilograms.
For a machine, weight_kg records the load value displayed or declared by the machine/user.
Trainlog does not claim that this value equals exact mechanical force at the body.
This distinction matters when comparing different machines.
13. Rest
rest_seconds is required for every session exercise.
It stores the planned rest interval in integer seconds.
Example:
60
means one minute.
Zero is valid when no planned rest exists.
v1 does not record measured rest between individual sets.
14. Body weight
body_weight_kg is optional.
It is a positive kilogram value associated with the session timestamp.
The TUI database may also support standalone body-weight observations; those records are outside this session-exchange document.
15. Body measurements
measurements is optional.
If present, it contains at least one measurement.
All measurements are circumferences in centimeters unless the field name itself defines another interpretation.
Frozen v1 measurement names proposed by Gate 1 review #1:
neck_cm
shoulders_cm
chest_cm
waist_cm
hips_cm
left_arm_cm
right_arm_cm
left_forearm_cm
right_forearm_cm
left_thigh_cm
right_thigh_cm
left_calf_cm
right_calf_cm
shoulders_cm means shoulder-girdle circumference, not straight-line shoulder width.
Left/right fields are intentionally separate so asymmetry can be followed over time.
16. Notes
Optional UTF-8 notes are supported at:
- session level;
- session-exercise level.
A present note must not be empty or whitespace-only.
Session notes are limited to 4000 characters.
Exercise notes are limited to 1000 characters.
Notes are user content and must be preserved exactly after validation.
17. Catalog completeness
The top-level exercise catalog must contain exactly the exercise identities referenced by session.exercises.
Therefore:
- every session exercise has one matching catalog entry;
- no catalog entry is unreferenced;
- an empty session exercise array requires an empty top-level exercise catalog.
This keeps each session export self-contained without silently importing unrelated Android catalog entries.
18. One workout entry per exercise
A session_id may contain a given exercise_id at most once.
All performed sets for that exercise belong to its single session-exercise entry.
This makes editing and analytics deterministic.
19. Existing local exercise with different display name
Identity wins over display text.
If the TUI already knows an exercise_id and an imported session carries a different display name for that same identifier:
- the session may still import;
- the existing canonical TUI identity is used;
- the import must surface a non-fatal metadata warning;
- the session import must not silently rename the canonical local exercise.
Catalog synchronization and deliberate renaming are separate operations outside the v1 session-import transaction.
20. Structural and semantic validation
A valid v1 document passes both:
- JSON Schema validation;
- Trainlog semantic validation.
Semantic rules include:
- unique exercise identifiers;
- normalized display-name uniqueness;
- explicit timestamp offsets;
- timestamp chronology;
- exact catalog/reference set equality;
- one workout entry per exercise;
- catalog
tracking_modematching target and actual sets; load_modematching weight presence;- non-blank notes.
21. Explicit non-goals for v1
The following are deliberately not represented by v1:
- distance;
- speed;
- velocity;
- heart rate;
- calories;
- measured per-set rest;
- supersets/circuits as first-class objects;
- arbitrary custom set metrics;
- exercise body-zone relations (carried by the separate versioned Body Zones V1 companion, never added to this frozen format);
- machine seat/settings metadata;
- photos;
- cloud synchronization.
Those features can be introduced later without corrupting the simple initial recorder.
22. Freeze criteria
Gate 1 may freeze v1 only after:
- canonical valid fixtures pass;
- canonical invalid fixtures fail for the intended reason;
- Android requirements are aligned;
- TUI requirements are aligned;
- the schema and semantic contract contain no known ambiguity;
- the reviewed commit is pushed and mirrored.
Gate 1 validation and mirrored review completed successfully.
The exchange format is now frozen:
TRAINLOG_FORMAT_V1=FROZEN
Any incompatible semantic or structural change requires a new exchange-format version.
23. Generated identifier policy
The wire format treats exercise_id and session_id as opaque identifiers satisfying their defined syntax.
Official Trainlog implementations that create new identifiers must generate random UUID version 4 values.
Generated exercise identifiers use:
ex_<uuid-v4>
Generated session identifiers use:
se_<uuid-v4>
Example:
ex_550e8400-e29b-41d4-a716-446655440000
se_550e8400-e29b-41d4-a716-446655440000
Importers accept any identifier valid under the v1 schema; they must not require that legacy or externally created IDs follow the UUID generation convention.
The generation rule exists to prevent collisions when Android and TUI can both create exercises independently.
24. Local catalog reconciliation
Document validity and local importability are separate concepts.
For each incoming exercise, the TUI reconciles against the local canonical catalog before the import transaction commits.
Same ID, same tracking mode
Reuse the existing exercise.
If the normalized display name differs, update name and normalized_name in
the existing catalog row. This is a stable-ID rename: completed session
references remain attached to the same row and no second exercise is created.
If another identity already owns the incoming normalized name, reject the
import as an identity conflict.
Same ID, different tracking mode
Reject the entire session import.
A repetition identity and duration identity are semantically incompatible.
Different ID, equivalent normalized name
Reject the entire session import as an identity conflict.
Trainlog must not silently:
- create a duplicate;
- merge identities;
- rewrite historical identifiers.
The user must explicitly reconcile the conflict.
New ID, unique normalized name
Create the exercise as part of the same database transaction as the session import.
Atomicity
Any hard catalog conflict aborts the entire import.
No partial session or exercise data may remain.
25. Gate 1 executable import contract
Before the production importer exists, catalog reconciliation is specified by:
tests/contract/catalog-import-cases.json
tools/validate_import_contract.py
Canonical validation requires both:
python tools/validate_json.py
python tools/validate_import_contract.py
Staged causal deletion V1
trainlog-causal-deletions V1 is independent of every published snapshot.
Its root contains format, integer version, generated_at, and at most 4096
operations; the complete UTF-8 artifact is limited to 4 MiB. Each operation
contains operation_id, target_kind, target_id, creator_id,
predecessor_revision_id, created_at, payload_sha256, and a null
publication_context. Unknown fields, duplicate keys/IDs, invalid timestamps,
invalid hashes, unknown ancestry, and concurrent ancestry are rejected
atomically. Timestamps are evidence, never last-writer-wins authority.
The artifact is available only through explicit repository/tool entry points. It is not selected by V3 transport. Null publication context is the typed handoff to the separate generation association ledger, not consumption proof.
predecessor_revision_id identifies either the canonical complete business
projection or a durable mutation revision created by a supported correction.
For execution drafts the projection is exactly the stable session and draft
revision identities; raw JSON serialization is not identity. Imported
operations undergo the same built-in/alias authorization as local operations.
Readers consume at most the 4 MiB limit plus one sentinel byte, exporters
enumerate at most 4096 operations plus one sentinel row, and a local deletion
is refused before its effect if the complete protection artifact would exceed
either bound.
The V1 target_kind set remains the frozen seven-kind set: session,
execution_draft, exercise, body_observation, custom_equipment,
feedback, and body_zone_relation. Heart-rate correction does not widen this
published contract.
Heart-rate corrections V1
trainlog-heart-rate-corrections V1 is a separate optional Android→desktop
full-generation companion named heart-rate-corrections, stored as
heart-rate-corrections-v1.json. Its root contains format, integer version,
generated_at, and at most 256 corrections; the complete UTF-8 artifact is
limited to 4 MiB. Each correction contains a stable UUIDv4-based operation_id,
stable capture_id, inclusive offset-aware cutoff, creator_id, exact
predecessor_revision_id, created_at, and canonical payload_sha256.
The predecessor hashes the complete original capture metadata, ordered raw BPM samples, and ordered RR children. The consumer requires the cutoff to equal the canonical session end and not precede the session start or latest exercise end. It deletes only samples strictly after the cutoff in one transaction, relies on the declared RR cascade, records durable causal state, and applies that state before any Heart Rate V1 snapshot in the same generation. Unknown fields, duplicate keys/identities, digest mismatch, ancestry mismatch, and incompatible capture/session ownership reject the containing generation atomically.
Staged generation manifest and acknowledgement V1
trainlog-sync-manifest V1 is a strict independent envelope. It contains
generation_id, run_id, producer {peer_id, kind}, consumer_peer_id, an
offset-aware generated_at, nullable parent_generation_id, and the complete
artifact descriptor array. A descriptor contains logical_name, format,
integer version, generation-relative filename, exact byte size, lowercase
SHA-256, and boolean required. Unknown root/descriptor fields, duplicate JSON
keys, duplicate names/paths, unknown required capabilities, unsafe paths, wrong
peer context, and missing required domains are rejected.
The inclusive outer limits are 64 KiB per manifest, 32 descriptors, 64 MiB per
artifact, 256 MiB per generation, 64 ASCII bytes per logical name, 240 UTF-8
bytes per relative path, path depth three, JSON depth eight, and 8192 JSON
nodes. Tighter domain bounds still apply. Artifacts live under
generations/<generation_id>/; the manifest is the final visibility marker.
SHA-256 proves exact byte integrity under that manifest, not authentication,
freshness, or snapshot coherence.
trainlog-sync-ack V1 contains ack_id, run/generation and both peer
identities, the exact manifest digest, consumed|rejected, durability,
offset-aware completion time, a diagnostic limited to 1024 UTF-8 bytes, and a
digest of the ACK payload without payload_sha256. consumed uses
sqlite-commit-full; rejected uses sqlite-commit-rejection. Exact replay
returns the stored ACK. A wrong peer, run, generation, manifest digest, ACK
digest, timestamp, result/durability pair, or conflicting replay is rejected.
Generation membership never changes a causal V1 operation. The immutable
operation retains null publication_context, its original ID and digest;
sync_causal_publications records first emission and retransmission by
operation/generation identity.
Operational full-generation run report
The Web/application boundary uses a separate bounded operational report; it
does not extend any frozen business artifact. It carries the stable run and
request IDs, trigger, requested/effective mode, peer and generation IDs,
canonical phase, monotonic progress revision, real or unavailable counters,
per-domain coverage, bounded draft summaries, diagnostics and separate AI
results. completed is accepted only after the worker supplies the correlated
manifest/ACK evidence. The report is at most 64 KiB and is replaced atomically
with private permissions.