maskweaver 0.9.8 → 0.10.0
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/plugin/index.js +66 -0
- 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
package/dist/plugin/index.js
CHANGED
|
@@ -594,7 +594,73 @@ function playCompletionSound(config) {
|
|
|
594
594
|
}
|
|
595
595
|
}
|
|
596
596
|
let state = null;
|
|
597
|
+
const OPENCODE_PACKAGES_DIR = path.join(os.homedir(), '.cache', 'opencode', 'packages');
|
|
598
|
+
const MASKWEAVER_PACKAGE_GLOB = 'maskweaver@*';
|
|
599
|
+
function getCacheVersion() {
|
|
600
|
+
if (!fs.existsSync(OPENCODE_PACKAGES_DIR))
|
|
601
|
+
return null;
|
|
602
|
+
const entries = fs.readdirSync(OPENCODE_PACKAGES_DIR);
|
|
603
|
+
for (const entry of entries) {
|
|
604
|
+
if (!entry.startsWith('maskweaver@'))
|
|
605
|
+
continue;
|
|
606
|
+
const pkgDir = path.join(OPENCODE_PACKAGES_DIR, entry, 'node_modules', 'maskweaver');
|
|
607
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
608
|
+
if (!fs.existsSync(pkgJsonPath))
|
|
609
|
+
continue;
|
|
610
|
+
try {
|
|
611
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
612
|
+
return { version: pkg.version, pkgDir: path.join(OPENCODE_PACKAGES_DIR, entry) };
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
function getLatestNpmVersion() {
|
|
621
|
+
try {
|
|
622
|
+
const result = spawnSync('npm', ['view', 'maskweaver', 'version'], {
|
|
623
|
+
encoding: 'utf-8',
|
|
624
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
625
|
+
timeout: 10000,
|
|
626
|
+
windowsHide: true,
|
|
627
|
+
});
|
|
628
|
+
if (result.status === 0 && result.stdout) {
|
|
629
|
+
return result.stdout.trim();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
catch { }
|
|
633
|
+
return null;
|
|
634
|
+
}
|
|
635
|
+
function checkAndInvalidateCache() {
|
|
636
|
+
const cached = getCacheVersion();
|
|
637
|
+
if (!cached) {
|
|
638
|
+
return { invalidated: false, cachedVersion: null, latestVersion: null };
|
|
639
|
+
}
|
|
640
|
+
if (cached.version === VERSION) {
|
|
641
|
+
return { invalidated: false, cachedVersion: cached.version, latestVersion: null };
|
|
642
|
+
}
|
|
643
|
+
const latest = getLatestNpmVersion();
|
|
644
|
+
if (latest && latest !== cached.version) {
|
|
645
|
+
try {
|
|
646
|
+
fs.rmSync(cached.pkgDir, { recursive: true, force: true });
|
|
647
|
+
return { invalidated: true, cachedVersion: cached.version, latestVersion: latest };
|
|
648
|
+
}
|
|
649
|
+
catch {
|
|
650
|
+
return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { invalidated: false, cachedVersion: cached.version, latestVersion: latest };
|
|
654
|
+
}
|
|
597
655
|
export const MaskweaverPlugin = async ({ client, directory, project, worktree, $, serverUrl }) => {
|
|
656
|
+
const cacheCheck = checkAndInvalidateCache();
|
|
657
|
+
if (cacheCheck.invalidated) {
|
|
658
|
+
pluginLog(client, 'warn', `Stale plugin cache detected (v${cacheCheck.cachedVersion}). Cleared — v${cacheCheck.latestVersion} will install on next restart.`);
|
|
659
|
+
pluginLog(client, 'warn', `Please restart OpenCode to activate maskweaver v${VERSION}.`);
|
|
660
|
+
}
|
|
661
|
+
else if (cacheCheck.cachedVersion && cacheCheck.cachedVersion !== VERSION) {
|
|
662
|
+
pluginLog(client, 'info', `Plugin cache v${cacheCheck.cachedVersion} — current is v${VERSION}. Restart recommended.`);
|
|
663
|
+
}
|
|
598
664
|
const pluginConfig = loadPluginConfig(directory, { client, verbose: false });
|
|
599
665
|
const configErrors = validateConfig(pluginConfig);
|
|
600
666
|
if (configErrors.length > 0) {
|
|
@@ -17,7 +17,7 @@ import { preparePhaseExecution, formatExecutionPlan, runAIVerification, generate
|
|
|
17
17
|
import { archiveChange } from '../../weave/stages/archive.js';
|
|
18
18
|
import { generateStatusReport, handleUserResponse } from '../../weave/stages/handoff.js';
|
|
19
19
|
import { analyzeCodebase, runGraphifyAnalysis, readMapResult } from '../../weave/stages/map.js';
|
|
20
|
-
import { interview as interviewStage } from '../../weave/stages/intake.js';
|
|
20
|
+
import { interview as interviewStage, listInterviewStates } from '../../weave/stages/intake.js';
|
|
21
21
|
import { executeBuildLoop, generateBuildState, generateBuildId, loadBuildState } from '../../weave/stages/build.js';
|
|
22
22
|
import { WeaveOrchestrator } from '../../weave/orchestrator.js';
|
|
23
23
|
import { getPhaseManager } from '../../weave/phase-manager.js';
|
|
@@ -1778,8 +1778,22 @@ async function handleMap(args, basePath) {
|
|
|
1778
1778
|
async function handleInterview(args, basePath) {
|
|
1779
1779
|
const docsPath = args.docsPath || 'docs';
|
|
1780
1780
|
const lines = [];
|
|
1781
|
+
const existingStates = listInterviewStates(basePath);
|
|
1782
|
+
const canResume = existingStates.some(s => s.status === 'in_progress');
|
|
1781
1783
|
lines.push('## 💬 Interview');
|
|
1782
1784
|
lines.push('');
|
|
1785
|
+
if (canResume && !args.resumeId) {
|
|
1786
|
+
lines.push('### 📋 Existing Interviews');
|
|
1787
|
+
lines.push('');
|
|
1788
|
+
for (const state of existingStates) {
|
|
1789
|
+
const statusIcon = state.status === 'in_progress' ? '🔄' : '✅';
|
|
1790
|
+
lines.push(`- ${statusIcon} \`${state.id}\` — ${state.rounds} rounds, **${state.status}**`);
|
|
1791
|
+
}
|
|
1792
|
+
lines.push('');
|
|
1793
|
+
lines.push('To resume an interview, use: `weave command=interview resumeId="<id>"`');
|
|
1794
|
+
lines.push('To start fresh: `weave command=interview`');
|
|
1795
|
+
lines.push('');
|
|
1796
|
+
}
|
|
1783
1797
|
const mapResult = await readMapResult(basePath);
|
|
1784
1798
|
if (mapResult) {
|
|
1785
1799
|
lines.push(`📍 Map available: \`${mapResult.mapPath}\``);
|
|
@@ -1797,14 +1811,56 @@ async function handleInterview(args, basePath) {
|
|
|
1797
1811
|
docsPath,
|
|
1798
1812
|
basePath,
|
|
1799
1813
|
mapResult,
|
|
1814
|
+
resumeId: args.resumeId,
|
|
1800
1815
|
});
|
|
1801
|
-
lines.push(
|
|
1816
|
+
lines.push('### 📄 Documents Analyzed');
|
|
1802
1817
|
lines.push(`- ${interviewResult.intake.documents.length} document(s) analyzed`);
|
|
1803
1818
|
lines.push(`- ${interviewResult.intake.features.length} feature(s) identified`);
|
|
1804
1819
|
lines.push('');
|
|
1820
|
+
if (interviewResult.intake.features.length > 0) {
|
|
1821
|
+
lines.push('**Features:**');
|
|
1822
|
+
for (const feature of interviewResult.intake.features) {
|
|
1823
|
+
lines.push(` - ${feature}`);
|
|
1824
|
+
}
|
|
1825
|
+
lines.push('');
|
|
1826
|
+
}
|
|
1827
|
+
if (interviewResult.ambiguityScore) {
|
|
1828
|
+
const score = interviewResult.ambiguityScore;
|
|
1829
|
+
const milestoneIcon = score.milestone === 'ready' ? '✅'
|
|
1830
|
+
: score.milestone === 'refined' ? '🟡'
|
|
1831
|
+
: score.milestone === 'progress' ? '🟠'
|
|
1832
|
+
: '🔴';
|
|
1833
|
+
lines.push('### 📊 Ambiguity Score');
|
|
1834
|
+
lines.push('');
|
|
1835
|
+
lines.push(`**${milestoneIcon} ${(score.overallScore * 100).toFixed(1)}% Ambiguity [${score.milestone.toUpperCase()}]**`);
|
|
1836
|
+
lines.push(`_${score.milestoneDescription}_`);
|
|
1837
|
+
lines.push('');
|
|
1838
|
+
lines.push('| Component | Clarity | Weight |');
|
|
1839
|
+
lines.push('|-----------|---------|--------|');
|
|
1840
|
+
const components = [
|
|
1841
|
+
score.breakdown.goalClarity,
|
|
1842
|
+
score.breakdown.constraintClarity,
|
|
1843
|
+
score.breakdown.successCriteriaClarity,
|
|
1844
|
+
];
|
|
1845
|
+
if (score.breakdown.contextClarity) {
|
|
1846
|
+
components.push(score.breakdown.contextClarity);
|
|
1847
|
+
}
|
|
1848
|
+
for (const comp of components) {
|
|
1849
|
+
const bar = '█'.repeat(Math.round(comp.clarityScore * 10)) + '░'.repeat(10 - Math.round(comp.clarityScore * 10));
|
|
1850
|
+
lines.push(`| ${comp.name} | ${bar} ${(comp.clarityScore * 100).toFixed(0)}% | ${(comp.weight * 100).toFixed(0)}% |`);
|
|
1851
|
+
}
|
|
1852
|
+
lines.push('');
|
|
1853
|
+
lines.push(`**Weakest area:** ${score.weakestArea}`);
|
|
1854
|
+
if (score.nextMilestone) {
|
|
1855
|
+
lines.push(`**Next milestone:** ${score.nextMilestone.label} (<= ${(score.nextMilestone.threshold * 100).toFixed(0)}%) — ${score.nextMilestone.description}`);
|
|
1856
|
+
}
|
|
1857
|
+
lines.push('');
|
|
1858
|
+
lines.push(`**Ready for Seed:** ${score.isReadyForSeed ? '✅ Yes' : '❌ No (ambiguity > 20%)'}`);
|
|
1859
|
+
lines.push(`**Ready for Gherkin:** ${score.readinessForGherkin ? '✅ Yes' : '❌ No (ambiguity > 30%)'}`);
|
|
1860
|
+
lines.push('');
|
|
1861
|
+
}
|
|
1805
1862
|
if (interviewResult.intake.structuralChanges && interviewResult.intake.structuralChanges.length > 0) {
|
|
1806
1863
|
lines.push('### ⚠️ Structural Changes Detected');
|
|
1807
|
-
lines.push('The following structural changes were identified from codebase analysis:');
|
|
1808
1864
|
lines.push('');
|
|
1809
1865
|
for (const sc of interviewResult.intake.structuralChanges) {
|
|
1810
1866
|
const icon = sc.breaking ? '🔴' : '🟡';
|
|
@@ -1814,13 +1870,73 @@ async function handleInterview(args, basePath) {
|
|
|
1814
1870
|
lines.push(` - Status: ${sc.agreed ? '✅ Agreed' : '⏳ Pending approval'}`);
|
|
1815
1871
|
}
|
|
1816
1872
|
lines.push('');
|
|
1817
|
-
|
|
1873
|
+
}
|
|
1874
|
+
if (interviewResult.generatedScenarios && interviewResult.generatedScenarios.length > 0) {
|
|
1875
|
+
lines.push('### 🥒 Generated Gherkin Scenarios');
|
|
1818
1876
|
lines.push('');
|
|
1877
|
+
const byFeature = new Map();
|
|
1878
|
+
for (const scenario of interviewResult.generatedScenarios) {
|
|
1879
|
+
const existing = byFeature.get(scenario.feature) || [];
|
|
1880
|
+
existing.push(scenario);
|
|
1881
|
+
byFeature.set(scenario.feature, existing);
|
|
1882
|
+
}
|
|
1883
|
+
for (const [feature, scenarios] of byFeature) {
|
|
1884
|
+
lines.push(`**Feature: ${feature}**`);
|
|
1885
|
+
lines.push('');
|
|
1886
|
+
for (const scenario of scenarios) {
|
|
1887
|
+
lines.push(` \`\`\`gherkin`);
|
|
1888
|
+
lines.push(` Scenario: ${scenario.scenario}`);
|
|
1889
|
+
for (const g of scenario.given)
|
|
1890
|
+
lines.push(` Given ${g}`);
|
|
1891
|
+
for (const w of scenario.when)
|
|
1892
|
+
lines.push(` When ${w}`);
|
|
1893
|
+
for (const t of scenario.then)
|
|
1894
|
+
lines.push(` Then ${t}`);
|
|
1895
|
+
lines.push(` \`\`\``);
|
|
1896
|
+
lines.push('');
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (interviewResult.intake.questions.length > 0) {
|
|
1901
|
+
lines.push('### ❓ Questions');
|
|
1902
|
+
lines.push('');
|
|
1903
|
+
lines.push('Answer the following questions to reduce ambiguity and generate better Gherkin scenarios:');
|
|
1904
|
+
lines.push('');
|
|
1905
|
+
for (const q of interviewResult.intake.questions) {
|
|
1906
|
+
const typeLabel = q.questionType === 'gherkin-given' ? '[GIVEN]'
|
|
1907
|
+
: q.questionType === 'gherkin-when' ? '[WHEN]'
|
|
1908
|
+
: q.questionType === 'gherkin-then' ? '[THEN]'
|
|
1909
|
+
: q.questionType === 'edge-case' ? '[EDGE]'
|
|
1910
|
+
: q.questionType === 'constraint' ? '[CONSTRAINT]'
|
|
1911
|
+
: '';
|
|
1912
|
+
lines.push(`**Q${q.id.replace('Q', '')}:** ${typeLabel ? typeLabel + ' ' : ''}${q.question}`);
|
|
1913
|
+
if (q.targetFeature) {
|
|
1914
|
+
lines.push(` → Target: _${q.targetFeature}_`);
|
|
1915
|
+
}
|
|
1916
|
+
if (q.options && q.options.length > 0) {
|
|
1917
|
+
lines.push(` Options: ${q.options.join(' | ')}`);
|
|
1918
|
+
}
|
|
1919
|
+
if (q.required) {
|
|
1920
|
+
lines.push(` ⚠️ _Required_`);
|
|
1921
|
+
}
|
|
1922
|
+
lines.push('');
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (interviewResult.isMultiRound) {
|
|
1926
|
+
lines.push('---');
|
|
1927
|
+
lines.push('');
|
|
1928
|
+
lines.push(`**Interview Round ${(interviewResult.interviewState?.currentRound || 1)} complete.**`);
|
|
1929
|
+
lines.push('More questions exist to refine requirements. Answer the remaining questions above and run `weave command=interview` again.');
|
|
1930
|
+
lines.push(`Interview ID: \`${interviewResult.interviewState?.interviewId}\``);
|
|
1931
|
+
lines.push('');
|
|
1932
|
+
}
|
|
1933
|
+
else if (interviewResult.satisfied) {
|
|
1934
|
+
lines.push('---');
|
|
1935
|
+
lines.push('');
|
|
1936
|
+
lines.push('✅ **Requirements are clear. Ready to generate specification and plan.**');
|
|
1937
|
+
lines.push('');
|
|
1938
|
+
lines.push('Next: `weave command=design docsPath="docs/"` to create the plan with full Gherkin acceptance criteria.');
|
|
1819
1939
|
}
|
|
1820
|
-
lines.push('### Questions');
|
|
1821
|
-
lines.push('Review the features and technical requirements above.');
|
|
1822
|
-
lines.push('');
|
|
1823
|
-
lines.push('Next: `weave command=design docsPath="docs/"` to create the plan.');
|
|
1824
1940
|
}
|
|
1825
1941
|
catch (e) {
|
|
1826
1942
|
return `Error during interview: ${e.message || e}`;
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.9.
|
|
1
|
+
export declare const VERSION = "0.9.9";
|
|
2
2
|
export declare function getVersionString(): string;
|
package/dist/version.js
CHANGED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GherkinScenario } from '../types.js';
|
|
2
|
+
import type { AmbiguityScore, IntakeResult } from './intake-types.js';
|
|
3
|
+
export declare const AMBIGUITY_THRESHOLD = 0.2;
|
|
4
|
+
export declare const GHERKIN_READINESS_THRESHOLD = 0.3;
|
|
5
|
+
export declare function scoreAmbiguity(features: string[], techReqs: IntakeResult['technicalRequirements'], answers: Record<string, string>, gherkinScenarios?: GherkinScenario[], isBrownfield?: boolean, codebaseMapAvailable?: boolean): AmbiguityScore;
|
|
@@ -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>;
|
|
@@ -2,6 +2,9 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { analyzeEnvironment } from '../environment/index.js';
|
|
4
4
|
import { readMapResult } from './map.js';
|
|
5
|
+
export { scoreAmbiguity } from './intake-ambiguity.js';
|
|
6
|
+
export { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers, } from './intake-questions.js';
|
|
7
|
+
export { saveInterviewState, loadInterviewState, listInterviewStates, getInterviewDir, } from './intake-persistence.js';
|
|
5
8
|
const DOC_EXTENSIONS = ['.md', '.txt', '.yaml', '.yml', '.json'];
|
|
6
9
|
const INDEX_FILES = ['index.md', 'README.md', 'readme.md'];
|
|
7
10
|
function discoverDocuments(basePath) {
|
|
@@ -44,18 +47,8 @@ function analyzeDocument(filePath) {
|
|
|
44
47
|
for (const bullet of bulletMatches.slice(0, 10)) {
|
|
45
48
|
keyPoints.push(bullet.replace(/^[-*]\s+/, ''));
|
|
46
49
|
}
|
|
47
|
-
return {
|
|
48
|
-
path: filePath,
|
|
49
|
-
title,
|
|
50
|
-
sections,
|
|
51
|
-
keyPoints,
|
|
52
|
-
};
|
|
50
|
+
return { path: filePath, title, sections, keyPoints };
|
|
53
51
|
}
|
|
54
|
-
const FEATURE_PATTERNS = [
|
|
55
|
-
/(?:기능|feature|functionality)[::]\s*(.+)/gi,
|
|
56
|
-
/(?:할 수 있다|can|should|must)\s+(.+)/gi,
|
|
57
|
-
/(?:구현|implement|build|create)\s+(.+)/gi,
|
|
58
|
-
];
|
|
59
52
|
function extractFeatures(documents) {
|
|
60
53
|
const features = new Set();
|
|
61
54
|
for (const doc of documents) {
|
|
@@ -105,38 +98,6 @@ function detectTechnicalRequirements(documents) {
|
|
|
105
98
|
}
|
|
106
99
|
return result;
|
|
107
100
|
}
|
|
108
|
-
function generateQuestions(features, techReqs) {
|
|
109
|
-
const questions = [];
|
|
110
|
-
let qId = 1;
|
|
111
|
-
if (!techReqs.frontend || techReqs.frontend.length === 0) {
|
|
112
|
-
questions.push({
|
|
113
|
-
id: `Q${qId++}`,
|
|
114
|
-
topic: '프론트엔드 기술',
|
|
115
|
-
question: '프론트엔드 프레임워크 선호도가 있으신가요?',
|
|
116
|
-
options: ['React', 'Vue', 'Next.js', 'Vanilla JS', '상관없음'],
|
|
117
|
-
required: true,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if (!techReqs.database || techReqs.database.length === 0) {
|
|
121
|
-
questions.push({
|
|
122
|
-
id: `Q${qId++}`,
|
|
123
|
-
topic: '데이터 저장',
|
|
124
|
-
question: '데이터를 어디에 저장할까요?',
|
|
125
|
-
options: ['로컬 스토리지 (오프라인)', '서버 DB (PostgreSQL/MySQL)', '클라우드 (Supabase/Firebase)'],
|
|
126
|
-
required: true,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (features.length > 3) {
|
|
130
|
-
questions.push({
|
|
131
|
-
id: `Q${qId++}`,
|
|
132
|
-
topic: '우선순위',
|
|
133
|
-
question: '가장 먼저 완성해야 하는 기능은 무엇인가요?',
|
|
134
|
-
options: features.slice(0, 5),
|
|
135
|
-
required: true,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
return questions;
|
|
139
|
-
}
|
|
140
101
|
async function searchSimilarProjects(features, techStack) {
|
|
141
102
|
const results = [];
|
|
142
103
|
try {
|
|
@@ -147,33 +108,22 @@ async function searchSimilarProjects(features, techStack) {
|
|
|
147
108
|
console.log('[Intake] Memory database not initialized, skipping similar project search');
|
|
148
109
|
return results;
|
|
149
110
|
}
|
|
150
|
-
const provider = memoryModule.createProvider({
|
|
151
|
-
type: 'text-only',
|
|
152
|
-
});
|
|
111
|
+
const provider = memoryModule.createProvider({ type: 'text-only' });
|
|
153
112
|
let searchResults = [];
|
|
154
113
|
if (provider) {
|
|
155
114
|
const embeddingResult = await provider.embed([query]);
|
|
156
115
|
const embedding = embeddingResult[0];
|
|
157
|
-
searchResults = memoryModule.hybridSearch(query, embedding, {
|
|
158
|
-
limit: 5,
|
|
159
|
-
minScore: 0.3,
|
|
160
|
-
});
|
|
116
|
+
searchResults = memoryModule.hybridSearch(query, embedding, { limit: 5, minScore: 0.3 });
|
|
161
117
|
}
|
|
162
118
|
else {
|
|
163
119
|
console.log('[Intake] Provider not available, using text search only');
|
|
164
120
|
const textResults = db.searchByText(query, 5);
|
|
165
|
-
searchResults = textResults.map((r) => ({
|
|
166
|
-
chunk: r.chunk,
|
|
167
|
-
score: r.score || 0.5,
|
|
168
|
-
}));
|
|
121
|
+
searchResults = textResults.map((r) => ({ chunk: r.chunk, score: r.score || 0.5 }));
|
|
169
122
|
}
|
|
170
123
|
for (const result of searchResults) {
|
|
171
124
|
const { chunk, score } = result;
|
|
172
125
|
const pathParts = chunk.path.split(/[/\\]/);
|
|
173
|
-
const projectName = pathParts.find((p) => !p.startsWith('.') &&
|
|
174
|
-
p !== 'memory' &&
|
|
175
|
-
p !== 'daily' &&
|
|
176
|
-
!p.endsWith('.md')) || 'Previous Project';
|
|
126
|
+
const projectName = pathParts.find((p) => !p.startsWith('.') && p !== 'memory' && p !== 'daily' && !p.endsWith('.md')) || 'Previous Project';
|
|
177
127
|
const relevantFeatures = features.filter(f => chunk.text.toLowerCase().includes(f.toLowerCase().slice(0, 10)));
|
|
178
128
|
if (relevantFeatures.length > 0 || score > 0.5) {
|
|
179
129
|
results.push({
|
|
@@ -200,7 +150,7 @@ export async function injectMapContext(map, features) {
|
|
|
200
150
|
continue;
|
|
201
151
|
const area = issue.area;
|
|
202
152
|
const promptId = `consent-${area.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
|
|
203
|
-
|
|
153
|
+
structuralChanges.push({
|
|
204
154
|
area,
|
|
205
155
|
currentState: issue.description,
|
|
206
156
|
proposedChange: issue.suggestion,
|
|
@@ -209,8 +159,7 @@ export async function injectMapContext(map, features) {
|
|
|
209
159
|
affectedFiles: issue.affectedFiles,
|
|
210
160
|
breaking: issue.severity === 'critical',
|
|
211
161
|
agreed: false,
|
|
212
|
-
};
|
|
213
|
-
structuralChanges.push(change);
|
|
162
|
+
});
|
|
214
163
|
consentPrompts.push({
|
|
215
164
|
id: promptId,
|
|
216
165
|
topic: area,
|
|
@@ -219,78 +168,15 @@ export async function injectMapContext(map, features) {
|
|
|
219
168
|
rationale: `"${issue.title}" — ${issue.description}`,
|
|
220
169
|
impact: issue.severity === 'critical' ? 'high' : 'medium',
|
|
221
170
|
breaking: issue.severity === 'critical',
|
|
222
|
-
options: [
|
|
223
|
-
'승인 — 지금 수정',
|
|
224
|
-
'승인 — build 단계에서 수정',
|
|
225
|
-
'보류 — 이후 재검토',
|
|
226
|
-
'무시 — 현재 구조 유지',
|
|
227
|
-
],
|
|
171
|
+
options: ['승인 — 지금 수정', '승인 — build 단계에서 수정', '보류 — 이후 재검토', '무시 — 현재 구조 유지'],
|
|
228
172
|
agreed: false,
|
|
229
173
|
});
|
|
230
174
|
}
|
|
231
175
|
return { structuralChanges, consentPrompts };
|
|
232
176
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const critical = map.structuralIssues.filter(i => i.severity === 'critical');
|
|
237
|
-
const warnings = map.structuralIssues.filter(i => i.severity === 'warning');
|
|
238
|
-
if (critical.length > 0) {
|
|
239
|
-
questions.unshift({
|
|
240
|
-
id: 'MAP-CRITICAL',
|
|
241
|
-
topic: '구조적 문제',
|
|
242
|
-
question: `코드베이스에서 ${critical.length}개의 Critical 이슈가 발견되었습니다. 계속 진행할까요?`,
|
|
243
|
-
options: ['이슈를 먼저 해결', '진행하되 build 단계에서 해결', '지금은 무시'],
|
|
244
|
-
required: true,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (warnings.length > 0) {
|
|
248
|
-
questions.push({
|
|
249
|
-
id: 'MAP-WARNINGS',
|
|
250
|
-
topic: '권장 구조 변경',
|
|
251
|
-
question: `${warnings.length}개의 경고가 있습니다. 구조 변경 권장사항을 검토하시겠습니까?`,
|
|
252
|
-
options: ['검토', '나중에 검토', '무시'],
|
|
253
|
-
required: false,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (features.length > 0) {
|
|
258
|
-
questions.push({
|
|
259
|
-
id: 'EXISTING-CODE',
|
|
260
|
-
topic: '기존 코드 활용',
|
|
261
|
-
question: '기존 코드베이스의 구조나 패턴을 유지하면서 구현하시겠습니까?',
|
|
262
|
-
options: ['최대한 기존 구조 유지', '필요시 구조 변경', '새로 작성'],
|
|
263
|
-
required: true,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return questions;
|
|
267
|
-
}
|
|
268
|
-
export async function interview(options) {
|
|
269
|
-
const basePath = options.basePath || process.cwd();
|
|
270
|
-
const intakeResult = await intake({
|
|
271
|
-
docsPath: options.docsPath,
|
|
272
|
-
onEvent: options.onEvent,
|
|
273
|
-
});
|
|
274
|
-
const map = options.mapResult !== undefined
|
|
275
|
-
? options.mapResult
|
|
276
|
-
: await readMapResult(basePath);
|
|
277
|
-
const { structuralChanges, consentPrompts } = await injectMapContext(map, intakeResult.features);
|
|
278
|
-
const enhancedQuestions = generateInterviewQuestions(intakeResult.features, intakeResult.technicalRequirements, map);
|
|
279
|
-
const intakeWithMap = {
|
|
280
|
-
...intakeResult,
|
|
281
|
-
codebaseMapPath: map ? map.mapPath : undefined,
|
|
282
|
-
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
283
|
-
consentPrompts: consentPrompts.length > 0 ? consentPrompts : undefined,
|
|
284
|
-
};
|
|
285
|
-
const hasUnansweredQuestions = enhancedQuestions.length > 0;
|
|
286
|
-
const hasPendingConsent = consentPrompts.length > 0 && consentPrompts.some(cp => !cp.agreed);
|
|
287
|
-
return {
|
|
288
|
-
intake: intakeWithMap,
|
|
289
|
-
agreedStructuralChanges: structuralChanges.filter(sc => sc.agreed),
|
|
290
|
-
userAnswers: {},
|
|
291
|
-
satisfied: !hasUnansweredQuestions && !hasPendingConsent,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
177
|
+
import { scoreAmbiguity } from './intake-ambiguity.js';
|
|
178
|
+
import { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers } from './intake-questions.js';
|
|
179
|
+
import { saveInterviewState, loadInterviewState } from './intake-persistence.js';
|
|
294
180
|
export async function intake(options) {
|
|
295
181
|
const { docsPath } = options;
|
|
296
182
|
const extendedOptions = options;
|
|
@@ -347,6 +233,7 @@ export async function intake(options) {
|
|
|
347
233
|
const { structuralChanges } = mapResult
|
|
348
234
|
? await injectMapContext(mapResult, features)
|
|
349
235
|
: { structuralChanges: [] };
|
|
236
|
+
const baselineAmbiguity = scoreAmbiguity(features, technicalRequirements, {}, undefined, mapResult !== null, mapResult !== null);
|
|
350
237
|
return {
|
|
351
238
|
documents,
|
|
352
239
|
features,
|
|
@@ -357,5 +244,102 @@ export async function intake(options) {
|
|
|
357
244
|
environment,
|
|
358
245
|
codebaseMapPath: mapResult?.mapPath,
|
|
359
246
|
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
247
|
+
ambiguityScore: baselineAmbiguity,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function interview(options) {
|
|
251
|
+
const basePath = options.basePath || process.cwd();
|
|
252
|
+
const now = new Date().toISOString();
|
|
253
|
+
let interviewState = options.resumeId
|
|
254
|
+
? loadInterviewState(basePath, options.resumeId)
|
|
255
|
+
: (!options.userAnswers ? loadInterviewState(basePath) : null);
|
|
256
|
+
const intakeResult = await intake({ docsPath: options.docsPath, onEvent: options.onEvent });
|
|
257
|
+
const map = options.mapResult !== undefined
|
|
258
|
+
? options.mapResult
|
|
259
|
+
: await readMapResult(basePath);
|
|
260
|
+
const { structuralChanges, consentPrompts } = await injectMapContext(map, intakeResult.features);
|
|
261
|
+
const existingAnswers = interviewState
|
|
262
|
+
? Object.assign({}, ...interviewState.rounds.map(r => r.answers))
|
|
263
|
+
: {};
|
|
264
|
+
const roundAnswers = options.userAnswers || {};
|
|
265
|
+
const allAnswers = { ...existingAnswers, ...roundAnswers };
|
|
266
|
+
const isBrownfield = map !== null;
|
|
267
|
+
const doneWhenMap = {};
|
|
268
|
+
for (const feature of intakeResult.features) {
|
|
269
|
+
doneWhenMap[feature] = `유저가 ${feature.toLowerCase().replace(/[을를이가은는]/g, '')}할 수 있다`;
|
|
270
|
+
}
|
|
271
|
+
const generatedScenarios = Object.keys(allAnswers).length > 0
|
|
272
|
+
? generateAcceptanceCriteriaFromAnswers(intakeResult.features, allAnswers, doneWhenMap)
|
|
273
|
+
: undefined;
|
|
274
|
+
const ambiguityScore = scoreAmbiguity(intakeResult.features, intakeResult.technicalRequirements, allAnswers, generatedScenarios, isBrownfield, map !== null);
|
|
275
|
+
const baseQuestions = generateQuestions(intakeResult.features, intakeResult.technicalRequirements);
|
|
276
|
+
const mapQuestions = generateInterviewQuestions(intakeResult.features, intakeResult.technicalRequirements, map);
|
|
277
|
+
const seenIds = new Set();
|
|
278
|
+
const mergedQuestions = [...baseQuestions, ...mapQuestions].filter(q => {
|
|
279
|
+
if (seenIds.has(q.id))
|
|
280
|
+
return false;
|
|
281
|
+
seenIds.add(q.id);
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
let gherkinQuestions = (!options.skipGherkinQuestions && !ambiguityScore.isReadyForSeed)
|
|
285
|
+
? generateGherkinQuestions(intakeResult.features, mergedQuestions, allAnswers, ambiguityScore)
|
|
286
|
+
: [];
|
|
287
|
+
const enhancedQuestions = mergedQuestions.filter(q => !Object.keys(allAnswers).some(k => k.toLowerCase().includes(q.id.toLowerCase())
|
|
288
|
+
|| k.toLowerCase().includes((q.targetFeature || '').toLowerCase())));
|
|
289
|
+
const allQuestions = [...enhancedQuestions, ...gherkinQuestions];
|
|
290
|
+
const interviewId = interviewState?.interviewId
|
|
291
|
+
|| `interview_${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
292
|
+
const roundNumber = interviewState ? interviewState.rounds.length + 1 : 1;
|
|
293
|
+
const currentRound = {
|
|
294
|
+
roundNumber,
|
|
295
|
+
questions: allQuestions,
|
|
296
|
+
answers: roundAnswers,
|
|
297
|
+
ambiguityBefore: interviewState?.rounds[interviewState.rounds.length - 1]?.ambiguityAfter,
|
|
298
|
+
ambiguityAfter: ambiguityScore,
|
|
299
|
+
gherkinGenerated: generatedScenarios,
|
|
300
|
+
timestamp: now,
|
|
301
|
+
};
|
|
302
|
+
if (interviewState) {
|
|
303
|
+
interviewState.rounds.push(currentRound);
|
|
304
|
+
interviewState.currentRound = roundNumber;
|
|
305
|
+
interviewState.status = ambiguityScore.isReadyForSeed ? 'completed' : 'in_progress';
|
|
306
|
+
interviewState.features = intakeResult.features;
|
|
307
|
+
interviewState.isBrownfield = isBrownfield;
|
|
308
|
+
interviewState.updatedAt = now;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
interviewState = {
|
|
312
|
+
interviewId,
|
|
313
|
+
status: ambiguityScore.isReadyForSeed ? 'completed' : 'in_progress',
|
|
314
|
+
initialContext: intakeResult.features.join(', '),
|
|
315
|
+
rounds: [currentRound],
|
|
316
|
+
currentRound: 1,
|
|
317
|
+
features: intakeResult.features,
|
|
318
|
+
isBrownfield,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
saveInterviewState(basePath, interviewState);
|
|
324
|
+
const intakeWithEverything = {
|
|
325
|
+
...intakeResult,
|
|
326
|
+
codebaseMapPath: map ? map.mapPath : undefined,
|
|
327
|
+
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
328
|
+
consentPrompts: consentPrompts.length > 0 ? consentPrompts : undefined,
|
|
329
|
+
ambiguityScore,
|
|
330
|
+
generatedScenarios,
|
|
331
|
+
questions: allQuestions,
|
|
332
|
+
};
|
|
333
|
+
const hasUnansweredQuestions = allQuestions.some(q => q.required);
|
|
334
|
+
const hasPendingConsent = consentPrompts.length > 0 && consentPrompts.some(cp => !cp.agreed);
|
|
335
|
+
return {
|
|
336
|
+
intake: intakeWithEverything,
|
|
337
|
+
agreedStructuralChanges: structuralChanges.filter(sc => sc.agreed),
|
|
338
|
+
userAnswers: allAnswers,
|
|
339
|
+
satisfied: ambiguityScore.isReadyForSeed && !hasUnansweredQuestions && !hasPendingConsent,
|
|
340
|
+
ambiguityScore,
|
|
341
|
+
generatedScenarios,
|
|
342
|
+
interviewState,
|
|
343
|
+
isMultiRound: !ambiguityScore.isReadyForSeed || hasUnansweredQuestions || hasPendingConsent,
|
|
360
344
|
};
|
|
361
345
|
}
|
|
@@ -494,9 +494,19 @@ export async function plan(options) {
|
|
|
494
494
|
}
|
|
495
495
|
catch {
|
|
496
496
|
}
|
|
497
|
+
const intakeScenarios = intake.generatedScenarios;
|
|
497
498
|
for (const phase of weavePlan.phases) {
|
|
498
499
|
if (!phase.acceptanceCriteria || phase.acceptanceCriteria.length === 0) {
|
|
499
|
-
|
|
500
|
+
const matchingScenarios = intakeScenarios
|
|
501
|
+
? intakeScenarios.filter(s => s.feature.toLowerCase().includes(phase.name.toLowerCase())
|
|
502
|
+
|| phase.name.toLowerCase().includes(s.feature.toLowerCase().slice(0, 10)))
|
|
503
|
+
: [];
|
|
504
|
+
if (matchingScenarios.length > 0) {
|
|
505
|
+
phase.acceptanceCriteria = matchingScenarios;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
phase.acceptanceCriteria = generateGherkinForPhase(phase);
|
|
509
|
+
}
|
|
500
510
|
}
|
|
501
511
|
}
|
|
502
512
|
try {
|