From b2a7a4885248a7f8190d9675d90317c626e810cd Mon Sep 17 00:00:00 2001 From: chromalchemy Date: Fri, 19 Jun 2026 16:45:05 -0400 Subject: [PATCH] Sync allium plugin references to upstream 3.4.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port forward the upstream juxt/allium changes since the original port base (82da292 → 8af2da4, plugin v3.4.0). Only two reference files gained content; all other skill/agent/rule files were unchanged upstream. - language-reference.md: zero-argument contract signatures, List list literals (`[ ... ]`, duplicate-retaining vs set literals), qualified default type names, and corresponding validation rules - patterns.md: Pattern 9 external-API `demands` side example plus demands/fulfils/provides direction-marker guidance - README.md: update upstream sync note to 8af2da4 (2026-06-19) Verified the synced references are byte-identical to upstream HEAD and the installed allium CLI 3.5.0 (language versions 1/2/3) validates the documented language version 3 via the `allium check` hook contract. 🤖 Generated with [ECA](https://eca.dev) Co-Authored-By: eca-agent --- plugins/allium/README.md | 8 +- .../allium/references/language-reference.md | 41 +++++- plugins/allium/references/patterns.md | 136 +++++++++++++++++- 3 files changed, 179 insertions(+), 6 deletions(-) diff --git a/plugins/allium/README.md b/plugins/allium/README.md index aeaecdc..76f65b5 100644 --- a/plugins/allium/README.md +++ b/plugins/allium/README.md @@ -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. diff --git a/plugins/allium/references/language-reference.md b/plugins/allium/references/language-reference.md index 13f639e..78f9bde 100644 --- a/plugins/allium/references/language-reference.md +++ b/plugins/allium/references/language-reference.md @@ -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 + 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: @@ -344,7 +355,7 @@ Primitive types have no properties or methods. For domain-specific string types **Compound types:** - `Set` — unordered collection of unique items -- `List` — ordered collection (use when order matters). A compound field type declared explicitly on entities +- `List` — ordered collection (use when order matters). A compound field type declared explicitly on entities. Populate it with a list literal (`[ ... ]`); see [Literals](#literals) - `Sequence` — 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` 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). @@ -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`, 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` 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 @@ -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 @@ -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`; 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`) @@ -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 diff --git a/plugins/allium/references/patterns.md b/plugins/allium/references/patterns.md index 571ee33..c4afddb 100644 --- a/plugins/allium/references/patterns.md +++ b/plugins/allium/references/patterns.md @@ -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`) | --- @@ -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 @@ -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.