@relipa/ai-flow-kit 0.0.5 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -69
- package/bin/aiflow.js +46 -2
- package/custom/rules/java/spring-boot-examples.md +329 -0
- package/custom/skills/read-study-requirement/SKILL.md +54 -0
- package/custom/skills/review-plan/SKILL.md +21 -0
- package/custom/templates/shared/gate-workflow.md +88 -88
- package/custom/templates/spring-boot.md +3 -302
- package/docs/common/AIFLOW.md +501 -462
- package/docs/common/CHANGELOG.md +162 -132
- package/docs/common/QUICK_START.md +44 -12
- package/docs/common/cli-reference.md +115 -6
- package/docs/common/getting-started.md +3 -0
- package/docs/project/ARCHITECTURE.md +28 -28
- package/package.json +6 -1
- package/scripts/doctor.js +52 -11
- package/scripts/guide.js +28 -5
- package/scripts/hooks/session-start.js +244 -145
- package/scripts/init.js +109 -50
- package/scripts/prompt.js +431 -431
- package/scripts/task.js +384 -0
- package/scripts/telemetry/cli.js +243 -243
- package/scripts/telemetry/config.js +91 -91
- package/scripts/telemetry/crypto.js +20 -20
- package/scripts/telemetry/flush.js +162 -162
- package/scripts/telemetry/record.js +138 -138
- package/scripts/use.js +94 -2
|
@@ -20,6 +20,27 @@ keywords: review, code review, approve, summary, checklist
|
|
|
20
20
|
- ✅ `superpowers:verification-before-completion` has run — tests pass
|
|
21
21
|
- ✅ `impact-analysis` skill has run
|
|
22
22
|
|
|
23
|
+
## Mode Selection
|
|
24
|
+
|
|
25
|
+
Check `mode` in `.aiflow/context/current.json`:
|
|
26
|
+
|
|
27
|
+
| Mode | Impact Analysis | Review Checklist | summary.md |
|
|
28
|
+
|------|----------------|-----------------|-----------|
|
|
29
|
+
| `fast` | Quick scan — reason from diff only | 3 items | Short format |
|
|
30
|
+
| `full` | Full grep scan across codebase | 10 items | Full format |
|
|
31
|
+
|
|
32
|
+
### Fast Track Impact Analysis (mode: fast)
|
|
33
|
+
Assess impact by reasoning about the changed files only:
|
|
34
|
+
1. List each file you modified
|
|
35
|
+
2. For each file: "Does this file have callers/dependents outside the ticket scope?"
|
|
36
|
+
3. If yes → list them and assign impact level
|
|
37
|
+
4. DO NOT run grep on the full codebase
|
|
38
|
+
|
|
39
|
+
### Fast Track Review Checklist (mode: fast)
|
|
40
|
+
- [ ] All tests pass
|
|
41
|
+
- [ ] No hardcoded credentials or secrets
|
|
42
|
+
- [ ] No SQL injection risk in new queries
|
|
43
|
+
|
|
23
44
|
---
|
|
24
45
|
|
|
25
46
|
## Process
|
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
## MANDATORY: Strict Gate Workflow
|
|
2
|
-
|
|
3
|
-
> **Absolute Rule: Complete Gate N before entering Gate N+1.**
|
|
4
|
-
> **Do not skip, shorten, or merge Gates.**
|
|
5
|
-
|
|
6
|
-
You have superpowers. When a ticket context exists in `.aiflow/context/current.json`:
|
|
7
|
-
- **Consult the "AI Skill Registry"** below to find instructions for each skill (`SKILL.md`).
|
|
8
|
-
- **AUTO-START Gate 1 immediately** — do NOT wait for the developer to ask.
|
|
9
|
-
- Read instructions and follow the gate sequence below — NO EXCEPTIONS.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
### GATE 1 — AI Analyze Requirement (auto-start)
|
|
14
|
-
|
|
15
|
-
**INVOKE:** `read-study-requirement` skill
|
|
16
|
-
|
|
17
|
-
AI actively reads ticket + source code to understand the requirement:
|
|
18
|
-
1. Read `.aiflow/context/current.json` — ticket info
|
|
19
|
-
2. Read source code — architecture, related files, data flow
|
|
20
|
-
3. If anything is unclear — ask ONE question at a time, wait for reply
|
|
21
|
-
4. Output `plan/[ticket-id]/requirement.md` with:
|
|
22
|
-
- Requirements summary, source code analysis
|
|
23
|
-
- Proposed solution and approach
|
|
24
|
-
- Impact analysis, effort estimate, testing plan
|
|
25
|
-
5. Display "GATE 1: Requirement doc ready" → wait for **APPROVED**
|
|
26
|
-
|
|
27
|
-
> **Tip:** If auto-start doesn't trigger, the developer can start this gate by typing: **"start"**, **"Gate 1"** or **"Analyze ticket"**.
|
|
28
|
-
|
|
29
|
-
DO NOT just check format — **understand the content and propose solutions**.
|
|
30
|
-
|
|
31
|
-
> **Telemetry:** Run `aiflow gate 1 start --ticket [ticket-id]` when starting this gate (auto-start or when developer types "start"/"Gate 1").
|
|
32
|
-
> Run `aiflow gate 1 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
### GATE 2 — Implementation Plan (wait for APPROVED)
|
|
37
|
-
|
|
38
|
-
**INVOKE:** `generate-spec` skill, then `superpowers:writing-plans`
|
|
39
|
-
|
|
40
|
-
- Create a detailed TDD implementation plan based on the approved requirement.
|
|
41
|
-
- Display: "GATE 2 PAUSED: type APPROVED to start coding".
|
|
42
|
-
- CODE WILL NOT BE GENERATED until "APPROVED" is received.
|
|
43
|
-
|
|
44
|
-
> **Telemetry:** Run `aiflow gate 2 start --ticket [ticket-id]` when starting this gate.
|
|
45
|
-
> Run `aiflow gate 2 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
46
|
-
|
|
47
|
-
---
|
|
48
|
-
|
|
49
|
-
### GATE 3 — Code Generation (TDD only)
|
|
50
|
-
|
|
51
|
-
Only runs after Gate 2 has been APPROVED.
|
|
52
|
-
|
|
53
|
-
**INVOKE:** `superpowers:test-driven-development`
|
|
54
|
-
- Complex feature (3+ files): `superpowers:subagent-driven-development`
|
|
55
|
-
- Write tests FIRST — run to confirm FAIL -> implement -> PASS.
|
|
56
|
-
- Bug fix EXTRA: `superpowers:systematic-debugging` + `investigate-bug` skill first.
|
|
57
|
-
|
|
58
|
-
> **Telemetry:** Run `aiflow gate 3 start --ticket [ticket-id]` when starting this gate.
|
|
59
|
-
|
|
60
|
-
---
|
|
61
|
-
|
|
62
|
-
### GATE 4 — AI Self-Review (wait for APPROVED)
|
|
63
|
-
|
|
64
|
-
**INVOKE:** `review-plan` skill
|
|
65
|
-
|
|
66
|
-
Mandatory order:
|
|
67
|
-
1. `superpowers:verification-before-completion` — all tests must PASS
|
|
68
|
-
2. `impact-analysis` skill — check for breaking changes
|
|
69
|
-
3. Tick `custom/rules/review-checklist.md`
|
|
70
|
-
4. Create `plan/[ticket-id]/summary.md`
|
|
71
|
-
|
|
72
|
-
Then: "GATE 4 PAUSED: type APPROVED or BUG: [description]"
|
|
73
|
-
- Coding bug -> fix -> repeat Gate 4.
|
|
74
|
-
- Requirement bug -> return to Gate 1.
|
|
75
|
-
|
|
76
|
-
> **Telemetry:** Run `aiflow gate 4 start --ticket [ticket-id]` when starting this gate.
|
|
77
|
-
> Run `aiflow gate 4 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
### GATE 5 — Peer Review and Done
|
|
82
|
-
|
|
83
|
-
Only runs after Gate 4 has been APPROVED.
|
|
84
|
-
|
|
85
|
-
**INVOKE:** `superpowers:requesting-code-review`
|
|
86
|
-
Guide on creating a Pull Request with the ticket link.
|
|
87
|
-
|
|
88
|
-
> **Telemetry:** Run `aiflow gate 5 start --ticket [ticket-id]` when starting this gate.
|
|
1
|
+
## MANDATORY: Strict Gate Workflow
|
|
2
|
+
|
|
3
|
+
> **Absolute Rule: Complete Gate N before entering Gate N+1.**
|
|
4
|
+
> **Do not skip, shorten, or merge Gates.**
|
|
5
|
+
|
|
6
|
+
You have superpowers. When a ticket context exists in `.aiflow/context/current.json`:
|
|
7
|
+
- **Consult the "AI Skill Registry"** below to find instructions for each skill (`SKILL.md`).
|
|
8
|
+
- **AUTO-START Gate 1 immediately** — do NOT wait for the developer to ask.
|
|
9
|
+
- Read instructions and follow the gate sequence below — NO EXCEPTIONS.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
### GATE 1 — AI Analyze Requirement (auto-start)
|
|
14
|
+
|
|
15
|
+
**INVOKE:** `read-study-requirement` skill
|
|
16
|
+
|
|
17
|
+
AI actively reads ticket + source code to understand the requirement:
|
|
18
|
+
1. Read `.aiflow/context/current.json` — ticket info
|
|
19
|
+
2. Read source code — architecture, related files, data flow
|
|
20
|
+
3. If anything is unclear — ask ONE question at a time, wait for reply
|
|
21
|
+
4. Output `plan/[ticket-id]/requirement.md` with:
|
|
22
|
+
- Requirements summary, source code analysis
|
|
23
|
+
- Proposed solution and approach
|
|
24
|
+
- Impact analysis, effort estimate, testing plan
|
|
25
|
+
5. Display "GATE 1: Requirement doc ready" → wait for **APPROVED**
|
|
26
|
+
|
|
27
|
+
> **Tip:** If auto-start doesn't trigger, the developer can start this gate by typing: **"start"**, **"Gate 1"** or **"Analyze ticket"**.
|
|
28
|
+
|
|
29
|
+
DO NOT just check format — **understand the content and propose solutions**.
|
|
30
|
+
|
|
31
|
+
> **Telemetry:** Run `aiflow gate 1 start --ticket [ticket-id]` when starting this gate (auto-start or when developer types "start"/"Gate 1").
|
|
32
|
+
> Run `aiflow gate 1 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### GATE 2 — Implementation Plan (wait for APPROVED)
|
|
37
|
+
|
|
38
|
+
**INVOKE:** `generate-spec` skill, then `superpowers:writing-plans`
|
|
39
|
+
|
|
40
|
+
- Create a detailed TDD implementation plan based on the approved requirement.
|
|
41
|
+
- Display: "GATE 2 PAUSED: type APPROVED to start coding".
|
|
42
|
+
- CODE WILL NOT BE GENERATED until "APPROVED" is received.
|
|
43
|
+
|
|
44
|
+
> **Telemetry:** Run `aiflow gate 2 start --ticket [ticket-id]` when starting this gate.
|
|
45
|
+
> Run `aiflow gate 2 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### GATE 3 — Code Generation (TDD only)
|
|
50
|
+
|
|
51
|
+
Only runs after Gate 2 has been APPROVED.
|
|
52
|
+
|
|
53
|
+
**INVOKE:** `superpowers:test-driven-development`
|
|
54
|
+
- Complex feature (3+ files): `superpowers:subagent-driven-development`
|
|
55
|
+
- Write tests FIRST — run to confirm FAIL -> implement -> PASS.
|
|
56
|
+
- Bug fix EXTRA: `superpowers:systematic-debugging` + `investigate-bug` skill first.
|
|
57
|
+
|
|
58
|
+
> **Telemetry:** Run `aiflow gate 3 start --ticket [ticket-id]` when starting this gate.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### GATE 4 — AI Self-Review (wait for APPROVED)
|
|
63
|
+
|
|
64
|
+
**INVOKE:** `review-plan` skill
|
|
65
|
+
|
|
66
|
+
Mandatory order:
|
|
67
|
+
1. `superpowers:verification-before-completion` — all tests must PASS
|
|
68
|
+
2. `impact-analysis` skill — check for breaking changes
|
|
69
|
+
3. Tick `custom/rules/review-checklist.md`
|
|
70
|
+
4. Create `plan/[ticket-id]/summary.md`
|
|
71
|
+
|
|
72
|
+
Then: "GATE 4 PAUSED: type APPROVED or BUG: [description]"
|
|
73
|
+
- Coding bug -> fix -> repeat Gate 4.
|
|
74
|
+
- Requirement bug -> return to Gate 1.
|
|
75
|
+
|
|
76
|
+
> **Telemetry:** Run `aiflow gate 4 start --ticket [ticket-id]` when starting this gate.
|
|
77
|
+
> Run `aiflow gate 4 approved --ticket [ticket-id]` immediately when APPROVED is received.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### GATE 5 — Peer Review and Done
|
|
82
|
+
|
|
83
|
+
Only runs after Gate 4 has been APPROVED.
|
|
84
|
+
|
|
85
|
+
**INVOKE:** `superpowers:requesting-code-review`
|
|
86
|
+
Guide on creating a Pull Request with the ticket link.
|
|
87
|
+
|
|
88
|
+
> **Telemetry:** Run `aiflow gate 5 start --ticket [ticket-id]` when starting this gate.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
You are an expert Java Spring Boot developer. Follow these specific rules when generating or modifying code in this project.
|
|
4
4
|
|
|
5
|
+
> **Code examples:** See `.rules/java/spring-boot-examples.md` — read it when generating code for a specific layer.
|
|
6
|
+
|
|
5
7
|
---
|
|
6
8
|
|
|
7
9
|
## Project Stack
|
|
@@ -63,25 +65,6 @@ com.company.project/
|
|
|
63
65
|
- Use `ResponseEntity<T>` for explicit HTTP status control
|
|
64
66
|
- Validate input with `\@Valid` + Bean Validation annotations
|
|
65
67
|
|
|
66
|
-
```java
|
|
67
|
-
// ✅ Good
|
|
68
|
-
\@PostMapping
|
|
69
|
-
public ResponseEntity<UserResponse> createUser(
|
|
70
|
-
\@Valid \@RequestBody CreateUserRequest request) {
|
|
71
|
-
UserResponse response = userService.create(request);
|
|
72
|
-
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// ❌ Bad — business logic in controller
|
|
76
|
-
\@PostMapping
|
|
77
|
-
public ResponseEntity<User> createUser(\@RequestBody User user) {
|
|
78
|
-
if (userRepository.existsByEmail(user.getEmail())) {
|
|
79
|
-
throw new RuntimeException("Email exists");
|
|
80
|
-
}
|
|
81
|
-
return ResponseEntity.ok(userRepository.save(user));
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
68
|
---
|
|
86
69
|
|
|
87
70
|
## Service Rules
|
|
@@ -93,59 +76,16 @@ public ResponseEntity<User> createUser(\@RequestBody User user) {
|
|
|
93
76
|
- Throw specific custom exceptions, not generic `RuntimeException`
|
|
94
77
|
- Never return an Entity — always convert to DTO via Mapper
|
|
95
78
|
|
|
96
|
-
```java
|
|
97
|
-
// ✅ Good
|
|
98
|
-
public interface UserService {
|
|
99
|
-
UserResponse create(CreateUserRequest request);
|
|
100
|
-
UserResponse findById(Long id);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
\@Service
|
|
104
|
-
\@RequiredArgsConstructor
|
|
105
|
-
public class UserServiceImpl implements UserService {
|
|
106
|
-
|
|
107
|
-
private final UserRepository userRepository;
|
|
108
|
-
private final UserMapper userMapper;
|
|
109
|
-
|
|
110
|
-
\@Override
|
|
111
|
-
\@Transactional
|
|
112
|
-
public UserResponse create(CreateUserRequest request) {
|
|
113
|
-
if (userRepository.existsByEmail(request.getEmail())) {
|
|
114
|
-
throw new DuplicateEmailException(request.getEmail());
|
|
115
|
-
}
|
|
116
|
-
User user = userMapper.toEntity(request);
|
|
117
|
-
User saved = userRepository.save(user);
|
|
118
|
-
return userMapper.toResponse(saved);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
```
|
|
122
|
-
|
|
123
79
|
---
|
|
124
80
|
|
|
125
81
|
## Repository Rules
|
|
126
82
|
|
|
127
|
-
- Extend `JpaRepository<Entity, ID>`
|
|
83
|
+
- Extend `JpaRepository<Entity, ID>`
|
|
128
84
|
- Use **method name queries** for simple queries
|
|
129
85
|
- Use `\@Query` (JPQL) for complex queries — avoid native SQL unless necessary
|
|
130
86
|
- Never add business logic here
|
|
131
87
|
- Use `\@EntityGraph` to solve N+1 problems
|
|
132
88
|
|
|
133
|
-
```java
|
|
134
|
-
// ✅ Good
|
|
135
|
-
public interface UserRepository extends JpaRepository<User, Long> {
|
|
136
|
-
|
|
137
|
-
boolean existsByEmail(String email);
|
|
138
|
-
|
|
139
|
-
Optional<User> findByEmailAndDeletedFalse(String email);
|
|
140
|
-
|
|
141
|
-
\@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
|
142
|
-
Optional<User> findByIdWithRoles(\@Param("id") Long id);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// ❌ Bad — N+1 problem
|
|
146
|
-
List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
|
|
147
|
-
```
|
|
148
|
-
|
|
149
89
|
---
|
|
150
90
|
|
|
151
91
|
## Entity Rules
|
|
@@ -157,36 +97,6 @@ List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
|
|
|
157
97
|
- For soft delete: add `deleted` boolean + `deletedAt` timestamp
|
|
158
98
|
- Extend `BaseEntity` for audit fields (`createdAt`, `updatedAt`)
|
|
159
99
|
|
|
160
|
-
```java
|
|
161
|
-
// ✅ Good
|
|
162
|
-
\@Entity
|
|
163
|
-
\@Table(name = "users")
|
|
164
|
-
\@Getter
|
|
165
|
-
\@Setter
|
|
166
|
-
\@NoArgsConstructor
|
|
167
|
-
\@AllArgsConstructor
|
|
168
|
-
\@Builder
|
|
169
|
-
public class User extends BaseEntity {
|
|
170
|
-
|
|
171
|
-
\@Id
|
|
172
|
-
\@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
173
|
-
private Long id;
|
|
174
|
-
|
|
175
|
-
\@Column(name = "email", nullable = false, unique = true, length = 255)
|
|
176
|
-
private String email;
|
|
177
|
-
|
|
178
|
-
\@Column(name = "full_name", nullable = false, length = 100)
|
|
179
|
-
private String fullName;
|
|
180
|
-
|
|
181
|
-
\@Column(name = "deleted", nullable = false)
|
|
182
|
-
\@Builder.Default
|
|
183
|
-
private boolean deleted = false;
|
|
184
|
-
|
|
185
|
-
\@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
|
186
|
-
private List<Order> orders = new ArrayList<>();
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
100
|
---
|
|
191
101
|
|
|
192
102
|
## DTO Rules
|
|
@@ -196,40 +106,6 @@ public class User extends BaseEntity {
|
|
|
196
106
|
- Validate in the Request DTO with Bean Validation (`\@NotBlank`, `\@Email`, `\@NotNull`, `\@Size`)
|
|
197
107
|
- Never expose internal fields (password hash, audit timestamps) in the Response DTO
|
|
198
108
|
|
|
199
|
-
```java
|
|
200
|
-
// Request DTO
|
|
201
|
-
\@Getter
|
|
202
|
-
\@NoArgsConstructor
|
|
203
|
-
\@AllArgsConstructor
|
|
204
|
-
\@Builder
|
|
205
|
-
public class CreateUserRequest {
|
|
206
|
-
|
|
207
|
-
\@NotBlank(message = "Email is required")
|
|
208
|
-
\@Email(message = "Invalid email format")
|
|
209
|
-
private String email;
|
|
210
|
-
|
|
211
|
-
\@NotBlank(message = "Full name is required")
|
|
212
|
-
\@Size(min = 2, max = 100, message = "Full name must be 2-100 characters")
|
|
213
|
-
private String fullName;
|
|
214
|
-
|
|
215
|
-
\@NotBlank(message = "Password is required")
|
|
216
|
-
\@Size(min = 8, message = "Password must be at least 8 characters")
|
|
217
|
-
private String password;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Response DTO
|
|
221
|
-
\@Getter
|
|
222
|
-
\@NoArgsConstructor
|
|
223
|
-
\@AllArgsConstructor
|
|
224
|
-
\@Builder
|
|
225
|
-
public class UserResponse {
|
|
226
|
-
private Long id;
|
|
227
|
-
private String email;
|
|
228
|
-
private String fullName;
|
|
229
|
-
private LocalDateTime createdAt;
|
|
230
|
-
}
|
|
231
|
-
```
|
|
232
|
-
|
|
233
109
|
---
|
|
234
110
|
|
|
235
111
|
## MapStruct Mapper Rules
|
|
@@ -238,23 +114,6 @@ public class UserResponse {
|
|
|
238
114
|
- Define explicit mappings with `\@Mapping` when field names differ
|
|
239
115
|
- Never do manual mapping (`new DTO(); dto.setField(entity.getField())`)
|
|
240
116
|
|
|
241
|
-
```java
|
|
242
|
-
\@Mapper(componentModel = "spring")
|
|
243
|
-
public interface UserMapper {
|
|
244
|
-
|
|
245
|
-
\@Mapping(target = "createdAt", source = "createdAt")
|
|
246
|
-
UserResponse toResponse(User user);
|
|
247
|
-
|
|
248
|
-
\@Mapping(target = "id", ignore = true)
|
|
249
|
-
\@Mapping(target = "deleted", ignore = true)
|
|
250
|
-
\@Mapping(target = "createdAt", ignore = true)
|
|
251
|
-
\@Mapping(target = "updatedAt", ignore = true)
|
|
252
|
-
User toEntity(CreateUserRequest request);
|
|
253
|
-
|
|
254
|
-
List<UserResponse> toResponseList(List<User> users);
|
|
255
|
-
}
|
|
256
|
-
```
|
|
257
|
-
|
|
258
117
|
---
|
|
259
118
|
|
|
260
119
|
## Exception Handling
|
|
@@ -264,46 +123,6 @@ public interface UserMapper {
|
|
|
264
123
|
- Return a consistent error response format
|
|
265
124
|
- Never expose stack traces to the client
|
|
266
125
|
|
|
267
|
-
```java
|
|
268
|
-
// Custom exception
|
|
269
|
-
public class ResourceNotFoundException extends RuntimeException {
|
|
270
|
-
public ResourceNotFoundException(String resource, Long id) {
|
|
271
|
-
super(String.format("%s with id %d not found", resource, id));
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Global handler
|
|
276
|
-
\@RestControllerAdvice
|
|
277
|
-
\@Slf4j
|
|
278
|
-
public class GlobalExceptionHandler {
|
|
279
|
-
|
|
280
|
-
\@ExceptionHandler(ResourceNotFoundException.class)
|
|
281
|
-
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
|
|
282
|
-
log.warn("Resource not found: {}", ex.getMessage());
|
|
283
|
-
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
284
|
-
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
\@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
288
|
-
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
|
|
289
|
-
List<String> errors = ex.getBindingResult()
|
|
290
|
-
.getFieldErrors()
|
|
291
|
-
.stream()
|
|
292
|
-
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
293
|
-
.toList();
|
|
294
|
-
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
|
295
|
-
.body(ErrorResponse.of("VALIDATION_FAILED", errors.toString()));
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
\@ExceptionHandler(Exception.class)
|
|
299
|
-
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
|
|
300
|
-
log.error("Unexpected error", ex);
|
|
301
|
-
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
302
|
-
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
```
|
|
306
|
-
|
|
307
126
|
---
|
|
308
127
|
|
|
309
128
|
## Testing Rules
|
|
@@ -314,104 +133,11 @@ public class GlobalExceptionHandler {
|
|
|
314
133
|
- Test happy path + edge cases + exception scenarios
|
|
315
134
|
- Use AssertJ: `assertThat(result).isEqualTo(expected)`
|
|
316
135
|
|
|
317
|
-
```java
|
|
318
|
-
\@ExtendWith(MockitoExtension.class)
|
|
319
|
-
class UserServiceImplTest {
|
|
320
|
-
|
|
321
|
-
\@Mock
|
|
322
|
-
private UserRepository userRepository;
|
|
323
|
-
|
|
324
|
-
\@Mock
|
|
325
|
-
private UserMapper userMapper;
|
|
326
|
-
|
|
327
|
-
\@InjectMocks
|
|
328
|
-
private UserServiceImpl userService;
|
|
329
|
-
|
|
330
|
-
\@Test
|
|
331
|
-
void create_shouldReturnUserResponse_whenEmailIsUnique() {
|
|
332
|
-
// Arrange
|
|
333
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
334
|
-
.email("test\@example.com")
|
|
335
|
-
.fullName("Test User")
|
|
336
|
-
.build();
|
|
337
|
-
User user = User.builder().id(1L).email("test\@example.com").build();
|
|
338
|
-
UserResponse expected = UserResponse.builder().id(1L).build();
|
|
339
|
-
|
|
340
|
-
given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
|
|
341
|
-
given(userMapper.toEntity(request)).willReturn(user);
|
|
342
|
-
given(userRepository.save(user)).willReturn(user);
|
|
343
|
-
given(userMapper.toResponse(user)).willReturn(expected);
|
|
344
|
-
|
|
345
|
-
// Act
|
|
346
|
-
UserResponse result = userService.create(request);
|
|
347
|
-
|
|
348
|
-
// Assert
|
|
349
|
-
assertThat(result).isEqualTo(expected);
|
|
350
|
-
verify(userRepository).save(user);
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
\@Test
|
|
354
|
-
void create_shouldThrowDuplicateEmailException_whenEmailAlreadyExists() {
|
|
355
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
356
|
-
.email("existing\@example.com").build();
|
|
357
|
-
given(userRepository.existsByEmail(request.getEmail())).willReturn(true);
|
|
358
|
-
|
|
359
|
-
assertThatThrownBy(() -> userService.create(request))
|
|
360
|
-
.isInstanceOf(DuplicateEmailException.class);
|
|
361
|
-
verify(userRepository, never()).save(any());
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
```
|
|
365
|
-
|
|
366
136
|
### Integration Tests (Controller layer)
|
|
367
137
|
- Use `\@SpringBootTest` + `\@AutoConfigureMockMvc`
|
|
368
138
|
- Test full HTTP flow with `MockMvc`
|
|
369
139
|
- Use `\@Sql` or Testcontainers for database state
|
|
370
140
|
|
|
371
|
-
```java
|
|
372
|
-
\@SpringBootTest
|
|
373
|
-
\@AutoConfigureMockMvc
|
|
374
|
-
class UserControllerIntegrationTest {
|
|
375
|
-
|
|
376
|
-
\@Autowired
|
|
377
|
-
private MockMvc mockMvc;
|
|
378
|
-
|
|
379
|
-
\@Autowired
|
|
380
|
-
private ObjectMapper objectMapper;
|
|
381
|
-
|
|
382
|
-
\@Test
|
|
383
|
-
void createUser_shouldReturn201_whenRequestIsValid() throws Exception {
|
|
384
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
385
|
-
.email("test\@example.com")
|
|
386
|
-
.fullName("Test User")
|
|
387
|
-
.password("password123")
|
|
388
|
-
.build();
|
|
389
|
-
|
|
390
|
-
mockMvc.perform(post("/api/v1/users")
|
|
391
|
-
.contentType(MediaType.APPLICATION_JSON)
|
|
392
|
-
.content(objectMapper.writeValueAsString(request)))
|
|
393
|
-
.andExpect(status().isCreated())
|
|
394
|
-
.andExpect(jsonPath("$.email").value("test\@example.com"))
|
|
395
|
-
.andExpect(jsonPath("$.id").isNumber());
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
\@Test
|
|
399
|
-
void createUser_shouldReturn400_whenEmailIsInvalid() throws Exception {
|
|
400
|
-
CreateUserRequest request = CreateUserRequest.builder()
|
|
401
|
-
.email("not-an-email")
|
|
402
|
-
.fullName("Test")
|
|
403
|
-
.password("password123")
|
|
404
|
-
.build();
|
|
405
|
-
|
|
406
|
-
mockMvc.perform(post("/api/v1/users")
|
|
407
|
-
.contentType(MediaType.APPLICATION_JSON)
|
|
408
|
-
.content(objectMapper.writeValueAsString(request)))
|
|
409
|
-
.andExpect(status().isBadRequest())
|
|
410
|
-
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
```
|
|
414
|
-
|
|
415
141
|
---
|
|
416
142
|
|
|
417
143
|
## Naming Conventions
|
|
@@ -458,14 +184,6 @@ GET /api/v1/users/{id}/orders → 200 nested resource
|
|
|
458
184
|
- Use `\@Transactional(readOnly = true)` on read-only service methods
|
|
459
185
|
- For heavy read operations, consider projection interfaces
|
|
460
186
|
|
|
461
|
-
```java
|
|
462
|
-
// ✅ Read-only transaction
|
|
463
|
-
\@Transactional(readOnly = true)
|
|
464
|
-
public Page<UserResponse> findAll(Pageable pageable) {
|
|
465
|
-
return userRepository.findAll(pageable).map(userMapper::toResponse);
|
|
466
|
-
}
|
|
467
|
-
```
|
|
468
|
-
|
|
469
187
|
---
|
|
470
188
|
|
|
471
189
|
## Security Rules
|
|
@@ -487,23 +205,6 @@ public Page<UserResponse> findAll(Pageable pageable) {
|
|
|
487
205
|
- `log.error` — unexpected exceptions (always include `ex` as the second argument)
|
|
488
206
|
- Never log sensitive data (password, credit card, token)
|
|
489
207
|
|
|
490
|
-
```java
|
|
491
|
-
\@Slf4j
|
|
492
|
-
\@Service
|
|
493
|
-
public class UserServiceImpl implements UserService {
|
|
494
|
-
|
|
495
|
-
public UserResponse findById(Long id) {
|
|
496
|
-
log.info("Finding user by id: {}", id);
|
|
497
|
-
return userRepository.findById(id)
|
|
498
|
-
.map(userMapper::toResponse)
|
|
499
|
-
.orElseThrow(() -> {
|
|
500
|
-
log.warn("User not found: id={}", id);
|
|
501
|
-
return new ResourceNotFoundException("User", id);
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
}
|
|
505
|
-
```
|
|
506
|
-
|
|
507
208
|
---
|
|
508
209
|
|
|
509
210
|
## Common Anti-Patterns to Avoid
|