incremnt 0.1.11 → 0.1.13
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/package.json +1 -1
- package/src/format.js +14 -1
- package/src/lib.js +6 -2
- package/src/logo.js +76 -0
- package/src/openrouter.js +76 -12
- package/src/queries.js +108 -1
package/package.json
CHANGED
package/src/format.js
CHANGED
|
@@ -145,6 +145,14 @@ function formatSessionShow(payload) {
|
|
|
145
145
|
lines.push(keyValue('Calories', `${payload.activeCalories} kcal`));
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
if (payload.aiCoachNotes) {
|
|
149
|
+
lines.push('');
|
|
150
|
+
lines.push(` ${chalk.bold('AI Summary')}`);
|
|
151
|
+
for (const line of payload.aiCoachNotes.split('\n')) {
|
|
152
|
+
lines.push(` ${line}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
148
156
|
return lines.join('\n');
|
|
149
157
|
}
|
|
150
158
|
|
|
@@ -462,11 +470,16 @@ function formatProposalDismissed(payload) {
|
|
|
462
470
|
|
|
463
471
|
// --- Main export ---
|
|
464
472
|
|
|
465
|
-
export function formatHelp() {
|
|
473
|
+
export function formatHelp(opts = {}) {
|
|
466
474
|
const cmd = (name, desc) => ` ${chalk.bold(name.padEnd(38))} ${chalk.dim(desc)}`;
|
|
475
|
+
const authBadge = opts.isAuthenticated
|
|
476
|
+
? chalk.green('\u25cf Authenticated')
|
|
477
|
+
: chalk.yellow("\u25cf Authentication required - run 'incremnt login'");
|
|
478
|
+
|
|
467
479
|
const lines = [
|
|
468
480
|
'',
|
|
469
481
|
header('INCREMNT') + dimDot() + chalk.dim('strength tracking CLI'),
|
|
482
|
+
` ${authBadge}`,
|
|
470
483
|
'',
|
|
471
484
|
header('USAGE'),
|
|
472
485
|
` incremnt <command> [options]`,
|
package/src/lib.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { clearSessionState, isSessionExpired, readSessionState, resolveConfigDir } from './state.js';
|
|
13
13
|
import { createTransport } from './transport.js';
|
|
14
14
|
import { formatPretty, formatHelp } from './format.js';
|
|
15
|
+
import { printLogo } from './logo.js';
|
|
15
16
|
|
|
16
17
|
function parseArgs(argv) {
|
|
17
18
|
const commandTokens = [];
|
|
@@ -66,12 +67,15 @@ export async function runCli(argv, stdout, stderr) {
|
|
|
66
67
|
dismiss: 'proposal-dismiss'
|
|
67
68
|
})[command] ?? command;
|
|
68
69
|
|
|
70
|
+
const sessionState = await readSessionState();
|
|
71
|
+
const isAuthenticated = Boolean(sessionState?.session && !isSessionExpired(sessionState.session));
|
|
72
|
+
|
|
69
73
|
if (!command || options.help) {
|
|
70
|
-
stdout
|
|
74
|
+
await printLogo(stdout);
|
|
75
|
+
stdout.write(`${formatHelp({ isAuthenticated })}\n`);
|
|
71
76
|
return 0;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
const sessionState = await readSessionState();
|
|
75
79
|
const transport = await createTransport(options, sessionState);
|
|
76
80
|
|
|
77
81
|
if (normalizedCommand === 'status') {
|
package/src/logo.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
const startColor = { r: 0, g: 255, b: 163 }; // #00ffa3
|
|
4
|
+
const endColor = { r: 59, g: 130, b: 246 }; // #3b82f6
|
|
5
|
+
|
|
6
|
+
function interpolateColor(start, end, factor) {
|
|
7
|
+
const r = Math.round(start.r + (end.r - start.r) * factor);
|
|
8
|
+
const g = Math.round(start.g + (end.g - start.g) * factor);
|
|
9
|
+
const b = Math.round(start.b + (end.b - start.b) * factor);
|
|
10
|
+
return { r, g, b };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Extracted from figlet 'ANSI Regular' output for 'INCREMNT'
|
|
14
|
+
// We use '#' in source so the ASCII art stays ASCII-only and easy to edit,
|
|
15
|
+
// then render as '█' block characters at runtime.
|
|
16
|
+
const rawLines = [
|
|
17
|
+
'## ### ## ###### ###### ####### ### ### ### ## ######## ',
|
|
18
|
+
'## #### ## ## ## ## ## #### #### #### ## ## ',
|
|
19
|
+
'## ## ## ## ## ###### ##### ## #### ## ## ## ## ## ',
|
|
20
|
+
'## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ',
|
|
21
|
+
'## ## #### ###### ## ## ####### ## ## ## #### ## '
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// 3D Drop Shadow offset exactly like Gemini CLI
|
|
25
|
+
const SHADOW_OFFSET_X = 1;
|
|
26
|
+
const SHADOW_OFFSET_Y = 1;
|
|
27
|
+
|
|
28
|
+
const totalLines = 5 + SHADOW_OFFSET_Y;
|
|
29
|
+
const lineLength = rawLines[0].length + SHADOW_OFFSET_X;
|
|
30
|
+
|
|
31
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
|
+
|
|
33
|
+
export async function printLogo(stdout = process.stdout) {
|
|
34
|
+
// Gradient starts at the 'M' character position
|
|
35
|
+
// I: 0, N: 3, C: 14, R: 21, E: 29, M: 37
|
|
36
|
+
const gradientStartIdx = 37;
|
|
37
|
+
|
|
38
|
+
stdout.write('\n');
|
|
39
|
+
for (let r = 0; r < totalLines; r++) {
|
|
40
|
+
let lineStr = '';
|
|
41
|
+
|
|
42
|
+
for (let c = 0; c < lineLength; c++) {
|
|
43
|
+
const fgY = r;
|
|
44
|
+
const fgX = c;
|
|
45
|
+
const hasFg = (fgY >= 0 && fgY < 5 && fgX >= 0 && fgX < rawLines[0].length && rawLines[fgY][fgX] === '#');
|
|
46
|
+
|
|
47
|
+
const bgY = r - SHADOW_OFFSET_Y;
|
|
48
|
+
const bgX = c - SHADOW_OFFSET_X;
|
|
49
|
+
const hasBg = (bgY >= 0 && bgY < 5 && bgX >= 0 && bgX < rawLines[0].length && rawLines[bgY][bgX] === '#');
|
|
50
|
+
|
|
51
|
+
if (hasFg) {
|
|
52
|
+
if (fgX < gradientStartIdx) {
|
|
53
|
+
lineStr += chalk.white('█');
|
|
54
|
+
} else {
|
|
55
|
+
const factor = Math.max(0, Math.min(1, (fgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
|
|
56
|
+
const col = interpolateColor(startColor, endColor, factor);
|
|
57
|
+
lineStr += chalk.rgb(col.r, col.g, col.b)('█');
|
|
58
|
+
}
|
|
59
|
+
} else if (hasBg) {
|
|
60
|
+
const shadowFactor = 0.35;
|
|
61
|
+
if (bgX < gradientStartIdx) {
|
|
62
|
+
lineStr += chalk.rgb(Math.round(255 * shadowFactor), Math.round(255 * shadowFactor), Math.round(255 * shadowFactor))('█');
|
|
63
|
+
} else {
|
|
64
|
+
const factor = Math.max(0, Math.min(1, (bgX - gradientStartIdx) / (rawLines[0].length - gradientStartIdx - 2)));
|
|
65
|
+
const col = interpolateColor(startColor, endColor, factor);
|
|
66
|
+
lineStr += chalk.rgb(Math.round(col.r * shadowFactor), Math.round(col.g * shadowFactor), Math.round(col.b * shadowFactor))('█');
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
lineStr += ' ';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
stdout.write(lineStr + '\n');
|
|
73
|
+
await sleep(80);
|
|
74
|
+
}
|
|
75
|
+
stdout.write('\n');
|
|
76
|
+
}
|
package/src/openrouter.js
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
const DEFAULT_MODEL = 'meta-llama/llama-3.1-8b-instruct:free';
|
|
2
2
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
3
|
-
const DEFAULT_MAX_TOKENS =
|
|
3
|
+
const DEFAULT_MAX_TOKENS = 500;
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const CYCLE_SUMMARY_PROMPT = `You are a strength coach reviewing a trainee's completed training cycle (typically one week). Write 3-4 short paragraphs separated by blank lines.
|
|
6
|
+
|
|
7
|
+
Your job is to give a cycle-level review — not a session-by-session recap. The app already shows set completion rate, individual session breakdowns, and deload adjustments — do NOT repeat any of that. Synthesize across the cycle.
|
|
8
|
+
|
|
9
|
+
Cover these in order of relevance (skip any that don't apply):
|
|
10
|
+
1. Overall cycle assessment: was this a build/deload/peak week? Did volume and intensity match the intent? If it was a deload, don't flag low numbers as a problem.
|
|
11
|
+
2. Progression commentary: the app made auto-progression decisions listed below. Comment on whether they look right given the data.
|
|
12
|
+
3. Multi-cycle trends: if previous cycle data is provided, note meaningful trends. Don't force trends where there aren't any.
|
|
13
|
+
4. Goal progress: if the trainee has strength goals, comment on trajectory.
|
|
14
|
+
5. One concrete thing to watch or change next cycle. Be specific.
|
|
6
15
|
|
|
7
16
|
Rules:
|
|
8
|
-
-
|
|
17
|
+
- Only state what the data shows. Never claim how something "felt."
|
|
18
|
+
- Reference specific exercises, weights, and reps. Use numbers, not vague praise.
|
|
9
19
|
- If there are PRs, mention them matter-of-factly. Do not celebrate or inflate them.
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
- Never use
|
|
14
|
-
- Never use -ing clauses that add fake depth ("indicating progressive overload", "demonstrating strength gains").
|
|
20
|
+
- If exercises were swapped from the plan, note the pattern and ask about it if recurring.
|
|
21
|
+
- Write like a training partner looking at a logbook, not a motivational coach. Short sentences, no filler, no cheerleading. Questions are good.
|
|
22
|
+
- Never use words like: impressive, crushed, solid, demonstrating, significant, incredible, fantastic, highlighting, smooth, controlled.
|
|
23
|
+
- Never use -ing clauses that add fake depth.
|
|
15
24
|
- No bullet points or lists.`;
|
|
16
25
|
|
|
17
26
|
export async function generateCoachingSummary(cycleContext, { apiKey, model, timeoutMs } = {}) {
|
|
@@ -35,7 +44,7 @@ export async function generateCoachingSummary(cycleContext, { apiKey, model, tim
|
|
|
35
44
|
body: JSON.stringify({
|
|
36
45
|
model: resolvedModel,
|
|
37
46
|
messages: [
|
|
38
|
-
{ role: 'system', content:
|
|
47
|
+
{ role: 'system', content: CYCLE_SUMMARY_PROMPT },
|
|
39
48
|
{ role: 'user', content: userContent }
|
|
40
49
|
],
|
|
41
50
|
max_tokens: DEFAULT_MAX_TOKENS,
|
|
@@ -61,15 +70,30 @@ export async function generateCoachingSummary(cycleContext, { apiKey, model, tim
|
|
|
61
70
|
}
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
function formatCycleContext(ctx) {
|
|
73
|
+
export function formatCycleContext(ctx) {
|
|
74
|
+
const intentLabel = ctx.cycleIntent ? ` (${ctx.cycleIntent})` : '';
|
|
65
75
|
const lines = [
|
|
66
|
-
`Program: ${ctx.programName}, Week ${ctx.cycleNumber}, ${ctx.totalSessions} session(s).`
|
|
76
|
+
`Program: ${ctx.programName}, Week ${ctx.cycleNumber}${intentLabel}, ${ctx.totalSessions} session(s).`
|
|
67
77
|
];
|
|
68
78
|
|
|
79
|
+
if (ctx.setCompletionRate) {
|
|
80
|
+
const pct = ctx.setCompletionRate.planned > 0
|
|
81
|
+
? Math.round(ctx.setCompletionRate.completed / ctx.setCompletionRate.planned * 100)
|
|
82
|
+
: 0;
|
|
83
|
+
lines.push(`Completion: ${ctx.setCompletionRate.completed}/${ctx.setCompletionRate.planned} sets (${pct}%).`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (ctx.adaptationNote) {
|
|
87
|
+
lines.push(`Adaptation: ${ctx.adaptationNote}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('Sessions:');
|
|
69
92
|
for (const session of ctx.sessions) {
|
|
70
93
|
const parts = [session.dayName || 'Session'];
|
|
71
94
|
if (session.totalVolume) parts.push(`${session.totalVolume} kg volume`);
|
|
72
95
|
if (session.effortScore) parts.push(`effort ${session.effortScore}/10`);
|
|
96
|
+
if (session.averageHeartRate) parts.push(`avg HR ${session.averageHeartRate}`);
|
|
73
97
|
lines.push(parts.join(', '));
|
|
74
98
|
|
|
75
99
|
for (const ex of session.exercises) {
|
|
@@ -84,19 +108,59 @@ function formatCycleContext(ctx) {
|
|
|
84
108
|
}
|
|
85
109
|
|
|
86
110
|
if (ctx.prsThisCycle.length > 0) {
|
|
87
|
-
lines.push('
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push('PRs this cycle:');
|
|
88
113
|
for (const pr of ctx.prsThisCycle) {
|
|
89
114
|
lines.push(` ${pr.exerciseName}: ${pr.weight}×${pr.reps} (e1RM ${pr.estimatedOneRM})`);
|
|
90
115
|
}
|
|
91
116
|
}
|
|
92
117
|
|
|
118
|
+
if (ctx.progressionDecisions?.length > 0) {
|
|
119
|
+
lines.push('');
|
|
120
|
+
lines.push('Progression decisions made by the app:');
|
|
121
|
+
for (const pd of ctx.progressionDecisions) {
|
|
122
|
+
const detail = pd.detail ? ` (${pd.detail})` : '';
|
|
123
|
+
const kind = pd.kind ? `[${pd.kind}] ` : '';
|
|
124
|
+
lines.push(` ${pd.exerciseName}: ${kind}${pd.action}${detail}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (ctx.swapPatterns?.length > 0) {
|
|
129
|
+
lines.push('');
|
|
130
|
+
lines.push('Exercise swaps:');
|
|
131
|
+
for (const sp of ctx.swapPatterns) {
|
|
132
|
+
lines.push(` ${sp.original} → ${sp.replacement} (${sp.count} of ${ctx.totalSessions} sessions)`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
93
136
|
if (ctx.goalProgress) {
|
|
137
|
+
lines.push('');
|
|
94
138
|
lines.push('Goal progress:');
|
|
95
139
|
for (const g of ctx.goalProgress) {
|
|
96
140
|
lines.push(` ${g.exerciseName}: ${g.progressPercent ?? '?'}% toward ${g.targetE1RM} e1RM`);
|
|
97
141
|
}
|
|
98
142
|
}
|
|
99
143
|
|
|
144
|
+
if (ctx.previousCycles?.length > 0) {
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push('Previous cycles:');
|
|
147
|
+
for (const pc of ctx.previousCycles) {
|
|
148
|
+
const summaryLine = pc.previousAISummary
|
|
149
|
+
? `\n Coach noted: "${pc.previousAISummary.split('\n')[0].slice(0, 120)}"`
|
|
150
|
+
: '';
|
|
151
|
+
lines.push(` Week ${pc.weekNumber}: ${pc.sessionCount} sessions, ${pc.totalVolume} kg total volume${summaryLine}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (ctx.exerciseTrends?.length > 0) {
|
|
156
|
+
lines.push('');
|
|
157
|
+
lines.push('Exercise trends (last 3 cycles):');
|
|
158
|
+
for (const et of ctx.exerciseTrends) {
|
|
159
|
+
const trendStr = et.trend.map((t) => t.e1RM).join(' → ');
|
|
160
|
+
lines.push(` ${et.exerciseName} e1RM: ${trendStr}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
100
164
|
return lines.join('\n');
|
|
101
165
|
}
|
|
102
166
|
|
package/src/queries.js
CHANGED
|
@@ -452,6 +452,10 @@ export function cycleSummaryContext(snapshot, programId) {
|
|
|
452
452
|
|
|
453
453
|
if (cycleSessions.length === 0) return null;
|
|
454
454
|
|
|
455
|
+
const firstSession = cycleSessions[0];
|
|
456
|
+
const cycleIntent = firstSession?.historicalContext?.programProgressionType ?? null;
|
|
457
|
+
const adaptationNote = firstSession?.historicalContext?.latestAdaptationSummary ?? null;
|
|
458
|
+
|
|
455
459
|
const priorSessions = (snapshot.sessions ?? []).filter(
|
|
456
460
|
(s) =>
|
|
457
461
|
s.programId === program.id &&
|
|
@@ -528,6 +532,89 @@ export function cycleSummaryContext(snapshot, programId) {
|
|
|
528
532
|
};
|
|
529
533
|
});
|
|
530
534
|
|
|
535
|
+
// Group prior sessions by week number for multi-cycle data
|
|
536
|
+
const sessionsByWeek = new Map();
|
|
537
|
+
for (const s of priorSessions) {
|
|
538
|
+
const wk = s.historicalContext?.programWeekNumber;
|
|
539
|
+
if (wk == null) continue;
|
|
540
|
+
if (!sessionsByWeek.has(wk)) sessionsByWeek.set(wk, []);
|
|
541
|
+
sessionsByWeek.get(wk).push(s);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Build previous cycle summaries (last 2 weeks before current, most recent first)
|
|
545
|
+
const priorWeeks = [...sessionsByWeek.keys()].sort((a, b) => b - a).slice(0, 2);
|
|
546
|
+
const cycleSummariesArr = snapshot.cycleSummaries ?? [];
|
|
547
|
+
const programCycleSummaries = cycleSummariesArr
|
|
548
|
+
.filter((cs) => cs.programId === program.id)
|
|
549
|
+
.sort((a, b) => String(b.completedDate).localeCompare(String(a.completedDate)));
|
|
550
|
+
|
|
551
|
+
const previousCycles = priorWeeks.map((wk, idx) => {
|
|
552
|
+
const weekSessions = sessionsByWeek.get(wk);
|
|
553
|
+
const totalVolume = weekSessions.reduce((sum, s) => sum + (s.summary?.totalVolume ?? s.volume ?? 0), 0);
|
|
554
|
+
const matchingCycleSummary = programCycleSummaries[idx] ?? null;
|
|
555
|
+
|
|
556
|
+
return {
|
|
557
|
+
weekNumber: wk,
|
|
558
|
+
sessionCount: weekSessions.length,
|
|
559
|
+
totalVolume: Math.round(totalVolume),
|
|
560
|
+
previousAISummary: matchingCycleSummary?.aiSummary ?? null
|
|
561
|
+
};
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// Build exercise e1RM trends across last 3 cycles (including current)
|
|
565
|
+
const currentExerciseKeys = new Set();
|
|
566
|
+
const exerciseDisplayNames = new Map();
|
|
567
|
+
for (const s of cycleSessions) {
|
|
568
|
+
for (const ex of s.exercises ?? []) {
|
|
569
|
+
const key = normalizeExerciseName(ex.name);
|
|
570
|
+
currentExerciseKeys.add(key);
|
|
571
|
+
if (!exerciseDisplayNames.has(key)) exerciseDisplayNames.set(key, ex.name);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const allWeeks = [...new Set([...priorWeeks, cycleWeekNumber])].sort((a, b) => a - b);
|
|
576
|
+
const trendWeeks = allWeeks.slice(-3);
|
|
577
|
+
const allSessions = [...priorSessions, ...cycleSessions];
|
|
578
|
+
|
|
579
|
+
const exerciseTrends = [];
|
|
580
|
+
for (const exKey of currentExerciseKeys) {
|
|
581
|
+
const trend = [];
|
|
582
|
+
for (const wk of trendWeeks) {
|
|
583
|
+
let bestE1RM = 0;
|
|
584
|
+
for (const s of allSessions) {
|
|
585
|
+
if ((s.historicalContext?.programWeekNumber ?? 0) !== wk) continue;
|
|
586
|
+
for (const ex of s.exercises ?? []) {
|
|
587
|
+
if (normalizeExerciseName(ex.name) !== exKey) continue;
|
|
588
|
+
for (const set of ex.sets ?? []) {
|
|
589
|
+
if (!set.isComplete) continue;
|
|
590
|
+
const score = Number(set.weight) * (1 + Number(set.reps) / 30);
|
|
591
|
+
if (score > bestE1RM) bestE1RM = score;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
if (bestE1RM > 0) {
|
|
596
|
+
trend.push({ week: wk, e1RM: Math.round(bestE1RM * 10) / 10 });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (trend.length >= 2) {
|
|
600
|
+
exerciseTrends.push({ exerciseName: exerciseDisplayNames.get(exKey), trend });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const swapCounts = new Map();
|
|
605
|
+
for (const session of cycleSessions) {
|
|
606
|
+
for (const exercise of session.exercises ?? []) {
|
|
607
|
+
if (exercise.swappedFrom) {
|
|
608
|
+
const key = `${exercise.swappedFrom}\u2192${exercise.name}`;
|
|
609
|
+
swapCounts.set(key, (swapCounts.get(key) ?? 0) + 1);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
const swapPatterns = [...swapCounts.entries()].map(([key, count]) => {
|
|
614
|
+
const [original, replacement] = key.split('\u2192');
|
|
615
|
+
return { original, replacement, count };
|
|
616
|
+
});
|
|
617
|
+
|
|
531
618
|
let goalProgress = null;
|
|
532
619
|
const plans = snapshot.strengthPlans ?? [];
|
|
533
620
|
const activePlan = plans.find(
|
|
@@ -548,13 +635,33 @@ export function cycleSummaryContext(snapshot, programId) {
|
|
|
548
635
|
});
|
|
549
636
|
}
|
|
550
637
|
|
|
638
|
+
const matchingSummary = programCycleSummaries[0] ?? null;
|
|
639
|
+
|
|
640
|
+
const progressionDecisions = (matchingSummary?.progressionUpdates ?? []).map((u) => ({
|
|
641
|
+
exerciseName: u.exerciseName,
|
|
642
|
+
action: u.action ?? u.change ?? null,
|
|
643
|
+
detail: u.detail ?? null,
|
|
644
|
+
kind: u.kind ?? null
|
|
645
|
+
}));
|
|
646
|
+
|
|
647
|
+
const setCompletionRate = matchingSummary
|
|
648
|
+
? { completed: matchingSummary.totalSetsCompleted ?? 0, planned: matchingSummary.totalSetsPlanned ?? 0 }
|
|
649
|
+
: null;
|
|
650
|
+
|
|
551
651
|
return {
|
|
552
652
|
programName: program.name,
|
|
553
653
|
cycleNumber: cycleWeekNumber,
|
|
554
654
|
totalSessions: cycleSessions.length,
|
|
555
655
|
sessions,
|
|
556
656
|
prsThisCycle,
|
|
557
|
-
goalProgress
|
|
657
|
+
goalProgress,
|
|
658
|
+
progressionDecisions,
|
|
659
|
+
setCompletionRate,
|
|
660
|
+
swapPatterns,
|
|
661
|
+
cycleIntent,
|
|
662
|
+
adaptationNote,
|
|
663
|
+
previousCycles,
|
|
664
|
+
exerciseTrends
|
|
558
665
|
};
|
|
559
666
|
}
|
|
560
667
|
|