Skip to content

Repository files navigation

@gymbile/wpl-validator

npm version CI License: Apache-2.0

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.

Install

npm install @gymbile/wpl-validator

Requires Node.js >= 20.10.

Usage

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}`);
  }
}

ValidationError shape

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)
}

With a catalog (resolves *_ref fields)

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.

Strict catalog mode

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_REQUIRED

Enforcement (Pass 3)

After 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).

Severity semantics

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;

Pipeline

The validator runs two passes in sequence:

  1. Pass 1 (schema): ajv compiles the canonical JSON Schema and checks the input against it. Failures emit SCHEMA_VIOLATION.
  2. 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.

Error codes

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.

Conformance

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.

Contributing

See CONTRIBUTING.md for dev setup, the rule-addition recipe, and release flow.

License

Apache-2.0. See LICENSE.

Trademark

"WPL" and "Wellness Plan Language" are trademarks of Gymbile. See the schema repo for naming policy.

About

TypeScript reference validator for WPL (Wellness Plan Language)

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages