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 134 135 136 137 138 139 140 141 142 143 144 145 | 4x 2x 87x 24x 85x 24x 22x 22x 85x 85x 85x 22x 63x 63x 8x 8x 24x 24x 3x 24x 12x 12x 12x 41x 41x 15x 15x 12x 8x 8x 24x | 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>;
/**
* Narrowed set of IDs that the model actually received from lookup_exercises
* during this attempt (and prior attempts in the same generation run).
* When supplied alongside validExerciseIds, an exercise_id is flagged as
* unknown_exercise if it is absent from EITHER set — providing defence-in-depth
* against IDs the model fabricated or borrowed from other contexts.
* Omit to use full-library membership only (back-compat).
*/
toolReturnedIds?: 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) {
const isUnknown =
!options.validExerciseIds.has(ex.exercise_id) ||
(options.toolReturnedIds !== undefined && !options.toolReturnedIds.has(ex.exercise_id));
if (isUnknown && !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 } : {}),
};
}
|