sdd-tool 0.7.2 → 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 +64 -4
- package/dist/cli/index.js +2912 -31
- 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 {
|
|
@@ -2776,6 +2778,249 @@ sdd prompt $ARGUMENTS
|
|
|
2776
2778
|
- \`--list\`: \uC0AC\uC6A9 \uAC00\uB2A5\uD55C \uD504\uB86C\uD504\uD2B8 \uBAA9\uB85D
|
|
2777
2779
|
|
|
2778
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.
|
|
2779
3024
|
`
|
|
2780
3025
|
}
|
|
2781
3026
|
];
|
|
@@ -3155,7 +3400,10 @@ var SpecMetadataSchema = z.object({
|
|
|
3155
3400
|
created: DateStringSchema,
|
|
3156
3401
|
depends: z.string().nullable().optional(),
|
|
3157
3402
|
command: z.string().optional(),
|
|
3158
|
-
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()
|
|
3159
3407
|
});
|
|
3160
3408
|
var RequirementLevelSchema = z.enum(["SHALL", "MUST", "SHOULD", "MAY"]);
|
|
3161
3409
|
var RequirementSchema = z.object({
|
|
@@ -3956,9 +4204,9 @@ async function validateSpecs(targetPath, options = {}) {
|
|
|
3956
4204
|
async function findSpecFiles(dirPath) {
|
|
3957
4205
|
const files = [];
|
|
3958
4206
|
async function scanDir(dir) {
|
|
3959
|
-
const { promises:
|
|
4207
|
+
const { promises: fs27 } = await import("fs");
|
|
3960
4208
|
try {
|
|
3961
|
-
const entries = await
|
|
4209
|
+
const entries = await fs27.readdir(dir, { withFileTypes: true });
|
|
3962
4210
|
for (const entry of entries) {
|
|
3963
4211
|
const fullPath = path3.join(dir, entry.name);
|
|
3964
4212
|
if (entry.isDirectory()) {
|
|
@@ -4733,7 +4981,8 @@ var ProposalMetadataSchema = z3.object({
|
|
|
4733
4981
|
});
|
|
4734
4982
|
var DeltaItemSchema = z3.object({
|
|
4735
4983
|
type: DeltaTypeSchema,
|
|
4736
|
-
target: z3.string(),
|
|
4984
|
+
target: z3.string().optional(),
|
|
4985
|
+
content: z3.string(),
|
|
4737
4986
|
before: z3.string().optional(),
|
|
4738
4987
|
after: z3.string().optional(),
|
|
4739
4988
|
description: z3.string().optional()
|
|
@@ -6081,19 +6330,19 @@ function detectCircularDependencies(graph) {
|
|
|
6081
6330
|
const cycles = [];
|
|
6082
6331
|
const visited = /* @__PURE__ */ new Set();
|
|
6083
6332
|
const recStack = /* @__PURE__ */ new Set();
|
|
6084
|
-
function dfs(nodeId,
|
|
6333
|
+
function dfs(nodeId, path42) {
|
|
6085
6334
|
visited.add(nodeId);
|
|
6086
6335
|
recStack.add(nodeId);
|
|
6087
6336
|
const node = graph.nodes.get(nodeId);
|
|
6088
6337
|
if (!node) return false;
|
|
6089
6338
|
for (const depId of node.dependsOn) {
|
|
6090
6339
|
if (!visited.has(depId)) {
|
|
6091
|
-
if (dfs(depId, [...
|
|
6340
|
+
if (dfs(depId, [...path42, nodeId])) {
|
|
6092
6341
|
return true;
|
|
6093
6342
|
}
|
|
6094
6343
|
} else if (recStack.has(depId)) {
|
|
6095
|
-
const cycleStart =
|
|
6096
|
-
const cycle = cycleStart >= 0 ? [...
|
|
6344
|
+
const cycleStart = path42.indexOf(depId);
|
|
6345
|
+
const cycle = cycleStart >= 0 ? [...path42.slice(cycleStart), nodeId, depId] : [nodeId, depId];
|
|
6097
6346
|
cycles.push({
|
|
6098
6347
|
cycle,
|
|
6099
6348
|
description: `\uC21C\uD658 \uC758\uC874\uC131: ${cycle.join(" \u2192 ")}`
|
|
@@ -10478,12 +10727,12 @@ async function runWatch(options) {
|
|
|
10478
10727
|
validationCount++;
|
|
10479
10728
|
if (result.success) {
|
|
10480
10729
|
const data = result.data;
|
|
10481
|
-
const hasErrors = data.
|
|
10482
|
-
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);
|
|
10483
10732
|
if (hasErrors) {
|
|
10484
10733
|
errorCount++;
|
|
10485
|
-
error(`\u274C \uAC80\uC99D \uC2E4\uD328: ${data.
|
|
10486
|
-
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) {
|
|
10487
10736
|
if (specResult.errors.length > 0) {
|
|
10488
10737
|
error(` ${specResult.file}:`);
|
|
10489
10738
|
for (const err of specResult.errors) {
|
|
@@ -10493,11 +10742,11 @@ async function runWatch(options) {
|
|
|
10493
10742
|
}
|
|
10494
10743
|
} else if (hasWarnings) {
|
|
10495
10744
|
if (!options.quiet) {
|
|
10496
|
-
warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.
|
|
10745
|
+
warn(`\u26A0\uFE0F \uAC80\uC99D \uC644\uB8CC: ${data.warnings}\uAC1C \uACBD\uACE0`);
|
|
10497
10746
|
}
|
|
10498
10747
|
} else {
|
|
10499
10748
|
if (!options.quiet) {
|
|
10500
|
-
success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.
|
|
10749
|
+
success2(`\u2705 \uAC80\uC99D \uD1B5\uACFC (${data.passed}\uAC1C \uC2A4\uD399)`);
|
|
10501
10750
|
}
|
|
10502
10751
|
}
|
|
10503
10752
|
} else {
|
|
@@ -11111,8 +11360,8 @@ async function generateReport(sddPath, options) {
|
|
|
11111
11360
|
const validationResult = await validateSpecs(sddPath, { strict: false });
|
|
11112
11361
|
if (validationResult.success) {
|
|
11113
11362
|
reportData.validation = validationResult.data;
|
|
11114
|
-
reportData.summary.validationErrors = validationResult.data.
|
|
11115
|
-
reportData.summary.validationWarnings = validationResult.data.
|
|
11363
|
+
reportData.summary.validationErrors = validationResult.data.failed;
|
|
11364
|
+
reportData.summary.validationWarnings = validationResult.data.warnings;
|
|
11116
11365
|
}
|
|
11117
11366
|
}
|
|
11118
11367
|
let content;
|
|
@@ -11160,14 +11409,14 @@ function renderHtmlReport(data) {
|
|
|
11160
11409
|
}
|
|
11161
11410
|
};
|
|
11162
11411
|
const statusBadge = (status) => {
|
|
11163
|
-
const
|
|
11412
|
+
const colors3 = {
|
|
11164
11413
|
draft: "#6b7280",
|
|
11165
11414
|
review: "#3b82f6",
|
|
11166
11415
|
approved: "#22c55e",
|
|
11167
11416
|
implemented: "#8b5cf6",
|
|
11168
11417
|
deprecated: "#ef4444"
|
|
11169
11418
|
};
|
|
11170
|
-
const color =
|
|
11419
|
+
const color = colors3[status] || "#6b7280";
|
|
11171
11420
|
return `<span style="background:${color};color:white;padding:2px 8px;border-radius:4px;font-size:12px;">${status}</span>`;
|
|
11172
11421
|
};
|
|
11173
11422
|
const specRows = data.specs.map((spec) => `
|
|
@@ -11490,11 +11739,12 @@ async function searchSpecs(sddPath, options = {}) {
|
|
|
11490
11739
|
return failure(indexResult.error);
|
|
11491
11740
|
}
|
|
11492
11741
|
const index = indexResult.data;
|
|
11493
|
-
|
|
11742
|
+
const filtered = filterByOptions(index, options);
|
|
11743
|
+
let results;
|
|
11494
11744
|
if (options.query) {
|
|
11495
|
-
results = searchByQuery(
|
|
11745
|
+
results = searchByQuery(filtered, options.query, options);
|
|
11496
11746
|
} else {
|
|
11497
|
-
results =
|
|
11747
|
+
results = filtered.map((item) => ({ ...item, score: 100, matches: [] }));
|
|
11498
11748
|
}
|
|
11499
11749
|
results = sortResults(results, options);
|
|
11500
11750
|
if (options.limit && options.limit > 0) {
|
|
@@ -11544,13 +11794,13 @@ async function collectSpecs(basePath, currentPath, index) {
|
|
|
11544
11794
|
index.push({
|
|
11545
11795
|
id: specId === "." ? entry.name : specId,
|
|
11546
11796
|
path: relativePath,
|
|
11547
|
-
title: metadata.title || specId,
|
|
11797
|
+
title: String(metadata.title || specId),
|
|
11548
11798
|
content,
|
|
11549
|
-
status: metadata.status || "unknown",
|
|
11550
|
-
phase: metadata.phase || "unknown",
|
|
11551
|
-
author: metadata.author || "",
|
|
11552
|
-
created: metadata.created || "",
|
|
11553
|
-
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]),
|
|
11554
11804
|
depends: parseDependencies(metadata.depends),
|
|
11555
11805
|
tags: parseTags(metadata.tags)
|
|
11556
11806
|
});
|
|
@@ -13030,6 +13280,2634 @@ async function runPrepare(feature, options) {
|
|
|
13030
13280
|
}
|
|
13031
13281
|
}
|
|
13032
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
|
+
|
|
13033
15911
|
// src/cli/index.ts
|
|
13034
15912
|
var require2 = createRequire(import.meta.url);
|
|
13035
15913
|
var pkg = require2("../../package.json");
|
|
@@ -13053,6 +15931,9 @@ registerQualityCommand(program);
|
|
|
13053
15931
|
registerReportCommand(program);
|
|
13054
15932
|
registerSearchCommand(program);
|
|
13055
15933
|
registerPrepareCommand(program);
|
|
15934
|
+
registerSyncCommand(program);
|
|
15935
|
+
registerDiffCommand(program);
|
|
15936
|
+
registerExportCommand(program);
|
|
13056
15937
|
function run() {
|
|
13057
15938
|
program.parse();
|
|
13058
15939
|
}
|