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)
フォークテスト:ファイルを変更する前に、「これはコアか、クライアントか?」と確認してください。判断できない場合は、境界が十分に明確ではありません。
使用する場面
- 既存プロジェクトに2つ目または3つ目のクライアントを追加する前
- プロジェクトが場当たり的に成長し、境界が曖昧になった後
- 共有コードに
if (client === 'acme')のようなチェックが入り始めたとき - 何が共有で、何が固有なのかを把握するための大規模なリファクタリングの前
- アーキテクチャを理解する必要がある新しい開発者を迎えるとき
- マルチクライアントプロジェクトの定期的な健全性チェック
モード
| モード | 起動条件 | 生成されるもの |
|---|---|---|
| audit | 「フォーク規律」「境界を確認して」 | 境界マップ+違反レポート |
| document | 「FORK.mdを書いて」「境界を文書化して」 | プロジェクト用のFORK.mdファイル |
| refactor | 「フォークを整理して」「境界を徹底して」 | リファクタリング計画+移行スクリプト |
デフォルト:audit
監査モード
ステップ1:プロジェクトの種類を特定する
これがマルチクライアントプロジェクトかどうか、またどのパターンを使用しているかを判断します。
| シグナル | パターン |
|---|---|
clients/ または tenants/ ディレクトリ | 明示的なマルチクライアント |
| クライアント名を含む複数の設定ファイル | 設定駆動のマルチクライアント |
共有パッケージとクライアントごとのパッケージを含む packages/ | モノレポ型マルチクライアント |
CLIENT_NAME や TENANT_ID のような環境変数 | 実行時マルチクライアント |
| デプロイメントが1つのみで、クライアントディレクトリがない | シングルクライアント(マルチクライアント化する可能性あり) |
シングルクライアントの場合は、プロジェクトの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 だけのものではありません。人が読むことを意識して書いてください