Skip to content

Repository files navigation

@gymbile/wpl-ai

npm version CI License

Compiler for WPL-AI — a human/LLM-friendly DSL that compiles to canonical WPL (Wellness Plan Language) JSON.

WPL-AI is the textual authoring format. WPL JSON is the runtime format. This package bridges the two.

WPL-AI source ──[tokenize]──▶ Tokens ──[parse]──▶ AST ──[compile]──▶ WPL JSON
                                                                       │
                                                       ┌───────────────┴─────────────┐
                                                       ▼                             ▼
                                              [@gymbile/wpl-validator]      [your runtime]
                                              schema + semantic checks      (training app, ...)

Where it fits in the WPL ecosystem

Repo / Package Role
gymbile/wpl Canonical JSON Schema, spec, conformance suite (source of truth)
@gymbile/wpl-ai (this repo) DSL → WPL JSON compiler (textual authoring)
@gymbile/wpl-validator Reference TypeScript validator (schema + semantic invariants)
wpl.dev Marketing site + interactive playground (consumes both)

@gymbile/wpl-ai depends on @gymbile/wpl-validator to validate every successful compile output against the canonical schema and semantic rules.


Installation

npm install @gymbile/wpl-ai

Requires Node ≥20. Ships dual ESM + CJS builds with TypeScript declarations.

// ESM
import { compileWplAi } from "@gymbile/wpl-ai";

// CommonJS
const { compileWplAi } = require("@gymbile/wpl-ai");

Quick start

import { compileWplAi } from "@gymbile/wpl-ai";

const source = `
PLAN "Simple Upper Body"
TYPE workout
VISIBILITY public

GOALS
  GOAL primary strength:
    name "Build Upper Body Strength"

PHASES
  PHASE "Single Session" (1 weeks):
    WEEK 1:
      DAY Monday training 35m "Upper Body Focus":
        warmup:
          arm_circles 2m
        main straight_sets:
          push_up 3x8..12 rpe 7 rest 60 seconds
        cooldown:
          chest_stretch 30s x2 sides both
`;

const result = compileWplAi(source);

if (result.ok) {
  console.log("Plan:", result.json);
  console.log("Schema/semantic valid:", result.validation.valid);
  if (!result.validation.valid) {
    for (const err of result.validation.errors) {
      console.warn(`[${err.code}] ${err.path}: ${err.message}`);
    }
  }
} else {
  console.error(result.formatted); // Pretty-printed compile errors
}

Public API

compileWplAi(source: string): CompileResult

The main entry point. Runs the full pipeline (lex → parse → compile → semantic checks → schema/semantic validation) and returns a discriminated-union result.

type CompileResult =
  | {
      ok: true;
      json: Record<string, unknown>;        // Compiled WPL JSON
      ast: Document;                        // Parser AST (useful for tooling)
      warnings: SemanticWarning[];          // DSL-level vocabulary/consistency warnings
      validation: ValidationResult;         // Schema + semantic findings (re-exported from wpl-validator)
      pointerMap: PointerSourceMap;         // JSONPointer → SourceRange (see below)
      repairs: Repair[];                    // Every tolerant normalisation the parser made (see Safety posture)
    }
  | {
      ok: false;
      errors: WplError[];                   // Lex/parse/compile errors with source positions
      formatted: string;                    // Human-readable error report (plain text, not ANSI-colored)
      summary: string;                      // One-line summary, e.g. "3 errors: 2 lex_unexpected_char, 1 parse_unexpected_token"
    };

formatted is plain text — no ANSI escape codes — so it is safe to render in browser editors as-is. If you want colorized terminal output, wrap the per-error pieces yourself. summary is a single line counting errors by type, suitable for status bars or log lines.

Success vs. failure semantics:

  • ok: false means the source was malformed — couldn't lex, parse, or compile to JSON. No JSON output exists.
  • ok: true means the compile produced JSON. The JSON is then validated against the canonical schema; result.validation.valid indicates whether validation passed. A successful compile can still produce schema-invalid JSON (e.g., the user wrote a workout plan with no phases — that compiles but fails semantic validation).
  • warnings are DSL-level advisory findings (unknown vocabulary, plan-type/activity-type inconsistencies). They don't make a plan invalid.

Source position tracking

Every JSON node the compiler emits is recorded in pointerMap keyed by RFC 6901 JSON Pointer, mapping to a SourceRange of character offsets in the original DSL source.

type SourceRange = { from: number; to: number };
type PointerSourceMap = Map<string, SourceRange>;

const range = result.pointerMap.get("/plan/phases/0/weeks/0/days/0");
// → { from: 142, to: 318 }   (or undefined if synthetic / not from source)

