endurance-coach 0.1.1 → 1.0.0

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.
Files changed (74) hide show
  1. package/README.md +3 -0
  2. package/dist/cli.js +318 -35
  3. package/dist/expander/expander.d.ts +20 -0
  4. package/dist/expander/expander.js +339 -0
  5. package/dist/expander/index.d.ts +8 -0
  6. package/dist/expander/index.js +9 -0
  7. package/dist/expander/types.d.ts +169 -0
  8. package/dist/expander/types.js +6 -0
  9. package/dist/expander/zones.d.ts +50 -0
  10. package/dist/expander/zones.js +159 -0
  11. package/dist/index.d.ts +4 -0
  12. package/dist/index.js +9 -1
  13. package/dist/schema/compact-plan.d.ts +175 -0
  14. package/dist/schema/compact-plan.js +64 -0
  15. package/dist/schema/compact-plan.schema.d.ts +277 -0
  16. package/dist/schema/compact-plan.schema.js +205 -0
  17. package/dist/templates/index.d.ts +10 -0
  18. package/dist/templates/index.js +13 -0
  19. package/dist/templates/interpolate.d.ts +51 -0
  20. package/dist/templates/interpolate.js +204 -0
  21. package/dist/templates/loader.d.ts +19 -0
  22. package/dist/templates/loader.js +129 -0
  23. package/dist/templates/template.schema.d.ts +401 -0
  24. package/dist/templates/template.schema.js +101 -0
  25. package/dist/templates/template.types.d.ts +155 -0
  26. package/dist/templates/template.types.js +7 -0
  27. package/dist/templates/yaml-parser.d.ts +15 -0
  28. package/dist/templates/yaml-parser.js +18 -0
  29. package/package.json +2 -1
  30. package/templates/bike/CLAUDE.md +7 -0
  31. package/templates/bike/easy.yaml +38 -0
  32. package/templates/bike/endurance.yaml +42 -0
  33. package/templates/bike/hills.yaml +80 -0
  34. package/templates/bike/overunders.yaml +81 -0
  35. package/templates/bike/rest.yaml +16 -0
  36. package/templates/bike/sweetspot.yaml +80 -0
  37. package/templates/bike/tempo.yaml +79 -0
  38. package/templates/bike/threshold.yaml +83 -0
  39. package/templates/bike/vo2max.yaml +84 -0
  40. package/templates/brick/CLAUDE.md +7 -0
  41. package/templates/brick/halfironman.yaml +72 -0
  42. package/templates/brick/ironman.yaml +72 -0
  43. package/templates/brick/olympic.yaml +70 -0
  44. package/templates/brick/sprint.yaml +70 -0
  45. package/templates/plan-viewer.html +22 -22
  46. package/templates/run/CLAUDE.md +7 -0
  47. package/templates/run/easy.yaml +36 -0
  48. package/templates/run/fartlek.yaml +40 -0
  49. package/templates/run/hills.yaml +36 -0
  50. package/templates/run/intervals.1k.yaml +63 -0
  51. package/templates/run/intervals.400.yaml +63 -0
  52. package/templates/run/intervals.800.yaml +63 -0
  53. package/templates/run/intervals.mile.yaml +64 -0
  54. package/templates/run/long.yaml +41 -0
  55. package/templates/run/progression.yaml +49 -0
  56. package/templates/run/race.5k.yaml +36 -0
  57. package/templates/run/recovery.yaml +36 -0
  58. package/templates/run/rest.yaml +16 -0
  59. package/templates/run/strides.yaml +49 -0
  60. package/templates/run/tempo.yaml +56 -0
  61. package/templates/run/threshold.yaml +56 -0
  62. package/templates/strength/CLAUDE.md +7 -0
  63. package/templates/strength/core.yaml +56 -0
  64. package/templates/strength/foundation.yaml +65 -0
  65. package/templates/strength/full.yaml +73 -0
  66. package/templates/strength/maintenance.yaml +62 -0
  67. package/templates/swim/CLAUDE.md +7 -0
  68. package/templates/swim/aerobic.yaml +67 -0
  69. package/templates/swim/easy.yaml +51 -0
  70. package/templates/swim/openwater.yaml +60 -0
  71. package/templates/swim/rest.yaml +16 -0
  72. package/templates/swim/technique.yaml +67 -0
  73. package/templates/swim/threshold.yaml +75 -0
  74. package/templates/swim/vo2max.yaml +88 -0
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Zone Calculator
3
+ *
4
+ * Calculates training zone ranges from threshold values.
5
+ * Uses standard physiological percentages for zone calculations.
6
+ */
7
+ // ============================================================================
8
+ // Heart Rate Zone Calculation
9
+ // ============================================================================
10
+ /**
11
+ * Standard 5-zone heart rate model based on LTHR percentages.
12
+ */
13
+ const HR_ZONE_DEFINITIONS = [
14
+ { zone: 1, name: "Recovery", percentLow: 0, percentHigh: 81 },
15
+ { zone: 2, name: "Aerobic", percentLow: 81, percentHigh: 89 },
16
+ { zone: 3, name: "Tempo", percentLow: 89, percentHigh: 94 },
17
+ { zone: 4, name: "Threshold", percentLow: 94, percentHigh: 100 },
18
+ { zone: 5, name: "VO2max", percentLow: 100, percentHigh: 106 },
19
+ ];
20
+ /**
21
+ * Calculate heart rate zones from LTHR.
22
+ *
23
+ * Uses the standard 5-zone model with percentages of LTHR.
24
+ */
25
+ export function calculateHRZones(config) {
26
+ const { lthr, maxHR, restingHR } = config;
27
+ const zones = HR_ZONE_DEFINITIONS.map((def) => ({
28
+ zone: def.zone,
29
+ name: def.name,
30
+ percentLow: def.percentLow,
31
+ percentHigh: def.percentHigh,
32
+ hrLow: Math.round(lthr * (def.percentLow / 100)),
33
+ hrHigh: Math.round(lthr * (def.percentHigh / 100)),
34
+ }));
35
+ return {
36
+ lthr,
37
+ maxHR,
38
+ restingHR,
39
+ zones,
40
+ };
41
+ }
42
+ // ============================================================================
43
+ // Pace Zone Calculation
44
+ // ============================================================================
45
+ /**
46
+ * Parse a pace string into seconds per unit.
47
+ *
48
+ * @example
49
+ * parsePace("5:30/km") // { seconds: 330, unit: "km" }
50
+ * parsePace("8:00/mi") // { seconds: 480, unit: "mi" }
51
+ * parsePace("5:30") // { seconds: 330, unit: undefined }
52
+ */
53
+ export function parsePace(pace) {
54
+ const match = pace.match(/^(\d+):(\d{2})(?:\/(\w+))?$/);
55
+ if (!match) {
56
+ throw new Error(`Invalid pace format: ${pace}`);
57
+ }
58
+ const minutes = parseInt(match[1], 10);
59
+ const seconds = parseInt(match[2], 10);
60
+ const unit = match[3];
61
+ return {
62
+ seconds: minutes * 60 + seconds,
63
+ unit,
64
+ };
65
+ }
66
+ /**
67
+ * Format seconds back to a pace string.
68
+ *
69
+ * @example
70
+ * formatPace(330, "km") // "5:30/km"
71
+ * formatPace(480) // "8:00"
72
+ */
73
+ export function formatPace(seconds, unit) {
74
+ const mins = Math.floor(seconds / 60);
75
+ const secs = Math.round(seconds % 60);
76
+ const base = `${mins}:${secs.toString().padStart(2, "0")}`;
77
+ return unit ? `${base}/${unit}` : base;
78
+ }
79
+ /**
80
+ * Standard pace zone definitions based on Jack Daniels' VDOT system.
81
+ * Offsets are in seconds per km from threshold pace.
82
+ */
83
+ const PACE_ZONE_DEFINITIONS = [
84
+ { zone: "E", name: "Easy", offsetSeconds: 60 }, // +60s from threshold
85
+ { zone: "M", name: "Marathon", offsetSeconds: 30 }, // +30s from threshold
86
+ { zone: "T", name: "Threshold", offsetSeconds: 0 }, // At threshold
87
+ { zone: "I", name: "Interval", offsetSeconds: -15 }, // -15s from threshold
88
+ { zone: "R", name: "Repetition", offsetSeconds: -30 }, // -30s from threshold
89
+ ];
90
+ /**
91
+ * Calculate pace zones from threshold pace.
92
+ */
93
+ export function calculatePaceZones(thresholdPace) {
94
+ const { seconds: thresholdSeconds, unit } = parsePace(thresholdPace);
95
+ const zones = PACE_ZONE_DEFINITIONS.map((def) => {
96
+ const paceSeconds = thresholdSeconds + def.offsetSeconds;
97
+ return {
98
+ zone: def.zone,
99
+ name: def.name,
100
+ pace: formatPace(paceSeconds, unit),
101
+ paceSeconds,
102
+ };
103
+ });
104
+ return {
105
+ thresholdPace,
106
+ thresholdPaceSeconds: thresholdSeconds,
107
+ zones,
108
+ };
109
+ }
110
+ // ============================================================================
111
+ // Combined Zone Calculation
112
+ // ============================================================================
113
+ /**
114
+ * Calculate all athlete zones from compact plan inputs.
115
+ */
116
+ export function calculateAthleteZones(hrConfig, paces) {
117
+ const zones = {};
118
+ // Calculate HR zones if LTHR provided
119
+ if (hrConfig) {
120
+ zones.run = zones.run || {};
121
+ zones.run.hr = calculateHRZones(hrConfig);
122
+ // Also apply to bike if no separate bike HR zones
123
+ zones.bike = zones.bike || {};
124
+ zones.bike.hr = calculateHRZones(hrConfig);
125
+ zones.maxHR = hrConfig.maxHR;
126
+ zones.restingHR = hrConfig.restingHR;
127
+ }
128
+ // Calculate pace zones if threshold pace provided
129
+ if (paces?.threshold) {
130
+ zones.run = zones.run || {};
131
+ zones.run.pace = calculatePaceZones(paces.threshold);
132
+ }
133
+ return zones;
134
+ }
135
+ // ============================================================================
136
+ // Utility Functions
137
+ // ============================================================================
138
+ /**
139
+ * Get the HR zone for a given heart rate.
140
+ */
141
+ export function getHRZoneForValue(hr, zones) {
142
+ return zones.zones.find((z) => hr >= z.hrLow && hr <= z.hrHigh);
143
+ }
144
+ /**
145
+ * Get the pace zone for a given pace.
146
+ */
147
+ export function getPaceZoneForValue(paceSeconds, zones) {
148
+ // Find the closest zone
149
+ let closest;
150
+ let minDiff = Infinity;
151
+ for (const zone of zones.zones) {
152
+ const diff = Math.abs(paceSeconds - zone.paceSeconds);
153
+ if (diff < minDiff) {
154
+ minDiff = diff;
155
+ closest = zone;
156
+ }
157
+ }
158
+ return closest;
159
+ }
package/dist/index.d.ts CHANGED
@@ -4,4 +4,8 @@
4
4
  * Public API for validating and working with training plans.
