incremnt 0.3.0 → 0.5.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.
- package/README.md +9 -2
- package/package.json +25 -4
- package/src/anonymize.js +12 -0
- package/src/coach-bakeoff.js +300 -0
- package/src/coach-facts.js +100 -0
- package/src/coach-prompt-variants.js +106 -0
- package/src/contract.js +56 -1
- package/src/exercise-aliases.js +163 -0
- package/src/format.js +64 -1
- package/src/increment-score-replay-data.js +486 -0
- package/src/increment-score-replay.js +822 -0
- package/src/lib.js +14 -2
- package/src/local.js +3 -3
- package/src/openrouter.js +1033 -179
- package/src/program-phase-resolver.js +206 -0
- package/src/prompt-security.js +13 -0
- package/src/promptfoo-domain-assert.cjs +4 -0
- package/src/promptfoo-evals.js +166 -0
- package/src/promptfoo-langfuse-scores.js +354 -0
- package/src/promptfoo-provider.cjs +14 -0
- package/src/promptfoo-tests.cjs +4 -0
- package/src/queries.js +2307 -164
- package/src/remote.js +144 -1
- package/src/state.js +9 -2
- package/src/stored-summary-eval-report.js +171 -0
- package/src/summary-evals.js +1445 -0
- package/src/sync-service.js +1557 -158
- package/src/workout-prompt-variants.js +52 -0
|
@@ -0,0 +1,822 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export const INCREMENT_SCORE_REPLAY_VERSION = 'increment-score-replay-v0.1';
|
|
4
|
+
export const PROVISIONAL_FORMULA_VERSION = 'increment-score-provisional-v0.1';
|
|
5
|
+
|
|
6
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
7
|
+
const SCORE_WEIGHTS = Object.freeze({
|
|
8
|
+
stimulus: 0.4,
|
|
9
|
+
progression: 0.2,
|
|
10
|
+
recovery: 0.2,
|
|
11
|
+
coverage: 0.15,
|
|
12
|
+
execution: 0.05
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const TARGET_SETS = Object.freeze({
|
|
16
|
+
chest: { label: 'Chest', target: 8 },
|
|
17
|
+
back: { label: 'Back', target: 8 },
|
|
18
|
+
quads: { label: 'Quads', target: 6 },
|
|
19
|
+
posterior: { label: 'Posterior', target: 6 },
|
|
20
|
+
shoulders: { label: 'Shoulders', target: 6 },
|
|
21
|
+
arms: { label: 'Arms', target: 6 }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const EXPORTED_REPLAY_INPUT_KIND = 'incrementScoreReplayInput';
|
|
25
|
+
|
|
26
|
+
const MUSCLE_ALIASES = new Map([
|
|
27
|
+
['chest', 'chest'],
|
|
28
|
+
['back', 'back'],
|
|
29
|
+
['lats', 'back'],
|
|
30
|
+
['upper back', 'back'],
|
|
31
|
+
['quads', 'quads'],
|
|
32
|
+
['quadriceps', 'quads'],
|
|
33
|
+
['legs', 'quads'],
|
|
34
|
+
['hamstrings', 'posterior'],
|
|
35
|
+
['glutes', 'posterior'],
|
|
36
|
+
['posterior chain', 'posterior'],
|
|
37
|
+
['shoulders', 'shoulders'],
|
|
38
|
+
['delts', 'shoulders'],
|
|
39
|
+
['biceps', 'arms'],
|
|
40
|
+
['triceps', 'arms'],
|
|
41
|
+
['arms', 'arms']
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
function clamp(value, min = 0, max = 100) {
|
|
45
|
+
if (!Number.isFinite(value)) return min;
|
|
46
|
+
return Math.max(min, Math.min(max, value));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function round(value, places = 0) {
|
|
50
|
+
const scale = 10 ** places;
|
|
51
|
+
return Math.round(value * scale) / scale;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseDateKey(value) {
|
|
55
|
+
if (!value) return null;
|
|
56
|
+
const raw = String(value);
|
|
57
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(raw)) return raw.slice(0, 10);
|
|
58
|
+
const parsed = new Date(raw);
|
|
59
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString().slice(0, 10);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function dateFromKey(dateKey) {
|
|
63
|
+
return new Date(`${dateKey}T00:00:00.000Z`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function addDays(dateKey, days) {
|
|
67
|
+
const date = dateFromKey(dateKey);
|
|
68
|
+
date.setUTCDate(date.getUTCDate() + days);
|
|
69
|
+
return date.toISOString().slice(0, 10);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function daysBetween(startDateKey, endDateKey) {
|
|
73
|
+
return Math.round((dateFromKey(endDateKey) - dateFromKey(startDateKey)) / MS_PER_DAY);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function dateRange(startDateKey, endDateKey) {
|
|
77
|
+
const dates = [];
|
|
78
|
+
for (let cursor = startDateKey; cursor <= endDateKey; cursor = addDays(cursor, 1)) {
|
|
79
|
+
dates.push(cursor);
|
|
80
|
+
}
|
|
81
|
+
return dates;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function weekStart(dateKey) {
|
|
85
|
+
const date = dateFromKey(dateKey);
|
|
86
|
+
const day = date.getUTCDay() || 7;
|
|
87
|
+
date.setUTCDate(date.getUTCDate() - day + 1);
|
|
88
|
+
return date.toISOString().slice(0, 10);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function weekEnd(dateKey) {
|
|
92
|
+
return addDays(weekStart(dateKey), 6);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function completedAtForSession(session) {
|
|
96
|
+
return session.completedAt ?? session.summary?.date ?? session.date ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeExerciseName(name) {
|
|
100
|
+
return String(name ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function normalizeMuscleGroup(value) {
|
|
104
|
+
const normalized = String(value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
|
|
105
|
+
return MUSCLE_ALIASES.get(normalized) ?? normalized;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function e1rmForSet(set) {
|
|
109
|
+
const weight = Number(set?.weight);
|
|
110
|
+
const reps = Number(set?.reps);
|
|
111
|
+
if (!Number.isFinite(weight) || !Number.isFinite(reps) || reps <= 0) return null;
|
|
112
|
+
if (weight <= 0) return reps;
|
|
113
|
+
return weight * (1 + reps / 30);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function completedSetsForExercise(exercise) {
|
|
117
|
+
return (exercise.sets ?? [])
|
|
118
|
+
.filter((set) => set?.isComplete !== false)
|
|
119
|
+
.map((set) => ({
|
|
120
|
+
weight: Number(set.weight ?? 0),
|
|
121
|
+
reps: Number(set.reps ?? 0),
|
|
122
|
+
rir: set.rir ?? exercise.rir ?? null,
|
|
123
|
+
estimatedOneRM: e1rmForSet(set)
|
|
124
|
+
}))
|
|
125
|
+
.filter((set) => set.reps > 0 && set.estimatedOneRM != null);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function completedSetsForExportedExercise(exercise) {
|
|
129
|
+
return (exercise.sets ?? [])
|
|
130
|
+
.filter((set) => set?.isComplete !== false)
|
|
131
|
+
.map((set) => {
|
|
132
|
+
const normalized = {
|
|
133
|
+
weight: Number(set.weightKg ?? set.weight ?? 0),
|
|
134
|
+
reps: Number(set.reps ?? 0),
|
|
135
|
+
rir: set.rir ?? null
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
...normalized,
|
|
139
|
+
estimatedOneRM: e1rmForSet(normalized)
|
|
140
|
+
};
|
|
141
|
+
})
|
|
142
|
+
.filter((set) => set.reps > 0 && set.estimatedOneRM != null);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function exportedExerciseName(exercise) {
|
|
146
|
+
if (exercise.customExercise) {
|
|
147
|
+
return exercise.customExerciseHash
|
|
148
|
+
? `Custom exercise ${String(exercise.customExerciseHash).slice(0, 8)}`
|
|
149
|
+
: 'Custom exercise';
|
|
150
|
+
}
|
|
151
|
+
return exercise.canonicalName ?? exercise.name ?? exercise.exerciseName ?? 'Unknown Exercise';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function normalizeExportedReplayInput(payload, options = {}) {
|
|
155
|
+
const sessions = (payload?.sessions ?? [])
|
|
156
|
+
.map((session) => {
|
|
157
|
+
const date = parseDateKey(session.date ?? session.completedAt);
|
|
158
|
+
if (!date) return null;
|
|
159
|
+
|
|
160
|
+
const plannedSetCounts = new Map((session.prescription?.plannedExercises ?? [])
|
|
161
|
+
.map((exercise) => [
|
|
162
|
+
exercise.canonicalName ?? exercise.exerciseName ?? exercise.name,
|
|
163
|
+
Number(exercise.plannedSets ?? 0)
|
|
164
|
+
])
|
|
165
|
+
.filter(([key, count]) => key && Number.isFinite(count) && count > 0));
|
|
166
|
+
|
|
167
|
+
const exercises = (session.exercises ?? []).map((exercise) => {
|
|
168
|
+
const primaryMuscle = normalizeMuscleGroup(exercise.muscleGroup);
|
|
169
|
+
const name = exportedExerciseName(exercise);
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
exerciseKey: String(exercise.canonicalName ?? exercise.customExerciseHash ?? normalizeExerciseName(name)),
|
|
173
|
+
primaryMuscle,
|
|
174
|
+
muscleLabel: TARGET_SETS[primaryMuscle]?.label ?? exercise.muscleGroup ?? primaryMuscle ?? 'Unknown',
|
|
175
|
+
completedSets: completedSetsForExportedExercise(exercise),
|
|
176
|
+
plannedSetCount: plannedSetCounts.get(exercise.canonicalName) ?? (Array.isArray(exercise.sets) ? exercise.sets.length : null)
|
|
177
|
+
};
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const completedSetCount = exercises.reduce((sum, exercise) => sum + exercise.completedSets.length, 0);
|
|
181
|
+
const plannedSetCount = exercises.reduce((sum, exercise) => {
|
|
182
|
+
if (exercise.plannedSetCount == null) return sum;
|
|
183
|
+
return sum + exercise.plannedSetCount;
|
|
184
|
+
}, 0);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
sessionId: session.id ?? null,
|
|
188
|
+
date,
|
|
189
|
+
completedAt: session.completedAt ?? session.date ?? null,
|
|
190
|
+
dayName: null,
|
|
191
|
+
completedSetCount,
|
|
192
|
+
plannedSetCount,
|
|
193
|
+
emptyStrengthSession: completedSetCount === 0,
|
|
194
|
+
exercises
|
|
195
|
+
};
|
|
196
|
+
})
|
|
197
|
+
.filter(Boolean)
|
|
198
|
+
.sort((left, right) => String(left.completedAt ?? left.date).localeCompare(String(right.completedAt ?? right.date)));
|
|
199
|
+
|
|
200
|
+
const dates = sessions.map((session) => session.date);
|
|
201
|
+
const fallbackDate = parseDateKey(options.asOf) ?? new Date().toISOString().slice(0, 10);
|
|
202
|
+
const from = options.from ?? payload?.replayWindow?.startDate ?? dates[0] ?? fallbackDate;
|
|
203
|
+
const to = options.to ?? payload?.replayWindow?.endDate ?? dates.at(-1) ?? from;
|
|
204
|
+
const accountAlias = payload?.source?.accountAlias;
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
replayVersion: INCREMENT_SCORE_REPLAY_VERSION,
|
|
208
|
+
formulaVersion: options.formulaVersion ?? payload?.formulaVersion ?? PROVISIONAL_FORMULA_VERSION,
|
|
209
|
+
source: options.source ?? payload?.source?.type ?? 'replay-input',
|
|
210
|
+
extractedAt: payload?.extractedAt ?? new Date().toISOString(),
|
|
211
|
+
user: {
|
|
212
|
+
label: options.userLabel ?? accountAlias ?? 'User',
|
|
213
|
+
ref: payload?.source?.accountHash ?? null
|
|
214
|
+
},
|
|
215
|
+
sessions,
|
|
216
|
+
healthMetrics: payload?.healthMetrics ?? {},
|
|
217
|
+
from,
|
|
218
|
+
to
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function normalizedIncrementScoreReplayInput(snapshot, options = {}) {
|
|
223
|
+
if (snapshot?.replayVersion === INCREMENT_SCORE_REPLAY_VERSION) return snapshot;
|
|
224
|
+
if (snapshot?.kind === EXPORTED_REPLAY_INPUT_KIND) return normalizeExportedReplayInput(snapshot, options);
|
|
225
|
+
|
|
226
|
+
const sessions = (snapshot?.sessions ?? [])
|
|
227
|
+
.map((session) => {
|
|
228
|
+
const date = parseDateKey(completedAtForSession(session));
|
|
229
|
+
if (!date) return null;
|
|
230
|
+
|
|
231
|
+
const exercises = (session.exercises ?? []).map((exercise) => {
|
|
232
|
+
const primaryMuscle = normalizeMuscleGroup(
|
|
233
|
+
exercise.muscleGroup ?? exercise.primaryMuscleGroup ?? exercise.primaryMuscle
|
|
234
|
+
);
|
|
235
|
+
return {
|
|
236
|
+
name: exercise.name ?? exercise.exerciseName ?? 'Unknown Exercise',
|
|
237
|
+
exerciseKey: normalizeExerciseName(exercise.name ?? exercise.exerciseName),
|
|
238
|
+
primaryMuscle,
|
|
239
|
+
muscleLabel: TARGET_SETS[primaryMuscle]?.label ?? exercise.muscleGroup ?? primaryMuscle ?? 'Unknown',
|
|
240
|
+
completedSets: completedSetsForExercise(exercise),
|
|
241
|
+
plannedSetCount: Array.isArray(exercise.sets) ? exercise.sets.length : null
|
|
242
|
+
};
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const completedSetCount = exercises.reduce((sum, exercise) => sum + exercise.completedSets.length, 0);
|
|
246
|
+
const plannedSetCount = exercises.reduce((sum, exercise) => {
|
|
247
|
+
if (exercise.plannedSetCount == null) return sum;
|
|
248
|
+
return sum + exercise.plannedSetCount;
|
|
249
|
+
}, 0);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
sessionId: session.id ?? null,
|
|
253
|
+
date,
|
|
254
|
+
completedAt: completedAtForSession(session),
|
|
255
|
+
dayName: session.dayName ?? session.summary?.dayName ?? null,
|
|
256
|
+
completedSetCount,
|
|
257
|
+
plannedSetCount,
|
|
258
|
+
emptyStrengthSession: completedSetCount === 0,
|
|
259
|
+
exercises
|
|
260
|
+
};
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
.sort((left, right) => String(left.completedAt ?? left.date).localeCompare(String(right.completedAt ?? right.date)));
|
|
264
|
+
|
|
265
|
+
const dates = sessions.map((session) => session.date);
|
|
266
|
+
const fallbackDate = parseDateKey(options.asOf) ?? new Date().toISOString().slice(0, 10);
|
|
267
|
+
const from = options.from ?? dates[0] ?? fallbackDate;
|
|
268
|
+
const to = options.to ?? dates.at(-1) ?? from;
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
replayVersion: INCREMENT_SCORE_REPLAY_VERSION,
|
|
272
|
+
formulaVersion: options.formulaVersion ?? PROVISIONAL_FORMULA_VERSION,
|
|
273
|
+
source: options.source ?? 'snapshot',
|
|
274
|
+
extractedAt: options.extractedAt ?? new Date().toISOString(),
|
|
275
|
+
user: redactUser(snapshot, options),
|
|
276
|
+
sessions,
|
|
277
|
+
healthMetrics: snapshot?.healthMetrics ?? {},
|
|
278
|
+
from,
|
|
279
|
+
to
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function redactUser(snapshot, options) {
|
|
284
|
+
const explicitLabel = options.userLabel ?? snapshot?.replayUserLabel ?? snapshot?.userLabel ?? 'User';
|
|
285
|
+
const rawId = snapshot?.account?.id ?? snapshot?.accountId ?? snapshot?.userId ?? null;
|
|
286
|
+
return {
|
|
287
|
+
label: explicitLabel,
|
|
288
|
+
ref: rawId ? createHash('sha256').update(String(rawId)).digest('hex').slice(0, 10) : null
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function sessionsInWindow(sessions, startDate, endDate) {
|
|
293
|
+
return sessions.filter((session) => session.date >= startDate && session.date <= endDate);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function healthReadingsInWindow(metrics, key, startDate, endDate) {
|
|
297
|
+
return (metrics?.[key] ?? []).filter((reading) => {
|
|
298
|
+
const date = parseDateKey(reading.date);
|
|
299
|
+
return date && date >= startDate && date <= endDate;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function buildProgressionEvents(sessions) {
|
|
304
|
+
const bestByExercise = new Map();
|
|
305
|
+
const groupedByDate = new Map();
|
|
306
|
+
|
|
307
|
+
for (const session of sessions) {
|
|
308
|
+
if (!groupedByDate.has(session.date)) groupedByDate.set(session.date, []);
|
|
309
|
+
groupedByDate.get(session.date).push(session);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const eventsByDate = new Map();
|
|
313
|
+
for (const date of [...groupedByDate.keys()].sort()) {
|
|
314
|
+
const events = [];
|
|
315
|
+
const sessionsForDate = groupedByDate.get(date);
|
|
316
|
+
|
|
317
|
+
for (const session of sessionsForDate) {
|
|
318
|
+
const eventKeys = new Set();
|
|
319
|
+
for (const exercise of session.exercises) {
|
|
320
|
+
for (const set of exercise.completedSets) {
|
|
321
|
+
const previousBest = bestByExercise.get(exercise.exerciseKey);
|
|
322
|
+
if (previousBest != null && set.estimatedOneRM > previousBest * 1.01 && !eventKeys.has(exercise.exerciseKey)) {
|
|
323
|
+
eventKeys.add(exercise.exerciseKey);
|
|
324
|
+
events.push({
|
|
325
|
+
date,
|
|
326
|
+
sessionId: session.sessionId,
|
|
327
|
+
exercise: exercise.name,
|
|
328
|
+
previousBest: round(previousBest, 1),
|
|
329
|
+
estimatedOneRM: round(set.estimatedOneRM, 1)
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
for (const session of sessionsForDate) {
|
|
337
|
+
for (const exercise of session.exercises) {
|
|
338
|
+
for (const set of exercise.completedSets) {
|
|
339
|
+
const current = bestByExercise.get(exercise.exerciseKey);
|
|
340
|
+
if (current == null || set.estimatedOneRM > current) {
|
|
341
|
+
bestByExercise.set(exercise.exerciseKey, set.estimatedOneRM);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (events.length > 0) eventsByDate.set(date, events);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return eventsByDate;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function componentScores({ windowSessions, windowProgressionEvents, healthMetrics, windowStart, date, earliestDate }) {
|
|
354
|
+
const completedSessions = windowSessions.filter((session) => session.completedSetCount > 0);
|
|
355
|
+
const emptySessions = windowSessions.filter((session) => session.emptyStrengthSession).length;
|
|
356
|
+
const completedSetCount = completedSessions.reduce((sum, session) => sum + session.completedSetCount, 0);
|
|
357
|
+
const plannedSetCount = windowSessions.reduce((sum, session) => sum + (session.plannedSetCount ?? 0), 0);
|
|
358
|
+
const completionRate = plannedSetCount > 0 ? completedSetCount / plannedSetCount : 1;
|
|
359
|
+
const setCountsByMuscle = {};
|
|
360
|
+
|
|
361
|
+
for (const session of completedSessions) {
|
|
362
|
+
for (const exercise of session.exercises) {
|
|
363
|
+
if (!TARGET_SETS[exercise.primaryMuscle]) continue;
|
|
364
|
+
setCountsByMuscle[exercise.primaryMuscle] = (setCountsByMuscle[exercise.primaryMuscle] ?? 0) + exercise.completedSets.length;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const stimulus = Object.entries(TARGET_SETS).reduce((sum, [muscle, target]) => {
|
|
369
|
+
return sum + clamp((setCountsByMuscle[muscle] ?? 0) / target.target, 0, 1);
|
|
370
|
+
}, 0) / Object.keys(TARGET_SETS).length * 100;
|
|
371
|
+
|
|
372
|
+
const coveredMuscles = Object.keys(setCountsByMuscle).filter((muscle) => setCountsByMuscle[muscle] > 0);
|
|
373
|
+
const coverage = coveredMuscles.length / Object.keys(TARGET_SETS).length * 100;
|
|
374
|
+
|
|
375
|
+
const progressionEventCount = windowProgressionEvents.reduce((sum, events) => sum + events.length, 0);
|
|
376
|
+
const historyDays = earliestDate ? daysBetween(earliestDate, date) + 1 : 0;
|
|
377
|
+
const progression = progressionEventCount > 0
|
|
378
|
+
? clamp(45 + progressionEventCount * 18)
|
|
379
|
+
: (historyDays < 14 ? 45 : 35);
|
|
380
|
+
|
|
381
|
+
const recovery = recoveryScore(completedSessions, healthMetrics, windowStart, date);
|
|
382
|
+
const execution = clamp((completedSessions.length / 3) * 85 + completionRate * 15 - emptySessions * 20);
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
stimulus: round(stimulus),
|
|
386
|
+
progression: round(progression),
|
|
387
|
+
recovery: round(recovery.score),
|
|
388
|
+
coverage: round(coverage),
|
|
389
|
+
execution: round(execution),
|
|
390
|
+
meta: {
|
|
391
|
+
completedSessions: completedSessions.length,
|
|
392
|
+
completedSetCount,
|
|
393
|
+
plannedSetCount,
|
|
394
|
+
completionRate: round(completionRate, 2),
|
|
395
|
+
emptySessions,
|
|
396
|
+
setCountsByMuscle,
|
|
397
|
+
coveredMuscles,
|
|
398
|
+
progressionEventCount,
|
|
399
|
+
recovery
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function recoveryScore(completedSessions, healthMetrics, windowStart, date) {
|
|
405
|
+
let score = 88;
|
|
406
|
+
const sameMuscleWarnings = [];
|
|
407
|
+
const lastSeenByMuscle = new Map();
|
|
408
|
+
|
|
409
|
+
for (const session of [...completedSessions].sort((left, right) => left.date.localeCompare(right.date))) {
|
|
410
|
+
const muscles = new Set(session.exercises
|
|
411
|
+
.filter((exercise) => exercise.completedSets.length > 0 && TARGET_SETS[exercise.primaryMuscle])
|
|
412
|
+
.map((exercise) => exercise.primaryMuscle));
|
|
413
|
+
|
|
414
|
+
for (const muscle of muscles) {
|
|
415
|
+
const previous = lastSeenByMuscle.get(muscle);
|
|
416
|
+
if (previous) {
|
|
417
|
+
const gapDays = daysBetween(previous, session.date);
|
|
418
|
+
if (gapDays < 1) {
|
|
419
|
+
score -= 25;
|
|
420
|
+
sameMuscleWarnings.push({ muscle, gapDays, severity: 'high' });
|
|
421
|
+
} else if (gapDays < 2) {
|
|
422
|
+
score -= 12;
|
|
423
|
+
sameMuscleWarnings.push({ muscle, gapDays, severity: 'medium' });
|
|
424
|
+
} else if (gapDays < 3) {
|
|
425
|
+
score -= 4;
|
|
426
|
+
sameMuscleWarnings.push({ muscle, gapDays, severity: 'low' });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
lastSeenByMuscle.set(muscle, session.date);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (completedSessions.length >= 5) score -= 5;
|
|
434
|
+
|
|
435
|
+
const hrv = healthReadingsInWindow(healthMetrics, 'hrv', windowStart, date);
|
|
436
|
+
const restingHR = healthReadingsInWindow(healthMetrics, 'restingHR', windowStart, date);
|
|
437
|
+
const sleep = healthReadingsInWindow(healthMetrics, 'sleep', windowStart, date);
|
|
438
|
+
const availableSignals = [hrv.length > 0, restingHR.length > 0, sleep.length > 0].filter(Boolean).length;
|
|
439
|
+
if (availableSignals >= 2) score += 4;
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
score: clamp(score),
|
|
443
|
+
sameMuscleWarnings,
|
|
444
|
+
availableSignals
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function totalScore(components) {
|
|
449
|
+
return round(
|
|
450
|
+
components.stimulus * SCORE_WEIGHTS.stimulus
|
|
451
|
+
+ components.progression * SCORE_WEIGHTS.progression
|
|
452
|
+
+ components.recovery * SCORE_WEIGHTS.recovery
|
|
453
|
+
+ components.coverage * SCORE_WEIGHTS.coverage
|
|
454
|
+
+ components.execution * SCORE_WEIGHTS.execution
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function dataQualityNotes({ healthMetrics, windowStart, date, earliestDate, completedSessions, noSignal }) {
|
|
459
|
+
const historyDays = earliestDate ? daysBetween(earliestDate, date) + 1 : 0;
|
|
460
|
+
const hrv = healthReadingsInWindow(healthMetrics, 'hrv', windowStart, date).length;
|
|
461
|
+
const restingHR = healthReadingsInWindow(healthMetrics, 'restingHR', windowStart, date).length;
|
|
462
|
+
const sleep = healthReadingsInWindow(healthMetrics, 'sleep', windowStart, date).length;
|
|
463
|
+
const availableRecoverySignals = [
|
|
464
|
+
hrv > 0 ? 'HRV' : null,
|
|
465
|
+
restingHR > 0 ? 'resting HR' : null,
|
|
466
|
+
sleep > 0 ? 'sleep' : null
|
|
467
|
+
].filter(Boolean);
|
|
468
|
+
const missingRecoverySignals = [
|
|
469
|
+
hrv === 0 ? 'HRV' : null,
|
|
470
|
+
restingHR === 0 ? 'resting HR' : null,
|
|
471
|
+
sleep === 0 ? 'sleep' : null
|
|
472
|
+
].filter(Boolean);
|
|
473
|
+
|
|
474
|
+
const confidenceScore = clamp(
|
|
475
|
+
(historyDays >= 28 ? 40 : historyDays >= 14 ? 28 : 15)
|
|
476
|
+
+ (completedSessions.length >= 3 ? 30 : completedSessions.length >= 1 ? 18 : 0)
|
|
477
|
+
+ availableRecoverySignals.length * 10
|
|
478
|
+
- (noSignal ? 20 : 0)
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
const confidence = noSignal || confidenceScore < 45
|
|
482
|
+
? 'low'
|
|
483
|
+
: confidenceScore < 70
|
|
484
|
+
? 'medium'
|
|
485
|
+
: 'high';
|
|
486
|
+
|
|
487
|
+
const notes = [];
|
|
488
|
+
notes.push(`History baseline: ${historyDays} day${historyDays === 1 ? '' : 's'} (${historyDays >= 28 ? 'high' : historyDays >= 14 ? 'medium' : 'low'}).`);
|
|
489
|
+
if (availableRecoverySignals.length > 0) {
|
|
490
|
+
notes.push(`Health recovery signals present: ${availableRecoverySignals.join(', ')}.`);
|
|
491
|
+
}
|
|
492
|
+
if (missingRecoverySignals.length > 0) {
|
|
493
|
+
notes.push(`Health recovery signals missing: ${missingRecoverySignals.join(', ')}.`);
|
|
494
|
+
}
|
|
495
|
+
notes.push('Intra-workout rest quality is not scored until set timing/rest metadata exists.');
|
|
496
|
+
if (noSignal) notes.push('No completed strength sets in the rolling 7-day window.');
|
|
497
|
+
|
|
498
|
+
return {
|
|
499
|
+
confidence,
|
|
500
|
+
confidenceScore,
|
|
501
|
+
healthSignals: {
|
|
502
|
+
hrvReadings: hrv,
|
|
503
|
+
restingHRReadings: restingHR,
|
|
504
|
+
sleepNights: sleep
|
|
505
|
+
},
|
|
506
|
+
notes
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function driverCandidates(row, previousScoredRow) {
|
|
511
|
+
if (row.state === 'no_signal') {
|
|
512
|
+
return {
|
|
513
|
+
positive: [],
|
|
514
|
+
negative: [{ component: 'signal', text: 'No logged strength signal in the rolling 7-day window.', impact: 100 }]
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const positives = [];
|
|
519
|
+
const negatives = [];
|
|
520
|
+
const meta = row.componentMeta;
|
|
521
|
+
|
|
522
|
+
if (row.components.stimulus >= 70) positives.push({ component: 'stimulus', text: 'Productive weekly strength stimulus is near target.', impact: row.components.stimulus });
|
|
523
|
+
if (row.components.coverage >= 70) positives.push({ component: 'coverage', text: 'Major muscle coverage is broad this week.', impact: row.components.coverage });
|
|
524
|
+
if (row.components.progression >= 63) positives.push({ component: 'progression', text: `${meta.progressionEventCount} progression signal${meta.progressionEventCount === 1 ? '' : 's'} in the window.`, impact: row.components.progression });
|
|
525
|
+
if (row.components.execution >= 80) positives.push({ component: 'execution', text: 'Session execution is strong for the week.', impact: row.components.execution });
|
|
526
|
+
if (row.components.recovery >= 80) positives.push({ component: 'recovery', text: 'Between-session recovery spacing is mostly clear.', impact: row.components.recovery });
|
|
527
|
+
|
|
528
|
+
const missingMuscles = Object.entries(TARGET_SETS)
|
|
529
|
+
.filter(([muscle]) => (meta.setCountsByMuscle[muscle] ?? 0) === 0)
|
|
530
|
+
.map(([, target]) => target.label);
|
|
531
|
+
if (missingMuscles.length > 0) negatives.push({ component: 'coverage', text: `${missingMuscles.slice(0, 3).join(', ')} missing from the rolling window.`, impact: 100 - row.components.coverage });
|
|
532
|
+
if (row.components.stimulus < 55) negatives.push({ component: 'stimulus', text: 'Completed working sets are below weekly target.', impact: 100 - row.components.stimulus });
|
|
533
|
+
if (meta.recovery.sameMuscleWarnings.length > 0) negatives.push({ component: 'recovery', text: 'Same-muscle sessions are clustered inside the recovery window.', impact: 100 - row.components.recovery });
|
|
534
|
+
if (row.components.progression < 45) negatives.push({ component: 'progression', text: 'Progression evidence is fading versus prior logged history.', impact: 100 - row.components.progression });
|
|
535
|
+
if (meta.emptySessions > 0) negatives.push({ component: 'execution', text: `${meta.emptySessions} empty strength session${meta.emptySessions === 1 ? '' : 's'} in the window.`, impact: 60 });
|
|
536
|
+
|
|
537
|
+
if (previousScoredRow?.components) {
|
|
538
|
+
for (const component of Object.keys(SCORE_WEIGHTS)) {
|
|
539
|
+
const delta = row.components[component] - previousScoredRow.components[component];
|
|
540
|
+
if (delta >= 12) positives.push({ component, text: `${component} improved ${round(delta)} points day over day.`, impact: delta });
|
|
541
|
+
if (delta <= -12) negatives.push({ component, text: `${component} dropped ${Math.abs(round(delta))} points day over day.`, impact: Math.abs(delta) });
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
positive: positives.sort((left, right) => right.impact - left.impact).slice(0, 3),
|
|
547
|
+
negative: negatives.sort((left, right) => right.impact - left.impact).slice(0, 3)
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
export function buildIncrementScoreReplay(inputOrSnapshot, options = {}) {
|
|
552
|
+
const input = normalizedIncrementScoreReplayInput(inputOrSnapshot, options);
|
|
553
|
+
|
|
554
|
+
const progressionEventsByDate = buildProgressionEvents(input.sessions);
|
|
555
|
+
const earliestDate = input.sessions[0]?.date ?? null;
|
|
556
|
+
const rows = [];
|
|
557
|
+
let previousRow = null;
|
|
558
|
+
let previousScoredRow = null;
|
|
559
|
+
|
|
560
|
+
for (const date of dateRange(input.from, input.to)) {
|
|
561
|
+
const windowStart = addDays(date, -6);
|
|
562
|
+
const windowSessions = sessionsInWindow(input.sessions, windowStart, date);
|
|
563
|
+
const completedSessions = windowSessions.filter((session) => session.completedSetCount > 0);
|
|
564
|
+
const noSignal = completedSessions.length === 0;
|
|
565
|
+
const windowProgressionEvents = dateRange(windowStart, date).map((key) => progressionEventsByDate.get(key) ?? []);
|
|
566
|
+
|
|
567
|
+
const quality = dataQualityNotes({
|
|
568
|
+
healthMetrics: input.healthMetrics,
|
|
569
|
+
windowStart,
|
|
570
|
+
date,
|
|
571
|
+
earliestDate,
|
|
572
|
+
completedSessions,
|
|
573
|
+
noSignal
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
let row;
|
|
577
|
+
if (noSignal) {
|
|
578
|
+
row = {
|
|
579
|
+
date,
|
|
580
|
+
windowStart,
|
|
581
|
+
windowEnd: date,
|
|
582
|
+
state: 'no_signal',
|
|
583
|
+
score: null,
|
|
584
|
+
scoreDelta: previousRow?.score != null ? null : null,
|
|
585
|
+
deltaState: previousRow?.score != null ? 'lost_signal' : 'no_signal',
|
|
586
|
+
trend: 'no_signal',
|
|
587
|
+
components: {
|
|
588
|
+
stimulus: 0,
|
|
589
|
+
progression: 0,
|
|
590
|
+
recovery: 0,
|
|
591
|
+
coverage: 0,
|
|
592
|
+
execution: 0
|
|
593
|
+
},
|
|
594
|
+
componentMeta: {
|
|
595
|
+
completedSessions: 0,
|
|
596
|
+
completedSetCount: 0,
|
|
597
|
+
plannedSetCount: windowSessions.reduce((sum, session) => sum + (session.plannedSetCount ?? 0), 0),
|
|
598
|
+
completionRate: 0,
|
|
599
|
+
emptySessions: windowSessions.filter((session) => session.emptyStrengthSession).length,
|
|
600
|
+
setCountsByMuscle: {},
|
|
601
|
+
coveredMuscles: [],
|
|
602
|
+
progressionEventCount: 0,
|
|
603
|
+
recovery: { score: 0, sameMuscleWarnings: [], availableSignals: 0 }
|
|
604
|
+
},
|
|
605
|
+
topDrivers: { positive: [], negative: [] },
|
|
606
|
+
dataQuality: quality
|
|
607
|
+
};
|
|
608
|
+
} else {
|
|
609
|
+
const components = componentScores({
|
|
610
|
+
windowSessions,
|
|
611
|
+
windowProgressionEvents,
|
|
612
|
+
healthMetrics: input.healthMetrics,
|
|
613
|
+
windowStart,
|
|
614
|
+
date,
|
|
615
|
+
earliestDate
|
|
616
|
+
});
|
|
617
|
+
const score = totalScore(components);
|
|
618
|
+
const scoreDelta = previousRow?.score != null ? score - previousRow.score : null;
|
|
619
|
+
row = {
|
|
620
|
+
date,
|
|
621
|
+
windowStart,
|
|
622
|
+
windowEnd: date,
|
|
623
|
+
state: 'scored',
|
|
624
|
+
score,
|
|
625
|
+
scoreDelta,
|
|
626
|
+
deltaState: previousRow?.state === 'no_signal' ? 'rebuilt' : (scoreDelta == null ? 'baseline' : 'scored'),
|
|
627
|
+
trend: trendFor(scoreDelta, previousRow),
|
|
628
|
+
components: {
|
|
629
|
+
stimulus: components.stimulus,
|
|
630
|
+
progression: components.progression,
|
|
631
|
+
recovery: components.recovery,
|
|
632
|
+
coverage: components.coverage,
|
|
633
|
+
execution: components.execution
|
|
634
|
+
},
|
|
635
|
+
componentMeta: components.meta,
|
|
636
|
+
topDrivers: { positive: [], negative: [] },
|
|
637
|
+
dataQuality: quality
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
row.topDrivers = driverCandidates(row, previousScoredRow);
|
|
642
|
+
rows.push(row);
|
|
643
|
+
previousRow = row;
|
|
644
|
+
if (row.score != null) previousScoredRow = row;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const weeklySummaries = buildWeeklySummaries(rows);
|
|
648
|
+
|
|
649
|
+
return {
|
|
650
|
+
replayVersion: input.replayVersion,
|
|
651
|
+
formulaVersion: input.formulaVersion,
|
|
652
|
+
source: input.source,
|
|
653
|
+
extractedAt: input.extractedAt,
|
|
654
|
+
user: input.user,
|
|
655
|
+
dateRange: { from: input.from, to: input.to },
|
|
656
|
+
dailyRows: rows,
|
|
657
|
+
weeklySummaries
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function trendFor(scoreDelta, previousRow) {
|
|
662
|
+
if (previousRow?.state === 'no_signal') return 'rebuilt';
|
|
663
|
+
if (scoreDelta == null) return 'baseline';
|
|
664
|
+
if (scoreDelta >= 3) return 'improving';
|
|
665
|
+
if (scoreDelta <= -3) return 'slipping';
|
|
666
|
+
return 'stable';
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function buildWeeklySummaries(rows) {
|
|
670
|
+
const grouped = new Map();
|
|
671
|
+
for (const row of rows) {
|
|
672
|
+
const key = weekStart(row.date);
|
|
673
|
+
if (!grouped.has(key)) grouped.set(key, []);
|
|
674
|
+
grouped.get(key).push(row);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const summaries = [];
|
|
678
|
+
let previousScoredSummary = null;
|
|
679
|
+
for (const [startDate, weekRows] of [...grouped.entries()].sort(([left], [right]) => left.localeCompare(right))) {
|
|
680
|
+
const scoredRows = weekRows.filter((row) => row.score != null);
|
|
681
|
+
const lastRow = [...weekRows].reverse().find((row) => row.score != null) ?? weekRows.at(-1);
|
|
682
|
+
const averageScore = scoredRows.length > 0
|
|
683
|
+
? round(scoredRows.reduce((sum, row) => sum + row.score, 0) / scoredRows.length)
|
|
684
|
+
: null;
|
|
685
|
+
const components = averageComponents(scoredRows);
|
|
686
|
+
const scoreDelta = lastRow?.score != null && previousScoredSummary?.score != null
|
|
687
|
+
? lastRow.score - previousScoredSummary.score
|
|
688
|
+
: null;
|
|
689
|
+
|
|
690
|
+
const summary = {
|
|
691
|
+
weekStart: startDate,
|
|
692
|
+
weekEnd: weekEnd(startDate),
|
|
693
|
+
state: scoredRows.length > 0 ? 'scored' : 'no_signal',
|
|
694
|
+
score: lastRow?.score ?? null,
|
|
695
|
+
averageScore,
|
|
696
|
+
scoreDelta,
|
|
697
|
+
trend: scoredRows.length === 0 ? 'no_signal' : trendFor(scoreDelta, previousScoredSummary),
|
|
698
|
+
scoredDays: scoredRows.length,
|
|
699
|
+
noSignalDays: weekRows.length - scoredRows.length,
|
|
700
|
+
components,
|
|
701
|
+
topDrivers: aggregateDrivers(weekRows),
|
|
702
|
+
confidenceNotes: [...new Set(weekRows.flatMap((row) => row.dataQuality.notes))].slice(0, 5)
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
summaries.push(summary);
|
|
706
|
+
if (summary.score != null) previousScoredSummary = summary;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return summaries;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function averageComponents(rows) {
|
|
713
|
+
if (rows.length === 0) {
|
|
714
|
+
return { stimulus: 0, progression: 0, recovery: 0, coverage: 0, execution: 0 };
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
return Object.fromEntries(Object.keys(SCORE_WEIGHTS).map((component) => [
|
|
718
|
+
component,
|
|
719
|
+
round(rows.reduce((sum, row) => sum + row.components[component], 0) / rows.length)
|
|
720
|
+
]));
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function aggregateDrivers(rows) {
|
|
724
|
+
const rank = (drivers) => {
|
|
725
|
+
const byText = new Map();
|
|
726
|
+
for (const driver of drivers) {
|
|
727
|
+
const current = byText.get(driver.text) ?? { ...driver, impact: 0, count: 0 };
|
|
728
|
+
current.impact += driver.impact;
|
|
729
|
+
current.count += 1;
|
|
730
|
+
byText.set(driver.text, current);
|
|
731
|
+
}
|
|
732
|
+
return [...byText.values()]
|
|
733
|
+
.sort((left, right) => right.impact - left.impact)
|
|
734
|
+
.slice(0, 3)
|
|
735
|
+
.map((driver) => ({
|
|
736
|
+
component: driver.component,
|
|
737
|
+
text: driver.text,
|
|
738
|
+
count: driver.count
|
|
739
|
+
}));
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
return {
|
|
743
|
+
positive: rank(rows.flatMap((row) => row.topDrivers.positive)),
|
|
744
|
+
negative: rank(rows.flatMap((row) => row.topDrivers.negative))
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function formatDelta(delta, state = 'scored') {
|
|
749
|
+
if (state === 'rebuilt') return 'rebuilt';
|
|
750
|
+
if (state === 'lost_signal') return 'lost signal';
|
|
751
|
+
if (state === 'no_signal') return 'no signal';
|
|
752
|
+
if (delta == null) return 'baseline';
|
|
753
|
+
return `${delta >= 0 ? '+' : ''}${delta}`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function formatScore(score) {
|
|
757
|
+
return score == null ? 'no signal' : String(score);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function driverLines(prefix, drivers) {
|
|
761
|
+
if (drivers.length === 0) return [`${prefix} none`];
|
|
762
|
+
return drivers.map((driver) => `${prefix} ${driver.text}`);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function formatIncrementScoreReplayMarkdown(replay) {
|
|
766
|
+
const lines = [];
|
|
767
|
+
lines.push(`# Increment Score Replay - ${replay.user.label}`);
|
|
768
|
+
lines.push('');
|
|
769
|
+
if (replay.user.ref) lines.push(`User ref: ${replay.user.ref}`);
|
|
770
|
+
lines.push(`Range: ${replay.dateRange.from} to ${replay.dateRange.to}`);
|
|
771
|
+
lines.push(`Formula: ${replay.formulaVersion}`);
|
|
772
|
+
lines.push(`Replay harness: ${replay.replayVersion}`);
|
|
773
|
+
lines.push(`Extracted at: ${replay.extractedAt}`);
|
|
774
|
+
lines.push('');
|
|
775
|
+
|
|
776
|
+
lines.push('## Daily Score Rows');
|
|
777
|
+
lines.push('');
|
|
778
|
+
lines.push('| Date | Score | Delta | State | Stimulus | Progression | Recovery | Coverage | Execution | Confidence |');
|
|
779
|
+
lines.push('| --- | ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | --- |');
|
|
780
|
+
for (const row of replay.dailyRows) {
|
|
781
|
+
lines.push([
|
|
782
|
+
row.date,
|
|
783
|
+
formatScore(row.score),
|
|
784
|
+
formatDelta(row.scoreDelta, row.deltaState),
|
|
785
|
+
row.state,
|
|
786
|
+
row.components.stimulus,
|
|
787
|
+
row.components.progression,
|
|
788
|
+
row.components.recovery,
|
|
789
|
+
row.components.coverage,
|
|
790
|
+
row.components.execution,
|
|
791
|
+
row.dataQuality.confidence
|
|
792
|
+
].join(' | ').replace(/^/, '| ').replace(/$/, ' |'));
|
|
793
|
+
}
|
|
794
|
+
lines.push('');
|
|
795
|
+
|
|
796
|
+
lines.push('## Weekly Summaries');
|
|
797
|
+
lines.push('');
|
|
798
|
+
for (const week of replay.weeklySummaries) {
|
|
799
|
+
lines.push(`### Week Ending ${week.weekEnd}`);
|
|
800
|
+
lines.push('');
|
|
801
|
+
lines.push(`Score: ${formatScore(week.score)} (${formatDelta(week.scoreDelta, week.trend)})`);
|
|
802
|
+
lines.push(`Trend: ${week.trend}`);
|
|
803
|
+
lines.push(`Scored days: ${week.scoredDays}; no-signal days: ${week.noSignalDays}`);
|
|
804
|
+
lines.push(`Components: stimulus ${week.components.stimulus}, progression ${week.components.progression}, recovery ${week.components.recovery}, coverage ${week.components.coverage}, execution ${week.components.execution}`);
|
|
805
|
+
lines.push('');
|
|
806
|
+
lines.push('Drivers:');
|
|
807
|
+
for (const line of driverLines('+', week.topDrivers.positive)) lines.push(line);
|
|
808
|
+
for (const line of driverLines('-', week.topDrivers.negative)) lines.push(line);
|
|
809
|
+
lines.push('');
|
|
810
|
+
lines.push('Data quality:');
|
|
811
|
+
for (const note of week.confidenceNotes) lines.push(`- ${note}`);
|
|
812
|
+
lines.push('');
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
lines.push('## Calibration Notes');
|
|
816
|
+
lines.push('');
|
|
817
|
+
lines.push('- Provisional formula only; replace the adapter when `onemore-9ar` lands.');
|
|
818
|
+
lines.push('- Snapshot adapter is read-only and emits redacted user refs only.');
|
|
819
|
+
lines.push('- No raw user exports should be committed with generated reports.');
|
|
820
|
+
|
|
821
|
+
return `${lines.join('\n')}\n`;
|
|
822
|
+
}
|