Reference TypeScript validator for WPL (Wellness Plan Language).
Validates compiled WPL JSON against:
- Pass 1: the canonical JSON Schema (Draft 2020-12, via ajv).
- Pass 2: semantic invariants from the WPL specification — duplicate-ID detection, ref resolution, prescription validity, etc.
Sister implementation: wpl_validator (Elixir). Both pass the shared conformance suite at every release.
npm install @gymbile/wpl-validatorRequires Node.js >= 20.10.
import { validate, type ValidationResult, type ValidationError } from '@gymbile/wpl-validator';
const plan = JSON.parse(planJson);
const result: ValidationResult = validate(plan);
if (!result.valid) {
for (const err of result.errors) {
console.error(`[${err.code}] ${err.path}: ${err.message}`);
}
}interface ValidationError {
path: string; // RFC 6901 JSON Pointer, e.g. "/plan/phases/0/weeks/1/days/2"
code: string; // e.g. "DUPLICATE_ID"
message: string; // human-readable summary
severity: 'error' | 'warning';
meta?: Record<string, unknown>; // structured detail (rule-specific keys)
}const result = validate(plan, {
catalog: {
exercises: new Set(['push_up', 'squat', 'deadlift']),
meals: new Set(['oatmeal', 'chicken_breast']),
},
});If no catalog is provided, UNRESOLVED_REF checks are skipped.
Pass requireCatalog: true to fail with CATALOG_REQUIRED when no catalog is supplied instead of silently skipping ref checks:
const result = validate(plan, { requireCatalog: true });
// If catalog is absent → result.errors includes CATALOG_REQUIREDAfter validating a plan, call enforce() to strip exercises that a personalization rule forbids for a specific client. This is the runtime safety pass: validate() checks that a plan is well-formed; enforce() makes it safe for this client.
import {
enforce,
type ClientContext,
type Rule,
type EnforcementResult,
} from '@gymbile/wpl-validator';
const plan = JSON.parse(planJson); // already validated
const ctx: ClientContext = {
injuries: ['lower_back'],
equipment: ['dumbbells'],
};
const rules: Rule[] = [
{
id: 'no-deadlift-lower-back',
condition: { field: 'injuries', op: 'contains', value: 'lower_back' },
actions: [{ type: 'forbid_exercise', exercise: 'deadlift' }],
},
];
const result: EnforcementResult = enforce(plan, ctx, rules);
// result.plan — deep-cloned plan with forbidden exercises removed
// result.stripped — [{ exercise, matched_rule, path }, ...]
// result.diagnostics — fail-closed warnings (unknown fields / action types)
// result.evaluated_rules — per-rule condition evaluation detail
if (result.stripped.length > 0) {
console.warn('Removed exercises:', result.stripped.map((s) => s.exercise));
}enforce() is fail-closed: if a rule references an unknown ClientContext field, it emits an UNKNOWN_CONDITION_FIELD diagnostic and treats the condition as unmet (the exercise is kept, not silently stripped).
result.valid is true unless at least one finding has severity: "error". Findings with severity: "warning" (currently only PHASE_DURATION_MISMATCH) appear in result.errors but do not invalidate the plan — they're advisory.
If you want to reject warnings too, filter the result yourself:
const hasAny = result.errors.length > 0;The validator runs two passes in sequence:
- Pass 1 (schema): ajv compiles the canonical JSON Schema and checks the input against it. Failures emit
SCHEMA_VIOLATION. - Pass 2 (semantic): a structural walker descends the plan tree (
plan → phases → weeks → days → blocks → activities, plus personalization rules, checkpoints, and points rules). Each registered rule visits the relevant nodes and emits findings.
If Pass 1 fails, Pass 2 is skipped — semantic checks assume schema-valid shape.
| Code | Severity | Description |
|---|---|---|
SCHEMA_VIOLATION |
error | Pass 1 ajv check failed (shape, type, enum, required field, etc.) |
DUPLICATE_ID |
error | Two siblings within the same scope share an id |
UNRESOLVED_REF |
error | An exercise_ref / meal_ref / meditation_ref doesn't exist in the supplied catalog |
EMPTY_PHASES_FOR_TYPE |
error | Plan type: "workout" or "hybrid" has zero phases |
INVALID_PRESCRIPTION |
error | Activity prescription has unknown type or missing required fields |
INVALID_PERSONALIZATION_RULE |
error | Personalization rule has malformed condition or invalid action type/scope |
INVALID_POINTS_RULE |
error | Points-system rule missing action/points or points not a non-negative integer |
PHASE_DURATION_MISMATCH |
warning | Phase declares duration: { value, unit } that doesn't match weeks.length |
Canonical reference (with meta.reason enums and JSON Pointer rules): error-codes.md.
Vendored schema and conformance suite from gymbile/wpl@v1.1.1. The conformance suite (3 valid + 10 invalid fixtures) is run on every CI build via npm test. The suite itself is not shipped to npm; see the upstream conformance directory.
A weekly drift-check workflow flags any divergence between the vendored copies in this repo and the latest upstream tag.
See CONTRIBUTING.md for dev setup, the rule-addition recipe, and release flow.
Apache-2.0. See LICENSE.
"WPL" and "Wellness Plan Language" are trademarks of Gymbile. See the schema repo for naming policy.