This is what lets the wpl.dev playground highlight validator errors at the right characters in the editor: validator emits path: "/plan/phases/0/duration", the playground looks it up in the map, gets a range, and renders an inline squiggle.

Synthetic JSON nodes (e.g. $schema literal, default values) don't have a corresponding source range — those pointers are absent from the map. Consumers must tolerate undefined lookups.

Error handling helpers

import { formatErrors, formatError, errorSummary } from "@gymbile/wpl-ai";
  • formatErrors(errors, source?) — pretty-prints all errors with caret pointers + source context. Used in result.formatted.
  • formatError(error, source?) — formats a single error.
  • errorSummary(errors) — one-line count-by-type summary.

WplError is a union of three discriminated types (LexerError, ParseError, CompileError); each carries a Location with line/column/offset.

Lower-level access

For tooling that wants to operate on intermediate stages (e.g., custom transformers, IDE integrations):

import { tokenize, parse, compile } from "@gymbile/wpl-ai";

const lex = tokenize(source);                 // → { ok, tokens? } | { ok: false, errors }
const ast = parse(lex.tokens);                // → { ok, document? } | { ok: false, errors }
const out = compile(ast.document);            // → { ok, json, pointerMap } | { ok: false, errors }

The high-level compileWplAi is just a façade over these stages.

Validation re-exports

import {
  validate,                  // re-export from @gymbile/wpl-validator
  type ValidationResult,
  type ValidationError,
} from "@gymbile/wpl-ai";

// Validate already-compiled WPL JSON without re-compiling
const result = validate(plan, { catalog: { exercises: new Set([...]) } });

See @gymbile/wpl-validator for full validator API (rules, catalog support, error codes).

Vocabulary utilities

The DSL accepts well-known vocabularies for goals, exercises, equipment, etc. These are exposed as both arrays (for enumeration) and Sets (for fast membership checks):

import {
  GOAL_CATEGORIES,           // ['weight_loss', 'muscle_gain', 'endurance', ...]
  EXERCISE_CATEGORIES,
  CARDIO_MODALITIES,
  NUTRITION_CATEGORIES,
  MEDITATION_CATEGORIES,
  RECOVERY_CATEGORIES,
  HABIT_CATEGORIES,
  MUSCLE_GROUPS,
  EQUIPMENT,
  FITNESS_LEVELS,
  MEASUREMENT_METRICS,
  WEIGHT_UNITS,
  DISTANCE_UNITS,
  STREAK_TYPES,
} from "@gymbile/wpl-ai";

import { ALL_EXERCISES, isKnownExercise } from "@gymbile/wpl-ai";
import { suggest, bestMatch, validateExercise } from "@gymbile/wpl-ai";
import { validateVocabulary } from "@gymbile/wpl-ai";

isKnownExercise("push_up");                   // → true
suggest("pushp", { limit: 3 });               // → ['push_up', 'push_press', ...]
bestMatch("bnech_press");                     // → 'bench_press' (fuzzy)

validateVocabulary is what powers the DSL-level SemanticWarnings — it flags references to unknown vocabulary terms with suggestions for the closest match.


DSL by example

The DSL is whitespace-significant and case-sensitive on keywords (uppercase) but tolerant on values.

Minimal plan

PLAN "Minimal"
TYPE workout
VISIBILITY private

PHASES
  PHASE "P1" (1 weeks):
    WEEK 1:
      DAY Monday rest "Rest day":

Workout with sets/reps + RPE

DAY Monday training 45m "Upper Body":
  main straight_sets:
    push_up 3x8..12 rpe 7 rest 60 seconds
    dumbbell_row 3x10 weight 10 kg rest 60 seconds
    overhead_press 3x8..10 weight 8 kg rest 90 seconds
  • 3x8..12 — 3 sets of 8–12 reps (range)
  • rpe 7 — Rate of Perceived Exertion 7/10
  • weight 10 kg — load
  • rest 60 seconds — inter-set rest

Cardio

main:
  cardio running continuous 20 minutes intensity heart_rate_zone 3

Cardio supports continuous, intervals (with patterns), and timed prescriptions. Intensity types: heart_rate_zone, rpe, pace.

HIIT circuit with personalization

PERSONALIZATION
  RULES
    WHEN injury contains knee:
      replace jump_squat -> goblet_squat
    WHEN injury contains shoulder:
      exclude overhead_press

PHASES
  PHASE "HIIT Session" (1 weeks):
    WEEK 1:
      DAY Wednesday training 25m "Full Body HIIT":
        main circuit:
          rounds 4
          rest_between_rounds 90 seconds
          kettlebell_swing 3x12 rest 20 seconds
          jump_squat 3x10 rest 20 seconds
          burpee 3x8 rest 20 seconds

Holistic plans (workout + nutrition + meditation + recovery + habits)

