What WPL actually is
- WPL-AI — a textual DSL the LLM emits. Looks roughly like indented blocks: PLAN, GOALS, PHASES, PHASE "..." (4 weeks), WEEK 1, DAY Monday training 45m "Lower body", WARMUP, MAIN straight_sets, exercise lines with sets, reps, RPE, and rest.
- The compiler + validator — Lex, parse, validate against a JSON Schema Draft 2020-12, run a rule evaluator that consumes a per-client ClientContext (injuries, equipment, menstrual-cycle anchor, flow days, flare windows, hormonal-contraception status) and applies rules of the form {condition, actions}. The action forbid_exercise strips matching exercises from the compiled plan tree before serving.
The architectural ancestry I'm borrowing from
- Terraform manifests are checked before they are applied. The plan phase exists exactly so the apply phase cannot be surprised.
- Kubernetes resources are admitted only after policy evaluation. The admission controllers are a separate, deterministic toolchain.
- LLVM IR is transformed under invariant-preserving passes. The bitcode is structurally legal at every stage; illegal IR fails fast.
Why not just JSON mode with the schema in the prompt
"Just use JSON mode with the schema" is harder than it looks at production scale. At long output lengths, schema-in-prompt is not enough. The compile/translate layer between the LLM and the strict format is doing real work. Removing it is not free.
Where the complexity comes from
- The type discriminator on the activity oneOf (26% of errors). WPL represents an activity as a tagged union — a type constant (resistance, cardio, mobility, ...) selects which inner prescription shape is valid. LLMs consistently flatten discriminated unions: they write one activity shape that mixes fields from multiple variants. This is well-documented in the structured-output literature; it's not a WPL problem, it's a JSON-Schema-on-LLM problem.
- additionalProperties: false (42% of errors). The schema refuses any field it doesn't know about. LLMs invent reasonable-sounding fields a human author would naturally include. Every invented field fails validation even if everything else is correct.
- ID uniqueness within week scope + slug pattern (672 errors). Activity IDs must match ^[a-z0-9][a-z0-9_-]*$ and be unique within a week. LLMs naturally reuse a stable ID for "the same exercise" across days — back_squat on Monday and Friday — which is the opposite of what the schema requires.
- Required fields at depth on the inner prescription (1,948 errors). exercise_ref, prescription.type, structured set/rep representations — each is required in specific positions in the tree. LLMs forget required fields at depth, especially in long outputs.
What "fails closed" actually means
- A compiled.ok = true result with a JSON plan tree that has passed every gate, or
- A compiled.ok = false result with a structured errors array, each entry containing:
- The error kind and type (PHASE_DURATION_MISMATCH, DUPLICATE_ID, ACTIVITY_BLOCK_MISMATCH, CYCLIC_SUBPLAN, UNRESOLVED_REF, ...)
- A human-readable message
- A precise location in the DSL source (line, column)
- A repair_hint carrying a JSON-pointer target and a copy-pasteable shape
Two things the contract explicitly does not do
Why the DSL is not the same as the schema
What changes for v0.7
- Allow additionalProperties on the activity block. Validator ignores unknown fields. The rule engine keeps reading only the fields it knows. We lose the ability to flag invented fields; we keep all safety scoring.
- Drop the type discriminator. Replace the oneOf with a single Activity shape where prescription fields are optional; runtime dispatch examines which fields are present. Discriminator-free schemas are the dominant pattern in LLM-friendly JSON.
- Compiler-generated IDs. LLM emits human-readable names; compiler computes canonical IDs and enforces uniqueness. Loses cross-reference convenience inside the LLM-emitted plan; gains a guaranteed-unique ID space.
The series continues → I Audited My Own AI-Safety Benchmark and Made the Numbers Look Worse (how I de-circularized my own evals) and My Safety Layer Had a Blind Spot in Production (the catalog drift that hid rehab clients from the safety layer).