Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 2x 1x 21x 21x 21x 21x 21x 21x 21x 21x 19x 19x 73x 73x 73x 73x 73x 19x 51x 51x 8x 8x 8x 51x 19x 21x 21x 3x 3x 21x 9x 9x 9x 29x 13x 13x 13x 29x 9x 7x 7x 7x 9x 21x 21x 21x 21x 21x 21x | import type { PrescribedExercise, MovementPattern, SessionType } from '@strengthsys/shared';
import { estimateWorkoutTime } from './estimate-workout-time.js';
// ── Config from spec/programming.allium ─────────────────────
export const VALIDATION_CONFIG = {
max_pattern_volume_pct_focus: 45,
max_pattern_volume_pct_other: 30,
time_overrun_threshold_minutes: 3,
fatigue_intensity_threshold_pct: 75,
metcon_intensity_ceiling_pct: 40,
} as const;
// ── Types ───────────────────────────────────────────────────
export type ValidationViolation = 'volume_concentration' | 'time_overrun' | 'unknown_exercise';
export interface ValidationResult {
valid: boolean;
violations: ValidationViolation[];
/**
* Present iff `violations` includes `'unknown_exercise'`. Listed in
* first-occurrence order, deduplicated. Used by the edge function to build
* the model-facing retry message (`buildUnknownExerciseViolationMessage`).
*/
unknown_exercise_ids?: string[];
}
export interface WorkoutForValidation {
session_type: SessionType;
time_cap_minutes: number;
prescribed_exercises: PrescribedExercise[];
}
export interface ValidateSessionOptions {
/**
* Set of valid exercise UUIDs from the library. When supplied, every
* `prescribed_exercises[].exercise_id` is checked for membership and
* any unknown IDs surface as a single `unknown_exercise` violation.
* Omit to skip the membership check (AI-facing dispatch path).
*/
validExerciseIds?: Set<string>;
}
// ── Helpers ─────────────────────────────────────────────────
/**
* Builds the model-facing violation message that lists the offending exercise
* IDs and instructs the model to re-call lookup_exercises before retrying.
*
* Message wording must NOT be changed without updating the corresponding
* integration test assertion in tests/edge-functions/workout-generation.test.ts.
*/
export function buildUnknownExerciseViolationMessage(unknownIds: string[]): string {
return `These exercise_id values are not in the library — re-call lookup_exercises and use only IDs from its output: ${unknownIds.join(', ')}`;
}
// ── Implementation ──────────────────────────────────────────
/**
* Validate a generated workout session against platform safety-net rules.
*
* Checks:
* 1. Volume concentration — no single movement pattern should exceed 45% of total set count
* 2. Time overrun — estimated time should not exceed time_cap by more than 3 minutes
* 3. Exercise library membership — when `options.validExerciseIds` is supplied, every
* `prescribed_exercises[].exercise_id` must be a member. Omit `options` (or omit
* `validExerciseIds`) to skip this check — the AI-facing dispatch path relies on this
* being a no-op when no valid-ID set is provided.
*/
export function validateSession(
workout: WorkoutForValidation,
options: ValidateSessionOptions = {},
): ValidationResult {
const primary = workout.prescribed_exercises.filter((e) => !e.is_backup);
const violations: ValidationViolation[] = [];
let unknownExerciseIds: string[] | undefined;
// ── Volume concentration check ────────────────────────────
const totalSets = primary.reduce((sum, ex) => sum + ex.sets.length, 0);
if (totalSets > 0) {
const setsPerPattern: Partial<Record<MovementPattern, number>> = {};
for (const ex of primary) {
const share = ex.sets.length / ex.movement_patterns.length;
for (const pattern of ex.movement_patterns) {
setsPerPattern[pattern] = (setsPerPattern[pattern] ?? 0) + share;
}
}
for (const pattern of Object.keys(setsPerPattern) as MovementPattern[]) {
const pct = (setsPerPattern[pattern]! / totalSets) * 100;
if (pct > VALIDATION_CONFIG.max_pattern_volume_pct_focus) {
violations.push('volume_concentration');
break;
}
}
}
// ── Time overrun check ────────────────────────────────────
const estimatedMinutes = estimateWorkoutTime(primary);
if (estimatedMinutes > workout.time_cap_minutes + VALIDATION_CONFIG.time_overrun_threshold_minutes) {
violations.push('time_overrun');
}
// ── Membership check ─────────────────────────────────────
if (options.validExerciseIds) {
const seen = new Set<string>();
const unknown: string[] = [];
// NOTE: membership applies to ALL prescribed exercises, including backups.
// This differs intentionally from the volume/time checks above (which filter
// to primary only). Membership is about whether the row can be persisted —
// an unknown backup exercise_id would still fail the FK constraint.
for (const ex of workout.prescribed_exercises) {
if (!options.validExerciseIds.has(ex.exercise_id) && !seen.has(ex.exercise_id)) {
unknown.push(ex.exercise_id);
seen.add(ex.exercise_id);
}
}
if (unknown.length > 0) {
violations.push('unknown_exercise');
unknownExerciseIds = unknown;
}
}
return {
valid: violations.length === 0,
violations,
...(unknownExerciseIds !== undefined ? { unknown_exercise_ids: unknownExerciseIds } : {}),
};
}
|