Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions plugins/allium/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,10 @@ The `allium` rule is path-scoped — it's automatically fetched when you work wi
This plugin was ported from the upstream Allium repository. For reproducibility and auditing, the last upstream commit used as a reference for this port is recorded below:

- Repository: [juxt/allium](https://github.com/juxt/allium)
- Commit: `82da292e989d518f79189fdfef4446d0d517c277`
- Author: Henry Garner
- Date: 2026-04-24 15:40:26 +0100
- Message: Simplify CLI section in README
- Commit: `8af2da4e557560277b6621e09785b63f5c262fb9`
- Author: yavorpanayotov
- Date: 2026-06-19 17:59:41 +0300
- Message: Bump plugin version to 3.4.0 (#50)

If you update the plugin from upstream in future, please update this section with the new commit hash and date.

Expand Down
41 changes: 40 additions & 1 deletion plugins/allium/references/language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,17 @@ contract Codec {

Contract bodies contain typed signatures and annotations (`@invariant`, `@guidance`). Entity, value, enum and variant declarations are prohibited inside contracts. Types referenced in signatures must be declared at module level or imported via `use`.

A zero-argument operation uses an empty parameter list:

```
contract Registry {
list_things: () -> Set<Foo>
health: () -> Status
}
```

`name: () -> ReturnType` is the only form for parameterless operations; a bare arrow (`name: -> ReturnType`) is not valid.

### Referencing contracts in surfaces

Surfaces reference contracts in a `contracts:` clause. Each entry uses `demands` or `fulfils` to indicate the direction of the obligation:
Expand Down Expand Up @@ -344,7 +355,7 @@ Primitive types have no properties or methods. For domain-specific string types

**Compound types:**
- `Set<T>` — unordered collection of unique items
- `List<T>` — ordered collection (use when order matters). A compound field type declared explicitly on entities
- `List<T>` — ordered collection (use when order matters). A compound field type declared explicitly on entities. Populate it with a list literal (`[ ... ]`); see [Literals](#literals)
- `Sequence<T>` — ordered collection produced by ordered relationships and their projections. `Sequence` is a subtype of `Set`: an ordered collection is assignable where an unordered one is expected, but not the reverse. `List<T>` is a field type you declare explicitly; `Sequence` is the collection type the checker infers when a relationship is ordered. Both carry ordering semantics, but they occupy different positions in the grammar
- `T?` — optional (may be absent). Reserved for genuinely optional fields: a user's nickname, a note that may or may not exist. For fields whose presence depends on lifecycle state, use a `when` clause instead (see below).

Expand Down Expand Up @@ -1202,12 +1213,21 @@ ensures: not exists document
permissions: { "documents.read", "documents.write" }
features: { basic_editing, api_access }

-- List literals
priorities: [ high, medium, low ]
retry_delays: [ 1.minute, 5.minutes, 30.minutes ]
empty_queue: []

-- Object literals (anonymous records, used in creation parameters and trigger emissions)
data: { candidate: candidate, time: time }
data: { slots: remaining_slots }
data: { unlocks_at: user.locked_until }
```

Set literals use braces (`{ ... }`); list literals use square brackets (`[ ... ]`). Both are valid in any expression position — default declarations, `ensures` clauses, object literal field values and nested within other literals.

A list literal `[expr1, expr2, ...]` produces `List<T>`, the explicitly-declared ordered collection field type. It does not produce `Set` (use a set literal for that) or `Sequence` (which the checker infers from ordered relationships and their projections, and is never written as a literal). The element type `T` is inferred from the elements; all elements must share a type. Heterogeneous elements such as `[1, "a"]` are a type error. An empty list literal `[]` takes its element type from the target field's declared type. Unlike set literals, which collapse duplicates, list literals retain them: `[low, low]` is a two-element list. List literals are the only way to populate a `List<T>` field from the spec layer.

Object literals are anonymous record types. They carry named fields but have no declared type. Use them for ad-hoc data in entity creation parameters and trigger emission payloads where defining a named type would add ceremony without clarity. Object literals always require explicit `key: value` pairs; `{ x }` is a set literal containing `x`, not an object with shorthand.

### Black box functions
Expand Down Expand Up @@ -1551,6 +1571,20 @@ default Role editor = {
}
```

The type name may be qualified with an import alias, the same as type references in rules and `ensures` clauses:

```
use "./gradient-policies.allium" as gp

default gp/Policy my_policy = {
name: "default",
max_gradient: 0.5,
decay: [ 1.0, 0.5, 0.25 ]
}
```

The field set and types are resolved against the imported module's entity declaration, so the checker validates the literal against the canonical schema and catches drift at check time. Previously `default` was the only site that required an unqualified type name even though qualified names work elsewhere.

---

## Modular specifications
Expand Down Expand Up @@ -1908,6 +1942,9 @@ A valid Allium specification must satisfy:
13. All lambdas are explicit (use `i => i.field` not `field`)
14. Inline enum fields cannot be compared with each other (whether on the same entity or across entities); use a named enum to share values across fields
14a. Dot-method calls on collections must use a recognised built-in name (`.count`, `.any()`, `.all()`, `.first`, `.last`, `.unique`, `.add()`, `.remove()`). Unrecognised dot-methods are errors. Domain-specific collection operations use free-standing black box function syntax
14b. A list literal (`[ ... ]`) produces `List<T>`; its elements must all share a type (heterogeneous elements are a type error)
14c. An empty list literal (`[]`) takes its element type from the target field's declared type; an empty list literal with no inferable target type is an error
14d. List literals retain duplicate elements; set literals (`{ ... }`) collapse them

**Sum type validity:**
15. Sum type discriminators use the pipe syntax with capitalised variant names (`A | B | C`)
Expand All @@ -1922,6 +1959,8 @@ A valid Allium specification must satisfy:
22. `given` bindings must reference entity types declared in the module or imported via `use`
23. Each binding name must be unique within the `given` block
24. Unqualified instance references in rules must resolve to a `given` binding, a `let` binding, a trigger parameter or a default entity instance
24a. A qualified type name in a `default` declaration (`default alias/Type name = { ... }`) must resolve against the imported module's entity declaration; the literal's field set and types are validated against that canonical schema
24b. Every field set by a `default` object literal must be a field declared on the named entity or value type; a field the type does not declare is an error. This applies recursively to nested object literals (validated against the nested field's type), so renaming or removing a field surfaces as a drift error at check time

**Config validity:**
25. Config parameters must have explicit types. Parameters with default values must declare them explicitly (literal, qualified reference or expression). Parameters without defaults are mandatory: consuming modules must supply a value
Expand Down
136 changes: 135 additions & 1 deletion plugins/allium/references/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Patterns elide common cross-cutting entities (`Email`, `Notification`, `AuditLog
| Usage Limits & Quotas | Limit checks in `requires`, metered resources, plan tiers, surfaces |
| Comments with Mentions | Nested entities, parsing triggers, cross-entity notifications, surfaces |
| Integrating Library Specs | External spec references, configuration, config parameter references, responding to external triggers |
| Framework Integration Contract | Contract declarations, expression-bearing invariants, contract references, programmatic surfaces |
| Framework Integration Contract | Contract declarations, expression-bearing invariants, contract references, programmatic surfaces, external-API client (`demands`) |

---

Expand Down Expand Up @@ -1800,6 +1800,8 @@ surface APIAccess {
- Feature flags (`can_use_feature(f)`)
- Interaction surface for usage dashboard and API surface with rate limit guarantee

> **Exposing an API vs. calling one.** The `APIAccess` surface here models *exposing* a rate-limited API to consumers — the surface owner is the API, and the consumer is the `facing` party. To model the opposite case, where your spec is a *client* calling out to a third-party API, SDK, or MCP server, do not reach for `provides:`; use a module-level `contract` referenced with `demands` from the caller's side. See Pattern 9, *Example: Calling an external API (the `demands` side)*.

---

## Pattern 7: Comments with Mentions
Expand Down Expand Up @@ -2862,6 +2864,138 @@ Do not use contracts for user-facing surfaces. If the external party is a person

Both describe things the surface supplies, but `provides` connects to the rule system while `fulfils` references a programmatic contract with typed signatures and invariants.

### Example: Calling an external API (the `demands` side)

The main example above is the surface owner *supplying* services to its counterpart (`fulfils EventSubmitter`). The mirror-image case is just as common, and easy to model wrongly: your spec is the **client** that calls out to an external typed API — a third-party REST service, a vendor SDK, or an MCP server. Reaching for `provides:` here is the wrong turn, because `provides` is for a user triggering a domain rule, not for one body of code calling another. The right construct is a module-level `contract` referenced with `demands` from the caller's surface: the spec requires the counterpart to implement the operations, and calls them.

This example models an address book that resolves postal addresses to coordinates through an external geocoding service. Our spec is purely the caller — it declares the typed boundary and records results, but never implements `geocode` itself.

```
-- allium: 3
-- geocoding-client.allium

------------------------------------------------------------
-- External Entities
------------------------------------------------------------

-- The third-party service we call. Its lifecycle is governed
-- elsewhere; we only model that it sits on the other side of the
-- boundary as the party that implements the API we demand.
external entity GeocodingService {}

------------------------------------------------------------
-- Value Types
------------------------------------------------------------

value Address {
line1: String
city: String
postcode: String
country: String
}

value LatLng {
latitude: Decimal
longitude: Decimal
}

value GeocodeResult {
location: LatLng
formatted_address: String
confidence: Decimal -- 0.0 (no match) to 1.0 (exact match)
}

------------------------------------------------------------
-- Contracts
------------------------------------------------------------

-- The typed boundary of the external geocoding API. Our spec is
-- the client: it calls these operations, it does not implement them.
contract GeocodingApi {
geocode: (address: Address) -> GeocodeResult?
reverse_geocode: (location: LatLng) -> GeocodeResult?

@invariant ReadOnly
-- geocode and reverse_geocode never mutate remote state.
-- A call with identical arguments is safe to retry and
-- yields an equivalent result.

@invariant NullOnNoMatch
-- When the provider finds no location for the input, the
-- operation returns null rather than a zero-confidence
-- result or an error.

@guidance
-- The provider rate-limits per API key. Callers should
-- debounce lookups triggered by partial address entry.
}

------------------------------------------------------------
-- Entities
------------------------------------------------------------

entity Place {
label: String
address: Address
status: unresolved | resolved | unlocatable

-- Populated from a successful GeocodingApi.geocode result
location: LatLng?
resolved_at: Timestamp?
}

------------------------------------------------------------
-- Rules
------------------------------------------------------------

rule RecordGeocodeResult {
when: GeocodeResolved(place, result)

requires: place.status = unresolved

ensures: place.location = result.location
ensures: place.status = resolved
ensures: place.resolved_at = now
}

rule RecordGeocodeMiss {
when: GeocodeNotFound(place)

requires: place.status = unresolved

ensures: place.status = unlocatable
}

------------------------------------------------------------
-- Surfaces
------------------------------------------------------------

-- Programmatic surface: our spec is the CALLER of the external API.
-- `demands` declares that we require the counterpart (the geocoding
-- service we face) to implement GeocodingApi; we do not supply it.
surface GeocodingClient {
facing provider: GeocodingService

contracts:
demands GeocodingApi

@guarantee NoUnresolvedWrites
-- A Place only stores a location obtained from a successful
-- GeocodingApi.geocode call; an unlocatable address never
-- receives coordinates.
}
```

Like the framework example, the rules handle domain state (recording a result) rather than spelling out the call itself — the `contract` declares the typed boundary, and `demands` on the surface records who must implement it.

**Reading the direction markers.** The choice between `demands`, `fulfils` and `provides` is about who implements what, and for whom:

- `demands GeocodingApi` — *I call out to the counterpart.* The facing party implements these operations; my spec invokes them. Use for "my spec is a client of an external API/SDK/MCP server".
- `fulfils EventSubmitter` — *I supply this API to the counterpart.* My spec implements these operations; the facing party invokes them. Use for "my spec is the service others integrate against" (as in the framework example above).
- `provides UserResetsPassword(...)` — *a user triggers an action and a domain rule fires.* Not a code-to-code contract at all; the action maps to a rule's external stimulus trigger. Use for human-facing surfaces.

A single surface can mix `demands` and `fulfils` (the framework demands evaluation logic while fulfilling submission and snapshot services). It should not use a `contracts:` clause and `provides:` to describe the *same* boundary: `provides` is for people, `demands`/`fulfils` for code.

### Invariant vs guarantee

`@guarantee` asserts a property of the surface boundary as a whole. `invariant` asserts a property scoped to the operations within a specific contract.
Expand Down