maskweaver 0.9.9 β†’ 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.
@@ -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}`;
@@ -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.9",
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",