给你的AI代理比提示更有用的东西。通过清晰性实现速度。
Allium
Allium是一种用于在领域层面捕获软件行为的正式语言。它介于非正式的功能描述和实现之间,提供了一种精确的方式来指定软件做什么,而不规定如何构建它。
这个名字来源于包含洋葱和青葱的植物科,延续了由Cucumber和Gherkin建立的行为规范工具的传统。
关键原则:
- 描述可观察的行为,而非实现
- 捕获在行为层面重要的领域逻辑
- 生成集成测试和端到端测试(而非单元测试)
- 在实现之前将歧义暴露出来
- 与实现无关:同一个规范可以用任何语言实现
Allium不指定编程语言或框架选择、数据库模式或存储机制、API设计或UI布局,以及内部算法(除非它们是领域层面的关注点)。
路由表
| 任务 | 工具 | 何时使用 |
|---|---|---|
编写或阅读.allium文件 |
本技能 | 你需要语言语法和结构 |
| 通过对话构建规范 | elicit技能 |
用户描述他们想要构建的功能或行为 |
| 从现有代码提取规范 | distill技能 |
用户有实现代码并希望从中获取规范 |
| 修改现有规范 | tend技能 |
用户希望对.allium文件进行有针对性的更改 |
| 检查规范与代码的一致性 | weed技能 |
用户希望发现或修复规范与实现之间的差异 |
| 从规范生成测试 | propagate技能 |
用户希望从规范生成测试、PBT属性或状态机测试 |
| 驱动整个循环直至收敛 | 本技能(参见驱动循环) | 用户希望端到端构建或协调一个功能——/allium <goal>自动运行收集→行动→验证→重复循环,直到规范、测试和代码一致 |
响应/allium(循环优先)
/allium是入口点。偏向自主路径——整个循环的价值正是偶尔使用单一技能所缺失的:
-
明确单一任务 → 直接路由到该技能(根据路由表);不要让用户浏览菜单。
-
目标或功能(例如“添加礼品卡”、“让密码重置工作”)→ 自行驱动整个循环端到端,而不是运行一个阶段。遵循驱动循环。
-
空白或模糊 → 以循环优先引导用户:提供驱动循环作为默认选项,然后列出各个技能作为控制路径,每个技能附带一行提示,并根据项目状态建议具体的起点(现有的
.allium规范?有代码但没有规范?需要协调的漂移?)。例如:告诉我一个目标,我会驱动整个循环——规范→测试→代码,直到它们一致。或者你自己运行一步:
elicit(从意图生成规范),distill(从现有代码生成规范),propagate(从规范生成测试),tend(编辑规范),weed(修复规范↔代码漂移)。你已经有代码但还没有.allium,所以我建议从distill开始——或者直接给我目标,我会端到端处理。
以循环为主导;将各个技能保留一步之遥,供希望手动控制的用户使用。一旦单个技能完成,主动建议下一步,而不是等待被询问。
Allium循环(推荐顺序)
这些技能不是一次性命令;它们组合成一个自主风格的循环——收集上下文→采取行动→验证→重复——驱动三个工件达成一致:规范(意图)、测试(契约)和代码(实现)。使用/elicit或/distill收集上下文(规范是持久的上下文);使用/propagate然后实现采取行动(在规范优先的工作中,先确认新测试失败——一个在实现之前就已经通过的测试要么已经被覆盖,要么是空洞的);通过运行测试、然后/weed、然后CLI结构检查来验证;重复直到收敛。验证是最重要的阶段,规范加测试加weed信号使循环值得信赖。在调用一个技能后,主动建议下一步,而不是等待被询问。要一次性运行整个循环直至收敛,只需给/allium一个目标——它会为你驱动循环,遵循驱动循环。
两个入口点,一个收敛循环:
- 规范优先(正向,从意图出发):
/elicit→/propagate→ 实现 →/weed;当需求变化时使用/tend然后重新/propagate。 - 代码优先(反向,从现有代码出发):
/distill→ 审查预期行为与意外行为 →/propagate→ 对代码运行测试 →/weed进行协调 → 按区域重复。
当测试通过、/weed报告无差异且没有未解决的问题时,工作“完成”(对于代码优先,还需要一次新的/distill没有发现新内容)。循环中的两条固定规则:永远不要削弱生成的测试使其通过(而是修复规范并重新传播),并将真正的歧义升级给人类而不是猜测。
实现本身是普通的编码——Allium生成规范和测试,而不是应用程序代码。有关完整演练、图表、退出条件和实现提示,请参见推荐循环参考。
快速语法总结
实体
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用于规则操作在模块作用域内存在一次的共享实例的规范。
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模式
Ensures子句有四种结果形式:
- 状态变化:
entity.field = value - 实体创建:
Entity.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)) produces a value
-- equivalent to the original for all supported types.
}
契约是模块级声明,通过名称在表面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)不同,后者使用@符号标记检查器不评估的内容。请参见不变量。
转换图(v3)
entity Order {
status: pending | confirmed | shipped | delivered | cancelled
transitions status {
pending -> confirmed
confirmed -> shipped
shipped -> delivered
pending -> cancelled
confirmed -> cancelled
terminal: delivered, cancelled
}
}
状态依赖字段存在性(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 InterviewerMatching.suggest -- 参见:detailed/interviewer-matching.allium
未解决问题
open question "管理员所有权——是否应将管理员分配到特定角色?"
验证
当安装了allium CLI时,钩子会在每次写入或编辑后自动验证.allium文件。在呈现结果之前修复任何报告的问题。如果CLI不可用,请对照语言参考进行验证。






