# Lemma
> Lemma: declarative business rule language. Translate natural-language policy to readable, deterministic `.lemma` specs. Inputs derived from rules — declare `data` with constraints. No runtime value invention.
Goal: produce Lemma source for human reading and system evaluation. Prefer named pipeline rules. No clever one-liners. Split unrelated policies to separate specs.
Authoring method, discovery loop, and output contract: see **Method**. Explanations opt-in (`explain: true` / `lemma run -x`).
**NO INLINE COMMENTS.** Lemma has no `#`, `//`, or `--` comment syntax. See **Syntax** for opening order and commentary placement.
---
**Method: write as a policy consultant, not a transcriber**
Syntax alone can encode the wrong policy. Never invent numbers, dates, or outcomes the source does not decide — ask those gaps before writing. No retroactive questions after authoring. No follow-up ideas after deliver.
**Process**
1. **Gather** — Collect sources (prose, statute, ticket, answers, field names). Inventory. Do not write Lemma yet.
2. **Interpret** — Restate as questions the system must answer. Name actors, triggers, windows. One coherent policy → one `spec` (you decide).
3. **Interrogate** — Ask only when the **source leaves a policy outcome undecided**. Phrase questions in plain language a non-Lemma author understands. Do not invent. Do not Author until answers or explicit acceptance.
4. **Scope** — One `spec` per coherent policy; split / `uses` only when the source clearly mixes unrelated domains; `@…` catalogs; temporal date only when the author confirms a start date — never guess.
5. **Model** — Public `data` API and named `rule` pipeline (you choose field shape — do not ask the author). Domain-principle `unless` defaults (see **Rules**). Denial = `false`/`no`; unanswerable = veto (see **Veto**). Prefer raw inputs over precomputed numbers. Encode rule gates the source states; omit advisory how-to that is not a gate. Name booleans for the fact you mean; prefer the natural predicate with `-> suggest` for the usual case (`data item_damaged: boolean -> suggest false`). Do not invent awkward opposites (`item_undamaged`) or negated names (`not_*`, `no_*`, `has_no_*`).
6. **Author** — After questions resolved. Commentary after `spec`, `meta` for provenance; no inline comments (see **Syntax**). Spec is the only documentation.
7. **Verify** — `check`, `show`, `evaluate` at boundaries (half-open ranges). Result text must match conditions. Then `add_spec` / `update_spec` to load.
8. **Deliver** — Always paste the full Lemma source in chat so the user can verify what was saved or loaded (use `source` if you need the loaded text). Confirm loaded. Close with a statement, not a question — e.g. tell them to say if anything requires adjustment. No pitch for more work. Stop.
**What to ask (and what not to)**
Ask when the source does not decide a **policy outcome**, e.g. a threshold boundary ("above €50" vs "€50 or more"), or whether rules start on a specific date.
Do **not** ask:
- Modeling shape (`data` field count, option vs boolean, one input vs two)
- Spec packaging (one vs split) when the source is one coherent policy
- Whether advisory prose (tips, how-to steps, "contact support") is "in scope" — encode gates; omit advice
- Edge cases the source never mentions
Plain language: "Is this policy effective from a specific date, or have these rules always been in effect?" — not "Effective date — pin a date on the spec, or leave undated?"
**Output contract (mandatory)**
- **Before Author** — Ask the few real gaps (if any). Wait. Do not invent.
- **At Deliver** — Full Lemma source in chat for user verify + loaded confirmation + assumptions the author confirmed. No new policy questions. No pitch for more work. Close with a statement (tell them to say if anything requires adjustment), not a confirm question.
**Principles**
- Spec is the only documentation: `-> help`, commentary, `meta` (see **Syntax**).
- `data` names are a public API. Prefer `-> option` for closed text sets.
- Decomposition is output quality: named intermediates, not mega-rules.
**Worked example**
Source: *"Standard shipping is €4.95. Free shipping when the order is €50 or more."*
Ask before writing (real gaps only):
1. At exactly €50 — free shipping or €4.95?
2. Is this shipping policy effective from a specific date, or have these rules always been in effect?
WRONG to ask: one `shipping` spec or split; how many `data` fields for destination; express vs economy when the source never mentions them; whether packing tips are hard rules.
After answers: Model / Author / Verify / `add_spec`. Fee rule uses domain-principle default (usual fee, free when threshold met). If author says free at €50 or more → `order_total >= 50 eur`. Paste source, deliver and stop.
---
**Mandatory spec opening order:**
```
spec <name> [<effective>]
[""" commentary — optional, but if present must be HERE """]
uses ...
data ...
rule ...
```
Commentary after `uses` or `data` is invalid. No `#`, `//`, `--` comments. Use descriptive names. Put user explanations outside code fences — never inside ` ```lemma ` blocks.
**Gotchas (parse errors)**
- No `or` operator. Disjunction via `unless` chains or separate boolean rules.
- Constraints (`-> help`, `-> option`, `-> minimum`, etc.) apply to `data` only. Rules have no constraints.
---
**Organization: spec → rule**
Default: one file, one implicit repo. No `repo` blocks unless multi-namespace workspace requested. Structure: **spec → rule**.
**Spec** = namespace for `data` and `rules`. **Rule** = named computed value. Reference rules by name; engine resolves if name is data or rule. One file can have multiple specs.
Hierarchical names: `spec employee/contract`. Effective date for temporal changes: `spec pricing 2026-01-01`.
Commentary placement: see **Syntax**.
**Example A — minimal single-spec file**
```lemma
spec pricing 2026-01-01
"""
Pricing rules for bulk and member discounts.
"""
data qty: number
data base_price: 100
data is_member: false
rule vat_amount: base_price * 21%
rule price_with_vat: base_price + vat_amount
rule bulk_discount:
qty >= 100 and price_with_vat > 500
rule discount: 0%
unless qty >= 10 then 10%
unless bulk_discount then 15%
unless is_member then 20%
rule discount_amount: base_price * discount
rule price_with_discount: base_price - discount_amount
```
**Example B — multi-spec composition (same file)**
```lemma
spec base_config
data standard_discount: 5%
data tax_rate: 21%
data base_price: number -> minimum 0 -> suggest 100
rule tax_amount: base_price * tax_rate
rule price_with_tax: base_price + tax_amount
rule discount_amount: base_price * standard_discount
rule discounted_price: base_price - discount_amount
rule final_price: discounted_price * (100% + tax_rate)
spec line_item
data qty: number -> minimum 0 -> suggest 10
uses pricing: base_config
rule line_total: pricing.final_price * qty
rule has_discount: pricing.standard_discount > 0%
spec simple_order
uses line: line_item
with line.qty: 100
rule order_total: line.line_total
rule effective_unit_price: order_total / line.qty
```
- `uses alias: target_spec`: imports spec in same file.
- Reference members: `alias.field` or `alias.rule_name`.
- `with alias.field: value`: sets imported data. Do not use `data alias.field`. Local `with name: …` invalid — use `data name: …`.
**LemmaBase — shared specs from the registry**
Specs on [LemmaBase.com](https://lemmabase.com) imported with `@` repo qualifiers. Search: [lemmabase.com/search?q=](https://lemmabase.com/search?q=) (e.g. `?q=finance`).
```lemma
spec invoicing
"""
Invoice lines using ISO country codes from LemmaBase.
"""
uses lemma units
uses iso: @iso/countries alpha2 2026-01-01
data price: measure
-> unit eur 1
data country: iso.code
rule tariff: 0 eur
unless country is "NL" then price * 5%
rule total: price + tariff
```
Forms:
- `uses @user/repo spec_name`: import registry spec (alias = spec name)
- `uses alias: @user/repo spec_name`: import with alias (`iso.field`)
- `uses @user/repo spec_name 2026-01-01`: pin effective date
Reference imported members: `iso.code`. Detail: [Registry](https://github.com/lemma/lemma/blob/main/cli/documentation/reference/registry.md).
`repo` blocks namespace specs across contexts (e.g., `repo accounting`). Skip unless asked. Details: [Composing specs](https://github.com/lemma/lemma/blob/main/cli/documentation/learn/composing_specs.md).
---
**Natural language → Lemma**
Request: *"Library charges €0.25/day for regular books, €0.50 for reference, €1 for new releases. First offense gets 50% off. Grace period 3 day except new releases. Block checkout if fee exceeds €10."*
Map logic:
- Book types → `data book_type` with `-> option` constraints
- Due and return dates → `data` input slots; overdue days derived by a rule
- First offense → `data` input slot
- Per-day rates → `rule daily_fee` with unless branches
- Grace period → `rule is_in_grace_period`
- Fee pipeline → `rule total_fee`, `rule final_fee`
- Checkout block → `rule can_checkout: yes` with `unless final_fee > 10 eur then no` (boolean, not veto)
**Example C — library fees (full spec)**
```lemma
spec library_fees
uses lemma units
data money: measure
-> decimals 2
-> unit eur 1.00
-> minimum 0 eur
data book_kind: text
-> option "regular"
-> option "reference"
-> option "new_release"
data book_type: book_kind
data is_first_offense: boolean
data due_date: date
data return_date: date
rule days_overdue: (due_date...return_date) as day as number
rule daily_fee: 0 eur
unless book_type is "regular" then 0.25 eur
unless book_type is "reference" then 0.5 eur
unless book_type is "new_release" then 1 eur
rule is_in_grace_period: days_overdue <= 3
unless book_type is "new_release" then no
rule total_fee: days_overdue * daily_fee
rule final_fee: total_fee
unless is_first_offense then total_fee * 50%
unless is_in_grace_period then 0 eur
rule can_checkout: yes
unless final_fee > 10 eur then no
```
---
**Data: constraint definitions, not placeholders**
`data` declares variables. Constraints define validity. Type-only `data` (no value) is an input slot. Use real domain values. Never `"TODO"` or dummy placeholders.
For which inputs a rule still needs at runtime, call MCP `guide` with no topic (evaluate guide): `list` → `show` once → `evaluate` → ask one `missing_data` field → repeat. Load `guide` topic `full` only when authoring new specs. `show` is the static catalog — not a required-input checklist. `-> help` is the literal CS ask string; do not replace the question with a different one.
**Example D — typed data (coffee order)**
```lemma
spec coffee_order
data money: measure
-> decimals 2
-> unit eur 1.00
-> unit gbp 1.17
-> unit usd 0.84
-> minimum 0 eur
data product: text
-> option "espresso"
-> option "latte"
-> option "cappuccino"
-> option "mocha"
data size: text
-> option "small"
-> option "medium"
-> option "large"
data age: number
-> maximum 100
-> minimum 0
data number_of_cups: number
-> maximum 10
data has_loyalty_card: boolean
```
- `age`, `number_of_cups`: input slots (type-only + bounds)
- `money`: custom measure type with units, decimals, minimum
- `product`, `size`: text enumeration via `-> option` (prefer over veto for static sets)
**Example E — data patterns**
Input slot:
```lemma
spec intake
data customer_age: number -> minimum 0 -> maximum 120
```
Fixed policy constant:
```lemma
spec fiscal_policy
data tax_rate: 21%
```
Text enumeration:
```lemma
spec membership
data status: text
-> option "active"
-> option "inactive"
```
Typed alias:
```lemma
spec accounts
data money: measure -> unit eur 1.00
data wallet: money -> minimum 0 eur
```
With help text (literal CS ask string):
```lemma
spec payroll
data pay_period: text
-> option "month"
-> option "week"
-> help "How often you are paid."
```
Boolean with usual-case suggest:
```lemma
spec returns
data item_damaged: boolean
-> suggest false
-> help "Item damaged?"
```
Constraints chain: `-> minimum`, `-> maximum`, `-> option`, `-> unit`, `-> decimals`, `-> suggest`, `-> help`, etc. Details in Reference.
---
**Standard library — `uses lemma units`**
Lemma embeds SI bases, derived compounds (force, pressure, energy, power, frequency, electrical), imperial, area/volume, and information (`bit`/`byte`) in `repo lemma` / `spec units`. Import: `uses lemma units`. Reference types: `units.mass`, `units.duration`, `units.length`, `units.force`. Unit names: **singular only** (`8 hour`). Length uses American `meter`. Durations (`hour`, `day`, `week`) require `units.duration`. No Celsius/Fahrenheit (kelvin only).
```lemma
spec logistics
"""
Physical shipment constraints using SI units from the standard library.
"""
uses lemma units
data package_weight: 12 kilogram
data shift_length: 8 hour
data route_distance: 45 kilometer
rule weight_grams: package_weight as gram
rule shift_hours: shift_length as hour
rule distance_km: route_distance as kilometer
rule is_heavy: package_weight > 20 kilogram
rule is_long_shift: shift_length >= 8 hour
```
Prefer `units.mass`, `units.duration`, `units.length` over redefining units. Convert in family: `as <unit>`. Strip unit: `amount as eur as number`. Cross-family relabel: `5 eur as kg` -> `5 kg`.
**Ranges — half-open intervals**
Ranges: lower bound inclusive, upper bound exclusive (`lo...hi`). Test with `in`. Width: `(lo...hi) as <unit> as number` (or `(lo...hi) as <unit>`). Bare `as number` on date/measure ranges fails. Typedefs: `number range`, `date range`, `measure range`, `ratio range`. Month/year intervals: `uses lemma units` and inline literals (`18 year...67 year`) or `units.calendar range`.
```lemma
spec eligibility
uses lemma units
data employee_age: 42 year
data performance_score: 75
data package_weight: 45 kilogram
data hire_date: 2024-01-15
data review_date: 2024-06-30
data discount_rate: 15%
data eligible_band: units.calendar range
-> suggest 18 year...67 year
data score_band: number range
-> suggest 0...100
rule is_working_age: employee_age in eligible_band
rule is_top_score: performance_score in 90...100
rule is_heavy: package_weight in 30 kilogram...80 kilogram
rule in_discount_band: discount_rate in 0%...50%
rule in_q2: hire_date in 2024-04-01...2024-07-01
rule review_days: (hire_date...review_date) as day
rule span_years: (1990-05-20...2024-06-15) as year
```
Upper bound exclusive: `67 year` is NOT inside `18 year...67 year` (returns false). Declare range slots on `data` for reuse, inline `value in lo...hi` for one-off.
Range over custom measure type (no SI import needed):
```lemma
spec freight
data weight: measure
-> unit gram 1
-> unit kilogram 1000
data load_band: weight range
-> suggest 30 kilogram...80 kilogram
rule inside_band: 45 kilogram in load_band
rule band_width: (30 kilogram...80 kilogram) as kilogram
```
**Derived measures — compound units**
Build compound units with `/`, `*`, `^`. Name derived unit, then give compound expression. Prior measure types must declare referenced base units (`eur`, `hour`, `employee`, `results`). Import `uses lemma units` if using time (`eur/hour`).
```lemma
spec contractor
uses lemma units
data money: measure
-> unit eur 1.00
data headcount: measure
-> unit employee 1
data outcome: measure
-> unit results 1
data wage_rate: measure
-> unit eur_per_second eur/second
-> unit eur_per_hour eur/hour
data productivity: measure
-> unit result_per_employee results/eur/hour/employee
data premium_per_head: measure
-> unit eur_hour_per_employee eur_per_hour/employee
data time_worked: 120 hour
data wage: wage_rate -> suggest 85 eur_per_hour
data yield_rate: productivity -> suggest 3 result_per_employee
rule total: wage * time_worked
rule is_high_yield: yield_rate >= 2 result_per_employee
```
Layer compound units: `eur_per_hour` builds on `eur` and `hour`. Dimensional checks run at plan time.
**Date predicates relative to `now`**
`now` is evaluation/effective instant. Import `uses lemma units` for duration windows.
| Form | Meaning |
|------|---------|
| `date in past` / `in future` | Before / after `now` |
| `date in past N day` / `in future N day` | In last / next N duration units |
| `past N day` / `future N day` | Relative date-range window |
| `date in calendar year\|month\|week` | Current calendar period |
| `date in past\|future calendar year\|month\|week` | Adjacent calendar period |
| `date not in calendar year\|month\|week` | Not current calendar period |
```lemma
spec recency
uses lemma units
data event_date: date
rule recent: event_date in past 7 day
rule this_year: event_date in calendar year
```
---
**Rules and unless: last matching clause wins**
Default expression, then `unless <condition> then <result>`. Source order; **bottommost match wins**. General first, specific last. Snake_case names; boolean predicates (`is_eligible`, `can_ship`). Named pipeline rules — no opaque mega-expressions.
**Domain-principle default**
Default is not "prefer yes/no." It is the answer **in principle for this rule's domain**. Experts must read top-to-bottom as: "In principle X; unless Y, then Z."
1. Name the question (`can_ship` → "Can we ship this order?").
2. Before special cases, what is true in principle? That is the default — from *this* rule's domain, not optimism or "start true and subtract failures."
3. What positive facts change the answer? Those are `unless` conditions — not `… is false then flip`.
4. Write `rule name: <principle> unless <positive conditions> then <exception>`.
Examples by domain: shipping often earned (`no` unless grant); discount often `0%`; fees use the policy's usual fee — not "free unless expensive."
Forbidden: double denial (`yes` / `unless … is false then no`); fail-each-check cascades; invented `*_compliant` default-yes helpers.
```lemma-skip
rule can_ship: no
unless in_stock
and address_complete
then yes
```
**Example F — overlapping unless (last wins)**
```lemma
spec vip_discount
data qty: number
data is_vip: boolean
rule discount: 0%
unless qty >= 10 then 10%
unless qty >= 50 then 20%
unless is_vip then 25%
```
VIP ordering 75 items gets **25%** (not 20%): both `qty >= 50` and `is_vip` match; bottommost wins.
**Example G — progressive unless chain**
```lemma
spec rules_and_unless
data is_premium: yes
data base_price: number -> minimum 0
data qty: number -> minimum 0
rule total_before_discount: base_price * qty
rule discount_percentage: 0%
unless qty >= 10 then 10%
unless qty >= 20 then 15%
unless is_premium then 20%
rule discount_amount: total_before_discount * discount_percentage
rule total_after_discount: total_before_discount - discount_amount
rule shipping_cost: 15
unless total_after_discount >= 100 then 10
unless total_after_discount >= 200 then 0
rule final_total: total_after_discount + shipping_cost
```
**Example H — short pipeline sketch**
```lemma
spec shipping_fees
uses lemma units
data item_weight: units.mass
data order_total: number -> minimum 0
rule base_rate: 22
rule weight_surcharge: 0
unless item_weight > 5 kilogram then 7.5
rule final_shipping: base_rate + weight_surcharge
unless order_total >= 100 then 0
```
---
**Veto: impossible to answer, not `false`**
Veto is like Rust's `Err` — rule has **no value** and propagates to dependents. Use veto when answer is impossible, not when business answer is false.
| Situation | Use |
|-----------|-----|
| Invalid/out-of-domain input | `unless ... then veto "reason"` |
| Unmapped choice / no rule applies | default `veto` + unless arm per choice |
| Normal business "no" | `false` or `no` |
| Test veto without propagating | `x is veto` (returns boolean) |
**Litmus test:** Can the question be answered? If yes, even when the answer is negative, use `true`/`false`. If the question itself is unanswerable for this input, use veto. "Is the customer eligible?" is always answerable (`true` or `false`). "What is the price of this coffee?" when the product is not on the menu is unanswerable (veto).
A vetoed rule is not `false`. `x is false` does not match a vetoed `x`. To test whether a rule vetoed, use `x is veto`.
Place veto unless clauses **last** to override other branches.
**Example I — enumeration with default veto**
```lemma
spec choice_mapping
data choice_field: number
rule selected: veto
unless choice_field is 1 then true
unless choice_field is 2 then false
unless choice_field is 3 then true
```
Default `veto` if unlisted. Each `unless` maps known choice. `false` is valid answer for choice 2.
**Example J — veto lookup + propagation**
```lemma
spec coffee_pricing
data money: measure
-> unit eur 1.00
-> decimals 2
data product: text
data size: text
rule base_price: veto "Unknown type of coffee"
unless product is "espresso" then 2.5 eur
unless product is "latte" then 3.5 eur
unless product is "cappuccino" then 3.5 eur
unless product is "mocha" then 4 eur
rule size_multiplier: veto "Unknown size of coffee"
unless size is "small" then 80%
unless size is "medium" then 100%
unless size is "large" then 120%
rule price_per_cup: base_price * size_multiplier
```
If `base_price` vetoes, `price_per_cup` vetoes automatically (propagates).
**Example K — veto vs boolean**
WRONG — veto for business decision:
```lemma-skip
rule can_checkout: veto
unless fee <= 10 eur then true
```
RIGHT — veto for invalid input, boolean for business logic:
```lemma
spec checkout_policy
data money: measure
-> unit eur 1.00
-> decimals 2
data customer_age: number
data fee: money
rule age_validation:
true
unless customer_age < 18 then veto "Customer must be 18 or older"
unless customer_age > 120 then veto "Invalid age"
rule can_checkout: yes
unless customer_age < 18 then no
unless fee > 10 eur then no
```
**Example L — veto propagation with unless fallback**
```lemma
spec scoring
data score: number
data use_default: boolean
rule validated_score: score
unless score < 0 then veto "Invalid score"
rule result: validated_score
unless use_default then 50
```
If `validated_score` vetoes but `use_default` is true, `result` is 50. Unless branch avoids needing vetoed value.
**Workflow checklist**
1. **Scope**: one spec per coherent policy; compose with `uses`; skip `repo` unless needed.
2. **Inputs**: every user-supplied fact is `data` with constraints.
3. **Outputs**: every answerable question is a `rule`.
4. **Factor**: intermediate calculations as named rules.
5. **Unless**: default first, general to specific, vetoes last.
6. **Validate**: `check`, then `show`, then `evaluate` (MCP `guide` with no topic for `missing_data` intake; topic `full` only when authoring).
7. **Advanced**: `uses lemma units`, ranges, compound units when needed.
---
**Anti-patterns**
Inline comments (WRONG — `#` fails to parse):
```lemma-skip
data customer_age: number -> minimum 0 # input
rule discount: 0% # default
```
Commentary after `uses` (WRONG). RIGHT: commentary immediately after `spec` (see **Syntax**).
Mega-rule (WRONG):
```lemma-skip
rule final_total:
base_price * qty
- base_price * qty * (0% unless qty >= 10 then 10% unless is_premium then 20%)
+ (15 unless base_price * qty >= 100 then 10 unless base_price * qty >= 200 then 0)
```
Decomposed pipeline (RIGHT):
```lemma
spec order_pricing
data base_price: number -> minimum 0
data qty: number -> minimum 0
rule subtotal: base_price * qty
rule discount_percentage: 0% unless qty >= 10 then 10%
rule discount_amount: subtotal * discount_percentage
rule total_after_discount: subtotal - discount_amount
rule shipping_cost: 15 unless total_after_discount >= 100 then 10
rule final_total: total_after_discount + shipping_cost
```
Hardcoded input (WRONG): `rule discount: 10 * 0.1`
RIGHT: `data qty: number` then `rule discount: qty * 0.1`
Wrong unless order (WRONG — VIP gets 20% not 25%):
```lemma-skip
rule discount: 0%
unless is_vip then 25%
unless qty >= 50 then 20%
```
RIGHT: specific override last (`qty` tiers first, `is_vip` last).
Placeholder (WRONG): `data customer_name: "TODO"`
RIGHT: `data customer_name: text`
Error vs Veto: `5 and "text"` = planning Error. `unless age > 120 then veto "…"` = runtime Veto.
Veto-as-rejection (WRONG — denial is answerable; see **Veto**):
```lemma-skip
rule is_eligible: true
unless age < 18 then veto "Must be 18+"
unless has_id is false then veto "ID required"
```
RIGHT: boolean rules composed with `and` (`is_old_enough and has_valid_id`).
Unnecessary `repo` (WRONG). RIGHT: single-file `spec` without `repo`.
No `or` (WRONG): `rule is_eligible: is_adult or has_guardian` — see **Syntax**; use `unless` or separate booleans.
Constraints on rules (WRONG): `rule discount: 10% -> help "…"` — `->` is data-only (see **Syntax**).
Domain-blind polarity / double denial (WRONG):
```lemma-skip
rule can_ship: yes
unless in_stock is false then no
unless address_complete is false then no
```
RIGHT: domain principle + positive grant (see **Rules**):
```lemma-skip
rule can_ship: no
unless in_stock and address_complete then yes
```
---
## See also
- [Learn guide](https://github.com/lemma/lemma/blob/main/cli/documentation/learn/readme.md)
- [Reference](https://github.com/lemma/lemma/blob/main/cli/documentation/reference/readme.md)
- [LemmaBase search](https://lemmabase.com/search?q=)
- [Examples](https://github.com/lemma/lemma/tree/main/cli/documentation/examples)
Decimals at JSON boundaries: pass as strings. Detail: [Numeric precision](https://github.com/lemma/lemma/blob/main/cli/documentation/learn/precision.md).