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/README.md +227 -183
- package/dist/cli/index.js +2972 -69
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +9 -4
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,
|
|
132
|
-
super(code, formatMessage(code,
|
|
133
|
+
constructor(code, path42) {
|
|
134
|
+
super(code, formatMessage(code, path42), ExitCode.FILE_SYSTEM_ERROR);
|
|
133
135
|
this.name = "FileSystemError";
|
|
134
|
-
this.path =
|
|
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\
|
|
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
|
-
\
|
|
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.
|
|
2144
|
-
2. \
|
|
2145
|
-
3. \
|
|
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
|
-
## \
|
|
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
|
-
|
|
2156
|
+
## \uBA85\uB839\uC5B4
|
|
2151
2157
|
|
|
2152
2158
|
\`\`\`bash
|
|
2153
|
-
# \
|
|
2154
|
-
|
|
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
|
-
# \
|
|
2157
|
-
|
|
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
|
-
|
|
2172
|
+
## \uAC10\uC9C0 \uB300\uC0C1
|
|
2161
2173
|
|
|
2162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2193
|
+
## \uCD9C\uB825 \uC608\uC2DC
|
|
2176
2194
|
|
|
2177
|
-
\`\`\`
|
|
2178
|
-
|
|
2195
|
+
\`\`\`
|
|
2196
|
+
=== SDD Prepare: user-auth ===
|
|
2179
2197
|
|
|
2180
|
-
|
|
2181
|
-
- ...
|
|
2198
|
+
\uBD84\uC11D \uB300\uC0C1: 3\uAC1C \uBB38\uC11C, 5\uAC1C \uD0DC\uC2A4\uD06C
|
|
2182
2199
|
|
|
2183
|
-
|
|
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
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2204
|
+
--- \uC2A4\uD0AC ---
|
|
2205
|
+
[x] test (\uC874\uC7AC)
|
|
2206
|
+
[ ] gen-api (\uC5C6\uC74C) \u2192 \uC0DD\uC131 \uD544\uC694
|
|
2189
2207
|
|
|
2190
|
-
|
|
2208
|
+
\uB204\uB77D\uB41C \uB3C4\uAD6C\uB97C \uC0DD\uC131\uD558\uC2DC\uACA0\uC2B5\uB2C8\uAE4C? (y/n)
|
|
2209
|
+
\`\`\`
|
|
2191
2210
|
|
|
2192
|
-
|
|
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:
|
|
4207
|
+
const { promises: fs27 } = await import("fs");
|
|
3938
4208
|
try {
|
|
3939
|
-
const entries = await
|
|
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,
|
|
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, [...
|
|
6340
|
+
if (dfs(depId, [...path42, nodeId])) {
|
|
6070
6341
|
return true;
|
|
6071
6342
|
}
|
|
6072
6343
|
} else if (recStack.has(depId)) {
|
|
6073
|
-
const cycleStart =
|
|
6074
|
-
const cycle = cycleStart >= 0 ? [...
|
|
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.
|
|
10460
|
-
const hasWarnings = data.
|
|
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.
|
|
10464
|
-
for (const specResult of data.
|
|
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.
|
|
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.
|
|
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.
|
|
11093
|
-
reportData.summary.validationWarnings = validationResult.data.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
11742
|
+
const filtered = filterByOptions(index, options);
|
|
11743
|
+
let results;
|
|
11472
11744
|
if (options.query) {
|
|
11473
|
-
results = searchByQuery(
|
|
11745
|
+
results = searchByQuery(filtered, options.query, options);
|
|
11474
11746
|
} else {
|
|
11475
|
-
results =
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
}
|