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.
@@ -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(`### Documents Analyzed`);
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
- lines.push('To proceed, agree to structural changes via `weave command=approve`.');
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.8";
1
+ export declare const VERSION = "0.9.9";
2
2
  export declare function getVersionString(): string;
package/dist/version.js CHANGED
@@ -1,4 +1,4 @@
1
- export const VERSION = '0.9.8';
1
+ export const VERSION = '0.9.9';
2
2
  export function getVersionString() {
3
3
  return `Maskweaver v${VERSION}`;
4
4
  }
@@ -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 { WeaveEvent, EnvironmentAnalysis, MapResult, StructuralChange, ConsentPrompt } 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
- }
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
- export interface InterviewOptions {
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
- const change = {
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
- function generateInterviewQuestions(features, techReqs, map) {
234
- const questions = generateQuestions(features, techReqs);
235
- if (map && map.structuralIssues.length > 0) {
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
- phase.acceptanceCriteria = generateGherkinForPhase(phase);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "maskweaver",
3
- "version": "0.9.8",
3
+ "version": "0.10.0",
4
4
  "description": "AI Expert Persona System - Give your AI coding assistant expert personalities (가면술사)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",