其他
从现有代码库反向蒸馏出领域级行为规格(Allium…
已有代码、想反向工程出「系统做什么、为什么重要」的行为规格时,按 bounded context 分片读代码并组装成一份去实现化的 spec。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 35/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 39/100 ×0.15
适用场景
从现有代码库反向蒸馏出领域级行为规格(A特定
解决思路
- 先确定运行模式:Interactive 模式(在对话中内联运行)直接向用户提问并等待回答;Non-interactive 模式(作为 `distill` subagent 运行,例如在 Allium loop 内,联系不到用户)从给定目标自行界定蒸馏范围,不要对判断题做猜测,把每个未确认的判断(有意行为 vs 意外行为、actor 身份、候选 process、范围排除项)记录为蒸馏出 spec 里的 `open question` 声明,并在最终输出中列出所有搁置的问题。
- 界定蒸馏范围:开始读代码前先问「我们要为这个代码库的哪个子集写 spec?」——mono repo 常包含多个不同系统,可能只需要其中一个服务或领域的 spec;先明确边界再动手。
- 问「有没有应该刻意排除的代码?」:legacy(为向后兼容保留但不属于核心系统)、incidental(日志、指标、部署等非领域级的支撑基础设施)、deprecated(计划移除的路径)、experimental(藏在 feature flag 背后、还不是设计决策)。
- 问「谁拥有这份 spec?」:mono repo 的不同部分可能由不同团队拥有,每个团队的 spec 应聚焦自己的领域。
- 对遇到的每条代码路径做 Would we rebuild this? 测试:「如果从零重建这个系统,这条会出现在需求里吗?」是则纳入 spec;属于 legacy、基础设施或 workaround 则排除(但记下 workaround 所解决的底层需求)。
- 在蒸馏出的 spec 顶部用注释记录范围决策:写明 Scope、Includes、Excludes 各是什么;每个 `.allium` 文件的第一行必须是版本标记 `-- allium: N`,N 用当前语言版本号。
- 做 Why 测试:对每个代码细节问「为什么 stakeholder 会在意这个?」——无法说清理由的多半是实现细节(例:邀请 7 天后过期 → 影响候选人体验 → 纳入;token 是 32 字节 URL-safe → 安全实现 → 不纳入;session 存在 Redis → 性能选择 → 不纳入;用 PostgreSQL JSONB → 数据库实现 → 不纳入;slot 状态变为 'proposed' → 影响候选人看到什么 → 纳入;接受邀请时发邮件 → 沟通需求 → 纳入)。
- 做 Could it be different? 测试:「换一种实现方式后它还是同一个系统吗?」是则多半是实现细节、应抽象掉;不是则多半是领域级、应纳入(例:`secrets.token_urlsafe(32)` 可不同 → 不纳入;7 天邀请有效期 不可不同、是设计决策 → 纳入;PostgreSQL 可换 → 不纳入;Pending/Confirmed/Completed 状态 是工作流 → 纳入)。
- 做 Template vs Instance 测试:判断这是某类事物的**类别**还是**具体实例**(Google OAuth vs 认证提供方、Slack webhook vs 通知渠道、SendGrid API vs 邮件投递、`timedelta(hours=3)` vs 确认截止时间);有时实例本身就是领域关注点,见具体细节判断法。
- 牢记代码是过度指定的:把 `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 天过期、slot 状态变为 'proposed');不在意则排除(token 长度、SQLAlchemy ORM、数据库事务 commit);邮件模板名则在模板本身是设计决策时才纳入。
- 区分手段与目的:写的是手段(代码如何实现)还是目的(系统需要什么结果)。`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 例):属于实现的信号是——Google 只是被选中的认证机制、可换成任何 OAuth provider、用户看不到也不关心用哪个 provider、代码是泛化写的(provider 作为参数);属于领域级的信号是——用户明确选择 Google(而非 Microsoft 等)、Sign in with Google 本身是一个功能、使用了 Google 特有的 scope 或权限、支持多个 provider 本身就是功能。判断方法:看 UI 和用户流程,若用户看到 Sign in with Google 是一个选项则领域级;若只看到 Sign in 而 Google 恰好在背后则实现。
- 数据库选择几乎总是实现(`Column(ARRAY(String))`、`JSONB` 写成 `skills: Set<String>`、`metadata: String?`);例外是系统明确向用户承诺 PostgreSQL 兼容或特定 PostgreSQL 特性。
- 第三方集成用同一标准判断(Greenhouse 例):属于实现时抽象为 `external entity Candidate { name: String; email: String; source: CandidateSource }`;属于产品级时显式建模 `greenhouse_id: String?` 并写出 `rule SyncFromGreenhouse`(when: GreenhouseWebhookReceived(candidate_data); ensures: Candidate.created(... greenhouse_id: candidate_data.id))。
- 用 Multiple implementations 启发式:只有一种 OAuth provider 多半是实现,有多种则多半是领域级;只有一个通知渠道多半是实现,同时有 Slack、email、SMS 则多半是领域级——存在多个实现说明「变化」本身就是领域关注点。
- 对超出少量文件规模的代码库不要自己通读:按编排模型做——(1) Map:把代码库划成 bounded context(Step 1 的轻量扫描,不深读);(2) Fan out:每个 bounded context 起一个 subagent,各自只读自己的切片并返回蒸馏后的 fragment(草稿 entity 含状态与转移边、草稿 rule 含 trigger/requires/ensures、external boundary、actor 与 config),每条都带 `file:line` 证据;subagent 只返回 spec fragment 与证据,绝不返回原始源码;给每个 subagent 它的目标路径、map 中的共享 entity 词汇表(保证各 context 命名一致)以及 Step 2–5 的提取指引,要求紧凑 fragment 而非散文评论;(3) Assemble:编排者只持有 map 与返回的 fragment、不持有源码,把 fragment 合并成一份 spec——去重跨切面 entity(`Email`、`Notification`、`AuditLog`)、统一术语(一个概念一个名字)、解析跨 context 引用;(4) 对组装后的 spec 执行 Step 6–7 的抽象与验证。这样做的价值:原始源码不在上下文中累积、不会被逐轮重复处理,每个 subagent 的切片返回 fragment 后即丢弃,读的行数一样但 token 开销小得多。
- 对真正小的代码库(少量文件)不要做 fan-out,直接读代码并原地执行 Step 1–7。
- Step 1 Map the territory:扫描(不要深读)以切出 bounded context 和共享词汇表并规划 fan-out,识别——入口点(API 路由、CLI 命令、消息处理器、定时任务)、领域模型(通常在 `models/`、`entities/`、`domain/`)、业务逻辑(services、use cases、handlers)、外部集成(与哪些第三方通信)、bounded context(按模块/包/功能区分组成内聚切片,作为 fan-out 单元,并记下出现在多个切片中的 entity 作为所有 subagent 必须一致使用的共享词汇表)。产出粗略 map,形如:Entry points(API 路由、Webhooks、Jobs)、Models、Services、Integrations、Bounded contexts(如 scheduling / invitations / intake)、Shared entities。
- Step 2 提取 entity 状态:看 enum 字段与 status 列,如 `status = Column(Enum('pending','accepted','declined','expired'))` 写成 `entity Invitation { status: pending | accepted | declined | expired }`;寻找 enum 定义、status/state 列、`STATUS_PENDING = 'pending'` 之类的常量、状态机库(如 `transitions`、`django-fsm`)。
- Step 2.5 识别候选 process:提取 entity 与状态后扫描暗示端到端流程的状态机,追踪每个 status 值在代码库各处被设置的位置(哪里发生 `status = 'interviewing'`?),把候选 process 拿给用户验证(「我看到一个有 applied → screening → interviewing → deciding → hired/rejected 状态的 entity,这是系统要支持的流程吗?」)。同时追踪跨 entity 数据流:若 entity A 的规则需要 entity B 的字段,顺着链条找该字段在何处被设置、由什么触发,并把链条拿给用户确认(「录用决定要求 `background_check_status = clear`,它由 `/api/webhooks/background-check` 的 webhook handler 设置,这条链对吗?」)。由提取出的规则生成转移图;该图是代码的派生视图;若图有缺口(非终态却没有出边),标为潜在问题。
- Step 3 提取转移:找到状态发生变化的地方,按对照表转换——`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、检查与校验(如 `assert balance >= 0`、类级 validator)可能对应带表达式的 invariant 而非 rule 的前置条件,需判断它描述的是系统级属性还是规则特有的守卫。
- Step 4 找时间触发:找定时任务与基于时间的逻辑(celery task、cron job),把批量扫描转成由时间表达式触发的 `when:`,如 `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(...) }` 与 `rule InterviewReminder { when: interview: Interview.slot.time - 1.hour <= now; requires: interview.status = scheduled; ensures: Notification.created(to: interview.interviewers, template: reminder) }`。
- Step 5 识别外部边界:找第三方 API 调用、webhook handler、import/export 函数,以及只被读取从未写入(或反之)的数据,这些往往指示 external entity(如 Greenhouse 导入的 Candidate 写成 `external entity Candidate { name: String; email: String }`)。当跨服务边界出现重复的接口模式(例如多个消费者期望同一套序列化契约)时,考虑用 `contract` 声明来复用,而不是重复内联 obligation 块。
- Step 5.5 从鉴权模式识别 actor:提取 surface(API endpoint)后检查认证与授权模式——API key 认证 → system actor(外部服务);基于角色的访问(`user.role == 'admin'`)→ 每个角色一个独立 actor;作用域访问(`user.org_id == resource.org_id`)→ 带 `within` 作用域的 actor;未认证 endpoint → 面向公众的 actor 或 system webhook。并向用户确认(「这个 endpoint 需要 admin 角色认证,Admin 是独立 actor,还是同一个用户的高权限形态?」)。
- Step 6 组装与抽象:若做过 fan out,先把返回的 fragment 组装成一份 spec——收集所有 entity、rule、boundary 与 config 值,去重跨切面 entity,统一术语使每个概念只有一个名字(两个 context 命名不同必须此时统一,见重复术语挑战)。然后通篇去掉实现细节:`candidate_id: Integer` → `candidacy: Candidacy`(关系而非外键)、删除 `token: String(32)`(实现细节)、`DateTime` → `Timestamp`(领域类型)、为清晰补上派生属性 `is_expired: expires_at <= now`。派生自其他 config 值的 config 值(如 `extended_timeout = base_timeout * 2`)应使用限定引用或表达式形式的默认值,而不是彼此独立的字面量。
- Step 7 用 stakeholder 验证:把提取出的 spec 当作假设——(1) 给原开发者看:「系统是这样做的吗?」;(2) 给 stakeholder 看:「系统应该这样做吗?」;(3) 找缺口:代码常有 bug 或缺失功能,spec 可能暴露它们。常见发现:「那段重试逻辑是个 hack,应该删掉」「我们本来想要 X 但从没建出来」「这两条代码路径本该一样却不一样」。在做进一步检查前先读 [assessing specs](../allium/references/assessing-specs.md) 判断 spec 成熟度,以确定它是否已可做流程级分析还是仍需结构工作。若 Allium CLI 可用,先对蒸馏出的 spec 运行 `allium check` 抓结构问题,再运行 `allium analyse` 找流程级缺口;`analyse` 的发现可驱动验证问题(「spec 里有个 rule 要求 `background_check.status = clear`,但没有任何 surface 捕获背景检查结果,这是不是由我们没看的代码处理的?」);如何把发现转成领域问题参见 [actioning findings](../allium/references/actioning-findings.md)。
- 识别 library spec 候选:蒸馏时留意那些实现通用集成模式而非应用特有逻辑的代码,它们属于 library spec(完整决策框架见 [recognising library spec opportunities](../elicit/references/library-spec-signals.md))。信号包括:第三方集成模块(如 `StripeWebhookHandler`、`GoogleOAuthProvider`)、配置驱动的集成(如 `OAUTH_CONFIG` 中列 google/microsoft)、带具体 provider 的通用模式(OAuth 流程、支付处理、邮件投递、日历同步、ATS 集成、文件存储)。危险信号:如果开始写出像 `rule ProcessStripeWebhook` 那样过于详细的 spec(那是 Stripe 的领域不是你的),停下来改成 `rule PaymentReceived { when: stripe/InvoicePaid(invoice) ... }`;整合 library spec 的详细示例见 [patterns.md Pattern 8](../allium/references/patterns.md)。
- 处理重复术语挑战:发现同一概念有两个术语(跨 spec、spec 内、spec 与代码之间)时视为阻塞问题。只在注释里承认重复(如 `-- Order vs Purchase ... 这两个是等价概念`)不是解决——不同部分按不同 spec 建出来会让两种术语都留在实现里:重复模型、冗余 join 表、两个方向的外键。做法:选定一个术语(决定前交叉参考相关 spec);更新所有引用,不要在新术语之外保留旧术语的注释或 see also;在 changelog 里记录重命名,而不是写在 spec 里。代码中的警告信号:两个模型表示同一概念(`Order` 和 `Purchase`)、两张表(`order_items`、`purchase_items`)、equivalent to X / same as Y 的注释。提取出的 spec 必须选一个术语,把另一个标为待移除的技术债。
- 处理隐式状态机挑战:代码常有不显式建模的状态,例如 `FeedbackRequest` 用 `requested_at` 已设 / `feedback_id` 为空 / `reminded_at` 为空等组合隐含 pending、reminded、submitted;把它们显式提取为 `status: pending | reminded | submitted` 字段。
- 处理逻辑分散挑战:同一概念规则散落在多处(API handler 里检查 `invitation.status != 'pending'`、model 里 `can_accept()` 检查过期、service 里检查 `slot not in invitation.slots`),合并为一条 rule,把各处检查集中成 `requires` 与 `ensures`。
- 处理死代码与历史事故挑战:不要把从未使用的功能、已修复 bug 的 workaround、永不执行的代码路径写进 spec;不确定时(1) 检查代码是否真的可达、(2) 问开发者是否有意为之、(3) 查 git 历史获取上下文。
- 处理缺失错误处理挑战:代码可能静默失败或错误处理不完整(如 `except SlackError: pass`);spec 应捕获意图行为而不是这个 bug,仍写 `ensures: Notification.created(to: user, channel: slack)`;当前实现是否正确处理失败,与系统应该做什么是两件事。
- 处理过度工程抽象挑战:企业代码常有层层抽象掩盖意图(`NotificationStrategy` 接口 + `SlackNotificationStrategy` 里隔 5 层才真正调用);切到实际行为即可,spec 不需要策略模式、依赖注入或抽象工厂,只需 `ensures: Notification.created(channel: slack, ...)`。
- 定稿前用「抽象够不够」checklist 自查:没有数据库列类型(Integer、VARCHAR 等);没有 ORM 或查询语法;没有 HTTP 状态码或 API 路径;没有框架特有概念(middleware、decorator 等);没有编程语言类型(int、str、List 等);没有代码里的变量名(改用领域术语);没有基础设施(Redis、Kafka、S3 等);外键已替换为关系;token/secret 已删除(身份的实现细节);时间戳使用领域 Duration 而非 timedelta/seconds。若仍有残留,问:「stakeholder 会把这条写进需求文档吗?」
- 用「术语一致性」checklist 自查:每个概念在整个 spec 中只有一个名字;没有 also known as 或 equivalent to 注释;已交叉参考相关 spec 排查冲突术语;代码中的重复模型已标为待移除的技术债。
- 蒸馏之后按需接续:若蒸馏暴露出需要结构化发现的缺口(需求不清、entity 关系复杂、业务规则未明说),用 `elicit` skill 填补;需求演进中的定向修改用 `tend` skill;检查 spec 与实现之间的持续对齐用 `weed` skill。
- 作为 Allium loop 内的 `distill` subagent 运行时,把结果作为单个 JSON 对象返回、不带其他内容,符合 [distill-result.schema.json](../allium/references/schemas/distill-result.schema.json):包含 `spec_path`、搁置的 `open_questions` 数组、一行 `summary` 说明 spec 覆盖什么;每个字段都要输出,空列表用 `[]`;你读过的源码留在调用方上下文之外,只有这些字段返回。交互式运行时按平常方式用散文呈现发现——typed record 是给机器交接的,不是给对话的。示例:`{"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"}`。
适用边界 · 注意事项
- 没有既有代码、需要从 stakeholder 访谈中正向引出需求时,用 `elicit` skill,而不是本蒸馏技能。
- 需求演进中的定向修改用 `tend`,检查 spec 与实现持续对齐用 `weed`,这两类任务不属于蒸馏。
- 真正的小代码库(仅少量文件)不要做 subagent fan-out,直接读并原地执行 Step 1–7。