name: fsd-es-lite description: FSD + ES-lite アーキテクチャに基づく機能開発スキル。新しいfeature/entity/widgetを追加する時、CQRS + Event Sourcingパターンで実装する時、scaffoldスクリプトを使う時に使用。AI-nativeアプリケーション開発向け。
FSD + ES-lite 開発スキル
Feature-Sliced Design (FSD) と Event Sourcing Lite (ES-lite) を組み合わせた開発プロセス。 PoC・小規模開発向けに最適化された軽量アーキテクチャ。
設計原則
1. 関数ベース実装(クラス禁止)
クラスを使わず、関数とvelonaのDIで実装する。
理由:
- クラスはインスタンス内での状態管理が複雑になりがち
- 関数は純粋で、テストしやすく、合成しやすい
- velonaで依存注入すれば、テスト時のモック差し替えも容易
// ✅ 関数ベース + velona DI
import { depend } from "velona";
export const executeCommand = depend(
{ eventStore, projector, clock: () => new Date().toISOString(), idGen: () => crypto.randomUUID() },
async (deps, command: Command, correlationId: string) => {
// 実装
}
);
// テスト時
executeCommand.inject({ eventStore: mockStore })(command, correlationId);
// ❌ クラスベース(使わない)
class CommandHandler {
constructor(private eventStore: IEventStore) {}
async execute(command: Command) { /* ... */ }
}
2. ES-lite統一
全featureでEvent Sourcing Lite(軽量版ES)に統一。
特徴:
- 全状態変更をイベントとして記録
- イベントリプレイで状態再構築
- AIエージェント + ユーザー操作を同じイベント系列で追跡
- CRUDとの混在を避け、学習コストを削減
3. 厳格なFSD構造
src/
shared/ # 共有インフラ・ユーティリティ
entities/ # ドメインモデル・スキーマ・表示UI(オプション)
features/ # ユースケース・API・状態管理
widgets/ # 複合UIブロック
pages/ # ルートコンポーネント
app/ # アプリ初期化
4. decide/apply パターン(純粋関数)
// decide: 状態 + コマンド → イベント(純粋関数)
function decide(
state: Aggregate | null,
command: Command,
meta: EventMeta
): Result<Event[], DecisionError>
// apply: 状態 + イベント → 新状態(純粋関数)
function apply(
state: Aggregate | null,
event: Event
): Aggregate
Scaffoldスクリプト
新しい機能を追加する際は、scaffoldスクリプトを使用して最小限のボイラープレートを生成する。 生成後、ドメインに合わせてカスタマイズする。
Feature全体を生成
npx tsx .claude/skills/fsd-es-lite/scripts/scaffold-feature.ts <feature-name>
生成されるファイル(最小構成):
entities/<name>/model/schema.ts- Zodドメインスキーマentities/<name>/model/events.ts- イベント型定義features/<name>/model/decide.ts- 決定ロジックfeatures/<name>/model/apply.ts- 適用ロジックfeatures/<name>/model/commands.ts- コマンド実行features/<name>/model/queries.ts- クエリfeatures/<name>/model/projector.ts- Projection更新features/<name>/api/routes.ts- Hono APIルートfeatures/<name>/api/contracts.ts- RPC型定義features/<name>/api/schemas.ts- バリデーションスキーマ
UIやhooksは必要に応じて追加する(scaffoldでは生成しない)
Entityのみを生成
npx tsx .claude/skills/fsd-es-lite/scripts/scaffold-entity.ts <entity-name>
生成されるファイル:
entities/<name>/model/schema.ts- Zodドメインスキーマentities/<name>/model/events.ts- イベント型定義entities/<name>/model/index.ts- エクスポートentities/<name>/ui/<Name>.tsx- UIコンポーネント(実装必須)entities/<name>/ui/index.ts- UIエクスポート
生成後、TODOコメントを参照してドメインに合わせて実装する
Widgetを生成
npx tsx .claude/skills/fsd-es-lite/scripts/scaffold-widget.ts <widget-name>
生成されるファイル:
widgets/<name>/ui/<Name>.tsx- メインコンポーネント(実装必須)widgets/<name>/ui/index.ts- UIエクスポートwidgets/<name>/model/index.ts- モデルエクスポートwidgets/<name>/index.ts- パブリックAPI
生成後、TODOコメントを参照してentities/featuresを組み合わせて実装する
開発フロー
新機能追加時
-
scaffoldで雛形生成
npx tsx .claude/skills/fsd-es-lite/scripts/scaffold-feature.ts order -
ドメインスキーマ定義 (
entities/order/model/schema.ts)- Zodでドメイン型を定義
- ステータス enum、集約、投影を定義
- ドメインに必要なフィールドを追加
-
イベント定義 (
entities/order/model/events.ts)- ドメインイベントを定義(Created, Updated, etc.)
- イベントファクトリ関数を追加
-
decide/apply実装 (
features/order/model/)decide.ts: ビジネスルールを純粋関数で実装apply.ts: イベント適用を純粋関数で実装- in-source testingでテスト追加
-
commands.ts実装
- velonaでDI
- EventStore.load → decide → EventStore.append → Projector.save
-
APIルート定義 (
features/order/api/routes.ts)- Honoでエンドポイント定義
- zValidatorでバリデーション
-
必要に応じてUI/hooksを追加
features/order/hooks/- TanStack Query hooksfeatures/order/ui/- UIコンポーネントentities/order/ui/- 表示専用コンポーネント
参照ドキュメント
詳細な実装パターンは以下を参照:
- ARCHITECTURE.md - アーキテクチャ詳細
- PATTERNS.md - コードパターン集
重要な設計判断
Result型と例外の使い分け
| 種類 | エラー例 | 対応 |
|---|---|---|
| Result型 | バリデーションエラー、バージョン競合 | 復帰可能 |
| 例外 | DBアクセスエラー | 復帰不能 |
楽観的ロック
// コマンドにexpectedVersionを含める
type UpdateCommand = {
type: "Update";
aggregateId: string;
expectedVersion: number;
// ...
};
// appendでバージョンチェック
await eventStore.append(aggregateId, events, expectedVersion);
// → ConflictErrorならリトライを促す
日時型の統一
全ての日時はISO 8601文字列(string)で統一。
// ✅ ISO 8601文字列
createdAt: z.string().datetime()
// ❌ Dateオブジェクト(使わない)
createdAt: z.date()
Projector失敗時の方針
イベントは保存済みなので、投影失敗はログのみ。後からリプレイで再構築可能。
try {
await deps.projector.save(next);
} catch (error) {
console.warn("Projection save failed:", error);
}
AI操作追跡
CommandLogでAI操作を追跡:
interface CommandLog {
actor: { type: 'user' | 'ai' | 'system'; id?: string };
aiInfo?: {
modelId: string;
runId: string;
promptHash?: string;
};
// ...
}