mock実行をできるように修正

This commit is contained in:
nrslib 2026-01-27 11:50:07 +09:00
parent 665b5187be
commit ccc19e83ff
16 changed files with 1357 additions and 107 deletions

View File

@ -159,7 +159,68 @@ Prohibited patterns:
| Hidden Dependencies | Child components implicitly calling APIs etc. |
| Non-idiomatic | Custom implementation ignoring language/FW conventions |
### 6. Unnecessary Backward Compatibility Code Detection
### 6. Abstraction Level Evaluation
**Conditional Branch Proliferation Detection:**
| Pattern | Judgment |
|---------|----------|
| Same if-else pattern in 3+ places | Abstract with polymorphism → **REJECT** |
| switch/case with 5+ branches | Consider Strategy/Map pattern |
| Flag arguments changing behavior | Split into separate functions → **REJECT** |
| Type-based branching (instanceof/typeof) | Replace with polymorphism → **REJECT** |
| Nested conditionals (3+ levels) | Early return or extract → **REJECT** |
**Abstraction Level Mismatch Detection:**
| Pattern | Problem | Fix |
|---------|---------|-----|
| Low-level details in high-level processing | Hard to read | Extract details to functions |
| Mixed abstraction levels in one function | Cognitive load | Align to same granularity |
| DB operations mixed with business logic | Responsibility violation | Separate to Repository layer |
| Config values mixed with processing logic | Hard to change | Externalize configuration |
**Good Abstraction Examples:**
```typescript
// ❌ Proliferating conditionals
function process(type: string) {
if (type === 'A') { /* process A */ }
else if (type === 'B') { /* process B */ }
else if (type === 'C') { /* process C */ }
// ...continues
}
// ✅ Abstract with Map pattern
const processors: Record<string, () => void> = {
A: processA,
B: processB,
C: processC,
};
function process(type: string) {
processors[type]?.();
}
```
```typescript
// ❌ Mixed abstraction levels
function createUser(data: UserData) {
// High level: business logic
validateUser(data);
// Low level: DB operation details
const conn = await pool.getConnection();
await conn.query('INSERT INTO users...');
conn.release();
}
// ✅ Aligned abstraction levels
function createUser(data: UserData) {
validateUser(data);
await userRepository.save(data); // Details hidden
}
```
### 7. Unnecessary Backward Compatibility Code Detection
**AI tends to leave unnecessary code "for backward compatibility." Don't overlook this.**
@ -188,7 +249,7 @@ Code that should be kept:
**Be suspicious when AI says "for backward compatibility."** Verify if it's really needed.
### 7. Workaround Detection
### 8. Workaround Detection
**Don't overlook compromises made to "just make it work."**
@ -203,7 +264,7 @@ Code that should be kept:
**Always point these out.** Temporary fixes become permanent.
### 8. Quality Attributes
### 9. Quality Attributes
| Attribute | Review Point |
|-----------|--------------|
@ -211,7 +272,7 @@ Code that should be kept:
| Maintainability | Easy to modify and fix |
| Observability | Logging and monitoring enabled |
### 9. Big Picture
### 10. Big Picture
**Caution**: Don't get lost in minor "clean code" nitpicks.
@ -222,7 +283,7 @@ Verify:
- Does it align with business requirements
- Is naming consistent with the domain
### 10. Change Scope Assessment
### 11. Change Scope Assessment
**Check change scope and include in report (non-blocking).**
@ -241,7 +302,7 @@ Verify:
**Include as suggestions (non-blocking):**
- If splittable, present splitting proposal
### 11. Circular Review Detection
### 12. Circular Review Detection
When review count is provided (e.g., "Review count: 3rd"), adjust judgment accordingly.

View File

@ -105,7 +105,45 @@ Perform self-check after implementation.
| Boy Scout | Leave touched areas slightly improved |
| Fail Fast | Detect errors early. Don't swallow them |
**When in doubt**: Choose Simple. Abstraction can come later.
**When in doubt**: Choose Simple.
## Abstraction Principles
**Before adding conditional branches, consider:**
- Does this condition exist elsewhere? → Abstract with a pattern
- Will more branches be added? → Use Strategy/Map pattern
- Branching on type? → Replace with polymorphism
```typescript
// ❌ Adding more conditionals
if (type === 'A') { ... }
else if (type === 'B') { ... }
else if (type === 'C') { ... } // Yet another one
// ✅ Abstract with Map
const handlers = { A: handleA, B: handleB, C: handleC };
handlers[type]?.();
```
**Align abstraction levels:**
- Keep same granularity of operations within one function
- Extract detailed processing to separate functions
- Don't mix "what to do" with "how to do it"
```typescript
// ❌ Mixed abstraction levels
function processOrder(order) {
validateOrder(order); // High level
const conn = pool.getConnection(); // Low level detail
conn.query('INSERT...'); // Low level detail
}
// ✅ Aligned abstraction levels
function processOrder(order) {
validateOrder(order);
saveOrder(order); // Details hidden
}
```
**Follow language/framework conventions:**
- Be Pythonic in Python, Kotlin-like in Kotlin
@ -134,6 +172,121 @@ Perform self-check after implementation.
- Children don't modify state directly (notify parent via events)
- State flows in one direction
## Error Handling
**Principle: Centralize error handling. Don't scatter try-catch everywhere.**
```typescript
// ❌ Try-catch everywhere
async function createUser(data) {
try {
const user = await userService.create(data)
return user
} catch (e) {
console.error(e)
throw new Error('Failed to create user')
}
}
// ✅ Centralized handling at upper layer
// Catch at Controller/Handler layer
// Or use @ControllerAdvice / ErrorBoundary
async function createUser(data) {
return await userService.create(data) // Let exceptions propagate
}
```
**Error handling placement:**
| Layer | Responsibility |
|-------|----------------|
| Domain/Service layer | Throw exceptions on business rule violations |
| Controller/Handler layer | Catch exceptions and convert to response |
| Global handler | Handle common exceptions (NotFound, auth errors, etc.) |
## Transformation Placement
**Principle: Put conversion methods on DTOs.**
```typescript
// ✅ Request/Response DTOs have conversion methods
interface CreateUserRequest {
name: string
email: string
}
function toUseCaseInput(req: CreateUserRequest): CreateUserInput {
return { name: req.name, email: req.email }
}
// Controller
const input = toUseCaseInput(request)
const output = await useCase.execute(input)
return UserResponse.from(output)
```
**Conversion direction:**
```
Request → toInput() → UseCase/Service → Output → Response.from()
```
## Extraction Decisions
**Rule of Three:**
- 1st time: Write it inline
- 2nd time: Don't extract yet (wait and see)
- 3rd time: Consider extraction
**Should extract:**
- Same logic in 3+ places
- Same style/UI pattern
- Same validation logic
- Same formatting logic
**Should NOT extract:**
- Similar but slightly different (forced generalization adds complexity)
- Used in only 1-2 places
- Based on "might use later" predictions
```typescript
// ❌ Over-generalization
function formatValue(value, type, options) {
if (type === 'currency') { ... }
else if (type === 'date') { ... }
else if (type === 'percentage') { ... }
}
// ✅ Separate functions by purpose
function formatCurrency(amount: number): string { ... }
function formatDate(date: Date): string { ... }
function formatPercentage(value: number): string { ... }
```
## Writing Tests
**Principle: Structure tests with "Given-When-Then".**
```typescript
test('returns NotFound error when user does not exist', async () => {
// Given: non-existent user ID
const nonExistentId = 'non-existent-id'
// When: attempt to get user
const result = await getUser(nonExistentId)
// Then: NotFound error is returned
expect(result.error).toBe('NOT_FOUND')
})
```
**Test priority:**
| Priority | Target |
|----------|--------|
| High | Business logic, state transitions |
| Medium | Edge cases, error handling |
| Low | Simple CRUD, UI appearance |
## Prohibited
- **Fallback value overuse** - Don't hide problems with `?? 'unknown'`, `|| 'default'`
@ -143,4 +296,5 @@ Perform self-check after implementation.
- **Direct object/array mutation** - Create new with spread operator
- **console.log** - Don't leave in production code
- **Hardcoded secrets**
- **Scattered try-catch** - Centralize error handling at upper layer

