mcpField
D
distill

distill

github.com/juxt开发5242.6k 次安装5 周前更新
代码仓库

关于 distill

distill 是从现有代码库提炼 Allium 规格的技能,帮助团队界定范围、识别领域行为并剔除实现细节;较大的代码读取任务可按上下文拆分。

主要功能

  1. 01提取领域级行为
  2. 02界定规格范围
  3. 03区分需求与实现
  4. 04按上下文拆分大型代码库
  5. 05记录未确认的判断

适用场景

  1. 01根据现有代码记录服务规格
  2. 02从规格中排除基础设施
  3. 03按限界上下文提炼单体仓库

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/juxt/allium --skill distill

安装后在 Claude Code 中输入/distill确认已加载。

附带文件references/worked-examples.md

详细介绍

规格提炼指南

本指南介绍如何从现有代码库中提取 Allium 规格。核心挑战与正向引导相同:找到合适的抽象层级。引导时,你要在实现思路出现时将其过滤掉;提炼时,你要过滤掉已经存在的实现细节。两者都需要同样的判断力,才能辨别领域层面哪些内容重要。

代码告诉你某项功能是如何运作的。规格描述它做什么以及为何重要。关键在于询问“利益相关者为什么在意这件事?”以及“在仍然属于同一个系统的前提下,这能否有所不同?”

交互模式

此技能以两种模式运行。以下所有要求向用户提问、提示用户或验证信息的指示,均须遵循相应模式:

  • 交互式——在对话中直接运行。直接向用户提问并等待回答。
  • 非交互式——作为 distill 子 Agent 运行(例如在 Allium 循环中),此时无法联系用户。根据收到的目标确定提炼范围,不要猜测需要判断的事项:将每项未经确认的判断——预期行为还是意外行为、参与者身份、候选流程、范围排除项——作为 open question 声明记录在提炼后的规格中,并在最终输出中列出暂存的问题。

确定提炼范围

开始查看代码前,先明确要编写什么的规格。并非每一行代码都值得写入规格。

首先要问的问题
  1. “我们要为此代码库的哪个子集编写规格?” 单体仓库通常包含多个不同的系统。你可能只需要为某项服务或某个领域编写规格。开始前要明确边界。

  2. “是否有代码应该刻意排除?”

    • 遗留代码:为向后兼容而保留、但不属于核心系统的功能
    • 附带代码:非领域层面的支持性基础设施(日志、指标、部署)
    • 已弃用路径:计划移除的代码
    • 实验性功能:由功能标志控制、尚未成为设计决策的功能
  3. “谁负责这份规格?” 单体仓库的不同部分可能由不同团队负责。每个团队的规格都应聚焦于自己的领域。

“我们会重新构建这个吗?”测试

对于遇到的每条代码路径,都要问:“如果从头重新构建这个系统,这会是需求的一部分吗?”

  • 是:纳入规格
  • 否,是遗留内容:排除
  • 否,是基础设施:排除
  • 否,是变通方案:排除(但要注明它所解决的底层需求)
记录范围决策

在提炼后的规格顶部,记录纳入和排除的内容:

-- 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 变成了简单的 candidacy
  • db.session.query(...) 变成了关系遍历
  • 完全移除了 secrets.token_urlsafe(32)(令牌属于实现细节)
  • datetime.utcnow() + timedelta(...) 变成了 now + 7.days
  • db.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. 绘制地图。 将代码库划分为有界上下文;进行轻量扫描(步骤 1),而非深入阅读。
  2. 分派任务。 为每个有界上下文启动一个子 Agent。每个子 Agent 只读取自己负责的部分,并返回提炼后的片段——实体草案(状态 + 转换边)、规则草案(触发条件 / 前提要求 / 确保结果)、外部边界、参与者和配置,并为每项提供 file:line 证据。子 Agent 返回规范片段和证据,绝不返回原始源码。向每个子 Agent 提供目标路径、地图中整理出的共享实体词汇表(确保各上下文使用统一名称),以及步骤 2–5 中的提取指南;要求提供精简片段,而不是说明性文字。
  3. 汇总。 你作为协调者,只保留地图和返回的片段,而非源码。将片段合并成一份规范:去重跨领域实体(Email、Notification、AuditLog),统一术语(每个概念使用一个名称,参见挑战参考),并解决跨上下文引用。
  4. 抽象并验证汇总后的规范(步骤 6–7)。

这样做的好处是:原始源码不会在你的上下文中不断累积,因此不会在每轮中反复处理;每个子 Agent 的片段返回后,其负责的源码部分就会被丢弃。你仍然会阅读所有相关代码行,只是不必一次读完,也不必反复阅读。最终得到的规范相同,所需令牌却少得多。

对于确实很小的代码库(只有少量文件),分派任务的开销不值得;直接阅读代码库,并按顺序内联执行步骤 1–7。

步骤 1:绘制全貌

进行扫描——不要深入阅读——以划分代码库中的有界上下文和共享词汇表,并规划任务分派。识别以下内容:

  1. 入口点。 API 路由、CLI 命令、消息处理器、定时任务。
  2. 领域模型。 通常位于 models/、entities/、domain/ 中。
  3. 业务逻辑。 服务、用例、处理器。
  4. 外部集成。 它会与哪些第三方通信?
  5. 有界上下文。 将以上内容分组成内聚的部分(按模块、包或功能领域划分)——这些部分将作为任务分派的单元。记下出现在多个部分中的实体;它们构成共享词汇表,所有子 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': raiserequires: x.status = pending
if x.expires_at < now: raiserequires: x.expires_at > now
if item not in collection: raiserequires: 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 步:与利益相关者验证

提取出的规范只是一个假设。请对其进行验证:

  1. 将规范展示给原始开发人员。“系统的行为是这样吗?”
  2. 将规范展示给利益相关者。“系统应该这样运行吗?”
  3. **查找缺漏。**代码可能有 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 为 null
  • reminded:已设置 reminded_at,feedback_id 为 null
  • submitted:已设置 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
    ...
}
挑战:死代码和历史遗留问题

代码库会不断积累已构建但从未使用的功能、针对现已修复的错误所做的变通方案,以及从未执行过的代码路径。

不要将这些内容纳入规范。如果不确定:

  1. 检查代码是否实际可达
  2. 询问开发者这是否有意为之
  3. 查看 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"
}

参考资料

  • 语言参考,完整的 Allium 语法
  • 评估规范,如何评估规范成熟度并选择合适的分析深度
  • 处理发现,如何将检查器发现转化为领域问题
  • 示例详解,使用 Python、TypeScript 和 Java 编写的完整代码转规范示例

评论

支持 Markdown