npx skills add https://github.com/juxt/allium --skill alliumAllium 是一种用于在领域层面捕获软件行为的规范语言。它介于非正式的功能描述与具体实现之间,提供了一种精确的方式来指定软件做什么,而不规定如何构建。
其名称来源于包含洋葱和青葱的植物科属,延续了由 Cucumber 和 Gherkin 建立的行为规范工具传统。
核心原则:
Allium 不指定编程语言或框架选择、数据库模式或存储机制、API 设计或 UI 布局,或内部算法(除非它们是领域层面的关注点)。
| 任务 | 工具 | 适用时机 |
|---|---|---|
编写或阅读 .allium 文件 | 此技能 | 需要了解语言语法和结构 |
| 通过对话构建规范 | elicit 技能 | 用户描述他们想要构建的功能或行为 |
| 从现有代码中提取规范 | distill 技能 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 用户已有实现代码,并希望从中得到规范 |
| 修改现有规范 | tend 代理 | 用户希望对 .allium 文件进行针对性修改 |
| 检查规范与代码的一致性 | weed 代理 | 用户希望发现或修复规范与实现之间的差异 |
| 根据规范生成测试 | propagate 技能 | 用户希望从规范生成测试、PBT 属性或状态机测试 |
entity Candidacy {
-- 字段
candidate: Candidate
role: Role
status: pending | active | completed | cancelled -- 内联枚举
retry_count: Integer
-- 关系
invitation: Invitation with candidacy = this -- 一对一
slots: InterviewSlot with candidacy = this -- 一对多
-- 投影
confirmed_slots: slots where status = confirmed
pending_slots: slots where status = pending
-- 派生字段
is_ready: confirmed_slots.count >= 3
has_expired: invitation.expires_at <= now
}
external entity Role { title: String, required_skills: Set<Skill>, location: Location }
value TimeRange { start: Timestamp, end: Timestamp, duration: end - start }
基础实体声明一个区分字段,其大写值用于命名变体。变体使用 variant 关键字。
entity Node {
path: Path
kind: Branch | Leaf -- 区分字段
}
variant Branch : Node {
children: List<Node?>
}
variant Leaf : Node {
data: List<Integer>
log: List<Integer>
}
小写的管道值是枚举字面量(status: pending | active)。大写的管道值是变体引用(kind: Branch | Leaf)。类型守卫(requires: 或 if 分支)可以缩小到特定变体并解锁其字段。
声明模块规则所操作的实体实例。所有规则都继承这些绑定。并非每个模块都需要:作用域由领域实体上的触发器限定的规则会从触发器获取其实体。given 适用于规则操作于每个模块作用域内仅存在一次的共享实例的规范。
given {
pipeline: HiringPipeline
calendar: InterviewCalendar
}
导入的模块实例通过限定名(scheduling/calendar)访问,不会出现在本地 given 块中。与表面 context 不同,后者为边界契约绑定了一个参数化作用域。
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
-- 非规范性的实现建议。
}
when: CandidateSelectsSlot(invitation, slot) — 来自系统外部的动作when: interview: Interview.status transitions_to scheduled — 实体状态改变(仅转换,非创建)when: interview: Interview.status becomes scheduled — 实体具有此值(无论是通过创建还是转换)when: invitation: Invitation.expires_at <= now — 基于时间的条件(始终添加 requires 守卫以防止重复触发)when: interview: Interview.all_feedback_in — 派生值变为真when: batch: DigestBatch.created — 当新实体被创建时触发when: AllConfirmationsResolved(candidacy) — 订阅来自另一个规则 ensures 子句的触发器发射所有实体作用域的触发器都使用显式的 var: Type 绑定。在不需要名称的地方使用 _ 作为丢弃绑定:when: _: Invitation.expires_at <= now,when: SomeEvent(_, slot)。
for 子句将规则体应用于集合中的每个元素一次:
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 子句有四种结果形式:
entity.field = valueEntity.created(...) — 唯一规范的创建动词TriggerName(params) — 发射一个事件供其他规则链接not exists entity — 断言实体不再存在这些形式可以与 for 迭代(for x in collection: ...)、if/else 条件语句和 let 绑定组合使用。
实体创建仅使用 .created()。领域含义存在于实体名称和规则名称中,而非创建动词中。
在状态变更赋值中,右侧表达式引用规则执行前的字段值。ensures 块内的条件(if 守卫、创建参数、触发器发射参数)引用的是结果状态。
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
}
表面定义了边界处的契约。facing 子句命名外部方,context 限定实体作用域。其余子句使用单一词汇表,无论边界是面向用户还是代码到代码:exposes(可见数据,支持对集合进行 for 迭代)、provides(可用操作,带有可选的 when 守卫)、contracts:(引用模块级的 contract 声明,带有 demands/fulfils 方向标记)、@guarantee(关于边界的命名散文断言)、@guidance(非规范性建议)、related(可从此表面访问的关联表面)、timeout(引用在表面上下文内应用的时间性规则)。
facing 子句接受一个参与者类型(带有相应的 actor 声明和 identified_by 映射)或直接接受一个实体类型。当边界有特定身份要求时使用参与者声明;当任何实例都可以交互时使用实体类型(例如,facing visitor: User)。对于外部方是代码的集成表面,声明一个具有最小 identified_by 表达式的参与者类型。在其 identified_by 表达式中引用 within 的参与者必须声明预期的上下文类型:within: Workspace。
exposes 块是字段级契约:实现返回的正是这些字段,消费者使用的正是这些字段。不要添加未列出的字段。不要省略已列出的字段。
contract Codec {
serialize: (value: Any) -> ByteArray
deserialize: (bytes: ByteArray) -> Any
@invariant Roundtrip
-- deserialize(serialize(value)) 为所有支持的类型
-- 生成一个等价于原始值的值。
}
契约是模块级声明,在表面 contracts: 子句(demands Codec,fulfils EventSubmitter)中通过名称引用。有关声明语法和引用规则,请参阅契约。
导航:interview.candidacy.candidate.email,reply_to?.author(可选),timezone ?? "UTC"(空值合并)。集合:slots.count,slot in invitation.slots,interviewers.any(i => i.can_solo),for item in collection: item.status = cancelled,permissions + inherited(集合并集),old - new(集合差集)。比较:status = pending,count >= 2,status in {confirmed, declined},provider not in providers。布尔逻辑:a and b,a or b,not a,a implies b。
use "github.com/allium-specs/google-oauth/abc123def" as oauth
限定名跨规范引用实体:oauth/Session。坐标是不可变的(git SHA 或内容哈希)。本地规范使用相对路径:use "./candidacy.allium" as candidacy。
config {
invitation_expiry: Duration = 7.days
max_login_attempts: Integer = 5
extended_expiry: Duration = invitation_expiry * 2 -- 表达式形式的默认值
sync_timeout: Duration = core/config.default_timeout -- 配置参数引用
}
规则将配置值引用为 config.invitation_expiry。对于默认实体实例,使用 default。
default Role viewer = { name: "viewer", permissions: { "documents.read" } }
invariant NonNegativeBalance {
for account in Accounts:
account.balance >= 0
}
包含表达式的不变式(invariant Name { expression })断言实体状态上的属性。它们是逻辑断言,而非运行时检查。区别于契约中的散文注解(@invariant Name),后者使用 @ 符号标记检查器不评估的内容。请参阅不变式。
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}
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 InterviewerMatching.suggest -- 参见:detailed/interviewer-matching.allium
open question "Admin ownership - should admins be assigned to specific roles?"
当 allium CLI 安装后,钩子会在每次写入或编辑后自动验证 .allium 文件。在呈现结果之前修复任何报告的问题。如果 CLI 不可用,请根据语言参考进行验证。
每周安装量
170
仓库
GitHub Stars
85
首次出现
2026年2月9日
安全审计
安装于
codex158
opencode155
gemini-cli155
github-copilot155
amp149
kimi-cli149
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:
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).
| 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 agent | User wants targeted changes to .allium files |
| Checking spec-to-code alignment | weed agent | 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 |
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 Role { title: String, required_skills: Set<Skill>, location: Location }
value TimeRange { start: Timestamp, end: Timestamp, duration: end - start }
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.
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 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.
}
when: CandidateSelectsSlot(invitation, slot) — action from outside the systemwhen: interview: Interview.status transitions_to scheduled — entity changed state (transition only, not creation)when: interview: Interview.status becomes scheduled — entity has this value, whether by creation or transitionwhen: invitation: Invitation.expires_at <= now — time-based condition (always add a requires guard against re-firing)when: interview: Interview.all_feedback_in — derived value becomes truewhen: batch: DigestBatch.created — fires when a new entity is createdAll 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).
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 clauses have four outcome forms:
entity.field = valueEntity.created(...) — the single canonical creation verbTriggerName(params) — emits an event for other rules to chain fromnot exists entity — asserts the entity no longer existsThese 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 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), (associated surfaces reachable from this one), (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.
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 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.
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, , . Boolean logic: , , , .
use "github.com/allium-specs/google-oauth/abc123def" as oauth
Qualified 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 {
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.
default Role viewer = { name: "viewer", permissions: { "documents.read" } }
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.
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}
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 InterviewerMatching.suggest -- see: detailed/interviewer-matching.allium
open question "Admin ownership - should admins be assigned to specific roles?"
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.
Weekly Installs
170
Repository
GitHub Stars
85
First Seen
Feb 9, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex158
opencode155
gemini-cli155
github-copilot155
amp149
kimi-cli149
测试策略完整指南:单元/集成/E2E测试金字塔与自动化实践
11,200 周安装
deep-debug 多智能体调试工具:解决复杂前端错误与React状态管理问题
162 周安装
React Native 架构指南:Expo 生产就绪模式、导航、状态管理与离线优先
198 周安装
feishu-cli-import:Markdown文件一键导入飞书文档,支持Mermaid/PlantUML图表和表格拆分
344 周安装
React Native Storybook 故事编写指南 - 组件故事格式(CSF)与最佳实践
185 周安装
React状态管理指南:Redux/Zustand/Jotai/React Query最佳实践与选择
201 周安装
认证授权实现模式:构建安全可扩展的认证与授权系统最佳实践
209 周安装
when: AllConfirmationsResolved(candidacy) — subscribes to a trigger emission from another rule's ensures clauserelatedtimeoutstatus in {confirmed, declined}provider not in providersa and ba or bnot aa implies b