View File

@ -631,7 +631,7 @@ steps:
pass_previous_response: true
transitions:
- condition: done
next_step: cqrs_es_review
next_step: ai_review
- condition: blocked
next_step: plan

View File

@ -159,7 +159,68 @@ Vertical Slice の判定基準:
| 隠れた依存 | 子コンポーネントが暗黙的にAPIを呼ぶ等 |
| 非イディオマティック | 言語・FWの作法を無視した独自実装 |
### 6. 不要な後方互換コードの検出
### 6. 抽象化レベルの評価
**条件分岐の肥大化検出:**
| パターン | 判定 |
|---------|------|
| 同じif-elseパターンが3箇所以上 | ポリモーフィズムで抽象化 → **REJECT** |
| switch/caseが5分岐以上 | Strategy/Mapパターンを検討 |
| フラグ引数で挙動を変える | 別関数に分割 → **REJECT** |
| 型による分岐instanceof/typeof | ポリモーフィズムに置換 → **REJECT** |
| ネストした条件分岐3段以上 | 早期リターンまたは抽出 → **REJECT** |
**抽象度の不一致検出:**
| パターン | 問題 | 修正案 |
|---------|------|--------|
| 高レベル処理の中に低レベル詳細 | 読みにくい | 詳細を関数に抽出 |
| 1関数内で抽象度が混在 | 認知負荷 | 同じ粒度に揃える |
| ビジネスロジックにDB操作が混在 | 責務違反 | Repository層に分離 |
| 設定値と処理ロジックが混在 | 変更困難 | 設定を外部化 |
**良い抽象化の例:**
```typescript
// ❌ 条件分岐の肥大化
function process(type: string) {
if (type === 'A') { /* 処理A */ }
else if (type === 'B') { /* 処理B */ }
else if (type === 'C') { /* 処理C */ }
// ...続く
}
// ✅ Mapパターンで抽象化
const processors: Record<string, () => void> = {
A: processA,
B: processB,
C: processC,
};
function process(type: string) {
processors[type]?.();
}
```
```typescript
// ❌ 抽象度の混在
function createUser(data: UserData) {
// 高レベル: ビジネスロジック
validateUser(data);
// 低レベル: DB操作の詳細
const conn = await pool.getConnection();
await conn.query('INSERT INTO users...');
conn.release();
}
// ✅ 抽象度を揃える
function createUser(data: UserData) {
validateUser(data);
await userRepository.save(data); // 詳細は隠蔽
}
```
### 7. 不要な後方互換コードの検出
**AIは「後方互換のために」不要なコードを残しがちである。これを見逃さない。**

View File

@ -105,7 +105,45 @@
| ボーイスカウト | 触った箇所は少し改善して去る |
| Fail Fast | エラーは早期に検出。握りつぶさない |
**迷ったら**: Simple を選ぶ。抽象化は後からでもできる。
**迷ったら**: Simple を選ぶ。
## 抽象化の原則
**条件分岐を追加する前に考える:**
- 同じ条件が他にもあるか → あればパターンで抽象化
- 今後も分岐が増えそうか → Strategy/Mapパターンを使う
- 型で分岐しているか → ポリモーフィズムで置換
```typescript
// ❌ 条件分岐を増やす
if (type === 'A') { ... }
else if (type === 'B') { ... }
else if (type === 'C') { ... } // また増えた
// ✅ Mapで抽象化
const handlers = { A: handleA, B: handleB, C: handleC };
handlers[type]?.();
```
**抽象度を揃える:**
- 1つの関数内では同じ粒度の処理を並べる
- 詳細な処理は別関数に切り出す
- 「何をするか」と「どうやるか」を混ぜない
```typescript
// ❌ 抽象度が混在
function processOrder(order) {
validateOrder(order); // 高レベル
const conn = pool.getConnection(); // 低レベル詳細
conn.query('INSERT...'); // 低レベル詳細
}
// ✅ 抽象度を揃える
function processOrder(order) {
validateOrder(order);
saveOrder(order); // 詳細は隠蔽
}
```
**言語・フレームワークの作法に従う:**
- Pythonなら Pythonic に、KotlinならKotlinらしく
@ -134,6 +172,121 @@
- 子は状態を直接変更しない(イベントを親に通知)
- 状態の流れは単方向
## エラーハンドリング
**原則: エラーは一元管理する。各所でtry-catchしない。**
```typescript
// ❌ 各所でtry-catch
async function createUser(data) {
try {
const user = await userService.create(data)
return user
} catch (e) {
console.error(e)
throw new Error('ユーザー作成に失敗しました')
}
}
// ✅ 上位層で一元処理
// Controller/Handler層でまとめてキャッチ
// または @ControllerAdvice / ErrorBoundary で処理
async function createUser(data) {
return await userService.create(data) // 例外はそのまま上に投げる
}
```
**エラー処理の配置:**
| 層 | 責務 |
|----|------|
| ドメイン/サービス層 | ビジネスルール違反時に例外をスロー |
| Controller/Handler層 | 例外をキャッチしてレスポンスに変換 |
| グローバルハンドラ | 共通例外NotFound, 認証エラー等)を処理 |
## 変換処理の配置
**原則: 変換メソッドはDTO側に持たせる。**
```typescript
// ✅ Request/Response DTOに変換メソッド
interface CreateUserRequest {
name: string
email: string
}
function toUseCaseInput(req: CreateUserRequest): CreateUserInput {
return { name: req.name, email: req.email }
}
// Controller
const input = toUseCaseInput(request)
const output = await useCase.execute(input)
return UserResponse.from(output)
```
**変換の方向:**
```
Request → toInput() → UseCase/Service → Output → Response.from()
```
## 共通化の判断
**3回ルール:**
- 1回目: そのまま書く
- 2回目: まだ共通化しない(様子見)
- 3回目: 共通化を検討
**共通化すべきもの:**
- 同じ処理が3箇所以上
- 同じスタイル/UIパターン
- 同じバリデーションロジック
- 同じフォーマット処理
**共通化すべきでないもの:**
- 似ているが微妙に違うもの(無理に汎用化すると複雑化)
- 1-2箇所しか使わないもの
- 「将来使うかも」という予測に基づくもの
```typescript
// ❌ 過度な汎用化
function formatValue(value, type, options) {
if (type === 'currency') { ... }
else if (type === 'date') { ... }
else if (type === 'percentage') { ... }
}
// ✅ 用途別に関数を分ける
function formatCurrency(amount: number): string { ... }
function formatDate(date: Date): string { ... }
function formatPercentage(value: number): string { ... }
```
## テストの書き方
**原則: テストは「Given-When-Then」で構造化する。**
```typescript
test('ユーザーが存在しない場合、NotFoundエラーを返す', async () => {
// Given: 存在しないユーザーID
const nonExistentId = 'non-existent-id'
// When: ユーザー取得を試みる
const result = await getUser(nonExistentId)
// Then: NotFoundエラーが返る
expect(result.error).toBe('NOT_FOUND')
})
```
**テストの優先度:**
| 優先度 | 対象 |
|--------|------|
| 高 | ビジネスロジック、状態遷移 |
| 中 | エッジケース、エラーハンドリング |
| 低 | 単純なCRUD、UIの見た目 |
## 禁止事項
- **フォールバック値の乱用** - `?? 'unknown'``|| 'default'` で問題を隠さない
@ -142,4 +295,5 @@
- **any型** - 型安全を破壊しない
- **オブジェクト/配列の直接変更** - スプレッド演算子で新規作成
- **console.log** - 本番コードに残さない
- **機密情報のハードコーディング**
- **機密情報のハードコーディング**
- **各所でのtry-catch** - エラーは上位層で一元処理

