@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.
Files changed (44) hide show
  1. package/README.md +294 -661
  2. package/bin/cli.js +8 -1
  3. package/core/artifacts/decision.md +1 -1
  4. package/core/artifacts/plan.md +5 -5
  5. package/core/artifacts/project.md +4 -2
  6. package/core/artifacts/sprints/_template/docs/release-notes.md +37 -0
  7. package/core/artifacts/sprints/_template/meta.md +54 -0
  8. package/core/artifacts/sprints/_template/retrospective.md +50 -0
  9. package/core/artifacts/sprints/_template/review-reports/review-template.md +49 -0
  10. package/core/artifacts/sprints/_template/tasks/task-template.md +43 -0
  11. package/core/roles/analyzer.md +6 -33
  12. package/core/roles/developer.md +69 -99
  13. package/core/roles/documenter.md +293 -0
  14. package/core/roles/manager.md +391 -341
  15. package/core/roles/planner.md +61 -79
  16. package/core/roles/reviewer.md +71 -129
  17. package/core/rules/document-priority.md +40 -38
  18. package/core/rules/escalation.md +15 -14
  19. package/core/rules/iteration.md +24 -24
  20. package/core/rules/rfc.md +1 -1
  21. package/core/rules/rollback.md +4 -4
  22. package/package.json +1 -1
  23. package/src/commands/run.js +95 -46
  24. package/src/commands/sessions.js +21 -1
  25. package/src/commands/sprint.js +262 -0
  26. package/src/commands/validate.js +133 -64
  27. package/src/commands/validate.test.js +84 -0
  28. package/src/utils/files.js +134 -134
  29. package/src/utils/sessionState.js +30 -11
  30. package/core/artifacts/architecture-options.md +0 -85
  31. package/core/artifacts/backlog.md +0 -177
  32. package/core/artifacts/current-sprint.md +0 -125
  33. package/core/artifacts/qa-report.md +0 -104
  34. package/core/artifacts/review-report.md +0 -103
  35. package/core/roles/architect.md +0 -270
  36. package/core/roles/qa.md +0 -306
  37. package/core/rules/role-state-protocol.md +0 -281
  38. package/core/rules/session-state.md +0 -255
  39. package/templates/cli/roles/cli-developer.md +0 -243
  40. package/templates/game/roles/game-logic.md +0 -198
  41. package/templates/game/roles/rendering.md +0 -142
  42. package/templates/library/roles/library-developer.md +0 -184
  43. package/templates/web-dev/roles/backend.md +0 -139
  44. 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
+ }
@@ -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(' 먼저 setup을 실행하세요.'));
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
- console.log('');
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 [name, validator] of Object.entries(validators)) {
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.cyan('━'.repeat(50)));
51
- console.log(chalk.white.bold('📊 검증 결과'));
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
- console.log(chalk.white.bold('📄 plan.md'));
59
+ printSection('plan.md');
69
60
 
70
61
  if (!fs.existsSync(filePath)) {
71
- console.log(chalk.red('파일 없음'));
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
- console.log(chalk.green(` ✓ 섹션 존재: ${section}`));
72
+ logPass(`섹션 존재: ${section}`);
82
73
  pass++;
83
74
  } else {
84
- console.log(chalk.red(` ✗ 섹션 누락: ${section}`));
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
- console.log(chalk.yellow(`TBD 항목: ${tbdMatches.length}개 (3개 초과)`));
83
+ logWarn(`TBD 항목: ${tbdMatches.length}개 (3개 초과)`);
93
84
  warn++;
94
85
  } else {
95
- console.log(chalk.green(`TBD 항목: ${tbdMatches.length}개`));
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
- console.log(chalk.white.bold('📄 project.md'));
98
+ printSection('project.md');
108
99
 
109
100
  if (!fs.existsSync(filePath)) {
110
- console.log(chalk.red('파일 없음'));
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
- console.log(chalk.green(` ✓ 섹션 존재: ${section}`));
111
+ logPass(`섹션 존재: ${section}`);
121
112
  pass++;
122
113
  } else {
123
- console.log(chalk.red(` ✗ 섹션 누락: ${section}`));
114
+ logFail(`섹션 누락: ${section}`);
124
115
  fail++;
125
116
  }
126
117
  }
