maskweaver 0.9.9 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/config-manager/npm-dist-tags.d.ts +7 -0
- package/dist/cli/config-manager/npm-dist-tags.js +15 -0
- package/dist/cli/config-manager/plugin-name-with-version.d.ts +1 -0
- package/dist/cli/config-manager/plugin-name-with-version.js +21 -0
- package/dist/cli/config-manager/version-compatibility.d.ts +9 -0
- package/dist/cli/config-manager/version-compatibility.js +81 -0
- package/dist/cli/install.js +26 -11
- package/dist/plugin/index.js +80 -41
- package/dist/plugin/tools/weave.js +124 -8
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/weave/stages/intake-ambiguity.d.ts +5 -0
- package/dist/weave/stages/intake-ambiguity.js +200 -0
- package/dist/weave/stages/intake-persistence.d.ts +9 -0
- package/dist/weave/stages/intake-persistence.js +66 -0
- package/dist/weave/stages/intake-questions.d.ts +6 -0
- package/dist/weave/stages/intake-questions.js +195 -0
- package/dist/weave/stages/intake-types.d.ts +116 -0
- package/dist/weave/stages/intake-types.js +1 -0
- package/dist/weave/stages/intake.d.ts +8 -55
- package/dist/weave/stages/intake.js +112 -128
- package/dist/weave/stages/plan.js +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export const AMBIGUITY_THRESHOLD = 0.20;
|
|
2
|
+
export const GHERKIN_READINESS_THRESHOLD = 0.30;
|
|
3
|
+
const GOAL_CLARITY_WEIGHT = 0.40;
|
|
4
|
+
const CONSTRAINT_CLARITY_WEIGHT = 0.30;
|
|
5
|
+
const SUCCESS_CRITERIA_CLARITY_WEIGHT = 0.30;
|
|
6
|
+
const BROWNFIELD_CONTEXT_WEIGHT = 0.15;
|
|
7
|
+
const BROWNFIELD_GOAL_WEIGHT = 0.35;
|
|
8
|
+
const BROWNFIELD_CONSTRAINT_WEIGHT = 0.25;
|
|
9
|
+
const BROWNFIELD_CRITERIA_WEIGHT = 0.25;
|
|
10
|
+
const MILESTONE_DEFINITIONS = [
|
|
11
|
+
{ maxScore: 1.0, label: 'initial', description: '핵심 요구사항만 파악됨. 제약조건과 성공 기준이 큰 공백.' },
|
|
12
|
+
{ maxScore: 0.40, label: 'progress', description: '대부분 요구사항 파악. 일부 세부사항과 경계 조건 누락.' },
|
|
13
|
+
{ maxScore: 0.30, label: 'refined', description: '성공 기준 일부 정의됨. 경계 조건과 비기능 요구사항 보강 필요.' },
|
|
14
|
+
{ maxScore: AMBIGUITY_THRESHOLD, label: 'ready', description: '모든 기준이 구체적이고 테스트 가능. Seed 생성 준비 완료.' },
|
|
15
|
+
];
|
|
16
|
+
function getMilestone(score) {
|
|
17
|
+
for (const m of MILESTONE_DEFINITIONS) {
|
|
18
|
+
if (score <= m.maxScore)
|
|
19
|
+
return { label: m.label, description: m.description };
|
|
20
|
+
}
|
|
21
|
+
return { label: 'initial', description: MILESTONE_DEFINITIONS[0].description };
|
|
22
|
+
}
|
|
23
|
+
function getNextMilestone(score) {
|
|
24
|
+
for (let i = MILESTONE_DEFINITIONS.length - 1; i >= 0; i--) {
|
|
25
|
+
const m = MILESTONE_DEFINITIONS[i];
|
|
26
|
+
if (score > m.maxScore) {
|
|
27
|
+
return { threshold: m.maxScore, label: m.label, description: m.description };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function buildJustification(category, score) {
|
|
33
|
+
const pct = (score * 100).toFixed(0);
|
|
34
|
+
switch (category) {
|
|
35
|
+
case 'goal':
|
|
36
|
+
if (score >= 0.70)
|
|
37
|
+
return `목표가 명확하고 Gherkin 시나리오로 변환 가능 (${pct}%)`;
|
|
38
|
+
if (score >= 0.40)
|
|
39
|
+
return `특징이 식별되었으나 세부사항 부족 (${pct}%)`;
|
|
40
|
+
return `목표 정의가 불충분. 더 많은 질문 필요 (${pct}%)`;
|
|
41
|
+
case 'constraint':
|
|
42
|
+
if (score >= 0.60)
|
|
43
|
+
return `제약조건이 잘 정의됨 (${pct}%)`;
|
|
44
|
+
if (score >= 0.30)
|
|
45
|
+
return `기술 스택은 파악되었으나 경계 조건 정의 필요 (${pct}%)`;
|
|
46
|
+
return `제약조건 정의가 필요함 (${pct}%)`;
|
|
47
|
+
case 'criteria':
|
|
48
|
+
if (score >= 0.70)
|
|
49
|
+
return `성공 기준이 Gherkin 시나리오로 충분히 정의됨 (${pct}%)`;
|
|
50
|
+
if (score >= 0.30)
|
|
51
|
+
return `일부 인수 조건 존재하나 보강 필요 (${pct}%)`;
|
|
52
|
+
return `인수 조건이 정의되지 않음 (${pct}%)`;
|
|
53
|
+
case 'context':
|
|
54
|
+
if (score >= 0.60)
|
|
55
|
+
return `기존 코드베이스 맥락이 충분히 파악됨 (${pct}%)`;
|
|
56
|
+
if (score >= 0.30)
|
|
57
|
+
return `코드베이스 맵은 있으나 세부 이해 부족 (${pct}%)`;
|
|
58
|
+
return `기존 코드베이스 맥락 파악이 필요함 (${pct}%)`;
|
|
59
|
+
default:
|
|
60
|
+
return `Clarity: ${pct}%`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function scoreAmbiguity(features, techReqs, answers, gherkinScenarios, isBrownfield = false, codebaseMapAvailable = false) {
|
|
64
|
+
const answerCount = Object.keys(answers).length;
|
|
65
|
+
const answerDepth = Object.values(answers).reduce((sum, a) => sum + Math.min(a.length / 100, 1.0), 0);
|
|
66
|
+
const hasFeatures = features.length > 0;
|
|
67
|
+
const featureDetailScore = hasFeatures
|
|
68
|
+
? Math.min(features.reduce((sum, f) => sum + Math.min(f.length / 30, 1.0), 0) / Math.max(features.length, 1), 1.0)
|
|
69
|
+
: 0;
|
|
70
|
+
let goalClarityScore = 0.0;
|
|
71
|
+
if (hasFeatures)
|
|
72
|
+
goalClarityScore += 0.25;
|
|
73
|
+
if (featureDetailScore > 0.3)
|
|
74
|
+
goalClarityScore += 0.15;
|
|
75
|
+
if (answerCount >= 3)
|
|
76
|
+
goalClarityScore += 0.15;
|
|
77
|
+
if (answerDepth >= 1.5)
|
|
78
|
+
goalClarityScore += 0.15;
|
|
79
|
+
if (gherkinScenarios && gherkinScenarios.length > 0)
|
|
80
|
+
goalClarityScore += 0.15;
|
|
81
|
+
if (answers && (answers['priority'] || Object.keys(answers).some(k => k.includes('우선순위') || k.includes('priority')))) {
|
|
82
|
+
goalClarityScore += 0.15;
|
|
83
|
+
}
|
|
84
|
+
let constraintClarityScore = 0.0;
|
|
85
|
+
const hasTechStack = (techReqs?.frontend?.length ?? 0) > 0 || (techReqs?.backend?.length ?? 0) > 0;
|
|
86
|
+
if (hasTechStack)
|
|
87
|
+
constraintClarityScore += 0.30;
|
|
88
|
+
if ((techReqs?.database?.length ?? 0) > 0)
|
|
89
|
+
constraintClarityScore += 0.15;
|
|
90
|
+
if (answerCount >= 5)
|
|
91
|
+
constraintClarityScore += 0.15;
|
|
92
|
+
if (answers && Object.keys(answers).some(k => k.includes('예외') || k.includes('edge') || k.includes('에러'))) {
|
|
93
|
+
constraintClarityScore += 0.20;
|
|
94
|
+
}
|
|
95
|
+
if (answers && Object.keys(answers).some(k => k.includes('제약') || k.includes('constraint') || k.includes('제한'))) {
|
|
96
|
+
constraintClarityScore += 0.20;
|
|
97
|
+
}
|
|
98
|
+
let successCriteriaClarityScore = 0.0;
|
|
99
|
+
if (gherkinScenarios && gherkinScenarios.length > 0) {
|
|
100
|
+
const gherkinThenCount = gherkinScenarios.reduce((sum, s) => sum + s.then.length, 0);
|
|
101
|
+
successCriteriaClarityScore += Math.min(gherkinThenCount * 0.10, 0.30);
|
|
102
|
+
if (gherkinScenarios.some(s => s.given.length > 0))
|
|
103
|
+
successCriteriaClarityScore += 0.15;
|
|
104
|
+
if (gherkinScenarios.some(s => s.when.length > 0))
|
|
105
|
+
successCriteriaClarityScore += 0.15;
|
|
106
|
+
if (gherkinScenarios.length >= features.length * 2)
|
|
107
|
+
successCriteriaClarityScore += 0.15;
|
|
108
|
+
}
|
|
109
|
+
if (answerCount >= 7)
|
|
110
|
+
successCriteriaClarityScore += 0.10;
|
|
111
|
+
if (answers && Object.keys(answers).some(k => k.includes('성공') || k.includes('측정') || k.includes('metric'))) {
|
|
112
|
+
successCriteriaClarityScore += 0.15;
|
|
113
|
+
}
|
|
114
|
+
let contextClarityScore = 0.0;
|
|
115
|
+
if (isBrownfield && codebaseMapAvailable) {
|
|
116
|
+
contextClarityScore += 0.40;
|
|
117
|
+
if (answers && Object.keys(answers).some(k => k.includes('기존') || k.includes('existing') || k.includes('구조'))) {
|
|
118
|
+
contextClarityScore += 0.30;
|
|
119
|
+
}
|
|
120
|
+
if (answers && Object.keys(answers).some(k => k.includes('마이그레이션') || k.includes('migration') || k.includes('리팩토링') || k.includes('refactor'))) {
|
|
121
|
+
contextClarityScore += 0.30;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
goalClarityScore = Math.min(1.0, Math.max(0.0, goalClarityScore));
|
|
125
|
+
constraintClarityScore = Math.min(1.0, Math.max(0.0, constraintClarityScore));
|
|
126
|
+
successCriteriaClarityScore = Math.min(1.0, Math.max(0.0, successCriteriaClarityScore));
|
|
127
|
+
contextClarityScore = Math.min(1.0, Math.max(0.0, contextClarityScore));
|
|
128
|
+
const goalComponent = {
|
|
129
|
+
name: 'Goal Clarity',
|
|
130
|
+
clarityScore: goalClarityScore,
|
|
131
|
+
weight: isBrownfield ? BROWNFIELD_GOAL_WEIGHT : GOAL_CLARITY_WEIGHT,
|
|
132
|
+
justification: buildJustification('goal', goalClarityScore),
|
|
133
|
+
thresholds: [
|
|
134
|
+
{ label: 'Features identified', minScore: 0.25, description: '특징이 식별되었는가?' },
|
|
135
|
+
{ label: 'Features detailed', minScore: 0.40, description: '특징에 세부사항이 있는가?' },
|
|
136
|
+
{ label: 'Questions answered', minScore: 0.55, description: '인터뷰 질문에 답했는가?' },
|
|
137
|
+
{ label: 'Gherkin ready', minScore: 0.70, description: 'Gherkin 시나리오로 변환 가능한가?' },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
const constraintComponent = {
|
|
141
|
+
name: 'Constraint Clarity',
|
|
142
|
+
clarityScore: constraintClarityScore,
|
|
143
|
+
weight: isBrownfield ? BROWNFIELD_CONSTRAINT_WEIGHT : CONSTRAINT_CLARITY_WEIGHT,
|
|
144
|
+
justification: buildJustification('constraint', constraintClarityScore),
|
|
145
|
+
thresholds: [
|
|
146
|
+
{ label: 'Tech stack known', minScore: 0.30, description: '기술 스택이 파악되었는가?' },
|
|
147
|
+
{ label: 'Edge cases covered', minScore: 0.50, description: '경계 조건이 다루어졌는가?' },
|
|
148
|
+
{ label: 'All constraints clear', minScore: 0.80, description: '모든 제약조건이 명확한가?' },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
const criteriaComponent = {
|
|
152
|
+
name: 'Success Criteria Clarity',
|
|
153
|
+
clarityScore: successCriteriaClarityScore,
|
|
154
|
+
weight: isBrownfield ? BROWNFIELD_CRITERIA_WEIGHT : SUCCESS_CRITERIA_CLARITY_WEIGHT,
|
|
155
|
+
justification: buildJustification('criteria', successCriteriaClarityScore),
|
|
156
|
+
thresholds: [
|
|
157
|
+
{ label: 'Basic AC exists', minScore: 0.30, description: '기본 인수 조건이 있는가?' },
|
|
158
|
+
{ label: 'Gherkin When/Then', minScore: 0.50, description: 'When/Then 시나리오가 있는가?' },
|
|
159
|
+
{ label: 'Full scenario coverage', minScore: 0.75, description: '충분한 시나리오 커버리지가 있는가?' },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
const breakdown = {
|
|
163
|
+
goalClarity: goalComponent,
|
|
164
|
+
constraintClarity: constraintComponent,
|
|
165
|
+
successCriteriaClarity: criteriaComponent,
|
|
166
|
+
};
|
|
167
|
+
if (isBrownfield && codebaseMapAvailable) {
|
|
168
|
+
breakdown.contextClarity = {
|
|
169
|
+
name: 'Context Clarity',
|
|
170
|
+
clarityScore: contextClarityScore,
|
|
171
|
+
weight: BROWNFIELD_CONTEXT_WEIGHT,
|
|
172
|
+
justification: buildJustification('context', contextClarityScore),
|
|
173
|
+
thresholds: [
|
|
174
|
+
{ label: 'Map available', minScore: 0.30, description: '코드베이스 맵이 있는가?' },
|
|
175
|
+
{ label: 'Patterns understood', minScore: 0.60, description: '기존 패턴이 파악되었는가?' },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const weightedClarity = Object.values(breakdown).reduce((sum, comp) => {
|
|
180
|
+
if (!comp)
|
|
181
|
+
return sum;
|
|
182
|
+
return sum + comp.clarityScore * comp.weight;
|
|
183
|
+
}, 0);
|
|
184
|
+
const overallScore = Math.round((1.0 - weightedClarity) * 10000) / 10000;
|
|
185
|
+
const components = [goalComponent, constraintComponent, criteriaComponent];
|
|
186
|
+
if (breakdown.contextClarity)
|
|
187
|
+
components.push(breakdown.contextClarity);
|
|
188
|
+
const weakest = components.reduce((min, c) => c.clarityScore < min.clarityScore ? c : min, components[0]);
|
|
189
|
+
const milestone = getMilestone(overallScore);
|
|
190
|
+
return {
|
|
191
|
+
overallScore,
|
|
192
|
+
breakdown,
|
|
193
|
+
isReadyForSeed: overallScore <= AMBIGUITY_THRESHOLD,
|
|
194
|
+
readinessForGherkin: overallScore <= GHERKIN_READINESS_THRESHOLD,
|
|
195
|
+
milestone: milestone.label,
|
|
196
|
+
milestoneDescription: milestone.description,
|
|
197
|
+
weakestArea: weakest.name,
|
|
198
|
+
nextMilestone: getNextMilestone(overallScore),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InterviewState } from './intake-types.js';
|
|
2
|
+
export declare function getInterviewDir(basePath: string): string;
|
|
3
|
+
export declare function saveInterviewState(basePath: string, state: InterviewState): void;
|
|
4
|
+
export declare function loadInterviewState(basePath: string, interviewId?: string): InterviewState | null;
|
|
5
|
+
export declare function listInterviewStates(basePath: string): Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
status: string;
|
|
8
|
+
rounds: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function getInterviewDir(basePath) {
|
|
4
|
+
const dir = path.join(basePath, '.opencode', 'weave', 'interview');
|
|
5
|
+
if (!fs.existsSync(dir)) {
|
|
6
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
function interviewStatePath(basePath, interviewId) {
|
|
11
|
+
return path.join(getInterviewDir(basePath), `${interviewId}.json`);
|
|
12
|
+
}
|
|
13
|
+
function getLatestInterviewId(basePath) {
|
|
14
|
+
const dir = getInterviewDir(basePath);
|
|
15
|
+
try {
|
|
16
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
17
|
+
if (files.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
return files[files.length - 1].replace('.json', '');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveInterviewState(basePath, state) {
|
|
26
|
+
const filePath = interviewStatePath(basePath, state.interviewId);
|
|
27
|
+
state.updatedAt = new Date().toISOString();
|
|
28
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
export function loadInterviewState(basePath, interviewId) {
|
|
31
|
+
const id = interviewId || getLatestInterviewId(basePath);
|
|
32
|
+
if (!id)
|
|
33
|
+
return null;
|
|
34
|
+
const filePath = interviewStatePath(basePath, id);
|
|
35
|
+
if (!fs.existsSync(filePath))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function listInterviewStates(basePath) {
|
|
46
|
+
const dir = getInterviewDir(basePath);
|
|
47
|
+
const results = [];
|
|
48
|
+
try {
|
|
49
|
+
for (const file of fs.readdirSync(dir)) {
|
|
50
|
+
if (!file.endsWith('.json'))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
54
|
+
const state = JSON.parse(content);
|
|
55
|
+
results.push({
|
|
56
|
+
id: state.interviewId,
|
|
57
|
+
status: state.status,
|
|
58
|
+
rounds: state.rounds.length,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MapResult, GherkinScenario } from '../types.js';
|
|
2
|
+
import type { Question, IntakeResult, AmbiguityScore } from './intake-types.js';
|
|
3
|
+
export declare function generateQuestions(features: string[], techReqs: IntakeResult['technicalRequirements']): Question[];
|
|
4
|
+
export declare function generateInterviewQuestions(features: string[], techReqs: IntakeResult['technicalRequirements'], map: MapResult | null): Question[];
|
|
5
|
+
export declare function generateGherkinQuestions(features: string[], existingQuestions: Question[], existingAnswers: Record<string, string>, ambiguity: AmbiguityScore): Question[];
|
|
6
|
+
export declare function generateAcceptanceCriteriaFromAnswers(features: string[], answers: Record<string, string>, doneWhenMap: Record<string, string>): GherkinScenario[];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export function generateQuestions(features, techReqs) {
|
|
2
|
+
const questions = [];
|
|
3
|
+
let qId = 1;
|
|
4
|
+
if (!techReqs.frontend || techReqs.frontend.length === 0) {
|
|
5
|
+
questions.push({
|
|
6
|
+
id: `Q${qId++}`,
|
|
7
|
+
topic: '프론트엔드 기술',
|
|
8
|
+
question: '프론트엔드 프레임워크 선호도가 있으신가요?',
|
|
9
|
+
options: ['React', 'Vue', 'Next.js', 'Vanilla JS', '상관없음'],
|
|
10
|
+
required: true,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (!techReqs.database || techReqs.database.length === 0) {
|
|
14
|
+
questions.push({
|
|
15
|
+
id: `Q${qId++}`,
|
|
16
|
+
topic: '데이터 저장',
|
|
17
|
+
question: '데이터를 어디에 저장할까요?',
|
|
18
|
+
options: ['로컬 스토리지 (오프라인)', '서버 DB (PostgreSQL/MySQL)', '클라우드 (Supabase/Firebase)'],
|
|
19
|
+
required: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (features.length > 3) {
|
|
23
|
+
questions.push({
|
|
24
|
+
id: `Q${qId++}`,
|
|
25
|
+
topic: '우선순위',
|
|
26
|
+
question: '가장 먼저 완성해야 하는 기능은 무엇인가요?',
|
|
27
|
+
options: features.slice(0, 5),
|
|
28
|
+
required: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return questions;
|
|
32
|
+
}
|
|
33
|
+
export function generateInterviewQuestions(features, techReqs, map) {
|
|
34
|
+
const questions = generateQuestions(features, techReqs);
|
|
35
|
+
if (map && map.structuralIssues.length > 0) {
|
|
36
|
+
const critical = map.structuralIssues.filter(i => i.severity === 'critical');
|
|
37
|
+
const warnings = map.structuralIssues.filter(i => i.severity === 'warning');
|
|
38
|
+
if (critical.length > 0) {
|
|
39
|
+
questions.unshift({
|
|
40
|
+
id: 'MAP-CRITICAL',
|
|
41
|
+
topic: '구조적 문제',
|
|
42
|
+
question: `코드베이스에서 ${critical.length}개의 Critical 이슈가 발견되었습니다. 계속 진행할까요?`,
|
|
43
|
+
options: ['이슈를 먼저 해결', '진행하되 build 단계에서 해결', '지금은 무시'],
|
|
44
|
+
required: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (warnings.length > 0) {
|
|
48
|
+
questions.push({
|
|
49
|
+
id: 'MAP-WARNINGS',
|
|
50
|
+
topic: '권장 구조 변경',
|
|
51
|
+
question: `${warnings.length}개의 경고가 있습니다. 구조 변경 권장사항을 검토하시겠습니까?`,
|
|
52
|
+
options: ['검토', '나중에 검토', '무시'],
|
|
53
|
+
required: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (features.length > 0) {
|
|
58
|
+
questions.push({
|
|
59
|
+
id: 'EXISTING-CODE',
|
|
60
|
+
topic: '기존 코드 활용',
|
|
61
|
+
question: '기존 코드베이스의 구조나 패턴을 유지하면서 구현하시겠습니까?',
|
|
62
|
+
options: ['최대한 기존 구조 유지', '필요시 구조 변경', '새로 작성'],
|
|
63
|
+
required: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return questions;
|
|
67
|
+
}
|
|
68
|
+
export function generateGherkinQuestions(features, existingQuestions, existingAnswers, ambiguity) {
|
|
69
|
+
const questions = [];
|
|
70
|
+
let qId = existingQuestions.length + 1;
|
|
71
|
+
if (ambiguity.weakestArea === 'Success Criteria Clarity' || ambiguity.overallScore > 0.3) {
|
|
72
|
+
for (let i = 0; i < Math.min(features.length, 3); i++) {
|
|
73
|
+
const feature = features[i];
|
|
74
|
+
const givenAnswered = Object.keys(existingAnswers).some(k => k.includes(`given-${i}`) || k.includes(`전제-${i}`));
|
|
75
|
+
if (!givenAnswered) {
|
|
76
|
+
questions.push({
|
|
77
|
+
id: `Q${qId++}`,
|
|
78
|
+
topic: 'Gherkin - Given',
|
|
79
|
+
question: `"${feature}" 기능을 사용하기 전에 어떤 전제조건이 필요합니까? (예: 로그인 되어 있어야 한다, 데이터가 존재해야 한다)`,
|
|
80
|
+
required: true,
|
|
81
|
+
questionType: 'gherkin-given',
|
|
82
|
+
targetFeature: feature,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const whenAnswered = Object.keys(existingAnswers).some(k => k.includes(`when-${i}`) || k.includes(`행동-${i}`));
|
|
86
|
+
if (!whenAnswered) {
|
|
87
|
+
questions.push({
|
|
88
|
+
id: `Q${qId++}`,
|
|
89
|
+
topic: 'Gherkin - When',
|
|
90
|
+
question: `"${feature}" 기능을 실행하기 위해 사용자가 어떤 행동을 하나요? (예: 버튼을 클릭한다, 값을 입력하고 제출한다)`,
|
|
91
|
+
required: true,
|
|
92
|
+
questionType: 'gherkin-when',
|
|
93
|
+
targetFeature: feature,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const thenAnswered = Object.keys(existingAnswers).some(k => k.includes(`then-${i}`) || k.includes(`결과-${i}`));
|
|
97
|
+
if (!thenAnswered) {
|
|
98
|
+
questions.push({
|
|
99
|
+
id: `Q${qId++}`,
|
|
100
|
+
topic: 'Gherkin - Then',
|
|
101
|
+
question: `"${feature}" 기능 실행 후 어떤 결과가 나와야 하나요? (예: 성공 메시지가 표시된다, 데이터가 저장된다, 화면이 갱신된다)`,
|
|
102
|
+
required: true,
|
|
103
|
+
questionType: 'gherkin-then',
|
|
104
|
+
targetFeature: feature,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (ambiguity.weakestArea === 'Constraint Clarity' || ambiguity.breakdown.constraintClarity.clarityScore < 0.5) {
|
|
110
|
+
const edgeAnswered = Object.keys(existingAnswers).some(k => k.includes('edge') || k.includes('예외') || k.includes('에러'));
|
|
111
|
+
if (!edgeAnswered) {
|
|
112
|
+
questions.push({
|
|
113
|
+
id: `Q${qId++}`,
|
|
114
|
+
topic: '경계 조건',
|
|
115
|
+
question: '비정상적인 상황(네트워크 오류, 잘못된 입력, 권한 없음 등)에서 어떻게 동작해야 하나요?',
|
|
116
|
+
required: true,
|
|
117
|
+
questionType: 'edge-case',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const constraintAnswered = Object.keys(existingAnswers).some(k => k.includes('constraint') || k.includes('제약') || k.includes('제한'));
|
|
121
|
+
if (!constraintAnswered) {
|
|
122
|
+
questions.push({
|
|
123
|
+
id: `Q${qId++}`,
|
|
124
|
+
topic: '제약조건',
|
|
125
|
+
question: '기술적/비기능적 제약조건이 있나요? (지원 브라우저, 성능 목표, 보안 요구사항, 데이터 제한 등)',
|
|
126
|
+
required: false,
|
|
127
|
+
questionType: 'constraint',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (ambiguity.weakestArea === 'Goal Clarity' || ambiguity.breakdown.goalClarity.clarityScore < 0.5) {
|
|
132
|
+
const successAnswered = Object.keys(existingAnswers).some(k => k.includes('성공') || k.includes('측정') || k.includes('metric'));
|
|
133
|
+
if (!successAnswered) {
|
|
134
|
+
questions.push({
|
|
135
|
+
id: `Q${qId++}`,
|
|
136
|
+
topic: '성공 지표',
|
|
137
|
+
question: '이 프로젝트의 성공을 어떻게 측정할 수 있을까요? (예: 사용자 100명 가입, 페이지 로드 2초 이내, 테스트 통과율 95%)',
|
|
138
|
+
required: false,
|
|
139
|
+
questionType: 'clarification',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return questions;
|
|
144
|
+
}
|
|
145
|
+
function findAnswer(answers, ...keys) {
|
|
146
|
+
for (const [k, v] of Object.entries(answers)) {
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
if (k.toLowerCase().includes(key.toLowerCase()))
|
|
149
|
+
return v;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
export function generateAcceptanceCriteriaFromAnswers(features, answers, doneWhenMap) {
|
|
155
|
+
const scenarios = [];
|
|
156
|
+
for (let i = 0; i < features.length; i++) {
|
|
157
|
+
const feature = features[i];
|
|
158
|
+
const doneWhen = doneWhenMap[feature] || `유저가 ${feature} 기능을 사용할 수 있다`;
|
|
159
|
+
const givenAnswer = findAnswer(answers, `given-${i}`, `전제-${i}`, feature, 'given');
|
|
160
|
+
const whenAnswer = findAnswer(answers, `when-${i}`, `행동-${i}`, feature, 'when');
|
|
161
|
+
const thenAnswer = findAnswer(answers, `then-${i}`, `결과-${i}`, feature, 'then');
|
|
162
|
+
const happyGiven = givenAnswer ? [givenAnswer] : [`${feature} 관련 기능이 구현되어 있다`];
|
|
163
|
+
const happyWhen = whenAnswer ? [whenAnswer] : [`유저가 ${feature} 기능을 사용한다`];
|
|
164
|
+
const happyThen = thenAnswer
|
|
165
|
+
? [thenAnswer, doneWhen].filter((v, idx, arr) => arr.indexOf(v) === idx)
|
|
166
|
+
: [doneWhen];
|
|
167
|
+
scenarios.push({
|
|
168
|
+
feature,
|
|
169
|
+
scenario: `${feature} - 정상 동작`,
|
|
170
|
+
given: happyGiven,
|
|
171
|
+
when: happyWhen,
|
|
172
|
+
then: happyThen,
|
|
173
|
+
});
|
|
174
|
+
const edgeAnswer = findAnswer(answers, 'edge', '에러', '예외', 'edge-cases');
|
|
175
|
+
if (edgeAnswer) {
|
|
176
|
+
scenarios.push({
|
|
177
|
+
feature,
|
|
178
|
+
scenario: `${feature} - 에러 처리`,
|
|
179
|
+
given: [`${feature} 관련 기능이 구현되어 있다`, '비정상적인 상황이 발생한다'],
|
|
180
|
+
when: ['오류 조건이 발생한다'],
|
|
181
|
+
then: [edgeAnswer],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
scenarios.push({
|
|
186
|
+
feature,
|
|
187
|
+
scenario: `${feature} - 에러 처리`,
|
|
188
|
+
given: [`${feature} 관련 기능이 구현되어 있다`],
|
|
189
|
+
when: [`유저가 ${feature} 기능을 비정상적으로 사용한다`],
|
|
190
|
+
then: ['적절한 에러 메시지가 표시된다'],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return scenarios;
|
|
195
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { WeaveEvent, EnvironmentAnalysis, MapResult, StructuralChange, ConsentPrompt, GherkinScenario } from '../types.js';
|
|
2
|
+
export interface IntakeOptions {
|
|
3
|
+
docsPath: string;
|
|
4
|
+
onEvent?: (event: WeaveEvent) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface IntakeResult {
|
|
7
|
+
documents: DocumentAnalysis[];
|
|
8
|
+
features: string[];
|
|
9
|
+
domainTerms: {
|
|
10
|
+
term: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}[];
|
|
13
|
+
technicalRequirements: {
|
|
14
|
+
frontend?: string[];
|
|
15
|
+
backend?: string[];
|
|
16
|
+
database?: string[];
|
|
17
|
+
other?: string[];
|
|
18
|
+
};
|
|
19
|
+
questions: Question[];
|
|
20
|
+
similarProjects?: string[];
|
|
21
|
+
environment?: EnvironmentAnalysis;
|
|
22
|
+
codebaseMapPath?: string;
|
|
23
|
+
structuralChanges?: StructuralChange[];
|
|
24
|
+
consentPrompts?: ConsentPrompt[];
|
|
25
|
+
ambiguityScore?: AmbiguityScore;
|
|
26
|
+
generatedScenarios?: GherkinScenario[];
|
|
27
|
+
}
|
|
28
|
+
export interface DocumentAnalysis {
|
|
29
|
+
path: string;
|
|
30
|
+
title: string;
|
|
31
|
+
sections: string[];
|
|
32
|
+
keyPoints: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface Question {
|
|
35
|
+
id: string;
|
|
36
|
+
topic: string;
|
|
37
|
+
question: string;
|
|
38
|
+
options?: string[];
|
|
39
|
+
required: boolean;
|
|
40
|
+
questionType?: 'clarification' | 'gherkin-given' | 'gherkin-when' | 'gherkin-then' | 'edge-case' | 'constraint' | 'priority' | 'technical';
|
|
41
|
+
targetFeature?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface AmbiguityComponent {
|
|
44
|
+
name: string;
|
|
45
|
+
clarityScore: number;
|
|
46
|
+
weight: number;
|
|
47
|
+
justification: string;
|
|
48
|
+
thresholds: {
|
|
49
|
+
label: string;
|
|
50
|
+
minScore: number;
|
|
51
|
+
description: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface AmbiguityBreakdown {
|
|
55
|
+
goalClarity: AmbiguityComponent;
|
|
56
|
+
constraintClarity: AmbiguityComponent;
|
|
57
|
+
successCriteriaClarity: AmbiguityComponent;
|
|
58
|
+
contextClarity?: AmbiguityComponent;
|
|
59
|
+
}
|
|
60
|
+
export interface AmbiguityScore {
|
|
61
|
+
overallScore: number;
|
|
62
|
+
breakdown: AmbiguityBreakdown;
|
|
63
|
+
isReadyForSeed: boolean;
|
|
64
|
+
readinessForGherkin: boolean;
|
|
65
|
+
milestone: 'initial' | 'progress' | 'refined' | 'ready';
|
|
66
|
+
milestoneDescription: string;
|
|
67
|
+
weakestArea: string;
|
|
68
|
+
nextMilestone?: {
|
|
69
|
+
threshold: number;
|
|
70
|
+
label: string;
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface InterviewRound {
|
|
75
|
+
roundNumber: number;
|
|
76
|
+
questions: Question[];
|
|
77
|
+
answers: Record<string, string>;
|
|
78
|
+
ambiguityBefore?: AmbiguityScore;
|
|
79
|
+
ambiguityAfter?: AmbiguityScore;
|
|
80
|
+
gherkinGenerated?: GherkinScenario[];
|
|
81
|
+
timestamp: string;
|
|
82
|
+
}
|
|
83
|
+
export interface InterviewState {
|
|
84
|
+
interviewId: string;
|
|
85
|
+
status: 'in_progress' | 'completed' | 'aborted';
|
|
86
|
+
initialContext: string;
|
|
87
|
+
rounds: InterviewRound[];
|
|
88
|
+
currentRound: number;
|
|
89
|
+
features: string[];
|
|
90
|
+
isBrownfield: boolean;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
updatedAt: string;
|
|
93
|
+
}
|
|
94
|
+
export interface InterviewOptions {
|
|
95
|
+
docsPath: string;
|
|
96
|
+
basePath?: string;
|
|
97
|
+
mapResult?: MapResult | null;
|
|
98
|
+
onEvent?: (event: WeaveEvent) => void;
|
|
99
|
+
resumeId?: string;
|
|
100
|
+
userAnswers?: Record<string, string>;
|
|
101
|
+
skipGherkinQuestions?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface InterviewResult {
|
|
104
|
+
intake: IntakeResult;
|
|
105
|
+
agreedStructuralChanges: StructuralChange[];
|
|
106
|
+
userAnswers: Record<string, string>;
|
|
107
|
+
satisfied: boolean;
|
|
108
|
+
ambiguityScore?: AmbiguityScore;
|
|
109
|
+
generatedScenarios?: GherkinScenario[];
|
|
110
|
+
interviewState?: InterviewState;
|
|
111
|
+
isMultiRound?: boolean;
|
|
112
|
+
}
|
|
113
|
+
export interface IntakeWithAnalysisOptions extends IntakeOptions {
|
|
114
|
+
skipEnvironmentAnalysis?: boolean;
|
|
115
|
+
warningsOnly?: boolean;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,60 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
documents: DocumentAnalysis[];
|
|
8
|
-
features: string[];
|
|
9
|
-
domainTerms: {
|
|
10
|
-
term: string;
|
|
11
|
-
description?: string;
|
|
12
|
-
}[];
|
|
13
|
-
technicalRequirements: {
|
|
14
|
-
frontend?: string[];
|
|
15
|
-
backend?: string[];
|
|
16
|
-
database?: string[];
|
|
17
|
-
other?: string[];
|
|
18
|
-
};
|
|
19
|
-
questions: Question[];
|
|
20
|
-
similarProjects?: string[];
|
|
21
|
-
environment?: EnvironmentAnalysis;
|
|
22
|
-
codebaseMapPath?: string;
|
|
23
|
-
structuralChanges?: StructuralChange[];
|
|
24
|
-
consentPrompts?: ConsentPrompt[];
|
|
25
|
-
}
|
|
26
|
-
export interface DocumentAnalysis {
|
|
27
|
-
path: string;
|
|
28
|
-
title: string;
|
|
29
|
-
sections: string[];
|
|
30
|
-
keyPoints: string[];
|
|
31
|
-
}
|
|
32
|
-
export interface Question {
|
|
33
|
-
id: string;
|
|
34
|
-
topic: string;
|
|
35
|
-
question: string;
|
|
36
|
-
options?: string[];
|
|
37
|
-
required: boolean;
|
|
38
|
-
}
|
|
1
|
+
import type { MapResult, StructuralChange, ConsentPrompt } from '../types.js';
|
|
2
|
+
export type { IntakeOptions, IntakeResult, DocumentAnalysis, Question, AmbiguityComponent, AmbiguityBreakdown, AmbiguityScore, InterviewRound, InterviewState, InterviewOptions, InterviewResult, IntakeWithAnalysisOptions, } from './intake-types.js';
|
|
3
|
+
export { scoreAmbiguity } from './intake-ambiguity.js';
|
|
4
|
+
export { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers, } from './intake-questions.js';
|
|
5
|
+
export { saveInterviewState, loadInterviewState, listInterviewStates, getInterviewDir, } from './intake-persistence.js';
|
|
6
|
+
import type { IntakeResult } from './intake-types.js';
|
|
39
7
|
export declare function injectMapContext(map: MapResult | null, features: string[]): Promise<{
|
|
40
8
|
structuralChanges: StructuralChange[];
|
|
41
9
|
consentPrompts: ConsentPrompt[];
|
|
42
10
|
}>;
|
|
43
|
-
|
|
44
|
-
docsPath: string;
|
|
45
|
-
basePath?: string;
|
|
46
|
-
mapResult?: MapResult | null;
|
|
47
|
-
onEvent?: (event: WeaveEvent) => void;
|
|
48
|
-
}
|
|
49
|
-
export interface InterviewResult {
|
|
50
|
-
intake: IntakeResult;
|
|
51
|
-
agreedStructuralChanges: StructuralChange[];
|
|
52
|
-
userAnswers: Record<string, string>;
|
|
53
|
-
satisfied: boolean;
|
|
54
|
-
}
|
|
55
|
-
export declare function interview(options: InterviewOptions): Promise<InterviewResult>;
|
|
56
|
-
export interface IntakeWithAnalysisOptions extends IntakeOptions {
|
|
57
|
-
skipEnvironmentAnalysis?: boolean;
|
|
58
|
-
warningsOnly?: boolean;
|
|
59
|
-
}
|
|
11
|
+
import type { IntakeOptions, IntakeWithAnalysisOptions, InterviewOptions, InterviewResult } from './intake-types.js';
|
|
60
12
|
export declare function intake(options: IntakeOptions | IntakeWithAnalysisOptions): Promise<IntakeResult>;
|
|
13
|
+
export declare function interview(options: InterviewOptions): Promise<InterviewResult>;
|