sdd-tool 0.7.1 → 1.0.1

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
@@ -19,7 +19,9 @@ var init_codes = __esm({
19
19
  VALIDATION_FAILED: 2,
20
20
  CONSTITUTION_VIOLATION: 3,
21
21
  FILE_SYSTEM_ERROR: 4,
22
- USER_CANCELLED: 5
22
+ USER_CANCELLED: 5,
23
+ INIT_ERROR: 6,
24
+ VALIDATION_ERROR: 7
23
25
  };
24
26
  ErrorCode = {
25
27
  // 일반 에러 (E0xx)
@@ -128,10 +130,10 @@ var init_base = __esm({
128
130
  };
129
131
  FileSystemError = class extends SddError {
130
132
  path;
131
- constructor(code, path36) {
132
- super(code, formatMessage(code, path36), ExitCode.FILE_SYSTEM_ERROR);
133
+ constructor(code, path42) {
134
+ super(code, formatMessage(code, path42), ExitCode.FILE_SYSTEM_ERROR);
133
135
  this.name = "FileSystemError";
134
- this.path = path36;
136
+ this.path = path42;
135
137
  }
136
138
  };
137
139
  ValidationError = class extends SddError {
@@ -2132,70 +2134,92 @@ erDiagram
2132
2134
  },
2133
2135
  {
2134
2136
  name: "sdd.prepare",
2135
- content: `\uAE30\uB2A5 \uAD6C\uD604\uC5D0 \uD544\uC694\uD55C \uD658\uACBD\uC744 \uC900\uBE44\uD569\uB2C8\uB2E4.
2137
+ content: `\uAE30\uB2A5 \uAD6C\uD604 \uC804 \uD544\uC694\uD55C \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8\uC640 \uC2A4\uD0AC\uC744 \uC810\uAC80\uD569\uB2C8\uB2E4.
2136
2138
 
2137
2139
  ## \uAC1C\uC694
2138
2140
 
2139
- \uC0C8\uB85C\uC6B4 \uAE30\uB2A5\uC744 \uAD6C\uD604\uD558\uAE30 \uC804\uC5D0 \uD544\uC694\uD55C \uB3C4\uAD6C, \uC758\uC874\uC131, \uC124\uC815\uC744 \uBD84\uC11D\uD558\uACE0 \uC900\uBE44\uD569\uB2C8\uB2E4.
2141
+ \uC2A4\uD399/\uACC4\uD68D/\uC791\uC5C5 \uBB38\uC11C\uB97C \uBD84\uC11D\uD558\uC5EC \uAD6C\uD604\uC5D0 \uD544\uC694\uD55C Claude Code \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8\uC640 \uC2A4\uD0AC\uC744 \uAC10\uC9C0\uD558\uACE0,
2142
+ \uB204\uB77D\uB41C \uB3C4\uAD6C\uAC00 \uC788\uC73C\uBA74 \uC790\uB3D9\uC73C\uB85C \uC0DD\uC131\uD569\uB2C8\uB2E4.
2140
2143
 
2141
2144
  ## \uC9C0\uC2DC\uC0AC\uD56D
2142
2145
 
2143
- 1. \uC2A4\uD399\uC744 \uBD84\uC11D\uD558\uC5EC \uD544\uC694\uD55C \uAE30\uC220\uC744 \uD30C\uC545\uD558\uC138\uC694
2144
- 2. \uD544\uC694\uD55C \uC758\uC874\uC131\uC744 \uB098\uC5F4\uD558\uC138\uC694
2145
- 3. \uD544\uC694\uD55C MCP \uC11C\uBC84\uAC00 \uC788\uB2E4\uBA74 \uD655\uC778\uD558\uC138\uC694
2146
- 4. AGENTS.md\uC5D0 \uD544\uC694\uD55C \uC9C0\uCE68\uC744 \uCD94\uAC00\uD558\uC138\uC694
2146
+ 1. \`sdd prepare <feature-id>\` \uBA85\uB839\uC5B4\uB97C \uC2E4\uD589\uD558\uC138\uC694
2147
+ 2. \uAC10\uC9C0\uB41C \uB3C4\uAD6C \uBAA9\uB85D\uACFC \uC874\uC7AC \uC5EC\uBD80\uB97C \uD655\uC778\uD558\uC138\uC694
2148
+ 3. \uB204\uB77D\uB41C \uB3C4\uAD6C \uC0DD\uC131 \uC5EC\uBD80\uB97C \uACB0\uC815\uD558\uC138\uC694
2147
2149
 
2148
- ## \uC900\uBE44 \uCCB4\uD06C\uB9AC\uC2A4\uD2B8
2150
+ ## \uC6CC\uD06C\uD50C\uB85C\uC6B0
2151
+
2152
+ \`\`\`
2153
+ /sdd.new \u2192 /sdd.plan \u2192 /sdd.tasks \u2192 sdd prepare \u2192 /sdd.implement
2154
+ \`\`\`
2149
2155
 
2150
- ### 1. \uC758\uC874\uC131 \uD655\uC778
2156
+ ## \uBA85\uB839\uC5B4
2151
2157
 
2152
2158
  \`\`\`bash
2153
- # \uD544\uC694\uD55C npm \uD328\uD0A4\uC9C0 \uD655\uC778
2154
- npm list [package-name]
2159
+ # \uAE30\uBCF8 \uC0AC\uC6A9 (\uB300\uD654\uD615)
2160
+ sdd prepare user-auth
2161
+
2162
+ # \uBBF8\uB9AC\uBCF4\uAE30 (\uD30C\uC77C \uC0DD\uC131 \uC5C6\uC74C)
2163
+ sdd prepare user-auth --dry-run
2155
2164
 
2156
- # \uC124\uCE58 \uD544\uC694\uC2DC
2157
- npm install [package-name]
2165
+ # \uC790\uB3D9 \uC2B9\uC778 (\uB204\uB77D \uB3C4\uAD6C \uC790\uB3D9 \uC0DD\uC131)
2166
+ sdd prepare user-auth --auto-approve
2167
+
2168
+ # JSON \uCD9C\uB825
2169
+ sdd prepare user-auth --json
2158
2170
  \`\`\`
2159
2171
 
2160
- ### 2. \uD658\uACBD \uC124\uC815
2172
+ ## \uAC10\uC9C0 \uB300\uC0C1
2161
2173
 
2162
- - [ ] \uD658\uACBD \uBCC0\uC218 \uD655\uC778 (.env)
2163
- - [ ] API \uD0A4 \uC124\uC815
2164
- - [ ] \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC5F0\uACB0
2174
+ ### \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8 (\`.claude/agents/*.md\`)
2165
2175
 
2166
- ### 3. MCP \uC11C\uBC84 (\uD544\uC694\uC2DC)
2176
+ | \uC5D0\uC774\uC804\uD2B8 | \uAC10\uC9C0 \uD0A4\uC6CC\uB4DC | \uC124\uBA85 |
2177
+ |----------|-------------|------|
2178
+ | test-runner | \uD14C\uC2A4\uD2B8, test, jest, vitest | \uD14C\uC2A4\uD2B8 \uC2E4\uD589 |
2179
+ | api-scaffold | api, rest, endpoint | API \uC2A4\uCE90\uD3F4\uB529 |
2180
+ | component-gen | component, \uCEF4\uD3EC\uB10C\uD2B8, react | \uCEF4\uD3EC\uB10C\uD2B8 \uC0DD\uC131 |
2181
+ | code-reviewer | review, \uB9AC\uBDF0 | \uCF54\uB4DC \uB9AC\uBDF0 |
2167
2182
 
2168
- \uD544\uC694\uD55C MCP \uC11C\uBC84 \uBAA9\uB85D:
2169
- - filesystem: \uD30C\uC77C \uC2DC\uC2A4\uD15C \uC811\uADFC
2170
- - github: GitHub API \uC5F0\uB3D9
2171
- - database: \uB370\uC774\uD130\uBCA0\uC774\uC2A4 \uC811\uADFC
2183
+ ### \uC2A4\uD0AC (\`.claude/skills/<name>/SKILL.md\`)
2172
2184
 
2173
- ### 4. AGENTS.md \uC5C5\uB370\uC774\uD2B8
2185
+ | \uC2A4\uD0AC | \uAC10\uC9C0 \uD0A4\uC6CC\uB4DC | \uC124\uBA85 |
2186
+ |------|-------------|------|
2187
+ | test | \uD14C\uC2A4\uD2B8, test | \uD14C\uC2A4\uD2B8 \uC791\uC131 |
2188
+ | gen-api | api, rest | API \uC0DD\uC131 |
2189
+ | gen-component | component | \uCEF4\uD3EC\uB10C\uD2B8 \uC0DD\uC131 |
2190
+ | db-migrate | database, \uB9C8\uC774\uADF8\uB808\uC774\uC158 | DB \uB9C8\uC774\uADF8\uB808\uC774\uC158 |
2191
+ | gen-doc | \uBB38\uC11C, doc | \uBB38\uC11C \uC0DD\uC131 |
2174
2192
 
2175
- \uAE30\uB2A5 \uAD6C\uD604\uC5D0 \uD544\uC694\uD55C \uC9C0\uCE68\uC744 AGENTS.md\uC5D0 \uCD94\uAC00:
2193
+ ## \uCD9C\uB825 \uC608\uC2DC
2176
2194
 
2177
- \`\`\`markdown
2178
- ## [\uAE30\uB2A5\uBA85] \uAD6C\uD604 \uC9C0\uCE68
2195
+ \`\`\`
2196
+ === SDD Prepare: user-auth ===
2179
2197
 
2180
- ### \uC0AC\uC6A9 \uAE30\uC220
2181
- - ...
2198
+ \uBD84\uC11D \uB300\uC0C1: 3\uAC1C \uBB38\uC11C, 5\uAC1C \uD0DC\uC2A4\uD06C
2182
2199
 
2183
- ### \uAD6C\uD604 \uADDC\uCE59
2184
- - ...
2200
+ --- \uC11C\uBE0C\uC5D0\uC774\uC804\uD2B8 ---
2201
+ [x] test-runner (\uC874\uC7AC)
2202
+ [ ] api-scaffold (\uC5C6\uC74C) \u2192 \uC0DD\uC131 \uD544\uC694
2185
2203
 
2186
- ### \uCC38\uACE0 \uC790\uB8CC
2187
- - ...
2188
- \`\`\`
2204
+ --- \uC2A4\uD0AC ---
2205
+ [x] test (\uC874\uC7AC)
2206
+ [ ] gen-api (\uC5C6\uC74C) \u2192 \uC0DD\uC131 \uD544\uC694
2189
2207
 
2190
- ## \uBA85\uB839\uC5B4
2208
+ \uB204\uB77D\uB41C \uB3C4\uAD6C\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/n)
2209
+ \`\`\`
2191
2210
 
2192
- \`\`\`bash
2193
- # \uD504\uB85C\uC81D\uD2B8 \uC0C1\uD0DC \uD655\uC778
2194
- sdd status
2211
+ ## \uC0DD\uC131 \uD30C\uC77C \uAD6C\uC870
2195
2212
 
2196
- # \uC2A4\uD399 \uAC80\uC99D
2197
- sdd validate
2198
2213
  \`\`\`
2214
+ .claude/
2215
+ \u251C\u2500\u2500 agents/
2216
+ \u2502 \u2514\u2500\u2500 api-scaffold.md # \uC5D0\uC774\uC804\uD2B8 \uC815\uC758
2217
+ \u2514\u2500\u2500 skills/
2218
+ \u2514\u2500\u2500 gen-api/
2219
+ \u2514\u2500\u2500 SKILL.md # \uC2A4\uD0AC \uC815\uC758
2220
+ \`\`\`
2221
+
2222
+ \uC644\uB8CC \uD6C4 \`/sdd.implement\`\uB85C \uAD6C\uD604\uC744 \uC2DC\uC791\uD558\uC138\uC694.
2199
2223
  `
2200
2224
  },
2201
2225
  {
@@ -2754,6 +2778,249 @@ sdd prompt $ARGUMENTS
2754
2778
  - \`--list\`: \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD504\uB86C\uD504\uD2B8 \uBAA9\uB85D
2755
2779
 
2756
2780
  \uD504\uB86C\uD504\uD2B8 \uB0B4\uC6A9\uC744 \uCD9C\uB825\uD558\uACE0 \uC0AC\uC6A9 \uBC29\uBC95\uC744 \uC548\uB0B4\uD574\uC8FC\uC138\uC694.
2781
+ `
2782
+ },
2783
+ {
2784
+ name: "sdd.sync",
2785
+ content: `---
2786
+ description: \uC2A4\uD399-\uCF54\uB4DC \uB3D9\uAE30\uD654 \uC0C1\uD0DC\uB97C \uAC80\uC99D\uD569\uB2C8\uB2E4
2787
+ allowed-tools: Bash, Read
2788
+ argument-hint: [specId] [--json] [--ci]
2789
+ ---
2790
+
2791
+ \uC2A4\uD399 \uC694\uAD6C\uC0AC\uD56D\uACFC \uCF54\uB4DC \uAD6C\uD604\uC758 \uB3D9\uAE30\uD654 \uC0C1\uD0DC\uB97C \uAC80\uC99D\uD569\uB2C8\uB2E4.
2792
+
2793
+ ## \uAC1C\uC694
2794
+
2795
+ \uC2A4\uD399\uC758 REQ-xxx \uC694\uAD6C\uC0AC\uD56D\uC774 \uCF54\uB4DC\uC5D0 \uAD6C\uD604\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD569\uB2C8\uB2E4.
2796
+ \uCF54\uB4DC\uC5D0\uC11C \`@spec REQ-xxx\` \uC8FC\uC11D\uC774\uB098 \uD14C\uC2A4\uD2B8\uC5D0\uC11C \`it('REQ-xxx: ...')\` \uD615\uC2DD\uC744 \uC778\uC2DD\uD569\uB2C8\uB2E4.
2797
+
2798
+ ## \uBA85\uB839\uC5B4
2799
+
2800
+ \`\`\`bash
2801
+ # \uC804\uCCB4 \uC2A4\uD399 \uB3D9\uAE30\uD654 \uAC80\uC99D
2802
+ sdd sync
2803
+
2804
+ # \uD2B9\uC815 \uC2A4\uD399\uB9CC \uAC80\uC99D
2805
+ sdd sync user-auth
2806
+
2807
+ # JSON \uCD9C\uB825
2808
+ sdd sync --json
2809
+
2810
+ # CI \uBAA8\uB4DC (\uB3D9\uAE30\uD654\uC728 \uC784\uACC4\uAC12 \uAC80\uC0AC)
2811
+ sdd sync --ci --threshold 80
2812
+
2813
+ # \uB9C8\uD06C\uB2E4\uC6B4 \uB9AC\uD3EC\uD2B8
2814
+ sdd sync --markdown
2815
+ \`\`\`
2816
+
2817
+ ## \uCF54\uB4DC \uC8FC\uC11D \uADDC\uCE59
2818
+
2819
+ \`\`\`typescript
2820
+ /**
2821
+ * \uC0AC\uC6A9\uC790 \uB85C\uADF8\uC778
2822
+ * @spec REQ-001
2823
+ * @spec REQ-002
2824
+ */
2825
+ export async function login() { ... }
2826
+ \`\`\`
2827
+
2828
+ ## \uD14C\uC2A4\uD2B8 \uB9E4\uD551
2829
+
2830
+ \`\`\`typescript
2831
+ it('REQ-001: \uC62C\uBC14\uB978 \uC790\uACA9 \uC99D\uBA85\uC73C\uB85C \uB85C\uADF8\uC778\uD55C\uB2E4', () => { ... });
2832
+ \`\`\`
2833
+
2834
+ ## \uCD9C\uB825 \uC608\uC2DC
2835
+
2836
+ \`\`\`
2837
+ === SDD Sync: \uC2A4\uD399-\uCF54\uB4DC \uB3D9\uAE30\uD654 \uAC80\uC99D ===
2838
+
2839
+ \uC2A4\uD399: 3\uAC1C, \uC694\uAD6C\uC0AC\uD56D: 15\uAC1C
2840
+
2841
+ \u2713 \uAD6C\uD604\uB428 (12/15)
2842
+ - REQ-001: \uC0AC\uC6A9\uC790 \uB85C\uADF8\uC778 (src/auth/login.ts:45)
2843
+ ...
2844
+
2845
+ \u2717 \uBBF8\uAD6C\uD604 (3/15)
2846
+ - REQ-010: \uBE44\uBC00\uBC88\uD638 \uC7AC\uC124\uC815
2847
+ ...
2848
+
2849
+ \uB3D9\uAE30\uD654\uC728: 80% (12/15)
2850
+ \`\`\`
2851
+
2852
+ \uB3D9\uAE30\uD654 \uACB0\uACFC\uB97C \uBD84\uC11D\uD558\uACE0 \uBBF8\uAD6C\uD604 \uC694\uAD6C\uC0AC\uD56D\uC5D0 \uB300\uD55C \uC870\uCE58\uB97C \uC81C\uC548\uD574\uC8FC\uC138\uC694.
2853
+ `
2854
+ },
2855
+ {
2856
+ name: "sdd.diff",
2857
+ content: `---
2858
+ description: \uC2A4\uD399 \uBCC0\uACBD\uC0AC\uD56D\uC744 \uC2DC\uAC01\uD654\uD569\uB2C8\uB2E4
2859
+ allowed-tools: Bash, Read
2860
+ argument-hint: [commit1] [commit2] [--staged] [--stat]
2861
+ ---
2862
+
2863
+ \uC2A4\uD399 \uD30C\uC77C\uC758 \uBCC0\uACBD\uC0AC\uD56D\uC744 \uAD6C\uC870\uC801\uC73C\uB85C \uBE44\uAD50\uD558\uC5EC \uC2DC\uAC01\uD654\uD569\uB2C8\uB2E4.
2864
+
2865
+ ## \uAC1C\uC694
2866
+
2867
+ Git diff\uC640 \uC720\uC0AC\uD558\uAC8C \uC2A4\uD399 \uBCC0\uACBD\uC744 \uBCF4\uC5EC\uC8FC\uB418, \uC694\uAD6C\uC0AC\uD56D/\uC2DC\uB098\uB9AC\uC624/\uD0A4\uC6CC\uB4DC \uBCC0\uACBD\uC744 \uAD6C\uC870\uC801\uC73C\uB85C \uC778\uC2DD\uD569\uB2C8\uB2E4.
2868
+
2869
+ ## \uBA85\uB839\uC5B4
2870
+
2871
+ \`\`\`bash
2872
+ # \uC791\uC5C5 \uB514\uB809\uD1A0\uB9AC \uBCC0\uACBD
2873
+ sdd diff
2874
+
2875
+ # \uC2A4\uD14C\uC774\uC9D5\uB41C \uBCC0\uACBD
2876
+ sdd diff --staged
2877
+
2878
+ # \uCEE4\uBC0B \uAC04 \uBE44\uAD50
2879
+ sdd diff abc123 def456
2880
+
2881
+ # \uBE0C\uB79C\uCE58 \uBE44\uAD50
2882
+ sdd diff main..feature/auth
2883
+
2884
+ # \uD2B9\uC815 \uC2A4\uD399\uB9CC
2885
+ sdd diff --spec user-auth
2886
+
2887
+ # \uC635\uC158
2888
+ sdd diff --stat # \uD1B5\uACC4 \uC694\uC57D
2889
+ sdd diff --name-only # \uD30C\uC77C\uBA85\uB9CC
2890
+ sdd diff --json # JSON \uCD9C\uB825
2891
+ sdd diff --no-color # \uCEEC\uB7EC \uC5C6\uC74C
2892
+ \`\`\`
2893
+
2894
+ ## \uAC10\uC9C0 \uD56D\uBAA9
2895
+
2896
+ - **\uC694\uAD6C\uC0AC\uD56D**: REQ-xxx \uCD94\uAC00/\uC218\uC815/\uC0AD\uC81C
2897
+ - **\uC2DC\uB098\uB9AC\uC624**: GIVEN-WHEN-THEN \uBCC0\uACBD
2898
+ - **\uD0A4\uC6CC\uB4DC**: RFC 2119 \uD0A4\uC6CC\uB4DC \uBCC0\uACBD (SHOULD \u2192 SHALL \uB4F1)
2899
+ - **\uBA54\uD0C0\uB370\uC774\uD130**: YAML frontmatter \uBCC0\uACBD
2900
+
2901
+ ## \uCD9C\uB825 \uC608\uC2DC
2902
+
2903
+ \`\`\`
2904
+ === SDD Diff ===
2905
+
2906
+ .sdd/specs/user-auth/spec.md
2907
+
2908
+ \uC694\uAD6C\uC0AC\uD56D \uBCC0\uACBD:
2909
+ ~ REQ-001: \uC0AC\uC6A9\uC790 \uB85C\uADF8\uC778
2910
+ - \uC2DC\uC2A4\uD15C\uC740 \uC774\uBA54\uC77C/\uBE44\uBC00\uBC88\uD638 \uB85C\uADF8\uC778\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHOULD)
2911
+ + \uC2DC\uC2A4\uD15C\uC740 \uC774\uBA54\uC77C/\uBE44\uBC00\uBC88\uD638 \uB85C\uADF8\uC778\uC744 \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(SHALL)
2912
+ \u26A0\uFE0F REQ-001: SHOULD \u2192 SHALL (\uAC15\uD654)
2913
+
2914
+ + REQ-005: \uC18C\uC15C \uB85C\uADF8\uC778
2915
+ + \uC2DC\uC2A4\uD15C\uC740 Google OAuth\uB97C \uC9C0\uC6D0\uD574\uC57C \uD55C\uB2E4(MAY)
2916
+
2917
+ \uC2DC\uB098\uB9AC\uC624 \uBCC0\uACBD:
2918
+ + Scenario: Google \uB85C\uADF8\uC778
2919
+ \`\`\`
2920
+
2921
+ ## \uD1B5\uACC4 \uC694\uC57D (--stat)
2922
+
2923
+ \`\`\`
2924
+ === SDD Diff --stat ===
2925
+
2926
+ .sdd/specs/user-auth/spec.md
2927
+ \uC694\uAD6C\uC0AC\uD56D: +1, ~1, -0
2928
+ \uC2DC\uB098\uB9AC\uC624: +1, ~0, -0
2929
+ \uD0A4\uC6CC\uB4DC \uBCC0\uACBD: 1\uAC1C (\uAC15\uD654: 1, \uC57D\uD654: 0)
2930
+
2931
+ \uCD1D \uBCC0\uACBD: 1\uAC1C \uD30C\uC77C, \uC694\uAD6C\uC0AC\uD56D +1 ~1 -0
2932
+ \`\`\`
2933
+
2934
+ \uBCC0\uACBD \uB0B4\uC6A9\uC744 \uBD84\uC11D\uD558\uACE0 \uC601\uD5A5\uB3C4\uB97C \uD3C9\uAC00\uD574\uC8FC\uC138\uC694.
2935
+ `
2936
+ },
2937
+ {
2938
+ name: "sdd.export",
2939
+ content: `---
2940
+ description: \uC2A4\uD399\uC744 HTML, JSON \uB4F1\uC73C\uB85C \uB0B4\uBCF4\uB0B4\uAE30
2941
+ allowed-tools: Bash, Read, Write
2942
+ argument-hint: [specId] [--format html] [-o output.html]
2943
+ ---
2944
+
2945
+ \uC2A4\uD399 \uBB38\uC11C\uB97C \uB2E4\uC591\uD55C \uD615\uC2DD\uC73C\uB85C \uB0B4\uBCF4\uB0C5\uB2C8\uB2E4.
2946
+
2947
+ ## \uAC1C\uC694
2948
+
2949
+ \uC2A4\uD399\uC744 HTML, JSON, \uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD\uC73C\uB85C \uBCC0\uD658\uD558\uC5EC \uD300\uC6D0, \uC774\uD574\uAD00\uACC4\uC790\uC640 \uACF5\uC720\uD569\uB2C8\uB2E4.
2950
+
2951
+ ## \uBA85\uB839\uC5B4
2952
+
2953
+ \`\`\`bash
2954
+ # \uB2E8\uC77C \uC2A4\uD399 HTML \uB0B4\uBCF4\uB0B4\uAE30
2955
+ sdd export user-auth --format html
2956
+
2957
+ # \uC804\uCCB4 \uC2A4\uD399 \uB0B4\uBCF4\uB0B4\uAE30
2958
+ sdd export --all --format html
2959
+
2960
+ # JSON \uD615\uC2DD
2961
+ sdd export user-auth --format json
2962
+
2963
+ # \uB9C8\uD06C\uB2E4\uC6B4 \uBCD1\uD569
2964
+ sdd export --all --format markdown
2965
+
2966
+ # \uCD9C\uB825 \uACBD\uB85C \uC9C0\uC815
2967
+ sdd export user-auth -o ./docs/user-auth.html
2968
+
2969
+ # \uB2E4\uD06C \uD14C\uB9C8
2970
+ sdd export --all --theme dark
2971
+
2972
+ # \uBAA9\uCC28 \uC81C\uC678
2973
+ sdd export user-auth --no-toc
2974
+ \`\`\`
2975
+
2976
+ ## \uC9C0\uC6D0 \uD615\uC2DD
2977
+
2978
+ | \uD615\uC2DD | \uC124\uBA85 |
2979
+ |------|------|
2980
+ | html | \uC2A4\uD0C0\uC77C \uD3EC\uD568 HTML (\uAE30\uBCF8\uAC12) |
2981
+ | json | \uAD6C\uC870\uD654\uB41C JSON |
2982
+ | markdown | \uB9C8\uD06C\uB2E4\uC6B4 \uBCD1\uD569 |
2983
+ | pdf | HTML \uC0DD\uC131 \uD6C4 \uBE0C\uB77C\uC6B0\uC800 \uC778\uC1C4 \uC548\uB0B4 |
2984
+
2985
+ ## HTML \uAE30\uB2A5
2986
+
2987
+ - \uBC18\uC751\uD615 \uB514\uC790\uC778
2988
+ - \uC790\uB3D9 \uBAA9\uCC28 \uC0DD\uC131
2989
+ - RFC 2119 \uD0A4\uC6CC\uB4DC \uAC15\uC870
2990
+ - GIVEN/WHEN/THEN \uC2DC\uAC01\uD654
2991
+ - \uB77C\uC774\uD2B8/\uB2E4\uD06C \uD14C\uB9C8
2992
+ - \uC778\uC1C4 \uCD5C\uC801\uD654
2993
+
2994
+ ## JSON \uAD6C\uC870
2995
+
2996
+ \`\`\`json
2997
+ {
2998
+ "id": "user-auth",
2999
+ "title": "\uC0AC\uC6A9\uC790 \uC778\uC99D",
3000
+ "requirements": [
3001
+ {
3002
+ "id": "REQ-001",
3003
+ "title": "\uB85C\uADF8\uC778",
3004
+ "keyword": "SHALL",
3005
+ "priority": "high"
3006
+ }
3007
+ ],
3008
+ "scenarios": [...]
3009
+ }
3010
+ \`\`\`
3011
+
3012
+ ## \uCD9C\uB825 \uC608\uC2DC
3013
+
3014
+ \`\`\`
3015
+ === SDD Export ===
3016
+
3017
+ \uD615\uC2DD: HTML
3018
+ \uC2A4\uD399: 3\uAC1C
3019
+ \uCD9C\uB825: ./specs.html
3020
+ \uD06C\uAE30: 45.32 KB
3021
+ \`\`\`
3022
+
3023
+ \uB0B4\uBCF4\uB0B8 \uD30C\uC77C\uC758 \uB0B4\uC6A9\uC744 \uD655\uC778\uD558\uACE0 \uD544\uC694\uC2DC \uCD94\uAC00 \uD615\uC2DD\uC73C\uB85C \uB0B4\uBCF4\uB0B4\uC138\uC694.
2757
3024
  `
2758
3025
  }
2759
3026
  ];
@@ -3133,7 +3400,10 @@ var SpecMetadataSchema = z.object({
3133
3400
  created: DateStringSchema,
3134
3401
  depends: z.string().nullable().optional(),
3135
3402
  command: z.string().optional(),
3136
- author: z.string().optional()
3403
+ author: z.string().optional(),
3404
+ id: z.string().optional(),
3405
+ constitution_version: z.string().optional(),
3406
+ dependencies: z.array(z.string()).optional()
3137
3407
  });
3138
3408
  var RequirementLevelSchema = z.enum(["SHALL", "MUST", "SHOULD", "MAY"]);
3139
3409
  var RequirementSchema = z.object({
@@ -3934,9 +4204,9 @@ async function validateSpecs(targetPath, options = {}) {
3934
4204
  async function findSpecFiles(dirPath) {
3935
4205
  const files = [];
3936
4206
  async function scanDir(dir) {
3937
- const { promises: fs22 } = await import("fs");
4207
+ const { promises: fs27 } = await import("fs");
3938
4208
  try {
3939
- const entries = await fs22.readdir(dir, { withFileTypes: true });
4209
+ const entries = await fs27.readdir(dir, { withFileTypes: true });
3940
4210
  for (const entry of entries) {
3941
4211
  const fullPath = path3.join(dir, entry.name);
3942
4212
  if (entry.isDirectory()) {
@@ -4711,7 +4981,8 @@ var ProposalMetadataSchema = z3.object({
4711
4981
  });
4712
4982
  var DeltaItemSchema = z3.object({
4713
4983
  type: DeltaTypeSchema,
4714
- target: z3.string(),
4984
+ target: z3.string().optional(),
4985
+ content: z3.string(),
4715
4986
  before: z3.string().optional(),
4716
4987
  after: z3.string().optional(),
4717
4988
  description: z3.string().optional()
@@ -6059,19 +6330,19 @@ function detectCircularDependencies(graph) {
6059
6330
  const cycles = [];
6060
6331
  const visited = /* @__PURE__ */ new Set();
6061
6332
  const recStack = /* @__PURE__ */ new Set();
6062
- function dfs(nodeId, path36) {
6333
+ function dfs(nodeId, path42) {
6063
6334
  visited.add(nodeId);
6064
6335
  recStack.add(nodeId);
6065
6336
  const node = graph.nodes.get(nodeId);
6066
6337
  if (!node) return false;
6067
6338
  for (const depId of node.dependsOn) {
6068
6339
  if (!visited.has(depId)) {
6069
- if (dfs(depId, [...path36, nodeId])) {
6340
+ if (dfs(depId, [...path42, nodeId])) {
6070
6341
  return true;
6071
6342
  }
6072
6343
  } else if (recStack.has(depId)) {
6073
- const cycleStart = path36.indexOf(depId);
6074
- const cycle = cycleStart >= 0 ? [...path36.slice(cycleStart), nodeId, depId] : [nodeId, depId];
6344
+ const cycleStart = path42.indexOf(depId);
6345
+ const cycle = cycleStart >= 0 ? [...path42.slice(cycleStart), nodeId, depId] : [nodeId, depId];
6075
6346
  cycles.push({
6076
6347
  cycle,
6077
6348
  description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
@@ -10456,12 +10727,12 @@ async function runWatch(options) {
10456
10727
  validationCount++;
10457
10728
  if (result.success) {
10458
10729
  const data = result.data;
10459
- const hasErrors = data.results.some((r) => r.errors.length > 0);
10460
- const hasWarnings = data.results.some((r) => r.warnings.length > 0);
10730
+ const hasErrors = data.files.some((r) => r.errors.length > 0);
10731
+ const hasWarnings = data.files.some((r) => r.warnings.length > 0);
10461
10732
  if (hasErrors) {
10462
10733
  errorCount++;
10463
- error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.errorCount}\uAC1C \uC5D0\uB7EC, ${data.warningCount}\uAC1C \uACBD\uACE0`);
10464
- for (const specResult of data.results) {
10734
+ error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.failed}\uAC1C \uC5D0\uB7EC, ${data.warnings}\uAC1C \uACBD\uACE0`);
10735
+ for (const specResult of data.files) {
10465
10736
  if (specResult.errors.length > 0) {
10466
10737
  error(` ${specResult.file}:`);
10467
10738
  for (const err of specResult.errors) {
@@ -10471,11 +10742,11 @@ async function runWatch(options) {
10471
10742
  }
10472
10743
  } else if (hasWarnings) {
10473
10744
  if (!options.quiet) {
10474
- warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warningCount}\uAC1C \uACBD\uACE0`);
10745
+ warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warnings}\uAC1C \uACBD\uACE0`);
10475
10746
  }
10476
10747
  } else {
10477
10748
  if (!options.quiet) {
10478
- success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.validCount}\uAC1C \uC2A4\uD399)`);
10749
+ success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.passed}\uAC1C \uC2A4\uD399)`);
10479
10750
  }
10480
10751
  }
10481
10752
  } else {
@@ -11089,8 +11360,8 @@ async function generateReport(sddPath, options) {
11089
11360
  const validationResult = await validateSpecs(sddPath, { strict: false });
11090
11361
  if (validationResult.success) {
11091
11362
  reportData.validation = validationResult.data;
11092
- reportData.summary.validationErrors = validationResult.data.errorCount;
11093
- reportData.summary.validationWarnings = validationResult.data.warningCount;
11363
+ reportData.summary.validationErrors = validationResult.data.failed;
11364
+ reportData.summary.validationWarnings = validationResult.data.warnings;
11094
11365
  }
11095
11366
  }
11096
11367
  let content;
@@ -11138,14 +11409,14 @@ function renderHtmlReport(data) {
11138
11409
  }
11139
11410
  };
11140
11411
  const statusBadge = (status) => {
11141
- const colors = {
11412
+ const colors3 = {
11142
11413
  draft: "#6b7280",
11143
11414
  review: "#3b82f6",
11144
11415
  approved: "#22c55e",
11145
11416
  implemented: "#8b5cf6",
11146
11417
  deprecated: "#ef4444"
11147
11418
  };
11148
- const color = colors[status] || "#6b7280";
11419
+ const color = colors3[status] || "#6b7280";
11149
11420
  return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;">${status}</span>`;
11150
11421
  };
11151
11422
  const specRows = data.specs.map((spec) => `
@@ -11468,11 +11739,12 @@ async function searchSpecs(sddPath, options = {}) {
11468
11739
  return failure(indexResult.error);
11469
11740
  }
11470
11741
  const index = indexResult.data;
11471
- let results = filterByOptions(index, options);
11742
+ const filtered = filterByOptions(index, options);
11743
+ let results;
11472
11744
  if (options.query) {
11473
- results = searchByQuery(results, options.query, options);
11745
+ results = searchByQuery(filtered, options.query, options);
11474
11746
  } else {
11475
- results = results.map((item) => ({ ...item, score: 100, matches: [] }));
11747
+ results = filtered.map((item) => ({ ...item, score: 100, matches: [] }));
11476
11748
  }
11477
11749
  results = sortResults(results, options);
11478
11750
  if (options.limit && options.limit > 0) {
@@ -11522,13 +11794,13 @@ async function collectSpecs(basePath, currentPath, index) {
11522
11794
  index.push({
11523
11795
  id: specId === "." ? entry.name : specId,
11524
11796
  path: relativePath,
11525
- title: metadata.title || specId,
11797
+ title: String(metadata.title || specId),
11526
11798
  content,
11527
- status: metadata.status || "unknown",
11528
- phase: metadata.phase || "unknown",
11529
- author: metadata.author || "",
11530
- created: metadata.created || "",
11531
- updated: metadata.updated || stat.mtime.toISOString().split("T")[0],
11799
+ status: String(metadata.status || "unknown"),
11800
+ phase: String(metadata.phase || "unknown"),
11801
+ author: String(metadata.author || ""),
11802
+ created: String(metadata.created || ""),
11803
+ updated: String(metadata.updated || stat.mtime.toISOString().split("T")[0]),
11532
11804
  depends: parseDependencies(metadata.depends),
11533
11805
  tags: parseTags(metadata.tags)
11534
11806
  });
@@ -13008,6 +13280,2634 @@ async function runPrepare(feature, options) {
13008
13280
  }
13009
13281
  }
13010
13282
 
13283
+ // src/core/sync/schemas.ts
13284
+ import { z as z9 } from "zod";
13285
+ var CodeLocationSchema = z9.object({
13286
+ file: z9.string(),
13287
+ line: z9.number(),
13288
+ type: z9.enum(["code", "test"]),
13289
+ text: z9.string().optional()
13290
+ });
13291
+ var RequirementStatusSchema = z9.object({
13292
+ id: z9.string(),
13293
+ // REQ-001
13294
+ specId: z9.string(),
13295
+ // user-auth
13296
+ title: z9.string().optional(),
13297
+ keyword: z9.enum(["SHALL", "MUST", "SHOULD", "MAY", "SHALL NOT", "MUST NOT"]).optional(),
13298
+ status: z9.enum(["implemented", "missing", "partial"]),
13299
+ locations: z9.array(CodeLocationSchema)
13300
+ });
13301
+ var SpecSummarySchema = z9.object({
13302
+ id: z9.string(),
13303
+ title: z9.string().optional(),
13304
+ requirementCount: z9.number(),
13305
+ implementedCount: z9.number(),
13306
+ missingCount: z9.number(),
13307
+ syncRate: z9.number()
13308
+ });
13309
+ var SyncResultSchema = z9.object({
13310
+ specs: z9.array(SpecSummarySchema),
13311
+ requirements: z9.array(RequirementStatusSchema),
13312
+ syncRate: z9.number(),
13313
+ implemented: z9.array(z9.string()),
13314
+ missing: z9.array(z9.string()),
13315
+ orphans: z9.array(CodeLocationSchema),
13316
+ totalRequirements: z9.number(),
13317
+ totalImplemented: z9.number()
13318
+ });
13319
+ var SyncOptionsSchema = z9.object({
13320
+ specId: z9.string().optional(),
13321
+ srcDir: z9.string().optional(),
13322
+ include: z9.array(z9.string()).optional(),
13323
+ exclude: z9.array(z9.string()).optional(),
13324
+ threshold: z9.number().optional(),
13325
+ ci: z9.boolean().optional(),
13326
+ json: z9.boolean().optional(),
13327
+ markdown: z9.boolean().optional()
13328
+ });
13329
+ var ExtractedRequirementSchema = z9.object({
13330
+ id: z9.string(),
13331
+ specId: z9.string(),
13332
+ title: z9.string().optional(),
13333
+ description: z9.string().optional(),
13334
+ keyword: z9.enum(["SHALL", "MUST", "SHOULD", "MAY", "SHALL NOT", "MUST NOT"]).optional(),
13335
+ line: z9.number()
13336
+ });
13337
+ var CodeReferenceSchema = z9.object({
13338
+ reqId: z9.string(),
13339
+ // REQ-001
13340
+ file: z9.string(),
13341
+ line: z9.number(),
13342
+ type: z9.enum(["code", "test"]),
13343
+ context: z9.string().optional()
13344
+ });
13345
+
13346
+ // src/core/sync/spec-parser.ts
13347
+ import { promises as fs22, statSync } from "fs";
13348
+ import path36 from "path";
13349
+ import matter8 from "gray-matter";
13350
+ var REQ_ID_PATTERN = /\b(REQ-\d+)\b/gi;
13351
+ var RFC_KEYWORD_PATTERN = /\((SHALL|MUST|SHOULD|MAY|SHALL NOT|MUST NOT)\)/i;
13352
+ var REQ_HEADER_PATTERN = /^#{2,4}\s*(REQ-\d+):\s*(.+)$/i;
13353
+ var SpecParser = class {
13354
+ constructor(projectRoot) {
13355
+ this.projectRoot = projectRoot;
13356
+ this.specsDir = path36.join(projectRoot, ".sdd", "specs");
13357
+ }
13358
+ specsDir;
13359
+ /**
13360
+ * 특정 스펙에서 요구사항 추출
13361
+ */
13362
+ async parseSpec(specId) {
13363
+ const specPath = path36.join(this.specsDir, specId, "spec.md");
13364
+ try {
13365
+ const content = await fs22.readFile(specPath, "utf-8");
13366
+ return this.extractRequirements(content, specId);
13367
+ } catch {
13368
+ const specDir = path36.join(this.specsDir, specId);
13369
+ try {
13370
+ const files = await fs22.readdir(specDir);
13371
+ const mdFiles = files.filter((f) => f.endsWith(".md"));
13372
+ const requirements = [];
13373
+ for (const file of mdFiles) {
13374
+ const filePath = path36.join(specDir, file);
13375
+ const content = await fs22.readFile(filePath, "utf-8");
13376
+ requirements.push(...this.extractRequirements(content, specId));
13377
+ }
13378
+ return requirements;
13379
+ } catch {
13380
+ return [];
13381
+ }
13382
+ }
13383
+ }
13384
+ /**
13385
+ * 모든 스펙에서 요구사항 추출
13386
+ */
13387
+ async parseAllSpecs() {
13388
+ const specIds = await this.listSpecs();
13389
+ const allRequirements = [];
13390
+ for (const specId of specIds) {
13391
+ const requirements = await this.parseSpec(specId);
13392
+ allRequirements.push(...requirements);
13393
+ }
13394
+ return allRequirements;
13395
+ }
13396
+ /**
13397
+ * 스펙 목록 조회
13398
+ */
13399
+ async listSpecs() {
13400
+ try {
13401
+ const entries = await fs22.readdir(this.specsDir, { withFileTypes: true });
13402
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
13403
+ } catch {
13404
+ return [];
13405
+ }
13406
+ }
13407
+ /**
13408
+ * 마크다운 컨텐츠에서 요구사항 추출
13409
+ */
13410
+ extractRequirements(content, specId) {
13411
+ const { content: body } = matter8(content);
13412
+ const lines = body.split("\n");
13413
+ const requirements = [];
13414
+ const seenIds = /* @__PURE__ */ new Set();
13415
+ for (let i = 0; i < lines.length; i++) {
13416
+ const line = lines[i];
13417
+ const lineNum = i + 1;
13418
+ const headerMatch = line.match(REQ_HEADER_PATTERN);
13419
+ if (headerMatch) {
13420
+ const id = headerMatch[1].toUpperCase();
13421
+ if (!seenIds.has(id)) {
13422
+ seenIds.add(id);
13423
+ const keyword = this.findKeywordInContext(lines, i);
13424
+ requirements.push({
13425
+ id,
13426
+ specId,
13427
+ title: headerMatch[2].trim(),
13428
+ line: lineNum,
13429
+ keyword
13430
+ });
13431
+ }
13432
+ continue;
13433
+ }
13434
+ const matches = line.matchAll(REQ_ID_PATTERN);
13435
+ for (const match of matches) {
13436
+ const id = match[1].toUpperCase();
13437
+ if (!seenIds.has(id)) {
13438
+ seenIds.add(id);
13439
+ const keywordMatch = line.match(RFC_KEYWORD_PATTERN);
13440
+ requirements.push({
13441
+ id,
13442
+ specId,
13443
+ line: lineNum,
13444
+ description: line.trim(),
13445
+ keyword: keywordMatch ? keywordMatch[1].toUpperCase() : void 0
13446
+ });
13447
+ }
13448
+ }
13449
+ }
13450
+ return requirements;
13451
+ }
13452
+ /**
13453
+ * 컨텍스트에서 RFC 2119 키워드 찾기
13454
+ */
13455
+ findKeywordInContext(lines, headerIndex) {
13456
+ for (let i = headerIndex; i < Math.min(headerIndex + 5, lines.length); i++) {
13457
+ const match = lines[i].match(RFC_KEYWORD_PATTERN);
13458
+ if (match) {
13459
+ return match[1].toUpperCase();
13460
+ }
13461
+ }
13462
+ return void 0;
13463
+ }
13464
+ /**
13465
+ * 스펙 디렉토리 존재 여부
13466
+ */
13467
+ exists() {
13468
+ try {
13469
+ const stat = statSync(this.specsDir);
13470
+ return stat.isDirectory();
13471
+ } catch {
13472
+ return false;
13473
+ }
13474
+ }
13475
+ };
13476
+
13477
+ // src/core/sync/code-scanner.ts
13478
+ import { promises as fs23, statSync as statSync2 } from "fs";
13479
+ import path37 from "path";
13480
+ import { glob } from "glob";
13481
+ var SPEC_ANNOTATION_PATTERN = /@spec:?\s+(REQ-\d+(?:\s*,\s*REQ-\d+)*)/gi;
13482
+ var REQ_ID_PATTERN2 = /REQ-\d+/gi;
13483
+ var DEFAULT_EXCLUDE = [
13484
+ "**/node_modules/**",
13485
+ "**/dist/**",
13486
+ "**/build/**",
13487
+ "**/.git/**",
13488
+ "**/coverage/**",
13489
+ "**/*.test.ts",
13490
+ "**/*.test.js",
13491
+ "**/*.spec.ts",
13492
+ "**/*.spec.js"
13493
+ ];
13494
+ var DEFAULT_INCLUDE = ["**/*.ts", "**/*.js", "**/*.tsx", "**/*.jsx"];
13495
+ var CodeScanner = class {
13496
+ constructor(projectRoot, options = {}) {
13497
+ this.projectRoot = projectRoot;
13498
+ this.srcDir = options.srcDir || path37.join(projectRoot, "src");
13499
+ this.include = options.include || DEFAULT_INCLUDE;
13500
+ this.exclude = options.exclude || DEFAULT_EXCLUDE;
13501
+ }
13502
+ srcDir;
13503
+ include;
13504
+ exclude;
13505
+ /**
13506
+ * 소스 코드에서 @spec 주석 스캔
13507
+ */
13508
+ async scan() {
13509
+ const files = await this.findFiles();
13510
+ const references = [];
13511
+ for (const file of files) {
13512
+ const fileRefs = await this.scanFile(file);
13513
+ references.push(...fileRefs);
13514
+ }
13515
+ return references;
13516
+ }
13517
+ /**
13518
+ * 단일 파일 스캔
13519
+ */
13520
+ async scanFile(filePath) {
13521
+ try {
13522
+ const content = await fs23.readFile(filePath, "utf-8");
13523
+ const lines = content.split("\n");
13524
+ const references = [];
13525
+ for (let i = 0; i < lines.length; i++) {
13526
+ const line = lines[i];
13527
+ const lineNum = i + 1;
13528
+ const matches = line.matchAll(SPEC_ANNOTATION_PATTERN);
13529
+ for (const match of matches) {
13530
+ const reqIds = match[1].matchAll(REQ_ID_PATTERN2);
13531
+ for (const reqMatch of reqIds) {
13532
+ references.push({
13533
+ reqId: reqMatch[0].toUpperCase(),
13534
+ file: path37.relative(this.projectRoot, filePath),
13535
+ line: lineNum,
13536
+ type: "code",
13537
+ context: line.trim()
13538
+ });
13539
+ }
13540
+ }
13541
+ }
13542
+ return references;
13543
+ } catch {
13544
+ return [];
13545
+ }
13546
+ }
13547
+ /**
13548
+ * 스캔할 파일 목록 조회
13549
+ */
13550
+ async findFiles() {
13551
+ const normalizedSrcDir = this.srcDir.replace(/\\/g, "/");
13552
+ const patterns = this.include.map((p) => `${normalizedSrcDir}/${p}`);
13553
+ try {
13554
+ const files = await glob(patterns, {
13555
+ ignore: this.exclude,
13556
+ absolute: true,
13557
+ nodir: true
13558
+ });
13559
+ return files;
13560
+ } catch {
13561
+ return [];
13562
+ }
13563
+ }
13564
+ /**
13565
+ * 소스 디렉토리 존재 여부
13566
+ */
13567
+ exists() {
13568
+ try {
13569
+ const stat = statSync2(this.srcDir);
13570
+ return stat.isDirectory();
13571
+ } catch {
13572
+ return false;
13573
+ }
13574
+ }
13575
+ };
13576
+
13577
+ // src/core/sync/test-scanner.ts
13578
+ import { promises as fs24 } from "fs";
13579
+ import path38 from "path";
13580
+ import { glob as glob2 } from "glob";
13581
+ var TEST_REQ_PATTERN = /(?:it|test|describe)\s*\(\s*['"`]([^'"`]*REQ-\d+[^'"`]*)/gi;
13582
+ var REQ_ID_PATTERN3 = /REQ-\d+/gi;
13583
+ var TEST_PATTERNS = [
13584
+ "**/*.test.ts",
13585
+ "**/*.test.js",
13586
+ "**/*.spec.ts",
13587
+ "**/*.spec.js",
13588
+ "**/test/**/*.ts",
13589
+ "**/test/**/*.js",
13590
+ "**/tests/**/*.ts",
13591
+ "**/tests/**/*.js",
13592
+ "**/__tests__/**/*.ts",
13593
+ "**/__tests__/**/*.js"
13594
+ ];
13595
+ var EXCLUDE_PATTERNS = ["**/node_modules/**", "**/dist/**", "**/build/**"];
13596
+ var TestScanner = class {
13597
+ constructor(projectRoot, options = {}) {
13598
+ this.projectRoot = projectRoot;
13599
+ this.testDirs = options.testDirs || [projectRoot];
13600
+ }
13601
+ testDirs;
13602
+ /**
13603
+ * 테스트 파일에서 REQ-xxx 참조 스캔
13604
+ */
13605
+ async scan() {
13606
+ const files = await this.findTestFiles();
13607
+ const references = [];
13608
+ for (const file of files) {
13609
+ const fileRefs = await this.scanFile(file);
13610
+ references.push(...fileRefs);
13611
+ }
13612
+ return references;
13613
+ }
13614
+ /**
13615
+ * 단일 테스트 파일 스캔
13616
+ */
13617
+ async scanFile(filePath) {
13618
+ try {
13619
+ const content = await fs24.readFile(filePath, "utf-8");
13620
+ const lines = content.split("\n");
13621
+ const references = [];
13622
+ for (let i = 0; i < lines.length; i++) {
13623
+ const line = lines[i];
13624
+ const lineNum = i + 1;
13625
+ const matches = line.matchAll(TEST_REQ_PATTERN);
13626
+ for (const match of matches) {
13627
+ const testDescription = match[1];
13628
+ const reqIds = testDescription.matchAll(REQ_ID_PATTERN3);
13629
+ for (const reqMatch of reqIds) {
13630
+ references.push({
13631
+ reqId: reqMatch[0].toUpperCase(),
13632
+ file: path38.relative(this.projectRoot, filePath),
13633
+ line: lineNum,
13634
+ type: "test",
13635
+ context: testDescription.trim()
13636
+ });
13637
+ }
13638
+ }
13639
+ const specMatch = line.match(/@spec:?\s+(REQ-\d+(?:\s*,\s*REQ-\d+)*)/i);
13640
+ if (specMatch) {
13641
+ const reqIds = specMatch[1].matchAll(REQ_ID_PATTERN3);
13642
+ for (const reqMatch of reqIds) {
13643
+ const reqId = reqMatch[0].toUpperCase();
13644
+ const exists = references.some(
13645
+ (r) => r.reqId === reqId && r.file === path38.relative(this.projectRoot, filePath) && r.line === lineNum
13646
+ );
13647
+ if (!exists) {
13648
+ references.push({
13649
+ reqId,
13650
+ file: path38.relative(this.projectRoot, filePath),
13651
+ line: lineNum,
13652
+ type: "test",
13653
+ context: line.trim()
13654
+ });
13655
+ }
13656
+ }
13657
+ }
13658
+ }
13659
+ return references;
13660
+ } catch {
13661
+ return [];
13662
+ }
13663
+ }
13664
+ /**
13665
+ * 테스트 파일 목록 조회
13666
+ */
13667
+ async findTestFiles() {
13668
+ const allFiles = [];
13669
+ for (const dir of this.testDirs) {
13670
+ const normalizedDir = dir.replace(/\\/g, "/");
13671
+ const patterns = TEST_PATTERNS.map((p) => `${normalizedDir}/${p}`);
13672
+ try {
13673
+ const files = await glob2(patterns, {
13674
+ ignore: EXCLUDE_PATTERNS,
13675
+ absolute: true,
13676
+ nodir: true
13677
+ });
13678
+ allFiles.push(...files);
13679
+ } catch {
13680
+ }
13681
+ }
13682
+ return [...new Set(allFiles)];
13683
+ }
13684
+ };
13685
+
13686
+ // src/core/sync/matcher.ts
13687
+ var SyncMatcher = class {
13688
+ /**
13689
+ * 스펙 요구사항과 코드 참조를 매칭하여 동기화 결과 생성
13690
+ */
13691
+ match(requirements, codeRefs, testRefs) {
13692
+ const allRefs = [...codeRefs, ...testRefs];
13693
+ const requirementStatuses = this.calculateStatuses(requirements, allRefs);
13694
+ const specs = this.calculateSpecSummaries(requirements, requirementStatuses);
13695
+ const orphans = this.findOrphans(requirements, allRefs);
13696
+ const implemented = requirementStatuses.filter((r) => r.status === "implemented").map((r) => r.id);
13697
+ const missing = requirementStatuses.filter((r) => r.status === "missing").map((r) => r.id);
13698
+ const totalRequirements = requirements.length;
13699
+ const totalImplemented = implemented.length;
13700
+ const syncRate = totalRequirements > 0 ? totalImplemented / totalRequirements * 100 : 100;
13701
+ return {
13702
+ specs,
13703
+ requirements: requirementStatuses,
13704
+ syncRate: Math.round(syncRate * 100) / 100,
13705
+ implemented,
13706
+ missing,
13707
+ orphans,
13708
+ totalRequirements,
13709
+ totalImplemented
13710
+ };
13711
+ }
13712
+ /**
13713
+ * 요구사항별 상태 계산
13714
+ */
13715
+ calculateStatuses(requirements, allRefs) {
13716
+ return requirements.map((req) => {
13717
+ const matchingRefs = allRefs.filter((ref) => ref.reqId === req.id);
13718
+ const codeLocations = matchingRefs.map((ref) => ({
13719
+ file: ref.file,
13720
+ line: ref.line,
13721
+ type: ref.type,
13722
+ text: ref.context
13723
+ }));
13724
+ let status;
13725
+ if (matchingRefs.length === 0) {
13726
+ status = "missing";
13727
+ } else {
13728
+ const hasCode = matchingRefs.some((r) => r.type === "code");
13729
+ const hasTest = matchingRefs.some((r) => r.type === "test");
13730
+ status = hasCode || hasTest ? "implemented" : "missing";
13731
+ }
13732
+ return {
13733
+ id: req.id,
13734
+ specId: req.specId,
13735
+ title: req.title,
13736
+ keyword: req.keyword,
13737
+ status,
13738
+ locations: codeLocations
13739
+ };
13740
+ });
13741
+ }
13742
+ /**
13743
+ * 스펙별 요약 계산
13744
+ */
13745
+ calculateSpecSummaries(requirements, statuses) {
13746
+ const specIds = [...new Set(requirements.map((r) => r.specId))];
13747
+ return specIds.map((specId) => {
13748
+ const specReqs = statuses.filter((s) => s.specId === specId);
13749
+ const implementedCount = specReqs.filter((s) => s.status === "implemented").length;
13750
+ const missingCount = specReqs.filter((s) => s.status === "missing").length;
13751
+ const requirementCount = specReqs.length;
13752
+ const syncRate = requirementCount > 0 ? implementedCount / requirementCount * 100 : 100;
13753
+ const firstReq = requirements.find((r) => r.specId === specId);
13754
+ return {
13755
+ id: specId,
13756
+ title: firstReq?.title,
13757
+ requirementCount,
13758
+ implementedCount,
13759
+ missingCount,
13760
+ syncRate: Math.round(syncRate * 100) / 100
13761
+ };
13762
+ });
13763
+ }
13764
+ /**
13765
+ * 고아 코드 찾기 (스펙에 없는 REQ-xxx 참조)
13766
+ */
13767
+ findOrphans(requirements, allRefs) {
13768
+ const knownReqIds = new Set(requirements.map((r) => r.id));
13769
+ const orphanRefs = allRefs.filter((ref) => !knownReqIds.has(ref.reqId));
13770
+ const seen = /* @__PURE__ */ new Set();
13771
+ return orphanRefs.filter((ref) => {
13772
+ const key = `${ref.file}:${ref.reqId}`;
13773
+ if (seen.has(key)) return false;
13774
+ seen.add(key);
13775
+ return true;
13776
+ }).map((ref) => ({
13777
+ file: ref.file,
13778
+ line: ref.line,
13779
+ type: ref.type,
13780
+ text: `${ref.reqId}: ${ref.context || ""}`
13781
+ }));
13782
+ }
13783
+ };
13784
+
13785
+ // src/core/sync/reporter.ts
13786
+ var colors = {
13787
+ reset: "\x1B[0m",
13788
+ green: "\x1B[32m",
13789
+ red: "\x1B[31m",
13790
+ yellow: "\x1B[33m",
13791
+ cyan: "\x1B[36m",
13792
+ gray: "\x1B[90m",
13793
+ bold: "\x1B[1m"
13794
+ };
13795
+ var SyncReporter = class {
13796
+ useColors;
13797
+ constructor(options = {}) {
13798
+ this.useColors = options.colors !== false;
13799
+ }
13800
+ /**
13801
+ * 터미널 출력용 포맷
13802
+ */
13803
+ formatTerminal(result) {
13804
+ const lines = [];
13805
+ lines.push(this.header("SDD Sync: \uC2A4\uD399-\uCF54\uB4DC \uB3D9\uAE30\uD654 \uAC80\uC99D"));
13806
+ lines.push("");
13807
+ lines.push(
13808
+ `\uC2A4\uD399: ${result.specs.length}\uAC1C, \uC694\uAD6C\uC0AC\uD56D: ${result.totalRequirements}\uAC1C`
13809
+ );
13810
+ lines.push("");
13811
+ if (result.implemented.length > 0) {
13812
+ lines.push(this.success(`\u2713 \uAD6C\uD604\uB428 (${result.implemented.length}/${result.totalRequirements})`));
13813
+ for (const status of result.requirements.filter((r) => r.status === "implemented")) {
13814
+ lines.push(this.formatImplementedReq(status));
13815
+ }
13816
+ lines.push("");
13817
+ }
13818
+ if (result.missing.length > 0) {
13819
+ lines.push(this.error(`\u2717 \uBBF8\uAD6C\uD604 (${result.missing.length}/${result.totalRequirements})`));
13820
+ for (const reqId of result.missing) {
13821
+ const status = result.requirements.find((r) => r.id === reqId);
13822
+ lines.push(` - ${reqId}${status?.title ? `: ${status.title}` : ""}`);
13823
+ }
13824
+ lines.push("");
13825
+ }
13826
+ if (result.orphans.length > 0) {
13827
+ lines.push(this.warning(`\u26A0 \uC2A4\uD399 \uC5C6\uB294 \uCF54\uB4DC (${result.orphans.length}\uAC1C)`));
13828
+ for (const orphan of result.orphans) {
13829
+ lines.push(` - ${orphan.file}:${orphan.line} (${orphan.text || "orphan"})`);
13830
+ }
13831
+ lines.push("");
13832
+ }
13833
+ const rateColor = result.syncRate >= 80 ? "green" : result.syncRate >= 50 ? "yellow" : "red";
13834
+ lines.push(
13835
+ this.colorize(
13836
+ `\uB3D9\uAE30\uD654\uC728: ${result.syncRate}% (${result.totalImplemented}/${result.totalRequirements})`,
13837
+ rateColor
13838
+ )
13839
+ );
13840
+ return lines.join("\n");
13841
+ }
13842
+ /**
13843
+ * JSON 출력용 포맷
13844
+ */
13845
+ formatJson(result) {
13846
+ return JSON.stringify(result, null, 2);
13847
+ }
13848
+ /**
13849
+ * 마크다운 출력용 포맷
13850
+ */
13851
+ formatMarkdown(result) {
13852
+ const lines = [];
13853
+ lines.push("# SDD Sync \uB9AC\uD3EC\uD2B8");
13854
+ lines.push("");
13855
+ lines.push(`> \uC0DD\uC131\uC77C: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`);
13856
+ lines.push("");
13857
+ lines.push("## \uC694\uC57D");
13858
+ lines.push("");
13859
+ lines.push(`| \uC9C0\uD45C | \uAC12 |`);
13860
+ lines.push(`|------|-----|`);
13861
+ lines.push(`| \uC2A4\uD399 \uC218 | ${result.specs.length}\uAC1C |`);
13862
+ lines.push(`| \uC694\uAD6C\uC0AC\uD56D \uC218 | ${result.totalRequirements}\uAC1C |`);
13863
+ lines.push(`| \uAD6C\uD604\uB428 | ${result.totalImplemented}\uAC1C |`);
13864
+ lines.push(`| \uBBF8\uAD6C\uD604 | ${result.missing.length}\uAC1C |`);
13865
+ lines.push(`| \uB3D9\uAE30\uD654\uC728 | ${result.syncRate}% |`);
13866
+ lines.push("");
13867
+ lines.push("## \uC2A4\uD399\uBCC4 \uD604\uD669");
13868
+ lines.push("");
13869
+ lines.push("| \uC2A4\uD399 | \uC694\uAD6C\uC0AC\uD56D | \uAD6C\uD604 | \uBBF8\uAD6C\uD604 | \uB3D9\uAE30\uD654\uC728 |");
13870
+ lines.push("|------|----------|------|--------|----------|");
13871
+ for (const spec of result.specs) {
13872
+ lines.push(
13873
+ `| ${spec.id} | ${spec.requirementCount} | ${spec.implementedCount} | ${spec.missingCount} | ${spec.syncRate}% |`
13874
+ );
13875
+ }
13876
+ lines.push("");
13877
+ if (result.implemented.length > 0) {
13878
+ lines.push("## \uAD6C\uD604\uB41C \uC694\uAD6C\uC0AC\uD56D");
13879
+ lines.push("");
13880
+ for (const status of result.requirements.filter((r) => r.status === "implemented")) {
13881
+ lines.push(`### ${status.id}${status.title ? `: ${status.title}` : ""}`);
13882
+ lines.push("");
13883
+ if (status.locations.length > 0) {
13884
+ lines.push("**\uCC38\uC870 \uC704\uCE58:**");
13885
+ for (const loc of status.locations) {
13886
+ lines.push(`- \`${loc.file}:${loc.line}\` (${loc.type})`);
13887
+ }
13888
+ }
13889
+ lines.push("");
13890
+ }
13891
+ }
13892
+ if (result.missing.length > 0) {
13893
+ lines.push("## \uBBF8\uAD6C\uD604 \uC694\uAD6C\uC0AC\uD56D");
13894
+ lines.push("");
13895
+ for (const reqId of result.missing) {
13896
+ const status = result.requirements.find((r) => r.id === reqId);
13897
+ lines.push(`- **${reqId}**${status?.title ? `: ${status.title}` : ""}`);
13898
+ }
13899
+ lines.push("");
13900
+ }
13901
+ if (result.orphans.length > 0) {
13902
+ lines.push("## \uC2A4\uD399 \uC5C6\uB294 \uCF54\uB4DC");
13903
+ lines.push("");
13904
+ lines.push("> \uB2E4\uC74C \uCF54\uB4DC\uB4E4\uC740 \uC2A4\uD399\uC5D0 \uC5C6\uB294 REQ-xxx\uB97C \uCC38\uC870\uD558\uACE0 \uC788\uC2B5\uB2C8\uB2E4.");
13905
+ lines.push("");
13906
+ for (const orphan of result.orphans) {
13907
+ lines.push(`- \`${orphan.file}:${orphan.line}\`: ${orphan.text || ""}`);
13908
+ }
13909
+ }
13910
+ return lines.join("\n");
13911
+ }
13912
+ /**
13913
+ * 구현된 요구사항 포맷
13914
+ */
13915
+ formatImplementedReq(status) {
13916
+ const title2 = status.title ? `: ${status.title}` : "";
13917
+ const locations = status.locations.slice(0, 2).map((loc) => `${loc.file}:${loc.line}`).join(", ");
13918
+ const more = status.locations.length > 2 ? ` +${status.locations.length - 2}` : "";
13919
+ return ` - ${status.id}${title2} ${this.gray(`(${locations}${more})`)}`;
13920
+ }
13921
+ /**
13922
+ * 컬러 적용
13923
+ */
13924
+ colorize(text, color) {
13925
+ if (!this.useColors) return text;
13926
+ return `${colors[color]}${text}${colors.reset}`;
13927
+ }
13928
+ header(text) {
13929
+ return this.colorize(`=== ${text} ===`, "bold");
13930
+ }
13931
+ success(text) {
13932
+ return this.colorize(text, "green");
13933
+ }
13934
+ error(text) {
13935
+ return this.colorize(text, "red");
13936
+ }
13937
+ warning(text) {
13938
+ return this.colorize(text, "yellow");
13939
+ }
13940
+ gray(text) {
13941
+ return this.colorize(text, "gray");
13942
+ }
13943
+ };
13944
+
13945
+ // src/core/sync/index.ts
13946
+ async function executeSync(projectRoot, options = {}) {
13947
+ try {
13948
+ const specParser = new SpecParser(projectRoot);
13949
+ let requirements;
13950
+ if (options.specId) {
13951
+ requirements = await specParser.parseSpec(options.specId);
13952
+ } else {
13953
+ requirements = await specParser.parseAllSpecs();
13954
+ }
13955
+ if (requirements.length === 0) {
13956
+ return {
13957
+ success: true,
13958
+ data: {
13959
+ result: {
13960
+ specs: [],
13961
+ requirements: [],
13962
+ syncRate: 100,
13963
+ implemented: [],
13964
+ missing: [],
13965
+ orphans: [],
13966
+ totalRequirements: 0,
13967
+ totalImplemented: 0
13968
+ },
13969
+ output: "\uC2A4\uD399\uC5D0 \uC694\uAD6C\uC0AC\uD56D\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
13970
+ }
13971
+ };
13972
+ }
13973
+ const codeScanner = new CodeScanner(projectRoot, {
13974
+ srcDir: options.srcDir,
13975
+ include: options.include,
13976
+ exclude: options.exclude
13977
+ });
13978
+ const codeRefs = await codeScanner.scan();
13979
+ const testScanner = new TestScanner(projectRoot);
13980
+ const testRefs = await testScanner.scan();
13981
+ const matcher = new SyncMatcher();
13982
+ const result = matcher.match(requirements, codeRefs, testRefs);
13983
+ const reporter = new SyncReporter({ colors: !options.json && !options.markdown });
13984
+ let output;
13985
+ if (options.json) {
13986
+ output = reporter.formatJson(result);
13987
+ } else if (options.markdown) {
13988
+ output = reporter.formatMarkdown(result);
13989
+ } else {
13990
+ output = reporter.formatTerminal(result);
13991
+ }
13992
+ if (options.ci) {
13993
+ const threshold = options.threshold ?? 100;
13994
+ if (result.syncRate < threshold) {
13995
+ return {
13996
+ success: false,
13997
+ data: { result, output },
13998
+ error: new Error(
13999
+ `\uB3D9\uAE30\uD654\uC728 ${result.syncRate}%\uAC00 \uC784\uACC4\uAC12 ${threshold}% \uBBF8\uB9CC\uC785\uB2C8\uB2E4.`
14000
+ )
14001
+ };
14002
+ }
14003
+ }
14004
+ return {
14005
+ success: true,
14006
+ data: { result, output }
14007
+ };
14008
+ } catch (error2) {
14009
+ return {
14010
+ success: false,
14011
+ error: error2 instanceof Error ? error2 : new Error(String(error2))
14012
+ };
14013
+ }
14014
+ }
14015
+
14016
+ // src/cli/commands/sync.ts
14017
+ init_fs();
14018
+ init_errors();
14019
+ init_types();
14020
+ async function executeSyncCommand(specId, options, projectRoot) {
14021
+ const syncResult = await executeSync(projectRoot, {
14022
+ ...options,
14023
+ specId
14024
+ });
14025
+ if (!syncResult.success) {
14026
+ return failure(syncResult.error || new Error("\uB3D9\uAE30\uD654 \uAC80\uC99D \uC2E4\uD328"));
14027
+ }
14028
+ return success({
14029
+ result: syncResult.data.result,
14030
+ output: syncResult.data.output
14031
+ });
14032
+ }
14033
+ function registerSyncCommand(program2) {
14034
+ program2.command("sync [specId]").description("\uC2A4\uD399-\uCF54\uB4DC \uB3D9\uAE30\uD654 \uC0C1\uD0DC\uB97C \uAC80\uC99D\uD569\uB2C8\uB2E4").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--markdown", "\uB9C8\uD06C\uB2E4\uC6B4 \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--ci", "CI \uBAA8\uB4DC (\uB3D9\uAE30\uD654\uC728 \uC784\uACC4\uAC12 \uAC80\uC0AC)").option("--threshold <percent>", "\uB3D9\uAE30\uD654\uC728 \uC784\uACC4\uAC12 (\uAE30\uBCF8: 100)", parseInt).option("--src <dir>", "\uC18C\uC2A4 \uB514\uB809\uD1A0\uB9AC (\uAE30\uBCF8: ./src)").option("--include <patterns>", "\uD3EC\uD568 \uD328\uD134 (\uCF64\uB9C8 \uAD6C\uBD84)", parsePatterns).option("--exclude <patterns>", "\uC81C\uC678 \uD328\uD134 (\uCF64\uB9C8 \uAD6C\uBD84)", parsePatterns).action(async (specId, opts) => {
14035
+ try {
14036
+ const projectRoot = await findSddRoot(process.cwd());
14037
+ if (!projectRoot) {
14038
+ error("SDD \uD504\uB85C\uC81D\uD2B8\uAC00 \uC544\uB2D9\uB2C8\uB2E4. sdd init\uC744 \uBA3C\uC800 \uC2E4\uD589\uD558\uC138\uC694.");
14039
+ process.exit(ExitCode.INIT_ERROR);
14040
+ }
14041
+ const options = {
14042
+ specId,
14043
+ json: opts.json,
14044
+ markdown: opts.markdown,
14045
+ ci: opts.ci,
14046
+ threshold: opts.threshold,
14047
+ srcDir: opts.src,
14048
+ include: opts.include,
14049
+ exclude: opts.exclude
14050
+ };
14051
+ const result = await executeSyncCommand(specId, options, projectRoot);
14052
+ if (!result.success) {
14053
+ error(result.error.message);
14054
+ process.exit(ExitCode.VALIDATION_ERROR);
14055
+ }
14056
+ console.log(result.data.output);
14057
+ if (options.ci) {
14058
+ const threshold = options.threshold ?? 100;
14059
+ if (result.data.result.syncRate < threshold) {
14060
+ process.exit(ExitCode.VALIDATION_ERROR);
14061
+ }
14062
+ }
14063
+ if (result.data.result.missing.length > 0 && !options.json) {
14064
+ process.exit(1);
14065
+ }
14066
+ process.exit(ExitCode.SUCCESS);
14067
+ } catch (error2) {
14068
+ error(error2 instanceof Error ? error2.message : String(error2));
14069
+ process.exit(ExitCode.GENERAL_ERROR);
14070
+ }
14071
+ });
14072
+ }
14073
+ function parsePatterns(value) {
14074
+ return value.split(",").map((p) => p.trim());
14075
+ }
14076
+
14077
+ // src/core/diff/schemas.ts
14078
+ import { z as z10 } from "zod";
14079
+ var Rfc2119KeywordSchema = z10.enum([
14080
+ "SHALL",
14081
+ "SHALL NOT",
14082
+ "SHOULD",
14083
+ "SHOULD NOT",
14084
+ "MAY",
14085
+ "MUST",
14086
+ "MUST NOT",
14087
+ "REQUIRED",
14088
+ "RECOMMENDED",
14089
+ "OPTIONAL"
14090
+ ]);
14091
+ var ChangeTypeSchema2 = z10.enum(["added", "modified", "removed"]);
14092
+ var KeywordImpactSchema = z10.enum(["strengthened", "weakened", "changed"]);
14093
+ var RequirementDiffSchema = z10.object({
14094
+ id: z10.string(),
14095
+ type: ChangeTypeSchema2,
14096
+ title: z10.string().optional(),
14097
+ before: z10.string().optional(),
14098
+ after: z10.string().optional()
14099
+ });
14100
+ var ScenarioDiffSchema = z10.object({
14101
+ name: z10.string(),
14102
+ type: ChangeTypeSchema2,
14103
+ before: z10.string().optional(),
14104
+ after: z10.string().optional()
14105
+ });
14106
+ var MetadataDiffSchema = z10.object({
14107
+ type: ChangeTypeSchema2,
14108
+ before: z10.record(z10.unknown()).optional(),
14109
+ after: z10.record(z10.unknown()).optional(),
14110
+ changedFields: z10.array(z10.string())
14111
+ });
14112
+ var KeywordChangeSchema = z10.object({
14113
+ reqId: z10.string(),
14114
+ before: Rfc2119KeywordSchema,
14115
+ after: Rfc2119KeywordSchema,
14116
+ impact: KeywordImpactSchema
14117
+ });
14118
+ var SpecDiffSchema = z10.object({
14119
+ file: z10.string(),
14120
+ requirements: z10.array(RequirementDiffSchema),
14121
+ scenarios: z10.array(ScenarioDiffSchema),
14122
+ metadata: MetadataDiffSchema.optional(),
14123
+ keywordChanges: z10.array(KeywordChangeSchema)
14124
+ });
14125
+ var DiffResultSchema = z10.object({
14126
+ files: z10.array(SpecDiffSchema),
14127
+ summary: z10.object({
14128
+ totalFiles: z10.number(),
14129
+ addedRequirements: z10.number(),
14130
+ modifiedRequirements: z10.number(),
14131
+ removedRequirements: z10.number(),
14132
+ addedScenarios: z10.number(),
14133
+ modifiedScenarios: z10.number(),
14134
+ removedScenarios: z10.number(),
14135
+ keywordChanges: z10.number()
14136
+ })
14137
+ });
14138
+ var DiffOptionsSchema = z10.object({
14139
+ staged: z10.boolean().optional(),
14140
+ stat: z10.boolean().optional(),
14141
+ nameOnly: z10.boolean().optional(),
14142
+ json: z10.boolean().optional(),
14143
+ noColor: z10.boolean().optional(),
14144
+ specId: z10.string().optional(),
14145
+ commit1: z10.string().optional(),
14146
+ commit2: z10.string().optional()
14147
+ });
14148
+
14149
+ // src/core/diff/git-diff.ts
14150
+ import { exec as exec2 } from "child_process";
14151
+ import { promisify as promisify2 } from "util";
14152
+ import path39 from "path";
14153
+ var execAsync2 = promisify2(exec2);
14154
+ var GitDiff = class {
14155
+ constructor(projectRoot) {
14156
+ this.projectRoot = projectRoot;
14157
+ }
14158
+ /**
14159
+ * Git diff 실행하여 변경된 스펙 파일 목록 조회
14160
+ */
14161
+ async getChangedSpecFiles(options = {}) {
14162
+ const specsDir = path39.join(this.projectRoot, ".sdd", "specs");
14163
+ const specPath = options.specPath ? path39.join(specsDir, options.specPath) : specsDir;
14164
+ const normalizedPath = specPath.replace(/\\/g, "/");
14165
+ try {
14166
+ let diffCommand;
14167
+ if (options.commit1 && options.commit2) {
14168
+ diffCommand = `git diff --name-status ${options.commit1} ${options.commit2} -- "${normalizedPath}"`;
14169
+ } else if (options.commit1) {
14170
+ diffCommand = `git diff --name-status ${options.commit1} -- "${normalizedPath}"`;
14171
+ } else if (options.staged) {
14172
+ diffCommand = `git diff --name-status --cached -- "${normalizedPath}"`;
14173
+ } else {
14174
+ diffCommand = `git diff --name-status -- "${normalizedPath}"`;
14175
+ }
14176
+ const { stdout } = await execAsync2(diffCommand, { cwd: this.projectRoot });
14177
+ if (!stdout.trim()) {
14178
+ return [];
14179
+ }
14180
+ const files = [];
14181
+ for (const line of stdout.trim().split("\n")) {
14182
+ const [status, filePath] = line.split(" ");
14183
+ if (!filePath || !filePath.endsWith(".md")) continue;
14184
+ files.push({
14185
+ path: filePath,
14186
+ status: this.parseStatus(status)
14187
+ });
14188
+ }
14189
+ return files;
14190
+ } catch {
14191
+ return [];
14192
+ }
14193
+ }
14194
+ /**
14195
+ * 파일의 이전/이후 내용 조회
14196
+ */
14197
+ async getFileContents(filePath, options = {}) {
14198
+ const normalizedPath = filePath.replace(/\\/g, "/");
14199
+ try {
14200
+ let before;
14201
+ let after;
14202
+ if (options.commit1) {
14203
+ try {
14204
+ const { stdout } = await execAsync2(
14205
+ `git show ${options.commit1}:"${normalizedPath}"`,
14206
+ { cwd: this.projectRoot }
14207
+ );
14208
+ before = stdout;
14209
+ } catch {
14210
+ }
14211
+ } else if (options.staged) {
14212
+ try {
14213
+ const { stdout } = await execAsync2(`git show HEAD:"${normalizedPath}"`, {
14214
+ cwd: this.projectRoot
14215
+ });
14216
+ before = stdout;
14217
+ } catch {
14218
+ }
14219
+ } else {
14220
+ try {
14221
+ const { stdout } = await execAsync2(`git show HEAD:"${normalizedPath}"`, {
14222
+ cwd: this.projectRoot
14223
+ });
14224
+ before = stdout;
14225
+ } catch {
14226
+ }
14227
+ }
14228
+ if (options.commit2) {
14229
+ try {
14230
+ const { stdout } = await execAsync2(
14231
+ `git show ${options.commit2}:"${normalizedPath}"`,
14232
+ { cwd: this.projectRoot }
14233
+ );
14234
+ after = stdout;
14235
+ } catch {
14236
+ }
14237
+ } else if (options.staged) {
14238
+ try {
14239
+ const { stdout } = await execAsync2(`git show :0:"${normalizedPath}"`, {
14240
+ cwd: this.projectRoot
14241
+ });
14242
+ after = stdout;
14243
+ } catch {
14244
+ }
14245
+ } else {
14246
+ try {
14247
+ const fs27 = await import("fs/promises");
14248
+ const absolutePath = path39.join(this.projectRoot, filePath);
14249
+ after = await fs27.readFile(absolutePath, "utf-8");
14250
+ } catch {
14251
+ }
14252
+ }
14253
+ return { before, after };
14254
+ } catch {
14255
+ return {};
14256
+ }
14257
+ }
14258
+ /**
14259
+ * Git 상태 문자를 ChangeType으로 변환
14260
+ */
14261
+ parseStatus(status) {
14262
+ switch (status.charAt(0).toUpperCase()) {
14263
+ case "A":
14264
+ return "added";
14265
+ case "D":
14266
+ return "deleted";
14267
+ case "M":
14268
+ case "R":
14269
+ case "C":
14270
+ default:
14271
+ return "modified";
14272
+ }
14273
+ }
14274
+ /**
14275
+ * Git 저장소 여부 확인
14276
+ */
14277
+ async isGitRepository() {
14278
+ try {
14279
+ await execAsync2("git rev-parse --git-dir", { cwd: this.projectRoot });
14280
+ return true;
14281
+ } catch {
14282
+ return false;
14283
+ }
14284
+ }
14285
+ /**
14286
+ * 현재 브랜치 이름 조회
14287
+ */
14288
+ async getCurrentBranch() {
14289
+ try {
14290
+ const { stdout } = await execAsync2("git rev-parse --abbrev-ref HEAD", {
14291
+ cwd: this.projectRoot
14292
+ });
14293
+ return stdout.trim();
14294
+ } catch {
14295
+ return null;
14296
+ }
14297
+ }
14298
+ };
14299
+
14300
+ // src/core/diff/keyword-diff.ts
14301
+ var KEYWORD_PATTERN = /\b(SHALL NOT|SHALL|MUST NOT|MUST|SHOULD NOT|SHOULD|REQUIRED|RECOMMENDED|OPTIONAL|MAY)\b/gi;
14302
+ function normalizeKeyword(keyword) {
14303
+ return keyword.toUpperCase();
14304
+ }
14305
+ function extractKeywords2(content) {
14306
+ const matches = content.match(KEYWORD_PATTERN);
14307
+ if (!matches) return [];
14308
+ return matches.map(normalizeKeyword);
14309
+ }
14310
+
14311
+ // src/core/diff/structural-diff.ts
14312
+ function parseMetadata2(content) {
14313
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
14314
+ if (!frontmatterMatch) return {};
14315
+ const yaml = frontmatterMatch[1];
14316
+ const metadata = {};
14317
+ for (const line of yaml.split("\n")) {
14318
+ const colonIndex = line.indexOf(":");
14319
+ if (colonIndex > 0) {
14320
+ const key = line.slice(0, colonIndex).trim();
14321
+ const value = line.slice(colonIndex + 1).trim();
14322
+ const cleanValue = value.replace(/^["']|["']$/g, "");
14323
+ metadata[key] = cleanValue;
14324
+ }
14325
+ }
14326
+ return metadata;
14327
+ }
14328
+ function parseRequirements2(content) {
14329
+ const requirements = /* @__PURE__ */ new Map();
14330
+ const reqPattern = /^#{2,3}\s+(REQ-\d+):?\s*(.*)$/gm;
14331
+ let match;
14332
+ const lines = content.split("\n");
14333
+ let currentReqId = null;
14334
+ let currentTitle = "";
14335
+ let currentContent = [];
14336
+ for (let i = 0; i < lines.length; i++) {
14337
+ const line = lines[i];
14338
+ const reqMatch = line.match(/^#{2,3}\s+(REQ-\d+):?\s*(.*)$/);
14339
+ if (reqMatch) {
14340
+ if (currentReqId) {
14341
+ requirements.set(currentReqId, {
14342
+ title: currentTitle,
14343
+ content: currentContent.join("\n").trim()
14344
+ });
14345
+ }
14346
+ currentReqId = reqMatch[1];
14347
+ currentTitle = reqMatch[2] || "";
14348
+ currentContent = [];
14349
+ } else if (currentReqId) {
14350
+ if (line.match(/^#{1,3}\s+/) && !line.match(/^#{2,3}\s+REQ-\d+/)) {
14351
+ requirements.set(currentReqId, {
14352
+ title: currentTitle,
14353
+ content: currentContent.join("\n").trim()
14354
+ });
14355
+ currentReqId = null;
14356
+ currentContent = [];
14357
+ } else {
14358
+ currentContent.push(line);
14359
+ }
14360
+ }
14361
+ }
14362
+ if (currentReqId) {
14363
+ requirements.set(currentReqId, {
14364
+ title: currentTitle,
14365
+ content: currentContent.join("\n").trim()
14366
+ });
14367
+ }
14368
+ return requirements;
14369
+ }
14370
+ function parseScenarios2(content) {
14371
+ const scenarios = /* @__PURE__ */ new Map();
14372
+ const lines = content.split("\n");
14373
+ let currentScenario = null;
14374
+ let currentContent = [];
14375
+ for (const line of lines) {
14376
+ const scenarioMatch = line.match(/^#{2,3}\s+Scenario\s*\d*:?\s*(.*)$/i);
14377
+ if (scenarioMatch) {
14378
+ if (currentScenario) {
14379
+ scenarios.set(currentScenario, currentContent.join("\n").trim());
14380
+ }
14381
+ currentScenario = scenarioMatch[1] || `Scenario ${scenarios.size + 1}`;
14382
+ currentContent = [];
14383
+ } else if (currentScenario) {
14384
+ if (line.match(/^#{1,3}\s+/) && !line.match(/GIVEN|WHEN|THEN|AND/i)) {
14385
+ scenarios.set(currentScenario, currentContent.join("\n").trim());
14386
+ currentScenario = null;
14387
+ currentContent = [];
14388
+ } else {
14389
+ currentContent.push(line);
14390
+ }
14391
+ }
14392
+ }
14393
+ if (currentScenario) {
14394
+ scenarios.set(currentScenario, currentContent.join("\n").trim());
14395
+ }
14396
+ return scenarios;
14397
+ }
14398
+ function parseSpec2(content) {
14399
+ return {
14400
+ metadata: parseMetadata2(content),
14401
+ requirements: parseRequirements2(content),
14402
+ scenarios: parseScenarios2(content)
14403
+ };
14404
+ }
14405
+ function diffMetadata(before, after) {
14406
+ const allKeys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
14407
+ const changedFields = [];
14408
+ for (const key of allKeys) {
14409
+ if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
14410
+ changedFields.push(key);
14411
+ }
14412
+ }
14413
+ if (changedFields.length === 0) {
14414
+ return void 0;
14415
+ }
14416
+ const beforeEmpty = Object.keys(before).length === 0;
14417
+ const afterEmpty = Object.keys(after).length === 0;
14418
+ return {
14419
+ type: beforeEmpty ? "added" : afterEmpty ? "removed" : "modified",
14420
+ before: beforeEmpty ? void 0 : before,
14421
+ after: afterEmpty ? void 0 : after,
14422
+ changedFields
14423
+ };
14424
+ }
14425
+ function diffRequirements(before, after) {
14426
+ const diffs = [];
14427
+ const allIds = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
14428
+ for (const id of allIds) {
14429
+ const beforeReq = before.get(id);
14430
+ const afterReq = after.get(id);
14431
+ if (!beforeReq && afterReq) {
14432
+ diffs.push({
14433
+ id,
14434
+ type: "added",
14435
+ title: afterReq.title,
14436
+ after: afterReq.content
14437
+ });
14438
+ } else if (beforeReq && !afterReq) {
14439
+ diffs.push({
14440
+ id,
14441
+ type: "removed",
14442
+ title: beforeReq.title,
14443
+ before: beforeReq.content
14444
+ });
14445
+ } else if (beforeReq && afterReq) {
14446
+ const beforeFull = `${beforeReq.title}
14447
+ ${beforeReq.content}`;
14448
+ const afterFull = `${afterReq.title}
14449
+ ${afterReq.content}`;
14450
+ if (beforeFull !== afterFull) {
14451
+ diffs.push({
14452
+ id,
14453
+ type: "modified",
14454
+ title: afterReq.title || beforeReq.title,
14455
+ before: beforeReq.content,
14456
+ after: afterReq.content
14457
+ });
14458
+ }
14459
+ }
14460
+ }
14461
+ return diffs.sort((a, b) => a.id.localeCompare(b.id));
14462
+ }
14463
+ function diffScenarios(before, after) {
14464
+ const diffs = [];
14465
+ const allNames = /* @__PURE__ */ new Set([...before.keys(), ...after.keys()]);
14466
+ for (const name of allNames) {
14467
+ const beforeScenario = before.get(name);
14468
+ const afterScenario = after.get(name);
14469
+ if (!beforeScenario && afterScenario) {
14470
+ diffs.push({
14471
+ name,
14472
+ type: "added",
14473
+ after: afterScenario
14474
+ });
14475
+ } else if (beforeScenario && !afterScenario) {
14476
+ diffs.push({
14477
+ name,
14478
+ type: "removed",
14479
+ before: beforeScenario
14480
+ });
14481
+ } else if (beforeScenario && afterScenario && beforeScenario !== afterScenario) {
14482
+ diffs.push({
14483
+ name,
14484
+ type: "modified",
14485
+ before: beforeScenario,
14486
+ after: afterScenario
14487
+ });
14488
+ }
14489
+ }
14490
+ return diffs;
14491
+ }
14492
+ function diffKeywords(beforeReqs, afterReqs) {
14493
+ const changes = [];
14494
+ for (const [id, afterReq] of afterReqs) {
14495
+ const beforeReq = beforeReqs.get(id);
14496
+ if (!beforeReq) continue;
14497
+ const beforeKeywords = extractKeywords2(beforeReq.content);
14498
+ const afterKeywords = extractKeywords2(afterReq.content);
14499
+ if (beforeKeywords.length > 0 && afterKeywords.length > 0) {
14500
+ const beforeMain = beforeKeywords[0];
14501
+ const afterMain = afterKeywords[0];
14502
+ if (beforeMain !== afterMain) {
14503
+ changes.push({
14504
+ reqId: id,
14505
+ before: beforeMain,
14506
+ after: afterMain,
14507
+ impact: getKeywordImpact(beforeMain, afterMain)
14508
+ });
14509
+ }
14510
+ }
14511
+ }
14512
+ return changes;
14513
+ }
14514
+ function getKeywordImpact(before, after) {
14515
+ const strength = {
14516
+ "SHALL": 3,
14517
+ "MUST": 3,
14518
+ "REQUIRED": 3,
14519
+ "SHALL NOT": 3,
14520
+ "MUST NOT": 3,
14521
+ "SHOULD": 2,
14522
+ "RECOMMENDED": 2,
14523
+ "SHOULD NOT": 2,
14524
+ "MAY": 1,
14525
+ "OPTIONAL": 1
14526
+ };
14527
+ const beforeStrength = strength[before] || 0;
14528
+ const afterStrength = strength[after] || 0;
14529
+ if (afterStrength > beforeStrength) return "strengthened";
14530
+ if (afterStrength < beforeStrength) return "weakened";
14531
+ return "changed";
14532
+ }
14533
+ function compareSpecs(beforeContent, afterContent, filePath) {
14534
+ const beforeSpec = beforeContent ? parseSpec2(beforeContent) : {
14535
+ metadata: {},
14536
+ requirements: /* @__PURE__ */ new Map(),
14537
+ scenarios: /* @__PURE__ */ new Map()
14538
+ };
14539
+ const afterSpec = afterContent ? parseSpec2(afterContent) : {
14540
+ metadata: {},
14541
+ requirements: /* @__PURE__ */ new Map(),
14542
+ scenarios: /* @__PURE__ */ new Map()
14543
+ };
14544
+ return {
14545
+ file: filePath,
14546
+ requirements: diffRequirements(beforeSpec.requirements, afterSpec.requirements),
14547
+ scenarios: diffScenarios(beforeSpec.scenarios, afterSpec.scenarios),
14548
+ metadata: diffMetadata(beforeSpec.metadata, afterSpec.metadata),
14549
+ keywordChanges: diffKeywords(beforeSpec.requirements, afterSpec.requirements)
14550
+ };
14551
+ }
14552
+
14553
+ // src/core/diff/formatter.ts
14554
+ var colors2 = {
14555
+ reset: "\x1B[0m",
14556
+ bold: "\x1B[1m",
14557
+ red: "\x1B[31m",
14558
+ green: "\x1B[32m",
14559
+ yellow: "\x1B[33m",
14560
+ blue: "\x1B[34m",
14561
+ magenta: "\x1B[35m",
14562
+ cyan: "\x1B[36m",
14563
+ gray: "\x1B[90m"
14564
+ };
14565
+ var DiffFormatter = class {
14566
+ options;
14567
+ constructor(options = {}) {
14568
+ this.options = {
14569
+ colors: options.colors ?? true,
14570
+ stat: options.stat ?? false,
14571
+ nameOnly: options.nameOnly ?? false
14572
+ };
14573
+ }
14574
+ /**
14575
+ * 컬러 적용
14576
+ */
14577
+ c(color, text) {
14578
+ if (!this.options.colors) return text;
14579
+ return `${colors2[color]}${text}${colors2.reset}`;
14580
+ }
14581
+ /**
14582
+ * 터미널 출력 포맷
14583
+ */
14584
+ formatTerminal(result) {
14585
+ if (result.files.length === 0) {
14586
+ return this.c("gray", "\uBCC0\uACBD\uB41C \uC2A4\uD399 \uD30C\uC77C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.");
14587
+ }
14588
+ if (this.options.nameOnly) {
14589
+ return this.formatNameOnly(result);
14590
+ }
14591
+ if (this.options.stat) {
14592
+ return this.formatStat(result);
14593
+ }
14594
+ return this.formatFull(result);
14595
+ }
14596
+ /**
14597
+ * 파일명만 출력
14598
+ */
14599
+ formatNameOnly(result) {
14600
+ return result.files.map((f) => f.file).join("\n");
14601
+ }
14602
+ /**
14603
+ * 통계 요약 출력
14604
+ */
14605
+ formatStat(result) {
14606
+ const lines = [];
14607
+ lines.push(this.c("bold", "=== SDD Diff --stat ==="));
14608
+ lines.push("");
14609
+ for (const file of result.files) {
14610
+ lines.push(this.c("cyan", file.file));
14611
+ const reqAdded = file.requirements.filter((r) => r.type === "added").length;
14612
+ const reqModified = file.requirements.filter((r) => r.type === "modified").length;
14613
+ const reqRemoved = file.requirements.filter((r) => r.type === "removed").length;
14614
+ const scenAdded = file.scenarios.filter((s) => s.type === "added").length;
14615
+ const scenModified = file.scenarios.filter((s) => s.type === "modified").length;
14616
+ const scenRemoved = file.scenarios.filter((s) => s.type === "removed").length;
14617
+ if (reqAdded + reqModified + reqRemoved > 0) {
14618
+ lines.push(` \uC694\uAD6C\uC0AC\uD56D: ${this.c("green", `+${reqAdded}`)}, ${this.c("yellow", `~${reqModified}`)}, ${this.c("red", `-${reqRemoved}`)}`);
14619
+ }
14620
+ if (scenAdded + scenModified + scenRemoved > 0) {
14621
+ lines.push(` \uC2DC\uB098\uB9AC\uC624: ${this.c("green", `+${scenAdded}`)}, ${this.c("yellow", `~${scenModified}`)}, ${this.c("red", `-${scenRemoved}`)}`);
14622
+ }
14623
+ if (file.keywordChanges.length > 0) {
14624
+ const strengthened = file.keywordChanges.filter((k) => k.impact === "strengthened").length;
14625
+ const weakened = file.keywordChanges.filter((k) => k.impact === "weakened").length;
14626
+ lines.push(` \uD0A4\uC6CC\uB4DC \uBCC0\uACBD: ${file.keywordChanges.length}\uAC1C (\uAC15\uD654: ${strengthened}, \uC57D\uD654: ${weakened})`);
14627
+ }
14628
+ lines.push("");
14629
+ }
14630
+ const { summary } = result;
14631
+ lines.push(this.c("bold", "\uCD1D \uBCC0\uACBD:"));
14632
+ lines.push(` ${summary.totalFiles}\uAC1C \uD30C\uC77C`);
14633
+ lines.push(` \uC694\uAD6C\uC0AC\uD56D: ${this.c("green", `+${summary.addedRequirements}`)} ${this.c("yellow", `~${summary.modifiedRequirements}`)} ${this.c("red", `-${summary.removedRequirements}`)}`);
14634
+ lines.push(` \uC2DC\uB098\uB9AC\uC624: ${this.c("green", `+${summary.addedScenarios}`)} ${this.c("yellow", `~${summary.modifiedScenarios}`)} ${this.c("red", `-${summary.removedScenarios}`)}`);
14635
+ if (summary.keywordChanges > 0) {
14636
+ lines.push(` \uD0A4\uC6CC\uB4DC \uBCC0\uACBD: ${this.c("magenta", `${summary.keywordChanges}\uAC1C`)}`);
14637
+ }
14638
+ return lines.join("\n");
14639
+ }
14640
+ /**
14641
+ * 전체 diff 출력
14642
+ */
14643
+ formatFull(result) {
14644
+ const lines = [];
14645
+ lines.push(this.c("bold", "=== SDD Diff ==="));
14646
+ lines.push("");
14647
+ for (const file of result.files) {
14648
+ lines.push(this.c("cyan", file.file));
14649
+ lines.push("");
14650
+ if (file.requirements.length > 0) {
14651
+ lines.push(this.c("bold", " \uC694\uAD6C\uC0AC\uD56D \uBCC0\uACBD:"));
14652
+ for (const req of file.requirements) {
14653
+ lines.push(...this.formatRequirementDiff(req));
14654
+ }
14655
+ lines.push("");
14656
+ }
14657
+ if (file.scenarios.length > 0) {
14658
+ lines.push(this.c("bold", " \uC2DC\uB098\uB9AC\uC624 \uBCC0\uACBD:"));
14659
+ for (const scen of file.scenarios) {
14660
+ lines.push(...this.formatScenarioDiff(scen));
14661
+ }
14662
+ lines.push("");
14663
+ }
14664
+ if (file.keywordChanges.length > 0) {
14665
+ lines.push(this.c("bold", " \uD0A4\uC6CC\uB4DC \uBCC0\uACBD:"));
14666
+ for (const kw of file.keywordChanges) {
14667
+ lines.push(this.formatKeywordChange(kw));
14668
+ }
14669
+ lines.push("");
14670
+ }
14671
+ }
14672
+ return lines.join("\n");
14673
+ }
14674
+ /**
14675
+ * 요구사항 diff 포맷
14676
+ */
14677
+ formatRequirementDiff(req) {
14678
+ const lines = [];
14679
+ const prefix = req.type === "added" ? "+" : req.type === "removed" ? "-" : "~";
14680
+ const color = req.type === "added" ? "green" : req.type === "removed" ? "red" : "yellow";
14681
+ lines.push(this.c(color, ` ${prefix} ${req.id}: ${req.title || ""}`));
14682
+ if (req.type === "modified" && req.before && req.after) {
14683
+ const beforeLines = req.before.split("\n").slice(0, 2);
14684
+ const afterLines = req.after.split("\n").slice(0, 2);
14685
+ for (const line of beforeLines) {
14686
+ if (line.trim()) {
14687
+ lines.push(this.c("red", ` - ${line.trim()}`));
14688
+ }
14689
+ }
14690
+ for (const line of afterLines) {
14691
+ if (line.trim()) {
14692
+ lines.push(this.c("green", ` + ${line.trim()}`));
14693
+ }
14694
+ }
14695
+ } else if (req.type === "added" && req.after) {
14696
+ const afterLines = req.after.split("\n").slice(0, 2);
14697
+ for (const line of afterLines) {
14698
+ if (line.trim()) {
14699
+ lines.push(this.c("green", ` + ${line.trim()}`));
14700
+ }
14701
+ }
14702
+ } else if (req.type === "removed" && req.before) {
14703
+ const beforeLines = req.before.split("\n").slice(0, 2);
14704
+ for (const line of beforeLines) {
14705
+ if (line.trim()) {
14706
+ lines.push(this.c("red", ` - ${line.trim()}`));
14707
+ }
14708
+ }
14709
+ }
14710
+ return lines;
14711
+ }
14712
+ /**
14713
+ * 시나리오 diff 포맷
14714
+ */
14715
+ formatScenarioDiff(scen) {
14716
+ const lines = [];
14717
+ const prefix = scen.type === "added" ? "+" : scen.type === "removed" ? "-" : "~";
14718
+ const color = scen.type === "added" ? "green" : scen.type === "removed" ? "red" : "yellow";
14719
+ lines.push(this.c(color, ` ${prefix} ${scen.name}`));
14720
+ const content = scen.after || scen.before || "";
14721
+ const gwt = content.match(/\*\*(GIVEN|WHEN|THEN)\*\*\s*(.+)/gi);
14722
+ if (gwt) {
14723
+ for (const match of gwt.slice(0, 3)) {
14724
+ lines.push(this.c("gray", ` ${match.trim()}`));
14725
+ }
14726
+ }
14727
+ return lines;
14728
+ }
14729
+ /**
14730
+ * 키워드 변경 포맷
14731
+ */
14732
+ formatKeywordChange(kw) {
14733
+ const impactEmoji = kw.impact === "strengthened" ? "\u26A0\uFE0F" : kw.impact === "weakened" ? "\u26A1" : "\u{1F504}";
14734
+ const impactText = kw.impact === "strengthened" ? "\uAC15\uD654" : kw.impact === "weakened" ? "\uC57D\uD654" : "\uBCC0\uACBD";
14735
+ const impactColor = kw.impact === "strengthened" ? "yellow" : kw.impact === "weakened" ? "magenta" : "blue";
14736
+ return ` ${impactEmoji} ${kw.reqId}: ${this.c("red", kw.before)} \u2192 ${this.c("green", kw.after)} (${this.c(impactColor, impactText)})`;
14737
+ }
14738
+ /**
14739
+ * JSON 출력 포맷
14740
+ */
14741
+ formatJson(result) {
14742
+ return JSON.stringify(result, null, 2);
14743
+ }
14744
+ /**
14745
+ * 마크다운 출력 포맷
14746
+ */
14747
+ formatMarkdown(result) {
14748
+ const lines = [];
14749
+ lines.push("# SDD Diff \uB9AC\uD3EC\uD2B8");
14750
+ lines.push("");
14751
+ lines.push("## \uC694\uC57D");
14752
+ lines.push("");
14753
+ lines.push("| \uD56D\uBAA9 | \uAC12 |");
14754
+ lines.push("|------|-----|");
14755
+ lines.push(`| \uBCC0\uACBD\uB41C \uD30C\uC77C | ${result.summary.totalFiles}\uAC1C |`);
14756
+ lines.push(`| \uCD94\uAC00\uB41C \uC694\uAD6C\uC0AC\uD56D | ${result.summary.addedRequirements}\uAC1C |`);
14757
+ lines.push(`| \uC218\uC815\uB41C \uC694\uAD6C\uC0AC\uD56D | ${result.summary.modifiedRequirements}\uAC1C |`);
14758
+ lines.push(`| \uC0AD\uC81C\uB41C \uC694\uAD6C\uC0AC\uD56D | ${result.summary.removedRequirements}\uAC1C |`);
14759
+ lines.push(`| \uCD94\uAC00\uB41C \uC2DC\uB098\uB9AC\uC624 | ${result.summary.addedScenarios}\uAC1C |`);
14760
+ lines.push(`| \uC218\uC815\uB41C \uC2DC\uB098\uB9AC\uC624 | ${result.summary.modifiedScenarios}\uAC1C |`);
14761
+ lines.push(`| \uC0AD\uC81C\uB41C \uC2DC\uB098\uB9AC\uC624 | ${result.summary.removedScenarios}\uAC1C |`);
14762
+ lines.push(`| \uD0A4\uC6CC\uB4DC \uBCC0\uACBD | ${result.summary.keywordChanges}\uAC1C |`);
14763
+ lines.push("");
14764
+ for (const file of result.files) {
14765
+ lines.push(`## ${file.file}`);
14766
+ lines.push("");
14767
+ if (file.requirements.length > 0) {
14768
+ lines.push("### \uC694\uAD6C\uC0AC\uD56D \uBCC0\uACBD");
14769
+ lines.push("");
14770
+ for (const req of file.requirements) {
14771
+ const emoji = req.type === "added" ? "\u2795" : req.type === "removed" ? "\u2796" : "\u270F\uFE0F";
14772
+ lines.push(`- ${emoji} **${req.id}**: ${req.title || ""}`);
14773
+ }
14774
+ lines.push("");
14775
+ }
14776
+ if (file.scenarios.length > 0) {
14777
+ lines.push("### \uC2DC\uB098\uB9AC\uC624 \uBCC0\uACBD");
14778
+ lines.push("");
14779
+ for (const scen of file.scenarios) {
14780
+ const emoji = scen.type === "added" ? "\u2795" : scen.type === "removed" ? "\u2796" : "\u270F\uFE0F";
14781
+ lines.push(`- ${emoji} **${scen.name}**`);
14782
+ }
14783
+ lines.push("");
14784
+ }
14785
+ if (file.keywordChanges.length > 0) {
14786
+ lines.push("### \uD0A4\uC6CC\uB4DC \uBCC0\uACBD");
14787
+ lines.push("");
14788
+ for (const kw of file.keywordChanges) {
14789
+ const emoji = kw.impact === "strengthened" ? "\u26A0\uFE0F" : kw.impact === "weakened" ? "\u26A1" : "\u{1F504}";
14790
+ lines.push(`- ${emoji} **${kw.reqId}**: \`${kw.before}\` \u2192 \`${kw.after}\``);
14791
+ }
14792
+ lines.push("");
14793
+ }
14794
+ }
14795
+ return lines.join("\n");
14796
+ }
14797
+ };
14798
+
14799
+ // src/core/diff/index.ts
14800
+ async function executeDiff(projectRoot, options = {}) {
14801
+ const gitDiff = new GitDiff(projectRoot);
14802
+ const isGit = await gitDiff.isGitRepository();
14803
+ if (!isGit) {
14804
+ return {
14805
+ success: false,
14806
+ error: {
14807
+ code: "NOT_GIT_REPOSITORY",
14808
+ message: "Git \uC800\uC7A5\uC18C\uAC00 \uC544\uB2D9\uB2C8\uB2E4."
14809
+ }
14810
+ };
14811
+ }
14812
+ const changedFiles = await gitDiff.getChangedSpecFiles({
14813
+ staged: options.staged,
14814
+ commit1: options.commit1,
14815
+ commit2: options.commit2,
14816
+ specPath: options.specId
14817
+ });
14818
+ const specDiffs = [];
14819
+ for (const file of changedFiles) {
14820
+ const { before, after } = await gitDiff.getFileContents(file.path, {
14821
+ staged: options.staged,
14822
+ commit1: options.commit1,
14823
+ commit2: options.commit2
14824
+ });
14825
+ const specDiff = compareSpecs(before, after, file.path);
14826
+ if (specDiff.requirements.length > 0 || specDiff.scenarios.length > 0 || specDiff.metadata || specDiff.keywordChanges.length > 0) {
14827
+ specDiffs.push(specDiff);
14828
+ }
14829
+ }
14830
+ const summary = {
14831
+ totalFiles: specDiffs.length,
14832
+ addedRequirements: 0,
14833
+ modifiedRequirements: 0,
14834
+ removedRequirements: 0,
14835
+ addedScenarios: 0,
14836
+ modifiedScenarios: 0,
14837
+ removedScenarios: 0,
14838
+ keywordChanges: 0
14839
+ };
14840
+ for (const diff of specDiffs) {
14841
+ for (const req of diff.requirements) {
14842
+ if (req.type === "added") summary.addedRequirements++;
14843
+ else if (req.type === "modified") summary.modifiedRequirements++;
14844
+ else if (req.type === "removed") summary.removedRequirements++;
14845
+ }
14846
+ for (const scen of diff.scenarios) {
14847
+ if (scen.type === "added") summary.addedScenarios++;
14848
+ else if (scen.type === "modified") summary.modifiedScenarios++;
14849
+ else if (scen.type === "removed") summary.removedScenarios++;
14850
+ }
14851
+ summary.keywordChanges += diff.keywordChanges.length;
14852
+ }
14853
+ const result = {
14854
+ files: specDiffs,
14855
+ summary
14856
+ };
14857
+ const formatter = new DiffFormatter({
14858
+ colors: !options.noColor,
14859
+ stat: options.stat,
14860
+ nameOnly: options.nameOnly
14861
+ });
14862
+ let output;
14863
+ if (options.json) {
14864
+ output = formatter.formatJson(result);
14865
+ } else {
14866
+ output = formatter.formatTerminal(result);
14867
+ }
14868
+ return {
14869
+ success: true,
14870
+ data: {
14871
+ result,
14872
+ output
14873
+ }
14874
+ };
14875
+ }
14876
+
14877
+ // src/cli/commands/diff.ts
14878
+ function registerDiffCommand(program2) {
14879
+ program2.command("diff [commit1] [commit2]").description("\uC2A4\uD399 \uBCC0\uACBD\uC0AC\uD56D \uC2DC\uAC01\uD654").option("--staged", "\uC2A4\uD14C\uC774\uC9D5\uB41C \uBCC0\uACBD\uC0AC\uD56D\uB9CC \uD45C\uC2DC").option("--stat", "\uBCC0\uACBD \uD1B5\uACC4 \uC694\uC57D \uD45C\uC2DC").option("--name-only", "\uBCC0\uACBD\uB41C \uD30C\uC77C\uBA85\uB9CC \uD45C\uC2DC").option("--json", "JSON \uD615\uC2DD\uC73C\uB85C \uCD9C\uB825").option("--no-color", "\uCEEC\uB7EC \uCD9C\uB825 \uBE44\uD65C\uC131\uD654").option("-s, --spec <id>", "\uD2B9\uC815 \uC2A4\uD399\uB9CC \uBE44\uAD50").action(async (commit1, commit2, options) => {
14880
+ const projectRoot = process.cwd();
14881
+ const result = await executeDiff(projectRoot, {
14882
+ staged: options.staged,
14883
+ stat: options.stat,
14884
+ nameOnly: options.nameOnly,
14885
+ json: options.json,
14886
+ noColor: !options.color,
14887
+ specId: options.spec,
14888
+ commit1,
14889
+ commit2
14890
+ });
14891
+ if (!result.success) {
14892
+ console.error(`\uC624\uB958: ${result.error?.message}`);
14893
+ process.exit(1);
14894
+ }
14895
+ console.log(result.data?.output);
14896
+ });
14897
+ }
14898
+
14899
+ // src/core/export/index.ts
14900
+ import { promises as fs26 } from "fs";
14901
+ import path41 from "path";
14902
+
14903
+ // src/core/export/schemas.ts
14904
+ import { z as z11 } from "zod";
14905
+ var ExportFormatSchema = z11.enum(["html", "pdf", "json", "markdown"]);
14906
+ var ThemeSchema = z11.enum(["light", "dark"]);
14907
+ var ExportOptionsSchema = z11.object({
14908
+ format: ExportFormatSchema.default("html"),
14909
+ output: z11.string().optional(),
14910
+ theme: ThemeSchema.default("light"),
14911
+ template: z11.string().optional(),
14912
+ includeToc: z11.boolean().default(true),
14913
+ includeConstitution: z11.boolean().default(false),
14914
+ includeChanges: z11.boolean().default(false),
14915
+ all: z11.boolean().default(false),
14916
+ specIds: z11.array(z11.string()).optional()
14917
+ });
14918
+ var ParsedRequirementSchema = z11.object({
14919
+ id: z11.string(),
14920
+ title: z11.string(),
14921
+ description: z11.string(),
14922
+ keyword: z11.string().optional(),
14923
+ priority: z11.enum(["high", "medium", "low"]).optional()
14924
+ });
14925
+ var ParsedScenarioSchema = z11.object({
14926
+ id: z11.string(),
14927
+ title: z11.string(),
14928
+ given: z11.array(z11.string()),
14929
+ when: z11.array(z11.string()),
14930
+ then: z11.array(z11.string()),
14931
+ and: z11.array(z11.string()).optional()
14932
+ });
14933
+ var ParsedSpecSchema2 = z11.object({
14934
+ id: z11.string(),
14935
+ title: z11.string(),
14936
+ status: z11.string().optional(),
14937
+ version: z11.string().optional(),
14938
+ created: z11.string().optional(),
14939
+ author: z11.string().optional(),
14940
+ description: z11.string().optional(),
14941
+ requirements: z11.array(ParsedRequirementSchema),
14942
+ scenarios: z11.array(ParsedScenarioSchema),
14943
+ dependencies: z11.array(z11.string()),
14944
+ metadata: z11.record(z11.unknown()),
14945
+ rawContent: z11.string()
14946
+ });
14947
+ var ExportResultSchema = z11.object({
14948
+ success: z11.boolean(),
14949
+ outputPath: z11.string().optional(),
14950
+ format: ExportFormatSchema,
14951
+ specsExported: z11.number(),
14952
+ size: z11.number().optional(),
14953
+ error: z11.string().optional()
14954
+ });
14955
+
14956
+ // src/core/export/spec-parser.ts
14957
+ import { promises as fs25 } from "fs";
14958
+ import path40 from "path";
14959
+ import { glob as glob3 } from "glob";
14960
+ function parseMetadata3(content) {
14961
+ const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/);
14962
+ if (!frontmatterMatch) return {};
14963
+ const yaml = frontmatterMatch[1];
14964
+ const metadata = {};
14965
+ for (const line of yaml.split("\n")) {
14966
+ const colonIndex = line.indexOf(":");
14967
+ if (colonIndex > 0) {
14968
+ const key = line.slice(0, colonIndex).trim();
14969
+ let value = line.slice(colonIndex + 1).trim();
14970
+ value = value.replace(/^["']|["']$/g, "");
14971
+ if (value === "true") metadata[key] = true;
14972
+ else if (value === "false") metadata[key] = false;
14973
+ else if (/^\d+$/.test(value)) metadata[key] = parseInt(value, 10);
14974
+ else metadata[key] = value;
14975
+ }
14976
+ }
14977
+ return metadata;
14978
+ }
14979
+ function extractKeyword(text) {
14980
+ const match = text.match(/\((SHALL NOT|SHALL|MUST NOT|MUST|SHOULD NOT|SHOULD|MAY|REQUIRED|RECOMMENDED|OPTIONAL)\)/i);
14981
+ return match ? match[1].toUpperCase() : void 0;
14982
+ }
14983
+ function getPriority(keyword) {
14984
+ if (!keyword) return void 0;
14985
+ const upper = keyword.toUpperCase();
14986
+ if (["SHALL", "MUST", "REQUIRED", "SHALL NOT", "MUST NOT"].includes(upper)) return "high";
14987
+ if (["SHOULD", "RECOMMENDED", "SHOULD NOT"].includes(upper)) return "medium";
14988
+ if (["MAY", "OPTIONAL"].includes(upper)) return "low";
14989
+ return void 0;
14990
+ }
14991
+ function parseRequirements3(content) {
14992
+ const requirements = [];
14993
+ const lines = content.split("\n");
14994
+ let currentReq = null;
14995
+ let currentContent = [];
14996
+ for (const line of lines) {
14997
+ const reqMatch = line.match(/^#{2,3}\s+(REQ-\d+):?\s*(.*)$/);
14998
+ if (reqMatch) {
14999
+ if (currentReq && currentReq.id) {
15000
+ const description = currentContent.join("\n").trim();
15001
+ const keyword = extractKeyword(description);
15002
+ requirements.push({
15003
+ id: currentReq.id,
15004
+ title: currentReq.title || "",
15005
+ description: description.replace(/\([A-Z\s]+\)/g, "").trim(),
15006
+ keyword,
15007
+ priority: getPriority(keyword)
15008
+ });
15009
+ }
15010
+ currentReq = {
15011
+ id: reqMatch[1],
15012
+ title: reqMatch[2] || ""
15013
+ };
15014
+ currentContent = [];
15015
+ } else if (currentReq) {
15016
+ if (line.match(/^#{1,3}\s+/) && !line.match(/^#{2,3}\s+REQ-\d+/)) {
15017
+ const description = currentContent.join("\n").trim();
15018
+ const keyword = extractKeyword(description);
15019
+ requirements.push({
15020
+ id: currentReq.id,
15021
+ title: currentReq.title || "",
15022
+ description: description.replace(/\([A-Z\s]+\)/g, "").trim(),
15023
+ keyword,
15024
+ priority: getPriority(keyword)
15025
+ });
15026
+ currentReq = null;
15027
+ currentContent = [];
15028
+ } else {
15029
+ currentContent.push(line);
15030
+ }
15031
+ }
15032
+ }
15033
+ if (currentReq && currentReq.id) {
15034
+ const description = currentContent.join("\n").trim();
15035
+ const keyword = extractKeyword(description);
15036
+ requirements.push({
15037
+ id: currentReq.id,
15038
+ title: currentReq.title || "",
15039
+ description: description.replace(/\([A-Z\s]+\)/g, "").trim(),
15040
+ keyword,
15041
+ priority: getPriority(keyword)
15042
+ });
15043
+ }
15044
+ return requirements;
15045
+ }
15046
+ function parseScenarios3(content) {
15047
+ const scenarios = [];
15048
+ const lines = content.split("\n");
15049
+ let currentScenario = null;
15050
+ let scenarioIndex = 0;
15051
+ for (const line of lines) {
15052
+ const scenarioMatch = line.match(/^#{2,3}\s+Scenario\s*(\d*):?\s*(.*)$/i);
15053
+ if (scenarioMatch) {
15054
+ if (currentScenario && currentScenario.id) {
15055
+ scenarios.push({
15056
+ id: currentScenario.id,
15057
+ title: currentScenario.title || "",
15058
+ given: currentScenario.given || [],
15059
+ when: currentScenario.when || [],
15060
+ then: currentScenario.then || [],
15061
+ and: currentScenario.and
15062
+ });
15063
+ }
15064
+ scenarioIndex++;
15065
+ currentScenario = {
15066
+ id: `scenario-${scenarioMatch[1] || scenarioIndex}`,
15067
+ title: scenarioMatch[2] || `Scenario ${scenarioIndex}`,
15068
+ given: [],
15069
+ when: [],
15070
+ then: [],
15071
+ and: []
15072
+ };
15073
+ } else if (currentScenario) {
15074
+ const gwtMatch = line.match(/[-*]\s*\*\*(GIVEN|WHEN|THEN|AND)\*\*\s*(.+)/i);
15075
+ if (gwtMatch) {
15076
+ const type = gwtMatch[1].toUpperCase();
15077
+ const text = gwtMatch[2].trim();
15078
+ if (type === "GIVEN") currentScenario.given?.push(text);
15079
+ else if (type === "WHEN") currentScenario.when?.push(text);
15080
+ else if (type === "THEN") currentScenario.then?.push(text);
15081
+ else if (type === "AND") currentScenario.and?.push(text);
15082
+ }
15083
+ if (line.match(/^#{1,3}\s+/) && !line.match(/Scenario/i)) {
15084
+ scenarios.push({
15085
+ id: currentScenario.id,
15086
+ title: currentScenario.title || "",
15087
+ given: currentScenario.given || [],
15088
+ when: currentScenario.when || [],
15089
+ then: currentScenario.then || [],
15090
+ and: currentScenario.and
15091
+ });
15092
+ currentScenario = null;
15093
+ }
15094
+ }
15095
+ }
15096
+ if (currentScenario && currentScenario.id) {
15097
+ scenarios.push({
15098
+ id: currentScenario.id,
15099
+ title: currentScenario.title || "",
15100
+ given: currentScenario.given || [],
15101
+ when: currentScenario.when || [],
15102
+ then: currentScenario.then || [],
15103
+ and: currentScenario.and
15104
+ });
15105
+ }
15106
+ return scenarios;
15107
+ }
15108
+ function parseDependencies2(metadata) {
15109
+ const deps = metadata.dependencies;
15110
+ if (Array.isArray(deps)) return deps.map(String);
15111
+ if (typeof deps === "string") return deps.split(",").map((s) => s.trim());
15112
+ return [];
15113
+ }
15114
+ function parseTitle(content) {
15115
+ const match = content.match(/^#\s+(.+)$/m);
15116
+ return match ? match[1].trim() : "";
15117
+ }
15118
+ function parseDescription(content) {
15119
+ const match = content.match(/^>\s*(.+)$/m);
15120
+ return match ? match[1].trim() : "";
15121
+ }
15122
+ async function parseSpecFile(filePath, specId) {
15123
+ const content = await fs25.readFile(filePath, "utf-8");
15124
+ const metadata = parseMetadata3(content);
15125
+ const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*\n?/, "");
15126
+ return {
15127
+ id: metadata.id || specId,
15128
+ title: metadata.title || parseTitle(body) || specId,
15129
+ status: metadata.status,
15130
+ version: metadata.version,
15131
+ created: metadata.created,
15132
+ author: metadata.author,
15133
+ description: parseDescription(body),
15134
+ requirements: parseRequirements3(body),
15135
+ scenarios: parseScenarios3(body),
15136
+ dependencies: parseDependencies2(metadata),
15137
+ metadata,
15138
+ rawContent: body
15139
+ };
15140
+ }
15141
+ async function parseAllSpecs(projectRoot) {
15142
+ const specsDir = path40.join(projectRoot, ".sdd", "specs");
15143
+ const normalizedDir = specsDir.replace(/\\/g, "/");
15144
+ const specFiles = await glob3(`${normalizedDir}/**/spec.md`, {
15145
+ absolute: true,
15146
+ nodir: true
15147
+ });
15148
+ const specs = [];
15149
+ for (const filePath of specFiles) {
15150
+ const specDir = path40.dirname(filePath);
15151
+ const specId = path40.basename(specDir);
15152
+ try {
15153
+ const spec = await parseSpecFile(filePath, specId);
15154
+ specs.push(spec);
15155
+ } catch {
15156
+ }
15157
+ }
15158
+ return specs;
15159
+ }
15160
+ async function parseSpecById(projectRoot, specId) {
15161
+ const specPath = path40.join(projectRoot, ".sdd", "specs", specId, "spec.md");
15162
+ try {
15163
+ return await parseSpecFile(specPath, specId);
15164
+ } catch {
15165
+ return null;
15166
+ }
15167
+ }
15168
+
15169
+ // src/core/export/styles.ts
15170
+ var lightTheme = `
15171
+ :root {
15172
+ --bg-color: #ffffff;
15173
+ --text-color: #1a1a2e;
15174
+ --heading-color: #16213e;
15175
+ --border-color: #e0e0e0;
15176
+ --code-bg: #f5f5f5;
15177
+ --link-color: #0066cc;
15178
+ --toc-bg: #fafafa;
15179
+ --req-high: #dc3545;
15180
+ --req-medium: #ffc107;
15181
+ --req-low: #28a745;
15182
+ --keyword-shall: #dc3545;
15183
+ --keyword-should: #fd7e14;
15184
+ --keyword-may: #28a745;
15185
+ --scenario-bg: #f8f9fa;
15186
+ --gwt-given: #17a2b8;
15187
+ --gwt-when: #6f42c1;
15188
+ --gwt-then: #28a745;
15189
+ }
15190
+ `;
15191
+ var darkTheme = `
15192
+ :root {
15193
+ --bg-color: #1a1a2e;
15194
+ --text-color: #e0e0e0;
15195
+ --heading-color: #ffffff;
15196
+ --border-color: #3a3a5e;
15197
+ --code-bg: #2a2a4e;
15198
+ --link-color: #66b3ff;
15199
+ --toc-bg: #16213e;
15200
+ --req-high: #ff6b6b;
15201
+ --req-medium: #ffd93d;
15202
+ --req-low: #6bcb77;
15203
+ --keyword-shall: #ff6b6b;
15204
+ --keyword-should: #ffa502;
15205
+ --keyword-may: #6bcb77;
15206
+ --scenario-bg: #16213e;
15207
+ --gwt-given: #4ecdc4;
15208
+ --gwt-when: #a66cff;
15209
+ --gwt-then: #6bcb77;
15210
+ }
15211
+ `;
15212
+ var baseStyles = `
15213
+ * {
15214
+ box-sizing: border-box;
15215
+ margin: 0;
15216
+ padding: 0;
15217
+ }
15218
+
15219
+ body {
15220
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
15221
+ line-height: 1.6;
15222
+ color: var(--text-color);
15223
+ background-color: var(--bg-color);
15224
+ max-width: 1200px;
15225
+ margin: 0 auto;
15226
+ padding: 2rem;
15227
+ }
15228
+
15229
+ h1, h2, h3, h4, h5, h6 {
15230
+ color: var(--heading-color);
15231
+ margin-top: 1.5em;
15232
+ margin-bottom: 0.5em;
15233
+ line-height: 1.3;
15234
+ }
15235
+
15236
+ h1 { font-size: 2.5rem; border-bottom: 3px solid var(--border-color); padding-bottom: 0.5rem; }
15237
+ h2 { font-size: 1.8rem; border-bottom: 1px solid var(--border-color); padding-bottom: 0.3rem; }
15238
+ h3 { font-size: 1.4rem; }
15239
+ h4 { font-size: 1.2rem; }
15240
+
15241
+ p { margin: 1em 0; }
15242
+
15243
+ a {
15244
+ color: var(--link-color);
15245
+ text-decoration: none;
15246
+ }
15247
+
15248
+ a:hover {
15249
+ text-decoration: underline;
15250
+ }
15251
+
15252
+ code {
15253
+ background: var(--code-bg);
15254
+ padding: 0.2em 0.4em;
15255
+ border-radius: 3px;
15256
+ font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
15257
+ font-size: 0.9em;
15258
+ }
15259
+
15260
+ pre {
15261
+ background: var(--code-bg);
15262
+ padding: 1rem;
15263
+ border-radius: 6px;
15264
+ overflow-x: auto;
15265
+ margin: 1em 0;
15266
+ }
15267
+
15268
+ pre code {
15269
+ background: none;
15270
+ padding: 0;
15271
+ }
15272
+
15273
+ /* \uBAA9\uCC28 */
15274
+ .toc {
15275
+ background: var(--toc-bg);
15276
+ border: 1px solid var(--border-color);
15277
+ border-radius: 8px;
15278
+ padding: 1.5rem;
15279
+ margin-bottom: 2rem;
15280
+ }
15281
+
15282
+ .toc h2 {
15283
+ font-size: 1.2rem;
15284
+ margin-top: 0;
15285
+ margin-bottom: 1rem;
15286
+ border: none;
15287
+ }
15288
+
15289
+ .toc ul {
15290
+ list-style: none;
15291
+ padding-left: 0;
15292
+ }
15293
+
15294
+ .toc li {
15295
+ margin: 0.5em 0;
15296
+ }
15297
+
15298
+ .toc ul ul {
15299
+ padding-left: 1.5rem;
15300
+ }
15301
+
15302
+ .toc a {
15303
+ display: inline-block;
15304
+ padding: 0.2em 0;
15305
+ }
15306
+
15307
+ /* \uBA54\uD0C0\uB370\uC774\uD130 */
15308
+ .metadata {
15309
+ display: flex;
15310
+ flex-wrap: wrap;
15311
+ gap: 1rem;
15312
+ margin: 1rem 0 2rem;
15313
+ padding: 1rem;
15314
+ background: var(--toc-bg);
15315
+ border-radius: 6px;
15316
+ font-size: 0.9rem;
15317
+ }
15318
+
15319
+ .metadata-item {
15320
+ display: flex;
15321
+ align-items: center;
15322
+ gap: 0.5rem;
15323
+ }
15324
+
15325
+ .metadata-label {
15326
+ font-weight: 600;
15327
+ color: var(--heading-color);
15328
+ }
15329
+
15330
+ .metadata-value {
15331
+ color: var(--text-color);
15332
+ }
15333
+
15334
+ .status-badge {
15335
+ display: inline-block;
15336
+ padding: 0.2em 0.6em;
15337
+ border-radius: 4px;
15338
+ font-size: 0.85em;
15339
+ font-weight: 500;
15340
+ }
15341
+
15342
+ .status-draft { background: #ffc107; color: #000; }
15343
+ .status-review { background: #17a2b8; color: #fff; }
15344
+ .status-approved { background: #28a745; color: #fff; }
15345
+
15346
+ /* \uC694\uAD6C\uC0AC\uD56D */
15347
+ .requirement {
15348
+ border: 1px solid var(--border-color);
15349
+ border-radius: 8px;
15350
+ padding: 1.5rem;
15351
+ margin: 1.5rem 0;
15352
+ border-left: 4px solid var(--border-color);
15353
+ }
15354
+
15355
+ .requirement.priority-high { border-left-color: var(--req-high); }
15356
+ .requirement.priority-medium { border-left-color: var(--req-medium); }
15357
+ .requirement.priority-low { border-left-color: var(--req-low); }
15358
+
15359
+ .requirement-header {
15360
+ display: flex;
15361
+ align-items: center;
15362
+ gap: 1rem;
15363
+ margin-bottom: 1rem;
15364
+ }
15365
+
15366
+ .requirement-id {
15367
+ font-family: monospace;
15368
+ font-weight: 600;
15369
+ font-size: 0.9rem;
15370
+ padding: 0.2em 0.5em;
15371
+ background: var(--code-bg);
15372
+ border-radius: 4px;
15373
+ }
15374
+
15375
+ .requirement-title {
15376
+ font-size: 1.2rem;
15377
+ font-weight: 600;
15378
+ margin: 0;
15379
+ }
15380
+
15381
+ .requirement-description {
15382
+ margin: 0;
15383
+ }
15384
+
15385
+ /* RFC 2119 \uD0A4\uC6CC\uB4DC */
15386
+ .rfc-keyword {
15387
+ font-weight: 700;
15388
+ padding: 0.1em 0.3em;
15389
+ border-radius: 3px;
15390
+ }
15391
+
15392
+ .rfc-shall, .rfc-must, .rfc-required {
15393
+ color: var(--keyword-shall);
15394
+ }
15395
+
15396
+ .rfc-should, .rfc-recommended {
15397
+ color: var(--keyword-should);
15398
+ }
15399
+
15400
+ .rfc-may, .rfc-optional {
15401
+ color: var(--keyword-may);
15402
+ }
15403
+
15404
+ /* \uC2DC\uB098\uB9AC\uC624 */
15405
+ .scenario {
15406
+ background: var(--scenario-bg);
15407
+ border-radius: 8px;
15408
+ padding: 1.5rem;
15409
+ margin: 1.5rem 0;
15410
+ }
15411
+
15412
+ .scenario-title {
15413
+ font-size: 1.2rem;
15414
+ margin-bottom: 1rem;
15415
+ }
15416
+
15417
+ .gwt-list {
15418
+ list-style: none;
15419
+ padding: 0;
15420
+ }
15421
+
15422
+ .gwt-item {
15423
+ display: flex;
15424
+ align-items: flex-start;
15425
+ gap: 0.75rem;
15426
+ margin: 0.75rem 0;
15427
+ padding: 0.5rem;
15428
+ background: var(--bg-color);
15429
+ border-radius: 4px;
15430
+ }
15431
+
15432
+ .gwt-keyword {
15433
+ font-weight: 700;
15434
+ min-width: 60px;
15435
+ padding: 0.2em 0.5em;
15436
+ border-radius: 4px;
15437
+ text-align: center;
15438
+ font-size: 0.85rem;
15439
+ }
15440
+
15441
+ .gwt-given .gwt-keyword { background: var(--gwt-given); color: #fff; }
15442
+ .gwt-when .gwt-keyword { background: var(--gwt-when); color: #fff; }
15443
+ .gwt-then .gwt-keyword { background: var(--gwt-then); color: #fff; }
15444
+ .gwt-and .gwt-keyword { background: var(--border-color); color: var(--text-color); }
15445
+
15446
+ .gwt-text {
15447
+ flex: 1;
15448
+ }
15449
+
15450
+ /* \uC758\uC874\uC131 */
15451
+ .dependencies {
15452
+ margin: 1rem 0;
15453
+ }
15454
+
15455
+ .dependency-tag {
15456
+ display: inline-block;
15457
+ padding: 0.3em 0.8em;
15458
+ background: var(--code-bg);
15459
+ border-radius: 20px;
15460
+ font-size: 0.85rem;
15461
+ margin: 0.25rem;
15462
+ }
15463
+
15464
+ /* \uD478\uD130 */
15465
+ .footer {
15466
+ margin-top: 3rem;
15467
+ padding-top: 1.5rem;
15468
+ border-top: 1px solid var(--border-color);
15469
+ text-align: center;
15470
+ font-size: 0.85rem;
15471
+ color: var(--text-color);
15472
+ opacity: 0.7;
15473
+ }
15474
+
15475
+ /* \uC778\uC1C4 \uC2A4\uD0C0\uC77C */
15476
+ @media print {
15477
+ body {
15478
+ max-width: none;
15479
+ padding: 0;
15480
+ }
15481
+
15482
+ .toc {
15483
+ page-break-after: always;
15484
+ }
15485
+
15486
+ .requirement, .scenario {
15487
+ page-break-inside: avoid;
15488
+ }
15489
+
15490
+ a {
15491
+ color: inherit;
15492
+ }
15493
+
15494
+ a::after {
15495
+ content: " (" attr(href) ")";
15496
+ font-size: 0.8em;
15497
+ color: #666;
15498
+ }
15499
+ }
15500
+
15501
+ /* \uBC18\uC751\uD615 */
15502
+ @media (max-width: 768px) {
15503
+ body {
15504
+ padding: 1rem;
15505
+ }
15506
+
15507
+ h1 { font-size: 1.8rem; }
15508
+ h2 { font-size: 1.4rem; }
15509
+ h3 { font-size: 1.2rem; }
15510
+
15511
+ .metadata {
15512
+ flex-direction: column;
15513
+ gap: 0.5rem;
15514
+ }
15515
+
15516
+ .requirement-header {
15517
+ flex-direction: column;
15518
+ align-items: flex-start;
15519
+ }
15520
+ }
15521
+ `;
15522
+
15523
+ // src/core/export/html-exporter.ts
15524
+ function escapeHtml(text) {
15525
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
15526
+ }
15527
+ function highlightKeywords(text) {
15528
+ return text.replace(
15529
+ /\b(SHALL NOT|SHALL|MUST NOT|MUST|SHOULD NOT|SHOULD|REQUIRED|RECOMMENDED|OPTIONAL|MAY)\b/gi,
15530
+ (match) => {
15531
+ const keyword = match.toUpperCase().replace(" ", "-").toLowerCase();
15532
+ return `<span class="rfc-keyword rfc-${keyword}">${match}</span>`;
15533
+ }
15534
+ );
15535
+ }
15536
+ function convertMarkdown(text) {
15537
+ return text.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/`(.+?)`/g, "<code>$1</code>").replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2">$1</a>');
15538
+ }
15539
+ function generateToc(specs) {
15540
+ const items = [];
15541
+ for (const spec of specs) {
15542
+ items.push(`<li><a href="#spec-${spec.id}">${escapeHtml(spec.title || spec.id)}</a>`);
15543
+ if (spec.requirements.length > 0 || spec.scenarios.length > 0) {
15544
+ items.push("<ul>");
15545
+ for (const req of spec.requirements) {
15546
+ items.push(`<li><a href="#${req.id}">${req.id}: ${escapeHtml(req.title)}</a></li>`);
15547
+ }
15548
+ for (const scenario of spec.scenarios) {
15549
+ items.push(`<li><a href="#${scenario.id}">${escapeHtml(scenario.title)}</a></li>`);
15550
+ }
15551
+ items.push("</ul>");
15552
+ }
15553
+ items.push("</li>");
15554
+ }
15555
+ return `
15556
+ <nav class="toc">
15557
+ <h2>\uBAA9\uCC28</h2>
15558
+ <ul>
15559
+ ${items.join("\n ")}
15560
+ </ul>
15561
+ </nav>`;
15562
+ }
15563
+ function renderMetadata(spec) {
15564
+ const items = [];
15565
+ if (spec.status) {
15566
+ const statusClass = `status-${spec.status}`;
15567
+ items.push(`
15568
+ <div class="metadata-item">
15569
+ <span class="metadata-label">\uC0C1\uD0DC:</span>
15570
+ <span class="status-badge ${statusClass}">${escapeHtml(spec.status)}</span>
15571
+ </div>`);
15572
+ }
15573
+ if (spec.version) {
15574
+ items.push(`
15575
+ <div class="metadata-item">
15576
+ <span class="metadata-label">\uBC84\uC804:</span>
15577
+ <span class="metadata-value">${escapeHtml(spec.version)}</span>
15578
+ </div>`);
15579
+ }
15580
+ if (spec.created) {
15581
+ items.push(`
15582
+ <div class="metadata-item">
15583
+ <span class="metadata-label">\uC791\uC131\uC77C:</span>
15584
+ <span class="metadata-value">${escapeHtml(spec.created)}</span>
15585
+ </div>`);
15586
+ }
15587
+ if (spec.author) {
15588
+ items.push(`
15589
+ <div class="metadata-item">
15590
+ <span class="metadata-label">\uC791\uC131\uC790:</span>
15591
+ <span class="metadata-value">${escapeHtml(spec.author)}</span>
15592
+ </div>`);
15593
+ }
15594
+ if (items.length === 0) return "";
15595
+ return `<div class="metadata">${items.join("")}</div>`;
15596
+ }
15597
+ function renderRequirement(req) {
15598
+ const priorityClass = req.priority ? `priority-${req.priority}` : "";
15599
+ const description = highlightKeywords(convertMarkdown(escapeHtml(req.description)));
15600
+ return `
15601
+ <article id="${req.id}" class="requirement ${priorityClass}">
15602
+ <div class="requirement-header">
15603
+ <span class="requirement-id">${req.id}</span>
15604
+ <h4 class="requirement-title">${escapeHtml(req.title)}</h4>
15605
+ </div>
15606
+ <p class="requirement-description">${description}</p>
15607
+ </article>`;
15608
+ }
15609
+ function renderScenario(scenario) {
15610
+ const gwtItems = [];
15611
+ for (const given of scenario.given) {
15612
+ gwtItems.push(`
15613
+ <li class="gwt-item gwt-given">
15614
+ <span class="gwt-keyword">GIVEN</span>
15615
+ <span class="gwt-text">${convertMarkdown(escapeHtml(given))}</span>
15616
+ </li>`);
15617
+ }
15618
+ for (const when of scenario.when) {
15619
+ gwtItems.push(`
15620
+ <li class="gwt-item gwt-when">
15621
+ <span class="gwt-keyword">WHEN</span>
15622
+ <span class="gwt-text">${convertMarkdown(escapeHtml(when))}</span>
15623
+ </li>`);
15624
+ }
15625
+ for (const then of scenario.then) {
15626
+ gwtItems.push(`
15627
+ <li class="gwt-item gwt-then">
15628
+ <span class="gwt-keyword">THEN</span>
15629
+ <span class="gwt-text">${convertMarkdown(escapeHtml(then))}</span>
15630
+ </li>`);
15631
+ }
15632
+ if (scenario.and) {
15633
+ for (const and of scenario.and) {
15634
+ gwtItems.push(`
15635
+ <li class="gwt-item gwt-and">
15636
+ <span class="gwt-keyword">AND</span>
15637
+ <span class="gwt-text">${convertMarkdown(escapeHtml(and))}</span>
15638
+ </li>`);
15639
+ }
15640
+ }
15641
+ return `
15642
+ <article id="${scenario.id}" class="scenario">
15643
+ <h4 class="scenario-title">${escapeHtml(scenario.title)}</h4>
15644
+ <ul class="gwt-list">
15645
+ ${gwtItems.join("")}
15646
+ </ul>
15647
+ </article>`;
15648
+ }
15649
+ function renderDependencies(dependencies) {
15650
+ if (dependencies.length === 0) return "";
15651
+ const tags = dependencies.map((dep) => `<span class="dependency-tag">${escapeHtml(dep)}</span>`).join("");
15652
+ return `
15653
+ <div class="dependencies">
15654
+ <h4>\uC758\uC874\uC131</h4>
15655
+ ${tags}
15656
+ </div>`;
15657
+ }
15658
+ function renderSpec(spec) {
15659
+ const sections = [];
15660
+ sections.push(`
15661
+ <section id="spec-${spec.id}" class="spec">
15662
+ <header>
15663
+ <h2>${escapeHtml(spec.title || spec.id)}</h2>
15664
+ ${spec.description ? `<p class="description">${convertMarkdown(escapeHtml(spec.description))}</p>` : ""}
15665
+ ${renderMetadata(spec)}
15666
+ </header>`);
15667
+ if (spec.dependencies.length > 0) {
15668
+ sections.push(renderDependencies(spec.dependencies));
15669
+ }
15670
+ if (spec.requirements.length > 0) {
15671
+ sections.push(`
15672
+ <section class="requirements">
15673
+ <h3>\uC694\uAD6C\uC0AC\uD56D</h3>
15674
+ ${spec.requirements.map(renderRequirement).join("")}
15675
+ </section>`);
15676
+ }
15677
+ if (spec.scenarios.length > 0) {
15678
+ sections.push(`
15679
+ <section class="scenarios">
15680
+ <h3>\uC2DC\uB098\uB9AC\uC624</h3>
15681
+ ${spec.scenarios.map(renderScenario).join("")}
15682
+ </section>`);
15683
+ }
15684
+ sections.push("</section>");
15685
+ return sections.join("\n");
15686
+ }
15687
+ function generateHtml(specs, options = {}) {
15688
+ const { theme = "light", includeToc = true, title: title2 = "SDD \uC2A4\uD399 \uBB38\uC11C" } = options;
15689
+ const themeStyles = theme === "dark" ? darkTheme : lightTheme;
15690
+ const specContents = specs.map(renderSpec).join("\n<hr>\n");
15691
+ const toc = includeToc ? generateToc(specs) : "";
15692
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15693
+ return `<!DOCTYPE html>
15694
+ <html lang="ko">
15695
+ <head>
15696
+ <meta charset="UTF-8">
15697
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
15698
+ <meta name="generator" content="SDD Tool">
15699
+ <title>${escapeHtml(title2)}</title>
15700
+ <style>
15701
+ ${themeStyles}
15702
+ ${baseStyles}
15703
+ </style>
15704
+ </head>
15705
+ <body>
15706
+ <header>
15707
+ <h1>${escapeHtml(title2)}</h1>
15708
+ </header>
15709
+
15710
+ ${toc}
15711
+
15712
+ <main>
15713
+ ${specContents}
15714
+ </main>
15715
+
15716
+ <footer class="footer">
15717
+ <p>Generated by SDD Tool on ${timestamp}</p>
15718
+ </footer>
15719
+ </body>
15720
+ </html>`;
15721
+ }
15722
+
15723
+ // src/core/export/json-exporter.ts
15724
+ function generateJson(specs, options = {}) {
15725
+ const { pretty = true, includeRawContent = false } = options;
15726
+ const exportData = specs.map((spec) => {
15727
+ const result = {
15728
+ id: spec.id,
15729
+ title: spec.title,
15730
+ status: spec.status,
15731
+ version: spec.version,
15732
+ created: spec.created,
15733
+ author: spec.author,
15734
+ description: spec.description,
15735
+ requirements: spec.requirements.map((req) => ({
15736
+ id: req.id,
15737
+ title: req.title,
15738
+ description: req.description,
15739
+ keyword: req.keyword,
15740
+ priority: req.priority
15741
+ })),
15742
+ scenarios: spec.scenarios.map((scenario) => ({
15743
+ id: scenario.id,
15744
+ title: scenario.title,
15745
+ given: scenario.given,
15746
+ when: scenario.when,
15747
+ then: scenario.then,
15748
+ and: scenario.and?.length ? scenario.and : void 0
15749
+ })),
15750
+ dependencies: spec.dependencies,
15751
+ metadata: spec.metadata
15752
+ };
15753
+ if (includeRawContent) {
15754
+ result.rawContent = spec.rawContent;
15755
+ }
15756
+ return result;
15757
+ });
15758
+ const output = specs.length === 1 ? exportData[0] : exportData;
15759
+ return pretty ? JSON.stringify(output, null, 2) : JSON.stringify(output);
15760
+ }
15761
+
15762
+ // src/core/export/index.ts
15763
+ function getDefaultOutputPath(projectRoot, format, specIds) {
15764
+ const baseName = specIds?.length === 1 ? specIds[0] : "specs";
15765
+ return path41.join(projectRoot, `${baseName}.${format}`);
15766
+ }
15767
+ async function executeExport(projectRoot, options = {}) {
15768
+ const {
15769
+ format = "html",
15770
+ output,
15771
+ theme = "light",
15772
+ includeToc = true,
15773
+ all = false,
15774
+ specIds
15775
+ } = options;
15776
+ try {
15777
+ let specs = [];
15778
+ if (all) {
15779
+ specs = await parseAllSpecs(projectRoot);
15780
+ } else if (specIds && specIds.length > 0) {
15781
+ for (const specId of specIds) {
15782
+ const spec = await parseSpecById(projectRoot, specId);
15783
+ if (spec) {
15784
+ specs.push(spec);
15785
+ }
15786
+ }
15787
+ } else {
15788
+ specs = await parseAllSpecs(projectRoot);
15789
+ }
15790
+ if (specs.length === 0) {
15791
+ return {
15792
+ success: false,
15793
+ format,
15794
+ specsExported: 0,
15795
+ error: "\uB0B4\uBCF4\uB0BC \uC2A4\uD399\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."
15796
+ };
15797
+ }
15798
+ const outputPath = output || getDefaultOutputPath(projectRoot, format, specIds);
15799
+ let content;
15800
+ switch (format) {
15801
+ case "html":
15802
+ content = generateHtml(specs, {
15803
+ theme,
15804
+ includeToc,
15805
+ title: specs.length === 1 ? specs[0].title : "SDD \uC2A4\uD399 \uBB38\uC11C"
15806
+ });
15807
+ break;
15808
+ case "json":
15809
+ content = generateJson(specs, { pretty: true });
15810
+ break;
15811
+ case "markdown":
15812
+ content = specs.map((spec) => {
15813
+ const header = `# ${spec.title || spec.id}
15814
+
15815
+ `;
15816
+ return header + spec.rawContent;
15817
+ }).join("\n\n---\n\n");
15818
+ break;
15819
+ case "pdf": {
15820
+ content = generateHtml(specs, {
15821
+ theme,
15822
+ includeToc,
15823
+ title: specs.length === 1 ? specs[0].title : "SDD \uC2A4\uD399 \uBB38\uC11C"
15824
+ });
15825
+ const htmlPath = outputPath.replace(/\.pdf$/, ".html");
15826
+ await fs26.writeFile(htmlPath, content, "utf-8");
15827
+ return {
15828
+ success: true,
15829
+ outputPath: htmlPath,
15830
+ format: "html",
15831
+ specsExported: specs.length,
15832
+ size: Buffer.byteLength(content, "utf-8"),
15833
+ error: `PDF \uC9C1\uC811 \uC0DD\uC131\uC740 \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. ${htmlPath} \uD30C\uC77C\uC744 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C \uC5F4\uC5B4 PDF\uB85C \uC778\uC1C4\uD558\uC138\uC694.`
15834
+ };
15835
+ }
15836
+ default:
15837
+ return {
15838
+ success: false,
15839
+ format,
15840
+ specsExported: 0,
15841
+ error: `\uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uD615\uC2DD: ${format}`
15842
+ };
15843
+ }
15844
+ const outputDir = path41.dirname(outputPath);
15845
+ await fs26.mkdir(outputDir, { recursive: true });
15846
+ await fs26.writeFile(outputPath, content, "utf-8");
15847
+ return {
15848
+ success: true,
15849
+ outputPath,
15850
+ format,
15851
+ specsExported: specs.length,
15852
+ size: Buffer.byteLength(content, "utf-8")
15853
+ };
15854
+ } catch (error2) {
15855
+ return {
15856
+ success: false,
15857
+ format,
15858
+ specsExported: 0,
15859
+ error: error2 instanceof Error ? error2.message : String(error2)
15860
+ };
15861
+ }
15862
+ }
15863
+ function formatExportResult(result) {
15864
+ if (!result.success) {
15865
+ return `\uC624\uB958: ${result.error}`;
15866
+ }
15867
+ const lines = [];
15868
+ lines.push("=== SDD Export ===");
15869
+ lines.push("");
15870
+ lines.push(`\uD615\uC2DD: ${result.format.toUpperCase()}`);
15871
+ lines.push(`\uC2A4\uD399: ${result.specsExported}\uAC1C`);
15872
+ if (result.outputPath) {
15873
+ lines.push(`\uCD9C\uB825: ${result.outputPath}`);
15874
+ }
15875
+ if (result.size) {
15876
+ const sizeKb = (result.size / 1024).toFixed(2);
15877
+ lines.push(`\uD06C\uAE30: ${sizeKb} KB`);
15878
+ }
15879
+ if (result.error) {
15880
+ lines.push("");
15881
+ lines.push(`\uCC38\uACE0: ${result.error}`);
15882
+ }
15883
+ return lines.join("\n");
15884
+ }
15885
+
15886
+ // src/cli/commands/export.ts
15887
+ function registerExportCommand(program2) {
15888
+ program2.command("export [specId...]").description("\uC2A4\uD399\uC744 HTML, JSON \uB4F1 \uB2E4\uC591\uD55C \uD615\uC2DD\uC73C\uB85C \uB0B4\uBCF4\uB0B4\uAE30").option("-f, --format <format>", "\uCD9C\uB825 \uD615\uC2DD (html, json, markdown, pdf)", "html").option("-o, --output <path>", "\uCD9C\uB825 \uD30C\uC77C \uACBD\uB85C").option("--theme <theme>", "\uD14C\uB9C8 (light, dark)", "light").option("--all", "\uC804\uCCB4 \uC2A4\uD399 \uB0B4\uBCF4\uB0B4\uAE30", false).option("--toc", "\uBAA9\uCC28 \uD3EC\uD568", true).option("--no-toc", "\uBAA9\uCC28 \uC81C\uC678").option("--include-constitution", "Constitution \uD3EC\uD568", false).option("--include-changes", "\uBCC0\uACBD \uC81C\uC548 \uD3EC\uD568", false).option("--json", "JSON \uD615\uC2DD \uCD9C\uB825 (\uACB0\uACFC \uBA54\uD0C0\uC815\uBCF4)").action(async (specIds, options) => {
15889
+ const projectRoot = process.cwd();
15890
+ const result = await executeExport(projectRoot, {
15891
+ format: options.format,
15892
+ output: options.output,
15893
+ theme: options.theme,
15894
+ includeToc: options.toc,
15895
+ includeConstitution: options.includeConstitution,
15896
+ includeChanges: options.includeChanges,
15897
+ all: options.all,
15898
+ specIds: specIds.length > 0 ? specIds : void 0
15899
+ });
15900
+ if (options.json) {
15901
+ console.log(JSON.stringify(result, null, 2));
15902
+ } else {
15903
+ console.log(formatExportResult(result));
15904
+ }
15905
+ if (!result.success) {
15906
+ process.exit(1);
15907
+ }
15908
+ });
15909
+ }
15910
+
13011
15911
  // src/cli/index.ts
13012
15912
  var require2 = createRequire(import.meta.url);
13013
15913
  var pkg = require2("../../package.json");
@@ -13031,6 +15931,9 @@ registerQualityCommand(program);
13031
15931
  registerReportCommand(program);
13032
15932
  registerSearchCommand(program);
13033
15933
  registerPrepareCommand(program);
15934
+ registerSyncCommand(program);
15935
+ registerDiffCommand(program);
15936
+ registerExportCommand(program);
13034
15937
  function run() {
13035
15938
  program.parse();
13036
15939
  }