5
5
  */
6
6
  export { validatePlan, validatePlanOrThrow, formatValidationErrors, getJsonSchema, TrainingPlanSchema, WorkoutSchema, TrainingWeekSchema, TrainingDaySchema, TrainingPhaseSchema, AthleteAssessmentSchema, AthleteZonesSchema, RaceStrategySchema, UnitPreferencesSchema, PlanMetaSchema, type TrainingPlan, type Workout, type TrainingWeek, type TrainingDay, type TrainingPhase, type AthleteAssessment, type AthleteZones, type RaceStrategy, type UnitPreferences, type ValidationResult, type ValidationError, } from "./schema/training-plan.schema.js";
7
+ export { validateCompactPlan, validateCompactPlanOrThrow, formatCompactValidationErrors, CompactPlanSchema, CompactAthleteSchema, CompactPhaseSchema, CompactWeekSchema, type CompactPlan, type CompactAthlete, type CompactPhase, type CompactWeek, type CompactWeekSchedule, type CompactRaceStrategy, type AthletePaces, type CompactValidationResult, type CompactValidationError, } from "./schema/compact-plan.schema.js";
8
+ export { parseWorkoutRef, parseWeekRange } from "./schema/compact-plan.js";
9
+ export { loadTemplates, loadTemplatesFromArray, getTemplatesPath, validateTemplate, validateTemplateOrThrow, interpolate, interpolateObject, createContext, parseYaml, stringifyYaml, type WorkoutTemplate, type TemplateParam, type TemplateParams, type TemplateRegistry, type InterpolationContext, } from "./templates/index.js";
10
+ export { expandPlan, expandWorkout, validateWorkoutRefs, calculateHRZones, calculatePaceZones, calculateAthleteZones, parsePace, formatPace, type ExpandedPlan, type ExpandedWeek, type ExpandedDay, type ExpandedWorkout, type ExpandedPhase, type ExpandedAthleteZones, type ExpansionOptions, } from "./expander/index.js";
7
11
  export type { Sport, WorkoutType, IntensityUnit, DurationUnit, StepType, SwimDistanceUnit, LandDistanceUnit, FirstDayOfWeek, IntensityTarget, DurationTarget, WorkoutStep, IntervalSet, StructuredWorkout, WeekSummary, HeartRateZones, PowerZones, SwimZones, PaceZones, } from "./schema/training-plan.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API for validating and working with training plans.