127
118
 
128
119
  // Frozen 상태 검사
129
- if (content.includes('Frozen') || content.includes('🔒')) {
130
- console.log(chalk.green('Frozen 상태 표시됨'));
120
+ if (content.includes('Frozen') || content.includes('??')) {
121
+ logPass('Frozen 상태 표시됨');
131
122
  pass++;
132
123
  } else {
133
- console.log(chalk.yellow('Frozen 상태 미표시'));
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
- console.log(chalk.yellow('모호한 버전 형식 (예: 1.x)'));
130
+ logWarn('모호한 버전 형식 (예: 1.x)');
140
131
  warn++;
141
132
  } else {
142
- console.log(chalk.green('버전 형식 양호'));
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 filePath = path.join(artifactsDir, 'backlog.md');
141
+ export function validateBacklog(artifactsDir) {
142
+ const backlogDir = path.join(artifactsDir, 'backlog');
152
143
  let pass = 0, fail = 0, warn = 0;
153
144
 
154
- console.log(chalk.white.bold('📄 backlog.md'));
145
+ printSection('backlog/');
155
146
 
156
- if (!fs.existsSync(filePath)) {
157
- console.log(chalk.red(' 파일 없음'));
158
- return { pass: 0, fail: 1, warn: 0 };
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 content = fs.readFileSync(filePath, 'utf-8');
153
+ const taskFiles = fs.readdirSync(backlogDir).filter(file => /^task-\d+\.md$/i.test(file));
162
154
 
163
- // Task 개수 검사
164
- const taskMatches = content.match(/TASK-\d+/gi) || [];
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
- console.log(chalk.red(' Task 없음'));
170
- fail++;
159
+ logWarn('대기 Task 없음');
160
+ warn++;
161
+ console.log('');
162
+ return { pass, fail, warn };
171
163
  }
172
164
 
173
- // 수용 조건 존재 검사
174
- if (content.includes('수용 조건') || content.includes('AC-') || content.includes('Acceptance')) {
175
- console.log(chalk.green(' ✓ 수용 조건 존재'));
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
- console.log(chalk.yellow(' ⚠ 수용 조건 미확인'));
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 filePath = path.join(artifactsDir, 'current-sprint.md');
182
+ export function validateSprint(artifactsDir) {
183
+ const sprintsDir = path.join(artifactsDir, 'sprints');
188
184
  let pass = 0, fail = 0, warn = 0;
189
185
 
190
- console.log(chalk.white.bold('📄 current-sprint.md'));
186
+ printSection('sprints/');
191
187
 
192
- if (!fs.existsSync(filePath)) {
193
- console.log(chalk.gray(' - 파일 없음 (스프린트 시작 전)'));
194
- return { pass: 0, fail: 0, warn: 0 };
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 content = fs.readFileSync(filePath, 'utf-8');
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
- if (/Sprint\s*#?\d+/i.test(content) || /스프린트\s*#?\d+/.test(content)) {
201
- console.log(chalk.green(' ✓ 스프린트 번호 존재'));
232
+ if (content.includes('Task 요약')) {
233
+ logPass('Task 요약 섹션 존재');
202
234
  pass++;
203
235
  } else {
204
- console.log(chalk.yellow(' 스프린트 번호 미확인'));
236
+ logWarn('Task 요약 섹션 미확인');
205
237
  warn++;
206
238
  }
207
239
 
208
- // 목표 섹션 검사
209
- if (content.includes('목표') || content.includes('Goal')) {
210
- console.log(chalk.green(' ✓ 목표 섹션 존재'));
211
- pass++;
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
- console.log(chalk.yellow(' 목표 섹션 미확인'));
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
+ });