6.1 KiB
Exercise alias sync canonicalization audit
Date: 2026-09-11
Scope: actual Android/desktop sync artifacts and the production import/export paths which carry an exercise identity. This is an engineering review artifact, not a format or architecture change.
Identity rule
exercise_aliases contains flattened compatibility identities. A source ID is
accepted as input identity and resolves to its live canonical exercise before
identity/conflict comparison. It must not recreate an exercise row. Catalogue,
session, body-zone, and equipment exports publish live canonical exercise IDs;
the alias companion is the only output which publishes retired source IDs.
Surface matrix
| Direction / artifact | Producer or consumer | Incoming/working treatment | Exported identity | Audit result |
|---|---|---|---|---|
Android → PC trainlog-exercise-aliases-v1.json |
tools/import_exercise_aliases.py |
Validates sorted, flattened A→B mappings; moves references from a compatible live A to B and deletes A before persisting the alias | n/a | Canonical; establishes durable identity before other imports |
Android → PC trainlog-mobile-export-v1/v2/v3.json exercise catalogue |
tools/import_mobile_export.py:import_exercises |
Resolves a persisted source alias before normalized-name reconciliation and builds raw-input-ID → canonical-ID mapping | n/a | Canonical; no source resurrection |
| Android → PC mobile session occurrences | tools/import_mobile_export.py:import_sessions and session_semantically_matches |
Uses the catalogue mapping before occurrence insertion and replay identity/content comparison | n/a | Canonical; preserves session ID and entry ID |
Android → PC trainlog-exercise-body-zones-v1.json |
tools/import_exercise_body_zones.py |
Resolves each input ID to one exercise row, groups equal A/B claims by canonical row, rejects divergent claims before writes | n/a | Canonical and order-independent |
Android → PC trainlog-equipment-associations-v2.json |
tools/import_equipment_associations.py |
Previously compared raw incoming A with stored B. Now resolves both through the flattened alias before corroborating (session_id, entry_id); distinct live canonical IDs and equipment divergence remain conflicts |
n/a | Repaired |
PC → Android trainlog-exercise-aliases-v1.json |
tools/export_exercise_aliases.py |
Reads the flattened compatibility table | A→B by definition | Correct exception: sole retired-ID publication |
PC → Android trainlog-pc-catalog-v1.json |
tools/export_pc_catalog.py |
Reads live exercises rows after merge removes A |
B | Canonical |
PC → Android trainlog-pc-mobile-export-v2/v3.json catalogue and occurrences |
tools/export_pc_mobile.py |
Reads live exercise rows and occurrence FKs joined to those rows | B | Canonical; entry/session identities unchanged |
Bidirectional trainlog-exercise-body-zones-v1.json export |
tools/export_exercise_body_zones.py |
Reads body-zone state joined to live exercises | B | Canonical |
Bidirectional trainlog-equipment-associations-v2.json export |
tools/export_equipment_associations.py |
Reads occurrences joined to live exercises | B | Canonical; equipment identity unchanged |
| PC → Android alias companion | TrainlogRepository.applyExerciseAliasesJson |
Imports/merges aliases before the catalogue; moves session/draft/metadata references and removes retired source | n/a | Canonical; no source resurrection |
| PC → Android catalogue | TrainlogRepository.applyPcCatalogJson |
Calls resolveExerciseId before lookup/reconciliation |
n/a | Canonical |
| PC → Android mobile session V2/V3 | TrainlogRepository.applyPcMobileExportJson |
Resolves catalogue and occurrence exercise IDs before row lookup, replay checks, and resumed-MAX checks | n/a | Canonical; entry/session identities unchanged |
| PC → Android body-zone companion | TrainlogRepository.applyExerciseBodyZonesJson |
Resolves input ID before row/baseline lookup | n/a | Canonical |
| PC → Android equipment companion V1/V2 | TrainlogRepository.applyPcEquipmentAssociationsJson |
Resolves input ID before occurrence lookup/corroboration | n/a | Canonical; genuine exercise/equipment conflicts rejected |
| Android → PC mobile/session, body-zone, and equipment producers | TrainlogRepository.buildMobileExportV3Json, buildExerciseBodyZonesJson, and buildEquipmentAssociationsJson |
Read live exercise rows referenced by catalogue/occurrence FKs | B | Canonical |
| Android → PC alias producer | TrainlogRepository.buildExerciseAliasesJson |
Reads the flattened compatibility table | A→B by definition | Correct exception: sole retired-ID publication |
Root cause and repaired precondition
The desktop equipment V2 consumer looked up the occurrence correctly by stable
(session_id, entry_id), but then compared exists[0] != exercise_id using raw
exercise-ID strings. Its only fallback required a selected V2 mobile proof and
the incoming exercise row to have disappeared. Production trainlog_sync_run()
does not pass that optional proof to this importer, and current Android publishes
V3, so a valid persistent A→B alias still failed as conflit exercice association.
The repaired precondition is equality after durable alias resolution. The V2 proof fallback remains bounded to historic reconciliation without a persistent alias. No import path changes a session ID, entry ID, equipment ID, occurrence, schema, or artifact version.
Regression coverage
tests/test_equipment_associations_exchange.py covers direct A→B acceptance
without V2 proof, no source recreation, and rejection of an unrelated live
canonical exercise.
tui/tests/test_sync_body_zone_wiring.c drives the real
trainlog_sync_run(TRAINLOG_SYNC_BIDIRECTIONAL) orchestration against mocked
MTP and temporary storage. It uses live
ex_2d488c08-194c-4051-a3c9-34471646c1d3 plus persistent alias
ex_43c7375f-934c-4650-930c-45807d2f2929 → live ID, imports historical A/B
session and companion rows, replays the run, checks both stable entries remain
bound to B, and verifies that all non-alias outbound artifacts contain B and do
not contain A.