vibe-commander 0.2.5 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/cli/commands/init-helpers.js +1 -1
- package/dist/adapters/cli/commands/set-unit.d.ts +2 -0
- package/dist/adapters/cli/commands/set-unit.js +29 -6
- package/dist/adapters/cli/scanner.js +23 -6
- package/dist/core/parsers/dep-line-parser.js +48 -2
- package/dist/core/renderers/index.d.ts +2 -0
- package/dist/core/renderers/index.js +2 -0
- package/dist/core/renderers/plan-question-updater.d.ts +41 -0
- package/dist/core/renderers/plan-question-updater.js +150 -0
- package/dist/core/resolvers/config-generator.d.ts +4 -3
- package/dist/core/resolvers/config-generator.js +81 -14
- package/dist/types/init.d.ts +4 -0
- package/examples/commands-template.md +53 -0
- package/package.json +1 -1
|
@@ -22,7 +22,7 @@ export { DEFAULT_COMMANDS_PATH, DEFAULT_ROADMAP_PATH, DEFAULT_PLAN_DIR, DEFAULT_
|
|
|
22
22
|
* @param mergeWith - 기존 설정 객체. 제공되면 스캔 결과와 병합한다.
|
|
23
23
|
*/
|
|
24
24
|
export function buildAndWriteConfig(answers, configPath, alreadyExists, scanResult, mergeWith) {
|
|
25
|
-
let configObj = buildConfigObject(answers);
|
|
25
|
+
let configObj = buildConfigObject(answers, scanResult);
|
|
26
26
|
if (mergeWith) {
|
|
27
27
|
configObj = mergeConfigs(mergeWith, configObj);
|
|
28
28
|
}
|
|
@@ -24,6 +24,8 @@ export interface SetUnitResult {
|
|
|
24
24
|
pairingQuestions: PairingQuestion[];
|
|
25
25
|
pairingAnswers: PairingAnswer[];
|
|
26
26
|
commandsUpdated: boolean;
|
|
27
|
+
planUpdated?: boolean;
|
|
28
|
+
questionsWrittenBack?: number;
|
|
27
29
|
warnings: string[];
|
|
28
30
|
dryRun?: boolean;
|
|
29
31
|
preview?: string;
|
|
@@ -18,6 +18,7 @@ import { parseBacklog } from '../../../core/parsers/backlog-parser.js';
|
|
|
18
18
|
import { classifyBacklog } from '../../../core/resolvers/backlog-resolver.js';
|
|
19
19
|
import { renderSection } from '../../../core/renderers/section-renderer.js';
|
|
20
20
|
import { updateSection } from '../../../core/renderers/section-updater.js';
|
|
21
|
+
import { updatePlanQuestions } from '../../../core/renderers/plan-question-updater.js';
|
|
21
22
|
import { findMarkerRange } from '../../../core/renderers/marker-utils.js';
|
|
22
23
|
import { extractSubsectionContent } from '../../../core/parsers/subsection-extractor.js';
|
|
23
24
|
import { normalizeLineEndings } from '../../../core/parsers/md-utils.js';
|
|
@@ -76,9 +77,30 @@ export async function handleSetUnit(args, projectRoot) {
|
|
|
76
77
|
pairingAnswers = await promptAllQuestions(context.pairingQuestions);
|
|
77
78
|
context.pairingAnswers = pairingAnswers;
|
|
78
79
|
}
|
|
79
|
-
// 3.
|
|
80
|
+
// 3. 계획서 Writeback (답변이 있고 dry-run이 아닐 때만)
|
|
81
|
+
let planUpdated = false;
|
|
82
|
+
let questionsWrittenBack = 0;
|
|
83
|
+
if (!dryRun && pairingAnswers.length > 0) {
|
|
84
|
+
try {
|
|
85
|
+
const planAbsPath = join(projectRoot, planRelPath);
|
|
86
|
+
const planContent = readFileSync(planAbsPath, 'utf-8');
|
|
87
|
+
const writebackResult = updatePlanQuestions(planContent, pairingAnswers, {
|
|
88
|
+
pairingQuestionSection: config.planParsing.pairingQuestionSection,
|
|
89
|
+
});
|
|
90
|
+
if (writebackResult.appliedCount > 0) {
|
|
91
|
+
writeFileSync(planAbsPath, writebackResult.updatedContent, 'utf-8');
|
|
92
|
+
planUpdated = true;
|
|
93
|
+
questionsWrittenBack = writebackResult.appliedCount;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
98
|
+
warnings.push(`계획서 Writeback 실패: ${msg}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// 4. 커맨드 파일 읽기 (보존 추출 + 업데이트에 재사용)
|
|
80
102
|
const cmdFile = readCommandsFile(config, projectRoot);
|
|
81
|
-
//
|
|
103
|
+
// 5. 기존 특별 요청사항 추출 (보존 로직)
|
|
82
104
|
const specialRequestsHeading = config.commandSections.specialRequestsHeading;
|
|
83
105
|
let preservedSpecialRequests;
|
|
84
106
|
if (cmdFile.success) {
|
|
@@ -87,12 +109,12 @@ export async function handleSetUnit(args, projectRoot) {
|
|
|
87
109
|
preservedSpecialRequests = extracted;
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
|
-
//
|
|
112
|
+
// 6. 섹션 렌더링 (Core 순수 함수)
|
|
91
113
|
const sectionBody = renderSection(context, unitTypeConfig, undefined, {
|
|
92
114
|
preservedSpecialRequests,
|
|
93
115
|
specialRequestsHeading,
|
|
94
116
|
});
|
|
95
|
-
//
|
|
117
|
+
// 7. 커맨드 파일 업데이트 (Adapter I/O) — dry-run 시 건너뜀
|
|
96
118
|
let commandsUpdated = false;
|
|
97
119
|
if (!dryRun) {
|
|
98
120
|
if (cmdFile.success) {
|
|
@@ -116,10 +138,10 @@ export async function handleSetUnit(args, projectRoot) {
|
|
|
116
138
|
else {
|
|
117
139
|
warnings.push(`커맨드 파일을 읽을 수 없습니다: ${config.paths.commands}`);
|
|
118
140
|
}
|
|
119
|
-
//
|
|
141
|
+
// 8. 활성 유닛 타입 상태 기록 (update-commit 자동 감지용)
|
|
120
142
|
writeActiveUnitType(projectRoot, unitTypeKey);
|
|
121
143
|
}
|
|
122
|
-
//
|
|
144
|
+
// 9. 결과 반환
|
|
123
145
|
return ok({
|
|
124
146
|
unitId,
|
|
125
147
|
unitType: unitTypeKey,
|
|
@@ -131,6 +153,7 @@ export async function handleSetUnit(args, projectRoot) {
|
|
|
131
153
|
pairingQuestions: context.pairingQuestions,
|
|
132
154
|
pairingAnswers,
|
|
133
155
|
commandsUpdated,
|
|
156
|
+
...(planUpdated && { planUpdated, questionsWrittenBack }),
|
|
134
157
|
warnings,
|
|
135
158
|
...(dryRun && { dryRun: true, preview: sectionBody }),
|
|
136
159
|
...(autoSelected && { autoSelected, selectedFrom }),
|
|
@@ -80,19 +80,25 @@ function detectIdPatterns(mdFiles) {
|
|
|
80
80
|
// [우선순위 1] PREFIX-NNN[Tag] (예: U-001[Mvp], RU-001[Mmp])
|
|
81
81
|
const tagMatch = name.match(/^([A-Z][A-Z0-9]*)-\d+\[/);
|
|
82
82
|
if (tagMatch?.[1]) {
|
|
83
|
-
upsertGroup(prefixGroups, tagMatch[1], name, true, false);
|
|
83
|
+
upsertGroup(prefixGroups, tagMatch[1], name, true, false, false);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
// [우선순위 1.5] PREFIX-NNN-XN (예: RU-010-S1, RU-013-Q2)
|
|
87
|
+
const subUnitMatch = name.match(/^([A-Z][A-Z0-9]*)-\d+-[A-Z]\d+$/);
|
|
88
|
+
if (subUnitMatch?.[1]) {
|
|
89
|
+
upsertGroup(prefixGroups, subUnitMatch[1], name, false, false, true);
|
|
84
90
|
continue;
|
|
85
91
|
}
|
|
86
92
|
// [우선순위 2] PREFIX-WORD-NNN (예: CP-MVP-01)
|
|
87
93
|
const compoundMatch = name.match(/^([A-Z][A-Z0-9]*)-[A-Z][A-Z0-9]*-\d+/);
|
|
88
94
|
if (compoundMatch?.[1]) {
|
|
89
|
-
upsertGroup(prefixGroups, compoundMatch[1], name, false, true);
|
|
95
|
+
upsertGroup(prefixGroups, compoundMatch[1], name, false, true, false);
|
|
90
96
|
continue;
|
|
91
97
|
}
|
|
92
98
|
// [우선순위 3] PREFIX-NNN (예: TASK-001)
|
|
93
99
|
const basicMatch = name.match(/^([A-Z][A-Z0-9]*)-\d+/);
|
|
94
100
|
if (basicMatch?.[1]) {
|
|
95
|
-
upsertGroup(prefixGroups, basicMatch[1], name, false, false);
|
|
101
|
+
upsertGroup(prefixGroups, basicMatch[1], name, false, false, false);
|
|
96
102
|
}
|
|
97
103
|
}
|
|
98
104
|
const patterns = [];
|
|
@@ -113,12 +119,14 @@ function detectIdPatterns(mdFiles) {
|
|
|
113
119
|
displayLabel,
|
|
114
120
|
matchCount: group.names.length,
|
|
115
121
|
examples: group.names.slice(0, 3),
|
|
122
|
+
hasTag: group.hasTag,
|
|
123
|
+
hasSubUnit: group.hasSubUnit,
|
|
116
124
|
});
|
|
117
125
|
}
|
|
118
126
|
patterns.sort((a, b) => b.matchCount - a.matchCount);
|
|
119
127
|
return patterns;
|
|
120
128
|
}
|
|
121
|
-
function upsertGroup(groups, prefix, name, hasTag, isCompound) {
|
|
129
|
+
function upsertGroup(groups, prefix, name, hasTag, isCompound, hasSubUnit) {
|
|
122
130
|
const existing = groups.get(prefix);
|
|
123
131
|
if (existing) {
|
|
124
132
|
existing.names.push(name);
|
|
@@ -126,9 +134,11 @@ function upsertGroup(groups, prefix, name, hasTag, isCompound) {
|
|
|
126
134
|
existing.hasTag = true;
|
|
127
135
|
if (isCompound)
|
|
128
136
|
existing.isCompound = true;
|
|
137
|
+
if (hasSubUnit)
|
|
138
|
+
existing.hasSubUnit = true;
|
|
129
139
|
}
|
|
130
140
|
else {
|
|
131
|
-
groups.set(prefix, { names: [name], hasTag, isCompound });
|
|
141
|
+
groups.set(prefix, { names: [name], hasTag, isCompound, hasSubUnit });
|
|
132
142
|
}
|
|
133
143
|
}
|
|
134
144
|
// ── 문서 디렉토리 감지 ──
|
|
@@ -136,6 +146,7 @@ const ROLE_KEYWORDS = {
|
|
|
136
146
|
plan: ['plan', 'plans', 'unit-plan', 'unit-plans'],
|
|
137
147
|
result: ['result', 'results', 'unit-result', 'unit-results', 'report', 'reports'],
|
|
138
148
|
runbook: ['runbook', 'runbooks', 'unit-runbook', 'unit-runbooks'],
|
|
149
|
+
refactorSub: ['refactor', 'refactors', 'refactoring'],
|
|
139
150
|
};
|
|
140
151
|
function detectDocRoots(mdFiles) {
|
|
141
152
|
const dirCounts = new Map();
|
|
@@ -159,7 +170,13 @@ function detectDocRoots(mdFiles) {
|
|
|
159
170
|
docRoots.push({ role, dirPath: dir, fileCount: count });
|
|
160
171
|
}
|
|
161
172
|
}
|
|
162
|
-
const roleOrder = {
|
|
173
|
+
const roleOrder = {
|
|
174
|
+
plan: 0,
|
|
175
|
+
result: 1,
|
|
176
|
+
runbook: 2,
|
|
177
|
+
refactorSub: 3,
|
|
178
|
+
other: 4,
|
|
179
|
+
};
|
|
163
180
|
docRoots.sort((a, b) => {
|
|
164
181
|
const orderDiff = (roleOrder[a.role] ?? 3) - (roleOrder[b.role] ?? 3);
|
|
165
182
|
if (orderDiff !== 0)
|
|
@@ -127,6 +127,29 @@ function extractDepFromLine(line, idPattern) {
|
|
|
127
127
|
matchEnd: match.index + match[0].length,
|
|
128
128
|
});
|
|
129
129
|
}
|
|
130
|
+
// 3. 평문 ID Fallback: 볼드/링크가 없고 idPattern이 제공된 경우에만 시도
|
|
131
|
+
if (candidates.length === 0 && idPattern) {
|
|
132
|
+
const plainIdRegex = safeRegex(idPattern);
|
|
133
|
+
if (plainIdRegex) {
|
|
134
|
+
// 앵커 제거한 버전으로 전체 스캔 (멀티 ID 지원용)
|
|
135
|
+
const scanPattern = plainIdRegex.source.replace(/^\^/, '');
|
|
136
|
+
const globalIdRegex = new RegExp(scanPattern, 'g');
|
|
137
|
+
let plainMatch;
|
|
138
|
+
while ((plainMatch = globalIdRegex.exec(content)) !== null) {
|
|
139
|
+
// 단, 평문 ID는 반드시 토큰 시작점(공백/마커 직후)이거나 콤마 뒤여야 함
|
|
140
|
+
const charBefore = content[plainMatch.index - 1];
|
|
141
|
+
const isStartOfToken = !charBefore || /[\s,:]/.test(charBefore);
|
|
142
|
+
if (isStartOfToken) {
|
|
143
|
+
candidates.push({
|
|
144
|
+
id: plainMatch[0],
|
|
145
|
+
source: 'plain',
|
|
146
|
+
matchStart: plainMatch.index,
|
|
147
|
+
matchEnd: plainMatch.index + plainMatch[0].length,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
130
153
|
if (candidates.length === 0)
|
|
131
154
|
return [];
|
|
132
155
|
const idRegex = idPattern ? safeRegex(idPattern) : null;
|
|
@@ -134,9 +157,27 @@ function extractDepFromLine(line, idPattern) {
|
|
|
134
157
|
? candidates
|
|
135
158
|
.filter((c) => idRegex.test(c.id))
|
|
136
159
|
.map((c) => {
|
|
137
|
-
//
|
|
160
|
+
// ID 패턴으로 실제 유닛 ID 부분만 추출 (정규화)
|
|
161
|
+
// 예: **[U-101](url)** -> U-101, U-101[Mmp]:설명 -> U-101[Mmp]
|
|
138
162
|
const actualMatch = c.id.match(idRegex);
|
|
139
|
-
|
|
163
|
+
if (actualMatch) {
|
|
164
|
+
const normalizedId = actualMatch[0];
|
|
165
|
+
if (c.source === 'plain') {
|
|
166
|
+
const offset = c.id.indexOf(normalizedId);
|
|
167
|
+
const newStart = c.matchStart + (offset !== -1 ? offset : 0);
|
|
168
|
+
return {
|
|
169
|
+
...c,
|
|
170
|
+
id: normalizedId,
|
|
171
|
+
matchStart: newStart,
|
|
172
|
+
matchEnd: newStart + normalizedId.length,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// 볼드/링크: ID만 정규화하고 매칭 범위(마커 포함)는 유지하여 설명 추출 보호
|
|
177
|
+
return { ...c, id: normalizedId };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return c;
|
|
140
181
|
})
|
|
141
182
|
: candidates[0]
|
|
142
183
|
? [candidates[0]]
|
|
@@ -158,6 +199,11 @@ function extractDepFromLine(line, idPattern) {
|
|
|
158
199
|
const c = filtered[0];
|
|
159
200
|
if (!c)
|
|
160
201
|
return [];
|
|
202
|
+
if (c.source === 'plain') {
|
|
203
|
+
const descText = content.slice(c.matchEnd);
|
|
204
|
+
const description = cleanupMultiDescription(descText);
|
|
205
|
+
return [{ unitId: c.id, description }];
|
|
206
|
+
}
|
|
161
207
|
const description = extractDescription(content, c.id, c.source);
|
|
162
208
|
return [{ unitId: c.id, description }];
|
|
163
209
|
}
|
|
@@ -13,6 +13,8 @@ export { updateSection, updateField } from './section-updater.js';
|
|
|
13
13
|
export type { UpdateResult, MarkerOptions } from './section-updater.js';
|
|
14
14
|
export { createBeginMarker, createEndMarker, findMarkerRange, isValidSectionKey, wrapWithMarkers, stripMarkers, } from './marker-utils.js';
|
|
15
15
|
export type { MarkerRange } from './marker-utils.js';
|
|
16
|
+
export { updatePlanQuestions } from './plan-question-updater.js';
|
|
17
|
+
export type { PlanQuestionUpdateResult } from './plan-question-updater.js';
|
|
16
18
|
export { COMMAND_REGISTRY, SCHEMA_SUBCOMMANDS, renderConfigSchema, renderCommandsSchema, renderTypesSchema, } from './schema-renderer.js';
|
|
17
19
|
export type { ArgSchema, FlagSchema, CommandSchema, TypeSchemaEntry, SchemaSubcommandInfo, SchemaResult, } from './schema-renderer.js';
|
|
18
20
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -15,6 +15,8 @@ export { renderSection } from './section-renderer.js';
|
|
|
15
15
|
export { updateSection, updateField } from './section-updater.js';
|
|
16
16
|
// 마커 유틸리티
|
|
17
17
|
export { createBeginMarker, createEndMarker, findMarkerRange, isValidSectionKey, wrapWithMarkers, stripMarkers, } from './marker-utils.js';
|
|
18
|
+
// 계획서 질문 Writeback
|
|
19
|
+
export { updatePlanQuestions } from './plan-question-updater.js';
|
|
18
20
|
// 스키마 렌더러
|
|
19
21
|
export { COMMAND_REGISTRY, SCHEMA_SUBCOMMANDS, renderConfigSchema, renderCommandsSchema, renderTypesSchema, } from './schema-renderer.js';
|
|
20
22
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 계획서 페어링 질문 Writeback — Layer 1 순수 함수
|
|
3
|
+
*
|
|
4
|
+
* 페어링 질문에 대한 CLI 답변을 계획서 원본 마크다운에 반영한다.
|
|
5
|
+
* `[ ]` → `[x]` 체크, 답변 텍스트 추가, 선택된 옵션 `✅` 마킹.
|
|
6
|
+
*
|
|
7
|
+
* pairing-question-extractor.ts의 QUESTION_LINE 정규식과 동일한
|
|
8
|
+
* 패턴을 사용하여 파싱/업데이트 간 일관성을 보장한다.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import type { PairingAnswer } from '../../types/index.js';
|
|
13
|
+
/** updatePlanQuestions 반환 타입 */
|
|
14
|
+
export interface PlanQuestionUpdateResult {
|
|
15
|
+
/** 업데이트된 계획서 전체 content */
|
|
16
|
+
updatedContent: string;
|
|
17
|
+
/** 실제 반영된 답변 수 */
|
|
18
|
+
appliedCount: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 계획서 content에서 페어링 질문 체크박스를 답변에 따라 업데이트한다
|
|
22
|
+
*
|
|
23
|
+
* 동작:
|
|
24
|
+
* 1. planContent에서 `## {pairingQuestionSection}` 섹션 시작/끝 라인 범위를 찾음
|
|
25
|
+
* 2. 섹션 내 각 질문 라인(`- [ ] **{id}**:`)을 순회
|
|
26
|
+
* 3. answers 배열에 해당 questionId가 있으면:
|
|
27
|
+
* - `[ ]` → `[x]`로 교체
|
|
28
|
+
* - 질문 라인 끝에 ` → **답변: {selectedOption 또는 freeText}**` 추가
|
|
29
|
+
* 4. 옵션 라인 중 선택된 옵션이 있으면 해당 라인에 `✅` 추가
|
|
30
|
+
* 5. answers에 없는 질문(건너뜀)은 원본 유지
|
|
31
|
+
* 6. 섹션 외 content는 그대로 보존
|
|
32
|
+
*
|
|
33
|
+
* @param planContent - 계획서 마크다운 전체 내용
|
|
34
|
+
* @param answers - CLI 프롬프트에서 수집된 답변 배열
|
|
35
|
+
* @param config - 페어링 질문 섹션명 설정
|
|
36
|
+
* @returns 업데이트된 content와 반영된 답변 수
|
|
37
|
+
*/
|
|
38
|
+
export declare function updatePlanQuestions(planContent: string, answers: PairingAnswer[], config: {
|
|
39
|
+
pairingQuestionSection: string;
|
|
40
|
+
}): PlanQuestionUpdateResult;
|
|
41
|
+
//# sourceMappingURL=plan-question-updater.d.ts.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 계획서 페어링 질문 Writeback — Layer 1 순수 함수
|
|
3
|
+
*
|
|
4
|
+
* 페어링 질문에 대한 CLI 답변을 계획서 원본 마크다운에 반영한다.
|
|
5
|
+
* `[ ]` → `[x]` 체크, 답변 텍스트 추가, 선택된 옵션 `✅` 마킹.
|
|
6
|
+
*
|
|
7
|
+
* pairing-question-extractor.ts의 QUESTION_LINE 정규식과 동일한
|
|
8
|
+
* 패턴을 사용하여 파싱/업데이트 간 일관성을 보장한다.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
import { findSectionLines, normalizeLineEndings } from '../parsers/md-utils.js';
|
|
13
|
+
/**
|
|
14
|
+
* 질문 라인 정규식 (pairing-question-extractor.ts와 동일 패턴)
|
|
15
|
+
*
|
|
16
|
+
* - checked: 체크 상태 ('x', 'X', 'v', 'V', 공백 등)
|
|
17
|
+
* - id: 질문 ID (예: "Q1")
|
|
18
|
+
* - text: 질문 본문
|
|
19
|
+
*/
|
|
20
|
+
const QUESTION_LINE = /^(?<prefix>[-*]\s+)\[(?<checked>[xXvV ]*)\]\s+\*\*(?<id>[^*]+)\*\*\s*(?<sep>[:\-—]?)\s*(?<text>.+)$/;
|
|
21
|
+
/**
|
|
22
|
+
* 하위 옵션 라인 정규식 (들여쓰인 리스트 항목)
|
|
23
|
+
*/
|
|
24
|
+
const SUB_OPTION_LINE = /^(?<indent>\s{2,})(?<bullet>[-*]\s+)(?<optionText>.+)$/;
|
|
25
|
+
/**
|
|
26
|
+
* 이미 체크된 상태인지 판별
|
|
27
|
+
*/
|
|
28
|
+
function isChecked(mark) {
|
|
29
|
+
const lower = mark.toLowerCase();
|
|
30
|
+
return lower.includes('x') || lower.includes('v');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 계획서 content에서 페어링 질문 체크박스를 답변에 따라 업데이트한다
|
|
34
|
+
*
|
|
35
|
+
* 동작:
|
|
36
|
+
* 1. planContent에서 `## {pairingQuestionSection}` 섹션 시작/끝 라인 범위를 찾음
|
|
37
|
+
* 2. 섹션 내 각 질문 라인(`- [ ] **{id}**:`)을 순회
|
|
38
|
+
* 3. answers 배열에 해당 questionId가 있으면:
|
|
39
|
+
* - `[ ]` → `[x]`로 교체
|
|
40
|
+
* - 질문 라인 끝에 ` → **답변: {selectedOption 또는 freeText}**` 추가
|
|
41
|
+
* 4. 옵션 라인 중 선택된 옵션이 있으면 해당 라인에 `✅` 추가
|
|
42
|
+
* 5. answers에 없는 질문(건너뜀)은 원본 유지
|
|
43
|
+
* 6. 섹션 외 content는 그대로 보존
|
|
44
|
+
*
|
|
45
|
+
* @param planContent - 계획서 마크다운 전체 내용
|
|
46
|
+
* @param answers - CLI 프롬프트에서 수집된 답변 배열
|
|
47
|
+
* @param config - 페어링 질문 섹션명 설정
|
|
48
|
+
* @returns 업데이트된 content와 반영된 답변 수
|
|
49
|
+
*/
|
|
50
|
+
export function updatePlanQuestions(planContent, answers, config) {
|
|
51
|
+
if (answers.length === 0) {
|
|
52
|
+
return { updatedContent: planContent, appliedCount: 0 };
|
|
53
|
+
}
|
|
54
|
+
const hasCRLF = planContent.includes('\r\n');
|
|
55
|
+
const normalized = normalizeLineEndings(planContent);
|
|
56
|
+
const sectionLines = findSectionLines(normalized, config.pairingQuestionSection);
|
|
57
|
+
if (sectionLines.length === 0) {
|
|
58
|
+
return { updatedContent: planContent, appliedCount: 0 };
|
|
59
|
+
}
|
|
60
|
+
const answerMap = new Map();
|
|
61
|
+
for (const a of answers) {
|
|
62
|
+
answerMap.set(a.questionId, a);
|
|
63
|
+
}
|
|
64
|
+
const lines = normalized.split('\n');
|
|
65
|
+
const { sectionStart, sectionEnd } = findSectionRange(lines, config.pairingQuestionSection);
|
|
66
|
+
if (sectionStart === -1) {
|
|
67
|
+
return { updatedContent: planContent, appliedCount: 0 };
|
|
68
|
+
}
|
|
69
|
+
let appliedCount = 0;
|
|
70
|
+
let currentAnswer;
|
|
71
|
+
for (let i = sectionStart + 1; i < sectionEnd; i++) {
|
|
72
|
+
const line = lines[i] ?? '';
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (trimmed === '')
|
|
75
|
+
continue;
|
|
76
|
+
const isTopLevel = !/^\s{2,}/.test(line);
|
|
77
|
+
if (isTopLevel) {
|
|
78
|
+
const qMatch = trimmed.match(QUESTION_LINE);
|
|
79
|
+
if (qMatch?.groups) {
|
|
80
|
+
const { checked, id } = qMatch.groups;
|
|
81
|
+
const questionId = (id ?? '').trim();
|
|
82
|
+
if (isChecked(checked ?? '')) {
|
|
83
|
+
currentAnswer = undefined;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const answer = answerMap.get(questionId);
|
|
87
|
+
if (answer) {
|
|
88
|
+
currentAnswer = answer;
|
|
89
|
+
const answerText = answer.selectedOption ?? answer.freeText ?? '';
|
|
90
|
+
const updatedLine = line.replace(/\[[ ]*\]/, '[x]').replace(/\s*$/, '');
|
|
91
|
+
lines[i] = `${updatedLine} → **답변: ${answerText}**`;
|
|
92
|
+
appliedCount++;
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
currentAnswer = undefined;
|
|
96
|
+
}
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (currentAnswer?.selectedOption && /^\s{2,}/.test(line)) {
|
|
101
|
+
const optMatch = line.match(SUB_OPTION_LINE);
|
|
102
|
+
if (optMatch?.groups?.optionText) {
|
|
103
|
+
const rawText = optMatch.groups.optionText.trim();
|
|
104
|
+
const cleanText = rawText
|
|
105
|
+
.replace(/^\s*✅\s*/, '')
|
|
106
|
+
.replace(/\s*✅\s*$/, '')
|
|
107
|
+
.trim();
|
|
108
|
+
if (cleanText === currentAnswer.selectedOption && !rawText.includes('✅')) {
|
|
109
|
+
lines[i] = `${line} ✅`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let result = lines.join('\n');
|
|
115
|
+
if (hasCRLF) {
|
|
116
|
+
result = result.replace(/\n/g, '\r\n');
|
|
117
|
+
}
|
|
118
|
+
return { updatedContent: result, appliedCount };
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* 섹션명에 해당하는 라인 범위(시작 헤딩 인덱스, 끝 인덱스)를 찾는다
|
|
122
|
+
*
|
|
123
|
+
* findSectionLines와 동일한 매칭 로직을 사용하되,
|
|
124
|
+
* 라인 인덱스 범위를 반환하여 원본 lines 배열을 직접 수정할 수 있게 한다.
|
|
125
|
+
*/
|
|
126
|
+
function findSectionRange(lines, sectionName) {
|
|
127
|
+
let sectionStart = -1;
|
|
128
|
+
let sectionEnd = lines.length;
|
|
129
|
+
let sectionHeadingLevel = 0;
|
|
130
|
+
for (let i = 0; i < lines.length; i++) {
|
|
131
|
+
const trimmed = (lines[i] ?? '').trim();
|
|
132
|
+
const headingMatch = trimmed.match(/^(#{1,6})\s+(.*)$/);
|
|
133
|
+
if (sectionStart !== -1 && headingMatch?.[1]) {
|
|
134
|
+
const level = headingMatch[1].length;
|
|
135
|
+
if (level <= sectionHeadingLevel) {
|
|
136
|
+
sectionEnd = i;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (sectionStart === -1 && headingMatch?.[2]) {
|
|
141
|
+
const headingText = headingMatch[2].trim();
|
|
142
|
+
if (headingText.includes(sectionName)) {
|
|
143
|
+
sectionStart = i;
|
|
144
|
+
sectionHeadingLevel = (headingMatch[1] ?? '').length;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { sectionStart, sectionEnd };
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=plan-question-updater.js.map
|
|
@@ -15,6 +15,7 @@ export declare const DEFAULT_ID_PATTERN = "^(U-\\d+|CP-).*";
|
|
|
15
15
|
export declare const DEFAULT_RUNBOOK_DIR = "vibe/unit-runbooks";
|
|
16
16
|
export declare const DEFAULT_COMMAND_SECTION = "# \uC720\uB2DB \uAD6C\uD604";
|
|
17
17
|
export declare const DEFAULT_HEADER_TEMPLATE = "### \uD604\uC7AC \uAD6C\uD604 \uC720\uB2DB: {{title}}\n- \uD604\uC7AC \uAD6C\uD604 \uC720\uB2DB \uAC1C\uBC1C \uACC4\uD68D\uC11C: @{{planPath}}\n- \uD604\uC7AC \uAD6C\uD604 Commit(\uBCC0\uACBD\uC810 \uD655\uC778\uD558\uC5EC \uB9E5\uB77D\uC73C\uB85C \uC0AC\uC6A9): -";
|
|
18
|
+
export declare const SUB_UNIT_HEADER_TEMPLATE = "### \uD604\uC7AC \uC720\uB2DB: {{title}}\n- \uACC4\uD68D\uC11C: @{{planPath}}\n- Commit: -";
|
|
18
19
|
/**
|
|
19
20
|
* 외부 주입 데이터(stdin 등)에서 InitAnswers를 추출한다
|
|
20
21
|
*/
|
|
@@ -26,8 +27,8 @@ export declare function buildAnswersFromScan(scanResult: ScanResult, overrides?:
|
|
|
26
27
|
/**
|
|
27
28
|
* InitAnswers → 설정 JSON 객체 구성 (최소 필드)
|
|
28
29
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
30
|
+
* scanResult가 제공되면 감지된 패턴별로 다중 unitType을 생성한다.
|
|
31
|
+
* 서브유닛 패턴(PREFIX-NNN-XN)이 감지된 prefix는 별도의 unitType으로 분리된다.
|
|
31
32
|
*/
|
|
32
|
-
export declare function buildConfigObject(answers: InitAnswers): Record<string, unknown>;
|
|
33
|
+
export declare function buildConfigObject(answers: InitAnswers, scanResult?: ScanResult): Record<string, unknown>;
|
|
33
34
|
//# sourceMappingURL=config-generator.d.ts.map
|
|
@@ -15,6 +15,7 @@ export const DEFAULT_ID_PATTERN = '^(U-\\d+|CP-).*';
|
|
|
15
15
|
export const DEFAULT_RUNBOOK_DIR = 'vibe/unit-runbooks';
|
|
16
16
|
export const DEFAULT_COMMAND_SECTION = '# 유닛 구현';
|
|
17
17
|
export const DEFAULT_HEADER_TEMPLATE = '### 현재 구현 유닛: {{title}}\n- 현재 구현 유닛 개발 계획서: @{{planPath}}\n- 현재 구현 Commit(변경점 확인하여 맥락으로 사용): -';
|
|
18
|
+
export const SUB_UNIT_HEADER_TEMPLATE = '### 현재 유닛: {{title}}\n- 계획서: @{{planPath}}\n- Commit: -';
|
|
18
19
|
/**
|
|
19
20
|
* Record<string, unknown> 오버라이드를 InitAnswers 기본값에 적용한다
|
|
20
21
|
*/
|
|
@@ -78,10 +79,10 @@ export function buildAnswersFromScan(scanResult, overrides) {
|
|
|
78
79
|
/**
|
|
79
80
|
* InitAnswers → 설정 JSON 객체 구성 (최소 필드)
|
|
80
81
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
82
|
+
* scanResult가 제공되면 감지된 패턴별로 다중 unitType을 생성한다.
|
|
83
|
+
* 서브유닛 패턴(PREFIX-NNN-XN)이 감지된 prefix는 별도의 unitType으로 분리된다.
|
|
83
84
|
*/
|
|
84
|
-
export function buildConfigObject(answers) {
|
|
85
|
+
export function buildConfigObject(answers, scanResult) {
|
|
85
86
|
const docRoots = {
|
|
86
87
|
plan: answers.planDir,
|
|
87
88
|
result: answers.resultDir,
|
|
@@ -89,7 +90,10 @@ export function buildConfigObject(answers) {
|
|
|
89
90
|
if (answers.runbookDir) {
|
|
90
91
|
docRoots['runbook'] = answers.runbookDir;
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
const unitTypes = scanResult
|
|
94
|
+
? buildUnitTypesFromScan(answers, scanResult, docRoots)
|
|
95
|
+
: [buildDefaultUnitType(answers)];
|
|
96
|
+
const config = {
|
|
93
97
|
$schema: 'https://vibe-commander/config-schema.json',
|
|
94
98
|
version: 1,
|
|
95
99
|
paths: {
|
|
@@ -97,17 +101,80 @@ export function buildConfigObject(answers) {
|
|
|
97
101
|
roadmap: answers.roadmapPath,
|
|
98
102
|
docRoots,
|
|
99
103
|
},
|
|
100
|
-
unitTypes
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
104
|
+
unitTypes,
|
|
105
|
+
};
|
|
106
|
+
if (unitTypes.length > 1) {
|
|
107
|
+
config['commandSections'] = { useMarkers: true };
|
|
108
|
+
}
|
|
109
|
+
return config;
|
|
110
|
+
}
|
|
111
|
+
function buildDefaultUnitType(answers) {
|
|
112
|
+
return {
|
|
113
|
+
key: 'implement',
|
|
114
|
+
displayName: '유닛 구현',
|
|
115
|
+
idPattern: answers.idPattern,
|
|
116
|
+
planDir: 'plan',
|
|
117
|
+
commandSection: answers.commandSection,
|
|
118
|
+
collectDeps: true,
|
|
119
|
+
headerTemplate: DEFAULT_HEADER_TEMPLATE,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 스캔 결과의 패턴 정보로 다중 unitType 배열을 구성한다.
|
|
124
|
+
*
|
|
125
|
+
* hasSubUnit이 true인 prefix는 서브유닛 타입 + 태그 타입으로 분리되고,
|
|
126
|
+
* 나머지 prefix는 하나의 implement 타입으로 통합된다.
|
|
127
|
+
*/
|
|
128
|
+
function buildUnitTypesFromScan(answers, scanResult, docRoots) {
|
|
129
|
+
const unitTypes = [];
|
|
130
|
+
const implementPrefixes = [];
|
|
131
|
+
for (const pattern of scanResult.idPatterns) {
|
|
132
|
+
if (!pattern.hasSubUnit) {
|
|
133
|
+
implementPrefixes.push(pattern.regex.replace(/^\^/, ''));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const subDir = scanResult.docRoots.find((d) => d.role === 'refactorSub');
|
|
137
|
+
if (subDir) {
|
|
138
|
+
docRoots['refactorSub'] = subDir.dirPath;
|
|
139
|
+
}
|
|
140
|
+
unitTypes.push({
|
|
141
|
+
key: `${pattern.prefix.toLowerCase()}-sub`,
|
|
142
|
+
displayName: `${pattern.prefix} 서브유닛`,
|
|
143
|
+
idPattern: `^${pattern.prefix}-\\d+-[A-Z]\\d+$`,
|
|
144
|
+
planDir: subDir ? 'refactorSub' : 'plan',
|
|
145
|
+
commandSection: `# ${pattern.prefix} 서브유닛`,
|
|
146
|
+
collectDeps: false,
|
|
147
|
+
headerTemplate: SUB_UNIT_HEADER_TEMPLATE,
|
|
148
|
+
});
|
|
149
|
+
if (pattern.hasTag) {
|
|
150
|
+
unitTypes.push({
|
|
151
|
+
key: pattern.prefix.toLowerCase(),
|
|
152
|
+
displayName: `${pattern.prefix} 유닛`,
|
|
153
|
+
idPattern: `^${pattern.prefix}-\\d+\\[.*\\]$`,
|
|
105
154
|
planDir: 'plan',
|
|
106
|
-
commandSection:
|
|
155
|
+
commandSection: `# ${pattern.prefix} 유닛`,
|
|
107
156
|
collectDeps: true,
|
|
108
|
-
headerTemplate:
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
}
|
|
157
|
+
headerTemplate: SUB_UNIT_HEADER_TEMPLATE,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (implementPrefixes.length > 0) {
|
|
162
|
+
const implPattern = implementPrefixes.length === 1
|
|
163
|
+
? `^${implementPrefixes[0] ?? ''}.*`
|
|
164
|
+
: `^(${implementPrefixes.join('|')}).*`;
|
|
165
|
+
unitTypes.push({
|
|
166
|
+
key: 'implement',
|
|
167
|
+
displayName: '유닛 구현',
|
|
168
|
+
idPattern: implPattern,
|
|
169
|
+
planDir: 'plan',
|
|
170
|
+
commandSection: answers.commandSection,
|
|
171
|
+
collectDeps: true,
|
|
172
|
+
headerTemplate: DEFAULT_HEADER_TEMPLATE,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else if (unitTypes.length === 0) {
|
|
176
|
+
unitTypes.push(buildDefaultUnitType(answers));
|
|
177
|
+
}
|
|
178
|
+
return unitTypes;
|
|
112
179
|
}
|
|
113
180
|
//# sourceMappingURL=config-generator.js.map
|
package/dist/types/init.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface DetectedPattern {
|
|
|
22
22
|
displayLabel: string;
|
|
23
23
|
matchCount: number;
|
|
24
24
|
examples: string[];
|
|
25
|
+
/** PREFIX-NNN[Tag] 형태의 태그 파일이 존재하는지 여부 */
|
|
26
|
+
hasTag: boolean;
|
|
27
|
+
/** PREFIX-NNN-XN 형태의 서브유닛 파일이 존재하는지 여부 */
|
|
28
|
+
hasSubUnit: boolean;
|
|
25
29
|
}
|
|
26
30
|
/** 감지된 문서 루트 디렉토리 */
|
|
27
31
|
export interface DetectedDocRoot {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!-- vc:begin:implement -->
|
|
2
|
+
# 유닛 구현
|
|
3
|
+
---------------------------------------------------
|
|
4
|
+
### 현재 구현 유닛: -
|
|
5
|
+
- 현재 구현 유닛 개발 계획서: -
|
|
6
|
+
- 현재 구현 Commit(변경점 확인하여 맥락으로 사용): -
|
|
7
|
+
|
|
8
|
+
### 의존 유닛 및 기능:
|
|
9
|
+
-
|
|
10
|
+
|
|
11
|
+
### 의존성 문서:
|
|
12
|
+
-
|
|
13
|
+
|
|
14
|
+
### 의존성 Commits (변경점 확인하여 맥락으로 사용):
|
|
15
|
+
-
|
|
16
|
+
|
|
17
|
+
### 특별 요청사항
|
|
18
|
+
-
|
|
19
|
+
---------------------------------------------------
|
|
20
|
+
<!-- vc:end:implement -->
|
|
21
|
+
|
|
22
|
+
<!-- vc:begin:refactor -->
|
|
23
|
+
# 리펙토링 유닛 제안
|
|
24
|
+
---------------------------------------------------
|
|
25
|
+
### 현재 리펙토링 유닛: -
|
|
26
|
+
- 현재 리펙토링 유닛 제안 계획서: -
|
|
27
|
+
- 현재 리펙토링 Commit(변경점 확인하여 맥락으로 사용): -
|
|
28
|
+
|
|
29
|
+
### 의존 유닛 및 기능:
|
|
30
|
+
-
|
|
31
|
+
|
|
32
|
+
### 의존성 문서:
|
|
33
|
+
-
|
|
34
|
+
|
|
35
|
+
### 의존성 Commits (변경점 확인하여 맥락으로 사용):
|
|
36
|
+
-
|
|
37
|
+
|
|
38
|
+
### 특별 요청사항
|
|
39
|
+
-
|
|
40
|
+
---------------------------------------------------
|
|
41
|
+
<!-- vc:end:refactor -->
|
|
42
|
+
|
|
43
|
+
<!-- vc:begin:refactor-sub -->
|
|
44
|
+
# 리펙토링 유닛 구현
|
|
45
|
+
---------------------------------------------------
|
|
46
|
+
### 현재 리펙토링 유닛: -
|
|
47
|
+
- 현재 리펙토링 유닛 제안 계획서: -
|
|
48
|
+
- 현재 리펙토링 Commit(변경점 확인하여 맥락으로 사용): -
|
|
49
|
+
|
|
50
|
+
### 특별 요청사항
|
|
51
|
+
-
|
|
52
|
+
---------------------------------------------------
|
|
53
|
+
<!-- vc:end:refactor-sub -->
|
package/package.json
CHANGED