From fd955315ccc9bb835a13eb94d206f1e01d893dfb Mon Sep 17 00:00:00 2001 From: fy59 Date: Wed, 9 Sep 2026 19:43:11 +0200 Subject: [PATCH] feat(training): complete training knowledge v1 --- .agents/skills/trainlog-anatomy/SKILL.md | 44 + AGENTS.md | 10 + CHANGELOG.md | 13 + README.md | 19 +- .../trainlog/data/TrainingKnowledgeCatalog.kt | 383 +++ .../trainlog/data/TrainlogRepository.kt | 360 +++ .../trainlog/data/TrainlogTimestamp.kt | 106 + .../labfytools/trainlog/ui/ExerciseScreen.kt | 73 + .../data/TrainingExerciseContextTest.kt | 206 ++ .../data/TrainingKnowledgeCatalogTest.kt | 182 ++ .../trainlog/data/TrainlogTimestampTest.kt | 33 + catalog/equipment-knowledge-v1.json | 2220 +++++++++++++++++ catalog/exercise-knowledge-v1.json | 1596 ++++++++++++ catalog/joint-actions-v1.json | 808 ++++++ catalog/movement-patterns-v1.json | 538 ++++ catalog/muscles-v1.json | 1509 +++++++++++ catalog/science-references-v1.json | 407 +++ catalog/training-knowledge-audit-v1.json | 1070 ++++++++ docs/android.md | 25 + docs/architecture.md | 33 + docs/current_state.md | 48 +- docs/database.md | 22 + docs/domain/anatomy_and_movement.md | 113 + .../exercise_equipment_interpretation.md | 117 + docs/domain/knowledge_audit.md | 29 + docs/domain/knowledge_system.md | 129 + docs/domain/programming_foundations.md | 93 + docs/exercise_data_model.md | 27 + ...raining_knowledge_v1_engineering_review.md | 122 + docs/reviews/training_knowledge_v1_resume.md | 120 + ...training_knowledge_v1_scientific_review.md | 154 ++ ...training_knowledge_v1_temporal_contract.md | 139 ++ .../training_knowledge_v1_temporal_review.md | 27 + docs/roadmap.md | 14 +- docs/tests.md | 45 + docs/tui.md | 15 + .../invalid/timestamp-space-separator.json | 10 + .../valid/temporal-extended-forms.json | 11 + tests/test_timestamp_validation.py | 57 + tests/test_training_knowledge.py | 133 + tools/generate_training_knowledge.py | 223 ++ tools/generate_training_knowledge_audit.py | 83 + tools/validate_json.py | 103 +- tools/validate_training_knowledge.py | 399 +++ tui/include/trainlog/database.h | 114 + tui/include/trainlog/training_context.h | 50 + tui/include/trainlog/training_knowledge.h | 219 ++ tui/meson.build | 47 + tui/src/database.c | 515 ++++ tui/src/timestamp.c | 108 + tui/src/timestamp.h | 29 + tui/src/training_context.c | 118 + tui/src/tui.c | 179 +- tui/tests/test_training_context.c | 362 +++ tui/tests/test_training_knowledge.c | 107 + tui/tests/test_tui_workflows.c | 57 +- 56 files changed, 13745 insertions(+), 28 deletions(-) create mode 100644 .agents/skills/trainlog-anatomy/SKILL.md create mode 100644 android/app/src/main/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalog.kt create mode 100644 android/app/src/main/java/com/labfytools/trainlog/data/TrainlogTimestamp.kt create mode 100644 android/app/src/test/java/com/labfytools/trainlog/data/TrainingExerciseContextTest.kt create mode 100644 android/app/src/test/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalogTest.kt create mode 100644 android/app/src/test/java/com/labfytools/trainlog/data/TrainlogTimestampTest.kt create mode 100644 catalog/equipment-knowledge-v1.json create mode 100644 catalog/exercise-knowledge-v1.json create mode 100644 catalog/joint-actions-v1.json create mode 100644 catalog/movement-patterns-v1.json create mode 100644 catalog/muscles-v1.json create mode 100644 catalog/science-references-v1.json create mode 100644 catalog/training-knowledge-audit-v1.json create mode 100644 docs/domain/anatomy_and_movement.md create mode 100644 docs/domain/exercise_equipment_interpretation.md create mode 100644 docs/domain/knowledge_audit.md create mode 100644 docs/domain/knowledge_system.md create mode 100644 docs/domain/programming_foundations.md create mode 100644 docs/reviews/training_knowledge_v1_engineering_review.md create mode 100644 docs/reviews/training_knowledge_v1_resume.md create mode 100644 docs/reviews/training_knowledge_v1_scientific_review.md create mode 100644 docs/reviews/training_knowledge_v1_temporal_contract.md create mode 100644 docs/reviews/training_knowledge_v1_temporal_review.md create mode 100644 tests/fixtures/invalid/timestamp-space-separator.json create mode 100644 tests/fixtures/valid/temporal-extended-forms.json create mode 100644 tests/test_timestamp_validation.py create mode 100644 tests/test_training_knowledge.py create mode 100644 tools/generate_training_knowledge.py create mode 100644 tools/generate_training_knowledge_audit.py create mode 100644 tools/validate_training_knowledge.py create mode 100644 tui/include/trainlog/training_context.h create mode 100644 tui/include/trainlog/training_knowledge.h create mode 100644 tui/src/timestamp.c create mode 100644 tui/src/timestamp.h create mode 100644 tui/src/training_context.c create mode 100644 tui/tests/test_training_context.c create mode 100644 tui/tests/test_training_knowledge.c diff --git a/.agents/skills/trainlog-anatomy/SKILL.md b/.agents/skills/trainlog-anatomy/SKILL.md new file mode 100644 index 0000000..f10f3a2 --- /dev/null +++ b/.agents/skills/trainlog-anatomy/SKILL.md @@ -0,0 +1,44 @@ +--- +name: trainlog-anatomy +description: Use Trainlog's cited anatomy and biomechanics knowledge for exercise targeting, BODY ZONES, equipment interpretation, substitution, MAX context, and future workout or program design. +--- + +# Trainlog anatomy + +Resolve domain decisions from scientific evidence before implementation. +Start at `docs/domain/` from the repository root and consult the relevant +canonical assets in `catalog/`: + +- `science-references-v1.json`: evidence, bibliographic metadata and limitations; +- `muscles-v1.json` and `joint-actions-v1.json`: functional anatomy; +- `movement-patterns-v1.json`: explicitly defined programming abstractions; +- `exercise-knowledge-v1.json`: variant-specific roles and BODY ZONE projection; +- `equipment-knowledge-v1.json`: physical equipment and compatible exercises. + +Follow `source_refs` to the cited references. Check that evidence supports the +actual variant and claim; structural validation is not scientific validation. +Use `anatomie` for substantive new research and `anatomie-xhigh` only when one +substantive investigation leaves material scientific ambiguity unresolved. +Scientific conclusions must be settled before implementation agents encode them. + +Preserve these distinctions: + +- Physical machine != exercise != movement pattern != joint action != muscle. +- BODY ZONE is a business/UX projection, not a complete anatomical taxonomy. +- Higher EMG amplitude does not establish better hypertrophy, strength outcomes, + or universal primary-muscle status. +- Manufacturer documentation identifies mechanics; it does not establish + anatomical outcomes. Do not invent a manufacturer, model or trajectory. + +Separate established anatomy, biomechanical interpretation, EMG, intervention +evidence, manufacturer statements and practical inference. Retain `high`, +`moderate` or `uncertain` confidence; downgrade unsupported claims. Unknown +custom exercises stay unclassified until their actual execution is established. + +Use existing exercise/equipment identities. Never infer runtime knowledge from +names, merge multifunction-machine exercises, overwrite persisted BODY ZONES, +or copy user history into scientific catalogs. MAX belongs to an exercise and +its equipment/resistance/execution context; assistance is not external load. + +TRAINING KNOWLEDGE V1 provides read-only knowledge and history composition. +Workout/program prescription and clinical rehabilitation are outside its scope. diff --git a/AGENTS.md b/AGENTS.md index 73ad8cf..12a54e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -324,3 +324,13 @@ A task is complete only when: - synchronization remains idempotent where applicable; - documentation describes the resulting state; - no known regression is intentionally left behind. + +## 14. Training knowledge + +Before domain decisions involving anatomy, biomechanics, exercise targeting, +BODY ZONES, machine/exercise interpretation, substitution, MAX interpretation, +workout generation or program generation, consult the project +[`trainlog-anatomy` skill](.agents/skills/trainlog-anatomy/SKILL.md), +`docs/domain/`, the scientific catalogs in `catalog/`, and their cited references. +Scientific knowledge is separate from runtime user data; preserve explicit +uncertainty and settle scientific conclusions before encoding behavior. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b98318..452c531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,19 @@ Detailed implementation chronology remains available in Git history and ### Added +- `TRAINING_KNOWLEDGE_V1` read-only scientific knowledge infrastructure: + six authored, versioned JSON catalogs with cited references; deterministic C + generation; Android immutable asset loading; stable-ID catalog queries; and + desktop/Android composition of persisted exercise zones, compatible + equipment, explicit MAX and bounded occurrence/set history. The feature + introduces no schema migration, database seeding, synchronization artifact, + runtime prescription or generated UUID association. Scientific review, + independent temporal review, final engineering review, repair verification, + and final executable validation passed. The initial audit's four findings + were closed by one bounded repair chain. See + `docs/reviews/training_knowledge_v1_temporal_contract.md` and + `docs/domain/knowledge_system.md`. + - `BODY_ZONES_V1`: the canonical `catalog/body-zones-v1.json` taxonomy with stable IDs, French display metadata, hierarchy, deterministic sort order and exact stable-exercise-ID migration evidence; diff --git a/README.md b/README.md index 9daf3e8..ccc81ca 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,25 @@ BODY_ZONE_SYNC_V1=PASS BODY_ZONES_DESKTOP_REAL_MIGRATION=PASS BODY_ZONES_ANDROID_DEVICE_VALIDATION=PASS -DESKTOP_TESTS=39/39 PASS +DESKTOP_TESTS=42/42 PASS (recorded validation checkpoint) ANDROID_BUILD=PASS + +TRAINING_KNOWLEDGE_V1=PASS ``` +`TRAINING_KNOWLEDGE_V1` has passed its bounded scientific review, independent +temporal delta review, final engineering audit, repair verification, and final +executable validation. Its C and +Android occurrence pages and latest-MAX context share the settled exact-instant +ordering and source-text cursor contract. The independent temporal review +returned PASS with no findings. The initial full-tranche audit's four findings +were resolved by one bounded repair chain and independently verified. Fresh +validation passed: strict build, 42 Meson tests, Python knowledge/temporal +tests, validators, headers, sanitizers, 12-form temporal probes, Android 56 +tests with one known fixture skip, and Java 17 debug assembly. +The [temporal correction record](docs/reviews/training_knowledge_v1_temporal_contract.md) +defines the accepted grammar, regression evidence and remaining review boundary. + ## Architecture ```text @@ -255,6 +270,8 @@ Exercise-zone metadata travels separately in the sole bidirectional - `docs/exchange_format.md`: frozen Trainlog JSON v1 contract; - `docs/tests.md`: validation strategy; - `docs/roadmap.md`: completed gates and future cursor; +- `docs/domain/knowledge_system.md`: read-only scientific knowledge system, + runtime-context boundary and documented future planning pipeline; - `AGENTS.md`: development contract. ## Development principles diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalog.kt b/android/app/src/main/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalog.kt new file mode 100644 index 0000000..7f74f66 --- /dev/null +++ b/android/app/src/main/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalog.kt @@ -0,0 +1,383 @@ +package com.labfytools.trainlog.data + +import android.content.Context +import org.json.JSONArray +import org.json.JSONObject +import java.util.Locale + +enum class KnowledgeConfidence { HIGH, MODERATE, UNCERTAIN } +enum class MuscleEntityType { MUSCLE, MUSCLE_REGION, MUSCLE_GROUP } +enum class ExerciseKnowledgeStatus { RESOLVED_FAMILY_VARIANT_LIMITED, CONDITIONAL, UNRESOLVED } +enum class BodyZoneAuditStatus { CONFIRMED, QUESTIONABLE, UNRESOLVED } +enum class BodyZoneAuditSeverity { NONE, INFORMATION } +enum class EquipmentScienceStatus { + SCIENTIFICALLY_DOCUMENTED, + MECHANICALLY_IDENTIFIED_ANATOMY_INCOMPLETE, + EQUIPMENT_IDENTITY_UNCERTAIN, +} +enum class MuscleRole { PRIMARY, SECONDARY, STABILIZERS } + +data class ScienceReference( + val refId: String, val title: String, val authorsOrOrganization: String, val year: Int?, + val type: String, val url: String, val topics: List, val notes: String, + val limitations: String, val doi: String?, val pmid: String?, val accessedOn: String, + val publicationNote: String?, +) + +data class MuscleKnowledge( + val muscleId: String, val displayName: String, val displayNameFr: String, + val entityType: MuscleEntityType, val anatomicalGroup: String, val aggregateGroupId: String?, + val bodyZoneId: String, val bodyZoneIds: List, val jointActionIds: List, + val primaryActions: List, val primaryActionsSemantics: String, + val overlapWarning: String, val functionalNotes: String, val confidence: KnowledgeConfidence, + val evidenceType: String, val sourceRefs: List, val memberMuscleIds: List, +) + +data class JointActionKnowledge( + val actionId: String, val displayNameFr: String, val definition: String, + val anatomicalRegion: String, val jointComplex: String, val jointOrComplex: String, + val principalPlane: String, val planeNotes: String, val contributingMuscleIds: List, + val contributorSemantics: String, val evidenceType: String, val confidence: KnowledgeConfidence, + val sourceRefs: List, val notes: String, +) + +data class MovementPatternKnowledge( + val patternId: String, val displayNameFr: String, val definition: String, + val typicalActionIds: List, val typicalBodyZoneIds: List, + val evidenceType: String, val confidence: KnowledgeConfidence, val sourceRefs: List, + val notes: String, val bodyZoneSemantics: String?, val parentPatternId: String?, +) + +data class ExerciseInterpretation( + val familyDescription: String, val actionIds: List, val patternIds: List, + val primaryMuscleIds: List, val secondaryMuscleIds: List, + val stabilizerMuscleIds: List, val primaryZoneId: String, + val secondaryZoneIds: List, val confidence: KnowledgeConfidence, + val evidenceType: String, val sourceRefs: List, val variantNotes: String, + val roleNotes: String, val requiredConfirmation: String? = null, +) { + fun muscles(role: MuscleRole): List = when (role) { + MuscleRole.PRIMARY -> primaryMuscleIds + MuscleRole.SECONDARY -> secondaryMuscleIds + MuscleRole.STABILIZERS -> stabilizerMuscleIds + } +} + +data class ScientificBodyZoneMapping(val primaryZoneId: String, val secondaryZoneIds: List) +data class ExistingBodyZoneMapping(val primaryZoneId: String?, val secondaryZoneIds: List) + +data class BodyZoneAudit( + val status: BodyZoneAuditStatus, val severity: BodyZoneAuditSeverity, + val confidence: KnowledgeConfidence, val rationale: String, + val existingPrimaryZoneId: String?, val existingSecondaryZoneIds: List, + val proposedMutation: String?, val sourceRefs: List, +) + +data class ExerciseKnowledge( + val exerciseId: String, val exerciseName: String, val equipmentIds: List, + val identityEvidence: String, val identityEvidenceType: String, + val resolutionStatus: ExerciseKnowledgeStatus, val confidence: KnowledgeConfidence, + val interpretation: ExerciseInterpretation?, val conditionalInterpretation: ExerciseInterpretation?, + val existingBodyZones: ExistingBodyZoneMapping, val sourceRefs: List, + val limitations: List, val equipmentLinkStatus: String?, val bodyZoneAudit: BodyZoneAudit, +) + +data class EquipmentCapabilityKnowledge( + val displayName: String, val exerciseIds: List, val linkStatus: String, + val interpretation: ExerciseInterpretation?, val requirements: String, +) + +data class EquipmentKnowledge( + val equipmentId: String, val manufacturer: String?, val model: String?, + val identificationStatus: String, val scienceStatus: EquipmentScienceStatus, + val catalogType: String, val catalogLoadSemantics: EquipmentLoadSemantics?, + val mechanics: String, val evidenceType: String, val confidence: KnowledgeConfidence, + val sourceRefs: List, val capabilities: List, + val requiresActualExercise: Boolean, val limitations: List, val auditNote: String?, + val scientificStatusScope: String, +) + +data class KnowledgeExerciseFilters( + val movementPatternId: String? = null, + val muscleId: String? = null, + val muscleRole: MuscleRole? = null, + val scientificZoneId: String? = null, + val includeZoneDescendants: Boolean = true, + val equipmentId: String? = null, +) + +/** + * Immutable TRAINING KNOWLEDGE V1 view over the six shared repository assets. + * WHY: strict loading makes malformed scientific data a deployment failure and + * prevents Android from quietly developing a second, hand-maintained taxonomy. + */ +class TrainingKnowledgeCatalog private constructor( + val references: List, val muscles: List, + val jointActions: List, val movementPatterns: List, + val exercises: List, val equipment: List, + private val bodyZones: BodyZoneCatalog, +) { + private val referencesById = references.associateBy { it.refId } + private val musclesById = muscles.associateBy { it.muscleId } + private val actionsById = jointActions.associateBy { it.actionId } + private val patternsById = movementPatterns.associateBy { it.patternId } + private val exercisesById = exercises.associateBy { it.exerciseId } + private val equipmentById = equipment.associateBy { it.equipmentId } + + fun getReference(refId: String) = referencesById[refId] + fun getMuscle(muscleId: String) = musclesById[muscleId] + fun getJointAction(actionId: String) = actionsById[actionId] + fun getMovementPattern(patternId: String) = patternsById[patternId] + fun getExerciseKnowledge(exerciseId: String): ExerciseKnowledge? = exercisesById[exerciseId] + fun getConditionalExerciseKnowledge(exerciseId: String): ExerciseKnowledge? = + exercisesById[exerciseId]?.takeIf { it.resolutionStatus == ExerciseKnowledgeStatus.CONDITIONAL } + fun getEquipmentKnowledge(equipmentId: String): EquipmentKnowledge? = equipmentById[equipmentId] + + fun getScientificBodyZoneMapping(exerciseId: String): ScientificBodyZoneMapping? = + resolvedInterpretation(exerciseId)?.let { ScientificBodyZoneMapping(it.primaryZoneId, it.secondaryZoneIds) } + + fun listExercisesByMovementPattern(patternId: String): List { + require(patternsById.containsKey(patternId)) { "pattern_id inconnu: $patternId" } + return queryExercises(KnowledgeExerciseFilters(movementPatternId = patternId)) + } + + fun listExercisesByMuscle(muscleId: String, role: MuscleRole): List { + require(musclesById.containsKey(muscleId)) { "muscle_id inconnu: $muscleId" } + return queryExercises(KnowledgeExerciseFilters(muscleId = muscleId, muscleRole = role)) + } + + fun listCompatibleExercises(equipmentId: String): List { + require(equipmentById.containsKey(equipmentId)) { "equipment_id inconnu: $equipmentId" } + return queryExercises(KnowledgeExerciseFilters(equipmentId = equipmentId)) + } + + fun queryExercises(filters: KnowledgeExerciseFilters): List { + filters.movementPatternId?.let { require(patternsById.containsKey(it)) { "pattern_id inconnu: $it" } } + filters.muscleId?.let { require(musclesById.containsKey(it)) { "muscle_id inconnu: $it" } } + require((filters.muscleId == null) == (filters.muscleRole == null)) { + "muscleId et muscleRole doivent être fournis ensemble" + } + filters.equipmentId?.let { require(equipmentById.containsKey(it)) { "equipment_id inconnu: $it" } } + val zones = filters.scientificZoneId?.let { + require(bodyZones.lookup(it) != null) { "zone_id inconnu: $it" } + if (filters.includeZoneDescendants) bodyZones.descendantsAndSelf(it) else setOf(it) + } + return exercises.filter { exercise -> + val interpretation = resolvedInterpretation(exercise.exerciseId) ?: return@filter false + (filters.movementPatternId == null || filters.movementPatternId in interpretation.patternIds) && + (filters.muscleId == null || filters.muscleId in interpretation.muscles(filters.muscleRole!!)) && + (filters.equipmentId == null || filters.equipmentId in exercise.equipmentIds) && + (zones == null || interpretation.primaryZoneId in zones || + interpretation.secondaryZoneIds.any { it in zones }) + } + } + + private fun resolvedInterpretation(exerciseId: String): ExerciseInterpretation? = + exercisesById[exerciseId]?.takeIf { + it.resolutionStatus == ExerciseKnowledgeStatus.RESOLVED_FAMILY_VARIANT_LIMITED + }?.interpretation + + companion object { + private val confidenceValues = KnowledgeConfidence.entries.associateBy { it.name.lowercase(Locale.ROOT) } + private val idPattern = Regex("[a-z][a-z0-9_]*") + private val exerciseIdPattern = Regex("ex_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}") + private val equipmentIdPattern = Regex("(?:[a-z][a-z0-9_]*|eq_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})") + private val scientificReferenceTypes = setOf("established_anatomy", "emg_evidence", "intervention_evidence") + + fun load(context: Context): TrainingKnowledgeCatalog = load( + context = context, + bodyZones = BodyZoneCatalog.load(context), + ) + + internal fun load(context: Context, bodyZones: BodyZoneCatalog): TrainingKnowledgeCatalog { + return load({ name -> context.assets.open(name).bufferedReader().use { it.readText() } }, bodyZones) + } + + internal fun load(assetText: (String) -> String, bodyZones: BodyZoneCatalog): TrainingKnowledgeCatalog { + fun root(name: String, format: String, collection: String): JSONArray { + val text = assetText(name) + DuplicateJsonKeyValidator.validate(text) + val value = JSONObject(text) + requireKeys(value, setOf("format", "version", collection), collection) + check(value.getString("format") == format) { "$collection: format non pris en charge" } + check(value.rawInt("version") == 1) { "$collection: version non prise en charge" } + return value.getJSONArray(collection) + } + val referenceJson = root("science-references-v1.json", "trainlog-science-references-v1", "references") + val muscleJson = root("muscles-v1.json", "trainlog-muscles-v1", "muscles") + val actionJson = root("joint-actions-v1.json", "trainlog-joint-actions-v1", "joint_actions") + val patternJson = root("movement-patterns-v1.json", "trainlog-movement-patterns-v1", "movement_patterns") + val exerciseJson = root("exercise-knowledge-v1.json", "trainlog-exercise-knowledge-v1", "exercises") + val equipmentJson = root("equipment-knowledge-v1.json", "trainlog-equipment-knowledge-v1", "equipment") + + val references = referenceJson.objects("ref_id") { item -> + requireKeys(item, setOf("ref_id", "title", "authors_or_organization", "year", "type", "url", "topics", "notes", "limitations", "doi", "pmid", "accessed_on"), "reference", setOf("publication_note")) + ScienceReference(item.text("ref_id"), item.text("title"), item.text("authors_or_organization"), item.nullableInt("year"), item.text("type"), item.text("url"), item.strings("topics"), item.text("notes"), item.text("limitations"), item.nullableString("doi"), item.nullableString("pmid"), item.text("accessed_on"), item.optionalString("publication_note")) + } + val refIds = references.map { it.refId }.toSet() + val referenceTypes = references.associate { it.refId to it.type } + + data class RawMuscle(val item: JSONObject) + val rawMuscles = muscleJson.objects("muscle_id") { RawMuscle(it) } + val muscleIds = rawMuscles.map { it.item.text("muscle_id") }.toSet() + val actionIds = actionJson.ids("action_id") + val patternIds = patternJson.ids("pattern_id") + val zoneIds = bodyZones.zones.map { it.zoneId }.toSet() + val equipmentIds = equipmentJson.ids("equipment_id") + val exerciseIds = exerciseJson.ids("exercise_id") + // CONTRACT: V1 catalogs are additive; identity, ordering, and cross-reference + // validation below define integrity without freezing today's collection sizes. + + val muscles = rawMuscles.map { raw -> + val item = raw.item + val required = setOf("muscle_id", "display_name", "display_name_fr", "entity_type", "anatomical_group", "aggregate_group_id", "body_zone_id", "body_zone_ids", "joint_action_ids", "primary_actions", "primary_actions_semantics", "overlap_warning", "functional_notes", "confidence", "evidence_type", "source_refs") + requireKeys(item, required, "muscle", setOf("member_muscle_ids")) + val aggregate = item.nullableString("aggregate_group_id") + check(aggregate == null || aggregate in muscleIds) { "aggregate_group_id pendant" } + val members = item.optionalStrings("member_muscle_ids") + check(members.all { it in muscleIds }) { "membre musculaire pendant" } + val zones = item.strings("body_zone_ids"); val actions = item.strings("joint_action_ids"); val primaryActions = item.strings("primary_actions"); val refs = item.strings("source_refs", true) + check(item.text("body_zone_id") in zoneIds && zones.all { it in zoneIds } && actions.all { it in actionIds } && primaryActions.all { it in actionIds } && refs.all { it in refIds }) { "référence croisée muscle invalide" } + MuscleKnowledge(item.text("muscle_id"), item.text("display_name"), item.text("display_name_fr"), enumValue(item.text("entity_type"), MuscleEntityType.entries), item.text("anatomical_group"), aggregate, item.text("body_zone_id"), zones, actions, primaryActions, item.text("primary_actions_semantics"), item.text("overlap_warning"), item.string("functional_notes"), confidence(item), item.text("evidence_type"), refs, members) + } + + val actions = actionJson.objects("action_id") { item -> + requireKeys(item, setOf("action_id", "display_name_fr", "definition", "anatomical_region", "joint_complex", "joint_or_complex", "principal_plane", "plane_notes", "contributing_muscle_ids", "contributor_semantics", "evidence_type", "confidence", "source_refs", "notes"), "joint_action") + val contributors = item.strings("contributing_muscle_ids"); val refs = item.strings("source_refs", true) + check(contributors.all { it in muscleIds } && refs.all { it in refIds }) { "référence croisée action invalide" } + JointActionKnowledge(item.text("action_id"), item.text("display_name_fr"), item.text("definition"), item.text("anatomical_region"), item.text("joint_complex"), item.text("joint_or_complex"), item.text("principal_plane"), item.text("plane_notes"), contributors, item.text("contributor_semantics"), item.text("evidence_type"), confidence(item), refs, item.text("notes")) + } + val patterns = patternJson.objects("pattern_id") { item -> + val required = setOf("pattern_id", "display_name_fr", "definition", "typical_action_ids", "typical_body_zone_ids", "evidence_type", "confidence", "source_refs", "notes") + requireKeys(item, required, "movement_pattern", setOf("body_zone_semantics", "parent_pattern_id")) + val typicalActions = item.strings("typical_action_ids"); val typicalZones = item.strings("typical_body_zone_ids"); val refs = item.strings("source_refs", true) + check(typicalActions.all { it in actionIds } && typicalZones.all { it in zoneIds } && refs.all { it in refIds }) { "référence croisée pattern invalide" } + val parent = item.optionalString("parent_pattern_id"); check(parent == null || parent in patternIds) { "parent_pattern_id pendant" } + MovementPatternKnowledge(item.text("pattern_id"), item.text("display_name_fr"), item.text("definition"), typicalActions, typicalZones, item.text("evidence_type"), confidence(item), refs, item.text("notes"), item.optionalString("body_zone_semantics"), parent) + } + fun interpretation(item: JSONObject, conditional: Boolean): ExerciseInterpretation { + val required = setOf("family_description", "action_ids", "pattern_ids", "primary_muscle_ids", "secondary_muscle_ids", "stabilizer_muscle_ids", "primary_zone_id", "secondary_zone_ids", "confidence", "evidence_type", "source_refs", "variant_notes", "role_notes") + requireKeys(item, required, "interpretation", if (conditional) setOf("required_confirmation") else emptySet()) + val actionRefs = item.strings("action_ids"); val patternRefs = item.strings("pattern_ids") + val primary = item.strings("primary_muscle_ids"); val secondary = item.strings("secondary_muscle_ids"); val stabilizers = item.strings("stabilizer_muscle_ids") + check((primary + secondary + stabilizers).size == (primary + secondary + stabilizers).toSet().size) { "muscle présent dans plusieurs rôles" } + val secondaryZones = item.strings("secondary_zone_ids"); val refs = item.strings("source_refs", true); val primaryZone = item.text("primary_zone_id") + check(actionRefs.all { it in actionIds } && patternRefs.all { it in patternIds } && (primary + secondary + stabilizers).all { it in muscleIds } && primaryZone in zoneIds && secondaryZones.all { it in zoneIds } && primaryZone !in secondaryZones && refs.all { it in refIds }) { "référence croisée interprétation invalide" } + return ExerciseInterpretation(item.text("family_description"), actionRefs, patternRefs, primary, secondary, stabilizers, primaryZone, secondaryZones, confidence(item), item.text("evidence_type"), refs, item.text("variant_notes"), item.text("role_notes"), item.optionalString("required_confirmation")) + } + val exercises = exerciseJson.objects("exercise_id") { item -> + val required = setOf("exercise_id", "exercise_name", "equipment_ids", "identity_evidence", "identity_evidence_type", "resolution_status", "confidence", "interpretation", "conditional_interpretation", "existing_body_zones", "source_refs", "limitations", "body_zone_audit") + requireKeys(item, required, "exercise", setOf("equipment_link_status")) + val status = enumValue(item.text("resolution_status"), ExerciseKnowledgeStatus.entries) + val regular = item.nullableObject("interpretation")?.let { interpretation(it, false) } + val conditional = item.nullableObject("conditional_interpretation")?.let { interpretation(it, true) } + check((status == ExerciseKnowledgeStatus.RESOLVED_FAMILY_VARIANT_LIMITED && regular != null && conditional == null) || (status == ExerciseKnowledgeStatus.CONDITIONAL && regular == null && conditional != null) || (status == ExerciseKnowledgeStatus.UNRESOLVED && regular == null && conditional == null)) { "interprétation incompatible avec resolution_status" } + val equipmentRefs = item.strings("equipment_ids"); val refs = item.strings("source_refs") + check(status == ExerciseKnowledgeStatus.UNRESOLVED || refs.isNotEmpty()) { "exercice résolu/conditionnel sans source" } + check(equipmentRefs.all { it in equipmentIds } && refs.all { it in refIds }) { "référence croisée exercice invalide" } + val existing = item.getJSONObject("existing_body_zones"); requireKeys(existing, setOf("primary_zone_id", "secondary_zone_ids"), "existing_body_zones") + val existingZones = ExistingBodyZoneMapping(existing.nullableString("primary_zone_id"), existing.strings("secondary_zone_ids")); check((existingZones.primaryZoneId == null || existingZones.primaryZoneId in zoneIds) && existingZones.secondaryZoneIds.all { it in zoneIds }) { "zone existante pendante" } + val audit = item.getJSONObject("body_zone_audit") + requireKeys(audit, setOf("status", "severity", "confidence", "rationale", "existing_primary_zone_id", "existing_secondary_zone_ids", "proposed_mutation", "source_refs"), "body_zone_audit") + val auditPrimary = audit.nullableString("existing_primary_zone_id") + val auditSecondary = audit.strings("existing_secondary_zone_ids") + val auditRefs = audit.strings("source_refs") + check((auditPrimary == null || auditPrimary in zoneIds) && auditSecondary.all { it in zoneIds }) { "zone d'audit pendante" } + check(auditRefs.all { it in refIds }) { "référence d'audit pendante" } + // INVARIANT: every non-unresolved BODY ZONE conclusion remains traceable + // to evidence, matching the canonical cross-catalog validator. + check(audit.text("status") == "unresolved" || auditRefs.isNotEmpty()) { "audit BODY ZONE résolu sans source" } + val bodyZoneAudit = BodyZoneAudit( + enumValue(audit.text("status"), BodyZoneAuditStatus.entries), + enumValue(audit.text("severity"), BodyZoneAuditSeverity.entries), + confidence(audit), audit.text("rationale"), auditPrimary, auditSecondary, + audit.nullableString("proposed_mutation"), auditRefs, + ) + ExerciseKnowledge(item.text("exercise_id"), item.text("exercise_name"), equipmentRefs, item.text("identity_evidence"), item.text("identity_evidence_type"), status, confidence(item), regular, conditional, existingZones, refs, item.strings("limitations"), item.optionalString("equipment_link_status"), bodyZoneAudit) + } + val linked = mutableSetOf>() + val equipment = equipmentJson.objects("equipment_id") { item -> + val required = setOf("equipment_id", "manufacturer", "model", "identification_status", "scientific_status", "catalog_type", "catalog_load_semantics", "mechanics", "evidence_type", "confidence", "source_refs", "capabilities", "requires_actual_exercise", "limitations", "scientific_status_scope") + requireKeys(item, required, "equipment", setOf("audit_note")) + val refs = item.strings("source_refs", true); check(refs.all { it in refIds }) { "référence équipement pendante" } + val conf = confidence(item); check(!(conf == KnowledgeConfidence.HIGH && item.text("evidence_type") == "manufacturer_statement")) { "cartographie anatomique élevée fondée seulement sur fabricant" } + // CONTRACT: HIGH equipment mappings require at least one admissible + // scientific source type; Android must accept exactly the validator policy. + check(conf != KnowledgeConfidence.HIGH || refs.any { referenceTypes[it] in scientificReferenceTypes }) { "cartographie équipement élevée sans source scientifique" } + val capabilities = item.getJSONArray("capabilities").objectsUnordered { capability -> + requireKeys(capability, setOf("display_name", "exercise_ids", "link_status", "interpretation", "requirements"), "capability") + val capabilityExercises = capability.strings("exercise_ids"); check(capabilityExercises.all { it in exerciseIds }) { "exercice fantôme dans capability" } + check(capability.text("link_status") in setOf("linked_existing", "linked_identity_conditional_interpretation", "unlinked_capability", "catalog_compatible_not_observed_occurrence")) { "link_status invalide" } + capabilityExercises.forEach { linked += it to item.text("equipment_id") } + // WHY: a one-sided capability would make desktop and Android + // compatibility queries disagree over the same immutable assets. + capabilityExercises.forEach { exerciseId -> + check(item.text("equipment_id") in exercises.single { it.exerciseId == exerciseId }.equipmentIds) { "compatibilité équipement/exercice asymétrique" } + } + EquipmentCapabilityKnowledge(capability.text("display_name"), capabilityExercises, capability.text("link_status"), capability.nullableObject("interpretation")?.let { interpretation(it, false) }, capability.text("requirements")) + } + EquipmentKnowledge(item.text("equipment_id"), item.nullableString("manufacturer"), item.nullableString("model"), item.text("identification_status"), enumValue(item.text("scientific_status"), EquipmentScienceStatus.entries), item.text("catalog_type"), item.nullableString("catalog_load_semantics")?.let { enumValue(it, EquipmentLoadSemantics.entries) }, item.text("mechanics"), item.text("evidence_type"), conf, refs, capabilities, item.rawBoolean("requires_actual_exercise"), item.strings("limitations"), item.optionalString("audit_note"), item.text("scientific_status_scope")) + } + exercises.forEach { exercise -> exercise.equipmentIds.forEach { check(exercise.exerciseId to it in linked) { "compatibilité exercice/équipement asymétrique" } } } + return TrainingKnowledgeCatalog(references, muscles, actions, patterns, exercises, equipment, bodyZones) + } + + private fun confidence(item: JSONObject): KnowledgeConfidence = confidenceValues[item.text("confidence")] ?: error("confidence invalide") + private fun > enumValue(value: String, entries: List): T = entries.firstOrNull { it.name.lowercase(Locale.ROOT) == value } ?: error("valeur enum invalide: $value") + private fun requireKeys(value: JSONObject, required: Set, where: String, optional: Set = emptySet()) { + val keys = value.keys().asSequence().toSet(); check(required.all { it in keys } && keys.all { it in required || it in optional }) { "$where: clés invalides" } + } + private fun JSONObject.text(key: String): String = get(key).let { check(it is String && it.isNotBlank()) { "$key: texte requis" }; it } + private fun JSONObject.string(key: String): String = get(key).let { check(it is String) { "$key: chaîne attendue" }; it } + private fun JSONObject.nullableString(key: String): String? = if (isNull(key)) null else get(key).let { check(it is String) { "$key: chaîne attendue" }; it } + private fun JSONObject.optionalString(key: String): String? = if (!has(key) || isNull(key)) null else nullableString(key) + private fun JSONObject.rawInt(key: String): Int = get(key).let { check(it is Int) { "$key: entier attendu" }; it } + private fun JSONObject.nullableInt(key: String): Int? = if (isNull(key)) null else rawInt(key) + private fun JSONObject.rawBoolean(key: String): Boolean = get(key).let { check(it is Boolean) { "$key: booléen attendu" }; it } + private fun JSONObject.nullableObject(key: String): JSONObject? = if (isNull(key)) null else get(key).let { check(it is JSONObject) { "$key: objet attendu" }; it } + private fun JSONObject.strings(key: String, nonempty: Boolean = false): List = get(key).let { value -> + check(value is JSONArray) { "$key: tableau attendu" }; List(value.length()) { index -> value.get(index).let { check(it is String && it.isNotBlank()) { "$key: chaîne vide/invalide" }; it } }.also { + check(it.size == it.toSet().size && (!nonempty || it.isNotEmpty())) { "$key: doublon ou tableau vide" } + if (key.endsWith("_ids") || key == "source_refs" || key == "joint_action_ids" || key == "primary_actions") { + check(it == it.sorted()) { "$key: ordre stable requis" } + } + } + } + private fun JSONObject.optionalStrings(key: String): List = if (has(key)) strings(key) else emptyList() + private fun JSONArray.ids(key: String): Set = objects(key) { it.text(key) }.toSet() + private fun JSONArray.objects(key: String, transform: (JSONObject) -> T): List { + val previous = mutableSetOf(); var last: String? = null + return objectsUnordered { item -> + val id = item.text(key) + val validId = when (key) { + "exercise_id" -> exerciseIdPattern.matches(id) + "equipment_id" -> equipmentIdPattern.matches(id) + else -> idPattern.matches(id) + } + check(validId) { "$key invalide" } + check(previous.add(id) && (last == null || last!! < id)) { "$key dupliqué ou hors ordre" } + last = id + transform(item) + } + } + private fun JSONArray.objectsUnordered(transform: (JSONObject) -> T): List = List(length()) { index -> get(index).let { check(it is JSONObject) { "objet attendu" }; transform(it) } } + } +} + +/** Small lexical pass because org.json otherwise silently accepts duplicate object keys. */ +private object DuplicateJsonKeyValidator { + fun validate(text: String) { Parser(text).parse() } + private class Parser(private val source: String) { + private var index = 0 + fun parse() { value(); whitespace(); check(index == source.length) { "JSON suffixe invalide" } } + private fun value() { whitespace(); check(index < source.length) { "JSON tronqué" }; when (source[index]) { '{' -> objectValue(); '[' -> arrayValue(); '"' -> string(); 't' -> literal("true"); 'f' -> literal("false"); 'n' -> literal("null"); else -> number() } } + private fun objectValue() { index++; whitespace(); val keys = mutableSetOf(); if (take('}')) return; while (true) { whitespace(); val key = string(); check(keys.add(key)) { "clé JSON dupliquée: $key" }; whitespace(); expect(':'); value(); whitespace(); if (take('}')) return; expect(',') } } + private fun arrayValue() { index++; whitespace(); if (take(']')) return; while (true) { value(); whitespace(); if (take(']')) return; expect(',') } } + private fun string(): String { expect('"'); val out = StringBuilder(); while (index < source.length) { val c = source[index++]; when (c) { '"' -> return out.toString(); '\\' -> { check(index < source.length); val escaped = source[index++]; if (escaped == 'u') { check(index + 4 <= source.length); val hex = source.substring(index, index + 4); check(hex.all { it.isDigit() || it.lowercaseChar() in 'a'..'f' }); out.append(hex.toInt(16).toChar()); index += 4 } else { check(escaped in "\"\\/bfnrt"); out.append(escaped) } }; else -> { check(c.code >= 0x20); out.append(c) } } }; error("chaîne JSON tronquée") } + private fun literal(value: String) { check(source.startsWith(value, index)); index += value.length } + private fun number() { val start = index; if (take('-')) Unit; check(index < source.length && source[index].isDigit()); if (source[index] == '0') index++ else while (index < source.length && source[index].isDigit()) index++; if (take('.')) { check(index < source.length && source[index].isDigit()); while (index < source.length && source[index].isDigit()) index++ }; if (index < source.length && source[index] in "eE") { index++; if (index < source.length && source[index] in "+-") index++; check(index < source.length && source[index].isDigit()); while (index < source.length && source[index].isDigit()) index++ }; check(index > start) } + private fun whitespace() { while (index < source.length && source[index].isWhitespace()) index++ } + private fun take(c: Char): Boolean { if (index < source.length && source[index] == c) { index++; return true }; return false } + private fun expect(c: Char) { check(take(c)) { "JSON: '$c' attendu à $index" } } + } +} diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt index 7780a9e..a9d35c6 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogRepository.kt @@ -184,6 +184,68 @@ sealed interface FinalizeActiveDraftResult { ) : FinalizeActiveDraftResult } +data class ExerciseOccurrenceCursor( + val startedAt: String, + val sessionId: String, + val entryId: String, +) + +data class ExerciseSetContext( + val position: Int, + val reps: Int?, + val durationSeconds: Int?, + /** Null and 0.0 are intentionally distinct actual observations. */ + val weightKg: Double?, +) + +data class ExerciseSetPage( + val sets: List, + val nextPosition: Int?, +) + +data class ExerciseOccurrenceContext( + val sessionId: String, + val entryId: String, + val startedAt: String, + val sessionType: SessionType, + val equipmentId: String?, + val equipmentDisplayName: String?, + val loadSemantics: EquipmentLoadSemantics?, + val recordingMode: RecordingMode, + val trackingMode: TrackingMode, + val dataFields: Int, + val setPreview: ExerciseSetPage?, + val continuousDurationSeconds: Int?, + val speedKmh: Double?, + val distanceKm: Double?, +) + +data class ExerciseOccurrencePage( + val occurrences: List, + val nextCursor: ExerciseOccurrenceCursor?, +) + +data class ExplicitMaxContext( + val sessionId: String, + val entryId: String, + val startedAt: String, + val maxWeightKg: Double, + val equipmentId: String?, + val equipmentDisplayName: String?, + val loadSemantics: EquipmentLoadSemantics?, +) + +data class TrainingExerciseContext( + val exercise: ExerciseProfile, + /** Persisted user classification; never replaced by scientific projection. */ + val persistedDirectZoneIds: List, + val persistedZoneIdsWithAncestors: List, + val knowledge: ExerciseKnowledge?, + val compatibleEquipment: List, + val latestExplicitMax: ExplicitMaxContext?, + val recentPerformance: ExerciseOccurrencePage, +) + class TrainlogRepository( context: Context, databaseName: String = @@ -191,6 +253,7 @@ class TrainlogRepository( ) { private val applicationContext = context.applicationContext private val bodyZones = BodyZoneCatalog.load(applicationContext) + private val trainingKnowledge = TrainingKnowledgeCatalog.load(applicationContext, bodyZones) private val database = TrainlogDatabaseHelper( applicationContext, @@ -201,6 +264,23 @@ class TrainlogRepository( database.close() } + fun getExerciseKnowledge(exerciseId: String): ExerciseKnowledge? = + trainingKnowledge.getExerciseKnowledge(exerciseId) + + fun getConditionalExerciseKnowledge(exerciseId: String): ExerciseKnowledge? = + trainingKnowledge.getConditionalExerciseKnowledge(exerciseId) + + fun getMuscleKnowledge(muscleId: String): MuscleKnowledge? = trainingKnowledge.getMuscle(muscleId) + fun getJointActionKnowledge(actionId: String): JointActionKnowledge? = trainingKnowledge.getJointAction(actionId) + fun getMovementPatternKnowledge(patternId: String): MovementPatternKnowledge? = trainingKnowledge.getMovementPattern(patternId) + fun getScienceReference(refId: String): ScienceReference? = trainingKnowledge.getReference(refId) + fun getEquipmentKnowledge(equipmentId: String): EquipmentKnowledge? = trainingKnowledge.getEquipmentKnowledge(equipmentId) + fun getScientificBodyZoneMapping(exerciseId: String): ScientificBodyZoneMapping? = trainingKnowledge.getScientificBodyZoneMapping(exerciseId) + fun listExercisesByMovementPattern(patternId: String): List = trainingKnowledge.listExercisesByMovementPattern(patternId) + fun listExercisesByMuscle(muscleId: String, role: MuscleRole): List = trainingKnowledge.listExercisesByMuscle(muscleId, role) + fun listCompatibleKnowledgeExercises(equipmentId: String): List = trainingKnowledge.listCompatibleExercises(equipmentId) + fun queryExerciseKnowledge(filters: KnowledgeExerciseFilters): List = trainingKnowledge.queryExercises(filters) + fun listEquipment(): List { val output = mutableListOf() database.readableDatabase.query( @@ -3172,6 +3252,286 @@ class TrainlogRepository( ) } + /** + * Compose immutable scientific metadata with exact persisted runtime state. + * WHY: names are editable labels, so every join and lookup stays on stable + * exercise_id; the transaction gives all components one SQLite read view. + */ + fun getTrainingExerciseContext( + exerciseId: String, + occurrenceLimit: Int = 8, + setPreviewLimit: Int = 8, + ): TrainingExerciseContext? { + require(occurrenceLimit in 1..MAX_OCCURRENCE_PAGE_SIZE) { "occurrenceLimit doit être compris entre 1 et $MAX_OCCURRENCE_PAGE_SIZE" } + require(setPreviewLimit in 1..MAX_SET_PAGE_SIZE) { "setPreviewLimit doit être compris entre 1 et $MAX_SET_PAGE_SIZE" } + val db = database.readableDatabase + val ownsTransaction = !db.inTransaction() + if (ownsTransaction) db.beginTransactionNonExclusive() + return try { + val exercise = readExerciseProfileExact(db, exerciseId) ?: return null + val direct = buildList { + exercise.primaryZoneId?.let(::add) + addAll(exercise.secondaryZoneIds) + } + val expanded = linkedSetOf() + direct.forEach { zoneId -> + expanded += zoneId + expanded += bodyZones.ancestors(zoneId).map { it.zoneId } + } + val knowledge = trainingKnowledge.getExerciseKnowledge(exerciseId) + val compatible = knowledge?.equipmentIds.orEmpty().mapNotNull(trainingKnowledge::getEquipmentKnowledge) + val result = TrainingExerciseContext( + exercise = exercise, + persistedDirectZoneIds = direct, + persistedZoneIdsWithAncestors = bodyZones.zones.map { it.zoneId }.filter { it in expanded }, + knowledge = knowledge, + compatibleEquipment = compatible, + latestExplicitMax = readLatestExplicitMax(db, exerciseId), + recentPerformance = readExerciseOccurrencePage(db, exerciseId, occurrenceLimit, null, setPreviewLimit), + ) + if (ownsTransaction) db.setTransactionSuccessful() + result + } finally { + if (ownsTransaction) db.endTransaction() + } + } + + /** Deterministic keyset page over current data; pages do not hold a cross-call snapshot. */ + fun listExerciseOccurrences( + exerciseId: String, + limit: Int, + after: ExerciseOccurrenceCursor? = null, + setPreviewLimit: Int = 8, + ): ExerciseOccurrencePage { + require(limit in 1..MAX_OCCURRENCE_PAGE_SIZE) { "limit doit être compris entre 1 et $MAX_OCCURRENCE_PAGE_SIZE" } + require(setPreviewLimit in 1..MAX_SET_PAGE_SIZE) { "setPreviewLimit doit être compris entre 1 et $MAX_SET_PAGE_SIZE" } + validateOccurrenceCursor(after) + val db = database.readableDatabase + val ownsTransaction = !db.inTransaction() + if (ownsTransaction) db.beginTransactionNonExclusive() + return try { + require(readExerciseProfileExact(db, exerciseId) != null) { "exercise_id inconnu: $exerciseId" } + val result = readExerciseOccurrencePage(db, exerciseId, limit, after, setPreviewLimit) + if (ownsTransaction) db.setTransactionSuccessful() + result + } finally { + if (ownsTransaction) db.endTransaction() + } + } + + /** Follow-up bounded set page for one exact occurrence; no global set load exists. */ + fun listExerciseOccurrenceSets( + exerciseId: String, + entryId: String, + limit: Int, + afterPosition: Int? = null, + ): ExerciseSetPage { + require(exerciseId.isNotBlank() && entryId.isNotBlank()) { "identités vides" } + require(limit in 1..MAX_SET_PAGE_SIZE) { "limit doit être compris entre 1 et $MAX_SET_PAGE_SIZE" } + require(afterPosition == null || afterPosition >= 0) { "position de curseur invalide" } + val db = database.readableDatabase + val ownsTransaction = !db.inTransaction() + if (ownsTransaction) db.beginTransactionNonExclusive() + return try { + val rowId = db.rawQuery( + "SELECT se.id,se.recording_mode FROM session_exercises se JOIN exercises e ON e.id=se.exercise_row_id WHERE e.exercise_id=? AND se.entry_id=?;", + arrayOf(exerciseId, entryId), + ).use { cursor -> + require(cursor.moveToFirst()) { "occurrence inconnue pour cet exercice" } + require(cursor.getString(1) == "sets") { "une activité continue ne possède pas de séries" } + cursor.getLong(0) + } + val result = readSetPage(db, rowId, limit, afterPosition) + if (ownsTransaction) db.setTransactionSuccessful() + result + } finally { + if (ownsTransaction) db.endTransaction() + } + } + + private fun readExerciseProfileExact(db: SQLiteDatabase, exerciseId: String): ExerciseProfile? = + db.rawQuery( + "SELECT exercise_id,name,normalized_name,recording_mode,tracking_mode,data_fields FROM exercises WHERE exercise_id=?;", + arrayOf(exerciseId), + ).use { cursor -> + if (!cursor.moveToFirst()) null else { + val zones = readExerciseBodyZones(db, exerciseId) + ExerciseProfile( + exerciseId = cursor.getString(0), name = cursor.getString(1), normalizedName = cursor.getString(2), + recordingMode = parseRecordingMode(cursor.getString(3)), trackingMode = parseTrackingMode(cursor.getString(4)), + dataFields = checkedNonNegativeInt(cursor.getLong(5), "data_fields"), primaryZoneId = zones.first, + secondaryZoneIds = zones.second, + ) + } + } + + private data class TemporalCandidate( + val rowId: Long, + val sessionId: String, + val entryId: String, + val startedAt: String, + val timestamp: TrainlogTimestampKey, + ) + + private fun compareTemporal(left: TemporalCandidate, right: TemporalCandidate): Int = + left.timestamp.compareTo(right.timestamp).takeIf { it != 0 } + ?: TrainlogTimestamp.compareIds(left.sessionId, right.sessionId).takeIf { it != 0 } + ?: TrainlogTimestamp.compareIds(left.entryId, right.entryId) + + private fun candidate(cursor: android.database.Cursor): TemporalCandidate { + val startedAt = cursor.requiredText(3, "started_at") + return TemporalCandidate( + rowId = cursor.getLong(0), + sessionId = cursor.requiredText(1, "session_id"), + entryId = cursor.requiredText(2, "entry_id"), + startedAt = startedAt, + timestamp = checkNotNull(TrainlogTimestamp.parse(startedAt)) { + "started_at persistant invalide pour l'occurrence" + }, + ) + } + + private fun retainCandidate(rows: MutableList, value: TemporalCandidate, capacity: Int) { + val position = rows.indexOfFirst { compareTemporal(value, it) > 0 }.let { if (it < 0) rows.size else it } + if (position < capacity) { + rows.add(position, value) + if (rows.size > capacity) rows.removeAt(rows.lastIndex) + } + } + + private fun readLatestExplicitMax(db: SQLiteDatabase, exerciseId: String): ExplicitMaxContext? { + val selected = mutableListOf() + db.rawQuery( + """SELECT se.id,s.session_id,se.entry_id,s.started_at FROM max_results mr + JOIN session_exercises se ON se.id=mr.session_exercise_row_id + JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id + WHERE e.exercise_id=? AND s.session_type='max_test';""", + arrayOf(exerciseId), + ).use { cursor -> while (cursor.moveToNext()) retainCandidate(selected, candidate(cursor), 1) } + val winner = selected.singleOrNull() ?: return null + return db.rawQuery( + """SELECT s.session_id,se.entry_id,s.started_at,mr.max_weight_kg, + eq.equipment_id,eq.display_name,eq.load_semantics + FROM max_results mr JOIN session_exercises se ON se.id=mr.session_exercise_row_id + JOIN sessions s ON s.id=se.session_row_id LEFT JOIN equipment eq ON eq.id=se.equipment_row_id + WHERE se.id=?;""", + arrayOf(winner.rowId.toString()), + ).use { cursor -> + check(cursor.moveToFirst()) { "résultat MAX sélectionné absent" } + ExplicitMaxContext( + sessionId = cursor.requiredText(0, "session_id"), entryId = cursor.requiredText(1, "entry_id"), + startedAt = cursor.requiredText(2, "started_at"), maxWeightKg = cursor.finiteNonNegativeDouble(3, "max_weight_kg", strictlyPositive = true), + equipmentId = cursor.optionalText(4), equipmentDisplayName = cursor.optionalText(5), + loadSemantics = cursor.optionalText(6)?.let(::parseLoadSemantics), + ) + } + } + + private fun readExerciseOccurrencePage( + db: SQLiteDatabase, exerciseId: String, limit: Int, after: ExerciseOccurrenceCursor?, setPreviewLimit: Int, + ): ExerciseOccurrencePage { + val afterCandidate = after?.let { + TemporalCandidate(-1L, it.sessionId, it.entryId, it.startedAt, + requireNotNull(TrainlogTimestamp.parse(it.startedAt))) + } + val selected = mutableListOf() + db.rawQuery( + """ + SELECT se.id,s.session_id,se.entry_id,s.started_at + FROM session_exercises se + JOIN sessions s ON s.id=se.session_row_id + JOIN exercises e ON e.id=se.exercise_row_id + WHERE e.exercise_id=?; + """.trimIndent(), arrayOf(exerciseId), + ).use { cursor -> + while (cursor.moveToNext()) { + val value = candidate(cursor) + // INVARIANT: selection and the exclusive cursor share compareTemporal. + if (afterCandidate == null || compareTemporal(value, afterCandidate) < 0) + retainCandidate(selected, value, limit + 1) + } + } + val hasMore = selected.size > limit + val kept = if (hasMore) selected.take(limit) else selected + val rows = kept.map { selectedRow -> + db.rawQuery( + """SELECT se.id,s.session_id,se.entry_id,s.started_at,s.session_type, + eq.equipment_id,eq.display_name,eq.load_semantics, + se.recording_mode,se.tracking_mode,se.data_fields, + ca.duration_seconds,ca.speed_kmh,ca.distance_km + FROM session_exercises se JOIN sessions s ON s.id=se.session_row_id + LEFT JOIN equipment eq ON eq.id=se.equipment_row_id + LEFT JOIN continuous_activity ca ON ca.session_exercise_row_id=se.id + WHERE se.id=?;""", + arrayOf(selectedRow.rowId.toString()), + ).use { cursor -> + check(cursor.moveToFirst()) { "occurrence sélectionnée absente" } + val recording = parseRecordingMode(cursor.requiredText(8, "recording_mode")) + val rowId = cursor.getLong(0) + val context = ExerciseOccurrenceContext( + sessionId = cursor.requiredText(1, "session_id"), entryId = cursor.requiredText(2, "entry_id"), + startedAt = cursor.requiredText(3, "started_at"), sessionType = SessionType.fromWire(cursor.requiredText(4, "session_type")), + equipmentId = cursor.optionalText(5), equipmentDisplayName = cursor.optionalText(6), + loadSemantics = cursor.optionalText(7)?.let(::parseLoadSemantics), recordingMode = recording, + trackingMode = parseTrackingMode(cursor.requiredText(9, "tracking_mode")), dataFields = checkedNonNegativeInt(cursor.getLong(10), "data_fields"), + setPreview = if (recording == RecordingMode.SETS) readSetPage(db, rowId, setPreviewLimit, null) else null, + continuousDurationSeconds = if (recording == RecordingMode.CONTINUOUS) cursor.requiredPositiveInt(11, "duration_seconds") else null, + speedKmh = cursor.optionalFinitePositiveDouble(12, "speed_kmh"), distanceKm = cursor.optionalFinitePositiveDouble(13, "distance_km"), + ) + if (recording == RecordingMode.SETS) check(cursor.isNull(11) && cursor.isNull(12) && cursor.isNull(13)) { "activité continue attachée à une occurrence SETS" } + context + } + } + val last = rows.lastOrNull() + return ExerciseOccurrencePage( + occurrences = rows, + nextCursor = if (hasMore && last != null) ExerciseOccurrenceCursor(last.startedAt, last.sessionId, last.entryId) else null, + ) + } + + private fun readSetPage(db: SQLiteDatabase, occurrenceRowId: Long, limit: Int, afterPosition: Int?): ExerciseSetPage { + val selection = if (afterPosition == null) "session_exercise_row_id=?" else "session_exercise_row_id=? AND position>?" + val args = if (afterPosition == null) arrayOf(occurrenceRowId.toString(), (limit + 1).toString()) else arrayOf(occurrenceRowId.toString(), afterPosition.toString(), (limit + 1).toString()) + val rows = mutableListOf() + db.rawQuery("SELECT position,reps,duration_seconds,weight_kg FROM performed_sets WHERE $selection ORDER BY position ASC LIMIT ?;", args).use { cursor -> + while (cursor.moveToNext()) { + val reps = if (cursor.isNull(1)) null else checkedNonNegativeInt(cursor.getLong(1), "reps") + val duration = if (cursor.isNull(2)) null else cursor.requiredPositiveInt(2, "duration_seconds") + check((reps == null) != (duration == null)) { "forme de série corrompue" } + rows += ExerciseSetContext(checkedNonNegativeInt(cursor.getLong(0), "position"), reps, duration, if (cursor.isNull(3)) null else cursor.finiteNonNegativeDouble(3, "weight_kg")) + } + } + val hasMore = rows.size > limit + val kept = if (hasMore) rows.take(limit) else rows + return ExerciseSetPage(kept, if (hasMore) kept.last().position else null) + } + + private fun validateOccurrenceCursor(cursor: ExerciseOccurrenceCursor?) { + if (cursor == null) return + require(cursor.startedAt.isNotBlank() && cursor.sessionId.isNotBlank() && cursor.entryId.isNotBlank()) { "curseur d'occurrence invalide" } + require(TrainlogTimestamp.parse(cursor.startedAt) != null) { "startedAt du curseur invalide" } + } + + private fun parseRecordingMode(value: String): RecordingMode = when (value) { + "sets" -> RecordingMode.SETS; "continuous" -> RecordingMode.CONTINUOUS; else -> error("recording_mode corrompu: $value") + } + private fun parseTrackingMode(value: String): TrackingMode = when (value) { + "reps" -> TrackingMode.REPS; "duration" -> TrackingMode.DURATION; else -> error("tracking_mode corrompu: $value") + } + private fun parseLoadSemantics(value: String): EquipmentLoadSemantics = runCatching { EquipmentLoadSemantics.valueOf(value.uppercase(Locale.ROOT)) }.getOrElse { error("load_semantics corrompu: $value") } + + private fun android.database.Cursor.requiredText(index: Int, name: String): String = getString(index)?.takeIf { it.isNotBlank() } ?: error("$name absent") + private fun android.database.Cursor.optionalText(index: Int): String? = if (isNull(index)) null else getString(index) + private fun android.database.Cursor.requiredPositiveInt(index: Int, name: String): Int = checkedNonNegativeInt(getLong(index), name).also { check(it > 0) { "$name doit être positif" } } + private fun android.database.Cursor.finiteNonNegativeDouble(index: Int, name: String, strictlyPositive: Boolean = false): Double = getDouble(index).also { check(it.isFinite() && if (strictlyPositive) it > 0.0 else it >= 0.0) { "$name invalide" } } + private fun android.database.Cursor.optionalFinitePositiveDouble(index: Int, name: String): Double? = if (isNull(index)) null else finiteNonNegativeDouble(index, name, true) + private fun checkedNonNegativeInt(value: Long, name: String): Int { check(value in 0..Int.MAX_VALUE.toLong()) { "$name hors plage" }; return value.toInt() } + + private companion object { + const val MAX_OCCURRENCE_PAGE_SIZE = 32 + const val MAX_SET_PAGE_SIZE = 64 + } + /** * Return one newest explicit measured max per movement. Equipment is * presentation context only and never participates in max identity. diff --git a/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogTimestamp.kt b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogTimestamp.kt new file mode 100644 index 0000000..7de40c6 --- /dev/null +++ b/android/app/src/main/java/com/labfytools/trainlog/data/TrainlogTimestamp.kt @@ -0,0 +1,106 @@ +package com.labfytools.trainlog.data + +internal data class TrainlogTimestampKey( + val utcSecond: Long, + val fraction: String, +) : Comparable { + override fun compareTo(other: TrainlogTimestampKey): Int { + utcSecond.compareTo(other.utcSecond).takeIf { it != 0 }?.let { return it } + val count = maxOf(fraction.length, other.fraction.length) + for (index in 0 until count) { + val left = fraction.getOrElse(index) { '0' } + val right = other.fraction.getOrElse(index) { '0' } + if (left != right) return left.compareTo(right) + } + return 0 + } +} + +internal object TrainlogTimestamp { + /** + * CONTRACT: parse the frozen ASCII timestamp grammar without inheriting a + * platform ISO parser's syntax, offset bounds, or fractional precision. + */ + fun parse(value: String): TrainlogTimestampKey? { + if (value.length < 17 || value.any { it.code > 0x7f }) return null + val year = digits(value, 0, 4) ?: return null + val month = digits(value, 5, 2) ?: return null + val day = digits(value, 8, 2) ?: return null + val hour = digits(value, 11, 2) ?: return null + val minute = digits(value, 14, 2) ?: return null + if (value.getOrNull(4) != '-' || value.getOrNull(7) != '-' || + value.getOrNull(10) !in listOf('T', 't') || value.getOrNull(13) != ':' || + year !in 1..9999 || month !in 1..12 || day !in 1..daysInMonth(year, month) || + hour !in 0..23 || minute !in 0..59 + ) return null + + var at = 16 + var second = 0 + var fraction = "" + if (value.getOrNull(at) == ':') { + second = digits(value, at + 1, 2) ?: return null + if (second !in 0..59) return null + at += 3 + if (value.getOrNull(at) == '.') { + val start = ++at + while (value.getOrNull(at)?.isAsciiDigit() == true) at++ + if (at == start) return null + fraction = value.substring(start, at) + } + } + + var offsetSeconds = 0L + if (at + 1 == value.length && value[at] in listOf('Z', 'z')) { + // UTC designator. + } else { + if (at + 6 != value.length || value.getOrNull(at) !in listOf('+', '-') || + value.getOrNull(at + 3) != ':' + ) return null + val offsetHour = digits(value, at + 1, 2) ?: return null + val offsetMinute = digits(value, at + 4, 2) ?: return null + if (offsetHour !in 0..23 || offsetMinute !in 0..59) return null + offsetSeconds = (offsetHour * 3600L + offsetMinute * 60L) * + if (value[at] == '+') 1L else -1L + } + val localSecond = dayNumber(year, month, day) * 86400L + + hour * 3600L + minute * 60L + second + return TrainlogTimestampKey(localSecond - offsetSeconds, fraction) + } + + /** Bytewise identity order shared with C's unsigned memcmp semantics. */ + fun compareIds(left: String, right: String): Int { + val leftBytes = left.toByteArray(Charsets.UTF_8) + val rightBytes = right.toByteArray(Charsets.UTF_8) + for (index in 0 until minOf(leftBytes.size, rightBytes.size)) { + val result = (leftBytes[index].toInt() and 0xff).compareTo(rightBytes[index].toInt() and 0xff) + if (result != 0) return result + } + return leftBytes.size.compareTo(rightBytes.size) + } + + private fun digits(value: String, start: Int, count: Int): Int? { + if (start < 0 || start + count > value.length) return null + var result = 0 + repeat(count) { offset -> + val character = value[start + offset] + if (!character.isAsciiDigit()) return null + result = result * 10 + (character - '0') + } + return result + } + + private fun Char.isAsciiDigit(): Boolean = this in '0'..'9' + private fun leap(year: Int): Boolean = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) + private fun daysInMonth(year: Int, month: Int): Int = when (month) { + 2 -> if (leap(year)) 29 else 28 + 4, 6, 9, 11 -> 30 + else -> 31 + } + + private fun dayNumber(year: Int, month: Int, day: Int): Long { + val prior = year - 1 + var days = prior * 365L + prior / 4 - prior / 100 + prior / 400 + for (current in 1 until month) days += daysInMonth(year, current) + return days + day - 1 + } +} diff --git a/android/app/src/main/java/com/labfytools/trainlog/ui/ExerciseScreen.kt b/android/app/src/main/java/com/labfytools/trainlog/ui/ExerciseScreen.kt index 7c5cc16..30c7550 100644 --- a/android/app/src/main/java/com/labfytools/trainlog/ui/ExerciseScreen.kt +++ b/android/app/src/main/java/com/labfytools/trainlog/ui/ExerciseScreen.kt @@ -19,6 +19,9 @@ import com.labfytools.trainlog.data.CreateExerciseResult import com.labfytools.trainlog.data.BodyZone import com.labfytools.trainlog.data.BodyZoneKind import com.labfytools.trainlog.data.EditExerciseResult +import com.labfytools.trainlog.data.ExerciseKnowledge +import com.labfytools.trainlog.data.ExerciseKnowledgeStatus +import com.labfytools.trainlog.data.KnowledgeConfidence import com.labfytools.trainlog.data.TrainlogRepository import com.labfytools.trainlog.model.ExerciseDataFields import com.labfytools.trainlog.model.ExerciseEditInput @@ -73,6 +76,7 @@ fun ExerciseScreen( var searchQuery by remember { mutableStateOf("") } var filterZoneId by remember { mutableStateOf(null) } var unclassifiedFilter by remember { mutableStateOf(false) } + var expandedKnowledgeIds by remember { mutableStateOf(emptySet()) } val zones = repository.listBodyZones() var message by @@ -513,6 +517,22 @@ fun ExerciseScreen( accent = colors.accent, onClick = { startEditing(exercise) }, ) + repository.getExerciseKnowledge(exercise.exerciseId)?.let { knowledge -> + val expanded = exercise.exerciseId in expandedKnowledgeIds + TrainlogAction( + label = if (expanded) "− Connaissances" else "+ Connaissances", + description = knowledgeSummary(knowledge), + accent = colors.muted, + onClick = { + expandedKnowledgeIds = if (expanded) { + expandedKnowledgeIds - exercise.exerciseId + } else { + expandedKnowledgeIds + exercise.exerciseId + } + }, + ) + if (expanded) KnowledgePanel(repository, knowledge) + } } } } @@ -532,6 +552,59 @@ fun ExerciseScreen( } } +@Composable +private fun KnowledgePanel(repository: TrainlogRepository, knowledge: ExerciseKnowledge) { + val colors = LocalTrainlogColors.current + /* WHY: conditional content is opened only under an explicit uncertainty + * label; it cannot be mistaken for an ordinary resolved classification. */ + val interpretation = when (knowledge.resolutionStatus) { + ExerciseKnowledgeStatus.RESOLVED_FAMILY_VARIANT_LIMITED -> knowledge.interpretation + ExerciseKnowledgeStatus.CONDITIONAL -> knowledge.conditionalInterpretation + ExerciseKnowledgeStatus.UNRESOLVED -> null + } + if (interpretation == null) { + TrainlogInfo("Classification scientifique non résolue.", colors.warning) + return + } + if (knowledge.resolutionStatus == ExerciseKnowledgeStatus.CONDITIONAL) { + TrainlogInfo( + "Interprétation conditionnelle · à confirmer : ${interpretation.requiredConfirmation.orEmpty()}", + colors.warning, + ) + } + val patterns = interpretation.patternIds.mapNotNull(repository::getMovementPatternKnowledge) + val primary = interpretation.primaryMuscleIds.mapNotNull(repository::getMuscleKnowledge) + val secondary = interpretation.secondaryMuscleIds.mapNotNull(repository::getMuscleKnowledge) + val primaryZone = repository.bodyZone(interpretation.primaryZoneId)?.displayName + ?: interpretation.primaryZoneId + val secondaryZones = interpretation.secondaryZoneIds.map { repository.bodyZone(it)?.displayName ?: it } + val runtimeEquipment = repository.listEquipment().associateBy { it.equipmentId } + val equipment = knowledge.equipmentIds.map { runtimeEquipment[it]?.displayName ?: it } + TrainlogInfo("Mouvement : ${patterns.joinToString { it.displayNameFr }.ifEmpty { "Non classé" }}") + TrainlogInfo("Muscles principaux : ${primary.joinToString { it.displayNameFr }.ifEmpty { "Non classés" }}") + TrainlogInfo("Muscles secondaires : ${secondary.joinToString { it.displayNameFr }.ifEmpty { "Aucun établi" }}") + TrainlogInfo( + "Zones scientifiques : $primaryZone" + + if (secondaryZones.isEmpty()) "" else " · secondaires : ${secondaryZones.joinToString()}", + ) + TrainlogInfo("Équipement compatible : ${equipment.joinToString().ifEmpty { "Non établi" }}") + TrainlogInfo("Confiance : ${confidenceLabel(interpretation.confidence)}", colors.muted) +} + +private fun knowledgeSummary(knowledge: ExerciseKnowledge): String = when (knowledge.resolutionStatus) { + ExerciseKnowledgeStatus.RESOLVED_FAMILY_VARIANT_LIMITED -> + "Classification scientifique · confiance ${confidenceLabel(knowledge.confidence)}" + ExerciseKnowledgeStatus.CONDITIONAL -> + "Classification conditionnelle · incertitude explicite" + ExerciseKnowledgeStatus.UNRESOLVED -> "Classification scientifique non résolue" +} + +private fun confidenceLabel(confidence: KnowledgeConfidence): String = when (confidence) { + KnowledgeConfidence.HIGH -> "élevée" + KnowledgeConfidence.MODERATE -> "modérée" + KnowledgeConfidence.UNCERTAIN -> "incertaine" +} + @Composable private fun BodyZoneChoices( zones: List, diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/TrainingExerciseContextTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/TrainingExerciseContextTest.kt new file mode 100644 index 0000000..a011843 --- /dev/null +++ b/android/app/src/test/java/com/labfytools/trainlog/data/TrainingExerciseContextTest.kt @@ -0,0 +1,206 @@ +package com.labfytools.trainlog.data + +import android.content.Context +import android.database.sqlite.SQLiteDatabase +import androidx.test.core.app.ApplicationProvider +import com.labfytools.trainlog.model.ExerciseEditInput +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.util.UUID + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class TrainingExerciseContextTest { + private val context: Context = ApplicationProvider.getApplicationContext() + private val databaseName = "knowledge-context-${UUID.randomUUID()}.db" + private val repository = TrainlogRepository(context, databaseName) + + @After fun close() { repository.close(); context.deleteDatabase(databaseName) } + + @Test + fun composesExactIdentityZonesEquipmentMaxAndBoundedOccurrencePages() { + seedFixture() + val exerciseId = LEG_PRESS_ID + val first = repository.getTrainingExerciseContext(exerciseId, occurrenceLimit = 2, setPreviewLimit = 2)!! + assertEquals("Nom modifiable", first.exercise.name) + assertEquals(listOf("thighs", "glutes"), first.persistedDirectZoneIds) + assertTrue("lower_body" in first.persistedZoneIdsWithAncestors) + assertEquals("thighs", repository.getScientificBodyZoneMapping(exerciseId)?.primaryZoneId) + assertTrue(first.compatibleEquipment.any { it.equipmentId == "leg_press" }) + + val maximum = first.latestExplicitMax!! + assertEquals(140.0, maximum.maxWeightKg, 0.0) + assertEquals(EquipmentLoadSemantics.ASSISTANCE, maximum.loadSemantics) + assertFalse(first.recentPerformance.occurrences.any { occurrence -> + occurrence.setPreview?.sets.orEmpty().any { it.weightKg == 999.0 } && occurrence.sessionType == com.labfytools.trainlog.model.SessionType.MAX_TEST + }) + assertEquals(2, first.recentPerformance.occurrences.size) + val cursor = first.recentPerformance.nextCursor!! + val second = repository.listExerciseOccurrences(exerciseId, 2, cursor, 2) + assertEquals(2, second.occurrences.size) + assertTrue(first.recentPerformance.occurrences.map { it.entryId }.toSet().intersect(second.occurrences.map { it.entryId }.toSet()).isEmpty()) + + val assistanceOccurrence = (first.recentPerformance.occurrences + second.occurrences).first { it.entryId == "entry_a" } + assertEquals(EquipmentLoadSemantics.ASSISTANCE, assistanceOccurrence.loadSemantics) + assertEquals(listOf(null, 0.0), assistanceOccurrence.setPreview!!.sets.map { it.weightKg }) + val setPage2 = repository.listExerciseOccurrenceSets(exerciseId, "entry_a", 2, assistanceOccurrence.setPreview.nextPosition) + assertEquals(listOf(22.5), setPage2.sets.map { it.weightKg }) + assertNull(setPage2.nextPosition) + + assertTrue(repository.getExerciseKnowledge(exerciseId) != null) + assertNull(repository.getTrainingExerciseContext("ex_00000000-0000-4000-8000-000000000000")) + } + + @Test + fun continuousOccurrenceHasNoArtificialSetsAndInvalidPagingFails() { + seedFixture() + val continuous = repository.getTrainingExerciseContext(CONTINUOUS_ID)!!.recentPerformance.occurrences.single() + assertNull(continuous.setPreview) + assertEquals(1800, continuous.continuousDurationSeconds) + assertEquals(8.0, continuous.speedKmh!!, 0.0) + assertTrue(runCatching { repository.listExerciseOccurrences(LEG_PRESS_ID, 0) }.isFailure) + assertTrue(runCatching { repository.listExerciseOccurrences(LEG_PRESS_ID, 2, ExerciseOccurrenceCursor("bad", "s", "e")) }.isFailure) + assertTrue(runCatching { repository.listExerciseOccurrenceSets(CONTINUOUS_ID, "entry_cont", 2) }.isFailure) + } + + @Test + fun occurrencesAndExplicitMaxOrderByInstantAcrossOffsetsWithStablePagingTies() { + seedFixture() + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + listOf( + arrayOf("offset_local_older", "2026-02-01T10:00:00+02:00", "training", "entry_offset_old"), + arrayOf("offset_utc_newer", "2026-02-01T09:00:00Z", "training", "entry_offset_new"), + arrayOf("tie_a", "2026-02-02T10:00:00+02:00", "training", "entry_tie_a"), + arrayOf("tie_z", "2026-02-02T08:00:00Z", "training", "entry_tie_z"), + arrayOf("max_local_older", "2025-03-01T10:00:00+02:00", "max_test", "entry_max_old"), + arrayOf("max_utc_newer", "2025-03-01T09:00:00Z", "max_test", "entry_max_new"), + ).forEach { row -> + db.execSQL("INSERT INTO sessions(session_id,started_at,session_type) VALUES(?,?,?);", row.copyOfRange(0, 3)) + db.execSQL("INSERT INTO session_exercises(session_row_id,exercise_row_id,position,recording_mode,tracking_mode,data_fields,entry_id) SELECT s.id,e.id,0,'sets','reps',0,? FROM sessions s JOIN exercises e ON e.exercise_id=? WHERE s.session_id=?;", arrayOf(row[3], LEG_PRESS_ID, row[0])) + } + db.execSQL("INSERT INTO max_results(session_exercise_row_id,max_weight_kg) SELECT id,150.0 FROM session_exercises WHERE entry_id='entry_max_old';") + db.execSQL("INSERT INTO max_results(session_exercise_row_id,max_weight_kg) SELECT id,160.0 FROM session_exercises WHERE entry_id='entry_max_new';") + } + + val tieFirst = repository.listExerciseOccurrences(LEG_PRESS_ID, 1) + assertEquals("tie_z", tieFirst.occurrences.single().sessionId) + val tieSecond = repository.listExerciseOccurrences(LEG_PRESS_ID, 1, tieFirst.nextCursor) + assertEquals("tie_a", tieSecond.occurrences.single().sessionId) + val following = repository.listExerciseOccurrences(LEG_PRESS_ID, 2, tieSecond.nextCursor) + assertEquals(listOf("offset_utc_newer", "offset_local_older"), following.occurrences.map { it.sessionId }) + + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + db.execSQL("UPDATE sessions SET started_at='2026-03-01T10:00:00+02:00' WHERE session_id='max_local_older';") + db.execSQL("UPDATE sessions SET started_at='2026-03-01T09:00:00Z' WHERE session_id='max_utc_newer';") + } + val maximum = repository.getTrainingExerciseContext(LEG_PRESS_ID)!!.latestExplicitMax!! + assertEquals("max_utc_newer", maximum.sessionId) + assertEquals(160.0, maximum.maxWeightKg, 0.0) + } + + @Test + fun exceptionalTimestampSpellingsRoundTripWithExactFractionsAndCorruptionFails() { + seedFixture() + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + listOf( + arrayOf("omit", "2030-01-05T10:00+15:00", "entry_omit"), + arrayOf("frac_low", "2030-01-04T10:00:00.12345678901234567890Z", "entry_frac_low"), + arrayOf("frac_high", "2030-01-04T10:00:00.12345678901234567891Z", "entry_frac_high"), + arrayOf("tie_a", "2030-01-04T12:00:00+02:00", "entry_tie_a2"), + arrayOf("tie_z", "2030-01-04T10:00:00-00:00", "entry_tie_z2"), + arrayOf("lower", "2030-01-03t10:00:00z", "entry_lower"), + arrayOf("high_offset", "2030-01-04T09:00:00+23:59", "entry_high_offset"), + ).forEach { row -> + db.execSQL("INSERT INTO sessions(session_id,started_at,session_type) VALUES(?,?,'training');", arrayOf(row[0], row[1])) + db.execSQL("INSERT INTO session_exercises(session_row_id,exercise_row_id,position,recording_mode,tracking_mode,data_fields,entry_id) SELECT s.id,e.id,0,'sets','reps',0,? FROM sessions s JOIN exercises e ON e.exercise_id=? WHERE s.session_id=?;", arrayOf(row[2], LEG_PRESS_ID, row[0])) + } + listOf( + arrayOf("max_offset", "2031-01-02T10:00:00+15:00", "entry_max_offset", 151.0), + arrayOf("max_true", "2031-01-01T20:00:00Z", "entry_max_true", 152.0), + ).forEach { row -> + db.execSQL("INSERT INTO sessions(session_id,started_at,session_type) VALUES(?,?,'max_test');", arrayOf(row[0], row[1])) + db.execSQL("INSERT INTO session_exercises(session_row_id,exercise_row_id,position,recording_mode,tracking_mode,data_fields,entry_id) SELECT s.id,e.id,0,'sets','reps',0,? FROM sessions s JOIN exercises e ON e.exercise_id=? WHERE s.session_id=?;", arrayOf(row[2], LEG_PRESS_ID, row[0])) + db.execSQL("INSERT INTO max_results(session_exercise_row_id,max_weight_kg) SELECT id,? FROM session_exercises WHERE entry_id=?;", arrayOf(row[3], row[2])) + } + } + + val expected = listOf("max_true", "max_offset", "omit", "frac_high", "frac_low", "tie_z", "tie_a", "lower", "high_offset") + var cursor: ExerciseOccurrenceCursor? = null + val seen = mutableSetOf() + expected.forEach { sessionId -> + val page = repository.listExerciseOccurrences(LEG_PRESS_ID, 1, cursor) + assertEquals(sessionId, page.occurrences.single().sessionId) + assertTrue(seen.add(page.occurrences.single().entryId)) + assertEquals(page.occurrences.single().startedAt, page.nextCursor!!.startedAt) + cursor = page.nextCursor + } + while (cursor != null) { + val page = repository.listExerciseOccurrences(LEG_PRESS_ID, 1, cursor) + page.occurrences.forEach { assertTrue(seen.add(it.entryId)) } + cursor = page.nextCursor + } + val maximum = repository.getTrainingExerciseContext(LEG_PRESS_ID)!!.latestExplicitMax!! + assertEquals("max_true", maximum.sessionId) + assertEquals(152.0, maximum.maxWeightKg, 0.0) + + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + db.execSQL("UPDATE sessions SET started_at='2030-01-03 10:00:00Z' WHERE session_id='lower';") + } + assertTrue(runCatching { repository.listExerciseOccurrences(LEG_PRESS_ID, 1) }.isFailure) + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + db.execSQL("UPDATE sessions SET started_at='2030-01-03T10:00:00Z' WHERE session_id='lower';") + db.execSQL("UPDATE sessions SET started_at='bad-max' WHERE session_id='max_offset';") + } + assertTrue(runCatching { repository.getTrainingExerciseContext(LEG_PRESS_ID) }.isFailure) + } + + private fun seedFixture() { + repository.listExercises() + SQLiteDatabase.openDatabase(context.getDatabasePath(databaseName).path, null, SQLiteDatabase.OPEN_READWRITE).use { db -> + db.execSQL("PRAGMA foreign_keys=ON;") + db.execSQL("INSERT INTO exercises(exercise_id,name,normalized_name,recording_mode,tracking_mode,data_fields) VALUES(?,?,?,?,?,?);", arrayOf(LEG_PRESS_ID, "Nom original", "nom original", "sets", "reps", 0)) + db.execSQL("INSERT INTO exercises(exercise_id,name,normalized_name,recording_mode,tracking_mode,data_fields) VALUES(?,?,?,?,?,?);", arrayOf(CONTINUOUS_ID, "Marche test", "marche test", "continuous", "duration", 3)) + db.execSQL("INSERT INTO exercise_body_zones(exercise_row_id,zone_id,role) SELECT id,'thighs','primary' FROM exercises WHERE exercise_id=?;", arrayOf(LEG_PRESS_ID)) + db.execSQL("INSERT INTO exercise_body_zones(exercise_row_id,zone_id,role) SELECT id,'glutes','secondary' FROM exercises WHERE exercise_id=?;", arrayOf(LEG_PRESS_ID)) + listOf( + arrayOf("session_latest", "2026-01-04T10:00:00+00:00", "training"), + arrayOf("session_tied", "2026-01-03T10:00:00+00:00", "training"), + arrayOf("session_max", "2026-01-02T10:00:00+00:00", "max_test"), + arrayOf("session_old", "2026-01-01T10:00:00+00:00", "training"), + arrayOf("session_cont", "2026-01-05T10:00:00+00:00", "training"), + ).forEach { db.execSQL("INSERT INTO sessions(session_id,started_at,session_type) VALUES(?,?,?);", it) } + fun occurrence(session: String, entry: String, position: Int, equipment: String? = null): Long { + db.execSQL( + "INSERT INTO session_exercises(session_row_id,exercise_row_id,position,recording_mode,tracking_mode,data_fields,equipment_row_id,entry_id) SELECT s.id,e.id,?,'sets','reps',0,eq.id,? FROM sessions s JOIN exercises e ON e.exercise_id=? LEFT JOIN equipment eq ON eq.equipment_id=? WHERE s.session_id=?;", + arrayOf(position, entry, LEG_PRESS_ID, equipment, session), + ) + return db.rawQuery("SELECT id FROM session_exercises WHERE entry_id=?;", arrayOf(entry)).use { it.moveToFirst(); it.getLong(0) } + } + val latest = occurrence("session_latest", "entry_latest", 0, "leg_press") + db.execSQL("INSERT INTO performed_sets(session_exercise_row_id,position,reps,weight_kg) VALUES(?,0,3,999.0);", arrayOf(latest)) + val a = occurrence("session_tied", "entry_a", 0, "assisted_dip_chin_machine") + db.execSQL("INSERT INTO performed_sets(session_exercise_row_id,position,reps,weight_kg) VALUES(?,0,8,NULL),(?,1,7,0.0),(?,2,6,22.5);", arrayOf(a, a, a)) + occurrence("session_tied", "entry_b", 1, "plate_loaded_leg_press") + val max = occurrence("session_max", "entry_max", 0, "assisted_dip_chin_machine") + db.execSQL("INSERT INTO max_results(session_exercise_row_id,max_weight_kg) VALUES(?,140.0);", arrayOf(max)) + occurrence("session_old", "entry_old", 0) + db.execSQL("INSERT INTO session_exercises(session_row_id,exercise_row_id,position,recording_mode,tracking_mode,data_fields,equipment_row_id,entry_id) SELECT s.id,e.id,0,'continuous','duration',3,eq.id,'entry_cont' FROM sessions s JOIN exercises e ON e.exercise_id=? LEFT JOIN equipment eq ON eq.equipment_id='treadmill' WHERE s.session_id='session_cont';", arrayOf(CONTINUOUS_ID)) + db.execSQL("INSERT INTO continuous_activity(session_exercise_row_id,duration_seconds,speed_kmh,distance_km) SELECT id,1800,8.0,4.0 FROM session_exercises WHERE entry_id='entry_cont';") + } + val profile = repository.listExercises().first { it.exerciseId == LEG_PRESS_ID } + val result = repository.editExercise(ExerciseEditInput(profile.exerciseId, "Nom modifiable", profile.recordingMode, profile.trackingMode, profile.dataFields, profile.primaryZoneId, profile.secondaryZoneIds)) + assertTrue(result is EditExerciseResult.Saved) + } + + companion object { + private const val LEG_PRESS_ID = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde" + private const val CONTINUOUS_ID = "ex_00000000-0000-4000-8000-000000000001" + } +} diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalogTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalogTest.kt new file mode 100644 index 0000000..e004f6a --- /dev/null +++ b/android/app/src/test/java/com/labfytools/trainlog/data/TrainingKnowledgeCatalogTest.kt @@ -0,0 +1,182 @@ +package com.labfytools.trainlog.data + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) +class TrainingKnowledgeCatalogTest { + private val context: Context = ApplicationProvider.getApplicationContext() + private val names = listOf( + "science-references-v1.json", "muscles-v1.json", "joint-actions-v1.json", + "movement-patterns-v1.json", "exercise-knowledge-v1.json", "equipment-knowledge-v1.json", + ) + + @Test + fun loadsAllStructuredCatalogsAndRepresentativeMappings() { + val catalog = TrainingKnowledgeCatalog.load(context) + assertEquals(listOf(23, 53, 35, 26, 23, 41), listOf(catalog.references.size, catalog.muscles.size, catalog.jointActions.size, catalog.movementPatterns.size, catalog.exercises.size, catalog.equipment.size)) + val legPress = catalog.getExerciseKnowledge("ex_b432623f-bfe9-4daf-a653-60ec7fdffbde") + assertEquals("knee_dominant", legPress?.interpretation?.patternIds?.single()) + assertTrue(catalog.listExercisesByMovementPattern("knee_dominant").any { it.exerciseId == legPress?.exerciseId }) + assertTrue(catalog.listExercisesByMuscle("quadriceps", MuscleRole.PRIMARY).any { it.exerciseId == legPress?.exerciseId }) + assertEquals("thighs", catalog.getScientificBodyZoneMapping(legPress!!.exerciseId)?.primaryZoneId) + assertEquals(BodyZoneAuditStatus.CONFIRMED, legPress.bodyZoneAudit.status) + assertEquals("thighs", legPress.bodyZoneAudit.existingPrimaryZoneId) + assertTrue(legPress.bodyZoneAudit.sourceRefs.isNotEmpty()) + + val multifunction = catalog.getEquipmentKnowledge("rear_delt_pec_fly") + assertEquals(2, multifunction?.capabilities?.size) + assertTrue(catalog.listCompatibleExercises("rear_delt_pec_fly").any { it.exerciseId == "ex_4cd2433e-80b1-478a-b8df-73fc6ef80962" }) + assertNotNull(catalog.getMuscle("deltoid_posterior")) + assertNotNull(catalog.getJointAction("shoulder_horizontal_abduction")) + assertNotNull(catalog.getReference(legPress.sourceRefs.first())) + } + + @Test + fun conditionalAndUnknownRecordsCannotLeakIntoOrdinaryQueries() { + val catalog = TrainingKnowledgeCatalog.load(context) + val chestPress = "ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af" + assertEquals(ExerciseKnowledgeStatus.CONDITIONAL, catalog.getConditionalExerciseKnowledge(chestPress)?.resolutionStatus) + assertNull(catalog.getScientificBodyZoneMapping(chestPress)) + assertFalse(catalog.queryExercises(KnowledgeExerciseFilters()).any { it.exerciseId == chestPress }) + assertNull(catalog.getExerciseKnowledge("ex_00000000-0000-4000-8000-000000000000")) + } + + @Test + fun acceptsAdditiveNonRuntimeKnowledgeRecord() { + val files = assetFiles() + val root = JSONObject(files.getValue("science-references-v1.json")) + root.getJSONArray("references").put( + JSONObject() + .put("ref_id", "zz_additive_reference") + .put("title", "Additive reference fixture") + .put("authors_or_organization", "Trainlog test") + .put("year", 2026) + .put("type", "test_fixture") + .put("url", "https://example.invalid/additive-reference") + .put("topics", org.json.JSONArray().put("validation")) + .put("notes", "Valid additive metadata record.") + .put("limitations", "Test fixture only.") + .put("doi", JSONObject.NULL) + .put("pmid", JSONObject.NULL) + .put("accessed_on", "2026-09-09"), + ) + files["science-references-v1.json"] = root.toString() + + val catalog = TrainingKnowledgeCatalog.load(files::getValue, BodyZoneCatalog.load(context)) + assertNotNull(catalog.getReference("zz_additive_reference")) + } + + @Test + fun rejectsVersionEnumsDanglingReferencesAndDuplicateKeys() { + assertInvalid { files -> files["muscles-v1.json"] = files.getValue("muscles-v1.json").replaceFirst("\"version\": 1", "\"version\": 2") } + assertInvalid { files -> files["muscles-v1.json"] = files.getValue("muscles-v1.json").replaceFirst("\"entity_type\": \"muscle\"", "\"entity_type\": \"organ\"") } + assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")); root.getJSONArray("exercises").getJSONObject(0).getJSONArray("source_refs").put("missing_ref") + files["exercise-knowledge-v1.json"] = root.toString() + } + assertInvalid { files -> files["science-references-v1.json"] = files.getValue("science-references-v1.json").replaceFirst("{", "{\"version\":1,") } + assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")) + val legPress = root.getJSONArray("exercises").let { rows -> (0 until rows.length()).map(rows::getJSONObject).first { it.getString("exercise_id") == "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde" } } + val equipment = legPress.getJSONArray("equipment_ids"); val first = equipment.getString(0) + equipment.put(0, equipment.getString(1)); equipment.put(1, first) + files["exercise-knowledge-v1.json"] = root.toString() + } + assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")); val interpretation = root.getJSONArray("exercises").getJSONObject(0).getJSONObject("interpretation") + interpretation.getJSONArray("secondary_muscle_ids").put(interpretation.getJSONArray("primary_muscle_ids").getString(0)) + files["exercise-knowledge-v1.json"] = root.toString() + } + assertInvalid { files -> + val root = JSONObject(files.getValue("equipment-knowledge-v1.json")); val equipment = root.getJSONArray("equipment").getJSONObject(0) + equipment.put("confidence", "high"); equipment.put("evidence_type", "manufacturer_statement") + files["equipment-knowledge-v1.json"] = root.toString() + } + assertInvalidAudit { it.getJSONArray("source_refs").put("missing_ref") } + assertInvalidAudit { it.put("existing_primary_zone_id", "missing_zone") } + assertInvalidAudit { it.put("status", "compatible") } + } + + @Test + fun rejectsInvalidRuntimeIdentitySyntax() { + assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")) + val row = root.getJSONArray("exercises").getJSONObject(0) + row.put("exercise_id", row.getString("exercise_id").dropLast(1) + "g") + files["exercise-knowledge-v1.json"] = root.toString() + } + assertInvalid { files -> + val root = JSONObject(files.getValue("equipment-knowledge-v1.json")) + val rows = root.getJSONArray("equipment") + val row = rows.getJSONObject(0) + val oldId = row.getString("equipment_id") + val invalidId = "$oldId-" + row.put("equipment_id", invalidId) + val exercises = JSONObject(files.getValue("exercise-knowledge-v1.json")) + val exerciseRows = exercises.getJSONArray("exercises") + for (index in 0 until exerciseRows.length()) { + val ids = exerciseRows.getJSONObject(index).getJSONArray("equipment_ids") + for (idIndex in 0 until ids.length()) if (ids.getString(idIndex) == oldId) ids.put(idIndex, invalidId) + } + files["equipment-knowledge-v1.json"] = root.toString() + files["exercise-knowledge-v1.json"] = exercises.toString() + } + } + + @Test + fun rejectsReverseCompatibilityHighEvidenceAndResolvedAuditGaps() { + assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")) + val rows = root.getJSONArray("exercises") + val row = (0 until rows.length()).map(rows::getJSONObject).first { it.getJSONArray("equipment_ids").length() > 0 } + row.put("equipment_ids", org.json.JSONArray()) + files["exercise-knowledge-v1.json"] = root.toString() + } + assertInvalid { files -> + val references = JSONObject(files.getValue("science-references-v1.json")).getJSONArray("references") + val nonScientificRef = (0 until references.length()).map(references::getJSONObject) + .first { it.getString("type") !in setOf("established_anatomy", "emg_evidence", "intervention_evidence") } + .getString("ref_id") + val root = JSONObject(files.getValue("equipment-knowledge-v1.json")) + val rows = root.getJSONArray("equipment") + val row = rows.getJSONObject(0) + row.put("confidence", "high") + row.put("source_refs", org.json.JSONArray().put(nonScientificRef)) + row.put("evidence_type", "mixed_evidence") + files["equipment-knowledge-v1.json"] = root.toString() + } + assertInvalidAudit { audit -> + audit.put("source_refs", org.json.JSONArray()) + audit.put("status", "confirmed") + } + } + + private fun assertInvalid(mutate: (MutableMap) -> Unit) { + val files = assetFiles() + mutate(files) + val failed = runCatching { TrainingKnowledgeCatalog.load(files::getValue, BodyZoneCatalog.load(context)) }.isFailure + assertTrue("catalogue corrompu accepté", failed) + } + + private fun assetFiles(): MutableMap = names.associateWith { name -> + context.assets.open(name).bufferedReader().use { it.readText() } + }.toMutableMap() + + private fun assertInvalidAudit(mutate: (JSONObject) -> Unit) = assertInvalid { files -> + val root = JSONObject(files.getValue("exercise-knowledge-v1.json")) + mutate(root.getJSONArray("exercises").getJSONObject(0).getJSONObject("body_zone_audit")) + files["exercise-knowledge-v1.json"] = root.toString() + } +} diff --git a/android/app/src/test/java/com/labfytools/trainlog/data/TrainlogTimestampTest.kt b/android/app/src/test/java/com/labfytools/trainlog/data/TrainlogTimestampTest.kt new file mode 100644 index 0000000..eeb150b --- /dev/null +++ b/android/app/src/test/java/com/labfytools/trainlog/data/TrainlogTimestampTest.kt @@ -0,0 +1,33 @@ +package com.labfytools.trainlog.data + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test + +class TrainlogTimestampTest { + @Test fun acceptsSettledGrammarAndComparesEveryFractionDigit() { + listOf( + "0001-01-01T00:00Z", "2000-02-29t23:59:59.1z", + "2026-09-05T18:34+23:59", "2026-09-05T18:34:12-00:00", + "9999-12-31T23:59:59.9-23:59", + ).forEach { assertNotNull(it, TrainlogTimestamp.parse(it)) } + val low = TrainlogTimestamp.parse("2026-01-01T00:00:00.12345678901234567890Z")!! + val high = TrainlogTimestamp.parse("2026-01-01T00:00:00.12345678901234567891Z")!! + val equal = TrainlogTimestamp.parse("2026-01-01T00:00:00.123456789012345678900Z")!! + assertTrue(low < high) + assertEquals(0, low.compareTo(equal)) + } + + @Test fun rejectsPlatformOnlyAndOutOfRangeForms() { + listOf( + "0000-01-01T00:00:00Z", "2026-02-29T00:00:00Z", + "2026-09-05 18:34:12+02:00", "20260905T183412+0200", + "2026-W36-5T18:34:12+02:00", "2026-09-05T18:34:12,5+02:00", + "2026-09-05T18:34:12+0200", "2026-09-05T18:34:12+02", + "2026-09-05T18:34.5Z", "2026-09-05T18:34:60Z", + "2026-09-05T18:34:12+24:00", + ).forEach { assertNull(it, TrainlogTimestamp.parse(it)) } + } +} diff --git a/catalog/equipment-knowledge-v1.json b/catalog/equipment-knowledge-v1.json new file mode 100644 index 0000000..de7e2b3 --- /dev/null +++ b/catalog/equipment-knowledge-v1.json @@ -0,0 +1,2220 @@ +{ + "format": "trainlog-equipment-knowledge-v1", + "version": 1, + "equipment": [ + { + "equipment_id": "abdominal", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Intended loaded crunch; actual spinal vs hip flexion and pad linkage need verification.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "capabilities": [ + { + "display_name": "abdominal crunch", + "exercise_ids": [ + "ex_9adf7566-f10c-443c-b63b-681d37665693" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "variant_notes": "Assumes resisted spinal flexion. If movement is predominantly hip flexion, iliopsoas contribution rises and classification needs review. Rectus regions are not independent upper/lower abdominal muscles.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "abdominal_bench", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "bench", + "catalog_load_semantics": "bodyweight", + "mechanics": "Support and body angle influence gravity moment; crunch and hip-flexing sit-up are distinct executions.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "capabilities": [ + { + "display_name": "abdominal crunch", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "variant_notes": "Assumes resisted spinal flexion. If movement is predominantly hip flexion, iliopsoas contribution rises and classification needs review. Rectus regions are not independent upper/lower abdominal muscles.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "arm_curl", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Elbow flexion against a machine lever; grip, arm support and cam unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "arm curl", + "exercise_ids": [ + "ex_ec619fc2-4685-4044-873c-86764bd4a0fe" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "arm_curl", + "action_ids": [ + "elbow_flexion" + ], + "pattern_ids": [ + "single_joint_elbow_flexion" + ], + "primary_muscle_ids": [ + "biceps_brachii", + "brachialis" + ], + "secondary_muscle_ids": [ + "brachioradialis" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "arms", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_upper" + ], + "variant_notes": "Grip/forearm orientation and shoulder support alter contributions. Curl label does not mean biceps exclusively.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "assisted_dip_chin_machine", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "assisted_bodyweight_machine", + "catalog_load_semantics": "assistance", + "mechanics": "Counterbalanced body support assists separate dip and chin/pull-up tasks. More assistance generally reduces net demand.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "lehman_2004", + "life_fitness_catalog_2024", + "nih_pectoralis", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "assisted dip", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "assisted_dip", + "action_ids": [ + "elbow_extension", + "shoulder_flexion" + ], + "pattern_ids": [ + "vertical_push" + ], + "primary_muscle_ids": [ + "pectoralis_major", + "triceps_brachii" + ], + "secondary_muscle_ids": [ + "deltoid_anterior" + ], + "stabilizer_muscle_ids": [ + "forearm_flexors", + "serratus_anterior" + ], + "primary_zone_id": "arms", + "secondary_zone_ids": [ + "chest", + "shoulders" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Supported-body dip with humerus returning from extension; chest vs triceps emphasis varies with torso/handle geometry. Vertical push is a broad direction convention, not an overhead press equivalent. Single primary BODY ZONE remains a conditional UX choice.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + }, + { + "display_name": "assisted chin pullup", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "assisted_chin_pullup", + "action_ids": [ + "elbow_flexion", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_flexors", + "rectus_abdominis" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Chin-up and pronated pull-up are grip variants requiring separate execution context. Assistance decreases demand; body mass, assistance mechanism and range matter.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "back_extension", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Resisted trunk extension; pelvic restraint determines spinal vs hip motion.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "back extension", + "exercise_ids": [ + "ex_474ec393-3efa-4aaa-8e08-1a0245ed7835" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "back_extension", + "action_ids": [ + "trunk_extension" + ], + "pattern_ids": [ + "trunk_extension" + ], + "primary_muscle_ids": [ + "erector_spinae" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "external_oblique", + "internal_oblique", + "rectus_abdominis" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "core" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "variant_notes": "Assumes pelvis restrained and actual spinal extension. If pelvis rotates with near-neutral spine, hip extensors may become major movers; observation needed before adding glutes/thighs or relabeling hip dominant.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "battle_ropes", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "functional_equipment", + "catalog_load_semantics": "bodyweight", + "mechanics": "Moving rope has mass/inertia and wave dynamics; catalog bodyweight is a legacy category, not a valid scalar load model.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": "Existing bodyweight category retained. Rope inertia is not quantified by bodyweight; review future explicit load vocabulary separately.", + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "converging_shoulder_press", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "machine", + "catalog_load_semantics": "external", + "mechanics": "Overhead press with convergence suggested by label; plane and resistance curve unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "machine shoulder press", + "exercise_ids": [ + "ex_6dfc7ffd-8891-464e-a995-808baf1b0d7b" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "machine_shoulder_press", + "action_ids": [ + "elbow_extension", + "scapular_upward_rotation", + "shoulder_abduction", + "shoulder_flexion" + ], + "pattern_ids": [ + "vertical_push" + ], + "primary_muscle_ids": [ + "deltoid_anterior", + "deltoid_middle" + ], + "secondary_muscle_ids": [ + "serratus_anterior", + "trapezius_lower", + "trapezius_upper", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "variant_notes": "Press overhead; exact plane, convergence, back support and available scapular motion are unverified. Clavicular pectoral contribution possible but not a reason to assign chest as a universal secondary zone.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "diverging_lat_pulldown", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Overhead-to-torso pull; divergent path indicated by supplied label, not independently measured.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "lat pulldown", + "exercise_ids": [ + "ex_b4d1daf1-de4a-4016-abdf-487bf6014ce6" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Arm plane and torso lean change relative adduction/extension; grip and divergent linkage do not prove regional isolation. Scapular control accompanies return from overhead.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "diverging_seated_row", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Horizontal pull on reportedly divergent handles; exact path unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "seated row", + "exercise_ids": [ + "ex_1b0c6b8b-b05e-4e6f-8809-5f7d85d668de" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Relative lat/scapular/posterior-deltoid roles depend on elbow path, height and torso support. Listed shoulder actions are alternatives/emphases across rows rather than fixed equal contributions.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "dual_adjustable_pulley", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cable_machine", + "catalog_load_semantics": "external", + "mechanics": "Two adjustable cable lines; independent stack labels, routing and attachment setup matter.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions", + "precor_pulley_example" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "dumbbells", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "free_weight", + "catalog_load_semantics": "external", + "mechanics": "Free mass under gravity plus inertia; exercise and unilateral/bilateral accounting required.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "eq_0a462c1e-9fb6-4fd7-b0a2-53a0e86f33c6", + "manufacturer": null, + "model": null, + "identification_status": "custom_identity_only", + "catalog_type": "custom_unresolved", + "catalog_load_semantics": null, + "mechanics": "Only durable custom identity and display label established; no geometry confirmation.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "machine chest press", + "exercise_ids": [ + "ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af" + ], + "link_status": "linked_identity_conditional_interpretation", + "interpretation": { + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Assumes seated anterior press near chest height; incline, seat height, elbow path and converging linkage change shoulder flexion/regional contributions. No independent upper/lower chest isolation.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm movement; label is insufficient anatomical evidence." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "Do not merge with similarly named supplied equipment." + ], + "scientific_status": "equipment_identity_uncertain", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "eq_3f987a36-bbb4-4e29-9c9f-1f201f1596e0", + "manufacturer": null, + "model": null, + "identification_status": "custom_identity_only", + "catalog_type": "custom_unresolved", + "catalog_load_semantics": null, + "mechanics": "Only durable custom identity and display label established; no geometry confirmation.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "capabilities": [ + { + "display_name": "abdominal crunch", + "exercise_ids": [ + "ex_4bd03d55-e644-436e-876e-07837824bde5" + ], + "link_status": "linked_identity_conditional_interpretation", + "interpretation": { + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "variant_notes": "Assumes resisted spinal flexion. If movement is predominantly hip flexion, iliopsoas contribution rises and classification needs review. Rectus regions are not independent upper/lower abdominal muscles.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm movement; label is insufficient anatomical evidence." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "Do not merge with similarly named supplied equipment." + ], + "scientific_status": "equipment_identity_uncertain", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "eq_c660cd61-5b17-498e-8d51-9eafcf7e2731", + "manufacturer": null, + "model": null, + "identification_status": "custom_identity_only", + "catalog_type": "custom_unresolved", + "catalog_load_semantics": null, + "mechanics": "Only durable custom identity and display label established; no geometry confirmation.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "machine chest press", + "exercise_ids": [ + "ex_34a5c9c3-c032-4dfb-be32-8bf09832f72b" + ], + "link_status": "linked_identity_conditional_interpretation", + "interpretation": { + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Assumes seated anterior press near chest height; incline, seat height, elbow path and converging linkage change shoulder flexion/regional contributions. No independent upper/lower chest isolation.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm movement; label is insufficient anatomical evidence." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "Do not merge with similarly named supplied equipment." + ], + "scientific_status": "equipment_identity_uncertain", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "functional_trainer", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cable_machine", + "catalog_load_semantics": "external", + "mechanics": "Adjustable cable line permits many exercises. Attachment, pulley height, direction, ratio and execution required.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions", + "precor_pulley_example" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "hip_abduction", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Pads resist thigh separation; hip-flexion and rotation settings unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "hip abduction", + "exercise_ids": [ + "ex_7e7cf906-2214-4066-bcb7-c16382d83b3b" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "hip_abduction", + "action_ids": [ + "hip_abduction" + ], + "pattern_ids": [ + "single_joint_hip_abduction" + ], + "primary_muscle_ids": [ + "gluteus_medius", + "gluteus_minimus" + ], + "secondary_muscle_ids": [ + "gluteus_maximus", + "tensor_fasciae_latae" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "glutes", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "variant_notes": "Seated hip-flexion angle changes fiber lines of action; no definitive gluteal ranking. Superior gluteus-maximus fibers may assist dynamically depending on hip angle; this is a conditional secondary role.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "hip_adduction", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Pads resist thigh approximation; hip/knee angles unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "hip adduction", + "exercise_ids": [ + "ex_01ff06dd-ad00-46ee-9b46-ed32cabedbef" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "hip_adduction", + "action_ids": [ + "hip_adduction" + ], + "pattern_ids": [ + "single_joint_hip_adduction" + ], + "primary_muscle_ids": [ + "hip_adductors" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_lower" + ], + "variant_notes": "Close thighs against resistance; different adductors and gracilis span different joints; seat/knee position affects contribution.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "indoor_cycle", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Cycle ergometer with flywheel/resistance; standing and seated pedaling are distinct contexts.", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "kettlebells", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "free_weight", + "catalog_load_semantics": "external", + "mechanics": "Free mass with offset grip; ballistic vs slow movement changes inertial demands.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "lat_pull", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Supplied vertical-pull machine; attachment, pulley ratio and arm path unresolved.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "lat pulldown", + "exercise_ids": [ + "ex_a72fa713-4b0e-431d-95e2-42d95beb77b1" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Arm plane and torso lean change relative adduction/extension; grip and divergent linkage do not prove regional isolation. Scapular control accompanies return from overhead.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "lat_pulldown", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Supplied vertical-pull machine; do not equate with other lat machines by name.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "lat pulldown", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Arm plane and torso lean change relative adduction/extension; grip and divergent linkage do not prove regional isolation. Scapular control accompanies return from overhead.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "leg_extension", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Distal lower-leg resistance creates knee-flexion external moment during extension effort; cam unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "leg extension", + "exercise_ids": [ + "ex_1872246a-39ae-44dc-b58d-f87e90ca49ab" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "leg_extension", + "action_ids": [ + "knee_extension" + ], + "pattern_ids": [ + "single_joint_knee_extension" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_lower" + ], + "variant_notes": "Seated resisted knee extension; rectus femoris is biarticular while vasti cross only the knee. Roller/axis alignment and hip angle affect mechanics.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "leg_press", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Supported multi-joint hip/knee extension against a selectorized mechanism.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "leg press", + "exercise_ids": [ + "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "leg_press", + "action_ids": [ + "hip_extension", + "knee_extension" + ], + "pattern_ids": [ + "knee_dominant" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [ + "adductor_magnus", + "gluteus_maximus" + ], + "stabilizer_muscle_ids": [ + "gastrocnemius", + "soleus" + ], + "primary_zone_id": "thighs", + "secondary_zone_ids": [ + "glutes" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "variant_notes": "Hip and knee extend together. Depth, seat/foot position and device geometry change joint moments. Hamstring coactivation does not make this a knee-flexor substitute. Selectorized and plate-loaded results stay separate contexts.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "low_row", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Lower handle start can change shoulder extension path; geometry unconfirmed.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "seated row", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Relative lat/scapular/posterior-deltoid roles depend on elbow path, height and torso support. Listed shoulder actions are alternatives/emphases across rows rather than fixed equal contributions.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "medicine_ball", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "functional_equipment", + "catalog_load_semantics": "external", + "mechanics": "External mass used in holds, throws or other tasks; velocity and impact matter.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "perfect_squat", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "plate_loaded_machine", + "catalog_load_semantics": "external", + "mechanics": "Guided squat capability suggested by identity; linkage, support and knee/hip torque distribution unconfirmed.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "plate_loaded_leg_press", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "plate_loaded_machine", + "catalog_load_semantics": "external", + "mechanics": "Plate-loaded multi-joint hip/knee extension; sled angle, lever arms, tare and friction unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "leg press", + "exercise_ids": [ + "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "leg_press", + "action_ids": [ + "hip_extension", + "knee_extension" + ], + "pattern_ids": [ + "knee_dominant" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [ + "adductor_magnus", + "gluteus_maximus" + ], + "stabilizer_muscle_ids": [ + "gastrocnemius", + "soleus" + ], + "primary_zone_id": "thighs", + "secondary_zone_ids": [ + "glutes" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "variant_notes": "Hip and knee extend together. Depth, seat/foot position and device geometry change joint moments. Hamstring coactivation does not make this a knee-flexor substitute. Selectorized and plate-loaded results stay separate contexts.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "prone_leg_curl", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Prone knee flexion; generally less hip flexion than seated configuration.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "prone leg curl", + "exercise_ids": [ + "ex_d7398d9f-d928-4d2e-94e9-74e201da55c5" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "prone_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "variant_notes": "Prone support generally uses less hip flexion than seated curl. Same knee-flexor family, distinct length/geometry context. Gastrocnemius assistance is position dependent.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "rear_delt_pec_fly", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_combo_machine", + "catalog_load_semantics": "external", + "mechanics": "Reconfigurable opposing shoulder horizontal-abduction and horizontal-adduction tasks; two separate exercise capabilities.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "franke_2015", + "life_fitness_catalog_2024", + "nih_deltoid", + "nih_pectoralis", + "openstax_upper", + "vigotsky_2018" + ], + "capabilities": [ + { + "display_name": "rear delt fly", + "exercise_ids": [ + "ex_4cd2433e-80b1-478a-b8df-73fc6ef80962" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "rear_delt_fly", + "action_ids": [ + "scapular_retraction", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "single_joint_shoulder_horizontal_abduction" + ], + "primary_muscle_ids": [ + "deltoid_posterior" + ], + "secondary_muscle_ids": [ + "rhomboids", + "trapezius_middle" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "back" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Reverse-fly configuration on combined machine. Scapular movement and elbow height affect back contribution; external rotation is not guaranteed by reverse-fly name.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + }, + { + "display_name": "pec fly", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "pec_fly", + "action_ids": [ + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "single_joint_shoulder_horizontal_adduction" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "shoulders" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Forward fly configuration with near-fixed elbow; differs from pressing and from rear-delt configuration. Triceps may stabilize but is not a required dynamic synergist.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "recumbent_bike", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Supported reclined cycling; hip position and crank location differ from upright bike.", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "rotary_torso", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Resistance to relative thorax-pelvis rotation; which segment is fixed unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "capabilities": [ + { + "display_name": "rotary torso", + "exercise_ids": [ + "ex_1a34814c-2e46-40fc-b1f4-6d60b8e5a3e0" + ], + "link_status": "catalog_compatible_not_observed_occurrence", + "interpretation": { + "family_description": "rotary_torso", + "action_ids": [ + "trunk_rotation" + ], + "pattern_ids": [ + "trunk_rotation" + ], + "primary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "variant_notes": "Thorax rotates relative to pelvis, or reverse with thorax restrained. Ipsilateral internal and contralateral external oblique pair for a direction. Do not claim isolated lumbar rotation or spot reduction.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "rowing_machine", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Cyclic leg drive plus trunk/arm sequence; drag setting is not external kilograms.", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "seated_dip", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Seated hand press-down against external resistance; shoulder excursion varies; not assisted bodyweight dip.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_pectoralis", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "seated dip", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "seated_dip", + "action_ids": [ + "elbow_extension" + ], + "pattern_ids": [ + "single_joint_elbow_extension" + ], + "primary_muscle_ids": [ + "triceps_brachii" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "pectoralis_major" + ], + "stabilizer_muscle_ids": [ + "forearm_flexors" + ], + "primary_zone_id": "arms", + "secondary_zone_ids": [ + "chest", + "shoulders" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Interpretation limited to the elbow-dominant component; pectoral/anterior-deltoid assistance is conditional on actual path. Machine seated pushdown/dip: shoulder excursion and mechanical linkage must be observed. Not equivalent to assisted bodyweight dip; some variants are multi-joint and require pattern review.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "seated_leg_curl", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Seated knee flexion; hip flexion changes biarticular hamstring length.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "seated leg curl", + "exercise_ids": [ + "ex_617007f9-7420-4408-91b9-8ffb77900f13", + "ex_a1ef5047-b44b-4c64-a6ed-c7a3bc13b163" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "seated_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "variant_notes": "Hip flexion lengthens biarticular hamstrings at matched knee angle; biceps short head is not lengthened through hip flexion. Do not infer all variants share MAX or equivalent hypertrophy.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "seated_row", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "selectorized_machine", + "catalog_load_semantics": "external", + "mechanics": "Seated horizontal pull; handle, support and linkage unknown.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "seated row", + "exercise_ids": [ + "ex_33f79331-871c-4eed-babe-346e53a99070" + ], + "link_status": "linked_existing", + "interpretation": { + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Relative lat/scapular/posterior-deltoid roles depend on elbow path, height and torso support. Listed shoulder actions are alternatives/emphases across rows rather than fixed equal contributions.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "stair_climber", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Repeated stepping against gravity/device; step geometry, speed and hand support matter.", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "suspension_trainer", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "functional_equipment", + "catalog_load_semantics": "bodyweight", + "mechanics": "Body angle, support points and strap line change fraction of bodyweight and joint moments.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "treadmill", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Moving belt supports walking/running; speed, grade and rail support change task.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "capabilities": [ + { + "display_name": "walking", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "walking", + "action_ids": [ + "ankle_dorsiflexion", + "ankle_plantarflexion", + "hip_extension", + "hip_flexion", + "knee_extension", + "knee_flexion" + ], + "pattern_ids": [ + "locomotion" + ], + "primary_muscle_ids": [ + "gastrocnemius", + "gluteus_maximus", + "quadriceps", + "soleus" + ], + "secondary_muscle_ids": [ + "hamstrings", + "iliopsoas", + "tibialis_anterior" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "gluteus_medius", + "gluteus_minimus" + ], + "primary_zone_id": "full_body", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "variant_notes": "Multi-phase gait: role lists summarize different phases, not simultaneous prime movers. Speed, grade, handrail support and gait mechanics unresolved. Full-body is only an optional UX candidate, not mandatory due to cardio metadata.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "upright_bike", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "cardio_machine", + "catalog_load_semantics": "cardio", + "mechanics": "Cyclic pedaling with seated geometry; crank resistance, cadence and seat fit matter.", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "vertical_chest_press", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "machine", + "catalog_load_semantics": "external", + "mechanics": "Vertical can describe apparatus orientation; presumed forward chest-level press, not proof of overhead pressing.", + "evidence_type": "biomechanical_interpretation", + "confidence": "moderate", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "capabilities": [ + { + "display_name": "machine chest press", + "exercise_ids": [], + "link_status": "unlinked_capability", + "interpretation": { + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Assumes seated anterior press near chest height; incline, seat height, elbow path and converging linkage change shoulder flexion/regional contributions. No independent upper/lower chest isolation.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "requirements": "Confirm actual exercise variant and equipment setup before treating as performed exercise; allocate no synthetic ex_ identity." + } + ], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "scientifically_documented", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + }, + { + "equipment_id": "weighted_bag", + "manufacturer": null, + "model": null, + "identification_status": "manufacturer_model_unresolved", + "catalog_type": "free_weight", + "catalog_load_semantics": "external", + "mechanics": "External mass; grip and load distribution vary. Carry, squat and lift are different exercises.", + "evidence_type": "practical_inference", + "confidence": "uncertain", + "source_refs": [ + "openstax_actions" + ], + "capabilities": [], + "requires_actual_exercise": true, + "limitations": [ + "No measured local trajectory, torque curve, calibration or force equivalence.", + "Manufacturer sources, when present, are generic examples only." + ], + "audit_note": null, + "scientific_status": "mechanically_identified_anatomy_incomplete", + "scientific_status_scope": "Exercise-family anatomy and generic mechanics only; scientifically_documented never certifies a local manufacturer/model, resistance curve or actual execution. Flexible/mode-dependent equipment needs a specified exercise." + } + ] +} diff --git a/catalog/exercise-knowledge-v1.json b/catalog/exercise-knowledge-v1.json new file mode 100644 index 0000000..b79fa51 --- /dev/null +++ b/catalog/exercise-knowledge-v1.json @@ -0,0 +1,1596 @@ +{ + "format": "trainlog-exercise-knowledge-v1", + "version": 1, + "exercises": [ + { + "exercise_id": "ex_01ff06dd-ad00-46ee-9b46-ed32cabedbef", + "exercise_name": "Hip adduction", + "equipment_ids": [ + "hip_adduction" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique hip_adduction (adducteurs de la cuisse).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "hip_adduction", + "action_ids": [ + "hip_adduction" + ], + "pattern_ids": [ + "single_joint_hip_adduction" + ], + "primary_muscle_ids": [ + "hip_adductors" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_lower" + ], + "variant_notes": "Close thighs against resistance; different adductors and gracilis span different joints; seat/knee position affects contribution.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [] + }, + "source_refs": [ + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_1872246a-39ae-44dc-b58d-f87e90ca49ab", + "exercise_name": "Leg extension", + "equipment_ids": [ + "leg_extension" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique leg_extension (quadriceps).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "leg_extension", + "action_ids": [ + "knee_extension" + ], + "pattern_ids": [ + "single_joint_knee_extension" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_lower" + ], + "variant_notes": "Seated resisted knee extension; rectus femoris is biarticular while vasti cross only the knee. Roller/axis alignment and hip angle affect mechanics.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [] + }, + "source_refs": [ + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_1a34814c-2e46-40fc-b1f4-6d60b8e5a3e0", + "exercise_name": "Rotary torso", + "equipment_ids": [ + "rotary_torso" + ], + "identity_evidence": "Stable catalog mapping identifies torso rotation; canonical rotary_torso is available but occurrence-equipment attachment is not established.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "rotary_torso", + "action_ids": [ + "trunk_rotation" + ], + "pattern_ids": [ + "trunk_rotation" + ], + "primary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "variant_notes": "Thorax rotates relative to pelvis, or reverse with thorax restrained. Ipsilateral internal and contralateral external oblique pair for a direction. Do not claim isolated lumbar rotation or spot reduction.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "core", + "secondary_zone_ids": [] + }, + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "catalog_compatible_not_observed_occurrence", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ] + } + }, + { + "exercise_id": "ex_1b0c6b8b-b05e-4e6f-8809-5f7d85d668de", + "exercise_name": "Diverged seated row", + "equipment_ids": [ + "diverging_seated_row" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique diverging_seated_row.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Relative lat/scapular/posterior-deltoid roles depend on elbow path, height and torso support. Listed shoulder actions are alternatives/emphases across rows rather than fixed equal contributions.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ] + }, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Back plus arms is defensible; posterior deltoid participates but shoulders need not be mandatory secondary for every row geometry.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ] + } + }, + { + "exercise_id": "ex_2d488c08-194c-4051-a3c9-34471646c1d3", + "exercise_name": "Gym échauffement", + "equipment_ids": [], + "identity_evidence": "Read-only live inventory; continuous/duration profile. Specific movement sequence not independently verified.", + "identity_evidence_type": "read_only_inventory", + "resolution_status": "unresolved", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": null, + "secondary_zone_ids": [] + }, + "source_refs": [], + "limitations": [ + "A generic warm-up can contain multiple different activities; no muscular mapping follows from the name." + ], + "body_zone_audit": { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [] + } + }, + { + "exercise_id": "ex_33f79331-871c-4eed-babe-346e53a99070", + "exercise_name": "Seated row", + "equipment_ids": [ + "seated_row" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique seated_row.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "variant_notes": "Relative lat/scapular/posterior-deltoid roles depend on elbow path, height and torso support. Listed shoulder actions are alternatives/emphases across rows rather than fixed equal contributions.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ] + }, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Back plus arms is defensible; posterior deltoid participates but shoulders need not be mandatory secondary for every row geometry.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ] + } + }, + { + "exercise_id": "ex_34a5c9c3-c032-4dfb-be32-8bf09832f72b", + "exercise_name": "Converting chest press", + "equipment_ids": [ + "eq_c660cd61-5b17-498e-8d51-9eafcf7e2731" + ], + "identity_evidence": "Identité stable et équipement custom durable eq_c660cd61-5b17-498e-8d51-9eafcf7e2731 nommé Converting chest press.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "conditional", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": { + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Assumes seated anterior press near chest height; incline, seat height, elbow path and converging linkage change shoulder flexion/regional contributions. No independent upper/lower chest isolation.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted.", + "required_confirmation": "Confirm actual motion, seat/back support, load direction and physical apparatus; label alone is insufficient." + }, + "existing_body_zones": { + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ] + }, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "chest", + "existing_secondary_zone_ids": [ + "arms", + "shoulders" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ] + } + }, + { + "exercise_id": "ex_43c7375f-934c-4650-930c-45807d2f2929", + "exercise_name": "Gym/Échauffement", + "equipment_ids": [], + "identity_evidence": "Read-only live inventory; continuous/duration profile. Specific movement sequence not independently verified.", + "identity_evidence_type": "read_only_inventory", + "resolution_status": "unresolved", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": null, + "secondary_zone_ids": [] + }, + "source_refs": [], + "limitations": [ + "A generic warm-up can contain multiple different activities; no muscular mapping follows from the name." + ], + "body_zone_audit": { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [] + } + }, + { + "exercise_id": "ex_474ec393-3efa-4aaa-8e08-1a0245ed7835", + "exercise_name": "Back extension", + "equipment_ids": [ + "back_extension" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique back_extension (extension lombaire et stabilisation du tronc).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "back_extension", + "action_ids": [ + "trunk_extension" + ], + "pattern_ids": [ + "trunk_extension" + ], + "primary_muscle_ids": [ + "erector_spinae" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "external_oblique", + "internal_oblique", + "rectus_abdominis" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "core" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "variant_notes": "Assumes pelvis restrained and actual spinal extension. If pelvis rotates with near-neutral spine, hip extensors may become major movers; observation needed before adding glutes/thighs or relabeling hip dominant.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "back", + "secondary_zone_ids": [ + "core" + ] + }, + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "questionable", + "severity": "information", + "confidence": "moderate", + "rationale": "Back plus core is compatible with spinal extension; hip-dominant execution could justify additional lower-limb roles after observation.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "core" + ], + "proposed_mutation": null, + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_4bd03d55-e644-436e-876e-07837824bde5", + "exercise_name": "Abdominal", + "equipment_ids": [ + "eq_3f987a36-bbb4-4e29-9c9f-1f201f1596e0" + ], + "identity_evidence": "Identité stable et équipement custom durable eq_3f987a36-bbb4-4e29-9c9f-1f201f1596e0 nommé Abdominal.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "conditional", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": { + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "variant_notes": "Assumes resisted spinal flexion. If movement is predominantly hip flexion, iliopsoas contribution rises and classification needs review. Rectus regions are not independent upper/lower abdominal muscles.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted.", + "required_confirmation": "Confirm actual motion, seat/back support, load direction and physical apparatus; label alone is insufficient." + }, + "existing_body_zones": { + "primary_zone_id": "core", + "secondary_zone_ids": [] + }, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ] + } + }, + { + "exercise_id": "ex_4cd2433e-80b1-478a-b8df-73fc6ef80962", + "exercise_name": "Rear Delt", + "equipment_ids": [ + "rear_delt_pec_fly" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique rear_delt_pec_fly, utilisée comme Rear Delt.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "rear_delt_fly", + "action_ids": [ + "scapular_retraction", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "single_joint_shoulder_horizontal_abduction" + ], + "primary_muscle_ids": [ + "deltoid_posterior" + ], + "secondary_muscle_ids": [ + "rhomboids", + "trapezius_middle" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "back" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Reverse-fly configuration on combined machine. Scapular movement and elbow height affect back contribution; external rotation is not guaranteed by reverse-fly name.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "back" + ] + }, + "source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Shoulders plus back matches posterior-deltoid horizontal abduction and scapular retraction; same device Pec Fly would instead target chest.", + "existing_primary_zone_id": "shoulders", + "existing_secondary_zone_ids": [ + "back" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ] + } + }, + { + "exercise_id": "ex_617007f9-7420-4408-91b9-8ffb77900f13", + "exercise_name": "Seated Leg", + "equipment_ids": [ + "seated_leg_curl" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique seated_leg_curl (ischio-jambiers).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "seated_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "variant_notes": "Hip flexion lengthens biarticular hamstrings at matched knee angle; biceps short head is not lengthened through hip flexion. Do not infer all variants share MAX or equivalent hypertrophy.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [] + }, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_6dfc7ffd-8891-464e-a995-808baf1b0d7b", + "exercise_name": "Converging Shoulder Press", + "equipment_ids": [ + "converging_shoulder_press" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique converging_shoulder_press.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "machine_shoulder_press", + "action_ids": [ + "elbow_extension", + "scapular_upward_rotation", + "shoulder_abduction", + "shoulder_flexion" + ], + "pattern_ids": [ + "vertical_push" + ], + "primary_muscle_ids": [ + "deltoid_anterior", + "deltoid_middle" + ], + "secondary_muscle_ids": [ + "serratus_anterior", + "trapezius_lower", + "trapezius_upper", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "variant_notes": "Press overhead; exact plane, convergence, back support and available scapular motion are unverified. Clavicular pectoral contribution possible but not a reason to assign chest as a universal secondary zone.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "shoulders", + "secondary_zone_ids": [ + "arms" + ] + }, + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "shoulders", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ] + } + }, + { + "exercise_id": "ex_7e7cf906-2214-4066-bcb7-c16382d83b3b", + "exercise_name": "Hip abduction", + "equipment_ids": [ + "hip_abduction" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique hip_abduction (abducteurs/fessiers).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "hip_abduction", + "action_ids": [ + "hip_abduction" + ], + "pattern_ids": [ + "single_joint_hip_abduction" + ], + "primary_muscle_ids": [ + "gluteus_medius", + "gluteus_minimus" + ], + "secondary_muscle_ids": [ + "gluteus_maximus", + "tensor_fasciae_latae" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "glutes", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "variant_notes": "Seated hip-flexion angle changes fiber lines of action; no definitive gluteal ranking. Superior gluteus-maximus fibers may assist dynamically depending on hip angle; this is a conditional secondary role.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "glutes", + "secondary_zone_ids": [] + }, + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Glutes is defensible for medius/minimus; tensor fasciae latae participation does not require all accessory muscles to create a secondary zone.", + "existing_primary_zone_id": "glutes", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af", + "exercise_name": "Chest press", + "equipment_ids": [ + "eq_0a462c1e-9fb6-4fd7-b0a2-53a0e86f33c6" + ], + "identity_evidence": "Identité stable et équipement custom durable eq_0a462c1e-9fb6-4fd7-b0a2-53a0e86f33c6 nommé Chest press.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "conditional", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": { + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "variant_notes": "Assumes seated anterior press near chest height; incline, seat height, elbow path and converging linkage change shoulder flexion/regional contributions. No independent upper/lower chest isolation.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted.", + "required_confirmation": "Confirm actual motion, seat/back support, load direction and physical apparatus; label alone is insufficient." + }, + "existing_body_zones": { + "primary_zone_id": "chest", + "secondary_zone_ids": [ + "arms", + "shoulders" + ] + }, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "chest", + "existing_secondary_zone_ids": [ + "arms", + "shoulders" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ] + } + }, + { + "exercise_id": "ex_9adf7566-f10c-443c-b63b-681d37665693", + "exercise_name": "Abdominal crunch", + "equipment_ids": [ + "abdominal" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique abdominal (crunch machine).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "primary_zone_id": "core", + "secondary_zone_ids": [], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "variant_notes": "Assumes resisted spinal flexion. If movement is predominantly hip flexion, iliopsoas contribution rises and classification needs review. Rectus regions are not independent upper/lower abdominal muscles.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "core", + "secondary_zone_ids": [] + }, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ] + } + }, + { + "exercise_id": "ex_a1ef5047-b44b-4c64-a6ed-c7a3bc13b163", + "exercise_name": "Seated leg curl", + "equipment_ids": [ + "seated_leg_curl" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique seated_leg_curl (ischio-jambiers).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "seated_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "variant_notes": "Hip flexion lengthens biarticular hamstrings at matched knee angle; biceps short head is not lengthened through hip flexion. Do not infer all variants share MAX or equivalent hypertrophy.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [] + }, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_a72fa713-4b0e-431d-95e2-42d95beb77b1", + "exercise_name": "Lat pull", + "equipment_ids": [ + "lat_pull" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique lat_pull.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Arm plane and torso lean change relative adduction/extension; grip and divergent linkage do not prove regional isolation. Scapular control accompanies return from overhead.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ] + }, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ] + } + }, + { + "exercise_id": "ex_b1e6ffc6-75b5-45ff-a3c0-e7433c58013d", + "exercise_name": "Marche", + "equipment_ids": [], + "identity_evidence": "Read-only live inventory; continuous/duration profile. Specific movement sequence not independently verified.", + "identity_evidence_type": "read_only_inventory", + "resolution_status": "conditional", + "confidence": "uncertain", + "interpretation": null, + "conditional_interpretation": { + "family_description": "walking", + "action_ids": [ + "ankle_dorsiflexion", + "ankle_plantarflexion", + "hip_extension", + "hip_flexion", + "knee_extension", + "knee_flexion" + ], + "pattern_ids": [ + "locomotion" + ], + "primary_muscle_ids": [ + "gastrocnemius", + "gluteus_maximus", + "quadriceps", + "soleus" + ], + "secondary_muscle_ids": [ + "hamstrings", + "iliopsoas", + "tibialis_anterior" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "gluteus_medius", + "gluteus_minimus" + ], + "primary_zone_id": "full_body", + "secondary_zone_ids": [], + "confidence": "uncertain", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "variant_notes": "Multi-phase gait: role lists summarize different phases, not simultaneous prime movers. Speed, grade, handrail support and gait mechanics unresolved. Full-body is only an optional UX candidate, not mandatory due to cardio metadata.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted.", + "required_confirmation": "Confirm walking execution and occurrence-equipment association; treadmill appears in aggregate inventory only." + }, + "existing_body_zones": { + "primary_zone_id": null, + "secondary_zone_ids": [] + }, + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "limitations": [ + "Walking is a plausible family, not a verified exact gait variant." + ], + "body_zone_audit": { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [] + } + }, + { + "exercise_id": "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde", + "exercise_name": "Leg press", + "equipment_ids": [ + "leg_press", + "plate_loaded_leg_press" + ], + "identity_evidence": "Identité stable réconciliée et occurrences liées aux équipements canoniques leg_press/plate_loaded_leg_press.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "leg_press", + "action_ids": [ + "hip_extension", + "knee_extension" + ], + "pattern_ids": [ + "knee_dominant" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [ + "adductor_magnus", + "gluteus_maximus" + ], + "stabilizer_muscle_ids": [ + "gastrocnemius", + "soleus" + ], + "primary_zone_id": "thighs", + "secondary_zone_ids": [ + "glutes" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "variant_notes": "Hip and knee extend together. Depth, seat/foot position and device geometry change joint moments. Hamstring coactivation does not make this a knee-flexor substitute. Selectorized and plate-loaded results stay separate contexts.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [ + "glutes" + ] + }, + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [ + "glutes" + ], + "proposed_mutation": null, + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ] + } + }, + { + "exercise_id": "ex_b4d1daf1-de4a-4016-abdf-487bf6014ce6", + "exercise_name": "Diverging lat pulldown", + "equipment_ids": [ + "diverging_lat_pulldown" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique diverging_lat_pulldown.", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "moderate", + "interpretation": { + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ], + "confidence": "moderate", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "variant_notes": "Arm plane and torso lean change relative adduction/extension; grip and divergent linkage do not prove regional isolation. Scapular control accompanies return from overhead.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "back", + "secondary_zone_ids": [ + "arms" + ] + }, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ] + } + }, + { + "exercise_id": "ex_d7398d9f-d928-4d2e-94e9-74e201da55c5", + "exercise_name": "Prone leg curl", + "equipment_ids": [ + "prone_leg_curl" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique prone_leg_curl (ischio-jambiers).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "prone_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "primary_zone_id": "thighs", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "variant_notes": "Prone support generally uses less hip flexion than seated curl. Same knee-flexor family, distinct length/geometry context. Gastrocnemius assistance is position dependent.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "thighs", + "secondary_zone_ids": [] + }, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ] + } + }, + { + "exercise_id": "ex_ec619fc2-4685-4044-873c-86764bd4a0fe", + "exercise_name": "Arm curl", + "equipment_ids": [ + "arm_curl" + ], + "identity_evidence": "Identité stable et occurrence liée à l’équipement canonique arm_curl (curl biceps).", + "identity_evidence_type": "repository_catalog_and_read_only_inventory", + "resolution_status": "resolved_family_variant_limited", + "confidence": "high", + "interpretation": { + "family_description": "arm_curl", + "action_ids": [ + "elbow_flexion" + ], + "pattern_ids": [ + "single_joint_elbow_flexion" + ], + "primary_muscle_ids": [ + "biceps_brachii", + "brachialis" + ], + "secondary_muscle_ids": [ + "brachioradialis" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors" + ], + "primary_zone_id": "arms", + "secondary_zone_ids": [], + "confidence": "high", + "evidence_type": "biomechanical_interpretation", + "source_refs": [ + "openstax_upper" + ], + "variant_notes": "Grip/forearm orientation and shoulder support alter contributions. Curl label does not mean biceps exclusively.", + "role_notes": "Qualitative plausible roles under the specified execution; not percentage force, EMG rankings or equivalent effective sets. Group and child anatomy must not be double-counted." + }, + "conditional_interpretation": null, + "existing_body_zones": { + "primary_zone_id": "arms", + "secondary_zone_ids": [] + }, + "source_refs": [ + "openstax_upper" + ], + "limitations": [ + "Manufacturer/model, detailed trajectory and resistance curve unresolved." + ], + "equipment_link_status": "recorded_occurrence_or_custom_identity", + "body_zone_audit": { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "arms", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_upper" + ] + } + } + ] +} diff --git a/catalog/joint-actions-v1.json b/catalog/joint-actions-v1.json new file mode 100644 index 0000000..7f4e4a8 --- /dev/null +++ b/catalog/joint-actions-v1.json @@ -0,0 +1,808 @@ +{ + "format": "trainlog-joint-actions-v1", + "version": 1, + "joint_actions": [ + { + "action_id": "ankle_dorsiflexion", + "definition": "Bring dorsum of foot toward shin", + "anatomical_region": "ankle", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Dorsiflexion de la cheville", + "joint_complex": "talocrural_joint", + "joint_or_complex": "talocrural_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "tibialis_anterior" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "ankle_plantarflexion", + "definition": "Point foot away from shin", + "anatomical_region": "ankle", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion plantaire de la cheville", + "joint_complex": "talocrural_joint", + "joint_or_complex": "talocrural_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "gastrocnemius", + "soleus" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "elbow_extension", + "definition": "Increase elbow angle", + "anatomical_region": "elbow", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension du coude", + "joint_complex": "humeroulnar_humeroradial_complex", + "joint_or_complex": "humeroulnar_humeroradial_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "triceps_brachii" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "elbow_flexion", + "definition": "Reduce elbow angle", + "anatomical_region": "elbow", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion du coude", + "joint_complex": "humeroulnar_humeroradial_complex", + "joint_or_complex": "humeroulnar_humeroradial_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "finger_flexion", + "definition": "Close fingers toward palm", + "anatomical_region": "finger", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion des doigts", + "joint_complex": "metacarpophalangeal_and_interphalangeal_joints", + "joint_or_complex": "metacarpophalangeal_and_interphalangeal_joints", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "forearm_flexors" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "forearm_pronation", + "definition": "Rotate forearm toward palm-back orientation", + "anatomical_region": "forearm", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Pronation de l’avant-bras", + "joint_complex": "proximal_and_distal_radioulnar_joints", + "joint_or_complex": "proximal_and_distal_radioulnar_joints", + "principal_plane": "transverse", + "plane_notes": "Axial rotation of forearm; spatial plane depends on forearm position.", + "contributing_muscle_ids": [ + "forearm_pronators" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "forearm_supination", + "definition": "Rotate forearm toward palm-forward orientation", + "anatomical_region": "forearm", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Supination de l’avant-bras", + "joint_complex": "proximal_and_distal_radioulnar_joints", + "joint_or_complex": "proximal_and_distal_radioulnar_joints", + "principal_plane": "transverse", + "plane_notes": "Axial rotation of forearm; spatial plane depends on forearm position.", + "contributing_muscle_ids": [ + "biceps_brachii", + "supinator" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_abduction", + "definition": "Move femur away from midline", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Abduction de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "frontal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "gluteus_medius", + "gluteus_minimus", + "tensor_fasciae_latae" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_adduction", + "definition": "Move femur toward midline", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Adduction de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "frontal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "adductor_magnus", + "hip_adductors" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_extension", + "definition": "Move femur posteriorly or rise from flexion", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "maeo_2021", + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "adductor_magnus", + "gluteus_maximus", + "hamstrings_biarticular" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_external_rotation", + "definition": "Rotate femur outward", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Rotation externe de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "gluteus_maximus" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_flexion", + "definition": "Bring femur toward anterior trunk", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "iliopsoas", + "rectus_femoris", + "tensor_fasciae_latae" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "hip_internal_rotation", + "definition": "Rotate femur inward", + "anatomical_region": "hip", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Rotation interne de la hanche", + "joint_complex": "acetabulofemoral_joint", + "joint_or_complex": "acetabulofemoral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "gluteus_minimus", + "tensor_fasciae_latae" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "knee_extension", + "definition": "Increase knee angle", + "anatomical_region": "knee", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension du genou", + "joint_complex": "tibiofemoral_complex", + "joint_or_complex": "tibiofemoral_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "quadriceps", + "rectus_femoris", + "vasti" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "knee_flexion", + "definition": "Reduce knee angle", + "anatomical_region": "knee", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "maeo_2021", + "openstax_actions", + "openstax_lower" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion du genou", + "joint_complex": "tibiofemoral_complex", + "joint_or_complex": "tibiofemoral_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "biceps_femoris_short_head", + "gastrocnemius", + "hamstrings", + "hamstrings_biarticular" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_depression", + "definition": "Move scapula inferiorly", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Abaissement scapulaire", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "pectoralis_minor", + "trapezius_lower" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_downward_rotation", + "definition": "Rotate glenoid downward on return", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Rotation scapulaire vers le bas", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "rhomboids" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_elevation", + "definition": "Move scapula superiorly", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Élévation scapulaire", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "trapezius_upper" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_protraction", + "definition": "Move scapula anterolaterally around thorax", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Protraction scapulaire", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "pectoralis_minor", + "serratus_anterior" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_retraction", + "definition": "Move scapula toward vertebral column", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Rétraction scapulaire", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "rhomboids", + "trapezius_middle" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "scapular_upward_rotation", + "definition": "Rotate glenoid upward during arm elevation", + "anatomical_region": "scapular", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Scapular actions are scapulothoracic descriptions produced through coordinated shoulder-girdle articulations.", + "display_name_fr": "Rotation scapulaire vers le haut", + "joint_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "joint_or_complex": "scapulothoracic_function_via_sternoclavicular_and_acromioclavicular_joints", + "principal_plane": "three_dimensional_scapular_motion", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "serratus_anterior", + "trapezius_lower", + "trapezius_upper" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_abduction", + "definition": "Move humerus away from trunk in frontal/scapular plane", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Abduction de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "frontal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_middle", + "supraspinatus" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_adduction", + "definition": "Move humerus toward trunk", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_pectoralis", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Adduction de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "frontal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "latissimus_dorsi", + "pectoralis_major", + "pectoralis_major_sternocostal", + "teres_major" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_extension", + "definition": "Move humerus posteriorly or return from flexion", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_posterior", + "latissimus_dorsi", + "pectoralis_major_sternocostal", + "teres_major" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_external_rotation", + "definition": "Rotate humerus outward about long axis", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Rotation externe de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_posterior", + "infraspinatus", + "teres_minor" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_flexion", + "definition": "Move humerus anteriorly/elevate forward relative to trunk", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_actions" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_anterior", + "pectoralis_major_clavicular" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_horizontal_abduction", + "definition": "Move elevated humerus backward in transverse plane", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "openstax_actions" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Abduction horizontale de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_posterior" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_horizontal_adduction", + "definition": "Bring elevated humerus across anterior trunk", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Adduction horizontale de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_anterior", + "pectoralis_major", + "pectoralis_major_clavicular", + "pectoralis_major_sternocostal" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "shoulder_internal_rotation", + "definition": "Rotate humerus inward about long axis", + "anatomical_region": "shoulder", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Rotation interne de l’épaule", + "joint_complex": "glenohumeral_joint", + "joint_or_complex": "glenohumeral_joint", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "deltoid_anterior", + "latissimus_dorsi", + "pectoralis_major", + "subscapularis", + "teres_major" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "trunk_extension", + "definition": "Extend vertebral column; not synonymous with hip extension", + "anatomical_region": "trunk", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_back" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension du tronc", + "joint_complex": "intervertebral_column_complex", + "joint_or_complex": "intervertebral_column_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "erector_spinae", + "multifidus" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "trunk_flexion", + "definition": "Flex vertebral column; not synonymous with hip flexion", + "anatomical_region": "trunk", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_trunk" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion du tronc", + "joint_complex": "intervertebral_column_complex", + "joint_or_complex": "intervertebral_column_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "external_oblique", + "internal_oblique", + "rectus_abdominis" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "trunk_lateral_flexion", + "definition": "Bend vertebral column sideways", + "anatomical_region": "trunk", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_back", + "openstax_trunk" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Inclinaison latérale du tronc", + "joint_complex": "intervertebral_column_complex", + "joint_or_complex": "intervertebral_column_complex", + "principal_plane": "frontal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "erector_spinae", + "external_oblique", + "internal_oblique", + "quadratus_lumborum" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "trunk_rotation", + "definition": "Rotate thorax and pelvis relative to each other", + "anatomical_region": "trunk", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_back", + "openstax_trunk" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Rotation du tronc", + "joint_complex": "intervertebral_column_complex", + "joint_or_complex": "intervertebral_column_complex", + "principal_plane": "transverse", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "external_oblique", + "internal_oblique", + "multifidus" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "wrist_extension", + "definition": "Bend dorsum of hand toward posterior forearm", + "anatomical_region": "wrist", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Extension du poignet", + "joint_complex": "radiocarpal_midcarpal_complex", + "joint_or_complex": "radiocarpal_midcarpal_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "forearm_extensors" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + }, + { + "action_id": "wrist_flexion", + "definition": "Bend palm toward anterior forearm", + "anatomical_region": "wrist", + "evidence_type": "established_anatomy", + "confidence": "high", + "source_refs": [ + "openstax_actions", + "openstax_upper" + ], + "notes": "Opposite motion in a loaded return may be controlled eccentrically by the same agonists.", + "display_name_fr": "Flexion du poignet", + "joint_complex": "radiocarpal_midcarpal_complex", + "joint_or_complex": "radiocarpal_midcarpal_complex", + "principal_plane": "sagittal", + "plane_notes": "Nominal anatomical plane; actual exercise can combine planes. Scapular kinematics are three-dimensional.", + "contributing_muscle_ids": [ + "forearm_flexors" + ], + "contributor_semantics": "Non-exhaustive anatomical capability list; not a primary-role list or inverse EMG ranking. Aggregate/member overlap retained explicitly." + } + ] +} diff --git a/catalog/movement-patterns-v1.json b/catalog/movement-patterns-v1.json new file mode 100644 index 0000000..5fb2e48 --- /dev/null +++ b/catalog/movement-patterns-v1.json @@ -0,0 +1,538 @@ +{ + "format": "trainlog-movement-patterns-v1", + "version": 1, + "movement_patterns": [ + { + "pattern_id": "cyclic_lower_limb", + "definition": "Repeated lower-limb propulsion such as cycling; not identical to gait.", + "typical_action_ids": [ + "hip_extension", + "hip_flexion", + "knee_extension", + "knee_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Propulsion cyclique des membres inférieurs", + "typical_body_zone_ids": [ + "calves", + "glutes", + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "cyclic_rowing", + "definition": "Repeated leg drive, trunk control and arm pull; cardio rower differs from seated resistance row.", + "typical_action_ids": [ + "elbow_flexion", + "hip_extension", + "knee_extension", + "shoulder_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Rame cyclique", + "typical_body_zone_ids": [ + "arms", + "back", + "core", + "glutes", + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "hip_dominant", + "definition": "Multi-joint task organized around hip extension with comparatively restrained knee excursion.", + "typical_action_ids": [ + "hip_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Mouvement à dominante hanche", + "typical_body_zone_ids": [ + "glutes", + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "horizontal_pull", + "definition": "Pull toward torso with shoulder extension/horizontal abduction and elbow flexion.", + "typical_action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Tirage horizontal", + "typical_body_zone_ids": [ + "arms", + "back" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "horizontal_push", + "definition": "Press resistance away anterior to torso with coordinated shoulder and elbow movement.", + "typical_action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Poussée horizontale", + "typical_body_zone_ids": [ + "arms", + "chest", + "shoulders" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "knee_dominant", + "definition": "Multi-joint lower-limb task with substantial knee-extension demand; hip extension remains involved.", + "typical_action_ids": [ + "hip_extension", + "knee_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Mouvement à dominante genou", + "typical_body_zone_ids": [ + "glutes", + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "locomotion", + "definition": "Repeated gait cycles with support and progression; treadmill belt can replace overground translation.", + "typical_action_ids": [ + "ankle_dorsiflexion", + "ankle_plantarflexion", + "hip_extension", + "hip_flexion", + "knee_extension", + "knee_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Locomotion", + "typical_body_zone_ids": [ + "calves", + "glutes", + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "shoulder_abduction", + "display_name_fr": "Abduction de l’épaule", + "definition": "Raise humerus laterally or in scapular plane against resistance; lateral-raise family. Scapular upward rotation accompanies larger elevation.", + "typical_action_ids": [ + "scapular_upward_rotation", + "shoulder_abduction" + ], + "typical_body_zone_ids": [ + "shoulders" + ], + "parent_pattern_id": null, + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "nih_deltoid", + "openstax_actions", + "openstax_upper" + ], + "notes": "Explicit authored task convention; anti-motion is a demand, not a new joint action or claim of muscle isolation." + }, + { + "pattern_id": "single_joint_ankle_plantarflexion", + "definition": "Predominant resisted ankle plantarflexion.", + "typical_action_ids": [ + "ankle_plantarflexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Flexion plantaire", + "typical_body_zone_ids": [ + "calves" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_elbow_extension", + "definition": "Predominant resisted elbow extension.", + "typical_action_ids": [ + "elbow_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Extension isolée du coude", + "typical_body_zone_ids": [ + "arms" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_elbow_flexion", + "definition": "Predominant resisted elbow flexion.", + "typical_action_ids": [ + "elbow_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Flexion isolée du coude", + "typical_body_zone_ids": [ + "arms" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_hip_abduction", + "definition": "Predominant resisted hip abduction.", + "typical_action_ids": [ + "hip_abduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Abduction isolée de hanche", + "typical_body_zone_ids": [ + "glutes" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_hip_adduction", + "definition": "Predominant resisted hip adduction.", + "typical_action_ids": [ + "hip_adduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Adduction isolée de hanche", + "typical_body_zone_ids": [ + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_knee_extension", + "definition": "Predominant resisted knee extension.", + "typical_action_ids": [ + "knee_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Extension isolée du genou", + "typical_body_zone_ids": [ + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_knee_flexion", + "definition": "Predominant resisted knee flexion.", + "typical_action_ids": [ + "knee_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Flexion isolée du genou", + "typical_body_zone_ids": [ + "thighs" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_shoulder_horizontal_abduction", + "definition": "Reverse-fly humeral horizontal abduction with approximately fixed elbow.", + "typical_action_ids": [ + "shoulder_horizontal_abduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Écarté inversé", + "typical_body_zone_ids": [ + "back", + "shoulders" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "single_joint_shoulder_horizontal_adduction", + "definition": "Fly-like humeral adduction across torso with approximately fixed elbow.", + "typical_action_ids": [ + "shoulder_horizontal_adduction" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Écarté pectoral", + "typical_body_zone_ids": [ + "chest", + "shoulders" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "trunk_anti_extension", + "display_name_fr": "Résistance à l’extension du tronc", + "definition": "Resist an external trunk-extension moment while maintaining intended spinal orientation. No dynamic joint action is required.", + "typical_action_ids": [], + "typical_body_zone_ids": [ + "core" + ], + "parent_pattern_id": "trunk_stabilization", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_back" + ], + "notes": "Explicit authored task convention; anti-motion is a demand, not a new joint action or claim of muscle isolation." + }, + { + "pattern_id": "trunk_anti_rotation", + "display_name_fr": "Résistance à la rotation du tronc", + "definition": "Resist an external rotational moment between thorax and pelvis while maintaining intended orientation.", + "typical_action_ids": [], + "typical_body_zone_ids": [ + "core" + ], + "parent_pattern_id": "trunk_stabilization", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_back" + ], + "notes": "Explicit authored task convention; anti-motion is a demand, not a new joint action or claim of muscle isolation." + }, + { + "pattern_id": "trunk_extension", + "definition": "Dynamic resisted spinal extension.", + "typical_action_ids": [ + "trunk_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Extension du tronc", + "typical_body_zone_ids": [ + "back", + "core" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "trunk_flexion", + "definition": "Dynamic resisted spinal flexion.", + "typical_action_ids": [ + "trunk_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Flexion du tronc", + "typical_body_zone_ids": [ + "core" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "trunk_lateral_stability", + "display_name_fr": "Stabilité latérale du tronc", + "definition": "Resist an external lateral-flexion moment while maintaining trunk orientation; not dynamic side bending.", + "typical_action_ids": [], + "typical_body_zone_ids": [ + "back", + "core" + ], + "parent_pattern_id": "trunk_stabilization", + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "nih_abdominal_wall", + "openstax_actions", + "openstax_back" + ], + "notes": "Explicit authored task convention; anti-motion is a demand, not a new joint action or claim of muscle isolation." + }, + { + "pattern_id": "trunk_rotation", + "definition": "Dynamic resisted thorax-pelvis rotation.", + "typical_action_ids": [ + "trunk_rotation" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Rotation du tronc", + "typical_body_zone_ids": [ + "core" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "trunk_stabilization", + "definition": "Maintain trunk orientation against external moments; anti-extension, anti-rotation and anti-lateral-flexion are demands, not anatomical joint motions.", + "typical_action_ids": [], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Stabilisation du tronc", + "typical_body_zone_ids": [ + "back", + "core" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "vertical_pull", + "definition": "Pull from overhead toward torso.", + "typical_action_ids": [ + "elbow_flexion", + "shoulder_adduction", + "shoulder_extension" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Tirage vertical", + "typical_body_zone_ids": [ + "arms", + "back" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + }, + { + "pattern_id": "vertical_push", + "definition": "Press overhead relative to torso with humeral elevation and elbow extension.", + "typical_action_ids": [ + "elbow_extension", + "scapular_upward_rotation", + "shoulder_abduction", + "shoulder_flexion" + ], + "evidence_type": "practical_inference", + "confidence": "moderate", + "source_refs": [ + "acsm_2009", + "openstax_actions" + ], + "notes": "Authored classification convention; typical actions are not mandatory in every variant and single-joint does not mean one muscle or zero stabilization.", + "display_name_fr": "Poussée verticale", + "typical_body_zone_ids": [ + "arms", + "shoulders" + ], + "body_zone_semantics": "Descriptive common participation, not a persisted exercise mapping or requirement that every listed zone be assigned." + } + ] +} diff --git a/catalog/muscles-v1.json b/catalog/muscles-v1.json new file mode 100644 index 0000000..b8cf861 --- /dev/null +++ b/catalog/muscles-v1.json @@ -0,0 +1,1509 @@ +{ + "format": "trainlog-muscles-v1", + "version": 1, + "muscles": [ + { + "muscle_id": "adductor_magnus", + "display_name": "Adductor magnus", + "entity_type": "muscle", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_adduction", + "hip_extension" + ], + "functional_notes": "Hip extension especially from posterior/hamstring portion; not all adductors share this role.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Grand adducteur", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_adduction", + "hip_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "biceps_brachii", + "display_name": "Biceps brachii", + "entity_type": "muscle", + "body_zone_id": "arms", + "joint_action_ids": [ + "elbow_flexion", + "forearm_supination" + ], + "functional_notes": "Both heads span shoulder; shoulder assistance depends on position.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Biceps brachial", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "elbow_flexion", + "forearm_supination" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "biceps_femoris_short_head", + "display_name": "Biceps femoris: short head", + "entity_type": "muscle_region", + "body_zone_id": "thighs", + "joint_action_ids": [ + "knee_flexion" + ], + "functional_notes": "Does not cross hip.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "maeo_2021" + ], + "display_name_fr": "Biceps fémoral : chef court", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": "hamstrings", + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "knee_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "brachialis", + "display_name": "Brachialis", + "entity_type": "muscle", + "body_zone_id": "arms", + "joint_action_ids": [ + "elbow_flexion" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Brachial", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "elbow_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "brachioradialis", + "display_name": "Brachioradialis", + "entity_type": "muscle", + "body_zone_id": "arms", + "joint_action_ids": [ + "elbow_flexion" + ], + "functional_notes": "Forearm muscle assigned to broad arms UX zone.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Brachioradial", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "elbow_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "deltoid_anterior", + "display_name": "Deltoid: anterior region", + "entity_type": "muscle_region", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_flexion", + "shoulder_horizontal_adduction", + "shoulder_internal_rotation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_deltoid" + ], + "display_name_fr": "Deltoïde antérieur", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_flexion", + "shoulder_horizontal_adduction", + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "deltoid_middle", + "display_name": "Deltoid: middle region", + "entity_type": "muscle_region", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_abduction" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_deltoid" + ], + "display_name_fr": "Deltoïde moyen", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_abduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "deltoid_posterior", + "display_name": "Deltoid: posterior region", + "entity_type": "muscle_region", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_extension", + "shoulder_external_rotation", + "shoulder_horizontal_abduction" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_deltoid" + ], + "display_name_fr": "Deltoïde postérieur", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_extension", + "shoulder_external_rotation", + "shoulder_horizontal_abduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "erector_spinae", + "display_name": "Erector spinae", + "entity_type": "muscle_group", + "body_zone_id": "back", + "joint_action_ids": [ + "trunk_extension", + "trunk_lateral_flexion" + ], + "functional_notes": "Posterior trunk anatomy also functionally belongs to core.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_back" + ], + "display_name_fr": "Érecteurs du rachis", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back", + "core" + ], + "primary_actions": [ + "trunk_extension", + "trunk_lateral_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "external_oblique", + "display_name": "External oblique", + "entity_type": "muscle", + "body_zone_id": "core", + "joint_action_ids": [ + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "functional_notes": "Unilateral contralateral rotation; stabilization is task dependent.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "display_name_fr": "Oblique externe", + "anatomical_group": "trunk_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "core" + ], + "primary_actions": [ + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "forearm_extensors", + "display_name": "Wrist and finger extensors", + "entity_type": "muscle_group", + "body_zone_id": "arms", + "joint_action_ids": [ + "wrist_extension" + ], + "functional_notes": "Maintain wrist orientation during gripping.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Extenseurs du poignet et des doigts", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "wrist_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "forearm_flexors", + "display_name": "Wrist and finger flexors", + "entity_type": "muscle_group", + "body_zone_id": "arms", + "joint_action_ids": [ + "finger_flexion", + "wrist_flexion" + ], + "functional_notes": "Grip participation is usually stabilizing in pulls.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Fléchisseurs du poignet et des doigts", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "finger_flexion", + "wrist_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "forearm_pronators", + "display_name": "Pronator teres and quadratus", + "entity_type": "muscle_group", + "body_zone_id": "arms", + "joint_action_ids": [ + "forearm_pronation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Pronateurs de l’avant-bras", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "forearm_pronation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "gastrocnemius", + "display_name": "Gastrocnemius", + "entity_type": "muscle", + "body_zone_id": "calves", + "joint_action_ids": [ + "ankle_plantarflexion", + "knee_flexion" + ], + "functional_notes": "Biarticular; force capacity depends on knee and ankle position.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Gastrocnémien", + "anatomical_group": "lower_leg", + "aggregate_group_id": null, + "body_zone_ids": [ + "calves" + ], + "primary_actions": [ + "ankle_plantarflexion", + "knee_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "gluteus_maximus", + "display_name": "Gluteus maximus", + "entity_type": "muscle", + "body_zone_id": "glutes", + "joint_action_ids": [ + "hip_extension", + "hip_external_rotation" + ], + "functional_notes": "Superior fibers can assist abduction; geometry-dependent.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Grand fessier", + "anatomical_group": "gluteal_region", + "aggregate_group_id": null, + "body_zone_ids": [ + "glutes" + ], + "primary_actions": [ + "hip_extension", + "hip_external_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "gluteus_medius", + "display_name": "Gluteus medius", + "entity_type": "muscle", + "body_zone_id": "glutes", + "joint_action_ids": [ + "hip_abduction" + ], + "functional_notes": "Pelvic stabilization; rotation contribution depends on fibers and hip angle.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "display_name_fr": "Moyen fessier", + "anatomical_group": "gluteal_region", + "aggregate_group_id": null, + "body_zone_ids": [ + "glutes" + ], + "primary_actions": [ + "hip_abduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "gluteus_minimus", + "display_name": "Gluteus minimus", + "entity_type": "muscle", + "body_zone_id": "glutes", + "joint_action_ids": [ + "hip_abduction", + "hip_internal_rotation" + ], + "functional_notes": "Hip/pelvic stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_gluteus_minimus" + ], + "display_name_fr": "Petit fessier", + "anatomical_group": "gluteal_region", + "aggregate_group_id": null, + "body_zone_ids": [ + "glutes" + ], + "primary_actions": [ + "hip_abduction", + "hip_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "hamstrings", + "display_name": "Hamstring knee-flexor group", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "knee_flexion" + ], + "functional_notes": "Biceps femoris long/short heads, semitendinosus and semimembranosus; short head does not extend hip.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "display_name_fr": "Groupe des fléchisseurs ischio-jambiers", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "knee_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "hamstrings_biarticular", + "display_name": "Biarticular hamstrings", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_extension", + "knee_flexion" + ], + "functional_notes": "Biceps femoris long head, semitendinosus and semimembranosus; hip flexion lengthens these at a matched knee angle.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "display_name_fr": "Ischio-jambiers biarticulaires", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": "hamstrings", + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_extension", + "knee_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "hip_adductors", + "display_name": "Adductor longus, brevis, magnus, gracilis and pectineus", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_adduction" + ], + "functional_notes": "Hip flexion/extension roles differ by muscle and joint angle; gracilis also flexes knee.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Adducteurs de la hanche", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_adduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "hip_flexor_group", + "display_name": "Hip flexor functional group", + "display_name_fr": "Fléchisseurs de la hanche", + "entity_type": "muscle_group", + "anatomical_group": "functional_group", + "aggregate_group_id": null, + "member_muscle_ids": [ + "iliopsoas", + "rectus_femoris", + "tensor_fasciae_latae" + ], + "body_zone_id": "thighs", + "body_zone_ids": [ + "thighs" + ], + "joint_action_ids": [ + "hip_abduction", + "hip_flexion", + "hip_internal_rotation", + "knee_extension" + ], + "primary_actions": [ + "hip_abduction", + "hip_flexion", + "hip_internal_rotation", + "knee_extension" + ], + "primary_actions_semantics": "Union of common member capabilities, not an action of every member; movement/stabilization roles require exercise context.", + "functional_notes": "Non-exhaustive functional grouping: pectineus, sartorius and other contributors are not separately represented here. Aggregate/member entries overlap and must never be counted as independent exposure.", + "confidence": "moderate", + "evidence_type": "practical_inference", + "source_refs": [ + "openstax_lower" + ], + "overlap_warning": "Do not sum group and members as separate muscles or training sets." + }, + { + "muscle_id": "iliopsoas", + "display_name": "Psoas major and iliacus", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_flexion" + ], + "functional_notes": "Functional hip-flexor grouping spans trunk and pelvis; thighs is a coarse projection.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Ilio-psoas", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "infraspinatus", + "display_name": "Infraspinatus", + "entity_type": "muscle", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_external_rotation" + ], + "functional_notes": "Rotator cuff; contributes humeral-head stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Infra-épineux", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": "rotator_cuff", + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_external_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "internal_oblique", + "display_name": "Internal oblique", + "entity_type": "muscle", + "body_zone_id": "core", + "joint_action_ids": [ + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "functional_notes": "Unilateral ipsilateral rotation.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "display_name_fr": "Oblique interne", + "anatomical_group": "trunk_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "core" + ], + "primary_actions": [ + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "latissimus_dorsi", + "display_name": "Latissimus dorsi", + "entity_type": "muscle", + "body_zone_id": "back", + "joint_action_ids": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_internal_rotation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Grand dorsal", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "multifidus", + "display_name": "Multifidus", + "entity_type": "muscle", + "body_zone_id": "back", + "joint_action_ids": [ + "trunk_extension", + "trunk_rotation" + ], + "functional_notes": "Segmental spinal control; rotation is contribution, not isolated prime-mover status.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_back" + ], + "display_name_fr": "Multifide", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back", + "core" + ], + "primary_actions": [ + "trunk_extension", + "trunk_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "pectoralis_major", + "display_name": "Pectoralis major", + "entity_type": "muscle", + "body_zone_id": "chest", + "joint_action_ids": [ + "shoulder_adduction", + "shoulder_horizontal_adduction", + "shoulder_internal_rotation" + ], + "functional_notes": "Regional participation depends on shoulder position.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_pectoralis", + "openstax_upper" + ], + "display_name_fr": "Grand pectoral", + "anatomical_group": "pectoral_region", + "aggregate_group_id": null, + "body_zone_ids": [ + "chest" + ], + "primary_actions": [ + "shoulder_adduction", + "shoulder_horizontal_adduction", + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "pectoralis_major_clavicular", + "display_name": "Pectoralis major: clavicular region", + "entity_type": "muscle_region", + "body_zone_id": "chest", + "joint_action_ids": [ + "shoulder_flexion", + "shoulder_horizontal_adduction" + ], + "functional_notes": "Upper-chest region; not a separately isolatable muscle.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_pectoralis" + ], + "display_name_fr": "Grand pectoral : portion claviculaire", + "anatomical_group": "pectoral_region", + "aggregate_group_id": "pectoralis_major", + "body_zone_ids": [ + "chest" + ], + "primary_actions": [ + "shoulder_flexion", + "shoulder_horizontal_adduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "pectoralis_major_sternocostal", + "display_name": "Pectoralis major: sternocostal region", + "entity_type": "muscle_region", + "body_zone_id": "chest", + "joint_action_ids": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_horizontal_adduction" + ], + "functional_notes": "Extension contribution from a flexed arm; lower fibers are regional tissue, not a separate lower-chest muscle.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_pectoralis" + ], + "display_name_fr": "Grand pectoral : portion sternocostale", + "anatomical_group": "pectoral_region", + "aggregate_group_id": "pectoralis_major", + "body_zone_ids": [ + "chest" + ], + "primary_actions": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_horizontal_adduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "pectoralis_minor", + "display_name": "Pectoralis minor", + "entity_type": "muscle", + "body_zone_id": "chest", + "joint_action_ids": [ + "scapular_depression", + "scapular_protraction" + ], + "functional_notes": "Scapular rather than humeral action.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Petit pectoral", + "anatomical_group": "pectoral_region", + "aggregate_group_id": null, + "body_zone_ids": [ + "chest" + ], + "primary_actions": [ + "scapular_depression", + "scapular_protraction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "quadratus_lumborum", + "display_name": "Quadratus lumborum", + "entity_type": "muscle", + "body_zone_id": "core", + "joint_action_ids": [ + "trunk_lateral_flexion" + ], + "functional_notes": "Pelvic/trunk stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_back" + ], + "display_name_fr": "Carré des lombes", + "anatomical_group": "trunk_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "core" + ], + "primary_actions": [ + "trunk_lateral_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "quadriceps", + "display_name": "Quadriceps femoris", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "knee_extension" + ], + "functional_notes": "Rectus femoris plus vastus lateralis, medialis and intermedius.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Quadriceps fémoral", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "knee_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "rectus_abdominis", + "display_name": "Rectus abdominis", + "entity_type": "muscle", + "body_zone_id": "core", + "joint_action_ids": [ + "trunk_flexion" + ], + "functional_notes": "Resists extension in an appropriate stabilization task.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "display_name_fr": "Droit de l’abdomen", + "anatomical_group": "trunk_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "core" + ], + "primary_actions": [ + "trunk_flexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "rectus_femoris", + "display_name": "Rectus femoris", + "entity_type": "muscle", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_flexion", + "knee_extension" + ], + "functional_notes": "Biarticular; differs from the three vasti.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Droit fémoral", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": "quadriceps", + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_flexion", + "knee_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "rhomboids", + "display_name": "Rhomboid major and minor", + "entity_type": "muscle_group", + "body_zone_id": "back", + "joint_action_ids": [ + "scapular_downward_rotation", + "scapular_retraction" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Rhomboïdes", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "scapular_downward_rotation", + "scapular_retraction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "rotator_cuff", + "display_name": "Rotator cuff", + "display_name_fr": "Coiffe des rotateurs", + "entity_type": "muscle_group", + "anatomical_group": "anatomical_muscle_group", + "aggregate_group_id": null, + "member_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "body_zone_id": "shoulders", + "body_zone_ids": [ + "shoulders" + ], + "joint_action_ids": [ + "shoulder_abduction", + "shoulder_external_rotation", + "shoulder_internal_rotation" + ], + "primary_actions": [ + "shoulder_abduction", + "shoulder_external_rotation", + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Union of common member capabilities, not an action of every member; movement/stabilization roles require exercise context.", + "functional_notes": "Four muscles form this group; joint stabilization is shared, rotation actions differ. Aggregate/member entries overlap and must never be counted as independent exposure.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "overlap_warning": "Do not sum group and members as separate muscles or training sets." + }, + { + "muscle_id": "serratus_anterior", + "display_name": "Serratus anterior", + "entity_type": "muscle", + "body_zone_id": "back", + "joint_action_ids": [ + "scapular_protraction", + "scapular_upward_rotation" + ], + "functional_notes": "Scapular stabilizer on lateral thorax; zone is a functional UX projection, not location proof.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Dentelé antérieur", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "scapular_protraction", + "scapular_upward_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "soleus", + "display_name": "Soleus", + "entity_type": "muscle", + "body_zone_id": "calves", + "joint_action_ids": [ + "ankle_plantarflexion" + ], + "functional_notes": "Does not cross knee; distinguish from gastrocnemius.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Soléaire", + "anatomical_group": "lower_leg", + "aggregate_group_id": null, + "body_zone_ids": [ + "calves" + ], + "primary_actions": [ + "ankle_plantarflexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "spinal_stabilizers", + "display_name": "Trunk and spinal stabilizer functional group", + "display_name_fr": "Stabilisateurs du tronc et du rachis", + "entity_type": "muscle_group", + "anatomical_group": "functional_group", + "aggregate_group_id": null, + "member_muscle_ids": [ + "erector_spinae", + "external_oblique", + "internal_oblique", + "multifidus", + "quadratus_lumborum", + "rectus_abdominis", + "transversus_abdominis" + ], + "body_zone_id": "back", + "body_zone_ids": [ + "back", + "core" + ], + "joint_action_ids": [ + "trunk_extension", + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "primary_actions": [ + "trunk_extension", + "trunk_flexion", + "trunk_lateral_flexion", + "trunk_rotation" + ], + "primary_actions_semantics": "Union of common member capabilities, not an action of every member; movement/stabilization roles require exercise context.", + "functional_notes": "Non-exhaustive task-dependent function; not a distinct anatomical muscle or a claim that all members act equally/simultaneously. Aggregate/member entries overlap and must never be counted as independent exposure.", + "confidence": "moderate", + "evidence_type": "practical_inference", + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "overlap_warning": "Do not sum group and members as separate muscles or training sets." + }, + { + "muscle_id": "subscapularis", + "display_name": "Subscapularis", + "entity_type": "muscle", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_internal_rotation" + ], + "functional_notes": "Rotator cuff; contributes humeral-head stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Subscapulaire", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": "rotator_cuff", + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "supinator", + "display_name": "Supinator", + "entity_type": "muscle", + "body_zone_id": "arms", + "joint_action_ids": [ + "forearm_supination" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Supinateur", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "forearm_supination" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "supraspinatus", + "display_name": "Supraspinatus", + "entity_type": "muscle", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_abduction" + ], + "functional_notes": "Rotator cuff; contributes humeral-head stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Supra-épineux", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": "rotator_cuff", + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_abduction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "tensor_fasciae_latae", + "display_name": "Tensor fasciae latae", + "entity_type": "muscle", + "body_zone_id": "thighs", + "joint_action_ids": [ + "hip_abduction", + "hip_flexion", + "hip_internal_rotation" + ], + "functional_notes": "Abduction machine is not exclusive gluteal isolation.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "display_name_fr": "Tenseur du fascia lata", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": null, + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "hip_abduction", + "hip_flexion", + "hip_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "teres_major", + "display_name": "Teres major", + "entity_type": "muscle", + "body_zone_id": "back", + "joint_action_ids": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_internal_rotation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Grand rond", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": null, + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "shoulder_adduction", + "shoulder_extension", + "shoulder_internal_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "teres_minor", + "display_name": "Teres minor", + "entity_type": "muscle", + "body_zone_id": "shoulders", + "joint_action_ids": [ + "shoulder_external_rotation" + ], + "functional_notes": "Rotator cuff; contributes humeral-head stabilization.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Petit rond", + "anatomical_group": "shoulder_musculature", + "aggregate_group_id": "rotator_cuff", + "body_zone_ids": [ + "shoulders" + ], + "primary_actions": [ + "shoulder_external_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "tibialis_anterior", + "display_name": "Tibialis anterior", + "entity_type": "muscle", + "body_zone_id": "calves", + "joint_action_ids": [ + "ankle_dorsiflexion" + ], + "functional_notes": "Anterior lower leg included in coarse calves zone.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Tibial antérieur", + "anatomical_group": "lower_leg", + "aggregate_group_id": null, + "body_zone_ids": [ + "calves" + ], + "primary_actions": [ + "ankle_dorsiflexion" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "transversus_abdominis", + "display_name": "Transversus abdominis", + "entity_type": "muscle", + "body_zone_id": "core", + "joint_action_ids": [], + "functional_notes": "Abdominal compression and trunk control; do not label primary dynamic trunk flexor.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "nih_abdominal_wall" + ], + "display_name_fr": "Transverse de l’abdomen", + "anatomical_group": "trunk_musculature", + "aggregate_group_id": null, + "body_zone_ids": [ + "core" + ], + "primary_actions": [], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "trapezius", + "display_name": "Trapezius", + "display_name_fr": "Trapèze", + "entity_type": "muscle_group", + "anatomical_group": "anatomical_muscle_with_regions", + "aggregate_group_id": null, + "member_muscle_ids": [ + "trapezius_lower", + "trapezius_middle", + "trapezius_upper" + ], + "body_zone_id": "back", + "body_zone_ids": [ + "back" + ], + "joint_action_ids": [ + "scapular_depression", + "scapular_elevation", + "scapular_retraction", + "scapular_upward_rotation" + ], + "primary_actions": [ + "scapular_depression", + "scapular_elevation", + "scapular_retraction", + "scapular_upward_rotation" + ], + "primary_actions_semantics": "Union of common member capabilities, not an action of every member; movement/stabilization roles require exercise context.", + "functional_notes": "Complete regional subdivision at this catalog granularity. Aggregate/member entries overlap and must never be counted as independent exposure.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "overlap_warning": "Do not sum group and members as separate muscles or training sets." + }, + { + "muscle_id": "trapezius_lower", + "display_name": "Trapezius: lower region", + "entity_type": "muscle_region", + "body_zone_id": "back", + "joint_action_ids": [ + "scapular_depression", + "scapular_upward_rotation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Trapèze : portion inférieure", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": "trapezius", + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "scapular_depression", + "scapular_upward_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "trapezius_middle", + "display_name": "Trapezius: middle region", + "entity_type": "muscle_region", + "body_zone_id": "back", + "joint_action_ids": [ + "scapular_retraction" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Trapèze : portion moyenne", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": "trapezius", + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "scapular_retraction" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "trapezius_upper", + "display_name": "Trapezius: upper region", + "entity_type": "muscle_region", + "body_zone_id": "back", + "joint_action_ids": [ + "scapular_elevation", + "scapular_upward_rotation" + ], + "functional_notes": "", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Trapèze : portion supérieure", + "anatomical_group": "posterior_trunk_and_scapular_system", + "aggregate_group_id": "trapezius", + "body_zone_ids": [ + "back" + ], + "primary_actions": [ + "scapular_elevation", + "scapular_upward_rotation" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "triceps_brachii", + "display_name": "Triceps brachii", + "entity_type": "muscle", + "body_zone_id": "arms", + "joint_action_ids": [ + "elbow_extension" + ], + "functional_notes": "Long head also crosses the shoulder; regional loading depends on position.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_upper" + ], + "display_name_fr": "Triceps brachial", + "anatomical_group": "arm_and_forearm", + "aggregate_group_id": null, + "body_zone_ids": [ + "arms" + ], + "primary_actions": [ + "elbow_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + }, + { + "muscle_id": "vasti", + "display_name": "Vastus lateralis, medialis and intermedius", + "entity_type": "muscle_group", + "body_zone_id": "thighs", + "joint_action_ids": [ + "knee_extension" + ], + "functional_notes": "Do not infer selective vastus isolation from foot angles.", + "confidence": "high", + "evidence_type": "established_anatomy", + "source_refs": [ + "openstax_lower" + ], + "display_name_fr": "Vastes du quadriceps", + "anatomical_group": "hip_and_thigh", + "aggregate_group_id": "quadriceps", + "body_zone_ids": [ + "thighs" + ], + "primary_actions": [ + "knee_extension" + ], + "primary_actions_semantics": "Common functional capabilities from anatomy, non-exhaustive and position dependent; this field does not assign primary exercise status.", + "overlap_warning": "Region/group and parent/member entries can overlap; do not double-count exposure." + } + ] +} diff --git a/catalog/science-references-v1.json b/catalog/science-references-v1.json new file mode 100644 index 0000000..749c751 --- /dev/null +++ b/catalog/science-references-v1.json @@ -0,0 +1,407 @@ +{ + "format": "trainlog-science-references-v1", + "version": 1, + "references": [ + { + "ref_id": "acsm_2009", + "title": "American College of Sports Medicine position stand. Progression models in resistance training for healthy adults.", + "year": 2009, + "url": "https://pubmed.ncbi.nlm.nih.gov/19204579/", + "doi": "10.1249/mss.0b013e3181915670", + "pmid": "19204579", + "topics": [ + "progression", + "specificity", + "programming" + ], + "notes": "Progressive overload and specificity organize resistance-training progression; adjustment should follow achieved performance and goals.", + "limitations": "Historical professional position stand; contemporary synthesis above takes precedence for comparative outcomes. No numeric recommendations adopted.", + "accessed_on": "2026-09-09", + "authors_or_organization": "American College of Sports Medicine.", + "type": "practical_inference" + }, + { + "ref_id": "currier_2023", + "title": "Resistance training prescription for muscle strength and hypertrophy in healthy adults: a systematic review and Bayesian network meta-analysis.", + "year": 2023, + "url": "https://pubmed.ncbi.nlm.nih.gov/37414459/", + "doi": "10.1136/bjsports-2023-106807", + "pmid": "37414459", + "topics": [ + "programming", + "load", + "sets", + "frequency" + ], + "notes": "Across reviewed adult trials, resistance training improved strength and hypertrophy; heavier loads ranked better for strength, and multiple sets characterized higher-ranked hypertrophy programs.", + "limitations": "Network rankings are not individual prescriptions or proof of one universally optimal combination.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Currier BS, Mcleod JC, Banfield L, Beyene J, Welton NJ, D'Souza AC, Keogh JAJ, Lin L, Coletta G, Yang A, Colenso-Semple L, Lau KJ, Verboom A, Phillips SM.", + "type": "intervention_evidence" + }, + { + "ref_id": "franke_2015", + "title": "Analysis of anterior, middle and posterior deltoid activation during single and multijoint exercises.", + "year": 2015, + "url": "https://pubmed.ncbi.nlm.nih.gov/24947920/", + "doi": null, + "pmid": "24947920", + "topics": [ + "rear_delt", + "rows" + ], + "notes": "Reverse pec-deck elicited greater posterior-deltoid surface EMG than the studied row/pulldown tasks.", + "limitations": "Twelve trained men; selected exercises/techniques only; no longitudinal hypertrophy comparison; no DOI reported in indexed record.", + "accessed_on": "2026-09-09", + "publication_note": "Print 2015; online indexed June 2014.", + "authors_or_organization": "Franke Rde A, Botton CE, Rodrigues R, Pinto RS, Lima CS.", + "type": "emg_evidence" + }, + { + "ref_id": "grgic_2022", + "title": "Effects of resistance training performed to repetition failure or non-failure on muscular strength and hypertrophy: A systematic review and meta-analysis.", + "year": 2022, + "url": "https://pubmed.ncbi.nlm.nih.gov/33497853/", + "doi": "10.1016/j.jshs.2021.01.007", + "pmid": "33497853", + "topics": [ + "failure", + "strength", + "hypertrophy" + ], + "notes": "Failure was not generally necessary for strength or hypertrophy gains in included comparisons.", + "limitations": "Nonfailure effort and volume differ between studies; this does not imply arbitrary easy sets equal hard sets.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Grgic J, Schoenfeld BJ, Orazem J, Sabol F.", + "type": "intervention_evidence" + }, + { + "ref_id": "kassiano_2022", + "title": "Does Varying Resistance Exercises Promote Superior Muscle Hypertrophy and Strength Gains? A Systematic Review.", + "year": 2022, + "url": "https://pubmed.ncbi.nlm.nih.gov/35438660/", + "doi": "10.1519/jsc.0000000000004258", + "pmid": "35438660", + "topics": [ + "selection", + "variation" + ], + "notes": "Systematic exercise variation may distribute regional stimulus and support task-specific strength; excessive random variation lacks support.", + "limitations": "Eight studies, all young men; limited generalizability and no universal rotation schedule.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Kassiano W, Nunes JP, Costa B, Ribeiro AS, Schoenfeld BJ, Cyrino ES.", + "type": "intervention_evidence" + }, + { + "ref_id": "lee_2015", + "title": "Enhanced muscle activity during lumbar extension exercise with pelvic stabilization.", + "year": 2015, + "url": "https://pubmed.ncbi.nlm.nih.gov/26730390/", + "doi": "10.12965/jer.150249", + "pmid": "26730390", + "topics": [ + "back_extension", + "pelvic_stabilization" + ], + "notes": "Pelvic stabilization altered lumbar-extensor excitation during extension testing.", + "limitations": "Acute small study; equipment restraint and execution matter; no claim of clinical benefit or universal isolation.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Lee HS.", + "type": "emg_evidence" + }, + { + "ref_id": "lehman_2004", + "title": "Variations in muscle activation levels during traditional latissimus dorsi weight training exercises: An experimental study.", + "year": 2004, + "url": "https://pubmed.ncbi.nlm.nih.gov/15228624/", + "doi": "10.1186/1476-5918-3-4", + "pmid": "15228624", + "topics": [ + "lat_pulldown", + "seated_row" + ], + "notes": "Measured latissimus, elbow-flexor and scapular-muscle activity differed across studied pulldown/row tasks.", + "limitations": "Isometric portions and surface recordings; no mapping of all divergent machine trajectories or outcome superiority.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Lehman GJ, Buchan DD, Lundy A, Myers N, Nalborczyk A.", + "type": "emg_evidence" + }, + { + "ref_id": "life_fitness_catalog_2024", + "title": "Life Fitness commercial product catalogue 2024", + "year": 2024, + "url": "https://www.lifefitness.com.au/wp-content/uploads/2024/02/Life-Fitness-Catalogue_2024_web.pdf", + "doi": null, + "pmid": null, + "topics": [ + "multifunction", + "equipment_identity" + ], + "notes": "Catalogue lists distinct Pectoral Fly/Rear Deltoid and Assist Dip Chin combination products.", + "limitations": "EXAMPLE ONLY: establishes available equipment concepts, not manufacturer/model identification in the observed gym or anatomical efficacy.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Life Fitness", + "type": "manufacturer_statement" + }, + { + "ref_id": "maeo_2021", + "title": "Greater Hamstrings Muscle Hypertrophy but Similar Damage Protection after Training at Long versus Short Muscle Lengths.", + "year": 2021, + "url": "https://pubmed.ncbi.nlm.nih.gov/33009197/", + "doi": "10.1249/mss.0000000000002523", + "pmid": "33009197", + "topics": [ + "hamstrings", + "seated_leg_curl", + "prone_leg_curl" + ], + "notes": "Within-person training found greater biarticular hamstring growth with seated than prone curls; both variants trained knee flexion.", + "limitations": "One protocol/population; not a guarantee for every machine, person, muscle region or strength task.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Maeo S, Huang M, Wu Y, Sakurai H, Kusagawa Y, Sugiyama T, Kanehisa H, Isaka T.", + "type": "intervention_evidence" + }, + { + "ref_id": "martin_fuentes_2020", + "title": "Evaluation of the Lower Limb Muscles' Electromyographic Activity during the Leg Press Exercise and Its Variants: A Systematic Review.", + "year": 2020, + "url": "https://pubmed.ncbi.nlm.nih.gov/32605065/", + "doi": "10.3390/ijerph17134626", + "pmid": "32605065", + "topics": [ + "leg_press" + ], + "notes": "Leg-press studies report substantial quadriceps excitation; variant findings are inconsistent.", + "limitations": "EMG review does not establish universal foot-position targeting, force shares or hypertrophy ranking.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Martín-Fuentes I, Oliva-Lozano JM, Muyor JM.", + "type": "emg_evidence" + }, + { + "ref_id": "nih_abdominal_wall", + "title": "Anatomy, Abdomen and Pelvis: Anterolateral Abdominal Wall", + "year": 2023, + "url": "https://www.ncbi.nlm.nih.gov/books/NBK525975/", + "doi": null, + "pmid": null, + "topics": [ + "core", + "trunk" + ], + "notes": "Rectus abdominis, internal/external obliques and transversus have movement, abdominal-wall tension and stabilization roles.", + "limitations": "Functional anatomy only; clinical sections are outside this handoff scope.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Kevin Seeras; Ryan N. Qasawa; Ricky Ju; Shivana Prakash", + "type": "established_anatomy" + }, + { + "ref_id": "nih_deltoid", + "title": "Anatomy, Shoulder and Upper Limb, Deltoid Muscle", + "year": 2024, + "url": "https://www.ncbi.nlm.nih.gov/books/NBK537056/", + "doi": null, + "pmid": null, + "topics": [ + "deltoids", + "shoulder" + ], + "notes": "Anterior, middle and posterior portions have distinct lines of action; relative contribution depends on arm position.", + "limitations": "Functional anatomy only; clinical sections are outside this handoff scope.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Adel Elzanie; Matthew A. Varacallo", + "type": "established_anatomy" + }, + { + "ref_id": "nih_gluteus_minimus", + "title": "Anatomy, Bony Pelvis and Lower Limb, Gluteus Minimus Muscle", + "year": 2023, + "url": "https://www.ncbi.nlm.nih.gov/books/NBK556144/", + "doi": null, + "pmid": null, + "topics": [ + "hip_abduction", + "glutes", + "gait" + ], + "notes": "Minimus abducts and stabilizes the hip; anterior fibers support internal rotation. Prevents erroneous adduction classification from textbook alternative text.", + "limitations": "Functional anatomy only; clinical sections are outside this handoff scope.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Anthony J. Greco; Renato C. Vilella", + "type": "established_anatomy" + }, + { + "ref_id": "nih_pectoralis", + "title": "Anatomy, Thorax, Pectoralis Major Major", + "year": 2023, + "url": "https://www.ncbi.nlm.nih.gov/books/NBK525991/", + "doi": null, + "pmid": null, + "topics": [ + "chest", + "shoulder" + ], + "notes": "Clavicular and sternocostal parts share shoulder adduction/internal rotation; clavicular fibers support flexion, sternocostal fibers extension from flexion. Regional anatomy does not prove isolated regional training.", + "limitations": "Functional anatomy only; clinical sections are outside this handoff scope.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Francesca Solari; Bracken Burns", + "type": "established_anatomy" + }, + { + "ref_id": "openstax_actions", + "title": "Anatomy and Physiology 2e: Types of Body Movements", + "year": 2022, + "url": "https://openstax.org/books/anatomy-and-physiology-2e/pages/9-5-types-of-body-movements", + "doi": null, + "pmid": null, + "topics": [ + "joint_actions" + ], + "notes": "Terminology of joint movements. Pattern labels in Trainlog remain authored programming abstractions.", + "limitations": "Textbook synthesis; not a machine-specific force or adaptation study.", + "accessed_on": "2026-09-09", + "authors_or_organization": "J. Gordon Betts; Kelly A. Young; James A. Wise; Eddie Johnson; Brandon Poe; Dean H. Kruse; Oksana Korol; Jody E. Johnson; Mark Womble; Peter DeSaix", + "type": "established_anatomy" + }, + { + "ref_id": "openstax_back", + "title": "Anatomy and Physiology 2e: Axial Muscles of the Head, Neck, and Back", + "year": 2022, + "url": "https://openstax.org/books/anatomy-and-physiology-2e/pages/11-3-axial-muscles-of-the-head-neck-and-back", + "doi": null, + "pmid": null, + "topics": [ + "spinal_extensors", + "multifidus" + ], + "notes": "Vertebral-column muscle functions including erector spinae and deep posterior muscles.", + "limitations": "Textbook synthesis; not a machine-specific force or adaptation study.", + "accessed_on": "2026-09-09", + "authors_or_organization": "J. Gordon Betts; Kelly A. Young; James A. Wise; Eddie Johnson; Brandon Poe; Dean H. Kruse; Oksana Korol; Jody E. Johnson; Mark Womble; Peter DeSaix", + "type": "established_anatomy" + }, + { + "ref_id": "openstax_lower", + "title": "Anatomy and Physiology 2e: Appendicular Muscles of the Pelvic Girdle and Lower Limbs", + "year": 2022, + "url": "https://openstax.org/books/anatomy-and-physiology-2e/pages/11-6-appendicular-muscles-of-the-pelvic-girdle-and-lower-limbs", + "doi": null, + "pmid": null, + "topics": [ + "hip", + "knee", + "ankle" + ], + "notes": "Functional lower-limb anatomy. Figure alternative text contains direction inconsistencies for minimus/pectineus/gracilis; use action definitions and NIH corroboration, not those phrases.", + "limitations": "Textbook synthesis; not a machine-specific force or adaptation study.", + "accessed_on": "2026-09-09", + "authors_or_organization": "J. Gordon Betts; Kelly A. Young; James A. Wise; Eddie Johnson; Brandon Poe; Dean H. Kruse; Oksana Korol; Jody E. Johnson; Mark Womble; Peter DeSaix", + "type": "established_anatomy" + }, + { + "ref_id": "openstax_trunk", + "title": "Anatomy and Physiology 2e: Axial Muscles of the Abdominal Wall and Thorax", + "year": 2022, + "url": "https://openstax.org/books/anatomy-and-physiology-2e/pages/11-4-axial-muscles-of-the-abdominal-wall-and-thorax", + "doi": null, + "pmid": null, + "topics": [ + "trunk", + "abdominals", + "spinal_extensors" + ], + "notes": "Trunk movement, abdominal compression and postural roles.", + "limitations": "Textbook synthesis; not a machine-specific force or adaptation study.", + "accessed_on": "2026-09-09", + "authors_or_organization": "J. Gordon Betts; Kelly A. Young; James A. Wise; Eddie Johnson; Brandon Poe; Dean H. Kruse; Oksana Korol; Jody E. Johnson; Mark Womble; Peter DeSaix", + "type": "established_anatomy" + }, + { + "ref_id": "openstax_upper", + "title": "Anatomy and Physiology 2e: Muscles of the Pectoral Girdle and Upper Limbs", + "year": 2022, + "url": "https://openstax.org/books/anatomy-and-physiology-2e/pages/11-5-muscles-of-the-pectoral-girdle-and-upper-limbs", + "doi": null, + "pmid": null, + "topics": [ + "shoulder", + "scapula", + "elbow", + "forearm" + ], + "notes": "Functional anatomy of upper-limb and scapular muscles; no exercise outcome ranking.", + "limitations": "Textbook synthesis; not a machine-specific force or adaptation study.", + "accessed_on": "2026-09-09", + "authors_or_organization": "J. Gordon Betts; Kelly A. Young; James A. Wise; Eddie Johnson; Brandon Poe; Dean H. Kruse; Oksana Korol; Jody E. Johnson; Mark Womble; Peter DeSaix", + "type": "established_anatomy" + }, + { + "ref_id": "precor_pulley_example", + "title": "Resolute Dual Adjustable Pulley RUD0915", + "year": null, + "url": "https://www.precor.com/en-US/products/RUD0915", + "doi": null, + "pmid": null, + "topics": [ + "cable", + "load_context" + ], + "notes": "Manufacturer documents a 4:1 cable ratio on this particular example, demonstrating why stack labels and handle resistance differ.", + "limitations": "EXAMPLE ONLY: not evidence that the user owns Precor or this model; local pulley ratios remain unresolved.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Precor", + "type": "manufacturer_statement" + }, + { + "ref_id": "refalo_2023", + "title": "Influence of Resistance Training Proximity-to-Failure on Skeletal Muscle Hypertrophy: A Systematic Review with Meta-analysis.", + "year": 2023, + "url": "https://pubmed.ncbi.nlm.nih.gov/36334240/", + "doi": "10.1007/s40279-022-01784-y", + "pmid": "36334240", + "topics": [ + "proximity_to_failure" + ], + "notes": "Failure definitions matter; available categorical comparisons do not establish a simple more-failure-is-better hypertrophy rule.", + "limitations": "Literature search predates newer trials; uncertainty in actual repetitions in reserve prevents exact universal thresholds.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Refalo MC, Helms ER, Trexler ET, Hamilton DL, Fyfe JJ.", + "type": "intervention_evidence" + }, + { + "ref_id": "vieira_2022", + "title": "Effects of Resistance Training to Muscle Failure on Acute Fatigue: A Systematic Review and Meta-Analysis.", + "year": 2022, + "url": "https://pubmed.ncbi.nlm.nih.gov/34881412/", + "doi": "10.1007/s40279-021-01602-x", + "pmid": "34881412", + "topics": [ + "fatigue", + "recovery", + "failure" + ], + "notes": "Failure training generated greater acute fatigue than nonfailure conditions in the reviewed experiments.", + "limitations": "Acute markers do not establish a universal recovery duration or predict individual long-term adaptation.", + "accessed_on": "2026-09-09", + "authors_or_organization": "Vieira JG, Sardeli AV, Dias MR, Filho JE, Campos Y, Sant'Ana L, Leitão L, Reis V, Wilk M, Novaes J, Vianna J.", + "type": "intervention_evidence" + }, + { + "ref_id": "vigotsky_2018", + "title": "Interpreting Signal Amplitudes in Surface Electromyography Studies in Sport and Rehabilitation Sciences.", + "year": 2017, + "url": "https://pubmed.ncbi.nlm.nih.gov/29354060/", + "doi": "10.3389/fphys.2017.00985", + "pmid": "29354060", + "topics": [ + "measurement", + "emg" + ], + "notes": "Surface EMG amplitude reflects a recording-dependent excitation signal and cannot directly rank hypertrophy, force or training effectiveness.", + "limitations": "Methodological review; anatomical inference and longitudinal outcomes require separate evidence.", + "accessed_on": "2026-09-09", + "publication_note": "Volume/index year 2017; online publication January 4, 2018.", + "authors_or_organization": "Vigotsky AD, Halperin I, Lehman GJ, Trajano GS, Vieira TM.", + "type": "emg_evidence" + } + ] +} diff --git a/catalog/training-knowledge-audit-v1.json b/catalog/training-knowledge-audit-v1.json new file mode 100644 index 0000000..205326b --- /dev/null +++ b/catalog/training-knowledge-audit-v1.json @@ -0,0 +1,1070 @@ +{ + "format": "trainlog-training-knowledge-audit-v1", + "version": 1, + "rows": [ + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_lower" + ], + "exercise_id": "ex_01ff06dd-ad00-46ee-9b46-ed32cabedbef", + "exercise_name": "Hip adduction", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "hip_adduction" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "openstax_lower" + ], + "family_description": "hip_adduction", + "action_ids": [ + "hip_adduction" + ], + "pattern_ids": [ + "single_joint_hip_adduction" + ], + "primary_muscle_ids": [ + "hip_adductors" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_lower" + ], + "exercise_id": "ex_1872246a-39ae-44dc-b58d-f87e90ca49ab", + "exercise_name": "Leg extension", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "leg_extension" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "openstax_lower" + ], + "family_description": "leg_extension", + "action_ids": [ + "knee_extension" + ], + "pattern_ids": [ + "single_joint_knee_extension" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "exercise_id": "ex_1a34814c-2e46-40fc-b1f4-6d60b8e5a3e0", + "exercise_name": "Rotary torso", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "rotary_torso" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "nih_abdominal_wall", + "openstax_back", + "openstax_trunk" + ], + "family_description": "rotary_torso", + "action_ids": [ + "trunk_rotation" + ], + "pattern_ids": [ + "trunk_rotation" + ], + "primary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "transversus_abdominis" + ], + "scientific_primary_zone_id": "core", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Back plus arms is defensible; posterior deltoid participates but shoulders need not be mandatory secondary for every row geometry.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "exercise_id": "ex_1b0c6b8b-b05e-4e6f-8809-5f7d85d668de", + "exercise_name": "Diverged seated row", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "diverging_seated_row" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "scientific_primary_zone_id": "back", + "scientific_secondary_zone_ids": [ + "arms" + ] + }, + { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [], + "exercise_id": "ex_2d488c08-194c-4051-a3c9-34471646c1d3", + "exercise_name": "Gym échauffement", + "resolution_status": "unresolved", + "equipment_ids": [], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [], + "family_description": null, + "action_ids": [], + "pattern_ids": [], + "primary_muscle_ids": [], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": null, + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Back plus arms is defensible; posterior deltoid participates but shoulders need not be mandatory secondary for every row geometry.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "exercise_id": "ex_33f79331-871c-4eed-babe-346e53a99070", + "exercise_name": "Seated row", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "seated_row" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "franke_2015", + "lehman_2004", + "openstax_upper" + ], + "family_description": "seated_row", + "action_ids": [ + "elbow_flexion", + "scapular_retraction", + "shoulder_extension", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "horizontal_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi", + "rhomboids", + "trapezius_middle" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "deltoid_posterior", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "forearm_extensors", + "forearm_flexors" + ], + "scientific_primary_zone_id": "back", + "scientific_secondary_zone_ids": [ + "arms" + ] + }, + { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "chest", + "existing_secondary_zone_ids": [ + "arms", + "shoulders" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "exercise_id": "ex_34a5c9c3-c032-4dfb-be32-8bf09832f72b", + "exercise_name": "Converting chest press", + "resolution_status": "conditional", + "equipment_ids": [ + "eq_c660cd61-5b17-498e-8d51-9eafcf7e2731" + ], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "chest", + "scientific_secondary_zone_ids": [ + "arms", + "shoulders" + ] + }, + { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [], + "exercise_id": "ex_43c7375f-934c-4650-930c-45807d2f2929", + "exercise_name": "Gym/Échauffement", + "resolution_status": "unresolved", + "equipment_ids": [], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [], + "family_description": null, + "action_ids": [], + "pattern_ids": [], + "primary_muscle_ids": [], + "secondary_muscle_ids": [], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": null, + "scientific_secondary_zone_ids": [] + }, + { + "status": "questionable", + "severity": "information", + "confidence": "moderate", + "rationale": "Back plus core is compatible with spinal extension; hip-dominant execution could justify additional lower-limb roles after observation.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "core" + ], + "proposed_mutation": null, + "source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "exercise_id": "ex_474ec393-3efa-4aaa-8e08-1a0245ed7835", + "exercise_name": "Back extension", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "back_extension" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "lee_2015", + "openstax_back", + "openstax_lower" + ], + "family_description": "back_extension", + "action_ids": [ + "trunk_extension" + ], + "pattern_ids": [ + "trunk_extension" + ], + "primary_muscle_ids": [ + "erector_spinae" + ], + "secondary_muscle_ids": [ + "multifidus" + ], + "stabilizer_muscle_ids": [ + "external_oblique", + "internal_oblique", + "rectus_abdominis" + ], + "scientific_primary_zone_id": "back", + "scientific_secondary_zone_ids": [ + "core" + ] + }, + { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "exercise_id": "ex_4bd03d55-e644-436e-876e-07837824bde5", + "exercise_name": "Abdominal", + "resolution_status": "conditional", + "equipment_ids": [ + "eq_3f987a36-bbb4-4e29-9c9f-1f201f1596e0" + ], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "scientific_primary_zone_id": "core", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Shoulders plus back matches posterior-deltoid horizontal abduction and scapular retraction; same device Pec Fly would instead target chest.", + "existing_primary_zone_id": "shoulders", + "existing_secondary_zone_ids": [ + "back" + ], + "proposed_mutation": null, + "source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ], + "exercise_id": "ex_4cd2433e-80b1-478a-b8df-73fc6ef80962", + "exercise_name": "Rear Delt", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "rear_delt_pec_fly" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "franke_2015", + "nih_deltoid", + "openstax_upper", + "vigotsky_2018" + ], + "family_description": "rear_delt_fly", + "action_ids": [ + "scapular_retraction", + "shoulder_horizontal_abduction" + ], + "pattern_ids": [ + "single_joint_shoulder_horizontal_abduction" + ], + "primary_muscle_ids": [ + "deltoid_posterior" + ], + "secondary_muscle_ids": [ + "rhomboids", + "trapezius_middle" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "shoulders", + "scientific_secondary_zone_ids": [ + "back" + ] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "exercise_id": "ex_617007f9-7420-4408-91b9-8ffb77900f13", + "exercise_name": "Seated Leg", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "seated_leg_curl" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "family_description": "seated_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "shoulders", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "exercise_id": "ex_6dfc7ffd-8891-464e-a995-808baf1b0d7b", + "exercise_name": "Converging Shoulder Press", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "converging_shoulder_press" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "nih_deltoid", + "openstax_upper" + ], + "family_description": "machine_shoulder_press", + "action_ids": [ + "elbow_extension", + "scapular_upward_rotation", + "shoulder_abduction", + "shoulder_flexion" + ], + "pattern_ids": [ + "vertical_push" + ], + "primary_muscle_ids": [ + "deltoid_anterior", + "deltoid_middle" + ], + "secondary_muscle_ids": [ + "serratus_anterior", + "trapezius_lower", + "trapezius_upper", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "shoulders", + "scientific_secondary_zone_ids": [ + "arms" + ] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Glutes is defensible for medius/minimus; tensor fasciae latae participation does not require all accessory muscles to create a secondary zone.", + "existing_primary_zone_id": "glutes", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "exercise_id": "ex_7e7cf906-2214-4066-bcb7-c16382d83b3b", + "exercise_name": "Hip abduction", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "hip_abduction" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "family_description": "hip_abduction", + "action_ids": [ + "hip_abduction" + ], + "pattern_ids": [ + "single_joint_hip_abduction" + ], + "primary_muscle_ids": [ + "gluteus_medius", + "gluteus_minimus" + ], + "secondary_muscle_ids": [ + "gluteus_maximus", + "tensor_fasciae_latae" + ], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "glutes", + "scientific_secondary_zone_ids": [] + }, + { + "status": "questionable", + "severity": "information", + "confidence": "uncertain", + "rationale": "Existing assignment is plausible for the named family but exact custom execution remains unverified. Retain persisted relation; no scientific auto-classification.", + "existing_primary_zone_id": "chest", + "existing_secondary_zone_ids": [ + "arms", + "shoulders" + ], + "proposed_mutation": null, + "source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "exercise_id": "ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af", + "exercise_name": "Chest press", + "resolution_status": "conditional", + "equipment_ids": [ + "eq_0a462c1e-9fb6-4fd7-b0a2-53a0e86f33c6" + ], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [ + "nih_deltoid", + "nih_pectoralis", + "openstax_upper" + ], + "family_description": "machine_chest_press", + "action_ids": [ + "elbow_extension", + "shoulder_horizontal_adduction" + ], + "pattern_ids": [ + "horizontal_push" + ], + "primary_muscle_ids": [ + "pectoralis_major" + ], + "secondary_muscle_ids": [ + "deltoid_anterior", + "triceps_brachii" + ], + "stabilizer_muscle_ids": [ + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "chest", + "scientific_secondary_zone_ids": [ + "arms", + "shoulders" + ] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "core", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "exercise_id": "ex_9adf7566-f10c-443c-b63b-681d37665693", + "exercise_name": "Abdominal crunch", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "abdominal" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "nih_abdominal_wall", + "openstax_trunk" + ], + "family_description": "abdominal_crunch", + "action_ids": [ + "trunk_flexion" + ], + "pattern_ids": [ + "trunk_flexion" + ], + "primary_muscle_ids": [ + "rectus_abdominis" + ], + "secondary_muscle_ids": [ + "external_oblique", + "internal_oblique" + ], + "stabilizer_muscle_ids": [ + "transversus_abdominis" + ], + "scientific_primary_zone_id": "core", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "exercise_id": "ex_a1ef5047-b44b-4c64-a6ed-c7a3bc13b163", + "exercise_name": "Seated leg curl", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "seated_leg_curl" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "family_description": "seated_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "exercise_id": "ex_a72fa713-4b0e-431d-95e2-42d95beb77b1", + "exercise_name": "Lat pull", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "lat_pull" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "back", + "scientific_secondary_zone_ids": [ + "arms" + ] + }, + { + "status": "unresolved", + "severity": "none", + "confidence": "uncertain", + "rationale": "Insufficient actual execution evidence to replace a valid unclassified relation.", + "existing_primary_zone_id": null, + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [], + "exercise_id": "ex_b1e6ffc6-75b5-45ff-a3c0-e7433c58013d", + "exercise_name": "Marche", + "resolution_status": "conditional", + "equipment_ids": [], + "knowledge_confidence": "uncertain", + "knowledge_source_refs": [ + "nih_gluteus_minimus", + "openstax_lower" + ], + "family_description": "walking", + "action_ids": [ + "ankle_dorsiflexion", + "ankle_plantarflexion", + "hip_extension", + "hip_flexion", + "knee_extension", + "knee_flexion" + ], + "pattern_ids": [ + "locomotion" + ], + "primary_muscle_ids": [ + "gastrocnemius", + "gluteus_maximus", + "quadriceps", + "soleus" + ], + "secondary_muscle_ids": [ + "hamstrings", + "iliopsoas", + "tibialis_anterior" + ], + "stabilizer_muscle_ids": [ + "erector_spinae", + "gluteus_medius", + "gluteus_minimus" + ], + "scientific_primary_zone_id": "full_body", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [ + "glutes" + ], + "proposed_mutation": null, + "source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "exercise_id": "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde", + "exercise_name": "Leg press", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "leg_press", + "plate_loaded_leg_press" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "martin_fuentes_2020", + "openstax_lower", + "vigotsky_2018" + ], + "family_description": "leg_press", + "action_ids": [ + "hip_extension", + "knee_extension" + ], + "pattern_ids": [ + "knee_dominant" + ], + "primary_muscle_ids": [ + "quadriceps" + ], + "secondary_muscle_ids": [ + "adductor_magnus", + "gluteus_maximus" + ], + "stabilizer_muscle_ids": [ + "gastrocnemius", + "soleus" + ], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [ + "glutes" + ] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "moderate", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "back", + "existing_secondary_zone_ids": [ + "arms" + ], + "proposed_mutation": null, + "source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "exercise_id": "ex_b4d1daf1-de4a-4016-abdf-487bf6014ce6", + "exercise_name": "Diverging lat pulldown", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "diverging_lat_pulldown" + ], + "knowledge_confidence": "moderate", + "knowledge_source_refs": [ + "lehman_2004", + "openstax_upper", + "vigotsky_2018" + ], + "family_description": "lat_pulldown", + "action_ids": [ + "elbow_flexion", + "scapular_downward_rotation", + "shoulder_adduction", + "shoulder_extension" + ], + "pattern_ids": [ + "vertical_pull" + ], + "primary_muscle_ids": [ + "latissimus_dorsi" + ], + "secondary_muscle_ids": [ + "biceps_brachii", + "brachialis", + "brachioradialis", + "teres_major" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors", + "infraspinatus", + "subscapularis", + "supraspinatus", + "teres_minor" + ], + "scientific_primary_zone_id": "back", + "scientific_secondary_zone_ids": [ + "arms" + ] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "thighs", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "exercise_id": "ex_d7398d9f-d928-4d2e-94e9-74e201da55c5", + "exercise_name": "Prone leg curl", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "prone_leg_curl" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "maeo_2021", + "openstax_lower" + ], + "family_description": "prone_leg_curl", + "action_ids": [ + "knee_flexion" + ], + "pattern_ids": [ + "single_joint_knee_flexion" + ], + "primary_muscle_ids": [ + "hamstrings" + ], + "secondary_muscle_ids": [ + "gastrocnemius" + ], + "stabilizer_muscle_ids": [], + "scientific_primary_zone_id": "thighs", + "scientific_secondary_zone_ids": [] + }, + { + "status": "confirmed", + "severity": "none", + "confidence": "high", + "rationale": "Compatible coarse projection of the identified movement family; secondary zones are selected synergists, not every stabilizer.", + "existing_primary_zone_id": "arms", + "existing_secondary_zone_ids": [], + "proposed_mutation": null, + "source_refs": [ + "openstax_upper" + ], + "exercise_id": "ex_ec619fc2-4685-4044-873c-86764bd4a0fe", + "exercise_name": "Arm curl", + "resolution_status": "resolved_family_variant_limited", + "equipment_ids": [ + "arm_curl" + ], + "knowledge_confidence": "high", + "knowledge_source_refs": [ + "openstax_upper" + ], + "family_description": "arm_curl", + "action_ids": [ + "elbow_flexion" + ], + "pattern_ids": [ + "single_joint_elbow_flexion" + ], + "primary_muscle_ids": [ + "biceps_brachii", + "brachialis" + ], + "secondary_muscle_ids": [ + "brachioradialis" + ], + "stabilizer_muscle_ids": [ + "forearm_extensors", + "forearm_flexors" + ], + "scientific_primary_zone_id": "arms", + "scientific_secondary_zone_ids": [] + } + ] +} diff --git a/docs/android.md b/docs/android.md index 74110ef..6b5fa56 100644 --- a/docs/android.md +++ b/docs/android.md @@ -510,3 +510,28 @@ Android requires `data_fields = 0` for `SETS`, while the desktop model/API currently accepts known supplemental bits on either recording mode. Supplied profiles do not exercise this difference. Supporting a future set-based supplemental field requires an explicit shared-model decision. + +## 18. Training knowledge V1 read APIs + +Android bundles the six authored training-knowledge catalogs as immutable +assets. `TrainingKnowledgeCatalog` validates and exposes their stable-ID +records; it is not a second manually authored scientific table. +`TrainlogRepository.getTrainingExerciseContext()` composes an exact persisted +exercise with its direct/ancestor persisted zones, optional scientific mapping, +compatible equipment, latest explicit MAX and recent occurrence/set preview. +`listExerciseOccurrences()` and `listExerciseOccurrenceSets()` provide bounded +follow-up pages. Occurrence and set limits are 1–32 and 1–64 respectively. +Cursors order current data chronologically by original timestamp, session ID +and occurrence ID, and do not preserve a snapshot across calls. + +This read-only feature makes no Android schema change (the runtime schema +remains v10), does not seed rows, and does not export/synchronize new data. It +does not implement recommendations, planned weights, set counts or fatigue +scores. Its occurrence and latest-MAX readers use the same explicit temporal +grammar, exact fractional comparison and bytewise ID tie breakers as C. +The Android writer's omitted-seconds form is admitted, and emitted cursors +retain the original source text. Production pagination/MAX parity tests pass. +The tranche is `TRAINING_KNOWLEDGE_V1=PASS`. The Android loader enforces canonical exercise +and equipment identity syntax, bidirectional exercise/capability compatibility, +HIGH evidence source type, and non-unresolved BODY ZONE audit evidence. Its +full source and uncertainty contract is in [Training knowledge system V1](domain/knowledge_system.md). diff --git a/docs/architecture.md b/docs/architecture.md index 77f7b7c..9a3e1f7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -477,3 +477,36 @@ masks to be zero for `SETS`. The shipped catalog uses supplemental speed and distance only with `CONTINUOUS`; defining cross-platform behavior for a future set-based supplemental field is a model-contract decision, not part of this reconciliation. + +## 14. Training knowledge V1 boundary + +`TRAINING_KNOWLEDGE_V1` is a read-only composition layer. Six versioned JSON +catalogs under `catalog/` are the only authored scientific source; generated C +data and Android asset loading derive from them. They contain cited anatomy, +movement, exercise and equipment knowledge, not user history. The generated +knowledge audit is evidence output, not an editable source. + +The desktop `training_knowledge.h` API exposes immutable catalog records and +stable-ID queries. `training_context.h` combines one exact persisted exercise +with its stored BODY ZONE relations, optional science, compatible equipment, +latest explicit maximum, and bounded occurrence/set history under one read +snapshot. Android provides the corresponding catalog and repository context. +This composition neither writes SQLite nor seeds catalog mappings. An unknown +runtime ID and a missing scientific record remain valid states. + +Scientific BODY ZONE projections and persisted BODY ZONE relations have +different ownership and are never substituted for one another. Labels and +generic equipment descriptions are not runtime identities; an equipment +capability does not create an `ex_` exercise or historical association. +The feature is `TRAINING_KNOWLEDGE_V1=PASS`. The temporal contract has +independent PASS evidence: +the readers parse the admitted source forms into exact instants, compare exact +fractions, then use bytewise session and entry ID ties; emitted exclusive +cursors preserve the original timestamp text and IDs. Selection and hydration +share a read snapshot, while separate page calls retain current-data semantics. +Malformed caller cursors and malformed matching stored timestamps fail +explicitly; a selected timestamp beyond C's 40-character output field also +fails explicitly. The initial full-tranche audit's stale temporal-documentation, +Android loader, Meson input, and C role-only query findings were resolved by one +bounded repair chain and independently verified. Its full contract, uncertainty boundary and +future-only planning architecture are in [Training knowledge system V1](domain/knowledge_system.md). diff --git a/docs/current_state.md b/docs/current_state.md index e65ebe6..e64dfd9 100644 --- a/docs/current_state.md +++ b/docs/current_state.md @@ -62,7 +62,9 @@ BODY_ZONES_DESKTOP_REAL_MIGRATION=PASS BODY_ZONES_TUI_REAL_VALIDATION=PASS BODY_ZONES_ANDROID_DEVICE_VALIDATION=PASS -DESKTOP_TESTS=39/39 PASS +TRAINING_KNOWLEDGE_V1=PASS + +DESKTOP_TESTS=42/42 PASS (recorded validation checkpoint) ANDROID_BUILD=PASS HARDWARE_SYNC_VALIDATION=HISTORICAL_PASS ``` @@ -72,6 +74,8 @@ HARDWARE_SYNC_VALIDATION=HISTORICAL_PASS Implemented: - C17/Notcurses true-color TUI (72x20 minimum, UTF-8 prompts, resize fallback); +- UTF-8 cell-aware scrolling training-knowledge screen, tested at the 72x20 + minimum terminal; - SQLite schema v11, with stable ordered `session_exercises.entry_id`, occurrence-level equipment identity, and desktop-local custom-equipment definitions, plus occurrence-owned `max_results`; its v9 -> v10 migration @@ -152,6 +156,48 @@ All Android screens use the shared compact `◆ TRAINLOG ◆` header: the Notcurses accent, muted context line, and flat touch layout reproduce the TUI plaque without literal terminal box drawing. +## Training knowledge V1 + +The implemented read-only training-knowledge layer loads six versioned JSON +catalogs as the sole authored scientific source, generates the immutable C +catalog representation, and loads the same assets on Android. It has no +database migration, no auto-seeding, and no synchronization artifact. The +desktop database remains schema v11 and Android remains schema v10. + +Desktop `training_knowledge.h` and Android `TrainingKnowledgeCatalog` expose +source-linked science lookups and resolved-candidate filters. Desktop +`training_context.h` and Android `TrainlogRepository` compose one real runtime +exercise with its persisted zones, compatible equipment, latest explicit MAX, +and bounded chronological occurrence/set history. Persisted zones remain +separate from scientific mappings; missing scientific knowledge is valid. + +The scientific review passed and the reviewed catalog bytes remain unchanged. +The independent temporal review returned `TEMPORAL_DELTA_REVIEW=PASS`, with no +temporal defects or repairs. It reviewed the grammar, calendar and offset +bounds, fraction precision, bytewise ties, cursor aliasing and exclusivity, +source capacity, snapshots, and Android/Python parity; it ran the targeted +Meson and four Python temporal tests. The initial full-tranche engineering +audit initially failed with stale temporal-defect documentation (BLOCKER), +Android loader parity gaps (BLOCKER), omitted Meson generator inputs (BLOCKER), +and a C role-only query mismatch (HIGH). One bounded repair chain resolved all +four findings; independent repair verification returned +`FINAL_REVIEW_REPAIR_VERIFICATION=PASS` and +`TRAINING_KNOWLEDGE_V1_ENGINEERING_REVIEW=PASS`. Fresh final validation passed: +strict build, 42 Meson tests, eight knowledge and four temporal Python tests, +knowledge/JSON/import validators, three C17 headers, affected C knowledge and +context tests under ASan/UBSan plus Python timestamp validation, normal and +sanitized 12-form temporal probes, and Android 56 tests with zero failures or +errors and one known missing-real-v9-fixture skip; Java 17 `assembleDebug` also +passed. Generated C is byte-identical with SHA-256 +`e8c099f67eb111d61621b5d76592c049823af5508d43e73ec646f22e4c377fca`; all six +Android assets are byte-identical. Preservation before and after repair confirms +schema v11/v10, unchanged catalog/science/temporal bytes, and unchanged real +database logical SHA-256 `26139cafeffbde3ec08f6ef23c5069e75afb9cd69ffffb40be5c099006fedc4d`, +counts, integrity, and foreign keys. `TRAINING_KNOWLEDGE_V1=PASS`. The +[temporal contract](reviews/training_knowledge_v1_temporal_contract.md) +defines the settled reader behavior. No manual Android install, manual TUI +visual validation or manual MTP validation is claimed for this tranche. + ## Synchronization Canonical exchange directory: diff --git a/docs/database.md b/docs/database.md index f1877c2..a39c6d2 100644 --- a/docs/database.md +++ b/docs/database.md @@ -546,3 +546,25 @@ left/right asymmetry percentages The optional estimation profile is desktop configuration, not database history. + +## 14. Training knowledge read boundary + +Training Knowledge V1 adds no table, migration, seed data or synchronization +artifact. The desktop database remains schema v11. Read-only context assembly +joins an exact existing exercise with its persisted BODY ZONE relations, +occurrence history, raw sets, actual equipment and latest explicit MAX, then +optionally attaches immutable catalog knowledge. Missing catalog knowledge is +valid and never causes a database mutation. The scientific catalog's BODY ZONE +projection is not stored in `exercise_body_zones` and does not replace user +classification. See [Training knowledge system V1](domain/knowledge_system.md). +Occurrence pagination and latest explicit MAX use an exact derived instant +comparison, then bytewise session/entry ID ties. They scan all matching +metadata and retain only bounded candidates before hydration, inside a read +snapshot. Original timestamp text stays unchanged. Malformed storage and a +selected timestamp beyond the existing 40-character C output capacity produce +explicit errors. The [temporal contract](reviews/training_knowledge_v1_temporal_contract.md) +distinguishes the accepted profile from accidental Python ISO extensions. +The temporal contract has independent PASS evidence. The initial audit's stale +temporal documentation, Android loader, Meson input, and C role-only query +findings were repaired, independently verified, and final-validated; +`TRAINING_KNOWLEDGE_V1=PASS`. diff --git a/docs/domain/anatomy_and_movement.md b/docs/domain/anatomy_and_movement.md new file mode 100644 index 0000000..df9fb99 --- /dev/null +++ b/docs/domain/anatomy_and_movement.md @@ -0,0 +1,113 @@ +# Functional anatomy and movement knowledge + +TRAINING KNOWLEDGE V1 separates anatomy from the exercise, its execution and +its physical equipment. A muscle can move a joint, assist another mover or +stabilize a segment; its role changes with posture, resistance direction and +movement phase. The catalogs record qualitative roles, not force percentages, +effective-set fractions or physiological measurements. + +The scientific references are in `catalog/science-references-v1.json`. +Functional entities, joint actions and authored movement conventions belong in +`catalog/muscles-v1.json`, `catalog/joint-actions-v1.json` and +`catalog/movement-patterns-v1.json`. A muscle region or muscle group is not a +new anatomical muscle. Parent groups and their component muscles must not be +summed as independent exposure. + +## Evidence and confidence + +Every interpretation distinguishes established anatomy, biomechanical +interpretation, EMG evidence, intervention evidence, manufacturer statements +and practical inference. Anatomy explains plausible function; longitudinal +training studies address adaptation. Surface EMG measures a signal affected +by recording and physiological conditions. Greater amplitude does not establish +greater muscle force, hypertrophy, strength improvement or universal primary +muscle status. [Vigotsky and colleagues](https://pubmed.ncbi.nlm.nih.gov/29354060/) + +Confidence uses exactly `high`, `moderate` and `uncertain`. Established anatomy +can have high confidence while its application to an unobserved machine variant +remains uncertain. Moderate confidence is appropriate when the exercise family +is clear but geometry changes secondary or stabilizing roles. Missing evidence +is retained explicitly, not replaced with a commercial-name rule. + +## Functional muscle coverage + +The catalog covers these functional distinctions: + +| Area | Functional distinctions | +|---|---| +| Chest | Pectoralis major, clavicular and sternocostal regions; pectoralis minor as a scapular muscle | +| Back and scapula | Latissimus dorsi, teres major, trapezius regions, rhomboids and serratus anterior | +| Shoulder | Anterior, middle and posterior deltoid; supraspinatus, infraspinatus, teres minor and subscapularis | +| Arms and forearms | Elbow flexors, triceps, grip/wrist flexors and extensors, pronation and supination | +| Trunk | Rectus abdominis, obliques, transversus abdominis, erector spinae, multifidus and quadratus lumborum | +| Hip | Gluteal muscles, tensor fasciae latae, iliopsoas and adductors | +| Thigh | Quadriceps with biarticular rectus femoris distinguished from vasti; biarticular hamstrings distinguished from biceps femoris short head | +| Lower leg | Gastrocnemius, soleus and tibialis anterior | + +Upper-limb anatomy supports shoulder, scapular, elbow and forearm distinctions. +Scapular movement is not interchangeable with glenohumeral movement. Stabilizing +the humeral head is not the same task as dynamically rotating the shoulder. +[OpenStax upper-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-5-muscles-of-the-pectoral-girdle-and-upper-limbs) + +Pectoral regions share actions but differ in orientation and contribution across +shoulder positions. Their existence does not establish separate isolatable +“upper” and “lower” chest muscles. Deltoid regions likewise have different +lines of action; the movement identifies the likely emphasis. +[NIH pectoralis anatomy](https://www.ncbi.nlm.nih.gov/books/NBK525991/), +[NIH deltoid anatomy](https://www.ncbi.nlm.nih.gov/books/NBK537056/) + +The hip and knee distinctions are essential: knee extension and knee flexion +are different functions despite both mapping to `thighs`. Gastrocnemius crosses +the knee and ankle; soleus does not cross the knee. Hip flexion changes the +length of biarticular hamstrings but not biceps femoris short head. +[OpenStax lower-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-6-appendicular-muscles-of-the-pelvic-girdle-and-lower-limbs), +[Maeo and colleagues](https://pubmed.ncbi.nlm.nih.gov/33009197/) + +Gluteus minimus abducts and stabilizes the hip. Some lower-limb textbook figure +alternative text describes direction inconsistently, including minimus and +adductor examples; those descriptions are not copied as anatomical truth. +The NIH account corroborates the minimus classification. +[NIH gluteus minimus anatomy](https://www.ncbi.nlm.nih.gov/books/NBK556144/) + +Spinal flexion/extension and hip flexion/extension must remain separate. +Abdominal-wall muscles combine movement and tension/control functions, while +posterior spinal muscles contribute extension and segmental control. +[OpenStax spinal anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-3-axial-muscles-of-the-head-neck-and-back), +[OpenStax trunk anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-4-axial-muscles-of-the-abdominal-wall-and-thorax), +[NIH abdominal-wall anatomy](https://www.ncbi.nlm.nih.gov/books/NBK525975/) + +## Actions, patterns and stabilization + +Joint actions describe motion. Their definitions include shoulder and scapular +actions, elbow flexion/extension, forearm rotation, hip actions, knee actions, +ankle actions and trunk flexion/extension/rotation/lateral flexion. +A loaded return may reverse the visible joint motion while the same agonists +control it eccentrically. +[OpenStax movement terminology](https://openstax.org/books/anatomy-and-physiology-2e/pages/9-5-types-of-body-movements) + +Horizontal/vertical push and pull, knee dominant, hip dominant and single-joint +patterns are Trainlog programming conventions grounded in those actions. +They are not universally standardized anatomical categories or exact torque +ratios. A single-joint exercise can involve many muscles and stabilizers. +A dip grouped as a vertical push is still mechanically different from an +overhead press. + +Trunk stabilization describes resisting an external moment while limiting +motion. Anti-extension, anti-rotation and anti-lateral-flexion are task demands, +not invented joint movements. Locomotion, cyclic pedaling and cyclic rowing +remain distinct; a cardio profile alone does not identify the action sequence. + +## BODY ZONES projection + +`catalog/body-zones-v1.json` remains the authoritative UX taxonomy. Its zones +are coarser than functional anatomy. Forearm muscles fall under `arms`, +posterior trunk muscles can relate to both `back` and `core`, and the lateral +thorax/scapular function of serratus anterior does not fit a simple surface +location rule. Scientific muscle-level projections explain these conventions; +they do not alter persisted exercise relations. + +A secondary BODY ZONE need not list every accessory or stabilizing muscle. +Absence of `shoulders` on a row does not deny posterior-deltoid participation. +Absence of `thighs` on hip abduction does not deny tensor fasciae latae +participation. `full_body` is not an automatic synonym for cardio or a command +to mark every zone. diff --git a/docs/domain/exercise_equipment_interpretation.md b/docs/domain/exercise_equipment_interpretation.md new file mode 100644 index 0000000..381c5be --- /dev/null +++ b/docs/domain/exercise_equipment_interpretation.md @@ -0,0 +1,117 @@ +# Exercise and equipment interpretation + +Scientific knowledge is keyed to existing stable exercise and equipment +identities. Display labels are identification clues. They are not anatomical +proof, runtime classification rules or grounds for merging identities. +`catalog/exercise-knowledge-v1.json` separates reviewed interpretations from +conditional candidates; `catalog/equipment-knowledge-v1.json` describes +physical apparatus and its exercise capabilities. + +The reviewed inventory contains 23 exercise identities and 41 equipment +identities: 38 supplied definitions and three durable custom entries. No +manufacturer or model has been confirmed for the local apparatus. Available +manufacturer examples remain examples; their ratios, trajectories and outcome +claims are not transferred to the local inventory. + +## Reviewed exercise families + +The following are biomechanical interpretations using the stated execution. +They are qualitative classifications, not measurements of individual force +contributions. Anatomy and the references linked below support the rationale. + +| Actual catalog exercise | Interpretation | Existing primary / secondary BODY ZONES | Main qualification | +|---|---|---|---| +| Abdominal crunch | Resisted spinal flexion; rectus and obliques | core | Confirm that movement is not predominantly hip flexion | +| Arm curl | Elbow flexion; biceps, brachialis and brachioradialis | arms | Grip and shoulder support change participation | +| Back extension | Spinal extension; erector spinae and multifidus | back / core | Pelvic restraint determines hip involvement | +| Converging Shoulder Press | Overhead push; deltoids with elbow extension | shoulders / arms | Plane, seat support and scapular freedom unresolved | +| Diverged seated row; Seated row | Horizontal pull; humeral extension and scapular retraction | back / arms | Elbow path determines lat/scapular/posterior-deltoid emphasis | +| Diverging lat pulldown; Lat pull | Vertical pull; shoulder adduction/extension and elbow flexion | back / arms | Grip and linkage do not establish regional isolation | +| Hip abduction | Abduction involving medius/minimus and other abductors | glutes | Tensor fasciae latae and superior maximus contributions vary | +| Hip adduction | Resisted thigh approximation by hip adductors | thighs | Hip and knee angles affect individual muscles | +| Leg extension | Resisted knee extension by quadriceps | thighs | Rectus femoris also crosses hip | +| Leg press | Combined knee/hip extension | thighs / glutes | Machine, depth and foot placement alter moments | +| Prone leg curl | Knee flexion with prone support | thighs | Distinct length context from seated curl | +| Seated Leg; Seated leg curl | Knee flexion with seated support | thighs | Two existing identities retained; common family does not merge IDs | +| Rear Delt | Reverse fly: humeral horizontal abduction with scapular contribution | shoulders / back | Distinct from Pec Fly on the same device | +| Rotary torso | Relative thorax-pelvis rotation; paired oblique action | core | Compatible supplied equipment is not proven occurrence linkage | + +Chest Press priority is addressed through a complete **conditional** anterior +press interpretation: pectoralis major as a primary mover, anterior deltoid and +triceps as synergists, shoulder horizontal adduction with elbow extension, +and `horizontal_push`. The custom `Chest press` and `Converting chest press` +identities have not independently established that execution. Their retained +chest/shoulders/arms relations are plausible historical mappings; the scientific +candidate stays uncertain until apparatus and motion are confirmed. +[NIH pectoralis anatomy](https://www.ncbi.nlm.nih.gov/books/NBK525991/), +[OpenStax upper-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-5-muscles-of-the-pectoral-girdle-and-upper-limbs) + +The custom `Abdominal` entry similarly retains a conditional crunch candidate, +not an asserted execution. Both generic warm-up identities remain unresolved. +`Marche` has a conditional walking interpretation; a continuous/duration profile +and an aggregate treadmill inventory do not establish all execution details or +a specific occurrence association. Unknown new custom exercises remain +unclassified until evidence is added explicitly. + +Seated versus prone curl has direct longitudinal evidence: the studied seated +condition produced greater growth of biarticular hamstrings. This supports +retaining length context while avoiding a universal machine or outcome ranking. +[Maeo and colleagues](https://pubmed.ncbi.nlm.nih.gov/33009197/) + +The leg-press review concerns EMG. It supports quadriceps involvement but does +not establish a robust universal foot-placement recipe or prove that hamstring +coactivation replaces knee-flexion training. +[Martín-Fuentes and colleagues](https://pubmed.ncbi.nlm.nih.gov/32605065/) + +Reverse pec-deck and row/pulldown investigations concern selected EMG tasks. +They corroborate plausible posterior-deltoid and scapular contributions without +ranking long-term growth. Divergent/convergent machine labels add no independent +outcome evidence. +[Franke and colleagues](https://pubmed.ncbi.nlm.nih.gov/24947920/), +[Lehman and colleagues](https://pubmed.ncbi.nlm.nih.gov/15228624/) + +Pelvic stabilization changes lumbar-extensor excitation. A Back Extension +execution dominated by hip motion may deserve different muscle roles from the +spinal-extension interpretation; that decision requires actual geometry and +execution evidence. +[Lee](https://pubmed.ncbi.nlm.nih.gov/26730390/) + +## Multifunction and unrestricted equipment + +A Rear Delt / Pec Fly device supports at least two distinct tasks. Rear Delt +uses shoulder horizontal abduction and a shoulder/back projection. Pec Fly +uses horizontal adduction, with a chest emphasis and possible anterior-deltoid +assistance. Pec Fly has no verified local exercise ID. It remains an unlinked +capability, not a fabricated catalog exercise. + +The assistance device likewise supports separate assisted dip and assisted +chin/pull-up capabilities. Neither has a verified local exercise ID. The +legacy `assisted_dip` and `assisted_chin` strings in the equipment manifest are +not creator exercise identities. Chin-up and pronated pull-up also require +specific grip/execution context. Manufacturer catalogs demonstrate that such +combination products exist, not that these examples identify the local model. +[Life Fitness equipment catalogue](https://www.lifefitness.com.au/wp-content/uploads/2024/02/Life-Fitness-Catalogue_2024_web.pdf) + +Functional trainers, adjustable pulleys, dumbbells, kettlebells, bags, +medicine balls and suspension straps require the actual exercise. Their +presence cannot establish a primary BODY ZONE. Cable height and routing, +attachment, stance and line of pull determine the task. A documented example +with a 4:1 cable ratio illustrates why local ratios must be verified separately. +[Precor RUD0915](https://www.precor.com/en-US/products/RUD0915) + +Benches and guided squat machines require support and movement details. Cardio +apparatus requires mode, speed/cadence, resistance and support context. A rowing +ergometer includes a leg/trunk/arm cycle and is not the same exercise as a +seated resistance row. Battle ropes have mass and inertia: their existing +`bodyweight` catalog category is retained as legacy metadata, not a calibrated +physical load model. Any future vocabulary change needs a separate explicit +compatibility decision. + +## BODY ZONE audit result + +No reviewed finding justifies an automatic persisted-zone change. Existing +assignments are compatible with the identified families or remain conditional +where observation is missing. Additional potential synergists are documented +at muscle level. A future correction must state its evidence, affected stable +identities and explicit migration separately; enrichment never silently +rewrites historical mappings. diff --git a/docs/domain/knowledge_audit.md b/docs/domain/knowledge_audit.md new file mode 100644 index 0000000..3801857 --- /dev/null +++ b/docs/domain/knowledge_audit.md @@ -0,0 +1,29 @@ +# Training knowledge science audit + +Generated deterministically from the six canonical knowledge catalogs. Conditional rows are candidates that require explicit confirmation and do not participate in ordinary resolved queries. + +| ID | Name | Actions | Patterns | Primary | Secondary | Stabilizers | Science primary zone | Science secondary zones | Existing primary zone | Existing secondary zones | Equipment | Confidence | Source refs | Audit status | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| ex_01ff06dd-ad00-46ee-9b46-ed32cabedbef | Hip adduction | hip_adduction | single_joint_hip_adduction | hip_adductors | — | — | thighs | — | thighs | — | hip_adduction | high | openstax_lower | confirmed | +| ex_1872246a-39ae-44dc-b58d-f87e90ca49ab | Leg extension | knee_extension | single_joint_knee_extension | quadriceps | — | — | thighs | — | thighs | — | leg_extension | high | openstax_lower | confirmed | +| ex_1a34814c-2e46-40fc-b1f4-6d60b8e5a3e0 | Rotary torso | trunk_rotation | trunk_rotation | external_oblique, internal_oblique | multifidus | erector_spinae, transversus_abdominis | core | — | core | — | rotary_torso | moderate | nih_abdominal_wall, openstax_back, openstax_trunk | confirmed | +| ex_1b0c6b8b-b05e-4e6f-8809-5f7d85d668de | Diverged seated row | elbow_flexion, scapular_retraction, shoulder_extension, shoulder_horizontal_abduction | horizontal_pull | latissimus_dorsi, rhomboids, trapezius_middle | biceps_brachii, brachialis, brachioradialis, deltoid_posterior, teres_major | erector_spinae, forearm_extensors, forearm_flexors | back | arms | back | arms | diverging_seated_row | moderate | franke_2015, lehman_2004, openstax_upper | confirmed | +| ex_2d488c08-194c-4051-a3c9-34471646c1d3 | Gym échauffement | — | — | — | — | — | — | — | — | — | — | uncertain | — | unresolved | +| ex_33f79331-871c-4eed-babe-346e53a99070 | Seated row | elbow_flexion, scapular_retraction, shoulder_extension, shoulder_horizontal_abduction | horizontal_pull | latissimus_dorsi, rhomboids, trapezius_middle | biceps_brachii, brachialis, brachioradialis, deltoid_posterior, teres_major | erector_spinae, forearm_extensors, forearm_flexors | back | arms | back | arms | seated_row | moderate | franke_2015, lehman_2004, openstax_upper | confirmed | +| ex_34a5c9c3-c032-4dfb-be32-8bf09832f72b | Converting chest press | elbow_extension, shoulder_horizontal_adduction | horizontal_push | pectoralis_major | deltoid_anterior, triceps_brachii | infraspinatus, subscapularis, supraspinatus, teres_minor | chest | arms, shoulders | chest | arms, shoulders | eq_c660cd61-5b17-498e-8d51-9eafcf7e2731 | uncertain | nih_deltoid, nih_pectoralis, openstax_upper | questionable | +| ex_43c7375f-934c-4650-930c-45807d2f2929 | Gym/Échauffement | — | — | — | — | — | — | — | — | — | — | uncertain | — | unresolved | +| ex_474ec393-3efa-4aaa-8e08-1a0245ed7835 | Back extension | trunk_extension | trunk_extension | erector_spinae | multifidus | external_oblique, internal_oblique, rectus_abdominis | back | core | back | core | back_extension | moderate | lee_2015, openstax_back, openstax_lower | questionable | +| ex_4bd03d55-e644-436e-876e-07837824bde5 | Abdominal | trunk_flexion | trunk_flexion | rectus_abdominis | external_oblique, internal_oblique | transversus_abdominis | core | — | core | — | eq_3f987a36-bbb4-4e29-9c9f-1f201f1596e0 | uncertain | nih_abdominal_wall, openstax_trunk | questionable | +| ex_4cd2433e-80b1-478a-b8df-73fc6ef80962 | Rear Delt | scapular_retraction, shoulder_horizontal_abduction | single_joint_shoulder_horizontal_abduction | deltoid_posterior | rhomboids, trapezius_middle | infraspinatus, subscapularis, supraspinatus, teres_minor | shoulders | back | shoulders | back | rear_delt_pec_fly | moderate | franke_2015, nih_deltoid, openstax_upper, vigotsky_2018 | confirmed | +| ex_617007f9-7420-4408-91b9-8ffb77900f13 | Seated Leg | knee_flexion | single_joint_knee_flexion | hamstrings | gastrocnemius | — | thighs | — | thighs | — | seated_leg_curl | high | maeo_2021, openstax_lower | confirmed | +| ex_6dfc7ffd-8891-464e-a995-808baf1b0d7b | Converging Shoulder Press | elbow_extension, scapular_upward_rotation, shoulder_abduction, shoulder_flexion | vertical_push | deltoid_anterior, deltoid_middle | serratus_anterior, trapezius_lower, trapezius_upper, triceps_brachii | infraspinatus, subscapularis, supraspinatus, teres_minor | shoulders | arms | shoulders | arms | converging_shoulder_press | moderate | nih_deltoid, openstax_upper | confirmed | +| ex_7e7cf906-2214-4066-bcb7-c16382d83b3b | Hip abduction | hip_abduction | single_joint_hip_abduction | gluteus_medius, gluteus_minimus | gluteus_maximus, tensor_fasciae_latae | — | glutes | — | glutes | — | hip_abduction | moderate | nih_gluteus_minimus, openstax_lower | confirmed | +| ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af | Chest press | elbow_extension, shoulder_horizontal_adduction | horizontal_push | pectoralis_major | deltoid_anterior, triceps_brachii | infraspinatus, subscapularis, supraspinatus, teres_minor | chest | arms, shoulders | chest | arms, shoulders | eq_0a462c1e-9fb6-4fd7-b0a2-53a0e86f33c6 | uncertain | nih_deltoid, nih_pectoralis, openstax_upper | questionable | +| ex_9adf7566-f10c-443c-b63b-681d37665693 | Abdominal crunch | trunk_flexion | trunk_flexion | rectus_abdominis | external_oblique, internal_oblique | transversus_abdominis | core | — | core | — | abdominal | moderate | nih_abdominal_wall, openstax_trunk | confirmed | +| ex_a1ef5047-b44b-4c64-a6ed-c7a3bc13b163 | Seated leg curl | knee_flexion | single_joint_knee_flexion | hamstrings | gastrocnemius | — | thighs | — | thighs | — | seated_leg_curl | high | maeo_2021, openstax_lower | confirmed | +| ex_a72fa713-4b0e-431d-95e2-42d95beb77b1 | Lat pull | elbow_flexion, scapular_downward_rotation, shoulder_adduction, shoulder_extension | vertical_pull | latissimus_dorsi | biceps_brachii, brachialis, brachioradialis, teres_major | forearm_extensors, forearm_flexors, infraspinatus, subscapularis, supraspinatus, teres_minor | back | arms | back | arms | lat_pull | moderate | lehman_2004, openstax_upper, vigotsky_2018 | confirmed | +| ex_b1e6ffc6-75b5-45ff-a3c0-e7433c58013d | Marche | ankle_dorsiflexion, ankle_plantarflexion, hip_extension, hip_flexion, knee_extension, knee_flexion | locomotion | gastrocnemius, gluteus_maximus, quadriceps, soleus | hamstrings, iliopsoas, tibialis_anterior | erector_spinae, gluteus_medius, gluteus_minimus | full_body | — | — | — | — | uncertain | nih_gluteus_minimus, openstax_lower | unresolved | +| ex_b432623f-bfe9-4daf-a653-60ec7fdffbde | Leg press | hip_extension, knee_extension | knee_dominant | quadriceps | adductor_magnus, gluteus_maximus | gastrocnemius, soleus | thighs | glutes | thighs | glutes | leg_press, plate_loaded_leg_press | moderate | martin_fuentes_2020, openstax_lower, vigotsky_2018 | confirmed | +| ex_b4d1daf1-de4a-4016-abdf-487bf6014ce6 | Diverging lat pulldown | elbow_flexion, scapular_downward_rotation, shoulder_adduction, shoulder_extension | vertical_pull | latissimus_dorsi | biceps_brachii, brachialis, brachioradialis, teres_major | forearm_extensors, forearm_flexors, infraspinatus, subscapularis, supraspinatus, teres_minor | back | arms | back | arms | diverging_lat_pulldown | moderate | lehman_2004, openstax_upper, vigotsky_2018 | confirmed | +| ex_d7398d9f-d928-4d2e-94e9-74e201da55c5 | Prone leg curl | knee_flexion | single_joint_knee_flexion | hamstrings | gastrocnemius | — | thighs | — | thighs | — | prone_leg_curl | high | maeo_2021, openstax_lower | confirmed | +| ex_ec619fc2-4685-4044-873c-86764bd4a0fe | Arm curl | elbow_flexion | single_joint_elbow_flexion | biceps_brachii, brachialis | brachioradialis | forearm_extensors, forearm_flexors | arms | — | arms | — | arm_curl | high | openstax_upper | confirmed | diff --git a/docs/domain/knowledge_system.md b/docs/domain/knowledge_system.md new file mode 100644 index 0000000..815ea69 --- /dev/null +++ b/docs/domain/knowledge_system.md @@ -0,0 +1,129 @@ +# Training knowledge system V1 + +`TRAINING_KNOWLEDGE_V1` is an implemented read-only, evidence-linked knowledge +layer with lifecycle status `TRAINING_KNOWLEDGE_V1=PASS`. Its bounded scientific +review, independent temporal review, final engineering audit, repair +verification, and final executable validation passed. It does not change the frozen +exchange formats, either SQLite schema, synchronization, +or the meaning of an exercise, BODY ZONE, occurrence, set, or measured maximum. + +The authored scientific source is the six versioned JSON catalogs in +[`catalog/`](../../catalog/): + +- `science-references-v1.json` (23 references); +- `muscles-v1.json` (53 muscles); +- `joint-actions-v1.json` (35 joint actions); +- `movement-patterns-v1.json` (26 movement patterns); +- `exercise-knowledge-v1.json` (23 exercise records); +- `equipment-knowledge-v1.json` (41 equipment records). + +The catalog loader and validator enforce IDs, ordering and cross-references. +The C representation is generated from those assets; Android loads the same +assets through `TrainingKnowledgeCatalog`. Neither C nor Kotlin contains a +manually maintained duplicate scientific table. The generated knowledge audit +is an audit artifact and is not an authored source or a file to edit directly; +the retained review-oriented [knowledge audit](knowledge_audit.md) and +[`training-knowledge-audit-v1.json`](../../catalog/training-knowledge-audit-v1.json) +provide the current navigable audit records. + +The catalog contains no runtime history and does not seed either database. +Unknown future exercise and equipment IDs remain valid runtime data. A display +name, legacy slug, equipment family, or catalog label never substitutes for an +actual `ex_` exercise ID. Consequently the documented capabilities +for Pec Fly, Assisted Dip and Assisted Chin do not create associations until a +real runtime exercise UUID exists. The two Leg press equipment variants remain +separate contexts; the unobserved Rotary compatibility never enters history. + +## Scientific scope and uncertainty + +Scientific mappings describe anatomy, mechanics, movement patterns and the +BODY ZONE projection used by the knowledge catalog. They do not overwrite the +persisted primary/secondary BODY ZONE relations selected for a runtime +exercise. Source type, notes, limitations, confidence and conditional +interpretation remain available through the APIs. + +The current inventory has six high-confidence, eleven moderate-confidence and +six uncertain exercise records. It represents 20 initial zone-catalog exercise +IDs plus three further reviewed IDs, 38 supplied equipment IDs and three +observed custom IDs. The BODY ZONE audit has 16 confirmed, four questionable +and three unresolved records. It records no database mutation. `Marche` is a +conditional candidate and remains excluded by normal resolved-knowledge +filters; two warmups and the custom Chest press, Converting and Abdominal +entries remain unresolved until their execution is known. Across equipment, +21 records are scientifically documented, 17 are mechanically identified with +incomplete anatomy, and three have uncertain equipment identity. All 41 have +unknown manufacturer and model: a generic-family source does not prove a local +machine model. + +The detailed evidence and limitations are in +[anatomy and movement](anatomy_and_movement.md), +[exercise and equipment interpretation](exercise_equipment_interpretation.md), +[programming foundations](programming_foundations.md), and the central +[`science-references-v1.json`](../../catalog/science-references-v1.json). +Project domain decisions follow the +[`trainlog-anatomy` guidance](../../.agents/skills/trainlog-anatomy/SKILL.md). + +## Read-only application contracts + +On desktop, [`training_knowledge.h`](../../tui/include/trainlog/training_knowledge.h) +provides immutable lookup, enumeration and resolved-knowledge query APIs. All +strings and records are borrowed generated storage with process-lifetime +validity; stable IDs, rather than labels, are keys. The query AND-combines its +optional scientific-zone, movement-pattern, muscle-role and available-equipment +filters. Conditional and unresolved entries cannot match it. + +[`training_context.h`](../../tui/include/trainlog/training_context.h) composes +one exact runtime `exercise_id` with its persisted zones, optional scientific +record, compatible equipment, latest explicit MAX, and actual occurrence/set +history. Missing science is valid; it never fabricates history. One load uses a +nested-safe read snapshot. It accepts an occurrence limit of 1–32 and a set +preview limit of 1–64 per occurrence; empty history remains empty. Follow-up cursors order current data by the +exact represented instant, then bytewise session ID and occurrence ID; they are not a +snapshot across calls, including when equivalent instants use different +offset text. + +The corrected C and Android readers use one explicit parser/comparator policy +for stored timestamps and exclusive cursors. They admit extended dates, +`T/t`, `Z/z`, offsets through `23:59`, arbitrary exact dot fractions and the +Android writer's omitted-seconds form. They scan all matching metadata and +retain bounded winners before hydration; no SQLite date parser selects the +page. Source text and identities remain unchanged. Malformed timestamps fail +explicitly, as does a selected value beyond C's existing output capacity. +The [temporal correction record](../reviews/training_knowledge_v1_temporal_contract.md) +documents parser-only extensions, limits and passing production regressions. + +Android exposes the same scientific lookups through `TrainingKnowledgeCatalog` +and composes the same boundary through +`TrainlogRepository.getTrainingExerciseContext()`, +`listExerciseOccurrences()` and `listExerciseOccurrenceSets()`. Its cursors +have the same current-data, chronological contract. Returned runtime context +keeps persisted zone IDs separate from the scientific mapping, includes actual +equipment and the latest explicit MAX, and preserves raw per-set values. + +No API infers range of motion, setup, actual force, or comparability of raw +kilogram labels. Equipment, resistance semantics and execution context remain +explicit. There are no prescriptions in V1. + +## Documented future pipeline only + +The following is an architectural boundary for later work, not a V1 generator, +scoring algorithm, proposal, database change, or user-interface behavior. + +```text +Session inputs + BODY ZONE, duration, goal, available equipment, recent history + -> movement functions + -> real resolved candidates + -> explicit availability + -> recent history and explicit-MAX context + -> future fatigue/recent-coverage interpretation + -> a future proposal +``` + +Candidate selection must seek diverse movement patterns rather than repeatedly +selecting the same muscle. A future multi-session program would additionally +take the goal, available days, functions/zones, recent frequency, recovery, +progression, equipment and history, then reason across sessions. Medical +constraints would be explicit inputs; no diagnosis or rehabilitation status is +to be inferred. V1 prescribes no exercises, weights, sets, fatigue scores, +progression or schedule. diff --git a/docs/domain/programming_foundations.md b/docs/domain/programming_foundations.md new file mode 100644 index 0000000..c05ebaf --- /dev/null +++ b/docs/domain/programming_foundations.md @@ -0,0 +1,93 @@ +# Programming foundations and MAX context + +TRAINING KNOWLEDGE V1 supplies knowledge for future reasoning about training. +It does not generate workouts, prescribe loads or provide clinical +rehabilitation advice. Principles below distinguish intervention findings from +Trainlog's practical interpretation of them. + +## Adaptation and exposure + +Resistance-training interventions improve strength and muscle size across many +studied configurations. Higher loads tend to favor high-load strength outcomes; +multiple sets characterize better-ranked hypertrophy configurations in a large +network meta-analysis. These are population-level comparisons, not an optimal +program for every person. Load, sets and frequency interact. +[Currier and colleagues](https://pubmed.ncbi.nlm.nih.gov/37414459/) + +Volume can mean sets, repetitions or accumulated load. Those are different +quantities. Machine kilograms multiplied by repetitions do not establish +equal muscle exposure across different devices. Direct and indirect muscle +participation cannot be assigned universal equal set credit. Frequency helps +distribute exposure and fatigue; no weekly frequency or unbounded more-volume +rule is encoded here. + +Relative external load and effort are also different. A heavy task need not +end in failure; a lighter task can. Evidence does not establish that momentary +failure is generally required for strength or hypertrophy, but it does not +imply that arbitrarily easy sets provide the same stimulus. Failure definitions, +actual effort and compared volume constrain interpretation. +[Grgic and colleagues](https://pubmed.ncbi.nlm.nih.gov/33497853/), +[Refalo and colleagues](https://pubmed.ncbi.nlm.nih.gov/36334240/) + +Failure conditions generally produce greater acute fatigue in the reviewed +experiments. Acute markers cannot supply a universal recovery clock or predict +an individual's long-term result. Repeated performance, actual execution and +context matter when a future system interprets recovery. +[Vieira and colleagues](https://pubmed.ncbi.nlm.nih.gov/34881412/) + +## Progression, selection and coverage + +Progression and specificity require consistent definitions of the task and +its successful performance. The historical ACSM position stand supplies these +organizing principles; its numeric schedules and load recommendations are not +adopted as V1 rules. Comparative outcomes are interpreted using the subsequent +reviews above. +[ACSM progression position stand](https://pubmed.ncbi.nlm.nih.gov/19204579/) + +Exercise variation can change regional exposure and task practice. The limited +intervention literature supports considering purposeful variation, not a +requirement to randomize exercises or change them as often as possible. Stable +exercise context also makes progress interpretable. +[Kassiano and colleagues](https://pubmed.ncbi.nlm.nih.gov/35438660/) + +Practical selection should compare the intended joint action, muscle role, +range of motion, stability demand and available equipment. BODY ZONES alone +cannot establish a substitute. Leg extension and leg curl share `thighs` while +training opposing knee actions. Seated and prone curls share knee flexion but +differ in the length context of biarticular muscles. +[OpenStax lower-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-6-appendicular-muscles-of-the-pelvic-girdle-and-lower-limbs), +[Maeo and colleagues](https://pubmed.ncbi.nlm.nih.gov/33009197/) + +Coverage therefore considers movement functions as well as zones: horizontal +and vertical pushing/pulling, knee and hip extension, knee flexion, hip +abduction/adduction, plantarflexion, trunk motion and resisting trunk motion. +This is an authored reasoning aid. It is not a universal checklist that proves +a program balanced, effective or suitable for a particular individual. + +## What a recorded MAX can establish + +`max_weight_kg` records an observed result owned by an exercise occurrence. +Interpreting it requires its resistance and execution context. At minimum, +consider exercise identity, physical equipment, external-versus-assistance +mode, machine settings, range of motion, support, technique and success +criterion. Unknown context stays unknown. + +An explicit MAX result without repetition count is not proven to be a one-rep +maximum. Ordinary sets do not become measured maxima by being the heaviest +available observation. No estimated 1RM or percentage prescription follows +from the value alone. A shared BODY ZONE, movement family or machine does not +make distinct exercises' results interchangeable. + +Stack labels need not equal handle resistance. Pulley routing, lever arms, +cams, tare and friction affect external moments; human moment arms add further +variation. Manufacturer documentation for an identified example demonstrates +a non-unit cable ratio, but supplies no conversion for the unconfirmed local +equipment. +[Precor pulley documentation](https://www.precor.com/en-US/products/RUD0915) + +For otherwise matched assisted performance, less help represents greater +unsupported demand. Body mass, assistance mechanism and motion remain part of +the comparison. Do not treat assistance as added external weight or assume +that body mass minus displayed assistance gives a calibrated effective load. +The source exercise and resistance context must remain visible when knowledge +is composed with history. diff --git a/docs/exercise_data_model.md b/docs/exercise_data_model.md index ac82e04..14d0a58 100644 --- a/docs/exercise_data_model.md +++ b/docs/exercise_data_model.md @@ -317,3 +317,30 @@ The newest successful explicit test is the current measured result. A separate same-mode historical record may be older. No estimated 1RM is mixed into this contract. + +## 12. Read-only training knowledge context + +Training Knowledge V1 adds no exercise profile field and no persisted training +prescription. It can associate a real stable `exercise_id` with source-linked +scientific knowledge, compatible equipment and a separate scientific BODY ZONE +projection. That projection is not the persisted primary/secondary BODY ZONE +classification and cannot rewrite it. + +The composed runtime view contains the actual exercise profile, persisted +zones, compatible equipment, latest explicit MAX and bounded chronological +occurrences with raw set values. Unknown custom and future runtime IDs remain +valid and may have no knowledge record. A name, legacy slug or generic machine +capability is never made into a phantom UUID association. Raw kilogram labels, +ROM, setup and actual force retain their recorded/unknown context; they do not +become interchangeable performance measurements. + +See [Training knowledge system V1](domain/knowledge_system.md) for catalog +ownership, confidence and the documented-only future planning boundary. +The current implementation lifecycle is `TRAINING_KNOWLEDGE_V1=PASS`. Occurrence pagination and latest +explicit MAX use the settled shared temporal policy: the admitted source text +is preserved, instants and arbitrary dot fractions compare exactly, and equal +instants use bytewise session and entry ID ties. Cursors are exclusive and use +the same comparison; malformed cursors or matching stored timestamps report +explicit errors. The initial audit's stale temporal documentation, Android +loader, Meson input, and C role-only query findings were repaired and +independently verified; final validation passed. diff --git a/docs/reviews/training_knowledge_v1_engineering_review.md b/docs/reviews/training_knowledge_v1_engineering_review.md new file mode 100644 index 0000000..04649fe --- /dev/null +++ b/docs/reviews/training_knowledge_v1_engineering_review.md @@ -0,0 +1,122 @@ +# Training Knowledge V1 — engineering checkpoint + +Date: 2026-09-09. Status: `TRAINING_KNOWLEDGE_V1_ENGINEERING_REVIEW=PASS`. + +Scientific review separately passed within its documented scope and uncertainty. +The independent temporal delta review returned `TEMPORAL_DELTA_REVIEW=PASS` +with no findings. One configured final-reviewer completed an initial full-tranche +audit, reported the findings recorded below, and its authorized bounded repair +chain completed. Independent bounded verification returned +`FINAL_REVIEW_REPAIR_VERIFICATION=PASS`; fresh final executable validation also +passed. `TRAINING_KNOWLEDGE_V1=PASS`. + +## Current implementation and contract + +Six canonical scientific catalogs feed generated C data and the Android asset +loader. Both clients expose scientific lookup/filter APIs and runtime context +composition. Android has a collapsible knowledge section; the TUI has a UTF-8 +cell-aware scrolling knowledge screen tested at 72×20. No schema, scientific +catalog, frozen exchange artifact or stored user history changed in this resume. + +The temporal repair covers the two new C readers and their Android counterparts: +occurrence pagination and latest explicit MAX. The established operational +profile and parser-only extension classification are recorded in the +[temporal contract](training_knowledge_v1_temporal_contract.md). All use exact +integer-second/fraction comparisons and bytewise stable-ID ties. Pagination +scans all matching metadata, retains at most `limit + 1`, then hydrates only +selected rows under one read snapshot. Emitted cursors preserve source text. + +C has the existing 40-character timestamp output limit: long valid candidates +are compared exactly, and a selected unrepresentable result fails explicitly. +Invalid caller cursors and malformed stored timestamps have distinct error +paths. No malformed date can disappear through SQLite NULL comparison. + +Python validation uses the same explicit grammar and exact chronology. The +parent reproduced and repaired an optional-checker dependency issue after the +worker implementation: a strict JSON Schema checker could reject omitted +seconds before Trainlog validation. A private per-call override now aligns the +schema-format check while preserving all other checker configuration. Actual +validation-path tests cover absent, strict and permissive optional checkers. + +## Original defect retained as historical evidence + +The original implementation combined an offset-limited cursor guard with +SQLite `julianday()` and a different Android parser. Its production probe was: + +| Newer source timestamp | Original outcome | Current independent probe | +|---|---|---| +| `2026-09-10T10:00:00+02:00` | control passed | PASS | +| `2026-09-10T10:00:00+14:30` | emitted cursor rejected | PASS | +| `2026-09-10T10:00:00+15:00` | newer occurrence omitted | PASS | +| `2026-09-10t10:00:00Z` | newer occurrence omitted | PASS | + +The independent current probe extends this to twelve spellings. Every case +uses two sessions in a temporary database, page size one, exact source/cursor +assertions, continuation and exhaustion. Both normal and sanitized builds pass. +Original evidence remains `/tmp/trainlog-knowledge-validation/cursor-probe.c` +and `cursor-probe.log`; fresh final probe evidence is under +`/tmp/trainlog-knowledge-review-resume/`. + +## Final validation evidence + +| Check | Result | +|---|---| +| `meson compile -C build` | PASS, fresh strict warning-as-error build | +| `meson test -C build --print-errorlogs` | 42 passed | +| Knowledge validator/generation tests | PASS; eight Python tests | +| Temporal Python tests | PASS; four tests including optional-checker independence | +| JSON/import-contract validators | PASS; six import cases | +| Three standalone public C17 headers | PASS, including `-pedantic-errors` | +| Two targeted C tests under ASan/UBSan | PASS; timestamp Python test also passes in that test invocation | +| Independent twelve-form C production probe | PASS normally and under ASan/UBSan | +| Android unit tests and debug assembly, Java 17 | PASS; 56 tests, zero failures/errors, one known fixture skip | +| Project skill validator and `git diff --check` | PASS | + +The known Android skip is `RealAndroidV9BodyZonesMigrationTest`, because its +external `TRAINLOG_ANDROID_V9_FIXTURE` is unavailable. No knowledge/temporal +test is skipped. Manual MTP, installed-device UI and real-terminal visual +checks were not performed or claimed. Sanitizer evidence is scoped to the +new affected C paths, not the complete GUI executable. + +## Independent review and final-review repair record + +The isolated temporal review was independent of the original implementation +and reviewed grammar/calendar constraints, offset bounds, fractions, byte ties, +cursor aliasing/exclusivity, source capacity, snapshots, and Android/Python +parity. It ran Meson `training_context` and `timestamp_validation` plus four +Python temporal tests, returned `TEMPORAL_DELTA_REVIEW=PASS`, and required no +repair. + +The configured final-reviewer then audited the full tranche. Its initial result +was FAIL and identified these concrete findings: + +| Severity | Finding | Authorized repair recorded in `/tmp/trainlog-knowledge-review-resume/repair.md` | +|---|---|---| +| BLOCKER | Stale temporal-defect documentation in `architecture.md`, `exercise_data_model.md`, and `tui.md` | Replaced claims of an unresolved reader defect with the independently passing temporal contract and correct repair/validation state. | +| BLOCKER | Android loader parity holes | Enforced canonical exercise/equipment identity syntax, reverse capability compatibility, HIGH evidence source type, and non-unresolved BODY ZONE audit evidence; added loader regressions. | +| BLOCKER | Meson generator inputs omitted `body-zones-v1.json` and `equipment-v1.json` although validation reads them | Declared both files as `training_knowledge_generated` inputs and proved regeneration without output-byte change. | +| HIGH | C query accepted a muscle role without a muscle ID, unlike Android's paired filter | Rejected role-only queries, documented the paired contract in the public header, and added a C regression. | + +The repair report records strict C17 syntax, knowledge validation, generated +output equality, targeted Meson test, Android loader tests, dependency proof, +and `git diff --check` as PASS. Independent bounded verification closed all +four findings and explicitly returned +`FINAL_REVIEW_REPAIR_VERIFICATION=PASS` and +`TRAINING_KNOWLEDGE_V1_ENGINEERING_REVIEW=PASS`. The fresh tester matrix then +passed: strict build; 42 Meson tests; eight knowledge and four temporal Python +tests; knowledge/JSON/import validators; three strict C17 headers; affected C +knowledge/context tests under ASan/UBSan plus timestamp validation; normal and +sanitized 12-form temporal probes; Android 56 tests with zero failures/errors +and one known missing-fixture skip; Java 17 debug assembly; skill validation; +and diff check. Fresh generated C is byte-identical with SHA-256 +`e8c099f67eb111d61621b5d76592c049823af5508d43e73ec646f22e4c377fca`; all six +Android assets are byte-identical. + +## Remaining observations + +The prior bounded review's secondary scientific BODY ZONE IDs on the TUI +knowledge page remain a nonblocking presentation inconsistency. Local machine +model/execution uncertainty and missing real exercise UUIDs remain as recorded +in the scientific review; no phantom identity or scientific reassessment was +introduced. Older lexical timestamp readers, arbitrary-length C output and +verified leap-event support are outside this bounded repair. diff --git a/docs/reviews/training_knowledge_v1_resume.md b/docs/reviews/training_knowledge_v1_resume.md new file mode 100644 index 0000000..dc99864 --- /dev/null +++ b/docs/reviews/training_knowledge_v1_resume.md @@ -0,0 +1,120 @@ +# Training Knowledge V1 — continuation record + +Checkpoint: 2026-09-09. Status: `TRAINING_KNOWLEDGE_V1=PASS`. + +This is a completed closeout record. Do not restart science, temporal +implementation, broad audit, or the bounded repair chain. The independent +temporal review returned `TEMPORAL_DELTA_REVIEW=PASS` with no findings. One +deep final audit initially failed, one bounded repair chain closed its four +findings, independent verification returned +`FINAL_REVIEW_REPAIR_VERIFICATION=PASS` and +`TRAINING_KNOWLEDGE_V1_ENGINEERING_REVIEW=PASS`, and fresh final executable +validation passed. + +## Baseline and protections + +- Repository: `/home/fy59/Documents/trainlog`. +- Baseline HEAD: `1aad7b48b3143783ccfd326c0145f495fd568442`. +- The initial worktree was clean; the current delta belongs to this tranche. +- This resume initially matched every file hash in the preserved final manifest. +- No stage, commit, push, reset, restore, stash or clean was performed. +- Desktop schema remains v11 and Android schema remains v10. +- Real database logical contents/counts and all scientific/frozen catalogs are + preserved. No device install, uninstall, keystore or user-history change. + +## Completed during the temporal correction + +1. `advisor` / Terra-high completed the isolated contract analysis. Its retained + handoff is `/tmp/trainlog-temporal-contract-settled.md`; the durable repository + record is [temporal contract](training_knowledge_v1_temporal_contract.md). +2. `worker` / Sol-medium implemented exact C/Kotlin timestamp parsers, + scan/select/hydrate occurrence and latest-MAX reads, coherent exclusive + cursors, nested read snapshots and production regressions. Python validation + uses the same grammar and exact fractional comparison. +3. Parent's independent production C probe passed all twelve temporal spellings + through two sessions, page size one, source/cursor equality and exhaustion. +4. A bounded additional validator defect was reproduced: a strict optional + JSON Schema format checker could still reject the Android no-seconds form + before semantic validation. A per-validation `date-time` override now uses + the Trainlog parser without mutating global/caller checkers. Document-path + regressions cover missing, strict and permissive checkers. +5. Full executable validation passed, including the independent C probe under + ASan/UBSan. Canonical documentation describes the settled behavior. + +Scientific review remains PASS within its recorded scope; no scientific delta +was introduced. Its reviewed catalog hashes remain applicable. + +## Completed independent review and repair chain + +The independent temporal reviewer covered grammar, calendar and offset ranges, +fractions, byte ties, cursor aliasing/exclusivity, source capacity, snapshots, +and Android/Python parity. It ran the targeted Meson and four Python temporal +tests and returned `TEMPORAL_DELTA_REVIEW=PASS`, with no temporal repair. + +The one final-reviewer audited the complete tranche. Its initial FAIL found +stale temporal-defect documentation (BLOCKER), Android loader parity holes +(BLOCKER), missing Meson generator dependencies (BLOCKER), and C acceptance of +a role-only muscle query (HIGH). The authorized repair report is +`/tmp/trainlog-knowledge-review-resume/repair.md`: it records the exact loader +constraints, Meson input declarations, C paired-filter rejection, regressions, +and repair-scope validation. Independent review of that delta closed all four +findings in `/tmp/trainlog-knowledge-review-resume/repair-verification.md`. +No scientific catalog, schema, frozen format, temporal implementation, or +user-history content changed. + +## Closeout evidence + +Fresh final validation recorded in +`/tmp/trainlog-knowledge-review-resume/full-results.json`, +`android-results.json`, `sanitizer-results.json`, +`probe-normal-results.json`, and `android-counts.json` passed: strict build; +42 Meson tests; eight knowledge and four temporal Python tests; +knowledge/JSON/import validators; three strict C17 headers; affected C +knowledge/context tests under ASan/UBSan plus timestamp validation; normal and +sanitized independent 12-form temporal probes; Android 56 tests with zero +failures/errors and one known missing-real-v9-fixture skip; Java 17 debug +assembly; skill validation; and diff check. Fresh generated C is byte-identical +with SHA-256 `e8c099f67eb111d61621b5d76592c049823af5508d43e73ec646f22e4c377fca`; +all six Android assets are byte-identical. + +Preservation before and after repair confirms desktop schema v11 and Android +schema v10; unchanged frozen/scientific catalogs and temporal implementation; +real database logical SHA-256 +`26139cafeffbde3ec08f6ef23c5069e75afb9cd69ffffb40be5c099006fedc4d`; counts +of 1 body observation, 6 continuous activities, 3 custom equipment, 23 zone +sync rows, 32 exercise-zone rows, 23 exercises, 19 max results, 28 performed +sets, 31 occurrences, and 3 sessions; `PRAGMA integrity_check = ok`; clean +foreign-key check; and an empty index. Current closeout capture locations are +`/tmp/trainlog-knowledge-review-resume/final.patch` and +`/tmp/trainlog-knowledge-review-resume/final-manifest.json`. The completed capture +includes all 19 modified tracked files and 37 untracked files, validates their +hashes and whitespace, and confirms the real index remained untouched and empty. + +## Validation and retained evidence + +- 42 desktop tests; eight Python knowledge tests; four Python temporal tests. +- Android tests and debug assembly: 56 tests, zero failures/errors, one skipped + `RealAndroidV9BodyZonesMigrationTest` because `TRAINLOG_ANDROID_V9_FIXTURE` + is unavailable. No knowledge/temporal test is skipped. +- Catalog, JSON and import-contract validators; project skill validator. +- Three public C17 headers with `-pedantic-errors` and strict warning flags. +- Two affected C tests under ASan/UBSan, plus the temporal Python test; independent + twelve-form production-API probe also passes under ASan/UBSan. +- `git diff --check` passes. Manual MTP/device/visual checks remain unclaimed. + +Current logs/results and reusable runner include +`/tmp/trainlog-knowledge-review-resume/full-results.json`, +`android-results.json`, `android-counts.json`, `sanitizer-results.json`, and +`probe-normal-results.json`. +Reusable sanitizer build: `/tmp/trainlog-knowledge-validation/build`. +Implementation record: `/tmp/trainlog-temporal-implementation.md`. + +The older `/tmp/trainlog-knowledge-final.*` files are a previous checkpoint, +not current complete evidence. Durable contract/evidence is in this review +directory when `/tmp` is absent. + +Known explicit limits: C output timestamp capacity 40 characters with explicit +failure for a selected longer value; no leap-event table (`:60` rejected); +O(N) metadata scan per page; older unrelated lexical history readers remain +future scope. No source-text normalization, migration or history rewrite is +part of this correction. diff --git a/docs/reviews/training_knowledge_v1_scientific_review.md b/docs/reviews/training_knowledge_v1_scientific_review.md new file mode 100644 index 0000000..8ff46f8 --- /dev/null +++ b/docs/reviews/training_knowledge_v1_scientific_review.md @@ -0,0 +1,154 @@ +# TRAINING KNOWLEDGE V1 — final scientific review + +Date: 2026-09-09. Result: **PASS within the documented scope and uncertainty**. +No blocking scientific correction or confidence downgrade is required. + +This bounded review assessed the six canonical scientific catalogs against the +previously researched source corpus, reviewed staging interpretations and +`schema_supplement.json`. It also inspected the generated BODY ZONE audit, +`docs/domain/` synthesis and Android's resolved-versus-conditional query guard. +It is a scientific review, not a build, persistence or UI validation report. +No production code, canonical catalog, database or persisted relation was edited +by this review. + +## Coverage and fidelity + +| Canonical collection | Count | Finding | +|---|---:|---| +| Scientific references | 23 | Bibliography, identifiers, source types, limitations and claims preserved | +| Functional muscle entities | 53 | Muscles, regions and groups remain distinct; membership overlap documented | +| Joint actions | 35 | Actions, joint complexes, nominal planes and non-exhaustive contributors preserved | +| Movement patterns | 26 | Authored conventions, French names, zones and anti-motion distinctions preserved | +| Exercise identities | 23 | 17 resolved families, 4 conditional candidates, 2 unresolved warm-ups | +| Equipment identities | 41 | 38 supplied and 3 custom; manufacturer and model remain null throughout | +| BODY ZONE audit rows | 23 | 16 confirmed, 4 questionable, 3 unresolved; no proposed mutation | + +Scientific fields compare equal to the settled staging data plus the reviewed +supplement after ignoring array order. References compare equal under the +canonical field renaming. The rotary-torso compatibility representation follows +`representation-decisions.md`: a symmetric UUID association marked +`catalog_compatible_not_observed_occurrence` does not assert observed history. + +The exercise confidence distribution is 6 `high`, 11 `moderate` and 6 +`uncertain`. The equipment scientific-status distribution is 21 +`scientifically_documented`, 17 `mechanically_identified_anatomy_incomplete` +and 3 `equipment_identity_uncertain`. Scientific documentation of a generic +machine family does not certify its local model, trajectory or calibration. + +## All six high-confidence exercise mappings + +Here `high` applies to the explicitly limited movement-family interpretation: +the relevant action, plausible muscle roles and coarse BODY ZONE projection. +It does not assert measured local force shares, a known manufacturer, exact +resistance curves, equal adaptation or transferable MAX. The source hierarchy +supports these bounded claims without requiring an EMG study for every simple +anatomical action. + +| Stable exercise ID | Identified variant and equipment | Actions, roles and zones | Evidence assessment | +|---|---|---|---| +| `ex_01ff06dd-ad00-46ee-9b46-ed32cabedbef` | Hip adduction; recorded `hip_adduction` context | Resisted hip adduction; adductor group; `thighs` | High retained. Established adductor function supports the family mapping; individual contributions depend on hip/knee position. | +| `ex_1872246a-39ae-44dc-b58d-f87e90ca49ab` | Leg extension; recorded `leg_extension` context | Knee extension; quadriceps; `thighs` | High retained. Rectus femoris/vasti distinction and alignment/hip-angle limitations are explicit. | +| `ex_617007f9-7420-4408-91b9-8ffb77900f13` | Seated Leg; recorded `seated_leg_curl` establishes curl interpretation beyond the ambiguous label | Knee flexion; hamstrings with possible gastrocnemius assistance; `thighs` | High retained. Equipment provenance identifies the variant; no name heuristic or identity merge. | +| `ex_a1ef5047-b44b-4c64-a6ed-c7a3bc13b163` | Seated leg curl; recorded `seated_leg_curl` | Knee flexion; hamstrings with gastrocnemius assistance; `thighs` | High retained. Hip-flexion length effect is confined to biarticular hamstrings; no outcome equivalence claimed. | +| `ex_d7398d9f-d928-4d2e-94e9-74e201da55c5` | Prone leg curl; recorded `prone_leg_curl` | Knee flexion; hamstrings with position-dependent gastrocnemius assistance; `thighs` | High retained. Prone/seated context remains distinct, without an unverified exact hip angle. | +| `ex_ec619fc2-4685-4044-873c-86764bd4a0fe` | Arm curl; recorded `arm_curl` | Elbow flexion; biceps/brachialis primary, brachioradialis secondary, plausible forearm stabilization; `arms` | High retained for qualitative family roles. Grip/shoulder support limitations prevent a universal quantitative ranking. | + +The first five rows use established lower-limb anatomy. OpenStax's main text +identifies the adductors, knee extensors and knee flexors and distinguishes +lower-leg plantarflexors. Inconsistent image alternative text is not used to +reverse an anatomical action. +[OpenStax lower-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-6-appendicular-muscles-of-the-pelvic-girdle-and-lower-limbs) + +The seated/prone curl distinction also has an intervention source which +explicitly considers biarticular hamstrings and biceps femoris short head. +Its longitudinal finding is not generalized to all people, machines or +strength tasks. [Maeo and colleagues](https://pubmed.ncbi.nlm.nih.gov/33009197/) + +The curl mapping is supported by the anatomical elbow-flexor and forearm +functions. Its primary/secondary labels are qualitative exercise interpretation, +not ratios established by the textbook. +[OpenStax upper-limb anatomy](https://openstax.org/books/anatomy-and-physiology-2e/pages/11-5-muscles-of-the-pectoral-girdle-and-upper-limbs) + +## Representation checks + +- Custom Chest press, Converting chest press and Abdominal retain + `interpretation: null`. Their useful candidate interpretations remain + explicitly conditional and uncertain. Chest candidates include the reviewed + `pectoralis_major` / `horizontal_push` interpretation only under stated + execution assumptions. +- Marche retains a conditional gait interpretation, without creating an + occurrence-to-treadmill association or assigning a persisted full-body zone. + Both generic warm-up identities have neither ordinary nor candidate mapping. +- Rear Delt is linked to its actual UUID on the combined apparatus. Pec Fly + is a separate unlinked capability. Assisted dip and assisted chin/pull-up + are separate unlinked capabilities with empty actual exercise-ID lists. + No capability or legacy manifest slug becomes a phantom exercise identity. +- The four added aggregates preserve member and overlap semantics. The + functional hip-flexor and spinal-stabilizer groups are non-exhaustive. + Their action lists describe member capabilities, not actions shared by every + member. Anatomical regions remain `muscle_region`. +- Action contributors are non-exhaustive; missing a possible contributor does + not claim absence of participation. Nominal planes are not trajectory + constraints. Anti-extension, anti-rotation and lateral stabilization have + no invented dynamic joint action and remain authored task conventions. +- Manufacturer/model fields are null for every physical identity. Example + manufacturer references remain mechanics/identification examples rather + than anatomical outcome evidence or local equipment identification. +- EMG findings remain distinct from intervention evidence and do not establish + force shares, hypertrophy rankings or universal primary-muscle status. + [Vigotsky and colleagues](https://pubmed.ncbi.nlm.nih.gov/29354060/) +- BODY ZONE audit `confirmed` means the existing coarse projection is + scientifically compatible under the stated interpretation. It does not + certify observed technique. `questionable` preserves conditional geometry + or identity; it is not a direction to mutate persistence. +- Android's inspected ordinary query path obtains only + `resolved_family_variant_limited` interpretations. Conditional/null records + cannot enter ordinary muscle, pattern or scientific-zone matches through + that guard. Broader runtime behavior remains the engineering review's scope. + +## Remaining uncertainty and handoff + +Exact local manufacturer/model, resistance curves, range settings and detailed +trajectories remain unresolved. Custom apparatus require actual execution +confirmation. Back Extension requires pelvic-restraint and spinal-versus-hip +motion evidence. Row/pulldown contributions depend on arm path and support; +hip-abduction contributions depend on hip angle. Rotary torso compatibility +must not be presented as a historical occurrence. Missing Pec Fly and assisted +exercise identities require real catalog creation or identity evidence before +linkage. Generic warm-ups require their actual activity sequences. + +The scientific foundation supports contextual MAX interpretation only. +Assistance is not external resistance; no conversion, estimated 1RM, percentage +prescription, universal recovery duration or exercise-equivalence guarantee is +justified by these catalogs. + +These uncertainties require observation or future scoped research. They do +not require xhigh escalation solely because the catalog is large. Scientific +implementation handoff: preserve the reviewed distinctions and current +confidence levels, and complete engineering validation separately. No BODY +ZONE migration is recommended. + +## Reviewed catalog content hashes + +SHA-256 values identify the exact reviewed catalog bytes. A subsequent change +to scientific fields requires a bounded delta review. + +| File | SHA-256 | +|---|---| +| `science-references-v1.json` | `ceef31455e0c1da1bc383d4b3a7dc63198cfb62ba0f80e26bc79bae77abb7836` | +| `muscles-v1.json` | `21cb08c9c2ca393d5c87cf73a607175b7c267061687767912a0c92769be5ff4b` | +| `joint-actions-v1.json` | `a66737f4419bcc4040d0abe5ca525b2a185e80f0bcf679091222eb540b3fd8a1` | +| `movement-patterns-v1.json` | `ddd4e3522ebcb4df4618ed2a952fdb5834c42e21e7220a0afd69ab8f3437f072` | +| `exercise-knowledge-v1.json` | `30ee4f400626af9a39c0547ae28360db94e76736462501e113f57721429ef43f` | +| `equipment-knowledge-v1.json` | `b96ca23026fa1f0eb94bc8c0b2a586951130919db5342ee5d60ff1353725acb2` | + +### Subsequent representation verification + +The authored BODY ZONE audit was subsequently embedded in each exercise record +so both platforms and the audit generator can read it from the canonical source. +An automated comparison verified that removing only `body_zone_audit` reproduces +the exact reviewed exercise-catalog SHA-256 above. All 23 embedded annotations +also equal the reviewed audit data after the documented status and array-order +normalization. No scientific assertion or confidence changed. The complete +exercise catalog now has SHA-256 +`a2c72e4743dc5dc24d1a7bbf9e8234442adc2d5ef6882f15cadea1cb9629d647`. diff --git a/docs/reviews/training_knowledge_v1_temporal_contract.md b/docs/reviews/training_knowledge_v1_temporal_contract.md new file mode 100644 index 0000000..f4712cb --- /dev/null +++ b/docs/reviews/training_knowledge_v1_temporal_contract.md @@ -0,0 +1,139 @@ +# Training Knowledge V1 — temporal contract and correction + +Date: 2026-09-09. Contract analysis: advisor / Terra-high, completed. +Temporal implementation and executable validation: PASS. Independent temporal +delta review: `TEMPORAL_DELTA_REVIEW=PASS`, with no findings or repairs. The +initial full-tranche audit found non-temporal defects and its authorized repair +chain completed. Independent bounded verification and fresh final validation +subsequently passed; `TRAINING_KNOWLEDGE_V1=PASS`. This record preserves the +temporal contract and does not expand scientific scope or declare a frozen +format decision. + +## Authority and compatibility + +The frozen [exchange timestamp rule](../exchange_format.md#7-session-timestamps) +requires RFC 3339 / ISO 8601 date-time text with an explicit UTC offset. The V1 +schema declares `format: date-time`. The operational reader profile is: + +```text +YYYY-MM-DD[Tt]HH:MM[:SS[.digits]](Z|z|±HH:MM) +``` + +It uses extended Gregorian dates in years 0001–9999, valid calendar days, +hours 00–23, minutes 00–59 and ordinary seconds 00–59. Fractions require +seconds and at least one ASCII digit after a dot. Every fractional digit +participates in comparison; trailing zeros do not change an instant. + +Numeric offsets range through ±23:59. Lowercase `t`/`z` and the offset grammar +follow [RFC 3339 §5.6](https://www.rfc-editor.org/rfc/rfc3339#section-5.6). +`-00:00` compares as a zero UTC offset while its distinct source spelling is +preserved. Omitted seconds are an application compatibility requirement: +Android persists `OffsetDateTime.now().toString()`, which can omit seconds +when seconds and fractional seconds are both zero. + +Basic dates, ISO week dates, spaces instead of `T/t`, comma fractions, compact +offsets and hour-only offsets are **NOT CONTRACTUALLY REQUIRED**. They are +rejected by the selected profile. The observed acceptance of these forms by +Python `datetime.fromisoformat()` is not normative evidence: this environment's +jsonschema 4.26.0 has no registered default `date-time` checker. No Trainlog +writer, pre-existing fixture or inspected real session establishes a need for +those extensions. All three real session timestamps match the selected +profile and fit the existing C result fields. + +Years outside 0001–9999 and `:60` remain outside the established operational +admission. Supporting real leap seconds would require a verified shared +leap-event schedule and matching validator behavior; the repair does not +guess a conversion or collapse a leap second into the following minute. +[RFC 3339 §5.7](https://www.rfc-editor.org/rfc/rfc3339#section-5.7) + +## Root cause and implementation + +The old cursor guard, SQLite `julianday()`, Java `OffsetDateTime.parse()` and +Python's ISO parser had different languages and precision. A `+14:30` source +produced a rejected cursor. SQLite returned NULL for valid `+15:00` and +lowercase-`t` values, allowing records to disappear. Floating-point Julian +days also did not preserve arbitrary fractional precision. + +The C and Kotlin readers now parse source text into an integer UTC second +and an exact fractional digit sequence. Both occurrence pagination and the +new latest-explicit-MAX reader order descending by: + +```text +(represented UTC instant, session_id bytes, entry_id bytes) +``` + +The cursor applies the same comparison exclusively. Equal instants with +different offsets or fractional trailing zeros use identity tie breakers, +never timestamp-text tie breakers. Cursor emission preserves the source +timestamp and both IDs. C supports aliasing the input and output cursor. + +Each read scans all matching metadata candidates and retains at most +`limit + 1` occurrence candidates, or one MAX candidate. Only selected rows +are hydrated. It does not use a timestamp SQL predicate, SQL temporal order +or SQL limit before comparison. Scan work is O(N) for the exercise history; +retained candidate count is O(limit), with no global dataset-size cap. Each +read has a snapshot covering selection and hydration. Separate page calls +retain the existing current-data semantics, without a cross-call snapshot. + +Malformed caller cursors fail before the scan. Malformed matching stored +timestamps cause an explicit database/consistency error; they are not +silently skipped. Source timestamps are never rewritten or normalized in +persistent storage. + +The C public timestamp fields retain their existing 40-character capacity. +Longer valid SQLite text is compared with full precision; if selected for +output, it produces `DATABASE_ERROR`. It is never truncated or silently +omitted. Android can return longer strings. This is an existing C API +representation limit, not a new wire precision bound. Arbitrary-length C +output would require a separate ownership/length API change. + +The Python validator uses the same grammar and exact chronology. It overrides +only `date-time` on a fresh per-validation format-checker instance, preserving +other checks and caller/global state. The document path therefore admits +Android's no-seconds form even when an optional strict RFC checker is present. + +## Regression and validation evidence + +Production C and Android tests cover mixed representations, page size one, +emitted cursors, continuation, exhaustion, no duplicates, chronological order, +equal-instant session/entry ties, near-equal fractions, malformed cursors and +stored values, and latest-MAX selection. C also checks selected and unselected +timestamps beyond its fixed output capacity. Python tests cover grammar, +exact chronology, date/offset bounds and missing/strict/permissive optional +format checkers through actual document validation. + +An independent C production-API probe exercises twelve admitted spellings: +`+02:00`, `+14:30`, `+15:00`, lowercase `t`, `Z`, lowercase `t/z`, omitted +seconds, `+23:59`, `-23:59`, `-00:00`, and two long fractional forms. Each case +uses two sessions in a temporary database, page size one, exact source/cursor +equality checks and traversal through exhaustion. It passes normally and +under ASan/UBSan. + +Full validation passes: 42 desktop tests; eight knowledge Python tests; four +timestamp Python tests; JSON/import/catalog validators; three standalone C17 +headers; Android unit tests and debug assembly (56 tests, zero failures or +errors, one pre-existing missing-fixture skip); two targeted C sanitizer tests +plus the timestamp Python test; project skill validation; `git diff --check`. +Logs and reusable runner: `/tmp/trainlog-temporal-validation/`. + +The independent temporal reviewer covered grammar, calendar validity, offset +ranges, exact fractions, bytewise ties, cursor aliasing and exclusivity, source +capacity, snapshot behavior, and Android/Python parity. It ran the targeted +Meson `training_context` and `timestamp_validation` tests and all four Python +temporal tests. It reported no temporal defect, repair, or unresolved temporal +boundary. + +## Preserved boundaries and remaining scope + +Desktop schema v11, Android schema v10, stable IDs, real database logical +contents and counts, BODY ZONES, equipment and all scientific catalog bytes +remain unchanged. Scientific review stays valid without a scientific delta. +No staging, commit, push, database deletion, migration, device installation, +uninstall, keystore change or history rewrite occurred. + +Older general history/export/MAX-list readers with lexical timestamp ordering +are outside this bounded new-reader repair. Leap-event support, arbitrary- +length C output and manual device/MTP/visual checks remain explicit future or +manual work. The previously recorded secondary-zone-ID TUI presentation +inconsistency remains nonblocking. Final-review repair verification and fresh +final validation completed with `TRAINING_KNOWLEDGE_V1=PASS`. diff --git a/docs/reviews/training_knowledge_v1_temporal_review.md b/docs/reviews/training_knowledge_v1_temporal_review.md new file mode 100644 index 0000000..b66da9a --- /dev/null +++ b/docs/reviews/training_knowledge_v1_temporal_review.md @@ -0,0 +1,27 @@ +# Training Knowledge V1 — independent temporal delta review + +Date: 2026-09-09. Result: `TEMPORAL_DELTA_REVIEW=PASS`. + +This isolated review found no temporal defect and required no repair. It does +not declare the wider `TRAINING_KNOWLEDGE_V1` tranche PASS or FROZEN. + +## Scope and evidence + +The reviewer independently examined the settled temporal reader contract for: + +- accepted grammar, calendar validation, and numeric offset range; +- exact fractional-second comparison and bytewise stable-ID ties; +- exclusive cursor behavior and C input/output cursor aliasing; +- source-text output capacity and explicit failure behavior; +- selection/hydration snapshot boundaries; and +- parity between C, Android, and Python document validation. + +It ran Meson `training_context` and `timestamp_validation` tests and all four +Python temporal tests. No temporal semantic discrepancy, repair, or remaining +temporal finding was reported. + +The settled admitted source profile, ordering, cursor, snapshot, and explicit +error semantics remain verbatim in the [temporal contract](training_knowledge_v1_temporal_contract.md). +The separate final-review repair verification and final validation subsequently +passed. `TRAINING_KNOWLEDGE_V1=PASS`; this record remains limited to the +independent temporal review and does not expand scientific scope. diff --git a/docs/roadmap.md b/docs/roadmap.md index 1a42aa8..2e6d772 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -31,7 +31,7 @@ BODY_ZONES_V1=PASS BODY_ZONE_SYNC_V1=PASS BODY_ZONES_ANDROID_DEVICE_VALIDATION=PASS -DESKTOP_TESTS=39/39 PASS +DESKTOP_TESTS=42/42 PASS (recorded validation checkpoint) TUI_NOTCURSES_V1=PASS NCURSESW_REMOVED_FROM_ACTIVE_TUI=PASS NOTCURSES_TRUECOLOR_THEME=PASS @@ -63,6 +63,18 @@ descendant-aware filters, unclassified history and one explicit-conflict sync companion. It does not implement a session generator, custom zones or proposed loads. +`TRAINING_KNOWLEDGE_V1=PASS` is read-only infrastructure. Scientific review, +independent temporal review, final engineering review, repair verification, and +final executable validation passed. The temporal contract preserves source text +and exact C/Android chronological pagination; one bounded repair chain closed +the audit's stale-documentation, Android-loader, Meson-input, and role-only C +query findings. +It provides catalog-backed scientific lookup and +runtime context composition without prescriptions, schema changes or catalog +seeding. The documented future session/program input pipeline is deliberately +not a roadmap gate or an implemented generator; see +[Training knowledge system V1](domain/knowledge_system.md). + `EXERCISE_EDIT_V1` is a completed capture correction: Android permits stable-ID renames, protects referenced profiles, and reconciles same-ID display metadata without duplicates. `ANDROID_BANNER_PARITY_V1` is a presentation-only diff --git a/docs/tests.md b/docs/tests.md index d5bd15a..f123977 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -528,3 +528,48 @@ inspection/pull. Applying the validated state to both real stores and running the two transport directions remains an out-of-sandbox hardware validation. Detailed retained evidence: [Android draft execution record](reviews/android_session_draft_v1_resume.md). + +## 15. Training knowledge V1 validation checkpoint + +The current implementation passes the training-knowledge catalog +validator, eight Python generation/catalog regressions, 42 desktop Meson tests, +and standalone C17 public-header checks for `training_knowledge.h`, +`training_context.h` and `database.h`. A targeted AddressSanitizer/Undefined- +Behavior-Sanitizer build passed the two new desktop API tests. Android +`testDebugUnitTest assembleDebug` passed with 56 tests, zero failures/errors, +and one skipped real-v9 fixture test because `TRAINLOG_ANDROID_V9_FIXTURE` was +not available. The TUI knowledge screen's UTF-8 cell-aware scrolling was tested +at the 72x20 minimum terminal. + +The catalog validator, JSON validator and import-contract validator also pass. +`git diff --check` passes. Scientific-review metadata and +catalog hashes were verified separately. + +Temporal regressions now cover the original `+14:30`, `+15:00` and lowercase-`t` +failure, lowercase `z`, omitted seconds, high/negative offsets, exact fractions, +stable identity ties, cursor reuse, traversal through exhaustion and malformed +stored values. C tests also cover selected/unselected values beyond its fixed +output capacity. Four Python tests cover the explicit grammar and exact +chronology, including actual document admission with missing, strict and +permissive optional JSON Schema format checkers. An independent twelve-form +production C probe passes normally and under ASan/UBSan. + +Fresh final validation passed: strict build; 42 Meson tests; eight Python +knowledge tests; four Python temporal tests; knowledge, JSON, and import +validators; three strict C17 headers; affected C knowledge/context tests under +ASan/UBSan plus Python timestamp validation; normal and sanitized independent +12-form temporal probes; Android 56 tests with zero failures/errors and one +known missing real-v9 fixture skip; Java 17 debug assembly; skill validation; +and `git diff --check`. Generated C is byte-identical with SHA-256 +`e8c099f67eb111d61621b5d76592c049823af5508d43e73ec646f22e4c377fca`; all six +Android assets are byte-identical. + +`TRAINING_KNOWLEDGE_V1=PASS`. The independent temporal review passed with no +findings; its coverage included parser grammar and limits, exact chronology, +ties, cursor semantics, source capacity, snapshots, and Android/Python parity. +The initial full audit's stale temporal documentation, Android loader, Meson +dependency, and C role-only query findings were repaired and independently +verified. See the [temporal contract](reviews/training_knowledge_v1_temporal_contract.md) +for the established contract. No real Android install, +manual TUI visual exercise, or manual MTP hardware validation was performed +for Training Knowledge V1. diff --git a/docs/tui.md b/docs/tui.md index 4b99aa2..17394de 100644 --- a/docs/tui.md +++ b/docs/tui.md @@ -465,3 +465,18 @@ Proportions can display: All estimates are explicitly labeled as estimates. No result is converted into a medical or diagnostic classification. + +## 18. Training knowledge infrastructure + +The desktop core includes immutable `training_knowledge.h` catalog access and +read-only `training_context.h` composition. The context uses real persisted IDs +and returns persisted zones, optional science, compatible equipment, latest +explicit MAX and bounded chronological history; it does not write data. The +TUI provides a UTF-8 cell-aware scrolling knowledge screen, tested at the 72x20 +minimum terminal. V1 provides no prescription, generator or scoring flow. Its +lifecycle is `TRAINING_KNOWLEDGE_V1=PASS`. Its occurrence and latest-MAX readers use the settled +temporal contract: exact instant/fraction ordering, bytewise stable-ID ties, +exclusive source-text cursors, bounded selection before hydration, and one +read snapshot per call. Malformed cursors and matching stored timestamps fail +explicitly. The full-tranche audit repair chain and independent verification +passed. See [Training knowledge system V1](domain/knowledge_system.md). diff --git a/tests/fixtures/invalid/timestamp-space-separator.json b/tests/fixtures/invalid/timestamp-space-separator.json new file mode 100644 index 0000000..7d4c61d --- /dev/null +++ b/tests/fixtures/invalid/timestamp-space-separator.json @@ -0,0 +1,10 @@ +{ + "format": "trainlog", + "version": 1, + "exercises": [], + "session": { + "session_id": "invalid-temporal-space", + "started_at": "2026-09-05 18:34:12+02:00", + "exercises": [] + } +} diff --git a/tests/fixtures/valid/temporal-extended-forms.json b/tests/fixtures/valid/temporal-extended-forms.json new file mode 100644 index 0000000..1a7eb19 --- /dev/null +++ b/tests/fixtures/valid/temporal-extended-forms.json @@ -0,0 +1,11 @@ +{ + "format": "trainlog", + "version": 1, + "exercises": [], + "session": { + "session_id": "temporal-extended-forms", + "started_at": "2026-09-05t18:34:12.12345678901234567890z", + "ended_at": "2026-09-06T18:34+23:59", + "exercises": [] + } +} diff --git a/tests/test_timestamp_validation.py b/tests/test_timestamp_validation.py new file mode 100644 index 0000000..d4e00e5 --- /dev/null +++ b/tests/test_timestamp_validation.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Focused frozen timestamp grammar and exact chronology regressions.""" +import sys +import unittest +from pathlib import Path +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tools")) +from validate_json import TrainlogSemanticError, parse_timestamp # noqa: E402 +import validate_json as validation # noqa: E402 + +class TimestampValidationTest(unittest.TestCase): + def test_document_admission_does_not_depend_on_optional_format_checker(self): + schema = validation.load_json(validation.SCHEMA_PATH) + # Simulate missing, stricter RFC-only, and permissive optional checkers. + # Exercise the actual document path, including schema checks before semantics. + for mode in ("missing", "strict", "permissive"): + checker = validation.jsonschema.FormatChecker() + checker.checkers.pop("date-time", None) + if mode != "missing": + checker.checks("date-time")( + lambda value, strict=mode == "strict": not strict + or not isinstance(value, str) + or len(value) > 19 and value[16] == ":" + ) + original_checks = checker.checkers.copy() + validator = validation.jsonschema.Draft202012Validator(schema, format_checker=checker) + with self.subTest(mode=mode): + self.assertEqual([], validation.validate_document( + validator, validation.VALID_FIXTURE_DIR / "temporal-extended-forms.json")) + self.assertTrue(validation.validate_document( + validator, validation.INVALID_FIXTURE_DIR / "timestamp-space-separator.json")) + self.assertEqual(original_checks, checker.checkers) + + def test_selected_grammar(self): + for value in ("0001-01-01T00:00Z", "2000-02-29t23:59:59.00000000000000000001z", + "2026-09-05T18:34+23:59", "2026-09-05T18:34:12-00:00", + "9999-12-31T23:59:59.9-23:59"): + with self.subTest(value=value): self.assertIsNotNone(parse_timestamp(value, "probe")) + + def test_platform_only_forms_rejected(self): + for value in ("0000-01-01T00:00:00Z", "2026-02-29T00:00:00Z", + "2026-09-05 18:34:12+02:00", "20260905T183412+0200", + "2026-W36-5T18:34:12+02:00", "2026-09-05T18:34:12,5+02:00", + "2026-09-05T18:34:12+0200", "2026-09-05T18:34:12+02", + "2026-09-05T18:34:12.5", "2026-09-05T18:34.5Z", + "2026-09-05T18:34:60Z", "2026-09-05T18:34:12+24:00"): + with self.subTest(value=value), self.assertRaises(TrainlogSemanticError): + parse_timestamp(value, "probe") + + def test_exact_fraction_and_rollover(self): + earlier = parse_timestamp("2026-01-01T00:00:00.12345678901234567890Z", "probe") + later = parse_timestamp("2026-01-01T00:00:00.12345678901234567891Z", "probe") + equal = parse_timestamp("2026-01-01T00:00:00.1234567890123456789000Z", "probe") + self.assertLess(earlier, later); self.assertEqual(earlier, equal) + self.assertLess(parse_timestamp("2025-12-31T09:14:59Z", "probe"), + parse_timestamp("2026-01-01T00:15:00+15:00", "probe")) + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_training_knowledge.py b/tests/test_training_knowledge.py new file mode 100644 index 0000000..9241098 --- /dev/null +++ b/tests/test_training_knowledge.py @@ -0,0 +1,133 @@ +import copy +import importlib.util +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "validate_training_knowledge", ROOT / "tools" / "validate_training_knowledge.py" +) +MODULE = importlib.util.module_from_spec(SPEC) +assert SPEC.loader is not None +SPEC.loader.exec_module(MODULE) + + +class TrainingKnowledgeValidationTest(unittest.TestCase): + files = ( + "body-zones-v1.json", "equipment-v1.json", "science-references-v1.json", "muscles-v1.json", + "joint-actions-v1.json", "movement-patterns-v1.json", + "exercise-knowledge-v1.json", "equipment-knowledge-v1.json", + "training-knowledge-audit-v1.json", + ) + + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.catalog = Path(self.temp.name) + for filename in self.files: + (self.catalog / filename).write_bytes((ROOT / "catalog" / filename).read_bytes()) + + def tearDown(self): + self.temp.cleanup() + + def mutate(self, filename, operation): + path = self.catalog / filename + value = json.loads(path.read_text(encoding="utf-8")) + operation(value) + path.write_text(json.dumps(value, ensure_ascii=False), encoding="utf-8") + + def assert_invalid(self): + with self.assertRaises(MODULE.ValidationError): + MODULE.validate(self.catalog) + + def test_valid_and_deterministic_generation(self): + MODULE.validate(self.catalog) + first = self.catalog / "first.c" + second = self.catalog / "second.c" + command = [sys.executable, str(ROOT / "tools" / "generate_training_knowledge.py"), + str(self.catalog)] + subprocess.run(command + [str(first)], check=True) + subprocess.run(command + [str(second)], check=True) + self.assertEqual(first.read_bytes(), second.read_bytes()) + audit = self.catalog / "regenerated-audit.json" + subprocess.run([sys.executable, str(ROOT / "tools" / "generate_training_knowledge_audit.py"), + str(self.catalog), str(audit)], check=True) + self.assertEqual(audit.read_bytes(), (self.catalog / "training-knowledge-audit-v1.json").read_bytes()) + markdown = self.catalog / "audit.md" + subprocess.run([sys.executable, str(ROOT / "tools" / "generate_training_knowledge_audit.py"), + str(self.catalog), str(markdown), "--markdown"], check=True) + self.assertEqual(markdown.read_bytes(), (ROOT / "docs/domain/knowledge_audit.md").read_bytes()) + + def test_unknown_nested_keys_and_types_are_validation_errors(self): + mutations = [ + lambda root: root["equipment"][0].__setitem__("unexpected_review_key", True), + lambda root: root["equipment"][0]["capabilities"][0].__setitem__("requirements", None), + lambda root: root["equipment"][0].__setitem__("requires_actual_exercise", 1), + ] + original = (self.catalog / "equipment-knowledge-v1.json").read_bytes() + for mutation in mutations: + (self.catalog / "equipment-knowledge-v1.json").write_bytes(original) + self.mutate("equipment-knowledge-v1.json", mutation) + self.assert_invalid() + + def test_additive_non_runtime_reference_is_valid(self): + def add(root): + row = copy.deepcopy(root["references"][-1]) + row["ref_id"] = "zz_additive_provenance" + row["title"] = "Additional provenance record" + root["references"].append(row) + self.mutate("science-references-v1.json", add) + MODULE.validate(self.catalog) + + def test_stale_generated_audit_is_rejected(self): + self.mutate("training-knowledge-audit-v1.json", + lambda root: root["rows"][0].__setitem__("rationale", "stale generated value")) + self.assert_invalid() + + def test_duplicate_object_key_rejected(self): + path = self.catalog / "science-references-v1.json" + text = path.read_text(encoding="utf-8") + path.write_text(text.replace('"version": 1', '"version": 1, "version": 1', 1), encoding="utf-8") + self.assert_invalid() + + def test_version_order_enum_role_and_cross_reference_mutations(self): + cases = [ + ("muscles-v1.json", lambda root: root.__setitem__("version", 2)), + ("muscles-v1.json", lambda root: root["muscles"].reverse()), + ("muscles-v1.json", lambda root: root["muscles"][0]["joint_action_ids"].reverse()), + ("muscles-v1.json", lambda root: root["muscles"][0].__setitem__("confidence", "limited")), + ("exercise-knowledge-v1.json", lambda root: root["exercises"][0]["interpretation"]. + __setitem__("primary_muscle_ids", ["missing_muscle"])), + ("exercise-knowledge-v1.json", lambda root: root["exercises"][0]["interpretation"]. + __setitem__("secondary_muscle_ids", root["exercises"][0]["interpretation"]["primary_muscle_ids"])), + ("equipment-knowledge-v1.json", lambda root: root["equipment"][0]["capabilities"][0]. + __setitem__("exercise_ids", ["ex_00000000-0000-4000-8000-000000000000"])), + ] + originals = {filename: (self.catalog / filename).read_bytes() for filename in self.files} + for filename, mutation in cases: + for restore, contents in originals.items(): + (self.catalog / restore).write_bytes(contents) + self.mutate(filename, mutation) + self.assert_invalid() + + def test_conditional_candidate_cannot_become_resolved_data(self): + def leak(root): + row = next(item for item in root["exercises"] if item["resolution_status"] == "conditional") + row["interpretation"] = copy.deepcopy(row["conditional_interpretation"]) + self.mutate("exercise-knowledge-v1.json", leak) + self.assert_invalid() + + def test_manufacturer_only_high_mapping_rejected(self): + def mutation(root): + root["equipment"][0]["confidence"] = "high" + root["equipment"][0]["evidence_type"] = "manufacturer_statement" + self.mutate("equipment-knowledge-v1.json", mutation) + self.assert_invalid() + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/generate_training_knowledge.py b/tools/generate_training_knowledge.py new file mode 100644 index 0000000..ef36bec --- /dev/null +++ b/tools/generate_training_knowledge.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +"""Generate deterministic C tables and read/query functions from knowledge V1.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +from validate_training_knowledge import ValidationError, validate + + +def c(value) -> str: + if value is None: + return "NULL" + if isinstance(value, list): + value = "\n".join(value) + return json.dumps(value, ensure_ascii=False) + + +def interp(value) -> str: + if value is None: + return "{NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL}" + keys = ("family_description", "action_ids", "pattern_ids", "primary_muscle_ids", + "secondary_muscle_ids", "stabilizer_muscle_ids", "primary_zone_id", + "secondary_zone_ids", "confidence", "evidence_type", "source_refs", + "variant_notes", "role_notes", "required_confirmation") + return "{" + ",".join(c(value.get(key)) for key in keys) + "}" + + +def load(path: Path, key: str): + return json.loads(path.read_text(encoding="utf-8"))[key] + + +def main() -> None: + if len(sys.argv) != 3: + raise SystemExit("usage: generate_training_knowledge.py CATALOG_DIR OUTPUT_C") + root, output = Path(sys.argv[1]), Path(sys.argv[2]) + try: + validate(root) + except ValidationError as error: + raise SystemExit(str(error)) from error + refs = load(root / "science-references-v1.json", "references") + muscles = load(root / "muscles-v1.json", "muscles") + actions = load(root / "joint-actions-v1.json", "joint_actions") + patterns = load(root / "movement-patterns-v1.json", "movement_patterns") + exercises = load(root / "exercise-knowledge-v1.json", "exercises") + equipment = load(root / "equipment-knowledge-v1.json", "equipment") + lines = ['#include "trainlog/training_knowledge.h"', + '#include "trainlog/body_zone_catalog.h"', "#include ", ""] + lines.append("static const TrainlogKnowledgeReference references[] = {") + for row in refs: + lines.append(" {%s}," % ",".join((c(row["ref_id"]), c(row["title"]), + c(row["authors_or_organization"]), str(row["year"] or 0), c(row["type"]), + c(row["url"]), c(row["doi"]), c(row["pmid"]), c(row["topics"]), + c(row["notes"]), c(row["limitations"]), c(row["accessed_on"]), + c(row.get("publication_note"))))) + lines.append("};") + lines.append("static const TrainlogKnowledgeBodyZoneAudit audits[] = {") + for row in exercises: + audit = row["body_zone_audit"] + interpretation = row["interpretation"] or row["conditional_interpretation"] + fields = (row["exercise_id"], audit["status"], audit["severity"], audit["rationale"], + audit["confidence"], audit["source_refs"], audit["existing_primary_zone_id"], + audit["existing_secondary_zone_ids"], + None if interpretation is None else interpretation["primary_zone_id"], + [] if interpretation is None else interpretation["secondary_zone_ids"], + audit["proposed_mutation"]) + lines.append(" {%s}," % ",".join(c(value) for value in fields)) + lines.append("};") + lines.append("static const TrainlogKnowledgeMuscle muscles[] = {") + for row in muscles: + lines.append(" {%s}," % ",".join(c(row.get(key)) for key in ( + "muscle_id", "display_name", "display_name_fr", "entity_type", "anatomical_group", + "aggregate_group_id", "member_muscle_ids", "body_zone_ids", "joint_action_ids", + "primary_actions", "primary_actions_semantics", "functional_notes", "confidence", "evidence_type", "source_refs", + "overlap_warning"))) + lines.append("};") + lines.append("static const TrainlogKnowledgeJointAction actions[] = {") + for row in actions: + lines.append(" {%s}," % ",".join(c(row.get(key)) for key in ( + "action_id", "display_name_fr", "definition", "anatomical_region", "joint_complex", + "principal_plane", "plane_notes", "contributing_muscle_ids", "contributor_semantics", "confidence", + "evidence_type", "source_refs", "notes"))) + lines.append("};") + lines.append("static const TrainlogKnowledgeMovementPattern patterns[] = {") + for row in patterns: + lines.append(" {%s}," % ",".join(c(row.get(key)) for key in ( + "pattern_id", "display_name_fr", "definition", "parent_pattern_id", "typical_action_ids", + "typical_body_zone_ids", "body_zone_semantics", "confidence", "evidence_type", "source_refs", "notes"))) + lines.append("};") + lines.append("static const TrainlogKnowledgeInterpretation exercise_interpretations[] = {") + for row in exercises: + lines.append(" %s," % interp(row["interpretation"])) + lines.append("};") + lines.append("static const TrainlogKnowledgeInterpretation exercise_conditionals[] = {") + for row in exercises: + lines.append(" %s," % interp(row["conditional_interpretation"])) + lines.append("};") + lines.append("static const TrainlogExerciseKnowledge exercises[] = {") + for index, row in enumerate(exercises): + resolved = "&exercise_interpretations[%d]" % index if row["interpretation"] else "NULL" + conditional = "&exercise_conditionals[%d]" % index if row["conditional_interpretation"] else "NULL" + fields = [c(row.get(key)) for key in ("exercise_id", "exercise_name", "resolution_status", + "confidence", "equipment_ids", "identity_evidence", "identity_evidence_type", "source_refs", + "limitations", "equipment_link_status")] + lines.append(" {%s,%s,%s}," % (",".join(fields), resolved, conditional)) + lines.append("};") + lines.append("static const TrainlogEquipmentKnowledge equipment[] = {") + for row in equipment: + fields = [c(row.get(key)) for key in ("equipment_id", "manufacturer", "model", "identification_status", + "scientific_status", "scientific_status_scope", "catalog_type", "catalog_load_semantics", "mechanics", "confidence", + "evidence_type", "source_refs", "limitations", "audit_note")] + fields.append("true" if row["requires_actual_exercise"] else "false") + lines.append(" {%s}," % ",".join(fields)) + lines.append("};") + capabilities = [(row["equipment_id"], cap) for row in equipment for cap in row["capabilities"]] + lines.append("static const TrainlogKnowledgeInterpretation capability_interpretations[] = {") + for _, cap in capabilities: + lines.append(" %s," % interp(cap["interpretation"])) + lines.append("};") + lines.append("static const TrainlogEquipmentCapability capabilities[] = {") + for index, (equipment_id, cap) in enumerate(capabilities): + pointer = "&capability_interpretations[%d]" % index if cap["interpretation"] else "NULL" + lines.append(" {%s,%s,%s,%s,%s,%s}," % (c(equipment_id), c(cap["display_name"]), + c(cap["exercise_ids"]), c(cap["link_status"]), c(cap["requirements"]), pointer)) + lines.append("};") + lines.append(r''' +static bool list_has(const char *list, const char *id) { + size_t length; + const char *at; + if (list == NULL || id == NULL || id[0] == '\0') return false; + length = strlen(id); + at = list; + while (*at != '\0') { + const char *end = strchr(at, '\n'); + size_t item_length = end == NULL ? strlen(at) : (size_t)(end - at); + if (item_length == length && memcmp(at, id, length) == 0) return true; + if (end == NULL) break; + at = end + 1; + } + return false; +} +#define DEFINE_ACCESSORS(prefix,type,array,key) \ +size_t prefix##_count(void) { return sizeof(array) / sizeof(array[0]); } \ +const type *prefix##_at(size_t index) { return index < prefix##_count() ? &array[index] : NULL; } \ +const type *prefix##_lookup(const char *id) { size_t i; if (id == NULL || id[0] == '\0') return NULL; \ + for (i=0; iconditional_interpretation; +} +static bool zone_matches(const TrainlogKnowledgeInterpretation *value, const char *zone, bool descendants) { + if (strcmp(value->primary_zone_id, zone) == 0 || list_has(value->secondary_zone_ids, zone)) return true; + if (!descendants) return false; + if (trainlog_body_zone_catalog_is_descendant(value->primary_zone_id, zone)) return true; + { const char *at = value->secondary_zone_ids; + while (at != NULL && *at != '\0') { const char *end = strchr(at, '\n'); char id[64]; + size_t n = end == NULL ? strlen(at) : (size_t)(end-at); if (n >= sizeof(id)) return false; + memcpy(id,at,n); id[n]='\0'; if (trainlog_body_zone_catalog_is_descendant(id,zone)) return true; + if (end == NULL) break; + at=end+1; } + } + return false; +} +TrainlogStatus trainlog_exercise_knowledge_query(const TrainlogKnowledgeQuery *query, + const TrainlogExerciseKnowledge **output, size_t capacity, size_t *output_count) { + size_t i, count=0; const TrainlogKnowledgeQuery empty={0}; + if (output_count == NULL || (capacity > 0 && output == NULL)) return TRAINLOG_STATUS_INVALID_ARGUMENT; + *output_count=0; if (query == NULL) query=∅ + if (query->muscle_role < TRAINLOG_KNOWLEDGE_ROLE_ANY || query->muscle_role > TRAINLOG_KNOWLEDGE_ROLE_STABILIZER) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + /* INVARIANT: a specific muscle role is meaningful only with a muscle ID. + * Keep the C query contract aligned with Android's paired filters. */ + if (query->muscle_id == NULL && query->muscle_role != TRAINLOG_KNOWLEDGE_ROLE_ANY) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + if (query->scientific_zone_id != NULL && trainlog_body_zone_catalog_lookup(query->scientific_zone_id) == NULL) + return TRAINLOG_STATUS_NOT_FOUND; + if (query->movement_pattern_id != NULL && trainlog_knowledge_movement_pattern_lookup(query->movement_pattern_id) == NULL) + return TRAINLOG_STATUS_NOT_FOUND; + if (query->muscle_id != NULL && trainlog_knowledge_muscle_lookup(query->muscle_id) == NULL) + return TRAINLOG_STATUS_NOT_FOUND; + if (query->available_equipment_id != NULL && trainlog_equipment_knowledge_lookup(query->available_equipment_id) == NULL) + return TRAINLOG_STATUS_NOT_FOUND; + for (i=0; iinterpretation; bool muscle_ok=true; + if (v == NULL) continue; + if (query->scientific_zone_id != NULL && !zone_matches(v,query->scientific_zone_id,query->include_zone_descendants)) continue; + if (query->movement_pattern_id != NULL && !list_has(v->pattern_ids,query->movement_pattern_id)) continue; + if (query->available_equipment_id != NULL && !list_has(e->equipment_ids,query->available_equipment_id)) continue; + if (query->muscle_id != NULL) { + switch (query->muscle_role) { + case TRAINLOG_KNOWLEDGE_ROLE_PRIMARY: muscle_ok=list_has(v->primary_muscle_ids,query->muscle_id); break; + case TRAINLOG_KNOWLEDGE_ROLE_SECONDARY: muscle_ok=list_has(v->secondary_muscle_ids,query->muscle_id); break; + case TRAINLOG_KNOWLEDGE_ROLE_STABILIZER: muscle_ok=list_has(v->stabilizer_muscle_ids,query->muscle_id); break; + case TRAINLOG_KNOWLEDGE_ROLE_ANY: muscle_ok=list_has(v->primary_muscle_ids,query->muscle_id) || + list_has(v->secondary_muscle_ids,query->muscle_id) || list_has(v->stabilizer_muscle_ids,query->muscle_id); break; + } + } + if (!muscle_ok) continue; + if (count < capacity) output[count]=e; + ++count; + } + *output_count=count; return count > capacity ? TRAINLOG_STATUS_INVALID_ARGUMENT : TRAINLOG_STATUS_OK; +} +''') + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +if __name__ == "__main__": + main() diff --git a/tools/generate_training_knowledge_audit.py b/tools/generate_training_knowledge_audit.py new file mode 100644 index 0000000..2e2ef29 --- /dev/null +++ b/tools/generate_training_knowledge_audit.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Regenerate the review audit solely from canonical TRAINING KNOWLEDGE V1.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + + +def rows_from_catalog(catalog: Path) -> list[dict]: + exercises = json.loads((catalog / "exercise-knowledge-v1.json").read_text(encoding="utf-8"))["exercises"] + rows = [] + for exercise in exercises: + authored = dict(exercise["body_zone_audit"]) + interpretation = exercise["interpretation"] or exercise["conditional_interpretation"] + authored.update({ + "exercise_id": exercise["exercise_id"], + "exercise_name": exercise["exercise_name"], + "resolution_status": exercise["resolution_status"], + "equipment_ids": exercise["equipment_ids"], + "knowledge_confidence": exercise["confidence"], + "knowledge_source_refs": exercise["source_refs"], + "family_description": None if interpretation is None else interpretation["family_description"], + "action_ids": [] if interpretation is None else interpretation["action_ids"], + "pattern_ids": [] if interpretation is None else interpretation["pattern_ids"], + "primary_muscle_ids": [] if interpretation is None else interpretation["primary_muscle_ids"], + "secondary_muscle_ids": [] if interpretation is None else interpretation["secondary_muscle_ids"], + "stabilizer_muscle_ids": [] if interpretation is None else interpretation["stabilizer_muscle_ids"], + "scientific_primary_zone_id": None if interpretation is None else interpretation["primary_zone_id"], + "scientific_secondary_zone_ids": [] if interpretation is None else interpretation["secondary_zone_ids"], + }) + rows.append(authored) + return rows + + +def generate(catalog: Path, output: Path) -> None: + rows = rows_from_catalog(catalog) + output.write_text(json.dumps({"format": "trainlog-training-knowledge-audit-v1", + "version": 1, "rows": rows}, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def markdown_cell(value) -> str: + if value is None: + return "—" + if isinstance(value, list): + value = ", ".join(value) if value else "—" + return str(value).replace("|", "\\|").replace("\n", " ") + + +def generate_markdown(catalog: Path, output: Path) -> None: + columns = (("exercise_id", "ID"), ("exercise_name", "Name"), ("action_ids", "Actions"), + ("pattern_ids", "Patterns"), ("primary_muscle_ids", "Primary"), + ("secondary_muscle_ids", "Secondary"), ("stabilizer_muscle_ids", "Stabilizers"), + ("scientific_primary_zone_id", "Science primary zone"), + ("scientific_secondary_zone_ids", "Science secondary zones"), + ("existing_primary_zone_id", "Existing primary zone"), + ("existing_secondary_zone_ids", "Existing secondary zones"), ("equipment_ids", "Equipment"), + ("knowledge_confidence", "Confidence"), ("knowledge_source_refs", "Source refs"), + ("status", "Audit status")) + lines = ["# Training knowledge science audit", "", + "Generated deterministically from the six canonical knowledge catalogs. Conditional rows are candidates that require explicit confirmation and do not participate in ordinary resolved queries.", "", + "| " + " | ".join(label for _, label in columns) + " |", + "| " + " | ".join("---" for _ in columns) + " |"] + for row in rows_from_catalog(catalog): + lines.append("| " + " | ".join(markdown_cell(row[key]) for key, _ in columns) + " |") + output.write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("catalog", type=Path) + parser.add_argument("output", type=Path) + parser.add_argument("--markdown", action="store_true") + args = parser.parse_args() + if args.markdown: + generate_markdown(args.catalog, args.output) + else: + generate(args.catalog, args.output) + + +if __name__ == "__main__": + main() diff --git a/tools/validate_json.py b/tools/validate_json.py index 5c757c8..151b232 100755 --- a/tools/validate_json.py +++ b/tools/validate_json.py @@ -4,12 +4,12 @@ from __future__ import annotations import argparse +from dataclasses import dataclass import json import math import re import sys import unicodedata -from datetime import datetime from pathlib import Path from typing import Any @@ -120,25 +120,67 @@ def normalize_exercise_name(name: str) -> str: return collapsed.casefold() -def parse_timestamp(value: str, field_name: str) -> datetime: - """Parse a Trainlog timestamp while requiring an explicit UTC offset.""" - candidate = value - if candidate.endswith("Z"): - candidate = candidate[:-1] + "+00:00" +@dataclass(frozen=True) +class TrainlogTimestamp: + """Exact comparable instant key; fraction has insignificant zeros removed.""" - try: - parsed = datetime.fromisoformat(candidate) - except ValueError as exc: - raise TrainlogSemanticError( - f"{field_name}: invalid date-time: {value!r}" - ) from exc + utc_second: int + fraction: str - if parsed.utcoffset() is None: - raise TrainlogSemanticError( - f"{field_name}: UTC offset is required: {value!r}" - ) + def __lt__(self, other: "TrainlogTimestamp") -> bool: + return (self.utc_second, self.fraction) < (other.utc_second, other.fraction) - return parsed + def __le__(self, other: "TrainlogTimestamp") -> bool: + return self == other or self < other + + +TIMESTAMP_PATTERN = re.compile( + r"(?P[0-9]{4})-(?P[0-9]{2})-(?P[0-9]{2})" + r"[Tt](?P[0-9]{2}):(?P[0-9]{2})" + r"(?::(?P[0-9]{2})(?:\.(?P[0-9]+))?)?" + r"(?P[Zz]|[+-][0-9]{2}:[0-9]{2})", + re.ASCII, +) + + +def _leap_year(year: int) -> bool: + return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0) + + +def _days_in_month(year: int, month: int) -> int: + if month == 2: + return 29 if _leap_year(year) else 28 + return 30 if month in {4, 6, 9, 11} else 31 + + +def _day_number(year: int, month: int, day: int) -> int: + prior = year - 1 + result = prior * 365 + prior // 4 - prior // 100 + prior // 400 + result += sum(_days_in_month(year, item) for item in range(1, month)) + return result + day - 1 + + +def parse_timestamp(value: str, field_name: str) -> TrainlogTimestamp: + """Parse the frozen Trainlog grammar without platform ISO extensions.""" + match = TIMESTAMP_PATTERN.fullmatch(value) if isinstance(value, str) else None + if match is None: + raise TrainlogSemanticError(f"{field_name}: invalid date-time: {value!r}") + year, month, day, hour, minute = ( + int(match[name]) for name in ("year", "month", "day", "hour", "minute") + ) + second = int(match["second"] or "0") + if year < 1 or month not in range(1, 13) or day not in range( + 1, _days_in_month(year, month) + 1) or hour > 23 or minute > 59 or second > 59: + raise TrainlogSemanticError(f"{field_name}: invalid date-time: {value!r}") + zone = match["zone"] + offset = 0 + if zone not in {"Z", "z"}: + offset_hour, offset_minute = int(zone[1:3]), int(zone[4:6]) + if offset_hour > 23 or offset_minute > 59: + raise TrainlogSemanticError(f"{field_name}: invalid date-time: {value!r}") + offset = (offset_hour * 3600 + offset_minute * 60) * (1 if zone[0] == "+" else -1) + local = _day_number(year, month, day) * 86400 + hour * 3600 + minute * 60 + second + return TrainlogTimestamp(local - offset, (match["fraction"] or "").rstrip("0")) def require_non_blank(value: str, field_name: str) -> None: @@ -358,8 +400,23 @@ def structural_errors( document: Any, ) -> list[str]: """Return deterministic human-readable JSON Schema errors.""" + # WHY: jsonschema's optional RFC checker requires seconds, while Trainlog's + # existing Android writer may omit them. Admission must not depend on which + # optional dependencies are installed. Keep other supplied format checks, + # and override only date-time on a fresh instance; never mutate global state. + checker = jsonschema.FormatChecker() + if validator.format_checker is not None: + checker.checkers = validator.format_checker.checkers.copy() + + @checker.checks("date-time", raises=TrainlogSemanticError) + def trainlog_date_time(value: Any) -> bool: + if isinstance(value, str): + parse_timestamp(value, "date-time") + return True # JSON Schema's type keyword handles non-string values. + + temporal_validator = validator.evolve(format_checker=checker) errors = sorted( - validator.iter_errors(document), + temporal_validator.iter_errors(document), key=lambda error: [str(part) for part in error.absolute_path], ) @@ -485,7 +542,15 @@ def main(argv: list[str]) -> int: validator.check_schema(schema) if not args.paths: - return run_suite(validator) + result = run_suite(validator) + try: + from validate_training_knowledge import ValidationError, validate as validate_knowledge + validate_knowledge(ROOT / "catalog") + print(f"PASS training-knowledge catalogs: {ROOT / 'catalog'}") + except ValidationError as error: + print(f"FAIL training-knowledge catalogs: {error}") + result = 1 + return result failed = False for path in args.paths: diff --git a/tools/validate_training_knowledge.py b/tools/validate_training_knowledge.py new file mode 100644 index 0000000..02b79d9 --- /dev/null +++ b/tools/validate_training_knowledge.py @@ -0,0 +1,399 @@ +#!/usr/bin/env python3 +"""Strict cross-catalog validation for TRAINING KNOWLEDGE V1.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path + + +class ValidationError(ValueError): + pass + + +def unique_object(pairs): + result = {} + for key, value in pairs: + if key in result: + raise ValidationError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def load(path: Path): + try: + with path.open(encoding="utf-8") as handle: + return json.load(handle, object_pairs_hook=unique_object) + except (OSError, json.JSONDecodeError) as error: + raise ValidationError(f"{path}: {error}") from error + + +def require(condition: bool, message: str) -> None: + if not condition: + raise ValidationError(message) + + +def strings(value, where: str, *, nonempty: bool = True, ordered: bool = True) -> None: + require(isinstance(value, list), f"{where}: expected array") + require(all(isinstance(item, str) and (item.strip() or not nonempty) for item in value), + f"{where}: expected strings") + require(len(value) == len(set(value)), f"{where}: duplicate values") + if ordered: + require(value == sorted(value), f"{where}: values must be sorted") + + +def text(value, where: str) -> None: + require(isinstance(value, str) and bool(value.strip()), f"{where}: expected non-empty string") + + +def exact_object(value, required: set[str], where: str, optional: set[str] | None = None) -> None: + optional = optional or set() + require(isinstance(value, dict), f"{where}: expected object") + require(required <= set(value) <= required | optional, f"{where}: invalid keys") + + +def nullable_text(value, where: str) -> None: + require(value is None or isinstance(value, str), f"{where}: expected string or null") + + +def confidence(value, where: str) -> None: + require(isinstance(value, str) and value in {"high", "moderate", "uncertain"}, + f"{where}: invalid confidence") + + +def refs_list(value, where: str, refs: set[str], *, nonempty: bool = True) -> None: + strings(value, where) + require(set(value) <= refs and (bool(value) or not nonempty), f"{where}: invalid references") + + +def envelope(root, fmt: str, key: str): + require(set(root) == {"format", "version", key}, f"{key}: invalid envelope") + require(root["format"] == fmt and root["version"] == 1 and not isinstance(root["version"], bool), + f"{key}: unsupported version") + require(isinstance(root[key], list), f"{key}: expected array") + return root[key] + + +def ids(rows, key: str, where: str) -> set[str]: + values = [] + for row in rows: + require(isinstance(row, dict), f"{where}: record must be object") + text(row.get(key), f"{where}.{key}") + values.append(row[key]) + require(values == sorted(values), f"{where}: records must be ordered by {key}") + require(len(values) == len(set(values)), f"{where}: duplicate {key}") + return set(values) + + +def validate_interpretation(value, where, refs, muscles, actions, patterns, zones): + required = {"family_description", "action_ids", "pattern_ids", "primary_muscle_ids", + "secondary_muscle_ids", "stabilizer_muscle_ids", "primary_zone_id", + "secondary_zone_ids", "confidence", "evidence_type", "source_refs", + "variant_notes", "role_notes"} + exact_object(value, required, where, {"required_confirmation"}) + for key in ("action_ids", "pattern_ids", "primary_muscle_ids", "secondary_muscle_ids", + "stabilizer_muscle_ids", "secondary_zone_ids", "source_refs"): + strings(value[key], f"{where}.{key}") + require(set(value["action_ids"]) <= actions, f"{where}: dangling action") + require(set(value["pattern_ids"]) <= patterns, f"{where}: dangling pattern") + role_lists = [set(value[key]) for key in ("primary_muscle_ids", "secondary_muscle_ids", "stabilizer_muscle_ids")] + require(set().union(*role_lists) <= muscles, f"{where}: dangling muscle") + require(sum(map(len, role_lists)) == len(set().union(*role_lists)), f"{where}: muscle appears in multiple roles") + require(value["primary_zone_id"] in zones and set(value["secondary_zone_ids"]) <= zones, + f"{where}: dangling zone") + require(value["primary_zone_id"] not in value["secondary_zone_ids"], f"{where}: duplicate zone role") + confidence(value["confidence"], f"{where}.confidence") + require(bool(value["source_refs"]) and set(value["source_refs"]) <= refs, f"{where}: dangling evidence") + for key in ("family_description", "evidence_type", "variant_notes", "role_notes"): + text(value[key], f"{where}.{key}") + if "required_confirmation" in value: + text(value["required_confirmation"], f"{where}.required_confirmation") + + +def validate(root: Path) -> None: + body = load(root / "body-zones-v1.json") + require(body.get("format") == "trainlog-body-zone-catalog" and body.get("version") == 1, + "unsupported body-zone catalog") + require(all(row.get("kind") in {"group", "leaf", "standalone"} for row in body.get("zones", [])), + "invalid body-zone kind") + zones = {row["zone_id"] for row in body["zones"]} + references = envelope(load(root / "science-references-v1.json"), "trainlog-science-references-v1", "references") + muscles_rows = envelope(load(root / "muscles-v1.json"), "trainlog-muscles-v1", "muscles") + actions_rows = envelope(load(root / "joint-actions-v1.json"), "trainlog-joint-actions-v1", "joint_actions") + patterns_rows = envelope(load(root / "movement-patterns-v1.json"), "trainlog-movement-patterns-v1", "movement_patterns") + exercise_rows = envelope(load(root / "exercise-knowledge-v1.json"), "trainlog-exercise-knowledge-v1", "exercises") + equipment_rows = envelope(load(root / "equipment-knowledge-v1.json"), "trainlog-equipment-knowledge-v1", "equipment") + refs = ids(references, "ref_id", "references") + muscles = ids(muscles_rows, "muscle_id", "muscles") + actions = ids(actions_rows, "action_id", "joint_actions") + patterns = ids(patterns_rows, "pattern_id", "movement_patterns") + exercises = ids(exercise_rows, "exercise_id", "exercises") + equipment = ids(equipment_rows, "equipment_id", "equipment") + snake_id = re.compile(r"[a-z][a-z0-9_]*") + equipment_id = re.compile(r"(?:[a-z][a-z0-9_]*|eq_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})") + require(all(snake_id.fullmatch(value) for value in refs | muscles | actions | patterns), + "invalid stable knowledge ID syntax") + require(all(equipment_id.fullmatch(value) for value in equipment), "invalid stable equipment ID syntax") + equipment_manifest = load(root / "equipment-v1.json") + require(isinstance(equipment_manifest, dict) and + set(equipment_manifest) == {"format", "version", "equipment", "exercise_equipment"} and + equipment_manifest["format"] == "trainlog-equipment-catalog" and + equipment_manifest["version"] == 1, "invalid equipment manifest") + manifest_equipment = equipment_manifest["equipment"] + require(isinstance(manifest_equipment, list), "equipment manifest rows invalid") + required_equipment = set() + for manifest_row in manifest_equipment: + require(isinstance(manifest_row, dict), "equipment manifest row invalid") + text(manifest_row.get("id"), "equipment manifest id") + require(manifest_row["id"] not in required_equipment, "duplicate equipment manifest id") + required_equipment.add(manifest_row["id"]) + mappings = body.get("exercise_mappings") + require(isinstance(mappings, list), "body-zone exercise mappings invalid") + required_exercises = set() + for mapping in mappings: + require(isinstance(mapping, dict), "body-zone exercise mapping invalid") + text(mapping.get("exercise_id"), "body-zone exercise mapping id") + require(mapping["exercise_id"] not in required_exercises, "duplicate body-zone exercise mapping") + required_exercises.add(mapping["exercise_id"]) + require(required_equipment <= equipment, "knowledge catalog misses supplied equipment") + require(required_exercises <= exercises, "knowledge catalog misses observed exercises") + + ref_required = {"ref_id", "title", "authors_or_organization", "year", "type", "url", "topics", + "notes", "limitations", "doi", "pmid", "accessed_on"} + for row in references: + exact_object(row, ref_required, "reference", {"publication_note"}) + for key in ("title", "authors_or_organization", "type", "url", "notes", "limitations", "accessed_on"): + text(row[key], f"reference.{key}") + require(row["year"] is None or isinstance(row["year"], int) and not isinstance(row["year"], bool), + "reference.year invalid") + strings(row["topics"], "reference.topics", ordered=False) + require(row["doi"] is None or isinstance(row["doi"], str), "reference.doi invalid") + require(row["pmid"] is None or isinstance(row["pmid"], str), "reference.pmid invalid") + if "publication_note" in row: + nullable_text(row["publication_note"], "reference.publication_note") + ref_types = {row["ref_id"]: row["type"] for row in references} + scientific_types = {"established_anatomy", "emg_evidence", "intervention_evidence"} + + for row in muscles_rows: + required = {"muscle_id", "display_name", "display_name_fr", "entity_type", "anatomical_group", + "aggregate_group_id", "body_zone_id", "body_zone_ids", "joint_action_ids", + "primary_actions", "primary_actions_semantics", "overlap_warning", "functional_notes", + "confidence", "evidence_type", "source_refs"} + exact_object(row, required, f"muscle {row['muscle_id']}", {"member_muscle_ids"}) + for key in ("display_name", "display_name_fr", "anatomical_group", "primary_actions_semantics", + "overlap_warning", "evidence_type"): + text(row[key], f"muscle.{key}") + require(isinstance(row["functional_notes"], str), "muscle.functional_notes invalid") + require(row["entity_type"] in {"muscle", "muscle_region", "muscle_group"}, "invalid muscle entity_type") + require(row["aggregate_group_id"] is None or row["aggregate_group_id"] in muscles, "dangling aggregate group") + if "member_muscle_ids" in row: + strings(row["member_muscle_ids"], "muscle.member_muscle_ids") + require(set(row["member_muscle_ids"]) <= muscles, "dangling group member") + require(row["body_zone_id"] in zones, "dangling muscle zone") + for key in ("body_zone_ids", "joint_action_ids", "primary_actions", "source_refs"): + strings(row[key], f"muscle.{key}") + require(set(row["body_zone_ids"]) <= zones and set(row["joint_action_ids"]) <= actions and + set(row["primary_actions"]) <= actions and set(row["source_refs"]) <= refs and row["source_refs"], + "muscle cross-reference failure") + confidence(row["confidence"], "muscle.confidence") + + for row in actions_rows: + required = {"action_id", "display_name_fr", "definition", "anatomical_region", "joint_complex", + "joint_or_complex", "principal_plane", "plane_notes", "contributing_muscle_ids", + "contributor_semantics", "evidence_type", "confidence", "source_refs", "notes"} + require(set(row) == required, f"action {row['action_id']}: invalid keys") + for key in ("display_name_fr", "definition", "anatomical_region", "joint_complex", "joint_or_complex", + "principal_plane", "plane_notes", "contributor_semantics", "evidence_type", "notes"): + text(row[key], f"action.{key}") + strings(row["contributing_muscle_ids"], "action.contributing_muscle_ids") + strings(row["source_refs"], "action.source_refs") + require(set(row["contributing_muscle_ids"]) <= muscles and set(row["source_refs"]) <= refs and row["source_refs"], + "action cross-reference failure") + confidence(row["confidence"], "action.confidence") + + for row in patterns_rows: + required = {"pattern_id", "display_name_fr", "definition", "typical_action_ids", "typical_body_zone_ids", + "evidence_type", "confidence", "source_refs", "notes"} + require(required <= set(row) <= required | {"body_zone_semantics", "parent_pattern_id"}, + f"pattern {row['pattern_id']}: invalid keys") + for key in ("display_name_fr", "definition", "evidence_type", "notes"): + text(row[key], f"pattern.{key}") + for key in ("body_zone_semantics", "parent_pattern_id"): + if key in row: + nullable_text(row[key], f"pattern.{key}") + for key in ("typical_action_ids", "typical_body_zone_ids", "source_refs"): + strings(row[key], f"pattern.{key}") + require(set(row["typical_action_ids"]) <= actions and set(row["typical_body_zone_ids"]) <= zones and + set(row["source_refs"]) <= refs and row["source_refs"], "pattern cross-reference failure") + if row.get("parent_pattern_id") is not None: + require(row["parent_pattern_id"] in patterns, "dangling parent pattern") + confidence(row["confidence"], "pattern.confidence") + + linked = set() + exercise_equipment = {} + for row in exercise_rows: + required = {"exercise_id", "exercise_name", "equipment_ids", "identity_evidence", + "identity_evidence_type", "resolution_status", "confidence", "interpretation", + "conditional_interpretation", "existing_body_zones", "source_refs", "limitations", + "body_zone_audit"} + require(required <= set(row) <= required | {"equipment_link_status"}, + f"exercise {row['exercise_id']}: invalid keys") + require(re.fullmatch(r"ex_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", + row["exercise_id"]) is not None, "invalid exercise UUID") + for key in ("exercise_name", "identity_evidence", "identity_evidence_type"): + text(row[key], f"exercise.{key}") + strings(row["limitations"], "exercise.limitations", ordered=False) + if "equipment_link_status" in row: + text(row["equipment_link_status"], "exercise.equipment_link_status") + exact_object(row["existing_body_zones"], {"primary_zone_id", "secondary_zone_ids"}, + "exercise.existing_body_zones") + require(row["existing_body_zones"]["primary_zone_id"] is None or + row["existing_body_zones"]["primary_zone_id"] in zones, + "exercise existing primary zone invalid") + strings(row["existing_body_zones"]["secondary_zone_ids"], "exercise.existing_body_zones.secondary_zone_ids") + require(set(row["existing_body_zones"]["secondary_zone_ids"]) <= zones, + "exercise existing secondary zones invalid") + body_audit = row["body_zone_audit"] + exact_object(body_audit, {"status", "severity", "confidence", "rationale", "existing_primary_zone_id", + "existing_secondary_zone_ids", "proposed_mutation", "source_refs"}, "exercise.body_zone_audit") + require(body_audit["status"] in {"confirmed", "questionable", "unresolved"}, "invalid embedded body-zone audit") + for key in ("severity", "rationale"): + text(body_audit[key], f"exercise.body_zone_audit.{key}") + confidence(body_audit["confidence"], "exercise.body_zone_audit.confidence") + require(body_audit["existing_primary_zone_id"] is None or body_audit["existing_primary_zone_id"] in zones, + "audit primary zone invalid") + strings(body_audit["existing_secondary_zone_ids"], "audit existing secondary zones") + require(set(body_audit["existing_secondary_zone_ids"]) <= zones, "audit secondary zone invalid") + nullable_text(body_audit["proposed_mutation"], "audit.proposed_mutation") + refs_list(body_audit["source_refs"], "exercise.body_zone_audit.source_refs", refs, + nonempty=body_audit["status"] != "unresolved") + status = row["resolution_status"] + require(status in {"resolved_family_variant_limited", "conditional", "unresolved"}, "invalid resolution status") + confidence(row["confidence"], "exercise.confidence") + strings(row["equipment_ids"], "exercise.equipment_ids") + exercise_equipment[row["exercise_id"]] = set(row["equipment_ids"]) + strings(row["source_refs"], "exercise.source_refs") + require(set(row["equipment_ids"]) <= equipment and set(row["source_refs"]) <= refs, + "exercise cross-reference failure") + if status == "resolved_family_variant_limited": + require(bool(row["source_refs"]), "resolved exercise lacks evidence") + require(row["conditional_interpretation"] is None, "resolved record has conditional interpretation") + validate_interpretation(row["interpretation"], f"exercise {row['exercise_id']}", refs, muscles, actions, patterns, zones) + if row["confidence"] == "high": + require(any(ref_types[ref] in scientific_types for ref in row["interpretation"]["source_refs"]), + "high exercise mapping lacks scientific source") + elif status == "conditional": + require(bool(row["source_refs"]), "conditional exercise lacks evidence") + require(row["interpretation"] is None, "conditional record leaks into resolved interpretation") + validate_interpretation(row["conditional_interpretation"], f"conditional {row['exercise_id']}", refs, muscles, actions, patterns, zones) + else: + require(row["interpretation"] is None and row["conditional_interpretation"] is None, + "unresolved record has interpretation") + + manufacturer_only = {"manufacturer_statement"} + equipment_required = {"equipment_id", "manufacturer", "model", "identification_status", "scientific_status", + "scientific_status_scope", "catalog_type", "catalog_load_semantics", "mechanics", "evidence_type", + "confidence", "source_refs", "capabilities", "requires_actual_exercise", "limitations"} + capability_required = {"display_name", "exercise_ids", "link_status", "interpretation", "requirements"} + for row in equipment_rows: + exact_object(row, equipment_required, f"equipment {row['equipment_id']}", {"audit_note"}) + for key in ("identification_status", "scientific_status_scope", "catalog_type", + "mechanics", "evidence_type"): + text(row[key], f"equipment.{key}") + for key in ("manufacturer", "model", "catalog_load_semantics"): + nullable_text(row[key], f"equipment.{key}") + if "audit_note" in row: + nullable_text(row["audit_note"], "equipment.audit_note") + require(isinstance(row["requires_actual_exercise"], bool), "equipment requires_actual_exercise invalid") + strings(row["limitations"], "equipment.limitations", ordered=False) + require(row["scientific_status"] in {"scientifically_documented", "mechanically_identified_anatomy_incomplete", + "equipment_identity_uncertain"}, "invalid equipment science_status") + require(row["confidence"] in {"high", "moderate", "uncertain"}, "invalid equipment confidence") + strings(row["source_refs"], "equipment.source_refs") + require(set(row["source_refs"]) <= refs and row["source_refs"], "equipment evidence failure") + if row["confidence"] == "high": + require(row["evidence_type"] not in manufacturer_only, "manufacturer-only high anatomical mapping") + require(any(ref_types[ref] in scientific_types for ref in row["source_refs"]), + "high equipment mapping lacks scientific source") + require(isinstance(row["capabilities"], list), "equipment capabilities invalid") + for capability in row["capabilities"]: + exact_object(capability, capability_required, "equipment capability") + for key in ("display_name", "link_status", "requirements"): + text(capability[key], f"capability.{key}") + strings(capability["exercise_ids"], "capability.exercise_ids") + require(set(capability["exercise_ids"]) <= exercises, "capability phantom exercise") + linked.update((exercise_id, row["equipment_id"]) for exercise_id in capability["exercise_ids"]) + for exercise_id in capability["exercise_ids"]: + require(row["equipment_id"] in exercise_equipment[exercise_id], + "equipment/exercise compatibility is not symmetric") + if capability["interpretation"] is not None: + validate_interpretation(capability["interpretation"], "equipment capability", refs, muscles, actions, patterns, zones) + for row in exercise_rows: + for equipment_id in row["equipment_ids"]: + require((row["exercise_id"], equipment_id) in linked, "exercise/equipment compatibility is not symmetric") + + audit = envelope(load(root / "training-knowledge-audit-v1.json"), + "trainlog-training-knowledge-audit-v1", "rows") + require(ids(audit, "exercise_id", "audit") == exercises, "audit does not cover exercise inventory") + for row in audit: + audit_required = {"status", "severity", "confidence", "rationale", "existing_primary_zone_id", + "existing_secondary_zone_ids", "proposed_mutation", "source_refs", "exercise_id", "exercise_name", + "resolution_status", "equipment_ids", "knowledge_confidence", "knowledge_source_refs", + "family_description", "action_ids", "pattern_ids", "primary_muscle_ids", "secondary_muscle_ids", + "stabilizer_muscle_ids", "scientific_primary_zone_id", "scientific_secondary_zone_ids"} + exact_object(row, audit_required, "audit row") + require(row["status"] in {"confirmed", "questionable", "unresolved"}, "invalid audit status") + confidence(row["confidence"], "audit.confidence") + confidence(row["knowledge_confidence"], "audit.knowledge_confidence") + for key in ("exercise_name", "resolution_status", "severity", "rationale"): + text(row[key], f"audit.{key}") + for key in ("family_description", "scientific_primary_zone_id", "proposed_mutation"): + nullable_text(row[key], f"audit.{key}") + for key in ("source_refs", "knowledge_source_refs", "equipment_ids", "action_ids", "pattern_ids", + "primary_muscle_ids", "secondary_muscle_ids", "stabilizer_muscle_ids", + "scientific_secondary_zone_ids"): + strings(row[key], f"audit.{key}") + require(set(row["source_refs"]) <= refs and set(row["knowledge_source_refs"]) <= refs, + "audit evidence failure") + require(set(row["equipment_ids"]) <= equipment and set(row["action_ids"]) <= actions and + set(row["pattern_ids"]) <= patterns and + set(row["primary_muscle_ids"] + row["secondary_muscle_ids"] + + row["stabilizer_muscle_ids"]) <= muscles and + set(row["scientific_secondary_zone_ids"]) <= zones, + "audit cross-reference failure") + require(row["status"] == "unresolved" or bool(row["source_refs"]), "resolved audit lacks evidence") + expected_audit = [] + for exercise in exercise_rows: + expected = dict(exercise["body_zone_audit"]) + interpretation = exercise["interpretation"] or exercise["conditional_interpretation"] + expected.update({ + "exercise_id": exercise["exercise_id"], "exercise_name": exercise["exercise_name"], + "resolution_status": exercise["resolution_status"], "equipment_ids": exercise["equipment_ids"], + "knowledge_confidence": exercise["confidence"], "knowledge_source_refs": exercise["source_refs"], + "family_description": None if interpretation is None else interpretation["family_description"], + "action_ids": [] if interpretation is None else interpretation["action_ids"], + "pattern_ids": [] if interpretation is None else interpretation["pattern_ids"], + "primary_muscle_ids": [] if interpretation is None else interpretation["primary_muscle_ids"], + "secondary_muscle_ids": [] if interpretation is None else interpretation["secondary_muscle_ids"], + "stabilizer_muscle_ids": [] if interpretation is None else interpretation["stabilizer_muscle_ids"], + "scientific_primary_zone_id": None if interpretation is None else interpretation["primary_zone_id"], + "scientific_secondary_zone_ids": [] if interpretation is None else interpretation["secondary_zone_ids"], + }) + expected_audit.append(expected) + require(audit == expected_audit, "generated audit is stale or inconsistent with canonical exercises") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("catalog", nargs="?", type=Path, default=Path("catalog")) + args = parser.parse_args() + try: + validate(args.catalog) + except ValidationError as error: + raise SystemExit(str(error)) from error + + +if __name__ == "__main__": + main() diff --git a/tui/include/trainlog/database.h b/tui/include/trainlog/database.h index be80f17..8460ef5 100644 --- a/tui/include/trainlog/database.h +++ b/tui/include/trainlog/database.h @@ -15,6 +15,11 @@ typedef struct TrainlogDatabase TrainlogDatabase; +/* Nestable read savepoints let composition services observe one database state + * without exposing SQLite or issuing writes to application data. */ +TrainlogStatus trainlog_database_read_snapshot_begin(TrainlogDatabase *database); +TrainlogStatus trainlog_database_read_snapshot_end(TrainlogDatabase *database, bool commit_snapshot); + /* WHY: session occurrences retain an equipment ID, while custom definitions * need durable presentation metadata. A reference alone is never a definition. */ typedef struct TrainlogCustomEquipment { @@ -386,6 +391,115 @@ TrainlogStatus trainlog_database_list_exercise_performance( size_t *output_count ); +/* TRAINING_KNOWLEDGE_RUNTIME_READ_V1 */ +#define TRAINLOG_OCCURRENCE_PAGE_MAX 32U +#define TRAINLOG_OCCURRENCE_SET_PAGE_MAX 64U + +typedef struct TrainlogExerciseOccurrenceCursor { + char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; + char session_id[TRAINLOG_ID_MAX + 1U]; + char entry_id[TRAINLOG_ID_MAX + 1U]; +} TrainlogExerciseOccurrenceCursor; + +typedef struct TrainlogExerciseOccurrence { + char session_id[TRAINLOG_ID_MAX + 1U]; + char entry_id[TRAINLOG_ID_MAX + 1U]; + char exercise_id[TRAINLOG_ID_MAX + 1U]; + char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; + char equipment_id[TRAINLOG_ID_MAX + 1U]; + TrainlogSessionType session_type; + TrainlogTrackingMode tracking_mode; + TrainlogRecordingMode recording_mode; + TrainlogExerciseDataFields data_fields; + TrainlogLoadMode load_mode; + size_t set_count; + int continuous_duration_seconds; + bool continuous_has_speed; + double continuous_speed_kmh; + bool continuous_has_distance; + double continuous_distance_km; +} TrainlogExerciseOccurrence; + +typedef struct TrainlogOccurrenceSet { + size_t position; + bool has_reps; + int reps; + bool has_duration; + int duration_seconds; + bool has_weight; + double weight_kg; +} TrainlogOccurrenceSet; + +typedef struct TrainlogLatestExplicitMax { + bool found; + char session_id[TRAINLOG_ID_MAX + 1U]; + char entry_id[TRAINLOG_ID_MAX + 1U]; + char started_at[TRAINLOG_TIMESTAMP_MAX + 1U]; + char equipment_id[TRAINLOG_ID_MAX + 1U]; + TrainlogLoadMode load_mode; + double max_weight_kg; +} TrainlogLatestExplicitMax; + +/* Exact-ID profile read; unknown IDs return NOT_FOUND. */ +TrainlogStatus trainlog_database_get_exercise_profile( + TrainlogDatabase *database, + const char *exercise_id, + TrainlogExercise *output +); + +/** + * Current-data keyset page ordered by the exact started_at instant, then + * session_id and entry_id bytewise DESC. Accepted timestamps use the frozen + * extended RFC3339 grammar, including T/t, Z/z, numeric offsets through + * 23:59, arbitrary fractional precision, and the Android writer's omitted + * seconds form. Equivalent trailing-zero fractions represent one instant. + * A non-NULL cursor is exclusive. Pages are not a cross-call snapshot under + * concurrent edits. Cursor arrays are borrowed for this call, must be NUL + * terminated within their declared capacities, and started_at must be a real + * accepted instant with an explicit numeric offset or Z/z. Malformed cursors + * are INVALID_ARGUMENT before any history query. Malformed matching persisted + * timestamps, or a selected timestamp too long for the fixed public output, + * are DATABASE_ERROR rather than omitted or truncated. limit must be + * 1..TRAINLOG_OCCURRENCE_PAGE_MAX. + */ +TrainlogStatus trainlog_database_list_exercise_occurrences_page( + TrainlogDatabase *database, + const char *exercise_id, + const TrainlogExerciseOccurrenceCursor *after, + size_t limit, + TrainlogExerciseOccurrence *output, + size_t *output_count, + bool *output_has_more, + TrainlogExerciseOccurrenceCursor *output_next +); + +/* Original set positions are preserved. after_position is exclusive; use -1 + * for the first page. Reps are non-negative, so zero preserves a failed + * attempt. Nullable values remain distinct from zero. Corrupt storage types or + * values outside the public int/size_t ranges return DATABASE_ERROR. No rows + * exist for continuous occurrences. Output remains caller-owned. */ +TrainlogStatus trainlog_database_list_occurrence_sets_page( + TrainlogDatabase *database, + const char *entry_id, + int after_position, + size_t limit, + TrainlogOccurrenceSet *output, + size_t *output_count, + bool *output_has_more, + int *output_next_position +); + +/* Reads max_results joined only through max_test sessions; ordinary large sets + * never qualify. It uses the same exact instant and bytewise-ID ordering as + * occurrence pagination. Output keeps the exact source timestamp and + * occurrence load/equipment context; corrupt or over-capacity selected storage + * returns DATABASE_ERROR. */ +TrainlogStatus trainlog_database_latest_explicit_max_context( + TrainlogDatabase *database, + const char *exercise_id, + TrainlogLatestExplicitMax *output +); + /* TRAINLOG_SESSION_EDIT_API */ diff --git a/tui/include/trainlog/training_context.h b/tui/include/trainlog/training_context.h new file mode 100644 index 0000000..9a7707b --- /dev/null +++ b/tui/include/trainlog/training_context.h @@ -0,0 +1,50 @@ +#ifndef TRAINLOG_TRAINING_CONTEXT_H +#define TRAINLOG_TRAINING_CONTEXT_H + +/** @file training_context.h Read-only runtime/scientific exercise composition. */ + +#include "trainlog/database.h" +#include "trainlog/training_knowledge.h" + +typedef struct TrainlogTrainingOccurrenceView { + TrainlogExerciseOccurrence occurrence; + size_t set_offset; + size_t set_count; + bool sets_have_more; + int next_set_position; +} TrainlogTrainingOccurrenceView; + +typedef struct TrainlogTrainingExerciseContext { + TrainlogExercise exercise; + TrainlogExerciseBodyZone *persisted_zones; + size_t persisted_zone_count; + const TrainlogExerciseKnowledge *knowledge; /* borrowed process-lifetime data */ + const TrainlogEquipmentKnowledge **compatible_equipment; /* owned pointer array */ + size_t compatible_equipment_count; + TrainlogLatestExplicitMax latest_max; + TrainlogTrainingOccurrenceView *occurrences; + size_t occurrence_count; + bool occurrences_have_more; + TrainlogExerciseOccurrenceCursor next_occurrence; + TrainlogOccurrenceSet *sets; + size_t set_count; +} TrainlogTrainingExerciseContext; + +/** + * Compose one exact runtime ID. occurrence_limit is 1..32 and set_preview_limit + * is 1..64 per occurrence. The caller owns output allocations and must call + * release after success. Unknown runtime IDs return NOT_FOUND; missing knowledge + * is valid and represented by NULL. One nested-safe database read snapshot + * covers the call. No catalog relationship fabricates occurrence history. + */ +TrainlogStatus trainlog_training_exercise_context_load( + TrainlogDatabase *database, + const char *exercise_id, + size_t occurrence_limit, + size_t set_preview_limit, + TrainlogTrainingExerciseContext *output +); + +void trainlog_training_exercise_context_release(TrainlogTrainingExerciseContext *context); + +#endif diff --git a/tui/include/trainlog/training_knowledge.h b/tui/include/trainlog/training_knowledge.h new file mode 100644 index 0000000..141c449 --- /dev/null +++ b/tui/include/trainlog/training_knowledge.h @@ -0,0 +1,219 @@ +#ifndef TRAINLOG_TRAINING_KNOWLEDGE_H +#define TRAINLOG_TRAINING_KNOWLEDGE_H + +/** + * @file training_knowledge.h + * @brief Immutable, evidence-linked TRAINING KNOWLEDGE V1 catalog API. + * + * All returned records and strings are borrowed from generated const storage + * and remain valid for process lifetime. Newline-separated ID fields contain + * exact stable IDs; labels are never used as identities. + */ + +#include +#include + +#include "trainlog/status.h" + +typedef enum TrainlogKnowledgeMuscleRole { + TRAINLOG_KNOWLEDGE_ROLE_ANY = 0, + TRAINLOG_KNOWLEDGE_ROLE_PRIMARY, + TRAINLOG_KNOWLEDGE_ROLE_SECONDARY, + TRAINLOG_KNOWLEDGE_ROLE_STABILIZER +} TrainlogKnowledgeMuscleRole; + +typedef struct TrainlogKnowledgeReference { + const char *ref_id; + const char *title; + const char *authors_or_organization; + int year; /* zero means the catalog explicitly records an unknown year */ + const char *type; + const char *url; + const char *doi; + const char *pmid; + const char *topics; + const char *notes; + const char *limitations; + const char *accessed_on; + const char *publication_note; +} TrainlogKnowledgeReference; + +typedef struct TrainlogKnowledgeMuscle { + const char *muscle_id; + const char *display_name; + const char *display_name_fr; + const char *entity_type; + const char *anatomical_group; + const char *aggregate_group_id; + const char *member_muscle_ids; + const char *body_zone_ids; + const char *joint_action_ids; + const char *primary_actions; + const char *primary_actions_semantics; + const char *functional_notes; + const char *confidence; + const char *evidence_type; + const char *source_refs; + const char *overlap_warning; +} TrainlogKnowledgeMuscle; + +typedef struct TrainlogKnowledgeJointAction { + const char *action_id; + const char *display_name_fr; + const char *definition; + const char *anatomical_region; + const char *joint_complex; + const char *principal_plane; + const char *plane_notes; + const char *contributing_muscle_ids; + const char *contributor_semantics; + const char *confidence; + const char *evidence_type; + const char *source_refs; + const char *notes; +} TrainlogKnowledgeJointAction; + +typedef struct TrainlogKnowledgeMovementPattern { + const char *pattern_id; + const char *display_name_fr; + const char *definition; + const char *parent_pattern_id; + const char *typical_action_ids; + const char *typical_body_zone_ids; + const char *body_zone_semantics; + const char *confidence; + const char *evidence_type; + const char *source_refs; + const char *notes; +} TrainlogKnowledgeMovementPattern; + +typedef struct TrainlogKnowledgeInterpretation { + const char *family_description; + const char *action_ids; + const char *pattern_ids; + const char *primary_muscle_ids; + const char *secondary_muscle_ids; + const char *stabilizer_muscle_ids; + const char *primary_zone_id; + const char *secondary_zone_ids; + const char *confidence; + const char *evidence_type; + const char *source_refs; + const char *variant_notes; + const char *role_notes; + const char *required_confirmation; +} TrainlogKnowledgeInterpretation; + +typedef struct TrainlogExerciseKnowledge { + const char *exercise_id; + const char *exercise_name; + const char *resolution_status; + const char *confidence; + const char *equipment_ids; + const char *identity_evidence; + const char *identity_evidence_type; + const char *source_refs; + const char *limitations; + const char *equipment_link_status; + const TrainlogKnowledgeInterpretation *interpretation; + const TrainlogKnowledgeInterpretation *conditional_interpretation; +} TrainlogExerciseKnowledge; + +/* WHY: the authored BODY ZONES review is part of the immutable scientific + * catalog contract. Exposing every field prevents clients from substituting + * the resolved interpretation for the review decision. All pointers are + * borrowed from generated storage and remain valid for process lifetime. */ +typedef struct TrainlogKnowledgeBodyZoneAudit { + const char *exercise_id; + const char *status; + const char *severity; + const char *rationale; + const char *confidence; + const char *source_refs; + const char *existing_primary_zone_id; + const char *existing_secondary_zone_ids; + const char *scientific_primary_zone_id; + const char *scientific_secondary_zone_ids; + const char *proposed_mutation; +} TrainlogKnowledgeBodyZoneAudit; + +typedef struct TrainlogEquipmentKnowledge { + const char *equipment_id; + const char *manufacturer; + const char *model; + const char *identification_status; + const char *scientific_status; + const char *scientific_status_scope; + const char *catalog_type; + const char *catalog_load_semantics; + const char *mechanics; + const char *confidence; + const char *evidence_type; + const char *source_refs; + const char *limitations; + const char *audit_note; + bool requires_actual_exercise; +} TrainlogEquipmentKnowledge; + +typedef struct TrainlogEquipmentCapability { + const char *equipment_id; + const char *display_name; + const char *exercise_ids; + const char *link_status; + const char *requirements; + const TrainlogKnowledgeInterpretation *interpretation; +} TrainlogEquipmentCapability; + +typedef struct TrainlogKnowledgeQuery { + const char *scientific_zone_id; + bool include_zone_descendants; + const char *movement_pattern_id; + const char *muscle_id; + TrainlogKnowledgeMuscleRole muscle_role; + const char *available_equipment_id; +} TrainlogKnowledgeQuery; + +size_t trainlog_knowledge_reference_count(void); +const TrainlogKnowledgeReference *trainlog_knowledge_reference_at(size_t index); +const TrainlogKnowledgeReference *trainlog_knowledge_reference_lookup(const char *ref_id); +size_t trainlog_knowledge_muscle_count(void); +const TrainlogKnowledgeMuscle *trainlog_knowledge_muscle_at(size_t index); +const TrainlogKnowledgeMuscle *trainlog_knowledge_muscle_lookup(const char *muscle_id); +size_t trainlog_knowledge_joint_action_count(void); +const TrainlogKnowledgeJointAction *trainlog_knowledge_joint_action_at(size_t index); +const TrainlogKnowledgeJointAction *trainlog_knowledge_joint_action_lookup(const char *action_id); +size_t trainlog_knowledge_movement_pattern_count(void); +const TrainlogKnowledgeMovementPattern *trainlog_knowledge_movement_pattern_at(size_t index); +const TrainlogKnowledgeMovementPattern *trainlog_knowledge_movement_pattern_lookup(const char *pattern_id); +size_t trainlog_exercise_knowledge_count(void); +const TrainlogExerciseKnowledge *trainlog_exercise_knowledge_at(size_t index); +const TrainlogExerciseKnowledge *trainlog_exercise_knowledge_lookup(const char *exercise_id); +/* at() returns NULL outside the immutable snapshot; lookup() returns NULL for + * NULL, empty, or unknown IDs. Neither function transfers ownership. */ +size_t trainlog_knowledge_body_zone_audit_count(void); +const TrainlogKnowledgeBodyZoneAudit *trainlog_knowledge_body_zone_audit_at(size_t index); +const TrainlogKnowledgeBodyZoneAudit *trainlog_knowledge_body_zone_audit_lookup(const char *exercise_id); +/* Only this explicitly named accessor exposes conditional candidate data. */ +const TrainlogKnowledgeInterpretation *trainlog_exercise_knowledge_conditional(const char *exercise_id); +size_t trainlog_equipment_knowledge_count(void); +const TrainlogEquipmentKnowledge *trainlog_equipment_knowledge_at(size_t index); +const TrainlogEquipmentKnowledge *trainlog_equipment_knowledge_lookup(const char *equipment_id); +size_t trainlog_equipment_capability_count(void); +const TrainlogEquipmentCapability *trainlog_equipment_capability_at(size_t index); + +/** + * AND-combine optional resolved-knowledge filters in stable exercise-ID order. + * muscle_id may be NULL only when muscle_role is ROLE_ANY; a specific role + * and muscle_id must be supplied together. Unknown filter IDs return NOT_FOUND. Malformed arguments or insufficient + * capacity return INVALID_ARGUMENT. output_count always receives the required + * count after valid filters are resolved, so truncation is never reported as + * success. Conditional and unresolved records can never match this function. + */ +TrainlogStatus trainlog_exercise_knowledge_query( + const TrainlogKnowledgeQuery *query, + const TrainlogExerciseKnowledge **output, + size_t capacity, + size_t *output_count +); + +#endif diff --git a/tui/meson.build b/tui/meson.build index d9df4cb..3c4b48d 100644 --- a/tui/meson.build +++ b/tui/meson.build @@ -30,6 +30,28 @@ body_zone_catalog_generated = custom_target( command: [find_program('python3'), meson.project_source_root() / 'tools/generate_body_zone_catalog.py', '@INPUT@', '@OUTPUT@'], ) +training_knowledge_generated = custom_target( + 'training_knowledge_generated', + input: [ + meson.project_source_root() / 'catalog/body-zones-v1.json', + meson.project_source_root() / 'catalog/equipment-v1.json', + meson.project_source_root() / 'catalog/science-references-v1.json', + meson.project_source_root() / 'catalog/muscles-v1.json', + meson.project_source_root() / 'catalog/joint-actions-v1.json', + meson.project_source_root() / 'catalog/movement-patterns-v1.json', + meson.project_source_root() / 'catalog/exercise-knowledge-v1.json', + meson.project_source_root() / 'catalog/equipment-knowledge-v1.json', + meson.project_source_root() / 'catalog/training-knowledge-audit-v1.json', + ], + output: 'training_knowledge_generated.c', + depend_files: [ + meson.project_source_root() / 'tools/generate_training_knowledge.py', + meson.project_source_root() / 'tools/validate_training_knowledge.py', + ], + command: [find_program('python3'), meson.project_source_root() / 'tools/generate_training_knowledge.py', + meson.project_source_root() / 'catalog', '@OUTPUT@'], +) + strict_c_args = [ '-D_POSIX_C_SOURCE=200809L', '-Wconversion', @@ -45,15 +67,18 @@ trainlog_core_sources = files( 'src/duration.c', 'src/id.c', 'src/timeutil.c', + 'src/training_context.c', 'src/usb.c', 'src/mtp.c', 'src/reps.c', 'src/measured_max.c', 'src/sync.c', 'src/sync_history.c', + 'src/timestamp.c', ) trainlog_core_sources += equipment_catalog_generated trainlog_core_sources += body_zone_catalog_generated +trainlog_core_sources += training_knowledge_generated trainlog_core = static_library( 'trainlog_core', @@ -142,6 +167,22 @@ test_body_zones = executable( ) test('body_zones', test_body_zones) +test_training_knowledge = executable( + 'test_training_knowledge', + 'tests/test_training_knowledge.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) +test('training_knowledge', test_training_knowledge) + +test_training_context = executable( + 'test_training_context', + 'tests/test_training_context.c', + dependencies: trainlog_core_dep, + c_args: strict_c_args, +) +test('training_context', test_training_context) + test_custom_equipment = executable( 'test_custom_equipment', 'tests/test_custom_equipment.c', @@ -399,6 +440,12 @@ test( python3_trainlog_tests = find_program('python3') +test( + 'timestamp_validation', + python3_trainlog_tests, + args: [meson.project_source_root() / 'tests/test_timestamp_validation.py'], +) + test( 'mobile_import_variable_sets', python3_trainlog_tests, diff --git a/tui/src/database.c b/tui/src/database.c index 1612bc8..20eed52 100644 --- a/tui/src/database.c +++ b/tui/src/database.c @@ -8,8 +8,10 @@ #include "trainlog/duration.h" #include "trainlog/equipment_catalog.h" #include "trainlog/id.h" +#include "timestamp.h" #include +#include #include #include #include @@ -19,8 +21,38 @@ struct TrainlogDatabase { sqlite3 *connection; + unsigned int read_snapshot_depth; }; +TrainlogStatus trainlog_database_read_snapshot_begin(TrainlogDatabase *database) +{ + char sql[64]; + if (database == NULL || database->connection == NULL || database->read_snapshot_depth == UINT_MAX) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + (void)snprintf(sql, sizeof(sql), "SAVEPOINT trainlog_read_%u;", database->read_snapshot_depth); + if (sqlite3_exec(database->connection, sql, NULL, NULL, NULL) != SQLITE_OK) + return TRAINLOG_STATUS_DATABASE_ERROR; + ++database->read_snapshot_depth; + return TRAINLOG_STATUS_OK; +} + +TrainlogStatus trainlog_database_read_snapshot_end(TrainlogDatabase *database, bool commit_snapshot) +{ + char sql[160]; + unsigned int depth; + if (database == NULL || database->connection == NULL || database->read_snapshot_depth == 0U) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + depth = database->read_snapshot_depth - 1U; + if (commit_snapshot) + (void)snprintf(sql, sizeof(sql), "RELEASE trainlog_read_%u;", depth); + else + (void)snprintf(sql, sizeof(sql), "ROLLBACK TO trainlog_read_%u; RELEASE trainlog_read_%u;", depth, depth); + if (sqlite3_exec(database->connection, sql, NULL, NULL, NULL) != SQLITE_OK) + return TRAINLOG_STATUS_DATABASE_ERROR; + database->read_snapshot_depth = depth; + return TRAINLOG_STATUS_OK; +} + static TrainlogStatus lookup_exercise_row_id( TrainlogDatabase *database, const char *exercise_id, @@ -6102,3 +6134,486 @@ TrainlogStatus trainlog_database_update_body_observation( ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; } + +/* TRAINING_KNOWLEDGE_RUNTIME_READ_V1 */ +static bool knowledge_copy_column(sqlite3_stmt *statement, int column, char *output, size_t capacity) +{ + const unsigned char *value; + size_t length; + if (statement == NULL || sqlite3_column_type(statement, column) != SQLITE_TEXT || + output == NULL || capacity == 0U) return false; + value = sqlite3_column_text(statement, column); + if (value == NULL) return false; + length = (size_t)sqlite3_column_bytes(statement, column); + if (length >= capacity) return false; + (void)memcpy(output, value, length + 1U); + return true; +} + +static bool knowledge_numeric_column(sqlite3_stmt *statement, int column) +{ + int type = sqlite3_column_type(statement, column); + return type == SQLITE_INTEGER || type == SQLITE_FLOAT; +} + +static bool knowledge_bounded_string(const char *value, size_t capacity, size_t *length) +{ + const char *end; + if (value == NULL || capacity == 0U) return false; + end = memchr(value, '\0', capacity); + if (end == NULL || end == value) return false; + if (length != NULL) *length = (size_t)(end - value); + return true; +} + +typedef struct KnowledgeTemporalCandidate { + sqlite3_int64 row_id; + char *started_at; + size_t started_at_length; + char *session_id; + size_t session_id_length; + char *entry_id; + size_t entry_id_length; + TrainlogTimestampKey timestamp; +} KnowledgeTemporalCandidate; + +static void knowledge_temporal_candidate_release(KnowledgeTemporalCandidate *candidate) +{ + if (candidate == NULL) return; + free(candidate->started_at); + free(candidate->session_id); + free(candidate->entry_id); + (void)memset(candidate, 0, sizeof(*candidate)); +} + +static TrainlogStatus knowledge_copy_dynamic_column( + sqlite3_stmt *statement, int column, char **output, size_t *output_length) +{ + const unsigned char *source; + size_t length; + char *copy; + if (sqlite3_column_type(statement, column) != SQLITE_TEXT) return TRAINLOG_STATUS_DATABASE_ERROR; + source = sqlite3_column_text(statement, column); + if (source == NULL) return TRAINLOG_STATUS_DATABASE_ERROR; + length = (size_t)sqlite3_column_bytes(statement, column); + if (length == 0U || memchr(source, '\0', length) != NULL || length == SIZE_MAX) + return TRAINLOG_STATUS_DATABASE_ERROR; + copy = malloc(length + 1U); + if (copy == NULL) return TRAINLOG_STATUS_SYSTEM_ERROR; + (void)memcpy(copy, source, length); + copy[length] = '\0'; + *output = copy; + *output_length = length; + return TRAINLOG_STATUS_OK; +} + +static int knowledge_bytes_compare( + const char *left, size_t left_length, const char *right, size_t right_length) +{ + size_t common = left_length < right_length ? left_length : right_length; + int result = memcmp(left, right, common); + if (result != 0) return result; + if (left_length == right_length) return 0; + return left_length < right_length ? -1 : 1; +} + +/* INVARIANT: This is the single ordering relation used for selection and the + * exclusive cursor. Source timestamp spelling never participates in a tie. */ +static int knowledge_temporal_candidate_compare( + const KnowledgeTemporalCandidate *left, const KnowledgeTemporalCandidate *right) +{ + int result = trainlog_timestamp_compare(&left->timestamp, &right->timestamp); + if (result != 0) return result; + result = knowledge_bytes_compare(left->session_id, left->session_id_length, + right->session_id, right->session_id_length); + if (result != 0) return result; + return knowledge_bytes_compare(left->entry_id, left->entry_id_length, + right->entry_id, right->entry_id_length); +} + +static TrainlogStatus knowledge_temporal_candidate_read( + sqlite3_stmt *statement, KnowledgeTemporalCandidate *output) +{ + TrainlogStatus status; + (void)memset(output, 0, sizeof(*output)); + if (sqlite3_column_type(statement, 0) != SQLITE_INTEGER) return TRAINLOG_STATUS_DATABASE_ERROR; + status = knowledge_copy_dynamic_column(statement, 1, &output->session_id, &output->session_id_length); + if (status == TRAINLOG_STATUS_OK) + status = knowledge_copy_dynamic_column(statement, 2, &output->entry_id, &output->entry_id_length); + if (status == TRAINLOG_STATUS_OK) + status = knowledge_copy_dynamic_column(statement, 3, &output->started_at, &output->started_at_length); + if (status != TRAINLOG_STATUS_OK || + !trainlog_timestamp_parse(output->started_at, output->started_at_length, &output->timestamp)) { + knowledge_temporal_candidate_release(output); + return status == TRAINLOG_STATUS_OK ? TRAINLOG_STATUS_DATABASE_ERROR : status; + } + output->row_id = sqlite3_column_int64(statement, 0); + return TRAINLOG_STATUS_OK; +} + +/* Retain a descending prefix. Capacity is at most page limit + one, so a + * sorted bounded array keeps memory independent of history cardinality. */ +static void knowledge_temporal_candidate_insert( + KnowledgeTemporalCandidate *items, size_t *count, size_t capacity, + KnowledgeTemporalCandidate *candidate) +{ + size_t position = 0U; + size_t index; + while (position < *count && + knowledge_temporal_candidate_compare(candidate, &items[position]) <= 0) ++position; + if (position >= capacity) { + knowledge_temporal_candidate_release(candidate); + return; + } + if (*count == capacity) knowledge_temporal_candidate_release(&items[capacity - 1U]); + else ++*count; + for (index = *count - 1U; index > position; --index) items[index] = items[index - 1U]; + items[position] = *candidate; + (void)memset(candidate, 0, sizeof(*candidate)); +} + +static bool knowledge_tracking_mode(const char *value, TrainlogTrackingMode *output) +{ + if (value == NULL || output == NULL) return false; + if (strcmp(value,"reps")==0) {*output=TRAINLOG_TRACKING_REPS;return true;} + if (strcmp(value,"duration")==0) {*output=TRAINLOG_TRACKING_DURATION;return true;} + return false; +} + +static bool knowledge_recording_mode(const char *value, TrainlogRecordingMode *output) +{ + if (value == NULL || output == NULL) return false; + if (strcmp(value,"sets")==0) {*output=TRAINLOG_RECORDING_SETS;return true;} + if (strcmp(value,"continuous")==0) {*output=TRAINLOG_RECORDING_CONTINUOUS;return true;} + return false; +} + +static bool knowledge_load_mode(const char *value, TrainlogLoadMode *output) +{ + if (value == NULL || output == NULL) return false; + if (strcmp(value,"none")==0) {*output=TRAINLOG_LOAD_NONE;return true;} + if (strcmp(value,"external")==0) {*output=TRAINLOG_LOAD_EXTERNAL;return true;} + if (strcmp(value,"assistance")==0) {*output=TRAINLOG_LOAD_ASSISTANCE;return true;} + return false; +} + +TrainlogStatus trainlog_database_get_exercise_profile( + TrainlogDatabase *database, const char *exercise_id, TrainlogExercise *output) +{ + sqlite3_stmt *statement = NULL; + int rc; + if (database == NULL || database->connection == NULL || exercise_id == NULL || exercise_id[0] == '\0' || + output == NULL) return TRAINLOG_STATUS_INVALID_ARGUMENT; + (void)memset(output, 0, sizeof(*output)); + rc = sqlite3_prepare_v2(database->connection, + "SELECT exercise_id,name,tracking_mode,recording_mode,data_fields FROM exercises WHERE exercise_id=?1;", + -1, &statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT); + if (rc != SQLITE_OK) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; } + rc = sqlite3_step(statement); + if (rc == SQLITE_DONE) { (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_NOT_FOUND; } + if (rc != SQLITE_ROW || !knowledge_copy_column(statement, 0, output->exercise_id, sizeof(output->exercise_id)) || + !knowledge_copy_column(statement, 1, output->name, sizeof(output->name)) || + sqlite3_column_type(statement, 4) != SQLITE_INTEGER) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + if (sqlite3_column_type(statement,2)!=SQLITE_TEXT || sqlite3_column_type(statement,3)!=SQLITE_TEXT || + !knowledge_tracking_mode((const char *)sqlite3_column_text(statement,2),&output->tracking_mode) || + !knowledge_recording_mode((const char *)sqlite3_column_text(statement,3),&output->recording_mode)) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + if ((sqlite3_column_int64(statement, 4) < 0) || + ((sqlite3_uint64)sqlite3_column_int64(statement, 4) > UINT32_MAX)) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + output->data_fields = (TrainlogExerciseDataFields)sqlite3_column_int64(statement, 4); + return sqlite3_finalize(statement) == SQLITE_OK ? TRAINLOG_STATUS_OK : TRAINLOG_STATUS_DATABASE_ERROR; +} + +TrainlogStatus trainlog_database_list_exercise_occurrences_page( + TrainlogDatabase *database, const char *exercise_id, const TrainlogExerciseOccurrenceCursor *after, + size_t limit, TrainlogExerciseOccurrence *output, size_t *output_count, bool *output_has_more, + TrainlogExerciseOccurrenceCursor *output_next) +{ + static const char *const SCAN_SQL = + "SELECT se.id,s.session_id,se.entry_id,s.started_at FROM session_exercises se " + "JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id " + "WHERE e.exercise_id=?1;"; + static const char *const HYDRATE_SQL = + "SELECT s.session_id,se.entry_id,e.exercise_id,s.started_at,COALESCE(se.equipment_id,'')," + "s.session_type,e.tracking_mode,se.recording_mode,se.data_fields,se.load_mode," + "(SELECT COUNT(*) FROM performed_sets ps WHERE ps.session_exercise_row_id=se.id)," + "ca.duration_seconds,ca.speed_kmh,ca.distance_km FROM session_exercises se " + "JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id " + "LEFT JOIN continuous_activity ca ON ca.session_exercise_row_id=se.id WHERE se.id=?1;"; + sqlite3_stmt *statement = NULL; + KnowledgeTemporalCandidate selected[TRAINLOG_OCCURRENCE_PAGE_MAX + 1U] = {{0}}; + KnowledgeTemporalCandidate cursor = {0}; + TrainlogExerciseOccurrenceCursor after_value; + TrainlogExercise profile; + TrainlogStatus status; + size_t selected_count = 0U, count = 0U, index; + size_t cursor_started_length = 0U, cursor_session_length = 0U, cursor_entry_length = 0U; + int rc; + bool snapshot = false; + if (output_count != NULL) *output_count = 0U; + if (output_has_more != NULL) *output_has_more = false; + if (database == NULL || database->connection == NULL || exercise_id == NULL || exercise_id[0] == '\0' || + limit == 0U || limit > TRAINLOG_OCCURRENCE_PAGE_MAX || output == NULL || output_count == NULL || + output_has_more == NULL || output_next == NULL || + (after != NULL && (!knowledge_bounded_string(after->started_at, sizeof(after->started_at), + &cursor_started_length) || + !knowledge_bounded_string(after->session_id, sizeof(after->session_id), &cursor_session_length) || + !knowledge_bounded_string(after->entry_id, sizeof(after->entry_id), &cursor_entry_length) || + !trainlog_timestamp_parse(after->started_at, cursor_started_length, &cursor.timestamp)))) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + if (after != NULL) { + after_value = *after; + cursor.started_at = after_value.started_at; cursor.started_at_length = cursor_started_length; + cursor.session_id = after_value.session_id; cursor.session_id_length = cursor_session_length; + cursor.entry_id = after_value.entry_id; cursor.entry_id_length = cursor_entry_length; + if (!trainlog_timestamp_parse(cursor.started_at, cursor_started_length, &cursor.timestamp)) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + } + status = trainlog_database_read_snapshot_begin(database); + if (status != TRAINLOG_STATUS_OK) return status; + snapshot = true; + status = trainlog_database_get_exercise_profile(database, exercise_id, &profile); + if (status != TRAINLOG_STATUS_OK) goto done; + (void)memset(output_next, 0, sizeof(*output_next)); + rc = sqlite3_prepare_v2(database->connection, SCAN_SQL, -1, &statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(statement, 1, exercise_id, -1, SQLITE_TRANSIENT); + if (rc != SQLITE_OK) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + while ((rc = sqlite3_step(statement)) == SQLITE_ROW) { + KnowledgeTemporalCandidate candidate; + status = knowledge_temporal_candidate_read(statement, &candidate); + if (status != TRAINLOG_STATUS_OK) goto done; + if (after == NULL || knowledge_temporal_candidate_compare(&candidate, &cursor) < 0) + knowledge_temporal_candidate_insert(selected, &selected_count, limit + 1U, &candidate); + knowledge_temporal_candidate_release(&candidate); + } + if (rc != SQLITE_DONE) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + rc = sqlite3_finalize(statement); statement = NULL; + if (rc != SQLITE_OK) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + statement = NULL; + *output_has_more = selected_count > limit; + count = *output_has_more ? limit : selected_count; + for (index = 0U; index < count; ++index) { + TrainlogExerciseOccurrence *item; + sqlite3_int64 fields; + sqlite3_int64 sets; + rc = sqlite3_prepare_v2(database->connection, HYDRATE_SQL, -1, &statement, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_int64(statement, 1, selected[index].row_id); + if (rc == SQLITE_OK) rc = sqlite3_step(statement); + if (rc != SQLITE_ROW) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + item = &output[index]; (void)memset(item, 0, sizeof(*item)); + if (!knowledge_copy_column(statement,0,item->session_id,sizeof(item->session_id)) || + !knowledge_copy_column(statement,1,item->entry_id,sizeof(item->entry_id)) || + !knowledge_copy_column(statement,2,item->exercise_id,sizeof(item->exercise_id)) || + !knowledge_copy_column(statement,3,item->started_at,sizeof(item->started_at)) || + !knowledge_copy_column(statement,4,item->equipment_id,sizeof(item->equipment_id)) || + sqlite3_column_type(statement,5)!=SQLITE_TEXT || + !session_type_from_sql((const char *)sqlite3_column_text(statement,5),&item->session_type)) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + if(sqlite3_column_type(statement,6)!=SQLITE_TEXT || sqlite3_column_type(statement,7)!=SQLITE_TEXT || + sqlite3_column_type(statement,9)!=SQLITE_TEXT || + !knowledge_tracking_mode((const char *)sqlite3_column_text(statement,6),&item->tracking_mode) || + !knowledge_recording_mode((const char *)sqlite3_column_text(statement,7),&item->recording_mode) || + !knowledge_load_mode((const char *)sqlite3_column_text(statement,9),&item->load_mode)) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + if (sqlite3_column_type(statement,8)!=SQLITE_INTEGER || sqlite3_column_type(statement,10)!=SQLITE_INTEGER) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + fields=sqlite3_column_int64(statement,8); sets=sqlite3_column_int64(statement,10); + if (fields<0 || (sqlite3_uint64)fields>UINT32_MAX || sets<0 || (sqlite3_uint64)sets>SIZE_MAX) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + item->data_fields=(TrainlogExerciseDataFields)fields; + item->set_count=(size_t)sets; + if (sqlite3_column_type(statement,11)!=SQLITE_NULL) { + sqlite3_int64 duration; + if (sqlite3_column_type(statement,11)!=SQLITE_INTEGER) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + duration=sqlite3_column_int64(statement,11); + if (duration<=0 || duration>INT_MAX) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + item->continuous_duration_seconds=(int)duration; + } + item->continuous_has_speed=sqlite3_column_type(statement,12)!=SQLITE_NULL; + if (item->continuous_has_speed && !knowledge_numeric_column(statement,12)) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + item->continuous_speed_kmh=sqlite3_column_double(statement,12); + item->continuous_has_distance=sqlite3_column_type(statement,13)!=SQLITE_NULL; + if (item->continuous_has_distance && !knowledge_numeric_column(statement,13)) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + item->continuous_distance_km=sqlite3_column_double(statement,13); + if ((item->continuous_has_speed && !isfinite(item->continuous_speed_kmh)) || + (item->continuous_has_distance && !isfinite(item->continuous_distance_km))) { + status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; + } + rc = sqlite3_step(statement); + if (rc != SQLITE_DONE) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + rc = sqlite3_finalize(statement); statement = NULL; + if (rc != SQLITE_OK) { status = TRAINLOG_STATUS_DATABASE_ERROR; goto done; } + } + *output_count=count; + if (count > 0U) { + (void)snprintf(output_next->started_at,sizeof(output_next->started_at),"%s",output[count-1U].started_at); + (void)snprintf(output_next->session_id,sizeof(output_next->session_id),"%s",output[count-1U].session_id); + (void)snprintf(output_next->entry_id,sizeof(output_next->entry_id),"%s",output[count-1U].entry_id); + } + status = TRAINLOG_STATUS_OK; +done: + (void)sqlite3_finalize(statement); + for (index = 0U; index < selected_count; ++index) + knowledge_temporal_candidate_release(&selected[index]); + if (snapshot) { + TrainlogStatus end_status = trainlog_database_read_snapshot_end(database, status == TRAINLOG_STATUS_OK); + if (status == TRAINLOG_STATUS_OK) status = end_status; + } + return status; +} + +TrainlogStatus trainlog_database_list_occurrence_sets_page( + TrainlogDatabase *database, const char *entry_id, int after_position, size_t limit, + TrainlogOccurrenceSet *output, size_t *output_count, bool *output_has_more, int *output_next_position) +{ + sqlite3_stmt *statement = NULL; + sqlite3_stmt *identity = NULL; + size_t count=0U; + int rc; + if (output_count != NULL) *output_count=0U; + if (output_has_more != NULL) *output_has_more=false; + if (database==NULL || database->connection==NULL || entry_id==NULL || entry_id[0]=='\0' || + after_position < -1 || limit==0U || limit>TRAINLOG_OCCURRENCE_SET_PAGE_MAX || output==NULL || + output_count==NULL || output_has_more==NULL || output_next_position==NULL) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + *output_next_position=after_position; + rc=sqlite3_prepare_v2(database->connection,"SELECT 1 FROM session_exercises WHERE entry_id=?1;",-1,&identity,NULL); + if(rc==SQLITE_OK)rc=sqlite3_bind_text(identity,1,entry_id,-1,SQLITE_TRANSIENT); + if(rc!=SQLITE_OK){(void)sqlite3_finalize(identity);return TRAINLOG_STATUS_DATABASE_ERROR;} + rc=sqlite3_step(identity); + if(rc==SQLITE_DONE){(void)sqlite3_finalize(identity);return TRAINLOG_STATUS_NOT_FOUND;} + if(rc!=SQLITE_ROW || sqlite3_finalize(identity)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR; + rc=sqlite3_prepare_v2(database->connection, + "SELECT ps.position,ps.reps,ps.duration_seconds,ps.weight_kg FROM performed_sets ps " + "JOIN session_exercises se ON se.id=ps.session_exercise_row_id WHERE se.entry_id=?1 AND ps.position>?2 " + "ORDER BY ps.position ASC LIMIT ?3;",-1,&statement,NULL); + if(rc==SQLITE_OK) rc=sqlite3_bind_text(statement,1,entry_id,-1,SQLITE_TRANSIENT); + if(rc==SQLITE_OK) rc=sqlite3_bind_int(statement,2,after_position); + if(rc==SQLITE_OK) rc=sqlite3_bind_int64(statement,3,(sqlite3_int64)(limit+1U)); + if(rc!=SQLITE_OK){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + while((rc=sqlite3_step(statement))==SQLITE_ROW){ TrainlogOccurrenceSet *item; sqlite3_int64 position; + if(count==limit){*output_has_more=true;break;} item=&output[count];(void)memset(item,0,sizeof(*item)); + if(sqlite3_column_type(statement,0)!=SQLITE_INTEGER){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + position=sqlite3_column_int64(statement,0); if(position<0 || position>INT_MAX){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + item->position=(size_t)position; item->has_reps=sqlite3_column_type(statement,1)!=SQLITE_NULL; + item->has_duration=sqlite3_column_type(statement,2)!=SQLITE_NULL; + if (item->has_reps) { + sqlite3_int64 reps; + if (sqlite3_column_type(statement,1)!=SQLITE_INTEGER) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + reps=sqlite3_column_int64(statement,1); + if (reps<0 || reps>INT_MAX) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + item->reps=(int)reps; + } + if (item->has_duration) { + sqlite3_int64 duration; + if (sqlite3_column_type(statement,2)!=SQLITE_INTEGER) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + duration=sqlite3_column_int64(statement,2); + if (duration<=0 || duration>INT_MAX) { + (void)sqlite3_finalize(statement); return TRAINLOG_STATUS_DATABASE_ERROR; + } + item->duration_seconds=(int)duration; + } + item->has_weight=sqlite3_column_type(statement,3)!=SQLITE_NULL; + if(item->has_weight && !knowledge_numeric_column(statement,3)){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + item->weight_kg=sqlite3_column_double(statement,3); + if(item->has_weight && !isfinite(item->weight_kg)){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + *output_next_position=(int)position; ++count; + } + if(rc!=SQLITE_DONE && rc!=SQLITE_ROW){(void)sqlite3_finalize(statement);return TRAINLOG_STATUS_DATABASE_ERROR;} + if(sqlite3_finalize(statement)!=SQLITE_OK)return TRAINLOG_STATUS_DATABASE_ERROR; + *output_count=count; return TRAINLOG_STATUS_OK; +} + +TrainlogStatus trainlog_database_latest_explicit_max_context( + TrainlogDatabase *database, const char *exercise_id, TrainlogLatestExplicitMax *output) +{ + static const char *const SCAN_SQL = + "SELECT se.id,s.session_id,se.entry_id,s.started_at FROM max_results mr " + "JOIN session_exercises se ON se.id=mr.session_exercise_row_id " + "JOIN sessions s ON s.id=se.session_row_id JOIN exercises e ON e.id=se.exercise_row_id " + "WHERE e.exercise_id=?1 AND s.session_type='max_test';"; + static const char *const HYDRATE_SQL = + "SELECT s.session_id,se.entry_id,s.started_at,COALESCE(se.equipment_id,''),se.load_mode,mr.max_weight_kg " + "FROM max_results mr JOIN session_exercises se ON se.id=mr.session_exercise_row_id " + "JOIN sessions s ON s.id=se.session_row_id WHERE se.id=?1;"; + sqlite3_stmt *statement=NULL; + KnowledgeTemporalCandidate selected[1] = {{0}}; + size_t selected_count = 0U; + int rc; + TrainlogExercise profile; + TrainlogStatus status; + bool snapshot = false; + if(database==NULL || database->connection==NULL || exercise_id==NULL || exercise_id[0]=='\0' || output==NULL) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + (void)memset(output,0,sizeof(*output)); + status = trainlog_database_read_snapshot_begin(database); + if (status != TRAINLOG_STATUS_OK) return status; + snapshot = true; + status=trainlog_database_get_exercise_profile(database,exercise_id,&profile); + if(status!=TRAINLOG_STATUS_OK)goto done; + rc=sqlite3_prepare_v2(database->connection,SCAN_SQL,-1,&statement,NULL); + if(rc==SQLITE_OK)rc=sqlite3_bind_text(statement,1,exercise_id,-1,SQLITE_TRANSIENT); + if(rc!=SQLITE_OK){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + while((rc=sqlite3_step(statement))==SQLITE_ROW){ + KnowledgeTemporalCandidate candidate; + status=knowledge_temporal_candidate_read(statement,&candidate); + if(status!=TRAINLOG_STATUS_OK)goto done; + knowledge_temporal_candidate_insert(selected,&selected_count,1U,&candidate); + knowledge_temporal_candidate_release(&candidate); + } + if(rc!=SQLITE_DONE){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + rc=sqlite3_finalize(statement); statement=NULL; + if(rc!=SQLITE_OK){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + if(selected_count==0U){status=TRAINLOG_STATUS_OK;goto done;} + rc=sqlite3_prepare_v2(database->connection,HYDRATE_SQL,-1,&statement,NULL); + if(rc==SQLITE_OK)rc=sqlite3_bind_int64(statement,1,selected[0].row_id); + if(rc==SQLITE_OK)rc=sqlite3_step(statement); + if(rc!=SQLITE_ROW || !knowledge_copy_column(statement,0,output->session_id,sizeof(output->session_id)) || + !knowledge_copy_column(statement,1,output->entry_id,sizeof(output->entry_id)) || + !knowledge_copy_column(statement,2,output->started_at,sizeof(output->started_at)) || + !knowledge_copy_column(statement,3,output->equipment_id,sizeof(output->equipment_id))){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + if(sqlite3_column_type(statement,4)!=SQLITE_TEXT || + !knowledge_load_mode((const char *)sqlite3_column_text(statement,4),&output->load_mode)){ + status=TRAINLOG_STATUS_DATABASE_ERROR;goto done; + } + if(!knowledge_numeric_column(statement,5)){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + output->max_weight_kg=sqlite3_column_double(statement,5); + if(!isfinite(output->max_weight_kg) || output->max_weight_kg<=0.0){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + output->found=true; + rc=sqlite3_step(statement); + if(rc!=SQLITE_DONE){status=TRAINLOG_STATUS_DATABASE_ERROR;goto done;} + rc=sqlite3_finalize(statement); statement=NULL; + status=rc==SQLITE_OK?TRAINLOG_STATUS_OK:TRAINLOG_STATUS_DATABASE_ERROR; +done: + (void)sqlite3_finalize(statement); + knowledge_temporal_candidate_release(&selected[0]); + if(snapshot){ + TrainlogStatus end_status=trainlog_database_read_snapshot_end(database,status==TRAINLOG_STATUS_OK); + if(status==TRAINLOG_STATUS_OK)status=end_status; + } + return status; +} diff --git a/tui/src/timestamp.c b/tui/src/timestamp.c new file mode 100644 index 0000000..7fdfd6d --- /dev/null +++ b/tui/src/timestamp.c @@ -0,0 +1,108 @@ +#include "timestamp.h" + +#include + +static bool parse_digits(const char *value, size_t count, int *output) +{ + size_t index; + int result = 0; + for (index = 0U; index < count; ++index) { + if (value[index] < '0' || value[index] > '9') return false; + result = result * 10 + (value[index] - '0'); + } + *output = result; + return true; +} + +static bool leap_year(int year) +{ + return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); +} + +static int days_in_month(int year, int month) +{ + static const int DAYS[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + return month == 2 && leap_year(year) ? 29 : DAYS[month - 1]; +} + +/* Proleptic Gregorian day number relative to 0001-01-01. */ +static int64_t day_number(int year, int month, int day) +{ + int prior_year = year - 1; + int64_t days = (int64_t)prior_year * 365 + prior_year / 4 - + prior_year / 100 + prior_year / 400; + int current_month; + for (current_month = 1; current_month < month; ++current_month) + days += days_in_month(year, current_month); + return days + day - 1; +} + +bool trainlog_timestamp_parse( + const char *value, size_t length, TrainlogTimestampKey *output) +{ + size_t zone; + int year, month, day, hour, minute, second = 0; + int offset_hour = 0, offset_minute = 0, offset_sign = 0; + int64_t local_second; + + if (value == NULL || output == NULL || length < 17U || + !parse_digits(value, 4U, &year) || value[4] != '-' || + !parse_digits(value + 5, 2U, &month) || value[7] != '-' || + !parse_digits(value + 8, 2U, &day) || + (value[10] != 'T' && value[10] != 't') || + !parse_digits(value + 11, 2U, &hour) || value[13] != ':' || + !parse_digits(value + 14, 2U, &minute)) return false; + if (year < 1 || month < 1 || month > 12 || day < 1 || + day > days_in_month(year, month) || hour > 23 || minute > 59) + return false; + + zone = 16U; + output->fraction = NULL; + output->fraction_length = 0U; + if (zone < length && value[zone] == ':') { + if (zone + 3U > length || !parse_digits(value + zone + 1U, 2U, &second) || second > 59) + return false; + zone += 3U; + if (zone < length && value[zone] == '.') { + size_t fraction_start = ++zone; + while (zone < length && value[zone] >= '0' && value[zone] <= '9') ++zone; + if (zone == fraction_start) return false; + output->fraction = value + fraction_start; + output->fraction_length = zone - fraction_start; + } + } + + if (zone + 1U == length && (value[zone] == 'Z' || value[zone] == 'z')) { + offset_sign = 0; + } else { + if (zone + 6U != length || (value[zone] != '+' && value[zone] != '-') || + value[zone + 3U] != ':' || + !parse_digits(value + zone + 1U, 2U, &offset_hour) || + !parse_digits(value + zone + 4U, 2U, &offset_minute) || + offset_hour > 23 || offset_minute > 59) return false; + offset_sign = value[zone] == '+' ? 1 : -1; + } + + local_second = day_number(year, month, day) * INT64_C(86400) + + (int64_t)hour * INT64_C(3600) + (int64_t)minute * INT64_C(60) + second; + output->utc_second = local_second - offset_sign * + ((int64_t)offset_hour * INT64_C(3600) + (int64_t)offset_minute * INT64_C(60)); + return true; +} + +int trainlog_timestamp_compare( + const TrainlogTimestampKey *left, const TrainlogTimestampKey *right) +{ + size_t index; + size_t count; + if (left->utc_second != right->utc_second) + return left->utc_second < right->utc_second ? -1 : 1; + count = left->fraction_length > right->fraction_length + ? left->fraction_length : right->fraction_length; + for (index = 0U; index < count; ++index) { + char left_digit = index < left->fraction_length ? left->fraction[index] : '0'; + char right_digit = index < right->fraction_length ? right->fraction[index] : '0'; + if (left_digit != right_digit) return left_digit < right_digit ? -1 : 1; + } + return 0; +} diff --git a/tui/src/timestamp.h b/tui/src/timestamp.h new file mode 100644 index 0000000..ea438c9 --- /dev/null +++ b/tui/src/timestamp.h @@ -0,0 +1,29 @@ +#ifndef TRAINLOG_INTERNAL_TIMESTAMP_H +#define TRAINLOG_INTERNAL_TIMESTAMP_H + +#include +#include +#include + +typedef struct TrainlogTimestampKey { + int64_t utc_second; + const char *fraction; + size_t fraction_length; +} TrainlogTimestampKey; + +/* CONTRACT: Parses the frozen Trainlog timestamp grammar from exactly length + * bytes. The key borrows fraction storage from value and remains valid only + * while value does. */ +bool trainlog_timestamp_parse( + const char *value, + size_t length, + TrainlogTimestampKey *output +); + +/* Returns less than, zero, or greater than zero by exact represented instant. */ +int trainlog_timestamp_compare( + const TrainlogTimestampKey *left, + const TrainlogTimestampKey *right +); + +#endif diff --git a/tui/src/training_context.c b/tui/src/training_context.c new file mode 100644 index 0000000..53dc2aa --- /dev/null +++ b/tui/src/training_context.c @@ -0,0 +1,118 @@ +#include "trainlog/training_context.h" + +#include +#include +#include + +static size_t id_list_count(const char *list) +{ + size_t count = 0U; + const char *at = list; + if (at == NULL || *at == '\0') return 0U; + count = 1U; + while (*at != '\0') { if (*at == '\n') ++count; ++at; } + return count; +} + +static const TrainlogEquipmentKnowledge *equipment_from_list(const char **at) +{ + const char *end; + char id[TRAINLOG_ID_MAX + 1U]; + size_t length; + if (at == NULL || *at == NULL || **at == '\0') return NULL; + end = strchr(*at, '\n'); + length = end == NULL ? strlen(*at) : (size_t)(end - *at); + if (length == 0U || length >= sizeof(id)) return NULL; + (void)memcpy(id, *at, length); id[length] = '\0'; + *at = end == NULL ? *at + length : end + 1; + return trainlog_equipment_knowledge_lookup(id); +} + +void trainlog_training_exercise_context_release(TrainlogTrainingExerciseContext *context) +{ + if (context == NULL) return; + free(context->persisted_zones); + free(context->compatible_equipment); + free(context->occurrences); + free(context->sets); + (void)memset(context, 0, sizeof(*context)); +} + +TrainlogStatus trainlog_training_exercise_context_load( + TrainlogDatabase *database, const char *exercise_id, size_t occurrence_limit, + size_t set_preview_limit, TrainlogTrainingExerciseContext *output) +{ + TrainlogTrainingExerciseContext result = {0}; + TrainlogStatus status; + const char *equipment_at; + size_t index; + size_t zone_count = 0U; + bool snapshot = false; + if (database == NULL || exercise_id == NULL || exercise_id[0] == '\0' || output == NULL || + occurrence_limit == 0U || occurrence_limit > TRAINLOG_OCCURRENCE_PAGE_MAX || + set_preview_limit == 0U || set_preview_limit > TRAINLOG_OCCURRENCE_SET_PAGE_MAX) + return TRAINLOG_STATUS_INVALID_ARGUMENT; + (void)memset(output, 0, sizeof(*output)); + status = trainlog_database_read_snapshot_begin(database); + if (status != TRAINLOG_STATUS_OK) return status; + snapshot = true; + status = trainlog_database_get_exercise_profile(database, exercise_id, &result.exercise); + if (status != TRAINLOG_STATUS_OK) goto done; + status = trainlog_database_list_exercise_body_zones(database, exercise_id, NULL, 0U, &zone_count); + if (status != TRAINLOG_STATUS_OK && !(status == TRAINLOG_STATUS_INVALID_ARGUMENT && zone_count > 0U)) goto done; + if (zone_count > SIZE_MAX / sizeof(*result.persisted_zones)) { status=TRAINLOG_STATUS_INVALID_ARGUMENT; goto done; } + if (zone_count > 0U) { + result.persisted_zones = calloc(zone_count, sizeof(*result.persisted_zones)); + if (result.persisted_zones == NULL) { status=TRAINLOG_STATUS_SYSTEM_ERROR; goto done; } + status=trainlog_database_list_exercise_body_zones(database,exercise_id,result.persisted_zones,zone_count,&result.persisted_zone_count); + if(status!=TRAINLOG_STATUS_OK)goto done; + } + result.knowledge=trainlog_exercise_knowledge_lookup(exercise_id); + if(result.knowledge!=NULL){ + result.compatible_equipment_count=id_list_count(result.knowledge->equipment_ids); + if(result.compatible_equipment_count>SIZE_MAX/sizeof(*result.compatible_equipment)){status=TRAINLOG_STATUS_INVALID_ARGUMENT;goto done;} + if(result.compatible_equipment_count>0U){ + result.compatible_equipment=calloc(result.compatible_equipment_count,sizeof(*result.compatible_equipment)); + if(result.compatible_equipment==NULL){status=TRAINLOG_STATUS_SYSTEM_ERROR;goto done;} + equipment_at=result.knowledge->equipment_ids; + for(index=0U;index0U && set_preview_limit>SIZE_MAX/result.occurrence_count){status=TRAINLOG_STATUS_INVALID_ARGUMENT;goto done;} + result.sets=calloc(result.occurrence_count*set_preview_limit,sizeof(*result.sets)); + if(result.occurrence_count>0U && result.sets==NULL){status=TRAINLOG_STATUS_SYSTEM_ERROR;goto done;} + for(index=0U;indexset_offset=result.set_count; view->next_set_position=-1; + if(view->occurrence.recording_mode==TRAINLOG_RECORDING_CONTINUOUS)continue; + status=trainlog_database_list_occurrence_sets_page(database,view->occurrence.entry_id,-1,set_preview_limit, + result.sets+result.set_count,&count,&view->sets_have_more,&view->next_set_position); + if (status != TRAINLOG_STATUS_OK) { + goto done; + } + view->set_count = count; + result.set_count += count; + } +done: + if (snapshot) { + TrainlogStatus end_status=trainlog_database_read_snapshot_end(database,status==TRAINLOG_STATUS_OK); + if(status==TRAINLOG_STATUS_OK)status=end_status; + } + if(status!=TRAINLOG_STATUS_OK){trainlog_training_exercise_context_release(&result);return status;} + *output=result; return TRAINLOG_STATUS_OK; +} diff --git a/tui/src/tui.c b/tui/src/tui.c index 28641f0..8be5a90 100644 --- a/tui/src/tui.c +++ b/tui/src/tui.c @@ -20,6 +20,7 @@ #include #include +#include #include #include "trainlog/terminal.h" @@ -39,6 +40,7 @@ #include "trainlog/reps.h" #include "trainlog/theme.h" #include "trainlog/timeutil.h" +#include "trainlog/training_knowledge.h" #include "trainlog/usb.h" #define MAX_EXERCISES 128U @@ -2380,6 +2382,180 @@ static bool edit_exercise_body_zones( return status == TRAINLOG_STATUS_OK; } +/* CONTRACT: knowledge text is read-only catalogue data. The screen stores + * display lines locally so navigation never changes the scientific record. */ +#define KNOWLEDGE_LINES_MAX 128U +#define KNOWLEDGE_LINE_MAX 256U + +static void knowledge_add_wrapped(char lines[][KNOWLEDGE_LINE_MAX], size_t *count, + const char *text, int width) +{ + const char *at = text == NULL ? "" : text; + size_t used = 0U; + int cells = 0; + + /* WHY: terminal columns count display cells, while French labels are UTF-8; + * utf8proc prevents a wrap from splitting an accented character or from + * writing past the right border for a wide code point. */ + while (*at != '\0' && *count < KNOWLEDGE_LINES_MAX) { + utf8proc_int32_t codepoint; + utf8proc_ssize_t bytes = utf8proc_iterate((const utf8proc_uint8_t *)at, + -1, &codepoint); + int codepoint_cells; + if (bytes <= 0) { codepoint = (unsigned char)*at; bytes = 1; } + codepoint_cells = utf8proc_charwidth(codepoint); + if (codepoint_cells < 0) codepoint_cells = 1; + if (cells > 0 && cells + codepoint_cells > width) { + lines[*count][used] = '\0'; + ++*count; + used = 0U; + cells = 0; + continue; + } + if (used + (size_t)bytes >= KNOWLEDGE_LINE_MAX - 1U) break; + (void)memcpy(lines[*count] + used, at, (size_t)bytes); + used += (size_t)bytes; + cells += codepoint_cells; + at += bytes; + } + /* INVARIANT: every entry is NUL-terminated before rendering, and the fixed + * line capacity bounds catalogue display independently of terminal size. */ + if (*count < KNOWLEDGE_LINES_MAX) { + lines[*count][used] = '\0'; + ++*count; + } +} + +static void knowledge_add_ids(char lines[][KNOWLEDGE_LINE_MAX], size_t *count, + const char *heading, const char *ids, bool muscles, + int width) +{ + const char *at = ids; + knowledge_add_wrapped(lines, count, heading, width); + while (at != NULL && *at != '\0' && *count < KNOWLEDGE_LINES_MAX) { + const char *end = strchr(at, '\n'); + size_t length = end == NULL ? strlen(at) : (size_t)(end - at); + char id[96]; + const char *label = NULL; + if (length >= sizeof(id)) break; + (void)memcpy(id, at, length); + id[length] = '\0'; + if (muscles) { + const TrainlogKnowledgeMuscle *item = trainlog_knowledge_muscle_lookup(id); + if (item != NULL) label = item->display_name_fr; + } else { + const TrainlogKnowledgeMovementPattern *item = + trainlog_knowledge_movement_pattern_lookup(id); + if (item != NULL) label = item->display_name_fr; + } + knowledge_add_wrapped(lines, count, label == NULL ? id : label, width); + if (end == NULL) break; + at = end + 1; + } +} + +static void knowledge_add_plain_ids(char lines[][KNOWLEDGE_LINE_MAX], size_t *count, + const char *heading, const char *ids, int width) +{ + const char *at = ids; + knowledge_add_wrapped(lines, count, heading, width); + while (at != NULL && *at != '\0' && *count < KNOWLEDGE_LINES_MAX) { + const char *end = strchr(at, '\n'); + size_t length = end == NULL ? strlen(at) : (size_t)(end - at); + char id[96]; + if (length >= sizeof(id)) break; + (void)memcpy(id, at, length); + id[length] = '\0'; + knowledge_add_wrapped(lines, count, id, width); + if (end == NULL) break; + at = end + 1; + } +} + +static void knowledge_add_zones(char lines[][KNOWLEDGE_LINE_MAX], size_t *count, + const TrainlogKnowledgeInterpretation *value, int width) +{ + const TrainlogBodyZone *zone; + knowledge_add_wrapped(lines, count, "Zones scientifiques :", width); + zone = trainlog_body_zone_catalog_lookup(value->primary_zone_id); + knowledge_add_wrapped(lines, count, + zone == NULL ? value->primary_zone_id : zone->display_name, width); + knowledge_add_plain_ids(lines, count, "Zones secondaires :", + value->secondary_zone_ids, width); +} + +static void screen_exercise_knowledge(const TrainlogExercise *exercise) +{ + const TrainlogExerciseKnowledge *record; + const TrainlogKnowledgeInterpretation *value; + const char *label; + char lines[KNOWLEDGE_LINES_MAX][KNOWLEDGE_LINE_MAX]; + size_t line_count; + size_t scroll = 0U; + int key; + if (exercise == NULL) return; + record = trainlog_exercise_knowledge_lookup(exercise->exercise_id); + value = record == NULL ? NULL : record->interpretation; + label = "Connaissances validées"; + if (value == NULL && record != NULL && record->conditional_interpretation != NULL) { + value = record->conditional_interpretation; + label = "Interprétation conditionnelle — à confirmer"; + } + for (;;) { + int rows = trainlog_terminal_rows(tui_terminal); + int columns = trainlog_terminal_columns(tui_terminal); + int viewport_rows; + int width; + size_t index; + if (rows < 20 || columns < 72) { + draw_shell("TRAINLOG — Connaissances exercice", "b/Échap retour"); + trainlog_terminal_printf(tui_terminal, 3, 4, "Terminal trop petit — minimum 72x20."); + trainlog_terminal_render(tui_terminal); + key = trainlog_terminal_get_key(tui_terminal); + if (key == 27 || key == 'b' || key == 'B' || key == 'k' || key == 'K') return; + continue; + } + width = columns - 8; + viewport_rows = rows - 6; + line_count = 0U; + knowledge_add_wrapped(lines, &line_count, exercise->name, width); + if (record == NULL || value == NULL) { + knowledge_add_wrapped(lines, &line_count, + record == NULL ? "Aucune fiche scientifique pour cet identifiant." + : "Interprétation scientifique non résolue.", width); + } else { + knowledge_add_wrapped(lines, &line_count, label, width); + knowledge_add_ids(lines, &line_count, "Mouvement :", value->pattern_ids, false, width); + knowledge_add_wrapped(lines, &line_count, "Confiance :", width); + knowledge_add_wrapped(lines, &line_count, value->confidence, width); + knowledge_add_ids(lines, &line_count, "Muscles principaux :", + value->primary_muscle_ids, true, width); + knowledge_add_ids(lines, &line_count, "Secondaires :", + value->secondary_muscle_ids, true, width); + knowledge_add_ids(lines, &line_count, "Stabilisateurs :", + value->stabilizer_muscle_ids, true, width); + knowledge_add_zones(lines, &line_count, value, width); + knowledge_add_plain_ids(lines, &line_count, "Sources :", value->source_refs, width); + } + if (scroll >= line_count) scroll = line_count == 0U ? 0U : line_count - 1U; + draw_shell("TRAINLOG — Connaissances exercice", + "↑↓ défiler Pg↑/Pg↓ page b/Échap/k retour"); + for (index = 0U; index < (size_t)viewport_rows && scroll + index < line_count; ++index) + trainlog_terminal_printf(tui_terminal, 3 + (int)index, 4, "%s", lines[scroll + index]); + trainlog_terminal_render(tui_terminal); + key = trainlog_terminal_get_key(tui_terminal); + if (key == 27 || key == 'b' || key == 'B' || key == 'k' || key == 'K') return; + if (key == TRAINLOG_KEY_UP && scroll > 0U) --scroll; + else if (key == TRAINLOG_KEY_DOWN && scroll + (size_t)viewport_rows < line_count) ++scroll; + else if (key == TRAINLOG_KEY_PAGE_UP) { + scroll = scroll > (size_t)viewport_rows ? scroll - (size_t)viewport_rows : 0U; + } else if (key == TRAINLOG_KEY_PAGE_DOWN && scroll + (size_t)viewport_rows < line_count) { + size_t maximum = line_count - (size_t)viewport_rows; + scroll = scroll + (size_t)viewport_rows < maximum ? scroll + (size_t)viewport_rows : maximum; + } + } +} + static void screen_exercise_detail( TrainlogDatabase *database, TrainlogExercise *exercise @@ -2418,7 +2594,7 @@ static void screen_exercise_detail( int key; if (total > 0U && selected >= total) selected = total - 1U; draw_shell("TRAINLOG — Fiche exercice", - "↑↓ équipement Entrée fiche e modifier p performance m max mesuré b/Échap retour"); + "↑↓ équipement Entrée fiche k connaissances p performance m max b/Échap retour"); trainlog_terminal_printf(tui_terminal, 3, 4, "%s", exercise->name); trainlog_terminal_printf(tui_terminal, 4, 4, "Identifiant : %s · suivi : %s", exercise->exercise_id, @@ -2479,6 +2655,7 @@ static void screen_exercise_detail( ? &explicit_items[selected] : &historic_items[selected - explicit_count]); else if (key == 'p' || key == 'P') screen_exercise_performance(database, exercise); else if (key == 'm' || key == 'M') screen_exercise_measured_max(database, exercise); + else if (key == 'k' || key == 'K') screen_exercise_knowledge(exercise); else if (key == 'e' || key == 'E') (void)edit_exercise_body_zones(database, exercise); } } diff --git a/tui/tests/test_training_context.c b/tui/tests/test_training_context.c new file mode 100644 index 0000000..710e906 --- /dev/null +++ b/tui/tests/test_training_context.c @@ -0,0 +1,362 @@ +#include "trainlog/training_context.h" + +#include +#include +#include +#include + +#include + +#define CHECK(test) do { if (!(test)) { fprintf(stderr, "CHECK failed: %s:%d: %s\n", \ + __FILE__, __LINE__, #test); return 1; } } while (0) + +static void base_occurrence(TrainlogSessionExerciseInput *value, const char *entry_id, + const char *exercise_id, const char *equipment_id, TrainlogLoadMode mode) +{ + (void)memset(value, 0, sizeof(*value)); + (void)snprintf(value->entry_id, sizeof(value->entry_id), "%s", entry_id); + (void)snprintf(value->exercise_id, sizeof(value->exercise_id), "%s", exercise_id); + (void)snprintf(value->equipment_id, sizeof(value->equipment_id), "%s", equipment_id); + value->recording_mode = TRAINLOG_RECORDING_SETS; + value->load_mode = mode; + value->rest_seconds = 60; + value->target_sets = 2; + value->target_reps = 10; + value->target_has_weight = mode != TRAINLOG_LOAD_NONE; + value->target_weight_kg = 20.0; +} + +static int insert_session(TrainlogDatabase *database, const char *session_id, const char *started_at, + TrainlogSessionType type, TrainlogSessionExerciseInput *items, size_t count) +{ + TrainlogSessionInput session = {0}; + (void)snprintf(session.session_id, sizeof(session.session_id), "%s", session_id); + (void)snprintf(session.started_at, sizeof(session.started_at), "%s", started_at); + (void)snprintf(session.ended_at, sizeof(session.ended_at), "%s", started_at); + session.session_type = type; + session.exercises = items; + session.exercise_count = count; + return trainlog_database_insert_session(database, &session) == TRAINLOG_STATUS_OK; +} + +int main(void) +{ + const char *exercise_id = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde"; + const char *zones[] = {"glutes"}; + TrainlogDatabase *database = NULL; + TrainlogSessionExerciseInput occurrences[2]; + TrainlogSessionExerciseInput max_occurrence; + TrainlogSessionExerciseInput assistance_occurrence; + TrainlogSessionExerciseInput continuous_occurrence; + TrainlogSessionExerciseInput large_occurrence; + TrainlogSessionExerciseInput temporal_occurrence; + TrainlogSessionExerciseInput temporal_max; + TrainlogSetInput first_sets[2] = {{8,0,false,0.0},{6,0,true,0.0}}; + TrainlogSetInput second_sets[1] = {{11,0,true,30.0}}; + TrainlogSetInput assistance_set[1] = {{9,0,true,15.0}}; + TrainlogSetInput large_sets[65]; + TrainlogTrainingExerciseContext context; + TrainlogExerciseOccurrence page[1]; + TrainlogExerciseOccurrenceCursor next; + TrainlogExerciseOccurrenceCursor cursor; + bool has_more = false; + size_t count = 0U; + size_t index; + TrainlogOccurrenceSet set_page[64]; + int next_position = -1; + char database_path[] = "/tmp/trainlog-context-XXXXXX"; + int database_fd = mkstemp(database_path); + sqlite3 *raw = NULL; + + CHECK(database_fd >= 0); + CHECK(close(database_fd) == 0); + CHECK(trainlog_database_open(database_path, &database) == TRAINLOG_STATUS_OK); + CHECK(trainlog_database_insert_exercise_profiled(database, exercise_id, "Leg press renamed", + "leg press renamed", TRAINLOG_TRACKING_REPS, TRAINLOG_RECORDING_SETS, 0U) == TRAINLOG_STATUS_OK); + CHECK(trainlog_database_replace_exercise_body_zones(database, exercise_id, "thighs", zones, 1U) + == TRAINLOG_STATUS_OK); + base_occurrence(&occurrences[0], "entry_a", exercise_id, "leg_press", TRAINLOG_LOAD_EXTERNAL); + occurrences[0].sets=first_sets; occurrences[0].set_count=2U; + base_occurrence(&occurrences[1], "entry_b", exercise_id, "plate_loaded_leg_press", TRAINLOG_LOAD_EXTERNAL); + occurrences[1].sets=second_sets; occurrences[1].set_count=1U; + CHECK(insert_session(database,"session_b","2026-09-08T12:00:00+02:00",TRAINLOG_SESSION_TRAINING,occurrences,2U)); + base_occurrence(&assistance_occurrence,"entry_assist",exercise_id,"leg_press",TRAINLOG_LOAD_ASSISTANCE); + assistance_occurrence.sets=assistance_set; assistance_occurrence.set_count=1U; + CHECK(insert_session(database,"session_a","2026-09-08T11:00:00+00:00",TRAINLOG_SESSION_TRAINING,&assistance_occurrence,1U)); + base_occurrence(&max_occurrence,"entry_max",exercise_id,"leg_press",TRAINLOG_LOAD_EXTERNAL); + max_occurrence.has_max_weight=true; max_occurrence.max_weight_kg=120.0; + max_occurrence.target_sets=0; max_occurrence.target_reps=0; + max_occurrence.target_has_weight=false; max_occurrence.target_weight_kg=0.0; + max_occurrence.rest_seconds=0; + max_occurrence.load_mode=TRAINLOG_LOAD_NONE; + CHECK(insert_session(database,"session_max","2026-09-09T10:00:00+02:00",TRAINLOG_SESSION_MAX_TEST,&max_occurrence,1U)); + + CHECK(trainlog_training_exercise_context_load(database,exercise_id,4U,1U,&context)==TRAINLOG_STATUS_OK); + CHECK(strcmp(context.exercise.name,"Leg press renamed")==0); + CHECK(context.knowledge!=NULL && context.knowledge->interpretation!=NULL); + CHECK(context.persisted_zone_count==2U); + CHECK(context.compatible_equipment_count==2U); + CHECK(context.latest_max.found && context.latest_max.max_weight_kg==120.0); + CHECK(context.occurrence_count==4U); + CHECK(strcmp(context.occurrences[0].occurrence.entry_id,"entry_max")==0); + CHECK(context.occurrences[0].set_count==0U); /* explicit MAX is not a fake set */ + CHECK(context.occurrences[1].occurrence.load_mode==TRAINLOG_LOAD_ASSISTANCE); + CHECK(strcmp(context.occurrences[2].occurrence.entry_id,"entry_b")==0); + CHECK(context.occurrences[2].set_count==1U && !context.occurrences[2].sets_have_more); + CHECK(strcmp(context.occurrences[3].occurrence.entry_id,"entry_a")==0); + CHECK(context.occurrences[3].set_count==1U && context.occurrences[3].sets_have_more); + CHECK(context.sets[context.occurrences[3].set_offset].has_weight==false); + trainlog_training_exercise_context_release(&context); + CHECK(trainlog_database_list_occurrence_sets_page(database,"entry_a",0,64U,set_page,&count,&has_more, + &next_position)==TRAINLOG_STATUS_OK); + CHECK(count==1U && !has_more && set_page[0].has_weight && set_page[0].weight_kg==0.0); + + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,NULL,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_OK); + CHECK(count==1U && has_more); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&next,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_OK); + CHECK(count==1U); + cursor=next; cursor.entry_id[0]='\0'; + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)memset(cursor.started_at,'x',sizeof(cursor.started_at)); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)memset(cursor.session_id,'x',sizeof(cursor.session_id)); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)memset(cursor.entry_id,'x',sizeof(cursor.entry_id)); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)snprintf(cursor.started_at,sizeof(cursor.started_at),"not-a-date"); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)snprintf(cursor.started_at,sizeof(cursor.started_at),"2026-13-01T00:00:00+00:00"); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + cursor=next; (void)snprintf(cursor.started_at,sizeof(cursor.started_at),"2026-02-30T00:00:00+00:00"); + CHECK(trainlog_database_list_exercise_occurrences_page(database,exercise_id,&cursor,1U,page,&count, + &has_more,&next)==TRAINLOG_STATUS_INVALID_ARGUMENT); + { + const char *invalid_timestamp[] = { + "0000-01-01T00:00:00Z", "2026-09-05 18:34:12Z", "20260905T183412Z", + "2026-W36-5T18:34:12Z", "2026-09-05T18:34:12,5Z", + "2026-09-05T18:34:12+0200", "2026-09-05T18:34:12+02", + "2026-09-05T18:34.5Z", "2026-09-05T18:34:60Z", "2026-09-05T18:34:12+24:00" + }; + for(index=0U;index +#include + +#define CHECK(test) do { if (!(test)) { fprintf(stderr, "CHECK failed: %s:%d: %s\n", \ + __FILE__, __LINE__, #test); return 1; } } while (0) + +static int contains(const TrainlogExerciseKnowledge *const *rows, size_t count, const char *id) +{ + size_t index; + for (index = 0; index < count; ++index) { + if (strcmp(rows[index]->exercise_id, id) == 0) return 1; + } + return 0; +} + +int main(void) +{ + const char *const rear_delt = "ex_4cd2433e-80b1-478a-b8df-73fc6ef80962"; + const char *const chest_press = "ex_8552dd77-fcd7-4f06-a1cc-d956eb1009af"; + const char *const leg_press = "ex_b432623f-bfe9-4daf-a653-60ec7fdffbde"; + const char *const rotary = "ex_1a34814c-2e46-40fc-b1f4-6d60b8e5a3e0"; + const TrainlogExerciseKnowledge *rows[32]; + const TrainlogExerciseKnowledge *record; + const TrainlogKnowledgeInterpretation *conditional; + TrainlogKnowledgeQuery query = {0}; + size_t count = 0U; + size_t index; + int rotary_capability_found = 0; + const TrainlogKnowledgeBodyZoneAudit *audit; + + CHECK(trainlog_knowledge_reference_count() == 23U); + CHECK(trainlog_knowledge_muscle_count() == 53U); + CHECK(trainlog_knowledge_joint_action_count() == 35U); + CHECK(trainlog_knowledge_movement_pattern_count() == 26U); + CHECK(trainlog_exercise_knowledge_count() == 23U); + CHECK(trainlog_equipment_knowledge_count() == 41U); + CHECK(trainlog_knowledge_body_zone_audit_count() == trainlog_exercise_knowledge_count()); + audit = trainlog_knowledge_body_zone_audit_lookup(leg_press); + CHECK(audit != NULL && strcmp(audit->status, "confirmed") == 0); + CHECK(audit->rationale[0] != '\0' && audit->source_refs[0] != '\0'); + audit = trainlog_knowledge_body_zone_audit_lookup(chest_press); + CHECK(audit != NULL && strcmp(audit->status, "questionable") == 0); + audit = trainlog_knowledge_body_zone_audit_lookup("ex_2d488c08-194c-4051-a3c9-34471646c1d3"); + CHECK(audit != NULL && strcmp(audit->status, "unresolved") == 0); + CHECK(trainlog_knowledge_body_zone_audit_at(trainlog_knowledge_body_zone_audit_count()) == NULL); + CHECK(trainlog_knowledge_body_zone_audit_lookup(NULL) == NULL); + CHECK(trainlog_knowledge_muscle_lookup("pectoralis_major") != NULL); + CHECK(trainlog_knowledge_joint_action_lookup("shoulder_horizontal_abduction") != NULL); + CHECK(trainlog_knowledge_movement_pattern_lookup("horizontal_pull") != NULL); + CHECK(trainlog_knowledge_reference_lookup("openstax_upper") != NULL); + CHECK(trainlog_equipment_knowledge_lookup("rear_delt_pec_fly") != NULL); + CHECK(trainlog_exercise_knowledge_lookup("ex_unknown") == NULL); + + record = trainlog_exercise_knowledge_lookup(rear_delt); + CHECK(record != NULL && record->interpretation != NULL); + CHECK(record->conditional_interpretation == NULL); + CHECK(strcmp(record->interpretation->primary_zone_id, "shoulders") == 0); + + record = trainlog_exercise_knowledge_lookup(chest_press); + CHECK(record != NULL && record->interpretation == NULL); + conditional = trainlog_exercise_knowledge_conditional(chest_press); + CHECK(conditional != NULL); + CHECK(strcmp(conditional->family_description, "machine_chest_press") == 0); + + query.muscle_id = "quadriceps"; + query.muscle_role = TRAINLOG_KNOWLEDGE_ROLE_PRIMARY; + query.available_equipment_id = "leg_press"; + CHECK(trainlog_exercise_knowledge_query(&query, rows, 32U, &count) == TRAINLOG_STATUS_OK); + CHECK(count == 1U && strcmp(rows[0]->exercise_id, leg_press) == 0); + + query = (TrainlogKnowledgeQuery){0}; + query.muscle_role = TRAINLOG_KNOWLEDGE_ROLE_PRIMARY; + CHECK(trainlog_exercise_knowledge_query(&query, rows, 32U, &count) == + TRAINLOG_STATUS_INVALID_ARGUMENT); + + query = (TrainlogKnowledgeQuery){0}; + query.scientific_zone_id = "upper_body"; + query.include_zone_descendants = true; + CHECK(trainlog_exercise_knowledge_query(&query, rows, 32U, &count) == TRAINLOG_STATUS_OK); + CHECK(contains(rows, count, rear_delt)); + CHECK(!contains(rows, count, chest_press)); + + query = (TrainlogKnowledgeQuery){0}; + query.movement_pattern_id = "horizontal_pull"; + CHECK(trainlog_exercise_knowledge_query(&query, rows, 0U, &count) == TRAINLOG_STATUS_INVALID_ARGUMENT); + CHECK(count > 0U); + CHECK(trainlog_exercise_knowledge_query(&query, rows, 32U, &count) == TRAINLOG_STATUS_OK); + CHECK(count >= 2U); + query.movement_pattern_id = "not_a_pattern"; + CHECK(trainlog_exercise_knowledge_query(&query, rows, 32U, &count) == TRAINLOG_STATUS_NOT_FOUND); + + for (index = 0; index < trainlog_equipment_capability_count(); ++index) { + const TrainlogEquipmentCapability *capability = trainlog_equipment_capability_at(index); + if (strcmp(capability->equipment_id, "rotary_torso") == 0 && + strstr(capability->exercise_ids, rotary) != NULL) { + rotary_capability_found = 1; + CHECK(strcmp(capability->link_status, + "catalog_compatible_not_observed_occurrence") == 0); + } + } + /* Catalog compatibility is static metadata; the API contains no occurrence + * row and therefore cannot fabricate performance history. */ + CHECK(rotary_capability_found); + return 0; +} diff --git a/tui/tests/test_tui_workflows.c b/tui/tests/test_tui_workflows.c index bfd3b0f..4a37185 100644 --- a/tui/tests/test_tui_workflows.c +++ b/tui/tests/test_tui_workflows.c @@ -16,6 +16,10 @@ struct TrainlogTerminal { size_t event_index; char output[32768]; size_t output_used; + int rows; + int columns; + bool coordinate_overflow; + bool text_overflow; }; struct TrainlogPanel { int unused; }; @@ -29,12 +33,14 @@ static void script(TrainlogTerminal *terminal, const int *events, size_t count) (void)memset(terminal, 0, sizeof(*terminal)); (void)memcpy(terminal->events, events, count * sizeof(events[0])); terminal->event_count = count; + terminal->rows = 24; + terminal->columns = 80; } TrainlogTerminal *trainlog_terminal_create(void) { return NULL; } void trainlog_terminal_destroy(TrainlogTerminal *terminal) { (void)terminal; } -int trainlog_terminal_rows(const TrainlogTerminal *terminal) { (void)terminal; return 24; } -int trainlog_terminal_columns(const TrainlogTerminal *terminal) { (void)terminal; return 80; } +int trainlog_terminal_rows(const TrainlogTerminal *terminal) { return terminal->rows; } +int trainlog_terminal_columns(const TrainlogTerminal *terminal) { return terminal->columns; } void trainlog_terminal_erase(TrainlogTerminal *terminal) { (void)terminal; } void trainlog_terminal_render(TrainlogTerminal *terminal) { (void)terminal; } void trainlog_terminal_style_on(TrainlogTerminal *terminal, TrainlogTextStyle style) { (void)terminal; (void)style; } @@ -44,14 +50,28 @@ void trainlog_terminal_printf(TrainlogTerminal *terminal, int row, int column, { va_list arguments; int written; - (void)row; - (void)column; + if (row < 0 || row >= terminal->rows || column < 0 || column >= terminal->columns) + terminal->coordinate_overflow = true; if (terminal->output_used >= sizeof(terminal->output)) return; va_start(arguments, format); written = vsnprintf(terminal->output + terminal->output_used, sizeof(terminal->output) - terminal->output_used, format, arguments); va_end(arguments); if (written > 0 && (size_t)written < sizeof(terminal->output) - terminal->output_used) { + const char *text = terminal->output + terminal->output_used; + const char *at = text; + int cells = 0; + while (*at != '\0') { + utf8proc_int32_t codepoint; + utf8proc_ssize_t bytes = utf8proc_iterate((const utf8proc_uint8_t *)at, + -1, &codepoint); + int codepoint_cells; + if (bytes <= 0) { bytes = 1; codepoint = (unsigned char)*at; } + codepoint_cells = utf8proc_charwidth(codepoint); + cells += codepoint_cells < 0 ? 1 : codepoint_cells; + at += bytes; + } + if (column + cells >= terminal->columns) terminal->text_overflow = true; terminal->output_used += (size_t)written; terminal->output[terminal->output_used++] = '\n'; terminal->output[terminal->output_used] = '\0'; @@ -240,12 +260,39 @@ static bool test_empty_sets_cannot_finish(void) return true; } +static bool test_knowledge_scrolls_long_lists_at_minimum_terminal(void) +{ + TrainlogExercise exercise; + TrainlogTerminal terminal; + const int events[] = { + TRAINLOG_KEY_PAGE_DOWN, TRAINLOG_KEY_PAGE_DOWN, TRAINLOG_KEY_PAGE_DOWN, + TRAINLOG_KEY_PAGE_DOWN, 'b' + }; + + (void)memset(&exercise, 0, sizeof(exercise)); + (void)snprintf(exercise.exercise_id, sizeof(exercise.exercise_id), "%s", + "ex_a72fa713-4b0e-431d-95e2-42d95beb77b1"); + (void)snprintf(exercise.name, sizeof(exercise.name), "%s", "Lat pull"); + script(&terminal, events, sizeof(events) / sizeof(events[0])); + terminal.rows = 20; + terminal.columns = 72; + tui_terminal = &terminal; + screen_exercise_knowledge(&exercise); + CHECK(!terminal.coordinate_overflow); + CHECK(!terminal.text_overflow); + CHECK(strstr(terminal.output, "Supra-épineux") != NULL); + CHECK(strstr(terminal.output, "Sources :") != NULL); + tui_terminal = NULL; + return true; +} + int main(void) { if (!test_assistance_creation_labels() || !test_duration_creation_starts_empty() || !test_append_requires_actual_and_rolls_back() || - !test_empty_sets_cannot_finish()) return 1; + !test_empty_sets_cannot_finish() || + !test_knowledge_scrolls_long_lists_at_minimum_terminal()) return 1; (void)printf("PASS tui_workflows\n"); return 0; }