maskweaver 0.9.5 → 0.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +638 -592
- package/README.md +671 -667
- package/dist/plugin/index.js +6 -2
- package/dist/shared/generate-agents.d.ts +22 -0
- package/dist/shared/generate-agents.js +341 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/weave/gherkin.d.ts +43 -0
- package/dist/weave/gherkin.js +307 -0
- package/dist/weave/phase-manager.js +59 -0
- package/dist/weave/stages/build.js +16 -0
- package/dist/weave/stages/execute.d.ts +8 -1
- package/dist/weave/stages/execute.js +105 -0
- package/dist/weave/stages/plan.js +24 -0
- package/dist/weave/stages/refine.js +75 -0
- package/dist/weave/types.d.ts +11 -1
- package/package.json +1 -1
- package/postinstall.mjs +174 -71
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
export function parseGherkinText(text) {
|
|
5
|
+
const lines = text.split(/\r?\n/).map(l => l.trim());
|
|
6
|
+
let feature = '';
|
|
7
|
+
let description = '';
|
|
8
|
+
const scenarios = [];
|
|
9
|
+
let current = null;
|
|
10
|
+
let inFeature = false;
|
|
11
|
+
for (const line of lines) {
|
|
12
|
+
if (!line || line.startsWith('#'))
|
|
13
|
+
continue;
|
|
14
|
+
const featureMatch = /^Feature:\s*(.+)$/i.exec(line);
|
|
15
|
+
if (featureMatch) {
|
|
16
|
+
feature = featureMatch[1].trim();
|
|
17
|
+
inFeature = true;
|
|
18
|
+
current = null;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!inFeature)
|
|
22
|
+
continue;
|
|
23
|
+
const scenarioMatch = /^Scenario:\s*(.+)$/i.exec(line);
|
|
24
|
+
if (scenarioMatch) {
|
|
25
|
+
if (current)
|
|
26
|
+
scenarios.push(current);
|
|
27
|
+
current = { name: scenarioMatch[1].trim(), given: [], when: [], then: [] };
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!current) {
|
|
31
|
+
if (!scenarioMatch && feature && !line.startsWith('Given') && !line.startsWith('When') && !line.startsWith('Then') && !line.startsWith('And')) {
|
|
32
|
+
description += (description ? ' ' : '') + line;
|
|
33
|
+
}
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const stepMatch = /^(Given|When|Then|And)\s+(.+)$/i.exec(line);
|
|
37
|
+
if (stepMatch) {
|
|
38
|
+
const keyword = stepMatch[1].toLowerCase();
|
|
39
|
+
const step = stepMatch[2].trim();
|
|
40
|
+
switch (keyword) {
|
|
41
|
+
case 'given':
|
|
42
|
+
current.given.push(step);
|
|
43
|
+
break;
|
|
44
|
+
case 'when':
|
|
45
|
+
current.when.push(step);
|
|
46
|
+
break;
|
|
47
|
+
case 'then':
|
|
48
|
+
current.then.push(step);
|
|
49
|
+
break;
|
|
50
|
+
case 'and':
|
|
51
|
+
if (current.then.length > 0)
|
|
52
|
+
current.then.push(step);
|
|
53
|
+
else if (current.when.length > 0)
|
|
54
|
+
current.when.push(step);
|
|
55
|
+
else
|
|
56
|
+
current.given.push(step);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (current)
|
|
62
|
+
scenarios.push(current);
|
|
63
|
+
if (!feature && scenarios.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
return { feature: feature || 'Untitled', description, scenarios };
|
|
66
|
+
}
|
|
67
|
+
export function parseGherkinBlock(block) {
|
|
68
|
+
const parsed = parseGherkinText(block);
|
|
69
|
+
if (!parsed)
|
|
70
|
+
return [];
|
|
71
|
+
return parsed.scenarios.map(scenario => ({
|
|
72
|
+
feature: parsed.feature,
|
|
73
|
+
scenario: scenario.name,
|
|
74
|
+
given: scenario.given,
|
|
75
|
+
when: scenario.when,
|
|
76
|
+
then: scenario.then,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
export function formatGherkinScenario(scenario) {
|
|
80
|
+
const lines = [];
|
|
81
|
+
lines.push(` Scenario: ${scenario.scenario}`);
|
|
82
|
+
for (const g of scenario.given)
|
|
83
|
+
lines.push(` Given ${g}`);
|
|
84
|
+
for (const w of scenario.when)
|
|
85
|
+
lines.push(` When ${w}`);
|
|
86
|
+
for (const t of scenario.then)
|
|
87
|
+
lines.push(` Then ${t}`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
90
|
+
export function formatGherkinFeature(featureName, scenarios) {
|
|
91
|
+
const lines = [];
|
|
92
|
+
lines.push(`Feature: ${featureName}`);
|
|
93
|
+
lines.push('');
|
|
94
|
+
for (const scenario of scenarios) {
|
|
95
|
+
lines.push(formatGherkinScenario(scenario));
|
|
96
|
+
lines.push('');
|
|
97
|
+
}
|
|
98
|
+
return lines.join('\n');
|
|
99
|
+
}
|
|
100
|
+
export function formatGherkinForTask(task) {
|
|
101
|
+
if (!task.acceptanceCriteria || task.acceptanceCriteria.length === 0)
|
|
102
|
+
return '';
|
|
103
|
+
return formatGherkinFeature(task.name, task.acceptanceCriteria);
|
|
104
|
+
}
|
|
105
|
+
export function formatGherkinChecklist(scenarios) {
|
|
106
|
+
const lines = [];
|
|
107
|
+
for (const scenario of scenarios) {
|
|
108
|
+
lines.push(`- [ ] ${scenario.scenario}`);
|
|
109
|
+
for (const t of scenario.then) {
|
|
110
|
+
lines.push(` - Then ${t}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return lines.join('\n');
|
|
114
|
+
}
|
|
115
|
+
const FEATURES_DIR = '.opencode/weave/features';
|
|
116
|
+
function getFeaturesDir(basePath) {
|
|
117
|
+
return path.join(basePath, FEATURES_DIR);
|
|
118
|
+
}
|
|
119
|
+
export async function ensureFeaturesDir(basePath) {
|
|
120
|
+
const dir = getFeaturesDir(basePath);
|
|
121
|
+
await mkdir(dir, { recursive: true });
|
|
122
|
+
return dir;
|
|
123
|
+
}
|
|
124
|
+
export async function writeFeatureFile(basePath, phaseId, phaseName, scenarios) {
|
|
125
|
+
if (scenarios.length === 0)
|
|
126
|
+
return '';
|
|
127
|
+
const dir = await ensureFeaturesDir(basePath);
|
|
128
|
+
const fileName = `${phaseId.toLowerCase()}.feature`;
|
|
129
|
+
const filePath = path.join(dir, fileName);
|
|
130
|
+
const content = formatGherkinFeature(phaseName, scenarios);
|
|
131
|
+
await writeFile(filePath, content, 'utf-8');
|
|
132
|
+
return path.relative(basePath, filePath).replace(/\\/g, '/');
|
|
133
|
+
}
|
|
134
|
+
export async function writeAllFeatureFiles(basePath, phases) {
|
|
135
|
+
const results = new Map();
|
|
136
|
+
for (const phase of phases) {
|
|
137
|
+
const allScenarios = collectPhaseScenarios(phase);
|
|
138
|
+
if (allScenarios.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
const featurePath = await writeFeatureFile(basePath, phase.id, phase.name, allScenarios);
|
|
141
|
+
if (featurePath) {
|
|
142
|
+
results.set(phase.id, featurePath);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return results;
|
|
146
|
+
}
|
|
147
|
+
function collectPhaseScenarios(phase) {
|
|
148
|
+
const scenarios = [];
|
|
149
|
+
if (phase.acceptanceCriteria) {
|
|
150
|
+
scenarios.push(...phase.acceptanceCriteria);
|
|
151
|
+
}
|
|
152
|
+
for (const task of phase.tasks) {
|
|
153
|
+
if (task.acceptanceCriteria) {
|
|
154
|
+
scenarios.push(...task.acceptanceCriteria);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return scenarios;
|
|
158
|
+
}
|
|
159
|
+
export function generateGherkinForPhase(phase) {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
feature: phase.name,
|
|
163
|
+
scenario: `${phase.name} - 정상 동작`,
|
|
164
|
+
given: [`${phase.name} 관련 기능이 구현되어 있다`],
|
|
165
|
+
when: [`유저가 ${phase.name} 기능을 사용한다`],
|
|
166
|
+
then: [`${phase.doneWhen}`],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
feature: phase.name,
|
|
170
|
+
scenario: `${phase.name} - 에러 처리`,
|
|
171
|
+
given: [`${phase.name} 관련 기능이 구현되어 있다`],
|
|
172
|
+
when: [`유저가 ${phase.name} 기능을 비정상적으로 사용한다`],
|
|
173
|
+
then: ['적절한 에러 메시지가 표시된다'],
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
export function generateGherkinForTask(task, phase) {
|
|
178
|
+
return {
|
|
179
|
+
feature: phase.name,
|
|
180
|
+
scenario: task.name,
|
|
181
|
+
given: [`${phase.name} 기능의 기본 환경이 준비되어 있다`],
|
|
182
|
+
when: [`${task.name}을/를 실행한다`],
|
|
183
|
+
then: [task.testCase || phase.doneWhen],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
export function detectBDDFramework(projectPath) {
|
|
187
|
+
const checks = [
|
|
188
|
+
() => detectCucumber(projectPath),
|
|
189
|
+
() => detectJestCucumber(projectPath),
|
|
190
|
+
() => detectPytestBdd(projectPath),
|
|
191
|
+
];
|
|
192
|
+
for (const check of checks) {
|
|
193
|
+
const result = check();
|
|
194
|
+
if (result.detected)
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
const featureDir = findFeatureDir(projectPath);
|
|
198
|
+
if (featureDir) {
|
|
199
|
+
return {
|
|
200
|
+
detected: true,
|
|
201
|
+
framework: 'unknown',
|
|
202
|
+
testCommand: null,
|
|
203
|
+
featureDir,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
207
|
+
}
|
|
208
|
+
function detectCucumber(projectPath) {
|
|
209
|
+
try {
|
|
210
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
211
|
+
if (!fs.existsSync(pkgPath))
|
|
212
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
213
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
214
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
215
|
+
if (deps['@cucumber/cucumber']) {
|
|
216
|
+
const featureDir = findFeatureDir(projectPath) || 'features';
|
|
217
|
+
return {
|
|
218
|
+
detected: true,
|
|
219
|
+
framework: 'cucumber',
|
|
220
|
+
testCommand: `npx cucumber-js`,
|
|
221
|
+
featureDir,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { }
|
|
226
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
227
|
+
}
|
|
228
|
+
function detectJestCucumber(projectPath) {
|
|
229
|
+
try {
|
|
230
|
+
const pkgPath = path.join(projectPath, 'package.json');
|
|
231
|
+
if (!fs.existsSync(pkgPath))
|
|
232
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
233
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
234
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
235
|
+
if (deps['jest-cucumber'] || deps['@jest-cucumber/core']) {
|
|
236
|
+
const featureDir = findFeatureDir(projectPath) || 'features';
|
|
237
|
+
return {
|
|
238
|
+
detected: true,
|
|
239
|
+
framework: 'jest-cucumber',
|
|
240
|
+
testCommand: `npx jest`,
|
|
241
|
+
featureDir,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch { }
|
|
246
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
247
|
+
}
|
|
248
|
+
function detectPytestBdd(projectPath) {
|
|
249
|
+
const indicators = [
|
|
250
|
+
path.join(projectPath, 'pyproject.toml'),
|
|
251
|
+
path.join(projectPath, 'requirements.txt'),
|
|
252
|
+
];
|
|
253
|
+
for (const indicator of indicators) {
|
|
254
|
+
if (!fs.existsSync(indicator))
|
|
255
|
+
continue;
|
|
256
|
+
try {
|
|
257
|
+
const content = fs.readFileSync(indicator, 'utf-8');
|
|
258
|
+
if (content.includes('pytest-bdd')) {
|
|
259
|
+
const featureDir = findFeatureDir(projectPath) || 'features';
|
|
260
|
+
return {
|
|
261
|
+
detected: true,
|
|
262
|
+
framework: 'pytest-bdd',
|
|
263
|
+
testCommand: 'pytest',
|
|
264
|
+
featureDir,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return { detected: false, framework: null, testCommand: null, featureDir: null };
|
|
273
|
+
}
|
|
274
|
+
function findFeatureDir(projectPath) {
|
|
275
|
+
const candidates = ['features', 'test/features', 'tests/features', 'specs/features'];
|
|
276
|
+
for (const candidate of candidates) {
|
|
277
|
+
if (fs.existsSync(path.join(projectPath, candidate))) {
|
|
278
|
+
return candidate;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
}
|
|
283
|
+
export function generateGherkinVerificationPrompt(phase, task) {
|
|
284
|
+
const scenarios = task?.acceptanceCriteria || phase.acceptanceCriteria || [];
|
|
285
|
+
if (scenarios.length === 0)
|
|
286
|
+
return '';
|
|
287
|
+
const lines = [];
|
|
288
|
+
lines.push('### Acceptance Criteria (Gherkin)');
|
|
289
|
+
lines.push('');
|
|
290
|
+
lines.push('Verify each scenario is satisfied by the implementation:');
|
|
291
|
+
lines.push('');
|
|
292
|
+
for (const scenario of scenarios) {
|
|
293
|
+
lines.push(`**${scenario.scenario}**`);
|
|
294
|
+
for (const g of scenario.given)
|
|
295
|
+
lines.push(`- Given: ${g}`);
|
|
296
|
+
for (const w of scenario.when)
|
|
297
|
+
lines.push(`- When: ${w}`);
|
|
298
|
+
for (const t of scenario.then)
|
|
299
|
+
lines.push(`- Then: ${t}`);
|
|
300
|
+
lines.push('');
|
|
301
|
+
}
|
|
302
|
+
lines.push('For each scenario, check:');
|
|
303
|
+
lines.push('1. Are all "Given" preconditions met?');
|
|
304
|
+
lines.push('2. Can the "When" action be performed?');
|
|
305
|
+
lines.push('3. Do all "Then" expected outcomes hold?');
|
|
306
|
+
return lines.join('\n');
|
|
307
|
+
}
|
|
@@ -149,6 +149,25 @@ function serializePlan(plan) {
|
|
|
149
149
|
if (phase.completedAt) {
|
|
150
150
|
lines.push(` completed_at: ${yamlEscapeString(phase.completedAt)}`);
|
|
151
151
|
}
|
|
152
|
+
if (phase.featurePath) {
|
|
153
|
+
lines.push(` feature_path: ${yamlEscapeString(phase.featurePath)}`);
|
|
154
|
+
}
|
|
155
|
+
if (phase.acceptanceCriteria && phase.acceptanceCriteria.length > 0) {
|
|
156
|
+
lines.push(' acceptance_criteria:');
|
|
157
|
+
for (const ac of phase.acceptanceCriteria) {
|
|
158
|
+
lines.push(` - feature: ${yamlEscapeString(ac.feature)}`);
|
|
159
|
+
lines.push(` scenario: ${yamlEscapeString(ac.scenario)}`);
|
|
160
|
+
lines.push(' given:');
|
|
161
|
+
for (const g of ac.given)
|
|
162
|
+
lines.push(` - ${yamlEscapeString(g)}`);
|
|
163
|
+
lines.push(' when:');
|
|
164
|
+
for (const w of ac.when)
|
|
165
|
+
lines.push(` - ${yamlEscapeString(w)}`);
|
|
166
|
+
lines.push(' then:');
|
|
167
|
+
for (const t of ac.then)
|
|
168
|
+
lines.push(` - ${yamlEscapeString(t)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
152
171
|
lines.push(' checklist:');
|
|
153
172
|
for (const item of phase.checklist) {
|
|
154
173
|
lines.push(` - ${yamlEscapeString(item)}`);
|
|
@@ -185,6 +204,22 @@ function serializePlan(plan) {
|
|
|
185
204
|
if (task.acceptanceRefs && task.acceptanceRefs.length > 0) {
|
|
186
205
|
lines.push(` acceptance_refs: [${task.acceptanceRefs.map(ref => yamlEscapeString(ref)).join(', ')}]`);
|
|
187
206
|
}
|
|
207
|
+
if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
|
|
208
|
+
lines.push(' acceptance_criteria:');
|
|
209
|
+
for (const ac of task.acceptanceCriteria) {
|
|
210
|
+
lines.push(` - feature: ${yamlEscapeString(ac.feature)}`);
|
|
211
|
+
lines.push(` scenario: ${yamlEscapeString(ac.scenario)}`);
|
|
212
|
+
lines.push(' given:');
|
|
213
|
+
for (const g of ac.given)
|
|
214
|
+
lines.push(` - ${yamlEscapeString(g)}`);
|
|
215
|
+
lines.push(' when:');
|
|
216
|
+
for (const w of ac.when)
|
|
217
|
+
lines.push(` - ${yamlEscapeString(w)}`);
|
|
218
|
+
lines.push(' then:');
|
|
219
|
+
for (const t of ac.then)
|
|
220
|
+
lines.push(` - ${yamlEscapeString(t)}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
188
223
|
if (task.maskUsed) {
|
|
189
224
|
lines.push(` mask_used: ${yamlEscapeString(task.maskUsed)}`);
|
|
190
225
|
}
|
|
@@ -202,6 +237,27 @@ function serializePlan(plan) {
|
|
|
202
237
|
}
|
|
203
238
|
return lines.join('\n');
|
|
204
239
|
}
|
|
240
|
+
function deserializeGherkinList(raw) {
|
|
241
|
+
if (!raw || !Array.isArray(raw))
|
|
242
|
+
return undefined;
|
|
243
|
+
const scenarios = [];
|
|
244
|
+
for (const item of raw) {
|
|
245
|
+
if (!item || typeof item !== 'object')
|
|
246
|
+
continue;
|
|
247
|
+
const feature = item.feature || '';
|
|
248
|
+
const scenario = item.scenario || '';
|
|
249
|
+
if (!feature && !scenario)
|
|
250
|
+
continue;
|
|
251
|
+
scenarios.push({
|
|
252
|
+
feature,
|
|
253
|
+
scenario,
|
|
254
|
+
given: Array.isArray(item.given) ? item.given.filter((g) => typeof g === 'string') : [],
|
|
255
|
+
when: Array.isArray(item.when) ? item.when.filter((w) => typeof w === 'string') : [],
|
|
256
|
+
then: Array.isArray(item.then) ? item.then.filter((t) => typeof t === 'string') : [],
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return scenarios.length > 0 ? scenarios : undefined;
|
|
260
|
+
}
|
|
205
261
|
export class PhaseManager {
|
|
206
262
|
basePath;
|
|
207
263
|
plan = null;
|
|
@@ -550,6 +606,7 @@ export class PhaseManager {
|
|
|
550
606
|
dependsOn: t.depends_on || t.dependsOn,
|
|
551
607
|
verify: t.verify,
|
|
552
608
|
acceptanceRefs: t.acceptance_refs || t.acceptanceRefs,
|
|
609
|
+
acceptanceCriteria: deserializeGherkinList(t.acceptance_criteria || t.acceptanceCriteria),
|
|
553
610
|
retryCount: t.retry_count || t.retryCount || 0,
|
|
554
611
|
maxRetries: t.max_retries || t.maxRetries || 5,
|
|
555
612
|
lastError: t.last_error || t.lastError,
|
|
@@ -561,6 +618,8 @@ export class PhaseManager {
|
|
|
561
618
|
startedAt: p.started_at || p.startedAt,
|
|
562
619
|
completedAt: p.completed_at || p.completedAt,
|
|
563
620
|
masksUsed: p.masks_used || p.masksUsed,
|
|
621
|
+
acceptanceCriteria: deserializeGherkinList(p.acceptance_criteria || p.acceptanceCriteria),
|
|
622
|
+
featurePath: p.feature_path || p.featurePath,
|
|
564
623
|
})),
|
|
565
624
|
};
|
|
566
625
|
}
|
|
@@ -204,6 +204,22 @@ function generateBrief(task, phase, plan, agentTier, mask) {
|
|
|
204
204
|
lines.push(`- [${v.kind}] ${v.value}`);
|
|
205
205
|
lines.push(``);
|
|
206
206
|
}
|
|
207
|
+
if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
|
|
208
|
+
lines.push(`### Acceptance Criteria (Gherkin)`);
|
|
209
|
+
lines.push(``);
|
|
210
|
+
lines.push(`Every scenario MUST pass before this task can be marked as completed.`);
|
|
211
|
+
lines.push(``);
|
|
212
|
+
for (const scenario of task.acceptanceCriteria) {
|
|
213
|
+
lines.push(`**Scenario: ${scenario.scenario}**`);
|
|
214
|
+
for (const g of scenario.given)
|
|
215
|
+
lines.push(` Given ${g}`);
|
|
216
|
+
for (const w of scenario.when)
|
|
217
|
+
lines.push(` When ${w}`);
|
|
218
|
+
for (const t of scenario.then)
|
|
219
|
+
lines.push(` Then ${t}`);
|
|
220
|
+
lines.push(``);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
207
223
|
if (plan.structuralChanges && plan.structuralChanges.length > 0) {
|
|
208
224
|
const agreed = plan.structuralChanges.filter(sc => sc.agreed);
|
|
209
225
|
if (agreed.length > 0) {
|
|
@@ -36,7 +36,7 @@ export declare function execute(options: ExecuteOptions): Promise<ExecuteResult>
|
|
|
36
36
|
export interface VerificationLayer {
|
|
37
37
|
name: string;
|
|
38
38
|
order: number;
|
|
39
|
-
type: 'build' | 'test' | 'visual' | 'api' | 'accessibility';
|
|
39
|
+
type: 'build' | 'test' | 'visual' | 'api' | 'accessibility' | 'gherkin';
|
|
40
40
|
enabled: boolean;
|
|
41
41
|
}
|
|
42
42
|
export interface VerificationResult {
|
|
@@ -55,6 +55,13 @@ export interface AIVerificationOptions {
|
|
|
55
55
|
enablePlaywright?: boolean;
|
|
56
56
|
enableDevTools?: boolean;
|
|
57
57
|
mode?: 'quick' | 'full';
|
|
58
|
+
gherkinScenarios?: Array<{
|
|
59
|
+
feature: string;
|
|
60
|
+
scenario: string;
|
|
61
|
+
given: string[];
|
|
62
|
+
when: string[];
|
|
63
|
+
then: string[];
|
|
64
|
+
}>;
|
|
58
65
|
}
|
|
59
66
|
export declare function runAIVerification(options: AIVerificationOptions): Promise<{
|
|
60
67
|
passed: boolean;
|
|
@@ -2,6 +2,7 @@ import { getOrchestrator } from '../orchestrator.js';
|
|
|
2
2
|
import { getPhaseManager } from '../phase-manager.js';
|
|
3
3
|
import { searchTroubleshooting } from '../knowledge/global.js';
|
|
4
4
|
import { analyzeParallelOpportunities, formatParallelAnalysis } from '../bridge.js';
|
|
5
|
+
import { detectBDDFramework } from '../gherkin.js';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
7
|
import * as path from 'node:path';
|
|
7
8
|
import { copyFile, mkdir } from 'node:fs/promises';
|
|
@@ -177,6 +178,50 @@ export function formatExecutionPlan(plan) {
|
|
|
177
178
|
lines.push(formatParallelAnalysis(parallelAnalysis));
|
|
178
179
|
lines.push('');
|
|
179
180
|
}
|
|
181
|
+
const allScenarios = plan.taskPlans
|
|
182
|
+
.filter(tp => tp.task.acceptanceCriteria && tp.task.acceptanceCriteria.length > 0)
|
|
183
|
+
.flatMap(tp => tp.task.acceptanceCriteria);
|
|
184
|
+
if (allScenarios.length > 0) {
|
|
185
|
+
lines.push('### Acceptance Criteria (Gherkin)');
|
|
186
|
+
lines.push('');
|
|
187
|
+
lines.push('Each task MUST satisfy its acceptance criteria before marking as passed.');
|
|
188
|
+
lines.push('');
|
|
189
|
+
for (const tp of plan.taskPlans) {
|
|
190
|
+
if (!tp.task.acceptanceCriteria || tp.task.acceptanceCriteria.length === 0)
|
|
191
|
+
continue;
|
|
192
|
+
lines.push(`**${tp.task.name}** (${tp.task.id}):`);
|
|
193
|
+
for (const scenario of tp.task.acceptanceCriteria) {
|
|
194
|
+
lines.push(` - Scenario: ${scenario.scenario}`);
|
|
195
|
+
for (const g of scenario.given)
|
|
196
|
+
lines.push(` - Given ${g}`);
|
|
197
|
+
for (const w of scenario.when)
|
|
198
|
+
lines.push(` - When ${w}`);
|
|
199
|
+
for (const t of scenario.then)
|
|
200
|
+
lines.push(` - Then ${t}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push('');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const basePathForDetect = plan.taskPlans[0]?.task?.files?.[0]
|
|
206
|
+
? path.dirname(plan.taskPlans[0].task.files[0])
|
|
207
|
+
: undefined;
|
|
208
|
+
if (allScenarios.length > 0 && basePathForDetect) {
|
|
209
|
+
try {
|
|
210
|
+
const bdd = detectBDDFramework(basePathForDetect);
|
|
211
|
+
if (bdd.detected) {
|
|
212
|
+
lines.push('### BDD Framework Detected');
|
|
213
|
+
lines.push('');
|
|
214
|
+
lines.push(`- Framework: ${bdd.framework}`);
|
|
215
|
+
if (bdd.testCommand)
|
|
216
|
+
lines.push(`- Test command: \`${bdd.testCommand}\``);
|
|
217
|
+
if (bdd.featureDir)
|
|
218
|
+
lines.push(`- Feature directory: \`${bdd.featureDir}\``);
|
|
219
|
+
lines.push('');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
180
225
|
lines.push('### Instructions');
|
|
181
226
|
lines.push('');
|
|
182
227
|
lines.push('For each task above, delegate using `Task(<agent_tier>)` with the specified mask.');
|
|
@@ -254,6 +299,9 @@ export async function runAIVerification(options) {
|
|
|
254
299
|
case 'accessibility':
|
|
255
300
|
result = await runAccessibilityVerification(options);
|
|
256
301
|
break;
|
|
302
|
+
case 'gherkin':
|
|
303
|
+
result = await runGherkinVerification(options);
|
|
304
|
+
break;
|
|
257
305
|
default:
|
|
258
306
|
continue;
|
|
259
307
|
}
|
|
@@ -293,6 +341,7 @@ function getVerificationLayers(options) {
|
|
|
293
341
|
{ name: 'Screenshot', order: 6, type: 'visual', enabled: options.enableScreenshots ?? false },
|
|
294
342
|
{ name: 'APICheck', order: 7, type: 'api', enabled: !!options.devServerUrl },
|
|
295
343
|
{ name: 'Accessibility', order: 8, type: 'accessibility', enabled: false },
|
|
344
|
+
{ name: 'GherkinAcceptance', order: 9, type: 'gherkin', enabled: (options.gherkinScenarios?.length ?? 0) > 0 },
|
|
296
345
|
];
|
|
297
346
|
}
|
|
298
347
|
async function runShellCommand(cmd, cwd, options) {
|
|
@@ -606,6 +655,62 @@ async function runAccessibilityVerification(options) {
|
|
|
606
655
|
logs.push('[A11y] Would run axe-core accessibility scan');
|
|
607
656
|
return { passed: true, logs, layer: 'Accessibility', duration: 0 };
|
|
608
657
|
}
|
|
658
|
+
async function runGherkinVerification(options) {
|
|
659
|
+
const startTime = Date.now();
|
|
660
|
+
const scenarios = options.gherkinScenarios || [];
|
|
661
|
+
const logs = [];
|
|
662
|
+
if (scenarios.length === 0) {
|
|
663
|
+
return { passed: true, logs: ['[Gherkin] No scenarios to verify'], layer: 'GherkinAcceptance', duration: 0 };
|
|
664
|
+
}
|
|
665
|
+
logs.push(`[Gherkin] Verifying ${scenarios.length} acceptance scenario(s)...`);
|
|
666
|
+
const bdd = detectBDDFramework(options.projectPath);
|
|
667
|
+
if (bdd.detected && bdd.testCommand) {
|
|
668
|
+
logs.push(`[Gherkin] BDD framework detected: ${bdd.framework}`);
|
|
669
|
+
logs.push(`[Gherkin] Running: ${bdd.testCommand}`);
|
|
670
|
+
const result = await runShellCommand(bdd.testCommand, options.projectPath);
|
|
671
|
+
if (result.stdout) {
|
|
672
|
+
for (const line of result.stdout.split(/\r?\n/).slice(-10)) {
|
|
673
|
+
if (line.trim())
|
|
674
|
+
logs.push(` > ${line}`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (result.exitCode === 0) {
|
|
678
|
+
logs.push(`[Gherkin] BDD tests passed`);
|
|
679
|
+
return { passed: true, logs, layer: 'GherkinAcceptance', duration: result.durationMs };
|
|
680
|
+
}
|
|
681
|
+
logs.push(`[Gherkin] BDD tests failed (exit=${result.exitCode})`);
|
|
682
|
+
if (result.stderr) {
|
|
683
|
+
for (const line of result.stderr.split(/\r?\n/).slice(-5)) {
|
|
684
|
+
if (line.trim())
|
|
685
|
+
logs.push(` > ${line}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return {
|
|
689
|
+
passed: false,
|
|
690
|
+
error: `${scenarios.length} Gherkin scenario(s) failed BDD verification`,
|
|
691
|
+
logs,
|
|
692
|
+
layer: 'GherkinAcceptance',
|
|
693
|
+
duration: result.durationMs,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
logs.push(`[Gherkin] No BDD framework detected. Generating AI verification checklist.`);
|
|
697
|
+
logs.push('');
|
|
698
|
+
logs.push('## Acceptance Scenarios to Verify:');
|
|
699
|
+
logs.push('');
|
|
700
|
+
for (const scenario of scenarios) {
|
|
701
|
+
logs.push(`**Scenario: ${scenario.scenario}**`);
|
|
702
|
+
for (const g of scenario.given)
|
|
703
|
+
logs.push(` Given ${g}`);
|
|
704
|
+
for (const w of scenario.when)
|
|
705
|
+
logs.push(` When ${w}`);
|
|
706
|
+
for (const t of scenario.then)
|
|
707
|
+
logs.push(` Then ${t}`);
|
|
708
|
+
logs.push('');
|
|
709
|
+
}
|
|
710
|
+
logs.push('[Gherkin] AI should verify each scenario by examining the implementation.');
|
|
711
|
+
logs.push('[Gherkin] If any "Then" assertion is not satisfied, report failure.');
|
|
712
|
+
return { passed: true, logs, layer: 'GherkinAcceptance', duration: Date.now() - startTime };
|
|
713
|
+
}
|
|
609
714
|
export function generateVerificationReport(results) {
|
|
610
715
|
const lines = [
|
|
611
716
|
'## AI Verification Results\n',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getPhaseManager } from '../phase-manager.js';
|
|
2
2
|
import { getEffectiveGdcConfig, runGdcMachineCommand, getGraphNodeIds, getGraphEdges, countGdcCheckIssues, } from '../gdc.js';
|
|
3
3
|
import { generateOpenSpecArtifacts, ensureOpenSpecWorkspace } from './openspec.js';
|
|
4
|
+
import { generateGherkinForPhase, generateGherkinForTask, writeAllFeatureFiles, } from '../gherkin.js';
|
|
4
5
|
const PHASE_SIZE_GUIDE = {
|
|
5
6
|
tooSmall: ['변수명 변경', '오타 수정'],
|
|
6
7
|
justRight: ['UI 컴포넌트 하나', '저장 기능', 'API 엔드포인트 하나'],
|
|
@@ -406,6 +407,9 @@ export async function plan(options) {
|
|
|
406
407
|
: undefined;
|
|
407
408
|
const shardScope = shardOriginalPhases.map(phase => phase.name).join(', ');
|
|
408
409
|
const shardHours = shardOriginalPhases.reduce((sum, phase) => sum + (phase.estimatedHours || 3), 0);
|
|
410
|
+
for (const phase of shardOriginalPhases) {
|
|
411
|
+
phase.acceptanceCriteria = generateGherkinForPhase(phase);
|
|
412
|
+
}
|
|
409
413
|
const shardPlan = await manager.createPlan({
|
|
410
414
|
planName: shardPlanName,
|
|
411
415
|
projectName,
|
|
@@ -490,6 +494,22 @@ export async function plan(options) {
|
|
|
490
494
|
}
|
|
491
495
|
catch {
|
|
492
496
|
}
|
|
497
|
+
for (const phase of weavePlan.phases) {
|
|
498
|
+
if (!phase.acceptanceCriteria || phase.acceptanceCriteria.length === 0) {
|
|
499
|
+
phase.acceptanceCriteria = generateGherkinForPhase(phase);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
const featurePaths = await writeAllFeatureFiles(basePath || process.cwd(), weavePlan.phases);
|
|
504
|
+
for (const [phaseId, featurePath] of featurePaths) {
|
|
505
|
+
const phase = weavePlan.phases.find(p => p.id === phaseId);
|
|
506
|
+
if (phase)
|
|
507
|
+
phase.featurePath = featurePath;
|
|
508
|
+
}
|
|
509
|
+
await manager.savePlan(weavePlan);
|
|
510
|
+
}
|
|
511
|
+
catch {
|
|
512
|
+
}
|
|
493
513
|
const summary = generatePlanSummary(weavePlan, estimatedTotalHours);
|
|
494
514
|
return {
|
|
495
515
|
plan: weavePlan,
|
|
@@ -508,6 +528,8 @@ function generateDefaultPhaseTasks(phase, gdc) {
|
|
|
508
528
|
.flatMap(nodeId => gdc?.nodeFileMap?.get(nodeId) || [])
|
|
509
529
|
.filter(Boolean)
|
|
510
530
|
.slice(0, 8);
|
|
531
|
+
const implCriteria = generateGherkinForTask({ id: `${baseId}-T1`, name: `${title} 구현`, testCase: phase.doneWhen }, phase);
|
|
532
|
+
const testCriteria = generateGherkinForTask({ id: `${baseId}-T2`, name: `${title} 테스트 추가/수정`, testCase: '관련 테스트가 통과한다' }, phase);
|
|
511
533
|
return [
|
|
512
534
|
{
|
|
513
535
|
id: `${baseId}-T1`,
|
|
@@ -523,6 +545,7 @@ function generateDefaultPhaseTasks(phase, gdc) {
|
|
|
523
545
|
`phase:${phase.id}`,
|
|
524
546
|
`done_when:${phase.doneWhen}`,
|
|
525
547
|
],
|
|
548
|
+
acceptanceCriteria: [implCriteria],
|
|
526
549
|
maxRetries: 3,
|
|
527
550
|
},
|
|
528
551
|
{
|
|
@@ -538,6 +561,7 @@ function generateDefaultPhaseTasks(phase, gdc) {
|
|
|
538
561
|
{ kind: 'command', value: 'gdc check --machine' },
|
|
539
562
|
],
|
|
540
563
|
acceptanceRefs: [`phase:${phase.id}:tests`],
|
|
564
|
+
acceptanceCriteria: [testCriteria],
|
|
541
565
|
maxRetries: 2,
|
|
542
566
|
},
|
|
543
567
|
{
|