sdd-tool 0.1.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/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/sdd.js +3 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +3979 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/index.d.ts +240 -0
- package/dist/index.js +160 -0
- package/dist/index.js.map +1 -0
- package/package.json +75 -0
- package/templates/agents.md +62 -0
- package/templates/constitution.md +43 -0
- package/templates/proposal.md +71 -0
- package/templates/spec.md +27 -0
|
@@ -0,0 +1,3979 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/cli/index.ts
|
|
8
|
+
import { Command } from "commander";
|
|
9
|
+
import { createRequire } from "module";
|
|
10
|
+
|
|
11
|
+
// src/cli/commands/init.ts
|
|
12
|
+
import path2 from "path";
|
|
13
|
+
|
|
14
|
+
// src/utils/fs.ts
|
|
15
|
+
import { promises as fs } from "fs";
|
|
16
|
+
import path from "path";
|
|
17
|
+
|
|
18
|
+
// src/errors/codes.ts
|
|
19
|
+
var ExitCode = {
|
|
20
|
+
SUCCESS: 0,
|
|
21
|
+
GENERAL_ERROR: 1,
|
|
22
|
+
VALIDATION_FAILED: 2,
|
|
23
|
+
CONSTITUTION_VIOLATION: 3,
|
|
24
|
+
FILE_SYSTEM_ERROR: 4,
|
|
25
|
+
USER_CANCELLED: 5
|
|
26
|
+
};
|
|
27
|
+
var ErrorCode = {
|
|
28
|
+
// 일반 에러 (E0xx)
|
|
29
|
+
UNKNOWN: "E001",
|
|
30
|
+
INVALID_ARGUMENT: "E002",
|
|
31
|
+
NOT_INITIALIZED: "E003",
|
|
32
|
+
// 파일 시스템 에러 (E1xx)
|
|
33
|
+
FILE_NOT_FOUND: "E101",
|
|
34
|
+
FILE_READ_ERROR: "E102",
|
|
35
|
+
FILE_WRITE_ERROR: "E103",
|
|
36
|
+
DIRECTORY_NOT_FOUND: "E104",
|
|
37
|
+
DIRECTORY_EXISTS: "E105",
|
|
38
|
+
// 스펙 검증 에러 (E2xx)
|
|
39
|
+
SPEC_PARSE_ERROR: "E201",
|
|
40
|
+
SPEC_INVALID_FORMAT: "E202",
|
|
41
|
+
SPEC_MISSING_REQUIRED: "E203",
|
|
42
|
+
RFC2119_VIOLATION: "E204",
|
|
43
|
+
GWT_INVALID_FORMAT: "E205",
|
|
44
|
+
// Constitution 에러 (E3xx)
|
|
45
|
+
CONSTITUTION_NOT_FOUND: "E301",
|
|
46
|
+
CONSTITUTION_PARSE_ERROR: "E302",
|
|
47
|
+
CONSTITUTION_VIOLATION: "E303",
|
|
48
|
+
// 변경 워크플로우 에러 (E4xx)
|
|
49
|
+
PROPOSAL_NOT_FOUND: "E401",
|
|
50
|
+
PROPOSAL_INVALID: "E402",
|
|
51
|
+
DELTA_CONFLICT: "E403",
|
|
52
|
+
ARCHIVE_FAILED: "E404",
|
|
53
|
+
// 분석 에러 (E5xx)
|
|
54
|
+
ANALYSIS_FAILED: "E501",
|
|
55
|
+
INSUFFICIENT_DATA: "E502"
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/errors/messages.ts
|
|
59
|
+
var ErrorMessages = {
|
|
60
|
+
// 일반 에러
|
|
61
|
+
[ErrorCode.UNKNOWN]: "\uC54C \uC218 \uC5C6\uB294 \uC624\uB958\uAC00 \uBC1C\uC0DD\uD588\uC2B5\uB2C8\uB2E4",
|
|
62
|
+
[ErrorCode.INVALID_ARGUMENT]: "\uC798\uBABB\uB41C \uC778\uC790\uC785\uB2C8\uB2E4: {0}",
|
|
63
|
+
[ErrorCode.NOT_INITIALIZED]: "SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694",
|
|
64
|
+
// 파일 시스템 에러
|
|
65
|
+
[ErrorCode.FILE_NOT_FOUND]: "\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {0}",
|
|
66
|
+
[ErrorCode.FILE_READ_ERROR]: "\uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: {0}",
|
|
67
|
+
[ErrorCode.FILE_WRITE_ERROR]: "\uD30C\uC77C \uC4F0\uAE30 \uC2E4\uD328: {0}",
|
|
68
|
+
[ErrorCode.DIRECTORY_NOT_FOUND]: "\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {0}",
|
|
69
|
+
[ErrorCode.DIRECTORY_EXISTS]: "\uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: {0}",
|
|
70
|
+
// 스펙 검증 에러
|
|
71
|
+
[ErrorCode.SPEC_PARSE_ERROR]: "\uC2A4\uD399 \uD30C\uC2F1 \uC2E4\uD328: {0}",
|
|
72
|
+
[ErrorCode.SPEC_INVALID_FORMAT]: "\uC798\uBABB\uB41C \uC2A4\uD399 \uD615\uC2DD: {0}",
|
|
73
|
+
[ErrorCode.SPEC_MISSING_REQUIRED]: "\uD544\uC218 \uD544\uB4DC \uB204\uB77D: {0}",
|
|
74
|
+
[ErrorCode.RFC2119_VIOLATION]: "RFC 2119 \uD615\uC2DD \uC704\uBC18: {0}",
|
|
75
|
+
[ErrorCode.GWT_INVALID_FORMAT]: "GIVEN-WHEN-THEN \uD615\uC2DD \uC704\uBC18: {0}",
|
|
76
|
+
// Constitution 에러
|
|
77
|
+
[ErrorCode.CONSTITUTION_NOT_FOUND]: "constitution.md\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
78
|
+
[ErrorCode.CONSTITUTION_PARSE_ERROR]: "Constitution \uD30C\uC2F1 \uC2E4\uD328: {0}",
|
|
79
|
+
[ErrorCode.CONSTITUTION_VIOLATION]: "Constitution \uC6D0\uCE59 \uC704\uBC18: {0}",
|
|
80
|
+
// 변경 워크플로우 에러
|
|
81
|
+
[ErrorCode.PROPOSAL_NOT_FOUND]: "\uC81C\uC548\uC11C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: {0}",
|
|
82
|
+
[ErrorCode.PROPOSAL_INVALID]: "\uC798\uBABB\uB41C \uC81C\uC548\uC11C \uD615\uC2DD: {0}",
|
|
83
|
+
[ErrorCode.DELTA_CONFLICT]: "\uB378\uD0C0 \uCDA9\uB3CC: {0}",
|
|
84
|
+
[ErrorCode.ARCHIVE_FAILED]: "\uC544\uCE74\uC774\uBE0C \uC2E4\uD328: {0}",
|
|
85
|
+
// 분석 에러
|
|
86
|
+
[ErrorCode.ANALYSIS_FAILED]: "\uBD84\uC11D \uC2E4\uD328: {0}",
|
|
87
|
+
[ErrorCode.INSUFFICIENT_DATA]: "\uBD84\uC11D\uC5D0 \uD544\uC694\uD55C \uB370\uC774\uD130 \uBD80\uC871: {0}"
|
|
88
|
+
};
|
|
89
|
+
function formatMessage(code, ...args) {
|
|
90
|
+
let message = ErrorMessages[code];
|
|
91
|
+
args.forEach((arg, index) => {
|
|
92
|
+
message = message.replace(`{${index}}`, arg);
|
|
93
|
+
});
|
|
94
|
+
return message;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/errors/base.ts
|
|
98
|
+
var SddError = class extends Error {
|
|
99
|
+
code;
|
|
100
|
+
exitCode;
|
|
101
|
+
constructor(code, message, exitCode = ExitCode.GENERAL_ERROR) {
|
|
102
|
+
super(message ?? formatMessage(code));
|
|
103
|
+
this.name = "SddError";
|
|
104
|
+
this.code = code;
|
|
105
|
+
this.exitCode = exitCode;
|
|
106
|
+
Error.captureStackTrace?.(this, this.constructor);
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 사용자 친화적 메시지
|
|
110
|
+
*/
|
|
111
|
+
toUserMessage() {
|
|
112
|
+
return `[${this.code}] ${this.message}`;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var FileSystemError = class extends SddError {
|
|
116
|
+
path;
|
|
117
|
+
constructor(code, path13) {
|
|
118
|
+
super(code, formatMessage(code, path13), ExitCode.FILE_SYSTEM_ERROR);
|
|
119
|
+
this.name = "FileSystemError";
|
|
120
|
+
this.path = path13;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var ValidationError = class extends SddError {
|
|
124
|
+
details;
|
|
125
|
+
constructor(code, details) {
|
|
126
|
+
super(code, formatMessage(code, details), ExitCode.VALIDATION_FAILED);
|
|
127
|
+
this.name = "ValidationError";
|
|
128
|
+
this.details = details;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var ChangeError = class extends SddError {
|
|
132
|
+
constructor(message) {
|
|
133
|
+
super(ErrorCode.UNKNOWN, message, ExitCode.GENERAL_ERROR);
|
|
134
|
+
this.name = "ChangeError";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/types/index.ts
|
|
139
|
+
function success(data) {
|
|
140
|
+
return { success: true, data };
|
|
141
|
+
}
|
|
142
|
+
function failure(error2) {
|
|
143
|
+
return { success: false, error: error2 };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/utils/fs.ts
|
|
147
|
+
async function fileExists(filePath) {
|
|
148
|
+
try {
|
|
149
|
+
await fs.access(filePath);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function directoryExists(dirPath) {
|
|
156
|
+
try {
|
|
157
|
+
const stat = await fs.stat(dirPath);
|
|
158
|
+
return stat.isDirectory();
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
async function readFile(filePath) {
|
|
164
|
+
try {
|
|
165
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
166
|
+
return success(content);
|
|
167
|
+
} catch (error2) {
|
|
168
|
+
if (error2.code === "ENOENT") {
|
|
169
|
+
return failure(new FileSystemError(ErrorCode.FILE_NOT_FOUND, filePath));
|
|
170
|
+
}
|
|
171
|
+
return failure(new FileSystemError(ErrorCode.FILE_READ_ERROR, filePath));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function writeFile(filePath, content) {
|
|
175
|
+
try {
|
|
176
|
+
const dir = path.dirname(filePath);
|
|
177
|
+
await fs.mkdir(dir, { recursive: true });
|
|
178
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
179
|
+
return success(void 0);
|
|
180
|
+
} catch {
|
|
181
|
+
return failure(new FileSystemError(ErrorCode.FILE_WRITE_ERROR, filePath));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
async function ensureDir(dirPath) {
|
|
185
|
+
try {
|
|
186
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
187
|
+
return success(void 0);
|
|
188
|
+
} catch {
|
|
189
|
+
return failure(new FileSystemError(ErrorCode.FILE_WRITE_ERROR, dirPath));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async function readDir(dirPath) {
|
|
193
|
+
try {
|
|
194
|
+
const entries = await fs.readdir(dirPath);
|
|
195
|
+
return success(entries);
|
|
196
|
+
} catch {
|
|
197
|
+
return failure(new FileSystemError(ErrorCode.DIRECTORY_NOT_FOUND, dirPath));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function findSddRoot(startPath = process.cwd()) {
|
|
201
|
+
let currentPath = path.resolve(startPath);
|
|
202
|
+
const root = path.parse(currentPath).root;
|
|
203
|
+
while (currentPath !== root) {
|
|
204
|
+
const sddPath = path.join(currentPath, ".sdd");
|
|
205
|
+
if (await directoryExists(sddPath)) {
|
|
206
|
+
return currentPath;
|
|
207
|
+
}
|
|
208
|
+
currentPath = path.dirname(currentPath);
|
|
209
|
+
}
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
async function copyDir(srcPath, destPath) {
|
|
213
|
+
try {
|
|
214
|
+
await fs.mkdir(destPath, { recursive: true });
|
|
215
|
+
const entries = await fs.readdir(srcPath, { withFileTypes: true });
|
|
216
|
+
for (const entry of entries) {
|
|
217
|
+
const srcEntry = path.join(srcPath, entry.name);
|
|
218
|
+
const destEntry = path.join(destPath, entry.name);
|
|
219
|
+
if (entry.isDirectory()) {
|
|
220
|
+
const result = await copyDir(srcEntry, destEntry);
|
|
221
|
+
if (!result.success) {
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
await fs.copyFile(srcEntry, destEntry);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return success(void 0);
|
|
229
|
+
} catch {
|
|
230
|
+
return failure(new FileSystemError(ErrorCode.FILE_WRITE_ERROR, destPath));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function removeDir(dirPath) {
|
|
234
|
+
try {
|
|
235
|
+
await fs.rm(dirPath, { recursive: true, force: true });
|
|
236
|
+
return success(void 0);
|
|
237
|
+
} catch {
|
|
238
|
+
return failure(new FileSystemError(ErrorCode.FILE_WRITE_ERROR, dirPath));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/utils/logger.ts
|
|
243
|
+
var logger_exports = {};
|
|
244
|
+
__export(logger_exports, {
|
|
245
|
+
debug: () => debug,
|
|
246
|
+
error: () => error,
|
|
247
|
+
info: () => info,
|
|
248
|
+
listItem: () => listItem,
|
|
249
|
+
newline: () => newline,
|
|
250
|
+
setLogLevel: () => setLogLevel,
|
|
251
|
+
success: () => success2,
|
|
252
|
+
title: () => title,
|
|
253
|
+
warn: () => warn
|
|
254
|
+
});
|
|
255
|
+
import chalk from "chalk";
|
|
256
|
+
var LOG_LEVELS = {
|
|
257
|
+
debug: 0,
|
|
258
|
+
info: 1,
|
|
259
|
+
warn: 2,
|
|
260
|
+
error: 3
|
|
261
|
+
};
|
|
262
|
+
var currentLevel = "info";
|
|
263
|
+
function setLogLevel(level) {
|
|
264
|
+
currentLevel = level;
|
|
265
|
+
}
|
|
266
|
+
function shouldLog(level) {
|
|
267
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[currentLevel];
|
|
268
|
+
}
|
|
269
|
+
function debug(message, ...args) {
|
|
270
|
+
if (shouldLog("debug")) {
|
|
271
|
+
console.log(chalk.gray(`[DEBUG] ${message}`), ...args);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function info(message, ...args) {
|
|
275
|
+
if (shouldLog("info")) {
|
|
276
|
+
console.log(chalk.blue(`\u2139 ${message}`), ...args);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function success2(message, ...args) {
|
|
280
|
+
if (shouldLog("info")) {
|
|
281
|
+
console.log(chalk.green(`\u2713 ${message}`), ...args);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function warn(message, ...args) {
|
|
285
|
+
if (shouldLog("warn")) {
|
|
286
|
+
console.log(chalk.yellow(`\u26A0 ${message}`), ...args);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function error(message, ...args) {
|
|
290
|
+
if (shouldLog("error")) {
|
|
291
|
+
console.error(chalk.red(`\u2717 ${message}`), ...args);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function title(message) {
|
|
295
|
+
console.log();
|
|
296
|
+
console.log(chalk.bold.cyan(message));
|
|
297
|
+
console.log(chalk.cyan("\u2500".repeat(message.length)));
|
|
298
|
+
}
|
|
299
|
+
function listItem(item, indent = 0) {
|
|
300
|
+
const prefix = " ".repeat(indent) + "\u2022 ";
|
|
301
|
+
console.log(prefix + item);
|
|
302
|
+
}
|
|
303
|
+
function newline() {
|
|
304
|
+
console.log();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/generators/agents-md.ts
|
|
308
|
+
function generateAgentsMd(options) {
|
|
309
|
+
const { projectName, projectDescription = "(\uD504\uB85C\uC81D\uD2B8 \uC124\uBA85\uC744 \uCD94\uAC00\uD558\uC138\uC694)" } = options;
|
|
310
|
+
return `# SDD Workflow Guide
|
|
311
|
+
|
|
312
|
+
> **${projectName}** - AI \uC5D0\uC774\uC804\uD2B8 \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC9C0\uCE68\uC11C
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## \uD544\uC218 \uD615\uC2DD \uADDC\uCE59 (\uC774 \uC139\uC158\uC740 \uC0C1\uB2E8 50\uC904 \uB0B4\uC5D0 \uC788\uC5B4\uC57C \uD568)
|
|
317
|
+
|
|
318
|
+
### RFC 2119 \uD0A4\uC6CC\uB4DC
|
|
319
|
+
|
|
320
|
+
| \uD0A4\uC6CC\uB4DC | \uC758\uBBF8 | \uC0AC\uC6A9 \uC608\uC2DC |
|
|
321
|
+
|--------|------|-----------|
|
|
322
|
+
| **SHALL** / **MUST** | \uC808\uB300 \uD544\uC218 | "\uC2DC\uC2A4\uD15C\uC740 \uC778\uC99D\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHALL)" |
|
|
323
|
+
| **SHOULD** | \uAD8C\uC7A5 (\uC608\uC678 \uAC00\uB2A5) | "\uC751\uB2F5 \uC2DC\uAC04\uC740 1\uCD08 \uC774\uB0B4\uC5EC\uC57C \uD55C\uB2E4(SHOULD)" |
|
|
324
|
+
| **MAY** | \uC120\uD0DD\uC801 | "\uB2E4\uD06C \uBAA8\uB4DC\uB97C \uC9C0\uC6D0\uD560 \uC218 \uC788\uB2E4(MAY)" |
|
|
325
|
+
| **SHALL NOT** | \uC808\uB300 \uAE08\uC9C0 | "\uD3C9\uBB38 \uBE44\uBC00\uBC88\uD638\uB97C \uC800\uC7A5\uD574\uC11C\uB294 \uC548 \uB41C\uB2E4(SHALL NOT)" |
|
|
326
|
+
|
|
327
|
+
### GIVEN-WHEN-THEN \uD615\uC2DD
|
|
328
|
+
|
|
329
|
+
\uBAA8\uB4E0 \uC694\uAD6C\uC0AC\uD56D\uC740 \uC544\uB798 \uD615\uC2DD\uC758 \uC2DC\uB098\uB9AC\uC624\uB97C \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4:
|
|
330
|
+
|
|
331
|
+
\`\`\`markdown
|
|
332
|
+
### Scenario: [\uC2DC\uB098\uB9AC\uC624\uBA85]
|
|
333
|
+
|
|
334
|
+
- **GIVEN** [\uC804\uC81C \uC870\uAC74]
|
|
335
|
+
- **WHEN** [\uD589\uB3D9/\uD2B8\uB9AC\uAC70]
|
|
336
|
+
- **THEN** [\uC608\uC0C1 \uACB0\uACFC]
|
|
337
|
+
\`\`\`
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## \uD504\uB85C\uC81D\uD2B8 \uAC1C\uC694
|
|
342
|
+
|
|
343
|
+
**\uD504\uB85C\uC81D\uD2B8**: ${projectName}
|
|
344
|
+
**\uC124\uBA85**: ${projectDescription}
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## \uB514\uB809\uD1A0\uB9AC \uAD6C\uC870
|
|
349
|
+
|
|
350
|
+
\`\`\`
|
|
351
|
+
.sdd/
|
|
352
|
+
\u251C\u2500\u2500 constitution.md # \uD504\uB85C\uC81D\uD2B8 \uD5CC\uBC95 (\uC6D0\uCE59, \uC81C\uC57D)
|
|
353
|
+
\u251C\u2500\u2500 AGENTS.md # \uC774 \uD30C\uC77C (AI \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC9C0\uCE68)
|
|
354
|
+
\u251C\u2500\u2500 specs/ # \uC2A4\uD399 \uBB38\uC11C
|
|
355
|
+
\u2502 \u2514\u2500\u2500 <feature>/
|
|
356
|
+
\u2502 \u2514\u2500\u2500 spec.md
|
|
357
|
+
\u251C\u2500\u2500 changes/ # \uBCC0\uACBD \uC81C\uC548
|
|
358
|
+
\u2502 \u2514\u2500\u2500 <id>/
|
|
359
|
+
\u2502 \u251C\u2500\u2500 proposal.md
|
|
360
|
+
\u2502 \u251C\u2500\u2500 delta.md
|
|
361
|
+
\u2502 \u2514\u2500\u2500 tasks.md
|
|
362
|
+
\u251C\u2500\u2500 archive/ # \uC644\uB8CC\uB41C \uBCC0\uACBD
|
|
363
|
+
\u2514\u2500\u2500 templates/ # \uD15C\uD50C\uB9BF \uD30C\uC77C
|
|
364
|
+
\`\`\`
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## \uC6CC\uD06C\uD50C\uB85C\uC6B0
|
|
369
|
+
|
|
370
|
+
### \uC2E0\uADDC \uAE30\uB2A5 \uC6CC\uD06C\uD50C\uB85C\uC6B0
|
|
371
|
+
|
|
372
|
+
1. \`/sdd:new <feature>\` - \uC2A4\uD399 \uCD08\uC548 \uC791\uC131
|
|
373
|
+
2. \`/sdd:plan\` - \uAD6C\uD604 \uACC4\uD68D \uC218\uB9BD
|
|
374
|
+
3. \`/sdd:tasks\` - \uC791\uC5C5 \uBD84\uD574 (\uB9C8\uCEE4: [P1-3], [\u2192T], [US])
|
|
375
|
+
4. \uC21C\uCC28\uC801 \uAD6C\uD604 \uBC0F \uD14C\uC2A4\uD2B8
|
|
376
|
+
5. \uB9AC\uBDF0 \uBC0F \uBA38\uC9C0
|
|
377
|
+
|
|
378
|
+
### \uBCC0\uACBD \uC6CC\uD06C\uD50C\uB85C\uC6B0
|
|
379
|
+
|
|
380
|
+
1. \`/sdd:change <id>\` - \uC81C\uC548\uC11C(proposal.md) \uC791\uC131
|
|
381
|
+
2. delta.md \uC791\uC131 (ADDED/MODIFIED/REMOVED)
|
|
382
|
+
3. tasks.md \uC791\uC5C5 \uBAA9\uB85D \uC0DD\uC131
|
|
383
|
+
4. \uAD6C\uD604
|
|
384
|
+
5. \`/sdd:apply\` - \uB378\uD0C0\uB97C \uC2A4\uD399\uC5D0 \uBCD1\uD569
|
|
385
|
+
6. \`/sdd:archive\` - \uC644\uB8CC\uB41C \uBCC0\uACBD \uC544\uCE74\uC774\uBE0C
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## \uAC80\uC99D
|
|
390
|
+
|
|
391
|
+
\uC2A4\uD399 \uBCC0\uACBD \uD6C4 \uD56D\uC0C1 \uAC80\uC99D\uC744 \uC2E4\uD589\uD558\uC138\uC694:
|
|
392
|
+
|
|
393
|
+
\`\`\`bash
|
|
394
|
+
sdd validate [path] # \uD615\uC2DD \uAC80\uC99D
|
|
395
|
+
sdd validate --strict # \uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC
|
|
396
|
+
\`\`\`
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uC694\uC57D
|
|
401
|
+
|
|
402
|
+
| \uBA85\uB839\uC5B4 | \uC124\uBA85 |
|
|
403
|
+
|--------|------|
|
|
404
|
+
| \`/sdd:init\` | \uD504\uB85C\uC81D\uD2B8 \uCD08\uAE30\uD654 |
|
|
405
|
+
| \`/sdd:constitution\` | Constitution \uC0DD\uC131/\uC218\uC815 |
|
|
406
|
+
| \`/sdd:new\` | \uC2E0\uADDC \uC2A4\uD399 \uC0DD\uC131 |
|
|
407
|
+
| \`/sdd:plan\` | \uAD6C\uD604 \uACC4\uD68D \uC218\uB9BD |
|
|
408
|
+
| \`/sdd:tasks\` | \uC791\uC5C5 \uBD84\uD574 |
|
|
409
|
+
| \`/sdd:change\` | \uBCC0\uACBD \uC81C\uC548 |
|
|
410
|
+
| \`/sdd:impact\` | \uC601\uD5A5\uB3C4 \uBD84\uC11D |
|
|
411
|
+
| \`/sdd:apply\` | \uB378\uD0C0 \uC801\uC6A9 |
|
|
412
|
+
| \`/sdd:archive\` | \uC544\uCE74\uC774\uBE0C |
|
|
413
|
+
| \`/sdd:validate\` | \uD615\uC2DD \uAC80\uC99D |
|
|
414
|
+
| \`/sdd:status\` | \uD604\uD669 \uC870\uD68C |
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## \uC791\uC5C5 \uB9C8\uCEE4
|
|
419
|
+
|
|
420
|
+
| \uB9C8\uCEE4 | \uC758\uBBF8 |
|
|
421
|
+
|------|------|
|
|
422
|
+
| [P] | \uC6B0\uC120\uC21C\uC704 \uC5C6\uC74C |
|
|
423
|
+
| [P1-3] | \uC6B0\uC120\uC21C\uC704 (1=\uB192\uC74C) |
|
|
424
|
+
| [\u2192T] | \uD14C\uC2A4\uD2B8 \uD544\uC694 |
|
|
425
|
+
| [US] | \uBD88\uD655\uC2E4/\uAC80\uD1A0 \uD544\uC694 |
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## \uCC38\uC870
|
|
430
|
+
|
|
431
|
+
- [Constitution](./constitution.md)
|
|
432
|
+
- [Specs](./specs/)
|
|
433
|
+
- [Changes](./changes/)
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// src/cli/commands/init.ts
|
|
438
|
+
function registerInitCommand(program2) {
|
|
439
|
+
program2.command("init").description("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4").option("-f, --force", "\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC \uB36E\uC5B4\uC4F0\uAE30").action(async (options) => {
|
|
440
|
+
try {
|
|
441
|
+
await runInit(options);
|
|
442
|
+
} catch (error2) {
|
|
443
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
444
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
async function runInit(options) {
|
|
449
|
+
const cwd = process.cwd();
|
|
450
|
+
const sddPath = path2.join(cwd, ".sdd");
|
|
451
|
+
if (await directoryExists(sddPath)) {
|
|
452
|
+
if (!options.force) {
|
|
453
|
+
error(".sdd/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4. --force \uC635\uC158\uC73C\uB85C \uB36E\uC5B4\uC4F8 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
454
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
455
|
+
}
|
|
456
|
+
warn("\uAE30\uC874 .sdd/ \uB514\uB809\uD1A0\uB9AC\uB97C \uB36E\uC5B4\uC501\uB2C8\uB2E4.");
|
|
457
|
+
}
|
|
458
|
+
info("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCD08\uAE30\uD654\uD569\uB2C8\uB2E4...");
|
|
459
|
+
const directories = [
|
|
460
|
+
".sdd",
|
|
461
|
+
".sdd/specs",
|
|
462
|
+
".sdd/changes",
|
|
463
|
+
".sdd/archive",
|
|
464
|
+
".sdd/templates"
|
|
465
|
+
];
|
|
466
|
+
for (const dir of directories) {
|
|
467
|
+
const result = await ensureDir(path2.join(cwd, dir));
|
|
468
|
+
if (!result.success) {
|
|
469
|
+
error(`\uB514\uB809\uD1A0\uB9AC \uC0DD\uC131 \uC2E4\uD328: ${dir}`);
|
|
470
|
+
process.exit(ExitCode.FILE_SYSTEM_ERROR);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
await createDefaultFiles(cwd);
|
|
474
|
+
await copyTemplates(cwd);
|
|
475
|
+
success2("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC5C8\uC2B5\uB2C8\uB2E4.");
|
|
476
|
+
newline();
|
|
477
|
+
info("\uC0DD\uC131\uB41C \uAD6C\uC870:");
|
|
478
|
+
listItem(".sdd/");
|
|
479
|
+
listItem("AGENTS.md", 1);
|
|
480
|
+
listItem("constitution.md", 1);
|
|
481
|
+
listItem("specs/", 1);
|
|
482
|
+
listItem("changes/", 1);
|
|
483
|
+
listItem("archive/", 1);
|
|
484
|
+
listItem("templates/", 1);
|
|
485
|
+
newline();
|
|
486
|
+
info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
487
|
+
listItem("constitution.md\uB97C \uC218\uC815\uD558\uC5EC \uD504\uB85C\uC81D\uD2B8 \uC6D0\uCE59\uC744 \uC815\uC758\uD558\uC138\uC694");
|
|
488
|
+
listItem("`sdd validate`\uB85C \uC2A4\uD399\uC744 \uAC80\uC99D\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4");
|
|
489
|
+
}
|
|
490
|
+
async function createDefaultFiles(cwd) {
|
|
491
|
+
const projectName = path2.basename(cwd);
|
|
492
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
493
|
+
const constitution = `---
|
|
494
|
+
version: 1.0.0
|
|
495
|
+
created: ${today}
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
# Constitution: ${projectName}
|
|
499
|
+
|
|
500
|
+
> \uC774 \uD504\uB85C\uC81D\uD2B8\uC758 \uBAA8\uB4E0 \uC124\uACC4\uC640 \uAD6C\uD604\uC740 \uC544\uB798 \uC6D0\uCE59\uC744 \uC900\uC218\uD574\uC57C \uD55C\uB2E4(SHALL).
|
|
501
|
+
|
|
502
|
+
## \uD575\uC2EC \uC6D0\uCE59
|
|
503
|
+
|
|
504
|
+
### 1. \uD488\uC9C8 \uC6B0\uC120
|
|
505
|
+
|
|
506
|
+
- \uBAA8\uB4E0 \uAE30\uB2A5\uC740 \uD14C\uC2A4\uD2B8\uC640 \uD568\uAED8 \uAD6C\uD604\uD574\uC57C \uD55C\uB2E4(SHALL)
|
|
507
|
+
- \uCF54\uB4DC \uB9AC\uBDF0 \uC5C6\uC774 \uBA38\uC9C0\uD574\uC11C\uB294 \uC548 \uB41C\uB2E4(SHALL NOT)
|
|
508
|
+
|
|
509
|
+
### 2. \uBA85\uC138 \uC6B0\uC120
|
|
510
|
+
|
|
511
|
+
- \uBAA8\uB4E0 \uAE30\uB2A5\uC740 \uC2A4\uD399 \uBB38\uC11C\uAC00 \uBA3C\uC800 \uC791\uC131\uB418\uC5B4\uC57C \uD55C\uB2E4(SHALL)
|
|
512
|
+
- \uC2A4\uD399\uC740 RFC 2119 \uD0A4\uC6CC\uB4DC\uB97C \uC0AC\uC6A9\uD574\uC57C \uD55C\uB2E4(SHALL)
|
|
513
|
+
- \uBAA8\uB4E0 \uC694\uAD6C\uC0AC\uD56D\uC740 GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uB97C \uD3EC\uD568\uD574\uC57C \uD55C\uB2E4(SHALL)
|
|
514
|
+
|
|
515
|
+
## \uAE08\uC9C0 \uC0AC\uD56D
|
|
516
|
+
|
|
517
|
+
- \uC2A4\uD399 \uC5C6\uC774 \uAE30\uB2A5\uC744 \uAD6C\uD604\uD574\uC11C\uB294 \uC548 \uB41C\uB2E4(SHALL NOT)
|
|
518
|
+
- \uD14C\uC2A4\uD2B8 \uC5C6\uC774 \uBC30\uD3EC\uD574\uC11C\uB294 \uC548 \uB41C\uB2E4(SHALL NOT)
|
|
519
|
+
|
|
520
|
+
## \uAE30\uC220 \uC2A4\uD0DD
|
|
521
|
+
|
|
522
|
+
- (\uD504\uB85C\uC81D\uD2B8\uC5D0 \uB9DE\uAC8C \uC218\uC815\uD558\uC138\uC694)
|
|
523
|
+
|
|
524
|
+
## \uD488\uC9C8 \uAE30\uC900
|
|
525
|
+
|
|
526
|
+
- \uD14C\uC2A4\uD2B8 \uCEE4\uBC84\uB9AC\uC9C0: 80% \uC774\uC0C1(SHOULD)
|
|
527
|
+
`;
|
|
528
|
+
await writeFile(path2.join(cwd, ".sdd", "constitution.md"), constitution);
|
|
529
|
+
const agents = generateAgentsMd({ projectName });
|
|
530
|
+
await writeFile(path2.join(cwd, ".sdd", "AGENTS.md"), agents);
|
|
531
|
+
}
|
|
532
|
+
async function copyTemplates(cwd) {
|
|
533
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
534
|
+
const specTemplate = `---
|
|
535
|
+
status: draft
|
|
536
|
+
created: ${today}
|
|
537
|
+
depends: null
|
|
538
|
+
---
|
|
539
|
+
|
|
540
|
+
# {{FEATURE_NAME}}
|
|
541
|
+
|
|
542
|
+
> \uAE30\uB2A5 \uC124\uBA85
|
|
543
|
+
|
|
544
|
+
---
|
|
545
|
+
|
|
546
|
+
## Requirement: {{REQUIREMENT_TITLE}}
|
|
547
|
+
|
|
548
|
+
\uC2DC\uC2A4\uD15C\uC740 {{DESCRIPTION}}\uD574\uC57C \uD55C\uB2E4(SHALL).
|
|
549
|
+
|
|
550
|
+
### Scenario: {{SCENARIO_NAME}}
|
|
551
|
+
|
|
552
|
+
- **GIVEN** {{GIVEN_CONDITION}}
|
|
553
|
+
- **WHEN** {{WHEN_ACTION}}
|
|
554
|
+
- **THEN** {{THEN_RESULT}}
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
## \uBE44\uACE0
|
|
559
|
+
|
|
560
|
+
\uCD94\uAC00 \uC124\uBA85\uC774\uB098 \uC81C\uC57D \uC870\uAC74
|
|
561
|
+
`;
|
|
562
|
+
const proposalTemplate = `---
|
|
563
|
+
id: CHG-{{ID}}
|
|
564
|
+
status: draft
|
|
565
|
+
created: ${today}
|
|
566
|
+
---
|
|
567
|
+
|
|
568
|
+
# \uBCC0\uACBD \uC81C\uC548: {{TITLE}}
|
|
569
|
+
|
|
570
|
+
> \uBCC0\uACBD \uBAA9\uC801 \uBC0F \uBC30\uACBD \uC124\uBA85
|
|
571
|
+
|
|
572
|
+
---
|
|
573
|
+
|
|
574
|
+
## \uBC30\uACBD
|
|
575
|
+
|
|
576
|
+
\uC65C \uC774 \uBCC0\uACBD\uC774 \uD544\uC694\uD55C\uAC00?
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
## \uC601\uD5A5 \uBC94\uC704
|
|
581
|
+
|
|
582
|
+
### \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399
|
|
583
|
+
|
|
584
|
+
- \`specs/{{SPEC_PATH}}\`
|
|
585
|
+
|
|
586
|
+
### \uBCC0\uACBD \uC720\uD615
|
|
587
|
+
|
|
588
|
+
- [ ] \uC2E0\uADDC \uCD94\uAC00 (ADDED)
|
|
589
|
+
- [ ] \uC218\uC815 (MODIFIED)
|
|
590
|
+
- [ ] \uC0AD\uC81C (REMOVED)
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## \uBCC0\uACBD \uB0B4\uC6A9
|
|
595
|
+
|
|
596
|
+
### ADDED
|
|
597
|
+
|
|
598
|
+
(\uC0C8\uB85C \uCD94\uAC00\uB418\uB294 \uB0B4\uC6A9)
|
|
599
|
+
|
|
600
|
+
### MODIFIED
|
|
601
|
+
|
|
602
|
+
#### Before
|
|
603
|
+
|
|
604
|
+
\`\`\`markdown
|
|
605
|
+
\uAE30\uC874 \uB0B4\uC6A9
|
|
606
|
+
\`\`\`
|
|
607
|
+
|
|
608
|
+
#### After
|
|
609
|
+
|
|
610
|
+
\`\`\`markdown
|
|
611
|
+
\uBCC0\uACBD\uB41C \uB0B4\uC6A9
|
|
612
|
+
\`\`\`
|
|
613
|
+
|
|
614
|
+
### REMOVED
|
|
615
|
+
|
|
616
|
+
(\uC0AD\uC81C\uB418\uB294 \uB0B4\uC6A9)
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## \uB9AC\uC2A4\uD06C \uD3C9\uAC00
|
|
621
|
+
|
|
622
|
+
- \uC601\uD5A5\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
|
|
623
|
+
- \uBCF5\uC7A1\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
|
|
624
|
+
`;
|
|
625
|
+
const deltaTemplate = `---
|
|
626
|
+
proposal: CHG-{{ID}}
|
|
627
|
+
created: ${today}
|
|
628
|
+
---
|
|
629
|
+
|
|
630
|
+
# Delta: {{TITLE}}
|
|
631
|
+
|
|
632
|
+
## ADDED
|
|
633
|
+
|
|
634
|
+
(\uCD94\uAC00\uB418\uB294 \uC2A4\uD399 \uB0B4\uC6A9)
|
|
635
|
+
|
|
636
|
+
## MODIFIED
|
|
637
|
+
|
|
638
|
+
### {{SPEC_PATH}}
|
|
639
|
+
|
|
640
|
+
#### Before
|
|
641
|
+
|
|
642
|
+
\`\`\`markdown
|
|
643
|
+
\uAE30\uC874 \uB0B4\uC6A9
|
|
644
|
+
\`\`\`
|
|
645
|
+
|
|
646
|
+
#### After
|
|
647
|
+
|
|
648
|
+
\`\`\`markdown
|
|
649
|
+
\uBCC0\uACBD\uB41C \uB0B4\uC6A9
|
|
650
|
+
\`\`\`
|
|
651
|
+
|
|
652
|
+
## REMOVED
|
|
653
|
+
|
|
654
|
+
(\uC0AD\uC81C\uB418\uB294 \uC2A4\uD399 \uCC38\uC870)
|
|
655
|
+
`;
|
|
656
|
+
const tasksTemplate = `---
|
|
657
|
+
spec: {{SPEC_ID}}
|
|
658
|
+
created: ${today}
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
# Tasks: {{FEATURE_NAME}}
|
|
662
|
+
|
|
663
|
+
## \uAC1C\uC694
|
|
664
|
+
|
|
665
|
+
- \uCD1D \uC791\uC5C5 \uC218: N\uAC1C
|
|
666
|
+
- \uC608\uC0C1 \uBCF5\uC7A1\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## \uC791\uC5C5 \uBAA9\uB85D
|
|
671
|
+
|
|
672
|
+
### Phase 1: \uAE30\uBC18 \uAD6C\uCD95
|
|
673
|
+
|
|
674
|
+
- [ ] [P1] \uC791\uC5C5 1 \uC124\uBA85
|
|
675
|
+
- [ ] [P1] \uC791\uC5C5 2 \uC124\uBA85
|
|
676
|
+
|
|
677
|
+
### Phase 2: \uD575\uC2EC \uAD6C\uD604
|
|
678
|
+
|
|
679
|
+
- [ ] [P2] \uC791\uC5C5 3 \uC124\uBA85
|
|
680
|
+
- [ ] [P2] \uC791\uC5C5 4 \uC124\uBA85
|
|
681
|
+
|
|
682
|
+
### Phase 3: \uB9C8\uBB34\uB9AC
|
|
683
|
+
|
|
684
|
+
- [ ] [P3] \uD14C\uC2A4\uD2B8 \uC791\uC131
|
|
685
|
+
- [ ] [P3] \uBB38\uC11C\uD654
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## \uC758\uC874\uC131 \uADF8\uB798\uD504
|
|
690
|
+
|
|
691
|
+
\`\`\`mermaid
|
|
692
|
+
graph LR
|
|
693
|
+
A[\uC791\uC5C5 1] --> B[\uC791\uC5C5 2]
|
|
694
|
+
B --> C[\uC791\uC5C5 3]
|
|
695
|
+
\`\`\`
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## \uB9C8\uCEE4 \uBC94\uB840
|
|
700
|
+
|
|
701
|
+
| \uB9C8\uCEE4 | \uC758\uBBF8 |
|
|
702
|
+
|------|------|
|
|
703
|
+
| [P1-3] | \uC6B0\uC120\uC21C\uC704 |
|
|
704
|
+
| [\u2192T] | \uD14C\uC2A4\uD2B8 \uD544\uC694 |
|
|
705
|
+
| [US] | \uBD88\uD655\uC2E4/\uAC80\uD1A0 \uD544\uC694 |
|
|
706
|
+
`;
|
|
707
|
+
await writeFile(path2.join(cwd, ".sdd", "templates", "spec.md"), specTemplate);
|
|
708
|
+
await writeFile(path2.join(cwd, ".sdd", "templates", "proposal.md"), proposalTemplate);
|
|
709
|
+
await writeFile(path2.join(cwd, ".sdd", "templates", "delta.md"), deltaTemplate);
|
|
710
|
+
await writeFile(path2.join(cwd, ".sdd", "templates", "tasks.md"), tasksTemplate);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/cli/commands/validate.ts
|
|
714
|
+
import path4 from "path";
|
|
715
|
+
import chalk2 from "chalk";
|
|
716
|
+
|
|
717
|
+
// src/core/spec/validator.ts
|
|
718
|
+
import path3 from "path";
|
|
719
|
+
|
|
720
|
+
// src/core/spec/parser.ts
|
|
721
|
+
import matter from "gray-matter";
|
|
722
|
+
|
|
723
|
+
// src/core/spec/schemas.ts
|
|
724
|
+
import { z } from "zod";
|
|
725
|
+
var SpecStatusSchema = z.enum(["draft", "review", "approved", "implemented"]);
|
|
726
|
+
var DateStringSchema = z.preprocess(
|
|
727
|
+
(val) => {
|
|
728
|
+
if (val instanceof Date) {
|
|
729
|
+
return val.toISOString().split("T")[0];
|
|
730
|
+
}
|
|
731
|
+
return val;
|
|
732
|
+
},
|
|
733
|
+
z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "\uB0A0\uC9DC \uD615\uC2DD: YYYY-MM-DD").optional()
|
|
734
|
+
);
|
|
735
|
+
var SpecMetadataSchema = z.object({
|
|
736
|
+
status: SpecStatusSchema.default("draft"),
|
|
737
|
+
created: DateStringSchema,
|
|
738
|
+
depends: z.string().nullable().optional(),
|
|
739
|
+
command: z.string().optional(),
|
|
740
|
+
author: z.string().optional()
|
|
741
|
+
});
|
|
742
|
+
var RequirementLevelSchema = z.enum(["SHALL", "MUST", "SHOULD", "MAY"]);
|
|
743
|
+
var RequirementSchema = z.object({
|
|
744
|
+
id: z.string(),
|
|
745
|
+
level: RequirementLevelSchema,
|
|
746
|
+
description: z.string(),
|
|
747
|
+
raw: z.string()
|
|
748
|
+
});
|
|
749
|
+
var ScenarioSchema = z.object({
|
|
750
|
+
name: z.string(),
|
|
751
|
+
given: z.array(z.string()).min(1, "GIVEN \uC870\uAC74\uC774 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4"),
|
|
752
|
+
when: z.string().min(1, "WHEN \uC870\uAC74\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"),
|
|
753
|
+
then: z.array(z.string()).min(1, "THEN \uACB0\uACFC\uAC00 \uCD5C\uC18C 1\uAC1C \uD544\uC694\uD569\uB2C8\uB2E4")
|
|
754
|
+
});
|
|
755
|
+
var ParsedSpecSchema = z.object({
|
|
756
|
+
title: z.string(),
|
|
757
|
+
description: z.string().optional(),
|
|
758
|
+
metadata: SpecMetadataSchema,
|
|
759
|
+
requirements: z.array(RequirementSchema),
|
|
760
|
+
scenarios: z.array(ScenarioSchema),
|
|
761
|
+
rawContent: z.string()
|
|
762
|
+
});
|
|
763
|
+
function extractRfc2119Keywords(text) {
|
|
764
|
+
const keywords = [];
|
|
765
|
+
if (/SHALL\s+NOT/i.test(text)) keywords.push("SHALL NOT");
|
|
766
|
+
if (/MUST\s+NOT/i.test(text)) keywords.push("MUST NOT");
|
|
767
|
+
if (/(?<!NOT\s)SHALL(?!\s+NOT)/i.test(text)) keywords.push("SHALL");
|
|
768
|
+
if (/(?<!NOT\s)MUST(?!\s+NOT)/i.test(text)) keywords.push("MUST");
|
|
769
|
+
if (/SHOULD/i.test(text)) keywords.push("SHOULD");
|
|
770
|
+
if (/MAY/i.test(text)) keywords.push("MAY");
|
|
771
|
+
return [...new Set(keywords)];
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// src/core/spec/parser.ts
|
|
775
|
+
function parseSpec(content) {
|
|
776
|
+
try {
|
|
777
|
+
const { data: rawMeta, content: body } = matter(content);
|
|
778
|
+
const metaResult = SpecMetadataSchema.safeParse(rawMeta);
|
|
779
|
+
if (!metaResult.success) {
|
|
780
|
+
const errors = metaResult.error.errors.map((e) => e.message).join(", ");
|
|
781
|
+
return failure(new ValidationError(ErrorCode.SPEC_INVALID_FORMAT, `\uBA54\uD0C0\uB370\uC774\uD130 \uC624\uB958: ${errors}`));
|
|
782
|
+
}
|
|
783
|
+
const metadata = metaResult.data;
|
|
784
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
785
|
+
if (!titleMatch) {
|
|
786
|
+
return failure(new ValidationError(ErrorCode.SPEC_MISSING_REQUIRED, "\uC81C\uBAA9(# Title)\uC774 \uD544\uC694\uD569\uB2C8\uB2E4"));
|
|
787
|
+
}
|
|
788
|
+
const title2 = titleMatch[1].trim();
|
|
789
|
+
const descMatch = body.match(/^#\s+.+\n+>\s*(.+)$/m);
|
|
790
|
+
const description = descMatch?.[1]?.trim();
|
|
791
|
+
const requirements = parseRequirements(body);
|
|
792
|
+
const scenarios = parseScenarios(body);
|
|
793
|
+
return success({
|
|
794
|
+
title: title2,
|
|
795
|
+
description,
|
|
796
|
+
metadata,
|
|
797
|
+
requirements,
|
|
798
|
+
scenarios,
|
|
799
|
+
rawContent: content
|
|
800
|
+
});
|
|
801
|
+
} catch (error2) {
|
|
802
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
803
|
+
return failure(new ValidationError(ErrorCode.SPEC_PARSE_ERROR, message));
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function parseRequirements(content) {
|
|
807
|
+
const requirements = [];
|
|
808
|
+
const reqSectionMatch = content.match(/##\s+(?:요구사항|Requirements?)\s*\n([\s\S]*?)(?=\n##\s+[^#]|\n---|\n$)/i);
|
|
809
|
+
if (!reqSectionMatch) {
|
|
810
|
+
return parseRequirementsFromContent(content);
|
|
811
|
+
}
|
|
812
|
+
return parseRequirementsFromContent(reqSectionMatch[1]);
|
|
813
|
+
}
|
|
814
|
+
function parseRequirementsFromContent(content) {
|
|
815
|
+
const requirements = [];
|
|
816
|
+
const lines = content.split("\n");
|
|
817
|
+
let reqId = 1;
|
|
818
|
+
for (const line of lines) {
|
|
819
|
+
const keywords = extractRfc2119Keywords(line);
|
|
820
|
+
if (keywords.length > 0) {
|
|
821
|
+
const level = keywords.includes("SHALL") || keywords.includes("MUST") ? keywords.includes("SHALL") ? "SHALL" : "MUST" : keywords.includes("SHOULD") ? "SHOULD" : "MAY";
|
|
822
|
+
requirements.push({
|
|
823
|
+
id: `REQ-${String(reqId++).padStart(3, "0")}`,
|
|
824
|
+
level,
|
|
825
|
+
description: line.trim(),
|
|
826
|
+
raw: line
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
return requirements;
|
|
831
|
+
}
|
|
832
|
+
function parseScenarios(content) {
|
|
833
|
+
const scenarios = [];
|
|
834
|
+
const scenarioRegex = /###\s+Scenario[:\s]+(.+?)(?=\n###|\n##|\n---|\n$)/gis;
|
|
835
|
+
let match;
|
|
836
|
+
while ((match = scenarioRegex.exec(content)) !== null) {
|
|
837
|
+
const sectionContent = match[0];
|
|
838
|
+
const nameMatch = sectionContent.match(/###\s+Scenario[:\s]+(.+)/i);
|
|
839
|
+
const name = nameMatch?.[1]?.trim() ?? "Unnamed";
|
|
840
|
+
const given = [];
|
|
841
|
+
const then = [];
|
|
842
|
+
let when = "";
|
|
843
|
+
const lines = sectionContent.split("\n");
|
|
844
|
+
for (const line of lines) {
|
|
845
|
+
const trimmed = line.trim();
|
|
846
|
+
const givenMatch = trimmed.match(/[-*]\s*\*?\*?GIVEN\*?\*?\s+(.+)/i);
|
|
847
|
+
if (givenMatch) {
|
|
848
|
+
given.push(givenMatch[1].trim());
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const whenMatch = trimmed.match(/[-*]\s*\*?\*?WHEN\*?\*?\s+(.+)/i);
|
|
852
|
+
if (whenMatch) {
|
|
853
|
+
when = whenMatch[1].trim();
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const thenMatch = trimmed.match(/[-*]\s*\*?\*?THEN\*?\*?\s+(.+)/i);
|
|
857
|
+
if (thenMatch) {
|
|
858
|
+
then.push(thenMatch[1].trim());
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
if (given.length > 0 && when && then.length > 0) {
|
|
862
|
+
scenarios.push({ name, given, when, then });
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
return scenarios;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/core/spec/validator.ts
|
|
869
|
+
async function validateSpecFile(filePath, options = {}) {
|
|
870
|
+
const result = {
|
|
871
|
+
file: filePath,
|
|
872
|
+
valid: true,
|
|
873
|
+
errors: [],
|
|
874
|
+
warnings: []
|
|
875
|
+
};
|
|
876
|
+
const readResult = await readFile(filePath);
|
|
877
|
+
if (!readResult.success) {
|
|
878
|
+
result.valid = false;
|
|
879
|
+
result.errors.push({
|
|
880
|
+
code: ErrorCode.FILE_READ_ERROR,
|
|
881
|
+
message: `\uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`,
|
|
882
|
+
location: { file: filePath }
|
|
883
|
+
});
|
|
884
|
+
return result;
|
|
885
|
+
}
|
|
886
|
+
const content = readResult.data;
|
|
887
|
+
const parseResult = parseSpec(content);
|
|
888
|
+
if (!parseResult.success) {
|
|
889
|
+
result.valid = false;
|
|
890
|
+
result.errors.push({
|
|
891
|
+
code: parseResult.error.code,
|
|
892
|
+
message: parseResult.error.message,
|
|
893
|
+
location: { file: filePath }
|
|
894
|
+
});
|
|
895
|
+
return result;
|
|
896
|
+
}
|
|
897
|
+
const spec = parseResult.data;
|
|
898
|
+
if (spec.requirements.length === 0) {
|
|
899
|
+
result.valid = false;
|
|
900
|
+
result.errors.push({
|
|
901
|
+
code: ErrorCode.RFC2119_VIOLATION,
|
|
902
|
+
message: "Requirement\uC5D0 RFC 2119 \uD0A4\uC6CC\uB4DC(SHALL, MUST, SHOULD, MAY)\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
903
|
+
location: { file: filePath }
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (spec.scenarios.length === 0) {
|
|
907
|
+
result.valid = false;
|
|
908
|
+
result.errors.push({
|
|
909
|
+
code: ErrorCode.GWT_INVALID_FORMAT,
|
|
910
|
+
message: "Scenario\uC5D0 GIVEN-WHEN-THEN \uD615\uC2DD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
911
|
+
location: { file: filePath }
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
if (!spec.metadata.created) {
|
|
915
|
+
result.warnings.push({
|
|
916
|
+
code: "W001",
|
|
917
|
+
message: "YAML frontmatter\uC5D0 created \uB0A0\uC9DC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4",
|
|
918
|
+
location: { file: filePath }
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
if (options.strict && result.warnings.length > 0) {
|
|
922
|
+
result.valid = false;
|
|
923
|
+
result.errors.push(...result.warnings.map((w) => ({
|
|
924
|
+
code: w.code,
|
|
925
|
+
message: `[STRICT] ${w.message}`,
|
|
926
|
+
location: w.location
|
|
927
|
+
})));
|
|
928
|
+
}
|
|
929
|
+
return result;
|
|
930
|
+
}
|
|
931
|
+
async function validateSpecs(targetPath, options = {}) {
|
|
932
|
+
const results = [];
|
|
933
|
+
let passed = 0;
|
|
934
|
+
let failed = 0;
|
|
935
|
+
let warnings = 0;
|
|
936
|
+
if (await directoryExists(targetPath)) {
|
|
937
|
+
const filesResult = await findSpecFiles(targetPath);
|
|
938
|
+
if (!filesResult.success) {
|
|
939
|
+
return failure(new ValidationError(ErrorCode.DIRECTORY_NOT_FOUND, targetPath));
|
|
940
|
+
}
|
|
941
|
+
for (const file of filesResult.data) {
|
|
942
|
+
const result = await validateSpecFile(file, options);
|
|
943
|
+
results.push(result);
|
|
944
|
+
if (result.valid) {
|
|
945
|
+
passed++;
|
|
946
|
+
} else {
|
|
947
|
+
failed++;
|
|
948
|
+
}
|
|
949
|
+
warnings += result.warnings.length;
|
|
950
|
+
}
|
|
951
|
+
} else {
|
|
952
|
+
const result = await validateSpecFile(targetPath, options);
|
|
953
|
+
results.push(result);
|
|
954
|
+
if (result.valid) {
|
|
955
|
+
passed++;
|
|
956
|
+
} else {
|
|
957
|
+
failed++;
|
|
958
|
+
}
|
|
959
|
+
warnings += result.warnings.length;
|
|
960
|
+
}
|
|
961
|
+
return success({
|
|
962
|
+
passed,
|
|
963
|
+
failed,
|
|
964
|
+
warnings,
|
|
965
|
+
files: results
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
async function findSpecFiles(dirPath) {
|
|
969
|
+
const files = [];
|
|
970
|
+
async function scanDir(dir) {
|
|
971
|
+
const { promises: fs8 } = await import("fs");
|
|
972
|
+
try {
|
|
973
|
+
const entries = await fs8.readdir(dir, { withFileTypes: true });
|
|
974
|
+
for (const entry of entries) {
|
|
975
|
+
const fullPath = path3.join(dir, entry.name);
|
|
976
|
+
if (entry.isDirectory()) {
|
|
977
|
+
await scanDir(fullPath);
|
|
978
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
979
|
+
const excludeFiles = [
|
|
980
|
+
"index.md",
|
|
981
|
+
"readme.md",
|
|
982
|
+
"plan.md",
|
|
983
|
+
"tasks.md",
|
|
984
|
+
"checklist.md"
|
|
985
|
+
];
|
|
986
|
+
if (!excludeFiles.includes(entry.name.toLowerCase())) {
|
|
987
|
+
files.push(fullPath);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
} catch {
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
await scanDir(dirPath);
|
|
995
|
+
return success(files);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// src/cli/commands/validate.ts
|
|
999
|
+
function registerValidateCommand(program2) {
|
|
1000
|
+
program2.command("validate").description("\uC2A4\uD399 \uD30C\uC77C \uD615\uC2DD\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4").argument("[path]", "\uAC80\uC99D\uD560 \uD30C\uC77C \uB610\uB294 \uB514\uB809\uD1A0\uB9AC", "").option("-s, --strict", "\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC").option("-q, --quiet", "\uC694\uC57D\uB9CC \uCD9C\uB825").action(async (targetPath, options) => {
|
|
1001
|
+
try {
|
|
1002
|
+
await runValidate(targetPath, options);
|
|
1003
|
+
} catch (error2) {
|
|
1004
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
1005
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
1006
|
+
}
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
async function runValidate(targetPath, options) {
|
|
1010
|
+
let resolvedPath;
|
|
1011
|
+
if (targetPath) {
|
|
1012
|
+
resolvedPath = path4.resolve(targetPath);
|
|
1013
|
+
} else {
|
|
1014
|
+
const sddRoot = await findSddRoot();
|
|
1015
|
+
if (!sddRoot) {
|
|
1016
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
1017
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
1018
|
+
}
|
|
1019
|
+
resolvedPath = path4.join(sddRoot, ".sdd", "specs");
|
|
1020
|
+
}
|
|
1021
|
+
if (!options.quiet) {
|
|
1022
|
+
info(`\uAC80\uC99D \uC911: ${resolvedPath}`);
|
|
1023
|
+
newline();
|
|
1024
|
+
}
|
|
1025
|
+
const result = await validateSpecs(resolvedPath, { strict: options.strict });
|
|
1026
|
+
if (!result.success) {
|
|
1027
|
+
error(result.error.message);
|
|
1028
|
+
process.exit(ExitCode.FILE_SYSTEM_ERROR);
|
|
1029
|
+
}
|
|
1030
|
+
const { passed, failed, warnings, files } = result.data;
|
|
1031
|
+
if (!options.quiet) {
|
|
1032
|
+
for (const file of files) {
|
|
1033
|
+
printFileResult(file, resolvedPath);
|
|
1034
|
+
}
|
|
1035
|
+
newline();
|
|
1036
|
+
}
|
|
1037
|
+
const passedText = chalk2.green(`${passed} passed`);
|
|
1038
|
+
const failedText = failed > 0 ? chalk2.red(`${failed} failed`) : `${failed} failed`;
|
|
1039
|
+
const warningsText = warnings > 0 ? chalk2.yellow(`${warnings} warnings`) : "";
|
|
1040
|
+
const summary = [passedText, failedText, warningsText].filter(Boolean).join(", ");
|
|
1041
|
+
console.log(`Result: ${summary}`);
|
|
1042
|
+
if (failed > 0) {
|
|
1043
|
+
process.exit(ExitCode.VALIDATION_FAILED);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function printFileResult(result, basePath) {
|
|
1047
|
+
const relativePath = path4.relative(basePath, result.file);
|
|
1048
|
+
if (result.valid) {
|
|
1049
|
+
console.log(chalk2.green("\u2713") + " " + relativePath);
|
|
1050
|
+
} else {
|
|
1051
|
+
console.log(chalk2.red("\u2717") + " " + relativePath);
|
|
1052
|
+
for (const error2 of result.errors) {
|
|
1053
|
+
const line = error2.location?.line ? `:${error2.location.line}` : "";
|
|
1054
|
+
console.log(chalk2.red(` - ${error2.message}${line}`));
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
for (const warning of result.warnings) {
|
|
1058
|
+
console.log(chalk2.yellow(` \u26A0 ${warning.message}`));
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/prompts/index.ts
|
|
1063
|
+
var FORMAT_GUIDE = `## \uD615\uC2DD \uADDC\uCE59 (\uD544\uC218)
|
|
1064
|
+
|
|
1065
|
+
### RFC 2119 \uD0A4\uC6CC\uB4DC
|
|
1066
|
+
|
|
1067
|
+
| \uD0A4\uC6CC\uB4DC | \uC758\uBBF8 | \uC0AC\uC6A9 \uC608\uC2DC |
|
|
1068
|
+
|--------|------|-----------|
|
|
1069
|
+
| **SHALL** / **MUST** | \uC808\uB300 \uD544\uC218 | "\uC2DC\uC2A4\uD15C\uC740 \uC778\uC99D\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHALL)" |
|
|
1070
|
+
| **SHOULD** | \uAD8C\uC7A5 (\uC608\uC678 \uAC00\uB2A5) | "\uC751\uB2F5 \uC2DC\uAC04\uC740 1\uCD08 \uC774\uB0B4\uC5EC\uC57C \uD55C\uB2E4(SHOULD)" |
|
|
1071
|
+
| **MAY** | \uC120\uD0DD\uC801 | "\uB2E4\uD06C \uBAA8\uB4DC\uB97C \uC9C0\uC6D0\uD560 \uC218 \uC788\uB2E4(MAY)" |
|
|
1072
|
+
| **SHALL NOT** | \uC808\uB300 \uAE08\uC9C0 | "\uD3C9\uBB38 \uBE44\uBC00\uBC88\uD638\uB97C \uC800\uC7A5\uD574\uC11C\uB294 \uC548 \uB41C\uB2E4(SHALL NOT)" |
|
|
1073
|
+
|
|
1074
|
+
### GIVEN-WHEN-THEN \uD615\uC2DD
|
|
1075
|
+
|
|
1076
|
+
\uBAA8\uB4E0 \uC694\uAD6C\uC0AC\uD56D\uC740 \uC544\uB798 \uD615\uC2DD\uC758 \uC2DC\uB098\uB9AC\uC624\uB97C \uD3EC\uD568\uD574\uC57C \uD569\uB2C8\uB2E4:
|
|
1077
|
+
|
|
1078
|
+
\`\`\`markdown
|
|
1079
|
+
### Scenario: [\uC2DC\uB098\uB9AC\uC624\uBA85]
|
|
1080
|
+
|
|
1081
|
+
- **GIVEN** [\uC804\uC81C \uC870\uAC74]
|
|
1082
|
+
- **WHEN** [\uD589\uB3D9/\uD2B8\uB9AC\uAC70]
|
|
1083
|
+
- **THEN** [\uC608\uC0C1 \uACB0\uACFC]
|
|
1084
|
+
\`\`\`
|
|
1085
|
+
`;
|
|
1086
|
+
var CHANGE_PROMPT = `# /sdd:change - \uBCC0\uACBD \uC81C\uC548
|
|
1087
|
+
|
|
1088
|
+
> \uAE30\uC874 \uC2A4\uD399\uC5D0 \uB300\uD55C \uBCC0\uACBD\uC744 \uC81C\uC548\uD569\uB2C8\uB2E4.
|
|
1089
|
+
|
|
1090
|
+
${FORMAT_GUIDE}
|
|
1091
|
+
|
|
1092
|
+
---
|
|
1093
|
+
|
|
1094
|
+
## \uC0DD\uC131 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1095
|
+
|
|
1096
|
+
- [ ] \uBCC0\uACBD \uB300\uC0C1 \uC2A4\uD399 \uD655\uC778\uB428
|
|
1097
|
+
- [ ] \uBCC0\uACBD \uC0AC\uC720\uAC00 \uBA85\uD655\uD568
|
|
1098
|
+
- [ ] \uC601\uD5A5 \uBC94\uC704\uAC00 \uD30C\uC545\uB428
|
|
1099
|
+
|
|
1100
|
+
---
|
|
1101
|
+
|
|
1102
|
+
## \uC0DD\uC131\uD560 \uD30C\uC77C
|
|
1103
|
+
|
|
1104
|
+
### 1. proposal.md
|
|
1105
|
+
|
|
1106
|
+
\`\`\`markdown
|
|
1107
|
+
---
|
|
1108
|
+
id: CHG-{ID}
|
|
1109
|
+
status: draft
|
|
1110
|
+
created: {TODAY}
|
|
1111
|
+
---
|
|
1112
|
+
|
|
1113
|
+
# \uBCC0\uACBD \uC81C\uC548: {TITLE}
|
|
1114
|
+
|
|
1115
|
+
> \uBCC0\uACBD \uBAA9\uC801 \uBC0F \uBC30\uACBD \uC124\uBA85
|
|
1116
|
+
|
|
1117
|
+
---
|
|
1118
|
+
|
|
1119
|
+
## \uBC30\uACBD
|
|
1120
|
+
|
|
1121
|
+
\uC65C \uC774 \uBCC0\uACBD\uC774 \uD544\uC694\uD55C\uAC00?
|
|
1122
|
+
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
## \uC601\uD5A5 \uBC94\uC704
|
|
1126
|
+
|
|
1127
|
+
### \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399
|
|
1128
|
+
|
|
1129
|
+
- \`specs/{SPEC_PATH}\`
|
|
1130
|
+
|
|
1131
|
+
### \uBCC0\uACBD \uC720\uD615
|
|
1132
|
+
|
|
1133
|
+
- [ ] \uC2E0\uADDC \uCD94\uAC00 (ADDED)
|
|
1134
|
+
- [ ] \uC218\uC815 (MODIFIED)
|
|
1135
|
+
- [ ] \uC0AD\uC81C (REMOVED)
|
|
1136
|
+
|
|
1137
|
+
---
|
|
1138
|
+
|
|
1139
|
+
## \uBCC0\uACBD \uB0B4\uC6A9
|
|
1140
|
+
|
|
1141
|
+
(ADDED/MODIFIED/REMOVED \uC139\uC158\uBCC4 \uC0C1\uC138 \uB0B4\uC6A9)
|
|
1142
|
+
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
## \uB9AC\uC2A4\uD06C \uD3C9\uAC00
|
|
1146
|
+
|
|
1147
|
+
- \uC601\uD5A5\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
|
|
1148
|
+
- \uBCF5\uC7A1\uB3C4: \uB0AE\uC74C/\uC911\uAC04/\uB192\uC74C
|
|
1149
|
+
\`\`\`
|
|
1150
|
+
|
|
1151
|
+
---
|
|
1152
|
+
|
|
1153
|
+
## \uC0DD\uC131 \uD6C4 \uD655\uC778
|
|
1154
|
+
|
|
1155
|
+
- [ ] \`sdd validate .sdd/changes/{ID}/proposal.md\` \uC2E4\uD589
|
|
1156
|
+
- [ ] \uB378\uD0C0 \uD615\uC2DD \uD655\uC778 (ADDED/MODIFIED/REMOVED)
|
|
1157
|
+
- [ ] \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uBAA9\uB85D \uD655\uC778
|
|
1158
|
+
`;
|
|
1159
|
+
var APPLY_PROMPT = `# /sdd:apply - \uBCC0\uACBD \uC801\uC6A9
|
|
1160
|
+
|
|
1161
|
+
> \uC2B9\uC778\uB41C \uBCC0\uACBD \uC81C\uC548\uC744 \uC2A4\uD399\uC5D0 \uC801\uC6A9\uD569\uB2C8\uB2E4.
|
|
1162
|
+
|
|
1163
|
+
---
|
|
1164
|
+
|
|
1165
|
+
## \uC801\uC6A9 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1166
|
+
|
|
1167
|
+
- [ ] proposal.md \uC0C1\uD0DC\uAC00 approved\uC778\uC9C0 \uD655\uC778
|
|
1168
|
+
- [ ] delta.md\uAC00 \uC874\uC7AC\uD558\uB294\uC9C0 \uD655\uC778
|
|
1169
|
+
- [ ] \uC601\uD5A5\uBC1B\uB294 \uBAA8\uB4E0 \uC2A4\uD399 \uD30C\uC77C \uD655\uC778
|
|
1170
|
+
|
|
1171
|
+
---
|
|
1172
|
+
|
|
1173
|
+
## \uC801\uC6A9 \uD504\uB85C\uC138\uC2A4
|
|
1174
|
+
|
|
1175
|
+
1. delta.md\uC5D0\uC11C \uBCC0\uACBD \uB0B4\uC6A9 \uCD94\uCD9C
|
|
1176
|
+
2. \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399 \uD30C\uC77C \uC218\uC815
|
|
1177
|
+
- ADDED: \uC0C8 \uC139\uC158 \uCD94\uAC00
|
|
1178
|
+
- MODIFIED: \uAE30\uC874 \uC139\uC158 \uC218\uC815
|
|
1179
|
+
- REMOVED: \uD574\uB2F9 \uC139\uC158 \uC0AD\uC81C
|
|
1180
|
+
3. \uC2A4\uD399 \uD30C\uC77C \uAC80\uC99D
|
|
1181
|
+
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
## \uC801\uC6A9 \uD6C4 \uD655\uC778
|
|
1185
|
+
|
|
1186
|
+
- [ ] \`sdd validate\` \uC2E4\uD589\uD558\uC5EC \uBAA8\uB4E0 \uC2A4\uD399 \uAC80\uC99D
|
|
1187
|
+
- [ ] \uBCC0\uACBD\uB41C \uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D \uD655\uC778
|
|
1188
|
+
- [ ] \uB2E4\uC74C \uB2E8\uACC4: \`/sdd:archive\` \uC2E4\uD589
|
|
1189
|
+
`;
|
|
1190
|
+
var ARCHIVE_PROMPT = `# /sdd:archive - \uBCC0\uACBD \uC544\uCE74\uC774\uBE0C
|
|
1191
|
+
|
|
1192
|
+
> \uC644\uB8CC\uB41C \uBCC0\uACBD \uC81C\uC548\uC744 \uC544\uCE74\uC774\uBE0C\uD569\uB2C8\uB2E4.
|
|
1193
|
+
|
|
1194
|
+
---
|
|
1195
|
+
|
|
1196
|
+
## \uC544\uCE74\uC774\uBE0C \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1197
|
+
|
|
1198
|
+
- [ ] \uBCC0\uACBD\uC774 \uC2A4\uD399\uC5D0 \uC801\uC6A9\uB418\uC5C8\uB294\uC9C0 \uD655\uC778
|
|
1199
|
+
- [ ] \uBAA8\uB4E0 \uD14C\uC2A4\uD2B8\uAC00 \uD1B5\uACFC\uD558\uB294\uC9C0 \uD655\uC778
|
|
1200
|
+
- [ ] \uC2A4\uD399 \uAC80\uC99D \uD1B5\uACFC \uD655\uC778
|
|
1201
|
+
|
|
1202
|
+
---
|
|
1203
|
+
|
|
1204
|
+
## \uC544\uCE74\uC774\uBE0C \uD504\uB85C\uC138\uC2A4
|
|
1205
|
+
|
|
1206
|
+
1. .sdd/changes/{ID}/ \uB514\uB809\uD1A0\uB9AC\uB97C .sdd/archive/\uB85C \uC774\uB3D9
|
|
1207
|
+
2. \uC544\uCE74\uC774\uBE0C \uB0A0\uC9DC\uB97C \uD30C\uC77C\uBA85\uC5D0 \uCD94\uAC00: {YYYY-MM-DD}-{ID}/
|
|
1208
|
+
3. proposal.md \uC0C1\uD0DC\uB97C archived\uB85C \uBCC0\uACBD
|
|
1209
|
+
|
|
1210
|
+
---
|
|
1211
|
+
|
|
1212
|
+
## \uC544\uCE74\uC774\uBE0C \uD6C4 \uD655\uC778
|
|
1213
|
+
|
|
1214
|
+
- [ ] .sdd/changes/{ID}/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC0AD\uC81C\uB428
|
|
1215
|
+
- [ ] .sdd/archive/{DATE}-{ID}/ \uB514\uB809\uD1A0\uB9AC\uAC00 \uC0DD\uC131\uB428
|
|
1216
|
+
- [ ] \uC544\uCE74\uC774\uBE0C\uB41C proposal.md \uC0C1\uD0DC \uD655\uC778
|
|
1217
|
+
`;
|
|
1218
|
+
var IMPACT_PROMPT = `# /sdd:impact - \uC601\uD5A5\uB3C4 \uBD84\uC11D
|
|
1219
|
+
|
|
1220
|
+
> \uC2A4\uD399 \uBCC0\uACBD \uC2DC \uAD00\uB828 \uC2A4\uD399 \uBC0F \uCF54\uB4DC\uC5D0 \uBBF8\uCE58\uB294 \uC601\uD5A5\uC744 \uBD84\uC11D\uD569\uB2C8\uB2E4.
|
|
1221
|
+
|
|
1222
|
+
---
|
|
1223
|
+
|
|
1224
|
+
## \uBD84\uC11D \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1225
|
+
|
|
1226
|
+
- [ ] \uBCC0\uACBD\uD560 \uC2A4\uD399 \uC2DD\uBCC4\uB428
|
|
1227
|
+
- [ ] \uBCC0\uACBD \uBC94\uC704 \uD30C\uC545\uB428
|
|
1228
|
+
|
|
1229
|
+
---
|
|
1230
|
+
|
|
1231
|
+
## \uBD84\uC11D \uD504\uB85C\uC138\uC2A4
|
|
1232
|
+
|
|
1233
|
+
1. \uB300\uC0C1 \uC2A4\uD399\uC758 \uC758\uC874\uC131 \uADF8\uB798\uD504 \uAD6C\uCD95
|
|
1234
|
+
2. \uC774 \uC2A4\uD399\uC5D0 \uC758\uC874\uD558\uB294 \uB2E4\uB978 \uC2A4\uD399 \uC2DD\uBCC4
|
|
1235
|
+
3. \uC601\uD5A5\uB3C4 \uC218\uC900 \uD3C9\uAC00 (\uB192\uC74C/\uC911\uAC04/\uB0AE\uC74C)
|
|
1236
|
+
4. \uB9AC\uC2A4\uD06C \uC810\uC218 \uC0B0\uCD9C (1-10)
|
|
1237
|
+
|
|
1238
|
+
---
|
|
1239
|
+
|
|
1240
|
+
## CLI \uC0AC\uC6A9\uBC95
|
|
1241
|
+
|
|
1242
|
+
\`\`\`bash
|
|
1243
|
+
# \uD2B9\uC815 \uAE30\uB2A5 \uC601\uD5A5\uB3C4 \uBD84\uC11D
|
|
1244
|
+
sdd impact <feature-name>
|
|
1245
|
+
|
|
1246
|
+
# \uC758\uC874\uC131 \uADF8\uB798\uD504 \uCD9C\uB825 (Mermaid)
|
|
1247
|
+
sdd impact --graph
|
|
1248
|
+
|
|
1249
|
+
# JSON \uD615\uC2DD \uCD9C\uB825
|
|
1250
|
+
sdd impact <feature-name> --json
|
|
1251
|
+
\`\`\`
|
|
1252
|
+
|
|
1253
|
+
---
|
|
1254
|
+
|
|
1255
|
+
## \uC601\uD5A5 \uC218\uC900 \uAE30\uC900
|
|
1256
|
+
|
|
1257
|
+
| \uC218\uC900 | \uAE30\uC900 | \uD45C\uC2DC |
|
|
1258
|
+
|------|------|------|
|
|
1259
|
+
| \uB192\uC74C | \uC9C1\uC811 \uC758\uC874, API \uBCC0\uACBD | \u{1F534} HIGH |
|
|
1260
|
+
| \uC911\uAC04 | \uAC04\uC811 \uC758\uC874, \uB370\uC774\uD130 \uACF5\uC720 | \u{1F7E1} MEDIUM |
|
|
1261
|
+
| \uB0AE\uC74C | UI \uCEF4\uD3EC\uB10C\uD2B8 \uACF5\uC720 | \u{1F7E2} LOW |
|
|
1262
|
+
|
|
1263
|
+
---
|
|
1264
|
+
|
|
1265
|
+
## \uB9AC\uC2A4\uD06C \uC810\uC218 \uD574\uC11D
|
|
1266
|
+
|
|
1267
|
+
- 1-3: \uB0AE\uC740 \uB9AC\uC2A4\uD06C - \uBC14\uB85C \uC9C4\uD589 \uAC00\uB2A5
|
|
1268
|
+
- 4-6: \uC911\uAC04 \uB9AC\uC2A4\uD06C - \uAC80\uD1A0 \uAD8C\uC7A5
|
|
1269
|
+
- 7-10: \uB192\uC740 \uB9AC\uC2A4\uD06C - \uC2E0\uC911\uD55C \uAC80\uD1A0 \uD544\uC694
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1273
|
+
## \uBD84\uC11D \uD6C4 \uC870\uCE58
|
|
1274
|
+
|
|
1275
|
+
- \uB192\uC740 \uB9AC\uC2A4\uD06C: \uAD00\uB828 \uD300\uACFC \uACF5\uC720, \uB2E8\uACC4\uC801 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uAC80\uD1A0
|
|
1276
|
+
- \uC911\uAC04 \uB9AC\uC2A4\uD06C: \uC601\uD5A5 \uC2A4\uD399 \uD14C\uC2A4\uD2B8 \uD655\uC778
|
|
1277
|
+
- \uB0AE\uC740 \uB9AC\uC2A4\uD06C: \uD45C\uC900 \uD504\uB85C\uC138\uC2A4 \uC9C4\uD589
|
|
1278
|
+
`;
|
|
1279
|
+
var NEW_PROMPT = `# /sdd:new - \uC2E0\uADDC \uAE30\uB2A5 \uBA85\uC138
|
|
1280
|
+
|
|
1281
|
+
> \uC0C8\uB85C\uC6B4 \uAE30\uB2A5\uC758 \uBA85\uC138\uB97C \uC791\uC131\uD569\uB2C8\uB2E4.
|
|
1282
|
+
|
|
1283
|
+
${FORMAT_GUIDE}
|
|
1284
|
+
|
|
1285
|
+
---
|
|
1286
|
+
|
|
1287
|
+
## \uC0DD\uC131 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1288
|
+
|
|
1289
|
+
- [ ] \uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D\uC774 \uBA85\uD655\uD788 \uC815\uC758\uB428
|
|
1290
|
+
- [ ] \uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC\uAC00 \uC791\uC131\uB428
|
|
1291
|
+
- [ ] \uAD00\uB828 \uC774\uD574\uAD00\uACC4\uC790\uC640 \uB17C\uC758 \uC644\uB8CC
|
|
1292
|
+
- [ ] \uAE30\uC874 \uAE30\uB2A5\uACFC\uC758 \uCDA9\uB3CC \uC5EC\uBD80 \uD655\uC778
|
|
1293
|
+
|
|
1294
|
+
---
|
|
1295
|
+
|
|
1296
|
+
## \uC0DD\uC131\uD560 \uD30C\uC77C
|
|
1297
|
+
|
|
1298
|
+
### 1. spec.md
|
|
1299
|
+
|
|
1300
|
+
\`\`\`markdown
|
|
1301
|
+
---
|
|
1302
|
+
id: {FEATURE_ID}
|
|
1303
|
+
title: "{TITLE}"
|
|
1304
|
+
status: draft
|
|
1305
|
+
created: {TODAY}
|
|
1306
|
+
depends: null
|
|
1307
|
+
---
|
|
1308
|
+
|
|
1309
|
+
# {TITLE}
|
|
1310
|
+
|
|
1311
|
+
> {DESCRIPTION}
|
|
1312
|
+
|
|
1313
|
+
---
|
|
1314
|
+
|
|
1315
|
+
## \uAC1C\uC694
|
|
1316
|
+
|
|
1317
|
+
{DESCRIPTION}
|
|
1318
|
+
|
|
1319
|
+
---
|
|
1320
|
+
|
|
1321
|
+
## \uC694\uAD6C\uC0AC\uD56D
|
|
1322
|
+
|
|
1323
|
+
### REQ-01: [\uC694\uAD6C\uC0AC\uD56D \uC81C\uBAA9]
|
|
1324
|
+
|
|
1325
|
+
[\uC694\uAD6C\uC0AC\uD56D \uC0C1\uC138 \uC124\uBA85]
|
|
1326
|
+
- \uC2DC\uC2A4\uD15C\uC740 [\uAE30\uB2A5]\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHALL)
|
|
1327
|
+
|
|
1328
|
+
---
|
|
1329
|
+
|
|
1330
|
+
## \uC2DC\uB098\uB9AC\uC624
|
|
1331
|
+
|
|
1332
|
+
### Scenario 1: [\uC2DC\uB098\uB9AC\uC624\uBA85]
|
|
1333
|
+
|
|
1334
|
+
- **GIVEN** [\uC804\uC81C \uC870\uAC74]
|
|
1335
|
+
- **WHEN** [\uD589\uB3D9/\uD2B8\uB9AC\uAC70]
|
|
1336
|
+
- **THEN** [\uC608\uC0C1 \uACB0\uACFC]
|
|
1337
|
+
|
|
1338
|
+
---
|
|
1339
|
+
|
|
1340
|
+
## \uBE44\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D
|
|
1341
|
+
|
|
1342
|
+
### \uC131\uB2A5
|
|
1343
|
+
- \uC751\uB2F5 \uC2DC\uAC04: [N]ms \uC774\uB0B4 (SHOULD)
|
|
1344
|
+
|
|
1345
|
+
### \uBCF4\uC548
|
|
1346
|
+
- [\uBCF4\uC548 \uC694\uAD6C\uC0AC\uD56D] (SHALL)
|
|
1347
|
+
\`\`\`
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1350
|
+
|
|
1351
|
+
## CLI \uC0AC\uC6A9\uBC95
|
|
1352
|
+
|
|
1353
|
+
\`\`\`bash
|
|
1354
|
+
# \uAE30\uBCF8 \uC0AC\uC6A9
|
|
1355
|
+
sdd new <feature-name>
|
|
1356
|
+
|
|
1357
|
+
# \uC635\uC158 \uC9C0\uC815
|
|
1358
|
+
sdd new <feature-name> --title "\uC81C\uBAA9" --description "\uC124\uBA85"
|
|
1359
|
+
|
|
1360
|
+
# \uACC4\uD68D \uBC0F \uC791\uC5C5\uB3C4 \uD568\uAED8 \uC0DD\uC131
|
|
1361
|
+
sdd new <feature-name> --all
|
|
1362
|
+
|
|
1363
|
+
# \uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC548 \uD568
|
|
1364
|
+
sdd new <feature-name> --no-branch
|
|
1365
|
+
\`\`\`
|
|
1366
|
+
|
|
1367
|
+
---
|
|
1368
|
+
|
|
1369
|
+
## \uC0DD\uC131 \uD6C4 \uD655\uC778
|
|
1370
|
+
|
|
1371
|
+
- [ ] \`sdd validate .sdd/specs/{FEATURE_ID}/spec.md\` \uC2E4\uD589
|
|
1372
|
+
- [ ] RFC 2119 \uD0A4\uC6CC\uB4DC \uC0AC\uC6A9 \uD655\uC778
|
|
1373
|
+
- [ ] GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624 \uD3EC\uD568 \uD655\uC778
|
|
1374
|
+
- [ ] \uB2E4\uC74C \uB2E8\uACC4: \`/sdd:plan\` \uC2E4\uD589
|
|
1375
|
+
`;
|
|
1376
|
+
var PLAN_PROMPT = `# /sdd:plan - \uAD6C\uD604 \uACC4\uD68D
|
|
1377
|
+
|
|
1378
|
+
> \uAE30\uB2A5 \uBA85\uC138\uC5D0 \uB300\uD55C \uAD6C\uD604 \uACC4\uD68D\uC744 \uC791\uC131\uD569\uB2C8\uB2E4.
|
|
1379
|
+
|
|
1380
|
+
---
|
|
1381
|
+
|
|
1382
|
+
## \uC0DD\uC131 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1383
|
+
|
|
1384
|
+
- [ ] \uBA85\uC138(spec.md)\uAC00 \uAC80\uD1A0 \uC644\uB8CC\uB428
|
|
1385
|
+
- [ ] \uAE30\uC220 \uC2A4\uD0DD \uACB0\uC815\uB428
|
|
1386
|
+
- [ ] \uC544\uD0A4\uD14D\uCC98 \uAC80\uD1A0 \uC644\uB8CC
|
|
1387
|
+
- [ ] \uC758\uC874\uC131 \uD655\uC778
|
|
1388
|
+
|
|
1389
|
+
---
|
|
1390
|
+
|
|
1391
|
+
## \uC0DD\uC131\uD560 \uD30C\uC77C
|
|
1392
|
+
|
|
1393
|
+
### 1. plan.md
|
|
1394
|
+
|
|
1395
|
+
\`\`\`markdown
|
|
1396
|
+
---
|
|
1397
|
+
feature: {FEATURE_ID}
|
|
1398
|
+
created: {TODAY}
|
|
1399
|
+
status: draft
|
|
1400
|
+
---
|
|
1401
|
+
|
|
1402
|
+
# \uAD6C\uD604 \uACC4\uD68D: {TITLE}
|
|
1403
|
+
|
|
1404
|
+
> {OVERVIEW}
|
|
1405
|
+
|
|
1406
|
+
---
|
|
1407
|
+
|
|
1408
|
+
## \uAE30\uC220 \uACB0\uC815
|
|
1409
|
+
|
|
1410
|
+
### \uACB0\uC815 1: [\uAE30\uC220 \uACB0\uC815 \uC0AC\uD56D]
|
|
1411
|
+
|
|
1412
|
+
**\uADFC\uAC70:** [\uACB0\uC815 \uADFC\uAC70]
|
|
1413
|
+
|
|
1414
|
+
**\uB300\uC548 \uAC80\uD1A0:**
|
|
1415
|
+
- [\uB300\uC548 1]
|
|
1416
|
+
- [\uB300\uC548 2]
|
|
1417
|
+
|
|
1418
|
+
---
|
|
1419
|
+
|
|
1420
|
+
## \uAD6C\uD604 \uB2E8\uACC4
|
|
1421
|
+
|
|
1422
|
+
### Phase 1: \uAE30\uBC18 \uAD6C\uC870
|
|
1423
|
+
|
|
1424
|
+
[\uAE30\uBC18 \uAD6C\uC870 \uC124\uBA85]
|
|
1425
|
+
|
|
1426
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
1427
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 1]
|
|
1428
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 2]
|
|
1429
|
+
|
|
1430
|
+
### Phase 2: \uD575\uC2EC \uAE30\uB2A5
|
|
1431
|
+
|
|
1432
|
+
[\uD575\uC2EC \uAE30\uB2A5 \uC124\uBA85]
|
|
1433
|
+
|
|
1434
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
1435
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 1]
|
|
1436
|
+
|
|
1437
|
+
---
|
|
1438
|
+
|
|
1439
|
+
## \uB9AC\uC2A4\uD06C \uBD84\uC11D
|
|
1440
|
+
|
|
1441
|
+
| \uB9AC\uC2A4\uD06C | \uC601\uD5A5\uB3C4 | \uC644\uD654 \uC804\uB7B5 |
|
|
1442
|
+
|--------|--------|----------|
|
|
1443
|
+
| [\uB9AC\uC2A4\uD06C] | \u{1F7E1} MEDIUM | [\uC804\uB7B5] |
|
|
1444
|
+
|
|
1445
|
+
---
|
|
1446
|
+
|
|
1447
|
+
## \uD14C\uC2A4\uD2B8 \uC804\uB7B5
|
|
1448
|
+
|
|
1449
|
+
- \uB2E8\uC704 \uD14C\uC2A4\uD2B8: \uCEE4\uBC84\uB9AC\uC9C0 80% \uC774\uC0C1
|
|
1450
|
+
- \uD1B5\uD569 \uD14C\uC2A4\uD2B8: API \uC5D4\uB4DC\uD3EC\uC778\uD2B8
|
|
1451
|
+
- E2E \uD14C\uC2A4\uD2B8: \uC8FC\uC694 \uC2DC\uB098\uB9AC\uC624
|
|
1452
|
+
\`\`\`
|
|
1453
|
+
|
|
1454
|
+
---
|
|
1455
|
+
|
|
1456
|
+
## CLI \uC0AC\uC6A9\uBC95
|
|
1457
|
+
|
|
1458
|
+
\`\`\`bash
|
|
1459
|
+
# \uACC4\uD68D \uC0DD\uC131
|
|
1460
|
+
sdd new plan <feature-id>
|
|
1461
|
+
|
|
1462
|
+
# \uC81C\uBAA9 \uC9C0\uC815
|
|
1463
|
+
sdd new plan <feature-id> --title "\uAD6C\uD604 \uACC4\uD68D"
|
|
1464
|
+
\`\`\`
|
|
1465
|
+
|
|
1466
|
+
---
|
|
1467
|
+
|
|
1468
|
+
## \uC0DD\uC131 \uD6C4 \uD655\uC778
|
|
1469
|
+
|
|
1470
|
+
- [ ] \uAE30\uC220 \uACB0\uC815 \uADFC\uAC70 \uD655\uC778
|
|
1471
|
+
- [ ] \uAD6C\uD604 \uB2E8\uACC4 \uC815\uC758
|
|
1472
|
+
- [ ] \uB9AC\uC2A4\uD06C \uBD84\uC11D \uC644\uB8CC
|
|
1473
|
+
- [ ] \uB2E4\uC74C \uB2E8\uACC4: \`/sdd:tasks\` \uC2E4\uD589
|
|
1474
|
+
`;
|
|
1475
|
+
var TASKS_PROMPT = `# /sdd:tasks - \uC791\uC5C5 \uBD84\uD574
|
|
1476
|
+
|
|
1477
|
+
> \uAD6C\uD604 \uACC4\uD68D\uC744 \uC2E4\uD589 \uAC00\uB2A5\uD55C \uC791\uC5C5\uC73C\uB85C \uBD84\uD574\uD569\uB2C8\uB2E4.
|
|
1478
|
+
|
|
1479
|
+
---
|
|
1480
|
+
|
|
1481
|
+
## \uC0DD\uC131 \uC804 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
1482
|
+
|
|
1483
|
+
- [ ] \uACC4\uD68D(plan.md)\uC774 \uC2B9\uC778\uB428
|
|
1484
|
+
- [ ] \uC791\uC5C5 \uADDC\uBAA8\uAC00 \uD30C\uC545\uB428
|
|
1485
|
+
- [ ] \uC758\uC874\uC131 \uAD00\uACC4 \uC815\uC758\uB428
|
|
1486
|
+
|
|
1487
|
+
---
|
|
1488
|
+
|
|
1489
|
+
## \uC0DD\uC131\uD560 \uD30C\uC77C
|
|
1490
|
+
|
|
1491
|
+
### 1. tasks.md
|
|
1492
|
+
|
|
1493
|
+
\`\`\`markdown
|
|
1494
|
+
---
|
|
1495
|
+
feature: {FEATURE_ID}
|
|
1496
|
+
created: {TODAY}
|
|
1497
|
+
total: {N}
|
|
1498
|
+
completed: 0
|
|
1499
|
+
---
|
|
1500
|
+
|
|
1501
|
+
# \uC791\uC5C5 \uBAA9\uB85D: {TITLE}
|
|
1502
|
+
|
|
1503
|
+
> \uCD1D {N}\uAC1C \uC791\uC5C5
|
|
1504
|
+
|
|
1505
|
+
---
|
|
1506
|
+
|
|
1507
|
+
## \uC9C4\uD589 \uC0C1\uD669
|
|
1508
|
+
|
|
1509
|
+
- \uB300\uAE30: {N}
|
|
1510
|
+
- \uC9C4\uD589 \uC911: 0
|
|
1511
|
+
- \uC644\uB8CC: 0
|
|
1512
|
+
- \uCC28\uB2E8\uB428: 0
|
|
1513
|
+
|
|
1514
|
+
---
|
|
1515
|
+
|
|
1516
|
+
## \uC791\uC5C5 \uBAA9\uB85D
|
|
1517
|
+
|
|
1518
|
+
### {FEATURE_ID}-task-001: [\uC791\uC5C5 \uC81C\uBAA9]
|
|
1519
|
+
|
|
1520
|
+
- **\uC0C1\uD0DC:** \uB300\uAE30
|
|
1521
|
+
- **\uC6B0\uC120\uC21C\uC704:** \u{1F534} HIGH
|
|
1522
|
+
- **\uC124\uBA85:** [\uC791\uC5C5 \uC124\uBA85]
|
|
1523
|
+
- **\uAD00\uB828 \uD30C\uC77C:**
|
|
1524
|
+
- \`src/path/to/file.ts\`
|
|
1525
|
+
- **\uC758\uC874\uC131:** \uC5C6\uC74C
|
|
1526
|
+
|
|
1527
|
+
### {FEATURE_ID}-task-002: [\uC791\uC5C5 \uC81C\uBAA9]
|
|
1528
|
+
|
|
1529
|
+
- **\uC0C1\uD0DC:** \uB300\uAE30
|
|
1530
|
+
- **\uC6B0\uC120\uC21C\uC704:** \u{1F7E1} MEDIUM
|
|
1531
|
+
- **\uC758\uC874\uC131:** {FEATURE_ID}-task-001
|
|
1532
|
+
\`\`\`
|
|
1533
|
+
|
|
1534
|
+
---
|
|
1535
|
+
|
|
1536
|
+
## CLI \uC0AC\uC6A9\uBC95
|
|
1537
|
+
|
|
1538
|
+
\`\`\`bash
|
|
1539
|
+
# \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131
|
|
1540
|
+
sdd new tasks <feature-id>
|
|
1541
|
+
\`\`\`
|
|
1542
|
+
|
|
1543
|
+
---
|
|
1544
|
+
|
|
1545
|
+
## \uC791\uC5C5 \uC644\uB8CC \uC870\uAC74
|
|
1546
|
+
|
|
1547
|
+
\uAC01 \uC791\uC5C5 \uC644\uB8CC \uC2DC:
|
|
1548
|
+
1. [ ] \uCF54\uB4DC \uC791\uC131 \uC644\uB8CC
|
|
1549
|
+
2. [ ] \uD14C\uC2A4\uD2B8 \uC791\uC131 \uBC0F \uD1B5\uACFC
|
|
1550
|
+
3. [ ] \uCF54\uB4DC \uB9AC\uBDF0 \uC644\uB8CC
|
|
1551
|
+
4. [ ] \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8
|
|
1552
|
+
|
|
1553
|
+
---
|
|
1554
|
+
|
|
1555
|
+
## \uB2E4\uC74C \uB2E8\uACC4
|
|
1556
|
+
|
|
1557
|
+
1. \uCCAB \uBC88\uC9F8 \uC791\uC5C5\uBD80\uD130 \uC21C\uCC28\uC801\uC73C\uB85C \uC9C4\uD589
|
|
1558
|
+
2. \uAC01 \uC791\uC5C5 \uC644\uB8CC \uD6C4 \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8
|
|
1559
|
+
3. \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC \uC2DC \`/sdd:archive\` \uC2E4\uD589
|
|
1560
|
+
`;
|
|
1561
|
+
var VALIDATE_PROMPT = `# /sdd:validate - \uC2A4\uD399 \uAC80\uC99D
|
|
1562
|
+
|
|
1563
|
+
> \uC2A4\uD399 \uD30C\uC77C\uC758 \uD615\uC2DD\uACFC \uB0B4\uC6A9\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4.
|
|
1564
|
+
|
|
1565
|
+
---
|
|
1566
|
+
|
|
1567
|
+
## \uAC80\uC99D \uB300\uC0C1
|
|
1568
|
+
|
|
1569
|
+
- .sdd/specs/ \uB514\uB809\uD1A0\uB9AC\uC758 \uBAA8\uB4E0 \uC2A4\uD399 \uD30C\uC77C
|
|
1570
|
+
- .sdd/changes/ \uB514\uB809\uD1A0\uB9AC\uC758 \uBAA8\uB4E0 \uBCC0\uACBD \uC81C\uC548
|
|
1571
|
+
|
|
1572
|
+
---
|
|
1573
|
+
|
|
1574
|
+
## \uAC80\uC99D \uD56D\uBAA9
|
|
1575
|
+
|
|
1576
|
+
### \uD544\uC218 \uD615\uC2DD
|
|
1577
|
+
|
|
1578
|
+
1. YAML frontmatter \uC874\uC7AC
|
|
1579
|
+
- status: draft | active | deprecated
|
|
1580
|
+
- created: YYYY-MM-DD
|
|
1581
|
+
- depends: null | [spec-id, ...]
|
|
1582
|
+
|
|
1583
|
+
2. RFC 2119 \uD0A4\uC6CC\uB4DC \uC0AC\uC6A9
|
|
1584
|
+
- SHALL, MUST, SHOULD, MAY, SHALL NOT
|
|
1585
|
+
|
|
1586
|
+
3. GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624
|
|
1587
|
+
- \uBAA8\uB4E0 Requirement\uC5D0 \uCD5C\uC18C 1\uAC1C Scenario
|
|
1588
|
+
|
|
1589
|
+
---
|
|
1590
|
+
|
|
1591
|
+
## CLI \uC0AC\uC6A9\uBC95
|
|
1592
|
+
|
|
1593
|
+
\`\`\`bash
|
|
1594
|
+
# \uC804\uCCB4 \uAC80\uC99D
|
|
1595
|
+
sdd validate
|
|
1596
|
+
|
|
1597
|
+
# \uD2B9\uC815 \uACBD\uB85C \uAC80\uC99D
|
|
1598
|
+
sdd validate .sdd/specs/feature/spec.md
|
|
1599
|
+
|
|
1600
|
+
# \uC5C4\uACA9 \uBAA8\uB4DC (\uACBD\uACE0\uB3C4 \uC5D0\uB7EC\uB85C \uCC98\uB9AC)
|
|
1601
|
+
sdd validate --strict
|
|
1602
|
+
|
|
1603
|
+
# \uC870\uC6A9\uD55C \uBAA8\uB4DC
|
|
1604
|
+
sdd validate --quiet
|
|
1605
|
+
\`\`\`
|
|
1606
|
+
|
|
1607
|
+
---
|
|
1608
|
+
|
|
1609
|
+
## \uAC80\uC99D \uACB0\uACFC
|
|
1610
|
+
|
|
1611
|
+
- \u2705 PASS: \uBAA8\uB4E0 \uAC80\uC99D \uD1B5\uACFC
|
|
1612
|
+
- \u26A0\uFE0F WARN: \uACBD\uACE0 (--strict\uC5D0\uC11C \uC2E4\uD328)
|
|
1613
|
+
- \u274C FAIL: \uD544\uC218 \uD56D\uBAA9 \uB204\uB77D
|
|
1614
|
+
`;
|
|
1615
|
+
var PROMPTS = {
|
|
1616
|
+
change: CHANGE_PROMPT,
|
|
1617
|
+
apply: APPLY_PROMPT,
|
|
1618
|
+
archive: ARCHIVE_PROMPT,
|
|
1619
|
+
impact: IMPACT_PROMPT,
|
|
1620
|
+
validate: VALIDATE_PROMPT,
|
|
1621
|
+
new: NEW_PROMPT,
|
|
1622
|
+
plan: PLAN_PROMPT,
|
|
1623
|
+
tasks: TASKS_PROMPT
|
|
1624
|
+
};
|
|
1625
|
+
function getPrompt(command) {
|
|
1626
|
+
return PROMPTS[command];
|
|
1627
|
+
}
|
|
1628
|
+
function getAvailableCommands() {
|
|
1629
|
+
return Object.keys(PROMPTS);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/cli/commands/prompt.ts
|
|
1633
|
+
function registerPromptCommand(program2) {
|
|
1634
|
+
program2.command("prompt [command]").description("\uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC \uD504\uB86C\uD504\uD2B8\uB97C \uCD9C\uB825\uD569\uB2C8\uB2E4").option("-l, --list", "\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBA85\uB839\uC5B4 \uBAA9\uB85D").action(async (command, options) => {
|
|
1635
|
+
try {
|
|
1636
|
+
await runPrompt(command, options);
|
|
1637
|
+
} catch (error2) {
|
|
1638
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
1639
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
async function runPrompt(command, options) {
|
|
1644
|
+
if (options.list || !command) {
|
|
1645
|
+
const commands = getAvailableCommands();
|
|
1646
|
+
info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC2AC\uB798\uC2DC \uCEE4\uB9E8\uB4DC:");
|
|
1647
|
+
newline();
|
|
1648
|
+
for (const cmd of commands) {
|
|
1649
|
+
listItem(`/sdd:${cmd}`);
|
|
1650
|
+
}
|
|
1651
|
+
newline();
|
|
1652
|
+
info("\uC0AC\uC6A9\uBC95: sdd prompt <command>");
|
|
1653
|
+
info("\uC608\uC2DC: sdd prompt change");
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
const prompt = getPrompt(command);
|
|
1657
|
+
if (!prompt) {
|
|
1658
|
+
error(`\uC54C \uC218 \uC5C6\uB294 \uBA85\uB839\uC5B4: ${command}`);
|
|
1659
|
+
info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uBA85\uB839\uC5B4: " + getAvailableCommands().join(", "));
|
|
1660
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
1661
|
+
}
|
|
1662
|
+
console.log(prompt);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/cli/commands/change.ts
|
|
1666
|
+
import path6 from "path";
|
|
1667
|
+
import { promises as fs3 } from "fs";
|
|
1668
|
+
|
|
1669
|
+
// src/core/change/schemas.ts
|
|
1670
|
+
import { z as z2 } from "zod";
|
|
1671
|
+
var ChangeStatusSchema = z2.enum([
|
|
1672
|
+
"draft",
|
|
1673
|
+
"proposed",
|
|
1674
|
+
"approved",
|
|
1675
|
+
"applied",
|
|
1676
|
+
"archived",
|
|
1677
|
+
"rejected"
|
|
1678
|
+
]);
|
|
1679
|
+
var DeltaTypeSchema = z2.enum(["ADDED", "MODIFIED", "REMOVED"]);
|
|
1680
|
+
var ImpactLevelSchema = z2.enum(["low", "medium", "high"]);
|
|
1681
|
+
var ProposalMetadataSchema = z2.object({
|
|
1682
|
+
id: z2.string().regex(/^CHG-\d{3,}$/, "ID \uD615\uC2DD: CHG-XXX"),
|
|
1683
|
+
status: ChangeStatusSchema,
|
|
1684
|
+
created: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/, "\uB0A0\uC9DC \uD615\uC2DD: YYYY-MM-DD"),
|
|
1685
|
+
updated: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
|
|
1686
|
+
target: z2.string().optional()
|
|
1687
|
+
});
|
|
1688
|
+
var DeltaItemSchema = z2.object({
|
|
1689
|
+
type: DeltaTypeSchema,
|
|
1690
|
+
target: z2.string(),
|
|
1691
|
+
before: z2.string().optional(),
|
|
1692
|
+
after: z2.string().optional(),
|
|
1693
|
+
description: z2.string().optional()
|
|
1694
|
+
});
|
|
1695
|
+
var DeltaMetadataSchema = z2.object({
|
|
1696
|
+
proposal: z2.string(),
|
|
1697
|
+
created: z2.string().regex(/^\d{4}-\d{2}-\d{2}$/)
|
|
1698
|
+
});
|
|
1699
|
+
var ProposalSchema = z2.object({
|
|
1700
|
+
metadata: ProposalMetadataSchema,
|
|
1701
|
+
title: z2.string(),
|
|
1702
|
+
rationale: z2.string().optional(),
|
|
1703
|
+
affectedSpecs: z2.array(z2.string()),
|
|
1704
|
+
changeType: z2.array(DeltaTypeSchema),
|
|
1705
|
+
summary: z2.string().optional(),
|
|
1706
|
+
riskLevel: ImpactLevelSchema.optional(),
|
|
1707
|
+
complexity: ImpactLevelSchema.optional()
|
|
1708
|
+
});
|
|
1709
|
+
var DeltaSchema = z2.object({
|
|
1710
|
+
metadata: DeltaMetadataSchema,
|
|
1711
|
+
title: z2.string(),
|
|
1712
|
+
added: z2.array(z2.string()).optional(),
|
|
1713
|
+
modified: z2.array(
|
|
1714
|
+
z2.object({
|
|
1715
|
+
target: z2.string(),
|
|
1716
|
+
before: z2.string(),
|
|
1717
|
+
after: z2.string()
|
|
1718
|
+
})
|
|
1719
|
+
).optional(),
|
|
1720
|
+
removed: z2.array(z2.string()).optional()
|
|
1721
|
+
});
|
|
1722
|
+
function generateChangeId(existingIds) {
|
|
1723
|
+
const maxId = existingIds.map((id) => {
|
|
1724
|
+
const match = id.match(/^CHG-(\d+)$/);
|
|
1725
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1726
|
+
}).reduce((max, curr) => Math.max(max, curr), 0);
|
|
1727
|
+
return `CHG-${String(maxId + 1).padStart(3, "0")}`;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/core/change/proposal.ts
|
|
1731
|
+
import matter2 from "gray-matter";
|
|
1732
|
+
import { z as z3 } from "zod";
|
|
1733
|
+
var PreprocessedProposalMetadataSchema = z3.object({
|
|
1734
|
+
id: z3.string().regex(/^CHG-\d{3,}$/, "ID \uD615\uC2DD: CHG-XXX"),
|
|
1735
|
+
status: z3.preprocess(
|
|
1736
|
+
(val) => typeof val === "string" ? val : "draft",
|
|
1737
|
+
z3.enum(["draft", "proposed", "approved", "applied", "archived", "rejected"])
|
|
1738
|
+
),
|
|
1739
|
+
created: z3.preprocess(
|
|
1740
|
+
(val) => {
|
|
1741
|
+
if (val instanceof Date) {
|
|
1742
|
+
return val.toISOString().split("T")[0];
|
|
1743
|
+
}
|
|
1744
|
+
return val;
|
|
1745
|
+
},
|
|
1746
|
+
z3.string().regex(/^\d{4}-\d{2}-\d{2}$/, "\uB0A0\uC9DC \uD615\uC2DD: YYYY-MM-DD")
|
|
1747
|
+
),
|
|
1748
|
+
updated: z3.preprocess(
|
|
1749
|
+
(val) => {
|
|
1750
|
+
if (val instanceof Date) {
|
|
1751
|
+
return val.toISOString().split("T")[0];
|
|
1752
|
+
}
|
|
1753
|
+
return val;
|
|
1754
|
+
},
|
|
1755
|
+
z3.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional()
|
|
1756
|
+
),
|
|
1757
|
+
target: z3.string().optional()
|
|
1758
|
+
});
|
|
1759
|
+
function parseProposal(content) {
|
|
1760
|
+
try {
|
|
1761
|
+
const { data: frontmatter, content: body } = matter2(content);
|
|
1762
|
+
const metadataResult = PreprocessedProposalMetadataSchema.safeParse(frontmatter);
|
|
1763
|
+
if (!metadataResult.success) {
|
|
1764
|
+
return failure(
|
|
1765
|
+
new ChangeError(`Proposal \uBA54\uD0C0\uB370\uC774\uD130 \uC624\uB958: ${metadataResult.error.message}`)
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
const titleMatch = body.match(/^#\s+(?:변경\s+제안:\s*)?(.+)$/m);
|
|
1769
|
+
const title2 = titleMatch?.[1]?.trim() || "";
|
|
1770
|
+
const rationaleMatch = body.match(/##\s*배경\s*([\s\S]*?)(?=\n##|$)/i);
|
|
1771
|
+
const rationale = rationaleMatch?.[1]?.trim() || "";
|
|
1772
|
+
const specsMatch = body.match(/##\s*영향\s*범위[\s\S]*?영향받는\s*스펙\s*([\s\S]*?)(?=\n###|\n##|$)/i);
|
|
1773
|
+
const affectedSpecs = [];
|
|
1774
|
+
if (specsMatch) {
|
|
1775
|
+
const specLines = specsMatch[1].match(/`([^`]+)`/g);
|
|
1776
|
+
if (specLines) {
|
|
1777
|
+
specLines.forEach((line) => {
|
|
1778
|
+
affectedSpecs.push(line.replace(/`/g, ""));
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
const changeType = [];
|
|
1783
|
+
if (body.includes("[x] \uC2E0\uADDC \uCD94\uAC00") || body.includes("[X] \uC2E0\uADDC \uCD94\uAC00")) {
|
|
1784
|
+
changeType.push("ADDED");
|
|
1785
|
+
}
|
|
1786
|
+
if (body.includes("[x] \uC218\uC815") || body.includes("[X] \uC218\uC815")) {
|
|
1787
|
+
changeType.push("MODIFIED");
|
|
1788
|
+
}
|
|
1789
|
+
if (body.includes("[x] \uC0AD\uC81C") || body.includes("[X] \uC0AD\uC81C")) {
|
|
1790
|
+
changeType.push("REMOVED");
|
|
1791
|
+
}
|
|
1792
|
+
const summaryMatch = body.match(/##\s*변경\s*내용\s*([\s\S]*?)(?=\n##|$)/i);
|
|
1793
|
+
const summary = summaryMatch?.[1]?.trim() || "";
|
|
1794
|
+
const riskMatch = body.match(/영향도:\s*(낮음|중간|높음)/i);
|
|
1795
|
+
const riskLevel = riskMatch ? riskMatch[1] === "\uB0AE\uC74C" ? "low" : riskMatch[1] === "\uB192\uC74C" ? "high" : "medium" : "medium";
|
|
1796
|
+
const complexityMatch = body.match(/복잡도:\s*(낮음|중간|높음)/i);
|
|
1797
|
+
const complexity = complexityMatch ? complexityMatch[1] === "\uB0AE\uC74C" ? "low" : complexityMatch[1] === "\uB192\uC74C" ? "high" : "medium" : "medium";
|
|
1798
|
+
return success({
|
|
1799
|
+
metadata: metadataResult.data,
|
|
1800
|
+
title: title2,
|
|
1801
|
+
rationale,
|
|
1802
|
+
affectedSpecs,
|
|
1803
|
+
changeType,
|
|
1804
|
+
summary,
|
|
1805
|
+
riskLevel,
|
|
1806
|
+
complexity,
|
|
1807
|
+
rawContent: body
|
|
1808
|
+
});
|
|
1809
|
+
} catch (error2) {
|
|
1810
|
+
return failure(
|
|
1811
|
+
new ChangeError(
|
|
1812
|
+
`Proposal \uD30C\uC2F1 \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
function generateProposal(options) {
|
|
1818
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1819
|
+
const specs = options.affectedSpecs || [];
|
|
1820
|
+
const types = options.changeType || ["MODIFIED"];
|
|
1821
|
+
return `---
|
|
1822
|
+
id: ${options.id}
|
|
1823
|
+
status: draft
|
|
1824
|
+
created: ${today}
|
|
1825
|
+
---
|
|
1826
|
+
|
|
1827
|
+
# \uBCC0\uACBD \uC81C\uC548: ${options.title}
|
|
1828
|
+
|
|
1829
|
+
> ${options.rationale || "\uBCC0\uACBD \uBAA9\uC801 \uBC0F \uBC30\uACBD \uC124\uBA85"}
|
|
1830
|
+
|
|
1831
|
+
---
|
|
1832
|
+
|
|
1833
|
+
## \uBC30\uACBD
|
|
1834
|
+
|
|
1835
|
+
${options.rationale || "\uC65C \uC774 \uBCC0\uACBD\uC774 \uD544\uC694\uD55C\uAC00?"}
|
|
1836
|
+
|
|
1837
|
+
---
|
|
1838
|
+
|
|
1839
|
+
## \uC601\uD5A5 \uBC94\uC704
|
|
1840
|
+
|
|
1841
|
+
### \uC601\uD5A5\uBC1B\uB294 \uC2A4\uD399
|
|
1842
|
+
|
|
1843
|
+
${specs.length > 0 ? specs.map((s) => `- \`${s}\``).join("\n") : "- `specs/{{SPEC_PATH}}`"}
|
|
1844
|
+
|
|
1845
|
+
### \uBCC0\uACBD \uC720\uD615
|
|
1846
|
+
|
|
1847
|
+
- [${types.includes("ADDED") ? "x" : " "}] \uC2E0\uADDC \uCD94\uAC00 (ADDED)
|
|
1848
|
+
- [${types.includes("MODIFIED") ? "x" : " "}] \uC218\uC815 (MODIFIED)
|
|
1849
|
+
- [${types.includes("REMOVED") ? "x" : " "}] \uC0AD\uC81C (REMOVED)
|
|
1850
|
+
|
|
1851
|
+
---
|
|
1852
|
+
|
|
1853
|
+
## \uBCC0\uACBD \uB0B4\uC6A9
|
|
1854
|
+
|
|
1855
|
+
### ADDED
|
|
1856
|
+
|
|
1857
|
+
(\uC0C8\uB85C \uCD94\uAC00\uB418\uB294 \uB0B4\uC6A9)
|
|
1858
|
+
|
|
1859
|
+
### MODIFIED
|
|
1860
|
+
|
|
1861
|
+
#### Before
|
|
1862
|
+
|
|
1863
|
+
\`\`\`markdown
|
|
1864
|
+
\uAE30\uC874 \uB0B4\uC6A9
|
|
1865
|
+
\`\`\`
|
|
1866
|
+
|
|
1867
|
+
#### After
|
|
1868
|
+
|
|
1869
|
+
\`\`\`markdown
|
|
1870
|
+
\uBCC0\uACBD\uB41C \uB0B4\uC6A9
|
|
1871
|
+
\`\`\`
|
|
1872
|
+
|
|
1873
|
+
### REMOVED
|
|
1874
|
+
|
|
1875
|
+
(\uC0AD\uC81C\uB418\uB294 \uB0B4\uC6A9)
|
|
1876
|
+
|
|
1877
|
+
---
|
|
1878
|
+
|
|
1879
|
+
## \uB9AC\uC2A4\uD06C \uD3C9\uAC00
|
|
1880
|
+
|
|
1881
|
+
- \uC601\uD5A5\uB3C4: \uC911\uAC04
|
|
1882
|
+
- \uBCF5\uC7A1\uB3C4: \uC911\uAC04
|
|
1883
|
+
`;
|
|
1884
|
+
}
|
|
1885
|
+
function updateProposalStatus(content, newStatus) {
|
|
1886
|
+
try {
|
|
1887
|
+
const { data: frontmatter, content: body } = matter2(content);
|
|
1888
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1889
|
+
const updatedFrontmatter = {
|
|
1890
|
+
...frontmatter,
|
|
1891
|
+
status: newStatus,
|
|
1892
|
+
updated: today
|
|
1893
|
+
};
|
|
1894
|
+
return success(matter2.stringify(body, updatedFrontmatter));
|
|
1895
|
+
} catch (error2) {
|
|
1896
|
+
return failure(
|
|
1897
|
+
new ChangeError(
|
|
1898
|
+
`\uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8 \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
1899
|
+
)
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// src/core/change/delta.ts
|
|
1905
|
+
import matter3 from "gray-matter";
|
|
1906
|
+
import { z as z4 } from "zod";
|
|
1907
|
+
var PreprocessedDeltaMetadataSchema = z4.object({
|
|
1908
|
+
proposal: z4.string(),
|
|
1909
|
+
created: z4.preprocess(
|
|
1910
|
+
(val) => {
|
|
1911
|
+
if (val instanceof Date) {
|
|
1912
|
+
return val.toISOString().split("T")[0];
|
|
1913
|
+
}
|
|
1914
|
+
return val;
|
|
1915
|
+
},
|
|
1916
|
+
z4.string().regex(/^\d{4}-\d{2}-\d{2}$/, "\uB0A0\uC9DC \uD615\uC2DD: YYYY-MM-DD")
|
|
1917
|
+
)
|
|
1918
|
+
});
|
|
1919
|
+
function generateDelta(options) {
|
|
1920
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
1921
|
+
let content = `---
|
|
1922
|
+
proposal: ${options.proposalId}
|
|
1923
|
+
created: ${today}
|
|
1924
|
+
---
|
|
1925
|
+
|
|
1926
|
+
# Delta: ${options.title}
|
|
1927
|
+
|
|
1928
|
+
## ADDED
|
|
1929
|
+
|
|
1930
|
+
`;
|
|
1931
|
+
if (options.added && options.added.length > 0) {
|
|
1932
|
+
content += options.added.join("\n\n");
|
|
1933
|
+
} else {
|
|
1934
|
+
content += "(\uCD94\uAC00\uB418\uB294 \uC2A4\uD399 \uB0B4\uC6A9)";
|
|
1935
|
+
}
|
|
1936
|
+
content += "\n\n## MODIFIED\n\n";
|
|
1937
|
+
if (options.modified && options.modified.length > 0) {
|
|
1938
|
+
options.modified.forEach((mod) => {
|
|
1939
|
+
content += `### ${mod.target}
|
|
1940
|
+
|
|
1941
|
+
`;
|
|
1942
|
+
content += `#### Before
|
|
1943
|
+
|
|
1944
|
+
\`\`\`markdown
|
|
1945
|
+
${mod.before}
|
|
1946
|
+
\`\`\`
|
|
1947
|
+
|
|
1948
|
+
`;
|
|
1949
|
+
content += `#### After
|
|
1950
|
+
|
|
1951
|
+
\`\`\`markdown
|
|
1952
|
+
${mod.after}
|
|
1953
|
+
\`\`\`
|
|
1954
|
+
|
|
1955
|
+
`;
|
|
1956
|
+
});
|
|
1957
|
+
} else {
|
|
1958
|
+
content += `### {{SPEC_PATH}}
|
|
1959
|
+
|
|
1960
|
+
#### Before
|
|
1961
|
+
|
|
1962
|
+
\`\`\`markdown
|
|
1963
|
+
\uAE30\uC874 \uB0B4\uC6A9
|
|
1964
|
+
\`\`\`
|
|
1965
|
+
|
|
1966
|
+
#### After
|
|
1967
|
+
|
|
1968
|
+
\`\`\`markdown
|
|
1969
|
+
\uBCC0\uACBD\uB41C \uB0B4\uC6A9
|
|
1970
|
+
\`\`\`
|
|
1971
|
+
|
|
1972
|
+
`;
|
|
1973
|
+
}
|
|
1974
|
+
content += "## REMOVED\n\n";
|
|
1975
|
+
if (options.removed && options.removed.length > 0) {
|
|
1976
|
+
content += options.removed.join("\n\n");
|
|
1977
|
+
} else {
|
|
1978
|
+
content += "(\uC0AD\uC81C\uB418\uB294 \uC2A4\uD399 \uCC38\uC870)";
|
|
1979
|
+
}
|
|
1980
|
+
return content;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/core/change/archive.ts
|
|
1984
|
+
import path5 from "path";
|
|
1985
|
+
import { promises as fs2 } from "fs";
|
|
1986
|
+
async function archiveChange(sddPath, changeId) {
|
|
1987
|
+
try {
|
|
1988
|
+
const changesPath = path5.join(sddPath, "changes");
|
|
1989
|
+
const archivePath = path5.join(sddPath, "archive");
|
|
1990
|
+
const sourceDir = path5.join(changesPath, changeId);
|
|
1991
|
+
if (!await directoryExists(sourceDir)) {
|
|
1992
|
+
return failure(new ChangeError(`\uBCC0\uACBD \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${changeId}`));
|
|
1993
|
+
}
|
|
1994
|
+
const today = /* @__PURE__ */ new Date();
|
|
1995
|
+
const yearMonth = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}`;
|
|
1996
|
+
const archiveMonthDir = path5.join(archivePath, yearMonth);
|
|
1997
|
+
await ensureDir(archiveMonthDir);
|
|
1998
|
+
const datePrefix = today.toISOString().split("T")[0];
|
|
1999
|
+
const archiveDir = path5.join(archiveMonthDir, `${datePrefix}-${changeId}`);
|
|
2000
|
+
const copyResult = await copyDir(sourceDir, archiveDir);
|
|
2001
|
+
if (!copyResult.success) {
|
|
2002
|
+
return failure(new ChangeError(`\uC544\uCE74\uC774\uBE0C \uBCF5\uC0AC \uC2E4\uD328: ${copyResult.error?.message}`));
|
|
2003
|
+
}
|
|
2004
|
+
const proposalPath = path5.join(archiveDir, "proposal.md");
|
|
2005
|
+
try {
|
|
2006
|
+
const proposalContent = await fs2.readFile(proposalPath, "utf-8");
|
|
2007
|
+
const updateResult = updateProposalStatus(proposalContent, "archived");
|
|
2008
|
+
if (updateResult.success) {
|
|
2009
|
+
await fs2.writeFile(proposalPath, updateResult.data);
|
|
2010
|
+
}
|
|
2011
|
+
} catch {
|
|
2012
|
+
}
|
|
2013
|
+
const removeResult = await removeDir(sourceDir);
|
|
2014
|
+
if (!removeResult.success) {
|
|
2015
|
+
return failure(new ChangeError(`\uC6D0\uBCF8 \uB514\uB809\uD1A0\uB9AC \uC0AD\uC81C \uC2E4\uD328: ${removeResult.error?.message}`));
|
|
2016
|
+
}
|
|
2017
|
+
return success({
|
|
2018
|
+
sourceDir,
|
|
2019
|
+
archiveDir,
|
|
2020
|
+
archivedAt: today.toISOString(),
|
|
2021
|
+
changeId
|
|
2022
|
+
});
|
|
2023
|
+
} catch (error2) {
|
|
2024
|
+
return failure(
|
|
2025
|
+
new ChangeError(
|
|
2026
|
+
`\uC544\uCE74\uC774\uBE0C \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2027
|
+
)
|
|
2028
|
+
);
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
async function listArchives(sddPath) {
|
|
2032
|
+
try {
|
|
2033
|
+
const archivePath = path5.join(sddPath, "archive");
|
|
2034
|
+
if (!await directoryExists(archivePath)) {
|
|
2035
|
+
return success([]);
|
|
2036
|
+
}
|
|
2037
|
+
const archives = [];
|
|
2038
|
+
const months = await fs2.readdir(archivePath);
|
|
2039
|
+
for (const month of months) {
|
|
2040
|
+
const monthPath = path5.join(archivePath, month);
|
|
2041
|
+
const stat = await fs2.stat(monthPath);
|
|
2042
|
+
if (!stat.isDirectory()) continue;
|
|
2043
|
+
const changes = await fs2.readdir(monthPath);
|
|
2044
|
+
for (const change of changes) {
|
|
2045
|
+
const changePath = path5.join(monthPath, change);
|
|
2046
|
+
const changeStat = await fs2.stat(changePath);
|
|
2047
|
+
if (!changeStat.isDirectory()) continue;
|
|
2048
|
+
const idMatch = change.match(/\d{4}-\d{2}-\d{2}-(CHG-\d+)/);
|
|
2049
|
+
const id = idMatch ? idMatch[1] : change;
|
|
2050
|
+
const dateMatch = change.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
2051
|
+
const archivedAt = dateMatch ? dateMatch[1] : month;
|
|
2052
|
+
let title2;
|
|
2053
|
+
try {
|
|
2054
|
+
const proposalPath = path5.join(changePath, "proposal.md");
|
|
2055
|
+
const proposalContent = await fs2.readFile(proposalPath, "utf-8");
|
|
2056
|
+
const parseResult = parseProposal(proposalContent);
|
|
2057
|
+
if (parseResult.success) {
|
|
2058
|
+
title2 = parseResult.data.title;
|
|
2059
|
+
}
|
|
2060
|
+
} catch {
|
|
2061
|
+
}
|
|
2062
|
+
archives.push({
|
|
2063
|
+
id,
|
|
2064
|
+
path: changePath,
|
|
2065
|
+
archivedAt,
|
|
2066
|
+
title: title2
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
archives.sort((a, b) => b.archivedAt.localeCompare(a.archivedAt));
|
|
2071
|
+
return success(archives);
|
|
2072
|
+
} catch (error2) {
|
|
2073
|
+
return failure(
|
|
2074
|
+
new ChangeError(
|
|
2075
|
+
`\uC544\uCE74\uC774\uBE0C \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2076
|
+
)
|
|
2077
|
+
);
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
async function listPendingChanges(sddPath) {
|
|
2081
|
+
try {
|
|
2082
|
+
const changesPath = path5.join(sddPath, "changes");
|
|
2083
|
+
if (!await directoryExists(changesPath)) {
|
|
2084
|
+
return success([]);
|
|
2085
|
+
}
|
|
2086
|
+
const changes = [];
|
|
2087
|
+
const dirs = await fs2.readdir(changesPath);
|
|
2088
|
+
for (const dir of dirs) {
|
|
2089
|
+
const changePath = path5.join(changesPath, dir);
|
|
2090
|
+
const stat = await fs2.stat(changePath);
|
|
2091
|
+
if (!stat.isDirectory()) continue;
|
|
2092
|
+
let status = "draft";
|
|
2093
|
+
let title2;
|
|
2094
|
+
let createdAt;
|
|
2095
|
+
try {
|
|
2096
|
+
const proposalPath = path5.join(changePath, "proposal.md");
|
|
2097
|
+
const proposalContent = await fs2.readFile(proposalPath, "utf-8");
|
|
2098
|
+
const parseResult = parseProposal(proposalContent);
|
|
2099
|
+
if (parseResult.success) {
|
|
2100
|
+
status = parseResult.data.metadata.status;
|
|
2101
|
+
title2 = parseResult.data.title;
|
|
2102
|
+
createdAt = parseResult.data.metadata.created;
|
|
2103
|
+
}
|
|
2104
|
+
} catch {
|
|
2105
|
+
}
|
|
2106
|
+
changes.push({
|
|
2107
|
+
id: dir,
|
|
2108
|
+
path: changePath,
|
|
2109
|
+
status,
|
|
2110
|
+
title: title2,
|
|
2111
|
+
createdAt
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
changes.sort((a, b) => (b.createdAt || "").localeCompare(a.createdAt || ""));
|
|
2115
|
+
return success(changes);
|
|
2116
|
+
} catch (error2) {
|
|
2117
|
+
return failure(
|
|
2118
|
+
new ChangeError(
|
|
2119
|
+
`\uBCC0\uACBD \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2120
|
+
)
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
// src/cli/commands/change.ts
|
|
2126
|
+
function registerChangeCommand(program2) {
|
|
2127
|
+
const change = program2.command("change [id]").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC0DD\uC131\uD558\uAC70\uB098 \uAD00\uB9AC\uD569\uB2C8\uB2E4").option("-l, --list", "\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D").option("-t, --title <title>", "\uBCC0\uACBD \uC81C\uC548 \uC81C\uBAA9").option("-s, --spec <spec>", "\uB300\uC0C1 \uC2A4\uD399 \uACBD\uB85C").action(async (id, options) => {
|
|
2128
|
+
try {
|
|
2129
|
+
await runChange(id, options);
|
|
2130
|
+
} catch (error2) {
|
|
2131
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
2132
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2133
|
+
}
|
|
2134
|
+
});
|
|
2135
|
+
change.command("apply <id>").description("\uBCC0\uACBD \uC81C\uC548\uC744 \uC2A4\uD399\uC5D0 \uC801\uC6A9\uD569\uB2C8\uB2E4").action(async (id) => {
|
|
2136
|
+
try {
|
|
2137
|
+
await runApply(id);
|
|
2138
|
+
} catch (error2) {
|
|
2139
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
2140
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
change.command("archive <id>").description("\uC644\uB8CC\uB41C \uBCC0\uACBD\uC744 \uC544\uCE74\uC774\uBE0C\uD569\uB2C8\uB2E4").action(async (id) => {
|
|
2144
|
+
try {
|
|
2145
|
+
await runArchive(id);
|
|
2146
|
+
} catch (error2) {
|
|
2147
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
2148
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2149
|
+
}
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
async function runChange(id, options) {
|
|
2153
|
+
const projectRoot = await findSddRoot();
|
|
2154
|
+
if (!projectRoot) {
|
|
2155
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
2156
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2157
|
+
}
|
|
2158
|
+
const sddPath = path6.join(projectRoot, ".sdd");
|
|
2159
|
+
if (options.list) {
|
|
2160
|
+
const result = await listPendingChanges(sddPath);
|
|
2161
|
+
if (!result.success) {
|
|
2162
|
+
error(result.error.message);
|
|
2163
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2164
|
+
}
|
|
2165
|
+
if (result.data.length === 0) {
|
|
2166
|
+
info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD:");
|
|
2170
|
+
newline();
|
|
2171
|
+
for (const change of result.data) {
|
|
2172
|
+
const statusIcon = change.status === "approved" ? "\u2713" : "\u25CB";
|
|
2173
|
+
listItem(`${statusIcon} ${change.id}: ${change.title || "(\uC81C\uBAA9 \uC5C6\uC74C)"} [${change.status}]`);
|
|
2174
|
+
}
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
if (id) {
|
|
2178
|
+
const changePath2 = path6.join(sddPath, "changes", id);
|
|
2179
|
+
if (!await directoryExists(changePath2)) {
|
|
2180
|
+
error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
|
|
2181
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2182
|
+
}
|
|
2183
|
+
const proposalPath = path6.join(changePath2, "proposal.md");
|
|
2184
|
+
try {
|
|
2185
|
+
const content = await fs3.readFile(proposalPath, "utf-8");
|
|
2186
|
+
const parseResult = parseProposal(content);
|
|
2187
|
+
if (parseResult.success) {
|
|
2188
|
+
info(`\uBCC0\uACBD \uC81C\uC548: ${parseResult.data.title}`);
|
|
2189
|
+
info(`\uC0C1\uD0DC: ${parseResult.data.metadata.status}`);
|
|
2190
|
+
info(`\uC0DD\uC131: ${parseResult.data.metadata.created}`);
|
|
2191
|
+
if (parseResult.data.affectedSpecs.length > 0) {
|
|
2192
|
+
info("\uC601\uD5A5 \uC2A4\uD399:");
|
|
2193
|
+
parseResult.data.affectedSpecs.forEach((spec) => listItem(spec, 1));
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
} catch {
|
|
2197
|
+
error("proposal.md\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2198
|
+
}
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const changesPath = path6.join(sddPath, "changes");
|
|
2202
|
+
await ensureDir(changesPath);
|
|
2203
|
+
const existingIds = [];
|
|
2204
|
+
try {
|
|
2205
|
+
const dirs = await fs3.readdir(changesPath);
|
|
2206
|
+
existingIds.push(...dirs.filter((d) => d.startsWith("CHG-")));
|
|
2207
|
+
} catch {
|
|
2208
|
+
}
|
|
2209
|
+
const newId = generateChangeId(existingIds);
|
|
2210
|
+
const title2 = options.title || "\uC0C8 \uBCC0\uACBD \uC81C\uC548";
|
|
2211
|
+
const affectedSpecs = options.spec ? [options.spec] : [];
|
|
2212
|
+
const changePath = path6.join(changesPath, newId);
|
|
2213
|
+
await ensureDir(changePath);
|
|
2214
|
+
const proposal = generateProposal({
|
|
2215
|
+
id: newId,
|
|
2216
|
+
title: title2,
|
|
2217
|
+
affectedSpecs
|
|
2218
|
+
});
|
|
2219
|
+
await writeFile(path6.join(changePath, "proposal.md"), proposal);
|
|
2220
|
+
const delta = generateDelta({
|
|
2221
|
+
proposalId: newId,
|
|
2222
|
+
title: title2
|
|
2223
|
+
});
|
|
2224
|
+
await writeFile(path6.join(changePath, "delta.md"), delta);
|
|
2225
|
+
success2(`\uBCC0\uACBD \uC81C\uC548\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${newId}`);
|
|
2226
|
+
newline();
|
|
2227
|
+
info("\uC0DD\uC131\uB41C \uD30C\uC77C:");
|
|
2228
|
+
listItem(`.sdd/changes/${newId}/proposal.md`);
|
|
2229
|
+
listItem(`.sdd/changes/${newId}/delta.md`);
|
|
2230
|
+
newline();
|
|
2231
|
+
info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
2232
|
+
listItem("proposal.md\uB97C \uC218\uC815\uD558\uC5EC \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uC791\uC131\uD558\uC138\uC694");
|
|
2233
|
+
listItem("delta.md\uC5D0 ADDED/MODIFIED/REMOVED\uB97C \uC791\uC131\uD558\uC138\uC694");
|
|
2234
|
+
listItem(`\`sdd change apply ${newId}\`\uB85C \uC801\uC6A9\uD558\uC138\uC694`);
|
|
2235
|
+
}
|
|
2236
|
+
async function runApply(id) {
|
|
2237
|
+
const projectRoot = await findSddRoot();
|
|
2238
|
+
if (!projectRoot) {
|
|
2239
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2240
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2241
|
+
}
|
|
2242
|
+
const sddPath = path6.join(projectRoot, ".sdd");
|
|
2243
|
+
const changePath = path6.join(sddPath, "changes", id);
|
|
2244
|
+
if (!await directoryExists(changePath)) {
|
|
2245
|
+
error(`\uBCC0\uACBD\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${id}`);
|
|
2246
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2247
|
+
}
|
|
2248
|
+
const proposalPath = path6.join(changePath, "proposal.md");
|
|
2249
|
+
try {
|
|
2250
|
+
const content = await fs3.readFile(proposalPath, "utf-8");
|
|
2251
|
+
const updateResult = updateProposalStatus(content, "applied");
|
|
2252
|
+
if (updateResult.success) {
|
|
2253
|
+
await fs3.writeFile(proposalPath, updateResult.data);
|
|
2254
|
+
}
|
|
2255
|
+
} catch {
|
|
2256
|
+
error("proposal.md\uB97C \uC5C5\uB370\uC774\uD2B8\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2257
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2258
|
+
}
|
|
2259
|
+
success2(`\uBCC0\uACBD\uC774 \uC801\uC6A9 \uC0C1\uD0DC\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${id}`);
|
|
2260
|
+
newline();
|
|
2261
|
+
info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
2262
|
+
listItem("delta.md\uB97C \uCC38\uC870\uD558\uC5EC \uC2A4\uD399\uC744 \uC218\uC815\uD558\uC138\uC694");
|
|
2263
|
+
listItem("\uAD6C\uD604\uC774 \uC644\uB8CC\uB418\uBA74 `sdd change archive ${id}`\uB97C \uC2E4\uD589\uD558\uC138\uC694");
|
|
2264
|
+
}
|
|
2265
|
+
async function runArchive(id) {
|
|
2266
|
+
const projectRoot = await findSddRoot();
|
|
2267
|
+
if (!projectRoot) {
|
|
2268
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
2269
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2270
|
+
}
|
|
2271
|
+
const sddPath = path6.join(projectRoot, ".sdd");
|
|
2272
|
+
const result = await archiveChange(sddPath, id);
|
|
2273
|
+
if (!result.success) {
|
|
2274
|
+
error(result.error.message);
|
|
2275
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2276
|
+
}
|
|
2277
|
+
success2(`\uBCC0\uACBD\uC774 \uC544\uCE74\uC774\uBE0C\uB418\uC5C8\uC2B5\uB2C8\uB2E4: ${id}`);
|
|
2278
|
+
info(`\uC704\uCE58: ${result.data.archiveDir}`);
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
// src/cli/commands/impact.ts
|
|
2282
|
+
import path9 from "path";
|
|
2283
|
+
|
|
2284
|
+
// src/core/impact/schemas.ts
|
|
2285
|
+
import { z as z5 } from "zod";
|
|
2286
|
+
var DependencyTypeSchema = z5.enum([
|
|
2287
|
+
"explicit",
|
|
2288
|
+
// frontmatter depends 필드
|
|
2289
|
+
"reference",
|
|
2290
|
+
// 문서 내 참조
|
|
2291
|
+
"data",
|
|
2292
|
+
// 데이터 모델 공유
|
|
2293
|
+
"api",
|
|
2294
|
+
// API 의존
|
|
2295
|
+
"component"
|
|
2296
|
+
// 컴포넌트 공유
|
|
2297
|
+
]);
|
|
2298
|
+
var ImpactLevelSchema2 = z5.enum(["low", "medium", "high"]);
|
|
2299
|
+
var RISK_WEIGHTS = {
|
|
2300
|
+
directDependency: 2,
|
|
2301
|
+
// 직접 의존
|
|
2302
|
+
indirectDependency: 1,
|
|
2303
|
+
// 간접 의존
|
|
2304
|
+
apiChange: 3,
|
|
2305
|
+
// API 변경
|
|
2306
|
+
dataModelChange: 2,
|
|
2307
|
+
// 데이터 모델 변경
|
|
2308
|
+
corePrinciple: 2
|
|
2309
|
+
// 핵심 원칙 관련
|
|
2310
|
+
};
|
|
2311
|
+
function getImpactLevel(score) {
|
|
2312
|
+
if (score <= 3) return "low";
|
|
2313
|
+
if (score <= 6) return "medium";
|
|
2314
|
+
return "high";
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
// src/core/impact/graph.ts
|
|
2318
|
+
import { promises as fs4 } from "fs";
|
|
2319
|
+
import path7 from "path";
|
|
2320
|
+
import matter4 from "gray-matter";
|
|
2321
|
+
async function buildDependencyGraph(specsPath) {
|
|
2322
|
+
try {
|
|
2323
|
+
const graph = {
|
|
2324
|
+
nodes: /* @__PURE__ */ new Map(),
|
|
2325
|
+
edges: []
|
|
2326
|
+
};
|
|
2327
|
+
if (!await directoryExists(specsPath)) {
|
|
2328
|
+
return success(graph);
|
|
2329
|
+
}
|
|
2330
|
+
const specFiles = await collectSpecFiles(specsPath);
|
|
2331
|
+
for (const filePath of specFiles) {
|
|
2332
|
+
const content = await fs4.readFile(filePath, "utf-8");
|
|
2333
|
+
const relativePath = path7.relative(specsPath, filePath);
|
|
2334
|
+
const specId = getSpecId(relativePath);
|
|
2335
|
+
const node = {
|
|
2336
|
+
id: specId,
|
|
2337
|
+
path: relativePath,
|
|
2338
|
+
title: extractTitle(content),
|
|
2339
|
+
dependsOn: [],
|
|
2340
|
+
dependedBy: []
|
|
2341
|
+
};
|
|
2342
|
+
const { data: frontmatter } = matter4(content);
|
|
2343
|
+
if (frontmatter.depends) {
|
|
2344
|
+
const explicitDeps = Array.isArray(frontmatter.depends) ? frontmatter.depends : [frontmatter.depends];
|
|
2345
|
+
for (const dep of explicitDeps) {
|
|
2346
|
+
if (dep && dep !== "null") {
|
|
2347
|
+
node.dependsOn.push(dep);
|
|
2348
|
+
graph.edges.push({
|
|
2349
|
+
from: specId,
|
|
2350
|
+
to: dep,
|
|
2351
|
+
type: "explicit",
|
|
2352
|
+
description: "frontmatter depends \uD544\uB4DC"
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
const references = extractReferences(content, specFiles.map((f) => getSpecId(path7.relative(specsPath, f))));
|
|
2358
|
+
for (const ref of references) {
|
|
2359
|
+
if (ref !== specId && !node.dependsOn.includes(ref)) {
|
|
2360
|
+
node.dependsOn.push(ref);
|
|
2361
|
+
graph.edges.push({
|
|
2362
|
+
from: specId,
|
|
2363
|
+
to: ref,
|
|
2364
|
+
type: "reference",
|
|
2365
|
+
description: "\uBB38\uC11C \uB0B4 \uCC38\uC870"
|
|
2366
|
+
});
|
|
2367
|
+
}
|
|
2368
|
+
}
|
|
2369
|
+
graph.nodes.set(specId, node);
|
|
2370
|
+
}
|
|
2371
|
+
for (const edge of graph.edges) {
|
|
2372
|
+
const targetNode = graph.nodes.get(edge.to);
|
|
2373
|
+
if (targetNode && !targetNode.dependedBy.includes(edge.from)) {
|
|
2374
|
+
targetNode.dependedBy.push(edge.from);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
return success(graph);
|
|
2378
|
+
} catch (error2) {
|
|
2379
|
+
return failure(
|
|
2380
|
+
new ChangeError(
|
|
2381
|
+
`\uC758\uC874\uC131 \uADF8\uB798\uD504 \uAD6C\uCD95 \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2382
|
+
)
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
async function collectSpecFiles(dirPath) {
|
|
2387
|
+
const files = [];
|
|
2388
|
+
const entries = await fs4.readdir(dirPath, { withFileTypes: true });
|
|
2389
|
+
for (const entry of entries) {
|
|
2390
|
+
const fullPath = path7.join(dirPath, entry.name);
|
|
2391
|
+
if (entry.isDirectory()) {
|
|
2392
|
+
files.push(...await collectSpecFiles(fullPath));
|
|
2393
|
+
} else if (entry.name.endsWith(".md") && entry.name !== "AGENTS.md") {
|
|
2394
|
+
files.push(fullPath);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
return files;
|
|
2398
|
+
}
|
|
2399
|
+
function getSpecId(relativePath) {
|
|
2400
|
+
return relativePath.replace(/\\/g, "/").replace(/\.md$/, "").replace(/\/spec$/, "");
|
|
2401
|
+
}
|
|
2402
|
+
function extractTitle(content) {
|
|
2403
|
+
const { content: body } = matter4(content);
|
|
2404
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
2405
|
+
return titleMatch?.[1]?.trim();
|
|
2406
|
+
}
|
|
2407
|
+
function extractReferences(content, allSpecIds) {
|
|
2408
|
+
const references = [];
|
|
2409
|
+
for (const specId of allSpecIds) {
|
|
2410
|
+
const patterns = [
|
|
2411
|
+
new RegExp(`\\[.*?\\]\\(.*?${escapeRegex(specId)}.*?\\)`, "gi"),
|
|
2412
|
+
// 마크다운 링크
|
|
2413
|
+
new RegExp(`specs/${escapeRegex(specId)}`, "gi"),
|
|
2414
|
+
// specs/ 경로
|
|
2415
|
+
new RegExp(`\`${escapeRegex(specId)}\``, "gi")
|
|
2416
|
+
// 백틱 내 참조
|
|
2417
|
+
];
|
|
2418
|
+
for (const pattern of patterns) {
|
|
2419
|
+
if (pattern.test(content) && !references.includes(specId)) {
|
|
2420
|
+
references.push(specId);
|
|
2421
|
+
break;
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
return references;
|
|
2426
|
+
}
|
|
2427
|
+
function escapeRegex(str) {
|
|
2428
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2429
|
+
}
|
|
2430
|
+
function generateMermaidGraph(graph, targetSpec) {
|
|
2431
|
+
let mermaid = "graph LR\n";
|
|
2432
|
+
for (const [id, node] of graph.nodes) {
|
|
2433
|
+
const label = node.title || id;
|
|
2434
|
+
const style = targetSpec === id ? "fill:#ff9" : "";
|
|
2435
|
+
mermaid += ` ${sanitizeId(id)}["${label}"]
|
|
2436
|
+
`;
|
|
2437
|
+
if (style) {
|
|
2438
|
+
mermaid += ` style ${sanitizeId(id)} ${style}
|
|
2439
|
+
`;
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
for (const edge of graph.edges) {
|
|
2443
|
+
const arrowStyle = edge.type === "explicit" ? "-->" : "-..->";
|
|
2444
|
+
mermaid += ` ${sanitizeId(edge.from)} ${arrowStyle} ${sanitizeId(edge.to)}
|
|
2445
|
+
`;
|
|
2446
|
+
}
|
|
2447
|
+
return mermaid;
|
|
2448
|
+
}
|
|
2449
|
+
function sanitizeId(id) {
|
|
2450
|
+
return id.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// src/core/impact/analyzer.ts
|
|
2454
|
+
import path8 from "path";
|
|
2455
|
+
async function analyzeImpact(sddPath, targetSpec) {
|
|
2456
|
+
try {
|
|
2457
|
+
const specsPath = path8.join(sddPath, "specs");
|
|
2458
|
+
if (!await directoryExists(specsPath)) {
|
|
2459
|
+
return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
|
|
2460
|
+
}
|
|
2461
|
+
const graphResult = await buildDependencyGraph(specsPath);
|
|
2462
|
+
if (!graphResult.success) {
|
|
2463
|
+
return failure(graphResult.error);
|
|
2464
|
+
}
|
|
2465
|
+
const graph = graphResult.data;
|
|
2466
|
+
const targetNode = graph.nodes.get(targetSpec);
|
|
2467
|
+
if (!targetNode) {
|
|
2468
|
+
return failure(new ChangeError(`\uC2A4\uD399\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${targetSpec}`));
|
|
2469
|
+
}
|
|
2470
|
+
const dependsOn = targetNode.dependsOn.map((depId) => {
|
|
2471
|
+
const depNode = graph.nodes.get(depId);
|
|
2472
|
+
const edge = graph.edges.find((e) => e.from === targetSpec && e.to === depId);
|
|
2473
|
+
return {
|
|
2474
|
+
id: depId,
|
|
2475
|
+
path: depNode?.path || depId,
|
|
2476
|
+
title: depNode?.title,
|
|
2477
|
+
level: "low",
|
|
2478
|
+
type: edge?.type || "reference",
|
|
2479
|
+
reason: edge?.description || "\uC758\uC874"
|
|
2480
|
+
};
|
|
2481
|
+
});
|
|
2482
|
+
const affectedBy = targetNode.dependedBy.map((depId) => {
|
|
2483
|
+
const depNode = graph.nodes.get(depId);
|
|
2484
|
+
const edge = graph.edges.find((e) => e.from === depId && e.to === targetSpec);
|
|
2485
|
+
const level = determineImpactLevel(edge?.type);
|
|
2486
|
+
return {
|
|
2487
|
+
id: depId,
|
|
2488
|
+
path: depNode?.path || depId,
|
|
2489
|
+
title: depNode?.title,
|
|
2490
|
+
level,
|
|
2491
|
+
type: edge?.type || "reference",
|
|
2492
|
+
reason: edge?.description || "\uC774 \uC2A4\uD399\uC5D0 \uC758\uC874\uD568"
|
|
2493
|
+
};
|
|
2494
|
+
});
|
|
2495
|
+
const riskScore = calculateRiskScore(dependsOn, affectedBy);
|
|
2496
|
+
const riskLevel = getImpactLevel(riskScore);
|
|
2497
|
+
const summary = generateSummary(targetSpec, dependsOn, affectedBy, riskScore);
|
|
2498
|
+
const recommendations = generateRecommendations(affectedBy, riskLevel);
|
|
2499
|
+
return success({
|
|
2500
|
+
targetSpec,
|
|
2501
|
+
dependsOn,
|
|
2502
|
+
affectedBy,
|
|
2503
|
+
riskScore,
|
|
2504
|
+
riskLevel,
|
|
2505
|
+
summary,
|
|
2506
|
+
recommendations
|
|
2507
|
+
});
|
|
2508
|
+
} catch (error2) {
|
|
2509
|
+
return failure(
|
|
2510
|
+
new ChangeError(
|
|
2511
|
+
`\uC601\uD5A5\uB3C4 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
|
|
2512
|
+
)
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function determineImpactLevel(type) {
|
|
2517
|
+
switch (type) {
|
|
2518
|
+
case "explicit":
|
|
2519
|
+
case "api":
|
|
2520
|
+
return "high";
|
|
2521
|
+
case "data":
|
|
2522
|
+
return "medium";
|
|
2523
|
+
default:
|
|
2524
|
+
return "low";
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
function calculateRiskScore(dependsOn, affectedBy) {
|
|
2528
|
+
let score = 0;
|
|
2529
|
+
const highImpactCount = affectedBy.filter((s) => s.level === "high").length;
|
|
2530
|
+
const mediumImpactCount = affectedBy.filter((s) => s.level === "medium").length;
|
|
2531
|
+
const lowImpactCount = affectedBy.filter((s) => s.level === "low").length;
|
|
2532
|
+
score += highImpactCount * RISK_WEIGHTS.directDependency;
|
|
2533
|
+
score += mediumImpactCount * RISK_WEIGHTS.indirectDependency;
|
|
2534
|
+
score += lowImpactCount * 0.5;
|
|
2535
|
+
if (affectedBy.some((s) => s.type === "api")) {
|
|
2536
|
+
score += RISK_WEIGHTS.apiChange;
|
|
2537
|
+
}
|
|
2538
|
+
if (affectedBy.some((s) => s.type === "data")) {
|
|
2539
|
+
score += RISK_WEIGHTS.dataModelChange;
|
|
2540
|
+
}
|
|
2541
|
+
return Math.min(10, Math.max(1, Math.round(score)));
|
|
2542
|
+
}
|
|
2543
|
+
function generateSummary(targetSpec, dependsOn, affectedBy, riskScore) {
|
|
2544
|
+
const parts = [];
|
|
2545
|
+
parts.push(`'${targetSpec}' \uC2A4\uD399 \uBCC0\uACBD \uC2DC:`);
|
|
2546
|
+
if (dependsOn.length > 0) {
|
|
2547
|
+
parts.push(`- ${dependsOn.length}\uAC1C \uC2A4\uD399\uC5D0 \uC758\uC874\uD568`);
|
|
2548
|
+
}
|
|
2549
|
+
if (affectedBy.length > 0) {
|
|
2550
|
+
parts.push(`- ${affectedBy.length}\uAC1C \uC2A4\uD399\uC5D0 \uC601\uD5A5\uC744 \uC90C`);
|
|
2551
|
+
const highCount = affectedBy.filter((s) => s.level === "high").length;
|
|
2552
|
+
if (highCount > 0) {
|
|
2553
|
+
parts.push(` - \uB192\uC740 \uC601\uD5A5: ${highCount}\uAC1C`);
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
parts.push(`- \uB9AC\uC2A4\uD06C \uC810\uC218: ${riskScore}/10`);
|
|
2557
|
+
return parts.join("\n");
|
|
2558
|
+
}
|
|
2559
|
+
function generateRecommendations(affectedBy, riskLevel) {
|
|
2560
|
+
const recommendations = [];
|
|
2561
|
+
if (riskLevel === "high") {
|
|
2562
|
+
recommendations.push("\uBCC0\uACBD \uC804 \uC601\uD5A5 \uBC1B\uB294 \uBAA8\uB4E0 \uC2A4\uD399\uC744 \uAC80\uD1A0\uD558\uC138\uC694.");
|
|
2563
|
+
recommendations.push("\uAD00\uB828 \uD300\uACFC \uBCC0\uACBD \uC0AC\uD56D\uC744 \uACF5\uC720\uD558\uC138\uC694.");
|
|
2564
|
+
recommendations.push("\uB2E8\uACC4\uC801 \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uACE0\uB824\uD558\uC138\uC694.");
|
|
2565
|
+
} else if (riskLevel === "medium") {
|
|
2566
|
+
recommendations.push("\uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399\uC758 \uD14C\uC2A4\uD2B8\uB97C \uD655\uC778\uD558\uC138\uC694.");
|
|
2567
|
+
recommendations.push("\uBCC0\uACBD \uD6C4 \uC601\uD5A5 \uC2A4\uD399 \uAC80\uC99D\uC744 \uC218\uD589\uD558\uC138\uC694.");
|
|
2568
|
+
} else {
|
|
2569
|
+
recommendations.push("\uD45C\uC900 \uBCC0\uACBD \uD504\uB85C\uC138\uC2A4\uB97C \uB530\uB974\uC138\uC694.");
|
|
2570
|
+
}
|
|
2571
|
+
const hasApiDep = affectedBy.some((s) => s.type === "api");
|
|
2572
|
+
if (hasApiDep) {
|
|
2573
|
+
recommendations.push("API \uBCC0\uACBD \uC2DC \uBC84\uC804 \uAD00\uB9AC\uB97C \uACE0\uB824\uD558\uC138\uC694.");
|
|
2574
|
+
}
|
|
2575
|
+
const hasDataDep = affectedBy.some((s) => s.type === "data");
|
|
2576
|
+
if (hasDataDep) {
|
|
2577
|
+
recommendations.push("\uB370\uC774\uD130 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uACC4\uD68D\uC744 \uC218\uB9BD\uD558\uC138\uC694.");
|
|
2578
|
+
}
|
|
2579
|
+
return recommendations;
|
|
2580
|
+
}
|
|
2581
|
+
function formatImpactResult(result) {
|
|
2582
|
+
const lines = [];
|
|
2583
|
+
lines.push(`\u{1F4CA} \uC601\uD5A5\uB3C4 \uBD84\uC11D: ${result.targetSpec}`);
|
|
2584
|
+
lines.push("");
|
|
2585
|
+
if (result.dependsOn.length > 0) {
|
|
2586
|
+
lines.push("\u{1F517} \uC758\uC874\uD558\uB294 \uC2A4\uD399 (\uC774 \uAE30\uB2A5\uC774 \uC0AC\uC6A9\uD558\uB294):");
|
|
2587
|
+
for (const dep of result.dependsOn) {
|
|
2588
|
+
lines.push(` \u2514\u2500 ${dep.id} (${dep.type})`);
|
|
2589
|
+
}
|
|
2590
|
+
lines.push("");
|
|
2591
|
+
}
|
|
2592
|
+
if (result.affectedBy.length > 0) {
|
|
2593
|
+
lines.push("\u26A0\uFE0F \uC601\uD5A5 \uBC1B\uB294 \uC2A4\uD399 (\uC774 \uAE30\uB2A5\uC744 \uC0AC\uC6A9\uD558\uB294):");
|
|
2594
|
+
for (const affected of result.affectedBy) {
|
|
2595
|
+
const icon = affected.level === "high" ? "\u{1F534}" : affected.level === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2596
|
+
lines.push(` \u251C\u2500 ${icon} ${affected.id} (${affected.type})`);
|
|
2597
|
+
}
|
|
2598
|
+
lines.push("");
|
|
2599
|
+
}
|
|
2600
|
+
const riskIcon = result.riskLevel === "high" ? "\u{1F534}" : result.riskLevel === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
2601
|
+
lines.push(`\u{1F4C8} \uB9AC\uC2A4\uD06C \uC810\uC218: ${result.riskScore}/10 ${riskIcon}`);
|
|
2602
|
+
lines.push("");
|
|
2603
|
+
if (result.recommendations.length > 0) {
|
|
2604
|
+
lines.push("\u{1F4A1} \uAD8C\uC7A5\uC0AC\uD56D:");
|
|
2605
|
+
for (const rec of result.recommendations) {
|
|
2606
|
+
lines.push(` - ${rec}`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
return lines.join("\n");
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
// src/cli/commands/impact.ts
|
|
2613
|
+
function registerImpactCommand(program2) {
|
|
2614
|
+
program2.command("impact [feature]").description("\uC2A4\uD399 \uBCC0\uACBD\uC758 \uC601\uD5A5\uB3C4\uB97C \uBD84\uC11D\uD569\uB2C8\uB2E4").option("-g, --graph", "\uC758\uC874\uC131 \uADF8\uB798\uD504 \uCD9C\uB825 (Mermaid)").option("-r, --reverse", "\uC5ED\uBC29\uD5A5 \uC601\uD5A5\uB3C4 \uBD84\uC11D").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").action(async (feature, options) => {
|
|
2615
|
+
try {
|
|
2616
|
+
await runImpact(feature, options);
|
|
2617
|
+
} catch (error2) {
|
|
2618
|
+
error(error2 instanceof Error ? error2.message : String(error2));
|
|
2619
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
}
|
|
2623
|
+
async function runImpact(feature, options) {
|
|
2624
|
+
const projectRoot = await findSddRoot();
|
|
2625
|
+
if (!projectRoot) {
|
|
2626
|
+
error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
2627
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2628
|
+
}
|
|
2629
|
+
const sddPath = path9.join(projectRoot, ".sdd");
|
|
2630
|
+
if (options.graph) {
|
|
2631
|
+
const graphResult = await buildDependencyGraph(path9.join(sddPath, "specs"));
|
|
2632
|
+
if (!graphResult.success) {
|
|
2633
|
+
error(graphResult.error.message);
|
|
2634
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2635
|
+
}
|
|
2636
|
+
const mermaid = generateMermaidGraph(graphResult.data, feature);
|
|
2637
|
+
if (options.json) {
|
|
2638
|
+
console.log(JSON.stringify({
|
|
2639
|
+
format: "mermaid",
|
|
2640
|
+
content: mermaid,
|
|
2641
|
+
nodes: Array.from(graphResult.data.nodes.values()),
|
|
2642
|
+
edges: graphResult.data.edges
|
|
2643
|
+
}, null, 2));
|
|
2644
|
+
} else {
|
|
2645
|
+
info("\uC758\uC874\uC131 \uADF8\uB798\uD504 (Mermaid):");
|
|
2646
|
+
newline();
|
|
2647
|
+
console.log("```mermaid");
|
|
2648
|
+
console.log(mermaid);
|
|
2649
|
+
console.log("```");
|
|
2650
|
+
}
|
|
2651
|
+
return;
|
|
2652
|
+
}
|
|
2653
|
+
if (!feature) {
|
|
2654
|
+
error("\uBD84\uC11D\uD560 \uAE30\uB2A5\uC744 \uC9C0\uC815\uD558\uC138\uC694.");
|
|
2655
|
+
info("\uC0AC\uC6A9\uBC95: sdd impact <feature>");
|
|
2656
|
+
info("\uC608\uC2DC: sdd impact auth");
|
|
2657
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2658
|
+
}
|
|
2659
|
+
const result = await analyzeImpact(sddPath, feature);
|
|
2660
|
+
if (!result.success) {
|
|
2661
|
+
error(result.error.message);
|
|
2662
|
+
process.exit(ExitCode.GENERAL_ERROR);
|
|
2663
|
+
}
|
|
2664
|
+
if (options.json) {
|
|
2665
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
2666
|
+
} else {
|
|
2667
|
+
console.log(formatImpactResult(result.data));
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
// src/cli/commands/new.ts
|
|
2672
|
+
import path10 from "path";
|
|
2673
|
+
import { promises as fs5 } from "fs";
|
|
2674
|
+
|
|
2675
|
+
// src/core/new/schemas.ts
|
|
2676
|
+
import { z as z6 } from "zod";
|
|
2677
|
+
var FeatureStatusSchema = z6.enum([
|
|
2678
|
+
"draft",
|
|
2679
|
+
// 초안 작성 중
|
|
2680
|
+
"specified",
|
|
2681
|
+
// 명세 완료
|
|
2682
|
+
"planned",
|
|
2683
|
+
// 계획 완료
|
|
2684
|
+
"tasked",
|
|
2685
|
+
// 작업 분해 완료
|
|
2686
|
+
"implementing",
|
|
2687
|
+
// 구현 중
|
|
2688
|
+
"completed"
|
|
2689
|
+
// 완료
|
|
2690
|
+
]);
|
|
2691
|
+
var TaskStatusSchema = z6.enum([
|
|
2692
|
+
"pending",
|
|
2693
|
+
// 대기 중
|
|
2694
|
+
"in_progress",
|
|
2695
|
+
// 진행 중
|
|
2696
|
+
"completed",
|
|
2697
|
+
// 완료
|
|
2698
|
+
"blocked"
|
|
2699
|
+
// 차단됨
|
|
2700
|
+
]);
|
|
2701
|
+
var TaskPrioritySchema = z6.enum([
|
|
2702
|
+
"high",
|
|
2703
|
+
// 높음
|
|
2704
|
+
"medium",
|
|
2705
|
+
// 중간
|
|
2706
|
+
"low"
|
|
2707
|
+
// 낮음
|
|
2708
|
+
]);
|
|
2709
|
+
var FeatureMetadataSchema = z6.object({
|
|
2710
|
+
id: z6.string(),
|
|
2711
|
+
title: z6.string(),
|
|
2712
|
+
status: FeatureStatusSchema,
|
|
2713
|
+
created: z6.string(),
|
|
2714
|
+
updated: z6.string().optional(),
|
|
2715
|
+
branch: z6.string().optional(),
|
|
2716
|
+
depends: z6.array(z6.string()).nullable().default(null)
|
|
2717
|
+
});
|
|
2718
|
+
var TaskItemSchema = z6.object({
|
|
2719
|
+
id: z6.string(),
|
|
2720
|
+
title: z6.string(),
|
|
2721
|
+
description: z6.string().optional(),
|
|
2722
|
+
status: TaskStatusSchema,
|
|
2723
|
+
priority: TaskPrioritySchema,
|
|
2724
|
+
assignee: z6.string().optional(),
|
|
2725
|
+
files: z6.array(z6.string()).optional(),
|
|
2726
|
+
dependencies: z6.array(z6.string()).optional()
|
|
2727
|
+
});
|
|
2728
|
+
var PlanSchema = z6.object({
|
|
2729
|
+
overview: z6.string(),
|
|
2730
|
+
techDecisions: z6.array(z6.object({
|
|
2731
|
+
decision: z6.string(),
|
|
2732
|
+
rationale: z6.string(),
|
|
2733
|
+
alternatives: z6.array(z6.string()).optional()
|
|
2734
|
+
})),
|
|
2735
|
+
phases: z6.array(z6.object({
|
|
2736
|
+
name: z6.string(),
|
|
2737
|
+
description: z6.string(),
|
|
2738
|
+
deliverables: z6.array(z6.string())
|
|
2739
|
+
})),
|
|
2740
|
+
risks: z6.array(z6.object({
|
|
2741
|
+
risk: z6.string(),
|
|
2742
|
+
mitigation: z6.string(),
|
|
2743
|
+
impact: z6.enum(["high", "medium", "low"])
|
|
2744
|
+
})).optional(),
|
|
2745
|
+
testingStrategy: z6.string().optional()
|
|
2746
|
+
});
|
|
2747
|
+
function generateFeatureId(name) {
|
|
2748
|
+
return name.toLowerCase().replace(/[^a-z0-9가-힣]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 50);
|
|
2749
|
+
}
|
|
2750
|
+
function generateTaskId(featureId, index) {
|
|
2751
|
+
return `${featureId}-task-${String(index).padStart(3, "0")}`;
|
|
2752
|
+
}
|
|
2753
|
+
function generateBranchName(featureId) {
|
|
2754
|
+
return `feature/${featureId}`;
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
// src/core/new/spec-generator.ts
|
|
2758
|
+
function generateSpec(options) {
|
|
2759
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2760
|
+
const status = options.status || "draft";
|
|
2761
|
+
const depends = options.depends?.length ? `
|
|
2762
|
+
- ${options.depends.join("\n - ")}` : "null";
|
|
2763
|
+
let content = `---
|
|
2764
|
+
id: ${options.id}
|
|
2765
|
+
title: "${options.title}"
|
|
2766
|
+
status: ${status}
|
|
2767
|
+
created: ${today}
|
|
2768
|
+
depends: ${depends}
|
|
2769
|
+
---
|
|
2770
|
+
|
|
2771
|
+
# ${options.title}
|
|
2772
|
+
|
|
2773
|
+
> ${options.description}
|
|
2774
|
+
|
|
2775
|
+
---
|
|
2776
|
+
|
|
2777
|
+
## \uAC1C\uC694
|
|
2778
|
+
|
|
2779
|
+
${options.description}
|
|
2780
|
+
|
|
2781
|
+
---
|
|
2782
|
+
|
|
2783
|
+
## \uC694\uAD6C\uC0AC\uD56D
|
|
2784
|
+
|
|
2785
|
+
`;
|
|
2786
|
+
if (options.requirements?.length) {
|
|
2787
|
+
options.requirements.forEach((req, index) => {
|
|
2788
|
+
content += `### REQ-${String(index + 1).padStart(2, "0")}: ${req.split(":")[0] || req}
|
|
2789
|
+
|
|
2790
|
+
${req}
|
|
2791
|
+
|
|
2792
|
+
`;
|
|
2793
|
+
});
|
|
2794
|
+
} else {
|
|
2795
|
+
content += `### REQ-01: [\uC694\uAD6C\uC0AC\uD56D \uC81C\uBAA9]
|
|
2796
|
+
|
|
2797
|
+
[\uC694\uAD6C\uC0AC\uD56D \uC0C1\uC138 \uC124\uBA85]
|
|
2798
|
+
- \uC2DC\uC2A4\uD15C\uC740 [\uAE30\uB2A5]\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHALL)
|
|
2799
|
+
|
|
2800
|
+
`;
|
|
2801
|
+
}
|
|
2802
|
+
content += `---
|
|
2803
|
+
|
|
2804
|
+
## \uC2DC\uB098\uB9AC\uC624
|
|
2805
|
+
|
|
2806
|
+
`;
|
|
2807
|
+
if (options.scenarios?.length) {
|
|
2808
|
+
options.scenarios.forEach((scenario, index) => {
|
|
2809
|
+
content += `### Scenario ${index + 1}: ${scenario.name}
|
|
2810
|
+
|
|
2811
|
+
- **GIVEN** ${scenario.given}
|
|
2812
|
+
- **WHEN** ${scenario.when}
|
|
2813
|
+
- **THEN** ${scenario.then}
|
|
2814
|
+
|
|
2815
|
+
`;
|
|
2816
|
+
});
|
|
2817
|
+
} else {
|
|
2818
|
+
content += `### Scenario 1: [\uC2DC\uB098\uB9AC\uC624\uBA85]
|
|
2819
|
+
|
|
2820
|
+
- **GIVEN** [\uC804\uC81C \uC870\uAC74]
|
|
2821
|
+
- **WHEN** [\uD589\uB3D9/\uD2B8\uB9AC\uAC70]
|
|
2822
|
+
- **THEN** [\uC608\uC0C1 \uACB0\uACFC]
|
|
2823
|
+
|
|
2824
|
+
`;
|
|
2825
|
+
}
|
|
2826
|
+
content += `---
|
|
2827
|
+
|
|
2828
|
+
## \uBE44\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D
|
|
2829
|
+
|
|
2830
|
+
### \uC131\uB2A5
|
|
2831
|
+
|
|
2832
|
+
- \uC751\uB2F5 \uC2DC\uAC04: [N]ms \uC774\uB0B4 (SHOULD)
|
|
2833
|
+
|
|
2834
|
+
### \uBCF4\uC548
|
|
2835
|
+
|
|
2836
|
+
- [\uBCF4\uC548 \uC694\uAD6C\uC0AC\uD56D] (SHALL)
|
|
2837
|
+
|
|
2838
|
+
---
|
|
2839
|
+
|
|
2840
|
+
## \uC81C\uC57D\uC0AC\uD56D
|
|
2841
|
+
|
|
2842
|
+
- [\uAE30\uC220\uC801 \uC81C\uC57D\uC0AC\uD56D]
|
|
2843
|
+
- [\uBE44\uC988\uB2C8\uC2A4 \uC81C\uC57D\uC0AC\uD56D]
|
|
2844
|
+
|
|
2845
|
+
---
|
|
2846
|
+
|
|
2847
|
+
## \uC6A9\uC5B4 \uC815\uC758
|
|
2848
|
+
|
|
2849
|
+
| \uC6A9\uC5B4 | \uC815\uC758 |
|
|
2850
|
+
|------|------|
|
|
2851
|
+
| [\uC6A9\uC5B41] | [\uC815\uC7581] |
|
|
2852
|
+
`;
|
|
2853
|
+
return content;
|
|
2854
|
+
}
|
|
2855
|
+
function parseSpecMetadata(content) {
|
|
2856
|
+
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
2857
|
+
if (!frontmatterMatch) {
|
|
2858
|
+
return null;
|
|
2859
|
+
}
|
|
2860
|
+
const frontmatter = frontmatterMatch[1];
|
|
2861
|
+
const lines = frontmatter.split("\n");
|
|
2862
|
+
const metadata = {};
|
|
2863
|
+
let currentKey = "";
|
|
2864
|
+
let isArray = false;
|
|
2865
|
+
const arrayItems = [];
|
|
2866
|
+
for (const line of lines) {
|
|
2867
|
+
if (line.startsWith(" - ")) {
|
|
2868
|
+
if (isArray) {
|
|
2869
|
+
arrayItems.push(line.replace(" - ", "").trim());
|
|
2870
|
+
}
|
|
2871
|
+
} else {
|
|
2872
|
+
if (isArray && currentKey) {
|
|
2873
|
+
metadata[currentKey] = arrayItems.length > 0 ? [...arrayItems] : null;
|
|
2874
|
+
arrayItems.length = 0;
|
|
2875
|
+
isArray = false;
|
|
2876
|
+
}
|
|
2877
|
+
const colonIndex = line.indexOf(":");
|
|
2878
|
+
if (colonIndex > 0) {
|
|
2879
|
+
const key = line.slice(0, colonIndex).trim();
|
|
2880
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
2881
|
+
if (value === "" || value === "|") {
|
|
2882
|
+
currentKey = key;
|
|
2883
|
+
isArray = true;
|
|
2884
|
+
} else if (value === "null") {
|
|
2885
|
+
metadata[key] = null;
|
|
2886
|
+
} else if (value.startsWith('"') && value.endsWith('"')) {
|
|
2887
|
+
metadata[key] = value.slice(1, -1);
|
|
2888
|
+
} else {
|
|
2889
|
+
metadata[key] = value;
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
}
|
|
2894
|
+
if (isArray && currentKey) {
|
|
2895
|
+
metadata[currentKey] = arrayItems.length > 0 ? arrayItems : null;
|
|
2896
|
+
}
|
|
2897
|
+
return {
|
|
2898
|
+
id: metadata.id,
|
|
2899
|
+
title: metadata.title,
|
|
2900
|
+
status: metadata.status,
|
|
2901
|
+
created: metadata.created,
|
|
2902
|
+
updated: metadata.updated,
|
|
2903
|
+
branch: metadata.branch,
|
|
2904
|
+
depends: metadata.depends
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2907
|
+
|
|
2908
|
+
// src/core/new/plan-generator.ts
|
|
2909
|
+
function generatePlan(options) {
|
|
2910
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2911
|
+
let content = `---
|
|
2912
|
+
feature: ${options.featureId}
|
|
2913
|
+
created: ${today}
|
|
2914
|
+
status: draft
|
|
2915
|
+
---
|
|
2916
|
+
|
|
2917
|
+
# \uAD6C\uD604 \uACC4\uD68D: ${options.featureTitle}
|
|
2918
|
+
|
|
2919
|
+
> ${options.overview}
|
|
2920
|
+
|
|
2921
|
+
---
|
|
2922
|
+
|
|
2923
|
+
## \uAC1C\uC694
|
|
2924
|
+
|
|
2925
|
+
${options.overview}
|
|
2926
|
+
|
|
2927
|
+
---
|
|
2928
|
+
|
|
2929
|
+
## \uAE30\uC220 \uACB0\uC815
|
|
2930
|
+
|
|
2931
|
+
`;
|
|
2932
|
+
if (options.techDecisions?.length) {
|
|
2933
|
+
options.techDecisions.forEach((td, index) => {
|
|
2934
|
+
content += `### \uACB0\uC815 ${index + 1}: ${td.decision}
|
|
2935
|
+
|
|
2936
|
+
**\uADFC\uAC70:** ${td.rationale}
|
|
2937
|
+
|
|
2938
|
+
`;
|
|
2939
|
+
if (td.alternatives?.length) {
|
|
2940
|
+
content += `**\uB300\uC548 \uAC80\uD1A0:**
|
|
2941
|
+
${td.alternatives.map((alt) => `- ${alt}`).join("\n")}
|
|
2942
|
+
|
|
2943
|
+
`;
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
} else {
|
|
2947
|
+
content += `### \uACB0\uC815 1: [\uAE30\uC220 \uACB0\uC815 \uC0AC\uD56D]
|
|
2948
|
+
|
|
2949
|
+
**\uADFC\uAC70:** [\uACB0\uC815 \uADFC\uAC70]
|
|
2950
|
+
|
|
2951
|
+
**\uB300\uC548 \uAC80\uD1A0:**
|
|
2952
|
+
- [\uB300\uC548 1]
|
|
2953
|
+
- [\uB300\uC548 2]
|
|
2954
|
+
|
|
2955
|
+
`;
|
|
2956
|
+
}
|
|
2957
|
+
content += `---
|
|
2958
|
+
|
|
2959
|
+
## \uAD6C\uD604 \uB2E8\uACC4
|
|
2960
|
+
|
|
2961
|
+
`;
|
|
2962
|
+
if (options.phases?.length) {
|
|
2963
|
+
options.phases.forEach((phase, index) => {
|
|
2964
|
+
content += `### Phase ${index + 1}: ${phase.name}
|
|
2965
|
+
|
|
2966
|
+
${phase.description}
|
|
2967
|
+
|
|
2968
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
2969
|
+
${phase.deliverables.map((d) => `- [ ] ${d}`).join("\n")}
|
|
2970
|
+
|
|
2971
|
+
`;
|
|
2972
|
+
});
|
|
2973
|
+
} else {
|
|
2974
|
+
content += `### Phase 1: \uAE30\uBC18 \uAD6C\uC870
|
|
2975
|
+
|
|
2976
|
+
[\uAE30\uBC18 \uAD6C\uC870 \uC124\uBA85]
|
|
2977
|
+
|
|
2978
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
2979
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 1]
|
|
2980
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 2]
|
|
2981
|
+
|
|
2982
|
+
### Phase 2: \uD575\uC2EC \uAE30\uB2A5
|
|
2983
|
+
|
|
2984
|
+
[\uD575\uC2EC \uAE30\uB2A5 \uC124\uBA85]
|
|
2985
|
+
|
|
2986
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
2987
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 1]
|
|
2988
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 2]
|
|
2989
|
+
|
|
2990
|
+
### Phase 3: \uD1B5\uD569 \uBC0F \uD14C\uC2A4\uD2B8
|
|
2991
|
+
|
|
2992
|
+
[\uD1B5\uD569 \uBC0F \uD14C\uC2A4\uD2B8 \uC124\uBA85]
|
|
2993
|
+
|
|
2994
|
+
**\uC0B0\uCD9C\uBB3C:**
|
|
2995
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 1]
|
|
2996
|
+
- [ ] [\uC0B0\uCD9C\uBB3C 2]
|
|
2997
|
+
|
|
2998
|
+
`;
|
|
2999
|
+
}
|
|
3000
|
+
content += `---
|
|
3001
|
+
|
|
3002
|
+
## \uB9AC\uC2A4\uD06C \uBD84\uC11D
|
|
3003
|
+
|
|
3004
|
+
`;
|
|
3005
|
+
if (options.risks?.length) {
|
|
3006
|
+
content += `| \uB9AC\uC2A4\uD06C | \uC601\uD5A5\uB3C4 | \uC644\uD654 \uC804\uB7B5 |
|
|
3007
|
+
|--------|--------|----------|
|
|
3008
|
+
`;
|
|
3009
|
+
options.risks.forEach((r) => {
|
|
3010
|
+
const impactIcon = r.impact === "high" ? "\u{1F534}" : r.impact === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
3011
|
+
content += `| ${r.risk} | ${impactIcon} ${r.impact.toUpperCase()} | ${r.mitigation} |
|
|
3012
|
+
`;
|
|
3013
|
+
});
|
|
3014
|
+
content += "\n";
|
|
3015
|
+
} else {
|
|
3016
|
+
content += `| \uB9AC\uC2A4\uD06C | \uC601\uD5A5\uB3C4 | \uC644\uD654 \uC804\uB7B5 |
|
|
3017
|
+
|--------|--------|----------|
|
|
3018
|
+
| [\uB9AC\uC2A4\uD06C 1] | \u{1F7E1} MEDIUM | [\uC644\uD654 \uC804\uB7B5] |
|
|
3019
|
+
|
|
3020
|
+
`;
|
|
3021
|
+
}
|
|
3022
|
+
content += `---
|
|
3023
|
+
|
|
3024
|
+
## \uD14C\uC2A4\uD2B8 \uC804\uB7B5
|
|
3025
|
+
|
|
3026
|
+
`;
|
|
3027
|
+
if (options.testingStrategy) {
|
|
3028
|
+
content += `${options.testingStrategy}
|
|
3029
|
+
|
|
3030
|
+
`;
|
|
3031
|
+
} else {
|
|
3032
|
+
content += `### \uB2E8\uC704 \uD14C\uC2A4\uD2B8
|
|
3033
|
+
|
|
3034
|
+
- \uAC01 \uBAA8\uB4C8\uBCC4 \uB2E8\uC704 \uD14C\uC2A4\uD2B8 \uC791\uC131
|
|
3035
|
+
- \uCEE4\uBC84\uB9AC\uC9C0 \uBAA9\uD45C: 80% \uC774\uC0C1
|
|
3036
|
+
|
|
3037
|
+
### \uD1B5\uD569 \uD14C\uC2A4\uD2B8
|
|
3038
|
+
|
|
3039
|
+
- API \uC5D4\uB4DC\uD3EC\uC778\uD2B8 \uD1B5\uD569 \uD14C\uC2A4\uD2B8
|
|
3040
|
+
- \uC2DC\uB098\uB9AC\uC624 \uAE30\uBC18 \uD14C\uC2A4\uD2B8
|
|
3041
|
+
|
|
3042
|
+
### E2E \uD14C\uC2A4\uD2B8
|
|
3043
|
+
|
|
3044
|
+
- \uC8FC\uC694 \uC0AC\uC6A9\uC790 \uC2DC\uB098\uB9AC\uC624 \uAC80\uC99D
|
|
3045
|
+
|
|
3046
|
+
`;
|
|
3047
|
+
}
|
|
3048
|
+
if (options.constitutionCompliance?.length) {
|
|
3049
|
+
content += `---
|
|
3050
|
+
|
|
3051
|
+
## \uD5CC\uBC95 \uC900\uC218 \uC0AC\uD56D
|
|
3052
|
+
|
|
3053
|
+
${options.constitutionCompliance.map((c) => `- ${c}`).join("\n")}
|
|
3054
|
+
`;
|
|
3055
|
+
}
|
|
3056
|
+
content += `
|
|
3057
|
+
---
|
|
3058
|
+
|
|
3059
|
+
## \uB2E4\uC74C \uB2E8\uACC4
|
|
3060
|
+
|
|
3061
|
+
1. [ ] \uC774 \uACC4\uD68D\uC5D0 \uB300\uD55C \uAC80\uD1A0 \uBC0F \uC2B9\uC778
|
|
3062
|
+
2. [ ] \`/sdd:tasks\` \uBA85\uB839\uC73C\uB85C \uC791\uC5C5 \uBD84\uD574
|
|
3063
|
+
3. [ ] \uAD6C\uD604 \uC2DC\uC791
|
|
3064
|
+
`;
|
|
3065
|
+
return content;
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/core/new/task-generator.ts
|
|
3069
|
+
function generateTasks(options) {
|
|
3070
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
3071
|
+
let content = `---
|
|
3072
|
+
feature: ${options.featureId}
|
|
3073
|
+
created: ${today}
|
|
3074
|
+
total: ${options.tasks.length}
|
|
3075
|
+
completed: 0
|
|
3076
|
+
---
|
|
3077
|
+
|
|
3078
|
+
# \uC791\uC5C5 \uBAA9\uB85D: ${options.featureTitle}
|
|
3079
|
+
|
|
3080
|
+
> \uCD1D ${options.tasks.length}\uAC1C \uC791\uC5C5
|
|
3081
|
+
|
|
3082
|
+
---
|
|
3083
|
+
|
|
3084
|
+
## \uC9C4\uD589 \uC0C1\uD669
|
|
3085
|
+
|
|
3086
|
+
- \uB300\uAE30: ${options.tasks.length}
|
|
3087
|
+
- \uC9C4\uD589 \uC911: 0
|
|
3088
|
+
- \uC644\uB8CC: 0
|
|
3089
|
+
- \uCC28\uB2E8\uB428: 0
|
|
3090
|
+
|
|
3091
|
+
---
|
|
3092
|
+
|
|
3093
|
+
## \uC791\uC5C5 \uBAA9\uB85D
|
|
3094
|
+
|
|
3095
|
+
`;
|
|
3096
|
+
options.tasks.forEach((task, index) => {
|
|
3097
|
+
const taskId = generateTaskId(options.featureId, index + 1);
|
|
3098
|
+
const priority = task.priority || "medium";
|
|
3099
|
+
const priorityIcon = priority === "high" ? "\u{1F534}" : priority === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
3100
|
+
content += `### ${taskId}: ${task.title}
|
|
3101
|
+
|
|
3102
|
+
- **\uC0C1\uD0DC:** \uB300\uAE30
|
|
3103
|
+
- **\uC6B0\uC120\uC21C\uC704:** ${priorityIcon} ${priority.toUpperCase()}
|
|
3104
|
+
`;
|
|
3105
|
+
if (task.description) {
|
|
3106
|
+
content += `- **\uC124\uBA85:** ${task.description}
|
|
3107
|
+
`;
|
|
3108
|
+
}
|
|
3109
|
+
if (task.files?.length) {
|
|
3110
|
+
content += `- **\uAD00\uB828 \uD30C\uC77C:**
|
|
3111
|
+
${task.files.map((f) => ` - \`${f}\``).join("\n")}
|
|
3112
|
+
`;
|
|
3113
|
+
}
|
|
3114
|
+
if (task.dependencies?.length) {
|
|
3115
|
+
content += `- **\uC758\uC874\uC131:** ${task.dependencies.join(", ")}
|
|
3116
|
+
`;
|
|
3117
|
+
}
|
|
3118
|
+
content += "\n";
|
|
3119
|
+
});
|
|
3120
|
+
content += `---
|
|
3121
|
+
|
|
3122
|
+
## \uC644\uB8CC \uC870\uAC74
|
|
3123
|
+
|
|
3124
|
+
\uAC01 \uC791\uC5C5 \uC644\uB8CC \uC2DC:
|
|
3125
|
+
1. [ ] \uCF54\uB4DC \uC791\uC131 \uC644\uB8CC
|
|
3126
|
+
2. [ ] \uD14C\uC2A4\uD2B8 \uC791\uC131 \uBC0F \uD1B5\uACFC
|
|
3127
|
+
3. [ ] \uCF54\uB4DC \uB9AC\uBDF0 \uC644\uB8CC
|
|
3128
|
+
4. [ ] \uBB38\uC11C \uC5C5\uB370\uC774\uD2B8
|
|
3129
|
+
|
|
3130
|
+
---
|
|
3131
|
+
|
|
3132
|
+
## \uB2E4\uC74C \uB2E8\uACC4
|
|
3133
|
+
|
|
3134
|
+
1. \uCCAB \uBC88\uC9F8 \uC791\uC5C5\uBD80\uD130 \uC21C\uCC28\uC801\uC73C\uB85C \uC9C4\uD589
|
|
3135
|
+
2. \uAC01 \uC791\uC5C5 \uC644\uB8CC \uD6C4 \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8
|
|
3136
|
+
3. \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC \uC2DC \`/sdd:archive\` \uC2E4\uD589
|
|
3137
|
+
`;
|
|
3138
|
+
return content;
|
|
3139
|
+
}
|
|
3140
|
+
function parseTasks(content) {
|
|
3141
|
+
const tasks = [];
|
|
3142
|
+
const taskMatches = content.matchAll(/### ([a-z0-9-]+): ([^\n]+)\s*\n([\s\S]*?)(?=\n###|\n---|$)/gi);
|
|
3143
|
+
for (const match of taskMatches) {
|
|
3144
|
+
const id = match[1];
|
|
3145
|
+
const title2 = match[2];
|
|
3146
|
+
const body = match[3];
|
|
3147
|
+
const statusMatch = body.match(/\*\*상태:\*\*\s*(\S+)/);
|
|
3148
|
+
let status = "pending";
|
|
3149
|
+
if (statusMatch) {
|
|
3150
|
+
const statusText = statusMatch[1].toLowerCase();
|
|
3151
|
+
if (statusText.includes("\uC9C4\uD589") || statusText === "in_progress") {
|
|
3152
|
+
status = "in_progress";
|
|
3153
|
+
} else if (statusText.includes("\uC644\uB8CC") || statusText === "completed") {
|
|
3154
|
+
status = "completed";
|
|
3155
|
+
} else if (statusText.includes("\uCC28\uB2E8") || statusText === "blocked") {
|
|
3156
|
+
status = "blocked";
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const priorityMatch = body.match(/\*\*우선순위:\*\*\s*(?:[🔴🟡🟢]\s*)?([A-Za-z]+)/u);
|
|
3160
|
+
let priority = "medium";
|
|
3161
|
+
if (priorityMatch) {
|
|
3162
|
+
const p = priorityMatch[1].toLowerCase();
|
|
3163
|
+
if (p === "high" || p === "\uB192\uC74C") priority = "high";
|
|
3164
|
+
else if (p === "low" || p === "\uB0AE\uC74C") priority = "low";
|
|
3165
|
+
}
|
|
3166
|
+
const descMatch = body.match(/\*\*설명:\*\*\s*([^\n]+)/);
|
|
3167
|
+
const description = descMatch ? descMatch[1] : void 0;
|
|
3168
|
+
const filesMatch = body.match(/\*\*관련 파일:\*\*\s*\n([\s\S]*?)(?=\n-\s*\*\*|\n\n|$)/);
|
|
3169
|
+
const files = filesMatch ? filesMatch[1].split("\n").filter((l) => l.includes("`")).map((l) => l.match(/`([^`]+)`/)?.[1] || "").filter(Boolean) : void 0;
|
|
3170
|
+
const depsMatch = body.match(/\*\*의존성:\*\*\s*([^\n]+)/);
|
|
3171
|
+
const dependencies = depsMatch ? depsMatch[1].split(",").map((d) => d.trim()).filter(Boolean) : void 0;
|
|
3172
|
+
tasks.push({
|
|
3173
|
+
id,
|
|
3174
|
+
title: title2,
|
|
3175
|
+
description,
|
|
3176
|
+
status,
|
|
3177
|
+
priority,
|
|
3178
|
+
files,
|
|
3179
|
+
dependencies
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
return tasks;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
// src/core/new/branch.ts
|
|
3186
|
+
import { exec } from "child_process";
|
|
3187
|
+
import { promisify } from "util";
|
|
3188
|
+
var execAsync = promisify(exec);
|
|
3189
|
+
var BranchError = class extends SddError {
|
|
3190
|
+
constructor(message) {
|
|
3191
|
+
super(ErrorCode.UNKNOWN, message, ExitCode.GENERAL_ERROR);
|
|
3192
|
+
this.name = "BranchError";
|
|
3193
|
+
}
|
|
3194
|
+
};
|
|
3195
|
+
async function isGitRepository(cwd) {
|
|
3196
|
+
try {
|
|
3197
|
+
await execAsync("git rev-parse --git-dir", { cwd });
|
|
3198
|
+
return true;
|
|
3199
|
+
} catch {
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
async function getCurrentBranch(cwd) {
|
|
3204
|
+
try {
|
|
3205
|
+
const { stdout } = await execAsync("git branch --show-current", { cwd });
|
|
3206
|
+
return { success: true, data: stdout.trim() };
|
|
3207
|
+
} catch (error2) {
|
|
3208
|
+
return {
|
|
3209
|
+
success: false,
|
|
3210
|
+
error: new BranchError(`\uD604\uC7AC \uBE0C\uB79C\uCE58\uB97C \uAC00\uC838\uC62C \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${error2}`)
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
}
|
|
3214
|
+
async function branchExists(branchName, cwd) {
|
|
3215
|
+
try {
|
|
3216
|
+
await execAsync(`git show-ref --verify --quiet refs/heads/${branchName}`, { cwd });
|
|
3217
|
+
return true;
|
|
3218
|
+
} catch {
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
}
|
|
3222
|
+
async function createBranch(featureId, options) {
|
|
3223
|
+
const branchName = generateBranchName(featureId);
|
|
3224
|
+
const checkout = options?.checkout ?? true;
|
|
3225
|
+
const cwd = options?.cwd;
|
|
3226
|
+
try {
|
|
3227
|
+
if (!await isGitRepository(cwd)) {
|
|
3228
|
+
return {
|
|
3229
|
+
success: false,
|
|
3230
|
+
error: new BranchError("Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4")
|
|
3231
|
+
};
|
|
3232
|
+
}
|
|
3233
|
+
if (await branchExists(branchName, cwd)) {
|
|
3234
|
+
return {
|
|
3235
|
+
success: false,
|
|
3236
|
+
error: new BranchError(`\uBE0C\uB79C\uCE58 '${branchName}'\uAC00 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4`)
|
|
3237
|
+
};
|
|
3238
|
+
}
|
|
3239
|
+
if (options?.baseBranch) {
|
|
3240
|
+
await execAsync(`git checkout ${options.baseBranch}`, { cwd });
|
|
3241
|
+
}
|
|
3242
|
+
if (checkout) {
|
|
3243
|
+
await execAsync(`git checkout -b ${branchName}`, { cwd });
|
|
3244
|
+
} else {
|
|
3245
|
+
await execAsync(`git branch ${branchName}`, { cwd });
|
|
3246
|
+
}
|
|
3247
|
+
return { success: true, data: branchName };
|
|
3248
|
+
} catch (error2) {
|
|
3249
|
+
return {
|
|
3250
|
+
success: false,
|
|
3251
|
+
error: new BranchError(`\uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC2E4\uD328: ${error2}`)
|
|
3252
|
+
};
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
async function listFeatureBranches(cwd) {
|
|
3256
|
+
try {
|
|
3257
|
+
const { stdout } = await execAsync('git branch --list "feature/*"', { cwd });
|
|
3258
|
+
const branches = stdout.split("\n").map((b) => b.trim().replace(/^\*\s*/, "")).filter(Boolean);
|
|
3259
|
+
return { success: true, data: branches };
|
|
3260
|
+
} catch (error2) {
|
|
3261
|
+
return {
|
|
3262
|
+
success: false,
|
|
3263
|
+
error: new BranchError(`\uBE0C\uB79C\uCE58 \uBAA9\uB85D \uC870\uD68C \uC2E4\uD328: ${error2}`)
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
|
|
3268
|
+
// src/core/new/checklist.ts
|
|
3269
|
+
var DEFAULT_CHECKLISTS = {
|
|
3270
|
+
"pre-spec": [
|
|
3271
|
+
"\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D\uC774 \uBA85\uD655\uD788 \uC815\uC758\uB428",
|
|
3272
|
+
"\uC0AC\uC6A9\uC790 \uC2A4\uD1A0\uB9AC\uAC00 \uC791\uC131\uB428",
|
|
3273
|
+
"\uAD00\uB828 \uC774\uD574\uAD00\uACC4\uC790\uC640 \uB17C\uC758 \uC644\uB8CC",
|
|
3274
|
+
"\uAE30\uC874 \uAE30\uB2A5\uACFC\uC758 \uCDA9\uB3CC \uC5EC\uBD80 \uD655\uC778"
|
|
3275
|
+
],
|
|
3276
|
+
"post-spec": [
|
|
3277
|
+
"RFC 2119 \uD0A4\uC6CC\uB4DC \uC0AC\uC6A9 \uD655\uC778 (SHALL, MUST, SHOULD, MAY)",
|
|
3278
|
+
"GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624 \uD3EC\uD568",
|
|
3279
|
+
"\uBE44\uAE30\uB2A5 \uC694\uAD6C\uC0AC\uD56D \uBA85\uC2DC",
|
|
3280
|
+
"sdd validate \uD1B5\uACFC"
|
|
3281
|
+
],
|
|
3282
|
+
"pre-plan": [
|
|
3283
|
+
"\uBA85\uC138\uAC00 \uC2B9\uC778\uB428",
|
|
3284
|
+
"\uAE30\uC220 \uC2A4\uD0DD \uACB0\uC815\uB428",
|
|
3285
|
+
"\uC544\uD0A4\uD14D\uCC98 \uAC80\uD1A0 \uC644\uB8CC",
|
|
3286
|
+
"\uC758\uC874\uC131 \uD655\uC778"
|
|
3287
|
+
],
|
|
3288
|
+
"post-plan": [
|
|
3289
|
+
"\uAD6C\uD604 \uB2E8\uACC4\uAC00 \uBA85\uD655\uD788 \uC815\uC758\uB428",
|
|
3290
|
+
"\uB9AC\uC2A4\uD06C \uBD84\uC11D \uC644\uB8CC",
|
|
3291
|
+
"\uD14C\uC2A4\uD2B8 \uC804\uB7B5 \uC218\uB9BD",
|
|
3292
|
+
"\uD5CC\uBC95 \uC900\uC218 \uC0AC\uD56D \uD655\uC778"
|
|
3293
|
+
],
|
|
3294
|
+
"pre-impl": [
|
|
3295
|
+
"\uC791\uC5C5\uC774 \uBD84\uD574\uB428 (tasks.md)",
|
|
3296
|
+
"\uBE0C\uB79C\uCE58\uAC00 \uC0DD\uC131\uB428",
|
|
3297
|
+
"\uAC1C\uBC1C \uD658\uACBD \uC900\uBE44",
|
|
3298
|
+
"\uAD00\uB828 \uD14C\uC2A4\uD2B8 \uD658\uACBD \uD655\uC778"
|
|
3299
|
+
],
|
|
3300
|
+
"post-impl": [
|
|
3301
|
+
"\uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC",
|
|
3302
|
+
"\uB2E8\uC704 \uD14C\uC2A4\uD2B8 \uC791\uC131 \uBC0F \uD1B5\uACFC",
|
|
3303
|
+
"\uD1B5\uD569 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC",
|
|
3304
|
+
"\uCF54\uB4DC \uCEE4\uBC84\uB9AC\uC9C0 \uBAA9\uD45C \uB2EC\uC131 (80%+)",
|
|
3305
|
+
"\uB9B0\uD2B8 \uBC0F \uD0C0\uC785 \uCCB4\uD06C \uD1B5\uACFC"
|
|
3306
|
+
],
|
|
3307
|
+
"pre-review": [
|
|
3308
|
+
"\uC140\uD504 \uCF54\uB4DC \uB9AC\uBDF0 \uC644\uB8CC",
|
|
3309
|
+
"\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8",
|
|
3310
|
+
"PR \uC124\uBA85 \uC791\uC131",
|
|
3311
|
+
"\uD14C\uC2A4\uD2B8 \uACB0\uACFC \uCCA8\uBD80"
|
|
3312
|
+
],
|
|
3313
|
+
"post-review": [
|
|
3314
|
+
"\uB9AC\uBDF0 \uD53C\uB4DC\uBC31 \uBC18\uC601",
|
|
3315
|
+
"\uCD5C\uC885 \uD14C\uC2A4\uD2B8 \uD1B5\uACFC",
|
|
3316
|
+
"\uC2A4\uD399 \uC0C1\uD0DC \uC5C5\uB370\uC774\uD2B8",
|
|
3317
|
+
"\uC544\uCE74\uC774\uBE0C \uC900\uBE44"
|
|
3318
|
+
]
|
|
3319
|
+
};
|
|
3320
|
+
function createChecklist(category) {
|
|
3321
|
+
const items = DEFAULT_CHECKLISTS[category];
|
|
3322
|
+
return items.map((text, index) => ({
|
|
3323
|
+
id: `${category}-${String(index + 1).padStart(2, "0")}`,
|
|
3324
|
+
text,
|
|
3325
|
+
checked: false,
|
|
3326
|
+
category
|
|
3327
|
+
}));
|
|
3328
|
+
}
|
|
3329
|
+
function checklistToMarkdown(items, title2) {
|
|
3330
|
+
let content = "";
|
|
3331
|
+
if (title2) {
|
|
3332
|
+
content += `## ${title2}
|
|
3333
|
+
|
|
3334
|
+
`;
|
|
3335
|
+
}
|
|
3336
|
+
items.forEach((item) => {
|
|
3337
|
+
const checkbox = item.checked ? "[x]" : "[ ]";
|
|
3338
|
+
content += `- ${checkbox} ${item.text}
|
|
3339
|
+
`;
|
|
3340
|
+
});
|
|
3341
|
+
return content;
|
|
3342
|
+
}
|
|
3343
|
+
function createWorkflowChecklists() {
|
|
3344
|
+
return {
|
|
3345
|
+
"\uBA85\uC138 \uC791\uC131 \uC804": createChecklist("pre-spec"),
|
|
3346
|
+
"\uBA85\uC138 \uC791\uC131 \uD6C4": createChecklist("post-spec"),
|
|
3347
|
+
"\uACC4\uD68D \uC791\uC131 \uC804": createChecklist("pre-plan"),
|
|
3348
|
+
"\uACC4\uD68D \uC791\uC131 \uD6C4": createChecklist("post-plan"),
|
|
3349
|
+
"\uAD6C\uD604 \uC804": createChecklist("pre-impl"),
|
|
3350
|
+
"\uAD6C\uD604 \uD6C4": createChecklist("post-impl"),
|
|
3351
|
+
"\uB9AC\uBDF0 \uC804": createChecklist("pre-review"),
|
|
3352
|
+
"\uB9AC\uBDF0 \uD6C4": createChecklist("post-review")
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
function generateFullChecklistMarkdown() {
|
|
3356
|
+
const checklists = createWorkflowChecklists();
|
|
3357
|
+
let content = `# SDD \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
|
|
3358
|
+
|
|
3359
|
+
> \uAC01 \uB2E8\uACC4\uBCC4\uB85C \uD655\uC778\uD574\uC57C \uD560 \uD56D\uBAA9\uB4E4\uC785\uB2C8\uB2E4.
|
|
3360
|
+
|
|
3361
|
+
---
|
|
3362
|
+
|
|
3363
|
+
`;
|
|
3364
|
+
for (const [title2, items] of Object.entries(checklists)) {
|
|
3365
|
+
content += checklistToMarkdown(items, title2);
|
|
3366
|
+
content += "\n---\n\n";
|
|
3367
|
+
}
|
|
3368
|
+
return content;
|
|
3369
|
+
}
|
|
3370
|
+
|
|
3371
|
+
// src/cli/commands/new.ts
|
|
3372
|
+
function registerNewCommand(program2) {
|
|
3373
|
+
const newCmd = program2.command("new").description("\uC0C8\uB85C\uC6B4 \uAE30\uB2A5 \uC0DD\uC131").argument("[name]", "\uAE30\uB2A5 \uC774\uB984").option("--title <title>", "\uAE30\uB2A5 \uC81C\uBAA9").option("--description <desc>", "\uAE30\uB2A5 \uC124\uBA85").option("--no-branch", "\uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC548 \uD568").option("--plan", "\uACC4\uD68D \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--tasks", "\uC791\uC5C5 \uBD84\uD574 \uD30C\uC77C\uB3C4 \uD568\uAED8 \uC0DD\uC131").option("--all", "\uBAA8\uB4E0 \uD30C\uC77C \uC0DD\uC131 (spec, plan, tasks)").option("--checklist", "\uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uD30C\uC77C \uC0DD\uC131").action(async (name, options) => {
|
|
3374
|
+
await handleNew(name, options);
|
|
3375
|
+
});
|
|
3376
|
+
newCmd.command("plan").description("\uAE30\uB2A5 \uAD6C\uD604 \uACC4\uD68D \uC0DD\uC131").argument("<feature>", "\uAE30\uB2A5 ID").option("--title <title>", "\uACC4\uD68D \uC81C\uBAA9").action(async (feature, opts) => {
|
|
3377
|
+
await handlePlan(feature, opts);
|
|
3378
|
+
});
|
|
3379
|
+
newCmd.command("tasks").description("\uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131").argument("<feature>", "\uAE30\uB2A5 ID").action(async (feature) => {
|
|
3380
|
+
await handleTasks(feature);
|
|
3381
|
+
});
|
|
3382
|
+
newCmd.command("checklist").description("\uC6CC\uD06C\uD50C\uB85C\uC6B0 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131").action(async () => {
|
|
3383
|
+
await handleChecklist();
|
|
3384
|
+
});
|
|
3385
|
+
}
|
|
3386
|
+
async function handleNew(name, options) {
|
|
3387
|
+
if (!name) {
|
|
3388
|
+
logger_exports.error("\uAE30\uB2A5 \uC774\uB984\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694: sdd new <name>");
|
|
3389
|
+
process.exit(1);
|
|
3390
|
+
}
|
|
3391
|
+
const featureId = generateFeatureId(name);
|
|
3392
|
+
const title2 = options.title || name;
|
|
3393
|
+
const description = options.description || `${title2} \uAE30\uB2A5 \uBA85\uC138`;
|
|
3394
|
+
const cwd = process.cwd();
|
|
3395
|
+
const sddPath = path10.join(cwd, ".sdd");
|
|
3396
|
+
const featurePath = path10.join(sddPath, "specs", featureId);
|
|
3397
|
+
try {
|
|
3398
|
+
if (!await fileExists(sddPath)) {
|
|
3399
|
+
logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 sdd init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
|
|
3400
|
+
process.exit(1);
|
|
3401
|
+
}
|
|
3402
|
+
await ensureDir(featurePath);
|
|
3403
|
+
const specContent = generateSpec({
|
|
3404
|
+
id: featureId,
|
|
3405
|
+
title: title2,
|
|
3406
|
+
description
|
|
3407
|
+
});
|
|
3408
|
+
await fs5.writeFile(path10.join(featurePath, "spec.md"), specContent, "utf-8");
|
|
3409
|
+
logger_exports.info(`\u2705 \uBA85\uC138 \uC0DD\uC131: ${featurePath}/spec.md`);
|
|
3410
|
+
if (options.branch !== false) {
|
|
3411
|
+
if (await isGitRepository(cwd)) {
|
|
3412
|
+
const result = await createBranch(featureId, { checkout: true, cwd });
|
|
3413
|
+
if (result.success) {
|
|
3414
|
+
logger_exports.info(`\u2705 \uBE0C\uB79C\uCE58 \uC0DD\uC131: ${result.data}`);
|
|
3415
|
+
} else {
|
|
3416
|
+
logger_exports.warn(`\u26A0\uFE0F \uBE0C\uB79C\uCE58 \uC0DD\uC131 \uC2E4\uD328: ${result.error.message}`);
|
|
3417
|
+
}
|
|
3418
|
+
} else {
|
|
3419
|
+
logger_exports.warn("\u26A0\uFE0F Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4. \uBE0C\uB79C\uCE58 \uC0DD\uC131\uC744 \uAC74\uB108\uB701\uB2C8\uB2E4.");
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
if (options.plan || options.all) {
|
|
3423
|
+
const planContent = generatePlan({
|
|
3424
|
+
featureId,
|
|
3425
|
+
featureTitle: title2,
|
|
3426
|
+
overview: description
|
|
3427
|
+
});
|
|
3428
|
+
await fs5.writeFile(path10.join(featurePath, "plan.md"), planContent, "utf-8");
|
|
3429
|
+
logger_exports.info(`\u2705 \uACC4\uD68D \uC0DD\uC131: ${featurePath}/plan.md`);
|
|
3430
|
+
}
|
|
3431
|
+
if (options.tasks || options.all) {
|
|
3432
|
+
const tasksContent = generateTasks({
|
|
3433
|
+
featureId,
|
|
3434
|
+
featureTitle: title2,
|
|
3435
|
+
tasks: [
|
|
3436
|
+
{ title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
|
|
3437
|
+
{ title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
|
|
3438
|
+
{ title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
|
|
3439
|
+
{ title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
|
|
3440
|
+
]
|
|
3441
|
+
});
|
|
3442
|
+
await fs5.writeFile(path10.join(featurePath, "tasks.md"), tasksContent, "utf-8");
|
|
3443
|
+
logger_exports.info(`\u2705 \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131: ${featurePath}/tasks.md`);
|
|
3444
|
+
}
|
|
3445
|
+
if (options.checklist || options.all) {
|
|
3446
|
+
const checklistContent = generateFullChecklistMarkdown();
|
|
3447
|
+
await fs5.writeFile(path10.join(featurePath, "checklist.md"), checklistContent, "utf-8");
|
|
3448
|
+
logger_exports.info(`\u2705 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131: ${featurePath}/checklist.md`);
|
|
3449
|
+
}
|
|
3450
|
+
logger_exports.info("");
|
|
3451
|
+
logger_exports.info(`\u{1F389} \uAE30\uB2A5 '${featureId}' \uC0DD\uC131 \uC644\uB8CC!`);
|
|
3452
|
+
logger_exports.info("");
|
|
3453
|
+
logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
3454
|
+
logger_exports.info(` 1. ${featurePath}/spec.md \uD3B8\uC9D1`);
|
|
3455
|
+
if (!(options.plan || options.all)) {
|
|
3456
|
+
logger_exports.info(" 2. sdd new plan " + featureId + " - \uACC4\uD68D \uC791\uC131");
|
|
3457
|
+
}
|
|
3458
|
+
if (!(options.tasks || options.all)) {
|
|
3459
|
+
logger_exports.info(" 3. sdd new tasks " + featureId + " - \uC791\uC5C5 \uBD84\uD574");
|
|
3460
|
+
}
|
|
3461
|
+
logger_exports.info(" 4. sdd validate - \uBA85\uC138 \uAC80\uC99D");
|
|
3462
|
+
} catch (error2) {
|
|
3463
|
+
logger_exports.error(`\uAE30\uB2A5 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
|
|
3464
|
+
process.exit(1);
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
async function handlePlan(feature, options) {
|
|
3468
|
+
const cwd = process.cwd();
|
|
3469
|
+
const featurePath = path10.join(cwd, ".sdd", "specs", feature);
|
|
3470
|
+
try {
|
|
3471
|
+
if (!await fileExists(featurePath)) {
|
|
3472
|
+
logger_exports.error(`\uAE30\uB2A5 '${feature}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
3473
|
+
process.exit(1);
|
|
3474
|
+
}
|
|
3475
|
+
let title2 = options.title || feature;
|
|
3476
|
+
const specPath = path10.join(featurePath, "spec.md");
|
|
3477
|
+
if (await fileExists(specPath)) {
|
|
3478
|
+
const specContent = await fs5.readFile(specPath, "utf-8");
|
|
3479
|
+
const titleMatch = specContent.match(/title:\s*"?([^"\n]+)"?/);
|
|
3480
|
+
if (titleMatch) {
|
|
3481
|
+
title2 = titleMatch[1];
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
const planContent = generatePlan({
|
|
3485
|
+
featureId: feature,
|
|
3486
|
+
featureTitle: title2,
|
|
3487
|
+
overview: `${title2} \uAD6C\uD604 \uACC4\uD68D`
|
|
3488
|
+
});
|
|
3489
|
+
await fs5.writeFile(path10.join(featurePath, "plan.md"), planContent, "utf-8");
|
|
3490
|
+
logger_exports.info(`\u2705 \uACC4\uD68D \uC0DD\uC131: ${featurePath}/plan.md`);
|
|
3491
|
+
logger_exports.info("");
|
|
3492
|
+
logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
3493
|
+
logger_exports.info(` 1. ${featurePath}/plan.md \uD3B8\uC9D1`);
|
|
3494
|
+
logger_exports.info(" 2. sdd new tasks " + feature + " - \uC791\uC5C5 \uBD84\uD574");
|
|
3495
|
+
} catch (error2) {
|
|
3496
|
+
logger_exports.error(`\uACC4\uD68D \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
|
|
3497
|
+
process.exit(1);
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
async function handleTasks(feature) {
|
|
3501
|
+
const cwd = process.cwd();
|
|
3502
|
+
const featurePath = path10.join(cwd, ".sdd", "specs", feature);
|
|
3503
|
+
try {
|
|
3504
|
+
if (!await fileExists(featurePath)) {
|
|
3505
|
+
logger_exports.error(`\uAE30\uB2A5 '${feature}'\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
3506
|
+
process.exit(1);
|
|
3507
|
+
}
|
|
3508
|
+
let title2 = feature;
|
|
3509
|
+
const specPath = path10.join(featurePath, "spec.md");
|
|
3510
|
+
if (await fileExists(specPath)) {
|
|
3511
|
+
const specContent = await fs5.readFile(specPath, "utf-8");
|
|
3512
|
+
const titleMatch = specContent.match(/title:\s*"?([^"\n]+)"?/);
|
|
3513
|
+
if (titleMatch) {
|
|
3514
|
+
title2 = titleMatch[1];
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
const tasksContent = generateTasks({
|
|
3518
|
+
featureId: feature,
|
|
3519
|
+
featureTitle: title2,
|
|
3520
|
+
tasks: [
|
|
3521
|
+
{ title: "\uAE30\uBC18 \uAD6C\uC870 \uC124\uC815", priority: "high" },
|
|
3522
|
+
{ title: "\uD575\uC2EC \uAE30\uB2A5 \uAD6C\uD604", priority: "high" },
|
|
3523
|
+
{ title: "\uD14C\uC2A4\uD2B8 \uC791\uC131", priority: "medium" },
|
|
3524
|
+
{ title: "\uBB38\uC11C \uC5C5\uB370\uC774\uD2B8", priority: "low" }
|
|
3525
|
+
]
|
|
3526
|
+
});
|
|
3527
|
+
await fs5.writeFile(path10.join(featurePath, "tasks.md"), tasksContent, "utf-8");
|
|
3528
|
+
logger_exports.info(`\u2705 \uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131: ${featurePath}/tasks.md`);
|
|
3529
|
+
logger_exports.info("");
|
|
3530
|
+
logger_exports.info("\uB2E4\uC74C \uB2E8\uACC4:");
|
|
3531
|
+
logger_exports.info(` 1. ${featurePath}/tasks.md \uD3B8\uC9D1`);
|
|
3532
|
+
logger_exports.info(" 2. \uAC01 \uC791\uC5C5 \uC21C\uCC28\uC801\uC73C\uB85C \uAD6C\uD604");
|
|
3533
|
+
} catch (error2) {
|
|
3534
|
+
logger_exports.error(`\uC791\uC5C5 \uBD84\uD574 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
|
|
3535
|
+
process.exit(1);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
async function handleChecklist() {
|
|
3539
|
+
const cwd = process.cwd();
|
|
3540
|
+
const sddPath = path10.join(cwd, ".sdd");
|
|
3541
|
+
try {
|
|
3542
|
+
if (!await fileExists(sddPath)) {
|
|
3543
|
+
logger_exports.error(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. \uBA3C\uC800 sdd init\uC744 \uC2E4\uD589\uD574\uC8FC\uC138\uC694.");
|
|
3544
|
+
process.exit(1);
|
|
3545
|
+
}
|
|
3546
|
+
const checklistContent = generateFullChecklistMarkdown();
|
|
3547
|
+
const outputPath = path10.join(sddPath, "checklist.md");
|
|
3548
|
+
await fs5.writeFile(outputPath, checklistContent, "utf-8");
|
|
3549
|
+
logger_exports.info(`\u2705 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131: ${outputPath}`);
|
|
3550
|
+
} catch (error2) {
|
|
3551
|
+
logger_exports.error(`\uCCB4\uD06C\uB9AC\uC2A4\uD2B8 \uC0DD\uC131 \uC2E4\uD328: ${error2}`);
|
|
3552
|
+
process.exit(1);
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
// src/cli/commands/status.ts
|
|
3557
|
+
import path11 from "path";
|
|
3558
|
+
import { promises as fs6 } from "fs";
|
|
3559
|
+
function registerStatusCommand(program2) {
|
|
3560
|
+
program2.command("status").description("SDD \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uC870\uD68C").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--verbose", "\uC0C1\uC138 \uC815\uBCF4 \uCD9C\uB825").action(async (options) => {
|
|
3561
|
+
await handleStatus(options);
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
async function handleStatus(options) {
|
|
3565
|
+
const cwd = process.cwd();
|
|
3566
|
+
const sddPath = path11.join(cwd, ".sdd");
|
|
3567
|
+
const status = {
|
|
3568
|
+
initialized: false,
|
|
3569
|
+
hasConstitution: false,
|
|
3570
|
+
hasAgents: false,
|
|
3571
|
+
features: [],
|
|
3572
|
+
pendingChanges: [],
|
|
3573
|
+
archivedChanges: 0,
|
|
3574
|
+
featureBranches: []
|
|
3575
|
+
};
|
|
3576
|
+
status.initialized = await fileExists(sddPath);
|
|
3577
|
+
if (!status.initialized) {
|
|
3578
|
+
if (options.json) {
|
|
3579
|
+
console.log(JSON.stringify(status, null, 2));
|
|
3580
|
+
} else {
|
|
3581
|
+
logger_exports.warn("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uCD08\uAE30\uD654\uB418\uC9C0 \uC54A\uC558\uC2B5\uB2C8\uB2E4.");
|
|
3582
|
+
logger_exports.info("sdd init \uBA85\uB839\uC5B4\uB85C \uCD08\uAE30\uD654\uD558\uC138\uC694.");
|
|
3583
|
+
}
|
|
3584
|
+
return;
|
|
3585
|
+
}
|
|
3586
|
+
status.hasConstitution = await fileExists(path11.join(sddPath, "constitution.md"));
|
|
3587
|
+
status.hasAgents = await fileExists(path11.join(sddPath, "AGENTS.md"));
|
|
3588
|
+
const specsPath = path11.join(sddPath, "specs");
|
|
3589
|
+
if (await fileExists(specsPath)) {
|
|
3590
|
+
const specsResult = await readDir(specsPath);
|
|
3591
|
+
if (specsResult.success) {
|
|
3592
|
+
for (const entry of specsResult.data) {
|
|
3593
|
+
const featurePath = path11.join(specsPath, entry);
|
|
3594
|
+
const stat = await fs6.stat(featurePath);
|
|
3595
|
+
if (stat.isDirectory()) {
|
|
3596
|
+
const featureInfo = await getFeatureInfo(entry, featurePath);
|
|
3597
|
+
status.features.push(featureInfo);
|
|
3598
|
+
}
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
const pendingResult = await listPendingChanges(sddPath);
|
|
3603
|
+
if (pendingResult.success) {
|
|
3604
|
+
status.pendingChanges = pendingResult.data;
|
|
3605
|
+
}
|
|
3606
|
+
const archiveResult = await listArchives(sddPath);
|
|
3607
|
+
if (archiveResult.success) {
|
|
3608
|
+
status.archivedChanges = archiveResult.data.length;
|
|
3609
|
+
}
|
|
3610
|
+
const currentBranchResult = await getCurrentBranch(cwd);
|
|
3611
|
+
if (currentBranchResult.success) {
|
|
3612
|
+
status.currentBranch = currentBranchResult.data;
|
|
3613
|
+
}
|
|
3614
|
+
const featureBranchesResult = await listFeatureBranches(cwd);
|
|
3615
|
+
if (featureBranchesResult.success) {
|
|
3616
|
+
status.featureBranches = featureBranchesResult.data;
|
|
3617
|
+
}
|
|
3618
|
+
if (options.json) {
|
|
3619
|
+
console.log(JSON.stringify(status, null, 2));
|
|
3620
|
+
} else {
|
|
3621
|
+
printStatus(status, options.verbose);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
async function getFeatureInfo(id, featurePath) {
|
|
3625
|
+
const info2 = {
|
|
3626
|
+
id,
|
|
3627
|
+
title: id,
|
|
3628
|
+
status: "unknown",
|
|
3629
|
+
hasSpec: false,
|
|
3630
|
+
hasPlan: false,
|
|
3631
|
+
hasTasks: false
|
|
3632
|
+
};
|
|
3633
|
+
const specPath = path11.join(featurePath, "spec.md");
|
|
3634
|
+
if (await fileExists(specPath)) {
|
|
3635
|
+
info2.hasSpec = true;
|
|
3636
|
+
const content = await fs6.readFile(specPath, "utf-8");
|
|
3637
|
+
const metadata = parseSpecMetadata(content);
|
|
3638
|
+
if (metadata) {
|
|
3639
|
+
info2.title = metadata.title;
|
|
3640
|
+
info2.status = metadata.status;
|
|
3641
|
+
}
|
|
3642
|
+
}
|
|
3643
|
+
info2.hasPlan = await fileExists(path11.join(featurePath, "plan.md"));
|
|
3644
|
+
const tasksPath = path11.join(featurePath, "tasks.md");
|
|
3645
|
+
if (await fileExists(tasksPath)) {
|
|
3646
|
+
info2.hasTasks = true;
|
|
3647
|
+
const content = await fs6.readFile(tasksPath, "utf-8");
|
|
3648
|
+
const tasks = parseTasks(content);
|
|
3649
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
3650
|
+
info2.taskProgress = {
|
|
3651
|
+
completed,
|
|
3652
|
+
total: tasks.length
|
|
3653
|
+
};
|
|
3654
|
+
}
|
|
3655
|
+
return info2;
|
|
3656
|
+
}
|
|
3657
|
+
function printStatus(status, verbose) {
|
|
3658
|
+
console.log("");
|
|
3659
|
+
console.log("\u{1F4CA} SDD \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC");
|
|
3660
|
+
console.log("\u2550".repeat(40));
|
|
3661
|
+
console.log("");
|
|
3662
|
+
console.log("\u{1F4C1} \uD504\uB85C\uC81D\uD2B8 \uAD6C\uC870:");
|
|
3663
|
+
console.log(` ${status.hasConstitution ? "\u2705" : "\u274C"} constitution.md`);
|
|
3664
|
+
console.log(` ${status.hasAgents ? "\u2705" : "\u274C"} AGENTS.md`);
|
|
3665
|
+
console.log("");
|
|
3666
|
+
if (status.features.length > 0) {
|
|
3667
|
+
console.log("\u{1F4CB} \uAE30\uB2A5 \uBAA9\uB85D:");
|
|
3668
|
+
for (const feature of status.features) {
|
|
3669
|
+
const statusIcon = getStatusIcon(feature.status);
|
|
3670
|
+
const files = [
|
|
3671
|
+
feature.hasSpec ? "spec" : "",
|
|
3672
|
+
feature.hasPlan ? "plan" : "",
|
|
3673
|
+
feature.hasTasks ? "tasks" : ""
|
|
3674
|
+
].filter(Boolean).join(", ");
|
|
3675
|
+
let progressStr = "";
|
|
3676
|
+
if (feature.taskProgress) {
|
|
3677
|
+
const { completed, total } = feature.taskProgress;
|
|
3678
|
+
const percent = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
3679
|
+
progressStr = ` [${completed}/${total} = ${percent}%]`;
|
|
3680
|
+
}
|
|
3681
|
+
console.log(` ${statusIcon} ${feature.title} (${feature.id})`);
|
|
3682
|
+
if (verbose) {
|
|
3683
|
+
console.log(` \uC0C1\uD0DC: ${feature.status}, \uD30C\uC77C: ${files}${progressStr}`);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3686
|
+
console.log("");
|
|
3687
|
+
} else {
|
|
3688
|
+
console.log("\u{1F4CB} \uAE30\uB2A5: \uC5C6\uC74C");
|
|
3689
|
+
console.log(" sdd new <name> \uBA85\uB839\uC5B4\uB85C \uC0C8 \uAE30\uB2A5\uC744 \uC0DD\uC131\uD558\uC138\uC694.");
|
|
3690
|
+
console.log("");
|
|
3691
|
+
}
|
|
3692
|
+
if (status.pendingChanges.length > 0) {
|
|
3693
|
+
console.log("\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD:");
|
|
3694
|
+
for (const change of status.pendingChanges) {
|
|
3695
|
+
console.log(` - ${change}`);
|
|
3696
|
+
}
|
|
3697
|
+
console.log("");
|
|
3698
|
+
}
|
|
3699
|
+
if (status.archivedChanges > 0 && verbose) {
|
|
3700
|
+
console.log(`\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD: ${status.archivedChanges}\uAC1C`);
|
|
3701
|
+
console.log("");
|
|
3702
|
+
}
|
|
3703
|
+
if (status.currentBranch) {
|
|
3704
|
+
console.log(`\u{1F500} \uD604\uC7AC \uBE0C\uB79C\uCE58: ${status.currentBranch}`);
|
|
3705
|
+
if (status.featureBranches.length > 0 && verbose) {
|
|
3706
|
+
console.log(" \uAE30\uB2A5 \uBE0C\uB79C\uCE58:");
|
|
3707
|
+
for (const branch of status.featureBranches) {
|
|
3708
|
+
const isCurrent = branch === status.currentBranch;
|
|
3709
|
+
console.log(` ${isCurrent ? "\u2192" : " "} ${branch}`);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
console.log("");
|
|
3713
|
+
}
|
|
3714
|
+
console.log("\u{1F4A1} \uB2E4\uC74C \uB2E8\uACC4:");
|
|
3715
|
+
if (status.features.length === 0) {
|
|
3716
|
+
console.log(" sdd new <name> - \uC0C8 \uAE30\uB2A5 \uC0DD\uC131");
|
|
3717
|
+
} else {
|
|
3718
|
+
const inProgress = status.features.find((f) => f.status === "implementing");
|
|
3719
|
+
if (inProgress) {
|
|
3720
|
+
console.log(` ${inProgress.id} \uAE30\uB2A5 \uAD6C\uD604 \uC911...`);
|
|
3721
|
+
if (inProgress.taskProgress) {
|
|
3722
|
+
const { completed, total } = inProgress.taskProgress;
|
|
3723
|
+
if (completed < total) {
|
|
3724
|
+
console.log(` sdd validate - \uC2A4\uD399 \uAC80\uC99D`);
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
} else {
|
|
3728
|
+
const draft = status.features.find((f) => f.status === "draft");
|
|
3729
|
+
if (draft) {
|
|
3730
|
+
console.log(` ${draft.id} \uAE30\uB2A5 \uBA85\uC138 \uC791\uC131 \uC644\uB8CC \uD6C4 /sdd:plan \uC2E4\uD589`);
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
console.log("");
|
|
3735
|
+
}
|
|
3736
|
+
function getStatusIcon(status) {
|
|
3737
|
+
switch (status) {
|
|
3738
|
+
case "draft":
|
|
3739
|
+
return "\u{1F4DD}";
|
|
3740
|
+
case "specified":
|
|
3741
|
+
return "\u{1F4C4}";
|
|
3742
|
+
case "planned":
|
|
3743
|
+
return "\u{1F4CB}";
|
|
3744
|
+
case "tasked":
|
|
3745
|
+
return "\u270F\uFE0F";
|
|
3746
|
+
case "implementing":
|
|
3747
|
+
return "\u{1F528}";
|
|
3748
|
+
case "completed":
|
|
3749
|
+
return "\u2705";
|
|
3750
|
+
default:
|
|
3751
|
+
return "\u2753";
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
// src/cli/commands/list.ts
|
|
3756
|
+
import path12 from "path";
|
|
3757
|
+
import { promises as fs7 } from "fs";
|
|
3758
|
+
function registerListCommand(program2) {
|
|
3759
|
+
const listCmd = program2.command("list").alias("ls").description("\uD56D\uBAA9 \uBAA9\uB85D \uC870\uD68C");
|
|
3760
|
+
listCmd.command("features").alias("f").description("\uAE30\uB2A5 \uBAA9\uB85D \uC870\uD68C").option("--status <status>", "\uC0C1\uD0DC\uBCC4 \uD544\uD130 (draft, specified, planned, etc.)").action(async (options) => {
|
|
3761
|
+
await listFeatures(options);
|
|
3762
|
+
});
|
|
3763
|
+
listCmd.command("changes").alias("c").description("\uBCC0\uACBD \uC81C\uC548 \uBAA9\uB85D \uC870\uD68C").option("--pending", "\uB300\uAE30 \uC911\uC778 \uBCC0\uACBD\uB9CC \uD45C\uC2DC").option("--archived", "\uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD\uB9CC \uD45C\uC2DC").action(async (options) => {
|
|
3764
|
+
await listChanges(options);
|
|
3765
|
+
});
|
|
3766
|
+
listCmd.command("specs").alias("s").description("\uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D \uC870\uD68C").action(async () => {
|
|
3767
|
+
await listSpecs();
|
|
3768
|
+
});
|
|
3769
|
+
listCmd.command("templates").alias("t").description("\uD15C\uD50C\uB9BF \uBAA9\uB85D \uC870\uD68C").action(async () => {
|
|
3770
|
+
await listTemplates();
|
|
3771
|
+
});
|
|
3772
|
+
listCmd.action(async () => {
|
|
3773
|
+
await listSummary();
|
|
3774
|
+
});
|
|
3775
|
+
}
|
|
3776
|
+
async function listFeatures(options) {
|
|
3777
|
+
const cwd = process.cwd();
|
|
3778
|
+
const specsPath = path12.join(cwd, ".sdd", "specs");
|
|
3779
|
+
if (!await fileExists(specsPath)) {
|
|
3780
|
+
logger_exports.warn("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
3781
|
+
return;
|
|
3782
|
+
}
|
|
3783
|
+
const result = await readDir(specsPath);
|
|
3784
|
+
if (!result.success) {
|
|
3785
|
+
logger_exports.error("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3786
|
+
return;
|
|
3787
|
+
}
|
|
3788
|
+
const features = [];
|
|
3789
|
+
for (const entry of result.data) {
|
|
3790
|
+
const featurePath = path12.join(specsPath, entry);
|
|
3791
|
+
const stat = await fs7.stat(featurePath);
|
|
3792
|
+
if (stat.isDirectory()) {
|
|
3793
|
+
const specPath = path12.join(featurePath, "spec.md");
|
|
3794
|
+
if (await fileExists(specPath)) {
|
|
3795
|
+
const content = await fs7.readFile(specPath, "utf-8");
|
|
3796
|
+
const metadata = parseSpecMetadata(content);
|
|
3797
|
+
if (metadata) {
|
|
3798
|
+
if (!options.status || metadata.status === options.status) {
|
|
3799
|
+
features.push({
|
|
3800
|
+
id: entry,
|
|
3801
|
+
title: metadata.title,
|
|
3802
|
+
status: metadata.status
|
|
3803
|
+
});
|
|
3804
|
+
}
|
|
3805
|
+
}
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
if (features.length === 0) {
|
|
3810
|
+
logger_exports.info("\uAE30\uB2A5\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3811
|
+
return;
|
|
3812
|
+
}
|
|
3813
|
+
console.log("");
|
|
3814
|
+
console.log("\u{1F4CB} \uAE30\uB2A5 \uBAA9\uB85D");
|
|
3815
|
+
console.log("\u2500".repeat(50));
|
|
3816
|
+
for (const f of features) {
|
|
3817
|
+
const statusIcon = getStatusIcon2(f.status);
|
|
3818
|
+
console.log(`${statusIcon} ${f.title} (${f.id}) - ${f.status}`);
|
|
3819
|
+
}
|
|
3820
|
+
console.log("");
|
|
3821
|
+
}
|
|
3822
|
+
async function listChanges(options) {
|
|
3823
|
+
const cwd = process.cwd();
|
|
3824
|
+
const sddPath = path12.join(cwd, ".sdd");
|
|
3825
|
+
if (!await fileExists(sddPath)) {
|
|
3826
|
+
logger_exports.warn(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
3827
|
+
return;
|
|
3828
|
+
}
|
|
3829
|
+
console.log("");
|
|
3830
|
+
if (!options.archived) {
|
|
3831
|
+
const pendingResult = await listPendingChanges(sddPath);
|
|
3832
|
+
if (pendingResult.success && pendingResult.data.length > 0) {
|
|
3833
|
+
console.log("\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD");
|
|
3834
|
+
console.log("\u2500".repeat(30));
|
|
3835
|
+
for (const change of pendingResult.data) {
|
|
3836
|
+
console.log(` - ${change}`);
|
|
3837
|
+
}
|
|
3838
|
+
console.log("");
|
|
3839
|
+
} else if (!options.pending) {
|
|
3840
|
+
console.log("\uB300\uAE30 \uC911\uC778 \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
if (!options.pending) {
|
|
3844
|
+
const archiveResult = await listArchives(sddPath);
|
|
3845
|
+
if (archiveResult.success && archiveResult.data.length > 0) {
|
|
3846
|
+
console.log("\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD");
|
|
3847
|
+
console.log("\u2500".repeat(30));
|
|
3848
|
+
for (const archive of archiveResult.data) {
|
|
3849
|
+
console.log(` - ${archive}`);
|
|
3850
|
+
}
|
|
3851
|
+
console.log("");
|
|
3852
|
+
} else if (!options.archived) {
|
|
3853
|
+
console.log("\uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
}
|
|
3857
|
+
async function listSpecs() {
|
|
3858
|
+
const cwd = process.cwd();
|
|
3859
|
+
const specsPath = path12.join(cwd, ".sdd", "specs");
|
|
3860
|
+
if (!await fileExists(specsPath)) {
|
|
3861
|
+
logger_exports.warn("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3862
|
+
return;
|
|
3863
|
+
}
|
|
3864
|
+
console.log("");
|
|
3865
|
+
console.log("\u{1F4C4} \uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D");
|
|
3866
|
+
console.log("\u2500".repeat(50));
|
|
3867
|
+
await walkSpecs(specsPath, "");
|
|
3868
|
+
console.log("");
|
|
3869
|
+
}
|
|
3870
|
+
async function walkSpecs(basePath, prefix) {
|
|
3871
|
+
const result = await readDir(basePath);
|
|
3872
|
+
if (!result.success) return;
|
|
3873
|
+
for (const entry of result.data) {
|
|
3874
|
+
const fullPath = path12.join(basePath, entry);
|
|
3875
|
+
const stat = await fs7.stat(fullPath);
|
|
3876
|
+
if (stat.isDirectory()) {
|
|
3877
|
+
console.log(`${prefix}\u{1F4C1} ${entry}/`);
|
|
3878
|
+
await walkSpecs(fullPath, prefix + " ");
|
|
3879
|
+
} else if (entry.endsWith(".md")) {
|
|
3880
|
+
console.log(`${prefix}\u{1F4C4} ${entry}`);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
async function listTemplates() {
|
|
3885
|
+
const cwd = process.cwd();
|
|
3886
|
+
const templatesPath = path12.join(cwd, ".sdd", "templates");
|
|
3887
|
+
if (!await fileExists(templatesPath)) {
|
|
3888
|
+
logger_exports.warn("\uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
const result = await readDir(templatesPath);
|
|
3892
|
+
if (!result.success) {
|
|
3893
|
+
logger_exports.error("\uD15C\uD50C\uB9BF \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.");
|
|
3894
|
+
return;
|
|
3895
|
+
}
|
|
3896
|
+
console.log("");
|
|
3897
|
+
console.log("\u{1F4D1} \uD15C\uD50C\uB9BF \uBAA9\uB85D");
|
|
3898
|
+
console.log("\u2500".repeat(30));
|
|
3899
|
+
for (const template of result.data.filter((f) => f.endsWith(".md"))) {
|
|
3900
|
+
console.log(` - ${template}`);
|
|
3901
|
+
}
|
|
3902
|
+
console.log("");
|
|
3903
|
+
}
|
|
3904
|
+
async function listSummary() {
|
|
3905
|
+
const cwd = process.cwd();
|
|
3906
|
+
const sddPath = path12.join(cwd, ".sdd");
|
|
3907
|
+
if (!await fileExists(sddPath)) {
|
|
3908
|
+
logger_exports.warn(".sdd \uB514\uB809\uD1A0\uB9AC\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
|
|
3909
|
+
return;
|
|
3910
|
+
}
|
|
3911
|
+
console.log("");
|
|
3912
|
+
console.log("\u{1F4CA} SDD \uD504\uB85C\uC81D\uD2B8 \uC694\uC57D");
|
|
3913
|
+
console.log("\u2550".repeat(40));
|
|
3914
|
+
const specsPath = path12.join(sddPath, "specs");
|
|
3915
|
+
let featureCount = 0;
|
|
3916
|
+
if (await fileExists(specsPath)) {
|
|
3917
|
+
const result = await readDir(specsPath);
|
|
3918
|
+
if (result.success) {
|
|
3919
|
+
for (const entry of result.data) {
|
|
3920
|
+
const stat = await fs7.stat(path12.join(specsPath, entry));
|
|
3921
|
+
if (stat.isDirectory()) featureCount++;
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
console.log(`\u{1F4CB} \uAE30\uB2A5: ${featureCount}\uAC1C`);
|
|
3926
|
+
const pendingResult = await listPendingChanges(sddPath);
|
|
3927
|
+
const pendingCount = pendingResult.success ? pendingResult.data.length : 0;
|
|
3928
|
+
console.log(`\u{1F4DD} \uB300\uAE30 \uC911\uC778 \uBCC0\uACBD: ${pendingCount}\uAC1C`);
|
|
3929
|
+
const archiveResult = await listArchives(sddPath);
|
|
3930
|
+
const archiveCount = archiveResult.success ? archiveResult.data.length : 0;
|
|
3931
|
+
console.log(`\u{1F4E6} \uC544\uCE74\uC774\uBE0C\uB41C \uBCC0\uACBD: ${archiveCount}\uAC1C`);
|
|
3932
|
+
console.log("");
|
|
3933
|
+
console.log("\uC0C1\uC138 \uC815\uBCF4:");
|
|
3934
|
+
console.log(" sdd list features - \uAE30\uB2A5 \uBAA9\uB85D");
|
|
3935
|
+
console.log(" sdd list changes - \uBCC0\uACBD \uBAA9\uB85D");
|
|
3936
|
+
console.log(" sdd list specs - \uC2A4\uD399 \uD30C\uC77C \uBAA9\uB85D");
|
|
3937
|
+
console.log(" sdd status - \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC");
|
|
3938
|
+
console.log("");
|
|
3939
|
+
}
|
|
3940
|
+
function getStatusIcon2(status) {
|
|
3941
|
+
switch (status) {
|
|
3942
|
+
case "draft":
|
|
3943
|
+
return "\u{1F4DD}";
|
|
3944
|
+
case "specified":
|
|
3945
|
+
return "\u{1F4C4}";
|
|
3946
|
+
case "planned":
|
|
3947
|
+
return "\u{1F4CB}";
|
|
3948
|
+
case "tasked":
|
|
3949
|
+
return "\u270F\uFE0F";
|
|
3950
|
+
case "implementing":
|
|
3951
|
+
return "\u{1F528}";
|
|
3952
|
+
case "completed":
|
|
3953
|
+
return "\u2705";
|
|
3954
|
+
default:
|
|
3955
|
+
return "\u2753";
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// src/cli/index.ts
|
|
3960
|
+
var require2 = createRequire(import.meta.url);
|
|
3961
|
+
var pkg = require2("../../package.json");
|
|
3962
|
+
var program = new Command();
|
|
3963
|
+
program.name("sdd").description(pkg.description).version(pkg.version);
|
|
3964
|
+
registerInitCommand(program);
|
|
3965
|
+
registerValidateCommand(program);
|
|
3966
|
+
registerPromptCommand(program);
|
|
3967
|
+
registerChangeCommand(program);
|
|
3968
|
+
registerImpactCommand(program);
|
|
3969
|
+
registerNewCommand(program);
|
|
3970
|
+
registerStatusCommand(program);
|
|
3971
|
+
registerListCommand(program);
|
|
3972
|
+
function run() {
|
|
3973
|
+
program.parse();
|
|
3974
|
+
}
|
|
3975
|
+
export {
|
|
3976
|
+
program,
|
|
3977
|
+
run
|
|
3978
|
+
};
|
|
3979
|
+
//# sourceMappingURL=index.js.map
|