lemma 0.9.3

A pure, declarative language for business rules.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
# 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).