@silbaram/artifact-driven-agent 0.1.5 → 0.1.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 +294 -661
- package/bin/cli.js +8 -1
- package/core/artifacts/decision.md +1 -1
- package/core/artifacts/plan.md +5 -5
- package/core/artifacts/project.md +4 -2
- package/core/artifacts/sprints/_template/docs/release-notes.md +37 -0
- package/core/artifacts/sprints/_template/meta.md +54 -0
- package/core/artifacts/sprints/_template/retrospective.md +50 -0
- package/core/artifacts/sprints/_template/review-reports/review-template.md +49 -0
- package/core/artifacts/sprints/_template/tasks/task-template.md +43 -0
- package/core/roles/analyzer.md +6 -33
- package/core/roles/developer.md +69 -99
- package/core/roles/documenter.md +293 -0
- package/core/roles/manager.md +391 -341
- package/core/roles/planner.md +61 -79
- package/core/roles/reviewer.md +71 -129
- package/core/rules/document-priority.md +40 -38
- package/core/rules/escalation.md +15 -14
- package/core/rules/iteration.md +24 -24
- package/core/rules/rfc.md +1 -1
- package/core/rules/rollback.md +4 -4
- package/package.json +1 -1
- package/src/commands/run.js +95 -46
- package/src/commands/sessions.js +21 -1
- package/src/commands/sprint.js +262 -0
- package/src/commands/validate.js +133 -64
- package/src/commands/validate.test.js +84 -0
- package/src/utils/files.js +134 -134
- package/src/utils/sessionState.js +30 -11
- package/core/artifacts/architecture-options.md +0 -85
- package/core/artifacts/backlog.md +0 -177
- package/core/artifacts/current-sprint.md +0 -125
- package/core/artifacts/qa-report.md +0 -104
- package/core/artifacts/review-report.md +0 -103
- package/core/roles/architect.md +0 -270
- package/core/roles/qa.md +0 -306
- package/core/rules/role-state-protocol.md +0 -281
- package/core/rules/session-state.md +0 -255
- package/templates/cli/roles/cli-developer.md +0 -243
- package/templates/game/roles/game-logic.md +0 -198
- package/templates/game/roles/rendering.md +0 -142
- package/templates/library/roles/library-developer.md +0 -184
- package/templates/web-dev/roles/backend.md +0 -139
- package/templates/web-dev/roles/frontend.md +0 -166
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { getWorkspaceDir, isWorkspaceSetup, getTimestamp } from '../utils/files.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 스프린트 관리 명령어
|
|
8
|
+
* @param {string} action - create / add / close / list
|
|
9
|
+
* @param {Array} args - 추가 인자
|
|
10
|
+
*/
|
|
11
|
+
export default async function sprint(action, ...args) {
|
|
12
|
+
if (!isWorkspaceSetup()) {
|
|
13
|
+
console.log(chalk.red('❌ 워크스페이스가 설정되지 않았습니다.'));
|
|
14
|
+
console.log(chalk.gray(' ada setup [template]을 먼저 실행하세요.'));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const workspace = getWorkspaceDir();
|
|
19
|
+
const sprintsDir = path.join(workspace, 'artifacts', 'sprints');
|
|
20
|
+
|
|
21
|
+
switch (action) {
|
|
22
|
+
case 'create':
|
|
23
|
+
await createSprint(sprintsDir);
|
|
24
|
+
break;
|
|
25
|
+
case 'add':
|
|
26
|
+
await addTasks(sprintsDir, args);
|
|
27
|
+
break;
|
|
28
|
+
case 'close':
|
|
29
|
+
await closeSprint(sprintsDir);
|
|
30
|
+
break;
|
|
31
|
+
case 'list':
|
|
32
|
+
await listSprints(sprintsDir);
|
|
33
|
+
break;
|
|
34
|
+
default:
|
|
35
|
+
console.log(chalk.red('❌ 알 수 없는 명령어입니다.'));
|
|
36
|
+
console.log('');
|
|
37
|
+
console.log(chalk.cyan('사용법:'));
|
|
38
|
+
console.log(chalk.gray(' ada sprint create - 새 스프린트 생성'));
|
|
39
|
+
console.log(chalk.gray(' ada sprint add task-001 ... - Task 추가'));
|
|
40
|
+
console.log(chalk.gray(' ada sprint close - 현재 스프린트 종료'));
|
|
41
|
+
console.log(chalk.gray(' ada sprint list - 스프린트 목록'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 새 스프린트 생성
|
|
48
|
+
*/
|
|
49
|
+
async function createSprint(sprintsDir) {
|
|
50
|
+
fs.ensureDirSync(sprintsDir);
|
|
51
|
+
|
|
52
|
+
// 현재 활성 스프린트 확인
|
|
53
|
+
const activeSprint = findActiveSprint(sprintsDir);
|
|
54
|
+
if (activeSprint) {
|
|
55
|
+
console.log(chalk.yellow(`⚠️ 이미 활성 스프린트가 있습니다: ${activeSprint}`));
|
|
56
|
+
console.log(chalk.gray(' 먼저 ada sprint close로 종료하세요.'));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 다음 스프린트 번호 계산
|
|
61
|
+
const sprints = fs.readdirSync(sprintsDir).filter(d => {
|
|
62
|
+
return fs.statSync(path.join(sprintsDir, d)).isDirectory() && !d.startsWith('_');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const sprintNumbers = sprints
|
|
66
|
+
.map(name => {
|
|
67
|
+
const match = name.match(/^sprint-(\d+)$/);
|
|
68
|
+
return match ? parseInt(match[1]) : 0;
|
|
69
|
+
})
|
|
70
|
+
.filter(n => n > 0);
|
|
71
|
+
|
|
72
|
+
const nextNumber = sprintNumbers.length > 0 ? Math.max(...sprintNumbers) + 1 : 1;
|
|
73
|
+
const sprintName = `sprint-${nextNumber}`;
|
|
74
|
+
const sprintPath = path.join(sprintsDir, sprintName);
|
|
75
|
+
|
|
76
|
+
// 템플릿 복사
|
|
77
|
+
const templatePath = path.join(sprintsDir, '_template');
|
|
78
|
+
if (!fs.existsSync(templatePath)) {
|
|
79
|
+
console.log(chalk.red('❌ 스프린트 템플릿이 없습니다.'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
fs.copySync(templatePath, sprintPath);
|
|
84
|
+
|
|
85
|
+
// meta.md 업데이트
|
|
86
|
+
const metaPath = path.join(sprintPath, 'meta.md');
|
|
87
|
+
let metaContent = fs.readFileSync(metaPath, 'utf-8');
|
|
88
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
89
|
+
|
|
90
|
+
metaContent = metaContent
|
|
91
|
+
.replace(/스프린트 번호 \| N/, `스프린트 번호 | ${nextNumber}`)
|
|
92
|
+
.replace(/상태 \| active \/ completed/, `상태 | active`)
|
|
93
|
+
.replace(/시작일 \| YYYY-MM-DD/, `시작일 | ${today}`)
|
|
94
|
+
.replace(/종료 예정 \| YYYY-MM-DD/, `종료 예정 | TBD`);
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(metaPath, metaContent);
|
|
97
|
+
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(chalk.green('✅ 새 스프린트가 생성되었습니다!'));
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.cyan(`📂 ${sprintName}/`));
|
|
102
|
+
console.log(chalk.gray(` - meta.md (스프린트 정보)`));
|
|
103
|
+
console.log(chalk.gray(` - tasks/ (Task 파일)`));
|
|
104
|
+
console.log(chalk.gray(` - review-reports/ (리뷰 리포트)`));
|
|
105
|
+
console.log(chalk.gray(` - docs/ (문서)`));
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(chalk.cyan('다음 단계:'));
|
|
108
|
+
console.log(chalk.gray(` ada sprint add task-001 task-002 - Task 추가`));
|
|
109
|
+
console.log('');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 현재 활성 스프린트 찾기
|
|
114
|
+
*/
|
|
115
|
+
function findActiveSprint(sprintsDir) {
|
|
116
|
+
if (!fs.existsSync(sprintsDir)) return null;
|
|
117
|
+
|
|
118
|
+
const sprints = fs.readdirSync(sprintsDir).filter(d => {
|
|
119
|
+
return fs.statSync(path.join(sprintsDir, d)).isDirectory() && !d.startsWith('_');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
for (const sprint of sprints) {
|
|
123
|
+
const metaPath = path.join(sprintsDir, sprint, 'meta.md');
|
|
124
|
+
if (fs.existsSync(metaPath)) {
|
|
125
|
+
const content = fs.readFileSync(metaPath, 'utf-8');
|
|
126
|
+
if (content.includes('상태 | active')) {
|
|
127
|
+
return sprint;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Task 추가
|
|
137
|
+
*/
|
|
138
|
+
async function addTasks(sprintsDir, taskIds) {
|
|
139
|
+
if (taskIds.length === 0) {
|
|
140
|
+
console.log(chalk.red('❌ Task ID를 지정하세요.'));
|
|
141
|
+
console.log(chalk.gray(' 예: ada sprint add task-001 task-002'));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const activeSprint = findActiveSprint(sprintsDir);
|
|
146
|
+
if (!activeSprint) {
|
|
147
|
+
console.log(chalk.red('❌ 활성 스프린트가 없습니다.'));
|
|
148
|
+
console.log(chalk.gray(' 먼저 ada sprint create로 생성하세요.'));
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const sprintPath = path.join(sprintsDir, activeSprint);
|
|
153
|
+
const backlogPath = path.join(getWorkspaceDir(), 'artifacts', 'backlog');
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(backlogPath)) {
|
|
156
|
+
console.log(chalk.red('❌ backlog/ 디렉토리가 없습니다.'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let addedCount = 0;
|
|
161
|
+
|
|
162
|
+
for (const taskId of taskIds) {
|
|
163
|
+
const taskFile = `${taskId}.md`;
|
|
164
|
+
const sourcePath = path.join(backlogPath, taskFile);
|
|
165
|
+
const destPath = path.join(sprintPath, 'tasks', taskFile);
|
|
166
|
+
|
|
167
|
+
if (!fs.existsSync(sourcePath)) {
|
|
168
|
+
console.log(chalk.yellow(`⚠️ ${taskId}: backlog에 없음 (건너뜀)`));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fs.existsSync(destPath)) {
|
|
173
|
+
console.log(chalk.yellow(`⚠️ ${taskId}: 이미 스프린트에 있음 (건너뜀)`));
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Task 파일 복사
|
|
178
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
179
|
+
addedCount++;
|
|
180
|
+
|
|
181
|
+
console.log(chalk.green(`✅ ${taskId} 추가됨`));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
console.log('');
|
|
185
|
+
console.log(chalk.cyan(`📊 ${addedCount}개 Task가 ${activeSprint}에 추가되었습니다.`));
|
|
186
|
+
console.log('');
|
|
187
|
+
console.log(chalk.gray(` meta.md를 업데이트하여 Task 목록을 갱신하세요.`));
|
|
188
|
+
console.log('');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 스프린트 종료
|
|
193
|
+
*/
|
|
194
|
+
async function closeSprint(sprintsDir) {
|
|
195
|
+
const activeSprint = findActiveSprint(sprintsDir);
|
|
196
|
+
if (!activeSprint) {
|
|
197
|
+
console.log(chalk.red('❌ 활성 스프린트가 없습니다.'));
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sprintPath = path.join(sprintsDir, activeSprint);
|
|
202
|
+
const metaPath = path.join(sprintPath, 'meta.md');
|
|
203
|
+
|
|
204
|
+
// meta.md 업데이트 (active → completed)
|
|
205
|
+
let metaContent = fs.readFileSync(metaPath, 'utf-8');
|
|
206
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
207
|
+
|
|
208
|
+
metaContent = metaContent
|
|
209
|
+
.replace(/상태 \| active/, `상태 | completed`)
|
|
210
|
+
.replace(/종료 예정 \| .*/, `종료 예정 | ${today}`);
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(metaPath, metaContent);
|
|
213
|
+
|
|
214
|
+
console.log('');
|
|
215
|
+
console.log(chalk.green(`✅ ${activeSprint}가 종료되었습니다!`));
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log(chalk.cyan('다음 단계:'));
|
|
218
|
+
console.log(chalk.gray(` 1. meta.md 회고 섹션 작성`));
|
|
219
|
+
console.log(chalk.gray(` 2. ada documenter [tool]로 문서 작성`));
|
|
220
|
+
console.log(chalk.gray(` 3. ada sprint create로 다음 스프린트 시작`));
|
|
221
|
+
console.log('');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* 스프린트 목록
|
|
226
|
+
*/
|
|
227
|
+
async function listSprints(sprintsDir) {
|
|
228
|
+
if (!fs.existsSync(sprintsDir)) {
|
|
229
|
+
console.log(chalk.yellow('⚠️ 스프린트가 없습니다.'));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const sprints = fs.readdirSync(sprintsDir)
|
|
234
|
+
.filter(d => {
|
|
235
|
+
return fs.statSync(path.join(sprintsDir, d)).isDirectory() && !d.startsWith('_');
|
|
236
|
+
})
|
|
237
|
+
.sort();
|
|
238
|
+
|
|
239
|
+
if (sprints.length === 0) {
|
|
240
|
+
console.log(chalk.yellow('⚠️ 스프린트가 없습니다.'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
console.log('');
|
|
245
|
+
console.log(chalk.cyan('📊 스프린트 목록'));
|
|
246
|
+
console.log(chalk.cyan('━'.repeat(60)));
|
|
247
|
+
|
|
248
|
+
for (const sprint of sprints) {
|
|
249
|
+
const metaPath = path.join(sprintsDir, sprint, 'meta.md');
|
|
250
|
+
if (fs.existsSync(metaPath)) {
|
|
251
|
+
const content = fs.readFileSync(metaPath, 'utf-8');
|
|
252
|
+
const statusMatch = content.match(/상태 \| (active|completed)/);
|
|
253
|
+
const status = statusMatch ? statusMatch[1] : 'unknown';
|
|
254
|
+
const statusIcon = status === 'active' ? '🟢' : '✅';
|
|
255
|
+
const statusText = status === 'active' ? chalk.green('진행 중') : chalk.gray('완료');
|
|
256
|
+
|
|
257
|
+
console.log(`${statusIcon} ${chalk.cyan(sprint)} - ${statusText}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log('');
|
|
262
|
+
}
|
package/src/commands/validate.js
CHANGED
|
@@ -5,18 +5,14 @@ import { getWorkspaceDir, isWorkspaceSetup } from '../utils/files.js';
|
|
|
5
5
|
|
|
6
6
|
export async function validate(doc) {
|
|
7
7
|
if (!isWorkspaceSetup()) {
|
|
8
|
-
console.log(chalk.red('
|
|
8
|
+
console.log(chalk.red('? 먼저 setup을 실행하세요.'));
|
|
9
9
|
process.exit(1);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
const workspace = getWorkspaceDir();
|
|
13
13
|
const artifactsDir = path.join(workspace, 'artifacts');
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
console.log(chalk.cyan('━'.repeat(50)));
|
|
17
|
-
console.log(chalk.cyan.bold('📋 문서 검증'));
|
|
18
|
-
console.log(chalk.cyan('━'.repeat(50)));
|
|
19
|
-
console.log('');
|
|
15
|
+
printSection('문서 검증');
|
|
20
16
|
|
|
21
17
|
let totalPass = 0;
|
|
22
18
|
let totalFail = 0;
|
|
@@ -37,7 +33,7 @@ export async function validate(doc) {
|
|
|
37
33
|
totalWarn += result.warn;
|
|
38
34
|
} else {
|
|
39
35
|
// 전체 검증
|
|
40
|
-
for (const
|
|
36
|
+
for (const validator of Object.values(validators)) {
|
|
41
37
|
const result = validator(artifactsDir);
|
|
42
38
|
totalPass += result.pass;
|
|
43
39
|
totalFail += result.fail;
|
|
@@ -45,15 +41,10 @@ export async function validate(doc) {
|
|
|
45
41
|
}
|
|
46
42
|
}
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
console.log(
|
|
50
|
-
console.log(chalk.
|
|
51
|
-
console.log(chalk.
|
|
52
|
-
console.log(chalk.cyan('━'.repeat(50)));
|
|
53
|
-
console.log('');
|
|
54
|
-
console.log(chalk.green(` ✓ PASS: ${totalPass}`));
|
|
55
|
-
console.log(chalk.red(` ✗ FAIL: ${totalFail}`));
|
|
56
|
-
console.log(chalk.yellow(` ⚠ WARN: ${totalWarn}`));
|
|
44
|
+
printSection('검증 결과');
|
|
45
|
+
console.log(chalk.green(` PASS: ${totalPass}`));
|
|
46
|
+
console.log(chalk.red(` FAIL: ${totalFail}`));
|
|
47
|
+
console.log(chalk.yellow(` WARN: ${totalWarn}`));
|
|
57
48
|
console.log('');
|
|
58
49
|
|
|
59
50
|
if (totalFail > 0) {
|
|
@@ -65,10 +56,10 @@ function validatePlan(artifactsDir) {
|
|
|
65
56
|
const filePath = path.join(artifactsDir, 'plan.md');
|
|
66
57
|
let pass = 0, fail = 0, warn = 0;
|
|
67
58
|
|
|
68
|
-
|
|
59
|
+
printSection('plan.md');
|
|
69
60
|
|
|
70
61
|
if (!fs.existsSync(filePath)) {
|
|
71
|
-
|
|
62
|
+
logFail('파일 없음');
|
|
72
63
|
return { pass: 0, fail: 1, warn: 0 };
|
|
73
64
|
}
|
|
74
65
|
|
|
@@ -78,10 +69,10 @@ function validatePlan(artifactsDir) {
|
|
|
78
69
|
const requiredSections = ['서비스 개요', '기능 목록', '비기능 요구사항'];
|
|
79
70
|
for (const section of requiredSections) {
|
|
80
71
|
if (content.includes(section)) {
|
|
81
|
-
|
|
72
|
+
logPass(`섹션 존재: ${section}`);
|
|
82
73
|
pass++;
|
|
83
74
|
} else {
|
|
84
|
-
|
|
75
|
+
logFail(`섹션 누락: ${section}`);
|
|
85
76
|
fail++;
|
|
86
77
|
}
|
|
87
78
|
}
|
|
@@ -89,10 +80,10 @@ function validatePlan(artifactsDir) {
|
|
|
89
80
|
// TBD 개수 검사
|
|
90
81
|
const tbdMatches = content.match(/TBD/gi) || [];
|
|
91
82
|
if (tbdMatches.length > 3) {
|
|
92
|
-
|
|
83
|
+
logWarn(`TBD 항목: ${tbdMatches.length}개 (3개 초과)`);
|
|
93
84
|
warn++;
|
|
94
85
|
} else {
|
|
95
|
-
|
|
86
|
+
logPass(`TBD 항목: ${tbdMatches.length}개`);
|
|
96
87
|
pass++;
|
|
97
88
|
}
|
|
98
89
|
|
|
@@ -104,10 +95,10 @@ function validateProject(artifactsDir) {
|
|
|
104
95
|
const filePath = path.join(artifactsDir, 'project.md');
|
|
105
96
|
let pass = 0, fail = 0, warn = 0;
|
|
106
97
|
|
|
107
|
-
|
|
98
|
+
printSection('project.md');
|
|
108
99
|
|
|
109
100
|
if (!fs.existsSync(filePath)) {
|
|
110
|
-
|
|
101
|
+
logFail('파일 없음');
|
|
111
102
|
return { pass: 0, fail: 1, warn: 0 };
|
|
112
103
|
}
|
|
113
104
|
|
|
@@ -117,29 +108,29 @@ function validateProject(artifactsDir) {
|
|
|
117
108
|
const requiredSections = ['프로젝트 규모', '기술 스택'];
|
|
118
109
|
for (const section of requiredSections) {
|
|
119
110
|
if (content.includes(section)) {
|
|
120
|
-
|
|
111
|
+
logPass(`섹션 존재: ${section}`);
|
|
121
112
|
pass++;
|
|
122
113
|
} else {
|
|
123
|
-
|
|
114
|
+
logFail(`섹션 누락: ${section}`);
|
|
124
115
|
fail++;
|
|
125
116
|
}
|
|
126
117
|
}
|
|
127
118
|
|
|
128
119
|
// Frozen 상태 검사
|
|
129
|
-
if (content.includes('Frozen') || content.includes('
|
|
130
|
-
|
|
120
|
+
if (content.includes('Frozen') || content.includes('??')) {
|
|
121
|
+
logPass('Frozen 상태 표시됨');
|
|
131
122
|
pass++;
|
|
132
123
|
} else {
|
|
133
|
-
|
|
124
|
+
logWarn('Frozen 상태 미표시');
|
|
134
125
|
warn++;
|
|
135
126
|
}
|
|
136
127
|
|
|
137
128
|
// 모호한 버전 검사 (예: 1.x, 2.x)
|
|
138
129
|
if (/\d+\.x/i.test(content)) {
|
|
139
|
-
|
|
130
|
+
logWarn('모호한 버전 형식 (예: 1.x)');
|
|
140
131
|
warn++;
|
|
141
132
|
} else {
|
|
142
|
-
|
|
133
|
+
logPass('버전 형식 양호');
|
|
143
134
|
pass++;
|
|
144
135
|
}
|
|
145
136
|
|
|
@@ -147,35 +138,40 @@ function validateProject(artifactsDir) {
|
|
|
147
138
|
return { pass, fail, warn };
|
|
148
139
|
}
|
|
149
140
|
|
|
150
|
-
function validateBacklog(artifactsDir) {
|
|
151
|
-
const
|
|
141
|
+
export function validateBacklog(artifactsDir) {
|
|
142
|
+
const backlogDir = path.join(artifactsDir, 'backlog');
|
|
152
143
|
let pass = 0, fail = 0, warn = 0;
|
|
153
144
|
|
|
154
|
-
|
|
145
|
+
printSection('backlog/');
|
|
155
146
|
|
|
156
|
-
if (!fs.existsSync(
|
|
157
|
-
|
|
158
|
-
|
|
147
|
+
if (!fs.existsSync(backlogDir) || !fs.statSync(backlogDir).isDirectory()) {
|
|
148
|
+
logWarn('backlog/ 디렉토리 없음 (Task 생성 전)');
|
|
149
|
+
console.log('');
|
|
150
|
+
return { pass: 0, fail: 0, warn: 1 };
|
|
159
151
|
}
|
|
160
152
|
|
|
161
|
-
const
|
|
153
|
+
const taskFiles = fs.readdirSync(backlogDir).filter(file => /^task-\d+\.md$/i.test(file));
|
|
162
154
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (taskMatches.length > 0) {
|
|
166
|
-
console.log(chalk.green(` ✓ Task 개수: ${taskMatches.length}개`));
|
|
155
|
+
if (taskFiles.length > 0) {
|
|
156
|
+
logPass(`Task 개수: ${taskFiles.length}개`);
|
|
167
157
|
pass++;
|
|
168
158
|
} else {
|
|
169
|
-
|
|
170
|
-
|
|
159
|
+
logWarn('대기 Task 없음');
|
|
160
|
+
warn++;
|
|
161
|
+
console.log('');
|
|
162
|
+
return { pass, fail, warn };
|
|
171
163
|
}
|
|
172
164
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
165
|
+
const missingAcceptance = taskFiles.filter(file => {
|
|
166
|
+
const content = fs.readFileSync(path.join(backlogDir, file), 'utf-8');
|
|
167
|
+
return !hasAcceptanceCriteria(content);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (missingAcceptance.length === 0) {
|
|
171
|
+
logPass(`수용 조건 존재 (${taskFiles.length}/${taskFiles.length})`);
|
|
176
172
|
pass++;
|
|
177
173
|
} else {
|
|
178
|
-
|
|
174
|
+
logWarn(`수용 조건 미확인 (${taskFiles.length - missingAcceptance.length}/${taskFiles.length})`);
|
|
179
175
|
warn++;
|
|
180
176
|
}
|
|
181
177
|
|
|
@@ -183,37 +179,110 @@ function validateBacklog(artifactsDir) {
|
|
|
183
179
|
return { pass, fail, warn };
|
|
184
180
|
}
|
|
185
181
|
|
|
186
|
-
function validateSprint(artifactsDir) {
|
|
187
|
-
const
|
|
182
|
+
export function validateSprint(artifactsDir) {
|
|
183
|
+
const sprintsDir = path.join(artifactsDir, 'sprints');
|
|
188
184
|
let pass = 0, fail = 0, warn = 0;
|
|
189
185
|
|
|
190
|
-
|
|
186
|
+
printSection('sprints/');
|
|
191
187
|
|
|
192
|
-
if (!fs.existsSync(
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
if (!fs.existsSync(sprintsDir) || !fs.statSync(sprintsDir).isDirectory()) {
|
|
189
|
+
logWarn('sprints/ 디렉토리 없음 (스프린트 시작 전)');
|
|
190
|
+
console.log('');
|
|
191
|
+
return { pass: 0, fail: 0, warn: 1 };
|
|
195
192
|
}
|
|
196
193
|
|
|
197
|
-
const
|
|
194
|
+
const sprintDirs = fs.readdirSync(sprintsDir, { withFileTypes: true })
|
|
195
|
+
.filter(dirent => dirent.isDirectory() && /^sprint-\d+$/.test(dirent.name))
|
|
196
|
+
.map(dirent => dirent.name);
|
|
197
|
+
|
|
198
|
+
if (sprintDirs.length === 0) {
|
|
199
|
+
logWarn('스프린트 디렉토리 없음 (스프린트 시작 전)');
|
|
200
|
+
warn++;
|
|
201
|
+
console.log('');
|
|
202
|
+
return { pass, fail, warn };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const latestSprint = sprintDirs.sort((a, b) => {
|
|
206
|
+
const numA = parseInt(a.split('-')[1], 10);
|
|
207
|
+
const numB = parseInt(b.split('-')[1], 10);
|
|
208
|
+
return numB - numA;
|
|
209
|
+
})[0];
|
|
210
|
+
|
|
211
|
+
const metaPath = path.join(sprintsDir, latestSprint, 'meta.md');
|
|
212
|
+
if (!fs.existsSync(metaPath)) {
|
|
213
|
+
logFail(`${latestSprint}/meta.md 없음`);
|
|
214
|
+
console.log('');
|
|
215
|
+
return { pass: 0, fail: 1, warn: 0 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const content = fs.readFileSync(metaPath, 'utf-8');
|
|
219
|
+
logInfo(`최신 스프린트: ${latestSprint}`);
|
|
220
|
+
|
|
221
|
+
const requiredFields = ['스프린트 번호', '상태', '시작일', '종료 예정', '목표'];
|
|
222
|
+
requiredFields.forEach(field => {
|
|
223
|
+
if (content.includes(field)) {
|
|
224
|
+
logPass(`필드 존재: ${field}`);
|
|
225
|
+
pass++;
|
|
226
|
+
} else {
|
|
227
|
+
logWarn(`필드 미확인: ${field}`);
|
|
228
|
+
warn++;
|
|
229
|
+
}
|
|
230
|
+
});
|
|
198
231
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
console.log(chalk.green(' ✓ 스프린트 번호 존재'));
|
|
232
|
+
if (content.includes('Task 요약')) {
|
|
233
|
+
logPass('Task 요약 섹션 존재');
|
|
202
234
|
pass++;
|
|
203
235
|
} else {
|
|
204
|
-
|
|
236
|
+
logWarn('Task 요약 섹션 미확인');
|
|
205
237
|
warn++;
|
|
206
238
|
}
|
|
207
239
|
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
240
|
+
const tasksDir = path.join(sprintsDir, latestSprint, 'tasks');
|
|
241
|
+
if (fs.existsSync(tasksDir) && fs.statSync(tasksDir).isDirectory()) {
|
|
242
|
+
const taskFiles = fs.readdirSync(tasksDir).filter(file => /^task-\d+\.md$/i.test(file));
|
|
243
|
+
if (taskFiles.length > 0) {
|
|
244
|
+
logPass(`Task 개수: ${taskFiles.length}개`);
|
|
245
|
+
pass++;
|
|
246
|
+
} else {
|
|
247
|
+
logWarn('Task 없음');
|
|
248
|
+
warn++;
|
|
249
|
+
}
|
|
212
250
|
} else {
|
|
213
|
-
|
|
251
|
+
logWarn('tasks/ 디렉토리 없음');
|
|
214
252
|
warn++;
|
|
215
253
|
}
|
|
216
254
|
|
|
217
255
|
console.log('');
|
|
218
256
|
return { pass, fail, warn };
|
|
219
257
|
}
|
|
258
|
+
|
|
259
|
+
function hasAcceptanceCriteria(content) {
|
|
260
|
+
return content.includes('수용 조건') || content.includes('Acceptance Criteria') || content.includes('AC-');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function printSection(title) {
|
|
264
|
+
const width = 60;
|
|
265
|
+
const line = '─'.repeat(width);
|
|
266
|
+
const paddedTitle = ` ${title}`.padEnd(width, ' ');
|
|
267
|
+
console.log('');
|
|
268
|
+
console.log(chalk.cyan(`┌${line}┐`));
|
|
269
|
+
console.log(chalk.cyan(`│${paddedTitle}│`));
|
|
270
|
+
console.log(chalk.cyan(`└${line}┘`));
|
|
271
|
+
console.log('');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function logPass(message) {
|
|
275
|
+
console.log(chalk.green(` [PASS] ${message}`));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function logWarn(message) {
|
|
279
|
+
console.log(chalk.yellow(` [WARN] ${message}`));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function logFail(message) {
|
|
283
|
+
console.log(chalk.red(` [FAIL] ${message}`));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function logInfo(message) {
|
|
287
|
+
console.log(chalk.cyan(` [INFO] ${message}`));
|
|
288
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { validateBacklog, validateSprint } from './validate.js';
|
|
6
|
+
|
|
7
|
+
function createArtifactsDir(t) {
|
|
8
|
+
const baseDir = path.join(process.cwd(), 'tmp', 'validate-tests');
|
|
9
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
10
|
+
|
|
11
|
+
const rootDir = fs.mkdtempSync(path.join(baseDir, 'case-'));
|
|
12
|
+
const artifactsDir = path.join(rootDir, 'artifacts');
|
|
13
|
+
fs.mkdirSync(artifactsDir, { recursive: true });
|
|
14
|
+
|
|
15
|
+
t.after(() => {
|
|
16
|
+
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return artifactsDir;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test('validateBacklog: backlog/의 task-*.md를 검증한다', (t) => {
|
|
23
|
+
const artifactsDir = createArtifactsDir(t);
|
|
24
|
+
const backlogDir = path.join(artifactsDir, 'backlog');
|
|
25
|
+
fs.mkdirSync(backlogDir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
fs.writeFileSync(
|
|
28
|
+
path.join(backlogDir, 'task-001.md'),
|
|
29
|
+
'# TASK-001\n\n## Acceptance Criteria\n- [ ] 준비\n'
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const result = validateBacklog(artifactsDir);
|
|
33
|
+
assert.equal(result.fail, 0);
|
|
34
|
+
assert.equal(result.warn, 0);
|
|
35
|
+
assert.ok(result.pass > 0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('validateBacklog: Task가 없으면 경고한다', (t) => {
|
|
39
|
+
const artifactsDir = createArtifactsDir(t);
|
|
40
|
+
const backlogDir = path.join(artifactsDir, 'backlog');
|
|
41
|
+
fs.mkdirSync(backlogDir, { recursive: true });
|
|
42
|
+
|
|
43
|
+
const result = validateBacklog(artifactsDir);
|
|
44
|
+
assert.equal(result.fail, 0);
|
|
45
|
+
assert.ok(result.warn >= 1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('validateSprint: 최신 스프린트 meta.md와 tasks를 확인한다', (t) => {
|
|
49
|
+
const artifactsDir = createArtifactsDir(t);
|
|
50
|
+
const sprintDir = path.join(artifactsDir, 'sprints', 'sprint-1');
|
|
51
|
+
const tasksDir = path.join(sprintDir, 'tasks');
|
|
52
|
+
fs.mkdirSync(tasksDir, { recursive: true });
|
|
53
|
+
|
|
54
|
+
fs.writeFileSync(
|
|
55
|
+
path.join(sprintDir, 'meta.md'),
|
|
56
|
+
[
|
|
57
|
+
'# Sprint 1 메타정보',
|
|
58
|
+
'',
|
|
59
|
+
'| 항목 | 값 |',
|
|
60
|
+
'|------|-----|',
|
|
61
|
+
'| 스프린트 번호 | 1 |',
|
|
62
|
+
'| 상태 | active |',
|
|
63
|
+
'| 시작일 | 2025-01-01 |',
|
|
64
|
+
'| 종료 예정 | 2025-01-07 |',
|
|
65
|
+
'| 목표 | 검증 |',
|
|
66
|
+
'',
|
|
67
|
+
'## Task 요약',
|
|
68
|
+
'',
|
|
69
|
+
'| Task | 상태 | 담당 | 우선순위 | 크기 |',
|
|
70
|
+
'|------|:----:|------|:--------:|:----:|',
|
|
71
|
+
'| task-001 | BACKLOG | - | P0 | S |',
|
|
72
|
+
''
|
|
73
|
+
].join('\n')
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
fs.writeFileSync(
|
|
77
|
+
path.join(tasksDir, 'task-001.md'),
|
|
78
|
+
'# TASK-001\n\n## Acceptance Criteria\n- [ ] 준비\n'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const result = validateSprint(artifactsDir);
|
|
82
|
+
assert.equal(result.fail, 0);
|
|
83
|
+
assert.equal(result.warn, 0);
|
|
84
|
+
});
|