@relipa/ai-flow-kit 0.0.5 → 0.0.6-beta.0
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 +224 -523
- 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 +89 -48
- package/scripts/guide.js +28 -5
- package/scripts/hooks/session-start.js +144 -45
- 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
package/README.md
CHANGED
|
@@ -23,81 +23,49 @@ Developers only need a single command to load ticket context → AI automaticall
|
|
|
23
23
|
> [!TIP]
|
|
24
24
|
> After running `aiflow init`, documentation files are automatically copied to `.aiflow/docs/` in your project for easy access.
|
|
25
25
|
|
|
26
|
-
- **[Quick Start Guide](
|
|
27
|
-
- **[Full Workflow (5 Gates)](
|
|
28
|
-
- **[Troubleshooting](
|
|
29
|
-
- **[Integration Guide](
|
|
30
|
-
- **[Changelog](
|
|
26
|
+
- **[Quick Start Guide](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/QUICK_START.md)** — Step-by-step instructions for developers.
|
|
27
|
+
- **[Full Workflow (5 Gates)](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/AIFLOW.md)** — In-depth look at the Gate process.
|
|
28
|
+
- **[Troubleshooting](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/troubleshooting.md)** — Common issues and fixes.
|
|
29
|
+
- **[Integration Guide](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/ai-integration.md)** — Advanced tool setup (Claude, Cursor, Gemini).
|
|
30
|
+
- **[Changelog](https://gitlab.relipa.vn/ai/ai-flow-kit/-/blob/main/docs/common/CHANGELOG.md)** — Version history and release notes.
|
|
31
31
|
|
|
32
32
|
---
|
|
33
33
|
|
|
34
34
|
## Overview Flow
|
|
35
35
|
|
|
36
36
|
```
|
|
37
|
-
|
|
38
|
-
│
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
│ │
|
|
70
|
-
│ 2. claude (open Claude terminal) │
|
|
71
|
-
│ │
|
|
72
|
-
│ 3. "Fix bug PROJ-33" (1 line) │
|
|
73
|
-
└──────────────────┬──────────────────┘
|
|
74
|
-
│
|
|
75
|
-
┌──────────────────▼──────────────────┐
|
|
76
|
-
│ Claude AI │
|
|
77
|
-
│ │
|
|
78
|
-
│ SessionStart hook injected │
|
|
79
|
-
│ using-superpowers skill │
|
|
80
|
-
│ ↓ │
|
|
81
|
-
│ Read CLAUDE.md (spring-boot rules) │
|
|
82
|
-
│ ↓ │
|
|
83
|
-
│ Read context/current.json (PROJ-33) │
|
|
84
|
-
│ ↓ │
|
|
85
|
-
│ Auto-detect: bug-fix task │
|
|
86
|
-
│ ↓ │
|
|
87
|
-
│ Invoke investigate-bug skill │
|
|
88
|
-
│ → inherits systematic-debugging │
|
|
89
|
-
│ → trace Controller→Service→Repo │
|
|
90
|
-
│ → git blame to understand history │
|
|
91
|
-
│ → propose minimal fix │
|
|
92
|
-
│ ↓ │
|
|
93
|
-
│ Invoke impact-analysis skill │
|
|
94
|
-
│ → check breaking changes │
|
|
95
|
-
└──────────────────┬──────────────────┘
|
|
96
|
-
│
|
|
97
|
-
┌──────────────────▼──────────────────┐
|
|
98
|
-
│ Developer review │
|
|
99
|
-
│ git diff → test → commit │
|
|
100
|
-
└──────────────────────────────────────┘
|
|
37
|
+
PM writes ticket on Backlog/Jira
|
|
38
|
+
│
|
|
39
|
+
▼
|
|
40
|
+
⛩️ GATE 1 — AI Analyze Requirement [AI + DEV]
|
|
41
|
+
aiflow use PROJ-33 → claude
|
|
42
|
+
AI loads ticket + reads source code
|
|
43
|
+
AI asks clarifying questions (max 1 in Fast mode)
|
|
44
|
+
AI outputs: plan/PROJ-33/requirement.md (Lite in Fast mode)
|
|
45
|
+
DEV reviews → "APPROVED"
|
|
46
|
+
│ APPROVED
|
|
47
|
+
▼
|
|
48
|
+
⛩️ GATE 2 — Implementation Plan [AI + DEV]
|
|
49
|
+
AI creates detailed coding plan (TDD steps)
|
|
50
|
+
DEV reviews → "APPROVED"
|
|
51
|
+
│ APPROVED
|
|
52
|
+
▼
|
|
53
|
+
⛩️ GATE 3 — Code Generation (TDD) [AI]
|
|
54
|
+
AI writes tests first → implement → tests pass
|
|
55
|
+
Commit each small step
|
|
56
|
+
│ Code done
|
|
57
|
+
▼
|
|
58
|
+
⛩️ GATE 4 — AI Self-Review [AI + DEV]
|
|
59
|
+
AI runs: verification + impact-analysis + checklist
|
|
60
|
+
AI generates: plan/PROJ-33/summary.md
|
|
61
|
+
DEV reviews → "APPROVED" or "BUG: [description]"
|
|
62
|
+
│ APPROVED
|
|
63
|
+
▼
|
|
64
|
+
⛩️ GATE 5 — Peer Review & PR [DEV + Peer]
|
|
65
|
+
Create Pull Request → peer review → merge
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
DONE
|
|
101
69
|
```
|
|
102
70
|
|
|
103
71
|
---
|
|
@@ -123,6 +91,7 @@ npm uninstall -g @relipa/ai-flow-kit
|
|
|
123
91
|
|---------|-------------|
|
|
124
92
|
| `aiflow init` | Setup framework, adapters, and multi-AI rules. |
|
|
125
93
|
| `aiflow use <ticket>` | Load ticket context (Backlog/Jira). |
|
|
94
|
+
| `aiflow task` | Manage multiple tasks — pause, switch, resume. |
|
|
126
95
|
| `aiflow prompt` | Generate tool-optimized prompts (`--env cursor`). |
|
|
127
96
|
| `aiflow telemetry` | Manage telemetry tracking (enable/disable/status). |
|
|
128
97
|
| `aiflow guide` | View integrated multi-AI integration guide. |
|
|
@@ -135,6 +104,9 @@ Initial setup for the project. Run once per project.
|
|
|
135
104
|
# Single framework + adapter
|
|
136
105
|
aiflow init --framework spring-boot --adapter backlog
|
|
137
106
|
|
|
107
|
+
# Force enable RTK token compression
|
|
108
|
+
aiflow init --with-rtk
|
|
109
|
+
|
|
138
110
|
# Multiple frameworks and adapters
|
|
139
111
|
aiflow init --framework spring-boot,reactjs --adapter backlog,jira
|
|
140
112
|
```
|
|
@@ -181,12 +153,52 @@ aiflow use PROJ-33 --comments-from 3
|
|
|
181
153
|
# Manual input
|
|
182
154
|
aiflow use --manual
|
|
183
155
|
|
|
156
|
+
# Load from local file (JSON or plain text)
|
|
157
|
+
aiflow use --file task.md
|
|
158
|
+
|
|
159
|
+
# Modes (Fast is default)
|
|
160
|
+
aiflow use PROJ-33 # Fast Mode: skip Q&A, target < 5 min
|
|
161
|
+
aiflow use PROJ-33 --full # Full Mode: deep analysis with Q&A
|
|
162
|
+
|
|
184
163
|
# Save with custom name
|
|
185
164
|
aiflow use PROJ-33 --save sprint-42-bug
|
|
186
165
|
```
|
|
187
166
|
|
|
188
167
|
---
|
|
189
168
|
|
|
169
|
+
### `aiflow task`
|
|
170
|
+
|
|
171
|
+
Manage multiple tasks — pause a task, switch to another, and resume later while keeping full progress.
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
aiflow task status # show active task and pending tasks
|
|
175
|
+
aiflow task list # list all saved tasks
|
|
176
|
+
aiflow task pause # pause current task (saves context + gate progress)
|
|
177
|
+
aiflow task pause --note "waiting for API spec" # pause with a note
|
|
178
|
+
aiflow task switch PROJ-99 # pause current + switch to another task
|
|
179
|
+
aiflow task resume PROJ-33 # resume a paused task (restores context + gate state)
|
|
180
|
+
aiflow task reset PROJ-33 # reset to Gate 1 (keeps ticket context, clears progress)
|
|
181
|
+
aiflow task remove PROJ-33 # permanently delete all saved data for the task
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Use case — switching tasks mid-flow:**
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# Working on PROJ-33 at Gate 2...
|
|
188
|
+
aiflow task pause # pauses and saves PROJ-33 progress
|
|
189
|
+
|
|
190
|
+
aiflow use PROJ-99 # loads urgent task (auto-pauses current)
|
|
191
|
+
claude # AI starts Gate 1 for PROJ-99
|
|
192
|
+
|
|
193
|
+
# Done with PROJ-99, resuming PROJ-33
|
|
194
|
+
aiflow task resume PROJ-33 # restores context
|
|
195
|
+
claude # AI resumes from Gate 2 automatically
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Task state (context + gate progress) is saved in `.aiflow/tasks/<taskId>/`.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
190
202
|
### `aiflow doctor`
|
|
191
203
|
|
|
192
204
|
Check if the setup is complete.
|
package/bin/aiflow.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require('commander');
|
|
4
4
|
const chalk = require('chalk');
|
|
@@ -15,6 +15,7 @@ const promptCommand = require('../scripts/prompt');
|
|
|
15
15
|
const removeCommand = require('../scripts/remove');
|
|
16
16
|
const guideCommand = require('../scripts/guide');
|
|
17
17
|
const telemetryCommand= require('../scripts/telemetry/cli');
|
|
18
|
+
const taskCommand = require('../scripts/task');
|
|
18
19
|
const { record } = require('../scripts/telemetry/record');
|
|
19
20
|
|
|
20
21
|
program
|
|
@@ -60,6 +61,8 @@ program
|
|
|
60
61
|
.option('-f, --framework <types>', 'framework(s), comma-separated (e.g. spring-boot,reactjs)')
|
|
61
62
|
.option('-a, --adapter <types>', 'adapter(s), comma-separated (e.g. backlog,jira)')
|
|
62
63
|
.option('-e, --env <types>', 'AI environment(s)/tool(s), comma-separated (e.g. cursor,gemini,copilot)')
|
|
64
|
+
.option('--with-rtk', 'force enable RTK token compression hook')
|
|
65
|
+
.option('--no-rtk', 'skip RTK setup even if RTK is detected')
|
|
63
66
|
.action((options) => {
|
|
64
67
|
options.frameworks = options.framework
|
|
65
68
|
? options.framework.split(',').map(s => s.trim()).filter(Boolean)
|
|
@@ -69,7 +72,7 @@ program
|
|
|
69
72
|
: [];
|
|
70
73
|
options.aiTools = options.env
|
|
71
74
|
? options.env.split(',').map(s => s.trim()).filter(Boolean)
|
|
72
|
-
: Object.keys(require('../scripts/init').AI_TOOL_FILES || {});
|
|
75
|
+
: Object.keys(require('../scripts/init').AI_TOOL_FILES || {}).filter(t => t !== 'generic');
|
|
73
76
|
|
|
74
77
|
record('command.invoked', { command: 'init' });
|
|
75
78
|
initCommand(options);
|
|
@@ -85,6 +88,8 @@ program
|
|
|
85
88
|
.option('--with-comments', 'load all comments (default: description only)')
|
|
86
89
|
.option('--comments-last <n>', 'load only the last N comments', parseInt)
|
|
87
90
|
.option('--comments-from <n>', 'load comments from index N onward (0-based)', parseInt)
|
|
91
|
+
.option('--fast', 'fast mode: minimal Q&A, concise requirement doc (Default)')
|
|
92
|
+
.option('--full', 'full mode: force complete analysis with Q&A (default)')
|
|
88
93
|
.action((target, options) => {
|
|
89
94
|
record('command.invoked', { command: 'use' });
|
|
90
95
|
useCommand(target, options);
|
|
@@ -118,6 +123,45 @@ program
|
|
|
118
123
|
console.log(detector.formatOutput(result, options.verbose));
|
|
119
124
|
});
|
|
120
125
|
|
|
126
|
+
// ── task ─────────────────────────────────────────────────────
|
|
127
|
+
const taskCmd = program.command('task').description('Manage multiple tasks — pause, switch, resume');
|
|
128
|
+
|
|
129
|
+
taskCmd
|
|
130
|
+
.command('status')
|
|
131
|
+
.description('Show active task and pending tasks')
|
|
132
|
+
.action(() => { record('command.invoked', { command: 'task.status' }); taskCommand('status'); });
|
|
133
|
+
|
|
134
|
+
taskCmd
|
|
135
|
+
.command('list')
|
|
136
|
+
.description('List all saved tasks')
|
|
137
|
+
.action(() => { record('command.invoked', { command: 'task.list' }); taskCommand('list'); });
|
|
138
|
+
|
|
139
|
+
taskCmd
|
|
140
|
+
.command('pause')
|
|
141
|
+
.description('Pause current task and save progress')
|
|
142
|
+
.option('-n, --note <text>', 'note about why you are pausing')
|
|
143
|
+
.action((opts) => { record('command.invoked', { command: 'task.pause' }); taskCommand('pause', { note: opts.note }); });
|
|
144
|
+
|
|
145
|
+
taskCmd
|
|
146
|
+
.command('switch <ticket-id>')
|
|
147
|
+
.description('Pause current task and switch to another')
|
|
148
|
+
.action((ticketId) => { record('command.invoked', { command: 'task.switch' }); taskCommand('switch', { taskId: ticketId }); });
|
|
149
|
+
|
|
150
|
+
taskCmd
|
|
151
|
+
.command('resume <ticket-id>')
|
|
152
|
+
.description('Resume a paused task')
|
|
153
|
+
.action((ticketId) => { record('command.invoked', { command: 'task.resume' }); taskCommand('resume', { taskId: ticketId }); });
|
|
154
|
+
|
|
155
|
+
taskCmd
|
|
156
|
+
.command('reset <ticket-id>')
|
|
157
|
+
.description('Reset task to Gate 1 (keeps ticket context, clears gate progress)')
|
|
158
|
+
.action((ticketId) => { record('command.invoked', { command: 'task.reset' }); taskCommand('reset', { taskId: ticketId }); });
|
|
159
|
+
|
|
160
|
+
taskCmd
|
|
161
|
+
.command('remove <ticket-id>')
|
|
162
|
+
.description('Permanently delete all saved data for a task')
|
|
163
|
+
.action((ticketId) => { record('command.invoked', { command: 'task.remove' }); taskCommand('remove', { taskId: ticketId }); });
|
|
164
|
+
|
|
121
165
|
// ── context ───────────────────────────────────────────────────
|
|
122
166
|
program
|
|
123
167
|
.command('context [action]')
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# Spring Boot Code Examples
|
|
2
|
+
|
|
3
|
+
Reference examples for each layer. Read the relevant section when generating code for that layer.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Controller
|
|
8
|
+
|
|
9
|
+
```java
|
|
10
|
+
// ✅ Good
|
|
11
|
+
\@PostMapping
|
|
12
|
+
public ResponseEntity<UserResponse> createUser(
|
|
13
|
+
\@Valid \@RequestBody CreateUserRequest request) {
|
|
14
|
+
UserResponse response = userService.create(request);
|
|
15
|
+
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ❌ Bad — business logic in controller
|
|
19
|
+
\@PostMapping
|
|
20
|
+
public ResponseEntity<User> createUser(\@RequestBody User user) {
|
|
21
|
+
if (userRepository.existsByEmail(user.getEmail())) {
|
|
22
|
+
throw new RuntimeException("Email exists");
|
|
23
|
+
}
|
|
24
|
+
return ResponseEntity.ok(userRepository.save(user));
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Service
|
|
31
|
+
|
|
32
|
+
```java
|
|
33
|
+
// Interface
|
|
34
|
+
public interface UserService {
|
|
35
|
+
UserResponse create(CreateUserRequest request);
|
|
36
|
+
UserResponse findById(Long id);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Implementation
|
|
40
|
+
\@Service
|
|
41
|
+
\@RequiredArgsConstructor
|
|
42
|
+
public class UserServiceImpl implements UserService {
|
|
43
|
+
|
|
44
|
+
private final UserRepository userRepository;
|
|
45
|
+
private final UserMapper userMapper;
|
|
46
|
+
|
|
47
|
+
\@Override
|
|
48
|
+
\@Transactional
|
|
49
|
+
public UserResponse create(CreateUserRequest request) {
|
|
50
|
+
if (userRepository.existsByEmail(request.getEmail())) {
|
|
51
|
+
throw new DuplicateEmailException(request.getEmail());
|
|
52
|
+
}
|
|
53
|
+
User user = userMapper.toEntity(request);
|
|
54
|
+
User saved = userRepository.save(user);
|
|
55
|
+
return userMapper.toResponse(saved);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
\@Override
|
|
59
|
+
\@Transactional(readOnly = true)
|
|
60
|
+
public UserResponse findById(Long id) {
|
|
61
|
+
log.info("Finding user by id: {}", id);
|
|
62
|
+
return userRepository.findById(id)
|
|
63
|
+
.map(userMapper::toResponse)
|
|
64
|
+
.orElseThrow(() -> {
|
|
65
|
+
log.warn("User not found: id={}", id);
|
|
66
|
+
return new ResourceNotFoundException("User", id);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
\@Transactional(readOnly = true)
|
|
71
|
+
public Page<UserResponse> findAll(Pageable pageable) {
|
|
72
|
+
return userRepository.findAll(pageable).map(userMapper::toResponse);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Repository
|
|
80
|
+
|
|
81
|
+
```java
|
|
82
|
+
// ✅ Good
|
|
83
|
+
public interface UserRepository extends JpaRepository<User, Long> {
|
|
84
|
+
|
|
85
|
+
boolean existsByEmail(String email);
|
|
86
|
+
|
|
87
|
+
Optional<User> findByEmailAndDeletedFalse(String email);
|
|
88
|
+
|
|
89
|
+
\@Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
|
|
90
|
+
Optional<User> findByIdWithRoles(\@Param("id") Long id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ❌ Bad — N+1 problem
|
|
94
|
+
List<Order> findAll(); // If Order has \@ManyToOne User, this causes N+1
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Entity
|
|
100
|
+
|
|
101
|
+
```java
|
|
102
|
+
\@Entity
|
|
103
|
+
\@Table(name = "users")
|
|
104
|
+
\@Getter
|
|
105
|
+
\@Setter
|
|
106
|
+
\@NoArgsConstructor
|
|
107
|
+
\@AllArgsConstructor
|
|
108
|
+
\@Builder
|
|
109
|
+
public class User extends BaseEntity {
|
|
110
|
+
|
|
111
|
+
\@Id
|
|
112
|
+
\@GeneratedValue(strategy = GenerationType.IDENTITY)
|
|
113
|
+
private Long id;
|
|
114
|
+
|
|
115
|
+
\@Column(name = "email", nullable = false, unique = true, length = 255)
|
|
116
|
+
private String email;
|
|
117
|
+
|
|
118
|
+
\@Column(name = "full_name", nullable = false, length = 100)
|
|
119
|
+
private String fullName;
|
|
120
|
+
|
|
121
|
+
\@Column(name = "deleted", nullable = false)
|
|
122
|
+
\@Builder.Default
|
|
123
|
+
private boolean deleted = false;
|
|
124
|
+
|
|
125
|
+
\@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
|
126
|
+
private List<Order> orders = new ArrayList<>();
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## DTO
|
|
133
|
+
|
|
134
|
+
```java
|
|
135
|
+
// Request DTO
|
|
136
|
+
\@Getter
|
|
137
|
+
\@NoArgsConstructor
|
|
138
|
+
\@AllArgsConstructor
|
|
139
|
+
\@Builder
|
|
140
|
+
public class CreateUserRequest {
|
|
141
|
+
|
|
142
|
+
\@NotBlank(message = "Email is required")
|
|
143
|
+
\@Email(message = "Invalid email format")
|
|
144
|
+
private String email;
|
|
145
|
+
|
|
146
|
+
\@NotBlank(message = "Full name is required")
|
|
147
|
+
\@Size(min = 2, max = 100, message = "Full name must be 2-100 characters")
|
|
148
|
+
private String fullName;
|
|
149
|
+
|
|
150
|
+
\@NotBlank(message = "Password is required")
|
|
151
|
+
\@Size(min = 8, message = "Password must be at least 8 characters")
|
|
152
|
+
private String password;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Response DTO
|
|
156
|
+
\@Getter
|
|
157
|
+
\@NoArgsConstructor
|
|
158
|
+
\@AllArgsConstructor
|
|
159
|
+
\@Builder
|
|
160
|
+
public class UserResponse {
|
|
161
|
+
private Long id;
|
|
162
|
+
private String email;
|
|
163
|
+
private String fullName;
|
|
164
|
+
private LocalDateTime createdAt;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## MapStruct Mapper
|
|
171
|
+
|
|
172
|
+
```java
|
|
173
|
+
\@Mapper(componentModel = "spring")
|
|
174
|
+
public interface UserMapper {
|
|
175
|
+
|
|
176
|
+
\@Mapping(target = "createdAt", source = "createdAt")
|
|
177
|
+
UserResponse toResponse(User user);
|
|
178
|
+
|
|
179
|
+
\@Mapping(target = "id", ignore = true)
|
|
180
|
+
\@Mapping(target = "deleted", ignore = true)
|
|
181
|
+
\@Mapping(target = "createdAt", ignore = true)
|
|
182
|
+
\@Mapping(target = "updatedAt", ignore = true)
|
|
183
|
+
User toEntity(CreateUserRequest request);
|
|
184
|
+
|
|
185
|
+
List<UserResponse> toResponseList(List<User> users);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Exception Handling
|
|
192
|
+
|
|
193
|
+
```java
|
|
194
|
+
// Custom exception
|
|
195
|
+
public class ResourceNotFoundException extends RuntimeException {
|
|
196
|
+
public ResourceNotFoundException(String resource, Long id) {
|
|
197
|
+
super(String.format("%s with id %d not found", resource, id));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Global handler
|
|
202
|
+
\@RestControllerAdvice
|
|
203
|
+
\@Slf4j
|
|
204
|
+
public class GlobalExceptionHandler {
|
|
205
|
+
|
|
206
|
+
\@ExceptionHandler(ResourceNotFoundException.class)
|
|
207
|
+
public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
|
|
208
|
+
log.warn("Resource not found: {}", ex.getMessage());
|
|
209
|
+
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
|
210
|
+
.body(ErrorResponse.of("NOT_FOUND", ex.getMessage()));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
\@ExceptionHandler(MethodArgumentNotValidException.class)
|
|
214
|
+
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException ex) {
|
|
215
|
+
List<String> errors = ex.getBindingResult()
|
|
216
|
+
.getFieldErrors()
|
|
217
|
+
.stream()
|
|
218
|
+
.map(e -> e.getField() + ": " + e.getDefaultMessage())
|
|
219
|
+
.toList();
|
|
220
|
+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
|
|
221
|
+
.body(ErrorResponse.of("VALIDATION_FAILED", errors.toString()));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
\@ExceptionHandler(Exception.class)
|
|
225
|
+
public ResponseEntity<ErrorResponse> handleGeneral(Exception ex) {
|
|
226
|
+
log.error("Unexpected error", ex);
|
|
227
|
+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
|
228
|
+
.body(ErrorResponse.of("INTERNAL_ERROR", "An unexpected error occurred"));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Unit Tests (Service layer)
|
|
236
|
+
|
|
237
|
+
```java
|
|
238
|
+
\@ExtendWith(MockitoExtension.class)
|
|
239
|
+
class UserServiceImplTest {
|
|
240
|
+
|
|
241
|
+
\@Mock
|
|
242
|
+
private UserRepository userRepository;
|
|
243
|
+
|
|
244
|
+
\@Mock
|
|
245
|
+
private UserMapper userMapper;
|
|
246
|
+
|
|
247
|
+
\@InjectMocks
|
|
248
|
+
private UserServiceImpl userService;
|
|
249
|
+
|
|
250
|
+
\@Test
|
|
251
|
+
void create_shouldReturnUserResponse_whenEmailIsUnique() {
|
|
252
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
253
|
+
.email("test\@example.com")
|
|
254
|
+
.fullName("Test User")
|
|
255
|
+
.build();
|
|
256
|
+
User user = User.builder().id(1L).email("test\@example.com").build();
|
|
257
|
+
UserResponse expected = UserResponse.builder().id(1L).build();
|
|
258
|
+
|
|
259
|
+
given(userRepository.existsByEmail(request.getEmail())).willReturn(false);
|
|
260
|
+
given(userMapper.toEntity(request)).willReturn(user);
|
|
261
|
+
given(userRepository.save(user)).willReturn(user);
|
|
262
|
+
given(userMapper.toResponse(user)).willReturn(expected);
|
|
263
|
+
|
|
264
|
+
UserResponse result = userService.create(request);
|
|
265
|
+
|
|
266
|
+
assertThat(result).isEqualTo(expected);
|
|
267
|
+
verify(userRepository).save(user);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
\@Test
|
|
271
|
+
void create_shouldThrowDuplicateEmailException_whenEmailAlreadyExists() {
|
|
272
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
273
|
+
.email("existing\@example.com").build();
|
|
274
|
+
given(userRepository.existsByEmail(request.getEmail())).willReturn(true);
|
|
275
|
+
|
|
276
|
+
assertThatThrownBy(() -> userService.create(request))
|
|
277
|
+
.isInstanceOf(DuplicateEmailException.class);
|
|
278
|
+
verify(userRepository, never()).save(any());
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## Integration Tests (Controller layer)
|
|
286
|
+
|
|
287
|
+
```java
|
|
288
|
+
\@SpringBootTest
|
|
289
|
+
\@AutoConfigureMockMvc
|
|
290
|
+
class UserControllerIntegrationTest {
|
|
291
|
+
|
|
292
|
+
\@Autowired
|
|
293
|
+
private MockMvc mockMvc;
|
|
294
|
+
|
|
295
|
+
\@Autowired
|
|
296
|
+
private ObjectMapper objectMapper;
|
|
297
|
+
|
|
298
|
+
\@Test
|
|
299
|
+
void createUser_shouldReturn201_whenRequestIsValid() throws Exception {
|
|
300
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
301
|
+
.email("test\@example.com")
|
|
302
|
+
.fullName("Test User")
|
|
303
|
+
.password("password123")
|
|
304
|
+
.build();
|
|
305
|
+
|
|
306
|
+
mockMvc.perform(post("/api/v1/users")
|
|
307
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
308
|
+
.content(objectMapper.writeValueAsString(request)))
|
|
309
|
+
.andExpect(status().isCreated())
|
|
310
|
+
.andExpect(jsonPath("$.email").value("test\@example.com"))
|
|
311
|
+
.andExpect(jsonPath("$.id").isNumber());
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
\@Test
|
|
315
|
+
void createUser_shouldReturn400_whenEmailIsInvalid() throws Exception {
|
|
316
|
+
CreateUserRequest request = CreateUserRequest.builder()
|
|
317
|
+
.email("not-an-email")
|
|
318
|
+
.fullName("Test")
|
|
319
|
+
.password("password123")
|
|
320
|
+
.build();
|
|
321
|
+
|
|
322
|
+
mockMvc.perform(post("/api/v1/users")
|
|
323
|
+
.contentType(MediaType.APPLICATION_JSON)
|
|
324
|
+
.content(objectMapper.writeValueAsString(request)))
|
|
325
|
+
.andExpect(status().isBadRequest())
|
|
326
|
+
.andExpect(jsonPath("$.code").value("VALIDATION_FAILED"));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
```
|
|
@@ -12,6 +12,60 @@ keywords: ticket, requirement, study, read, context, backlog, jira, analyze, und
|
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
15
|
+
## Mode Selection
|
|
16
|
+
|
|
17
|
+
Check `mode` in `.aiflow/context/current.json` before starting:
|
|
18
|
+
|
|
19
|
+
| Mode | Trigger | Behaviour |
|
|
20
|
+
|------|---------|-----------|
|
|
21
|
+
| `fast` | `aiflow use TICKET --fast` | Jump to **[Fast Track](#fast-track-mode-fast)** below |
|
|
22
|
+
| `full` | `aiflow use TICKET --full` | Use standard steps — full Q&A + all 6 sections |
|
|
23
|
+
| `auto` (default) | `aiflow use TICKET` | Standard steps |
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Fast Track (mode: fast)
|
|
28
|
+
|
|
29
|
+
> Target: Gate 1 complete in **< 5 minutes**, minimum token usage.
|
|
30
|
+
|
|
31
|
+
1. Read `.aiflow/context/current.json` — ticket title, description, acceptance criteria
|
|
32
|
+
2. Identify **directly affected files** from ticket text and file names only.
|
|
33
|
+
DO NOT trace data flow or read unrelated source files.
|
|
34
|
+
3. Q&A: Ask at most **ONE** question if there is a blocking ambiguity.
|
|
35
|
+
If the ticket + source file names make intent clear → skip Q&A entirely.
|
|
36
|
+
Wait for answer before writing the requirement doc.
|
|
37
|
+
4. Output `plan/[ticket-id]/requirement.md` with **3 sections only**:
|
|
38
|
+
|
|
39
|
+
```markdown
|
|
40
|
+
# Requirement: [Ticket ID] — [Title]
|
|
41
|
+
|
|
42
|
+
**Date:** [YYYY-MM-DD] **Mode:** fast **Status:** ⏸️ Waiting for DEV Approval
|
|
43
|
+
|
|
44
|
+
## 1. Requirements Summary
|
|
45
|
+
[2–4 sentence restatement of what needs to change and why]
|
|
46
|
+
|
|
47
|
+
**Acceptance Criteria:**
|
|
48
|
+
- [ ] [each AC from ticket]
|
|
49
|
+
|
|
50
|
+
## 3. Files to Create / Modify
|
|
51
|
+
| Action | File | Change |
|
|
52
|
+
|--------|------|--------|
|
|
53
|
+
| Modify | [path] | [what changes] |
|
|
54
|
+
|
|
55
|
+
## 5. Effort Estimate
|
|
56
|
+
| Task | Size |
|
|
57
|
+
|------|------|
|
|
58
|
+
| Tests | S/M/L |
|
|
59
|
+
| Implementation | S/M/L |
|
|
60
|
+
| **Total** | **S/M/L** |
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
5. Display Gate 1 prompt and wait for `APPROVED`.
|
|
64
|
+
|
|
65
|
+
> After APPROVED, proceed to Gate 2. The lean doc is intentional — Gate 2 plan will fill in the details.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
15
69
|
## Steps
|
|
16
70
|
|
|
17
71
|
### Step 1: Load Ticket & Read Source Code
|