关于 fork-discipline
Fork Discipline 是用于检查代码库中共享平台代码与部署专属客户端代码边界的技能,可为多客户端项目绘制边界、报告常见问题,并生成文档或重构计划。
主要功能
- 01识别多客户端项目模式
- 02绘制共享代码与客户端代码边界
- 03查找边界违规和配置问题
- 04生成边界报告或 FORK.md
- 05规划重构和迁移脚本
适用场景
- 01新增客户端前检查架构
- 02审计边界模糊的代码库
- 03记录共享与客户端专属部分
- 04规划多客户端重构
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill fork-discipline安装后在 Claude Code 中输入/fork-discipline确认已加载。
详细介绍
分支规范
审查多客户端代码库中的核心/客户端边界。每个多客户端项目都应清晰区分共享平台代码(核心)与特定部署代码(客户端)。此技能可找出边界模糊之处,并说明如何修复。
原则
project/
src/ ← CORE: shared platform code. Never modified per client.
config/ ← DEFAULTS: base config, feature flags, sensible defaults.
clients/
client-name/ ← CLIENT: everything that varies per deployment.
config ← overrides merged over defaults
content ← seed data, KB articles, templates
schema ← domain tables, migrations (numbered 0100+)
custom/ ← bespoke features (routes, pages, tools)
分支测试:修改任何文件之前,先问自己:“这是核心代码还是客户端代码?”如果无法判断,说明边界还不够清晰。
适用场景
- 为现有项目添加第二个或第三个客户端之前
- 项目在自然发展后边界变得模糊时
- 发现共享代码中开始出现
if (client === 'acme')检查时 - 进行重大重构之前,以了解哪些内容是共享的,哪些是特定的
- 新开发者入职,需要了解架构时
- 定期检查多客户端项目的健康状况
模式
| 模式 | 触发条件 | 产出内容 |
|---|---|---|
| audit | “fork discipline”“check the boundary” | 边界图 + 违规报告 |
| document | “write FORK.md”“document the boundary” | 项目的 FORK.md 文件 |
| refactor | “clean up the fork”“enforce the boundary” | 重构计划 + 迁移脚本 |
默认:audit
审计模式
步骤 1:检测项目类型
判断这是一个多客户端项目,以及它采用了哪种模式:
| 信号 | 模式 |
|---|---|
clients/ 或 tenants/ 目录 | 显式多客户端 |
| 多个以客户端命名的配置文件 | 配置驱动的多客户端 |
packages/ 中包含共享包和每个客户端专属包 | Monorepo 多客户端 |
CLIENT_NAME 或 TENANT_ID 等环境变量 | 运行时多客户端 |
| 只有一个部署,没有客户端目录 | 单客户端(可能正朝多客户端发展) |
如果是单客户端项目:检查项目的 CLAUDE.md 或代码库是否表明它将发展为多客户端项目。如果是,则审计其就绪情况。如果确定永远只有一个客户端,则无需使用此技能。
步骤 2:绘制边界图
扫描代码库,绘制边界图:
CORE (shared by all clients):
src/server/ → API routes, middleware, auth
src/client/ → React components, hooks, pages
src/db/schema.ts → Shared database schema
migrations/0001-0050 → Core migrations
CLIENT (per-deployment):
clients/acme/config.ts → Client overrides
clients/acme/kb/ → Knowledge base articles
clients/acme/seed.sql → Seed data
migrations/0100+ → Client schema extensions
BLURRED (needs attention):
src/server/routes/acme-custom.ts → Client code in core!
src/config/defaults.ts line 47 → Hardcoded client domain
步骤 3:查找违规
扫描以下特定反模式:
核心代码中的客户端名称
# Search for hardcoded client identifiers in shared code
grep -rn "acme\|smith\|client_name_here" src/ --include="*.ts" --include="*.tsx"
# Search for client-specific conditionals
grep -rn "if.*client.*===\|switch.*client\|case.*['\"]acme" src/ --include="*.ts" --include="*.tsx"
# Search for environment-based client checks in shared code
grep -rn "CLIENT_NAME\|TENANT_ID\|process.env.*CLIENT" src/ --include="*.ts" --include="*.tsx"
严重程度:高。核心代码中的每一处硬编码客户端检查,都意味着下一个客户端需要修改共享代码。
替换配置而非合并
检查客户端配置是替换整个文件,还是与默认配置合并:
// BAD — client config is a complete replacement
// clients/acme/config.ts
export default {
theme: { primary: '#1E40AF' },
features: { emailOutbox: true },
// Missing all other defaults — they're lost
}
// GOOD — client config is a delta merged over defaults
// clients/acme/config.ts
export default {
theme: { primary: '#1E40AF' }, // Only overrides what's different
}
// config/defaults.ts has everything else
查找:可疑地过大的客户端配置文件(接近默认配置文件的大小),或定义了默认配置已处理字段的客户端配置。
严重程度:中。过时的客户端配置会遗漏新的默认值和功能。
分散的客户端代码
检查特定客户端的代码是否位于客户端目录之外:
# Files with client names in their path but inside src/
find src/ -name "*acme*" -o -name "*smith*" -o -name "*client-name*"
# Routes or pages that serve a single client
grep -rn "// only for\|// acme only\|// client-specific" src/ --include="*.ts" --include="*.tsx"
严重程度:高。src/ 中的客户端代码意味着核心并非真正共享。
缺少扩展点
检查核心是否提供了无需修改即可自定义客户端的机制:
| 扩展点 | 检查方法 | 支持的功能 |
|---|---|---|
| 配置合并 | config/ 是否有合并函数? | 覆盖客户端配置而不替换整个配置 |
| 动态导入 | 核心是否会查找 clients/{name}/custom/? | 客户端专属路由/页面 |
| 功能标志 | 功能是否由配置而非代码控制? | 按客户端启用/禁用 |
| 主题令牌 | 颜色/样式是否通过变量定义,而非硬编码? | 视觉自定义 |
| 内容注入 | 客户端能否提供种子数据、模板? | 客户端专属内容 |
| 钩子/事件系统 | 客户端能否扩展行为而不打补丁? | 自定义业务逻辑 |
严重程度:中。缺少扩展点会迫使客户端代码进入核心。
迁移编号冲突
# List all migration files with their numbers
ls migrations/ | sort | head -20
# Check if client migrations are in the reserved ranges
# Core: 0001-0099, Client domain: 0100-0199, Client custom: 0200+
严重程度:在造成冲突之前为低,造成冲突后为严重。
功能标志与客户端检查
// BAD — client name check
if (clientName === 'acme') {
showEmailOutbox = true;
}
// GOOD — feature flag in config
if (config.features.emailOutbox) {
showEmailOutbox = true;
}
查找行为根据客户端身份而非配置进行分支的模式。
第 4 步:生成报告
写入 .jez/artifacts/fork-discipline-audit.md:
# Fork Discipline Audit: [Project Name]
**Date**: YYYY-MM-DD
**Pattern**: [explicit multi-client / config-driven / monorepo / single-heading-multi]
**Clients**: [list of client deployments]
## Boundary Map
### Core (shared)
| Path | Purpose | Clean? |
|------|---------|--------|
| src/server/ | API routes | Yes / No — [issue] |
### Client (per-deployment)
| Client | Config | Content | Schema | Custom |
|--------|--------|---------|--------|--------|
| acme | config.ts | kb/ | 0100-0120 | custom/routes/ |
### Blurred (needs attention)
| Path | Problem | Suggested fix |
|------|---------|--------------|
| src/routes/acme-custom.ts | Client code in core | Move to clients/acme/custom/ |
## Violations
### High Severity
[List with file:line, description, fix]
### Medium Severity
[List with file:line, description, fix]
### Low Severity
[List]
## Extension Points
| Point | Present? | Notes |
|-------|----------|-------|
| Config merge | Yes/No | |
| Dynamic imports | Yes/No | |
| Feature flags | Yes/No | |
## Health Score
[1-10] — [explanation]
## Top 3 Recommendations
1. [Highest impact fix]
2. [Second priority]
3. [Third priority]
文档模式
在项目根目录生成 FORK.md,记录边界:
# Fork Discipline
## Architecture
This project serves multiple clients from a shared codebase.
### What's Core (don't modify per client)
[List of directories and their purpose]
### What's Client (varies per deployment)
[Client directory structure with explanation]
### How to Add a New Client
1. Copy `clients/_template/` to `clients/new-client/`
2. Edit `config.ts` with client overrides
3. Add seed data to `content/`
4. Create migrations numbered 0100+
5. Deploy with `CLIENT=new-client wrangler deploy`
### The Fork Test
Before modifying any file: is this core or client?
- Core → change in `src/`, all clients benefit
- Client → change in `clients/name/`, no other client affected
- Can't tell → the boundary needs fixing first
### Migration Numbering
| Range | Owner |
|-------|-------|
| 0001-0099 | Core platform |
| 0100-0199 | Client domain schema |
| 0200+ | Client custom features |
### Config Merge Pattern
Client configs are shallow-merged over defaults:
[Show the actual merge code from the project]
重构模式
审核后,生成落实边界的具体步骤:
1. 将客户端代码移出核心代码
对于客户端代码位于 src/ 中的每项违规:
# Create client directory if it doesn't exist
mkdir -p clients/acme/custom/routes
# Move the file
git mv src/routes/acme-custom.ts clients/acme/custom/routes/
# Update imports in core to use dynamic discovery
2. 用功能标志替代客户端检查
对于核心代码中的每个 if (client === ...):
// Before (in src/)
if (clientName === 'acme') {
app.route('/email-outbox', emailRoutes);
}
// After (in src/) — feature flag
if (config.features.emailOutbox) {
app.route('/email-outbox', emailRoutes);
}
// After (in clients/acme/config.ts) — client enables it
export default {
features: { emailOutbox: true }
}
3. 实现配置合并
如果项目采用替换配置而非合并:
// config/resolve.ts
import defaults from './defaults';
export function resolveConfig(clientConfig: Partial<Config>): Config {
return {
...defaults,
...clientConfig,
features: { ...defaults.features, ...clientConfig.features },
theme: { ...defaults.theme, ...clientConfig.theme },
};
}
4. 为自定义路由添加扩展点
如果客户端需要自定义路由,但目前会修改核心代码:
// src/server/index.ts — auto-discover client routes
const clientRoutes = await import(`../../clients/${clientName}/custom/routes`)
.catch(() => null);
if (clientRoutes?.default) {
app.route('/custom', clientRoutes.default);
}
5. 生成重构脚本
编写脚本并保存到 .jez/scripts/fork-refactor.sh,该脚本应:
- 创建客户端目录结构
- 移动已识别的文件
- 更新导入路径
- 生成 FORK.md
运行的合适时机
| 客户端数量 | 操作 |
|---|---|
| 1 | 不要重构。只需记录边界(FORK.md),以便了解边界所在。 |
| 2 | 运行审核。修复高严重性违规。开始采用配置合并模式。 |
| 3+ | 运行完整重构模式。边界必须清晰——你现在已经知道哪些内容会变化。 |
纪律规则 5:在第 3 个客户端之前不要抽象。只有 1 个客户端时,你是在猜测。有 2 个时,你是在寻找模式。有 3 个或更多时,你才知道哪些内容确实会变化。
提示
- 在添加新客户端之前运行,而不是之后
- 配置合并是投资回报率最高的单项重构——优先处理
- 即使只有 1 个客户端,功能标志也优于
if (client)。“大部分都一样,除了……” = 使用功能标志,而不是分叉。 - FORK.md 是写给团队看的,不只是给 Claude 看——要以供人阅读的方式来写