@pwf-dev/core 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,1411 @@
1
+ 'use strict';
2
+
3
+ var yaml = require('yaml');
4
+ var Ajv = require('ajv');
5
+ var addFormats = require('ajv-formats');
6
+
7
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
8
+
9
+ var Ajv__default = /*#__PURE__*/_interopDefault(Ajv);
10
+ var addFormats__default = /*#__PURE__*/_interopDefault(addFormats);
11
+
12
+ // src/parse.ts
13
+
14
+ // schema/pwf-v1.json
15
+ var pwf_v1_default = {
16
+ $schema: "http://json-schema.org/draft-07/schema#",
17
+ $id: "https://pwf.dev/schema/v1/plan.json",
18
+ title: "PWF Plan v1",
19
+ description: "Portable Workout Format v1 schema for validating plan documents",
20
+ type: "object",
21
+ required: ["plan_version", "cycle"],
22
+ additionalProperties: false,
23
+ properties: {
24
+ plan_version: {
25
+ type: "integer",
26
+ const: 1,
27
+ description: "Specification version (must be 1)"
28
+ },
29
+ meta: {
30
+ $ref: "#/$defs/Meta"
31
+ },
32
+ glossary: {
33
+ type: "object",
34
+ description: "Term definitions for exercises and concepts used in this plan",
35
+ maxProperties: 100,
36
+ additionalProperties: {
37
+ type: "string",
38
+ minLength: 1,
39
+ maxLength: 500
40
+ },
41
+ propertyNames: {
42
+ pattern: "^[a-zA-Z0-9\\s\\-']+$",
43
+ minLength: 1,
44
+ maxLength: 50
45
+ }
46
+ },
47
+ cycle: {
48
+ $ref: "#/$defs/Cycle"
49
+ }
50
+ },
51
+ $defs: {
52
+ Meta: {
53
+ type: "object",
54
+ description: "Plan metadata for display and organization",
55
+ additionalProperties: false,
56
+ required: ["title"],
57
+ properties: {
58
+ id: {
59
+ type: "string",
60
+ description: "Unique plan identifier"
61
+ },
62
+ title: {
63
+ type: "string",
64
+ maxLength: 80,
65
+ minLength: 1,
66
+ description: "Plan display name"
67
+ },
68
+ description: {
69
+ type: "string",
70
+ description: "Brief plan description"
71
+ },
72
+ author: {
73
+ type: "string",
74
+ description: "Coach or creator name"
75
+ },
76
+ status: {
77
+ type: "string",
78
+ enum: ["draft", "active", "completed", "archived"],
79
+ default: "draft",
80
+ description: "Plan status"
81
+ },
82
+ activated_at: {
83
+ type: "string",
84
+ format: "date-time",
85
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})$",
86
+ description: "ISO 8601 timestamp when plan was activated"
87
+ },
88
+ completed_at: {
89
+ type: "string",
90
+ format: "date-time",
91
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(Z|[+-]\\d{2}:\\d{2})$",
92
+ description: "ISO 8601 timestamp when plan was completed"
93
+ },
94
+ equipment: {
95
+ type: "array",
96
+ items: {
97
+ type: "string"
98
+ },
99
+ uniqueItems: true,
100
+ description: "Required equipment tags"
101
+ },
102
+ daysPerWeek: {
103
+ type: "integer",
104
+ minimum: 1,
105
+ maximum: 7,
106
+ description: "Intended training frequency"
107
+ },
108
+ recommendedFirst: {
109
+ type: "boolean",
110
+ default: false,
111
+ description: "Suggest as starter plan"
112
+ },
113
+ tags: {
114
+ type: "array",
115
+ items: {
116
+ type: "string"
117
+ },
118
+ description: "Searchable tags"
119
+ },
120
+ athlete_profile: {
121
+ $ref: "#/$defs/AthleteProfile"
122
+ }
123
+ }
124
+ },
125
+ AthleteProfile: {
126
+ type: "object",
127
+ description: "Athlete metrics for endurance training",
128
+ additionalProperties: false,
129
+ properties: {
130
+ ftp_watts: {
131
+ type: "integer",
132
+ minimum: 0,
133
+ description: "Functional Threshold Power in watts"
134
+ },
135
+ threshold_hr_bpm: {
136
+ type: "integer",
137
+ minimum: 0,
138
+ maximum: 250,
139
+ description: "Threshold heart rate in BPM"
140
+ },
141
+ max_hr_bpm: {
142
+ type: "integer",
143
+ minimum: 0,
144
+ maximum: 250,
145
+ description: "Maximum heart rate in BPM"
146
+ },
147
+ threshold_pace_sec_per_km: {
148
+ type: "integer",
149
+ minimum: 0,
150
+ description: "Threshold pace in seconds per kilometer"
151
+ },
152
+ weight_kg: {
153
+ type: "number",
154
+ minimum: 0,
155
+ description: "Athlete weight in kilograms"
156
+ }
157
+ }
158
+ },
159
+ Cycle: {
160
+ type: "object",
161
+ description: "Training cycle containing workout days",
162
+ additionalProperties: false,
163
+ required: ["days"],
164
+ properties: {
165
+ start_date: {
166
+ type: "string",
167
+ format: "date",
168
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
169
+ description: "ISO 8601 date (YYYY-MM-DD)"
170
+ },
171
+ notes: {
172
+ type: "string",
173
+ description: "Cycle-level coaching notes"
174
+ },
175
+ days: {
176
+ type: "array",
177
+ minItems: 1,
178
+ items: {
179
+ $ref: "#/$defs/Day"
180
+ },
181
+ description: "Training days in this cycle"
182
+ }
183
+ }
184
+ },
185
+ Day: {
186
+ type: "object",
187
+ description: "Single training day",
188
+ additionalProperties: false,
189
+ required: ["exercises"],
190
+ properties: {
191
+ id: {
192
+ type: "string",
193
+ description: "Unique day identifier"
194
+ },
195
+ order: {
196
+ type: "integer",
197
+ minimum: 0,
198
+ description: "Day sequence (0-indexed)"
199
+ },
200
+ focus: {
201
+ type: "string",
202
+ description: "Training focus or theme"
203
+ },
204
+ notes: {
205
+ type: "string",
206
+ description: "Day-level coaching notes"
207
+ },
208
+ scheduled_date: {
209
+ type: "string",
210
+ format: "date",
211
+ pattern: "^\\d{4}-\\d{2}-\\d{2}$",
212
+ description: "Planned workout date"
213
+ },
214
+ target_session_length_min: {
215
+ type: "integer",
216
+ minimum: 1,
217
+ description: "Expected duration in minutes"
218
+ },
219
+ exercises: {
220
+ type: "array",
221
+ minItems: 1,
222
+ items: {
223
+ $ref: "#/$defs/Exercise"
224
+ },
225
+ description: "Exercises in this day"
226
+ }
227
+ }
228
+ },
229
+ Exercise: {
230
+ type: "object",
231
+ description: "Single exercise definition",
232
+ additionalProperties: false,
233
+ required: ["modality"],
234
+ properties: {
235
+ id: {
236
+ type: "string",
237
+ description: "Unique exercise identifier"
238
+ },
239
+ name: {
240
+ type: "string",
241
+ description: "Exercise name"
242
+ },
243
+ modality: {
244
+ type: "string",
245
+ enum: ["strength", "countdown", "stopwatch", "interval", "cycling", "running", "rowing", "swimming"],
246
+ description: "Exercise type"
247
+ },
248
+ target_sets: {
249
+ type: "integer",
250
+ minimum: 1,
251
+ description: "Target number of sets"
252
+ },
253
+ target_reps: {
254
+ type: "integer",
255
+ minimum: 1,
256
+ description: "Target reps per set"
257
+ },
258
+ target_duration_sec: {
259
+ type: "integer",
260
+ minimum: 1,
261
+ description: "Target duration in seconds"
262
+ },
263
+ target_distance_meters: {
264
+ type: "number",
265
+ minimum: 0,
266
+ description: "Target distance in meters"
267
+ },
268
+ target_load: {
269
+ type: "string",
270
+ description: "Loading guidance (weight, RPE, %1RM)"
271
+ },
272
+ target_weight_percent: {
273
+ type: "number",
274
+ minimum: 0,
275
+ maximum: 200,
276
+ description: "Target weight as percentage of reference max (requires percent_of)"
277
+ },
278
+ percent_of: {
279
+ type: "string",
280
+ enum: ["1rm", "3rm", "5rm", "10rm"],
281
+ description: "Reference max for percentage calculation (requires target_weight_percent)"
282
+ },
283
+ reference_exercise: {
284
+ type: "string",
285
+ description: "Reference another exercise's max for percentage calculation"
286
+ },
287
+ cues: {
288
+ type: "string",
289
+ description: "Form cues (alias for target_notes)"
290
+ },
291
+ target_notes: {
292
+ type: "string",
293
+ description: "Coaching notes for this exercise"
294
+ },
295
+ link: {
296
+ type: "string",
297
+ format: "uri",
298
+ pattern: "^https://",
299
+ description: "Tutorial URL (HTTPS only)"
300
+ },
301
+ image: {
302
+ type: "string",
303
+ format: "uri",
304
+ pattern: "^https://",
305
+ description: "Demo image URL (HTTPS only)"
306
+ },
307
+ group: {
308
+ type: "string",
309
+ minLength: 1,
310
+ maxLength: 50,
311
+ pattern: "^[a-zA-Z0-9_-]+$",
312
+ description: "Group identifier for supersets/circuits (alphanumeric, hyphens, underscores only)"
313
+ },
314
+ group_type: {
315
+ type: "string",
316
+ enum: ["superset", "circuit"],
317
+ description: "Type of exercise grouping"
318
+ },
319
+ rest_between_sets_sec: {
320
+ type: "integer",
321
+ minimum: 0,
322
+ description: "Rest period in seconds between sets"
323
+ },
324
+ rest_after_sec: {
325
+ type: "integer",
326
+ minimum: 0,
327
+ description: "Rest period in seconds after completing all sets"
328
+ },
329
+ zones: {
330
+ type: "array",
331
+ minItems: 1,
332
+ items: {
333
+ $ref: "#/$defs/TrainingZone"
334
+ },
335
+ description: "Training zones for endurance workouts"
336
+ },
337
+ ramp: {
338
+ $ref: "#/$defs/RampConfig"
339
+ },
340
+ interval_phases: {
341
+ type: "array",
342
+ minItems: 1,
343
+ items: {
344
+ $ref: "#/$defs/IntervalPhase"
345
+ },
346
+ description: "Structured interval phases for complex endurance workouts"
347
+ }
348
+ }
349
+ },
350
+ TrainingZone: {
351
+ type: "object",
352
+ description: "Training zone specification",
353
+ additionalProperties: false,
354
+ required: ["zone"],
355
+ properties: {
356
+ zone: {
357
+ type: "integer",
358
+ minimum: 1,
359
+ maximum: 7,
360
+ description: "Training zone number (1-7)"
361
+ },
362
+ duration_sec: {
363
+ type: "integer",
364
+ minimum: 0,
365
+ description: "Duration in this zone (seconds)"
366
+ },
367
+ target_power_watts: {
368
+ type: "integer",
369
+ minimum: 0,
370
+ description: "Target power in watts"
371
+ },
372
+ target_hr_bpm: {
373
+ type: "integer",
374
+ minimum: 0,
375
+ maximum: 250,
376
+ description: "Target heart rate in BPM"
377
+ },
378
+ target_pace_sec_per_km: {
379
+ type: "integer",
380
+ minimum: 0,
381
+ description: "Target pace in seconds per kilometer"
382
+ }
383
+ }
384
+ },
385
+ RampConfig: {
386
+ type: "object",
387
+ description: "Ramp configuration for gradual intensity changes",
388
+ additionalProperties: false,
389
+ required: ["start_power_watts", "end_power_watts", "duration_sec"],
390
+ properties: {
391
+ start_power_watts: {
392
+ type: "integer",
393
+ minimum: 0,
394
+ description: "Starting power in watts"
395
+ },
396
+ end_power_watts: {
397
+ type: "integer",
398
+ minimum: 0,
399
+ description: "Ending power in watts"
400
+ },
401
+ duration_sec: {
402
+ type: "integer",
403
+ minimum: 1,
404
+ description: "Ramp duration in seconds"
405
+ },
406
+ step_duration_sec: {
407
+ type: "integer",
408
+ minimum: 1,
409
+ description: "Duration of each step in seconds"
410
+ }
411
+ }
412
+ },
413
+ IntervalPhase: {
414
+ type: "object",
415
+ description: "Phase in a structured interval workout",
416
+ additionalProperties: false,
417
+ required: ["name", "duration_sec"],
418
+ properties: {
419
+ name: {
420
+ type: "string",
421
+ minLength: 1,
422
+ description: "Phase name (e.g., 'warmup', 'work', 'recovery')"
423
+ },
424
+ duration_sec: {
425
+ type: "integer",
426
+ minimum: 1,
427
+ description: "Phase duration in seconds"
428
+ },
429
+ target_power_watts: {
430
+ type: "integer",
431
+ minimum: 0,
432
+ description: "Target power in watts"
433
+ },
434
+ target_hr_bpm: {
435
+ type: "integer",
436
+ minimum: 0,
437
+ maximum: 250,
438
+ description: "Target heart rate in BPM"
439
+ },
440
+ target_pace_sec_per_km: {
441
+ type: "integer",
442
+ minimum: 0,
443
+ description: "Target pace in seconds per kilometer"
444
+ },
445
+ cadence_rpm: {
446
+ type: "integer",
447
+ minimum: 0,
448
+ description: "Target cadence in RPM"
449
+ }
450
+ }
451
+ }
452
+ }
453
+ };
454
+
455
+ // schema/pwf-history-v1.json
456
+ var pwf_history_v1_default = {
457
+ $schema: "http://json-schema.org/draft-07/schema#",
458
+ $id: "https://pwf.dev/schema/v1/history.json",
459
+ title: "PWF History Export v1",
460
+ description: "Schema for validating PWF workout history exports",
461
+ type: "object",
462
+ required: ["history_version", "exported_at", "workouts"],
463
+ additionalProperties: false,
464
+ properties: {
465
+ history_version: {
466
+ type: "integer",
467
+ const: 1,
468
+ description: "Specification version (must be 1)"
469
+ },
470
+ exported_at: {
471
+ type: "string",
472
+ format: "date-time",
473
+ description: "ISO 8601 datetime when export was created"
474
+ },
475
+ export_source: {
476
+ $ref: "#/$defs/ExportSource"
477
+ },
478
+ units: {
479
+ $ref: "#/$defs/Units"
480
+ },
481
+ workouts: {
482
+ type: "array",
483
+ items: {
484
+ $ref: "#/$defs/Workout"
485
+ },
486
+ description: "Completed workout sessions"
487
+ },
488
+ personal_records: {
489
+ type: "array",
490
+ items: {
491
+ $ref: "#/$defs/PersonalRecord"
492
+ },
493
+ description: "Personal records achieved"
494
+ },
495
+ body_measurements: {
496
+ type: "array",
497
+ items: {
498
+ $ref: "#/$defs/BodyMeasurement"
499
+ },
500
+ description: "Body measurements recorded"
501
+ }
502
+ },
503
+ $defs: {
504
+ ExportSource: {
505
+ type: "object",
506
+ additionalProperties: false,
507
+ properties: {
508
+ app_name: {
509
+ type: "string",
510
+ description: "Application name"
511
+ },
512
+ app_version: {
513
+ type: "string",
514
+ description: "Application version"
515
+ },
516
+ platform: {
517
+ type: "string",
518
+ enum: ["ios", "android", "web", "desktop"],
519
+ description: "Platform"
520
+ },
521
+ preferred_units: {
522
+ $ref: "#/$defs/Units",
523
+ description: "User's preferred units"
524
+ }
525
+ }
526
+ },
527
+ Units: {
528
+ type: "object",
529
+ additionalProperties: false,
530
+ properties: {
531
+ weight: {
532
+ type: "string",
533
+ enum: ["kg", "lb"],
534
+ default: "kg"
535
+ },
536
+ distance: {
537
+ type: "string",
538
+ enum: ["meters", "kilometers", "miles", "feet", "yards"],
539
+ default: "meters"
540
+ }
541
+ }
542
+ },
543
+ Workout: {
544
+ type: "object",
545
+ required: ["date", "exercises"],
546
+ additionalProperties: false,
547
+ properties: {
548
+ id: {
549
+ type: "string",
550
+ description: "Unique workout identifier"
551
+ },
552
+ date: {
553
+ type: "string",
554
+ format: "date",
555
+ description: "Workout date (YYYY-MM-DD)"
556
+ },
557
+ started_at: {
558
+ type: "string",
559
+ format: "date-time",
560
+ description: "Start timestamp"
561
+ },
562
+ ended_at: {
563
+ type: "string",
564
+ format: "date-time",
565
+ description: "End timestamp"
566
+ },
567
+ duration_sec: {
568
+ type: "integer",
569
+ minimum: 0,
570
+ description: "Total duration in seconds"
571
+ },
572
+ title: {
573
+ type: "string",
574
+ description: "Workout title"
575
+ },
576
+ notes: {
577
+ type: "string",
578
+ description: "Workout notes"
579
+ },
580
+ plan_id: {
581
+ type: "string",
582
+ description: "Reference to PWF plan"
583
+ },
584
+ plan_day_id: {
585
+ type: "string",
586
+ description: "Reference to plan day"
587
+ },
588
+ exercises: {
589
+ type: "array",
590
+ items: {
591
+ $ref: "#/$defs/CompletedExercise"
592
+ },
593
+ description: "Exercises performed"
594
+ },
595
+ telemetry: {
596
+ $ref: "#/$defs/WorkoutTelemetry",
597
+ description: "Telemetry metrics for entire workout session (PWF v2)"
598
+ },
599
+ devices: {
600
+ type: "array",
601
+ items: {
602
+ $ref: "#/$defs/DeviceInfo"
603
+ },
604
+ description: "Devices used during workout (PWF v2)"
605
+ },
606
+ sport: {
607
+ $ref: "#/$defs/Sport",
608
+ description: "Primary sport for this workout (PWF v2.1)"
609
+ },
610
+ sport_segments: {
611
+ type: "array",
612
+ items: {
613
+ $ref: "#/$defs/SportSegment"
614
+ },
615
+ description: "Sport segments for multi-sport workouts like triathlon (PWF v2.1)"
616
+ }
617
+ }
618
+ },
619
+ CompletedExercise: {
620
+ type: "object",
621
+ required: ["name", "sets"],
622
+ additionalProperties: false,
623
+ properties: {
624
+ id: {
625
+ type: "string",
626
+ description: "Unique exercise identifier"
627
+ },
628
+ name: {
629
+ type: "string",
630
+ minLength: 1,
631
+ description: "Exercise name"
632
+ },
633
+ modality: {
634
+ type: "string",
635
+ enum: ["strength", "countdown", "stopwatch", "interval", "swimming"],
636
+ description: "Exercise modality"
637
+ },
638
+ notes: {
639
+ type: "string",
640
+ description: "Exercise-level notes"
641
+ },
642
+ sets: {
643
+ type: "array",
644
+ items: {
645
+ $ref: "#/$defs/CompletedSet"
646
+ },
647
+ description: "Completed sets"
648
+ },
649
+ pool_config: {
650
+ $ref: "#/$defs/PoolConfig",
651
+ description: "Pool configuration for swimming exercises (PWF v2.1)"
652
+ },
653
+ sport: {
654
+ $ref: "#/$defs/Sport",
655
+ description: "Sport classification for this exercise (PWF v2.1)"
656
+ }
657
+ }
658
+ },
659
+ CompletedSet: {
660
+ type: "object",
661
+ additionalProperties: false,
662
+ properties: {
663
+ set_number: {
664
+ type: "integer",
665
+ minimum: 1,
666
+ description: "Set order (1-indexed)"
667
+ },
668
+ set_type: {
669
+ type: "string",
670
+ enum: ["working", "warmup", "dropset", "failure", "amrap"],
671
+ default: "working",
672
+ description: "Type of set"
673
+ },
674
+ reps: {
675
+ type: "integer",
676
+ minimum: 0,
677
+ description: "Repetitions completed"
678
+ },
679
+ weight_kg: {
680
+ type: "number",
681
+ minimum: 0,
682
+ description: "Weight in kilograms"
683
+ },
684
+ weight_lb: {
685
+ type: "number",
686
+ minimum: 0,
687
+ description: "Weight in pounds"
688
+ },
689
+ duration_sec: {
690
+ type: "integer",
691
+ minimum: 0,
692
+ description: "Duration in seconds"
693
+ },
694
+ distance_meters: {
695
+ type: "number",
696
+ minimum: 0,
697
+ description: "Distance in meters"
698
+ },
699
+ rpe: {
700
+ type: "number",
701
+ minimum: 0,
702
+ maximum: 10,
703
+ description: "Rate of Perceived Exertion (1-10 scale)"
704
+ },
705
+ rir: {
706
+ type: "integer",
707
+ minimum: 0,
708
+ maximum: 10,
709
+ description: "Reps in Reserve (alternative to RPE)"
710
+ },
711
+ notes: {
712
+ type: "string",
713
+ description: "Set-level notes"
714
+ },
715
+ is_pr: {
716
+ type: "boolean",
717
+ description: "Whether this set was a personal record"
718
+ },
719
+ completed_at: {
720
+ type: "string",
721
+ format: "date-time",
722
+ description: "When set was completed"
723
+ },
724
+ telemetry: {
725
+ $ref: "#/$defs/SetTelemetry",
726
+ description: "Telemetry metrics for this set (PWF v2)"
727
+ },
728
+ swimming: {
729
+ $ref: "#/$defs/SwimmingSetData",
730
+ description: "Swimming-specific data for this set (PWF v2.1)"
731
+ }
732
+ }
733
+ },
734
+ PersonalRecord: {
735
+ type: "object",
736
+ required: ["exercise_name", "record_type", "value", "achieved_at"],
737
+ additionalProperties: false,
738
+ properties: {
739
+ exercise_name: {
740
+ type: "string",
741
+ minLength: 1,
742
+ description: "Exercise name"
743
+ },
744
+ record_type: {
745
+ type: "string",
746
+ enum: ["1rm", "max_weight_3rm", "max_weight_5rm", "max_weight_8rm", "max_weight_10rm", "max_weight", "max_reps", "max_volume", "max_duration", "max_distance", "fastest_time"],
747
+ description: "Type of record"
748
+ },
749
+ value: {
750
+ type: "number",
751
+ description: "Record value"
752
+ },
753
+ unit: {
754
+ type: "string",
755
+ description: "Unit for the value"
756
+ },
757
+ achieved_at: {
758
+ type: "string",
759
+ format: "date",
760
+ description: "Date achieved"
761
+ },
762
+ workout_id: {
763
+ type: "string",
764
+ description: "Reference to workout"
765
+ },
766
+ notes: {
767
+ type: "string",
768
+ description: "Additional notes"
769
+ }
770
+ }
771
+ },
772
+ BodyMeasurement: {
773
+ type: "object",
774
+ required: ["date"],
775
+ additionalProperties: false,
776
+ properties: {
777
+ date: {
778
+ type: "string",
779
+ format: "date",
780
+ description: "Measurement date"
781
+ },
782
+ recorded_at: {
783
+ type: "string",
784
+ format: "date-time",
785
+ description: "Exact timestamp"
786
+ },
787
+ weight_kg: {
788
+ type: "number",
789
+ minimum: 0,
790
+ description: "Body weight in kg"
791
+ },
792
+ weight_lb: {
793
+ type: "number",
794
+ minimum: 0,
795
+ description: "Body weight in lb"
796
+ },
797
+ body_fat_percent: {
798
+ type: "number",
799
+ minimum: 0,
800
+ maximum: 100,
801
+ description: "Body fat percentage"
802
+ },
803
+ notes: {
804
+ type: "string",
805
+ description: "Notes"
806
+ },
807
+ measurements: {
808
+ $ref: "#/$defs/BodyDimensions"
809
+ }
810
+ }
811
+ },
812
+ BodyDimensions: {
813
+ type: "object",
814
+ additionalProperties: false,
815
+ properties: {
816
+ neck_cm: { type: "number", minimum: 0 },
817
+ shoulders_cm: { type: "number", minimum: 0 },
818
+ chest_cm: { type: "number", minimum: 0 },
819
+ waist_cm: { type: "number", minimum: 0 },
820
+ hips_cm: { type: "number", minimum: 0 },
821
+ bicep_left_cm: { type: "number", minimum: 0 },
822
+ bicep_right_cm: { type: "number", minimum: 0 },
823
+ forearm_left_cm: { type: "number", minimum: 0 },
824
+ forearm_right_cm: { type: "number", minimum: 0 },
825
+ thigh_left_cm: { type: "number", minimum: 0 },
826
+ thigh_right_cm: { type: "number", minimum: 0 },
827
+ calf_left_cm: { type: "number", minimum: 0 },
828
+ calf_right_cm: { type: "number", minimum: 0 }
829
+ }
830
+ },
831
+ Sport: {
832
+ type: "string",
833
+ enum: [
834
+ "swimming",
835
+ "cycling",
836
+ "running",
837
+ "rowing",
838
+ "transition",
839
+ "strength",
840
+ "strength-training",
841
+ "hiking",
842
+ "walking",
843
+ "yoga",
844
+ "pilates",
845
+ "cross-fit",
846
+ "calisthenics",
847
+ "cardio",
848
+ "cross-country-skiing",
849
+ "downhill-skiing",
850
+ "snowboarding",
851
+ "stand-up-paddling",
852
+ "kayaking",
853
+ "elliptical",
854
+ "stair-climbing",
855
+ "other"
856
+ ],
857
+ description: "Sport classification (PWF v2.1)"
858
+ },
859
+ WorkoutTelemetry: {
860
+ type: "object",
861
+ additionalProperties: false,
862
+ description: "Telemetry metrics for entire workout session (PWF v2)",
863
+ properties: {
864
+ heart_rate_avg: { type: "integer", minimum: 0, description: "Average heart rate (bpm)" },
865
+ heart_rate_max: { type: "integer", minimum: 0, description: "Maximum heart rate (bpm)" },
866
+ heart_rate_min: { type: "integer", minimum: 0, description: "Minimum heart rate (bpm)" },
867
+ power_avg: { type: "integer", minimum: 0, description: "Average power (watts)" },
868
+ power_max: { type: "integer", minimum: 0, description: "Maximum power (watts)" },
869
+ total_distance_m: { type: "number", minimum: 0, description: "Total distance in meters" },
870
+ total_distance_km: { type: "number", minimum: 0, description: "Total distance in kilometers" },
871
+ total_distance_mi: { type: "number", minimum: 0, description: "Total distance in miles" },
872
+ total_elevation_gain_m: { type: "number", description: "Total elevation gain in meters" },
873
+ total_elevation_gain_ft: { type: "number", description: "Total elevation gain in feet" },
874
+ total_elevation_loss_m: { type: "number", description: "Total elevation loss in meters" },
875
+ total_elevation_loss_ft: { type: "number", description: "Total elevation loss in feet" },
876
+ speed_avg_kph: { type: "number", minimum: 0, description: "Average speed in km/h" },
877
+ speed_avg_mph: { type: "number", minimum: 0, description: "Average speed in mph" },
878
+ speed_max_kph: { type: "number", minimum: 0, description: "Maximum speed in km/h" },
879
+ speed_max_mph: { type: "number", minimum: 0, description: "Maximum speed in mph" },
880
+ pace_avg_sec_per_km: { type: "integer", minimum: 0, description: "Average pace in seconds per km" },
881
+ pace_avg_sec_per_mi: { type: "integer", minimum: 0, description: "Average pace in seconds per mile" },
882
+ cadence_avg: { type: "integer", minimum: 0, description: "Average cadence (RPM or SPM)" },
883
+ temperature_c: { type: "number", description: "Temperature in Celsius" },
884
+ temperature_f: { type: "number", description: "Temperature in Fahrenheit" },
885
+ humidity_percent: { type: "number", minimum: 0, maximum: 100, description: "Humidity percentage" },
886
+ total_calories: { type: "integer", minimum: 0, description: "Total calories burned" },
887
+ gps_route_id: { type: "string", description: "GPS route identifier" },
888
+ gps_route: { $ref: "#/$defs/GpsRoute", description: "Full GPS route data (PWF v2.1)" },
889
+ advanced_metrics: { $ref: "#/$defs/AdvancedMetrics", description: "Advanced physiological metrics (PWF v2.1)" },
890
+ power_metrics: { $ref: "#/$defs/PowerMetrics", description: "Power-based cycling metrics (PWF v2.1)" },
891
+ time_in_zones: { $ref: "#/$defs/TimeInZones", description: "Time in HR/power zones (PWF v2.1)" }
892
+ }
893
+ },
894
+ SetTelemetry: {
895
+ type: "object",
896
+ additionalProperties: false,
897
+ description: "Telemetry metrics for a completed set (PWF v2)",
898
+ properties: {
899
+ heart_rate_avg: { type: "integer", minimum: 0, description: "Average heart rate (bpm)" },
900
+ heart_rate_max: { type: "integer", minimum: 0, description: "Maximum heart rate (bpm)" },
901
+ heart_rate_min: { type: "integer", minimum: 0, description: "Minimum heart rate (bpm)" },
902
+ power_avg: { type: "integer", minimum: 0, description: "Average power (watts)" },
903
+ power_max: { type: "integer", minimum: 0, description: "Maximum power (watts)" },
904
+ power_min: { type: "integer", minimum: 0, description: "Minimum power (watts)" },
905
+ elevation_gain_m: { type: "number", description: "Elevation gain in meters" },
906
+ elevation_gain_ft: { type: "number", description: "Elevation gain in feet" },
907
+ elevation_loss_m: { type: "number", description: "Elevation loss in meters" },
908
+ elevation_loss_ft: { type: "number", description: "Elevation loss in feet" },
909
+ speed_avg_mps: { type: "number", minimum: 0, description: "Average speed in m/s" },
910
+ speed_avg_kph: { type: "number", minimum: 0, description: "Average speed in km/h" },
911
+ speed_avg_mph: { type: "number", minimum: 0, description: "Average speed in mph" },
912
+ speed_max_mps: { type: "number", minimum: 0, description: "Maximum speed in m/s" },
913
+ speed_max_kph: { type: "number", minimum: 0, description: "Maximum speed in km/h" },
914
+ speed_max_mph: { type: "number", minimum: 0, description: "Maximum speed in mph" },
915
+ pace_avg_sec_per_km: { type: "integer", minimum: 0, description: "Average pace in seconds per km" },
916
+ pace_avg_sec_per_mi: { type: "integer", minimum: 0, description: "Average pace in seconds per mile" },
917
+ cadence_avg: { type: "integer", minimum: 0, description: "Average cadence (RPM or SPM)" },
918
+ cadence_max: { type: "integer", minimum: 0, description: "Maximum cadence (RPM or SPM)" },
919
+ temperature_c: { type: "number", description: "Temperature in Celsius" },
920
+ temperature_f: { type: "number", description: "Temperature in Fahrenheit" },
921
+ humidity_percent: { type: "number", minimum: 0, maximum: 100, description: "Humidity percentage" },
922
+ calories: { type: "integer", minimum: 0, description: "Calories burned in this set" },
923
+ stroke_rate: { type: "integer", minimum: 0, description: "Stroke rate for swimming/rowing (strokes per minute)" },
924
+ gps_route_id: { type: "string", description: "GPS route identifier" },
925
+ time_series: { $ref: "#/$defs/TimeSeriesData", description: "Second-by-second time-series data (PWF v2.1)" }
926
+ }
927
+ },
928
+ AdvancedMetrics: {
929
+ type: "object",
930
+ additionalProperties: false,
931
+ description: "Advanced physiological and performance metrics (PWF v2.1)",
932
+ properties: {
933
+ training_effect: { type: "number", minimum: 0, maximum: 5, description: "Aerobic Training Effect score (0.0-5.0)" },
934
+ anaerobic_training_effect: { type: "number", minimum: 0, maximum: 5, description: "Anaerobic Training Effect score (0.0-5.0)" },
935
+ recovery_time_hours: { type: "integer", minimum: 0, description: "Recommended recovery time in hours" },
936
+ vo2_max_estimate: { type: "number", minimum: 0, description: "VO2 Max estimate in ml/kg/min" },
937
+ lactate_threshold: { $ref: "#/$defs/LactateThreshold", description: "Lactate threshold data" },
938
+ performance_condition: { type: "integer", minimum: -20, maximum: 20, description: "Real-time performance assessment (-20 to +20)" },
939
+ training_load: { type: "integer", minimum: 0, description: "Cumulative training stress (0-1000+)" },
940
+ training_status: { $ref: "#/$defs/TrainingStatus", description: "Training status assessment" }
941
+ }
942
+ },
943
+ LactateThreshold: {
944
+ type: "object",
945
+ additionalProperties: false,
946
+ description: "Lactate threshold tracking",
947
+ properties: {
948
+ heart_rate_bpm: { type: "integer", minimum: 0, description: "Heart rate at lactate threshold (bpm)" },
949
+ speed_mps: { type: "number", minimum: 0, description: "Speed at lactate threshold (m/s)" },
950
+ power_watts: { type: "integer", minimum: 0, description: "Power at lactate threshold (watts, for cycling)" },
951
+ detected_at: { type: "string", format: "date-time", description: "When threshold was detected/calculated" }
952
+ }
953
+ },
954
+ TrainingStatus: {
955
+ type: "string",
956
+ enum: ["detraining", "recovery", "maintaining", "productive", "peaking", "overreaching", "unknown"],
957
+ description: "Training status classification"
958
+ },
959
+ PowerMetrics: {
960
+ type: "object",
961
+ additionalProperties: false,
962
+ description: "Power-based cycling metrics (PWF v2.1)",
963
+ properties: {
964
+ normalized_power: { type: "integer", minimum: 0, description: "Normalized Power (NP) - weighted average accounting for variability" },
965
+ training_stress_score: { type: "number", minimum: 0, description: "Training Stress Score (TSS) - quantifies training load" },
966
+ intensity_factor: { type: "number", minimum: 0, description: "Intensity Factor (IF) - ratio of NP to FTP" },
967
+ variability_index: { type: "number", minimum: 0, description: "Variability Index (VI) - ratio of NP to average power" },
968
+ ftp_watts: { type: "integer", minimum: 0, description: "Functional Threshold Power used for calculations (watts)" },
969
+ total_work_kj: { type: "number", minimum: 0, description: "Total work in kilojoules" },
970
+ left_right_balance: { type: "number", minimum: 0, maximum: 100, description: "Left/right power balance (percentage left)" },
971
+ left_pedal_smoothness: { type: "number", minimum: 0, maximum: 100, description: "Average left pedal smoothness (percentage)" },
972
+ right_pedal_smoothness: { type: "number", minimum: 0, maximum: 100, description: "Average right pedal smoothness (percentage)" },
973
+ left_torque_effectiveness: { type: "number", minimum: 0, maximum: 100, description: "Average left torque effectiveness (percentage)" },
974
+ right_torque_effectiveness: { type: "number", minimum: 0, maximum: 100, description: "Average right torque effectiveness (percentage)" }
975
+ }
976
+ },
977
+ TimeInZones: {
978
+ type: "object",
979
+ additionalProperties: false,
980
+ description: "Time spent in heart rate and power zones (PWF v2.1)",
981
+ properties: {
982
+ hr_zones_sec: { type: "array", items: { type: "integer", minimum: 0 }, description: "Time in each HR zone (seconds per zone)" },
983
+ power_zones_sec: { type: "array", items: { type: "integer", minimum: 0 }, description: "Time in each power zone (seconds per zone)" },
984
+ hr_zone_boundaries: { type: "array", items: { type: "integer", minimum: 0 }, description: "HR zone boundaries in bpm" },
985
+ power_zone_boundaries: { type: "array", items: { type: "integer", minimum: 0 }, description: "Power zone boundaries in watts" },
986
+ pace_zones_sec: { type: "array", items: { type: "integer", minimum: 0 }, description: "Time in each pace zone (seconds per zone)" },
987
+ pace_zone_boundaries: { type: "array", items: { type: "integer", minimum: 0 }, description: "Pace zone boundaries (seconds per km)" }
988
+ }
989
+ },
990
+ SportSegment: {
991
+ type: "object",
992
+ required: ["segment_id", "sport", "segment_index"],
993
+ additionalProperties: false,
994
+ description: "A segment within a multi-sport workout (PWF v2.1)",
995
+ properties: {
996
+ segment_id: { type: "string", description: "Segment identifier" },
997
+ sport: { $ref: "#/$defs/Sport", description: "Sport for this segment" },
998
+ segment_index: { type: "integer", minimum: 0, description: "Segment number in sequence (0-indexed)" },
999
+ started_at: { type: "string", format: "date-time", description: "When segment started (ISO 8601)" },
1000
+ duration_sec: { type: "integer", minimum: 0, description: "Segment duration in seconds" },
1001
+ distance_m: { type: "number", minimum: 0, description: "Distance covered in this segment (meters)" },
1002
+ exercise_ids: { type: "array", items: { type: "string" }, description: "Exercises/sets completed during this segment" },
1003
+ telemetry: { $ref: "#/$defs/WorkoutTelemetry", description: "Telemetry specific to this segment" },
1004
+ transition: { $ref: "#/$defs/TransitionData", description: "Transition data after this segment" },
1005
+ notes: { type: "string", description: "Notes specific to this segment" }
1006
+ }
1007
+ },
1008
+ TransitionData: {
1009
+ type: "object",
1010
+ required: ["transition_id", "from_sport", "to_sport"],
1011
+ additionalProperties: false,
1012
+ description: "Transition between sports in a multi-sport event (PWF v2.1)",
1013
+ properties: {
1014
+ transition_id: { type: "string", description: "Transition identifier (e.g., T1, T2)" },
1015
+ from_sport: { $ref: "#/$defs/Sport", description: "From which sport" },
1016
+ to_sport: { $ref: "#/$defs/Sport", description: "To which sport" },
1017
+ duration_sec: { type: "integer", minimum: 0, description: "Transition duration in seconds" },
1018
+ started_at: { type: "string", format: "date-time", description: "When transition started (ISO 8601)" },
1019
+ heart_rate_avg: { type: "integer", minimum: 0, description: "Average heart rate during transition" },
1020
+ notes: { type: "string", description: "Notes about transition (e.g., equipment changes)" }
1021
+ }
1022
+ },
1023
+ GpsRoute: {
1024
+ type: "object",
1025
+ required: ["route_id", "positions"],
1026
+ additionalProperties: false,
1027
+ description: "A GPS route/track containing multiple positions (PWF v2.1)",
1028
+ properties: {
1029
+ route_id: { type: "string", description: "Unique identifier for this route" },
1030
+ name: { type: "string", description: "Human-readable route name" },
1031
+ positions: { type: "array", items: { $ref: "#/$defs/GpsPosition" }, description: "GPS positions in chronological order" },
1032
+ total_distance_m: { type: "number", minimum: 0, description: "Total distance calculated from GPS (meters)" },
1033
+ total_ascent_m: { type: "number", minimum: 0, description: "Total elevation gain (meters)" },
1034
+ total_descent_m: { type: "number", minimum: 0, description: "Total elevation loss (meters)" },
1035
+ min_elevation_m: { type: "number", description: "Minimum elevation on route (meters)" },
1036
+ max_elevation_m: { type: "number", description: "Maximum elevation on route (meters)" },
1037
+ bbox_sw_lat: { type: "number", minimum: -90, maximum: 90, description: "Bounding box - southwest corner latitude" },
1038
+ bbox_sw_lng: { type: "number", minimum: -180, maximum: 180, description: "Bounding box - southwest corner longitude" },
1039
+ bbox_ne_lat: { type: "number", minimum: -90, maximum: 90, description: "Bounding box - northeast corner latitude" },
1040
+ bbox_ne_lng: { type: "number", minimum: -180, maximum: 180, description: "Bounding box - northeast corner longitude" },
1041
+ recording_mode: { type: "string", description: "Recording mode (e.g., auto, smart, 1s, gps_only)" },
1042
+ gps_fix: { $ref: "#/$defs/GpsFix", description: "GPS fix quality indicator" }
1043
+ }
1044
+ },
1045
+ GpsPosition: {
1046
+ type: "object",
1047
+ required: ["latitude_deg", "longitude_deg", "timestamp"],
1048
+ additionalProperties: false,
1049
+ description: "A single GPS position/waypoint with timestamp (PWF v2.1)",
1050
+ properties: {
1051
+ latitude_deg: { type: "number", minimum: -90, maximum: 90, description: "Latitude in decimal degrees (WGS84)" },
1052
+ longitude_deg: { type: "number", minimum: -180, maximum: 180, description: "Longitude in decimal degrees (WGS84)" },
1053
+ timestamp: { type: "string", format: "date-time", description: "Timestamp when position was recorded (ISO 8601)" },
1054
+ elevation_m: { type: "number", description: "Elevation/altitude above sea level (meters)" },
1055
+ accuracy_m: { type: "number", minimum: 0, description: "Horizontal accuracy/uncertainty (meters)" },
1056
+ speed_mps: { type: "number", minimum: 0, description: "Speed at this point (meters per second)" },
1057
+ heading_deg: { type: "number", minimum: 0, maximum: 360, description: "Heading/bearing (degrees from north, 0-360)" },
1058
+ heart_rate_bpm: { type: "integer", minimum: 0, description: "Heart rate at this position (bpm)" },
1059
+ power_watts: { type: "integer", minimum: 0, description: "Power at this position (watts)" },
1060
+ cadence: { type: "integer", minimum: 0, description: "Cadence at this position (RPM or SPM)" },
1061
+ temperature_c: { type: "number", description: "Temperature at this position (Celsius)" }
1062
+ }
1063
+ },
1064
+ GpsFix: {
1065
+ type: "string",
1066
+ enum: ["none", "fix_2d", "fix_3d", "dgps", "unknown"],
1067
+ description: "GPS fix quality indicator"
1068
+ },
1069
+ TimeSeriesData: {
1070
+ type: "object",
1071
+ required: ["timestamps"],
1072
+ additionalProperties: false,
1073
+ description: "Columnar time-series data for second-by-second telemetry (PWF v2.1)",
1074
+ properties: {
1075
+ timestamps: { type: "array", items: { type: "string", format: "date-time" }, description: "Timestamps for each record (ISO 8601). All other arrays must match this length." },
1076
+ elapsed_sec: { type: "array", items: { type: "integer", minimum: 0 }, description: "Elapsed time in seconds since start" },
1077
+ heart_rate: { type: "array", items: { type: "integer", minimum: 0 }, description: "Heart rate readings (bpm)" },
1078
+ power: { type: "array", items: { type: "integer", minimum: 0 }, description: "Power readings (watts)" },
1079
+ cadence: { type: "array", items: { type: "integer", minimum: 0 }, description: "Cadence readings (RPM for cycling, SPM for running/swimming)" },
1080
+ speed_mps: { type: "array", items: { type: "number", minimum: 0 }, description: "Speed readings (meters per second)" },
1081
+ distance_m: { type: "array", items: { type: "number", minimum: 0 }, description: "Distance readings (cumulative meters)" },
1082
+ elevation_m: { type: "array", items: { type: "number" }, description: "Elevation/altitude readings (meters)" },
1083
+ temperature_c: { type: "array", items: { type: "number" }, description: "Temperature readings (Celsius)" },
1084
+ latitude: { type: "array", items: { type: "number", minimum: -90, maximum: 90 }, description: "Latitude readings (decimal degrees)" },
1085
+ longitude: { type: "array", items: { type: "number", minimum: -180, maximum: 180 }, description: "Longitude readings (decimal degrees)" },
1086
+ grade_percent: { type: "array", items: { type: "number", minimum: -100, maximum: 100 }, description: "Grade/slope readings (percentage)" },
1087
+ respiration_rate: { type: "array", items: { type: "integer", minimum: 0 }, description: "Respiration rate (breaths per minute)" },
1088
+ core_temperature_c: { type: "array", items: { type: "number" }, description: "Core body temperature (Celsius)" },
1089
+ muscle_oxygen_percent: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Muscle oxygen saturation (percentage)" },
1090
+ power_balance: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Left/right power balance (percentage left)" },
1091
+ left_pedal_smoothness: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Left pedal smoothness (percentage)" },
1092
+ right_pedal_smoothness: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Right pedal smoothness (percentage)" },
1093
+ left_torque_effectiveness: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Left torque effectiveness (percentage)" },
1094
+ right_torque_effectiveness: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Right torque effectiveness (percentage)" },
1095
+ stride_length_m: { type: "array", items: { type: "number", minimum: 0 }, description: "Running stride length (meters)" },
1096
+ vertical_oscillation_cm: { type: "array", items: { type: "number", minimum: 0 }, description: "Running vertical oscillation (centimeters)" },
1097
+ ground_contact_time_ms: { type: "array", items: { type: "integer", minimum: 0 }, description: "Running ground contact time (milliseconds)" },
1098
+ ground_contact_balance: { type: "array", items: { type: "number", minimum: 0, maximum: 100 }, description: "Running ground contact balance (percentage left)" },
1099
+ stroke_rate: { type: "array", items: { type: "integer", minimum: 0 }, description: "Swimming stroke rate (strokes per minute)" },
1100
+ stroke_count: { type: "array", items: { type: "integer", minimum: 0 }, description: "Swimming stroke count (cumulative)" },
1101
+ swolf: { type: "array", items: { type: "integer", minimum: 0 }, description: "Swimming SWOLF score" },
1102
+ stroke_type: { type: "array", items: { $ref: "#/$defs/StrokeType" }, description: "Swimming stroke type at each point" }
1103
+ }
1104
+ },
1105
+ PoolConfig: {
1106
+ type: "object",
1107
+ required: ["pool_length"],
1108
+ additionalProperties: false,
1109
+ description: "Pool configuration for swimming workouts (PWF v2.1)",
1110
+ properties: {
1111
+ pool_length: { type: "number", minimum: 0, description: "Length of the pool in the specified units" },
1112
+ pool_length_unit: { $ref: "#/$defs/PoolLengthUnit", description: "Unit for pool length (meters or yards)" }
1113
+ }
1114
+ },
1115
+ PoolLengthUnit: {
1116
+ type: "string",
1117
+ enum: ["meters", "yards"],
1118
+ default: "meters",
1119
+ description: "Unit for pool length measurement"
1120
+ },
1121
+ SwimmingSetData: {
1122
+ type: "object",
1123
+ additionalProperties: false,
1124
+ description: "Swimming-specific data for a completed set (PWF v2.1)",
1125
+ properties: {
1126
+ lengths: { type: "array", items: { $ref: "#/$defs/SwimmingLength" }, description: "Individual lengths within this set/lap" },
1127
+ stroke_type: { $ref: "#/$defs/StrokeType", description: "Primary stroke type for the set (if all lengths same stroke)" },
1128
+ total_lengths: { type: "integer", minimum: 0, description: "Total number of lengths in this set" },
1129
+ active_lengths: { type: "integer", minimum: 0, description: "Number of active lengths (excludes rest at wall)" },
1130
+ swolf_avg: { type: "integer", minimum: 0, description: "Average SWOLF across all lengths in this set" },
1131
+ drill_mode: { type: "boolean", description: "Whether this set was drill work (technique focus)" }
1132
+ }
1133
+ },
1134
+ SwimmingLength: {
1135
+ type: "object",
1136
+ required: ["length_number", "stroke_type", "duration_sec"],
1137
+ additionalProperties: false,
1138
+ description: "A single length (one pool length) within a swimming set/lap (PWF v2.1)",
1139
+ properties: {
1140
+ length_number: { type: "integer", minimum: 1, description: "Length number within the set (1-indexed)" },
1141
+ stroke_type: { $ref: "#/$defs/StrokeType", description: "Stroke type used for this length" },
1142
+ duration_sec: { type: "integer", minimum: 0, description: "Duration of this length in seconds" },
1143
+ stroke_count: { type: "integer", minimum: 0, description: "Number of strokes taken during this length" },
1144
+ swolf: { type: "integer", minimum: 0, description: "SWOLF score (duration + stroke_count) - lower is better" },
1145
+ started_at: { type: "string", format: "date-time", description: "Timestamp when this length started (ISO 8601)" },
1146
+ active: { type: "boolean", description: "Whether this was an active length (vs. rest at wall)" }
1147
+ }
1148
+ },
1149
+ StrokeType: {
1150
+ type: "string",
1151
+ enum: ["freestyle", "backstroke", "breaststroke", "butterfly", "drill", "mixed", "im"],
1152
+ description: "Swimming stroke type for pool and open water swimming"
1153
+ },
1154
+ DeviceInfo: {
1155
+ type: "object",
1156
+ required: ["device_type", "manufacturer"],
1157
+ additionalProperties: false,
1158
+ description: "Information about a device used during the workout (PWF v2)",
1159
+ properties: {
1160
+ device_index: { type: "integer", minimum: 0, maximum: 255, description: "Device index for multi-device workouts (e.g., 0=watch, 1=HRM, 2=power meter)" },
1161
+ device_type: { $ref: "#/$defs/DeviceType", description: "Type of device" },
1162
+ manufacturer: { type: "string", description: "Device manufacturer (can be a known manufacturer or custom string)" },
1163
+ product: { type: "string", description: "Specific product/model name" },
1164
+ serial_number: { type: "string", description: "Unique device serial number" },
1165
+ software_version: { type: "string", description: "Software/firmware version" },
1166
+ hardware_version: { type: "string", description: "Hardware version" },
1167
+ battery: { $ref: "#/$defs/BatteryInfo", description: "Battery information" },
1168
+ cumulative_operating_time_hours: { type: "number", minimum: 0, description: "Cumulative operating time in hours" },
1169
+ connection: { $ref: "#/$defs/ConnectionInfo", description: "Connection information for sensors" },
1170
+ calibration: { $ref: "#/$defs/CalibrationInfo", description: "Calibration information for sensors" }
1171
+ }
1172
+ },
1173
+ DeviceType: {
1174
+ type: "string",
1175
+ enum: ["watch", "bike_computer", "heart_rate_monitor", "power_meter", "speed_sensor", "cadence_sensor", "speed_cadence_sensor", "foot_pod", "smart_trainer", "camera", "phone", "other"],
1176
+ description: "Type of device"
1177
+ },
1178
+ BatteryInfo: {
1179
+ type: "object",
1180
+ additionalProperties: false,
1181
+ description: "Battery information for a device",
1182
+ properties: {
1183
+ start_percent: { type: "integer", minimum: 0, maximum: 100, description: "Battery level at start of workout (percentage)" },
1184
+ end_percent: { type: "integer", minimum: 0, maximum: 100, description: "Battery level at end of workout (percentage)" },
1185
+ voltage: { type: "number", minimum: 0, description: "Battery voltage" },
1186
+ status: { $ref: "#/$defs/BatteryStatus", description: "Battery status indicator" }
1187
+ }
1188
+ },
1189
+ BatteryStatus: {
1190
+ type: "string",
1191
+ enum: ["good", "low", "critical", "charging", "unknown"],
1192
+ description: "Battery status indicator"
1193
+ },
1194
+ ConnectionInfo: {
1195
+ type: "object",
1196
+ required: ["connection_type"],
1197
+ additionalProperties: false,
1198
+ description: "Connection information for wireless sensors",
1199
+ properties: {
1200
+ connection_type: { $ref: "#/$defs/ConnectionType", description: "Type of connection" },
1201
+ ant_device_number: { type: "integer", minimum: 0, description: "ANT+ device number (for ANT+ sensors)" },
1202
+ bluetooth_id: { type: "string", description: "Bluetooth MAC address or identifier" }
1203
+ }
1204
+ },
1205
+ ConnectionType: {
1206
+ type: "string",
1207
+ enum: ["local", "ant_plus", "bluetooth_le", "bluetooth", "wifi", "usb", "unknown"],
1208
+ description: "Type of device connection"
1209
+ },
1210
+ CalibrationInfo: {
1211
+ type: "object",
1212
+ additionalProperties: false,
1213
+ description: "Calibration information for sensors (e.g., power meters)",
1214
+ properties: {
1215
+ calibration_factor: { type: "number", description: "Calibration factor or zero offset" },
1216
+ last_calibrated: { type: "string", format: "date-time", description: "Timestamp of last calibration" },
1217
+ auto_zero_enabled: { type: "boolean", description: "Auto-zero setting (for power meters)" }
1218
+ }
1219
+ }
1220
+ }
1221
+ };
1222
+
1223
+ // src/validate.ts
1224
+ var ajv = new Ajv__default.default({
1225
+ allErrors: true,
1226
+ strict: false
1227
+ });
1228
+ addFormats__default.default(ajv);
1229
+ var planValidator = ajv.compile(pwf_v1_default);
1230
+ var historyValidator = ajv.compile(pwf_history_v1_default);
1231
+ var identifierPattern = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
1232
+ function decodePointerSegment(segment) {
1233
+ return segment.replace(/~1/g, "/").replace(/~0/g, "~");
1234
+ }
1235
+ function appendSegment(path, segment) {
1236
+ if (/^\d+$/.test(segment)) {
1237
+ return `${path}[${segment}]`;
1238
+ }
1239
+ if (identifierPattern.test(segment)) {
1240
+ return path ? `${path}.${segment}` : segment;
1241
+ }
1242
+ const escaped = segment.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
1243
+ return `${path}['${escaped}']`;
1244
+ }
1245
+ function formatPath(pointer) {
1246
+ if (!pointer) {
1247
+ return "";
1248
+ }
1249
+ const segments = pointer.split("/").slice(1).map(decodePointerSegment);
1250
+ return segments.reduce((path, segment) => appendSegment(path, segment), "");
1251
+ }
1252
+ function formatErrorPath(error) {
1253
+ let path = formatPath(error.instancePath);
1254
+ if (error.keyword === "required" && typeof error.params.missingProperty === "string") {
1255
+ path = appendSegment(path, error.params.missingProperty);
1256
+ }
1257
+ if (error.keyword === "additionalProperties" && typeof error.params.additionalProperty === "string") {
1258
+ path = appendSegment(path, error.params.additionalProperty);
1259
+ }
1260
+ return path;
1261
+ }
1262
+ function ajvErrorsToIssues(errors) {
1263
+ return errors.map((error) => ({
1264
+ path: formatErrorPath(error),
1265
+ message: error.message,
1266
+ severity: "error"
1267
+ }));
1268
+ }
1269
+ function validatePlan(plan, _options = {}) {
1270
+ const valid = planValidator(plan);
1271
+ if (valid) {
1272
+ return [];
1273
+ }
1274
+ return ajvErrorsToIssues(planValidator.errors);
1275
+ }
1276
+ function validateHistory(history, _options = {}) {
1277
+ const valid = historyValidator(history);
1278
+ if (valid) {
1279
+ return [];
1280
+ }
1281
+ return ajvErrorsToIssues(historyValidator.errors);
1282
+ }
1283
+
1284
+ // src/parse.ts
1285
+ function yamlParse(text) {
1286
+ try {
1287
+ return { value: yaml.parse(text) };
1288
+ } catch (error) {
1289
+ const message = error instanceof Error ? error.message : "Invalid YAML";
1290
+ return {
1291
+ issue: {
1292
+ path: "",
1293
+ message,
1294
+ severity: "error"
1295
+ }
1296
+ };
1297
+ }
1298
+ }
1299
+ function parsePlan(text, options = {}) {
1300
+ const result = yamlParse(text);
1301
+ if (result.issue) {
1302
+ return [result.issue];
1303
+ }
1304
+ const issues = validatePlan(result.value, options);
1305
+ if (issues.length > 0) {
1306
+ return issues;
1307
+ }
1308
+ return result.value;
1309
+ }
1310
+ function parseHistory(text, options = {}) {
1311
+ const result = yamlParse(text);
1312
+ if (result.issue) {
1313
+ return [result.issue];
1314
+ }
1315
+ const issues = validateHistory(result.value, options);
1316
+ if (issues.length > 0) {
1317
+ return issues;
1318
+ }
1319
+ return result.value;
1320
+ }
1321
+ function isValidationIssueList(value) {
1322
+ return Array.isArray(value) && value.every(
1323
+ (item) => item && typeof item === "object" && "message" in item && "severity" in item && "path" in item
1324
+ );
1325
+ }
1326
+ function toYAML(value) {
1327
+ return yaml.stringify(value);
1328
+ }
1329
+ function fromYAML(text) {
1330
+ return yaml.parse(text);
1331
+ }
1332
+
1333
+ // src/builder.ts
1334
+ var PlanBuilder = class {
1335
+ constructor() {
1336
+ this.currentDayIndex = null;
1337
+ this.plan = {
1338
+ plan_version: 1,
1339
+ cycle: {
1340
+ days: []
1341
+ }
1342
+ };
1343
+ }
1344
+ version(version) {
1345
+ this.plan.plan_version = version;
1346
+ return this;
1347
+ }
1348
+ meta(meta) {
1349
+ this.plan.meta = meta;
1350
+ return this;
1351
+ }
1352
+ glossary(glossary) {
1353
+ this.plan.glossary = glossary;
1354
+ return this;
1355
+ }
1356
+ addDay(focus, dayOverrides = {}) {
1357
+ const day = {
1358
+ ...focus ? { focus } : {},
1359
+ ...dayOverrides
1360
+ };
1361
+ day.exercises = day.exercises ?? [];
1362
+ if (!this.plan.cycle) {
1363
+ this.plan.cycle = { days: [] };
1364
+ }
1365
+ this.plan.cycle.days = this.plan.cycle.days ?? [];
1366
+ this.plan.cycle.days.push(day);
1367
+ this.currentDayIndex = this.plan.cycle.days.length - 1;
1368
+ return this;
1369
+ }
1370
+ addExercise(name, exercise) {
1371
+ if (this.currentDayIndex === null || !this.plan.cycle?.days?.[this.currentDayIndex]) {
1372
+ throw new Error("addExercise requires an active day. Call addDay first.");
1373
+ }
1374
+ const day = this.plan.cycle.days[this.currentDayIndex];
1375
+ day.exercises = day.exercises ?? [];
1376
+ day.exercises.push({ name, ...exercise });
1377
+ return this;
1378
+ }
1379
+ toYAML() {
1380
+ return toYAML(this.build());
1381
+ }
1382
+ build() {
1383
+ if (!this.plan.cycle || this.plan.cycle.days.length === 0) {
1384
+ throw new Error("Plan requires at least one day.");
1385
+ }
1386
+ for (const [index, day] of this.plan.cycle.days.entries()) {
1387
+ if (!day.exercises || day.exercises.length === 0) {
1388
+ throw new Error(`Day ${index + 1} requires at least one exercise.`);
1389
+ }
1390
+ }
1391
+ const document = this.plan;
1392
+ Object.defineProperty(document, "toYAML", {
1393
+ value: () => toYAML(document),
1394
+ enumerable: false
1395
+ });
1396
+ return document;
1397
+ }
1398
+ };
1399
+
1400
+ exports.PlanBuilder = PlanBuilder;
1401
+ exports.fromYAML = fromYAML;
1402
+ exports.historySchema = pwf_history_v1_default;
1403
+ exports.isValidationIssueList = isValidationIssueList;
1404
+ exports.parseHistory = parseHistory;
1405
+ exports.parsePlan = parsePlan;
1406
+ exports.planSchema = pwf_v1_default;
1407
+ exports.toYAML = toYAML;
1408
+ exports.validateHistory = validateHistory;
1409
+ exports.validatePlan = validatePlan;
1410
+ //# sourceMappingURL=index.cjs.map
1411
+ //# sourceMappingURL=index.cjs.map