5
5
  */
6
- // Schema validation
6
+ // Full Schema validation (v1.0 - expanded format)
7
7
  export {
8
8
  // Validation functions
9
9
  validatePlan, validatePlanOrThrow, formatValidationErrors, getJsonSchema,
@@ -11,3 +11,11 @@ validatePlan, validatePlanOrThrow, formatValidationErrors, getJsonSchema,
11
11
  TrainingPlanSchema,
12
12
  // Component schemas (for partial validation)
13
13
  WorkoutSchema, TrainingWeekSchema, TrainingDaySchema, TrainingPhaseSchema, AthleteAssessmentSchema, AthleteZonesSchema, RaceStrategySchema, UnitPreferencesSchema, PlanMetaSchema, } from "./schema/training-plan.schema.js";
14
+ // Compact Schema validation (v2.0 - template-based format)
15
+ export { validateCompactPlan, validateCompactPlanOrThrow, formatCompactValidationErrors, CompactPlanSchema, CompactAthleteSchema, CompactPhaseSchema, CompactWeekSchema, } from "./schema/compact-plan.schema.js";
16
+ // Compact plan utilities
17
+ export { parseWorkoutRef, parseWeekRange } from "./schema/compact-plan.js";
18
+ // Templates
19
+ export { loadTemplates, loadTemplatesFromArray, getTemplatesPath, validateTemplate, validateTemplateOrThrow, interpolate, interpolateObject, createContext, parseYaml, stringifyYaml, } from "./templates/index.js";
20
+ // Expander
21
+ export { expandPlan, expandWorkout, validateWorkoutRefs, calculateHRZones, calculatePaceZones, calculateAthleteZones, parsePace, formatPace, } from "./expander/index.js";
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Compact Training Plan Schema
3
+ *
4
+ * A minimal schema for AI models to generate training plans.
5
+ * The compact format is expanded to the full format for HTML rendering.
6
+ *
7
+ * Key design principles:
8
+ * - Minimal output for models to generate
9
+ * - Template references instead of full workout definitions
10
+ * - Athlete paces/zones as inputs, system calculates ranges
11
+ */
12
+ export type Sport = "swim" | "bike" | "run" | "strength" | "brick" | "race" | "rest";
13
+ export type FirstDayOfWeek = "monday" | "sunday";
14
+ export type DistanceUnit = "km" | "mi";
15
+ /**
16
+ * Athlete's pace values for different workout intensities.
17
+ * Models specify these once, templates interpolate them.
18
+ */
19
+ export interface AthletePaces {
20
+ easy?: string;
21
+ long?: string;
22
+ tempo?: string;
23
+ threshold?: string;
24
+ marathon?: string;
25
+ halfMarathon?: string;
26
+ interval?: string;
27
+ r200?: string;
28
+ r400?: string;
29
+ r800?: string;
30
+ r1k?: string;
31
+ rMile?: string;
32
+ bikeFtp?: number;
33
+ bikeEasy?: string;
34
+ bikeTempo?: string;
35
+ bikeThreshold?: string;
36
+ swimCss?: string;
37
+ swimEasy?: string;
38
+ swimTempo?: string;
39
+ }
40
+ /**
41
+ * Heart rate zone configuration.
42
+ * Only LTHR required - zone ranges are calculated automatically.
43
+ */
44
+ export interface HRZoneConfig {
45
+ lthr: number;
46
+ maxHR?: number;
47
+ restingHR?: number;
48
+ }
49
+ /**
50
+ * Athlete's training zones.
51
+ * Minimal input, system calculates full zone ranges.
52
+ */
53
+ export interface AthleteZones {
54
+ hr?: HRZoneConfig;
55
+ }
56
+ /**
57
+ * Athlete preferences and constraints.
58
+ */
59
+ export interface AthleteConstraints {
60
+ daysPerWeek?: number | string;
61
+ preferredDays?: string[];
62
+ maxLongRunHours?: number;
63
+ maxLongBikeHours?: number;
64
+ notes?: string[];
65
+ }
66
+ /**
67
+ * Complete athlete configuration in compact format.
68
+ */
69
+ export interface CompactAthlete {
70
+ name: string;
71
+ event: string;
72
+ eventDate: string;
73
+ paces: AthletePaces;
74
+ zones?: AthleteZones;
75
+ constraints?: AthleteConstraints;
76
+ unit?: DistanceUnit;
77
+ firstDayOfWeek?: FirstDayOfWeek;
78
+ }
79
+ /**
80
+ * A training phase defines a block of focused training.
81
+ * Weeks can be a range string or array of week numbers.
82
+ */
83
+ export interface CompactPhase {
84
+ name: string;
85
+ weeks: string | number[];
86
+ focus: string;
87
+ keyWorkouts?: string[];
88
+ }
89
+ /**
90
+ * A workout reference is a template ID with optional parameters.
91
+ *
92
+ * Format: "template.id" or "template.id(param)" or "template.id(param1, param2)"
93
+ *
94
+ * Examples:
95
+ * - "easy(30)" → easy run, 30 minutes
96
+ * - "intervals.400(6)" → 6x400m intervals
97
+ * - "long(90)" → 90-minute long run
98
+ * - "rest" → rest day (no params)
99
+ * - "tempo(20)" → 20-minute tempo section
100
+ */
101
+ export type WorkoutRef = string;
102
+ /**
103
+ * Day of the week as used in the schedule.
104
+ */
105
+ export type DayOfWeek = "Mon" | "Tue" | "Wed" | "Thu" | "Fri" | "Sat" | "Sun";
106
+ /**
107
+ * A week's workout schedule maps days to workout references.
108
+ * Days without workouts are implicitly rest days.
109
+ */
110
+ export interface CompactWeekSchedule {
111
+ [day: string]: WorkoutRef | WorkoutRef[];
112
+ }
113
+ /**
114
+ * A training week in the compact format.
115
+ */
116
+ export interface CompactWeek {
117
+ week: number;
118
+ phase: string;
119
+ focus?: string;
120
+ isRecoveryWeek?: boolean;
121
+ targetHours?: number;
122
+ workouts: CompactWeekSchedule;
123
+ }
124
+ /**
125
+ * Simplified race strategy for the compact format.
126
+ */
127
+ export interface CompactRaceStrategy {
128
+ goalTime?: string;
129
+ pacing?: {
130
+ swim?: string;
131
+ bike?: string;
132
+ run?: string;
133
+ };
134
+ nutrition?: string;
135
+ notes?: string[];
136
+ }
137
+ /**
138
+ * The complete compact training plan.
139
+ *
140
+ * This is what AI models generate. It's transformed to the full
141
+ * expanded format for HTML rendering and device export.
142
+ */
143
+ export interface CompactPlan {
144
+ version: "2.0";
145
+ athlete: CompactAthlete;
146
+ phases: CompactPhase[];
147
+ weeks: CompactWeek[];
148
+ raceStrategy?: CompactRaceStrategy;
149
+ }
150
+ /**
151
+ * Parsed workout reference.
152
+ */
153
+ export interface ParsedWorkoutRef {
154
+ templateId: string;
155
+ params: (string | number)[];
156
+ }
157
+ /**
158
+ * Parse a workout reference string into its components.
159
+ *
160
+ * Examples:
161
+ * "easy(30)" → { templateId: "easy", params: [30] }
162
+ * "intervals.400(6)" → { templateId: "intervals.400", params: [6] }
163
+ * "rest" → { templateId: "rest", params: [] }
164
+ * "tempo(20, 90)" → { templateId: "tempo", params: [20, 90] }
165
+ */
166
+ export declare function parseWorkoutRef(ref: string): ParsedWorkoutRef;
167
+ /**
168
+ * Parse a week range string into an array of week numbers.
169
+ *
170
+ * Examples:
171
+ * "1-3" → [1, 2, 3]
172
+ * "4-6" → [4, 5, 6]
173
+ * [1, 2, 3] → [1, 2, 3] (passthrough)
174
+ */
175
+ export declare function parseWeekRange(weeks: string | number[]): number[];
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Compact Training Plan Schema
3
+ *
4
+ * A minimal schema for AI models to generate training plans.
5
+ * The compact format is expanded to the full format for HTML rendering.
6
+ *
7
+ * Key design principles:
8
+ * - Minimal output for models to generate
9
+ * - Template references instead of full workout definitions
10
+ * - Athlete paces/zones as inputs, system calculates ranges
11
+ */
12
+ /**
13
+ * Parse a workout reference string into its components.
14
+ *
15
+ * Examples:
16
+ * "easy(30)" → { templateId: "easy", params: [30] }
17
+ * "intervals.400(6)" → { templateId: "intervals.400", params: [6] }
18
+ * "rest" → { templateId: "rest", params: [] }
19
+ * "tempo(20, 90)" → { templateId: "tempo", params: [20, 90] }
20
+ */
21
+ export function parseWorkoutRef(ref) {
22
+ const match = ref.match(/^([a-zA-Z0-9_.]+)(?:\(([^)]*)\))?$/);
23
+ if (!match) {
24
+ throw new Error(`Invalid workout reference: "${ref}"`);
25
+ }
26
+ const templateId = match[1];
27
+ const paramsStr = match[2];
28
+ let params = [];
29
+ if (paramsStr) {
30
+ params = paramsStr.split(",").map((p) => {
31
+ const trimmed = p.trim();
32
+ const num = Number(trimmed);
33
+ return isNaN(num) ? trimmed : num;
34
+ });
35
+ }
36
+ return { templateId, params };
37
+ }
38
+ /**
39
+ * Parse a week range string into an array of week numbers.
40
+ *
41
+ * Examples:
42
+ * "1-3" → [1, 2, 3]
43
+ * "4-6" → [4, 5, 6]
44
+ * [1, 2, 3] → [1, 2, 3] (passthrough)
45
+ */
46
+ export function parseWeekRange(weeks) {
47
+ if (Array.isArray(weeks)) {
48
+ return weeks;
49
+ }
50
+ const match = weeks.match(/^(\d+)-(\d+)$/);
51
+ if (!match) {
52
+ throw new Error(`Invalid week range: "${weeks}"`);
53
+ }
54
+ const start = parseInt(match[1], 10);
55
+ const end = parseInt(match[2], 10);
56
+ if (start > end) {
57
+ throw new Error(`Invalid week range: start (${start}) > end (${end})`);
58
+ }
59
+ const result = [];
60
+ for (let i = start; i <= end; i++) {
61
+ result.push(i);
62
+ }
63
+ return result;
64
+ }