Switch to dark mode

learn

Composing Specs

Once a Spec can reference its own Data and Rules, it can import another Spec with Uses and reference its members through an alias. This chapter covers composition, temporal versions, planning checks, and (last) repositories and registry imports for advanced usage.

For syntax details see Using other Specs.

Importing another Spec

A Lemma source file defines one or more Specs. Each Spec is a namespace of Data and Rules. With Uses, you import another Spec under an alias and refer to its members through that alias. Read a value with alias.field, use a dependency Rule with alias.rule_name, and reuse an imported Data declaration as a parent type: data country: iso.code.

spec premium_membership

data discount_rate: 10%

rule free_shipping_threshold: 50

rule monthly_bonus_points: 100


spec membership_benefits

uses membership: premium_membership

data monthly_spend: 150

rule discount: monthly_spend * membership.discount_rate

rule bonus_points: membership.monthly_bonus_points
  • uses membership: premium_membership binds alias membership to Spec premium_membership.
  • uses premium_membership uses an implicit alias: last path segment of the target name.
  • membership.discount_rate reads Data from the imported Spec.
  • membership.monthly_bonus_points uses a Rule from the imported Spec.

A minimal two-Spec example:

spec base_employee

data name:   "John Doe"
data salary: 5000


spec manager

uses employee: base_employee

with employee.name:   "Alice Smith"
with employee.salary: 8000

rule manager_bonus: employee.salary * 0.15

uses alias: target imports a Spec under an explicit alias; a bare uses target defaults the alias to the last path segment. Read Data and Rules as alias.field or alias.rule_name.

Setting Data on an import (With)

Uses registers an import; it does not set runtime values on the dependency. Use with alias.field: … to assign a literal or reference to a Data slot declared on the imported Spec:

spec inner

data x: number
  -> suggest 1


spec outer

uses i: inner
with i.x: 42

rule r: i.x
  • with i.x: 42 sets Data x on inner to 42.
  • with i.x: i is an error: i is a Spec reference, not a value.
  • with copy: i.x is a parse error: With must use an import path on the left (with i.x: …), not a local name.

The left-hand side of With must be an import path; local slots use Data. Runtime inputs to run can still override bound import paths (e.g. i.x) where planning allows.

See Setting Data on an imported Spec.

Temporal versions

The same Spec name may appear several times with different effective datetimes (spec Name 2025-05-01). Each declaration is immutable; you add a new row on the timeline instead of editing history in place.

spec pricing

data base_price: 20
data quantity:        number

rule total: base_price * quantity


spec pricing 2025-01-01

data base_price: 25
data quantity:   number

rule total: base_price * quantity

Run with different effective dates:

lemma run pricing --effective 2024-06-01   # uses base_price: 20
lemma run pricing --effective 2025-06-01   # uses base_price: 25

Which row applies at evaluation time is determined by the --effective instant (CLI) or Accept-Datetime (HTTP) for the Spec you run.

Unpinned vs pinned Uses

Form Meaning
uses dep Unpinned. Every temporal version of dep whose range intersects this Spec's range can matter. Planning may split this Spec into temporal slices at dependency effective_from boundaries. Values resolved through the import can change when dep gains a new row.
uses dep 2025-06-01 Pinned to an instant. One body of dep active at that datetime, including its transitive imports. Later rows of dep do not affect this edge.

An unpinned import (uses pricing) follows the dependency's timeline; a pinned import (uses p: pricing 2025-06-01) freezes the dependency at that instant.

Unpinned: values follow the timeline

spec policy

rule discount: 10


spec policy 2025-05-01

rule discount: 25


spec shop 2025-01-01

uses p: policy

rule d: p.discount

Evaluating shop in early 2025 yields d = 10; from May 2025 onward, d = 25. The Uses line is unchanged; the resolved policy body changes at the slice boundary.

A consumer with no effective date on its Spec line (origin) still gets slices when a dependency's effective_from falls inside its range.

Pinned: freeze a dependency at one instant

spec finance

data money: measure
  -> unit eur 1.00


spec finance 2025-07-01

data money: measure
  -> unit eur 1.00
  -> unit usd 0.91


spec shop 2025-01-01

uses f: finance 2025-02-01

data money: f.money
data price: money

rule doubled: price * 2