PLAN "Holistic Wellness Week"
TYPE hybrid

PHASES
  PHASE "Week 1" (1 weeks):
    WEEK 1:
      DAY Monday training 60m "Strength + Mindfulness":
        main:
          push_up 3x10 rest 60 seconds
        meditation mindfulness 10 minutes:
          name "Post-Workout Calm"
        nutrition post_workout:
          name "Recovery Shake"
          timing after_workout 30 minutes
          macros protein 25..30 g, carbs 40 g
        recovery stretching 15 minutes:
          chest_stretch 30s x2 sides both
        habit water_intake:
          target 8 glasses
          frequency daily

Four full reference fixtures live in __tests__/fixtures.ts:

  • simple-upper-body — beginner workout
  • hiit-circuit-personalization — intermediate HIIT with personalization rules
  • holistic-wellness-week — full hybrid plan exercising every activity type
  • nutrition-with-timing — nutrition plan exercising meal-timing emission

These same fixtures are used as integration tests asserting that the compiled JSON validates cleanly against the canonical schema.


Pipeline architecture

┌──────────────────────────────────────────────────────────────────────────┐
│  src/lexer.ts          — DSL → Tokens                                    │
│                          Tracks line, column, offset on every token      │
│                                                                           │
│  src/parser.ts         — Tokens → AST (Document)                         │
│                          AST nodes carry SourceRange (range field)       │
│                          ~3500 lines covering full grammar               │
│                                                                           │
│  src/grammar.ts        — Single-source-of-truth keyword/enum tables     │
│                          (parser, lexer, vocabularies all reference this)│
│                                                                           │
│  src/compiler.ts       — AST → WPL JSON                                  │
│                          Threads a CompileContext through emission sites │
│                          for pointer-to-source-range tracking            │
│                                                                           │
│  src/compile-context.ts — `withSegment(seg, ast, fn)` records the       │
│                           current JSON pointer + AST source range as the │
│                           compiler descends, then pops on exit.          │
│                                                                           │
│  src/validator.ts      — DSL-level semantic warnings                     │
│                          (plan-type vs. activity-type consistency, etc.) │
│                                                                           │
│  src/vocabularies.ts   — Canonical vocabulary lists (goal categories,   │
│                          muscle groups, equipment, etc.)                 │
│  src/vocabulary-matcher.ts — Fuzzy-match unknown terms to nearest known │
│  src/exercise-matcher.ts   — Same for exercises specifically            │
│  src/exercises.ts          — ALL_EXERCISES catalog                      │
│                                                                           │
│  src/index.ts          — Public façade                                   │
└──────────────────────────────────────────────────────────────────────────┘

All AST node types live in src/types.ts (~70 exported interfaces/types). The error hierarchy is in src/errors.ts.


Two layers of validation

There are two distinct validation steps. Both run on every compile:

1. Compile-time semantic warnings (this package)

validateSemantics() walks the AST and emits SemanticWarning[] for:

  • Unknown vocabulary terms (with closest-match suggestions)
  • Activity types that don't fit the plan type (e.g., a workout-typed plan with only nutrition activities)
  • Unrecognised exercise refs
  • Other DSL-level coherence checks

These are advisory — they never make a compile fail. Surfaced in the playground as yellow squiggles.

2. Schema + semantic validation (@gymbile/wpl-validator)

After compile produces JSON, the validator runs two passes against the canonical schema:

  • Pass 1 — JSON Schema (Draft 2020-12) shape check via ajv
  • Pass 2 — semantic invariants (DUPLICATE_ID, UNRESOLVED_REF, INVALID_PRESCRIPTION, etc.)

Findings are returned as result.validation: ValidationResult. Severity matters: error-severity findings make result.validation.valid === false; warning-severity findings (e.g., PHASE_DURATION_MISMATCH) don't.

See validator error codes for the full list.


Safety posture (2.0)

Version 2.0 tightens the parser's safety-adjacent paths from silent-best-effort to fail-closed.

Fail-closed safety sections

Any ALL-CAPS section whose name matches REQUIRE*, CONTRA*, SAFETY*, PRECAUTION*, MEDICAL*, or CLEARANCE* that the parser doesn't recognise is now a hard parse error (ok: false). Previously these were skipped silently — a typo like REQUIREMENTS: erased every contraindication entry with no trace.

Strict contraindications

Unknown severity or action values on a contraindication block are now hard errors instead of silent downgrades. This closes the path where a typo'd action (e.g. "ignroe") would silently become "exclude", changing clinical intent without any signal.

Repairs ledger

Every tolerant normalisation that does succeed is recorded in result.repairs: Repair[] on the ok: true branch. Orchestrators can inspect this after every compile:

import { compileWplAi } from "@gymbile/wpl-ai";

