sdd-tool 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -128,10 +128,10 @@ var init_base = __esm({
128
128
  };
129
129
  FileSystemError = class extends SddError {
130
130
  path;
131
- constructor(code, path20) {
132
- super(code, formatMessage(code, path20), ExitCode.FILE_SYSTEM_ERROR);
131
+ constructor(code, path27) {
132
+ super(code, formatMessage(code, path27), ExitCode.FILE_SYSTEM_ERROR);
133
133
  this.name = "FileSystemError";
134
- this.path = path20;
134
+ this.path = path27;
135
135
  }
136
136
  };
137
137
  ValidationError = class extends SddError {
@@ -3631,9 +3631,9 @@ async function validateSpecs(targetPath, options = {}) {
3631
3631
  async function findSpecFiles(dirPath) {
3632
3632
  const files = [];
3633
3633
  async function scanDir(dir) {
3634
- const { promises: fs12 } = await import("fs");
3634
+ const { promises: fs15 } = await import("fs");
3635
3635
  try {
3636
- const entries = await fs12.readdir(dir, { withFileTypes: true });
3636
+ const entries = await fs15.readdir(dir, { withFileTypes: true });
3637
3637
  for (const entry of entries) {
3638
3638
  const fullPath = path3.join(dir, entry.name);
3639
3639
  if (entry.isDirectory()) {
@@ -5635,19 +5635,19 @@ function detectCircularDependencies(graph) {
5635
5635
  const cycles = [];
5636
5636
  const visited = /* @__PURE__ */ new Set();
5637
5637
  const recStack = /* @__PURE__ */ new Set();
5638
- function dfs(nodeId, path20) {
5638
+ function dfs(nodeId, path27) {
5639
5639
  visited.add(nodeId);
5640
5640
  recStack.add(nodeId);
5641
5641
  const node = graph.nodes.get(nodeId);
5642
5642
  if (!node) return false;
5643
5643
  for (const depId of node.dependsOn) {
5644
5644
  if (!visited.has(depId)) {
5645
- if (dfs(depId, [...path20, nodeId])) {
5645
+ if (dfs(depId, [...path27, nodeId])) {
5646
5646
  return true;
5647
5647
  }
5648
5648
  } else if (recStack.has(depId)) {
5649
- const cycleStart = path20.indexOf(depId);
5650
- const cycle = cycleStart >= 0 ? [...path20.slice(cycleStart), nodeId, depId] : [nodeId, depId];
5649
+ const cycleStart = path27.indexOf(depId);
5650
+ const cycle = cycleStart >= 0 ? [...path27.slice(cycleStart), nodeId, depId] : [nodeId, depId];
5651
5651
  cycles.push({
5652
5652
  cycle,
5653
5653
  description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
@@ -8139,12 +8139,354 @@ function displayConstitutionGuide() {
8139
8139
  }
8140
8140
 
8141
8141
  // src/cli/commands/migrate.ts
8142
- import path17 from "path";
8143
- import { promises as fs11 } from "fs";
8142
+ import path18 from "path";
8143
+ import { promises as fs12 } from "fs";
8144
8144
  init_errors();
8145
8145
  init_fs();
8146
8146
  init_new();
8147
8147
  init_schemas();
8148
+
8149
+ // src/core/migrate/detector.ts
8150
+ init_types();
8151
+ init_errors();
8152
+ init_fs();
8153
+ import path17 from "path";
8154
+ import fs11 from "fs/promises";
8155
+ async function detectExternalTools(projectRoot) {
8156
+ try {
8157
+ const results = [];
8158
+ const openspecResult = await detectOpenSpec(projectRoot);
8159
+ if (openspecResult) {
8160
+ results.push(openspecResult);
8161
+ }
8162
+ const speckitResult = await detectSpecKit(projectRoot);
8163
+ if (speckitResult) {
8164
+ results.push(speckitResult);
8165
+ }
8166
+ const sddResult = await detectSdd(projectRoot);
8167
+ if (sddResult) {
8168
+ results.push(sddResult);
8169
+ }
8170
+ return success(results);
8171
+ } catch (error2) {
8172
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8173
+ }
8174
+ }
8175
+ async function detectOpenSpec(projectRoot) {
8176
+ const openspecPath = path17.join(projectRoot, "openspec");
8177
+ if (!await directoryExists(openspecPath)) {
8178
+ return null;
8179
+ }
8180
+ const specsPath = path17.join(openspecPath, "specs");
8181
+ const changesPath = path17.join(openspecPath, "changes");
8182
+ const agentsPath = path17.join(openspecPath, "AGENTS.md");
8183
+ const hasAgents = await fileExists(agentsPath);
8184
+ const hasSpecs = await directoryExists(specsPath);
8185
+ const hasChanges = await directoryExists(changesPath);
8186
+ if (!hasSpecs && !hasChanges && !hasAgents) {
8187
+ return null;
8188
+ }
8189
+ const specs = [];
8190
+ if (hasSpecs) {
8191
+ const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
8192
+ for (const entry of specDirs) {
8193
+ if (entry.isDirectory()) {
8194
+ const specPath = path17.join(specsPath, entry.name);
8195
+ const specFile = path17.join(specPath, "spec.md");
8196
+ if (await fileExists(specFile)) {
8197
+ const content = await fs11.readFile(specFile, "utf-8");
8198
+ const title2 = extractTitle2(content);
8199
+ const status = extractFrontmatterField(content, "status");
8200
+ specs.push({
8201
+ id: entry.name,
8202
+ title: title2,
8203
+ path: specPath,
8204
+ status
8205
+ });
8206
+ }
8207
+ }
8208
+ }
8209
+ }
8210
+ return {
8211
+ tool: "openspec",
8212
+ path: openspecPath,
8213
+ specCount: specs.length,
8214
+ specs,
8215
+ confidence: hasAgents ? "high" : hasSpecs && hasChanges ? "medium" : "low"
8216
+ };
8217
+ }
8218
+ async function detectSpecKit(projectRoot) {
8219
+ const specifyPath = path17.join(projectRoot, ".specify");
8220
+ if (!await directoryExists(specifyPath)) {
8221
+ return null;
8222
+ }
8223
+ const specsPath = path17.join(specifyPath, "specs");
8224
+ const memoryPath = path17.join(projectRoot, "memory");
8225
+ const constitutionPath = path17.join(memoryPath, "constitution.md");
8226
+ const hasSpecs = await directoryExists(specsPath);
8227
+ const hasConstitution = await fileExists(constitutionPath);
8228
+ if (!hasSpecs) {
8229
+ return null;
8230
+ }
8231
+ const specs = [];
8232
+ const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
8233
+ for (const entry of specDirs) {
8234
+ if (entry.isDirectory()) {
8235
+ const specPath = path17.join(specsPath, entry.name);
8236
+ const specFile = path17.join(specPath, "spec.md");
8237
+ const planFile = path17.join(specPath, "plan.md");
8238
+ const tasksFile = path17.join(specPath, "tasks.md");
8239
+ const hasSpec = await fileExists(specFile);
8240
+ const hasPlan = await fileExists(planFile);
8241
+ const hasTasks = await fileExists(tasksFile);
8242
+ if (hasSpec || hasPlan) {
8243
+ let title2;
8244
+ let status;
8245
+ if (hasSpec) {
8246
+ const content = await fs11.readFile(specFile, "utf-8");
8247
+ title2 = extractTitle2(content);
8248
+ status = extractFrontmatterField(content, "status");
8249
+ }
8250
+ specs.push({
8251
+ id: entry.name,
8252
+ title: title2,
8253
+ path: specPath,
8254
+ status: hasTasks ? "in-progress" : status
8255
+ });
8256
+ }
8257
+ }
8258
+ }
8259
+ return {
8260
+ tool: "speckit",
8261
+ path: specifyPath,
8262
+ specCount: specs.length,
8263
+ specs,
8264
+ confidence: hasConstitution ? "high" : "medium"
8265
+ };
8266
+ }
8267
+ async function detectSdd(projectRoot) {
8268
+ const sddPath = path17.join(projectRoot, ".sdd");
8269
+ if (!await directoryExists(sddPath)) {
8270
+ return null;
8271
+ }
8272
+ const specsPath = path17.join(sddPath, "specs");
8273
+ const configPath = path17.join(sddPath, "config.yaml");
8274
+ if (!await directoryExists(specsPath)) {
8275
+ return null;
8276
+ }
8277
+ const specs = [];
8278
+ const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
8279
+ for (const entry of specDirs) {
8280
+ if (entry.isDirectory()) {
8281
+ const specPath = path17.join(specsPath, entry.name);
8282
+ const specFile = path17.join(specPath, "spec.md");
8283
+ if (await fileExists(specFile)) {
8284
+ const content = await fs11.readFile(specFile, "utf-8");
8285
+ const title2 = extractTitle2(content);
8286
+ const status = extractFrontmatterField(content, "status");
8287
+ specs.push({
8288
+ id: entry.name,
8289
+ title: title2,
8290
+ path: specPath,
8291
+ status
8292
+ });
8293
+ }
8294
+ }
8295
+ }
8296
+ return {
8297
+ tool: "sdd",
8298
+ path: sddPath,
8299
+ specCount: specs.length,
8300
+ specs,
8301
+ confidence: await fileExists(configPath) ? "high" : "medium"
8302
+ };
8303
+ }
8304
+ async function migrateFromOpenSpec(sourcePath, targetPath, options = {}) {
8305
+ try {
8306
+ const specsPath = path17.join(sourcePath, "specs");
8307
+ const targetSpecsPath = path17.join(targetPath, "specs");
8308
+ let specsCreated = 0;
8309
+ let specsSkipped = 0;
8310
+ const errors = [];
8311
+ if (!await directoryExists(specsPath)) {
8312
+ return failure(new ChangeError("OpenSpec specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
8313
+ }
8314
+ const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
8315
+ for (const entry of specDirs) {
8316
+ if (!entry.isDirectory()) continue;
8317
+ const sourceSpecPath = path17.join(specsPath, entry.name);
8318
+ const targetSpecPath = path17.join(targetSpecsPath, entry.name);
8319
+ if (await directoryExists(targetSpecPath)) {
8320
+ if (!options.overwrite) {
8321
+ specsSkipped++;
8322
+ continue;
8323
+ }
8324
+ }
8325
+ try {
8326
+ if (!options.dryRun) {
8327
+ await fs11.mkdir(targetSpecPath, { recursive: true });
8328
+ const files = await fs11.readdir(sourceSpecPath);
8329
+ for (const file of files) {
8330
+ const sourceFile = path17.join(sourceSpecPath, file);
8331
+ const targetFile = path17.join(targetSpecPath, file);
8332
+ const stat = await fs11.stat(sourceFile);
8333
+ if (stat.isFile()) {
8334
+ let content = await fs11.readFile(sourceFile, "utf-8");
8335
+ if (file === "spec.md") {
8336
+ content = convertOpenSpecToSdd(content, entry.name);
8337
+ }
8338
+ await fs11.writeFile(targetFile, content);
8339
+ }
8340
+ }
8341
+ }
8342
+ specsCreated++;
8343
+ } catch (error2) {
8344
+ errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
8345
+ }
8346
+ }
8347
+ return success({
8348
+ source: "openspec",
8349
+ targetPath,
8350
+ specsCreated,
8351
+ specsSkipped,
8352
+ errors
8353
+ });
8354
+ } catch (error2) {
8355
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8356
+ }
8357
+ }
8358
+ async function migrateFromSpecKit(sourcePath, targetPath, options = {}) {
8359
+ try {
8360
+ const specsPath = path17.join(sourcePath, "specs");
8361
+ const targetSpecsPath = path17.join(targetPath, "specs");
8362
+ let specsCreated = 0;
8363
+ let specsSkipped = 0;
8364
+ const errors = [];
8365
+ if (!await directoryExists(specsPath)) {
8366
+ return failure(new ChangeError("Spec Kit specs \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
8367
+ }
8368
+ const specDirs = await fs11.readdir(specsPath, { withFileTypes: true });
8369
+ for (const entry of specDirs) {
8370
+ if (!entry.isDirectory()) continue;
8371
+ const sourceSpecPath = path17.join(specsPath, entry.name);
8372
+ const targetSpecPath = path17.join(targetSpecsPath, entry.name);
8373
+ if (await directoryExists(targetSpecPath)) {
8374
+ if (!options.overwrite) {
8375
+ specsSkipped++;
8376
+ continue;
8377
+ }
8378
+ }
8379
+ try {
8380
+ if (!options.dryRun) {
8381
+ await fs11.mkdir(targetSpecPath, { recursive: true });
8382
+ const files = await fs11.readdir(sourceSpecPath);
8383
+ for (const file of files) {
8384
+ const sourceFile = path17.join(sourceSpecPath, file);
8385
+ const targetFile = path17.join(targetSpecPath, file);
8386
+ const stat = await fs11.stat(sourceFile);
8387
+ if (stat.isFile()) {
8388
+ let content = await fs11.readFile(sourceFile, "utf-8");
8389
+ if (file === "spec.md") {
8390
+ content = convertSpecKitToSdd(content, entry.name);
8391
+ }
8392
+ await fs11.writeFile(targetFile, content);
8393
+ }
8394
+ }
8395
+ }
8396
+ specsCreated++;
8397
+ } catch (error2) {
8398
+ errors.push(`${entry.name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
8399
+ }
8400
+ }
8401
+ return success({
8402
+ source: "speckit",
8403
+ targetPath,
8404
+ specsCreated,
8405
+ specsSkipped,
8406
+ errors
8407
+ });
8408
+ } catch (error2) {
8409
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
8410
+ }
8411
+ }
8412
+ function convertOpenSpecToSdd(content, specId) {
8413
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
8414
+ if (!frontmatterMatch) {
8415
+ const title2 = extractTitle2(content) || specId;
8416
+ return `---
8417
+ id: ${specId}
8418
+ title: ${title2}
8419
+ phase: migrated
8420
+ status: draft
8421
+ source: openspec
8422
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
8423
+ ---
8424
+
8425
+ ${content}`;
8426
+ }
8427
+ let newFrontmatter = frontmatterMatch[1];
8428
+ if (!newFrontmatter.includes("phase:")) {
8429
+ newFrontmatter += "\nphase: migrated";
8430
+ }
8431
+ if (!newFrontmatter.includes("source:")) {
8432
+ newFrontmatter += "\nsource: openspec";
8433
+ }
8434
+ if (!newFrontmatter.includes("migrated_at:")) {
8435
+ newFrontmatter += `
8436
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
8437
+ }
8438
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
8439
+ ${newFrontmatter}
8440
+ ---`);
8441
+ }
8442
+ function convertSpecKitToSdd(content, specId) {
8443
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
8444
+ if (!frontmatterMatch) {
8445
+ const title2 = extractTitle2(content) || specId;
8446
+ return `---
8447
+ id: ${specId}
8448
+ title: ${title2}
8449
+ phase: migrated
8450
+ status: draft
8451
+ source: speckit
8452
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}
8453
+ ---
8454
+
8455
+ ${content}`;
8456
+ }
8457
+ let newFrontmatter = frontmatterMatch[1];
8458
+ if (!newFrontmatter.includes("phase:")) {
8459
+ newFrontmatter += "\nphase: migrated";
8460
+ }
8461
+ if (!newFrontmatter.includes("source:")) {
8462
+ newFrontmatter += "\nsource: speckit";
8463
+ }
8464
+ if (!newFrontmatter.includes("migrated_at:")) {
8465
+ newFrontmatter += `
8466
+ migrated_at: ${(/* @__PURE__ */ new Date()).toISOString()}`;
8467
+ }
8468
+ return content.replace(/^---\n[\s\S]*?\n---/, `---
8469
+ ${newFrontmatter}
8470
+ ---`);
8471
+ }
8472
+ function extractTitle2(content) {
8473
+ const fmMatch = content.match(/^---\n[\s\S]*?title:\s*['"]?([^'"\n]+)['"]?\n[\s\S]*?\n---/);
8474
+ if (fmMatch) {
8475
+ return fmMatch[1].trim();
8476
+ }
8477
+ const h1Match = content.match(/^#\s+(.+)$/m);
8478
+ if (h1Match) {
8479
+ return h1Match[1].trim();
8480
+ }
8481
+ return void 0;
8482
+ }
8483
+ function extractFrontmatterField(content, field) {
8484
+ const regex = new RegExp(`^---\\n[\\s\\S]*?${field}:\\s*['"]?([^'"\\n]+)['"]?\\n[\\s\\S]*?\\n---`);
8485
+ const match = content.match(regex);
8486
+ return match ? match[1].trim() : void 0;
8487
+ }
8488
+
8489
+ // src/cli/commands/migrate.ts
8148
8490
  function registerMigrateCommand(program2) {
8149
8491
  const migrate = program2.command("migrate").description("\uAE30\uC874 \uBB38\uC11C\uB97C SDD \uD615\uC2DD\uC73C\uB85C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4");
8150
8492
  migrate.command("docs <source>").description("\uB9C8\uD06C\uB2E4\uC6B4 \uBB38\uC11C\uB97C spec.md \uD615\uC2DD\uC73C\uB85C \uBCC0\uD658\uD569\uB2C8\uB2E4").option("-o, --output <dir>", "\uCD9C\uB825 \uB514\uB809\uD1A0\uB9AC").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").action(async (source, options) => {
@@ -8171,6 +8513,30 @@ function registerMigrateCommand(program2) {
8171
8513
  process.exit(ExitCode.GENERAL_ERROR);
8172
8514
  }
8173
8515
  });
8516
+ migrate.command("detect").description("\uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uC678\uBD80 SDD \uB3C4\uAD6C\uB97C \uAC10\uC9C0\uD569\uB2C8\uB2E4").option("-p, --path <path>", "\uAC80\uC0C9 \uACBD\uB85C").action(async (options) => {
8517
+ try {
8518
+ await runDetect(options);
8519
+ } catch (error2) {
8520
+ error(error2 instanceof Error ? error2.message : String(error2));
8521
+ process.exit(ExitCode.GENERAL_ERROR);
8522
+ }
8523
+ });
8524
+ migrate.command("openspec [source]").description("OpenSpec \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
8525
+ try {
8526
+ await runMigrateOpenSpec(source, options);
8527
+ } catch (error2) {
8528
+ error(error2 instanceof Error ? error2.message : String(error2));
8529
+ process.exit(ExitCode.GENERAL_ERROR);
8530
+ }
8531
+ });
8532
+ migrate.command("speckit [source]").description("Spec Kit \uD504\uB85C\uC81D\uD2B8\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD569\uB2C8\uB2E4").option("--dry-run", "\uC2E4\uC81C \uD30C\uC77C \uC0DD\uC131 \uC5C6\uC774 \uBBF8\uB9AC\uBCF4\uAE30").option("--overwrite", "\uAE30\uC874 \uC2A4\uD399 \uB36E\uC5B4\uC4F0\uAE30").action(async (source, options) => {
8533
+ try {
8534
+ await runMigrateSpecKit(source, options);
8535
+ } catch (error2) {
8536
+ error(error2 instanceof Error ? error2.message : String(error2));
8537
+ process.exit(ExitCode.GENERAL_ERROR);
8538
+ }
8539
+ });
8174
8540
  }
8175
8541
  async function runMigrateDocs(source, options) {
8176
8542
  const projectRoot = await findSddRoot();
@@ -8178,10 +8544,10 @@ async function runMigrateDocs(source, options) {
8178
8544
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. --output \uC635\uC158\uC744 \uC0AC\uC6A9\uD558\uAC70\uB098 sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8179
8545
  process.exit(ExitCode.GENERAL_ERROR);
8180
8546
  }
8181
- const sourcePath = path17.resolve(source);
8547
+ const sourcePath = path18.resolve(source);
8182
8548
  let files = [];
8183
8549
  try {
8184
- const stat = await fs11.stat(sourcePath);
8550
+ const stat = await fs12.stat(sourcePath);
8185
8551
  if (stat.isDirectory()) {
8186
8552
  files = await collectMarkdownFiles(sourcePath);
8187
8553
  } else if (stat.isFile()) {
@@ -8197,7 +8563,7 @@ async function runMigrateDocs(source, options) {
8197
8563
  }
8198
8564
  info(`${files.length}\uAC1C \uD30C\uC77C \uBC1C\uACAC`);
8199
8565
  newline();
8200
- const outputDir = options.output ? path17.resolve(options.output) : path17.join(projectRoot, ".sdd", "specs");
8566
+ const outputDir = options.output ? path18.resolve(options.output) : path18.join(projectRoot, ".sdd", "specs");
8201
8567
  const summary = {
8202
8568
  total: files.length,
8203
8569
  succeeded: 0,
@@ -8209,10 +8575,10 @@ async function runMigrateDocs(source, options) {
8209
8575
  summary.results.push(result);
8210
8576
  if (result.success) {
8211
8577
  summary.succeeded++;
8212
- info(`\u2705 ${path17.basename(file)} \u2192 ${result.target}`);
8578
+ info(`\u2705 ${path18.basename(file)} \u2192 ${result.target}`);
8213
8579
  } else {
8214
8580
  summary.failed++;
8215
- error(`\u274C ${path17.basename(file)}: ${result.error}`);
8581
+ error(`\u274C ${path18.basename(file)}: ${result.error}`);
8216
8582
  }
8217
8583
  }
8218
8584
  newline();
@@ -8224,25 +8590,25 @@ async function runMigrateDocs(source, options) {
8224
8590
  }
8225
8591
  async function migrateDocument(filePath, outputDir, dryRun) {
8226
8592
  try {
8227
- const content = await fs11.readFile(filePath, "utf-8");
8593
+ const content = await fs12.readFile(filePath, "utf-8");
8228
8594
  const analysis = analyzeDocument(content);
8229
- const featureId = generateFeatureId(analysis.title || path17.basename(filePath, ".md"));
8595
+ const featureId = generateFeatureId(analysis.title || path18.basename(filePath, ".md"));
8230
8596
  const specContent = generateSpec({
8231
8597
  id: featureId,
8232
- title: analysis.title || path17.basename(filePath, ".md"),
8598
+ title: analysis.title || path18.basename(filePath, ".md"),
8233
8599
  description: analysis.description || "",
8234
8600
  requirements: analysis.requirements,
8235
8601
  scenarios: analysis.scenarios
8236
8602
  });
8237
- const targetDir = path17.join(outputDir, featureId);
8238
- const targetPath = path17.join(targetDir, "spec.md");
8603
+ const targetDir = path18.join(outputDir, featureId);
8604
+ const targetPath = path18.join(targetDir, "spec.md");
8239
8605
  if (!dryRun) {
8240
8606
  await ensureDir(targetDir);
8241
8607
  await writeFile(targetPath, specContent);
8242
8608
  }
8243
8609
  return {
8244
8610
  source: filePath,
8245
- target: path17.relative(process.cwd(), targetPath),
8611
+ target: path18.relative(process.cwd(), targetPath),
8246
8612
  success: true
8247
8613
  };
8248
8614
  } catch (error2) {
@@ -8287,14 +8653,14 @@ function analyzeDocument(content) {
8287
8653
  };
8288
8654
  }
8289
8655
  async function runAnalyze(file) {
8290
- const filePath = path17.resolve(file);
8656
+ const filePath = path18.resolve(file);
8291
8657
  if (!await fileExists(filePath)) {
8292
8658
  error(`\uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${file}`);
8293
8659
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
8294
8660
  }
8295
- const content = await fs11.readFile(filePath, "utf-8");
8661
+ const content = await fs12.readFile(filePath, "utf-8");
8296
8662
  const analysis = analyzeDocument(content);
8297
- info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${path17.basename(file)}`);
8663
+ info(`\u{1F4CA} \uBB38\uC11C \uBD84\uC11D: ${path18.basename(file)}`);
8298
8664
  newline();
8299
8665
  info(`\uC81C\uBAA9: ${analysis.title || "(\uC5C6\uC74C)"}`);
8300
8666
  info(`\uC124\uBA85: ${analysis.description || "(\uC5C6\uC74C)"}`);
@@ -8337,7 +8703,7 @@ async function runAnalyze(file) {
8337
8703
  }
8338
8704
  }
8339
8705
  async function runScan(dir, options) {
8340
- const dirPath = path17.resolve(dir);
8706
+ const dirPath = path18.resolve(dir);
8341
8707
  if (!await directoryExists(dirPath)) {
8342
8708
  error(`\uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dir}`);
8343
8709
  process.exit(ExitCode.FILE_SYSTEM_ERROR);
@@ -8353,7 +8719,7 @@ async function runScan(dir, options) {
8353
8719
  const results = [];
8354
8720
  for (const file of files) {
8355
8721
  try {
8356
- const content = await fs11.readFile(file, "utf-8");
8722
+ const content = await fs12.readFile(file, "utf-8");
8357
8723
  const analysis = analyzeDocument(content);
8358
8724
  results.push({ file, analysis });
8359
8725
  } catch {
@@ -8368,7 +8734,7 @@ async function runScan(dir, options) {
8368
8734
  const partial = [];
8369
8735
  const notReady = [];
8370
8736
  for (const { file, analysis } of results) {
8371
- const relativePath = path17.relative(process.cwd(), file);
8737
+ const relativePath = path18.relative(process.cwd(), file);
8372
8738
  if (analysis.hasRfc2119 && analysis.hasScenarios) {
8373
8739
  ready.push(relativePath);
8374
8740
  } else if (analysis.hasRfc2119 || analysis.hasScenarios || analysis.requirements.length > 0) {
@@ -8415,15 +8781,15 @@ async function collectMarkdownFiles(dirPath) {
8415
8781
  async function collectFilesWithExtensions(dirPath, extensions) {
8416
8782
  const files = [];
8417
8783
  async function scan(dir) {
8418
- const entries = await fs11.readdir(dir, { withFileTypes: true });
8784
+ const entries = await fs12.readdir(dir, { withFileTypes: true });
8419
8785
  for (const entry of entries) {
8420
- const fullPath = path17.join(dir, entry.name);
8786
+ const fullPath = path18.join(dir, entry.name);
8421
8787
  if (entry.isDirectory()) {
8422
8788
  if (!["node_modules", ".git", ".sdd", "dist", "build"].includes(entry.name)) {
8423
8789
  await scan(fullPath);
8424
8790
  }
8425
8791
  } else if (entry.isFile()) {
8426
- const ext = path17.extname(entry.name).toLowerCase();
8792
+ const ext = path18.extname(entry.name).toLowerCase();
8427
8793
  if (extensions.some((e) => e.toLowerCase() === ext)) {
8428
8794
  if (!["agents.md", "readme.md", "changelog.md", "license.md"].includes(entry.name.toLowerCase())) {
8429
8795
  files.push(fullPath);
@@ -8435,9 +8801,200 @@ async function collectFilesWithExtensions(dirPath, extensions) {
8435
8801
  await scan(dirPath);
8436
8802
  return files;
8437
8803
  }
8804
+ async function runDetect(options) {
8805
+ const projectRoot = options.path ? path18.resolve(options.path) : process.cwd();
8806
+ info("\u{1F50D} \uC678\uBD80 SDD \uB3C4\uAD6C \uAC10\uC9C0 \uC911...");
8807
+ info(` \uACBD\uB85C: ${projectRoot}`);
8808
+ newline();
8809
+ const result = await detectExternalTools(projectRoot);
8810
+ if (!result.success) {
8811
+ error(result.error.message);
8812
+ process.exit(ExitCode.GENERAL_ERROR);
8813
+ }
8814
+ const tools = result.data;
8815
+ if (tools.length === 0) {
8816
+ info("\uAC10\uC9C0\uB41C \uC678\uBD80 SDD \uB3C4\uAD6C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.");
8817
+ return;
8818
+ }
8819
+ for (const tool of tools) {
8820
+ const icon = getToolIcon(tool.tool);
8821
+ const confidence = getConfidenceLabel(tool.confidence);
8822
+ info(`${icon} ${getToolName(tool.tool)}`);
8823
+ info(` \uACBD\uB85C: ${tool.path}`);
8824
+ info(` \uC2E0\uB8B0\uB3C4: ${confidence}`);
8825
+ info(` \uC2A4\uD399 \uC218: ${tool.specCount}\uAC1C`);
8826
+ if (tool.specs.length > 0) {
8827
+ newline();
8828
+ info(" \uBC1C\uACAC\uB41C \uC2A4\uD399:");
8829
+ for (const spec of tool.specs.slice(0, 5)) {
8830
+ const status = spec.status ? ` [${spec.status}]` : "";
8831
+ listItem(`${spec.id}: ${spec.title || "(\uC81C\uBAA9 \uC5C6\uC74C)"}${status}`, 2);
8832
+ }
8833
+ if (tool.specs.length > 5) {
8834
+ info(` ... \uC678 ${tool.specs.length - 5}\uAC1C`);
8835
+ }
8836
+ }
8837
+ newline();
8838
+ }
8839
+ const openspec = tools.find((t) => t.tool === "openspec");
8840
+ const speckit = tools.find((t) => t.tool === "speckit");
8841
+ if (openspec || speckit) {
8842
+ info("\u{1F4A1} \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uBA85\uB839\uC5B4:");
8843
+ if (openspec) {
8844
+ listItem(`sdd migrate openspec "${openspec.path}"`, 1);
8845
+ }
8846
+ if (speckit) {
8847
+ listItem(`sdd migrate speckit "${speckit.path}"`, 1);
8848
+ }
8849
+ }
8850
+ }
8851
+ async function runMigrateOpenSpec(source, options) {
8852
+ const projectRoot = await findSddRoot();
8853
+ if (!projectRoot) {
8854
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8855
+ process.exit(ExitCode.GENERAL_ERROR);
8856
+ }
8857
+ let sourcePath;
8858
+ if (source) {
8859
+ sourcePath = path18.resolve(source);
8860
+ } else {
8861
+ const detectResult = await detectExternalTools(projectRoot);
8862
+ if (!detectResult.success) {
8863
+ error(detectResult.error.message);
8864
+ process.exit(ExitCode.GENERAL_ERROR);
8865
+ }
8866
+ const openspec = detectResult.data.find((t) => t.tool === "openspec");
8867
+ if (!openspec) {
8868
+ error("OpenSpec \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
8869
+ process.exit(ExitCode.GENERAL_ERROR);
8870
+ }
8871
+ sourcePath = openspec.path;
8872
+ }
8873
+ const sddPath = path18.join(projectRoot, ".sdd");
8874
+ info("\u{1F504} OpenSpec\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
8875
+ info(` \uC18C\uC2A4: ${sourcePath}`);
8876
+ info(` \uB300\uC0C1: ${sddPath}`);
8877
+ if (options.dryRun) {
8878
+ warn(" (dry-run \uBAA8\uB4DC)");
8879
+ }
8880
+ newline();
8881
+ const result = await migrateFromOpenSpec(sourcePath, sddPath, {
8882
+ dryRun: options.dryRun,
8883
+ overwrite: options.overwrite
8884
+ });
8885
+ if (!result.success) {
8886
+ error(result.error.message);
8887
+ process.exit(ExitCode.GENERAL_ERROR);
8888
+ }
8889
+ const data = result.data;
8890
+ success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
8891
+ info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
8892
+ info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
8893
+ if (data.errors.length > 0) {
8894
+ newline();
8895
+ warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
8896
+ for (const error2 of data.errors) {
8897
+ error(` - ${error2}`);
8898
+ }
8899
+ }
8900
+ if (options.dryRun) {
8901
+ newline();
8902
+ info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
8903
+ }
8904
+ }
8905
+ async function runMigrateSpecKit(source, options) {
8906
+ const projectRoot = await findSddRoot();
8907
+ if (!projectRoot) {
8908
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8909
+ process.exit(ExitCode.GENERAL_ERROR);
8910
+ }
8911
+ let sourcePath;
8912
+ if (source) {
8913
+ sourcePath = path18.resolve(source);
8914
+ } else {
8915
+ const detectResult = await detectExternalTools(projectRoot);
8916
+ if (!detectResult.success) {
8917
+ error(detectResult.error.message);
8918
+ process.exit(ExitCode.GENERAL_ERROR);
8919
+ }
8920
+ const speckit = detectResult.data.find((t) => t.tool === "speckit");
8921
+ if (!speckit) {
8922
+ error("Spec Kit \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uACBD\uB85C\uB97C \uC9C1\uC811 \uC9C0\uC815\uD558\uC138\uC694.");
8923
+ process.exit(ExitCode.GENERAL_ERROR);
8924
+ }
8925
+ sourcePath = speckit.path;
8926
+ }
8927
+ const sddPath = path18.join(projectRoot, ".sdd");
8928
+ info("\u{1F504} Spec Kit\uC5D0\uC11C \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC911...");
8929
+ info(` \uC18C\uC2A4: ${sourcePath}`);
8930
+ info(` \uB300\uC0C1: ${sddPath}`);
8931
+ if (options.dryRun) {
8932
+ warn(" (dry-run \uBAA8\uB4DC)");
8933
+ }
8934
+ newline();
8935
+ const result = await migrateFromSpecKit(sourcePath, sddPath, {
8936
+ dryRun: options.dryRun,
8937
+ overwrite: options.overwrite
8938
+ });
8939
+ if (!result.success) {
8940
+ error(result.error.message);
8941
+ process.exit(ExitCode.GENERAL_ERROR);
8942
+ }
8943
+ const data = result.data;
8944
+ success2("\u2705 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC");
8945
+ info(` \uC0DD\uC131: ${data.specsCreated}\uAC1C`);
8946
+ info(` \uC2A4\uD0B5: ${data.specsSkipped}\uAC1C`);
8947
+ if (data.errors.length > 0) {
8948
+ newline();
8949
+ warn("\u26A0\uFE0F \uC77C\uBD80 \uC624\uB958 \uBC1C\uC0DD:");
8950
+ for (const error2 of data.errors) {
8951
+ error(` - ${error2}`);
8952
+ }
8953
+ }
8954
+ if (options.dryRun) {
8955
+ newline();
8956
+ info("\uC2E4\uC81C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uC744 \uC218\uD589\uD558\uB824\uBA74 --dry-run \uC635\uC158\uC744 \uC81C\uAC70\uD558\uC138\uC694.");
8957
+ }
8958
+ }
8959
+ function getToolIcon(tool) {
8960
+ switch (tool) {
8961
+ case "openspec":
8962
+ return "\u{1F4E6}";
8963
+ case "speckit":
8964
+ return "\u{1F527}";
8965
+ case "sdd":
8966
+ return "\u{1F4CB}";
8967
+ default:
8968
+ return "\u2753";
8969
+ }
8970
+ }
8971
+ function getToolName(tool) {
8972
+ switch (tool) {
8973
+ case "openspec":
8974
+ return "OpenSpec";
8975
+ case "speckit":
8976
+ return "Spec Kit";
8977
+ case "sdd":
8978
+ return "SDD";
8979
+ default:
8980
+ return tool;
8981
+ }
8982
+ }
8983
+ function getConfidenceLabel(confidence) {
8984
+ switch (confidence) {
8985
+ case "high":
8986
+ return "\uB192\uC74C \u2713";
8987
+ case "medium":
8988
+ return "\uC911\uAC04";
8989
+ case "low":
8990
+ return "\uB0AE\uC74C";
8991
+ default:
8992
+ return confidence;
8993
+ }
8994
+ }
8438
8995
 
8439
8996
  // src/cli/commands/cicd.ts
8440
- import path18 from "path";
8997
+ import path19 from "path";
8441
8998
  init_errors();
8442
8999
  init_fs();
8443
9000
  function registerCicdCommand(program2) {
@@ -8489,16 +9046,16 @@ async function runSetup(platform, options) {
8489
9046
  listItem("PR/MR \uC0DD\uC131 \uC2DC \uC790\uB3D9\uC73C\uB85C \uC2A4\uD399 \uAC80\uC99D\uC774 \uC2E4\uD589\uB429\uB2C8\uB2E4");
8490
9047
  }
8491
9048
  async function setupGitHubActions(projectRoot, strict) {
8492
- const workflowDir = path18.join(projectRoot, ".github", "workflows");
9049
+ const workflowDir = path19.join(projectRoot, ".github", "workflows");
8493
9050
  await ensureDir(workflowDir);
8494
9051
  const workflowContent = generateGitHubWorkflow(strict);
8495
- const workflowPath = path18.join(workflowDir, "sdd-validate.yml");
9052
+ const workflowPath = path19.join(workflowDir, "sdd-validate.yml");
8496
9053
  await writeFile(workflowPath, workflowContent);
8497
9054
  info(`\u2705 GitHub Actions \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC0DD\uC131: .github/workflows/sdd-validate.yml`);
8498
9055
  }
8499
9056
  async function setupGitLabCI(projectRoot, strict) {
8500
9057
  const ciContent = generateGitLabCI(strict);
8501
- const ciPath = path18.join(projectRoot, ".gitlab-ci-sdd.yml");
9058
+ const ciPath = path19.join(projectRoot, ".gitlab-ci-sdd.yml");
8502
9059
  await writeFile(ciPath, ciContent);
8503
9060
  info(`\u2705 GitLab CI \uAD6C\uC131 \uC0DD\uC131: .gitlab-ci-sdd.yml`);
8504
9061
  info(" (\uAE30\uC874 .gitlab-ci.yml\uC5D0 include\uD558\uAC70\uB098 \uBCD1\uD569\uD558\uC138\uC694)");
@@ -8594,7 +9151,7 @@ async function runHooksSetup(type, options) {
8594
9151
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8595
9152
  process.exit(ExitCode.GENERAL_ERROR);
8596
9153
  }
8597
- const hooksDir = path18.join(projectRoot, ".husky");
9154
+ const hooksDir = path19.join(projectRoot, ".husky");
8598
9155
  if (options.install) {
8599
9156
  info("husky \uC124\uCE58 \uBC29\uBC95:");
8600
9157
  newline();
@@ -8608,7 +9165,7 @@ async function runHooksSetup(type, options) {
8608
9165
  const hooks = type ? [type] : ["pre-commit", "pre-push"];
8609
9166
  for (const hook of hooks) {
8610
9167
  const hookContent = generateHookScript(hook);
8611
- const hookPath = path18.join(hooksDir, hook);
9168
+ const hookPath = path19.join(hooksDir, hook);
8612
9169
  await writeFile(hookPath, hookContent);
8613
9170
  info(`\u2705 ${hook} \uD6C5 \uC0DD\uC131: .husky/${hook}`);
8614
9171
  }
@@ -8690,7 +9247,7 @@ async function runCiCheck(options) {
8690
9247
  let hasErrors = false;
8691
9248
  let hasWarnings = false;
8692
9249
  info("1. Constitution \uAC80\uC99D...");
8693
- const constitutionPath = path18.join(projectRoot, ".sdd", "constitution.md");
9250
+ const constitutionPath = path19.join(projectRoot, ".sdd", "constitution.md");
8694
9251
  if (await fileExists(constitutionPath)) {
8695
9252
  info(" \u2705 constitution.md \uC874\uC7AC");
8696
9253
  } else {
@@ -8698,7 +9255,7 @@ async function runCiCheck(options) {
8698
9255
  hasWarnings = true;
8699
9256
  }
8700
9257
  info("2. \uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC \uD655\uC778...");
8701
- const specsPath = path18.join(projectRoot, ".sdd", "specs");
9258
+ const specsPath = path19.join(projectRoot, ".sdd", "specs");
8702
9259
  if (await directoryExists(specsPath)) {
8703
9260
  info(" \u2705 specs/ \uB514\uB809\uD1A0\uB9AC \uC874\uC7AC");
8704
9261
  } else {
@@ -8708,7 +9265,7 @@ async function runCiCheck(options) {
8708
9265
  info("3. \uAE30\uBCF8 \uAD6C\uC870 \uD655\uC778...");
8709
9266
  const requiredDirs = ["changes", "archive", "templates"];
8710
9267
  for (const dir of requiredDirs) {
8711
- const dirPath = path18.join(projectRoot, ".sdd", dir);
9268
+ const dirPath = path19.join(projectRoot, ".sdd", dir);
8712
9269
  if (await directoryExists(dirPath)) {
8713
9270
  info(` \u2705 ${dir}/ \uC874\uC7AC`);
8714
9271
  } else {
@@ -8736,7 +9293,7 @@ async function runCiCheck(options) {
8736
9293
  }
8737
9294
 
8738
9295
  // src/cli/commands/transition.ts
8739
- import path19 from "path";
9296
+ import path20 from "path";
8740
9297
  init_errors();
8741
9298
  init_fs();
8742
9299
  function registerTransitionCommand(program2) {
@@ -8767,9 +9324,9 @@ async function runNewToChange(specId, options) {
8767
9324
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8768
9325
  process.exit(ExitCode.GENERAL_ERROR);
8769
9326
  }
8770
- const sddPath = path19.join(projectRoot, ".sdd");
8771
- const specsPath = path19.join(sddPath, "specs");
8772
- const specPath = path19.join(specsPath, specId, "spec.md");
9327
+ const sddPath = path20.join(projectRoot, ".sdd");
9328
+ const specsPath = path20.join(sddPath, "specs");
9329
+ const specPath = path20.join(specsPath, specId, "spec.md");
8773
9330
  if (!await fileExists(specPath)) {
8774
9331
  error(`\uC2A4\uD399\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specId}`);
8775
9332
  info("\uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uC2A4\uD399 \uBAA9\uB85D\uC740 `sdd list`\uB85C \uD655\uC778\uD558\uC138\uC694.");
@@ -8779,7 +9336,7 @@ async function runNewToChange(specId, options) {
8779
9336
  newline();
8780
9337
  info(`\uB300\uC0C1 \uC2A4\uD399: ${specId}`);
8781
9338
  const changeId = generateChangeId();
8782
- const changePath = path19.join(sddPath, "changes", changeId);
9339
+ const changePath = path20.join(sddPath, "changes", changeId);
8783
9340
  await ensureDir(changePath);
8784
9341
  const specContent = await readFile(specPath);
8785
9342
  if (!specContent.success) {
@@ -8789,11 +9346,11 @@ async function runNewToChange(specId, options) {
8789
9346
  const title2 = options.title || `${specId} \uAE30\uB2A5 \uD655\uC7A5`;
8790
9347
  const reason = options.reason || "new \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
8791
9348
  const proposalContent = generateTransitionProposal(specId, title2, reason, "new-to-change");
8792
- await writeFile(path19.join(changePath, "proposal.md"), proposalContent);
9349
+ await writeFile(path20.join(changePath, "proposal.md"), proposalContent);
8793
9350
  const deltaContent = generateDeltaTemplate(specId);
8794
- await writeFile(path19.join(changePath, "delta.md"), deltaContent);
9351
+ await writeFile(path20.join(changePath, "delta.md"), deltaContent);
8795
9352
  const tasksContent = generateTasksTemplate();
8796
- await writeFile(path19.join(changePath, "tasks.md"), tasksContent);
9353
+ await writeFile(path20.join(changePath, "tasks.md"), tasksContent);
8797
9354
  newline();
8798
9355
  success2(`\uC804\uD658 \uC644\uB8CC! \uBCC0\uACBD \uC81C\uC548\uC774 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
8799
9356
  newline();
@@ -8815,8 +9372,8 @@ async function runChangeToNew(changeId, options) {
8815
9372
  error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
8816
9373
  process.exit(ExitCode.GENERAL_ERROR);
8817
9374
  }
8818
- const sddPath = path19.join(projectRoot, ".sdd");
8819
- const changePath = path19.join(sddPath, "changes", changeId);
9375
+ const sddPath = path20.join(projectRoot, ".sdd");
9376
+ const changePath = path20.join(sddPath, "changes", changeId);
8820
9377
  if (!await directoryExists(changePath)) {
8821
9378
  error(`\uBCC0\uACBD \uC81C\uC548\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${changeId}`);
8822
9379
  info("\uC9C4\uD589 \uC911\uC778 \uBCC0\uACBD \uBAA9\uB85D\uC740 `sdd change -l`\uB85C \uD655\uC778\uD558\uC138\uC694.");
@@ -8825,7 +9382,7 @@ async function runChangeToNew(changeId, options) {
8825
9382
  info("=== \uC6CC\uD06C\uD50C\uB85C\uC6B0 \uC804\uD658: change \u2192 new ===");
8826
9383
  newline();
8827
9384
  info(`\uC6D0\uBCF8 \uBCC0\uACBD: ${changeId}`);
8828
- const proposalPath = path19.join(changePath, "proposal.md");
9385
+ const proposalPath = path20.join(changePath, "proposal.md");
8829
9386
  let extractedTitle = "";
8830
9387
  if (await fileExists(proposalPath)) {
8831
9388
  const proposalContent = await readFile(proposalPath);
@@ -8837,8 +9394,8 @@ async function runChangeToNew(changeId, options) {
8837
9394
  }
8838
9395
  }
8839
9396
  const featureName = options.name || extractedTitle.toLowerCase().replace(/\s+/g, "-") || `feature-from-${changeId}`;
8840
- const specsPath = path19.join(sddPath, "specs");
8841
- const newSpecPath = path19.join(specsPath, featureName);
9397
+ const specsPath = path20.join(sddPath, "specs");
9398
+ const newSpecPath = path20.join(specsPath, featureName);
8842
9399
  if (await directoryExists(newSpecPath)) {
8843
9400
  error(`\uC2A4\uD399\uC774 \uC774\uBBF8 \uC874\uC7AC\uD569\uB2C8\uB2E4: ${featureName}`);
8844
9401
  info("\uB2E4\uB978 \uC774\uB984\uC744 \uC9C0\uC815\uD558\uC138\uC694: --name <name>");
@@ -8847,12 +9404,12 @@ async function runChangeToNew(changeId, options) {
8847
9404
  await ensureDir(newSpecPath);
8848
9405
  const reason = options.reason || "change \uC6CC\uD06C\uD50C\uB85C\uC6B0\uC5D0\uC11C \uC804\uD658\uB428";
8849
9406
  const specContent = generateTransitionSpec(featureName, extractedTitle || featureName, reason, changeId);
8850
- await writeFile(path19.join(newSpecPath, "spec.md"), specContent);
9407
+ await writeFile(path20.join(newSpecPath, "spec.md"), specContent);
8851
9408
  const planContent = generatePlanTemplate(featureName);
8852
- await writeFile(path19.join(newSpecPath, "plan.md"), planContent);
9409
+ await writeFile(path20.join(newSpecPath, "plan.md"), planContent);
8853
9410
  const tasksContent = generateTasksTemplate();
8854
- await writeFile(path19.join(newSpecPath, "tasks.md"), tasksContent);
8855
- const statusPath = path19.join(changePath, ".status");
9411
+ await writeFile(path20.join(newSpecPath, "tasks.md"), tasksContent);
9412
+ const statusPath = path20.join(changePath, ".status");
8856
9413
  await writeFile(statusPath, JSON.stringify({
8857
9414
  status: "transitioned",
8858
9415
  transitionedTo: featureName,
@@ -9086,6 +9643,1135 @@ status: pending
9086
9643
  `;
9087
9644
  }
9088
9645
 
9646
+ // src/cli/commands/watch.ts
9647
+ import path22 from "path";
9648
+
9649
+ // src/core/watch/watcher.ts
9650
+ import chokidar from "chokidar";
9651
+ import path21 from "path";
9652
+ import { EventEmitter } from "events";
9653
+ var SpecWatcher = class extends EventEmitter {
9654
+ watcher = null;
9655
+ specsPath;
9656
+ debounceMs;
9657
+ debounceTimer = null;
9658
+ pendingEvents = [];
9659
+ isRunning = false;
9660
+ constructor(options) {
9661
+ super();
9662
+ this.specsPath = options.specsPath;
9663
+ this.debounceMs = options.debounceMs ?? 500;
9664
+ }
9665
+ /**
9666
+ * 감시 시작
9667
+ */
9668
+ start() {
9669
+ if (this.isRunning) {
9670
+ return;
9671
+ }
9672
+ const ignored = [
9673
+ "**/node_modules/**",
9674
+ "**/.git/**",
9675
+ "**/.*"
9676
+ ];
9677
+ this.watcher = chokidar.watch(this.specsPath, {
9678
+ persistent: true,
9679
+ ignoreInitial: true,
9680
+ ignored,
9681
+ awaitWriteFinish: {
9682
+ stabilityThreshold: 100,
9683
+ pollInterval: 100
9684
+ }
9685
+ });
9686
+ this.watcher.on("add", (filePath) => this.handleEvent("add", filePath)).on("change", (filePath) => this.handleEvent("change", filePath)).on("unlink", (filePath) => this.handleEvent("unlink", filePath)).on("error", (error2) => this.emit("error", error2)).on("ready", () => {
9687
+ this.isRunning = true;
9688
+ this.emit("ready");
9689
+ });
9690
+ }
9691
+ /**
9692
+ * 감시 중지
9693
+ */
9694
+ async stop() {
9695
+ if (this.debounceTimer) {
9696
+ clearTimeout(this.debounceTimer);
9697
+ this.debounceTimer = null;
9698
+ }
9699
+ if (this.watcher) {
9700
+ await this.watcher.close();
9701
+ this.watcher = null;
9702
+ }
9703
+ this.isRunning = false;
9704
+ this.pendingEvents = [];
9705
+ }
9706
+ /**
9707
+ * 실행 상태 확인
9708
+ */
9709
+ get running() {
9710
+ return this.isRunning;
9711
+ }
9712
+ /**
9713
+ * 파일 이벤트 처리
9714
+ */
9715
+ handleEvent(type, filePath) {
9716
+ if (!filePath.endsWith(".md")) {
9717
+ return;
9718
+ }
9719
+ const event = {
9720
+ type,
9721
+ path: filePath,
9722
+ relativePath: path21.relative(this.specsPath, filePath),
9723
+ timestamp: /* @__PURE__ */ new Date()
9724
+ };
9725
+ this.pendingEvents.push(event);
9726
+ if (this.debounceTimer) {
9727
+ clearTimeout(this.debounceTimer);
9728
+ }
9729
+ this.debounceTimer = setTimeout(() => {
9730
+ this.flushEvents();
9731
+ }, this.debounceMs);
9732
+ }
9733
+ /**
9734
+ * 대기 중인 이벤트 처리
9735
+ */
9736
+ flushEvents() {
9737
+ if (this.pendingEvents.length === 0) {
9738
+ return;
9739
+ }
9740
+ const events = [...this.pendingEvents];
9741
+ this.pendingEvents = [];
9742
+ this.debounceTimer = null;
9743
+ this.emit("change", events);
9744
+ }
9745
+ };
9746
+ function createWatcher(options) {
9747
+ return new SpecWatcher(options);
9748
+ }
9749
+
9750
+ // src/cli/commands/watch.ts
9751
+ init_fs();
9752
+ init_errors();
9753
+ function registerWatchCommand(program2) {
9754
+ program2.command("watch").description("\uC2A4\uD399 \uD30C\uC77C \uBCC0\uACBD\uC744 \uC2E4\uC2DC\uAC04 \uAC10\uC2DC\uD558\uACE0 \uC790\uB3D9 \uAC80\uC99D\uD569\uB2C8\uB2E4").option("--no-validate", "\uC790\uB3D9 \uAC80\uC99D \uBE44\uD65C\uC131\uD654").option("--impact", "\uC601\uD5A5\uB3C4 \uBD84\uC11D \uD3EC\uD568").option("-q, --quiet", "\uC131\uACF5 \uC2DC \uCD9C\uB825 \uC0DD\uB7B5").option("--debounce <ms>", "\uB514\uBC14\uC6B4\uC2A4 \uC2DC\uAC04 (\uAE30\uBCF8: 500ms)", "500").action(async (options) => {
9755
+ try {
9756
+ await runWatch(options);
9757
+ } catch (error2) {
9758
+ error(error2 instanceof Error ? error2.message : String(error2));
9759
+ process.exit(ExitCode.GENERAL_ERROR);
9760
+ }
9761
+ });
9762
+ }
9763
+ async function runWatch(options) {
9764
+ const projectRoot = await findSddRoot();
9765
+ if (!projectRoot) {
9766
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
9767
+ process.exit(ExitCode.GENERAL_ERROR);
9768
+ }
9769
+ const sddPath = path22.join(projectRoot, ".sdd");
9770
+ const specsPath = path22.join(sddPath, "specs");
9771
+ const debounceMs = parseInt(options.debounce || "500", 10);
9772
+ info("\u{1F441}\uFE0F Watch \uBAA8\uB4DC \uC2DC\uC791");
9773
+ info(` \uACBD\uB85C: ${specsPath}`);
9774
+ info(` \uB514\uBC14\uC6B4\uC2A4: ${debounceMs}ms`);
9775
+ info(` \uAC80\uC99D: ${options.validate !== false ? "\uD65C\uC131\uD654" : "\uBE44\uD65C\uC131\uD654"}`);
9776
+ newline();
9777
+ info("\uD30C\uC77C \uBCC0\uACBD\uC744 \uAC10\uC2DC \uC911... (Ctrl+C\uB85C \uC885\uB8CC)");
9778
+ newline();
9779
+ const watcher = createWatcher({
9780
+ specsPath,
9781
+ debounceMs
9782
+ });
9783
+ let validationCount = 0;
9784
+ let errorCount = 0;
9785
+ watcher.on("change", async (events) => {
9786
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString();
9787
+ const addCount = events.filter((e) => e.type === "add").length;
9788
+ const changeCount = events.filter((e) => e.type === "change").length;
9789
+ const unlinkCount = events.filter((e) => e.type === "unlink").length;
9790
+ const parts = [];
9791
+ if (addCount > 0) parts.push(`\uCD94\uAC00 ${addCount}`);
9792
+ if (changeCount > 0) parts.push(`\uC218\uC815 ${changeCount}`);
9793
+ if (unlinkCount > 0) parts.push(`\uC0AD\uC81C ${unlinkCount}`);
9794
+ info(`[${timestamp}] \uBCC0\uACBD \uAC10\uC9C0: ${parts.join(", ")}`);
9795
+ for (const event of events) {
9796
+ const icon = event.type === "add" ? "\u2795" : event.type === "change" ? "\u270F\uFE0F" : "\u274C";
9797
+ info(` ${icon} ${event.relativePath}`);
9798
+ }
9799
+ if (options.validate !== false) {
9800
+ newline();
9801
+ info("\u{1F50D} \uAC80\uC99D \uC2E4\uD589 \uC911...");
9802
+ const result = await validateSpecs(sddPath, { strict: false });
9803
+ validationCount++;
9804
+ if (result.success) {
9805
+ const data = result.data;
9806
+ const hasErrors = data.results.some((r) => r.errors.length > 0);
9807
+ const hasWarnings = data.results.some((r) => r.warnings.length > 0);
9808
+ if (hasErrors) {
9809
+ errorCount++;
9810
+ error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.errorCount}\uAC1C \uC5D0\uB7EC, ${data.warningCount}\uAC1C \uACBD\uACE0`);
9811
+ for (const specResult of data.results) {
9812
+ if (specResult.errors.length > 0) {
9813
+ error(` ${specResult.file}:`);
9814
+ for (const err of specResult.errors) {
9815
+ error(` - ${err}`);
9816
+ }
9817
+ }
9818
+ }
9819
+ } else if (hasWarnings) {
9820
+ if (!options.quiet) {
9821
+ warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warningCount}\uAC1C \uACBD\uACE0`);
9822
+ }
9823
+ } else {
9824
+ if (!options.quiet) {
9825
+ success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.validCount}\uAC1C \uC2A4\uD399)`);
9826
+ }
9827
+ }
9828
+ } else {
9829
+ errorCount++;
9830
+ error(`\u274C \uAC80\uC99D \uC624\uB958: ${result.error.message}`);
9831
+ }
9832
+ newline();
9833
+ }
9834
+ });
9835
+ watcher.on("error", (error2) => {
9836
+ error(`\uAC10\uC2DC \uC624\uB958: ${error2.message}`);
9837
+ });
9838
+ watcher.on("ready", () => {
9839
+ success2("\u2705 \uAC10\uC2DC \uC900\uBE44 \uC644\uB8CC");
9840
+ newline();
9841
+ });
9842
+ const cleanup = async () => {
9843
+ newline();
9844
+ info("Watch \uBAA8\uB4DC \uC885\uB8CC \uC911...");
9845
+ await watcher.stop();
9846
+ newline();
9847
+ info("\u{1F4CA} \uC138\uC158 \uC694\uC57D:");
9848
+ info(` \uAC80\uC99D \uC2E4\uD589: ${validationCount}\uD68C`);
9849
+ info(` \uC5D0\uB7EC \uBC1C\uC0DD: ${errorCount}\uD68C`);
9850
+ process.exit(0);
9851
+ };
9852
+ process.on("SIGINT", cleanup);
9853
+ process.on("SIGTERM", cleanup);
9854
+ watcher.start();
9855
+ await new Promise(() => {
9856
+ });
9857
+ }
9858
+
9859
+ // src/cli/commands/quality.ts
9860
+ import path24 from "path";
9861
+
9862
+ // src/core/quality/analyzer.ts
9863
+ init_types();
9864
+ init_errors();
9865
+ init_fs();
9866
+ import path23 from "path";
9867
+ import { promises as fs13 } from "fs";
9868
+ function getGrade(percentage) {
9869
+ if (percentage >= 90) return "A";
9870
+ if (percentage >= 80) return "B";
9871
+ if (percentage >= 70) return "C";
9872
+ if (percentage >= 60) return "D";
9873
+ return "F";
9874
+ }
9875
+ function scoreRfc2119(content) {
9876
+ const maxScore = 10;
9877
+ const details = [];
9878
+ const suggestions = [];
9879
+ const keywords = ["SHALL", "MUST", "SHOULD", "MAY", "SHALL NOT", "MUST NOT", "SHOULD NOT"];
9880
+ const found = [];
9881
+ for (const kw of keywords) {
9882
+ const regex = new RegExp(`\\b${kw}\\b`, "gi");
9883
+ const matches = content.match(regex);
9884
+ if (matches && matches.length > 0) {
9885
+ found.push(`${kw}: ${matches.length}\uAC1C`);
9886
+ }
9887
+ }
9888
+ let score = 0;
9889
+ if (found.length > 0) {
9890
+ score = Math.min(maxScore, found.length * 2);
9891
+ details.push(`\uBC1C\uACAC\uB41C \uD0A4\uC6CC\uB4DC: ${found.join(", ")}`);
9892
+ } else {
9893
+ details.push("RFC 2119 \uD0A4\uC6CC\uB4DC\uAC00 \uBC1C\uACAC\uB418\uC9C0 \uC54A\uC74C");
9894
+ suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 SHALL, MUST, SHOULD, MAY \uD0A4\uC6CC\uB4DC\uB97C \uC0AC\uC6A9\uD558\uC138\uC694");
9895
+ }
9896
+ return {
9897
+ name: "RFC 2119 \uD0A4\uC6CC\uB4DC",
9898
+ score,
9899
+ maxScore,
9900
+ percentage: score / maxScore * 100,
9901
+ details,
9902
+ suggestions
9903
+ };
9904
+ }
9905
+ function scoreScenarios(content) {
9906
+ const maxScore = 20;
9907
+ const details = [];
9908
+ const suggestions = [];
9909
+ const givenCount = (content.match(/\*\*GIVEN\*\*|\bGIVEN\b/gi) || []).length;
9910
+ const whenCount = (content.match(/\*\*WHEN\*\*|\bWHEN\b/gi) || []).length;
9911
+ const thenCount = (content.match(/\*\*THEN\*\*|\bTHEN\b/gi) || []).length;
9912
+ const scenarioCount = Math.min(givenCount, whenCount, thenCount);
9913
+ let score = 0;
9914
+ if (scenarioCount > 0) {
9915
+ score = Math.min(maxScore, scenarioCount * 5);
9916
+ details.push(`\uC644\uC804\uD55C \uC2DC\uB098\uB9AC\uC624: ${scenarioCount}\uAC1C`);
9917
+ details.push(`GIVEN: ${givenCount}, WHEN: ${whenCount}, THEN: ${thenCount}`);
9918
+ } else {
9919
+ details.push("GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uAC00 \uC5C6\uC74C");
9920
+ suggestions.push("\uCD5C\uC18C 2\uAC1C \uC774\uC0C1\uC758 GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624\uB97C \uC791\uC131\uD558\uC138\uC694");
9921
+ }
9922
+ if (scenarioCount < 2 && scenarioCount > 0) {
9923
+ suggestions.push("\uCD94\uAC00 \uC2DC\uB098\uB9AC\uC624 \uC791\uC131\uC744 \uAD8C\uC7A5\uD569\uB2C8\uB2E4 (\uCD5C\uC18C 2\uAC1C)");
9924
+ }
9925
+ return {
9926
+ name: "GIVEN-WHEN-THEN \uC2DC\uB098\uB9AC\uC624",
9927
+ score,
9928
+ maxScore,
9929
+ percentage: score / maxScore * 100,
9930
+ details,
9931
+ suggestions
9932
+ };
9933
+ }
9934
+ function scoreRequirements(content) {
9935
+ const maxScore = 15;
9936
+ const details = [];
9937
+ const suggestions = [];
9938
+ const reqIdPattern = /REQ-\d+|REQ-[A-Z]+-\d+/gi;
9939
+ const reqIds = content.match(reqIdPattern) || [];
9940
+ const hasRequirementsSection = /^##\s*(요구사항|Requirements)/im.test(content);
9941
+ let score = 0;
9942
+ if (hasRequirementsSection) {
9943
+ score += 5;
9944
+ details.push("\uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC774 \uC874\uC7AC\uD568");
9945
+ } else {
9946
+ suggestions.push("## \uC694\uAD6C\uC0AC\uD56D \uC139\uC158\uC744 \uCD94\uAC00\uD558\uC138\uC694");
9947
+ }
9948
+ if (reqIds.length > 0) {
9949
+ score += Math.min(10, reqIds.length * 2);
9950
+ details.push(`\uC694\uAD6C\uC0AC\uD56D ID: ${reqIds.length}\uAC1C (${[...new Set(reqIds)].slice(0, 3).join(", ")}...)`);
9951
+ } else {
9952
+ suggestions.push("\uC694\uAD6C\uC0AC\uD56D\uC5D0 REQ-01 \uD615\uC2DD\uC758 ID\uB97C \uBD80\uC5EC\uD558\uC138\uC694");
9953
+ }
9954
+ return {
9955
+ name: "\uC694\uAD6C\uC0AC\uD56D \uBA85\uD655\uC131",
9956
+ score,
9957
+ maxScore,
9958
+ percentage: score / maxScore * 100,
9959
+ details,
9960
+ suggestions
9961
+ };
9962
+ }
9963
+ function scoreDependencies(spec) {
9964
+ const maxScore = 10;
9965
+ const details = [];
9966
+ const suggestions = [];
9967
+ let score = 0;
9968
+ if (spec.frontmatter.depends) {
9969
+ const deps = Array.isArray(spec.frontmatter.depends) ? spec.frontmatter.depends : [spec.frontmatter.depends];
9970
+ if (deps.length > 0 && deps[0] !== null) {
9971
+ score = maxScore;
9972
+ details.push(`\uC758\uC874\uC131: ${deps.join(", ")}`);
9973
+ } else {
9974
+ score = 5;
9975
+ details.push("\uC758\uC874\uC131 \uC5C6\uC74C (\uBA85\uC2DC\uC801 \uC120\uC5B8)");
9976
+ }
9977
+ } else {
9978
+ details.push("\uC758\uC874\uC131 \uD544\uB4DC\uAC00 \uC5C6\uC74C");
9979
+ suggestions.push("frontmatter\uC5D0 depends \uD544\uB4DC\uB97C \uCD94\uAC00\uD558\uC138\uC694");
9980
+ }
9981
+ return {
9982
+ name: "\uC758\uC874\uC131 \uBA85\uC2DC",
9983
+ score,
9984
+ maxScore,
9985
+ percentage: score / maxScore * 100,
9986
+ details,
9987
+ suggestions
9988
+ };
9989
+ }
9990
+ function scoreStructure(content) {
9991
+ const maxScore = 15;
9992
+ const details = [];
9993
+ const suggestions = [];
9994
+ const requiredSections = [
9995
+ { pattern: /^#\s+.+/m, name: "\uC81C\uBAA9 (H1)" },
9996
+ { pattern: /^##\s*(요구사항|Requirements)/im, name: "\uC694\uAD6C\uC0AC\uD56D \uC139\uC158" },
9997
+ { pattern: /^##\s*(시나리오|Scenario)/im, name: "\uC2DC\uB098\uB9AC\uC624 \uC139\uC158" }
9998
+ ];
9999
+ const optionalSections = [
10000
+ { pattern: /^##\s*(개요|Overview|설명|Description)/im, name: "\uAC1C\uC694/\uC124\uBA85 \uC139\uC158" },
10001
+ { pattern: /^##\s*(제약|Constraints|제한)/im, name: "\uC81C\uC57D\uC0AC\uD56D \uC139\uC158" },
10002
+ { pattern: /^##\s*(비고|Notes|참고)/im, name: "\uBE44\uACE0 \uC139\uC158" }
10003
+ ];
10004
+ let score = 0;
10005
+ const foundRequired = [];
10006
+ const missingRequired = [];
10007
+ for (const section of requiredSections) {
10008
+ if (section.pattern.test(content)) {
10009
+ foundRequired.push(section.name);
10010
+ score += 4;
10011
+ } else {
10012
+ missingRequired.push(section.name);
10013
+ }
10014
+ }
10015
+ for (const section of optionalSections) {
10016
+ if (section.pattern.test(content)) {
10017
+ score += 1;
10018
+ }
10019
+ }
10020
+ score = Math.min(maxScore, score);
10021
+ if (foundRequired.length > 0) {
10022
+ details.push(`\uD544\uC218 \uC139\uC158: ${foundRequired.join(", ")}`);
10023
+ }
10024
+ if (missingRequired.length > 0) {
10025
+ suggestions.push(`\uB204\uB77D\uB41C \uC139\uC158: ${missingRequired.join(", ")}`);
10026
+ }
10027
+ return {
10028
+ name: "\uBB38\uC11C \uAD6C\uC870",
10029
+ score,
10030
+ maxScore,
10031
+ percentage: score / maxScore * 100,
10032
+ details,
10033
+ suggestions
10034
+ };
10035
+ }
10036
+ function scoreConstitution(spec, hasConstitution) {
10037
+ const maxScore = 10;
10038
+ const details = [];
10039
+ const suggestions = [];
10040
+ let score = 0;
10041
+ if (!hasConstitution) {
10042
+ score = maxScore;
10043
+ details.push("Constitution \uBBF8\uC124\uC815 (\uAC80\uC0AC \uC0DD\uB7B5)");
10044
+ } else if (spec.frontmatter.constitution_version) {
10045
+ score = maxScore;
10046
+ details.push(`Constitution \uBC84\uC804: ${spec.frontmatter.constitution_version}`);
10047
+ } else {
10048
+ details.push("constitution_version \uD544\uB4DC \uC5C6\uC74C");
10049
+ suggestions.push("frontmatter\uC5D0 constitution_version\uC744 \uCD94\uAC00\uD558\uC138\uC694");
10050
+ }
10051
+ return {
10052
+ name: "Constitution \uC900\uC218",
10053
+ score,
10054
+ maxScore,
10055
+ percentage: score / maxScore * 100,
10056
+ details,
10057
+ suggestions
10058
+ };
10059
+ }
10060
+ function scoreLinks(content) {
10061
+ const maxScore = 10;
10062
+ const details = [];
10063
+ const suggestions = [];
10064
+ const linkPattern = /\[([^\]]+)\]\(([^)]+)\)/g;
10065
+ const links = [...content.matchAll(linkPattern)];
10066
+ let score = 5;
10067
+ if (links.length > 0) {
10068
+ score = Math.min(maxScore, 5 + links.length);
10069
+ details.push(`\uB9C1\uD06C: ${links.length}\uAC1C`);
10070
+ } else {
10071
+ details.push("\uB9C1\uD06C \uC5C6\uC74C");
10072
+ suggestions.push("\uAD00\uB828 \uBB38\uC11C\uB098 \uC678\uBD80 \uCC38\uC870 \uB9C1\uD06C\uB97C \uCD94\uAC00\uD558\uBA74 \uC88B\uC2B5\uB2C8\uB2E4");
10073
+ }
10074
+ return {
10075
+ name: "\uCC38\uC870 \uB9C1\uD06C",
10076
+ score,
10077
+ maxScore,
10078
+ percentage: score / maxScore * 100,
10079
+ details,
10080
+ suggestions
10081
+ };
10082
+ }
10083
+ function scoreMetadata(spec) {
10084
+ const maxScore = 10;
10085
+ const details = [];
10086
+ const suggestions = [];
10087
+ const requiredFields = ["id", "title", "status"];
10088
+ const optionalFields = ["created", "updated", "author", "version"];
10089
+ let score = 0;
10090
+ const missingRequired = [];
10091
+ for (const field of requiredFields) {
10092
+ if (spec.frontmatter[field]) {
10093
+ score += 2;
10094
+ } else {
10095
+ missingRequired.push(field);
10096
+ }
10097
+ }
10098
+ for (const field of optionalFields) {
10099
+ if (spec.frontmatter[field]) {
10100
+ score += 1;
10101
+ }
10102
+ }
10103
+ score = Math.min(maxScore, score);
10104
+ const presentFields = Object.keys(spec.frontmatter).filter(
10105
+ (k) => spec.frontmatter[k] !== null && spec.frontmatter[k] !== void 0
10106
+ );
10107
+ details.push(`\uBA54\uD0C0\uB370\uC774\uD130 \uD544\uB4DC: ${presentFields.length}\uAC1C`);
10108
+ if (missingRequired.length > 0) {
10109
+ suggestions.push(`\uD544\uC218 \uD544\uB4DC \uB204\uB77D: ${missingRequired.join(", ")}`);
10110
+ }
10111
+ return {
10112
+ name: "\uBA54\uD0C0\uB370\uC774\uD130 \uC644\uC131\uB3C4",
10113
+ score,
10114
+ maxScore,
10115
+ percentage: score / maxScore * 100,
10116
+ details,
10117
+ suggestions
10118
+ };
10119
+ }
10120
+ async function analyzeSpecQuality(specPath, sddPath) {
10121
+ try {
10122
+ if (!await fileExists(specPath)) {
10123
+ return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${specPath}`));
10124
+ }
10125
+ const contentResult = await readFile(specPath);
10126
+ if (!contentResult.success) {
10127
+ return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC744 \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10128
+ }
10129
+ const content = contentResult.data;
10130
+ const parseResult = parseSpec(content);
10131
+ if (!parseResult.success) {
10132
+ return failure(new ChangeError(`\uC2A4\uD399 \uD30C\uC2F1 \uC2E4\uD328: ${parseResult.error.message}`));
10133
+ }
10134
+ const spec = parseResult.data;
10135
+ const constitutionPath = path23.join(sddPath, "constitution.md");
10136
+ const hasConstitution = await fileExists(constitutionPath);
10137
+ const items = [
10138
+ scoreRfc2119(content),
10139
+ scoreScenarios(content),
10140
+ scoreRequirements(content),
10141
+ scoreDependencies(spec),
10142
+ scoreStructure(content),
10143
+ scoreConstitution(spec, hasConstitution),
10144
+ scoreLinks(content),
10145
+ scoreMetadata(spec)
10146
+ ];
10147
+ const totalScore = items.reduce((sum, item) => sum + item.score, 0);
10148
+ const maxScore = items.reduce((sum, item) => sum + item.maxScore, 0);
10149
+ const percentage = Math.round(totalScore / maxScore * 100);
10150
+ const grade = getGrade(percentage);
10151
+ const topSuggestions = items.filter((item) => item.suggestions.length > 0).sort((a, b) => a.percentage - b.percentage).slice(0, 3).flatMap((item) => item.suggestions);
10152
+ const specId = spec.frontmatter.id || path23.basename(path23.dirname(specPath));
10153
+ const summary = `\uC2A4\uD399 '${specId}'\uC758 \uD488\uC9C8 \uC810\uC218: ${totalScore}/${maxScore} (${percentage}%, \uB4F1\uAE09: ${grade})`;
10154
+ return success({
10155
+ specId,
10156
+ specPath,
10157
+ totalScore,
10158
+ maxScore,
10159
+ percentage,
10160
+ grade,
10161
+ items,
10162
+ summary,
10163
+ topSuggestions
10164
+ });
10165
+ } catch (error2) {
10166
+ return failure(
10167
+ new ChangeError(
10168
+ `\uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
10169
+ )
10170
+ );
10171
+ }
10172
+ }
10173
+ async function analyzeProjectQuality(sddPath) {
10174
+ try {
10175
+ const specsPath = path23.join(sddPath, "specs");
10176
+ if (!await directoryExists(specsPath)) {
10177
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10178
+ }
10179
+ const specFiles = [];
10180
+ await findSpecFiles2(specsPath, specFiles);
10181
+ if (specFiles.length === 0) {
10182
+ return failure(new ChangeError("\uC2A4\uD399 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
10183
+ }
10184
+ const specResults = [];
10185
+ for (const specFile of specFiles) {
10186
+ const result = await analyzeSpecQuality(specFile, sddPath);
10187
+ if (result.success) {
10188
+ specResults.push(result.data);
10189
+ }
10190
+ }
10191
+ if (specResults.length === 0) {
10192
+ return failure(new ChangeError("\uBD84\uC11D \uAC00\uB2A5\uD55C \uC2A4\uD399\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."));
10193
+ }
10194
+ const totalPercentage = specResults.reduce((sum, r) => sum + r.percentage, 0);
10195
+ const averagePercentage = Math.round(totalPercentage / specResults.length);
10196
+ const averageScore = Math.round(
10197
+ specResults.reduce((sum, r) => sum + r.totalScore, 0) / specResults.length
10198
+ );
10199
+ const grade = getGrade(averagePercentage);
10200
+ const summary = `\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8: \uD3C9\uADE0 ${averagePercentage}% (\uB4F1\uAE09: ${grade}), ${specResults.length}\uAC1C \uC2A4\uD399 \uBD84\uC11D`;
10201
+ return success({
10202
+ averageScore,
10203
+ averagePercentage,
10204
+ grade,
10205
+ totalSpecs: specResults.length,
10206
+ specResults,
10207
+ summary
10208
+ });
10209
+ } catch (error2) {
10210
+ return failure(
10211
+ new ChangeError(
10212
+ `\uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D \uC2E4\uD328: ${error2 instanceof Error ? error2.message : String(error2)}`
10213
+ )
10214
+ );
10215
+ }
10216
+ }
10217
+ async function findSpecFiles2(dir, files) {
10218
+ const entries = await fs13.readdir(dir, { withFileTypes: true });
10219
+ for (const entry of entries) {
10220
+ const fullPath = path23.join(dir, entry.name);
10221
+ if (entry.isDirectory()) {
10222
+ await findSpecFiles2(fullPath, files);
10223
+ } else if (entry.name === "spec.md") {
10224
+ files.push(fullPath);
10225
+ }
10226
+ }
10227
+ }
10228
+ function formatQualityResult(result) {
10229
+ const lines = [];
10230
+ const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10231
+ lines.push(`\u{1F4CA} \uD488\uC9C8 \uBD84\uC11D: ${result.specId}`);
10232
+ lines.push(` ${gradeIcon} \uB4F1\uAE09: ${result.grade} (${result.percentage}%)`);
10233
+ lines.push(` \u{1F4C8} \uC810\uC218: ${result.totalScore}/${result.maxScore}`);
10234
+ lines.push("");
10235
+ lines.push("\u{1F4CB} \uD56D\uBAA9\uBCC4 \uC810\uC218:");
10236
+ for (const item of result.items) {
10237
+ const icon = item.percentage >= 80 ? "\u2705" : item.percentage >= 60 ? "\u{1F7E1}" : "\u{1F534}";
10238
+ lines.push(` ${icon} ${item.name}: ${item.score}/${item.maxScore} (${Math.round(item.percentage)}%)`);
10239
+ for (const detail of item.details) {
10240
+ lines.push(` \u2514\u2500 ${detail}`);
10241
+ }
10242
+ }
10243
+ lines.push("");
10244
+ if (result.topSuggestions.length > 0) {
10245
+ lines.push("\u{1F4A1} \uAC1C\uC120 \uC81C\uC548:");
10246
+ for (const suggestion of result.topSuggestions) {
10247
+ lines.push(` - ${suggestion}`);
10248
+ }
10249
+ }
10250
+ return lines.join("\n");
10251
+ }
10252
+ function formatProjectQualityResult(result) {
10253
+ const lines = [];
10254
+ const gradeIcon = result.grade === "A" ? "\u{1F3C6}" : result.grade === "B" ? "\u2705" : result.grade === "C" ? "\u{1F7E1}" : result.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10255
+ lines.push("\u{1F4CA} \uD504\uB85C\uC81D\uD2B8 \uD488\uC9C8 \uBD84\uC11D");
10256
+ lines.push(` ${gradeIcon} \uD3C9\uADE0 \uB4F1\uAE09: ${result.grade} (${result.averagePercentage}%)`);
10257
+ lines.push(` \u{1F4C8} \uBD84\uC11D\uB41C \uC2A4\uD399: ${result.totalSpecs}\uAC1C`);
10258
+ lines.push("");
10259
+ const gradeCount = { A: 0, B: 0, C: 0, D: 0, F: 0 };
10260
+ for (const spec of result.specResults) {
10261
+ gradeCount[spec.grade]++;
10262
+ }
10263
+ lines.push("\u{1F4C8} \uB4F1\uAE09 \uBD84\uD3EC:");
10264
+ if (gradeCount.A > 0) lines.push(` \u{1F3C6} A: ${gradeCount.A}\uAC1C`);
10265
+ if (gradeCount.B > 0) lines.push(` \u2705 B: ${gradeCount.B}\uAC1C`);
10266
+ if (gradeCount.C > 0) lines.push(` \u{1F7E1} C: ${gradeCount.C}\uAC1C`);
10267
+ if (gradeCount.D > 0) lines.push(` \u{1F7E0} D: ${gradeCount.D}\uAC1C`);
10268
+ if (gradeCount.F > 0) lines.push(` \u{1F534} F: ${gradeCount.F}\uAC1C`);
10269
+ lines.push("");
10270
+ lines.push("\u{1F4CB} \uC2A4\uD399\uBCC4 \uC810\uC218:");
10271
+ const sortedSpecs = [...result.specResults].sort((a, b) => b.percentage - a.percentage);
10272
+ for (const spec of sortedSpecs) {
10273
+ const icon = spec.grade === "A" ? "\u{1F3C6}" : spec.grade === "B" ? "\u2705" : spec.grade === "C" ? "\u{1F7E1}" : spec.grade === "D" ? "\u{1F7E0}" : "\u{1F534}";
10274
+ lines.push(` ${icon} ${spec.specId}: ${spec.percentage}% (${spec.grade})`);
10275
+ }
10276
+ return lines.join("\n");
10277
+ }
10278
+
10279
+ // src/cli/commands/quality.ts
10280
+ init_fs();
10281
+ init_errors();
10282
+ function registerQualityCommand(program2) {
10283
+ program2.command("quality [feature]").description("\uC2A4\uD399 \uD488\uC9C8\uC744 \uBD84\uC11D\uD558\uACE0 \uC810\uC218\uB97C \uC0B0\uCD9C\uD569\uB2C8\uB2E4").option("-a, --all", "\uC804\uCCB4 \uD504\uB85C\uC81D\uD2B8 \uBD84\uC11D").option("--json", "JSON \uD615\uC2DD \uCD9C\uB825").option("--min-score <score>", "\uCD5C\uC18C \uC810\uC218 \uAE30\uC900 (\uC774\uD558 \uC2DC \uC5D0\uB7EC)", "0").action(async (feature, options) => {
10284
+ try {
10285
+ await runQuality(feature, options);
10286
+ } catch (error2) {
10287
+ error(error2 instanceof Error ? error2.message : String(error2));
10288
+ process.exit(ExitCode.GENERAL_ERROR);
10289
+ }
10290
+ });
10291
+ }
10292
+ async function runQuality(feature, options) {
10293
+ const projectRoot = await findSddRoot();
10294
+ if (!projectRoot) {
10295
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
10296
+ process.exit(ExitCode.GENERAL_ERROR);
10297
+ }
10298
+ const sddPath = path24.join(projectRoot, ".sdd");
10299
+ const minScore = parseInt(options.minScore || "0", 10);
10300
+ if (options.all || !feature) {
10301
+ const result2 = await analyzeProjectQuality(sddPath);
10302
+ if (!result2.success) {
10303
+ error(result2.error.message);
10304
+ process.exit(ExitCode.GENERAL_ERROR);
10305
+ }
10306
+ if (options.json) {
10307
+ console.log(JSON.stringify(result2.data, null, 2));
10308
+ } else {
10309
+ console.log(formatProjectQualityResult(result2.data));
10310
+ }
10311
+ if (result2.data.averagePercentage < minScore) {
10312
+ newline();
10313
+ error(`\uD488\uC9C8 \uC810\uC218\uAC00 \uCD5C\uC18C \uAE30\uC900(${minScore}%) \uBBF8\uB2EC\uC785\uB2C8\uB2E4.`);
10314
+ process.exit(ExitCode.VALIDATION_ERROR);
10315
+ }
10316
+ return;
10317
+ }
10318
+ const specPath = path24.join(sddPath, "specs", feature, "spec.md");
10319
+ const result = await analyzeSpecQuality(specPath, sddPath);
10320
+ if (!result.success) {
10321
+ error(result.error.message);
10322
+ process.exit(ExitCode.GENERAL_ERROR);
10323
+ }
10324
+ if (options.json) {
10325
+ console.log(JSON.stringify(result.data, null, 2));
10326
+ } else {
10327
+ console.log(formatQualityResult(result.data));
10328
+ }
10329
+ if (result.data.percentage < minScore) {
10330
+ newline();
10331
+ error(`\uD488\uC9C8 \uC810\uC218\uAC00 \uCD5C\uC18C \uAE30\uC900(${minScore}%) \uBBF8\uB2EC\uC785\uB2C8\uB2E4.`);
10332
+ process.exit(ExitCode.VALIDATION_ERROR);
10333
+ }
10334
+ }
10335
+
10336
+ // src/cli/commands/report.ts
10337
+ import path26 from "path";
10338
+
10339
+ // src/core/report/reporter.ts
10340
+ init_types();
10341
+ init_errors();
10342
+ import path25 from "path";
10343
+ import fs14 from "fs/promises";
10344
+ init_fs();
10345
+ async function loadSpecList(specsPath) {
10346
+ try {
10347
+ if (!await fileExists(specsPath)) {
10348
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10349
+ }
10350
+ const result = await readDir(specsPath);
10351
+ if (!result.success) {
10352
+ return failure(new ChangeError("\uC2A4\uD399 \uB514\uB809\uD1A0\uB9AC\uB97C \uC77D\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4."));
10353
+ }
10354
+ const specs = [];
10355
+ for (const entry of result.data) {
10356
+ const featurePath = path25.join(specsPath, entry);
10357
+ const stat = await fs14.stat(featurePath);
10358
+ if (stat.isDirectory()) {
10359
+ const specFile = path25.join(featurePath, "spec.md");
10360
+ if (await fileExists(specFile)) {
10361
+ const content = await fs14.readFile(specFile, "utf-8");
10362
+ const metadata = parseSpecMetadata2(content);
10363
+ specs.push({
10364
+ id: entry,
10365
+ title: metadata?.title,
10366
+ phase: metadata?.phase,
10367
+ status: metadata?.status,
10368
+ description: metadata?.description
10369
+ });
10370
+ }
10371
+ }
10372
+ }
10373
+ return success(specs);
10374
+ } catch (error2) {
10375
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
10376
+ }
10377
+ }
10378
+ function parseSpecMetadata2(content) {
10379
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
10380
+ if (!frontmatterMatch) return null;
10381
+ const frontmatter = frontmatterMatch[1];
10382
+ const result = {};
10383
+ const lines = frontmatter.split("\n");
10384
+ for (const line of lines) {
10385
+ const match = line.match(/^(\w+):\s*['"]?([^'"]+)['"]?$/);
10386
+ if (match) {
10387
+ result[match[1]] = match[2].trim();
10388
+ }
10389
+ }
10390
+ return {
10391
+ title: result.title,
10392
+ phase: result.phase,
10393
+ status: result.status,
10394
+ description: result.description
10395
+ };
10396
+ }
10397
+ async function generateReport(sddPath, options) {
10398
+ try {
10399
+ const specsPath = path25.join(sddPath, "specs");
10400
+ const specsResult = await loadSpecList(specsPath);
10401
+ if (!specsResult.success) {
10402
+ return failure(specsResult.error);
10403
+ }
10404
+ const specs = specsResult.data;
10405
+ const reportData = {
10406
+ title: options.title || "SDD \uD504\uB85C\uC81D\uD2B8 \uB9AC\uD3EC\uD2B8",
10407
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
10408
+ projectPath: sddPath,
10409
+ specs,
10410
+ summary: {
10411
+ totalSpecs: specs.length,
10412
+ byPhase: {},
10413
+ byStatus: {}
10414
+ }
10415
+ };
10416
+ for (const spec of specs) {
10417
+ const phase = spec.phase || "unknown";
10418
+ reportData.summary.byPhase[phase] = (reportData.summary.byPhase[phase] || 0) + 1;
10419
+ const status = spec.status || "unknown";
10420
+ reportData.summary.byStatus[status] = (reportData.summary.byStatus[status] || 0) + 1;
10421
+ }
10422
+ if (options.includeQuality !== false) {
10423
+ const qualityResult = await analyzeProjectQuality(sddPath);
10424
+ if (qualityResult.success) {
10425
+ reportData.quality = qualityResult.data;
10426
+ reportData.summary.averageQuality = qualityResult.data.averagePercentage;
10427
+ }
10428
+ }
10429
+ if (options.includeValidation !== false) {
10430
+ const validationResult = await validateSpecs(sddPath, { strict: false });
10431
+ if (validationResult.success) {
10432
+ reportData.validation = validationResult.data;
10433
+ reportData.summary.validationErrors = validationResult.data.errorCount;
10434
+ reportData.summary.validationWarnings = validationResult.data.warningCount;
10435
+ }
10436
+ }
10437
+ let content;
10438
+ switch (options.format) {
10439
+ case "html":
10440
+ content = renderHtmlReport(reportData);
10441
+ break;
10442
+ case "markdown":
10443
+ content = renderMarkdownReport(reportData);
10444
+ break;
10445
+ case "json":
10446
+ content = JSON.stringify(reportData, null, 2);
10447
+ break;
10448
+ default:
10449
+ return failure(new ChangeError(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${options.format}`));
10450
+ }
10451
+ if (options.outputPath) {
10452
+ await fs14.mkdir(path25.dirname(options.outputPath), { recursive: true });
10453
+ await fs14.writeFile(options.outputPath, content, "utf-8");
10454
+ }
10455
+ return success({
10456
+ format: options.format,
10457
+ content,
10458
+ outputPath: options.outputPath
10459
+ });
10460
+ } catch (error2) {
10461
+ return failure(new ChangeError(error2 instanceof Error ? error2.message : String(error2)));
10462
+ }
10463
+ }
10464
+ function renderHtmlReport(data) {
10465
+ const gradeColor = (grade) => {
10466
+ switch (grade) {
10467
+ case "A":
10468
+ return "#22c55e";
10469
+ case "B":
10470
+ return "#84cc16";
10471
+ case "C":
10472
+ return "#eab308";
10473
+ case "D":
10474
+ return "#f97316";
10475
+ case "F":
10476
+ return "#ef4444";
10477
+ default:
10478
+ return "#6b7280";
10479
+ }
10480
+ };
10481
+ const statusBadge = (status) => {
10482
+ const colors = {
10483
+ draft: "#6b7280",
10484
+ review: "#3b82f6",
10485
+ approved: "#22c55e",
10486
+ implemented: "#8b5cf6",
10487
+ deprecated: "#ef4444"
10488
+ };
10489
+ const color = colors[status] || "#6b7280";
10490
+ return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;">${status}</span>`;
10491
+ };
10492
+ const specRows = data.specs.map((spec) => `
10493
+ <tr>
10494
+ <td><strong>${spec.id}</strong></td>
10495
+ <td>${spec.title || "-"}</td>
10496
+ <td>${spec.phase || "-"}</td>
10497
+ <td>${statusBadge(spec.status || "unknown")}</td>
10498
+ <td>${spec.description || "-"}</td>
10499
+ </tr>
10500
+ `).join("");
10501
+ const qualityRows = data.quality?.results.map((q) => `
10502
+ <tr>
10503
+ <td>${q.specId}</td>
10504
+ <td>${q.percentage}%</td>
10505
+ <td style="color:${gradeColor(q.grade)};font-weight:bold;">${q.grade}</td>
10506
+ <td>${q.totalScore}/${q.maxScore}</td>
10507
+ </tr>
10508
+ `).join("") || "";
10509
+ const validationRows = data.validation?.results.map((v) => `
10510
+ <tr>
10511
+ <td>${v.file}</td>
10512
+ <td style="color:${v.errors.length > 0 ? "#ef4444" : "#22c55e"};">${v.errors.length > 0 ? "\u274C \uC2E4\uD328" : "\u2705 \uD1B5\uACFC"}</td>
10513
+ <td>${v.errors.length}</td>
10514
+ <td>${v.warnings.length}</td>
10515
+ </tr>
10516
+ `).join("") || "";
10517
+ return `<!DOCTYPE html>
10518
+ <html lang="ko">
10519
+ <head>
10520
+ <meta charset="UTF-8">
10521
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
10522
+ <title>${data.title}</title>
10523
+ <style>
10524
+ * { box-sizing: border-box; margin: 0; padding: 0; }
10525
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #1f2937; background: #f9fafb; padding: 2rem; }
10526
+ .container { max-width: 1200px; margin: 0 auto; }
10527
+ h1 { font-size: 2rem; margin-bottom: 0.5rem; color: #111827; }
10528
+ h2 { font-size: 1.5rem; margin: 2rem 0 1rem; color: #374151; border-bottom: 2px solid #e5e7eb; padding-bottom: 0.5rem; }
10529
+ .meta { color: #6b7280; margin-bottom: 2rem; }
10530
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
10531
+ .card { background: white; border-radius: 8px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
10532
+ .card-title { font-size: 0.875rem; color: #6b7280; margin-bottom: 0.5rem; }
10533
+ .card-value { font-size: 2rem; font-weight: bold; color: #111827; }
10534
+ .card-value.success { color: #22c55e; }
10535
+ .card-value.warning { color: #eab308; }
10536
+ .card-value.error { color: #ef4444; }
10537
+ table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 2rem; }
10538
+ th, td { padding: 0.75rem 1rem; text-align: left; border-bottom: 1px solid #e5e7eb; }
10539
+ th { background: #f3f4f6; font-weight: 600; color: #374151; }
10540
+ tr:hover { background: #f9fafb; }
10541
+ .phase-chart { display: flex; gap: 0.5rem; flex-wrap: wrap; }
10542
+ .phase-item { background: #e0e7ff; color: #3730a3; padding: 0.25rem 0.75rem; border-radius: 9999px; font-size: 0.875rem; }
10543
+ </style>
10544
+ </head>
10545
+ <body>
10546
+ <div class="container">
10547
+ <h1>${data.title}</h1>
10548
+ <p class="meta">\uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")} | \uACBD\uB85C: ${data.projectPath}</p>
10549
+
10550
+ <div class="summary">
10551
+ <div class="card">
10552
+ <div class="card-title">\uCD1D \uC2A4\uD399 \uC218</div>
10553
+ <div class="card-value">${data.summary.totalSpecs}</div>
10554
+ </div>
10555
+ ${data.summary.averageQuality !== void 0 ? `
10556
+ <div class="card">
10557
+ <div class="card-title">\uD3C9\uADE0 \uD488\uC9C8</div>
10558
+ <div class="card-value ${data.summary.averageQuality >= 80 ? "success" : data.summary.averageQuality >= 60 ? "warning" : "error"}">${data.summary.averageQuality.toFixed(1)}%</div>
10559
+ </div>` : ""}
10560
+ ${data.summary.validationErrors !== void 0 ? `
10561
+ <div class="card">
10562
+ <div class="card-title">\uAC80\uC99D \uC5D0\uB7EC</div>
10563
+ <div class="card-value ${data.summary.validationErrors === 0 ? "success" : "error"}">${data.summary.validationErrors}</div>
10564
+ </div>` : ""}
10565
+ ${data.summary.validationWarnings !== void 0 ? `
10566
+ <div class="card">
10567
+ <div class="card-title">\uAC80\uC99D \uACBD\uACE0</div>
10568
+ <div class="card-value ${data.summary.validationWarnings === 0 ? "success" : "warning"}">${data.summary.validationWarnings}</div>
10569
+ </div>` : ""}
10570
+ </div>
10571
+
10572
+ <h2>Phase\uBCC4 \uBD84\uD3EC</h2>
10573
+ <div class="phase-chart">
10574
+ ${Object.entries(data.summary.byPhase).map(([phase, count]) => `
10575
+ <span class="phase-item">${phase}: ${count}</span>
10576
+ `).join("")}
10577
+ </div>
10578
+
10579
+ <h2>\uC2A4\uD399 \uBAA9\uB85D</h2>
10580
+ <table>
10581
+ <thead>
10582
+ <tr>
10583
+ <th>ID</th>
10584
+ <th>\uC81C\uBAA9</th>
10585
+ <th>Phase</th>
10586
+ <th>\uC0C1\uD0DC</th>
10587
+ <th>\uC124\uBA85</th>
10588
+ </tr>
10589
+ </thead>
10590
+ <tbody>
10591
+ ${specRows}
10592
+ </tbody>
10593
+ </table>
10594
+
10595
+ ${data.quality ? `
10596
+ <h2>\uD488\uC9C8 \uBD84\uC11D</h2>
10597
+ <table>
10598
+ <thead>
10599
+ <tr>
10600
+ <th>\uC2A4\uD399 ID</th>
10601
+ <th>\uC810\uC218</th>
10602
+ <th>\uB4F1\uAE09</th>
10603
+ <th>\uC0C1\uC138</th>
10604
+ </tr>
10605
+ </thead>
10606
+ <tbody>
10607
+ ${qualityRows}
10608
+ </tbody>
10609
+ </table>` : ""}
10610
+
10611
+ ${data.validation ? `
10612
+ <h2>\uAC80\uC99D \uACB0\uACFC</h2>
10613
+ <table>
10614
+ <thead>
10615
+ <tr>
10616
+ <th>\uD30C\uC77C</th>
10617
+ <th>\uC0C1\uD0DC</th>
10618
+ <th>\uC5D0\uB7EC</th>
10619
+ <th>\uACBD\uACE0</th>
10620
+ </tr>
10621
+ </thead>
10622
+ <tbody>
10623
+ ${validationRows}
10624
+ </tbody>
10625
+ </table>` : ""}
10626
+
10627
+ <footer style="margin-top:3rem;padding-top:1rem;border-top:1px solid #e5e7eb;color:#6b7280;font-size:0.875rem;">
10628
+ Generated by SDD CLI v0.5.0
10629
+ </footer>
10630
+ </div>
10631
+ </body>
10632
+ </html>`;
10633
+ }
10634
+ function renderMarkdownReport(data) {
10635
+ const lines = [];
10636
+ lines.push(`# ${data.title}`);
10637
+ lines.push("");
10638
+ lines.push(`> \uC0DD\uC131: ${new Date(data.generatedAt).toLocaleString("ko-KR")}`);
10639
+ lines.push(`> \uACBD\uB85C: ${data.projectPath}`);
10640
+ lines.push("");
10641
+ lines.push("## \uC694\uC57D");
10642
+ lines.push("");
10643
+ lines.push(`| \uD56D\uBAA9 | \uAC12 |`);
10644
+ lines.push(`|------|-----|`);
10645
+ lines.push(`| \uCD1D \uC2A4\uD399 \uC218 | ${data.summary.totalSpecs} |`);
10646
+ if (data.summary.averageQuality !== void 0) {
10647
+ lines.push(`| \uD3C9\uADE0 \uD488\uC9C8 | ${data.summary.averageQuality.toFixed(1)}% |`);
10648
+ }
10649
+ if (data.summary.validationErrors !== void 0) {
10650
+ lines.push(`| \uAC80\uC99D \uC5D0\uB7EC | ${data.summary.validationErrors} |`);
10651
+ }
10652
+ if (data.summary.validationWarnings !== void 0) {
10653
+ lines.push(`| \uAC80\uC99D \uACBD\uACE0 | ${data.summary.validationWarnings} |`);
10654
+ }
10655
+ lines.push("");
10656
+ lines.push("## Phase\uBCC4 \uBD84\uD3EC");
10657
+ lines.push("");
10658
+ for (const [phase, count] of Object.entries(data.summary.byPhase)) {
10659
+ lines.push(`- **${phase}**: ${count}\uAC1C`);
10660
+ }
10661
+ lines.push("");
10662
+ lines.push("## \uC0C1\uD0DC\uBCC4 \uBD84\uD3EC");
10663
+ lines.push("");
10664
+ for (const [status, count] of Object.entries(data.summary.byStatus)) {
10665
+ lines.push(`- **${status}**: ${count}\uAC1C`);
10666
+ }
10667
+ lines.push("");
10668
+ lines.push("## \uC2A4\uD399 \uBAA9\uB85D");
10669
+ lines.push("");
10670
+ lines.push("| ID | \uC81C\uBAA9 | Phase | \uC0C1\uD0DC |");
10671
+ lines.push("|----|------|-------|------|");
10672
+ for (const spec of data.specs) {
10673
+ lines.push(`| ${spec.id} | ${spec.title || "-"} | ${spec.phase || "-"} | ${spec.status || "-"} |`);
10674
+ }
10675
+ lines.push("");
10676
+ if (data.quality) {
10677
+ lines.push("## \uD488\uC9C8 \uBD84\uC11D");
10678
+ lines.push("");
10679
+ lines.push(`\uD3C9\uADE0 \uC810\uC218: **${data.quality.averagePercentage.toFixed(1)}%** (${data.quality.averageGrade})`);
10680
+ lines.push("");
10681
+ lines.push("| \uC2A4\uD399 ID | \uC810\uC218 | \uB4F1\uAE09 |");
10682
+ lines.push("|---------|------|------|");
10683
+ for (const q of data.quality.results) {
10684
+ lines.push(`| ${q.specId} | ${q.percentage}% | ${q.grade} |`);
10685
+ }
10686
+ lines.push("");
10687
+ }
10688
+ if (data.validation) {
10689
+ lines.push("## \uAC80\uC99D \uACB0\uACFC");
10690
+ lines.push("");
10691
+ lines.push(`- \uAC80\uC99D\uB41C \uC2A4\uD399: ${data.validation.validCount}\uAC1C`);
10692
+ lines.push(`- \uC5D0\uB7EC: ${data.validation.errorCount}\uAC1C`);
10693
+ lines.push(`- \uACBD\uACE0: ${data.validation.warningCount}\uAC1C`);
10694
+ lines.push("");
10695
+ if (data.validation.errorCount > 0 || data.validation.warningCount > 0) {
10696
+ lines.push("### \uC0C1\uC138 \uACB0\uACFC");
10697
+ lines.push("");
10698
+ for (const v of data.validation.results) {
10699
+ if (v.errors.length > 0 || v.warnings.length > 0) {
10700
+ lines.push(`#### ${v.file}`);
10701
+ for (const e of v.errors) {
10702
+ lines.push(`- ${e}`);
10703
+ }
10704
+ for (const w of v.warnings) {
10705
+ lines.push(`- ${w}`);
10706
+ }
10707
+ lines.push("");
10708
+ }
10709
+ }
10710
+ }
10711
+ }
10712
+ lines.push("---");
10713
+ lines.push("*Generated by SDD CLI v0.5.0*");
10714
+ return lines.join("\n");
10715
+ }
10716
+
10717
+ // src/cli/commands/report.ts
10718
+ init_fs();
10719
+ init_errors();
10720
+ function registerReportCommand(program2) {
10721
+ program2.command("report").description("\uC2A4\uD399 \uB9AC\uD3EC\uD2B8\uB97C \uC0DD\uC131\uD569\uB2C8\uB2E4").option("-f, --format <format>", "\uCD9C\uB825 \uD615\uC2DD (html, markdown, json)", "html").option("-o, --output <path>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C").option("--title <title>", "\uB9AC\uD3EC\uD2B8 \uC81C\uBAA9").option("--no-quality", "\uD488\uC9C8 \uBD84\uC11D \uC81C\uC678").option("--no-validation", "\uAC80\uC99D \uACB0\uACFC \uC81C\uC678").action(async (options) => {
10722
+ try {
10723
+ await runReport(options);
10724
+ } catch (error2) {
10725
+ error(error2 instanceof Error ? error2.message : String(error2));
10726
+ process.exit(ExitCode.GENERAL_ERROR);
10727
+ }
10728
+ });
10729
+ }
10730
+ async function runReport(options) {
10731
+ const projectRoot = await findSddRoot();
10732
+ if (!projectRoot) {
10733
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. `sdd init`\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
10734
+ process.exit(ExitCode.GENERAL_ERROR);
10735
+ }
10736
+ const sddPath = path26.join(projectRoot, ".sdd");
10737
+ const format = options.format || "html";
10738
+ if (!["html", "markdown", "json"].includes(format)) {
10739
+ error(`\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD\uC785\uB2C8\uB2E4: ${format}`);
10740
+ info("\uC9C0\uC6D0 \uD615\uC2DD: html, markdown, json");
10741
+ process.exit(ExitCode.VALIDATION_ERROR);
10742
+ }
10743
+ let outputPath = options.output;
10744
+ if (!outputPath) {
10745
+ const ext = format === "markdown" ? "md" : format;
10746
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
10747
+ outputPath = path26.join(projectRoot, `sdd-report-${timestamp}.${ext}`);
10748
+ } else if (!path26.isAbsolute(outputPath)) {
10749
+ outputPath = path26.join(projectRoot, outputPath);
10750
+ }
10751
+ info("\u{1F4CA} \uB9AC\uD3EC\uD2B8 \uC0DD\uC131 \uC911...");
10752
+ info(` \uD615\uC2DD: ${format}`);
10753
+ info(` \uD488\uC9C8 \uBD84\uC11D: ${options.quality !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
10754
+ info(` \uAC80\uC99D \uACB0\uACFC: ${options.validation !== false ? "\uD3EC\uD568" : "\uC81C\uC678"}`);
10755
+ newline();
10756
+ const result = await generateReport(sddPath, {
10757
+ format,
10758
+ outputPath,
10759
+ title: options.title,
10760
+ includeQuality: options.quality !== false,
10761
+ includeValidation: options.validation !== false
10762
+ });
10763
+ if (!result.success) {
10764
+ error(result.error.message);
10765
+ process.exit(ExitCode.GENERAL_ERROR);
10766
+ }
10767
+ success2(`\u2705 \uB9AC\uD3EC\uD2B8\uAC00 \uC0DD\uC131\uB418\uC5C8\uC2B5\uB2C8\uB2E4.`);
10768
+ info(` \uACBD\uB85C: ${result.data.outputPath}`);
10769
+ if (!options.output && format === "json") {
10770
+ newline();
10771
+ console.log(result.data.content);
10772
+ }
10773
+ }
10774
+
9089
10775
  // src/cli/index.ts
9090
10776
  var require2 = createRequire(import.meta.url);
9091
10777
  var pkg = require2("../../package.json");
@@ -9104,6 +10790,9 @@ registerStartCommand(program);
9104
10790
  registerMigrateCommand(program);
9105
10791
  registerCicdCommand(program);
9106
10792
  registerTransitionCommand(program);
10793
+ registerWatchCommand(program);
10794
+ registerQualityCommand(program);
10795
+ registerReportCommand(program);
9107
10796
  function run() {
9108
10797
  program.parse();
9109
10798
  }