View File

@ -32,6 +32,15 @@
### 1. Aggregate設計
**原則: Aggregateは判断に必要なフィールドのみ保持する**
Command ModelAggregateの役割は「コマンドを受けて判断し、イベントを発行する」こと。
クエリ用データはRead ModelProjectionが担当する。
**「判断に必要」とは:**
- `if`/`require`の条件分岐に使う
- インスタンスメソッドでイベント発行時にフィールド値を参照する
**必須チェック:**
| 基準 | 判定 |
@ -40,12 +49,49 @@
| Aggregate間の直接参照ID参照でない | REJECT |
| Aggregateが100行を超える | 分割を検討 |
| ビジネス不変条件がAggregate外にある | REJECT |
| 判断に使わないフィールドを保持 | REJECT |
**良いAggregate:**
- 整合性境界が明確
- ID参照で他Aggregateを参照
- コマンドを受け取り、イベントを発行
- 不変条件を内部で保護
```kotlin
// ✅ 判断に必要なフィールドのみ
data class Order(
val orderId: String, // イベント発行時に使用
val status: OrderStatus // 状態チェックに使用
) {
fun confirm(confirmedBy: String): OrderConfirmedEvent {
require(status == OrderStatus.PENDING) { "確定できる状態ではありません" }
return OrderConfirmedEvent(
orderId = orderId,
confirmedBy = confirmedBy,
confirmedAt = LocalDateTime.now()
)
}
}
// ❌ 判断に使わないフィールドを保持
data class Order(
val orderId: String,
val customerId: String, // 判断に未使用
val shippingAddress: Address, // 判断に未使用
val status: OrderStatus
)
```
**追加操作がないAggregateはIDのみ:**
```kotlin
// ✅ 作成のみで追加操作がない場合
data class Notification(val notificationId: String) {
companion object {
fun create(customerId: String, message: String): NotificationCreatedEvent {
return NotificationCreatedEvent(
notificationId = UUID.randomUUID().toString(),
customerId = customerId,
message = message
)
}
}
}
```
### 2. イベント設計
@ -60,7 +106,7 @@
| CRUDスタイルのイベントUpdated, Deleted | 要検討 |
**良いイベント:**
```
```kotlin
// Good: ドメインの意図が明確
OrderPlaced, PaymentReceived, ItemShipped
@ -108,7 +154,57 @@ OrderUpdated, OrderDeleted
- イベントから冪等に再構築可能
- Writeモデルから完全に独立
### 5. 結果整合性
### 5. Query側の設計
**原則: ControllerはQueryGatewayを使う。Repositoryを直接使わない。**
**レイヤー間の型:**
- `application/query/` - Query結果の型例: `OrderDetail`
- `adapter/protocol/` - RESTレスポンスの型例: `OrderDetailResponse`
- QueryHandlerはapplication層の型を返し、Controllerがadapter層の型に変換
```kotlin
// application/query/OrderDetail.kt
data class OrderDetail(
val orderId: String,
val customerName: String,
val totalAmount: Money
)
// adapter/protocol/OrderDetailResponse.kt
data class OrderDetailResponse(...) {
companion object {
fun from(detail: OrderDetail) = OrderDetailResponse(...)
}
}
// QueryHandler - application層の型を返す
@QueryHandler
fun handle(query: GetOrderDetailQuery): OrderDetail? {
val entity = repository.findById(query.id) ?: return null
return OrderDetail(...)
}
// Controller - adapter層の型に変換
@GetMapping("/{id}")
fun getById(@PathVariable id: String): ResponseEntity<OrderDetailResponse> {
val detail = queryGateway.query(
GetOrderDetailQuery(id),
OrderDetail::class.java
).join() ?: throw NotFoundException("...")
return ResponseEntity.ok(OrderDetailResponse.from(detail))
}
```
**構成:**
```
Controller (adapter) → QueryGateway → QueryHandler (application) → Repository
↓ ↓
Response.from(detail) OrderDetail
```
### 6. 結果整合性
**必須チェック:**
@ -118,7 +214,176 @@ OrderUpdated, OrderDeleted
| 整合性遅延が許容範囲を超える | アーキテクチャ再検討 |
| 補償トランザクションが未定義 | 障害シナリオの検討を要求 |
### 6. アンチパターン検出
### 7. Saga vs EventHandler
**原則: Sagaは「競合が発生する複数アグリゲート間の操作」にのみ使用する**
**Sagaが必要なケース:**
```
複数のアクターが同じリソースを取り合う場合
例: 在庫確保10人が同時に同じ商品を注文
OrderPlacedEvent
↓ InventoryReservationSaga
ReserveInventoryCommand → Inventory集約同時実行を直列化
InventoryReservedEvent → ConfirmOrderCommand
InventoryReservationFailedEvent → CancelOrderCommand
```
**Sagaが不要なケース:**
```
競合が発生しない操作
例: 注文キャンセル時の在庫解放
OrderCancelledEvent
↓ InventoryReleaseHandler単純なEventHandler
ReleaseInventoryCommand
InventoryReleasedEvent
```
**判断基準:**
| 状況 | Saga | EventHandler |
|------|------|--------------|
| リソースの取り合いがある | ✅ | - |
| 補償トランザクションが必要 | ✅ | - |
| 競合しない単純な連携 | - | ✅ |
| 失敗時は再試行で十分 | - | ✅ |
**アンチパターン:**
```kotlin
// ❌ ライフサイクル管理のためにSagaを使う
@Saga
class OrderLifecycleSaga {
// 注文の全状態遷移をSagaで追跡
// PLACED → CONFIRMED → SHIPPED → DELIVERED
}
// ✅ 結果整合性が必要な操作だけをSagaで処理
@Saga
class InventoryReservationSaga {
// 在庫確保の同時実行制御のみ
}
```
**Sagaはライフサイクル管理ツールではない。** 結果整合性が必要な「操作」単位で作成する。
### 8. 例外 vs イベント(失敗時の選択)
**原則: 監査不要な失敗は例外、監査が必要な失敗はイベント**
**例外アプローチ(推奨:ほとんどのケース):**
```kotlin
// ドメインモデル: バリデーション失敗時に例外をスロー
fun reserveInventory(orderId: String, quantity: Int): InventoryReservedEvent {
if (availableQuantity < quantity) {
throw InsufficientInventoryException("在庫が不足しています")
}
return InventoryReservedEvent(productId, orderId, quantity)
}
// Saga: exceptionally でキャッチして補償アクション
commandGateway.send<Any>(command)
.exceptionally { ex ->
commandGateway.send<Any>(CancelOrderCommand(
orderId = orderId,
reason = ex.cause?.message ?: "在庫確保に失敗しました"
))
null
}
```
**イベントアプローチ(稀なケース):**
```kotlin
// 監査が必要な場合のみ
data class PaymentFailedEvent(
val paymentId: String,
val reason: String,
val attemptedAmount: Money
) : PaymentEvent
```
**判断基準:**
| 質問 | 例外 | イベント |
|------|------|----------|
| この失敗を後で確認する必要があるか? | No | Yes |
| 規制やコンプライアンスで記録が必要か? | No | Yes |
| Sagaだけが失敗を気にするか? | Yes | No |
| Event Storeに残すと価値があるか? | No | Yes |
**デフォルトは例外アプローチ。** 監査要件がある場合のみイベントを検討する。
### 9. 抽象化レベルの評価
**条件分岐の肥大化検出:**
| パターン | 判定 |
|---------|------|
| 同じif-elseパターンが3箇所以上 | ポリモーフィズムで抽象化 → **REJECT** |
| switch/caseが5分岐以上 | Strategy/Mapパターンを検討 |
| イベント種別による分岐が増殖 | イベントハンドラを分離 → **REJECT** |
| Aggregate内の状態分岐が複雑 | State Patternを検討 |
**抽象度の不一致検出:**
| パターン | 問題 | 修正案 |
|---------|------|--------|
| CommandHandlerにDB操作詳細 | 責務違反 | Repository層に分離 |
| EventHandlerにビジネスロジック | 責務違反 | ドメインサービスに抽出 |
| Aggregateに永続化処理 | レイヤー違反 | EventStore経由に変更 |
| Projectionに計算ロジック | 保守困難 | 専用サービスに抽出 |
**良い抽象化の例:**
```kotlin
// ❌ イベント種別による分岐の増殖
@EventHandler
fun on(event: DomainEvent) {
when (event) {
is OrderPlacedEvent -> handleOrderPlaced(event)
is OrderConfirmedEvent -> handleOrderConfirmed(event)
is OrderShippedEvent -> handleOrderShipped(event)
// ...どんどん増える
}
}
// ✅ イベントごとにハンドラを分離
@EventHandler
fun on(event: OrderPlacedEvent) { ... }
@EventHandler
fun on(event: OrderConfirmedEvent) { ... }
@EventHandler
fun on(event: OrderShippedEvent) { ... }
```
```kotlin
// ❌ 状態による分岐が複雑
fun process(command: ProcessCommand) {
when (status) {
PENDING -> if (command.type == "approve") { ... } else if (command.type == "reject") { ... }
APPROVED -> if (command.type == "ship") { ... }
// ...複雑化
}
}
// ✅ State Patternで抽象化
sealed class OrderState {
abstract fun handle(command: ProcessCommand): List<DomainEvent>
}
class PendingState : OrderState() {
override fun handle(command: ProcessCommand) = when (command) {
is ApproveCommand -> listOf(OrderApprovedEvent(...))
is RejectCommand -> listOf(OrderRejectedEvent(...))
else -> throw InvalidCommandException()
}
}
```
### 10. アンチパターン検出
以下を見つけたら **REJECT**:
@ -131,7 +396,59 @@ OrderUpdated, OrderDeleted
| Missing Events | 重要なドメインイベントが欠落 |
| God Aggregate | 1つのAggregateに全責務が集中 |
### 7. インフラ層
### 11. テスト戦略
**原則: レイヤーごとにテスト方針を分ける**
**テストピラミッド:**
```
┌─────────────┐
│ E2E Test │ ← 少数:全体フロー確認
├─────────────┤
│ Integration │ ← Command→Event→Projection→Query の連携確認
├─────────────┤
│ Unit Test │ ← 多数:各レイヤー独立テスト
└─────────────┘
```
**Command側Aggregate:**
```kotlin
// AggregateTestFixture使用
@Test
fun `確定コマンドでイベントが発行される`() {
fixture
.given(OrderPlacedEvent(...))
.`when`(ConfirmOrderCommand(orderId, confirmedBy))
.expectSuccessfulHandlerExecution()
.expectEvents(OrderConfirmedEvent(...))
}
```
**Query側:**
```kotlin
// Read Model直接セットアップ + QueryGateway
@Test
fun `注文詳細が取得できる`() {
// Given: Read Modelを直接セットアップ
orderRepository.save(OrderEntity(...))
// When: QueryGateway経由でクエリ実行
val detail = queryGateway.query(GetOrderDetailQuery(orderId), ...).join()
// Then
assertEquals(expectedDetail, detail)
}
```
**チェック項目:**
| 観点 | 判定 |
|------|------|
| Aggregateテストが状態ではなくイベントを検証している | 必須 |
| Query側テストがCommand経由でデータを作っていない | 推奨 |
| 統合テストでAxonの非同期処理を考慮している | 必須 |
### 12. インフラ層
**確認事項:**
- イベントストアの選択は適切か
@ -147,6 +464,7 @@ OrderUpdated, OrderDeleted
| Aggregate設計に問題 | REJECT |
| イベント設計が不適切 | REJECT |
| 結果整合性の考慮不足 | REJECT |
| 抽象化レベルの不一致 | REJECT |
| 軽微な改善点のみ | APPROVE改善提案は付記 |
## 口調の特徴
@ -162,3 +480,5 @@ OrderUpdated, OrderDeleted
- **イベントの質にこだわる**: イベントはドメインの歴史書である
- **結果整合性を恐れない**: 正しく設計されたESは強整合性より堅牢
- **過度な複雑さを警戒**: シンプルなCRUDで十分なケースにCQRS+ESを強制しない
- **Aggregateは軽く保つ**: 判断に不要なフィールドは持たない
- **Sagaを乱用しない**: 競合制御が必要な操作にのみ使用する

