takt: # タスク指示書: 専門知識のknowledgeへの抽出と付与
## 概要 既存のスタンス/インストラクションに埋め込まれているフロントエンド・バックエンド等の専門知識をknowledgeファイルとして抽出し、抽出元に適切に付与する。 --- ## タスク ### 1. 専門知識の抽出(優先度: 高) 既存のスタンス・インストラクションファイルをレビューし、以下の専門知識を特定・抽出: - **フロントエンド知識**(React、CSS、UI/UXなど) - **バックエンド知識**(API設計、DB、サーバーサイドなど) - **その他の専門知識**(発見したもの) 抽出した知識をknowledgeファイルとして作成する。 ### 2. 抽出元への付与(優先度: 高) 抽出した知識を、元々その知識を使用していたスタンス/インストラクションに付与設定する。 - 抽出元 = 付与先 --- ## 確認方法 - 抽出後、元のスタンス/インストラクションから専門知識が分離されていること - 抽出元にknowledgeが正しく付与設定されていること
This commit is contained in:
parent
e7d5dbfb33
commit
b7c2a4db08
427
resources/global/en/knowledge/architecture.md
Normal file
427
resources/global/en/knowledge/architecture.md
Normal file
@ -0,0 +1,427 @@
|
||||
# Architecture Knowledge
|
||||
|
||||
## Structure & Design
|
||||
|
||||
**File Organization:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Single file > 200 lines | Consider splitting |
|
||||
| Single file > 300 lines | REJECT |
|
||||
| Single file with multiple responsibilities | REJECT |
|
||||
| Unrelated code coexisting | REJECT |
|
||||
|
||||
**Module Structure:**
|
||||
|
||||
- High cohesion: Related functionality grouped together
|
||||
- Low coupling: Minimal inter-module dependencies
|
||||
- No circular dependencies
|
||||
- Appropriate directory hierarchy
|
||||
|
||||
**Function Design:**
|
||||
|
||||
- One responsibility per function
|
||||
- Consider splitting functions over 30 lines
|
||||
- Side effects clearly defined
|
||||
|
||||
**Layer Design:**
|
||||
|
||||
- Dependency direction: Upper layers -> Lower layers (reverse prohibited)
|
||||
- Controller -> Service -> Repository flow maintained
|
||||
- 1 interface = 1 responsibility (no giant Service classes)
|
||||
|
||||
**Directory Structure:**
|
||||
|
||||
Structure pattern selection:
|
||||
|
||||
| Pattern | Use Case | Example |
|
||||
|---------|----------|---------|
|
||||
| Layered | Small scale, CRUD-centric | `controllers/`, `services/`, `repositories/` |
|
||||
| Vertical Slice | Medium-large scale, high feature independence | `features/auth/`, `features/order/` |
|
||||
| Hybrid | Common foundation + feature modules | `core/` + `features/` |
|
||||
|
||||
Vertical Slice Architecture (organizing code by feature):
|
||||
|
||||
```
|
||||
src/
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── LoginCommand.ts
|
||||
│ │ ├── LoginHandler.ts
|
||||
│ │ ├── AuthRepository.ts
|
||||
│ │ └── auth.test.ts
|
||||
│ └── order/
|
||||
│ ├── CreateOrderCommand.ts
|
||||
│ ├── CreateOrderHandler.ts
|
||||
│ └── ...
|
||||
└── shared/ # Shared across features
|
||||
├── database/
|
||||
└── middleware/
|
||||
```
|
||||
|
||||
Vertical Slice criteria:
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Single feature spans 3+ layers | Consider slicing |
|
||||
| Minimal inter-feature dependencies | Recommend slicing |
|
||||
| Over 50% shared processing | Keep layered |
|
||||
| Team organized by features | Slicing required |
|
||||
|
||||
Prohibited patterns:
|
||||
|
||||
| Pattern | Problem |
|
||||
|---------|---------|
|
||||
| Bloated `utils/` | Becomes graveyard of unclear responsibilities |
|
||||
| Lazy placement in `common/` | Dependencies become unclear |
|
||||
| Excessive nesting (4+ levels) | Navigation difficulty |
|
||||
| Mixed features and layers | `features/services/` prohibited |
|
||||
|
||||
**Separation of Concerns:**
|
||||
|
||||
- Read and write responsibilities separated
|
||||
- Data fetching at root (View/Controller), passed to children
|
||||
- Error handling centralized (no try-catch scattered everywhere)
|
||||
- Business logic not leaking into Controller/View
|
||||
|
||||
## Code Quality Detection
|
||||
|
||||
**Explanatory Comment (What/How) Detection Criteria:**
|
||||
|
||||
Detect comments that simply restate code behavior in natural language.
|
||||
|
||||
| Judgment | Criteria |
|
||||
|----------|----------|
|
||||
| REJECT | Restates code behavior in natural language |
|
||||
| REJECT | Repeats what is already obvious from function/variable names |
|
||||
| REJECT | JSDoc that only paraphrases the function name without adding information |
|
||||
| OK | Explains why a particular implementation was chosen |
|
||||
| OK | Explains the reason behind seemingly unusual behavior |
|
||||
| Best | No comment needed — the code itself communicates intent |
|
||||
|
||||
```typescript
|
||||
// REJECT - Restates code (What)
|
||||
// If interrupted, abort immediately
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// REJECT - Restates the loop
|
||||
// Check transitions in order
|
||||
for (const transition of step.transitions) {
|
||||
|
||||
// REJECT - Repeats the function name
|
||||
/** Check if status matches transition condition. */
|
||||
export function matchesCondition(status: Status, condition: TransitionCondition): boolean {
|
||||
|
||||
// OK - Design decision (Why)
|
||||
// User interruption takes priority over piece-defined transitions
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// OK - Reason behind seemingly odd behavior
|
||||
// stay can cause loops, but is only used when explicitly specified by the user
|
||||
return step.name;
|
||||
```
|
||||
|
||||
**Direct State Mutation Detection Criteria:**
|
||||
|
||||
Detect direct mutation of arrays or objects.
|
||||
|
||||
```typescript
|
||||
// REJECT - Direct array mutation
|
||||
const steps: Step[] = getSteps();
|
||||
steps.push(newStep); // Mutates original array
|
||||
steps.splice(index, 1); // Mutates original array
|
||||
steps[0].status = 'done'; // Nested object also mutated directly
|
||||
|
||||
// OK - Immutable operations
|
||||
const withNew = [...steps, newStep];
|
||||
const without = steps.filter((_, i) => i !== index);
|
||||
const updated = steps.map((s, i) =>
|
||||
i === 0 ? { ...s, status: 'done' } : s
|
||||
);
|
||||
|
||||
// REJECT - Direct object mutation
|
||||
function updateConfig(config: Config) {
|
||||
config.logLevel = 'debug'; // Mutates argument directly
|
||||
config.steps.push(newStep); // Nested mutation too
|
||||
return config;
|
||||
}
|
||||
|
||||
// OK - Returns new object
|
||||
function updateConfig(config: Config): Config {
|
||||
return {
|
||||
...config,
|
||||
logLevel: 'debug',
|
||||
steps: [...config.steps, newStep],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Security (Basic Checks)
|
||||
|
||||
- Injection prevention (SQL, Command, XSS)
|
||||
- User input validation
|
||||
- Hardcoded sensitive information
|
||||
|
||||
## Testability
|
||||
|
||||
- Dependency injection enabled
|
||||
- Mockable design
|
||||
- Tests are written
|
||||
|
||||
## Anti-Pattern Detection
|
||||
|
||||
REJECT when these patterns are found:
|
||||
|
||||
| Anti-Pattern | Problem |
|
||||
|--------------|---------|
|
||||
| God Class/Component | Single class with too many responsibilities |
|
||||
| Feature Envy | Frequently accessing other modules' data |
|
||||
| Shotgun Surgery | Single change ripples across multiple files |
|
||||
| Over-generalization | Variants and extension points not currently needed |
|
||||
| Hidden Dependencies | Child components implicitly calling APIs etc. |
|
||||
| Non-idiomatic | Custom implementation ignoring language/FW conventions |
|
||||
|
||||
## 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
|
||||
}
|
||||
```
|
||||
|
||||
## Workaround Detection
|
||||
|
||||
Don't overlook compromises made to "just make it work."
|
||||
|
||||
| Pattern | Example |
|
||||
|---------|---------|
|
||||
| Unnecessary package additions | Mystery libraries added just to make things work |
|
||||
| Test deletion/skipping | `@Disabled`, `.skip()`, commented out |
|
||||
| Empty implementations/stubs | `return null`, `// TODO: implement`, `pass` |
|
||||
| Mock data in production | Hardcoded dummy data |
|
||||
| Swallowed errors | Empty `catch {}`, `rescue nil` |
|
||||
| Magic numbers | Unexplained `if (status == 3)` |
|
||||
|
||||
## Strict TODO Comment Prohibition
|
||||
|
||||
"We'll do it later" never gets done. What's not done now is never done.
|
||||
|
||||
TODO comments are immediate REJECT.
|
||||
|
||||
```kotlin
|
||||
// REJECT - Future-looking TODO
|
||||
// TODO: Add authorization check by facility ID
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
|
||||
// APPROVE - Implement now
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
val currentUserFacilityId = getCurrentUserFacilityId()
|
||||
val holiday = findHolidayById(id)
|
||||
require(holiday.facilityId == currentUserFacilityId) {
|
||||
"Cannot delete holiday from another facility"
|
||||
}
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
```
|
||||
|
||||
Only acceptable TODO cases:
|
||||
|
||||
| Condition | Example | Judgment |
|
||||
|-----------|---------|----------|
|
||||
| External dependency prevents implementation + Issued | `// TODO(#123): Implement after API key obtained` | Acceptable |
|
||||
| Technical constraint prevents + Issued | `// TODO(#456): Waiting for library bug fix` | Acceptable |
|
||||
| "Future implementation", "add later" | `// TODO: Add validation` | REJECT |
|
||||
| "No time for now" | `// TODO: Refactor` | REJECT |
|
||||
|
||||
Correct handling:
|
||||
- Needed now → Implement now
|
||||
- Not needed now → Delete the code
|
||||
- External blocker → Create issue and include ticket number in comment
|
||||
|
||||
## DRY Violation Detection
|
||||
|
||||
Detect duplicate code.
|
||||
|
||||
| Pattern | Judgment |
|
||||
|---------|----------|
|
||||
| Same logic in 3+ places | Immediate REJECT - Extract to function/method |
|
||||
| Same validation in 2+ places | Immediate REJECT - Extract to validator function |
|
||||
| Similar components 3+ | Immediate REJECT - Create shared component |
|
||||
| Copy-paste derived code | Immediate REJECT - Parameterize or abstract |
|
||||
|
||||
AHA principle (Avoid Hasty Abstractions) balance:
|
||||
- 2 duplications → Wait and see
|
||||
- 3 duplications → Extract immediately
|
||||
- Different domain duplications → Don't abstract (e.g., customer validation vs admin validation are different)
|
||||
|
||||
## Spec Compliance Verification
|
||||
|
||||
Verify that changes comply with the project's documented specifications.
|
||||
|
||||
Verification targets:
|
||||
|
||||
| Target | What to Check |
|
||||
|--------|---------------|
|
||||
| CLAUDE.md / README.md | Conforms to schema definitions, design principles, constraints |
|
||||
| Type definitions / Zod schemas | New fields reflected in schemas |
|
||||
| YAML/JSON config files | Follows documented format |
|
||||
|
||||
Specific checks:
|
||||
|
||||
1. When config files (YAML, etc.) are modified or added:
|
||||
- Cross-reference with schema definitions in CLAUDE.md, etc.
|
||||
- No ignored or invalid fields present
|
||||
- No required fields missing
|
||||
|
||||
2. When type definitions or interfaces are modified:
|
||||
- Documentation schema descriptions are updated
|
||||
- Existing config files are compatible with new schema
|
||||
|
||||
REJECT when these patterns are found:
|
||||
|
||||
| Pattern | Problem |
|
||||
|---------|---------|
|
||||
| Fields not in the spec | Ignored or unexpected behavior |
|
||||
| Invalid values per spec | Runtime error or silently ignored |
|
||||
| Violation of documented constraints | Against design intent |
|
||||
|
||||
## Call Chain Verification
|
||||
|
||||
When new parameters/fields are added, verify not just the changed file but also callers.
|
||||
|
||||
Verification steps:
|
||||
1. When finding new optional parameters or interface fields, `Grep` all callers
|
||||
2. Check if all callers pass the new parameter
|
||||
3. If fallback value (`?? default`) exists, verify if fallback is used as intended
|
||||
|
||||
Danger patterns:
|
||||
|
||||
| Pattern | Problem | Detection |
|
||||
|---------|---------|-----------|
|
||||
| `options.xxx ?? fallback` where all callers omit `xxx` | Feature implemented but always falls back | grep callers |
|
||||
| Tests set values directly with mocks | Don't go through actual call chain | Check test construction |
|
||||
| `executeXxx()` doesn't receive `options` it uses internally | No route to pass value from above | Check function signature |
|
||||
|
||||
```typescript
|
||||
// Missing wiring: No route to receive projectCwd
|
||||
export async function executePiece(config, cwd, task) {
|
||||
const engine = new PieceEngine(config, cwd, task); // No options
|
||||
}
|
||||
|
||||
// Wired: Can pass projectCwd
|
||||
export async function executePiece(config, cwd, task, options?) {
|
||||
const engine = new PieceEngine(config, cwd, task, options);
|
||||
}
|
||||
```
|
||||
|
||||
Logically dead code due to caller constraints:
|
||||
|
||||
Call chain verification applies not only to "missing wiring" but also to the reverse — unnecessary guards for conditions that callers already guarantee.
|
||||
|
||||
| Pattern | Problem | Detection |
|
||||
|---------|---------|-----------|
|
||||
| TTY check when all callers require TTY | Unreachable branch remains | grep all callers' preconditions |
|
||||
| Null guard when callers already check null | Redundant defense | Trace caller constraints |
|
||||
| Runtime type check when TypeScript types constrain | Not trusting type safety | Check TypeScript type constraints |
|
||||
|
||||
Verification steps:
|
||||
1. When finding defensive branches (TTY check, null guard, etc.), grep all callers
|
||||
2. If all callers already guarantee the condition, guard is unnecessary → REJECT
|
||||
3. If some callers don't guarantee it, keep the guard
|
||||
|
||||
## Quality Attributes
|
||||
|
||||
| Attribute | Review Point |
|
||||
|-----------|--------------|
|
||||
| Scalability | Design handles increased load |
|
||||
| Maintainability | Easy to modify and fix |
|
||||
| Observability | Logging and monitoring enabled |
|
||||
|
||||
## Big Picture
|
||||
|
||||
Don't get lost in minor "clean code" nitpicks.
|
||||
|
||||
Verify:
|
||||
- How will this code evolve in the future
|
||||
- Is scaling considered
|
||||
- Is technical debt being created
|
||||
- Does it align with business requirements
|
||||
- Is naming consistent with the domain
|
||||
|
||||
## Change Scope Assessment
|
||||
|
||||
Check change scope and include in report (non-blocking).
|
||||
|
||||
| Scope Size | Lines Changed | Action |
|
||||
|------------|---------------|--------|
|
||||
| Small | ~200 lines | Review as-is |
|
||||
| Medium | 200-500 lines | Review as-is |
|
||||
| Large | 500+ lines | Continue review. Suggest splitting if possible |
|
||||
|
||||
Note: Some tasks require large changes. Don't REJECT based on line count alone.
|
||||
|
||||
Verify:
|
||||
- Changes are logically cohesive (no unrelated changes mixed in)
|
||||
- Coder's scope declaration matches actual changes
|
||||
|
||||
Include as suggestions (non-blocking):
|
||||
- If splittable, present splitting proposal
|
||||
417
resources/global/en/knowledge/cqrs-es.md
Normal file
417
resources/global/en/knowledge/cqrs-es.md
Normal file
@ -0,0 +1,417 @@
|
||||
# CQRS+ES Knowledge
|
||||
|
||||
## Aggregate Design
|
||||
|
||||
Aggregates hold only fields necessary for decision-making.
|
||||
|
||||
Command Model (Aggregate) role is to "receive commands, make decisions, and emit events". Query data is handled by Read Model (Projection).
|
||||
|
||||
"Necessary for decision" means:
|
||||
- Used in `if`/`require` conditional branches
|
||||
- Field value referenced when emitting events in instance methods
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Aggregate spans multiple transaction boundaries | REJECT |
|
||||
| Direct references between Aggregates (not ID references) | REJECT |
|
||||
| Aggregate exceeds 100 lines | Consider splitting |
|
||||
| Business invariants exist outside Aggregate | REJECT |
|
||||
| Holding fields not used for decisions | REJECT |
|
||||
|
||||
Good Aggregate:
|
||||
```kotlin
|
||||
// Only fields necessary for decisions
|
||||
data class Order(
|
||||
val orderId: String, // Used when emitting events
|
||||
val status: OrderStatus // Used for state checking
|
||||
) {
|
||||
fun confirm(confirmedBy: String): OrderConfirmedEvent {
|
||||
require(status == OrderStatus.PENDING) { "Cannot confirm in this state" }
|
||||
return OrderConfirmedEvent(
|
||||
orderId = orderId,
|
||||
confirmedBy = confirmedBy,
|
||||
confirmedAt = LocalDateTime.now()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Holding fields not used for decisions (NG)
|
||||
data class Order(
|
||||
val orderId: String,
|
||||
val customerId: String, // Not used for decisions
|
||||
val shippingAddress: Address, // Not used for decisions
|
||||
val status: OrderStatus
|
||||
)
|
||||
```
|
||||
|
||||
Aggregates with no additional operations have ID only:
|
||||
```kotlin
|
||||
// When only creation, no additional operations
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event Design
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Event not in past tense (Created → Create) | REJECT |
|
||||
| Event contains logic | REJECT |
|
||||
| Event contains internal state of other Aggregates | REJECT |
|
||||
| Event schema not version controlled | Warning |
|
||||
| CRUD-style events (Updated, Deleted) | Needs review |
|
||||
|
||||
Good Events:
|
||||
```kotlin
|
||||
// Good: Domain intent is clear
|
||||
OrderPlaced, PaymentReceived, ItemShipped
|
||||
|
||||
// Bad: CRUD style
|
||||
OrderUpdated, OrderDeleted
|
||||
```
|
||||
|
||||
Event Granularity:
|
||||
- Too fine: `OrderFieldChanged` → Domain intent unclear
|
||||
- Appropriate: `ShippingAddressChanged` → Intent is clear
|
||||
- Too coarse: `OrderModified` → What changed is unclear
|
||||
|
||||
## Command Handlers
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Handler directly manipulates DB | REJECT |
|
||||
| Handler modifies multiple Aggregates | REJECT |
|
||||
| No command validation | REJECT |
|
||||
| Handler executes queries to make decisions | Needs review |
|
||||
|
||||
Good Command Handler:
|
||||
```
|
||||
1. Receive command
|
||||
2. Restore Aggregate from event store
|
||||
3. Apply command to Aggregate
|
||||
4. Save emitted events
|
||||
```
|
||||
|
||||
## Projection Design
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Projection issues commands | REJECT |
|
||||
| Projection references Write model | REJECT |
|
||||
| Single projection serves multiple use cases | Needs review |
|
||||
| Design that cannot be rebuilt | REJECT |
|
||||
|
||||
Good Projection:
|
||||
- Optimized for specific read use case
|
||||
- Idempotently reconstructible from events
|
||||
- Completely independent from Write model
|
||||
|
||||
## Query Side Design
|
||||
|
||||
Controller uses QueryGateway. Does not use Repository directly.
|
||||
|
||||
Types between layers:
|
||||
- `application/query/` - Query result types (e.g., `OrderDetail`)
|
||||
- `adapter/protocol/` - REST response types (e.g., `OrderDetailResponse`)
|
||||
- QueryHandler returns application layer types, Controller converts to adapter layer types
|
||||
|
||||
```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 - returns application layer type
|
||||
@QueryHandler
|
||||
fun handle(query: GetOrderDetailQuery): OrderDetail? {
|
||||
val entity = repository.findById(query.id) ?: return null
|
||||
return OrderDetail(...)
|
||||
}
|
||||
|
||||
// Controller - converts to adapter layer type
|
||||
@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))
|
||||
}
|
||||
```
|
||||
|
||||
Structure:
|
||||
```
|
||||
Controller (adapter) → QueryGateway → QueryHandler (application) → Repository
|
||||
↓ ↓
|
||||
Response.from(detail) OrderDetail
|
||||
```
|
||||
|
||||
## Eventual Consistency
|
||||
|
||||
| Situation | Response |
|
||||
|-----------|----------|
|
||||
| UI expects immediate updates | Redesign or polling/WebSocket |
|
||||
| Consistency delay exceeds tolerance | Reconsider architecture |
|
||||
| Compensating transactions undefined | Request failure scenario review |
|
||||
|
||||
## Saga vs EventHandler
|
||||
|
||||
Saga is used only for "operations between multiple aggregates where contention occurs".
|
||||
|
||||
Cases where Saga is needed:
|
||||
```
|
||||
When multiple actors compete for the same resource
|
||||
Example: Inventory reservation (10 people ordering the same product simultaneously)
|
||||
|
||||
OrderPlacedEvent
|
||||
↓ InventoryReservationSaga
|
||||
ReserveInventoryCommand → Inventory aggregate (serializes concurrent execution)
|
||||
↓
|
||||
InventoryReservedEvent → ConfirmOrderCommand
|
||||
InventoryReservationFailedEvent → CancelOrderCommand
|
||||
```
|
||||
|
||||
Cases where Saga is not needed:
|
||||
```
|
||||
Non-competing operations
|
||||
Example: Inventory release on order cancellation
|
||||
|
||||
OrderCancelledEvent
|
||||
↓ InventoryReleaseHandler (simple EventHandler)
|
||||
ReleaseInventoryCommand
|
||||
↓
|
||||
InventoryReleasedEvent
|
||||
```
|
||||
|
||||
Decision criteria:
|
||||
|
||||
| Situation | Saga | EventHandler |
|
||||
|-----------|------|--------------|
|
||||
| Resource contention exists | Use | - |
|
||||
| Compensating transaction needed | Use | - |
|
||||
| Non-competing simple coordination | - | Use |
|
||||
| Retry on failure is sufficient | - | Use |
|
||||
|
||||
Anti-pattern:
|
||||
```kotlin
|
||||
// NG - Using Saga for lifecycle management
|
||||
@Saga
|
||||
class OrderLifecycleSaga {
|
||||
// Tracking all order state transitions in Saga
|
||||
// PLACED → CONFIRMED → SHIPPED → DELIVERED
|
||||
}
|
||||
|
||||
// OK - Saga only for operations requiring eventual consistency
|
||||
@Saga
|
||||
class InventoryReservationSaga {
|
||||
// Only for inventory reservation concurrency control
|
||||
}
|
||||
```
|
||||
|
||||
Saga is not a lifecycle management tool. Create it per "operation" that requires eventual consistency.
|
||||
|
||||
## Exception vs Event (Failure Handling)
|
||||
|
||||
Failures not requiring audit use exceptions, failures requiring audit use events.
|
||||
|
||||
Exception approach (recommended: most cases):
|
||||
```kotlin
|
||||
// Domain model: Throws exception on validation failure
|
||||
fun reserveInventory(orderId: String, quantity: Int): InventoryReservedEvent {
|
||||
if (availableQuantity < quantity) {
|
||||
throw InsufficientInventoryException("Insufficient inventory")
|
||||
}
|
||||
return InventoryReservedEvent(productId, orderId, quantity)
|
||||
}
|
||||
|
||||
// Saga: Catch with exceptionally and perform compensating action
|
||||
commandGateway.send<Any>(command)
|
||||
.exceptionally { ex ->
|
||||
commandGateway.send<Any>(CancelOrderCommand(
|
||||
orderId = orderId,
|
||||
reason = ex.cause?.message ?: "Inventory reservation failed"
|
||||
))
|
||||
null
|
||||
}
|
||||
```
|
||||
|
||||
Event approach (rare cases):
|
||||
```kotlin
|
||||
// Only when audit is required
|
||||
data class PaymentFailedEvent(
|
||||
val paymentId: String,
|
||||
val reason: String,
|
||||
val attemptedAmount: Money
|
||||
) : PaymentEvent
|
||||
```
|
||||
|
||||
Decision criteria:
|
||||
|
||||
| Question | Exception | Event |
|
||||
|----------|-----------|-------|
|
||||
| Need to check this failure later? | No | Yes |
|
||||
| Required by regulations/compliance? | No | Yes |
|
||||
| Only Saga cares about the failure? | Yes | No |
|
||||
| Is there value in keeping it in Event Store? | No | Yes |
|
||||
|
||||
Default is exception approach. Consider events only when audit requirements exist.
|
||||
|
||||
## 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 |
|
||||
| Event type branching proliferating | Separate event handlers → REJECT |
|
||||
| Complex state branching in Aggregate | Consider State Pattern |
|
||||
|
||||
**Abstraction level mismatch detection:**
|
||||
|
||||
| Pattern | Problem | Fix |
|
||||
|---------|---------|-----|
|
||||
| DB operation details in CommandHandler | Responsibility violation | Separate to Repository layer |
|
||||
| Business logic in EventHandler | Responsibility violation | Extract to domain service |
|
||||
| Persistence in Aggregate | Layer violation | Change to EventStore route |
|
||||
| Calculation logic in Projection | Hard to maintain | Extract to dedicated service |
|
||||
|
||||
Good abstraction examples:
|
||||
|
||||
```kotlin
|
||||
// Event type branching proliferation (NG)
|
||||
@EventHandler
|
||||
fun on(event: DomainEvent) {
|
||||
when (event) {
|
||||
is OrderPlacedEvent -> handleOrderPlaced(event)
|
||||
is OrderConfirmedEvent -> handleOrderConfirmed(event)
|
||||
is OrderShippedEvent -> handleOrderShipped(event)
|
||||
// ...keeps growing
|
||||
}
|
||||
}
|
||||
|
||||
// Separate handlers per event (OK)
|
||||
@EventHandler
|
||||
fun on(event: OrderPlacedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderConfirmedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderShippedEvent) { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// Complex state branching (NG)
|
||||
fun process(command: ProcessCommand) {
|
||||
when (status) {
|
||||
PENDING -> if (command.type == "approve") { ... } else if (command.type == "reject") { ... }
|
||||
APPROVED -> if (command.type == "ship") { ... }
|
||||
// ...gets complex
|
||||
}
|
||||
}
|
||||
|
||||
// Abstracted with State Pattern (OK)
|
||||
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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Anti-pattern Detection
|
||||
|
||||
REJECT if found:
|
||||
|
||||
| Anti-pattern | Problem |
|
||||
|--------------|---------|
|
||||
| CRUD Disguise | Just splitting CRUD into Command/Query |
|
||||
| Anemic Domain Model | Aggregate is just a data structure |
|
||||
| Event Soup | Meaningless events proliferate |
|
||||
| Temporal Coupling | Implicit dependency on event order |
|
||||
| Missing Events | Important domain events are missing |
|
||||
| God Aggregate | All responsibilities in one Aggregate |
|
||||
|
||||
## Test Strategy
|
||||
|
||||
Separate test strategies by layer.
|
||||
|
||||
Test Pyramid:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ E2E Test │ ← Few: Overall flow confirmation
|
||||
├─────────────┤
|
||||
│ Integration │ ← Command→Event→Projection→Query coordination
|
||||
├─────────────┤
|
||||
│ Unit Test │ ← Many: Each layer tested independently
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
Command side (Aggregate):
|
||||
```kotlin
|
||||
// Using AggregateTestFixture
|
||||
@Test
|
||||
fun `confirm command emits event`() {
|
||||
fixture
|
||||
.given(OrderPlacedEvent(...))
|
||||
.`when`(ConfirmOrderCommand(orderId, confirmedBy))
|
||||
.expectSuccessfulHandlerExecution()
|
||||
.expectEvents(OrderConfirmedEvent(...))
|
||||
}
|
||||
```
|
||||
|
||||
Query side:
|
||||
```kotlin
|
||||
// Direct Read Model setup + QueryGateway
|
||||
@Test
|
||||
fun `can get order details`() {
|
||||
// Given: Setup Read Model directly
|
||||
orderRepository.save(OrderEntity(...))
|
||||
|
||||
// When: Execute query via QueryGateway
|
||||
val detail = queryGateway.query(GetOrderDetailQuery(orderId), ...).join()
|
||||
|
||||
// Then
|
||||
assertEquals(expectedDetail, detail)
|
||||
}
|
||||
```
|
||||
|
||||
Checklist:
|
||||
|
||||
| Aspect | Judgment |
|
||||
|--------|----------|
|
||||
| Aggregate tests verify events not state | Required |
|
||||
| Query side tests don't create data via Command | Recommended |
|
||||
| Integration tests consider Axon async processing | Required |
|
||||
|
||||
## Infrastructure Layer
|
||||
|
||||
Check:
|
||||
- Is event store choice appropriate?
|
||||
- Does messaging infrastructure meet requirements?
|
||||
- Is snapshot strategy defined?
|
||||
- Is event serialization format appropriate?
|
||||
497
resources/global/en/knowledge/frontend.md
Normal file
497
resources/global/en/knowledge/frontend.md
Normal file
@ -0,0 +1,497 @@
|
||||
# Frontend Knowledge
|
||||
|
||||
## Component Design
|
||||
|
||||
Do not write everything in one file. Always split components.
|
||||
|
||||
Required splits:
|
||||
- Has its own state → Must split
|
||||
- JSX over 50 lines → Split
|
||||
- Reusable → Split
|
||||
- Multiple responsibilities → Split
|
||||
- Independent section within page → Split
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Component over 200 lines | Consider splitting |
|
||||
| Component over 300 lines | REJECT |
|
||||
| Display and logic mixed | Consider separation |
|
||||
| Props drilling (3+ levels) | Consider state management |
|
||||
| Component with multiple responsibilities | REJECT |
|
||||
|
||||
Good Component:
|
||||
- Single responsibility: Does one thing well
|
||||
- Self-contained: Dependencies are clear
|
||||
- Testable: Side effects are isolated
|
||||
|
||||
Component Classification:
|
||||
|
||||
| Type | Responsibility | Example |
|
||||
|------|----------------|---------|
|
||||
| Container | Data fetching, state management | `UserListContainer` |
|
||||
| Presentational | Display only | `UserCard` |
|
||||
| Layout | Arrangement, structure | `PageLayout`, `Grid` |
|
||||
| Utility | Common functionality | `ErrorBoundary`, `Portal` |
|
||||
|
||||
Directory Structure:
|
||||
```
|
||||
features/{feature-name}/
|
||||
├── components/
|
||||
│ ├── {feature}-view.tsx # Main view (composes children)
|
||||
│ ├── {sub-component}.tsx # Sub-components
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
Child components do not modify their own state. They bubble events to parent, and parent manipulates state.
|
||||
|
||||
```tsx
|
||||
// ❌ Child modifies its own state
|
||||
const ChildBad = ({ initialValue }: { initialValue: string }) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
return <input value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// ✅ Parent manages state, child notifies via callback
|
||||
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} />
|
||||
}
|
||||
```
|
||||
|
||||
Exception (OK for child to have local state):
|
||||
- UI-only temporary state (hover, focus, animation)
|
||||
- Completely local state that doesn't need to be communicated to parent
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Unnecessary global state | Consider localizing |
|
||||
| Same state managed in multiple places | Needs normalization |
|
||||
| State changes from child to parent (reverse data flow) | REJECT |
|
||||
| API response stored as-is in state | Consider normalization |
|
||||
| Inappropriate useEffect dependencies | REJECT |
|
||||
|
||||
State Placement Guidelines:
|
||||
|
||||
| State Nature | Recommended Placement |
|
||||
|--------------|----------------------|
|
||||
| Temporary UI state (modal open/close, etc.) | Local (useState) |
|
||||
| Form input values | Local or form library |
|
||||
| Shared across multiple components | Context or state management library |
|
||||
| Server data cache | Data fetching library (TanStack Query, etc.) |
|
||||
|
||||
## Data Fetching
|
||||
|
||||
API calls are made in root (View) components and passed to children via props.
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Fetch at root, pass to children
|
||||
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 - Child fetches its own data
|
||||
const OrderSummary = ({ orderId }) => {
|
||||
const { data: order } = useGetOrder(orderId)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
When UI state changes affect parameters (week switching, filters, etc.):
|
||||
|
||||
Manage state at View level and pass callbacks to components.
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - State managed at View level
|
||||
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 - Component manages state + data fetching
|
||||
const WeeklyCalendar = ({ facilityId }) => {
|
||||
const [currentWeek, setCurrentWeek] = useState(...)
|
||||
const { data } = useListSchedules({ facilityId, from, to })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
Exceptions (component-level fetching allowed):
|
||||
|
||||
| Case | Reason |
|
||||
|------|--------|
|
||||
| Infinite scroll | Depends on scroll position (internal UI state) |
|
||||
| Search autocomplete | Real-time search based on input value |
|
||||
| Independent widget | Notification badge, weather, etc. Completely unrelated to parent data |
|
||||
| Real-time updates | WebSocket/Polling auto-updates |
|
||||
| Modal detail fetch | Fetch additional data only when opened |
|
||||
|
||||
Decision criteria: "Is there no point in parent managing this / Does it not affect parent?"
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Direct fetch in component | Separate to Container layer |
|
||||
| No error handling | REJECT |
|
||||
| Loading state not handled | REJECT |
|
||||
| No cancellation handling | Warning |
|
||||
| N+1 query-like fetching | REJECT |
|
||||
|
||||
## Shared Components and Abstraction
|
||||
|
||||
Common UI patterns should be shared components. Copy-paste of inline styles is prohibited.
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Copy-pasted inline styles
|
||||
<button className="p-2 text-[var(--text-secondary)] hover:...">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
// ✅ CORRECT - Use shared component
|
||||
<IconButton onClick={onClose} aria-label="Close">
|
||||
<X className="w-5 h-5" />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
Patterns to make shared components:
|
||||
- Icon buttons (close, edit, delete, etc.)
|
||||
- Loading/error displays
|
||||
- Status badges
|
||||
- Tab switching
|
||||
- Label + value display (detail screens)
|
||||
- Search input
|
||||
- Color legends
|
||||
|
||||
Avoid over-generalization:
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Forcing stepper variant into IconButton
|
||||
export const iconButtonVariants = cva('...', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: '...',
|
||||
outlined: '...', // ← Stepper-specific, not used elsewhere
|
||||
},
|
||||
size: {
|
||||
medium: 'p-2',
|
||||
stepper: 'w-8 h-8', // ← Only used with outlined
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// ✅ CORRECT - Purpose-specific component
|
||||
export function StepperButton(props) {
|
||||
return (
|
||||
<button className="w-8 h-8 rounded-full border ..." {...props}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Signs to make separate components:
|
||||
- Implicit constraints like "this variant is always with this size"
|
||||
- Added variant is clearly different from original component's purpose
|
||||
- Props specification becomes complex on the usage side
|
||||
|
||||
## Abstraction Level Evaluation
|
||||
|
||||
**Conditional branch bloat detection:**
|
||||
|
||||
| Pattern | Judgment |
|
||||
|---------|----------|
|
||||
| Same conditional in 3+ places | Extract to shared component → **REJECT** |
|
||||
| Props-based branching with 5+ types | Consider component split |
|
||||
| Nested ternaries in render | Early return or component separation → **REJECT** |
|
||||
| Type-based render branching | Consider polymorphic components |
|
||||
|
||||
**Abstraction level mismatch detection:**
|
||||
|
||||
| Pattern | Problem | Fix |
|
||||
|---------|---------|-----|
|
||||
| Data fetching logic mixed in JSX | Hard to read | Extract to custom hook |
|
||||
| Business logic mixed in component | Responsibility violation | Separate to hooks/utils |
|
||||
| Style calculation logic scattered | Hard to maintain | Extract to utility function |
|
||||
| Same transformation in multiple places | DRY violation | Extract to common function |
|
||||
|
||||
Good abstraction examples:
|
||||
|
||||
```tsx
|
||||
// ❌ Conditional bloat
|
||||
function UserBadge({ user }) {
|
||||
if (user.role === 'admin') {
|
||||
return <span className="bg-red-500">Admin</span>
|
||||
} else if (user.role === 'moderator') {
|
||||
return <span className="bg-yellow-500">Moderator</span>
|
||||
} else if (user.role === 'premium') {
|
||||
return <span className="bg-purple-500">Premium</span>
|
||||
} else {
|
||||
return <span className="bg-gray-500">User</span>
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Abstracted with Map
|
||||
const ROLE_CONFIG = {
|
||||
admin: { label: 'Admin', className: 'bg-red-500' },
|
||||
moderator: { label: 'Moderator', className: 'bg-yellow-500' },
|
||||
premium: { label: 'Premium', className: 'bg-purple-500' },
|
||||
default: { label: 'User', 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
|
||||
// ❌ Mixed abstraction levels
|
||||
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()} USD</div>
|
||||
))
|
||||
}
|
||||
|
||||
// ✅ Aligned abstraction levels
|
||||
function OrderList() {
|
||||
const { data: orders } = useOrders() // Hide data fetching
|
||||
|
||||
return orders.map(order => (
|
||||
<OrderItem key={order.id} order={order} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
## Frontend and Backend Separation of Concerns
|
||||
|
||||
### Display Format Responsibility
|
||||
|
||||
Backend returns "data", frontend converts to "display format".
|
||||
|
||||
```tsx
|
||||
// ✅ Frontend: Convert to display format
|
||||
export function formatPrice(amount: number): string {
|
||||
return `$${amount.toLocaleString()}`
|
||||
}
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return format(date, 'MMM d, yyyy')
|
||||
}
|
||||
```
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Backend returns display strings | Suggest design review |
|
||||
| Same format logic copy-pasted | Unify to utility function |
|
||||
| Inline formatting in component | Extract to function |
|
||||
|
||||
### Domain Logic Placement (Smart UI Elimination)
|
||||
|
||||
Domain logic (business rules) belongs in the backend. Frontend only displays and edits state.
|
||||
|
||||
What is domain logic:
|
||||
- Aggregate business rules (stock validation, price calculation, status transitions)
|
||||
- Business constraint validation
|
||||
- Invariant enforcement
|
||||
|
||||
Frontend responsibilities:
|
||||
- Display state received from server
|
||||
- Collect user input and send commands to backend
|
||||
- Manage UI-only temporary state (focus, hover, modal open/close)
|
||||
- Display format conversion (formatting, sorting, filtering)
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Price calculation/stock validation in frontend | Move to backend → **REJECT** |
|
||||
| Status transition rules in frontend | Move to backend → **REJECT** |
|
||||
| Business validation in frontend | Move to backend → **REJECT** |
|
||||
| Recalculating server-computable values in frontend | Redundant → **REJECT** |
|
||||
|
||||
Good vs Bad Examples:
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Business rules in frontend
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
const totalPrice = order.items.reduce((sum, item) =>
|
||||
sum + item.price * item.quantity, 0
|
||||
)
|
||||
const canCheckout = totalPrice >= 100 && order.items.every(i => i.stock > 0)
|
||||
|
||||
return <button disabled={!canCheckout}>Checkout</button>
|
||||
}
|
||||
|
||||
// ✅ GOOD - Display state received from server
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
// totalPrice, canCheckout are received from server
|
||||
return (
|
||||
<>
|
||||
<div>{formatPrice(order.totalPrice)}</div>
|
||||
<button disabled={!order.canCheckout}>Checkout</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Status transition logic in frontend
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
const canStart = task.status === 'pending' && task.assignee !== null
|
||||
const canComplete = task.status === 'in_progress' && /* complex conditions... */
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>Start</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>Complete</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ GOOD - Server returns allowed actions
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
// task.allowedActions = ['start', 'cancel'], etc., calculated by server
|
||||
const canStart = task.allowedActions.includes('start')
|
||||
const canComplete = task.allowedActions.includes('complete')
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>Start</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>Complete</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
Exceptions (OK to have logic in frontend):
|
||||
|
||||
| Case | Reason |
|
||||
|------|--------|
|
||||
| UI-only validation | UX feedback like "required field", "max length" (must also validate on server) |
|
||||
| Client-side filter/sort | Changing display order of lists received from server |
|
||||
| Display condition branching | UI control like "show details if logged in" |
|
||||
| Real-time feedback | Preview display during input |
|
||||
|
||||
Decision criteria: "Would the business break if this calculation differs from the server?"
|
||||
- YES → Place in backend (domain logic)
|
||||
- NO → OK in frontend (display logic)
|
||||
|
||||
## Performance
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Unnecessary re-renders | Needs optimization |
|
||||
| Large lists without virtualization | Warning |
|
||||
| Unoptimized images | Warning |
|
||||
| Unused code in bundle | Check tree-shaking |
|
||||
| Excessive memoization | Verify necessity |
|
||||
|
||||
Optimization Checklist:
|
||||
- Are `React.memo` / `useMemo` / `useCallback` appropriate?
|
||||
- Are large lists using virtual scroll?
|
||||
- Is Code Splitting appropriate?
|
||||
- Are images lazy loaded?
|
||||
|
||||
Anti-patterns:
|
||||
|
||||
```tsx
|
||||
// ❌ New object every render
|
||||
<Child style={{ color: 'red' }} />
|
||||
|
||||
// ✅ Constant or useMemo
|
||||
const style = useMemo(() => ({ color: 'red' }), []);
|
||||
<Child style={style} />
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Interactive elements without keyboard support | REJECT |
|
||||
| Images without alt attribute | REJECT |
|
||||
| Form elements without labels | REJECT |
|
||||
| Information conveyed by color only | REJECT |
|
||||
| Missing focus management (modals, etc.) | REJECT |
|
||||
|
||||
Checklist:
|
||||
- Using semantic HTML?
|
||||
- Are ARIA attributes appropriate (not excessive)?
|
||||
- Is keyboard navigation possible?
|
||||
- Does it make sense with a screen reader?
|
||||
- Is color contrast sufficient?
|
||||
|
||||
## TypeScript/Type Safety
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Use of `any` type | REJECT |
|
||||
| Excessive type assertions (as) | Needs review |
|
||||
| No Props type definition | REJECT |
|
||||
| Inappropriate event handler types | Needs fix |
|
||||
|
||||
## Frontend Security
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| dangerouslySetInnerHTML usage | Check XSS risk |
|
||||
| Unsanitized user input | REJECT |
|
||||
| Sensitive data stored in frontend | REJECT |
|
||||
| CSRF token not used | Needs verification |
|
||||
|
||||
## Testability
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| No data-testid, etc. | Warning |
|
||||
| Structure difficult to test | Consider separation |
|
||||
| Business logic embedded in UI | REJECT |
|
||||
|
||||
## Anti-pattern Detection
|
||||
|
||||
REJECT if found:
|
||||
|
||||
| Anti-pattern | Problem |
|
||||
|--------------|---------|
|
||||
| God Component | All features concentrated in one component |
|
||||
| Prop Drilling | Deep props bucket brigade |
|
||||
| Inline Styles abuse | Maintainability degradation |
|
||||
| useEffect hell | Dependencies too complex |
|
||||
| Premature Optimization | Unnecessary memoization |
|
||||
| Magic Strings | Hardcoded strings |
|
||||
| Hidden Dependencies | Child components with hidden API calls |
|
||||
| Over-generalization | Components forced to be generic |
|
||||
164
resources/global/en/knowledge/security.md
Normal file
164
resources/global/en/knowledge/security.md
Normal file
@ -0,0 +1,164 @@
|
||||
# Security Knowledge
|
||||
|
||||
## AI-Generated Code Security Issues
|
||||
|
||||
AI-generated code has unique vulnerability patterns.
|
||||
|
||||
| Pattern | Risk | Example |
|
||||
|---------|------|---------|
|
||||
| Plausible but dangerous defaults | High | `cors: { origin: '*' }` looks fine but is dangerous |
|
||||
| Outdated security practices | Medium | Using deprecated encryption, old auth patterns |
|
||||
| Incomplete validation | High | Validates format but not business rules |
|
||||
| Over-trusting inputs | Critical | Assumes internal APIs are always safe |
|
||||
| Copy-paste vulnerabilities | High | Same dangerous pattern repeated in multiple files |
|
||||
|
||||
Require extra scrutiny:
|
||||
- Auth/authorization logic (AI tends to miss edge cases)
|
||||
- Input validation (AI may check syntax but miss semantics)
|
||||
- Error messages (AI may expose internal details)
|
||||
- Config files (AI may use dangerous defaults from training data)
|
||||
|
||||
## Injection Attacks
|
||||
|
||||
**SQL Injection:**
|
||||
|
||||
- SQL construction via string concatenation → REJECT
|
||||
- Not using parameterized queries → REJECT
|
||||
- Unsanitized input in ORM raw queries → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
db.query(`SELECT * FROM users WHERE id = ${userId}`)
|
||||
|
||||
// OK
|
||||
db.query('SELECT * FROM users WHERE id = ?', [userId])
|
||||
```
|
||||
|
||||
**Command Injection:**
|
||||
|
||||
- Unvalidated input in `exec()`, `spawn()` → REJECT
|
||||
- Insufficient escaping in shell command construction → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
exec(`ls ${userInput}`)
|
||||
|
||||
// OK
|
||||
execFile('ls', [sanitizedInput])
|
||||
```
|
||||
|
||||
**XSS (Cross-Site Scripting):**
|
||||
|
||||
- Unescaped output to HTML/JS → REJECT
|
||||
- Improper use of `innerHTML`, `dangerouslySetInnerHTML` → REJECT
|
||||
- Direct embedding of URL parameters → REJECT
|
||||
|
||||
## Authentication & Authorization
|
||||
|
||||
**Authentication issues:**
|
||||
|
||||
- Hardcoded credentials → Immediate REJECT
|
||||
- Plaintext password storage → Immediate REJECT
|
||||
- Weak hash algorithms (MD5, SHA1) → REJECT
|
||||
- Improper session token management → REJECT
|
||||
|
||||
**Authorization issues:**
|
||||
|
||||
- Missing permission checks → REJECT
|
||||
- IDOR (Insecure Direct Object Reference) → REJECT
|
||||
- Privilege escalation possibility → REJECT
|
||||
|
||||
```typescript
|
||||
// NG - No permission check
|
||||
app.get('/user/:id', (req, res) => {
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
|
||||
// OK
|
||||
app.get('/user/:id', authorize('read:user'), (req, res) => {
|
||||
if (req.user.id !== req.params.id && !req.user.isAdmin) {
|
||||
return res.status(403).send('Forbidden')
|
||||
}
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
```
|
||||
|
||||
## Data Protection
|
||||
|
||||
**Sensitive information exposure:**
|
||||
|
||||
- Hardcoded API keys, secrets → Immediate REJECT
|
||||
- Sensitive info in logs → REJECT
|
||||
- Internal info exposure in error messages → REJECT
|
||||
- Committed `.env` files → REJECT
|
||||
|
||||
**Data validation:**
|
||||
|
||||
- Unvalidated input values → REJECT
|
||||
- Missing type checks → REJECT
|
||||
- No size limits set → REJECT
|
||||
|
||||
## Cryptography
|
||||
|
||||
- Use of weak crypto algorithms → REJECT
|
||||
- Fixed IV/Nonce usage → REJECT
|
||||
- Hardcoded encryption keys → Immediate REJECT
|
||||
- No HTTPS (production) → REJECT
|
||||
|
||||
## File Operations
|
||||
|
||||
**Path Traversal:**
|
||||
|
||||
- File paths containing user input → REJECT
|
||||
- Insufficient `../` sanitization → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
const filePath = path.join(baseDir, userInput)
|
||||
fs.readFile(filePath)
|
||||
|
||||
// OK
|
||||
const safePath = path.resolve(baseDir, userInput)
|
||||
if (!safePath.startsWith(path.resolve(baseDir))) {
|
||||
throw new Error('Invalid path')
|
||||
}
|
||||
```
|
||||
|
||||
**File Upload:**
|
||||
|
||||
- No file type validation → REJECT
|
||||
- No file size limits → REJECT
|
||||
- Allowing executable file uploads → REJECT
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Packages with known vulnerabilities → REJECT
|
||||
- Unmaintained packages → Warning
|
||||
- Unnecessary dependencies → Warning
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Stack trace exposure in production → REJECT
|
||||
- Detailed error message exposure → REJECT
|
||||
- Swallowing security events → REJECT
|
||||
|
||||
## Rate Limiting & DoS Protection
|
||||
|
||||
- No rate limiting (auth endpoints) → Warning
|
||||
- Resource exhaustion attack possibility → Warning
|
||||
- Infinite loop possibility → REJECT
|
||||
|
||||
## OWASP Top 10 Checklist
|
||||
|
||||
| Category | Check Items |
|
||||
|----------|-------------|
|
||||
| A01 Broken Access Control | Authorization checks, CORS config |
|
||||
| A02 Cryptographic Failures | Encryption, sensitive data protection |
|
||||
| A03 Injection | SQL, Command, XSS |
|
||||
| A04 Insecure Design | Security design patterns |
|
||||
| A05 Security Misconfiguration | Default settings, unnecessary features |
|
||||
| A06 Vulnerable Components | Dependency vulnerabilities |
|
||||
| A07 Auth Failures | Authentication mechanisms |
|
||||
| A08 Software Integrity | Code signing, CI/CD |
|
||||
| A09 Logging Failures | Security logging |
|
||||
| A10 SSRF | Server-side requests |
|
||||
@ -40,434 +40,6 @@ Code is read far more often than it is written. Poorly structured code destroys
|
||||
- Give vague feedback ("clean this up" is prohibited)
|
||||
- Review AI-specific issues (AI Reviewer's job)
|
||||
|
||||
## Review Target Distinction
|
||||
|
||||
**Important**: Distinguish between source files and generated files.
|
||||
|
||||
| Type | Location | Review Target |
|
||||
|------|----------|---------------|
|
||||
| Generated reports | `.takt/reports/` | Not a review target |
|
||||
| Reports in git diff | `.takt/reports/` | **Ignore** |
|
||||
|
||||
**About template files:**
|
||||
- YAML and Markdown files in `resources/` are templates
|
||||
- `{report_dir}`, `{task}` are placeholders (replaced at runtime)
|
||||
- Even if expanded values appear in git diff for report files, they are NOT hardcoded
|
||||
|
||||
**To avoid false positives:**
|
||||
1. Before flagging "hardcoded values", **verify if the file is source or report**
|
||||
2. Files under `.takt/reports/` are generated during piece execution - not review targets
|
||||
3. Ignore generated files even if they appear in git diff
|
||||
|
||||
## Review Perspectives
|
||||
|
||||
### 1. Structure & Design
|
||||
|
||||
**File Organization:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Single file > 200 lines | Consider splitting |
|
||||
| Single file > 300 lines | REJECT |
|
||||
| Single file with multiple responsibilities | REJECT |
|
||||
| Unrelated code coexisting | REJECT |
|
||||
|
||||
**Module Structure:**
|
||||
- High cohesion: Related functionality grouped together
|
||||
- Low coupling: Minimal inter-module dependencies
|
||||
- No circular dependencies
|
||||
- Appropriate directory hierarchy
|
||||
|
||||
**Function Design:**
|
||||
- One responsibility per function
|
||||
- Consider splitting functions over 30 lines
|
||||
- Side effects clearly defined
|
||||
|
||||
**Layer Design:**
|
||||
- Dependency direction: Upper layers -> Lower layers (reverse prohibited)
|
||||
- Controller -> Service -> Repository flow maintained
|
||||
- 1 interface = 1 responsibility (no giant Service classes)
|
||||
|
||||
**Directory Structure:**
|
||||
|
||||
Structure pattern selection:
|
||||
|
||||
| Pattern | Use Case | Example |
|
||||
|---------|----------|---------|
|
||||
| Layered | Small scale, CRUD-centric | `controllers/`, `services/`, `repositories/` |
|
||||
| Vertical Slice | Medium-large scale, high feature independence | `features/auth/`, `features/order/` |
|
||||
| Hybrid | Common foundation + feature modules | `core/` + `features/` |
|
||||
|
||||
Vertical Slice Architecture (organizing code by feature):
|
||||
|
||||
```
|
||||
src/
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── LoginCommand.ts
|
||||
│ │ ├── LoginHandler.ts
|
||||
│ │ ├── AuthRepository.ts
|
||||
│ │ └── auth.test.ts
|
||||
│ └── order/
|
||||
│ ├── CreateOrderCommand.ts
|
||||
│ ├── CreateOrderHandler.ts
|
||||
│ └── ...
|
||||
└── shared/ # Shared across features
|
||||
├── database/
|
||||
└── middleware/
|
||||
```
|
||||
|
||||
Vertical Slice criteria:
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Single feature spans 3+ layers | Consider slicing |
|
||||
| Minimal inter-feature dependencies | Recommend slicing |
|
||||
| Over 50% shared processing | Keep layered |
|
||||
| Team organized by features | Slicing required |
|
||||
|
||||
Prohibited patterns:
|
||||
|
||||
| Pattern | Problem |
|
||||
|---------|---------|
|
||||
| Bloated `utils/` | Becomes graveyard of unclear responsibilities |
|
||||
| Lazy placement in `common/` | Dependencies become unclear |
|
||||
| Excessive nesting (4+ levels) | Navigation difficulty |
|
||||
| Mixed features and layers | `features/services/` prohibited |
|
||||
|
||||
**Separation of Concerns:**
|
||||
- Read and write responsibilities separated
|
||||
- Data fetching at root (View/Controller), passed to children
|
||||
- Error handling centralized (no try-catch scattered everywhere)
|
||||
- Business logic not leaking into Controller/View
|
||||
|
||||
### 2. Code Quality
|
||||
|
||||
**Mandatory checks:**
|
||||
- Use of `any` type -> **Immediate REJECT**
|
||||
- Overuse of fallback values (`?? 'unknown'`) -> **REJECT** (see examples below)
|
||||
- Explanatory comments (What/How comments) -> **REJECT** (see examples below)
|
||||
- Unused code ("just in case" code) -> **REJECT** (see examples below)
|
||||
- Direct state mutation (not immutable) -> **REJECT** (see examples below)
|
||||
|
||||
**Design principles:**
|
||||
- Simple > Easy: Readability prioritized
|
||||
- DRY: No more than 3 duplications
|
||||
- YAGNI: Only what's needed now
|
||||
- Fail Fast: Errors detected and reported early
|
||||
- Idiomatic: Follows language/framework conventions
|
||||
|
||||
**Explanatory Comment (What/How) Detection Criteria:**
|
||||
|
||||
Comments must only explain design decisions not evident from code (Why), never restate what the code does (What/How). If the code is clear enough, no comment is needed at all.
|
||||
|
||||
| Judgment | Criteria |
|
||||
|----------|----------|
|
||||
| **REJECT** | Restates code behavior in natural language |
|
||||
| **REJECT** | Repeats what is already obvious from function/variable names |
|
||||
| **REJECT** | JSDoc that only paraphrases the function name without adding information |
|
||||
| OK | Explains why a particular implementation was chosen |
|
||||
| OK | Explains the reason behind seemingly unusual behavior |
|
||||
| Best | No comment needed — the code itself communicates intent |
|
||||
|
||||
```typescript
|
||||
// ❌ REJECT - Restates code (What)
|
||||
// If interrupted, abort immediately
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// ❌ REJECT - Restates the loop
|
||||
// Check transitions in order
|
||||
for (const transition of step.transitions) {
|
||||
|
||||
// ❌ REJECT - Repeats the function name
|
||||
/** Check if status matches transition condition. */
|
||||
export function matchesCondition(status: Status, condition: TransitionCondition): boolean {
|
||||
|
||||
// ✅ OK - Design decision (Why)
|
||||
// User interruption takes priority over piece-defined transitions
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// ✅ OK - Reason behind seemingly odd behavior
|
||||
// stay can cause loops, but is only used when explicitly specified by the user
|
||||
return step.name;
|
||||
|
||||
// ✅ Best - No comment needed. Code is self-evident
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
```
|
||||
|
||||
**Direct State Mutation Detection Criteria:**
|
||||
|
||||
Directly mutating objects or arrays makes changes hard to track and causes unexpected side effects. Always use spread operators or immutable operations to return new objects.
|
||||
|
||||
```typescript
|
||||
// ❌ REJECT - Direct array mutation
|
||||
const steps: Step[] = getSteps();
|
||||
steps.push(newStep); // Mutates original array
|
||||
steps.splice(index, 1); // Mutates original array
|
||||
steps[0].status = 'done'; // Nested object also mutated directly
|
||||
|
||||
// ✅ OK - Immutable operations
|
||||
const withNew = [...steps, newStep];
|
||||
const without = steps.filter((_, i) => i !== index);
|
||||
const updated = steps.map((s, i) =>
|
||||
i === 0 ? { ...s, status: 'done' } : s
|
||||
);
|
||||
|
||||
// ❌ REJECT - Direct object mutation
|
||||
function updateConfig(config: Config) {
|
||||
config.logLevel = 'debug'; // Mutates argument directly
|
||||
config.steps.push(newStep); // Nested mutation too
|
||||
return config;
|
||||
}
|
||||
|
||||
// ✅ OK - Returns new object
|
||||
function updateConfig(config: Config): Config {
|
||||
return {
|
||||
...config,
|
||||
logLevel: 'debug',
|
||||
steps: [...config.steps, newStep],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Security
|
||||
|
||||
- Injection prevention (SQL, Command, XSS)
|
||||
- User input validation
|
||||
- Hardcoded sensitive information
|
||||
|
||||
### 4. Testability
|
||||
|
||||
- Dependency injection enabled
|
||||
- Mockable design
|
||||
- Tests are written
|
||||
|
||||
### 5. Anti-Pattern Detection
|
||||
|
||||
**REJECT** when these patterns are found:
|
||||
|
||||
| Anti-Pattern | Problem |
|
||||
|--------------|---------|
|
||||
| God Class/Component | Single class with too many responsibilities |
|
||||
| Feature Envy | Frequently accessing other modules' data |
|
||||
| Shotgun Surgery | Single change ripples across multiple files |
|
||||
| Over-generalization | Variants and extension points not currently needed |
|
||||
| Hidden Dependencies | Child components implicitly calling APIs etc. |
|
||||
| Non-idiomatic | Custom implementation ignoring language/FW conventions |
|
||||
| Logically dead defensive code | Guards for conditions already guaranteed by all callers |
|
||||
|
||||
**Logically dead defensive code:**
|
||||
|
||||
Call chain verification applies not only to "missing wiring" but also to the reverse — **unnecessary guards for conditions that callers already guarantee**.
|
||||
|
||||
| Pattern | Problem | Detection |
|
||||
|---------|---------|-----------|
|
||||
| TTY check when all callers require TTY | Unreachable branch remains | grep all callers' preconditions |
|
||||
| Null guard when callers already check null | Redundant defense | Trace caller constraints |
|
||||
| Runtime type check when TypeScript types constrain | Not trusting type safety | Check TypeScript type constraints |
|
||||
|
||||
**Verification:**
|
||||
1. When you find a defensive branch (TTY check, null guard, etc.), grep all callers
|
||||
2. If all callers already guarantee the condition, the guard is unnecessary → **REJECT**
|
||||
3. If some callers don't guarantee it, keep the guard
|
||||
|
||||
### 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. Workaround Detection
|
||||
|
||||
**Don't overlook compromises made to "just make it work."**
|
||||
|
||||
| Pattern | Example |
|
||||
|---------|---------|
|
||||
| Unnecessary package additions | Mystery libraries added just to make things work |
|
||||
| Test deletion/skipping | `@Disabled`, `.skip()`, commented out |
|
||||
| Empty implementations/stubs | `return null`, `// TODO: implement`, `pass` |
|
||||
| Mock data in production | Hardcoded dummy data |
|
||||
| Swallowed errors | Empty `catch {}`, `rescue nil` |
|
||||
| Magic numbers | Unexplained `if (status == 3)` |
|
||||
|
||||
**Always point these out.** Temporary fixes become permanent.
|
||||
|
||||
### 8. Spec Compliance Verification
|
||||
|
||||
**Verify that changes comply with the project's documented specifications.**
|
||||
|
||||
**Verification targets:**
|
||||
|
||||
| Target | What to Check |
|
||||
|--------|---------------|
|
||||
| CLAUDE.md / README.md | Conforms to schema definitions, design principles, constraints |
|
||||
| Type definitions / Zod schemas | New fields reflected in schemas |
|
||||
| YAML/JSON config files | Follows documented format |
|
||||
|
||||
**Specific checks:**
|
||||
|
||||
1. When config files (YAML, etc.) are modified or added:
|
||||
- Cross-reference with schema definitions in CLAUDE.md, etc.
|
||||
- No ignored or invalid fields present
|
||||
- No required fields missing
|
||||
|
||||
2. When type definitions or interfaces are modified:
|
||||
- Documentation schema descriptions are updated
|
||||
- Existing config files are compatible with new schema
|
||||
|
||||
3. When piece definitions are modified:
|
||||
- Correct fields used for step type (normal vs. parallel)
|
||||
- No unnecessary fields remaining (e.g., `next` on parallel sub-steps)
|
||||
|
||||
**REJECT when these patterns are found:**
|
||||
|
||||
| Pattern | Problem |
|
||||
|---------|---------|
|
||||
| Fields not in the spec | Ignored or unexpected behavior |
|
||||
| Invalid values per spec | Runtime error or silently ignored |
|
||||
| Violation of documented constraints | Against design intent |
|
||||
| Step type / field mismatch | Sign of copy-paste error |
|
||||
|
||||
### 9. Quality Attributes
|
||||
|
||||
| Attribute | Review Point |
|
||||
|-----------|--------------|
|
||||
| Scalability | Design handles increased load |
|
||||
| Maintainability | Easy to modify and fix |
|
||||
| Observability | Logging and monitoring enabled |
|
||||
|
||||
### 10. Big Picture
|
||||
|
||||
**Caution**: Don't get lost in minor "clean code" nitpicks.
|
||||
|
||||
Verify:
|
||||
- How will this code evolve in the future
|
||||
- Is scaling considered
|
||||
- Is technical debt being created
|
||||
- Does it align with business requirements
|
||||
- Is naming consistent with the domain
|
||||
|
||||
### 11. Boy Scout Rule
|
||||
|
||||
**Leave the code better than you found it.** If changed files have structural issues, flag them for refactoring within the task scope.
|
||||
|
||||
**In scope:**
|
||||
- Existing issues within changed files (dead code, poor naming, broken abstractions)
|
||||
- Structural issues within changed modules (mixed responsibilities, unnecessary dependencies)
|
||||
|
||||
**Out of scope:**
|
||||
- Issues in unchanged files (record as existing issues only)
|
||||
- Refactoring that significantly exceeds the task scope (suggest as non-blocking)
|
||||
|
||||
**Judgment:**
|
||||
|
||||
| Situation | Judgment |
|
||||
|-----------|----------|
|
||||
| Clear issues within changed files | **REJECT** — require fix together |
|
||||
| Structural issues within changed modules | **REJECT** — fix if within scope |
|
||||
| Issues in unchanged files | Record only (non-blocking) |
|
||||
|
||||
**Following poor existing code as justification for leaving problems is not acceptable.** If existing code is bad, improve it rather than match it.
|
||||
|
||||
### 12. Change Scope Assessment
|
||||
|
||||
**Check change scope and include in report (non-blocking).**
|
||||
|
||||
| Scope Size | Lines Changed | Action |
|
||||
|------------|---------------|--------|
|
||||
| Small | ~200 lines | Review as-is |
|
||||
| Medium | 200-500 lines | Review as-is |
|
||||
| Large | 500+ lines | Continue review. Suggest splitting if possible |
|
||||
|
||||
**Note:** Some tasks require large changes. Don't REJECT based on line count alone.
|
||||
|
||||
**Verify:**
|
||||
- Changes are logically cohesive (no unrelated changes mixed in)
|
||||
- Coder's scope declaration matches actual changes
|
||||
|
||||
**Include as suggestions (non-blocking):**
|
||||
- If splittable, present splitting proposal
|
||||
|
||||
### 13. Circular Review Detection
|
||||
|
||||
When review count is provided (e.g., "Review count: 3rd"), adjust judgment accordingly.
|
||||
|
||||
**From the 3rd review onwards:**
|
||||
|
||||
1. Check if the same type of issues are recurring
|
||||
2. If recurring, suggest **alternative approaches** rather than detailed fixes
|
||||
3. Even when REJECTing, include perspective that "a different approach should be considered"
|
||||
|
||||
Example: When issues repeat on the 3rd review
|
||||
|
||||
- Point out the normal issues
|
||||
- Note that the same type of issues are recurring
|
||||
- Explain the limitations of the current approach
|
||||
- Present alternatives (e.g., redesign with a different pattern, introduce new technology)
|
||||
|
||||
**Point**: Rather than repeating "fix this again", step back and suggest a different path.
|
||||
|
||||
## Important
|
||||
|
||||
**Be specific.** These are prohibited:
|
||||
|
||||
@ -28,117 +28,6 @@ The truth of a domain is inscribed in events. State is merely a temporary projec
|
||||
- Snapshot strategies
|
||||
- Replay and rebuild
|
||||
|
||||
## Review Criteria
|
||||
|
||||
### 1. Aggregate Design
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Aggregate spans multiple transaction boundaries | REJECT |
|
||||
| Direct references between Aggregates (not ID references) | REJECT |
|
||||
| Aggregate exceeds 100 lines | Consider splitting |
|
||||
| Business invariants exist outside Aggregate | REJECT |
|
||||
|
||||
**Good Aggregate:**
|
||||
- Clear consistency boundary
|
||||
- References other Aggregates by ID
|
||||
- Receives commands, emits events
|
||||
- Protects invariants internally
|
||||
|
||||
### 2. Event Design
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Event not in past tense (Created → Create) | REJECT |
|
||||
| Event contains logic | REJECT |
|
||||
| Event contains internal state of other Aggregates | REJECT |
|
||||
| Event schema not version controlled | Warning |
|
||||
| CRUD-style events (Updated, Deleted) | Needs review |
|
||||
|
||||
**Good Events:**
|
||||
```
|
||||
// Good: Domain intent is clear
|
||||
OrderPlaced, PaymentReceived, ItemShipped
|
||||
|
||||
// Bad: CRUD style
|
||||
OrderUpdated, OrderDeleted
|
||||
```
|
||||
|
||||
**Event Granularity:**
|
||||
- Too fine: `OrderFieldChanged` → Domain intent unclear
|
||||
- Appropriate: `ShippingAddressChanged` → Intent is clear
|
||||
- Too coarse: `OrderModified` → What changed is unclear
|
||||
|
||||
### 3. Command Handlers
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Handler directly manipulates DB | REJECT |
|
||||
| Handler modifies multiple Aggregates | REJECT |
|
||||
| No command validation | REJECT |
|
||||
| Handler executes queries to make decisions | Needs review |
|
||||
|
||||
**Good Command Handler:**
|
||||
```
|
||||
1. Receive command
|
||||
2. Restore Aggregate from event store
|
||||
3. Apply command to Aggregate
|
||||
4. Save emitted events
|
||||
```
|
||||
|
||||
### 4. Projection Design
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Projection issues commands | REJECT |
|
||||
| Projection references Write model | REJECT |
|
||||
| Single projection serves multiple use cases | Needs review |
|
||||
| Design that cannot be rebuilt | REJECT |
|
||||
|
||||
**Good Projection:**
|
||||
- Optimized for specific read use case
|
||||
- Idempotently reconstructible from events
|
||||
- Completely independent from Write model
|
||||
|
||||
### 5. Eventual Consistency
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Situation | Response |
|
||||
|-----------|----------|
|
||||
| UI expects immediate updates | Redesign or polling/WebSocket |
|
||||
| Consistency delay exceeds tolerance | Reconsider architecture |
|
||||
| Compensating transactions undefined | Request failure scenario review |
|
||||
|
||||
### 6. Anti-pattern Detection
|
||||
|
||||
**REJECT** if found:
|
||||
|
||||
| Anti-pattern | Problem |
|
||||
|--------------|---------|
|
||||
| CRUD Disguise | Just splitting CRUD into Command/Query |
|
||||
| Anemic Domain Model | Aggregate is just a data structure |
|
||||
| Event Soup | Meaningless events proliferate |
|
||||
| Temporal Coupling | Implicit dependency on event order |
|
||||
| Missing Events | Important domain events are missing |
|
||||
| God Aggregate | All responsibilities in one Aggregate |
|
||||
|
||||
### 7. Infrastructure Layer
|
||||
|
||||
**Check:**
|
||||
- Is the event store choice appropriate?
|
||||
- Does the messaging infrastructure meet requirements?
|
||||
- Is snapshot strategy defined?
|
||||
- Is event serialization format appropriate?
|
||||
|
||||
## Important
|
||||
|
||||
- **Don't overlook superficial CQRS**: Just splitting CRUD into Command/Query is meaningless
|
||||
|
||||
@ -32,533 +32,6 @@ The user interface is the only point of contact between the system and users. No
|
||||
- WAI-ARIA compliance
|
||||
- Responsive design
|
||||
|
||||
## Review Criteria
|
||||
|
||||
### 1. Component Design
|
||||
|
||||
**Principle: Do not write everything in one file. Always split components.**
|
||||
|
||||
**Required splits:**
|
||||
- Has its own state → Must split
|
||||
- JSX over 50 lines → Split
|
||||
- Reusable → Split
|
||||
- Multiple responsibilities → Split
|
||||
- Independent section within page → Split
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Component over 200 lines | Consider splitting |
|
||||
| Component over 300 lines | REJECT |
|
||||
| Display and logic mixed | Consider separation |
|
||||
| Props drilling (3+ levels) | Consider state management |
|
||||
| Component with multiple responsibilities | REJECT |
|
||||
|
||||
**Good Component:**
|
||||
- Single responsibility: Does one thing well
|
||||
- Self-contained: Dependencies are clear
|
||||
- Testable: Side effects are isolated
|
||||
|
||||
**Component Classification:**
|
||||
|
||||
| Type | Responsibility | Example |
|
||||
|------|----------------|---------|
|
||||
| Container | Data fetching, state management | `UserListContainer` |
|
||||
| Presentational | Display only | `UserCard` |
|
||||
| Layout | Arrangement, structure | `PageLayout`, `Grid` |
|
||||
| Utility | Common functionality | `ErrorBoundary`, `Portal` |
|
||||
|
||||
**Directory Structure:**
|
||||
```
|
||||
features/{feature-name}/
|
||||
├── components/
|
||||
│ ├── {feature}-view.tsx # Main view (composes children)
|
||||
│ ├── {sub-component}.tsx # Sub-components
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### 2. State Management
|
||||
|
||||
**Principle: Child components do not modify their own state. They bubble events to parent, and parent manipulates state.**
|
||||
|
||||
```tsx
|
||||
// ❌ Child modifies its own state
|
||||
const ChildBad = ({ initialValue }: { initialValue: string }) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
return <input value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// ✅ Parent manages state, child notifies via callback
|
||||
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} />
|
||||
}
|
||||
```
|
||||
|
||||
**Exception (OK for child to have local state):**
|
||||
- UI-only temporary state (hover, focus, animation)
|
||||
- Completely local state that doesn't need to be communicated to parent
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Unnecessary global state | Consider localizing |
|
||||
| Same state managed in multiple places | Needs normalization |
|
||||
| State changes from child to parent (reverse data flow) | REJECT |
|
||||
| API response stored as-is in state | Consider normalization |
|
||||
| Inappropriate useEffect dependencies | REJECT |
|
||||
|
||||
**State Placement Guidelines:**
|
||||
|
||||
| State Nature | Recommended Placement |
|
||||
|--------------|----------------------|
|
||||
| Temporary UI state (modal open/close, etc.) | Local (useState) |
|
||||
| Form input values | Local or form library |
|
||||
| Shared across multiple components | Context or state management library |
|
||||
| Server data cache | Data fetching library (TanStack Query, etc.) |
|
||||
|
||||
### 3. Data Fetching
|
||||
|
||||
**Principle: API calls are made in root (View) components and passed to children via props.**
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - Fetch at root, pass to children
|
||||
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 - Child fetches its own data
|
||||
const OrderSummary = ({ orderId }) => {
|
||||
const { data: order } = useGetOrder(orderId)
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Reasons:**
|
||||
- Data flow is explicit and traceable
|
||||
- Child components are pure presentation (easier to test)
|
||||
- No hidden dependencies in child components
|
||||
|
||||
**When UI state changes affect parameters (week switching, filters, etc.):**
|
||||
|
||||
Manage state at View level and pass callbacks to components.
|
||||
|
||||
```tsx
|
||||
// ✅ CORRECT - State managed at View level
|
||||
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 - Component manages state + data fetching
|
||||
const WeeklyCalendar = ({ facilityId }) => {
|
||||
const [currentWeek, setCurrentWeek] = useState(...)
|
||||
const { data } = useListSchedules({ facilityId, from, to })
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Exceptions (component-level fetching allowed):**
|
||||
|
||||
| Case | Reason |
|
||||
|------|--------|
|
||||
| Infinite scroll | Depends on scroll position (internal UI state) |
|
||||
| Search autocomplete | Real-time search based on input value |
|
||||
| Independent widget | Notification badge, weather, etc. Completely unrelated to parent data |
|
||||
| Real-time updates | WebSocket/Polling auto-updates |
|
||||
| Modal detail fetch | Fetch additional data only when opened |
|
||||
|
||||
**Decision criteria: "Is there no point in parent managing this / Does it not affect parent?"**
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Direct fetch in component | Separate to Container layer |
|
||||
| No error handling | REJECT |
|
||||
| Loading state not handled | REJECT |
|
||||
| No cancellation handling | Warning |
|
||||
| N+1 query-like fetching | REJECT |
|
||||
|
||||
### 4. Shared Components and Abstraction
|
||||
|
||||
**Principle: Common UI patterns should be shared components. Copy-paste of inline styles is prohibited.**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Copy-pasted inline styles
|
||||
<button className="p-2 text-[var(--text-secondary)] hover:...">
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
// ✅ CORRECT - Use shared component
|
||||
<IconButton onClick={onClose} aria-label="Close">
|
||||
<X className="w-5 h-5" />
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
**Patterns to make shared components:**
|
||||
- Icon buttons (close, edit, delete, etc.)
|
||||
- Loading/error displays
|
||||
- Status badges
|
||||
- Tab switching
|
||||
- Label + value display (detail screens)
|
||||
- Search input
|
||||
- Color legends
|
||||
|
||||
**Avoid over-generalization:**
|
||||
|
||||
```tsx
|
||||
// ❌ WRONG - Forcing stepper variant into IconButton
|
||||
export const iconButtonVariants = cva('...', {
|
||||
variants: {
|
||||
variant: {
|
||||
default: '...',
|
||||
outlined: '...', // ← Stepper-specific, not used elsewhere
|
||||
},
|
||||
size: {
|
||||
medium: 'p-2',
|
||||
stepper: 'w-8 h-8', // ← Only used with outlined
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// ✅ CORRECT - Purpose-specific component
|
||||
export function StepperButton(props) {
|
||||
return (
|
||||
<button className="w-8 h-8 rounded-full border ..." {...props}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Signs to make separate components:**
|
||||
- Implicit constraints like "this variant is always with this size"
|
||||
- Added variant is clearly different from original component's purpose
|
||||
- Props specification becomes complex on the usage side
|
||||
|
||||
### 5. Abstraction Level Evaluation
|
||||
|
||||
**Conditional branch bloat detection:**
|
||||
|
||||
| Pattern | Judgment |
|
||||
|---------|----------|
|
||||
| Same conditional in 3+ places | Extract to shared component → **REJECT** |
|
||||
| Props-based branching with 5+ types | Consider component split |
|
||||
| Nested ternaries in render | Early return or component separation → **REJECT** |
|
||||
| Type-based render branching | Consider polymorphic components |
|
||||
|
||||
**Abstraction level mismatch detection:**
|
||||
|
||||
| Pattern | Problem | Fix |
|
||||
|---------|---------|-----|
|
||||
| Data fetching logic mixed in JSX | Hard to read | Extract to custom hook |
|
||||
| Business logic mixed in component | Responsibility violation | Separate to hooks/utils |
|
||||
| Style calculation logic scattered | Hard to maintain | Extract to utility function |
|
||||
| Same transformation in multiple places | DRY violation | Extract to common function |
|
||||
|
||||
**Good abstraction examples:**
|
||||
```tsx
|
||||
// ❌ Conditional bloat
|
||||
function UserBadge({ user }) {
|
||||
if (user.role === 'admin') {
|
||||
return <span className="bg-red-500">Admin</span>
|
||||
} else if (user.role === 'moderator') {
|
||||
return <span className="bg-yellow-500">Moderator</span>
|
||||
} else if (user.role === 'premium') {
|
||||
return <span className="bg-purple-500">Premium</span>
|
||||
} else {
|
||||
return <span className="bg-gray-500">User</span>
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ Abstracted with Map
|
||||
const ROLE_CONFIG = {
|
||||
admin: { label: 'Admin', className: 'bg-red-500' },
|
||||
moderator: { label: 'Moderator', className: 'bg-yellow-500' },
|
||||
premium: { label: 'Premium', className: 'bg-purple-500' },
|
||||
default: { label: 'User', 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
|
||||
// ❌ Mixed abstraction levels
|
||||
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()} USD</div>
|
||||
))
|
||||
}
|
||||
|
||||
// ✅ Aligned abstraction levels
|
||||
function OrderList() {
|
||||
const { data: orders } = useOrders() // Hide data fetching
|
||||
|
||||
return orders.map(order => (
|
||||
<OrderItem key={order.id} order={order} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Frontend and Backend Separation of Concerns
|
||||
|
||||
#### 6.1 Display Format Responsibility
|
||||
|
||||
**Principle: Backend returns "data", frontend converts to "display format".**
|
||||
|
||||
```tsx
|
||||
// ✅ Frontend: Convert to display format
|
||||
export function formatPrice(amount: number): string {
|
||||
return `$${amount.toLocaleString()}`
|
||||
}
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return format(date, 'MMM d, yyyy')
|
||||
}
|
||||
```
|
||||
|
||||
**Reasons:**
|
||||
- Display format is a UI concern, not backend responsibility
|
||||
- Easy to support internationalization
|
||||
- Frontend can flexibly change display
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Backend returns display strings | Suggest design review |
|
||||
| Same format logic copy-pasted | Unify to utility function |
|
||||
| Inline formatting in component | Extract to function |
|
||||
|
||||
#### 6.2 Domain Logic Placement (Smart UI Elimination)
|
||||
|
||||
**Principle: Domain logic (business rules) belongs in the backend. Frontend only displays and edits state.**
|
||||
|
||||
**What is domain logic:**
|
||||
- Aggregate business rules (stock validation, price calculation, status transitions)
|
||||
- Business constraint validation
|
||||
- Invariant enforcement
|
||||
|
||||
**Frontend responsibilities:**
|
||||
- Display state received from server
|
||||
- Collect user input and send commands to backend
|
||||
- Manage UI-only temporary state (focus, hover, modal open/close)
|
||||
- Display format conversion (formatting, sorting, filtering)
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Price calculation/stock validation in frontend | Move to backend → **REJECT** |
|
||||
| Status transition rules in frontend | Move to backend → **REJECT** |
|
||||
| Business validation in frontend | Move to backend → **REJECT** |
|
||||
| Recalculating server-computable values in frontend | Redundant → **REJECT** |
|
||||
|
||||
**Good vs Bad Examples:**
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Business rules in frontend
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
const totalPrice = order.items.reduce((sum, item) =>
|
||||
sum + item.price * item.quantity, 0
|
||||
)
|
||||
const canCheckout = totalPrice >= 100 && order.items.every(i => i.stock > 0)
|
||||
|
||||
return <button disabled={!canCheckout}>Checkout</button>
|
||||
}
|
||||
|
||||
// ✅ GOOD - Display state received from server
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
// totalPrice, canCheckout are received from server
|
||||
return (
|
||||
<>
|
||||
<div>{formatPrice(order.totalPrice)}</div>
|
||||
<button disabled={!order.canCheckout}>Checkout</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ❌ BAD - Status transition logic in frontend
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
const canStart = task.status === 'pending' && task.assignee !== null
|
||||
const canComplete = task.status === 'in_progress' && /* complex conditions... */
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>Start</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>Complete</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// ✅ GOOD - Server returns allowed actions
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
// task.allowedActions = ['start', 'cancel'], etc., calculated by server
|
||||
const canStart = task.allowedActions.includes('start')
|
||||
const canComplete = task.allowedActions.includes('complete')
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>Start</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>Complete</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
**Exceptions (OK to have logic in frontend):**
|
||||
|
||||
| Case | Reason |
|
||||
|------|--------|
|
||||
| UI-only validation | UX feedback like "required field", "max length" (must also validate on server) |
|
||||
| Client-side filter/sort | Changing display order of lists received from server |
|
||||
| Display condition branching | UI control like "show details if logged in" |
|
||||
| Real-time feedback | Preview display during input |
|
||||
|
||||
**Decision criteria: "Would the business break if this calculation differs from the server?"**
|
||||
- YES → Place in backend (domain logic)
|
||||
- NO → OK in frontend (display logic)
|
||||
|
||||
### 7. Performance
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Unnecessary re-renders | Needs optimization |
|
||||
| Large lists without virtualization | Warning |
|
||||
| Unoptimized images | Warning |
|
||||
| Unused code in bundle | Check tree-shaking |
|
||||
| Excessive memoization | Verify necessity |
|
||||
|
||||
**Optimization Checklist:**
|
||||
- [ ] Are `React.memo` / `useMemo` / `useCallback` appropriate?
|
||||
- [ ] Are large lists using virtual scroll?
|
||||
- [ ] Is Code Splitting appropriate?
|
||||
- [ ] Are images lazy loaded?
|
||||
|
||||
**Anti-patterns:**
|
||||
|
||||
```tsx
|
||||
// ❌ New object every render
|
||||
<Child style={{ color: 'red' }} />
|
||||
|
||||
// ✅ Constant or useMemo
|
||||
const style = useMemo(() => ({ color: 'red' }), []);
|
||||
<Child style={style} />
|
||||
```
|
||||
|
||||
### 8. Accessibility
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Interactive elements without keyboard support | REJECT |
|
||||
| Images without alt attribute | REJECT |
|
||||
| Form elements without labels | REJECT |
|
||||
| Information conveyed by color only | REJECT |
|
||||
| Missing focus management (modals, etc.) | REJECT |
|
||||
|
||||
**Checklist:**
|
||||
- [ ] Using semantic HTML?
|
||||
- [ ] Are ARIA attributes appropriate (not excessive)?
|
||||
- [ ] Is keyboard navigation possible?
|
||||
- [ ] Does it make sense with a screen reader?
|
||||
- [ ] Is color contrast sufficient?
|
||||
|
||||
### 9. TypeScript/Type Safety
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| Use of `any` type | REJECT |
|
||||
| Excessive type assertions (as) | Needs review |
|
||||
| No Props type definition | REJECT |
|
||||
| Inappropriate event handler types | Needs fix |
|
||||
|
||||
### 10. Frontend Security
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| dangerouslySetInnerHTML usage | Check XSS risk |
|
||||
| Unsanitized user input | REJECT |
|
||||
| Sensitive data stored in frontend | REJECT |
|
||||
| CSRF token not used | Needs verification |
|
||||
|
||||
### 11. Testability
|
||||
|
||||
**Required Checks:**
|
||||
|
||||
| Criteria | Judgment |
|
||||
|----------|----------|
|
||||
| No data-testid, etc. | Warning |
|
||||
| Structure difficult to test | Consider separation |
|
||||
| Business logic embedded in UI | REJECT |
|
||||
|
||||
### 12. Anti-pattern Detection
|
||||
|
||||
**REJECT** if found:
|
||||
|
||||
| Anti-pattern | Problem |
|
||||
|--------------|---------|
|
||||
| God Component | All features concentrated in one component |
|
||||
| Prop Drilling | Deep props bucket brigade |
|
||||
| Inline Styles abuse | Maintainability degradation |
|
||||
| useEffect hell | Dependencies too complex |
|
||||
| Premature Optimization | Unnecessary memoization |
|
||||
| Magic Strings | Hardcoded strings |
|
||||
| Hidden Dependencies | Child components with hidden API calls |
|
||||
| Over-generalization | Components forced to be generic |
|
||||
|
||||
## Important
|
||||
|
||||
- **Prioritize user experience**: UX over technical correctness
|
||||
|
||||
@ -30,164 +30,6 @@ Security cannot be retrofitted. It must be built in from the design stage; "we'l
|
||||
- Write code yourself (only provide feedback and fix suggestions)
|
||||
- Review design or code quality (that's Architect's role)
|
||||
|
||||
## AI-Generated Code: Special Attention
|
||||
|
||||
AI-generated code has unique vulnerability patterns.
|
||||
|
||||
**Common security issues in AI-generated code:**
|
||||
|
||||
| Pattern | Risk | Example |
|
||||
|---------|------|---------|
|
||||
| Plausible but dangerous defaults | High | `cors: { origin: '*' }` looks fine but is dangerous |
|
||||
| Outdated security practices | Medium | Using deprecated encryption, old auth patterns |
|
||||
| Incomplete validation | High | Validates format but not business rules |
|
||||
| Over-trusting inputs | Critical | Assumes internal APIs are always safe |
|
||||
| Copy-paste vulnerabilities | High | Same dangerous pattern repeated in multiple files |
|
||||
|
||||
**Require extra scrutiny:**
|
||||
- Auth/authorization logic (AI tends to miss edge cases)
|
||||
- Input validation (AI may check syntax but miss semantics)
|
||||
- Error messages (AI may expose internal details)
|
||||
- Config files (AI may use dangerous defaults from training data)
|
||||
|
||||
## Review Perspectives
|
||||
|
||||
### 1. Injection Attacks
|
||||
|
||||
**SQL Injection:**
|
||||
- SQL construction via string concatenation → **REJECT**
|
||||
- Not using parameterized queries → **REJECT**
|
||||
- Unsanitized input in ORM raw queries → **REJECT**
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
db.query(`SELECT * FROM users WHERE id = ${userId}`)
|
||||
|
||||
// OK
|
||||
db.query('SELECT * FROM users WHERE id = ?', [userId])
|
||||
```
|
||||
|
||||
**Command Injection:**
|
||||
- Unvalidated input in `exec()`, `spawn()` → **REJECT**
|
||||
- Insufficient escaping in shell command construction → **REJECT**
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
exec(`ls ${userInput}`)
|
||||
|
||||
// OK
|
||||
execFile('ls', [sanitizedInput])
|
||||
```
|
||||
|
||||
**XSS (Cross-Site Scripting):**
|
||||
- Unescaped output to HTML/JS → **REJECT**
|
||||
- Improper use of `innerHTML`, `dangerouslySetInnerHTML` → **REJECT**
|
||||
- Direct embedding of URL parameters → **REJECT**
|
||||
|
||||
### 2. Authentication & Authorization
|
||||
|
||||
**Authentication issues:**
|
||||
- Hardcoded credentials → **Immediate REJECT**
|
||||
- Plaintext password storage → **Immediate REJECT**
|
||||
- Weak hash algorithms (MD5, SHA1) → **REJECT**
|
||||
- Improper session token management → **REJECT**
|
||||
|
||||
**Authorization issues:**
|
||||
- Missing permission checks → **REJECT**
|
||||
- IDOR (Insecure Direct Object Reference) → **REJECT**
|
||||
- Privilege escalation possibility → **REJECT**
|
||||
|
||||
```typescript
|
||||
// NG - No permission check
|
||||
app.get('/user/:id', (req, res) => {
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
|
||||
// OK
|
||||
app.get('/user/:id', authorize('read:user'), (req, res) => {
|
||||
if (req.user.id !== req.params.id && !req.user.isAdmin) {
|
||||
return res.status(403).send('Forbidden')
|
||||
}
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
```
|
||||
|
||||
### 3. Data Protection
|
||||
|
||||
**Sensitive information exposure:**
|
||||
- Hardcoded API keys, secrets → **Immediate REJECT**
|
||||
- Sensitive info in logs → **REJECT**
|
||||
- Internal info exposure in error messages → **REJECT**
|
||||
- Committed `.env` files → **REJECT**
|
||||
|
||||
**Data validation:**
|
||||
- Unvalidated input values → **REJECT**
|
||||
- Missing type checks → **REJECT**
|
||||
- No size limits set → **REJECT**
|
||||
|
||||
### 4. Cryptography
|
||||
|
||||
- Use of weak crypto algorithms → **REJECT**
|
||||
- Fixed IV/Nonce usage → **REJECT**
|
||||
- Hardcoded encryption keys → **Immediate REJECT**
|
||||
- No HTTPS (production) → **REJECT**
|
||||
|
||||
### 5. File Operations
|
||||
|
||||
**Path Traversal:**
|
||||
- File paths containing user input → **REJECT**
|
||||
- Insufficient `../` sanitization → **REJECT**
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
const filePath = path.join(baseDir, userInput)
|
||||
fs.readFile(filePath)
|
||||
|
||||
// OK
|
||||
const safePath = path.resolve(baseDir, userInput)
|
||||
if (!safePath.startsWith(path.resolve(baseDir))) {
|
||||
throw new Error('Invalid path')
|
||||
}
|
||||
```
|
||||
|
||||
**File Upload:**
|
||||
- No file type validation → **REJECT**
|
||||
- No file size limits → **REJECT**
|
||||
- Allowing executable file uploads → **REJECT**
|
||||
|
||||
### 6. Dependencies
|
||||
|
||||
- Packages with known vulnerabilities → **REJECT**
|
||||
- Unmaintained packages → Warning
|
||||
- Unnecessary dependencies → Warning
|
||||
|
||||
### 7. Error Handling
|
||||
|
||||
- Stack trace exposure in production → **REJECT**
|
||||
- Detailed error message exposure → **REJECT**
|
||||
- Swallowing security events → **REJECT**
|
||||
|
||||
### 8. Rate Limiting & DoS Protection
|
||||
|
||||
- No rate limiting (auth endpoints) → Warning
|
||||
- Resource exhaustion attack possibility → Warning
|
||||
- Infinite loop possibility → **REJECT**
|
||||
|
||||
### 9. OWASP Top 10 Checklist
|
||||
|
||||
| Category | Check Items |
|
||||
|----------|-------------|
|
||||
| A01 Broken Access Control | Authorization checks, CORS config |
|
||||
| A02 Cryptographic Failures | Encryption, sensitive data protection |
|
||||
| A03 Injection | SQL, Command, XSS |
|
||||
| A04 Insecure Design | Security design patterns |
|
||||
| A05 Security Misconfiguration | Default settings, unnecessary features |
|
||||
| A06 Vulnerable Components | Dependency vulnerabilities |
|
||||
| A07 Auth Failures | Authentication mechanisms |
|
||||
| A08 Software Integrity | Code signing, CI/CD |
|
||||
| A09 Logging Failures | Security logging |
|
||||
| A10 SSRF | Server-side requests |
|
||||
|
||||
## Important
|
||||
|
||||
**Don't miss anything**: Security vulnerabilities get exploited in production. One oversight can lead to a critical incident.
|
||||
|
||||
@ -20,6 +20,9 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
architecture: ../knowledge/architecture.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
architect-planner: ../personas/architect-planner.md
|
||||
@ -224,6 +227,7 @@ movements:
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
stance: review
|
||||
knowledge: architecture
|
||||
report:
|
||||
name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
|
||||
@ -28,6 +28,11 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
cqrs-es: ../knowledge/cqrs-es.md
|
||||
security: ../knowledge/security.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
@ -196,6 +201,7 @@ movements:
|
||||
edit: false
|
||||
persona: cqrs-es-reviewer
|
||||
stance: review
|
||||
knowledge: cqrs-es
|
||||
report:
|
||||
name: 04-cqrs-es-review.md
|
||||
format: cqrs-es-review
|
||||
@ -215,6 +221,7 @@ movements:
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
stance: review
|
||||
knowledge: frontend
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
@ -234,6 +241,7 @@ movements:
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
stance: review
|
||||
knowledge: security
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: security-review
|
||||
|
||||
@ -28,6 +28,11 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
@ -195,6 +200,7 @@ movements:
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
stance: review
|
||||
knowledge: architecture
|
||||
report:
|
||||
name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
@ -214,6 +220,7 @@ movements:
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
stance: review
|
||||
knowledge: frontend
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
@ -233,6 +240,7 @@ movements:
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
stance: review
|
||||
knowledge: security
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: security-review
|
||||
|
||||
427
resources/global/ja/knowledge/architecture.md
Normal file
427
resources/global/ja/knowledge/architecture.md
Normal file
@ -0,0 +1,427 @@
|
||||
# アーキテクチャ知識
|
||||
|
||||
## 構造・設計
|
||||
|
||||
**ファイル分割**
|
||||
|
||||
| 基準 | 判定 |
|
||||
|--------------|------|
|
||||
| 1ファイル200行超 | 分割を検討 |
|
||||
| 1ファイル300行超 | REJECT |
|
||||
| 1ファイルに複数の責務 | REJECT |
|
||||
| 関連性の低いコードが同居 | REJECT |
|
||||
|
||||
**モジュール構成**
|
||||
|
||||
- 高凝集: 関連する機能がまとまっているか
|
||||
- 低結合: モジュール間の依存が最小限か
|
||||
- 循環依存がないか
|
||||
- 適切なディレクトリ階層か
|
||||
|
||||
**関数設計**
|
||||
|
||||
- 1関数1責務になっているか
|
||||
- 30行を超える関数は分割を検討
|
||||
- 副作用が明確か
|
||||
|
||||
**レイヤー設計**
|
||||
|
||||
- 依存の方向: 上位層 → 下位層(逆方向禁止)
|
||||
- Controller → Service → Repository の流れが守られているか
|
||||
- 1インターフェース = 1責務(巨大なServiceクラス禁止)
|
||||
|
||||
**ディレクトリ構造**
|
||||
|
||||
構造パターンの選択:
|
||||
|
||||
| パターン | 適用場面 | 例 |
|
||||
|---------|---------|-----|
|
||||
| レイヤード | 小規模、CRUD中心 | `controllers/`, `services/`, `repositories/` |
|
||||
| Vertical Slice | 中〜大規模、機能独立性が高い | `features/auth/`, `features/order/` |
|
||||
| ハイブリッド | 共通基盤 + 機能モジュール | `core/` + `features/` |
|
||||
|
||||
Vertical Slice Architecture(機能単位でコードをまとめる構造):
|
||||
|
||||
```
|
||||
src/
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── LoginCommand.ts
|
||||
│ │ ├── LoginHandler.ts
|
||||
│ │ ├── AuthRepository.ts
|
||||
│ │ └── auth.test.ts
|
||||
│ └── order/
|
||||
│ ├── CreateOrderCommand.ts
|
||||
│ ├── CreateOrderHandler.ts
|
||||
│ └── ...
|
||||
└── shared/ # 複数featureで共有
|
||||
├── database/
|
||||
└── middleware/
|
||||
```
|
||||
|
||||
Vertical Slice の判定基準:
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 1機能が3ファイル以上のレイヤーに跨る | Slice化を検討 |
|
||||
| 機能間の依存がほぼない | Slice化推奨 |
|
||||
| 共通処理が50%以上 | レイヤード維持 |
|
||||
| チームが機能別に分かれている | Slice化必須 |
|
||||
|
||||
禁止パターン:
|
||||
|
||||
| パターン | 問題 |
|
||||
|---------|------|
|
||||
| `utils/` の肥大化 | 責務不明の墓場になる |
|
||||
| `common/` への安易な配置 | 依存関係が不明確になる |
|
||||
| 深すぎるネスト(4階層超) | ナビゲーション困難 |
|
||||
| 機能とレイヤーの混在 | `features/services/` は禁止 |
|
||||
|
||||
**責務の分離**
|
||||
|
||||
- 読み取りと書き込みの責務が分かれているか
|
||||
- データ取得はルート(View/Controller)で行い、子に渡しているか
|
||||
- エラーハンドリングが一元化されているか(各所でtry-catch禁止)
|
||||
- ビジネスロジックがController/Viewに漏れていないか
|
||||
|
||||
## コード品質の検出手法
|
||||
|
||||
**説明コメント(What/How)の検出基準**
|
||||
|
||||
コードの動作をそのまま言い換えているコメントを検出する。
|
||||
|
||||
| 判定 | 基準 |
|
||||
|------|------|
|
||||
| REJECT | コードの動作をそのまま自然言語で言い換えている |
|
||||
| REJECT | 関数名・変数名から明らかなことを繰り返している |
|
||||
| REJECT | JSDocが関数名の言い換えだけで情報を追加していない |
|
||||
| OK | なぜその実装を選んだかの設計判断を説明している |
|
||||
| OK | 一見不自然に見える挙動の理由を説明している |
|
||||
| 最良 | コメントなしでコード自体が意図を語っている |
|
||||
|
||||
```typescript
|
||||
// REJECT - コードの言い換え(What)
|
||||
// If interrupted, abort immediately
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// REJECT - ループの存在を言い換えただけ
|
||||
// Check transitions in order
|
||||
for (const transition of step.transitions) {
|
||||
|
||||
// REJECT - 関数名の繰り返し
|
||||
/** Check if status matches transition condition. */
|
||||
export function matchesCondition(status: Status, condition: TransitionCondition): boolean {
|
||||
|
||||
// OK - 設計判断の理由(Why)
|
||||
// ユーザー中断はピース定義のトランジションより優先する
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// OK - 一見不自然な挙動の理由
|
||||
// stay はループを引き起こす可能性があるが、ユーザーが明示的に指定した場合のみ使われる
|
||||
return step.name;
|
||||
```
|
||||
|
||||
**状態の直接変更の検出基準**
|
||||
|
||||
配列やオブジェクトの直接変更(ミューテーション)を検出する。
|
||||
|
||||
```typescript
|
||||
// REJECT - 配列の直接変更
|
||||
const steps: Step[] = getSteps();
|
||||
steps.push(newStep); // 元の配列を破壊
|
||||
steps.splice(index, 1); // 元の配列を破壊
|
||||
steps[0].status = 'done'; // ネストされたオブジェクトも直接変更
|
||||
|
||||
// OK - イミュータブルな操作
|
||||
const withNew = [...steps, newStep];
|
||||
const without = steps.filter((_, i) => i !== index);
|
||||
const updated = steps.map((s, i) =>
|
||||
i === 0 ? { ...s, status: 'done' } : s
|
||||
);
|
||||
|
||||
// REJECT - オブジェクトの直接変更
|
||||
function updateConfig(config: Config) {
|
||||
config.logLevel = 'debug'; // 引数を直接変更
|
||||
config.steps.push(newStep); // ネストも直接変更
|
||||
return config;
|
||||
}
|
||||
|
||||
// OK - 新しいオブジェクトを返す
|
||||
function updateConfig(config: Config): Config {
|
||||
return {
|
||||
...config,
|
||||
logLevel: 'debug',
|
||||
steps: [...config.steps, newStep],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## セキュリティ(基本チェック)
|
||||
|
||||
- インジェクション対策(SQL, コマンド, XSS)
|
||||
- ユーザー入力の検証
|
||||
- 機密情報のハードコーディング
|
||||
|
||||
## テスタビリティ
|
||||
|
||||
- 依存性注入が可能な設計か
|
||||
- モック可能か
|
||||
- テストが書かれているか
|
||||
|
||||
## アンチパターン検出
|
||||
|
||||
以下のパターンを見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| God Class/Component | 1つのクラスが多くの責務を持っている |
|
||||
| Feature Envy | 他モジュールのデータを頻繁に参照している |
|
||||
| Shotgun Surgery | 1つの変更が複数ファイルに波及する構造 |
|
||||
| 過度な汎用化 | 今使わないバリアントや拡張ポイント |
|
||||
| 隠れた依存 | 子コンポーネントが暗黙的にAPIを呼ぶ等 |
|
||||
| 非イディオマティック | 言語・FWの作法を無視した独自実装 |
|
||||
|
||||
## 抽象化レベルの評価
|
||||
|
||||
**条件分岐の肥大化検出**
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じ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); // 詳細は隠蔽
|
||||
}
|
||||
```
|
||||
|
||||
## その場しのぎの検出
|
||||
|
||||
「とりあえず動かす」ための妥協を見逃さない。
|
||||
|
||||
| パターン | 例 |
|
||||
|---------|-----|
|
||||
| 不要なパッケージ追加 | 動かすためだけに入れた謎のライブラリ |
|
||||
| テストの削除・スキップ | `@Disabled`、`.skip()`、コメントアウト |
|
||||
| 空実装・スタブ放置 | `return null`、`// TODO: implement`、`pass` |
|
||||
| モックデータの本番混入 | ハードコードされたダミーデータ |
|
||||
| エラー握りつぶし | 空の `catch {}`、`rescue nil` |
|
||||
| マジックナンバー | 説明なしの `if (status == 3)` |
|
||||
|
||||
## TODOコメントの厳格な禁止
|
||||
|
||||
「将来やる」は決してやらない。今やらないことは永遠にやらない。
|
||||
|
||||
TODOコメントは即REJECT。
|
||||
|
||||
```kotlin
|
||||
// REJECT - 将来を見越したTODO
|
||||
// TODO: 施設IDによる認可チェックを追加
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
|
||||
// APPROVE - 今実装する
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
val currentUserFacilityId = getCurrentUserFacilityId()
|
||||
val holiday = findHolidayById(id)
|
||||
require(holiday.facilityId == currentUserFacilityId) {
|
||||
"Cannot delete holiday from another facility"
|
||||
}
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
```
|
||||
|
||||
TODOが許容される唯一のケース:
|
||||
|
||||
| 条件 | 例 | 判定 |
|
||||
|------|-----|------|
|
||||
| 外部依存で今は実装不可 + Issue化済み | `// TODO(#123): APIキー取得後に実装` | 許容 |
|
||||
| 技術的制約で回避不可 + Issue化済み | `// TODO(#456): ライブラリバグ修正待ち` | 許容 |
|
||||
| 「将来実装」「後で追加」 | `// TODO: バリデーション追加` | REJECT |
|
||||
| 「時間がないので」 | `// TODO: リファクタリング` | REJECT |
|
||||
|
||||
正しい対処:
|
||||
- 今必要 → 今実装する
|
||||
- 今不要 → コードを削除する
|
||||
- 外部要因で不可 → Issue化してチケット番号をコメントに入れる
|
||||
|
||||
## DRY違反の検出
|
||||
|
||||
重複コードを検出する。
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じロジックが3箇所以上 | 即REJECT - 関数/メソッドに抽出 |
|
||||
| 同じバリデーションが2箇所以上 | 即REJECT - バリデーター関数に抽出 |
|
||||
| 似たようなコンポーネントが3個以上 | 即REJECT - 共通コンポーネント化 |
|
||||
| コピペで派生したコード | 即REJECT - パラメータ化または抽象化 |
|
||||
|
||||
AHA原則(Avoid Hasty Abstractions)とのバランス:
|
||||
- 2回の重複 → 様子見
|
||||
- 3回の重複 → 即抽出
|
||||
- ドメインが異なる重複 → 抽象化しない(例: 顧客用バリデーションと管理者用バリデーションは別物)
|
||||
|
||||
## 仕様準拠の検証
|
||||
|
||||
変更が、プロジェクトの文書化された仕様に準拠しているか検証する。
|
||||
|
||||
検証対象:
|
||||
|
||||
| 対象 | 確認内容 |
|
||||
|------|---------|
|
||||
| CLAUDE.md / README.md | スキーマ定義、設計原則、制約に従っているか |
|
||||
| 型定義・Zodスキーマ | 新しいフィールドがスキーマに反映されているか |
|
||||
| YAML/JSON設定ファイル | 文書化されたフォーマットに従っているか |
|
||||
|
||||
具体的なチェック:
|
||||
|
||||
1. 設定ファイル(YAML等)を変更・追加した場合:
|
||||
- CLAUDE.md等に記載されたスキーマ定義と突合する
|
||||
- 無視されるフィールドや無効なフィールドが含まれていないか
|
||||
- 必須フィールドが欠落していないか
|
||||
|
||||
2. 型定義やインターフェースを変更した場合:
|
||||
- ドキュメントのスキーマ説明が更新されているか
|
||||
- 既存の設定ファイルが新しいスキーマと整合するか
|
||||
|
||||
このパターンを見つけたら REJECT:
|
||||
|
||||
| パターン | 問題 |
|
||||
|---------|------|
|
||||
| 仕様に存在しないフィールドの使用 | 無視されるか予期しない動作 |
|
||||
| 仕様上無効な値の設定 | 実行時エラーまたは無視される |
|
||||
| 文書化された制約への違反 | 設計意図に反する |
|
||||
|
||||
## 呼び出しチェーン検証
|
||||
|
||||
新しいパラメータ・フィールドが追加された場合、変更ファイル内だけでなく呼び出し元も検証する。
|
||||
|
||||
検証手順:
|
||||
1. 新しいオプショナルパラメータや interface フィールドを見つけたら、`Grep` で全呼び出し元を検索
|
||||
2. 全呼び出し元が新しいパラメータを渡しているか確認
|
||||
3. フォールバック値(`?? default`)がある場合、フォールバックが使われるケースが意図通りか確認
|
||||
|
||||
危険パターン:
|
||||
|
||||
| パターン | 問題 | 検出方法 |
|
||||
|---------|------|---------|
|
||||
| `options.xxx ?? fallback` で全呼び出し元が `xxx` を省略 | 機能が実装されているのに常にフォールバック | grep で呼び出し元を確認 |
|
||||
| テストがモックで直接値をセット | 実際の呼び出しチェーンを経由しない | テストの構築方法を確認 |
|
||||
| `executeXxx()` が内部で使う `options` を引数で受け取らない | 上位から値を渡す口がない | 関数シグネチャを確認 |
|
||||
|
||||
```typescript
|
||||
// 配線漏れ: projectCwd を受け取る口がない
|
||||
export async function executePiece(config, cwd, task) {
|
||||
const engine = new PieceEngine(config, cwd, task); // options なし
|
||||
}
|
||||
|
||||
// 配線済み: projectCwd を渡せる
|
||||
export async function executePiece(config, cwd, task, options?) {
|
||||
const engine = new PieceEngine(config, cwd, task, options);
|
||||
}
|
||||
```
|
||||
|
||||
呼び出し元の制約による論理的デッドコード:
|
||||
|
||||
呼び出しチェーンの検証は「配線漏れ」だけでなく、逆方向——呼び出し元が既に保証している条件に対する不要な防御コード——にも適用する。
|
||||
|
||||
| パターン | 問題 | 検出方法 |
|
||||
|---------|------|---------|
|
||||
| 呼び出し元がTTY必須なのに関数内でTTYチェック | 到達しない分岐が残る | grep で全呼び出し元の前提条件を確認 |
|
||||
| 呼び出し元がnullチェック済みなのに再度nullガード | 冗長な防御 | 呼び出し元の制約を追跡 |
|
||||
| 呼び出し元が型で制約しているのにランタイムチェック | 型安全を信頼していない | TypeScriptの型制約を確認 |
|
||||
|
||||
検証手順:
|
||||
1. 防御的な条件分岐(TTYチェック、nullガード等)を見つけたら、grep で全呼び出し元を確認
|
||||
2. 全呼び出し元がその条件を既に保証しているなら、防御は不要 → REJECT
|
||||
3. 一部の呼び出し元が保証していない場合は、防御を残す
|
||||
|
||||
## 品質特性
|
||||
|
||||
| 特性 | 確認観点 |
|
||||
|------|---------|
|
||||
| Scalability | 負荷増加に対応できる設計か |
|
||||
| Maintainability | 変更・修正が容易か |
|
||||
| Observability | ログ・監視が可能な設計か |
|
||||
|
||||
## 大局観
|
||||
|
||||
細かい「クリーンコード」の指摘に終始しない。
|
||||
|
||||
確認すべきこと:
|
||||
- このコードは将来どう変化するか
|
||||
- スケーリングの必要性は考慮されているか
|
||||
- 技術的負債を生んでいないか
|
||||
- ビジネス要件と整合しているか
|
||||
- 命名がドメインと一貫しているか
|
||||
|
||||
## 変更スコープの評価
|
||||
|
||||
変更スコープを確認し、レポートに記載する(ブロッキングではない)。
|
||||
|
||||
| スコープサイズ | 変更行数 | 対応 |
|
||||
|---------------|---------|------|
|
||||
| Small | 〜200行 | そのままレビュー |
|
||||
| Medium | 200-500行 | そのままレビュー |
|
||||
| Large | 500行以上 | レビューは継続。分割可能か提案を付記 |
|
||||
|
||||
大きな変更が必要なタスクもある。行数だけでREJECTしない。
|
||||
|
||||
確認すること:
|
||||
- 変更が論理的にまとまっているか(無関係な変更が混在していないか)
|
||||
- Coderのスコープ宣言と実際の変更が一致しているか
|
||||
|
||||
提案として記載すること(ブロッキングではない):
|
||||
- 分割可能な場合は分割案を提示
|
||||
417
resources/global/ja/knowledge/cqrs-es.md
Normal file
417
resources/global/ja/knowledge/cqrs-es.md
Normal file
@ -0,0 +1,417 @@
|
||||
# CQRS+ES知識
|
||||
|
||||
## Aggregate設計
|
||||
|
||||
Aggregateは判断に必要なフィールドのみ保持する。
|
||||
|
||||
Command Model(Aggregate)の役割は「コマンドを受けて判断し、イベントを発行する」こと。クエリ用データはRead Model(Projection)が担当する。
|
||||
|
||||
「判断に必要」とは:
|
||||
- `if`/`require`の条件分岐に使う
|
||||
- インスタンスメソッドでイベント発行時にフィールド値を参照する
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| Aggregateが複数のトランザクション境界を跨ぐ | REJECT |
|
||||
| Aggregate間の直接参照(ID参照でない) | REJECT |
|
||||
| Aggregateが100行を超える | 分割を検討 |
|
||||
| ビジネス不変条件がAggregate外にある | REJECT |
|
||||
| 判断に使わないフィールドを保持 | REJECT |
|
||||
|
||||
良い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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断に使わないフィールドを保持(NG)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## イベント設計
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| イベントが過去形でない(Created → Create) | REJECT |
|
||||
| イベントにロジックが含まれる | REJECT |
|
||||
| イベントが他Aggregateの内部状態を含む | REJECT |
|
||||
| イベントのスキーマがバージョン管理されていない | 警告 |
|
||||
| CRUDスタイルのイベント(Updated, Deleted) | 要検討 |
|
||||
|
||||
良いイベント:
|
||||
```kotlin
|
||||
// Good: ドメインの意図が明確
|
||||
OrderPlaced, PaymentReceived, ItemShipped
|
||||
|
||||
// Bad: CRUDスタイル
|
||||
OrderUpdated, OrderDeleted
|
||||
```
|
||||
|
||||
イベント粒度:
|
||||
- 細かすぎ: `OrderFieldChanged` → ドメインの意図が不明
|
||||
- 適切: `ShippingAddressChanged` → 意図が明確
|
||||
- 粗すぎ: `OrderModified` → 何が変わったか不明
|
||||
|
||||
## コマンドハンドラ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| ハンドラがDBを直接操作 | REJECT |
|
||||
| ハンドラが複数Aggregateを変更 | REJECT |
|
||||
| コマンドのバリデーションがない | REJECT |
|
||||
| ハンドラがクエリを実行して判断 | 要検討 |
|
||||
|
||||
良いコマンドハンドラ:
|
||||
```
|
||||
1. コマンドを受け取る
|
||||
2. Aggregateをイベントストアから復元
|
||||
3. Aggregateにコマンドを適用
|
||||
4. 発行されたイベントを保存
|
||||
```
|
||||
|
||||
## プロジェクション設計
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| プロジェクションがコマンドを発行 | REJECT |
|
||||
| プロジェクションがWriteモデルを参照 | REJECT |
|
||||
| 複数のユースケースを1つのプロジェクションで賄う | 要検討 |
|
||||
| リビルド不可能な設計 | REJECT |
|
||||
|
||||
良いプロジェクション:
|
||||
- 特定の読み取りユースケースに最適化
|
||||
- イベントから冪等に再構築可能
|
||||
- Writeモデルから完全に独立
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
## 結果整合性
|
||||
|
||||
| 状況 | 対応 |
|
||||
|------|------|
|
||||
| UIが即座に更新を期待している | 設計見直し or ポーリング/WebSocket |
|
||||
| 整合性遅延が許容範囲を超える | アーキテクチャ再検討 |
|
||||
| 補償トランザクションが未定義 | 障害シナリオの検討を要求 |
|
||||
|
||||
## Saga vs EventHandler
|
||||
|
||||
Sagaは「競合が発生する複数アグリゲート間の操作」にのみ使用する。
|
||||
|
||||
Sagaが必要なケース:
|
||||
```
|
||||
複数のアクターが同じリソースを取り合う場合
|
||||
例: 在庫確保(10人が同時に同じ商品を注文)
|
||||
|
||||
OrderPlacedEvent
|
||||
↓ InventoryReservationSaga
|
||||
ReserveInventoryCommand → Inventory集約(同時実行を直列化)
|
||||
↓
|
||||
InventoryReservedEvent → ConfirmOrderCommand
|
||||
InventoryReservationFailedEvent → CancelOrderCommand
|
||||
```
|
||||
|
||||
Sagaが不要なケース:
|
||||
```
|
||||
競合が発生しない操作
|
||||
例: 注文キャンセル時の在庫解放
|
||||
|
||||
OrderCancelledEvent
|
||||
↓ InventoryReleaseHandler(単純なEventHandler)
|
||||
ReleaseInventoryCommand
|
||||
↓
|
||||
InventoryReleasedEvent
|
||||
```
|
||||
|
||||
判断基準:
|
||||
|
||||
| 状況 | Saga | EventHandler |
|
||||
|------|------|--------------|
|
||||
| リソースの取り合いがある | 使う | - |
|
||||
| 補償トランザクションが必要 | 使う | - |
|
||||
| 競合しない単純な連携 | - | 使う |
|
||||
| 失敗時は再試行で十分 | - | 使う |
|
||||
|
||||
アンチパターン:
|
||||
```kotlin
|
||||
// NG - ライフサイクル管理のためにSagaを使う
|
||||
@Saga
|
||||
class OrderLifecycleSaga {
|
||||
// 注文の全状態遷移をSagaで追跡
|
||||
// PLACED → CONFIRMED → SHIPPED → DELIVERED
|
||||
}
|
||||
|
||||
// OK - 結果整合性が必要な操作だけをSagaで処理
|
||||
@Saga
|
||||
class InventoryReservationSaga {
|
||||
// 在庫確保の同時実行制御のみ
|
||||
}
|
||||
```
|
||||
|
||||
Sagaはライフサイクル管理ツールではない。結果整合性が必要な「操作」単位で作成する。
|
||||
|
||||
## 例外 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 |
|
||||
|
||||
デフォルトは例外アプローチ。監査要件がある場合のみイベントを検討する。
|
||||
|
||||
## 抽象化レベルの評価
|
||||
|
||||
**条件分岐の肥大化検出**
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じif-elseパターンが3箇所以上 | ポリモーフィズムで抽象化 → REJECT |
|
||||
| switch/caseが5分岐以上 | Strategy/Mapパターンを検討 |
|
||||
| イベント種別による分岐が増殖 | イベントハンドラを分離 → REJECT |
|
||||
| Aggregate内の状態分岐が複雑 | State Patternを検討 |
|
||||
|
||||
**抽象度の不一致検出**
|
||||
|
||||
| パターン | 問題 | 修正案 |
|
||||
|---------|------|--------|
|
||||
| CommandHandlerにDB操作詳細 | 責務違反 | Repository層に分離 |
|
||||
| EventHandlerにビジネスロジック | 責務違反 | ドメインサービスに抽出 |
|
||||
| Aggregateに永続化処理 | レイヤー違反 | EventStore経由に変更 |
|
||||
| Projectionに計算ロジック | 保守困難 | 専用サービスに抽出 |
|
||||
|
||||
良い抽象化の例:
|
||||
|
||||
```kotlin
|
||||
// イベント種別による分岐の増殖(NG)
|
||||
@EventHandler
|
||||
fun on(event: DomainEvent) {
|
||||
when (event) {
|
||||
is OrderPlacedEvent -> handleOrderPlaced(event)
|
||||
is OrderConfirmedEvent -> handleOrderConfirmed(event)
|
||||
is OrderShippedEvent -> handleOrderShipped(event)
|
||||
// ...どんどん増える
|
||||
}
|
||||
}
|
||||
|
||||
// イベントごとにハンドラを分離(OK)
|
||||
@EventHandler
|
||||
fun on(event: OrderPlacedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderConfirmedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderShippedEvent) { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// 状態による分岐が複雑(NG)
|
||||
fun process(command: ProcessCommand) {
|
||||
when (status) {
|
||||
PENDING -> if (command.type == "approve") { ... } else if (command.type == "reject") { ... }
|
||||
APPROVED -> if (command.type == "ship") { ... }
|
||||
// ...複雑化
|
||||
}
|
||||
}
|
||||
|
||||
// State Patternで抽象化(OK)
|
||||
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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## アンチパターン検出
|
||||
|
||||
以下を見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| CRUD偽装 | CQRSの形だけ真似てCRUD実装 |
|
||||
| Anemic Domain Model | Aggregateが単なるデータ構造 |
|
||||
| Event Soup | 意味のないイベントが乱発される |
|
||||
| Temporal Coupling | イベント順序に暗黙の依存 |
|
||||
| Missing Events | 重要なドメインイベントが欠落 |
|
||||
| God Aggregate | 1つのAggregateに全責務が集中 |
|
||||
|
||||
## テスト戦略
|
||||
|
||||
レイヤーごとにテスト方針を分ける。
|
||||
|
||||
テストピラミッド:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 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の非同期処理を考慮している | 必須 |
|
||||
|
||||
## インフラ層
|
||||
|
||||
確認事項:
|
||||
- イベントストアの選択は適切か
|
||||
- メッセージング基盤は要件を満たすか
|
||||
- スナップショット戦略は定義されているか
|
||||
- イベントのシリアライズ形式は適切か
|
||||
497
resources/global/ja/knowledge/frontend.md
Normal file
497
resources/global/ja/knowledge/frontend.md
Normal file
@ -0,0 +1,497 @@
|
||||
# フロントエンド専門知識
|
||||
|
||||
## コンポーネント設計
|
||||
|
||||
1ファイルにベタ書きしない。必ずコンポーネント分割する。
|
||||
|
||||
分離が必須なケース:
|
||||
- 独自のstateを持つ → 必ず分離
|
||||
- 50行超のJSX → 分離
|
||||
- 再利用可能 → 分離
|
||||
- 責務が複数 → 分離
|
||||
- ページ内の独立したセクション → 分離
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 1コンポーネント200行超 | 分割を検討 |
|
||||
| 1コンポーネント300行超 | REJECT |
|
||||
| 表示とロジックが混在 | 分離を検討 |
|
||||
| Props drilling(3階層以上) | 状態管理の導入を検討 |
|
||||
| 複数の責務を持つコンポーネント | REJECT |
|
||||
|
||||
良いコンポーネント:
|
||||
- 単一責務: 1つのことをうまくやる
|
||||
- 自己完結: 必要な依存が明確
|
||||
- テスト可能: 副作用が分離されている
|
||||
|
||||
コンポーネント分類:
|
||||
|
||||
| 種類 | 責務 | 例 |
|
||||
|------|------|-----|
|
||||
| Container | データ取得・状態管理 | `UserListContainer` |
|
||||
| Presentational | 表示のみ | `UserCard` |
|
||||
| Layout | 配置・構造 | `PageLayout`, `Grid` |
|
||||
| Utility | 共通機能 | `ErrorBoundary`, `Portal` |
|
||||
|
||||
ディレクトリ構成:
|
||||
```
|
||||
features/{feature-name}/
|
||||
├── components/
|
||||
│ ├── {feature}-view.tsx # メインビュー(子を組み合わせる)
|
||||
│ ├── {sub-component}.tsx # サブコンポーネント
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
## 状態管理
|
||||
|
||||
子コンポーネントは自身で状態を変更しない。イベントを親にバブリングし、親が状態を操作する。
|
||||
|
||||
```tsx
|
||||
// 子が自分で状態を変更(NG)
|
||||
const ChildBad = ({ initialValue }: { initialValue: string }) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
return <input value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// 親が状態を管理、子はコールバックで通知(OK)
|
||||
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専用の一時状態(ホバー、フォーカス、アニメーション)
|
||||
- 親に伝える必要がない完全にローカルな状態
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 不要なグローバル状態 | ローカル化を検討 |
|
||||
| 同じ状態が複数箇所で管理 | 正規化が必要 |
|
||||
| 子から親への状態変更(逆方向データフロー) | REJECT |
|
||||
| APIレスポンスをそのまま状態に | 正規化を検討 |
|
||||
| useEffectの依存配列が不適切 | REJECT |
|
||||
|
||||
状態配置の判断基準:
|
||||
|
||||
| 状態の性質 | 推奨配置 |
|
||||
|-----------|---------|
|
||||
| UIの一時的な状態(モーダル開閉等) | ローカル(useState) |
|
||||
| フォームの入力値 | ローカル or フォームライブラリ |
|
||||
| 複数コンポーネントで共有 | Context or 状態管理ライブラリ |
|
||||
| サーバーデータのキャッシュ | TanStack Query等のデータフェッチライブラリ |
|
||||
|
||||
## データ取得
|
||||
|
||||
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 |
|
||||
|
||||
## 共有コンポーネントと抽象化
|
||||
|
||||
同じパターンの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指定が複雑になる
|
||||
|
||||
## 抽象化レベルの評価
|
||||
|
||||
### 条件分岐の肥大化検出
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じ条件分岐が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} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
## フロントエンドとバックエンドの責務分離
|
||||
|
||||
### 表示形式の責務
|
||||
|
||||
バックエンドは「データ」を返し、フロントエンドが「表示形式」に変換する。
|
||||
|
||||
```tsx
|
||||
// フロントエンド: 表示形式に変換
|
||||
export function formatPrice(amount: number): string {
|
||||
return `¥${amount.toLocaleString()}`
|
||||
}
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return format(date, 'yyyy年M月d日')
|
||||
}
|
||||
```
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| バックエンドが表示用文字列を返している | 設計見直しを提案 |
|
||||
| 同じフォーマット処理が複数箇所にコピペ | ユーティリティ関数に統一 |
|
||||
| コンポーネント内でインラインフォーマット | 関数に抽出 |
|
||||
|
||||
### ドメインロジックの配置(SmartUI排除)
|
||||
|
||||
ドメインロジック(ビジネスルール)はバックエンドに配置。フロントエンドは状態の表示・編集のみ。
|
||||
|
||||
ドメインロジックとは:
|
||||
- 集約のビジネスルール(在庫判定、価格計算、ステータス遷移)
|
||||
- バリデーション(業務制約の検証)
|
||||
- 不変条件の保証
|
||||
|
||||
フロントエンドの責務:
|
||||
- サーバーから受け取った状態を表示
|
||||
- ユーザー入力を収集し、コマンドとしてバックエンドに送信
|
||||
- UI専用の一時状態管理(フォーカス、ホバー、モーダル開閉)
|
||||
- 表示形式の変換(フォーマット、ソート、フィルタ)
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| フロントエンドで価格計算・在庫判定 | バックエンドに移動 → REJECT |
|
||||
| フロントエンドでステータス遷移ルール | バックエンドに移動 → REJECT |
|
||||
| フロントエンドでビジネスバリデーション | バックエンドに移動 → REJECT |
|
||||
| サーバー側で計算可能な値をフロントで再計算 | 冗長 → REJECT |
|
||||
|
||||
良い例 vs 悪い例:
|
||||
|
||||
```tsx
|
||||
// BAD - フロントエンドでビジネスルール
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
const totalPrice = order.items.reduce((sum, item) =>
|
||||
sum + item.price * item.quantity, 0
|
||||
)
|
||||
const canCheckout = totalPrice >= 1000 && order.items.every(i => i.stock > 0)
|
||||
|
||||
return <button disabled={!canCheckout}>注文確定</button>
|
||||
}
|
||||
|
||||
// GOOD - バックエンドから受け取った状態を表示
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
// totalPrice, canCheckout はサーバーから受け取る
|
||||
return (
|
||||
<>
|
||||
<div>{formatPrice(order.totalPrice)}</div>
|
||||
<button disabled={!order.canCheckout}>注文確定</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// BAD - フロントエンドでステータス遷移判定
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
const canStart = task.status === 'pending' && task.assignee !== null
|
||||
const canComplete = task.status === 'in_progress' && /* 複雑な条件... */
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>開始</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>完了</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// GOOD - サーバーが許可するアクションを返す
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
// task.allowedActions = ['start', 'cancel'] など、サーバーが計算
|
||||
const canStart = task.allowedActions.includes('start')
|
||||
const canComplete = task.allowedActions.includes('complete')
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>開始</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>完了</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
例外(フロントエンドにロジックを置いてもOK):
|
||||
|
||||
| ケース | 理由 |
|
||||
|--------|------|
|
||||
| UI専用バリデーション | 「必須入力」「文字数制限」等のUXフィードバック(サーバー側でも検証必須) |
|
||||
| クライアント側フィルタ/ソート | サーバーから受け取ったリストの表示順序変更 |
|
||||
| 表示条件の分岐 | 「ログイン済みなら詳細表示」等のUI制御 |
|
||||
| リアルタイムフィードバック | 入力中のプレビュー表示 |
|
||||
|
||||
判断基準: 「この計算結果がサーバーとズレたら業務が壊れるか?」
|
||||
- YES → バックエンドに配置(ドメインロジック)
|
||||
- NO → フロントエンドでもOK(表示ロジック)
|
||||
|
||||
## パフォーマンス
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 不要な再レンダリング | 最適化が必要 |
|
||||
| 大きなリストの仮想化なし | 警告 |
|
||||
| 画像の最適化なし | 警告 |
|
||||
| バンドルに未使用コード | tree-shakingを確認 |
|
||||
| メモ化の過剰使用 | 本当に必要か確認 |
|
||||
|
||||
最適化チェックリスト:
|
||||
- `React.memo` / `useMemo` / `useCallback` は適切か
|
||||
- 大きなリストは仮想スクロール対応か
|
||||
- Code Splittingは適切か
|
||||
- 画像はlazy loadingされているか
|
||||
|
||||
アンチパターン:
|
||||
|
||||
```tsx
|
||||
// レンダリングごとに新しいオブジェクト
|
||||
<Child style={{ color: 'red' }} />
|
||||
|
||||
// 定数化 or useMemo
|
||||
const style = useMemo(() => ({ color: 'red' }), []);
|
||||
<Child style={style} />
|
||||
```
|
||||
|
||||
## アクセシビリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| インタラクティブ要素にキーボード対応なし | REJECT |
|
||||
| 画像にalt属性なし | REJECT |
|
||||
| フォーム要素にlabelなし | REJECT |
|
||||
| 色だけで情報を伝達 | REJECT |
|
||||
| フォーカス管理の欠如(モーダル等) | REJECT |
|
||||
|
||||
チェックリスト:
|
||||
- セマンティックHTMLを使用しているか
|
||||
- ARIA属性は適切か(過剰でないか)
|
||||
- キーボードナビゲーション可能か
|
||||
- スクリーンリーダーで意味が通じるか
|
||||
- カラーコントラストは十分か
|
||||
|
||||
## TypeScript/型安全性
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| `any` 型の使用 | REJECT |
|
||||
| 型アサーション(as)の乱用 | 要検討 |
|
||||
| Props型定義なし | REJECT |
|
||||
| イベントハンドラの型が不適切 | 修正が必要 |
|
||||
|
||||
## フロントエンドセキュリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| dangerouslySetInnerHTML使用 | XSSリスクを確認 |
|
||||
| ユーザー入力の未サニタイズ | REJECT |
|
||||
| 機密情報のフロントエンド保存 | REJECT |
|
||||
| CSRFトークンの未使用 | 要確認 |
|
||||
|
||||
## テスタビリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| data-testid等の未付与 | 警告 |
|
||||
| テスト困難な構造 | 分離を検討 |
|
||||
| ビジネスロジックのUIへの埋め込み | REJECT |
|
||||
|
||||
## アンチパターン検出
|
||||
|
||||
以下を見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| God Component | 1コンポーネントに全機能が集中 |
|
||||
| Prop Drilling | 深いPropsバケツリレー |
|
||||
| Inline Styles乱用 | 保守性低下 |
|
||||
| useEffect地獄 | 依存関係が複雑すぎる |
|
||||
| Premature Optimization | 不要なメモ化 |
|
||||
| Magic Strings | ハードコードされた文字列 |
|
||||
| Hidden Dependencies | 子コンポーネントの隠れたAPI呼び出し |
|
||||
| Over-generalization | 無理やり汎用化したコンポーネント |
|
||||
164
resources/global/ja/knowledge/security.md
Normal file
164
resources/global/ja/knowledge/security.md
Normal file
@ -0,0 +1,164 @@
|
||||
# セキュリティ知識
|
||||
|
||||
## AI生成コードのセキュリティ問題
|
||||
|
||||
AI生成コードには特有の脆弱性パターンがある。
|
||||
|
||||
| パターン | リスク | 例 |
|
||||
|---------|--------|-----|
|
||||
| もっともらしいが危険なデフォルト | 高 | `cors: { origin: '*' }` は問題なく見えるが危険 |
|
||||
| 古いセキュリティプラクティス | 中 | 非推奨の暗号化、古い認証パターンの使用 |
|
||||
| 不完全なバリデーション | 高 | 形式は検証するがビジネスルールを検証しない |
|
||||
| 入力を過度に信頼 | 重大 | 内部APIは常に安全と仮定 |
|
||||
| コピペによる脆弱性 | 高 | 同じ危険なパターンが複数ファイルで繰り返される |
|
||||
|
||||
特に厳しく審査が必要:
|
||||
- 認証・認可ロジック(AIはエッジケースを見落としがち)
|
||||
- 入力バリデーション(AIは構文を検証しても意味を見落とす可能性)
|
||||
- エラーメッセージ(AIは内部詳細を露出する可能性)
|
||||
- 設定ファイル(AIは学習データから危険なデフォルトを使う可能性)
|
||||
|
||||
## インジェクション攻撃
|
||||
|
||||
**SQLインジェクション**
|
||||
|
||||
- 文字列連結によるSQL構築 → REJECT
|
||||
- パラメータ化クエリの不使用 → REJECT
|
||||
- ORMの raw query での未サニタイズ入力 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
db.query(`SELECT * FROM users WHERE id = ${userId}`)
|
||||
|
||||
// OK
|
||||
db.query('SELECT * FROM users WHERE id = ?', [userId])
|
||||
```
|
||||
|
||||
**コマンドインジェクション**
|
||||
|
||||
- `exec()`, `spawn()` での未検証入力 → REJECT
|
||||
- シェルコマンド構築時のエスケープ不足 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
exec(`ls ${userInput}`)
|
||||
|
||||
// OK
|
||||
execFile('ls', [sanitizedInput])
|
||||
```
|
||||
|
||||
**XSS (Cross-Site Scripting)**
|
||||
|
||||
- HTML/JSへの未エスケープ出力 → REJECT
|
||||
- `innerHTML`, `dangerouslySetInnerHTML` の不適切な使用 → REJECT
|
||||
- URLパラメータの直接埋め込み → REJECT
|
||||
|
||||
## 認証・認可
|
||||
|
||||
**認証の問題**
|
||||
|
||||
- ハードコードされたクレデンシャル → 即REJECT
|
||||
- 平文パスワードの保存 → 即REJECT
|
||||
- 弱いハッシュアルゴリズム (MD5, SHA1) → REJECT
|
||||
- セッショントークンの不適切な管理 → REJECT
|
||||
|
||||
**認可の問題**
|
||||
|
||||
- 権限チェックの欠如 → REJECT
|
||||
- IDOR (Insecure Direct Object Reference) → REJECT
|
||||
- 権限昇格の可能性 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG - 権限チェックなし
|
||||
app.get('/user/:id', (req, res) => {
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
|
||||
// OK
|
||||
app.get('/user/:id', authorize('read:user'), (req, res) => {
|
||||
if (req.user.id !== req.params.id && !req.user.isAdmin) {
|
||||
return res.status(403).send('Forbidden')
|
||||
}
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
```
|
||||
|
||||
## データ保護
|
||||
|
||||
**機密情報の露出**
|
||||
|
||||
- APIキー、シークレットのハードコーディング → 即REJECT
|
||||
- ログへの機密情報出力 → REJECT
|
||||
- エラーメッセージでの内部情報露出 → REJECT
|
||||
- `.env` ファイルのコミット → REJECT
|
||||
|
||||
**データ検証**
|
||||
|
||||
- 入力値の未検証 → REJECT
|
||||
- 型チェックの欠如 → REJECT
|
||||
- サイズ制限の未設定 → REJECT
|
||||
|
||||
## 暗号化
|
||||
|
||||
- 弱い暗号アルゴリズムの使用 → REJECT
|
||||
- 固定IV/Nonceの使用 → REJECT
|
||||
- 暗号化キーのハードコーディング → 即REJECT
|
||||
- HTTPSの未使用(本番環境) → REJECT
|
||||
|
||||
## ファイル操作
|
||||
|
||||
**パストラバーサル**
|
||||
|
||||
- ユーザー入力を含むファイルパス → REJECT
|
||||
- `../` のサニタイズ不足 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
const filePath = path.join(baseDir, userInput)
|
||||
fs.readFile(filePath)
|
||||
|
||||
// OK
|
||||
const safePath = path.resolve(baseDir, userInput)
|
||||
if (!safePath.startsWith(path.resolve(baseDir))) {
|
||||
throw new Error('Invalid path')
|
||||
}
|
||||
```
|
||||
|
||||
**ファイルアップロード**
|
||||
|
||||
- ファイルタイプの未検証 → REJECT
|
||||
- ファイルサイズ制限なし → REJECT
|
||||
- 実行可能ファイルのアップロード許可 → REJECT
|
||||
|
||||
## 依存関係
|
||||
|
||||
- 既知の脆弱性を持つパッケージ → REJECT
|
||||
- メンテナンスされていないパッケージ → 警告
|
||||
- 不必要な依存関係 → 警告
|
||||
|
||||
## エラーハンドリング
|
||||
|
||||
- スタックトレースの本番露出 → REJECT
|
||||
- 詳細なエラーメッセージの露出 → REJECT
|
||||
- エラーの握りつぶし(セキュリティイベント) → REJECT
|
||||
|
||||
## レート制限・DoS対策
|
||||
|
||||
- レート制限の欠如(認証エンドポイント) → 警告
|
||||
- リソース枯渇攻撃の可能性 → 警告
|
||||
- 無限ループの可能性 → REJECT
|
||||
|
||||
## OWASP Top 10 チェックリスト
|
||||
|
||||
| カテゴリ | 確認事項 |
|
||||
|---------|---------|
|
||||
| A01 Broken Access Control | 認可チェック、CORS設定 |
|
||||
| A02 Cryptographic Failures | 暗号化、機密データ保護 |
|
||||
| A03 Injection | SQL, コマンド, XSS |
|
||||
| A04 Insecure Design | セキュリティ設計パターン |
|
||||
| A05 Security Misconfiguration | デフォルト設定、不要な機能 |
|
||||
| A06 Vulnerable Components | 依存関係の脆弱性 |
|
||||
| A07 Auth Failures | 認証メカニズム |
|
||||
| A08 Software Integrity | コード署名、CI/CD |
|
||||
| A09 Logging Failures | セキュリティログ |
|
||||
| A10 SSRF | サーバーサイドリクエスト |
|
||||
@ -24,431 +24,3 @@
|
||||
- 軽微な問題でも後に持ち越さない。今修正できる問題は今修正させる
|
||||
- 「条件付き承認」はしない。問題があれば差し戻す
|
||||
- 既存コードの踏襲を理由にした問題の放置は認めない
|
||||
|
||||
## ドメイン知識
|
||||
|
||||
### 構造・設計
|
||||
|
||||
**ファイル分割**
|
||||
|
||||
| 基準 | 判定 |
|
||||
|--------------|------|
|
||||
| 1ファイル200行超 | 分割を検討 |
|
||||
| 1ファイル300行超 | REJECT |
|
||||
| 1ファイルに複数の責務 | REJECT |
|
||||
| 関連性の低いコードが同居 | REJECT |
|
||||
|
||||
**モジュール構成**
|
||||
|
||||
- 高凝集: 関連する機能がまとまっているか
|
||||
- 低結合: モジュール間の依存が最小限か
|
||||
- 循環依存がないか
|
||||
- 適切なディレクトリ階層か
|
||||
|
||||
**関数設計**
|
||||
|
||||
- 1関数1責務になっているか
|
||||
- 30行を超える関数は分割を検討
|
||||
- 副作用が明確か
|
||||
|
||||
**レイヤー設計**
|
||||
|
||||
- 依存の方向: 上位層 → 下位層(逆方向禁止)
|
||||
- Controller → Service → Repository の流れが守られているか
|
||||
- 1インターフェース = 1責務(巨大なServiceクラス禁止)
|
||||
|
||||
**ディレクトリ構造**
|
||||
|
||||
構造パターンの選択:
|
||||
|
||||
| パターン | 適用場面 | 例 |
|
||||
|---------|---------|-----|
|
||||
| レイヤード | 小規模、CRUD中心 | `controllers/`, `services/`, `repositories/` |
|
||||
| Vertical Slice | 中〜大規模、機能独立性が高い | `features/auth/`, `features/order/` |
|
||||
| ハイブリッド | 共通基盤 + 機能モジュール | `core/` + `features/` |
|
||||
|
||||
Vertical Slice Architecture(機能単位でコードをまとめる構造):
|
||||
|
||||
```
|
||||
src/
|
||||
├── features/
|
||||
│ ├── auth/
|
||||
│ │ ├── LoginCommand.ts
|
||||
│ │ ├── LoginHandler.ts
|
||||
│ │ ├── AuthRepository.ts
|
||||
│ │ └── auth.test.ts
|
||||
│ └── order/
|
||||
│ ├── CreateOrderCommand.ts
|
||||
│ ├── CreateOrderHandler.ts
|
||||
│ └── ...
|
||||
└── shared/ # 複数featureで共有
|
||||
├── database/
|
||||
└── middleware/
|
||||
```
|
||||
|
||||
Vertical Slice の判定基準:
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 1機能が3ファイル以上のレイヤーに跨る | Slice化を検討 |
|
||||
| 機能間の依存がほぼない | Slice化推奨 |
|
||||
| 共通処理が50%以上 | レイヤード維持 |
|
||||
| チームが機能別に分かれている | Slice化必須 |
|
||||
|
||||
禁止パターン:
|
||||
|
||||
| パターン | 問題 |
|
||||
|---------|------|
|
||||
| `utils/` の肥大化 | 責務不明の墓場になる |
|
||||
| `common/` への安易な配置 | 依存関係が不明確になる |
|
||||
| 深すぎるネスト(4階層超) | ナビゲーション困難 |
|
||||
| 機能とレイヤーの混在 | `features/services/` は禁止 |
|
||||
|
||||
**責務の分離**
|
||||
|
||||
- 読み取りと書き込みの責務が分かれているか
|
||||
- データ取得はルート(View/Controller)で行い、子に渡しているか
|
||||
- エラーハンドリングが一元化されているか(各所でtry-catch禁止)
|
||||
- ビジネスロジックがController/Viewに漏れていないか
|
||||
|
||||
### コード品質の検出手法
|
||||
|
||||
**説明コメント(What/How)の検出基準**
|
||||
|
||||
コードの動作をそのまま言い換えているコメントを検出する。
|
||||
|
||||
| 判定 | 基準 |
|
||||
|------|------|
|
||||
| REJECT | コードの動作をそのまま自然言語で言い換えている |
|
||||
| REJECT | 関数名・変数名から明らかなことを繰り返している |
|
||||
| REJECT | JSDocが関数名の言い換えだけで情報を追加していない |
|
||||
| OK | なぜその実装を選んだかの設計判断を説明している |
|
||||
| OK | 一見不自然に見える挙動の理由を説明している |
|
||||
| 最良 | コメントなしでコード自体が意図を語っている |
|
||||
|
||||
```typescript
|
||||
// REJECT - コードの言い換え(What)
|
||||
// If interrupted, abort immediately
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// REJECT - ループの存在を言い換えただけ
|
||||
// Check transitions in order
|
||||
for (const transition of step.transitions) {
|
||||
|
||||
// REJECT - 関数名の繰り返し
|
||||
/** Check if status matches transition condition. */
|
||||
export function matchesCondition(status: Status, condition: TransitionCondition): boolean {
|
||||
|
||||
// OK - 設計判断の理由(Why)
|
||||
// ユーザー中断はピース定義のトランジションより優先する
|
||||
if (status === 'interrupted') {
|
||||
return ABORT_STEP;
|
||||
}
|
||||
|
||||
// OK - 一見不自然な挙動の理由
|
||||
// stay はループを引き起こす可能性があるが、ユーザーが明示的に指定した場合のみ使われる
|
||||
return step.name;
|
||||
```
|
||||
|
||||
**状態の直接変更の検出基準**
|
||||
|
||||
配列やオブジェクトの直接変更(ミューテーション)を検出する。
|
||||
|
||||
```typescript
|
||||
// REJECT - 配列の直接変更
|
||||
const steps: Step[] = getSteps();
|
||||
steps.push(newStep); // 元の配列を破壊
|
||||
steps.splice(index, 1); // 元の配列を破壊
|
||||
steps[0].status = 'done'; // ネストされたオブジェクトも直接変更
|
||||
|
||||
// OK - イミュータブルな操作
|
||||
const withNew = [...steps, newStep];
|
||||
const without = steps.filter((_, i) => i !== index);
|
||||
const updated = steps.map((s, i) =>
|
||||
i === 0 ? { ...s, status: 'done' } : s
|
||||
);
|
||||
|
||||
// REJECT - オブジェクトの直接変更
|
||||
function updateConfig(config: Config) {
|
||||
config.logLevel = 'debug'; // 引数を直接変更
|
||||
config.steps.push(newStep); // ネストも直接変更
|
||||
return config;
|
||||
}
|
||||
|
||||
// OK - 新しいオブジェクトを返す
|
||||
function updateConfig(config: Config): Config {
|
||||
return {
|
||||
...config,
|
||||
logLevel: 'debug',
|
||||
steps: [...config.steps, newStep],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### セキュリティ(基本チェック)
|
||||
|
||||
- インジェクション対策(SQL, コマンド, XSS)
|
||||
- ユーザー入力の検証
|
||||
- 機密情報のハードコーディング
|
||||
|
||||
### テスタビリティ
|
||||
|
||||
- 依存性注入が可能な設計か
|
||||
- モック可能か
|
||||
- テストが書かれているか
|
||||
|
||||
### アンチパターン検出
|
||||
|
||||
以下のパターンを見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| God Class/Component | 1つのクラスが多くの責務を持っている |
|
||||
| Feature Envy | 他モジュールのデータを頻繁に参照している |
|
||||
| Shotgun Surgery | 1つの変更が複数ファイルに波及する構造 |
|
||||
| 過度な汎用化 | 今使わないバリアントや拡張ポイント |
|
||||
| 隠れた依存 | 子コンポーネントが暗黙的にAPIを呼ぶ等 |
|
||||
| 非イディオマティック | 言語・FWの作法を無視した独自実装 |
|
||||
|
||||
### 抽象化レベルの評価
|
||||
|
||||
**条件分岐の肥大化検出**
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じ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); // 詳細は隠蔽
|
||||
}
|
||||
```
|
||||
|
||||
### その場しのぎの検出
|
||||
|
||||
「とりあえず動かす」ための妥協を見逃さない。
|
||||
|
||||
| パターン | 例 |
|
||||
|---------|-----|
|
||||
| 不要なパッケージ追加 | 動かすためだけに入れた謎のライブラリ |
|
||||
| テストの削除・スキップ | `@Disabled`、`.skip()`、コメントアウト |
|
||||
| 空実装・スタブ放置 | `return null`、`// TODO: implement`、`pass` |
|
||||
| モックデータの本番混入 | ハードコードされたダミーデータ |
|
||||
| エラー握りつぶし | 空の `catch {}`、`rescue nil` |
|
||||
| マジックナンバー | 説明なしの `if (status == 3)` |
|
||||
|
||||
### TODOコメントの厳格な禁止
|
||||
|
||||
「将来やる」は決してやらない。今やらないことは永遠にやらない。
|
||||
|
||||
TODOコメントは即REJECT。
|
||||
|
||||
```kotlin
|
||||
// REJECT - 将来を見越したTODO
|
||||
// TODO: 施設IDによる認可チェックを追加
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
|
||||
// APPROVE - 今実装する
|
||||
fun deleteCustomHoliday(@PathVariable id: String) {
|
||||
val currentUserFacilityId = getCurrentUserFacilityId()
|
||||
val holiday = findHolidayById(id)
|
||||
require(holiday.facilityId == currentUserFacilityId) {
|
||||
"Cannot delete holiday from another facility"
|
||||
}
|
||||
deleteCustomHolidayInputPort.execute(input)
|
||||
}
|
||||
```
|
||||
|
||||
TODOが許容される唯一のケース:
|
||||
|
||||
| 条件 | 例 | 判定 |
|
||||
|------|-----|------|
|
||||
| 外部依存で今は実装不可 + Issue化済み | `// TODO(#123): APIキー取得後に実装` | 許容 |
|
||||
| 技術的制約で回避不可 + Issue化済み | `// TODO(#456): ライブラリバグ修正待ち` | 許容 |
|
||||
| 「将来実装」「後で追加」 | `// TODO: バリデーション追加` | REJECT |
|
||||
| 「時間がないので」 | `// TODO: リファクタリング` | REJECT |
|
||||
|
||||
正しい対処:
|
||||
- 今必要 → 今実装する
|
||||
- 今不要 → コードを削除する
|
||||
- 外部要因で不可 → Issue化してチケット番号をコメントに入れる
|
||||
|
||||
### DRY違反の検出
|
||||
|
||||
重複コードを検出する。
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じロジックが3箇所以上 | 即REJECT - 関数/メソッドに抽出 |
|
||||
| 同じバリデーションが2箇所以上 | 即REJECT - バリデーター関数に抽出 |
|
||||
| 似たようなコンポーネントが3個以上 | 即REJECT - 共通コンポーネント化 |
|
||||
| コピペで派生したコード | 即REJECT - パラメータ化または抽象化 |
|
||||
|
||||
AHA原則(Avoid Hasty Abstractions)とのバランス:
|
||||
- 2回の重複 → 様子見
|
||||
- 3回の重複 → 即抽出
|
||||
- ドメインが異なる重複 → 抽象化しない(例: 顧客用バリデーションと管理者用バリデーションは別物)
|
||||
|
||||
### 仕様準拠の検証
|
||||
|
||||
変更が、プロジェクトの文書化された仕様に準拠しているか検証する。
|
||||
|
||||
検証対象:
|
||||
|
||||
| 対象 | 確認内容 |
|
||||
|------|---------|
|
||||
| CLAUDE.md / README.md | スキーマ定義、設計原則、制約に従っているか |
|
||||
| 型定義・Zodスキーマ | 新しいフィールドがスキーマに反映されているか |
|
||||
| YAML/JSON設定ファイル | 文書化されたフォーマットに従っているか |
|
||||
|
||||
具体的なチェック:
|
||||
|
||||
1. 設定ファイル(YAML等)を変更・追加した場合:
|
||||
- CLAUDE.md等に記載されたスキーマ定義と突合する
|
||||
- 無視されるフィールドや無効なフィールドが含まれていないか
|
||||
- 必須フィールドが欠落していないか
|
||||
|
||||
2. 型定義やインターフェースを変更した場合:
|
||||
- ドキュメントのスキーマ説明が更新されているか
|
||||
- 既存の設定ファイルが新しいスキーマと整合するか
|
||||
|
||||
このパターンを見つけたら REJECT:
|
||||
|
||||
| パターン | 問題 |
|
||||
|---------|------|
|
||||
| 仕様に存在しないフィールドの使用 | 無視されるか予期しない動作 |
|
||||
| 仕様上無効な値の設定 | 実行時エラーまたは無視される |
|
||||
| 文書化された制約への違反 | 設計意図に反する |
|
||||
|
||||
### 呼び出しチェーン検証
|
||||
|
||||
新しいパラメータ・フィールドが追加された場合、変更ファイル内だけでなく呼び出し元も検証する。
|
||||
|
||||
検証手順:
|
||||
1. 新しいオプショナルパラメータや interface フィールドを見つけたら、`Grep` で全呼び出し元を検索
|
||||
2. 全呼び出し元が新しいパラメータを渡しているか確認
|
||||
3. フォールバック値(`?? default`)がある場合、フォールバックが使われるケースが意図通りか確認
|
||||
|
||||
危険パターン:
|
||||
|
||||
| パターン | 問題 | 検出方法 |
|
||||
|---------|------|---------|
|
||||
| `options.xxx ?? fallback` で全呼び出し元が `xxx` を省略 | 機能が実装されているのに常にフォールバック | grep で呼び出し元を確認 |
|
||||
| テストがモックで直接値をセット | 実際の呼び出しチェーンを経由しない | テストの構築方法を確認 |
|
||||
| `executeXxx()` が内部で使う `options` を引数で受け取らない | 上位から値を渡す口がない | 関数シグネチャを確認 |
|
||||
|
||||
```typescript
|
||||
// 配線漏れ: projectCwd を受け取る口がない
|
||||
export async function executePiece(config, cwd, task) {
|
||||
const engine = new PieceEngine(config, cwd, task); // options なし
|
||||
}
|
||||
|
||||
// 配線済み: projectCwd を渡せる
|
||||
export async function executePiece(config, cwd, task, options?) {
|
||||
const engine = new PieceEngine(config, cwd, task, options);
|
||||
}
|
||||
```
|
||||
|
||||
呼び出し元の制約による論理的デッドコード:
|
||||
|
||||
呼び出しチェーンの検証は「配線漏れ」だけでなく、逆方向——呼び出し元が既に保証している条件に対する不要な防御コード——にも適用する。
|
||||
|
||||
| パターン | 問題 | 検出方法 |
|
||||
|---------|------|---------|
|
||||
| 呼び出し元がTTY必須なのに関数内でTTYチェック | 到達しない分岐が残る | grep で全呼び出し元の前提条件を確認 |
|
||||
| 呼び出し元がnullチェック済みなのに再度nullガード | 冗長な防御 | 呼び出し元の制約を追跡 |
|
||||
| 呼び出し元が型で制約しているのにランタイムチェック | 型安全を信頼していない | TypeScriptの型制約を確認 |
|
||||
|
||||
検証手順:
|
||||
1. 防御的な条件分岐(TTYチェック、nullガード等)を見つけたら、grep で全呼び出し元を確認
|
||||
2. 全呼び出し元がその条件を既に保証しているなら、防御は不要 → REJECT
|
||||
3. 一部の呼び出し元が保証していない場合は、防御を残す
|
||||
|
||||
### 品質特性
|
||||
|
||||
| 特性 | 確認観点 |
|
||||
|------|---------|
|
||||
| Scalability | 負荷増加に対応できる設計か |
|
||||
| Maintainability | 変更・修正が容易か |
|
||||
| Observability | ログ・監視が可能な設計か |
|
||||
|
||||
### 大局観
|
||||
|
||||
細かい「クリーンコード」の指摘に終始しない。
|
||||
|
||||
確認すべきこと:
|
||||
- このコードは将来どう変化するか
|
||||
- スケーリングの必要性は考慮されているか
|
||||
- 技術的負債を生んでいないか
|
||||
- ビジネス要件と整合しているか
|
||||
- 命名がドメインと一貫しているか
|
||||
|
||||
### 変更スコープの評価
|
||||
|
||||
変更スコープを確認し、レポートに記載する(ブロッキングではない)。
|
||||
|
||||
| スコープサイズ | 変更行数 | 対応 |
|
||||
|---------------|---------|------|
|
||||
| Small | 〜200行 | そのままレビュー |
|
||||
| Medium | 200-500行 | そのままレビュー |
|
||||
| Large | 500行以上 | レビューは継続。分割可能か提案を付記 |
|
||||
|
||||
大きな変更が必要なタスクもある。行数だけでREJECTしない。
|
||||
|
||||
確認すること:
|
||||
- 変更が論理的にまとまっているか(無関係な変更が混在していないか)
|
||||
- Coderのスコープ宣言と実際の変更が一致しているか
|
||||
|
||||
提案として記載すること(ブロッキングではない):
|
||||
- 分割可能な場合は分割案を提示
|
||||
|
||||
@ -26,421 +26,3 @@
|
||||
- 読み取りと書き込みは本質的に異なる関心事であり、無理に統合しない
|
||||
- 形だけのCQRSを見逃さない。CRUDをCommand/Queryに分けただけでは意味がない
|
||||
- シンプルなCRUDで十分なケースにCQRS+ESを強制しない
|
||||
|
||||
## ドメイン知識
|
||||
|
||||
### Aggregate設計
|
||||
|
||||
Aggregateは判断に必要なフィールドのみ保持する。
|
||||
|
||||
Command Model(Aggregate)の役割は「コマンドを受けて判断し、イベントを発行する」こと。クエリ用データはRead Model(Projection)が担当する。
|
||||
|
||||
「判断に必要」とは:
|
||||
- `if`/`require`の条件分岐に使う
|
||||
- インスタンスメソッドでイベント発行時にフィールド値を参照する
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| Aggregateが複数のトランザクション境界を跨ぐ | REJECT |
|
||||
| Aggregate間の直接参照(ID参照でない) | REJECT |
|
||||
| Aggregateが100行を超える | 分割を検討 |
|
||||
| ビジネス不変条件がAggregate外にある | REJECT |
|
||||
| 判断に使わないフィールドを保持 | REJECT |
|
||||
|
||||
良い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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 判断に使わないフィールドを保持(NG)
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### イベント設計
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| イベントが過去形でない(Created → Create) | REJECT |
|
||||
| イベントにロジックが含まれる | REJECT |
|
||||
| イベントが他Aggregateの内部状態を含む | REJECT |
|
||||
| イベントのスキーマがバージョン管理されていない | 警告 |
|
||||
| CRUDスタイルのイベント(Updated, Deleted) | 要検討 |
|
||||
|
||||
良いイベント:
|
||||
```kotlin
|
||||
// Good: ドメインの意図が明確
|
||||
OrderPlaced, PaymentReceived, ItemShipped
|
||||
|
||||
// Bad: CRUDスタイル
|
||||
OrderUpdated, OrderDeleted
|
||||
```
|
||||
|
||||
イベント粒度:
|
||||
- 細かすぎ: `OrderFieldChanged` → ドメインの意図が不明
|
||||
- 適切: `ShippingAddressChanged` → 意図が明確
|
||||
- 粗すぎ: `OrderModified` → 何が変わったか不明
|
||||
|
||||
### コマンドハンドラ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| ハンドラがDBを直接操作 | REJECT |
|
||||
| ハンドラが複数Aggregateを変更 | REJECT |
|
||||
| コマンドのバリデーションがない | REJECT |
|
||||
| ハンドラがクエリを実行して判断 | 要検討 |
|
||||
|
||||
良いコマンドハンドラ:
|
||||
```
|
||||
1. コマンドを受け取る
|
||||
2. Aggregateをイベントストアから復元
|
||||
3. Aggregateにコマンドを適用
|
||||
4. 発行されたイベントを保存
|
||||
```
|
||||
|
||||
### プロジェクション設計
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| プロジェクションがコマンドを発行 | REJECT |
|
||||
| プロジェクションがWriteモデルを参照 | REJECT |
|
||||
| 複数のユースケースを1つのプロジェクションで賄う | 要検討 |
|
||||
| リビルド不可能な設計 | REJECT |
|
||||
|
||||
良いプロジェクション:
|
||||
- 特定の読み取りユースケースに最適化
|
||||
- イベントから冪等に再構築可能
|
||||
- Writeモデルから完全に独立
|
||||
|
||||
### 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
|
||||
```
|
||||
|
||||
### 結果整合性
|
||||
|
||||
| 状況 | 対応 |
|
||||
|------|------|
|
||||
| UIが即座に更新を期待している | 設計見直し or ポーリング/WebSocket |
|
||||
| 整合性遅延が許容範囲を超える | アーキテクチャ再検討 |
|
||||
| 補償トランザクションが未定義 | 障害シナリオの検討を要求 |
|
||||
|
||||
### Saga vs EventHandler
|
||||
|
||||
Sagaは「競合が発生する複数アグリゲート間の操作」にのみ使用する。
|
||||
|
||||
Sagaが必要なケース:
|
||||
```
|
||||
複数のアクターが同じリソースを取り合う場合
|
||||
例: 在庫確保(10人が同時に同じ商品を注文)
|
||||
|
||||
OrderPlacedEvent
|
||||
↓ InventoryReservationSaga
|
||||
ReserveInventoryCommand → Inventory集約(同時実行を直列化)
|
||||
↓
|
||||
InventoryReservedEvent → ConfirmOrderCommand
|
||||
InventoryReservationFailedEvent → CancelOrderCommand
|
||||
```
|
||||
|
||||
Sagaが不要なケース:
|
||||
```
|
||||
競合が発生しない操作
|
||||
例: 注文キャンセル時の在庫解放
|
||||
|
||||
OrderCancelledEvent
|
||||
↓ InventoryReleaseHandler(単純なEventHandler)
|
||||
ReleaseInventoryCommand
|
||||
↓
|
||||
InventoryReleasedEvent
|
||||
```
|
||||
|
||||
判断基準:
|
||||
|
||||
| 状況 | Saga | EventHandler |
|
||||
|------|------|--------------|
|
||||
| リソースの取り合いがある | 使う | - |
|
||||
| 補償トランザクションが必要 | 使う | - |
|
||||
| 競合しない単純な連携 | - | 使う |
|
||||
| 失敗時は再試行で十分 | - | 使う |
|
||||
|
||||
アンチパターン:
|
||||
```kotlin
|
||||
// NG - ライフサイクル管理のためにSagaを使う
|
||||
@Saga
|
||||
class OrderLifecycleSaga {
|
||||
// 注文の全状態遷移をSagaで追跡
|
||||
// PLACED → CONFIRMED → SHIPPED → DELIVERED
|
||||
}
|
||||
|
||||
// OK - 結果整合性が必要な操作だけをSagaで処理
|
||||
@Saga
|
||||
class InventoryReservationSaga {
|
||||
// 在庫確保の同時実行制御のみ
|
||||
}
|
||||
```
|
||||
|
||||
Sagaはライフサイクル管理ツールではない。結果整合性が必要な「操作」単位で作成する。
|
||||
|
||||
### 例外 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 |
|
||||
|
||||
デフォルトは例外アプローチ。監査要件がある場合のみイベントを検討する。
|
||||
|
||||
### 抽象化レベルの評価
|
||||
|
||||
**条件分岐の肥大化検出**
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じif-elseパターンが3箇所以上 | ポリモーフィズムで抽象化 → REJECT |
|
||||
| switch/caseが5分岐以上 | Strategy/Mapパターンを検討 |
|
||||
| イベント種別による分岐が増殖 | イベントハンドラを分離 → REJECT |
|
||||
| Aggregate内の状態分岐が複雑 | State Patternを検討 |
|
||||
|
||||
**抽象度の不一致検出**
|
||||
|
||||
| パターン | 問題 | 修正案 |
|
||||
|---------|------|--------|
|
||||
| CommandHandlerにDB操作詳細 | 責務違反 | Repository層に分離 |
|
||||
| EventHandlerにビジネスロジック | 責務違反 | ドメインサービスに抽出 |
|
||||
| Aggregateに永続化処理 | レイヤー違反 | EventStore経由に変更 |
|
||||
| Projectionに計算ロジック | 保守困難 | 専用サービスに抽出 |
|
||||
|
||||
良い抽象化の例:
|
||||
|
||||
```kotlin
|
||||
// イベント種別による分岐の増殖(NG)
|
||||
@EventHandler
|
||||
fun on(event: DomainEvent) {
|
||||
when (event) {
|
||||
is OrderPlacedEvent -> handleOrderPlaced(event)
|
||||
is OrderConfirmedEvent -> handleOrderConfirmed(event)
|
||||
is OrderShippedEvent -> handleOrderShipped(event)
|
||||
// ...どんどん増える
|
||||
}
|
||||
}
|
||||
|
||||
// イベントごとにハンドラを分離(OK)
|
||||
@EventHandler
|
||||
fun on(event: OrderPlacedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderConfirmedEvent) { ... }
|
||||
|
||||
@EventHandler
|
||||
fun on(event: OrderShippedEvent) { ... }
|
||||
```
|
||||
|
||||
```kotlin
|
||||
// 状態による分岐が複雑(NG)
|
||||
fun process(command: ProcessCommand) {
|
||||
when (status) {
|
||||
PENDING -> if (command.type == "approve") { ... } else if (command.type == "reject") { ... }
|
||||
APPROVED -> if (command.type == "ship") { ... }
|
||||
// ...複雑化
|
||||
}
|
||||
}
|
||||
|
||||
// State Patternで抽象化(OK)
|
||||
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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### アンチパターン検出
|
||||
|
||||
以下を見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| CRUD偽装 | CQRSの形だけ真似てCRUD実装 |
|
||||
| Anemic Domain Model | Aggregateが単なるデータ構造 |
|
||||
| Event Soup | 意味のないイベントが乱発される |
|
||||
| Temporal Coupling | イベント順序に暗黙の依存 |
|
||||
| Missing Events | 重要なドメインイベントが欠落 |
|
||||
| God Aggregate | 1つのAggregateに全責務が集中 |
|
||||
|
||||
### テスト戦略
|
||||
|
||||
レイヤーごとにテスト方針を分ける。
|
||||
|
||||
テストピラミッド:
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 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の非同期処理を考慮している | 必須 |
|
||||
|
||||
### インフラ層
|
||||
|
||||
確認事項:
|
||||
- イベントストアの選択は適切か
|
||||
- メッセージング基盤は要件を満たすか
|
||||
- スナップショット戦略は定義されているか
|
||||
- イベントのシリアライズ形式は適切か
|
||||
|
||||
@ -27,501 +27,3 @@
|
||||
- アクセシビリティは後付け困難。最初から組み込む
|
||||
- 過度な抽象化を警戒。シンプルに保つ
|
||||
- フレームワークの作法に従う。独自パターンより標準的なアプローチ
|
||||
|
||||
## ドメイン知識
|
||||
|
||||
### コンポーネント設計
|
||||
|
||||
1ファイルにベタ書きしない。必ずコンポーネント分割する。
|
||||
|
||||
分離が必須なケース:
|
||||
- 独自のstateを持つ → 必ず分離
|
||||
- 50行超のJSX → 分離
|
||||
- 再利用可能 → 分離
|
||||
- 責務が複数 → 分離
|
||||
- ページ内の独立したセクション → 分離
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 1コンポーネント200行超 | 分割を検討 |
|
||||
| 1コンポーネント300行超 | REJECT |
|
||||
| 表示とロジックが混在 | 分離を検討 |
|
||||
| Props drilling(3階層以上) | 状態管理の導入を検討 |
|
||||
| 複数の責務を持つコンポーネント | REJECT |
|
||||
|
||||
良いコンポーネント:
|
||||
- 単一責務: 1つのことをうまくやる
|
||||
- 自己完結: 必要な依存が明確
|
||||
- テスト可能: 副作用が分離されている
|
||||
|
||||
コンポーネント分類:
|
||||
|
||||
| 種類 | 責務 | 例 |
|
||||
|------|------|-----|
|
||||
| Container | データ取得・状態管理 | `UserListContainer` |
|
||||
| Presentational | 表示のみ | `UserCard` |
|
||||
| Layout | 配置・構造 | `PageLayout`, `Grid` |
|
||||
| Utility | 共通機能 | `ErrorBoundary`, `Portal` |
|
||||
|
||||
ディレクトリ構成:
|
||||
```
|
||||
features/{feature-name}/
|
||||
├── components/
|
||||
│ ├── {feature}-view.tsx # メインビュー(子を組み合わせる)
|
||||
│ ├── {sub-component}.tsx # サブコンポーネント
|
||||
│ └── index.ts
|
||||
├── hooks/
|
||||
├── types.ts
|
||||
└── index.ts
|
||||
```
|
||||
|
||||
### 状態管理
|
||||
|
||||
子コンポーネントは自身で状態を変更しない。イベントを親にバブリングし、親が状態を操作する。
|
||||
|
||||
```tsx
|
||||
// 子が自分で状態を変更(NG)
|
||||
const ChildBad = ({ initialValue }: { initialValue: string }) => {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
return <input value={value} onChange={e => setValue(e.target.value)} />
|
||||
}
|
||||
|
||||
// 親が状態を管理、子はコールバックで通知(OK)
|
||||
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専用の一時状態(ホバー、フォーカス、アニメーション)
|
||||
- 親に伝える必要がない完全にローカルな状態
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 不要なグローバル状態 | ローカル化を検討 |
|
||||
| 同じ状態が複数箇所で管理 | 正規化が必要 |
|
||||
| 子から親への状態変更(逆方向データフロー) | REJECT |
|
||||
| APIレスポンスをそのまま状態に | 正規化を検討 |
|
||||
| useEffectの依存配列が不適切 | REJECT |
|
||||
|
||||
状態配置の判断基準:
|
||||
|
||||
| 状態の性質 | 推奨配置 |
|
||||
|-----------|---------|
|
||||
| UIの一時的な状態(モーダル開閉等) | ローカル(useState) |
|
||||
| フォームの入力値 | ローカル or フォームライブラリ |
|
||||
| 複数コンポーネントで共有 | Context or 状態管理ライブラリ |
|
||||
| サーバーデータのキャッシュ | TanStack Query等のデータフェッチライブラリ |
|
||||
|
||||
### データ取得
|
||||
|
||||
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 |
|
||||
|
||||
### 共有コンポーネントと抽象化
|
||||
|
||||
同じパターンの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指定が複雑になる
|
||||
|
||||
### 抽象化レベルの評価
|
||||
|
||||
**条件分岐の肥大化検出**
|
||||
|
||||
| パターン | 判定 |
|
||||
|---------|------|
|
||||
| 同じ条件分岐が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} />
|
||||
))
|
||||
}
|
||||
```
|
||||
|
||||
### フロントエンドとバックエンドの責務分離
|
||||
|
||||
**表示形式の責務**
|
||||
|
||||
バックエンドは「データ」を返し、フロントエンドが「表示形式」に変換する。
|
||||
|
||||
```tsx
|
||||
// フロントエンド: 表示形式に変換
|
||||
export function formatPrice(amount: number): string {
|
||||
return `¥${amount.toLocaleString()}`
|
||||
}
|
||||
|
||||
export function formatDate(date: Date): string {
|
||||
return format(date, 'yyyy年M月d日')
|
||||
}
|
||||
```
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| バックエンドが表示用文字列を返している | 設計見直しを提案 |
|
||||
| 同じフォーマット処理が複数箇所にコピペ | ユーティリティ関数に統一 |
|
||||
| コンポーネント内でインラインフォーマット | 関数に抽出 |
|
||||
|
||||
**ドメインロジックの配置(SmartUI排除)**
|
||||
|
||||
ドメインロジック(ビジネスルール)はバックエンドに配置。フロントエンドは状態の表示・編集のみ。
|
||||
|
||||
ドメインロジックとは:
|
||||
- 集約のビジネスルール(在庫判定、価格計算、ステータス遷移)
|
||||
- バリデーション(業務制約の検証)
|
||||
- 不変条件の保証
|
||||
|
||||
フロントエンドの責務:
|
||||
- サーバーから受け取った状態を表示
|
||||
- ユーザー入力を収集し、コマンドとしてバックエンドに送信
|
||||
- UI専用の一時状態管理(フォーカス、ホバー、モーダル開閉)
|
||||
- 表示形式の変換(フォーマット、ソート、フィルタ)
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| フロントエンドで価格計算・在庫判定 | バックエンドに移動 → REJECT |
|
||||
| フロントエンドでステータス遷移ルール | バックエンドに移動 → REJECT |
|
||||
| フロントエンドでビジネスバリデーション | バックエンドに移動 → REJECT |
|
||||
| サーバー側で計算可能な値をフロントで再計算 | 冗長 → REJECT |
|
||||
|
||||
良い例 vs 悪い例:
|
||||
|
||||
```tsx
|
||||
// BAD - フロントエンドでビジネスルール
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
const totalPrice = order.items.reduce((sum, item) =>
|
||||
sum + item.price * item.quantity, 0
|
||||
)
|
||||
const canCheckout = totalPrice >= 1000 && order.items.every(i => i.stock > 0)
|
||||
|
||||
return <button disabled={!canCheckout}>注文確定</button>
|
||||
}
|
||||
|
||||
// GOOD - バックエンドから受け取った状態を表示
|
||||
function OrderForm({ order }: { order: Order }) {
|
||||
// totalPrice, canCheckout はサーバーから受け取る
|
||||
return (
|
||||
<>
|
||||
<div>{formatPrice(order.totalPrice)}</div>
|
||||
<button disabled={!order.canCheckout}>注文確定</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
// BAD - フロントエンドでステータス遷移判定
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
const canStart = task.status === 'pending' && task.assignee !== null
|
||||
const canComplete = task.status === 'in_progress' && /* 複雑な条件... */
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>開始</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>完了</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
// GOOD - サーバーが許可するアクションを返す
|
||||
function TaskCard({ task }: { task: Task }) {
|
||||
// task.allowedActions = ['start', 'cancel'] など、サーバーが計算
|
||||
const canStart = task.allowedActions.includes('start')
|
||||
const canComplete = task.allowedActions.includes('complete')
|
||||
|
||||
return (
|
||||
<>
|
||||
<button onClick={startTask} disabled={!canStart}>開始</button>
|
||||
<button onClick={completeTask} disabled={!canComplete}>完了</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
例外(フロントエンドにロジックを置いてもOK):
|
||||
|
||||
| ケース | 理由 |
|
||||
|--------|------|
|
||||
| UI専用バリデーション | 「必須入力」「文字数制限」等のUXフィードバック(サーバー側でも検証必須) |
|
||||
| クライアント側フィルタ/ソート | サーバーから受け取ったリストの表示順序変更 |
|
||||
| 表示条件の分岐 | 「ログイン済みなら詳細表示」等のUI制御 |
|
||||
| リアルタイムフィードバック | 入力中のプレビュー表示 |
|
||||
|
||||
判断基準: 「この計算結果がサーバーとズレたら業務が壊れるか?」
|
||||
- YES → バックエンドに配置(ドメインロジック)
|
||||
- NO → フロントエンドでもOK(表示ロジック)
|
||||
|
||||
### パフォーマンス
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| 不要な再レンダリング | 最適化が必要 |
|
||||
| 大きなリストの仮想化なし | 警告 |
|
||||
| 画像の最適化なし | 警告 |
|
||||
| バンドルに未使用コード | tree-shakingを確認 |
|
||||
| メモ化の過剰使用 | 本当に必要か確認 |
|
||||
|
||||
最適化チェックリスト:
|
||||
- `React.memo` / `useMemo` / `useCallback` は適切か
|
||||
- 大きなリストは仮想スクロール対応か
|
||||
- Code Splittingは適切か
|
||||
- 画像はlazy loadingされているか
|
||||
|
||||
アンチパターン:
|
||||
|
||||
```tsx
|
||||
// レンダリングごとに新しいオブジェクト
|
||||
<Child style={{ color: 'red' }} />
|
||||
|
||||
// 定数化 or useMemo
|
||||
const style = useMemo(() => ({ color: 'red' }), []);
|
||||
<Child style={style} />
|
||||
```
|
||||
|
||||
### アクセシビリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| インタラクティブ要素にキーボード対応なし | REJECT |
|
||||
| 画像にalt属性なし | REJECT |
|
||||
| フォーム要素にlabelなし | REJECT |
|
||||
| 色だけで情報を伝達 | REJECT |
|
||||
| フォーカス管理の欠如(モーダル等) | REJECT |
|
||||
|
||||
チェックリスト:
|
||||
- セマンティックHTMLを使用しているか
|
||||
- ARIA属性は適切か(過剰でないか)
|
||||
- キーボードナビゲーション可能か
|
||||
- スクリーンリーダーで意味が通じるか
|
||||
- カラーコントラストは十分か
|
||||
|
||||
### TypeScript/型安全性
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| `any` 型の使用 | REJECT |
|
||||
| 型アサーション(as)の乱用 | 要検討 |
|
||||
| Props型定義なし | REJECT |
|
||||
| イベントハンドラの型が不適切 | 修正が必要 |
|
||||
|
||||
### フロントエンドセキュリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| dangerouslySetInnerHTML使用 | XSSリスクを確認 |
|
||||
| ユーザー入力の未サニタイズ | REJECT |
|
||||
| 機密情報のフロントエンド保存 | REJECT |
|
||||
| CSRFトークンの未使用 | 要確認 |
|
||||
|
||||
### テスタビリティ
|
||||
|
||||
| 基準 | 判定 |
|
||||
|------|------|
|
||||
| data-testid等の未付与 | 警告 |
|
||||
| テスト困難な構造 | 分離を検討 |
|
||||
| ビジネスロジックのUIへの埋め込み | REJECT |
|
||||
|
||||
### アンチパターン検出
|
||||
|
||||
以下を見つけたら REJECT:
|
||||
|
||||
| アンチパターン | 問題 |
|
||||
|---------------|------|
|
||||
| God Component | 1コンポーネントに全機能が集中 |
|
||||
| Prop Drilling | 深いPropsバケツリレー |
|
||||
| Inline Styles乱用 | 保守性低下 |
|
||||
| useEffect地獄 | 依存関係が複雑すぎる |
|
||||
| Premature Optimization | 不要なメモ化 |
|
||||
| Magic Strings | ハードコードされた文字列 |
|
||||
| Hidden Dependencies | 子コンポーネントの隠れたAPI呼び出し |
|
||||
| Over-generalization | 無理やり汎用化したコンポーネント |
|
||||
|
||||
@ -24,168 +24,3 @@
|
||||
- 「信頼しない、検証する」が基本原則
|
||||
- 1つの脆弱性がシステム全体を危険にさらす。見逃しは許されない
|
||||
- AI生成コードには特有の脆弱性パターンがある。特に厳しく審査する
|
||||
|
||||
## ドメイン知識
|
||||
|
||||
### AI生成コードのセキュリティ問題
|
||||
|
||||
AI生成コードには特有の脆弱性パターンがある。
|
||||
|
||||
| パターン | リスク | 例 |
|
||||
|---------|--------|-----|
|
||||
| もっともらしいが危険なデフォルト | 高 | `cors: { origin: '*' }` は問題なく見えるが危険 |
|
||||
| 古いセキュリティプラクティス | 中 | 非推奨の暗号化、古い認証パターンの使用 |
|
||||
| 不完全なバリデーション | 高 | 形式は検証するがビジネスルールを検証しない |
|
||||
| 入力を過度に信頼 | 重大 | 内部APIは常に安全と仮定 |
|
||||
| コピペによる脆弱性 | 高 | 同じ危険なパターンが複数ファイルで繰り返される |
|
||||
|
||||
特に厳しく審査が必要:
|
||||
- 認証・認可ロジック(AIはエッジケースを見落としがち)
|
||||
- 入力バリデーション(AIは構文を検証しても意味を見落とす可能性)
|
||||
- エラーメッセージ(AIは内部詳細を露出する可能性)
|
||||
- 設定ファイル(AIは学習データから危険なデフォルトを使う可能性)
|
||||
|
||||
### インジェクション攻撃
|
||||
|
||||
**SQLインジェクション**
|
||||
|
||||
- 文字列連結によるSQL構築 → REJECT
|
||||
- パラメータ化クエリの不使用 → REJECT
|
||||
- ORMの raw query での未サニタイズ入力 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
db.query(`SELECT * FROM users WHERE id = ${userId}`)
|
||||
|
||||
// OK
|
||||
db.query('SELECT * FROM users WHERE id = ?', [userId])
|
||||
```
|
||||
|
||||
**コマンドインジェクション**
|
||||
|
||||
- `exec()`, `spawn()` での未検証入力 → REJECT
|
||||
- シェルコマンド構築時のエスケープ不足 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
exec(`ls ${userInput}`)
|
||||
|
||||
// OK
|
||||
execFile('ls', [sanitizedInput])
|
||||
```
|
||||
|
||||
**XSS (Cross-Site Scripting)**
|
||||
|
||||
- HTML/JSへの未エスケープ出力 → REJECT
|
||||
- `innerHTML`, `dangerouslySetInnerHTML` の不適切な使用 → REJECT
|
||||
- URLパラメータの直接埋め込み → REJECT
|
||||
|
||||
### 認証・認可
|
||||
|
||||
**認証の問題**
|
||||
|
||||
- ハードコードされたクレデンシャル → 即REJECT
|
||||
- 平文パスワードの保存 → 即REJECT
|
||||
- 弱いハッシュアルゴリズム (MD5, SHA1) → REJECT
|
||||
- セッショントークンの不適切な管理 → REJECT
|
||||
|
||||
**認可の問題**
|
||||
|
||||
- 権限チェックの欠如 → REJECT
|
||||
- IDOR (Insecure Direct Object Reference) → REJECT
|
||||
- 権限昇格の可能性 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG - 権限チェックなし
|
||||
app.get('/user/:id', (req, res) => {
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
|
||||
// OK
|
||||
app.get('/user/:id', authorize('read:user'), (req, res) => {
|
||||
if (req.user.id !== req.params.id && !req.user.isAdmin) {
|
||||
return res.status(403).send('Forbidden')
|
||||
}
|
||||
return db.getUser(req.params.id)
|
||||
})
|
||||
```
|
||||
|
||||
### データ保護
|
||||
|
||||
**機密情報の露出**
|
||||
|
||||
- APIキー、シークレットのハードコーディング → 即REJECT
|
||||
- ログへの機密情報出力 → REJECT
|
||||
- エラーメッセージでの内部情報露出 → REJECT
|
||||
- `.env` ファイルのコミット → REJECT
|
||||
|
||||
**データ検証**
|
||||
|
||||
- 入力値の未検証 → REJECT
|
||||
- 型チェックの欠如 → REJECT
|
||||
- サイズ制限の未設定 → REJECT
|
||||
|
||||
### 暗号化
|
||||
|
||||
- 弱い暗号アルゴリズムの使用 → REJECT
|
||||
- 固定IV/Nonceの使用 → REJECT
|
||||
- 暗号化キーのハードコーディング → 即REJECT
|
||||
- HTTPSの未使用(本番環境) → REJECT
|
||||
|
||||
### ファイル操作
|
||||
|
||||
**パストラバーサル**
|
||||
|
||||
- ユーザー入力を含むファイルパス → REJECT
|
||||
- `../` のサニタイズ不足 → REJECT
|
||||
|
||||
```typescript
|
||||
// NG
|
||||
const filePath = path.join(baseDir, userInput)
|
||||
fs.readFile(filePath)
|
||||
|
||||
// OK
|
||||
const safePath = path.resolve(baseDir, userInput)
|
||||
if (!safePath.startsWith(path.resolve(baseDir))) {
|
||||
throw new Error('Invalid path')
|
||||
}
|
||||
```
|
||||
|
||||
**ファイルアップロード**
|
||||
|
||||
- ファイルタイプの未検証 → REJECT
|
||||
- ファイルサイズ制限なし → REJECT
|
||||
- 実行可能ファイルのアップロード許可 → REJECT
|
||||
|
||||
### 依存関係
|
||||
|
||||
- 既知の脆弱性を持つパッケージ → REJECT
|
||||
- メンテナンスされていないパッケージ → 警告
|
||||
- 不必要な依存関係 → 警告
|
||||
|
||||
### エラーハンドリング
|
||||
|
||||
- スタックトレースの本番露出 → REJECT
|
||||
- 詳細なエラーメッセージの露出 → REJECT
|
||||
- エラーの握りつぶし(セキュリティイベント) → REJECT
|
||||
|
||||
### レート制限・DoS対策
|
||||
|
||||
- レート制限の欠如(認証エンドポイント) → 警告
|
||||
- リソース枯渇攻撃の可能性 → 警告
|
||||
- 無限ループの可能性 → REJECT
|
||||
|
||||
### OWASP Top 10 チェックリスト
|
||||
|
||||
| カテゴリ | 確認事項 |
|
||||
|---------|---------|
|
||||
| A01 Broken Access Control | 認可チェック、CORS設定 |
|
||||
| A02 Cryptographic Failures | 暗号化、機密データ保護 |
|
||||
| A03 Injection | SQL, コマンド, XSS |
|
||||
| A04 Insecure Design | セキュリティ設計パターン |
|
||||
| A05 Security Misconfiguration | デフォルト設定、不要な機能 |
|
||||
| A06 Vulnerable Components | 依存関係の脆弱性 |
|
||||
| A07 Auth Failures | 認証メカニズム |
|
||||
| A08 Software Integrity | コード署名、CI/CD |
|
||||
| A09 Logging Failures | セキュリティログ |
|
||||
| A10 SSRF | サーバーサイドリクエスト |
|
||||
|
||||
@ -20,6 +20,9 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
architecture: ../knowledge/architecture.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
architect-planner: ../personas/architect-planner.md
|
||||
@ -224,6 +227,7 @@ movements:
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
stance: review
|
||||
knowledge: architecture
|
||||
report:
|
||||
name: 05-architect-review.md
|
||||
format: architecture-review
|
||||
|
||||
@ -37,6 +37,11 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
cqrs-es: ../knowledge/cqrs-es.md
|
||||
security: ../knowledge/security.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
@ -205,6 +210,7 @@ movements:
|
||||
edit: false
|
||||
persona: cqrs-es-reviewer
|
||||
stance: review
|
||||
knowledge: cqrs-es
|
||||
report:
|
||||
name: 04-cqrs-es-review.md
|
||||
format: cqrs-es-review
|
||||
@ -224,6 +230,7 @@ movements:
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
stance: review
|
||||
knowledge: frontend
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
@ -243,6 +250,7 @@ movements:
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
stance: review
|
||||
knowledge: security
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: security-review
|
||||
|
||||
@ -28,6 +28,11 @@ stances:
|
||||
review: ../stances/review.md
|
||||
testing: ../stances/testing.md
|
||||
|
||||
knowledge:
|
||||
frontend: ../knowledge/frontend.md
|
||||
security: ../knowledge/security.md
|
||||
architecture: ../knowledge/architecture.md
|
||||
|
||||
personas:
|
||||
planner: ../personas/planner.md
|
||||
coder: ../personas/coder.md
|
||||
@ -195,6 +200,7 @@ movements:
|
||||
edit: false
|
||||
persona: architecture-reviewer
|
||||
stance: review
|
||||
knowledge: architecture
|
||||
report:
|
||||
name: 04-architect-review.md
|
||||
format: architecture-review
|
||||
@ -214,6 +220,7 @@ movements:
|
||||
edit: false
|
||||
persona: frontend-reviewer
|
||||
stance: review
|
||||
knowledge: frontend
|
||||
report:
|
||||
name: 05-frontend-review.md
|
||||
format: frontend-review
|
||||
@ -233,6 +240,7 @@ movements:
|
||||
edit: false
|
||||
persona: security-reviewer
|
||||
stance: review
|
||||
knowledge: security
|
||||
report:
|
||||
name: 06-security-review.md
|
||||
format: security-review
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user