shop keeps the February 2025 finance shape (EUR only) even when you evaluate in September 2025. Pinning locks a tariff or regulation snapshot into the consumer.

Planning checks

Before run, planning validates the dependency graph. Two temporal failures authors see most often:

Temporal coverage (gaps)

Unpinned uses dep requires dep to exist for every instant in the consumer's temporal range. If the consumer starts in January but dep only exists from July, planning fails (no active version / not active at that instant).

Fix: add an earlier spec dep row, move the consumer's effective_from later, or pin uses dep 2025-08-01 so the consumer no longer requires dep to cover its whole range.

Interface compatibility (contract changes)

When unpinned imports span multiple rows of the same dependency, every row the consumer touches must expose compatible types for the same names (Rule result types, Data types, compatible measure units, etc.). New names only in a later row are fine if the consumer does not use them.

Incompatible example: money gains a usd unit in a later finance row while shop still has unpinned uses finance and data price: finance.money. Planning reports that the dependency changed its interface between temporal slices.

Fix: pin uses f: finance 2025-02-01, or add a new temporal row of shop written for the new finance body.

Coverage is presence on the timeline; interface validation is type sameness across slices the consumer needs.

Same name, different bodies

You may import an earlier temporal row that shares your base name:

spec finance 2026-01-01

data rate: 1


spec finance 2027-01-01

uses f26: finance 2026-01-01

rule ok: f26.rate

Planning rejects a Uses edge that resolves to the same Spec body (self-reference):

spec finance

uses finance


spec finance 2026-01-01

uses finance 2026-01-01

A later row must not use unpinned uses finance when that resolves to itself at that row's instant. Pin an earlier row (uses f26: finance 2026-01-01) instead.

Cycles across temporal rows (2026 → 2027 → 2026) are rejected as dependency cycles.

Repositories

When Specs in the same workspace share a name, Repo blocks namespace them so they do not collide. Cross-repo targets use a repo qualifier on the Uses line:

repo accounting

spec invoice

data total: 1


spec billing

uses inv: accounting invoice

rule out: inv.total

Cross-repo targets use a repo qualifier on the Uses line (accounting invoice). When you run a Spec from the workspace (main) repository, use its unqualified name; the CLI does not pick between two loaded Specs with the same name in different repos.

Most workspaces never need Repo blocks: files without one belong to the implicit workspace repository. Registry dependencies live in their own @owner/name repositories (see Registry below).

Registry

When Specs live outside your workspace, import them from a registry with @owner/repo qualifiers:

spec invoicing

uses @iso/countries alpha2

data price: measure
  -> unit eur 1

data country: alpha2.code

rule tariff: 0 eur
  unless country is "NL" then price * 5%

rule total: price + tariff

Fetch dependencies before running:

lemma fetch --all           # fetch all @... dependencies into lemma_deps/
lemma fetch @iso/countries -f   # force re-fetch if content changed

Importing from a registry uses @owner/name on the uses line (for example uses iso: @iso/countries alpha2). The engine does not fetch the network; load sources with lemma fetch (or your embedder) first. See Registry.

Evaluating a composed Spec

You always run (or call the API for) a named root Spec in a repository, with an effective instant:

lemma run membership_benefits --effective 2025-03-01
  • The engine selects the consumer's temporal slice that contains that instant.
  • Unpinned imports: paths such as p.discount use dependency bodies resolved for that slice.
  • Pinned imports: the dependency body stays at the pinned instant.

To inspect the static interface for that temporal slice (types, constraints, rules after normalize):

lemma show membership_benefits --effective 2025-03-01

Overlay-aware discovery of what a concrete run still needs comes from each rule's missing_data, not from show.

Quick decision guide

Goal Pattern
Track compatible dependency rows across the consumer's lifetime uses dep (unpinned)
Lock a regulation / tariff / spec version at a known date uses dep 2025-06-01
Import an earlier row of the same Spec name uses prev: finance 2026-01-01
Reuse a Data shape from a library uses iso: @iso/countries alpha2 and data x: iso.code
Set Data on an imported Spec with alias.field: value
Read import Data in a Rule rule r: alias.field

If planning fails, check whether the message is coverage, interface, self-reference, or cycle. Each remedy is different (see above).

Next up

Numeric precision: exact rational arithmetic, limits, and client parsing.