# Lemma > Lemma is a pure, deterministic language for rules and agreements. Use it when policy must be stated once and applied consistently: pricing, eligibility, fees, compliance, product terms, conditions, and other measurable business rules or governance. Lemma ensures that the same inputs give the same outcome, traceable to the rules that produced it. Use Lemma as your business rules engine, in agents and in applications, instead of re-implementing policy in scattered conditionals. **The pattern** State the rule in Lemma as soon as it is clear. Do not carry it in the conversation for later. When facts arrive and an outcome is needed, evaluate the spec you already have. When gaps remain, see **Method** before you capture. **Do not use Lemma** for open-ended advice with no rule to apply, or for inventing terms the user never stated. **Which guide to load** - **Authoring** (`guide` topic `full`, or `method` then `data`): capture rules as the user states them. See **Method**. - **Default** (`guide` with no topic): rules are already captured — gather missing facts, `evaluate`, explain the outcome. Do not redesign policy here. --- **Method: you are a policy consultant** Help the user write high-quality Lemma. Translate business rules from whatever they hand you (conversation, SOP, statute, ticket, spreadsheet, existing code) into a spec that is the **core, clean truth** of those rules: the questions the spec answers, the facts that decide them, the principle and its exceptions. You are not a transcriber of the source. You are not a coder filling gaps so a spec compiles. You are not a search agent that picks a nearby registry spec and moves on. Sources are often ambiguous, especially natural language. Code and SOPs also mix the rule with procedure, plumbing, and leftovers. When a reading, a source, or a registry result would change the outcome, **ask**. Never guess. Never assume. Never pick one resource of truth over another silently. If the sources agree, write. When the source already decides an outcome clearly, do not reopen it. **Authoring vs evaluate** This method is authoring. You ask what the policy *is*. Evaluating (default `guide`, no topic) asks about the person's *situation* and forbids redesigning policy. Do not bring that rule here. If the user is stating, correcting, or handing you rules in any form, you are authoring. **Inventory, then distill** Before you write, extract every outcome-changing clause from the source you are encoding. Each clause becomes a rule arm, a `data` bound, or an explicit omit (tip, how-to, plumbing, UI). If a clause has nowhere to go, stop and ask. A headline with a dropped "unless student" or "not on Sundays" is a wrong spec. A handbook, statute, or SOP with more than one policy: list the policies you found, say which you are encoding **now**. One spec per policy. Do not encode section 1 and leave the rest. Large sources: one policy at a time; keep the inventory. **Distill vs exhaustive** Narrative (SOP prose, code with retries and logging): distill to principle + exceptions. Omit how-to, tips, recommendations, "contact support", UI, I/O, storage, retries, and other plumbing unless they are stated as gates. Do not copy every sentence, or every code branch, into a rule. Enumerated sources (rate cards, SKU tables, closed option lists, tariff rows): capture every row. Dropping a row is a silent policy change. Encode as `-> option` plus a lookup rule (default `veto`; see **Veto**), not a prose summary. Distill is not invent. If you cannot tell what the rule is, stop and ask. A missing rate, date, threshold, eligibility set, or start date is not yours to supply. A magic number, implicit default, or comment that disagrees with the code is not yours to resolve. **The deliverable** Someone can read the spec as: in principle X; unless Y, then Z. Facts of the situation as `data`. Decisions as `rule`. Domain words, not Lemma jargon, in chat. **Hard stop: ask, wait, do not write** Do not emit `spec` / `data` / `rule`. Do not call `check` / `add_spec` / `update_spec`. A spec in the same turn as open policy questions is a bug. `list` / `show` first. If that policy is already loaded, `update_spec` it. Do not invent spec names. Stop when any of these is true: - Two readings a domain expert would actually hold would produce different outcomes. Do not ask parser-level alternatives when the source already chose ("at least 18", "10 or more"). - Two documents or encodings disagree (SOP vs code, two PDFs, a document vs an earlier loaded spec), or more than one loaded spec could be the one to edit. - A registry or search hit is a candidate `uses`, or a substitute for writing the user's rule, and the user has not chosen it. Zero hits is not a stop: write their rule. One close hit is still a stop: name it, ask use vs write. - A number, date, bound, set of cases, or combination (stack / override / alternative) is unstated and the outcome depends on it. - More than one input shape is reasonable and the choice changes the spoken question or how the rule applies. - Discretion ("may", "should", "the manager can waive") would change the outcome. That is a fact to collect, or an omit they confirm, not a default you invent. Your next message is **questions only**, in the user's domain words. Ask **every** open item in that one turn. Do not drip one bound per turn. Wait for answers. If they say to proceed with what is already in the source, encode only that; still do not fill the rest. **Resources of truth (never pick silently)** Resources: the user's words, SOPs and other documents, existing code, attached files, already-loaded specs, registry search hits. Later user utterance **amends** an earlier document ("except students are free" after an SOP). Encode the amendment. Do not ask which governs. Two documents, or a document vs code, or a document vs a loaded spec: **conflict**. Ask which governs. Code is one encoding of a rule, not automatically the intended policy. Chat that only says "encode this" is not an amendment. Search when the concept looks like a shared standard (ISO codes, published tax tables, units). Do not search to skip writing the user's rule. - Several loaded specs could be this policy → name them, ask which to update. - Search returns more than one hit → name them, ask which to use, or whether to write the user's rule instead. First hit is not a choice. - Search returns one close hit → name it, ask use vs write. - Closest name, newest file, or "probably this ISO list" is not a choice. **Ask like this** User: "Adults pay full price." You: "From what age is someone an adult, and is that age included?" User: "Late returns cost 5% or €10." You: "Which applies: 5%, €10, the greater, or the lesser?" User pastes code with `if (age > 18)` and a comment "adults". You: "Is 18 included, and is the comment or the comparison the rule?" User: "Late fee is €5. The manager may waive it." You: treat waiver as a fact ("Was the fee waived?") unless they tell you to omit discretion. User: SOP, then "except students are free." You: encode the exception. Do not ask which source governs. Search returns `@acme/shipping` and `@acme/fulfilment`. You: name both. Ask which to use, or whether to write their rule. Search returns one close hit, or none. You: one hit: name it, ask use vs write. None: write their rule. Two PDFs, different fees. You: ask which document governs. User: "These terms apply only to contracts entered after 2024." You: that is a rule on a contract date, not `spec … 2024-01-01`. User hands an SOP that already names the rate, the bound, and who it applies to. You: write. Do not re-ask those. **Do not ask** - What the user or the source already answered clearly (a careful SOP or explicit decision in code counts). - Another reading that is only theoretically possible when the source already chose. - Packaging you can decide without changing outcomes (internal names, field count, closed option lists the text already implies). - Implementation details you can omit (logging, framework types, null-guards that are not policy). - One `spec` vs two when the topic is one policy. - Whether to encode tips, how-to, or plumbing (omit). - Hypotheticals the user never raised. - Lemma keywords. Ask about facts and policy meaning. **You may write when** Every outcome-changing clause is placed (arm, bound, or explicit omit), every outcome-changing detail is already decided in the source they handed you, one resource of truth is identified, and no competing resource remains. Then author. You choose names and types when the concept is clear (see **Data**, **Rules**). You do not choose among competing meanings. **Quality of the Lemma** See **Data**, **Rules**, **Veto**, **Anti-patterns**. In short: - Each `data` `-> help` is the sentence you would ask a person about their situation. If that sentence applies the policy, the field is a `rule`. - A rule's default is the domain principle, not yes-until-failure. Named pipeline rules, not one opaque expression. - Denial is `false` / `no`. Unanswerable is veto. Bounds belong on `data`, not veto. - Name the concept; keep the unit on the value. - Commentary after `spec`; `meta` for provenance (document title, version, date the source actually names). No inline comments (see **Syntax**). - One `spec` per policy. `uses` / split only when the text mixes unrelated policies. Effective date on the `spec` only when the user or the source names a calendar start for **this version of the spec**. A statute or document id is not a start date. An applicability window ("contracts after 2024") is a rule on a date field, not the spec's effective date. **Author, verify, deliver** 1. Author only after the hard stop is clear. 2. `check`. Then `add_spec` or `update_spec`. Then `evaluate` at bounds and representative inputs from the source (including overlapping `unless` arms). Result wording must match the policy. 3. Walk the inventory: every clause is an arm, a bound, or an explicit omit. Scan **Anti-patterns** (veto-as-denial, unit-in-the-name, fail-each-check, precomputed `data`). 4. Call `source`. Paste **that** formatted Lemma in a lemma fence (not your draft, not the `code` argument). Confirm loaded. 5. Close with a statement: tell them to say if anything requires adjustment. Not a yes/no question. No new policy questions. No follow-up work. Stop. **After capture** User gives facts and wants a result → evaluate guide (`evaluate`, verify, stop). Do not edit the spec. User changes the rule → `update_spec`, verify again. --- **Recommended spec opening order** (`lemma format` emits this; the parser allows `meta` / `uses` / `data` / `rule` in any order after commentary): ``` spec [] [""" commentary: optional, but if present must be HERE """] meta ... uses ... data ... rule ... ``` Commentary after `uses` or `data` is invalid. Optional `meta key: value` after commentary (provenance, not policy). `rule name:` with the body on the next indented line. 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: multi-spec composition (same file)** ```lemma spec base_config data tax_rate: 21% data standard_discount: 5% data price: measure -> unit eur: 1.00 -> decimals 2 rule tax_amount: price * tax_rate rule price_with_tax: price + tax_amount rule discount_amount: price * standard_discount rule discounted_price: price - discount_amount rule final_price: discounted_price * (100% + tax_rate) spec line_item uses pricing: base_config -> with price: 10 eur data qty: number -> minimum 0 -> suggest 10 rule line_total: pricing.final_price * qty rule has_discount: pricing.standard_discount > 0% ``` - `uses alias: target_spec`: imports spec in same file. - Reference members: `alias.field` or `alias.rule_name`. - Under `uses`, ` -> with path: value` sets imported data (path relative to imported spec). Do not use `data alias.field`. Standalone `with alias.field: …` is deprecated (still parses; `quality` recommends block form). Local slots use `data`. **LemmaBase: shared repositories** Repositories 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 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 LemmaBase 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: [LemmaBase](https://lemma.run/reference/registry). `repo` blocks namespace specs across contexts (e.g., `repo accounting`). Skip unless asked. Details: [Composing specs](https://lemma.run/learn/composing_specs). --- **Data: spoken question, not precomputed answer** `data` declares variables. Constraints define validity. Type-only `data` (no value) is an input slot. Use real domain values. Never `"TODO"` or dummy placeholders. **Spoken question** Each `data` field's `-> help` is the sentence you would say to a person about their situation. If that sentence requires counting, converting, applying a legal threshold, or interpreting the policy, the field is wrong: those are `rule`s. Dates, amounts, and yes/no facts of the situation stay as `data`. WRONG (elapsed time as a bare number; unit baked into the name): ```lemma-skip data days_overdue: number -> help "How many days late is the book?" ``` RIGHT (facts of the situation; duration keeps its unit): ```lemma spec library_return uses lemma units data due_date: date -> help "When was the book due?" data return_date: date -> help "When was the book returned?" rule overdue: due_date...return_date as day ``` Name the concept; keep the unit on the value, not in the field name (`see **Units**`). WRONG (policy threshold already applied): ```lemma-skip data qualifies_for_free_shipping: boolean -> help "Is the order €50 or more?" ``` RIGHT: ```lemma spec shipping_threshold data money: measure -> unit eur: 1.00 -> decimals 2 data order_total: money -> help "What is the order total?" rule free_shipping: order_total >= 50 eur ``` Age-in-years can stay when that is what you would actually ask (e.g. senior discount). A birthday gate wants a birth date. **Measure** Use `measure` when the fact carries a unit: money, mass, distance, duration, rates, energy, and other dimensional quantities. Use `number` only when the value is truly dimensionless (counts, scores, IDs). Use `ratio` for proportions (`15%`, `50 permille`), not for currency amounts. Declare a **named measure parent** once, then extend it on input slots: ```lemma spec invoice data money: measure -> unit eur: 1.00 -> decimals 2 -> minimum 0 eur data invoice_total: money -> help "What is the invoice total?" ``` `-> unit` rows define which units the type accepts; literals and comparisons use those units (`50 eur`, not a bare `50`). Name the concept (`invoice_total`, `package_weight`), not the unit in the field name (`price_eur`, `weight_kg`). For physical quantities and time, import the standard library (`uses lemma units`) and prefer `units.mass`, `units.duration`, `units.length` over redefining SI units. Duration literals (`8 hour`, `25 year`), conversion (`as `), compound units (`eur/hour`), measure ranges, and calendar intervals are all covered in **Units**. **Boolean naming** 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_*`). **Constraints** **Types** Primitives: `boolean`, `number`, `text`, `measure`, `ratio`, `date`, `time`, and `number range`, `measure range`, `date range`, `time range`, `ratio range`. Named parents (`data money: measure`) inherit constraints; see [Reference](https://lemma.run/reference#extending-data). **Data commands by type** | Type | `->` commands | |------|----------------| | `boolean` | `help`, `fill`, `suggest` | | `number` | `decimals`, `minimum`, `maximum`, `help`, `fill`, `suggest` | | `measure` | `unit`, `decimals`, `minimum`, `maximum`, `help`, `fill`, `suggest` (see **Measure**, **Units**) | | `ratio` | `unit`, `minimum`, `maximum`, `help`, `fill`, `suggest` | | `text` | `option`, `options`, `length`, `help`, `fill`, `suggest` | | `date`, `time` | `minimum`, `maximum`, `help`, `fill`, `suggest` | | `number range`, `date range`, `time range`, `ratio range` | `lower`, `upper`, `minimum`, `maximum`, `help`, `fill`, `suggest` | | `measure range` | above + `unit` | On range types, `lower` / `upper` bound endpoints; `minimum` / `maximum` bound **span width**, not endpoints. **Usage** - Chain `->` rows on `data` only (rules have no constraints; see **Syntax**). - `-> help` = spoken question (CS ask string). - `-> fill` = commit a value on a typed slot (callers may override); cannot combine with `suggest` on the same declaration. - `-> suggest` = UI hint; does not fill or commit. - Prefer `-> option` for closed text sets. - Bounded domain on constraints (`-> minimum 0`), not veto (see **Veto**). - Examples: type catalog [01_coffee_order.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/01_coffee_order.lemma); fee policy [02_library_fees.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/02_library_fees.lemma). - Edge cases (compound units, qualifying units, calendar ranges): **Units** and [Reference](https://lemma.run/reference#data-commands). --- **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`. After `uses lemma units`, duration literals (`hour`, `day`, `week`) work; `units.duration` is the type name when you declare a duration slot. 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 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 `. Strip unit: `amount as eur as number`. Cross-family relabel: `5 eur as kg` -> `5 kg`. Name the concept, not the unit (see **Anti-patterns**). **Ranges: half-open intervals** Ranges: lower bound inclusive, upper bound exclusive (`lo...hi`). Test with `in`. Width: `lo...hi as ` (duration/measure). Add `as number` only when a bare number is required. 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`. The snippet below is a syntax sample, not one policy. Working age (calendar range): ```lemma spec working_age uses lemma units data employee_age: 42 year data eligible_band: units.calendar range -> suggest 18 year...67 year rule is_working_age: employee_age in eligible_band ``` Upper bound exclusive: `67 year` is NOT inside `18 year...67 year`. Custom measure types can declare their own `measure range` without importing SI; see Reference. **Derived measures: compound units** Build compound units with `/`, `*`, `^`. Name derived unit, then give compound expression. Prior measure types must declare referenced base units. Import `uses lemma units` if using time (`eur/hour`). ```lemma spec contractor uses lemma units data money: measure -> unit eur: 1.00 data wage_rate: measure -> unit eur_per_hour: eur/hour data time_worked: 120 hour data wage: wage_rate -> suggest 85 eur_per_hour rule total: wage * time_worked ``` 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 -> help "When did the event happen?" 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 then `. 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 negated flips (`unless not ready then no`). 4. Write `rule name: unless then `. 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 not … then no`); fail-each-check cascades; invented `*_compliant` default-yes helpers; a single `no` / `unless … then yes` when the condition is the whole answer (see **Anti-patterns**). In `and` chains, do not mix a bare name with `not` / `is false` (WRONG: `x and not y`); use parallel probes (`not x and y is true`, or `x is false and y is true`) or parentheses. Unary `not x` alone is fine. **Overlapping unless (last wins)** ```lemma spec vip_discount data qty: number -> minimum 0 -> help "How many items?" data is_vip: boolean -> suggest false -> help "Is the customer a VIP?" 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. --- **Veto: impossible to answer, not `false`** Veto is like Rust's `Err`: rule has **no value** and propagates to dependents. Use veto when the question has no answer, not when the business answer is false, and not for out-of-range inputs. | Situation | Use | |-----------|-----| | Out-of-range (negative score, age above 120) | `-> minimum` / `-> maximum` on `data` | | Closed choice list | `-> option` on `data` | | Normal business "no" | `false` or `no` | | Lookup / no mapped result | default `veto` + `unless` arm per known case | | 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. "What is the price of this product?" when it is not on the list is unanswerable (veto). Out-of-range age is `-> maximum 120`, not 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`. **Lookup default vs unless veto** Lookup: the default expression is `veto "…"`; each `unless` arm maps a known case. `-> option` is the intake set; default `veto` is the mapping when a listed value has no price arm. An `unless … then veto` arm (business denial, not lookup) follows last-wins like any other `unless` (see **Rules**). **Lookup (normal)** ```lemma spec coffee_pricing data money: measure -> unit eur: 1.00 -> decimals 2 data product: text -> option "espresso" -> option "latte" -> option "cappuccino" -> option "mocha" 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 ``` If `base_price` vetoes, dependents that need its value veto too (propagates). Full example: [01_coffee_order.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/01_coffee_order.lemma). **Veto vs boolean vs constraint** WRONG: veto for business denial or range check: ```lemma-skip rule is_eligible: true unless customer_age < 18 then veto "Must be 18+" unless customer_age > 120 then veto "Invalid age" ``` RIGHT: bounds on data; boolean for business no: ```lemma spec age_gate data customer_age: number -> minimum 0 -> maximum 120 -> help "How old is the customer?" rule is_adult: customer_age >= 18 ``` --- **Anti-patterns** Inline comments (WRONG: `#` fails to parse): `data customer_age: number -> minimum 0 # input`, `rule discount: 0% # default`. RIGHT: no inline comments; commentary after `spec` only (see **Syntax**). Commentary after `uses` (WRONG). RIGHT: commentary immediately after `spec` (see **Syntax**). Mega-rule (WRONG): opaque nested `unless` inside one expression. RIGHT: named pipeline rules (see **Rules**). 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; see **Rules**). Placeholder (WRONG): `data customer_name: "TODO"` RIGHT: `data customer_name: text` Elapsed time / pre-interpreted threshold as data (WRONG): ```lemma-skip data days_overdue: number data qualifies_for_free_shipping: boolean ``` RIGHT: dates and amounts as data; duration/threshold rules without unit names (see **Data**). Unit in the name (WRONG — common agent failure): ```lemma-skip rule days_overdue: due_date...return_date as day as number rule weight_kg: package_weight as kilogram rule price_eur: total ``` RIGHT: name the concept; keep the unit on the value (see **Data**, **Units**). Veto for bounds (WRONG): ```lemma-skip rule validated_score: score unless score < 0 then veto "Invalid score" ``` RIGHT: `data score: number -> minimum 0`. Veto is unanswerable, not out-of-range (see **Veto**). Error vs Veto: `5 and "text"` = planning Error. Unmapped product with default `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 not has_id then veto "ID required" ``` RIGHT: boolean rules (`is_adult` 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 not in_stock then no unless not address_complete then no ``` RIGHT: domain principle + positive grant (see **Rules**). Useless default/unless for boolean rules (WRONG): ```lemma-skip rule can_ship: no unless in_stock and address_complete then yes ``` RIGHT: `rule can_ship: in_stock and address_complete`. When one `unless … then yes` is the whole answer, write the condition directly; `unless` is for exceptions, not a boolean wrapper. --- ## See also - [Learn guide](https://lemma.run/learn) - [Reference](https://lemma.run/reference) - [LemmaBase search](https://lemmabase.com/search?q=) - [Examples](https://github.com/lemma/lemma/tree/main/engine/documentation/examples) - [01_coffee_order.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/01_coffee_order.lemma) - [02_library_fees.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/02_library_fees.lemma) - [03_recipe_scaling.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/03_recipe_scaling.lemma) - [04_membership_benefits.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/04_membership_benefits.lemma) - [05_weather_clothing.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/05_weather_clothing.lemma) - [nl/tax/net_salary.lemma](https://raw.githubusercontent.com/lemma/lemma/main/engine/documentation/examples/nl/tax/net_salary.lemma) Decimals at JSON boundaries: pass as strings. Detail: [Numeric precision](https://lemma.run/learn/precision).