View File

@ -36,6 +36,15 @@
### 1. コンポーネント設計
**原則: 1ファイルにベタ書きしない。必ずコンポーネント分割する。**
**分離が必須なケース:**
- 独自のstateを持つ → 必ず分離
- 50行超のJSX → 分離
- 再利用可能 → 分離
- 責務が複数 → 分離
- ページ内の独立したセクション → 分離
**必須チェック:**
| 基準 | 判定 |
@ -60,8 +69,44 @@
| Layout | 配置・構造 | `PageLayout`, `Grid` |
| Utility | 共通機能 | `ErrorBoundary`, `Portal` |
**ディレクトリ構成:**
```
features/{feature-name}/
├── components/
│ ├── {feature}-view.tsx # メインビュー(子を組み合わせる)
│ ├── {sub-component}.tsx # サブコンポーネント
│ └── index.ts
├── hooks/
├── types.ts
└── index.ts
```
### 2. 状態管理
**原則: 子コンポーネントは自身で状態を変更しない。イベントを親にバブリングし、親が状態を操作する。**
```tsx
// ❌ 子が自分で状態を変更
const ChildBad = ({ initialValue }: { initialValue: string }) => {
const [value, setValue] = useState(initialValue)
return <input value={value} onChange={e => setValue(e.target.value)} />
}
// ✅ 親が状態を管理、子はコールバックで通知
const ChildGood = ({ value, onChange }: { value: string; onChange: (v: string) => void }) => {
return <input value={value} onChange={e => onChange(e.target.value)} />
}
const Parent = () => {
const [value, setValue] = useState('')
return <ChildGood value={value} onChange={setValue} />
}
```
**例外子がローカルstate持ってOK:**
- UI専用の一時状態ホバー、フォーカス、アニメーション
- 親に伝える必要がない完全にローカルな状態
**必須チェック:**
| 基準 | 判定 |
@ -81,7 +126,252 @@
| 複数コンポーネントで共有 | Context or 状態管理ライブラリ |
| サーバーデータのキャッシュ | TanStack Query等のデータフェッチライブラリ |
### 3. パフォーマンス
### 3. データ取得
**原則: API呼び出しはルートViewコンポーネントで行い、子コンポーネントにはpropsで渡す。**
```tsx
// ✅ CORRECT - ルートでデータ取得、子に渡す
const OrderDetailView = () => {
const { data: order, isLoading, error } = useGetOrder(orderId)
const { data: items } = useListOrderItems(orderId)
if (isLoading) return <Skeleton />
if (error) return <ErrorDisplay error={error} />
return (
<OrderSummary
order={order}
items={items}
onItemSelect={handleItemSelect}
/>
)
}
// ❌ WRONG - 子コンポーネントが自分でデータ取得
const OrderSummary = ({ orderId }) => {
const { data: order } = useGetOrder(orderId)
// ...
}
```
**理由:**
- データフローが明示的で追跡しやすい
- 子コンポーネントは純粋なプレゼンテーション(テストしやすい)
- 子コンポーネントに隠れた依存関係がなくなる
**UIの状態変更でパラメータが変わる場合週切り替え、フィルタ等:**
状態もViewレベルで管理し、コンポーネントにはコールバックを渡す。
```tsx
// ✅ CORRECT - 状態もViewで管理
const ScheduleView = () => {
const [currentWeek, setCurrentWeek] = useState(startOfWeek(new Date()))
const { data } = useListSchedules({
from: format(currentWeek, 'yyyy-MM-dd'),
to: format(endOfWeek(currentWeek), 'yyyy-MM-dd'),
})
return (
<WeeklyCalendar
schedules={data?.items ?? []}
currentWeek={currentWeek}
onWeekChange={setCurrentWeek}
/>
)
}
// ❌ WRONG - コンポーネント内で状態管理+データ取得
const WeeklyCalendar = ({ facilityId }) => {
const [currentWeek, setCurrentWeek] = useState(...)
const { data } = useListSchedules({ facilityId, from, to })
// ...
}
```
**例外(コンポーネント内フェッチが許容されるケース):**
| ケース | 理由 |
|--------|------|
| 無限スクロール | スクロール位置というUI内部状態に依存 |
| 検索オートコンプリート | 入力値に依存したリアルタイム検索 |
| 独立したウィジェット | 通知バッジ、天気等。親のデータと完全に無関係 |
| リアルタイム更新 | WebSocket/Pollingでの自動更新 |
| モーダル内の詳細取得 | 開いたときだけ追加データを取得 |
**判断基準: 「親が管理する意味がない / 親に影響を与えない」ケースのみ許容。**
**必須チェック:**
| 基準 | 判定 |
|------|------|
| コンポーネント内で直接fetch | Container層に分離 |
| エラーハンドリングなし | REJECT |
| ローディング状態の未処理 | REJECT |
| キャンセル処理なし | 警告 |
| N+1クエリ的なフェッチ | REJECT |
### 4. 共有コンポーネントと抽象化
**原則: 同じパターンのUIは共有コンポーネント化する。インラインスタイルのコピペは禁止。**
```tsx
// ❌ WRONG - インラインスタイルのコピペ
<button className="p-2 text-[var(--text-secondary)] hover:...">
<X className="w-5 h-5" />
</button>
// ✅ CORRECT - 共有コンポーネント使用
<IconButton onClick={onClose} aria-label="閉じる">
<X className="w-5 h-5" />
</IconButton>
```
**共有コンポーネント化すべきパターン:**
- アイコンボタン(閉じる、編集、削除等)
- ローディング/エラー表示
- ステータスバッジ
- タブ切り替え
- ラベル+値の表示(詳細画面)
- 検索入力
- カラー凡例
**過度な汎用化を避ける:**
```tsx
// ❌ WRONG - IconButtonに無理やりステッパー用バリアントを追加
export const iconButtonVariants = cva('...', {
variants: {
variant: {
default: '...',
outlined: '...', // ← ステッパー専用、他で使わない
},
size: {
medium: 'p-2',
stepper: 'w-8 h-8', // ← outlinedとセットでしか使わない
},
},
})
// ✅ CORRECT - 用途別に専用コンポーネント
export function StepperButton(props) {
return (
<button className="w-8 h-8 rounded-full border ..." {...props}>
<Plus className="w-4 h-4" />
</button>
)
}
```
**別コンポーネントにすべきサイン:**
- 「このvariantはこのsizeとセット」のような暗黙の制約がある
- 追加したvariantが元のコンポーネントの用途と明らかに違う
- 使う側のprops指定が複雑になる
### 5. 抽象化レベルの評価
**条件分岐の肥大化検出:**
| パターン | 判定 |
|---------|------|
| 同じ条件分岐が3箇所以上 | 共通コンポーネントに抽出 → **REJECT** |
| propsによる分岐が5種類以上 | コンポーネント分割を検討 |
| render内の三項演算子のネスト | 早期リターンまたはコンポーネント分離 → **REJECT** |
| 型による分岐レンダリング | ポリモーフィックコンポーネントを検討 |
**抽象度の不一致検出:**
| パターン | 問題 | 修正案 |
|---------|------|--------|
| データ取得ロジックがJSXに混在 | 読みにくい | カスタムフックに抽出 |
| ビジネスロジックがコンポーネントに混在 | 責務違反 | hooks/utilsに分離 |
| スタイル計算ロジックが散在 | 保守困難 | ユーティリティ関数に抽出 |
| 同じ変換処理が複数箇所に | DRY違反 | 共通関数に抽出 |
**良い抽象化の例:**
```tsx
// ❌ 条件分岐が肥大化
function UserBadge({ user }) {
if (user.role === 'admin') {
return <span className="bg-red-500">管理者</span>
} else if (user.role === 'moderator') {
return <span className="bg-yellow-500">モデレーター</span>
} else if (user.role === 'premium') {
return <span className="bg-purple-500">プレミアム</span>
} else {
return <span className="bg-gray-500">一般</span>
}
}
// ✅ Mapで抽象化
const ROLE_CONFIG = {
admin: { label: '管理者', className: 'bg-red-500' },
moderator: { label: 'モデレーター', className: 'bg-yellow-500' },
premium: { label: 'プレミアム', className: 'bg-purple-500' },
default: { label: '一般', className: 'bg-gray-500' },
}
function UserBadge({ user }) {
const config = ROLE_CONFIG[user.role] ?? ROLE_CONFIG.default
return <span className={config.className}>{config.label}</span>
}
```
```tsx
// ❌ 抽象度が混在
function OrderList() {
const [orders, setOrders] = useState([])
useEffect(() => {
fetch('/api/orders')
.then(res => res.json())
.then(data => setOrders(data))
}, [])
return orders.map(order => (
<div>{order.total.toLocaleString()}円</div>
))
}
// ✅ 抽象度を揃える
function OrderList() {
const { data: orders } = useOrders() // データ取得を隠蔽
return orders.map(order => (
<OrderItem key={order.id} order={order} />
))
}
```
### 6. データと表示形式の責務分離
**原則: バックエンドは「データ」を返し、フロントエンドが「表示形式」に変換する。**
```tsx
// ✅ フロントエンド: 表示形式に変換
export function formatPrice(amount: number): string {
return `¥${amount.toLocaleString()}`
}
export function formatDate(date: Date): string {
return format(date, 'yyyy年M月d日')
}
```
**理由:**
- 表示形式はUI要件であり、バックエンドの責務ではない
- 国際化対応が容易
- フロントエンドが柔軟に表示を変更できる
**必須チェック:**
| 基準 | 判定 |
|------|------|
| バックエンドが表示用文字列を返している | 設計見直しを提案 |
| 同じフォーマット処理が複数箇所にコピペ | ユーティリティ関数に統一 |
| コンポーネント内でインラインフォーマット | 関数に抽出 |
### 7. パフォーマンス
**必須チェック:**
@ -102,40 +392,15 @@
**アンチパターン:**
```tsx
// Bad: レンダリングごとに新しいオブジェクト
// レンダリングごとに新しいオブジェクト
<Child style={{ color: 'red' }} />
// Good: 定数化 or useMemo
// 定数化 or useMemo
const style = useMemo(() => ({ color: 'red' }), []);
<Child style={style} />
```
### 4. データフェッチ
**必須チェック:**
| 基準 | 判定 |
|------|------|
| コンポーネント内で直接fetch | Container層に分離 |
| エラーハンドリングなし | REJECT |
| ローディング状態の未処理 | REJECT |
| キャンセル処理なし | 警告 |
| N+1クエリ的なフェッチ | REJECT |
**推奨パターン:**
```tsx
// Good: データフェッチはルートで
function UserPage() {
const { data, isLoading, error } = useQuery(['user', id], fetchUser);
if (isLoading) return <Skeleton />;
if (error) return <ErrorDisplay error={error} />;
return <UserProfile user={data} />;
}
```
### 5. アクセシビリティ
### 8. アクセシビリティ
**必須チェック:**
@ -154,7 +419,7 @@ function UserPage() {
- [ ] スクリーンリーダーで意味が通じるか
- [ ] カラーコントラストは十分か
### 6. TypeScript/型安全性
### 9. TypeScript/型安全性
**必須チェック:**
@ -165,7 +430,7 @@ function UserPage() {
| Props型定義なし | REJECT |
| イベントハンドラの型が不適切 | 修正が必要 |
### 7. フロントエンドセキュリティ
### 10. フロントエンドセキュリティ
**必須チェック:**
@ -176,7 +441,7 @@ function UserPage() {
| 機密情報のフロントエンド保存 | REJECT |
| CSRFトークンの未使用 | 要確認 |
### 8. テスタビリティ
### 11. テスタビリティ
**必須チェック:**
@ -186,7 +451,7 @@ function UserPage() {
| テスト困難な構造 | 分離を検討 |
| ビジネスロジックのUIへの埋め込み | REJECT |
### 9. アンチパターン検出
### 12. アンチパターン検出
以下を見つけたら **REJECT**:
@ -198,6 +463,8 @@ function UserPage() {
| useEffect地獄 | 依存関係が複雑すぎる |
| Premature Optimization | 不要なメモ化 |
| Magic Strings | ハードコードされた文字列 |
| Hidden Dependencies | 子コンポーネントの隠れたAPI呼び出し |
| Over-generalization | 無理やり汎用化したコンポーネント |
## 判定基準
@ -206,6 +473,7 @@ function UserPage() {
| コンポーネント設計に問題 | REJECT |
| 状態管理に問題 | REJECT |
| アクセシビリティ違反 | REJECT |
| 抽象化レベルの不一致 | REJECT |
| パフォーマンス問題 | REJECT重大な場合 |
| 軽微な改善点のみ | APPROVE改善提案は付記 |
@ -223,3 +491,5 @@ function UserPage() {
- **アクセシビリティは後付け困難**: 最初から組み込む
- **過度な抽象化を警戒**: シンプルに保つ
- **フレームワークの作法に従う**: 独自パターンより標準的なアプローチ
- **データ取得はルートで**: 子コンポーネントに隠れた依存を作らない
- **制御されたコンポーネント**: 状態の流れは単方向

View File

@ -631,7 +631,7 @@ steps:
pass_previous_response: true
transitions:
- condition: done
next_step: cqrs_es_review
next_step: ai_review
- condition: blocked
next_step: plan

View File

@ -6,17 +6,15 @@ import { execSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { basename, dirname } from 'node:path';
import {
callClaude,
callClaudeCustom,
callClaudeAgent,
callClaudeSkill,
ClaudeCallOptions,
type ClaudeCallOptions,
} from '../claude/client.js';
import { type StreamCallback, type PermissionHandler, type AskUserQuestionHandler } from '../claude/process.js';
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js';
import { loadCustomAgents, loadAgentPrompt } from '../config/loader.js';
import { loadGlobalConfig } from '../config/globalConfig.js';
import { loadProjectConfig } from '../config/projectConfig.js';
import { getProvider, type ProviderType, type ProviderCallOptions } from '../providers/index.js';
import type { AgentResponse, CustomAgentConfig } from '../models/types.js';
export type { StreamCallback };
@ -26,7 +24,7 @@ export interface RunAgentOptions {
cwd: string;
sessionId?: string;
model?: string;
provider?: 'claude' | 'codex';
provider?: 'claude' | 'codex' | 'mock';
/** Resolved path to agent prompt file */
agentPath?: string;
/** Allowed tools for this agent run */
@ -40,9 +38,8 @@ export interface RunAgentOptions {
bypassPermissions?: boolean;
}
type AgentProvider = 'claude' | 'codex';
function resolveProvider(cwd: string, options?: RunAgentOptions, agentConfig?: CustomAgentConfig): AgentProvider {
function resolveProvider(cwd: string, options?: RunAgentOptions, agentConfig?: CustomAgentConfig): ProviderType {
// Mock provider must be explicitly specified (no fallback)
if (options?.provider) return options.provider;
if (agentConfig?.provider) return agentConfig.provider;
const projectConfig = loadProjectConfig(cwd);
@ -137,33 +134,22 @@ export async function runCustomAgent(
systemPrompt = `${systemPrompt}\n\n${options.statusRulesPrompt}`;
}
const tools = allowedTools;
const provider = resolveProvider(options.cwd, options, agentConfig);
const model = resolveModel(options.cwd, options, agentConfig);
if (provider === 'codex') {
const callOptions: CodexCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
model,
statusPatterns: agentConfig.statusPatterns,
onStream: options.onStream,
};
return callCodexCustom(agentConfig.name, task, systemPrompt, callOptions);
}
const providerType = resolveProvider(options.cwd, options, agentConfig);
const provider = getProvider(providerType);
const callOptions: ClaudeCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
allowedTools: tools,
model,
statusPatterns: agentConfig.statusPatterns,
const callOptions: ProviderCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
allowedTools,
model: resolveModel(options.cwd, options, agentConfig),
statusPatterns: agentConfig.statusPatterns,
onStream: options.onStream,
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
};
return callClaudeCustom(agentConfig.name, task, systemPrompt, callOptions);
return provider.callCustom(agentConfig.name, task, systemPrompt, callOptions);
}
/**
@ -221,26 +207,14 @@ export async function runAgent(
systemPrompt = `${systemPrompt}\n\n${options.statusRulesPrompt}`;
}
const tools = options.allowedTools;
const provider = resolveProvider(options.cwd, options);
const model = resolveModel(options.cwd, options);
const providerType = resolveProvider(options.cwd, options);
const provider = getProvider(providerType);
if (provider === 'codex') {
const callOptions: CodexCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
model,
systemPrompt,
onStream: options.onStream,
};
return callCodex(agentName, task, callOptions);
}
const callOptions: ClaudeCallOptions = {
const callOptions: ProviderCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
allowedTools: tools,
model,
allowedTools: options.allowedTools,
model: resolveModel(options.cwd, options),
systemPrompt,
onStream: options.onStream,
onPermissionRequest: options.onPermissionRequest,
@ -248,7 +222,7 @@ export async function runAgent(
bypassPermissions: options.bypassPermissions,
};
return callClaude(agentName, task, callOptions);
return provider.call(agentName, task, callOptions);
}
// Fallback: Look for custom agent by name

85
src/mock/client.ts Normal file
View File

@ -0,0 +1,85 @@
/**
* Mock agent client for testing
*
* Returns immediate fixed responses without any API calls.
* Useful for testing workflows without incurring costs or latency.
*/
import { randomUUID } from 'node:crypto';
import type { StreamCallback, StreamEvent } from '../claude/process.js';
import type { AgentResponse } from '../models/types.js';
/** Options for mock calls */
export interface MockCallOptions {
cwd: string;
sessionId?: string;
onStream?: StreamCallback;
/** Fixed response content (optional, defaults to generic mock response) */
mockResponse?: string;
/** Fixed status to return (optional, defaults to 'done') */
mockStatus?: 'done' | 'blocked' | 'approved' | 'rejected' | 'improve';
}
/**
* Generate a mock session ID
*/
function generateMockSessionId(): string {
return `mock-session-${randomUUID()}`;
}
/**
* Call mock agent - returns immediate fixed response
*/
export async function callMock(
agentName: string,
prompt: string,
options: MockCallOptions
): Promise<AgentResponse> {
const sessionId = options.sessionId ?? generateMockSessionId();
const status = options.mockStatus ?? 'done';
const statusMarker = `[MOCK:${status.toUpperCase()}]`;
const content = options.mockResponse ??
`${statusMarker}\n\nMock response for agent "${agentName}".\nPrompt: ${prompt.slice(0, 100)}${prompt.length > 100 ? '...' : ''}`;
// Emit stream events if callback is provided
if (options.onStream) {
const initEvent: StreamEvent = {
type: 'init',
data: { model: 'mock-model', sessionId },
};
options.onStream(initEvent);
const textEvent: StreamEvent = {
type: 'text',
data: { text: content },
};
options.onStream(textEvent);
const resultEvent: StreamEvent = {
type: 'result',
data: { success: true, result: content, sessionId },
};
options.onStream(resultEvent);
}
return {
agent: agentName,
status,
content,
timestamp: new Date(),
sessionId,
};
}
/**
* Call mock agent with custom system prompt (same as callMock for mock provider)
*/
export async function callMockCustom(
agentName: string,
prompt: string,
_systemPrompt: string,
options: MockCallOptions
): Promise<AgentResponse> {
// For mock, system prompt is ignored - just return fixed response
return callMock(agentName, prompt, options);
}

View File

@ -60,7 +60,7 @@ export const WorkflowStepRawSchema = z.object({
/** Display name for the agent (shown in output). Falls back to agent basename if not specified */
agent_name: z.string().optional(),
allowed_tools: z.array(z.string()).optional(),
provider: z.enum(['claude', 'codex']).optional(),
provider: z.enum(['claude', 'codex', 'mock']).optional(),
model: z.string().optional(),
instruction: z.string().optional(),
instruction_template: z.string().optional(),
@ -94,7 +94,7 @@ export const CustomAgentConfigSchema = z.object({
status_patterns: z.record(z.string(), z.string()).optional(),
claude_agent: z.string().optional(),
claude_skill: z.string().optional(),
provider: z.enum(['claude', 'codex']).optional(),
provider: z.enum(['claude', 'codex', 'mock']).optional(),
model: z.string().optional(),
}).refine(
(data) => data.prompt_file || data.prompt || data.claude_agent || data.claude_skill,
@ -116,7 +116,7 @@ export const GlobalConfigSchema = z.object({
trusted_directories: z.array(z.string()).optional().default([]),
default_workflow: z.string().optional().default('default'),
log_level: z.enum(['debug', 'info', 'warn', 'error']).optional().default('info'),
provider: z.enum(['claude', 'codex']).optional().default('claude'),
provider: z.enum(['claude', 'codex', 'mock']).optional().default('claude'),
model: z.string().optional(),
debug: DebugConfigSchema.optional(),
});
@ -125,7 +125,7 @@ export const GlobalConfigSchema = z.object({
export const ProjectConfigSchema = z.object({
workflow: z.string().optional(),
agents: z.array(CustomAgentConfigSchema).optional(),
provider: z.enum(['claude', 'codex']).optional(),
provider: z.enum(['claude', 'codex', 'mock']).optional(),
});
/**

View File

@ -65,7 +65,7 @@ export interface WorkflowStep {
/** Resolved absolute path to agent prompt file (set by loader) */
agentPath?: string;
/** Provider override for this step */
provider?: 'claude' | 'codex';
provider?: 'claude' | 'codex' | 'mock';
/** Model override for this step */
model?: string;
instructionTemplate: string;
@ -129,7 +129,7 @@ export interface CustomAgentConfig {
statusPatterns?: Record<string, string>;
claudeAgent?: string;
claudeSkill?: string;
provider?: 'claude' | 'codex';
provider?: 'claude' | 'codex' | 'mock';
model?: string;
}
@ -148,7 +148,7 @@ export interface GlobalConfig {
trustedDirectories: string[];
defaultWorkflow: string;
logLevel: 'debug' | 'info' | 'warn' | 'error';
provider?: 'claude' | 'codex';
provider?: 'claude' | 'codex' | 'mock';
model?: string;
debug?: DebugConfig;
}
@ -157,5 +157,5 @@ export interface GlobalConfig {
export interface ProjectConfig {
workflow?: string;
agents?: CustomAgentConfig[];
provider?: 'claude' | 'codex';
provider?: 'claude' | 'codex' | 'mock';
}

43
src/providers/claude.ts Normal file
View File

@ -0,0 +1,43 @@
/**
* Claude provider implementation
*/
import { callClaude, callClaudeCustom, type ClaudeCallOptions } from '../claude/client.js';
import type { AgentResponse } from '../models/types.js';
import type { Provider, ProviderCallOptions } from './index.js';
/** Claude provider - wraps existing Claude client */
export class ClaudeProvider implements Provider {
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: ClaudeCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
allowedTools: options.allowedTools,
model: options.model,
systemPrompt: options.systemPrompt,
statusPatterns: options.statusPatterns,
onStream: options.onStream,
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
};
return callClaude(agentName, prompt, callOptions);
}
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: ClaudeCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
allowedTools: options.allowedTools,
model: options.model,
statusPatterns: options.statusPatterns,
onStream: options.onStream,
onPermissionRequest: options.onPermissionRequest,
onAskUserQuestion: options.onAskUserQuestion,
bypassPermissions: options.bypassPermissions,
};
return callClaudeCustom(agentName, prompt, systemPrompt, callOptions);
}
}

35
src/providers/codex.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* Codex provider implementation
*/
import { callCodex, callCodexCustom, type CodexCallOptions } from '../codex/client.js';
import type { AgentResponse } from '../models/types.js';
import type { Provider, ProviderCallOptions } from './index.js';
/** Codex provider - wraps existing Codex client */
export class CodexProvider implements Provider {
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: CodexCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
model: options.model,
systemPrompt: options.systemPrompt,
statusPatterns: options.statusPatterns,
onStream: options.onStream,
};
return callCodex(agentName, prompt, callOptions);
}
async callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: CodexCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
model: options.model,
statusPatterns: options.statusPatterns,
onStream: options.onStream,
};
return callCodexCustom(agentName, prompt, systemPrompt, callOptions);
}
}

63
src/providers/index.ts Normal file
View File

@ -0,0 +1,63 @@
/**
* Provider abstraction layer
*
* Provides a unified interface for different agent providers (Claude, Codex, Mock).
* This enables adding new providers without modifying the runner logic.
*/
import type { StreamCallback, PermissionHandler, AskUserQuestionHandler } from '../claude/process.js';
import type { AgentResponse } from '../models/types.js';
import { ClaudeProvider } from './claude.js';
import { CodexProvider } from './codex.js';
import { MockProvider } from './mock.js';
/** Common options for all providers */
export interface ProviderCallOptions {
cwd: string;
sessionId?: string;
model?: string;
systemPrompt?: string;
allowedTools?: string[];
statusPatterns?: Record<string, string>;
onStream?: StreamCallback;
onPermissionRequest?: PermissionHandler;
onAskUserQuestion?: AskUserQuestionHandler;
bypassPermissions?: boolean;
}
/** Provider interface - all providers must implement this */
export interface Provider {
/** Call the provider with a prompt (using systemPrompt from options if provided) */
call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
/** Call the provider with explicit system prompt */
callCustom(agentName: string, prompt: string, systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse>;
}
/** Provider type */
export type ProviderType = 'claude' | 'codex' | 'mock';
/** Provider registry */
const providers: Record<ProviderType, Provider> = {
claude: new ClaudeProvider(),
codex: new CodexProvider(),
mock: new MockProvider(),
};
/**
* Get a provider instance by type
*/
export function getProvider(type: ProviderType): Provider {
const provider = providers[type];
if (!provider) {
throw new Error(`Unknown provider type: ${type}`);
}
return provider;
}
/**
* Register a custom provider
*/
export function registerProvider(type: string, provider: Provider): void {
(providers as Record<string, Provider>)[type] = provider;
}

30
src/providers/mock.ts Normal file
View File

@ -0,0 +1,30 @@
/**
* Mock provider implementation
*/
import { callMock, callMockCustom, type MockCallOptions } from '../mock/client.js';
import type { AgentResponse } from '../models/types.js';
import type { Provider, ProviderCallOptions } from './index.js';
/** Mock provider - wraps existing Mock client */
export class MockProvider implements Provider {
async call(agentName: string, prompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: MockCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
onStream: options.onStream,
};
return callMock(agentName, prompt, callOptions);
}
async callCustom(agentName: string, prompt: string, _systemPrompt: string, options: ProviderCallOptions): Promise<AgentResponse> {
const callOptions: MockCallOptions = {
cwd: options.cwd,
sessionId: options.sessionId,
onStream: options.onStream,
};
return callMockCustom(agentName, prompt, _systemPrompt, callOptions);
}
}