关于 distill
distill 是从现有代码库提炼 Allium 规格的技能,帮助团队界定范围、识别领域行为并剔除实现细节;较大的代码读取任务可按上下文拆分。
主要功能
- 01提取领域级行为
- 02界定规格范围
- 03区分需求与实现
- 04按上下文拆分大型代码库
- 05记录未确认的判断
适用场景
- 01根据现有代码记录服务规格
- 02从规格中排除基础设施
- 03按限界上下文提炼单体仓库
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ juxt/ allium --skill distill安装后在 Claude Code 中输入/distill确认已加载。
附带文件references/worked-examples.md
详细介绍
规格提炼指南
本指南介绍如何从现有代码库中提取 Allium 规格。核心挑战与正向引导相同:找到合适的抽象层级。引导时,你要在实现思路出现时将其过滤掉;提炼时,你要过滤掉已经存在的实现细节。两者都需要同样的判断力,才能辨别领域层面哪些内容重要。
代码告诉你某项功能是如何运作的。规格描述它做什么以及为何重要。关键在于询问“利益相关者为什么在意这件事?”以及“在仍然属于同一个系统的前提下,这能否有所不同?”
交互模式
此技能以两种模式运行。以下所有要求向用户提问、提示用户或验证信息的指示,均须遵循相应模式:
- 交互式——在对话中直接运行。直接向用户提问并等待回答。
- 非交互式——作为
distill子 Agent 运行(例如在 Allium 循环中),此时无法联系用户。根据收到的目标确定提炼范围,不要猜测需要判断的事项:将每项未经确认的判断——预期行为还是意外行为、参与者身份、候选流程、范围排除项——作为open question声明记录在提炼后的规格中,并在最终输出中列出暂存的问题。
确定提炼范围
开始查看代码前,先明确要编写什么的规格。并非每一行代码都值得写入规格。
首先要问的问题
-
“我们要为此代码库的哪个子集编写规格?” 单体仓库通常包含多个不同的系统。你可能只需要为某项服务或某个领域编写规格。开始前要明确边界。
-
“是否有代码应该刻意排除?”
- 遗留代码:为向后兼容而保留、但不属于核心系统的功能
- 附带代码:非领域层面的支持性基础设施(日志、指标、部署)
- 已弃用路径:计划移除的代码
- 实验性功能:由功能标志控制、尚未成为设计决策的功能
-
“谁负责这份规格?” 单体仓库的不同部分可能由不同团队负责。每个团队的规格都应聚焦于自己的领域。
“我们会重新构建这个吗?”测试
对于遇到的每条代码路径,都要问:“如果从头重新构建这个系统,这会是需求的一部分吗?”
- 是:纳入规格
- 否,是遗留内容:排除
- 否,是基础设施:排除
- 否,是变通方案:排除(但要注明它所解决的底层需求)
记录范围决策
在提炼后的规格顶部,记录纳入和排除的内容:
-- allium: 3
-- interview-scheduling.allium
-- Scope: Interview scheduling flow only
-- Includes: Candidacy, Interview, InterviewSlot, Invitation, Feedback
-- Excludes:
-- - User authentication (use auth library spec)
-- - Analytics/reporting (separate spec)
-- - Legacy V1 API (deprecated, not specified)
-- - Greenhouse sync (use greenhouse library spec)
版本标记(-- allium: N)必须是每个 .allium 文件的第一行。使用当前的语言版本号。
找到合适的抽象层级
提炼和引导面临相同的根本挑战:选择纳入哪些内容。无论你是在听利益相关者描述功能,还是在阅读实现该功能的代码,以下测试都适用。
“为什么”测试
针对代码中的每项细节,都要问:“利益相关者为什么在意这件事?”
| 代码细节 | 为什么? | 是否纳入? |
|---|---|---|
| 邀请在 7 天后过期 | 影响候选人的体验 | 是 |
| 令牌为 32 字节 URL 安全格式 | 安全实现方式 | 否 |
| 会话存储在 Redis 中 | 性能选择 | 否 |
| 使用 PostgreSQL JSONB | 数据库实现方式 | 否 |
| 时段状态变为 'proposed' | 影响候选人看到的内容 | 是 |
| 接受邀请时发送邮件 | 沟通需求 | 是 |
如果你无法说清楚利益相关者为何在意某项内容,那它很可能属于实现细节。
“能否采用不同方式实现?”测试
询问:“在仍然是同一个系统的前提下,这部分能否采用不同方式实现?”
- 如果可以:很可能是实现细节,应予以抽象
- 如果不可以:很可能属于领域层面,应予以纳入
| 细节 | 能否采用不同方式? | 是否纳入? |
|---|---|---|
secrets.token_urlsafe(32) | 可以,任何安全的令牌生成方式都行 | 否 |
| 邀请有效期为 7 天 | 不可以,这是设计决策 | 是 |
| PostgreSQL 数据库 | 可以,任何数据库都行 | 否 |
| “待处理、已确认、已完成”状态 | 不可以,这是工作流 | 是 |
“模板与实例”测试
这是一个事物的类别,还是一个具体实例?
| 实例(通常属于实现层面) | 模板(通常属于领域层面) |
|---|---|
| Google OAuth | 身份验证提供商 |
| Slack webhook | 通知渠道 |
| SendGrid API | 电子邮件发送 |
timedelta(hours=3) | 确认期限 |
有时,实例本身就是领域关注点。请参阅下文“具体细节问题”。
提炼思维
代码包含过多细节
代码的每一行都做出了某些决策,而这些决策在领域层面可能无关紧要:
# Code tells you:
def send_invitation(candidate_id: int, slot_ids: List[int]) -> Invitation:
candidate = db.session.query(Candidate).get(candidate_id)
slots = db.session.query(InterviewSlot).filter(
InterviewSlot.id.in_(slot_ids),
InterviewSlot.status == 'confirmed'
).all()
invitation = Invitation(
candidate_id=candidate_id,
token=secrets.token_urlsafe(32),
expires_at=datetime.utcnow() + timedelta(days=7),
status='pending'
)
db.session.add(invitation)
for slot in slots:
slot.status = 'proposed'
invitation.slots.append(slot)
db.session.commit()
send_email(
to=candidate.email,
template='interview_invitation',
context={'invitation': invitation, 'slots': slots}
)
return invitation
-- Specification should say:
rule SendInvitation {
when: SendInvitation(candidacy, slots)
requires: slots.all(s => s.status = confirmed)
ensures:
for s in slots:
s.status = proposed
ensures: Invitation.created(
candidacy: candidacy,
slots: slots,
expires_at: now + 7.days,
status: pending
)
ensures: Email.created(
to: candidacy.candidate.email,
template: interview_invitation
)
}
我们删去了以下内容:
candidate_id: int变成了简单的candidacydb.session.query(...)变成了关系遍历- 完全移除了
secrets.token_urlsafe(32)(令牌属于实现细节) datetime.utcnow() + timedelta(...)变成了now + 7.daysdb.session.add/commit已隐含在created中invitation.slots.append(slot)已隐含在关系中
问问“产品负责人会在意吗?”
对于代码中的每个细节,都要问:
| 代码细节 | 产品负责人会在意吗? | 是否纳入? |
|---|---|---|
| 邀请在 7 天后过期 | 会,影响候选人的体验 | 是 |
| 令牌为 32 字节且可用于 URL | 不会,属于安全实现 | 否 |
| 使用 SQLAlchemy ORM | 不会,属于持久化机制 | 否 |
| 电子邮件模板名称 | 也许,如果模板属于设计决策 | 也许 |
| 时段状态变为 'proposed' | 会,影响候选人看到的内容 | 是 |
| 提交数据库事务 | 不会,属于实现细节 | 否 |
区分手段与目的
**手段:**代码如何实现某件事。
**目的:**系统需要达成什么结果。
| 手段(代码) | 目的(规格说明) |
|---|---|
requests.post('https://slack.com/api/...') | Notification.created(channel: slack) |
candidate.oauth_token = google.exchange(code) | Candidate authenticated |
redis.setex(f'session:{id}', 86400, data) | Session.created(expires: 24.hours) |
for slot in slots: slot.status = 'cancelled' | for s in slots: s.status = cancelled |
具体细节问题
最难判断的是:具体细节何时属于领域,何时只是实现?
Google OAuth 示例
你找到了这段代码:
OAUTH_PROVIDERS = {
'google': GoogleOAuthProvider(client_id=..., client_secret=...),
}
def authenticate(provider: str, code: str) -> User:
return OAUTH_PROVIDERS[provider].authenticate(code)
问题:“Google OAuth”属于领域层面还是实现层面?
属于实现层面的情况:
- Google 只是选用的身份验证机制
- 它可以替换为任何 OAuth 提供商
- 用户看不到或不在意使用的是哪个提供商
- 代码以通用方式编写(提供商是一个参数)
属于领域层面的情况:
- 用户明确选择 Google(而不是 Microsoft 等)
- “使用 Google 登录”是一项功能
- 使用了 Google 专属的作用域或权限
- 支持多个提供商是一项功能
**判断方法:**查看 UI 和用户流程。如果用户看到“使用 Google 登录”并将其作为一个选项,那么它属于领域层面。如果用户只看到“登录”,而 Google 只是在背后提供支持,那么它属于实现层面。
数据库选择示例
你找到了 PostgreSQL 专用代码:
from sqlalchemy.dialects.postgresql import JSONB, ARRAY
class Candidate(Base):
skills = Column(ARRAY(String))
metadata = Column(JSONB)
**几乎总是属于实现层面。**规格说明应写成:
entity Candidate {
skills: Set<String>
metadata: String? -- or model specific fields
}
具体数据库很少属于领域层面。例外情况:系统明确向用户承诺兼容 PostgreSQL,或承诺使用 PostgreSQL 的特定功能。
第三方集成示例
你发现了 Greenhouse ATS 集成:
class GreenhouseSync:
def import_candidate(self, greenhouse_id: str) -> Candidate:
data = self.client.get_candidate(greenhouse_id)
return Candidate(
name=data['name'],
email=data['email'],
greenhouse_id=greenhouse_id,
source='greenhouse'
)
可能属于以下任一情况:
如果符合以下条件,则属于实现细节:
- Greenhouse 只是候选人的来源
- 可以换成 Lever、Workable 等
- 该集成只是“导入候选人”的实现细节
规范:
external entity Candidate {
name: String
email: String
source: CandidateSource
}
如果符合以下条件,则属于产品层面:
- “Greenhouse 集成”是一项卖点
- 用户可以配置与 Greenhouse 的连接
- 提供 Greenhouse 专属功能(例如将反馈同步回 Greenhouse)
规范:
external entity Candidate {
name: String
email: String
greenhouse_id: String? -- explicitly modeled
}
rule SyncFromGreenhouse {
when: GreenhouseWebhookReceived(candidate_data)
ensures: Candidate.created(
...
greenhouse_id: candidate_data.id
)
}
“多种实现”启发式
查看代码库中的差异:
- 如果只有一个 OAuth 提供方,可能属于实现细节
- 如果有多个 OAuth 提供方,可能属于领域层面
- 如果只有一种通知渠道,可能属于实现细节
- 如果同时支持 Slack、电子邮件和 SMS,可能属于领域层面
存在多种实现,说明这种差异本身就是领域关注点。
提炼流程
提炼需要阅读大量代码,但最终产出的是一份精简规范。代价高昂的错误,是让所有源码堆积在一个上下文窗口中,导致每轮都要重新读取。保持工作集精简:将以读取为主的步骤交由子 Agent 协调处理,只保留提炼后的输出。
协调模式
对于超过少量文件的代码库,不要自己通读整个代码库。请改为:
- 绘制地图。 将代码库划分为有界上下文;进行轻量扫描(步骤 1),而非深入阅读。
- 分派任务。 为每个有界上下文启动一个子 Agent。每个子 Agent 只读取自己负责的部分,并返回提炼后的片段——实体草案(状态 + 转换边)、规则草案(触发条件 / 前提要求 / 确保结果)、外部边界、参与者和配置,并为每项提供
file:line证据。子 Agent 返回规范片段和证据,绝不返回原始源码。向每个子 Agent 提供目标路径、地图中整理出的共享实体词汇表(确保各上下文使用统一名称),以及步骤 2–5 中的提取指南;要求提供精简片段,而不是说明性文字。 - 汇总。 你作为协调者,只保留地图和返回的片段,而非源码。将片段合并成一份规范:去重跨领域实体(
Email、Notification、AuditLog),统一术语(每个概念使用一个名称,参见挑战参考),并解决跨上下文引用。 - 抽象并验证汇总后的规范(步骤 6–7)。
这样做的好处是:原始源码不会在你的上下文中不断累积,因此不会在每轮中反复处理;每个子 Agent 的片段返回后,其负责的源码部分就会被丢弃。你仍然会阅读所有相关代码行,只是不必一次读完,也不必反复阅读。最终得到的规范相同,所需令牌却少得多。
对于确实很小的代码库(只有少量文件),分派任务的开销不值得;直接阅读代码库,并按顺序内联执行步骤 1–7。
步骤 1:绘制全貌
进行扫描——不要深入阅读——以划分代码库中的有界上下文和共享词汇表,并规划任务分派。识别以下内容:
- 入口点。 API 路由、CLI 命令、消息处理器、定时任务。
- 领域模型。 通常位于
models/、entities/、domain/中。 - 业务逻辑。 服务、用例、处理器。
- 外部集成。 它会与哪些第三方通信?
- 有界上下文。 将以上内容分组成内聚的部分(按模块、包或功能领域划分)——这些部分将作为任务分派的单元。记下出现在多个部分中的实体;它们构成共享词汇表,所有子 Agent 都必须始终使用一致的名称。
创建一份粗略地图:
Entry points:
- API: /api/candidates/*, /api/interviews/*, /api/invitations/*
- Webhooks: /webhooks/greenhouse, /webhooks/calendar
- Jobs: send_reminders, expire_invitations, sync_calendars
Models:
- Candidate, Interview, InterviewSlot, Invitation, Feedback
Services:
- SchedulingService, NotificationService, CalendarService
Integrations:
- Google Calendar, Slack, Greenhouse, SendGrid
Bounded contexts (fan-out units):
- scheduling: Interview, InterviewSlot (SchedulingService, /api/interviews)
- invitations: Invitation, Feedback (/api/invitations, expire job)
- intake: Candidate (Greenhouse webhook) — external
Shared entities: Candidate, Interview (appear across contexts)
步骤 2–5 是每个受分派任务的子 Agent 在自己负责的部分中应用的提取指南(对于小型代码库,则由你直接应用)。将这些指南与目标路径及共享词汇表一起交给每个子 Agent;收集片段,并按照协调模式进行汇总。
步骤 2:提取实体状态
查看枚举字段和状态列:
class Invitation(Base):
status = Column(Enum('pending', 'accepted', 'declined', 'expired'))
转换为:
entity Invitation {
status: pending | accepted | declined | expired
}
查找枚举定义、状态或状态列、STATUS_PENDING = 'pending' 之类的常量,以及状态机库(例如 transitions、django-fsm)。
步骤 2.5:识别候选流程
提取实体及其状态后,扫描可能表示端到端流程的状态机。追踪代码库中每个状态值的设置位置(status = 'interviewing' 在哪里发生?)。向用户展示候选流程以供确认:“我看到一个实体具有 applied → screening → interviewing → deciding → hired/rejected 这些状态。系统是否应该支持这一流程?”
同时追踪跨实体的数据流。如果实体 A 上的规则需要实体 B 的字段,就沿着链路追踪:实体 B 的字段在哪里设置,又由什么触发?展示这一链路:“招聘决定要求 background_check_status = clear。该值由 /api/webhooks/background-check 处的 webhook 处理程序设置。这条链路是否正确?”
根据提取出的规则生成状态转换图。该图是从代码中推导出的视图。如果图中存在缺口(某些状态没有后续转换,但并非终止状态),请将其标记为潜在问题。
步骤 3:提取状态转换
查找状态变更的位置:
def accept_invitation(invitation_id: int, slot_id: int):
invitation = get_invitation(invitation_id)
if invitation.status != 'pending':
raise InvalidStateError()
if invitation.expires_at < datetime.utcnow():
raise ExpiredError()
slot = get_slot(slot_id)
if slot not in invitation.slots:
raise InvalidSlotError()
invitation.status = 'accepted'
slot.status = 'booked'
# Release other slots
for other_slot in invitation.slots:
if other_slot.id != slot_id:
other_slot.status = 'available'
# Create the interview
interview = Interview(
candidate_id=invitation.candidate_id,
slot_id=slot_id,
status='scheduled'
)
notify_interviewers(interview)
send_confirmation_email(invitation.candidate, interview)
提取:
rule CandidateAcceptsInvitation {
when: CandidateAccepts(invitation, slot)
requires: invitation.status = pending
requires: invitation.expires_at > now
requires: slot in invitation.slots
ensures: invitation.status = accepted
ensures: slot.status = booked
ensures:
for s in invitation.slots:
if s != slot: s.status = available
ensures: Interview.created(
candidacy: invitation.candidacy,
slot: slot,
status: scheduled
)
ensures: Notification.created(to: slot.interviewers, ...)
ensures: Email.created(to: invitation.candidate.email, ...)
}
关键提取模式:
| 代码模式 | 规范模式 |
|---|---|
if x.status != 'pending': raise | requires: x.status = pending |
if x.expires_at < now: raise | requires: x.expires_at > now |
if item not in collection: raise | requires: item in collection |
x.status = 'accepted' | ensures: x.status = accepted |
Model.create(...) | ensures: Model.created(...) |
send_email(...) | ensures: Email.created(...) |
notify(...) | ensures: Notification.created(...) |
代码中发现的断言、检查和验证(例如 assert balance >= 0、类级验证器)可能对应带表达式的不变量,而不是规则前置条件。请判断它们描述的是系统范围的属性,还是特定规则的守卫条件。
步骤 4:查找时间触发器
查找定时任务和基于时间的逻辑:
# In celery tasks or cron jobs
@app.task
def expire_invitations():
expired = Invitation.query.filter(
Invitation.status == 'pending',
Invitation.expires_at < datetime.utcnow()
).all()
for invitation in expired:
invitation.status = 'expired'
for slot in invitation.slots:
slot.status = 'available'
notify_candidate_expired(invitation)
@app.task
def send_reminders():
upcoming = Interview.query.filter(
Interview.status == 'scheduled',
Interview.slot.time.between(
datetime.utcnow() + timedelta(hours=1),
datetime.utcnow() + timedelta(hours=2)
)
).all()
for interview in upcoming:
send_reminder_notification(interview)
提取:
rule InvitationExpires {
when: invitation: Invitation.expires_at <= now
requires: invitation.status = pending
ensures: invitation.status = expired
ensures:
for s in invitation.slots:
s.status = available
ensures: CandidateInformed(candidate: invitation.candidate, about: invitation_expired)
}
rule InterviewReminder {
when: interview: Interview.slot.time - 1.hour <= now
requires: interview.status = scheduled
ensures: Notification.created(to: interview.interviewers, template: reminder)
}
步骤 5:识别外部边界
查找第三方 API 调用、webhook 处理程序、导入/导出函数,以及只读不写(或反之)的数据。
这些通常表示外部实体:
# Candidate data comes from Greenhouse, we don't create it
def import_from_greenhouse(webhook_data):
candidate = Candidate.query.filter_by(
greenhouse_id=webhook_data['id']
).first()
if not candidate:
candidate = Candidate(greenhouse_id=webhook_data['id'])
candidate.name = webhook_data['name']
candidate.email = webhook_data['email']
表示:
external entity Candidate {
name: String
email: String
}
如果服务边界之间出现重复的接口模式(例如多个使用方都需要相同的序列化约定),这表明应声明可复用的 contract,而不是重复编写内联义务块。
步骤 5.5:根据身份验证模式识别参与者
从 API 端点中提取接口后,通过检查身份验证和授权模式来识别参与者。不同的身份验证上下文通常对应不同的参与者:
- API 密钥身份验证 → 系统参与者(外部服务)
- 基于角色的访问(
user.role == 'admin')→ 每种角色对应一个不同的参与者 - 作用域访问(
user.org_id == resource.org_id)→ 使用within作用域的参与者 - 未经身份验证的端点 → 面向公众的参与者或系统 webhook
请用户确认:“此端点要求通过管理员角色身份验证。‘Admin’ 是独立的参与者,还是与拥有更高权限的普通用户是同一个人?”
第 6 步:汇总并抽象
如果你拆分了任务,首先将返回的片段汇总为一份规范:收集所有实体、规则、边界和配置值;去重跨领域实体,并统一术语,确保每个概念只有一个名称(如果两个上下文对同一概念使用了不同名称,现在就必须统一——参见下文的“术语重复挑战”)。然后检查汇总后的规范,移除实现细节。
之前(过于具体):
entity Invitation {
candidate_id: Integer
token: String(32)
created_at: DateTime
expires_at: DateTime
status: pending | accepted | declined | expired
}
之后(领域级):
entity Invitation {
candidacy: Candidacy
created_at: Timestamp
expires_at: Timestamp
status: pending | accepted | declined | expired
is_expired: expires_at <= now
}
变更:
candidate_id: Integer变为candidacy: Candidacy(关系,而非外键)- 移除了
token: String(32)(实现细节) DateTime变为Timestamp(领域类型)- 添加了派生属性
is_expired,以使规范更清晰
由其他配置值推导得出的配置值(例如 extended_timeout = base_timeout * 2),应在配置块中使用限定引用或表达式形式的默认值,而不是使用彼此独立的字面值。
第 7 步:与利益相关者验证
提取出的规范只是一个假设。请对其进行验证:
- 将规范展示给原始开发人员。“系统的行为是这样吗?”
- 将规范展示给利益相关者。“系统应该这样运行吗?”
- **查找缺漏。**代码可能有 bug 或缺失的功能;规范或许能揭示这些问题。
常见发现:
- “哦,那段重试逻辑只是个临时解决方案,我们应该移除它。”
- “其实我们想要 X,但一直没实现。”
- “这两条代码路径应该相同,但实际上并不相同。”
在运行进一步检查之前,请阅读评估规范,以判断提炼后的规范成熟度。这可以帮助你确定规范是否已准备好进行流程级分析,还是仍需进行结构调整。
如果可以使用 Allium CLI,请对提炼后的规范运行 allium check,以发现结构问题,然后运行 allium analyse,识别流程级缺漏。analyse 的发现可用于提出验证问题:“提炼后的规范有一条规则,要求 background_check.status = clear,但没有任何界面收集背景调查结果。这是否由我们尚未检查的代码库部分处理?”有关如何将发现转化为领域问题,请参阅处理分析发现。
识别候选库规范
提炼过程中,请留意实现通用集成模式而非特定于应用逻辑的代码。这些内容应归入库规范。完整的决策框架(需提出的问题、处理方式和常见提取内容)请参阅识别库规范机会。
代码中的信号
留意以下可能表明存在库规范的模式:
第三方集成模块:
class StripeWebhookHandler:
def handle_invoice_paid(self, event):
...
class GoogleOAuthProvider:
def exchange_code(self, code):
...
由配置驱动的集成:
OAUTH_CONFIG = {
'google': {'client_id': ..., 'scopes': ...},
'microsoft': {'client_id': ..., 'scopes': ...},
}
使用特定提供商实现的通用模式: OAuth 流程、支付处理、邮件投递、日历同步、ATS 集成、文件存储。
警示信号:规范中包含集成逻辑
如果你发现自己正在编写这样的规范,请停下来重新考虑:
-- TOO DETAILED - this is Stripe's domain, not yours
rule ProcessStripeWebhook {
when: WebhookReceived(payload, signature)
requires: verify_stripe_signature(payload, signature)
let event = parse_stripe_event(payload)
if event.type = "invoice.paid":
...
}
应改为:
-- Application responds to payment events (integration handled elsewhere)
rule PaymentReceived {
when: stripe/InvoicePaid(invoice)
...
}
有关集成库规范的详细示例,请参阅 patterns.md 第 8 种模式。
常见的提炼挑战
挑战:术语重复
当你发现同一概念有两个术语(跨规范、同一规范内,或规范与代码之间)时,应将其视为阻塞问题。
-- BAD: Acknowledges duplication without resolving it
-- Order vs Purchase
-- checkout.allium uses "Purchase" - these are equivalent concepts.
这并不能解决问题。如果代码库的不同部分基于不同的规范构建,这两个术语最终都会出现在实现中:重复的模型、冗余的联结表,以及指向两个方向的外键。
应采取的措施:
- 选定一个术语。做出决定前,交叉参照相关规范。
- 更新所有引用。不要在注释或“另请参阅”说明中保留旧术语。
- 在变更日志中记录重命名,而不是在规范本身中记录。
代码中的警示信号:
- 两个模型表示同一概念(
Order和Purchase) - 两张联结表(
order_items、purchase_items) - 类似“等同于 X”或“与 Y 相同”的注释
提取出的规范必须选定一个术语。将另一个术语标记为待清除的技术债务。
挑战:隐式状态机
代码通常包含未建模的隐式状态:
# No explicit status field, but there's a state machine hiding here
class FeedbackRequest:
interview_id = Column(Integer)
interviewer_id = Column(Integer)
requested_at = Column(DateTime)
reminded_at = Column(DateTime, nullable=True)
feedback_id = Column(Integer, nullable=True) # FK to Feedback if submitted
隐式状态包括:
pending:已设置 requested_at,feedback_id 为 null,reminded_at 为 nullreminded:已设置 reminded_at,feedback_id 为 nullsubmitted:已设置 feedback_id
将其提取为显式状态:
entity FeedbackRequest {
interview: Interview
interviewer: Interviewer
requested_at: Timestamp
reminded_at: Timestamp?
status: pending | reminded | submitted
}
挑战:分散的逻辑
同一概念规则可能分散在多个地方:
# In API handler
def accept_invitation(request):
if invitation.status != 'pending':
return error(400, "Already responded")
...
# In model
class Invitation:
def can_accept(self):
return self.expires_at > datetime.utcnow()
# In service
def process_acceptance(invitation, slot):
if slot not in invitation.slots:
raise InvalidSlot()
...
将其整合为一条规则:
rule CandidateAccepts {
when: CandidateAccepts(invitation, slot)
requires: invitation.status = pending
requires: invitation.expires_at > now
requires: slot in invitation.slots
...
}
挑战:死代码和历史遗留问题
代码库会不断积累已构建但从未使用的功能、针对现已修复的错误所做的变通方案,以及从未执行过的代码路径。
不要将这些内容纳入规范。如果不确定:
- 检查代码是否实际可达
- 询问开发者这是否有意为之
- 查看 git 历史了解背景
挑战:缺少错误处理
代码可能会静默失败,或错误处理不完整:
def send_notification(user, message):
try:
slack.send(user.slack_id, message)
except SlackError:
pass # Silently ignore failures
规范应描述预期行为,而不是错误:
ensures: Notification.created(to: user, channel: slack)
当前实现是否妥善处理了故障,与系统应当如何运行是两回事。
挑战:过度设计的抽象
企业代码库中常有模糊意图的抽象层:
public interface NotificationStrategy {
void notify(NotificationContext context);
}
public class SlackNotificationStrategy implements NotificationStrategy {
@Override
public void notify(NotificationContext context) {
// Actual Slack call buried 5 levels deep
}
}
直接说明实际行为。规范不需要策略模式、依赖注入或抽象工厂。只需写明:ensures: Notification.created(channel: slack, ...)
检查清单:抽象是否足够?
在最终确定提炼后的规范前:
- 不包含数据库列类型(Integer、VARCHAR 等)
- 不包含 ORM 或查询语法
- 不包含 HTTP 状态码或 API 路径
- 不包含框架专属概念(中间件、装饰器等)
- 不包含编程语言类型(int、str、List 等)
- 不包含代码中的变量名(使用领域术语)
- 不包含基础设施(Redis、Kafka、S3 等)
- 用关系替代外键
- 移除令牌/密钥(身份的实现细节)
- 时间戳使用领域 Duration,而不是 timedelta/秒
如果仍有任何此类内容,请问:“利益相关者会把它写进需求文档吗?”
检查清单:术语一致性
- 每个概念在整个规范中只有一个名称
- 没有“也称为”或“等同于”的注释
- 已交叉参照相关规范,检查术语冲突
- 已将代码中的重复模型标记为待清除的技术债务
提炼后
提取出的规范只是起点。如果提炼过程发现需要结构化探索的空白(需求不明确、实体关系复杂、业务规则未说明),请使用 elicit 技能补全。随着需求演变进行针对性修改时,请使用 tend 技能。要检查规范与实现之间是否持续保持一致,请使用 weed 技能。
类型化结果(循环交接)
在 Allium 循环中作为 distill 子 Agent 运行时,请将结果作为一个符合 distill-result.schema.json 的单一 JSON 对象返回,不要附带其他内容:包含 spec_path、暂存的 open_questions,以及一行概述规范涵盖内容的 summary。输出所有字段,空列表使用 []。你读取的源内容不会进入调用方的上下文;只有这些字段会返回。以交互方式运行时,照常用文字呈现你的发现——类型化记录用于机器交接,而非对话。
{
"phase": "distill",
"spec_path": "giftcard.allium",
"open_questions": ["Is forcing an over-redeemed balance to zero intended or accidental?"],
"summary": "GiftCard redemption and status lifecycle"
}