const result = compileWplAi(source);

if (result.ok) {
  for (const r of result.repairs) {
    // r.type: "skipped_section" | "skipped_block" | "exercise_substitution"
    //       | "unknown_exercise" | "defaulted_value" | "discarded_modifier"
    // r.message: human-readable explanation
    // r.line / r.column: source position (when available)
    console.warn(`[repair:${r.type}] ${r.message}`);
  }
}

Typical repairs: fuzzy exercise substitutions (from/to/similarity), exercise refs kept verbatim that are absent from the catalog, unrecognised ALL-CAPS sections that don't match a safety pattern (safe to skip), and lenient-default fabrications.

Unknown-exercise warnings

Compile-time warnings now include a semantic warning for every exercise ref that is not in the ALL_EXERCISES catalog. Previously the 1.12 comments claimed this warning existed but it was never emitted.

Runtime stripping — @gymbile/wpl-validator's enforce()

The compile-time safety invariants above cover the DSL→JSON path. The complementary runtime half is enforce() from @gymbile/wpl-validator: given a compiled WPL JSON plan and a user's health profile, it strips or modifies any activities that violate the user's active contraindications — before the plan is ever served. The end-to-end invariant test (__tests__/safety-invariant.test.ts) asserts that a contraindicated exercise compiled through compileWplAi cannot survive a call to enforce().


Development

git clone https://github.com/gymbile/wpl-ai.git
cd wpl-ai
npm install
npm test          # 942+ tests across lexer, parser, compiler, validator, integration
npm run typecheck # tsc --noEmit
npm run build     # tsup → dist/ (ESM + CJS + .d.ts + .d.cts)

Test layout:

File Coverage
__tests__/lexer.test.ts 292 tests — every token type, edge cases, error positions
__tests__/parser.test.ts 140 tests — every grammar production
__tests__/grammar.test.ts Grammar table consistency
__tests__/compiler.test.ts 172 tests — compiler emission shapes
__tests__/compile-context.test.ts Pointer-tracking contract
__tests__/validator.test.ts DSL-level semantic warnings
__tests__/vocabularies.test.ts Vocabulary table integrity
__tests__/exercise-matcher.test.ts Fuzzy matching, suggestions
__tests__/integration.test.ts 107 end-to-end DSL → JSON tests
__tests__/compileWplAi-validation.test.ts Compiled output validates against @gymbile/wpl-validator
__tests__/pointer-map.test.ts Source ranges resolve to expected DSL substrings

Versioning & releases

Version Highlights
1.1.1 Fix NutritionTiming compiler emission (relative/absolute shape per schema); lex before_workout/after_workout keywords
1.1.0 Depend on @gymbile/wpl-validator; expose pointerMap on CompileResult; compiler emits schema-valid Activity shapes (prescription, name, target nesting); drops in-package schema-validator.ts
1.0.0 Initial extraction from wpl.dev — full lexer/parser/compiler/DSL-semantic validator

See CHANGELOG.md for full release history.

Releases are tagged on the GitHub repo and published to npm on tag push. Pin to a specific version (@gymbile/wpl-ai@1.1.1) for reproducibility.


Conformance

The compile-conformance corpus lives in the sibling gymbile/wpl repo at conformance/compile/fixtures/. The runner at __tests__/conformance.test.ts discovers it automatically via a relative path (../../wpl/conformance/compile/fixtures/) — check out wpl/ next to wpl-ai/ on disk and it works with no configuration. Set WPL_CORPUS_DIR to override the path (useful for CI that checks out repos at arbitrary locations). Run with npm test -- conformance. When the corpus is absent, the suite skips cleanly and exits 0.


Stability

  • Public API surface: compileWplAi and the types it returns are stable. Re-exports of validator types follow @gymbile/wpl-validator's semver.
  • Lower-level exports (tokenize, parse, compile, AST types in Document): semi-stable. Used by the wpl.dev playground; we won't break them lightly, but they're not as battle-tested as the high-level façade.
  • Vocabulary tables can grow additively in minor releases. New categories don't break consumers, but ID strings won't be renamed within a major.
  • DSL grammar is additive only within a major. New keywords/syntax may be added in minors; existing valid inputs continue to compile.
  • Output JSON shape follows the canonical WPL schema. Schema changes are coordinated through gymbile/wpl releases.

License

Apache-2.0. Patent grant included.

"WPL" and "Wellness Plan Language" are trademarks of Gymbile. The compiler is open under the license above; implementations may declare conformance ("WPL-compatible") but may not be named "WPL" or imply endorsement by Gymbile. Forks must rename. See the schema repo's trademark policy for details.


Related

About

Compiler for WPL-AI — the human/LLM-friendly DSL that compiles to canonical WPL (Wellness Plan Language) JSON

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages