vibe-commander 0.2.0 → 0.2.2
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/AGENTS.md +18 -0
- package/README.ko.md +643 -0
- package/README.md +643 -0
- package/dist/adapters/cli/commands/context-resolver.js +30 -0
- package/dist/adapters/cli/commands/git-helpers.js +5 -3
- package/dist/adapters/cli/commands/init-helpers.js +1 -1
- package/dist/adapters/cli/commands/io-helpers.js +6 -2
- package/dist/adapters/cli/commands/list-units.d.ts +2 -3
- package/dist/adapters/cli/commands/list-units.js +8 -34
- package/dist/adapters/cli/commands/schema.js +7 -3
- package/dist/adapters/cli/commands/set-unit.d.ts +5 -0
- package/dist/adapters/cli/commands/set-unit.js +149 -10
- package/dist/adapters/cli/commands/skill-install.js +10 -4
- package/dist/adapters/cli/formatters-schema.d.ts +17 -0
- package/dist/adapters/cli/formatters-schema.js +155 -0
- package/dist/adapters/cli/formatters-unit.d.ts +0 -8
- package/dist/adapters/cli/formatters-unit.js +4 -146
- package/dist/adapters/cli/index.js +2 -1
- package/dist/adapters/cli/output.js +3 -0
- package/dist/adapters/cli/router.d.ts +1 -12
- package/dist/adapters/cli/router.js +12 -107
- package/dist/adapters/cli/stdin-parser.d.ts +23 -0
- package/dist/adapters/cli/stdin-parser.js +121 -0
- package/dist/config/schema.d.ts +2 -9
- package/dist/config/schema.js +143 -56
- package/dist/core/parsers/dep-line-parser.js +7 -3
- package/dist/core/parsers/dependency-extractor.js +80 -14
- package/dist/core/parsers/index.d.ts +2 -1
- package/dist/core/parsers/index.js +3 -1
- package/dist/core/parsers/plan-parser-helpers.js +5 -0
- package/dist/core/parsers/subsection-extractor.d.ts +20 -0
- package/dist/core/parsers/subsection-extractor.js +56 -0
- package/dist/core/parsers/title-extractor.d.ts +16 -0
- package/dist/core/parsers/title-extractor.js +26 -0
- package/dist/core/renderers/index.d.ts +2 -1
- package/dist/core/renderers/index.js +1 -1
- package/dist/core/renderers/marker-utils.js +5 -3
- package/dist/core/renderers/schema-renderer.d.ts +16 -0
- package/dist/core/renderers/schema-renderer.js +21 -0
- package/dist/core/renderers/section-renderer.d.ts +9 -1
- package/dist/core/renderers/section-renderer.js +14 -5
- package/dist/core/resolvers/backlog-resolver.d.ts +37 -0
- package/dist/core/resolvers/backlog-resolver.js +50 -0
- package/dist/core/resolvers/index.d.ts +0 -2
- package/examples/nextjs-web.config.json +73 -0
- package/examples/python-backend.config.json +91 -0
- package/examples/tauri-app.config.json +95 -0
- package/examples/vscode-tasks.json +101 -0
- package/package.json +14 -2
- package/schemas/config-schema.json +409 -0
- package/skills/claude/SKILL.md +22 -0
- package/skills/common/cli-reference.md +22 -0
- package/skills/cursor/SKILL.md +22 -0
- package/dist/adapters/cli/formatters.d.ts +0 -2
- package/dist/adapters/cli/formatters.js +0 -3
package/dist/config/schema.js
CHANGED
|
@@ -12,27 +12,43 @@ import { z } from 'zod';
|
|
|
12
12
|
/** 기본 경로 설정 스키마 */
|
|
13
13
|
export const pathsSchema = z.object({
|
|
14
14
|
/** 커맨드 파일 경로 (프로젝트 루트 기준) */
|
|
15
|
-
commands: z
|
|
15
|
+
commands: z
|
|
16
|
+
.string()
|
|
17
|
+
.default('commands.md')
|
|
18
|
+
.describe('Command file path where unit context sections are managed'),
|
|
16
19
|
/** 로드맵 파일 경로. null이면 list-units 비활성화 */
|
|
17
|
-
roadmap: z
|
|
20
|
+
roadmap: z
|
|
21
|
+
.string()
|
|
22
|
+
.nullable()
|
|
23
|
+
.default('roadmap.md')
|
|
24
|
+
.describe('Roadmap/backlog file path. Set to null to disable list-units command'),
|
|
18
25
|
/** 역할별 문서 루트 디렉토리 (키: 역할명, 값: 상대 경로) */
|
|
19
|
-
docRoots: z
|
|
26
|
+
docRoots: z
|
|
27
|
+
.record(z.string(), z.string())
|
|
28
|
+
.default({
|
|
20
29
|
plan: 'unit-plans',
|
|
21
30
|
result: 'unit-results',
|
|
22
31
|
runbook: 'unit-runbooks',
|
|
23
|
-
})
|
|
32
|
+
})
|
|
33
|
+
.describe('Document root directories by role (key: role name, value: relative path)'),
|
|
24
34
|
/** 리팩토링 서브유닛 디렉토리. null이면 서브유닛 미사용 */
|
|
25
|
-
refactorSubDir: z
|
|
35
|
+
refactorSubDir: z
|
|
36
|
+
.string()
|
|
37
|
+
.nullable()
|
|
38
|
+
.default(null)
|
|
39
|
+
.describe('Refactoring sub-unit directory. Set to null if sub-units are not used'),
|
|
26
40
|
});
|
|
27
41
|
// ── 유닛 유형 ──
|
|
28
42
|
/** 유닛 유형 정의 스키마 */
|
|
29
43
|
export const unitTypeSchema = z.object({
|
|
30
44
|
/** 유형 키 (예: "implement", "refactor") */
|
|
31
|
-
key: z.string(),
|
|
45
|
+
key: z.string().describe("Unique type key (e.g. 'implement', 'refactor', 'feature')"),
|
|
32
46
|
/** 표시 이름 (예: "유닛 구현"). 미제공 시 key 값 활용 */
|
|
33
|
-
displayName: z.string().optional(),
|
|
47
|
+
displayName: z.string().optional().describe('Human-readable display name for this type'),
|
|
34
48
|
/** 유닛 ID 매칭 정규식 패턴 (예: "^(U-\\d+|CP-).*") */
|
|
35
|
-
idPattern: z
|
|
49
|
+
idPattern: z
|
|
50
|
+
.string()
|
|
51
|
+
.refine((val) => {
|
|
36
52
|
try {
|
|
37
53
|
new RegExp(val);
|
|
38
54
|
return true;
|
|
@@ -40,46 +56,83 @@ export const unitTypeSchema = z.object({
|
|
|
40
56
|
catch {
|
|
41
57
|
return false;
|
|
42
58
|
}
|
|
43
|
-
}, { message: '유효한 정규식 문자열이어야 합니다' })
|
|
59
|
+
}, { message: '유효한 정규식 문자열이어야 합니다' })
|
|
60
|
+
.describe("Regex pattern to match unit IDs (e.g. '^U-\\\\d+.*', '^FEAT-\\\\d+$')"),
|
|
44
61
|
/** 계획서 디렉토리 (paths.docRoots 키 참조) */
|
|
45
|
-
planDir: z.string(),
|
|
62
|
+
planDir: z.string().describe('Document root key (from paths.docRoots) where plans are stored'),
|
|
46
63
|
/** 커맨드 파일 내 대상 섹션 헤더 (예: "# 유닛 구현") */
|
|
47
|
-
commandSection: z
|
|
64
|
+
commandSection: z
|
|
65
|
+
.string()
|
|
66
|
+
.describe("Target section header in the command file (e.g. '# Unit Implementation')"),
|
|
48
67
|
/** 의존성 수집 여부 */
|
|
49
|
-
collectDeps: z
|
|
68
|
+
collectDeps: z
|
|
69
|
+
.boolean()
|
|
70
|
+
.describe('Whether to collect dependency documents and commits for this type'),
|
|
50
71
|
/** 섹션 헤더 템플릿. {{unitId}}, {{title}} 등 변수 치환 지원 */
|
|
51
|
-
headerTemplate: z
|
|
72
|
+
headerTemplate: z
|
|
73
|
+
.string()
|
|
74
|
+
.describe('Section header template with variable interpolation: {{unitId}}, {{title}}, {{titleOnly}}, {{planPath}}, {{shortId}}'),
|
|
52
75
|
});
|
|
53
76
|
// ── 문서 탐색 ──
|
|
54
77
|
/** 개별 문서 역할의 탐색 규칙 스키마 */
|
|
55
78
|
export const docDiscoveryRuleSchema = z.object({
|
|
56
79
|
/** 파일 탐색 패턴 (예: "{{unitId}}.md") */
|
|
57
|
-
pattern: z
|
|
80
|
+
pattern: z
|
|
81
|
+
.string()
|
|
82
|
+
.describe('File search pattern. Supports {{unitId}} and {{shortId}} variables'),
|
|
58
83
|
/** glob 패턴 사용 여부 (기본: false) */
|
|
59
|
-
glob: z
|
|
84
|
+
glob: z
|
|
85
|
+
.boolean()
|
|
86
|
+
.optional()
|
|
87
|
+
.default(false)
|
|
88
|
+
.describe('Whether to use glob matching (true) or exact match (false)'),
|
|
60
89
|
});
|
|
61
90
|
/** 문서 탐색 설정 스키마 (역할별 규칙 맵) */
|
|
62
|
-
export const docDiscoverySchema = z
|
|
91
|
+
export const docDiscoverySchema = z
|
|
92
|
+
.record(z.string(), docDiscoveryRuleSchema)
|
|
93
|
+
.describe('Document discovery rules by role. Defines file name patterns for finding unit documents');
|
|
63
94
|
// ── 계획서 파싱 ──
|
|
64
95
|
/** 메타데이터 테이블 파싱 설정 스키마 */
|
|
65
96
|
export const metadataTableSchema = z.object({
|
|
66
97
|
/** ID 필드명 */
|
|
67
|
-
idField: z
|
|
98
|
+
idField: z
|
|
99
|
+
.string()
|
|
100
|
+
.default('Unit ID')
|
|
101
|
+
.describe('Field name for the unit ID in the metadata table'),
|
|
68
102
|
/** Phase 필드명 */
|
|
69
|
-
phaseField: z
|
|
103
|
+
phaseField: z
|
|
104
|
+
.string()
|
|
105
|
+
.default('Phase')
|
|
106
|
+
.describe('Field name for the phase in the metadata table'),
|
|
70
107
|
/** 의존성 필드명 */
|
|
71
|
-
dependsField: z
|
|
108
|
+
dependsField: z
|
|
109
|
+
.string()
|
|
110
|
+
.default('의존성')
|
|
111
|
+
.describe('Field name for dependencies in the metadata table'),
|
|
72
112
|
});
|
|
73
113
|
/** 계획서 파싱 설정 스키마 */
|
|
74
114
|
export const planParsingSchema = z.object({
|
|
75
115
|
/** 제목 추출 소스 ("h1" | "frontmatter:title") */
|
|
76
|
-
titleSource: z
|
|
116
|
+
titleSource: z
|
|
117
|
+
.enum(['h1', 'frontmatter:title'])
|
|
118
|
+
.default('h1')
|
|
119
|
+
.describe("How to extract the unit title: 'h1' from first heading, 'frontmatter:title' from YAML frontmatter"),
|
|
77
120
|
/** 의존성 추출 소스 ("section" | "frontmatter" | "metadata-table") */
|
|
78
|
-
dependsSource: z
|
|
121
|
+
dependsSource: z
|
|
122
|
+
.enum(['section', 'frontmatter', 'metadata-table'])
|
|
123
|
+
.default('section')
|
|
124
|
+
.describe("How to extract dependencies: 'section' from markdown section, 'frontmatter' from YAML, 'metadata-table' from 2-column table"),
|
|
79
125
|
/** 의존성 섹션 헤더명. null이면 섹션 기반 추출 비활성화 */
|
|
80
|
-
dependsSectionName: z
|
|
126
|
+
dependsSectionName: z
|
|
127
|
+
.string()
|
|
128
|
+
.nullable()
|
|
129
|
+
.default('이전 작업에서 가져올 것')
|
|
130
|
+
.describe('Dependency section header name. Set to null to disable section-based extraction'),
|
|
81
131
|
/** 페어링 질문 섹션 헤더명 */
|
|
82
|
-
pairingQuestionSection: z
|
|
132
|
+
pairingQuestionSection: z
|
|
133
|
+
.string()
|
|
134
|
+
.default('페어링 질문')
|
|
135
|
+
.describe('Section header name for pairing questions (checkbox items)'),
|
|
83
136
|
/** 메타데이터 테이블 파싱 설정 */
|
|
84
137
|
metadataTable: metadataTableSchema.prefault({}),
|
|
85
138
|
});
|
|
@@ -87,24 +140,28 @@ export const planParsingSchema = z.object({
|
|
|
87
140
|
/** 상태 표시 매핑 스키마 */
|
|
88
141
|
export const statusMapSchema = z.object({
|
|
89
142
|
/** 대기 상태 표시 */
|
|
90
|
-
ready: z.string().default('⏸️'),
|
|
143
|
+
ready: z.string().default('⏸️').describe('Ready/waiting status indicator'),
|
|
91
144
|
/** 진행중 상태 표시 */
|
|
92
|
-
inProgress: z.string().default('🚧'),
|
|
145
|
+
inProgress: z.string().default('🚧').describe('In-progress status indicator'),
|
|
93
146
|
/** 완료 상태 표시 */
|
|
94
|
-
completed: z.string().default('✅'),
|
|
147
|
+
completed: z.string().default('✅').describe('Completed status indicator'),
|
|
95
148
|
/** 차단 상태 표시 */
|
|
96
|
-
blocked: z.string().default('❌'),
|
|
149
|
+
blocked: z.string().default('❌').describe('Blocked status indicator'),
|
|
97
150
|
});
|
|
98
151
|
/** 백로그(로드맵) 파싱 설정 스키마 */
|
|
99
152
|
export const backlogParsingSchema = z.object({
|
|
100
153
|
/** 백로그 섹션 시작 헤더 */
|
|
101
|
-
sectionHeader: z
|
|
154
|
+
sectionHeader: z
|
|
155
|
+
.string()
|
|
156
|
+
.default('## 작업 백로그')
|
|
157
|
+
.describe('Backlog section start header in the roadmap file'),
|
|
102
158
|
/** 백로그 항목 매칭 정규식 (named group 지원) */
|
|
103
159
|
entryPattern: z
|
|
104
160
|
.string()
|
|
105
|
-
.default('ID=\\[(?<id>.+?)\\]\\((?<path>[^)]+)\\)\\s*\\|\\s*(?<title>[^|]+)\\|\\s*Depends=(?<deps>[^|]*)\\|\\s*(?<status>.+)')
|
|
161
|
+
.default('ID=\\[(?<id>.+?)\\]\\((?<path>[^)]+)\\)\\s*\\|\\s*(?<title>[^|]+)\\|\\s*Depends=(?<deps>[^|]*)\\|\\s*(?<status>.+)')
|
|
162
|
+
.describe('Regex pattern with named groups (id, path, title, deps, status) to match backlog entries'),
|
|
106
163
|
/** 완료된 유닛 섹션 헤더 */
|
|
107
|
-
completedSection: z.string().default('### 완료'),
|
|
164
|
+
completedSection: z.string().default('### 완료').describe('Section header for completed units'),
|
|
108
165
|
/** 상태 문자열 ↔ 내부 상태 키 매핑 */
|
|
109
166
|
statusMap: statusMapSchema.prefault({}),
|
|
110
167
|
});
|
|
@@ -112,40 +169,69 @@ export const backlogParsingSchema = z.object({
|
|
|
112
169
|
/** 커밋 탐색 설정 스키마 */
|
|
113
170
|
export const commitSearchSchema = z.object({
|
|
114
171
|
/** 탐색 전략 ("git-log-grep" | "conventional-commit" | "disabled") */
|
|
115
|
-
strategy: z
|
|
172
|
+
strategy: z
|
|
173
|
+
.string()
|
|
174
|
+
.default('git-log-grep')
|
|
175
|
+
.describe("Search strategy: 'git-log-grep', 'conventional-commit', or 'disabled'"),
|
|
116
176
|
/** grep에 사용할 ID 변환 방식 (예: "shortId") */
|
|
117
|
-
extractId: z
|
|
177
|
+
extractId: z
|
|
178
|
+
.string()
|
|
179
|
+
.default('shortId')
|
|
180
|
+
.describe("ID transformation for grep: 'shortId' removes phase tag (e.g. U-001[Mvp] → U-001)"),
|
|
118
181
|
/** 최대 결과 수 */
|
|
119
|
-
maxResults: z.number().default(5),
|
|
182
|
+
maxResults: z.number().default(5).describe('Maximum number of commits to return per dependency'),
|
|
120
183
|
/**
|
|
121
184
|
* 제외할 경로 glob 패턴 목록.
|
|
122
185
|
* 변경 파일이 모두 이 패턴에 매칭되는 커밋은 필터링됨 (문서 전용 커밋 제거).
|
|
123
186
|
* 기본값 [] → 필터 미적용
|
|
124
187
|
*/
|
|
125
|
-
excludePaths: z
|
|
188
|
+
excludePaths: z
|
|
189
|
+
.array(z.string())
|
|
190
|
+
.default([])
|
|
191
|
+
.describe("Glob patterns for paths to exclude (e.g. 'docs/**' to filter doc-only commits)"),
|
|
126
192
|
/**
|
|
127
193
|
* 필수 포함 경로 glob 패턴 목록.
|
|
128
194
|
* 변경 파일 중 하나라도 이 패턴에 매칭되어야 통과 (소스 코드 커밋만 허용).
|
|
129
195
|
* 기본값 [] → 필터 미적용
|
|
130
196
|
*/
|
|
131
|
-
requirePaths: z
|
|
197
|
+
requirePaths: z
|
|
198
|
+
.array(z.string())
|
|
199
|
+
.default([])
|
|
200
|
+
.describe("Glob patterns that at least one changed file must match (e.g. 'src/**' for source-only commits)"),
|
|
132
201
|
});
|
|
133
202
|
// ── 커맨드 파일 섹션 ──
|
|
134
203
|
/** 커맨드 파일 섹션 설정 스키마 */
|
|
135
204
|
export const commandSectionsSchema = z.object({
|
|
136
205
|
/** 섹션 구분자 문자열 */
|
|
137
|
-
separator: z
|
|
206
|
+
separator: z
|
|
207
|
+
.string()
|
|
208
|
+
.default('---------------------------------------------------')
|
|
209
|
+
.describe('Section separator string in the command file'),
|
|
138
210
|
/** 다른 섹션 보존 여부 */
|
|
139
|
-
preserveOtherSections: z
|
|
211
|
+
preserveOtherSections: z
|
|
212
|
+
.boolean()
|
|
213
|
+
.default(true)
|
|
214
|
+
.describe('Whether to preserve sections not managed by the current unit type'),
|
|
140
215
|
/** Commit SHA를 기록할 필드 키워드 (기본: "Commit") */
|
|
141
|
-
commitFieldName: z
|
|
216
|
+
commitFieldName: z
|
|
217
|
+
.string()
|
|
218
|
+
.default('Commit')
|
|
219
|
+
.describe('Field keyword for recording commit SHA in the command file'),
|
|
142
220
|
/**
|
|
143
221
|
* HTML 주석 마커 사용 여부.
|
|
144
222
|
* true이면 섹션 경계에 `<!-- vc:begin:{key} -->` / `<!-- vc:end:{key} -->` 마커를 삽입하고,
|
|
145
223
|
* 섹션 탐색 시 마커를 우선 사용한다 (Fallback으로 기존 헤더 기반 탐색).
|
|
146
224
|
* 기본값 false로 기존 프로젝트 하위 호환 보장.
|
|
147
225
|
*/
|
|
148
|
-
useMarkers: z
|
|
226
|
+
useMarkers: z
|
|
227
|
+
.boolean()
|
|
228
|
+
.default(false)
|
|
229
|
+
.describe('Use HTML comment markers (<!-- vc:begin/end -->) for safer section boundary detection'),
|
|
230
|
+
/** 특별 요청사항 섹션 헤딩 (기본: "### 특별 요청사항") */
|
|
231
|
+
specialRequestsHeading: z
|
|
232
|
+
.string()
|
|
233
|
+
.default('### 특별 요청사항')
|
|
234
|
+
.describe('Sub-heading for special requests in the command file'),
|
|
149
235
|
});
|
|
150
236
|
// ── 최상위 설정 ──
|
|
151
237
|
/** 기본 문서 탐색 설정 */
|
|
@@ -157,13 +243,16 @@ export const DEFAULT_DOC_DISCOVERY = {
|
|
|
157
243
|
/** 프로젝트 설정 스키마 (vibe-commander.config.json) */
|
|
158
244
|
export const projectConfigSchema = z.object({
|
|
159
245
|
/** JSON Schema URL (IDE 자동완성용) */
|
|
160
|
-
$schema: z.string().optional(),
|
|
246
|
+
$schema: z.string().optional().describe('JSON Schema URL for IDE autocomplete and validation'),
|
|
161
247
|
/** 설정 파일 버전 (현재 1 고정) */
|
|
162
|
-
version: z.literal(1),
|
|
248
|
+
version: z.literal(1).describe('Configuration file version (currently 1)'),
|
|
163
249
|
/** 기본 경로 설정 */
|
|
164
250
|
paths: pathsSchema.prefault({}),
|
|
165
251
|
/** 유닛 유형 정의 배열 (순서 = ID 매칭 우선순위, 최소 1개 필수) */
|
|
166
|
-
unitTypes: z
|
|
252
|
+
unitTypes: z
|
|
253
|
+
.array(unitTypeSchema)
|
|
254
|
+
.min(1)
|
|
255
|
+
.describe('Unit type definitions. Array order determines ID matching priority (first match wins)'),
|
|
167
256
|
/** 문서 탐색 규칙 (역할별) */
|
|
168
257
|
docDiscovery: docDiscoverySchema.default(DEFAULT_DOC_DISCOVERY),
|
|
169
258
|
/** 계획서 파싱 규칙 */
|
|
@@ -175,23 +264,21 @@ export const projectConfigSchema = z.object({
|
|
|
175
264
|
/** 커맨드 파일 섹션 설정 */
|
|
176
265
|
commandSections: commandSectionsSchema.prefault({}),
|
|
177
266
|
/** 기본 특별 요청사항 목록 */
|
|
178
|
-
defaultSpecialRequests: z
|
|
267
|
+
defaultSpecialRequests: z
|
|
268
|
+
.array(z.string())
|
|
269
|
+
.default([])
|
|
270
|
+
.describe('Default special request strings appended to every unit context'),
|
|
179
271
|
/** 유닛 유형별 추가 특별 요청사항 (키: unitType.key) */
|
|
180
|
-
specialRequestsByType: z
|
|
272
|
+
specialRequestsByType: z
|
|
273
|
+
.record(z.string(), z.array(z.string()))
|
|
274
|
+
.default({})
|
|
275
|
+
.describe('Additional special requests per unit type (key: unitType.key, value: string array)'),
|
|
181
276
|
/** 키 기반 선택적 특별 요청사항 (--request 옵션용, 키: 사용자 정의 식별자, 값: 텍스트) */
|
|
182
|
-
customRequests: z
|
|
277
|
+
customRequests: z
|
|
278
|
+
.record(z.string(), z.string())
|
|
279
|
+
.default({})
|
|
280
|
+
.describe('Named special requests selectable via --request option (key: identifier, value: text)'),
|
|
183
281
|
});
|
|
184
282
|
/** 설정 파일명 */
|
|
185
283
|
export const CONFIG_FILENAME = 'vibe-commander.config.json';
|
|
186
|
-
/**
|
|
187
|
-
* 프로젝트 설정 스키마를 JSON Schema (Draft 2020-12) 형식으로 반환한다
|
|
188
|
-
*
|
|
189
|
-
* Zod 4의 z.toJSONSchema() 내장 기능을 활용하여 변환.
|
|
190
|
-
* 에이전트가 설정 파일 구조를 런타임에 조회할 수 있도록 한다.
|
|
191
|
-
*
|
|
192
|
-
* @returns JSON Schema 호환 객체
|
|
193
|
-
*/
|
|
194
|
-
export function getConfigJsonSchema() {
|
|
195
|
-
return z.toJSONSchema(projectConfigSchema);
|
|
196
|
-
}
|
|
197
284
|
//# sourceMappingURL=schema.js.map
|
|
@@ -115,7 +115,7 @@ function extractDepFromLine(line, idPattern) {
|
|
|
115
115
|
}
|
|
116
116
|
if (candidates.length === 0)
|
|
117
117
|
return null;
|
|
118
|
-
// idPattern으로
|
|
118
|
+
// idPattern으로 필터링: 볼드 헤더(`**헤더**: [ID](url)`)에서 비유닛 볼드를 건너뜀
|
|
119
119
|
const idRegex = idPattern ? safeRegex(idPattern) : null;
|
|
120
120
|
const validCandidate = idRegex ? candidates.find((c) => idRegex.test(c.id)) : candidates[0];
|
|
121
121
|
if (!validCandidate)
|
|
@@ -150,8 +150,12 @@ function extractDescription(content, unitId, source) {
|
|
|
150
150
|
const afterMatch = content.match(afterPattern);
|
|
151
151
|
if (!afterMatch?.[1])
|
|
152
152
|
return '';
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
let rest = afterMatch[1].trim();
|
|
154
|
+
// 구분자 제거: 시작 부분의 :, — (공백 유연) 또는 — , - (공백 필수)
|
|
155
|
+
// : -10% 같은 경우 :만 제거하고 -10%는 보존하기 위해 순차적으로 처리
|
|
156
|
+
rest = rest.replace(/^[:—]\s*/, '');
|
|
157
|
+
rest = rest.replace(/^[—-]\s+/, '');
|
|
158
|
+
return rest.trim();
|
|
155
159
|
}
|
|
156
160
|
/**
|
|
157
161
|
* 하위 항목에서 artifact 텍스트를 추출한다
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { ok } from '../../types/index.js';
|
|
18
18
|
import { parseMetadataTable } from './metadata-parser.js';
|
|
19
|
-
import { FRONTMATTER_RE, findSectionLines, deduplicateBy, normalizeLineEndings, isNoneDependency, } from './md-utils.js';
|
|
19
|
+
import { FRONTMATTER_RE, findSectionLines, deduplicateBy, normalizeLineEndings, isNoneDependency, escapeRegex, } from './md-utils.js';
|
|
20
20
|
import { parseDependencyLines } from './dep-line-parser.js';
|
|
21
21
|
/**
|
|
22
22
|
* 계획서에서 의존 유닛 정보를 추출한다
|
|
@@ -79,10 +79,7 @@ export function extractDependenciesWithNotes(content, config, idPattern) {
|
|
|
79
79
|
}
|
|
80
80
|
// 2. Fallback: 메타데이터 테이블 (설정 소스가 metadata-table이면 이미 시도했으므로 건너뜀)
|
|
81
81
|
if (config.dependsSource !== 'metadata-table') {
|
|
82
|
-
const
|
|
83
|
-
const fromTable = fromTableResult.success
|
|
84
|
-
? fromTableResult.data.depends.map((unitId) => ({ unitId, description: '' }))
|
|
85
|
-
: [];
|
|
82
|
+
const fromTable = extractFromMetadataTable(normalized, config.metadataTable);
|
|
86
83
|
if (fromTable.length > 0) {
|
|
87
84
|
return ok({
|
|
88
85
|
deps: deduplicateBy(fromTable, (d) => d.unitId),
|
|
@@ -161,33 +158,102 @@ function extractFromFrontmatter(content) {
|
|
|
161
158
|
}
|
|
162
159
|
return [];
|
|
163
160
|
}
|
|
164
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* 메타데이터 테이블에서 의존성을 추출한다
|
|
163
|
+
*
|
|
164
|
+
* raw 값에 구분자(`—`, `:`, `-`)로 설명이 포함된 경우:
|
|
165
|
+
* `| 의존성 | U-101 — Token 발급 |` → `{ unitId: "U-101", description: "Token 발급" }`
|
|
166
|
+
*
|
|
167
|
+
* parseMetadataTable은 콤마 분리 후 전체 토큰을 unitId로 반환하므로,
|
|
168
|
+
* 여기서 unitId와 description을 분리한다.
|
|
169
|
+
*/
|
|
165
170
|
function extractFromMetadataTable(content, tableConfig) {
|
|
166
171
|
const tableResult = parseMetadataTable(content, tableConfig);
|
|
167
172
|
if (!tableResult.success)
|
|
168
173
|
return [];
|
|
169
|
-
return tableResult.data.depends.map((
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
return tableResult.data.depends.map((rawToken) => splitUnitIdAndDescription(rawToken));
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* 구분자 패턴:
|
|
178
|
+
* 1. 콜론(:) 또는 em dash(—)는 공백 유연하게 허용
|
|
179
|
+
* 2. 하이픈(-)은 유닛 ID와 혼동될 수 있으므로 반드시 앞뒤 공백 필요
|
|
180
|
+
*/
|
|
181
|
+
const DESC_SEPARATOR_RE = /^(?:(.+?)\s*([—:])\s*(.+)|(.+?)\s+-\s+(.+))$/;
|
|
182
|
+
/**
|
|
183
|
+
* `U-101 — Token 발급` 같은 토큰에서 unitId와 description을 분리한다
|
|
184
|
+
*
|
|
185
|
+
* 구분자가 없으면 전체가 unitId, description은 빈 문자열.
|
|
186
|
+
*/
|
|
187
|
+
function splitUnitIdAndDescription(rawToken) {
|
|
188
|
+
const match = rawToken.match(DESC_SEPARATOR_RE);
|
|
189
|
+
if (match) {
|
|
190
|
+
// 1번 그룹(ID), 3번 그룹(Desc) 또는 4번 그룹(ID), 5번 그룹(Desc)
|
|
191
|
+
const unitId = (match[1] || match[4] || '').trim();
|
|
192
|
+
const description = (match[3] || match[5] || '').trim();
|
|
193
|
+
if (unitId && description) {
|
|
194
|
+
return { unitId, description };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return { unitId: rawToken, description: '' };
|
|
173
198
|
}
|
|
174
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* 본문 전체에서 ID 패턴으로 의존성을 스캔한다 (Fallback 최후 수단)
|
|
201
|
+
*
|
|
202
|
+
* ID가 발견된 줄에서 구분자(`—`, `:`, `-`) 이후 텍스트를 description으로 추출 시도.
|
|
203
|
+
* 같은 줄에 설명이 없으면 다음 줄이 하위 항목인지 확인하여 추출.
|
|
204
|
+
*/
|
|
175
205
|
function extractFromBodyScan(content, idPattern) {
|
|
176
206
|
try {
|
|
177
207
|
const regex = new RegExp(idPattern, 'g');
|
|
178
208
|
const matches = content.match(regex);
|
|
179
209
|
if (!matches)
|
|
180
210
|
return [];
|
|
181
|
-
// 중복 제거
|
|
182
211
|
const unique = [...new Set(matches)];
|
|
212
|
+
const lines = content.split('\n');
|
|
183
213
|
return unique.map((unitId) => ({
|
|
184
214
|
unitId,
|
|
185
|
-
description:
|
|
215
|
+
description: extractDescriptionFromLines(lines, unitId),
|
|
186
216
|
}));
|
|
187
217
|
}
|
|
188
218
|
catch {
|
|
189
|
-
// 잘못된 정규식 — 빈 배열 반환 (Graceful Degradation)
|
|
190
219
|
return [];
|
|
191
220
|
}
|
|
192
221
|
}
|
|
222
|
+
/**
|
|
223
|
+
* 본문 라인에서 unitId가 포함된 첫 번째 줄에서 description을 추출한다
|
|
224
|
+
*
|
|
225
|
+
* `**U-002**: 공유 타입`, `[U-002](url) — 설명`, `U-002의 ...` 패턴에서
|
|
226
|
+
* 구분자 이후 텍스트를 description으로 반환.
|
|
227
|
+
* 같은 줄에 설명이 없으면 다음 줄의 텍스트를 확인.
|
|
228
|
+
*/
|
|
229
|
+
function extractDescriptionFromLines(lines, unitId) {
|
|
230
|
+
const escapedId = escapeRegex(unitId);
|
|
231
|
+
const linePattern = new RegExp(escapedId);
|
|
232
|
+
for (let i = 0; i < lines.length; i++) {
|
|
233
|
+
const line = lines[i] ?? '';
|
|
234
|
+
if (!linePattern.test(line))
|
|
235
|
+
continue;
|
|
236
|
+
// 1. 같은 줄에서 추출 시도
|
|
237
|
+
// 링크 형식: [ID](url) 뒤의 텍스트
|
|
238
|
+
const linkDesc = new RegExp(`\\[${escapedId}\\]\\([^)]*\\)\\s*[—:\\-]\\s*(.+)`);
|
|
239
|
+
const linkMatch = line.match(linkDesc);
|
|
240
|
+
if (linkMatch?.[1])
|
|
241
|
+
return linkMatch[1].trim();
|
|
242
|
+
// 볼드 형식: **ID** 뒤의 텍스트
|
|
243
|
+
const boldDesc = new RegExp(`\\*\\*${escapedId}\\*\\*\\s*[—:\\-]\\s*(.+)`);
|
|
244
|
+
const boldMatch = line.match(boldDesc);
|
|
245
|
+
if (boldMatch?.[1])
|
|
246
|
+
return boldMatch[1].trim();
|
|
247
|
+
// 2. 같은 줄에 구분자 설명이 없으면 다음 줄 확인
|
|
248
|
+
if (i + 1 < lines.length) {
|
|
249
|
+
const nextLine = (lines[i + 1] ?? '').trim();
|
|
250
|
+
// 다음 줄이 비어있지 않고, 다른 리스트 항목이나 헤더가 아니면 설명으로 간주
|
|
251
|
+
if (nextLine !== '' && !/^[-*#]/.test(nextLine)) {
|
|
252
|
+
return nextLine;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
return '';
|
|
258
|
+
}
|
|
193
259
|
//# sourceMappingURL=dependency-extractor.js.map
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
export { parseUnitPlan } from './plan-parser.js';
|
|
10
10
|
export type { ParseUnitPlanResult } from './plan-parser.js';
|
|
11
11
|
export { parseBacklog } from './backlog-parser.js';
|
|
12
|
-
export {
|
|
12
|
+
export { extractSubsectionContent } from './subsection-extractor.js';
|
|
13
|
+
export { extractTitle, extractTitleFromContent } from './title-extractor.js';
|
|
13
14
|
export { parseMetadataTable } from './metadata-parser.js';
|
|
14
15
|
export type { MetadataTableResult } from './metadata-parser.js';
|
|
15
16
|
export { extractDependencies, extractDependenciesWithNotes } from './dependency-extractor.js';
|
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
export { parseUnitPlan } from './plan-parser.js';
|
|
11
11
|
// 백로그 파서
|
|
12
12
|
export { parseBacklog } from './backlog-parser.js';
|
|
13
|
+
// 서브섹션 추출기
|
|
14
|
+
export { extractSubsectionContent } from './subsection-extractor.js';
|
|
13
15
|
// 개별 추출기 (필요 시 단독 사용)
|
|
14
|
-
export { extractTitle } from './title-extractor.js';
|
|
16
|
+
export { extractTitle, extractTitleFromContent } from './title-extractor.js';
|
|
15
17
|
export { parseMetadataTable } from './metadata-parser.js';
|
|
16
18
|
export { extractDependencies, extractDependenciesWithNotes } from './dependency-extractor.js';
|
|
17
19
|
export { extractPairingQuestions } from './pairing-question-extractor.js';
|
|
@@ -61,6 +61,11 @@ export function extractPhase(content, config) {
|
|
|
61
61
|
export function extractDependsWithWarning(content, config, idPattern, warnings) {
|
|
62
62
|
const result = extractDependenciesWithNotes(content, config, idPattern);
|
|
63
63
|
if (result.success) {
|
|
64
|
+
// description이 빈 항목이 있으면 보강 힌트 추가
|
|
65
|
+
const emptyDescCount = result.data.deps.filter((d) => d.description === '').length;
|
|
66
|
+
if (emptyDescCount > 0) {
|
|
67
|
+
warnings.push(`일부 의존 유닛의 설명이 비어 있습니다 (${String(emptyDescCount)}개). 의존 계획서 파일의 제목에서 설명을 추출합니다.`);
|
|
68
|
+
}
|
|
64
69
|
return { depends: result.data.deps, contextNotes: result.data.contextNotes };
|
|
65
70
|
}
|
|
66
71
|
// 의존성 추출 실패 — 빈 결과 + 경고
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서브섹션 추출기 — Layer 1 순수 함수
|
|
3
|
+
*
|
|
4
|
+
* 섹션 본문에서 특정 `###` 서브섹션 콘텐츠를 추출한다.
|
|
5
|
+
* `renderSection`의 보존 로직에서 기존 특별 요청사항을 추출하는 데 사용.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 콘텐츠에서 지정된 서브헤딩의 콘텐츠를 추출한다
|
|
11
|
+
*
|
|
12
|
+
* 서브헤딩 라인 자체도 결과에 포함된다.
|
|
13
|
+
* 다음 동급 이상(### 이하 레벨) 헤딩 또는 콘텐츠 끝까지 추출한다.
|
|
14
|
+
*
|
|
15
|
+
* @param content - 검색 대상 콘텐츠
|
|
16
|
+
* @param subHeading - 찾을 서브헤딩 텍스트 (예: "### 특별 요청사항")
|
|
17
|
+
* @returns 서브헤딩 포함 콘텐츠 또는 null (미발견/빈 콘텐츠)
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractSubsectionContent(content: string, subHeading: string): string | null;
|
|
20
|
+
//# sourceMappingURL=subsection-extractor.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 서브섹션 추출기 — Layer 1 순수 함수
|
|
3
|
+
*
|
|
4
|
+
* 섹션 본문에서 특정 `###` 서브섹션 콘텐츠를 추출한다.
|
|
5
|
+
* `renderSection`의 보존 로직에서 기존 특별 요청사항을 추출하는 데 사용.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import { normalizeLineEndings } from './md-utils.js';
|
|
10
|
+
/**
|
|
11
|
+
* 콘텐츠에서 지정된 서브헤딩의 콘텐츠를 추출한다
|
|
12
|
+
*
|
|
13
|
+
* 서브헤딩 라인 자체도 결과에 포함된다.
|
|
14
|
+
* 다음 동급 이상(### 이하 레벨) 헤딩 또는 콘텐츠 끝까지 추출한다.
|
|
15
|
+
*
|
|
16
|
+
* @param content - 검색 대상 콘텐츠
|
|
17
|
+
* @param subHeading - 찾을 서브헤딩 텍스트 (예: "### 특별 요청사항")
|
|
18
|
+
* @returns 서브헤딩 포함 콘텐츠 또는 null (미발견/빈 콘텐츠)
|
|
19
|
+
*/
|
|
20
|
+
export function extractSubsectionContent(content, subHeading) {
|
|
21
|
+
if (!content.trim() || !subHeading.trim())
|
|
22
|
+
return null;
|
|
23
|
+
const normalized = normalizeLineEndings(content);
|
|
24
|
+
const lines = normalized.split('\n');
|
|
25
|
+
const needle = subHeading.trim();
|
|
26
|
+
const hashMatch = needle.match(/^(#+)\s/);
|
|
27
|
+
if (!hashMatch?.[1])
|
|
28
|
+
return null;
|
|
29
|
+
const level = hashMatch[1].length;
|
|
30
|
+
let startIdx = -1;
|
|
31
|
+
for (let i = 0; i < lines.length; i++) {
|
|
32
|
+
if ((lines[i] ?? '').trim() === needle) {
|
|
33
|
+
startIdx = i;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (startIdx === -1)
|
|
38
|
+
return null;
|
|
39
|
+
let endIdx = lines.length;
|
|
40
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
41
|
+
const line = (lines[i] ?? '').trim();
|
|
42
|
+
const lineHashMatch = line.match(/^(#+)\s/);
|
|
43
|
+
if (lineHashMatch?.[1] && lineHashMatch[1].length <= level) {
|
|
44
|
+
endIdx = i;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const extracted = lines.slice(startIdx, endIdx);
|
|
49
|
+
while (extracted.length > 0 && (extracted[extracted.length - 1] ?? '').trim() === '') {
|
|
50
|
+
extracted.pop();
|
|
51
|
+
}
|
|
52
|
+
if (extracted.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
return extracted.join('\n');
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=subsection-extractor.js.map
|
|
@@ -33,4 +33,20 @@ import type { ToolResult } from '../../types/index.js';
|
|
|
33
33
|
* ```
|
|
34
34
|
*/
|
|
35
35
|
export declare function extractTitle(content: string, titleSource: string): ToolResult<string | undefined>;
|
|
36
|
+
/**
|
|
37
|
+
* 마크다운 콘텐츠에서 H1 제목의 순수 작업명을 추출한다
|
|
38
|
+
*
|
|
39
|
+
* H1 형식 `# U-101[Mmp]: Ephemeral Token 발급`에서
|
|
40
|
+
* unitId prefix(`U-101[Mmp]:`)를 제거하고 순수 작업명만 반환.
|
|
41
|
+
*
|
|
42
|
+
* 지원 형식:
|
|
43
|
+
* - `# U-101[Mmp]: 작업명` → `작업명`
|
|
44
|
+
* - `# U-101: 작업명` → `작업명`
|
|
45
|
+
* - `# 작업명만` → `작업명만` (unitId prefix 없는 경우)
|
|
46
|
+
* - `# U-101[Mmp]: 작업명 — 부제목` → `작업명 — 부제목`
|
|
47
|
+
*
|
|
48
|
+
* @param content - 마크다운 전체 내용
|
|
49
|
+
* @returns 순수 작업명 문자열 (H1이 없으면 빈 문자열)
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractTitleFromContent(content: string): string;
|
|
36
52
|
//# sourceMappingURL=title-extractor.d.ts.map
|
|
@@ -45,6 +45,32 @@ export function extractTitle(content, titleSource) {
|
|
|
45
45
|
// 알 수 없는 소스 — undefined 반환 (Graceful Degradation)
|
|
46
46
|
return ok(undefined);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* 마크다운 콘텐츠에서 H1 제목의 순수 작업명을 추출한다
|
|
50
|
+
*
|
|
51
|
+
* H1 형식 `# U-101[Mmp]: Ephemeral Token 발급`에서
|
|
52
|
+
* unitId prefix(`U-101[Mmp]:`)를 제거하고 순수 작업명만 반환.
|
|
53
|
+
*
|
|
54
|
+
* 지원 형식:
|
|
55
|
+
* - `# U-101[Mmp]: 작업명` → `작업명`
|
|
56
|
+
* - `# U-101: 작업명` → `작업명`
|
|
57
|
+
* - `# 작업명만` → `작업명만` (unitId prefix 없는 경우)
|
|
58
|
+
* - `# U-101[Mmp]: 작업명 — 부제목` → `작업명 — 부제목`
|
|
59
|
+
*
|
|
60
|
+
* @param content - 마크다운 전체 내용
|
|
61
|
+
* @returns 순수 작업명 문자열 (H1이 없으면 빈 문자열)
|
|
62
|
+
*/
|
|
63
|
+
export function extractTitleFromContent(content) {
|
|
64
|
+
const normalized = normalizeLineEndings(content);
|
|
65
|
+
const h1 = extractH1Title(normalized);
|
|
66
|
+
if (!h1)
|
|
67
|
+
return '';
|
|
68
|
+
// `ID: 설명` 또는 `ID — 설명` 패턴에서 설명 부분만 추출
|
|
69
|
+
const prefixMatch = h1.match(/^[A-Za-z]+-\d+(?:-\w+)?(?:\[[^\]]*\])?\s*[:—-]\s*(.+)/);
|
|
70
|
+
if (prefixMatch?.[1])
|
|
71
|
+
return prefixMatch[1].trim();
|
|
72
|
+
return h1;
|
|
73
|
+
}
|
|
48
74
|
// ── 내부 헬퍼 ──
|
|
49
75
|
/**
|
|
50
76
|
* 첫 번째 H1 헤더에서 제목을 추출한다
|
|
@@ -8,10 +8,11 @@
|
|
|
8
8
|
export { toShortId, toBareId, toSlug, buildTemplateVars, interpolatePattern, } from './interpolate.js';
|
|
9
9
|
export { extractTitleOnly, buildSectionVars, interpolateTemplate } from './template-engine.js';
|
|
10
10
|
export { renderSection } from './section-renderer.js';
|
|
11
|
+
export type { RenderSectionOptions } from './section-renderer.js';
|
|
11
12
|
export { updateSection, updateField } from './section-updater.js';
|
|
12
13
|
export type { UpdateResult, MarkerOptions } from './section-updater.js';
|
|
13
14
|
export { createBeginMarker, createEndMarker, findMarkerRange, isValidSectionKey, wrapWithMarkers, stripMarkers, } from './marker-utils.js';
|
|
14
15
|
export type { MarkerRange } from './marker-utils.js';
|
|
15
|
-
export { COMMAND_REGISTRY, SCHEMA_SUBCOMMANDS, renderCommandsSchema, renderTypesSchema, } from './schema-renderer.js';
|
|
16
|
+
export { COMMAND_REGISTRY, SCHEMA_SUBCOMMANDS, renderConfigSchema, renderCommandsSchema, renderTypesSchema, } from './schema-renderer.js';
|
|
16
17
|
export type { ArgSchema, FlagSchema, CommandSchema, TypeSchemaEntry, SchemaSubcommandInfo, SchemaResult, } from './schema-renderer.js';
|
|
17
18
|
//# sourceMappingURL=index.d.ts.map
|