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.
@@ -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