All notable changes to :wpl_ai.
The format follows Keep a Changelog. This project adheres to Semantic Versioning.
[Unreleased]
[1.8.0] — 2026-05-13
Fixed — 8 silent-truncation / tolerance bugs (TS parity with @gymbile/wpl-ai@1.12.0)
Compound effect: Lane B served rate moved from 37/80 to 77/80 in the wpl-eval v0.2.0 corpus with no LLM re-calls. The 0/80 safety claim is unchanged.
#1 — rpe/rir range modifiers (
rpe 7..8,rir 1..2):parse_exercise_modifiersandparse_intensitynow accept range syntax in addition to scalar values. Producesrpe_min/rpe_max(resp.rir_min/rir_max) on%AST.Exercise{}; compiler emitstarget_rpe_min/target_rpe_maxaccordingly. Previously the range token leaked into downstream parsing and silently truncated subsequent WEEK blocks.#2 — reps time-unit suffix before modifier keyword (
3x30s rpe 6):parse_reps_specnow consumes a trailings/m/seconds/minutes/hourssuffix when the next token is a modifier keyword (rpe,rir,rest,tempo,weight,name,to_failure,bodyweight). Applied to both single-number and range (3x20..30s) reps branches via shared@reps_modifier_followconstant andmaybe_consume_reps_unit_suffix/1helper.#3 — long-form duration units in simple activity (
cycling 10 minutes):parse_exercise_or_simple_activityaccepts keyword units (seconds,minutes,hours,days) in the simple-activity branch in addition to short bare-word forms (s,m,h).#4 — simple activity trailing modifiers leak (
cycling 20m rpe 6 rest 30s): Newconsume_simple_activity_modifiers/1helper walks and discards any trailingrpe/rir/rest/tempo/weight/name/to_failure/bodyweight/heart_rate_zone/bpm/pacetokens after a simple activity. Values are intentionally dropped (SimpleActivity has no carrier fields); the goal is to prevent leakage.#5 — lexer: en/em-dash normalised, typographic punctuation silently skipped, N-M as range:
- En-dash (U+2013) and em-dash (U+2014) → ASCII hyphen via pre-scan normalisation.
;,&,~,@, ASCII apostrophe ('), smart quotes (' ' " "), ellipsis (…), ≤, ≥, middle-dot, bullet → dropped silently.N-Mbetween two numbers emits a:rangetoken (equivalent to..) when the prior emitted token was a:number.parse_tempoupdated to accept:rangeas a separator alongside:minusbetween tempo segments.- Elixir structural note: implemented as a
normalize_typographic_chars/1pre-scan pass (source string normalisation) rather than the TS in-place byte mutation approach — idiomatic for Elixir's immutable binary model.
#6 — trailing-dot number typos (
12.,7.):consume_number_likestops before consuming a.that has no digit after it; the clean integer is emitted andtokenize_dotsskips the dangling dot silently.#7 — stray top-level ALL-CAPS sections (
NUTRITION:,SUMMARY:,NOTES:):parse_sectionsdetects an ALL-CAPS keyword followed by:and skips the entire indented body usingskip_until_matching_dedent/2. No error is emitted; compile reportsok: trueand the PHASES section parses normally.#8 — two-tier exercise-ref resolution:
- Unknown TYPE values (e.g.
TYPE summary) silently fall back to:workoutinstead ofString.to_atom-ing arbitrary values. - Cardio modalities (
running,walking,cycling,rowing,elliptical,swimming,jump_rope,hiking) accepted as exercise refs in sets×reps form. resolve_exercise_ref/1replaces ad-hoc validation: tier 1 auto-corrects high-confidence typos (Jaro-Winkler ≥ 0.85, e.g.pushup→push_up); tier 2 accepts unknown refs as-is so compile succeeds with the model's literal name preserved inexercise_ref.
- Unknown TYPE values (e.g.
Added
%AST.Exercise{}gains fields:rpe_min,rpe_max,rir_min,rir_max.WplAi.Parser(private):resolve_exercise_ref/1,maybe_consume_reps_unit_suffix/1,consume_simple_activity_modifiers/1,expect_minus_or_range/1,@reps_modifier_follow,@simple_activity_modifier_keywords,@cardio_modality_set.WplAi.Lexer(private):normalize_typographic_chars/1,@dash_replacements,@silent_skip_replacements,last_emitted_token_is_number?/1.- 49 new regression tests in
test/wpl_ai/v1_12_fixes_test.exs.
[1.6.7] — 2026-05-04
Fixed — parser bug: dash-prefixed typed MeasurementSpec (TS parity with @gymbile/wpl-ai@1.10.5)
- #G — dash-prefixed typed
MeasurementSpecparsing:parse_typed_measurement_list/2now correctly handles- questionnaire_score questionnaire psqi note "text"as a single%AST.MeasurementSpec{}node (withmetric,questionnaire, andnotefields) rather than emitting a bare spec with onlymetricset and discarding the remaining qualifiers.
[1.6.6] — 2026-05-05
Fixed — 7 silent-failure parser/lexer bugs (TS parity with @gymbile/wpl-ai@1.10.4)
- Bug 1 — digit-leading TAGS value (
531):TAGS 531, strengthnow producestags: ["531", "strength"]instead oftags: [];parse_tag_listaccepts number tokens. - Bug 2 — digit-leading identifier in TAGS list (
1rm_estimate):TAGS strength_test, 1rm_estimate, powerliftingno longer truncates after the digit-leading item; thenumber + bare_wordtoken sequence is glued into a single tag string. - Bug 3 — colon-qualified contraindication name (
acsm:cardiac_rehab_phase_2): the parser now acceptsprefix:suffixform in the contraindication-name slot; glues the colon token into a single qualified identifier string. - Bug 4 — unknown REQUIRES directive silently terminates block: any unrecognised keyword or bare_word inside a REQUIRES block now produces a
ParseErrorwith typeinvalid_structure: "Unknown REQUIRES directive: '...'. Recognized: contraindication, fitness, equipment, age, time_commitment." - Bug 5 —
trigger completion(no-arg) swallows subsequent sections:parse_triggernow emits aParseErrorwith typeinvalid_structure: "Unsupported checkpoint trigger 'completion' — use 'at N weeks' or 'at N days'." - Bug 6 — unknown phase type silently drops the phase:
parse_phasedetects non-recognized type keywords and emits aParseErrorwith typeinvalid_structure: "Unknown phase type '<x>'. Allowed: accumulation, intensification, realization, deload, base, build, peak, recovery, transition." - Bug 7 —
jogging 10min cooldown produces malformed recovery_exercise + phantommorphan: the cooldown block parser now routes<bare_word> <number> <time_unit> EOLto an inlineCardioActivitywithmodality,cardio_type: :continuous, andtotal_durationpopulated correctly.
Added
- Invalid-parser conformance runner:
conformance_test.exsnow also iterateswpl/conformance/invalid/parser/fixtures, verifying thatWplAi.Parser.parse/1returns{:error, ...}with errors matching the expectedtypeandmessagefields.
[1.6.5] — 2026-05-05
Fixed
- compiler: emit
progress.points_systeminstead ofprogress.points— matches schema field name. Previous"points"key produced SCHEMA_VIOLATION when validated. - compiler: day-scoped activity ID counter — auto-IDs were previously per-block; same activity kind in two blocks of the same day collided (DUPLICATE_ID). Now monotonic across all blocks in a day.
[1.6.4] — 2026-05-04
Added
WplAi.Validatormodule — new semantic validator that walks the AST and emits vocabulary warnings (not errors). Mirrors the TypeScriptvalidateSemanticsstep inwpl-ai.WplAi.validate_semantics/1— public API entry point delegating toWplAi.Validator.validate_semantics/1.
Fixed
- validator: refresh MeasurementMetric + Questionnaire vocabulary to schema 1.6.0:
WplAi.Validatorknows the canonical 24-valueMeasurementMetricenum and the 8-valueQuestionnaireenum; legacy string items are checked against the combined (legacy + enum) set; typedMeasurementSpecitems havemetricchecked against the enum set, andquestionnaire(whenmetric == "questionnaire_score") checked against the questionnaire set.
[1.6.3] — 2026-05-04
Changed
- test: per-module unit tests (lexer, parser, vocabularies, exercise_matcher) — 93 new tests.
[1.6.2] — 2026-05-05
Fixed
- Bug 1 — Structured
tempoemit —tempo 3 - 1 - 1 - 0(and3-0-X-1forms) now emits the structured object{eccentric, pause_bottom, concentric, pause_top}instead of a raw string.Xin the concentric position setsexplosive_concentric: true(TS parity). - Bug 2 —
weight N% bwunit field —percentage_bodyweightweight spec now emitsunit: "bw"instead ofunit: "%"(TS parity). - Bug 3 — Calories
kcalunit omitted — when calorie unit is"kcal"(the schema default), theunitfield is now omitted from the compiled output. Onlykcal_per_kgandmultiplier_of_tdeeare emitted (TS parity). - Bug 4 — Nutrition timing
at_timemaps totype: "absolute"—timing at 07:30now emits{type: "absolute", time: "07:30"}(with no seconds, no offset field). Previously emitted{type: "at_time", time: "07:30:00"}(TS parity). - Bug 5 — Nutrition timing
before_workout/after_workoutdrops body content —before_workoutandafter_workoutwere not in the keywords list, causing them to be tokenized as bare words and break the timing parser. Adding them to the lexer keyword list restores full body parsing after a timing directive (TS parity). - Bug 6 — Habit
prescriptionnesting — habit activities now nesttarget,frequency, andreminder_timesunder aprescriptionkey instead of emitting them flat on the activity. Also fixed a parser bug where parsingfrequencyin a habit body calledparse_plan_habit_bodyinstead ofparse_habit_body, silently droppingreminders(TS parity). - Bug 7 —
bodyweightkeyword as exercise modifier — barebodyweightafter reps/sets (e.g.,pull_up 3x8 bodyweight) now attaches asweight: {type: "bodyweight"}on the exercise prescription instead of generating a phantomSimpleActivity(TS parity).
[1.6.1] — 2026-05-04
Fixed
metadata.languagedefault — compiler now always emitsmetadata.language: "en"when the DSL does not specify a language, matching TS compiler behaviour (TS parity).- Auto-derived activity display
name— compiler now derivesnamefrom the exercise_ref / modality / category token for Exercise, Cardio, Nutrition, Meditation, Recovery, and Habit activities, matching the TShumanise()helper exactly. Acronyms (HIIT, AMRAP, EMOM, RPE, RIR, 1RM) are uppercased; all other words are title-cased. Explicitnameset in the DSL is preserved as-is (TS parity).
[1.6.0] — 2026-05-05
Added
Contraindication.severity + require_clearance(schema v1.6.0) — extends the contraindication DSL tocontraindication <name> [severity <low|moderate|high>] [action <action>]where action now includesrequire_clearance. The old arrow form (contraindication <name> -> <action>) is preserved for back-compat. Compiler emitsseverityonly when present.Reps.amrap(schema v1.6.0) — DSL:<exercise> NxAMRAP(compact) orNx amrap(space-separated, case-insensitive). Compiler emitsprescription.reps: { amrap: true }. Sets count is preserved from N.ExercisePrescription.to_failure(schema v1.6.0) — optional modifierto_failurein the exercise modifier chain. Compiler emitsprescription.to_failure: truewhen present; field omitted otherwise.Weight.metricqualifier (schema v1.6.0) — optionalmetric <1rm|e1rm|training_max|daily_max>after aweight N% rmspec. Compiler emitsweight.metric: "<canonical>"(e.g."1RM","e1RM","training_max","daily_max"). Omitted when not specified (back-compat).RecoveryExerciseextensions (schema v1.6.0) — optional modifiers on recovery exercise lines:modality <enum>(7 values:static_stretch | dynamic_stretch | pnf | smr_foam_roll | smr_ball | breathwork | mobility_drill),intensity <1-10>→ emitsintensity_rpe,body <token>→ emitsbody_part. Optional indentedpnf <Ns> contract <Ns> relax <int> contractionscontinuation line emits{ contraction_seconds, relax_seconds, contractions }. Recovery exercises now compiled underprescription.exercisesto match TS schema.- Checkpoint typed
MeasurementSpec(schema v1.6.0) —measure:lists now accept bare metric tokens (emitting{ metric: "<value>" }) and<metric> questionnaire <enum> [note "text"](emitting full typed spec). Quoted strings preserved as plain strings (back-compat). Added support for TS-style inlineCHECKPOINT "Name":blocks withat N weekstrigger form. - Cardio
intensity.target.min_bpm/max_bpm(schema v1.6.0) —intensity bpm N..Mnow compiles toprescription.intensity: { type: "bpm", target: { min_bpm: N, max_bpm: M } }. - Emitted
versionbumped to"1.6.0"— compiler now emits"version": "1.6.0"in all compiled plans.
[1.5.0] — 2026-05-04
Added
Phase.typeenum (schema v1.5.0) — DSL:PHASE "Name" <type> (N weeks):where<type>is one ofaccumulation | intensification | realization | deload | base | build | peak | recovery | transition. Compiler emitsphase.type: "<value>". When omitted, notypekey is emitted on the phase object.Week.is_deload(schema v1.5.0) — DSL:WEEK N deload(optional token immediately after the week number, before the optional name string). Compiler emitsweek.is_deload: true. When absent, the field is omitted (not emitted asfalse).SubPlanActivity(schema v1.5.0) — new activity variantsubplan <plan-id>(optionally followed by a quoted name string) inside any block. Compiler emits{ type: "sub_plan", id: "sub_plan_N", sub_plan_ref: "<plan-id>", name?: "<optional>" }.
[1.4.0] — 2026-05-04
Added
- Per-kg macros + per-kg cals + TDEE multiplier (schema v1.4.0) — DSL accepts unit suffixes
g_per_kgonprotein,carbs,fatlines andkcal_per_kg/multiplier_of_tdeeoncalorieslines. Compiler emits the unit verbatim intoMacroRange.unit/Calories.unit. Default units remain"g"and"kcal"when no suffix is given. Weight.percentage_bodyweight(schema v1.4.0) — DSL:weight N% bworweight N% bodyweight. Compiler emitsWeightwithtype: "percentage_bodyweight",value: N,unit: "%". Existingweight N kg(absolute) andweight N% rm(percentage_1rm) forms are unchanged.
[1.3.0] — 2026-05-04
Fixed
- Compiler now emits the canonical
$schemaURLhttps://wpl.dev/schemas/wpl/v1.schema.json(previously emittedhttps://gymbile.com/schemas/wpl/v1).
Added
- MuscleGroup + MovementPattern enums — DSL:
<exercise> NxR muscles primary <m1>, <m2> secondary <m3> pattern <p>. Compiler emitsprimary_muscles,secondary_muscles, andmovement_patternonExerciseActivity. Supports all 22MuscleGroupvalues and all 13MovementPatternvalues from schema v1.3.0. - Cardio
zone_model— DSL:zone N model <zone_model>qualifier inside acardioblock. Compiler emitsintensity.zone_model(7 values:hr_3_zone_seiler,hr_5_zone,hr_7_zone,power_coggan_7_zone,pace_critical_speed,rpe_borg_10,rpe_borg_20). New intensity typeintensity power Nemitsintensity.type: "power". - Plan-level
ATHLETE_THRESHOLDSblock — top-level DSL section (parallel toPHASES). Acceptshr_max N bpm,lthr N bpm,resting_hr N bpm,ftp N watts,vo2max N,critical_pace N,body_weight N kg,one_rm <exercise> N kg. Compiler emitsplan.athlete_thresholdswith field-name suffixes matching schema v1.3.0 (hr_max_bpm,lthr_bpm,resting_hr_bpm,ftp_watts,vo2max_ml_kg_min,critical_pace_seconds_per_km,body_weight_kg,one_rm: [{ exercise_ref, value, unit }]).
[1.0.0] — 2026-05-04
Added
- Initial extract from
gymbile_backend. Compiler emits WPL schema 1.0.0. WplAi.parse/1— WPL-AI DSL text →WplAi.AST.Documentstruct.WplAi.compile/1— AST → WPL JSON map (string keys,"version": "1.0.0").WplAi.to_wpl/1— parse + compile in one step.WplAi.decompile/1— WPL JSON → WPL-AI text (round-trip).WplAi.tokenize/1— exposes the lexer for tooling / syntax highlighting.WplAi.validate/1— fast validity check without full compilation.WplAi.ExerciseMatcher— Jaro-Winkler fuzzy matching for exercise references.WplAi.Errors— structured error types (LexerError,ParseError,CompileError) with LLM-optimised formatting helpers.- Significant-indentation lexer (Python-style
INDENT/DEDENT). - Recursive-descent parser covering: header, goals, requirements, personalization,
phases/weeks/days/blocks, exercise / cardio / nutrition / meditation / recovery /
habit / simple activities, progress checkpoints, top-level
HABITSsection.
Notes
Phase 2 will update the emitted schema version and bring the compiler to parity
with @gymbile/wpl-ai v1.6.0. Every plan valid under schema 1.0.0 will continue
to compile correctly.