install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/elicit-requirements" ~/.claude/skills/majiayu000-claude-skill-registry-elicit-requirements && rm -rf "$T"
manifest:
skills/data/elicit-requirements/SKILL.mdsource content
civicship-api 要件定義支援
新機能の要件を段階的に引き出し、既存システムとの整合性を考慮した仕様書形式でまとめます。要件の曖昧さを排除し、実装可能な形に落とし込みます。
使用方法
# 機能概要を指定して要件ヒアリングを開始 /elicit-requirements ポイント有効期限機能 # 既存Issueから要件を抽出 /elicit-requirements #123
引数:
: 機能名、概要、またはIssue番号$ARGUMENTS
要件定義プロセス
ステップ1: 初期ヒアリング(5W1H)
ユーザーから提供された情報を基に、以下の観点で質問を生成:
Why(なぜ)- ビジネス価値
## この機能が必要な理由 - ビジネス上の課題は何ですか? - 誰のためのものですか?(エンドユーザー、管理者、運営者) - 現状の何が不便ですか? - この機能で解決できることは何ですか? - 優先度はどれくらいですか?(高/中/低)
What(何を)- 機能概要
## 実現したい機能 - 具体的に何をする機能ですか? - どのような操作を提供しますか? - ユーザーに見える変化は何ですか? - 既存の機能との関係は?(拡張、置き換え、新規)
Who(誰が)- ユーザー・権限
## 利用者と権限 - この機能を使うのは誰ですか? - [ ] 一般ユーザー(User) - [ ] コミュニティマネージャー(Manager) - [ ] 管理者(Admin) - [ ] システム(自動処理) - [ ] 外部サービス(LINE、Firebase) - 認証は必要ですか? - 権限チェックは必要ですか? - RLS(Row-Level Security)の範囲は? - [ ] public(全ユーザー) - [ ] onlyBelongingCommunity(所属コミュニティのみ) - [ ] internal(管理者のみ)
When(いつ)- タイミング・トリガー
## 実行タイミング - いつ実行されますか? - [ ] ユーザー操作時(GraphQL Mutation) - [ ] 定期実行(Cron) - [ ] イベント駆動(Webhook、Pub/Sub) - [ ] バッチ処理 - 実行頻度は? - タイムアウト要件は?
Where(どこで)- 適用範囲
## 適用範囲 - どのドメインに影響しますか? - [ ] account(User, Community, Membership, Wallet) - [ ] experience(Opportunity, Reservation, Participation) - [ ] reward(Utility, Ticket) - [ ] transaction(Point Transfer) - [ ] notification(LINE Messaging) - [ ] content(Article, Image) - [ ] location(Place, City, State) - 対象データの範囲は? - 全コミュニティ? 特定コミュニティのみ? - 全ユーザー? 特定条件のユーザーのみ?
How(どのように)- 実現方法
## 技術的な実現方法 - どのレイヤーで実装しますか? - [ ] GraphQL API(Query/Mutation) - [ ] 内部Service(他ドメインから呼び出し) - [ ] バッチ処理(Firebase Functions) - [ ] Webhook(外部サービス連携) - データの永続化は必要ですか? - [ ] 新しいテーブル作成 - [ ] 既存テーブルへのカラム追加 - [ ] 既存テーブルの変更のみ - トランザクション要件は? - [ ] 単一操作(単純なCRUD) - [ ] 複数テーブル更新(トランザクション必須) - [ ] 分散トランザクション(複数ドメイン)
ステップ2: 既存システムの調査
要件ヒアリング結果を基に、関連する既存実装を調査:
関連ドメインの特定
# 関連ドメインのディレクトリを検索 ls -la src/application/domain/ # 関連するGraphQLスキーマを検索 find src/application/domain -name "*.graphql" -path "*/${RELATED_DOMAIN}/*" # 関連するモデルをPrismaスキーマで確認 grep -A 20 "model t_${TABLE_NAME}" prisma/schema.prisma
類似機能の調査
# 類似のUseCase実装を検索 find src/application/domain -name "usecase.ts" -path "*/${RELATED_DOMAIN}/*" # 類似のService実装を検索 find src/application/domain -name "service.ts" -path "*/${RELATED_DOMAIN}/*" # 類似のGraphQL Mutationを検索 grep -r "mutation.*Create\|mutation.*Update" src/application/domain --include="*.graphql"
既存の制約・ルールの確認
## 既存システムの制約 以下のファイルから関連するビジネスルールを抽出: - **認証ルール**: `src/presentation/graphql/rule.ts` - **RLSパターン**: 関連ドメインの `data/repository.ts` - **バリデーション**: 関連ドメインの `service.ts` - **トランザクションパターン**: 関連ドメインの `usecase.ts`
ステップ3: 受け入れ条件の定義
Given-When-Then 形式で受け入れ条件を明確化:
## 受け入れ条件 ### シナリオ1: [正常系の説明] **Given(前提条件):** - ユーザーがコミュニティに所属している - ウォレットに十分なポイントがある - [その他の前提条件] **When(操作):** - GraphQL Mutation `xxxCreate` を実行 - 入力パラメータ: `{ field1: "value1", field2: 100 }` **Then(期待結果):** - データベースに新しいレコードが作成される - ポイントが減算される - LINE通知が送信される - GraphQLレスポンスが返る: `{ id, field1, field2, createdAt }` --- ### シナリオ2: [異常系の説明] **Given(前提条件):** - ユーザーがコミュニティに所属していない **When(操作):** - GraphQL Mutation `xxxCreate` を実行 **Then(期待結果):** - エラーが返る: `PERMISSION_DENIED` - データベースは変更されない - トランザクションがロールバックされる --- ### シナリオ3: [境界値の説明] **Given(前提条件):** - ウォレットのポイントが0 **When(操作):** - 1ポイント使用する操作を実行 **Then(期待結果):** - エラーが返る: `INSUFFICIENT_POINTS` - データベースは変更されない
ステップ4: データモデルの設計
Prismaスキーマの変更が必要な場合、設計案を提示:
## データモデル設計 ### オプションA: 新規テーブル作成 \`\`\`prisma model t_xxx { id String @id @default(cuid()) userId String communityId String // 新しいフィールド fieldName String status XxxStatus @default(ACTIVE) // リレーション user t_users @relation(fields: [userId], references: [id], onDelete: Cascade) community t_communities @relation(fields: [communityId], references: [id], onDelete: Cascade) // タイムスタンプ createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // インデックス @@index([userId]) @@index([communityId]) @@index([status]) @@map("t_xxx") } enum XxxStatus { ACTIVE INACTIVE EXPIRED } \`\`\` **マイグレーション名:** `add_xxx_table` --- ### オプションB: 既存テーブルへのカラム追加 \`\`\`prisma model t_existing { // 既存のフィールド id String @id @default(cuid()) // 追加フィールド newField String? newStatus XxxStatus? @default(ACTIVE) // ... } \`\`\` **マイグレーション名:** `add_existing_new_field` **注意:** 既存データへの影響を確認すること
ステップ5: GraphQLスキーマ設計
GraphQL型、Query、Mutationの設計案を提示:
## GraphQLスキーマ設計 ### 型定義(type.graphql) \`\`\`graphql """ [機能の説明] """ type GqlXxx { """ID""" id: ID! """フィールドの説明""" fieldName: String! """ステータス""" status: GqlXxxStatus! """所属コミュニティ""" community: GqlCommunity! """作成日時""" createdAt: DateTime! """更新日時""" updatedAt: DateTime! } """ステータス""" enum GqlXxxStatus { """有効""" ACTIVE """無効""" INACTIVE """期限切れ""" EXPIRED } """作成入力""" input GqlXxxCreateInput { """フィールドの説明""" fieldName: String! """その他のフィールド""" otherField: Int! } """作成結果""" type GqlXxxCreatePayload { """作成されたXxx""" xxx: GqlXxx! } """更新入力""" input GqlXxxUpdateInput { """ID""" id: ID! """フィールドの説明(オプショナル)""" fieldName: String } """更新結果""" type GqlXxxUpdatePayload { """更新されたXxx""" xxx: GqlXxx! } \`\`\` --- ### Query定義(query.graphql) \`\`\`graphql extend type Query { """ Xxx一覧を取得 """ xxxList( """コミュニティID""" communityId: ID! """ステータスフィルタ""" status: GqlXxxStatus """ページネーション用カーソル""" cursor: ID """取得件数(デフォルト: 20)""" limit: Int ): GqlXxxConnection! """ Xxx詳細を取得 """ xxx( """ID""" id: ID! ): GqlXxx } """ページネーション結果""" type GqlXxxConnection { """Xxxリスト""" nodes: [GqlXxx!]! """次ページのカーソル""" cursor: ID """次ページが存在するか""" hasMore: Boolean! } \`\`\` --- ### Mutation定義(mutation.graphql) \`\`\`graphql extend type Mutation { """ Xxxを作成(マネージャー権限) """ xxxCreate( """入力パラメータ""" input: GqlXxxCreateInput! """権限情報""" permission: GqlPermission! ): GqlXxxCreatePayload! """ Xxxを更新(マネージャー権限) """ xxxUpdate( """入力パラメータ""" input: GqlXxxUpdateInput! """権限情報""" permission: GqlPermission! ): GqlXxxUpdatePayload! """ Xxxを削除(マネージャー権限) """ xxxDelete( """ID""" id: ID! """権限情報""" permission: GqlPermission! ): GqlXxxDeletePayload! } """削除結果""" type GqlXxxDeletePayload { """削除されたID""" id: ID! } \`\`\`
ステップ6: アーキテクチャ設計
DDD/Clean Architectureに従った実装方針を提示:
## アーキテクチャ設計 ### ドメイン配置 **新規ドメイン作成が必要な場合:** \`\`\` src/application/domain/{category}/{domain-name}/ ├── controller/ │ ├── resolver.ts # GraphQL Resolver │ └── dataloader.ts # N+1防止 ├── usecase.ts # トランザクション管理 ├── service.ts # ビジネスロジック ├── data/ │ ├── repository.ts # Prismaクエリ │ ├── interface.ts # Repository契約 │ ├── converter.ts # 入力変換 │ └── type.ts # 型定義 ├── presenter.ts # レスポンスフォーマット └── schema/ ├── query.graphql ├── mutation.graphql └── type.graphql \`\`\` **カテゴリ選択:** - `account` - ユーザー、コミュニティ、ウォレット関連 - `experience` - 体験、予約、参加関連 - `reward` - ポイント、チケット、特典関連 - `transaction` - ポイント送受信関連 - `notification` - 通知関連 - `content` - コンテンツ関連 - `location` - 地理情報関連 --- ### レイヤー責任分担 #### **Resolver(controller/resolver.ts)** \`\`\`typescript @injectable() export default class XxxResolver { constructor( @inject("XxxUseCase") private usecase: XxxUseCase ) {} Query = { xxxList: async (_, args, ctx) => { return this.usecase.listXxx(args, ctx); }, xxx: async (_, { id }, ctx) => { return this.usecase.getXxx(id, ctx); } }; Mutation = { xxxCreate: async (_, { input, permission }, ctx) => { return this.usecase.managerCreateXxx({ input, permission }, ctx); } }; Xxx = { community: (parent, _, ctx) => ctx.loaders.community.load(parent.communityId) }; } \`\`\` **責任:** UseCaseを呼ぶだけ、ビジネスロジックなし --- #### **UseCase(usecase.ts)** \`\`\`typescript @injectable() export default class XxxUseCase { constructor( @inject("XxxService") private service: XxxService, @inject("WalletService") private walletService: WalletService ) {} async managerCreateXxx({ input, permission }, ctx: IContext) { // トランザクション管理(UseCaseの責務) return ctx.issuer.onlyBelongingCommunity(ctx, async (tx) => { // 1. Xxxを作成 const xxx = await this.service.createXxx(ctx, input, permission.communityId, tx); // 2. ポイント減算(他ドメインのServiceを呼ぶ) if (input.pointCost > 0) { await this.walletService.transferPoints(ctx, { fromUserId: ctx.userId, toUserId: permission.communityId, points: input.pointCost }, tx); } // 3. Presenterでフォーマット return XxxPresenter.create(xxx); }); } } \`\`\` **責任:** フロー制御、トランザクション管理、他ドメインServiceの呼び出し --- #### **Service(service.ts)** \`\`\`typescript @injectable() export default class XxxService { constructor( @inject("XxxRepository") private repo: IXxxRepository, @inject("XxxConverter") private converter: XxxConverter ) {} async createXxx( ctx: IContext, input: GqlXxxCreateInput, communityId: string, tx: Prisma.TransactionClient ): Promise<PrismaXxx> { // 1. バリデーション if (input.fieldName.length === 0) { throw new Error("INVALID_INPUT"); } // 2. ビジネスルール const existingCount = await this.repo.countByCommunity(ctx, communityId, tx); if (existingCount >= 100) { throw new Error("MAX_LIMIT_REACHED"); } // 3. 入力変換 const data = this.converter.toCreateData(input, communityId); // 4. Repository呼び出し(txを渡す) return await this.repo.create(ctx, data, tx); } } \`\`\` **責任:** ビジネスロジック、バリデーション、txの伝播(分岐なし) --- #### **Repository(data/repository.ts)** \`\`\`typescript @injectable() export default class XxxRepository implements IXxxRepository { async create( ctx: IContext, data: Prisma.t_xxxCreateInput, tx: Prisma.TransactionClient ): Promise<PrismaXxx> { // txは必須(Mutationのため) return tx.t_xxx.create({ data, select: xxxSelect }); } async findById( ctx: IContext, id: string, tx?: Prisma.TransactionClient ): Promise<PrismaXxx | null> { // txはオプショナル(Queryのため) if (tx) { return tx.t_xxx.findUnique({ where: { id }, select: xxxSelect }); } return ctx.issuer.public(ctx, (tx) => tx.t_xxx.findUnique({ where: { id }, select: xxxSelect }) ); } } \`\`\` **責任:** Prismaクエリ、RLS、txの分岐処理(Queryのみ) --- #### **Converter(data/converter.ts)** \`\`\`typescript @injectable() export default class XxxConverter { toCreateData( input: GqlXxxCreateInput, communityId: string ): Prisma.t_xxxCreateInput { return { id: cuid(), communityId, fieldName: input.fieldName, status: "ACTIVE", createdAt: new Date(), updatedAt: new Date() }; } } \`\`\` **責任:** GraphQL入力 → Prisma形式、純粋関数(副作用なし) --- #### **Presenter(presenter.ts)** \`\`\`typescript export default class XxxPresenter { static create(xxx: PrismaXxx): GqlXxxCreatePayload { return { xxx: this.toGraphQL(xxx) }; } static toGraphQL(xxx: PrismaXxx): GqlXxx { return { id: xxx.id, fieldName: xxx.fieldName, status: xxx.status, communityId: xxx.communityId, createdAt: xxx.createdAt, updatedAt: xxx.updatedAt }; } } \`\`\` **責任:** Prisma型 → GraphQL型、純粋関数(ビジネスロジックなし)
ステップ7: テスト戦略
テストケースの設計:
## テスト戦略 ### ユニットテスト(Service層) **ファイル:** `__tests__/unit/{domain}/service.test.ts` \`\`\`typescript describe("XxxService", () => { let service: XxxService; let mockRepo: jest.Mocked<IXxxRepository>; let mockConverter: jest.Mocked<XxxConverter>; beforeEach(() => { mockRepo = { create: jest.fn(), findById: jest.fn(), countByCommunity: jest.fn() } as any; mockConverter = { toCreateData: jest.fn() } as any; service = new XxxService(mockRepo, mockConverter); }); describe("createXxx", () => { it("正常系: Xxxを作成できる", async () => { const input: GqlXxxCreateInput = { fieldName: "test" }; const communityId = "community-id"; const tx = mockTransaction(); mockConverter.toCreateData.mockReturnValue(mockData); mockRepo.create.mockResolvedValue(mockXxx); mockRepo.countByCommunity.mockResolvedValue(0); const result = await service.createXxx(ctx, input, communityId, tx); expect(result).toEqual(mockXxx); expect(mockRepo.create).toHaveBeenCalledWith(ctx, mockData, tx); }); it("異常系: 上限に達している場合エラー", async () => { mockRepo.countByCommunity.mockResolvedValue(100); await expect( service.createXxx(ctx, input, communityId, tx) ).rejects.toThrow("MAX_LIMIT_REACHED"); }); }); }); \`\`\` --- ### 統合テスト(UseCase層) **ファイル:** `__tests__/integration/{domain}/usecase.test.ts` \`\`\`typescript describe("XxxUseCase Integration", () => { let usecase: XxxUseCase; let ctx: IContext; beforeEach(async () => { usecase = container.resolve(XxxUseCase); ctx = await createTestContext(); }); afterEach(async () => { await cleanupTestData(); }); it("managerCreateXxx: トランザクション内でXxxを作成", async () => { const input: GqlXxxCreateInput = { fieldName: "test" }; const permission = { communityId: "community-id" }; const result = await usecase.managerCreateXxx({ input, permission }, ctx); expect(result.xxx).toBeDefined(); expect(result.xxx.fieldName).toBe("test"); // データベースで確認 const saved = await prisma.t_xxx.findUnique({ where: { id: result.xxx.id } }); expect(saved).toBeDefined(); }); }); \`\`\` --- ### E2Eテスト(GraphQL API) **ファイル:** `__tests__/e2e/{domain}/graphql.test.ts` \`\`\`typescript describe("Xxx GraphQL API", () => { let server: ApolloServer; let token: string; beforeAll(async () => { server = await createTestServer(); token = await generateTestToken({ role: "manager" }); }); it("xxxCreate: GraphQL経由でXxxを作成", async () => { const mutation = \` mutation { xxxCreate( input: { fieldName: "test" } permission: { communityId: "community-id" } ) { xxx { id fieldName status } } } \`; const response = await server.executeOperation( { query: mutation }, { req: { headers: { authorization: \`Bearer \${token}\` } } } ); expect(response.data?.xxxCreate.xxx).toBeDefined(); expect(response.data?.xxxCreate.xxx.fieldName).toBe("test"); }); }); \`\`\` --- ### テストカバレッジ目標 | レイヤー | 目標カバレッジ | |---------|---------------| | Service | 90%以上 | | UseCase | 85%以上 | | Converter | 95%以上 | | Presenter | 95%以上 | | Repository | 80%以上(統合テストでカバー) | | Resolver | 70%以上(E2Eテストでカバー) |
ステップ8: 非機能要件の確認
パフォーマンス、セキュリティ、運用面の要件を確認:
## 非機能要件 ### パフォーマンス - [ ] **レスポンスタイム目標:** 〇〇ms以内 - [ ] **想定同時実行数:** 〇〇リクエスト/秒 - [ ] **N+1問題対策:** DataLoaderを使用 - [ ] **ページネーション:** カーソルベース(大量データ対応) - [ ] **インデックス:** 頻繁にクエリされるカラムに追加 ### セキュリティ - [ ] **認証:** Firebase Auth必須 - [ ] **認可:** RLS(ctx.issuer)を使用 - [ ] **入力バリデーション:** Service層で実施 - [ ] **SQLインジェクション対策:** Prismaが自動対応 - [ ] **センシティブデータ:** ログに出力しない - [ ] **レート制限:** 必要に応じて設定 ### 運用 - [ ] **監視:** エラーログをFirebase Crashlyticsに送信 - [ ] **アラート:** 〇〇以上のエラー率で通知 - [ ] **バックアップ:** データベース定期バックアップ - [ ] **ロールバック計画:** マイグレーション失敗時の手順 - [ ] **ドキュメント:** README、CHANGELOG更新 ### スケーラビリティ - [ ] **データ増加への対応:** パーティショニング、アーカイブ戦略 - [ ] **コミュニティ数の増加:** 水平スケーリング可能か - [ ] **外部API制限:** レート制限、リトライロジック
ステップ9: 依存関係と制約の整理
実装時の依存関係と制約を明確化:
## 依存関係 ### 他ドメインへの依存 - **WalletService** - ポイント減算 - **NotificationService** - LINE通知送信 - **ImageService** - 画像アップロード - **CommunityService** - コミュニティ情報取得 ### 外部サービスへの依存 - **Firebase Auth** - 認証トークン検証 - **Google Cloud Storage** - 画像保存 - **LINE Messaging API** - 通知配信 ### データベース制約 - **外部キー:** t_communities, t_users - **ユニーク制約:** (communityId, fieldName) の組み合わせ - **NOT NULL制約:** fieldName, status --- ## 技術的制約 - **TypeScript:** 型安全性を保つ - **Prisma ORM:** 生SQLは原則禁止 - **GraphQL命名規則:** 型は `Gql*` プレフィックス - **テーブル命名規則:** `t_*` プレフィックス - **トランザクション:** UseCaseで管理、Serviceに伝播 - **RLS:** 全てのクエリで `ctx.issuer` を使用
ステップ10: 要件定義書の生成
全ての情報を統合し、実装可能な形の要件定義書を出力:
# 要件定義書: [機能名] **作成日:** YYYY-MM-DD **ステータス:** Draft / Review / Approved **優先度:** High / Medium / Low --- ## 1. 概要 ### 1.1 背景・目的 [ビジネス課題と解決したいこと] ### 1.2 スコープ **対象:** - [含まれるもの] **対象外:** - [含まれないもの] --- ## 2. 機能要件 ### 2.1 ユーザーストーリー **As a** [ユーザーロール] **I want to** [やりたいこと] **So that** [達成したい目的] ### 2.2 受け入れ条件 [Given-When-Thenシナリオ] ### 2.3 画面/API仕様 [GraphQLスキーマ設計] --- ## 3. データ設計 ### 3.1 データモデル [Prismaスキーマ設計] ### 3.2 マイグレーション計画 **マイグレーション名:** `add_xxx_table` **影響範囲:** - 新規テーブル作成 - 既存データへの影響: なし --- ## 4. アーキテクチャ設計 ### 4.1 ドメイン配置 [ディレクトリ構造] ### 4.2 レイヤー責任 [Resolver、UseCase、Service、Repositoryの役割] --- ## 5. 非機能要件 ### 5.1 パフォーマンス [レスポンスタイム、スループット] ### 5.2 セキュリティ [認証、認可、バリデーション] ### 5.3 運用 [監視、アラート、バックアップ] --- ## 6. テスト計画 ### 6.1 テストケース [ユニット、統合、E2Eテストのケース] ### 6.2 カバレッジ目標 [レイヤー別の目標カバレッジ] --- ## 7. 実装計画 ### 7.1 タスク分解 - [ ] Prismaスキーマ変更 - [ ] マイグレーション実行 - [ ] GraphQLスキーマ定義 - [ ] ドメイン実装(Resolver、UseCase、Service、Repository) - [ ] テスト実装 - [ ] ドキュメント更新 ### 7.2 見積もり **実装工数:** 〇〇日 **テスト工数:** 〇〇日 **総工数:** 〇〇日 --- ## 8. リスクと対策 | リスク | 影響度 | 発生確率 | 対策 | |--------|--------|----------|------| | [リスク1] | High/Medium/Low | High/Medium/Low | [対策] | --- ## 9. 依存関係 ### 9.1 他機能への依存 [依存する既存機能] ### 9.2 他機能への影響 [この機能が影響を与える既存機能] --- ## 10. 承認 **レビュー担当者:** - [ ] プロダクトオーナー - [ ] テックリード - [ ] セキュリティ担当 **承認日:** YYYY-MM-DD
活用例
例1: ポイント有効期限機能
/elicit-requirements ポイント有効期限機能
期待される質問:
- ポイント有効期限はどのタイミングで設定しますか?(付与時、一律設定)
- 有効期限切れのポイントはどうなりますか?(自動失効、通知あり)
- 既存のポイントに対する影響は?(遡及適用、新規のみ)
生成される要件定義:
- データモデル:
にt_wallets
カラム追加expiresAt - GraphQLスキーマ:
にGqlWallet
フィールド追加expiresAt - バッチ処理: 有効期限切れポイントの自動失効
- 通知: 有効期限7日前にLINE通知
例2: スキルマッチング機能
/elicit-requirements スキルマッチング機能
期待される質問:
- ユーザーのスキルはどう登録しますか?(タグ形式、階層構造)
- マッチングアルゴリズムは?(完全一致、類似度スコア)
- どの画面で使いますか?(Opportunity一覧、推薦)
生成される要件定義:
- データモデル:
,t_user_skills
テーブル追加t_opportunity_skills - GraphQLスキーマ:
型、GqlSkill
Query追加skillMatch - アルゴリズム: スキルタグの部分一致スコアリング
注意事項
要件定義の原則
- ✅ 具体的に: 曖昧な表現を避ける
- ✅ 測定可能に: 受け入れ条件を明確にする
- ✅ 実現可能に: 既存システムとの整合性を確認
- ✅ 関連性を保つ: ビジネス価値と紐づける
- ✅ 期限を設定: 優先度とスケジュールを決める
よくある落とし穴
- ❌ 要件が曖昧: 「柔軟に」「適切に」などの抽象表現
- ❌ 実装詳細の混入: 要件定義で技術選定しない
- ❌ スコープクリープ: 途中で要件を追加しない
- ❌ 既存システム無視: 互換性を考慮しない
参考資料
以下については
@CLAUDE.md を参照してください:
- アーキテクチャパターン
- トランザクション管理
- RLSの使い方
- GraphQL命名規則