
Allium
- 3.1k installs
- 442 repo stars
- Updated July 22, 2026
- juxt/allium
allium is an agent skill for the Allium behavior specification language covering entities, rules, surfaces, contracts, and modular .allium file syntax.
About
Allium is a formal language for specifying observable software behavior between informal feature descriptions and code. It models entities, relationships, rules with when-requires-ensures clauses, surfaces at system boundaries, contracts, invariants, transition graphs, and modular imports without prescribing language, database, or UI choices. The skill documents entity syntax with projections and derived fields, trigger types from external stimuli to temporal and chained events, ensures patterns for state changes and Entity.created, and surface exposes-provides-related vocabulary. A routing table delegates elicitation, distillation, tending, weed alignment checks, and test propagation to companion skills. Allium targets integration and end-to-end tests, surfaces ambiguities early, and stays implementation agnostic. When the allium CLI is installed, a hook validates files after edits. Use it when authoring .allium files, reviewing domain specs, or choosing the right companion skill for eliciting, distilling, or generating tests from specifications.
- Formal .allium syntax for entities, rules, surfaces, contracts, and invariants.
- Implementation-agnostic specs focused on observable behavior, not frameworks.
- Routing table to elicit, distill, tend, weed, and propagate companion skills.
- Trigger and ensures patterns including chained events and Entity.created.
- Generates integration and end-to-end tests, not unit tests.
Allium by the numbers
- 3,090 all-time installs (skills.sh)
- +153 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #312 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 4, 2026 (Skillselion catalog sync)
allium capabilities & compatibility
- Capabilities
- entity, relationship, and projection modeling in · rule triggers and ensures clause authoring · surface boundary contracts with exposes and prov · contract and invariant declarations · modular spec imports with qualified names · routing to elicit, distill, tend, weed, and prop
- Use cases
- testing · documentation · planning
What allium says it does
Describes observable behaviour, not implementation
Generates integration and end-to-end tests (not unit tests)
Forces ambiguities into the open before implementation
npx skills add https://github.com/juxt/allium --skill alliumAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.1k |
|---|---|
| repo stars | ★ 442 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 22, 2026 |
| Repository | juxt/allium ↗ |
How do I specify domain behavior precisely enough to generate tests without locking in implementation details?
Write and read .allium behavior specs that capture domain logic, surfaces, and rules before implementation or test generation.
Who is it for?
Teams defining observable behavior specs that feed integration tests and alignment checks.
Skip if: Skip when you only need unit tests, UI mockups, or database schema design without behavioral rules.
When should I use this skill?
User writes or reads .allium files, mentions Allium specs, or needs language syntax and routing to companion skills.
What you get
Validated .allium specifications with clear entities, rules, surfaces, and routing to the right companion workflow.
- Domain questions from findings
- Spec gap clarification prompts
Files
Allium
Allium is a formal language for capturing software behaviour at the domain level. It sits between informal feature descriptions and implementation, providing a precise way to specify what software does without prescribing how it's built.
The name comes from the botanical family containing onions and shallots, continuing a tradition in behaviour specification tooling established by Cucumber and Gherkin.
Key principles:
- Describes observable behaviour, not implementation
- Captures domain logic that matters at the behavioural level
- Generates integration and end-to-end tests (not unit tests)
- Forces ambiguities into the open before implementation
- Implementation-agnostic: the same spec could be implemented in any language
Allium does NOT specify programming language or framework choices, database schemas or storage mechanisms, API designs or UI layouts, or internal algorithms (unless they are domain-level concerns).
Routing table
| Task | Tool | When |
|---|---|---|
Writing or reading .allium files | this skill | You need language syntax and structure |
| Building a spec through conversation | elicit skill | User describes a feature or behaviour they want to build |
| Extracting a spec from existing code | distill skill | User has implementation code and wants a spec from it |
| Modifying an existing spec | tend skill | User wants targeted changes to .allium files |
| Checking spec-to-code alignment | weed skill | User wants to find or fix divergences between spec and implementation |
| Generating tests from a spec | propagate skill | User wants to generate tests, PBT properties or state machine tests from a specification |
Quick syntax summary
Entity
entity Candidacy {
-- Fields
candidate: Candidate
role: Role
status: pending | active | completed | cancelled -- inline enum
retry_count: Integer
-- Relationships
invitation: Invitation with candidacy = this -- one-to-one
slots: InterviewSlot with candidacy = this -- one-to-many
-- Projections
confirmed_slots: slots where status = confirmed
pending_slots: slots where status = pending
-- Derived
is_ready: confirmed_slots.count >= 3
has_expired: invitation.expires_at <= now
}External entity
external entity Role { title: String, required_skills: Set<Skill>, location: Location }Value type
value TimeRange { start: Timestamp, end: Timestamp, duration: end - start }Sum type
A base entity declares a discriminator field whose capitalised values name the variants. Variants use the variant keyword.
entity Node {
path: Path
kind: Branch | Leaf -- discriminator field
}
variant Branch : Node {
children: List<Node?>
}
variant Leaf : Node {
data: List<Integer>
log: List<Integer>
}Lowercase pipe values are enum literals (status: pending | active). Capitalised values are variant references (kind: Branch | Leaf). Type guards (requires: or if branches) narrow to a variant and unlock its fields.
Module given
Declares the entity instances a module's rules operate on. All rules inherit these bindings. Not every module needs one: rules scoped by triggers on domain entities get their entities from the trigger. given is for specs where rules operate on shared instances that exist once per module scope.
given {
pipeline: HiringPipeline
calendar: InterviewCalendar
}Imported module instances are accessed via qualified names (scheduling/calendar) and do not appear in the local given block. Distinct from surface context, which binds a parametric scope for a boundary contract.
Rule
rule InvitationExpires {
when: invitation: Invitation.expires_at <= now
requires: invitation.status = pending
let remaining = invitation.proposed_slots where status != cancelled
ensures: invitation.status = expired
ensures:
for s in remaining:
s.status = cancelled
@guidance
-- Non-normative implementation advice.
}Trigger types
- External stimulus:
when: CandidateSelectsSlot(invitation, slot)— action from outside the system - State transition:
when: interview: Interview.status transitions_to scheduled— entity changed state (transition only, not creation) - State becomes:
when: interview: Interview.status becomes scheduled— entity has this value, whether by creation or transition - Temporal:
when: invitation: Invitation.expires_at <= now— time-based condition (always add arequiresguard against re-firing) - Derived condition:
when: interview: Interview.all_feedback_in— derived value becomes true - Entity creation:
when: batch: DigestBatch.created— fires when a new entity is created - Chained:
when: AllConfirmationsResolved(candidacy)— subscribes to a trigger emission from another rule's ensures clause
All entity-scoped triggers use explicit var: Type binding. Use _ as a discard binding where the name is not needed: when: _: Invitation.expires_at <= now, when: SomeEvent(_, slot).
Rule-level iteration
A for clause applies the rule body once per element in a collection:
rule ProcessDigests {
when: schedule: DigestSchedule.next_run_at <= now
for user in Users where notification_setting.digest_enabled:
let settings = user.notification_setting
ensures: DigestBatch.created(user: user, ...)
}Ensures patterns
Ensures clauses have four outcome forms:
- State changes:
entity.field = value - Entity creation:
Entity.created(...)— the single canonical creation verb - Trigger emission:
TriggerName(params)— emits an event for other rules to chain from - Entity removal:
not exists entity— asserts the entity no longer exists
These forms compose with for iteration (for x in collection: ...), if/else conditionals and let bindings.
Entity creation uses .created() exclusively. Domain meaning lives in entity names and rule names, not in creation verbs.
In state change assignments, the right-hand expression references pre-rule field values. Conditions within ensures blocks (if guards, creation parameters, trigger emission parameters) reference the resulting state.
Surface
surface InterviewerDashboard {
facing viewer: Interviewer
context assignment: SlotConfirmation where interviewer = viewer
exposes:
assignment.slot.time
assignment.status
provides:
InterviewerConfirmsSlot(viewer, assignment.slot)
when assignment.status = pending
related:
InterviewDetail(assignment.slot.interview)
when assignment.slot.interview != null
}Surfaces define contracts at boundaries. The facing clause names the external party, context scopes the entity. The remaining clauses use a single vocabulary regardless of whether the boundary is user-facing or code-to-code: exposes (visible data, supports for iteration over collections), provides (available operations with optional when-guards), contracts: (references module-level contract declarations with demands/fulfils direction markers), @guarantee (named prose assertions about the boundary), @guidance (non-normative advice), related (associated surfaces reachable from this one), timeout (references to temporal rules that apply within the surface's context).
The facing clause accepts either an actor type (with a corresponding actor declaration and identified_by mapping) or an entity type directly. Use actor declarations when the boundary has specific identity requirements; use entity types when any instance can interact (e.g., facing visitor: User). For integration surfaces where the external party is code, declare an actor type with a minimal identified_by expression. Actors that reference within in their identified_by expression must declare the expected context type: within: Workspace.
Surface-to-implementation contract
The exposes block is the field-level contract: the implementation returns exactly these fields, the consumer uses exactly these fields. Do not add fields not listed. Do not omit fields that are listed.
Contract
contract Codec {
serialize: (value: Any) -> ByteArray
deserialize: (bytes: ByteArray) -> Any
@invariant Roundtrip
-- deserialize(serialize(value)) produces a value
-- equivalent to the original for all supported types.
}Contracts are module-level declarations referenced by name in surface contracts: clauses (demands Codec, fulfils EventSubmitter). See Contracts for declaration syntax and referencing rules.
Expressions
Navigation: interview.candidacy.candidate.email, reply_to?.author (optional), timezone ?? "UTC" (null coalescing). Collections: slots.count, slot in invitation.slots, interviewers.any(i => i.can_solo), for item in collection: item.status = cancelled, permissions + inherited (set union), old - new (set difference). Comparisons: status = pending, count >= 2, status in {confirmed, declined}, provider not in providers. Boolean logic: a and b, a or b, not a, a implies b.
Modular specs
use "github.com/allium-specs/google-oauth/abc123def" as oauthQualified names reference entities across specs: oauth/Session. Coordinates are immutable (git SHAs or content hashes). Local specs use relative paths: use "./candidacy.allium" as candidacy.
Config
config {
invitation_expiry: Duration = 7.days
max_login_attempts: Integer = 5
extended_expiry: Duration = invitation_expiry * 2 -- expression-form default
sync_timeout: Duration = core/config.default_timeout -- config parameter reference
}Rules reference config values as config.invitation_expiry. For default entity instances, use default.
Defaults
default Role viewer = { name: "viewer", permissions: { "documents.read" } }Invariant
invariant NonNegativeBalance {
for account in Accounts:
account.balance >= 0
}Expression-bearing invariants (invariant Name { expression }) assert properties over entity state. They are logical assertions, not runtime checks. Distinct from prose annotations (@invariant Name) in contracts, which use the @ sigil to mark content the checker does not evaluate. See Invariants.
Transition graph (v3)
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}State-dependent field presence (v3)
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
customer: Customer
total: Money
tracking_number: String when status = shipped | delivered
shipped_at: Timestamp when status = shipped | delivered
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}Deferred specs
deferred InterviewerMatching.suggest -- see: detailed/interviewer-matching.alliumOpen questions
open question "Admin ownership - should admins be assigned to specific roles?"Verification
When the allium CLI is installed, a hook validates .allium files automatically after every write or edit. Fix any reported issues before presenting the result. If the CLI is not available, verify against the language reference.
References
- Language reference — full syntax for entities, rules, expressions, surfaces, contracts, invariants and validation
- Test generation — generating tests from specifications
- Patterns — 9 worked patterns: auth, RBAC, invitations, soft delete, notifications, usage limits, comments, library spec integration, framework integration contract
Actioning findings
When allium analyse produces findings, translate each into a domain question rather than presenting raw output. The user should never see finding types, evidence chains or JSON. They should hear a question that helps them improve their spec.
Finding types and question strategies
missing_producer
A rule's requires clause references a value that nothing in the spec establishes. The data dependency is unsatisfied.
Ask about the source. Work backward from the requirement: "The hiring decision needs the background check to be clear, but nothing in the spec says where background check results come from. Is this provided by an external service, or does someone enter it manually?"
The searched field in the finding shows what the checker looked for. If it found a partial chain (a rule that could produce the value, but whose trigger is itself unreachable), follow the chain: "There's a rule to handle background check results, but nothing triggers it. How do results get into the system?"
unreachable_trigger
A rule listens for a trigger that no surface provides and no other rule emits. The rule can never fire.
Ask about the entry point. "This rule handles background check results, but nothing in the spec says where they come from. Is this a webhook from an external service? A screen where someone enters the result? Something else?"
If the trigger name suggests an external system, prompt for whether it should be a surface (human-facing) or a contract integration point (system-facing).
dead_transition
A transition is declared in the graph and witnessed by a rule, but the rule's guards can never be satisfied. The transition exists on paper but is impossible in practice.
Ask what's needed. "The spec says a candidacy can move from screening to interviewing, but that requires the background check to be clear. I can't find a path through the spec that produces a clear background check. What needs to happen for this transition to work?"
The finding's evidence shows which guard is unsatisfiable and why. Use this to frame the question in terms of what's missing, not what's broken.
deadlock
A non-terminal state has no achievable exit. The entity can reach this state but can never leave it.
Ask what happens when things stall. "If a candidacy reaches the screening state and the background check never completes, the candidacy is stuck. What should happen in that situation? Is there a timeout? Can someone manually override it?"
If the finding includes cycle evidence (states that loop without reaching terminal), frame it differently: "The spec allows a job to bounce between retrying and waiting indefinitely without ever completing or failing. Is there a maximum number of retries, or a timeout that breaks the cycle?"
conflict
Two rules with different triggers can both fire in the same state and would set the same field to different values. The outcome is ambiguous.
Ask about priority. "If a membership is active and both the expiry timer fires and an admin extends it at the same moment, which should win? Should the extension prevent the expiry, or should the expiry take priority?"
This is distinct from actor choice (where one actor picks between alternatives). Conflicts arise from independent triggers that the spec doesn't order.
invariant_risk
A rule's ensures clause could produce a state that violates a declared invariant. The requires clause may not prevent it.
Ask whether to guard or revise. "The spec says at most one candidate per role can be hired, but the hiring rule doesn't prevent a second hire if the role hasn't been marked as filled. Should we add a guard that checks the role is still open, or is the invariant too strict?"
The finding's evidence shows the mechanism — how the ensures clause is inconsistent with the invariant. Use this to suggest a specific fix rather than asking an open-ended question.
Choosing which finding to present
When analyse returns multiple findings, pick the most relevant one. Apply these criteria in order:
1. If a finding chains into another (a dead_transition caused by a missing_producer caused by an unreachable_trigger), present the root cause first — even if it's in a different entity. Frame it in terms of its effect on the entity the user is working on. 2. If the user is working on a specific entity, pick a finding that affects that entity. 3. If the user just added a rule, pick a finding related to that rule's data flow. 4. If the user asked about completeness, pick the highest-impact finding first — deadlocks before broken data flow chains, broken chains before unreachable triggers.
A deadlock or invariant_risk finding indicates the spec may be structurally unsound. Surface these before continuing to build on the affected entity — adding more rules to a deadlocked lifecycle compounds the problem. Other finding types (missing_producer, unreachable_trigger, dead_transition, conflict) are gaps worth resolving but don't necessarily block further work.
Present one finding at a time. Let the user resolve it before surfacing the next. If analyse returns more than five or six findings, present the most impactful two or three individually, then summarise the rest by category: "There are also three unreachable triggers and two missing producers — would you like to work through those, or focus on something else?"
Assessing specs
When working with an Allium spec, assess its maturity before deciding what to do next. Spec maturity isn't uniform — a well-developed entity with full rules and surfaces can sit alongside a newly sketched entity with just a transition graph, in the same file.
Spec-level assessment
Read the spec and note which constructs are present:
| What's present | What it tells you |
|---|---|
| Entities with fields, no transition graphs | Domain concepts identified but lifecycles not yet explored |
| Transition graphs on entities | Lifecycles sketched — the user knows the states and intended flows |
| Rules witnessing transitions | Behaviour specified — triggers, guards and outcomes defined |
| Surfaces with exposes and provides | Boundaries defined — who sees what and can do what |
| Actors with identified_by | Roles identified and formalised |
| Invariants | Cross-cutting properties asserted |
| Open questions | Known unknowns documented |
| Deferred specifications | Complexity acknowledged and scoped for later |
A spec with entities and transition graphs but no rules is coarse. The right next step is filling in rules ("what triggers this transition?"). A spec with rules but no surfaces has behaviour without boundaries. The right next step is asking about actors and what they see.
Per-entity assessment
Each entity can be at a different level of development. Check:
- Has a transition graph? The lifecycle is sketched.
- Has witnessing rules for all transitions? Every declared edge has a rule that produces it.
- Has surfaces providing all external triggers? Every rule that listens for an external stimulus has a surface that provides it.
- Has all `requires` clauses traceable to a producer? Every precondition can be satisfied by a prior rule or surface in the spec.
An entity that has all four is structurally complete. It may still lack exception transitions, temporal triggers or failure paths — those are explored through obstacle elicitation, not structural assessment. An entity missing the fourth criterion has gaps the user may not be aware of.
When to use check vs analyse
If the Allium CLI is available:
The two commands produce different kinds of output. check produces diagnostics: line-level structural warnings (syntax issues, unreachable values, unused fields). analyse produces findings: process-level results with typed evidence (missing producers, dead transitions, deadlocks). Both are returned as JSON. See actioning findings for how to translate findings into domain questions.
Run allium check after every edit. It validates what's written — syntax, field resolution, transition graph structure, witnessing rules. It's fast and useful at every stage, including coarse specs.
Run allium analyse at natural checkpoints: when the user asks about completeness, when at least one entity has both witnessing rules and surfaces defined, when transitioning from one entity to another, or when stepping back to review. It reasons about what's missing — data flow gaps, unreachable transitions, deadlocks.
If the CLI is not available, fall back to the language reference for validation. The first time this fallback happens, note: "I'll validate against the language reference instead. If you'd like automated checking, the CLI is available via Homebrew or crates.io — see the README for details."
If allium analyse fails with an unrecognised command error, the installed CLI predates the analyse feature. Fall back to conversational analysis (trace data flow and reachability by reading the spec) and don't retry analyse in the same session. Mention that updating the CLI would enable automated process-level checking.
Adjusting your approach
Work at the right level for each part of the spec:
- A coarse entity calls for walkthrough questions: "What triggers this transition? Who's involved at this step?"
- A detailed entity with rules calls for gap analysis: "This rule requires a value that nothing in the spec produces. Where does it come from?"
- A well-specified entity calls for validation: "Here's the lifecycle as I understand it — does this match your mental model?"
Don't apply detailed analysis to a coarse spec (it produces noise about things that haven't been written yet). Don't ask exploratory questions about an entity that already has rules and surfaces covering all declared transitions, including exception paths (the user has already answered them).
Communicating with stakeholders
Users are not expected to read or write Allium syntax. When discussing the spec with stakeholders, translate constructs into domain language:
- Instead of showing a transition graph, describe the lifecycle: "A candidacy starts as applied, moves through screening and interviewing, and ends as either hired or rejected."
- Instead of showing a rule, describe the behaviour: "When the recruiter advances a candidate, the system checks that the background check is clear before moving to interviews."
- Instead of showing a surface, describe the interaction: "The recruiter sees a queue of candidates awaiting screening, with their name and the role they applied for."
- Instead of listing
open_questions, pose them directly: "One thing we haven't resolved — what happens to in-progress candidacies when a role is closed?"
When validating the spec, describe what it says and ask whether that matches expectations. Don't present the spec itself for review unless the user has shown they're comfortable reading it. The spec is the artefact; the conversation is in domain terms.
Migrating from Allium v1 to v2
This guide covers every change between Allium v1 and v2. It is written for both humans reviewing the release and LLMs tasked with upgrading v1 specifications.
If you are an LLM migrating a v1 spec, read this document in full, then work through the checklist at the end. The checklist verifies completeness but does not repeat the syntax rules and examples you will need from the sections above it.
---
What changed
Version 2 adds six capabilities to the language. None of the existing v1 syntax was removed or altered; every v1 construct still means what it meant before. The changes are:
1. Contract references (demands, fulfils) in surfaces, for expressing programmatic integration contracts with typed signatures and invariants. 2. Module-level contracts (contract), direction-agnostic obligation declarations that surfaces reference via a contracts: clause. 3. Guidance annotations (@guidance) in rules, contracts and surfaces, for non-normative implementation advice. 4. Expression-bearing invariants (invariant Name { expression }), machine-readable assertions at top-level and entity-level scope. 5. The `implies` operator, a boolean operator available in all expression contexts. 6. Config composition — config parameter defaults that reference imported module parameters by qualified name, with arithmetic expressions for derived defaults.
Because all changes are additive, a v1 spec is valid v2 once the version marker is updated. No existing syntax needs rewriting.
---
Required changes
1. Update the version marker
The first line of every .allium file must change from version 1 to version 2. This is the only change required for every spec.
v1:
-- allium: 1v2:
-- allium: 22. Adjust section order (only when adopting new constructs)
V2 introduces two new sections. The full section order is now:
use declarations
Given
External Entities
Value Types
Contracts ← new, between Value Types and Enumerations
Enumerations
Entities and Variants
Config
Defaults
Rules
Invariants ← new, between Rules and Actor Declarations
Actor Declarations
Surfaces
Deferred Specifications
Open QuestionsEmpty sections are still omitted. No existing sections moved: the two new sections slot between existing ones. If your v1 spec does not adopt contracts or expression-bearing invariants, no section headers need adding and the existing order is already correct.
If you add contracts, place the section header after Value Types:
------------------------------------------------------------
-- Contracts
------------------------------------------------------------If you add expression-bearing invariants, place the section header after Rules:
------------------------------------------------------------
-- Invariants
---------------------------------------------------------------
New constructs available in v2
These constructs did not exist in v1. They are optional: a migrated spec does not need to use them. But they are available, and specs that would benefit from them should adopt them.
Contract references in surfaces (demands, fulfils)
V1 surfaces had exposes, provides, guarantee, related and timeout. V2 adds a contracts: clause for programmatic integration contracts.
Use demands when the surface requires something from the counterpart. Use fulfils when the surface supplies something to the counterpart. Each entry references a module-level contract declaration by name.
contract DeterministicEvaluation {
evaluate: (event_name: String, payload: ByteArray) -> EventOutcome
@invariant Determinism
-- For identical inputs, evaluate must produce
-- byte-identical outputs across all instances.
@guidance
-- Avoid allocating during evaluation where possible.
}
contract EventSubmitter {
submit: (key: String, event_name: String, payload: ByteArray) -> EventSubmission
}
surface DomainIntegration {
facing framework: FrameworkRuntime
contracts:
demands DeterministicEvaluation
fulfils EventSubmitter
}Syntax rules:
contracts:entries usedemandsorfulfilsfollowed by a PascalCase contract name.- Each contract name may appear at most once per surface.
- Referenced contract names must resolve to a
contractdeclaration in scope. - Contract bodies contain typed signatures and
@-prefixed annotations (@invariant,@guidance). No entity, value, enum or variant declarations.
When to add contract references to an existing v1 surface: if the surface describes a boundary between code (framework and module, service and plugin, API and consumer) rather than between a user and an application, and the contract involves typed operations with specific properties.
Module-level contracts
Contracts are declared at module level in the Contracts section. Surfaces reference them via the contracts: clause.
-- Module-level declaration (in the Contracts section)
contract Codec {
serialize: (value: Any) -> ByteArray
deserialize: (bytes: ByteArray) -> Any
@invariant Roundtrip
-- deserialize(serialize(value)) produces a value
-- equivalent to the original for all supported types.
}
contract EventSubmitter {
submit: (event: DomainEvent) -> Acknowledgement
}
-- Surface references contracts with direction markers
surface DataPipeline {
facing processor: ProcessorModule
contracts:
demands Codec
fulfils EventSubmitter
}Syntax rules:
contracts:entries usedemandsorfulfilsfollowed by a contract name.- Contract identity is determined by module-qualified name. Same-named contracts from different modules are a structural error.
- Contracts are imported atomically via
use. Partial imports are not supported.
Guidance annotations in rules
Rules can now end with an @guidance annotation containing non-normative implementation advice.
-- v1: no guidance clause
rule ExpireInvitation {
when: invitation: Invitation.expires_at <= now
requires: invitation.status = pending
ensures: invitation.status = expired
}
-- v2: guidance added as final annotation
rule ExpireInvitation {
when: invitation: Invitation.expires_at <= now
requires: invitation.status = pending
ensures: invitation.status = expired
@guidance
-- Expire in a background job rather than blocking the
-- request path. Batch expiration where possible.
}Syntax rules:
@guidancemust appear after all structural clauses and after all other annotations in its containing construct.- Content is opaque prose using indented comment syntax (
--). The checker does not parse it. @guidanceis also valid inside contracts and at surface level. In contracts it provides implementation advice scoped to that contract's operations. At surface level it provides advice about the boundary as a whole.- The
@sigil marks prose annotations: constructs whose structure (placement, ordering) the checker validates, but whose content it does not evaluate. The same sigil convention applies to@invariantand@guarantee.
Expression-bearing invariants
V1 had no mechanism for machine-readable assertions over entity state. V2 adds expression-bearing invariants at two scopes.
Top-level invariants assert system-wide properties. They go in the new Invariants section after Rules:
invariant NonNegativeBalance {
for account in Accounts:
account.balance >= 0
}
invariant UniqueEmail {
for a in Users:
for b in Users:
a != b implies a.email != b.email
}Entity-level invariants assert properties scoped to a single entity. They go inside entity declarations alongside fields:
entity Account {
balance: Decimal
credit_limit: Decimal
status: active | frozen | closed
invariant SufficientFunds {
balance >= -credit_limit
}
invariant FrozenAccountsCannotTransact {
status = frozen implies pending_transactions.count = 0
}
}Syntax rules:
- Expression-bearing invariants use
invariant Name { expression }(no@, braces). - Prose-only invariants in contracts use
@invariant Name(with@, no colon). These are distinct constructs. - Invariant names are PascalCase.
- Expressions must be pure: no
.add(),.remove(),.created(), no trigger emissions, nonow. for x in Collection:inside an invariant body is a universal quantifier (all elements must satisfy).
When to add invariants to a migrated spec: if the spec has properties that should always hold (non-negative balances, uniqueness constraints, referential integrity) and those properties are currently implicit or expressed only in prose comments.
The implies operator
V2 adds implies to the expression language. a implies b is equivalent to not a or b. It has the lowest precedence of any boolean operator, binding looser than and and or.
implies is available in all expression contexts, not only invariants. It reads naturally in requires guards, derived boolean values and if conditions:
-- In a requires clause
requires: user.role = admin implies user.mfa_enabled
-- In a derived value
is_compliant: is_verified implies documents.count > 0
-- In an invariant
invariant ClosedAccountsEmpty {
for account in Accounts:
account.status = closed implies account.balance = 0
}Config parameter references and expressions
V1 config parameters could only have literal defaults. V2 allows defaults to reference parameters from imported modules, and to use arithmetic expressions.
use "./core.allium" as core
config {
-- Literal default (valid in both v1 and v2)
max_retries: Integer = 3
-- Qualified reference default (v2 only)
batch_size: Integer = core/config.batch_size
-- Expression default (v2 only)
extended_timeout: Duration = core/config.base_timeout * 2
buffer_size: Integer = core/config.batch_size + 10
retry_limit: Integer = max_retries - 1
}Syntax rules:
- Qualified references use the form
alias/config.param_name. - Arithmetic operators:
+,-,*,/with standard precedence. Parentheses for explicit precedence. - Both local and qualified references are valid in expressions.
- The config reference graph must be acyclic.
- Type compatibility: Integer with Integer, Duration with Duration (for
+/-), Duration with Integer (for*//), Integer with Duration (for*only), Decimal with Decimal, Decimal with Integer (for*//), Integer with Decimal (for*only). Scalar multiplication is commutative (2 * core/config.timeoutandcore/config.timeout * 2are both valid). Addition and subtraction require matching types. - Expressions resolve once at config resolution time, not dynamically.
When to use config references in a migrated spec: when a consuming spec duplicates a library spec's config value, or derives a value from it (double the timeout, batch size minus a buffer).
---
Naming convention additions
V2 extends PascalCase to two new constructs:
| Construct | Convention | Example |
|---|---|---|
| Contract names | PascalCase | Codec |
| Invariant names | PascalCase | NonNegativeBalance |
All other naming conventions are unchanged from v1.
---
Migration checklist
Use this checklist when upgrading a v1 spec to v2. Items marked required must be done. Items marked optional should be done when the spec would benefit.
- [ ] Required. Change
-- allium: 1to-- allium: 2on the first line. - [ ] Required if adopting new constructs. Verify section order matches v2 (Contracts after Value Types, Invariants after Rules). If neither section is present, existing order is already correct.
- [ ] Optional. If the spec has surfaces describing code-to-code boundaries, consider declaring
contractblocks and referencing them via acontracts:clause withdemands/fulfils. - [ ] Optional. If rules or surfaces have implementation-specific notes in comments, consider moving them into
@guidanceannotations (valid as the final annotation in rules and at surface level). - [ ] Optional. If the spec has properties that must always hold (uniqueness, non-negativity, referential constraints), express them as
invariant Name { expression }blocks. - [ ] Optional. If any expression (invariants, requires, derived values) would read more clearly with implication logic, use the
impliesoperator. - [ ] Optional. If config defaults duplicate or derive from imported module parameters, use qualified references and expressions.
---
Quick reference
| V1 | V2 | Change type |
|---|---|---|
-- allium: 1 | -- allium: 2 | Required |
| Sections: Value Types → Enumerations | Sections: Value Types → Contracts → Enumerations | Required (if contracts present) |
| Sections: Rules → Actor Declarations | Sections: Rules → Invariants → Actor Declarations | Required (if invariants present) |
| No contract references in surfaces | contracts: clause with demands/fulfils entries | Additive |
| No module-level contracts | contract Name { ... } in Contracts section | Additive |
No @guidance annotation | @guidance in rules (final annotation), contracts and surfaces | Additive |
| No expression-bearing invariants | invariant Name { expression } at top-level and entity-level | Additive |
No implies operator | a implies b (lowest boolean precedence) | Additive |
| Config defaults are literals only | Config defaults can reference alias/config.param and use arithmetic | Additive |
Migrating from Allium v2 to v3
This guide covers every change between Allium v2 and v3. It is written for both humans reviewing the release and LLMs tasked with upgrading v2 specifications.
If you are an LLM migrating a v2 spec, read this document in full, then work through the checklist at the end. The checklist verifies completeness but does not repeat the syntax rules and examples you will need from the sections above it.
---
What changed
Version 3 adds six capabilities and one enforcement change to the language. All v2 constructs retain their meaning. The changes are:
1. Transition graphs (transitions field_name { ... }), authoritative opt-in declarations of valid lifecycle transitions for enum status fields. 2. State-dependent field presence (when clause on field declarations), tying a field's presence to the entity's lifecycle state rather than using static ? optionality. 3. Derived value `when` propagation, automatic inference of when sets for derived values computed from state-dependent fields. 4. Backtick-quoted enum literals (` de-CH-1996 ), allowing external standard values that fall outside snake_case conventions. 5. **Ordered collection semantics** (Sequence<T>), distinguishing ordered from unordered collections and restricting .first/.last` to ordered types. 6. Black box function syntax for collection operations, reserving dot-method syntax for built-in operations and requiring free-standing call syntax for domain-specific collection operations.
Because the first five changes are additive, a v2 spec that does not use custom dot-methods on collections is valid v3 once the version marker is updated. Specs that use custom dot-methods need rewriting (see Enforcement change below).
---
Required changes
1. Update the version marker
The first line of every .allium file must change from version 2 to version 3.
v2:
-- allium: 2v3:
-- allium: 32. Rewrite custom dot-methods on collections (if present)
V3 reserves dot-method syntax on collections for built-in operations only. The full set of built-in dot-methods is: .count, .any(), .all(), .first, .last, .unique, .add(), .remove(). Any other dot-method call on a collection is now a checker error.
If your v2 spec used dot-method syntax for domain-specific collection operations, rewrite them to free-standing black box function syntax with the collection as the first argument:
v2:
events.filter(e => e.recent)
copies.grouped_by(r => r.output_payloads)
pending.min_by(e => e.offset)v3:
filter(events, e => e.recent)
grouped_by(copies, r => r.output_payloads)
min_by(pending, e => e.offset)If your v2 spec did not use custom dot-methods, no rewriting is needed.
---
New constructs available in v3
These constructs did not exist in v2. They are optional: a migrated spec does not need to use them. But they are available, and specs that would benefit from them should adopt them.
Transition graphs
V2 entities derived their valid transitions implicitly from the rules that operated on them. V3 adds an opt-in mechanism for declaring the valid transitions explicitly, inside the entity body.
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}When a transition graph is declared, it is authoritative: rules whose ensures clauses produce transitions not in the graph are validation errors. The checker also enforces that every non-terminal state has at least one outbound edge and that every declared edge is witnessed by at least one rule.
Syntax rules:
- The graph lives inside the entity body, below the field it governs, introduced by
transitions field_name. - Each line in the block is a directed edge:
from_state -> to_state. - Terminal states are declared with
terminal:followed by a comma-separated list. Absence of outbound edges does not imply terminal status; the declaration is required. - Every value on the enum field must appear in at least one edge or as a terminal. Every value in the graph must exist on the field. Drift is a hard error.
- Entities with multiple status fields use independent single-field graphs.
- Entities without a declared graph continue to derive transition validity from rules alone, with no change in checker behaviour. The checker does not suggest adding graphs to entities that lack them.
When to add transition graphs to a migrated spec: when the entity has a lifecycle field with well-understood valid transitions and you want the checker to enforce them. Particularly valuable for entities where incorrect transitions would be hard to detect from rule inspection alone.
State-dependent field presence (when clause)
V2 used ? to mark fields that might be absent. In lifecycle entities, many fields are absent in some states and guaranteed present in others, but ? cannot express this distinction. V3 adds a when clause on field declarations that ties presence to lifecycle state.
entity Document {
status: active | deleted
deleted_at: Timestamp when status = deleted
deleted_by: User when status = deleted
transitions status {
active -> deleted
deleted -> active
terminal: deleted
}
}Fields without when are present in all states. Fields with when are present only when the named status field holds one of the listed values. The when clause references a single status field; that field must have a transitions block.
Presence and absence obligations. The checker enforces obligations at transition boundaries:
- Entering the
whenset (source state outside, target state inside): the rule must set the field. - Leaving the
whenset (source state inside, target state outside): the rule must clear the field (set tonull). - Moving within or outside the
whenset: no obligation.
rule SoftDelete {
when: SoftDelete(document, actor)
requires: document.status = active
ensures:
document.status = deleted
document.deleted_at = now -- entering when set: must set
document.deleted_by = actor -- entering when set: must set
}
rule RestoreDocument {
when: RestoreDocument(document)
requires: document.status = deleted
ensures:
document.status = active
document.deleted_at = null -- leaving when set: must clear
document.deleted_by = null -- leaving when set: must clear
}Accessing a when-qualified field without a requires guard narrowing to a qualifying state is an error.
`?` and `when` are orthogonal. reviewer_notes: String? when review = approved | rejected means the field exists in those states but may be null within them. ? is genuine optionality; when is lifecycle-dependent presence. A field may carry both.
When to adopt `when` clauses in a migrated spec: when existing ? fields are not genuinely optional but are instead absent before a certain lifecycle stage and guaranteed present after it. The soft-delete pattern, order fulfilment pipelines and invitation workflows are common candidates. Replace the ? with a when clause referencing the appropriate status values, and add a transitions block if one does not already exist.
Derived value when propagation
Derived values computed from when-qualified fields automatically inherit the intersection of their inputs' when sets:
entity Order {
status: pending | confirmed | shipped | delivered
shipped_at: Timestamp when status = shipped | delivered
delivery_confirmed_at: Timestamp when status = delivered
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
terminal: delivered
}
-- Inferred: when status = delivered
-- (intersection of {shipped, delivered} and {delivered})
days_in_transit: delivery_confirmed_at - shipped_at
}The checker infers this; the author does not declare it. An author may optionally annotate a derived value with an explicit when clause as documentation. When present, the checker verifies it matches the inferred set. A mismatch is an error.
Backtick-quoted enum literals
V2 enum literals were restricted to snake_case. V3 allows backtick quoting for values that reference external standards with non-snake_case characters:
enum InterfaceLanguage { en | de | fr | `de-CH-1996` | es | `zh-Hant-TW` | `sr-Latn` }
enum CacheDirective { `no-cache` | `no-store` | `must-revalidate` | `max-age` }Syntax rules:
- Backtick-quoted literals are values, not identifiers. They participate in equality comparison and assignment.
- The checker does not apply case convention rules inside backticks. Comparison is byte-exact after UTF-8 encoding.
- Quoted and unquoted forms are distinct values with no implicit normalisation:
de_ch_1996and `de-CH-1996` are different values. - Backtick-quoted literals are permitted in enum declarations (named and inline), literal comparisons in rules and
ensuresclauses. - They are not permitted in identifier positions (field names, entity names, rule names, etc.) and cannot appear in arithmetic expressions.
When to use backtick-quoted literals in a migrated spec: when enum values reference external standards (BCP 47 language tags, MIME types, HTTP cache directives, currency codes) whose canonical form uses hyphens, dots, mixed case or leading digits. Replace any workaround encodings (underscore-substituted forms) with the standard's canonical form in backticks.
Ordered collection semantics
V2 treated all collections uniformly. V3 introduces a type distinction between ordered and unordered collections:
Set<T>— unordered collection of unique items (unchanged from v2)List<T>— ordered collection, declared explicitly as a compound field type on entitiesSequence<T>— ordered collection produced by ordered relationships and their projections. A subtype ofSet: assignable where an unordered collection is expected, but not the reverse
Syntax rules:
.firstand.lastare restricted to ordered collections (SequenceorList<T>). Using them on aSetis a warning in v3, becoming a hard error in the next version..uniquededuplicates a collection but always produces an unorderedSet, even when the source is ordered.- Set arithmetic (
+,-) on ordered collections produces unordered results. The checker reports an error if the result is used where an ordered collection is expected. for item in collection:iterates in declared order when the source is aSequenceorList<T>. When the source is aSet, iteration order is unspecified.- Projections preserve ordering: if the source is a
Sequence,wherefiltering and-> fieldextraction produce aSequencein the same relative order.
When to adopt ordered semantics in a migrated spec: when the order of items in a collection carries domain meaning (priority lists, attempt sequences, ranked preferences). If order does not matter, continue using Set<T>.
Enforcement change: black box collection operations
V3 reserves dot-method syntax on collections for the built-in set: .count, .any(), .all(), .first, .last, .unique, .add(), .remove(). The checker rejects any other dot-method call on a collection.
Domain-specific collection operations must use free-standing black box function syntax with the collection as the first argument:
-- Built-in: dot-method syntax (unchanged)
interviewers.any(i => i.can_solo)
confirmations.all(c => c.status = confirmed)
slots.count
-- Domain-specific: free-standing syntax (enforced in v3)
filter(events, e => e.recent)
grouped_by(copies, r => r.output_payloads)
min_by(pending, e => e.offset)
flatMap(groups, g => g.deferred_events)This was the recommended convention in v2 but was not enforced. V3 makes it a hard error.
---
Naming convention additions
V3 does not add new naming conventions. All naming rules are unchanged from v2. Backtick-quoted enum literals are exempt from case convention rules (the checker does not apply snake_case rules inside backticks).
---
Migration checklist
Use this checklist when upgrading a v2 spec to v3. Items marked required must be done. Items marked optional should be done when the spec would benefit.
- [ ] Required. Change
-- allium: 2to-- allium: 3on the first line. - [ ] Required if applicable. Rewrite any custom dot-method calls on collections to free-standing black box function syntax.
- [ ] Optional. If entities have lifecycle fields with well-understood valid transitions, add
transitions field_name { ... }blocks. - [ ] Optional. If fields are typed
?but are structurally absent before a lifecycle stage and present after it, replace?with awhenclause and ensure the referenced status field has atransitionsblock. - [ ] Optional. If enum values reference external standards with non-snake_case characters, replace workaround encodings with backtick-quoted canonical forms.
- [ ] Optional. If collection order carries domain meaning, adopt
List<T>for explicitly ordered fields and note that relationships producingSequencewill have ordering semantics when the ordered relationship declaration syntax is introduced. - [ ] Optional. Review
.firstand.lastusage onSetcollections. These produce a warning in v3 and will become errors in the next version. Replace with explicit ordering or remove.
---
Quick reference
| V2 | V3 | Change type |
|---|---|---|
-- allium: 2 | -- allium: 3 | Required |
| No transition graph syntax | transitions field_name { from -> to; terminal: ... } | Additive |
deleted_at: Timestamp? (static optionality) | deleted_at: Timestamp when status = deleted (state-dependent) | Additive |
No derived value when propagation | Derived values inherit intersected when sets from inputs | Additive |
| Enum literals restricted to snake_case | Backtick-quoted literals for external standards (` de-CH-1996 `) | Additive |
| All collections treated uniformly | Set<T> (unordered), List<T> and Sequence<T> (ordered) | Additive |
| Custom dot-methods on collections permitted | Dot-methods reserved for built-ins; custom ops use free-standing syntax | Enforcement |
Test generation
This document describes categories of tests that a person, agent or skill should derive from an Allium specification. It is not a tool to invoke. The taxonomy maps spec constructs to test obligations; how those tests are expressed depends on the target language and test framework.
From an Allium specification, generate:
Entity and value type tests (per entity and value type):
- Verify all declared fields are present with correct types
- Verify optional fields accept null and non-null values
- Verify optional navigation (
?.) short-circuits to null when the left side is absent - Verify null coalescing (
??) produces the default when the left side is null and the original value otherwise - Verify relationships navigate to the correct related entities
- Verify join lookups (
Entity{field1, field2}) resolve to the correct instance, or null when no match exists - For value types, verify equality is structural (by field values, not reference)
Enumeration tests (per named enum):
- Verify fields typed with the same named enum are comparable
- Verify
inandnot inmembership tests work against set literals of enum values
Sum type tests (per sum type):
- Verify each variant has its variant-specific fields accessible within a type guard
- Verify variant-specific fields are inaccessible outside type guards
- Verify all variants listed in the discriminator are handled in conditional logic
- Verify an entity cannot be multiple variants simultaneously
- Verify creation uses the variant name, not the base entity name
- Verify a
.createdtrigger on the base entity fires for any variant and can be narrowed with a type guard
Derived value and projection tests (per derived value or projection):
- Verify derived values compute correctly for representative entity states
- Verify projections filter correctly against their
wherepredicate - Verify projections with
-> fieldmapping extract the correct field and exclude nulls - Verify parameterised derived values return correct results for representative arguments
- Verify derived values involving
noware volatile (re-evaluate on each read) - Verify built-in collection operations (
.any(),.all(),.count, set+/-,.first,.last) produce correct results - Verify black box collection functions (free-standing calls like
filter(collection, predicate)) are treated as opaque with implementation-defined semantics
Default instance tests (per default declaration):
- Verify the named instance exists unconditionally
- Verify all specified field values match the declaration
- Verify cross-references between defaults resolve correctly (e.g.
inherits_from: viewer)
Config tests (per config block):
- Verify each parameter has its declared default value when not overridden
- Verify overriding a parameter replaces the default
- Verify mandatory parameters (no default) cause an error when omitted
- Verify expression-form defaults evaluate correctly, including type compatibility of arithmetic operands
- Verify qualified references to imported config resolve through the override chain
- Verify config parameters that depend on other parameters resolve in the correct order
Invariant tests (per expression-bearing invariant Name { expr }):
- Verify the invariant holds after every state-changing rule that touches the constrained entities
- Verify the invariant holds for edge-case entity states (boundary values, empty collections)
- For entity-level invariants, verify they hold after any field mutation on the entity
- For invariants using
implies, verify the consequent holds when the antecedent is true and that the invariant is trivially satisfied when the antecedent is false
Rule tests (per rule):
- Success case: all preconditions met, verify all postconditions hold
- Failure cases: verify the rule is rejected when each
requiresclause independently fails - Edge cases: boundary values for numeric conditions
- For conditional ensures, verify each branch fires under the correct condition and that
ifguards read resulting state, not pre-rule state - For entity-creating ensures, verify the created entity has the specified field values
- For
letbindings, verify the bound value is correct and available to subsequent clauses - For
not existsin ensures, verify the entity is removed from the system - For bulk updates (
forin ensures), verify the postcondition applies to every element in the collection - For rule-level
foriteration, verify the rule body executes for each matching element - For chained triggers (trigger emission in ensures), verify the downstream rule fires with the correct parameters
State transition tests (per entity with status enum):
- Valid transitions succeed via their rules
- Invalid transitions are rejected (no rule allows them)
- Terminal states have no outbound transitions
- For
transitions_totriggers, verify the rule does not fire on entity creation - For
becomestriggers, verify the rule fires both on creation and on transition
State-dependent field tests (per field with a when clause):
- Verify the field is present (has a meaningful value) when the entity is in a qualifying state
- Verify the field is absent (has no meaningful value) when the entity is outside the qualifying states
- When a rule transitions into the
whenset, verify it sets the field (entering obligation) - When a rule transitions out of the
whenset, verify it clears the field (leaving obligation) - When a rule moves within the
whenset, verify no obligation fires (field is already present) - When two rules converge on the same qualifying state, verify both set the field
- Verify accessing a
when-qualified field without a state guard is rejected - For derived values computed from
when-qualified fields, verify the inferredwhenset matches the intersection of the inputs'whensets
How "present" and "absent" are tested depends on how the implementation represents the entity. When the entity is modelled as a sealed hierarchy or variant type (one class per lifecycle state), presence and absence are structural: the field exists on one variant and not another. The compiler enforces the when clause. When the entity is modelled as a single mutable class with nullable fields, test that the field is meaningfully populated in qualifying states and null or empty outside them. Both representations are valid for the same spec; the choice is an implementation concern. The spec-level concept is lifecycle-dependent presence; the test adapts to the representation.
Temporal tests (per time-based trigger):
- Before deadline: rule does not fire, state unchanged
- At deadline: rule fires, postconditions hold
- After deadline: rule has already fired, does not re-fire
- Verify
requiresguard prevents re-firing when entity remains in the qualifying state - Verify temporal triggers on optional fields do not fire when the field is null
Communication tests (per Notification/Email/etc):
- Verify communication is triggered by the correct rule
- Verify recipient is correct
- Verify template and data are passed
Surface tests (per surface):
- Exposure tests: verify each item in
exposesis accessible to the specified party - For
foriteration inexposes, verify each element in the collection is exposed - Provides availability: verify provided operations appear when their
whenconditions are true - Provides unavailability: verify provided operations are hidden when
whenconditions are false, including when the corresponding rule'srequiresclauses are not met - Actor identification: verify only entities matching the actor's
identified_bypredicate can interact - For actors with
within, verify interaction is scoped to the declared context (e.g. actions in one workspace do not affect another) - Party restriction: verify the surface is not accessible to other party types
- Context scoping: verify the surface instance is absent when no entity matches the
contextpredicate - Related surface navigation: verify navigation to related surfaces resolves to the correct context entity
- Guarantee tests: verify stated
@guaranteeannotations hold across the boundary - Timeout tests: verify the referenced temporal rule fires within the surface's context
Contract declaration tests (per contract declaration):
- Verify the implementation satisfies each typed signature in the contract
- Verify
@invariantannotations are honoured across the boundary - For surfaces that
demanda contract, verify the counterpart provides all signatures - For surfaces that
fulfila contract, verify this surface supplies all signatures
Cross-module tests (per use declaration):
- Verify qualified entity references resolve to the imported module's entities
- Verify rules that respond to external triggers (state transitions, trigger emissions) from imported modules fire correctly
- Verify external entities referenced as type placeholders accept the consuming spec's concrete type
Cross-rule interaction tests (per rule with entity-creating ensures):
- Verify guards prevent duplicate entity creation when sibling rules re-trigger on the same parent
- Verify
providesentries are unavailable when anyrequiresconjunct of the corresponding rule is false
Scenario tests (per specification):
- Happy path through the main rule chain (follow chained triggers from entry point to terminal state)
- Edge cases and error paths at each decision point
- When two rules could fire on the same entity, verify the resulting state is consistent regardless of order
Concurrency note: The language reference does not formally define rule atomicity or evaluation order. Treat rules as atomic (completing entirely or not at all) as a reasonable default.
Constructs without dedicated test categories: given block bindings are exercised through rule tests that reference them. deferred specifications are tested in their own modules. open question declarations are checker warnings, not test obligations. exists as an if-condition is covered by rule conditional ensures tests. Discard bindings (_) have no observable effect to test.
Related skills
How it compares
Pick allium over generic code-review skills when input is Allium analyse output that must become product spec questions.
FAQ
Does Allium prescribe a programming language?
No. It describes observable behavior and stays implementation agnostic.
What tests does Allium generate?
Integration and end-to-end tests via the propagate skill, not unit tests.
How do I build a spec through conversation?
Use the elicit companion skill; this skill covers syntax and structure.
Is Allium safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.