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.
- package/dist/plugin/tools/weave.js +124 -8
- package/dist/weave/stages/intake-ambiguity.d.ts +5 -0
- package/dist/weave/stages/intake-ambiguity.js +200 -0
- package/dist/weave/stages/intake-persistence.d.ts +9 -0
- package/dist/weave/stages/intake-persistence.js +66 -0
- package/dist/weave/stages/intake-questions.d.ts +6 -0
- package/dist/weave/stages/intake-questions.js +195 -0
- package/dist/weave/stages/intake-types.d.ts +116 -0
- package/dist/weave/stages/intake-types.js +1 -0
- package/dist/weave/stages/intake.d.ts +8 -55
- package/dist/weave/stages/intake.js +112 -128
- package/dist/weave/stages/plan.js +11 -1
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ import { preparePhaseExecution, formatExecutionPlan, runAIVerification, generate
|
|
|
17
17
|
import { archiveChange } from '../../weave/stages/archive.js';
|
|
18
18
|
import { generateStatusReport, handleUserResponse } from '../../weave/stages/handoff.js';
|
|
19
19
|
import { analyzeCodebase, runGraphifyAnalysis, readMapResult } from '../../weave/stages/map.js';
|
|
20
|
-
import { interview as interviewStage } from '../../weave/stages/intake.js';
|
|
20
|
+
import { interview as interviewStage, listInterviewStates } from '../../weave/stages/intake.js';
|
|
21
21
|
import { executeBuildLoop, generateBuildState, generateBuildId, loadBuildState } from '../../weave/stages/build.js';
|
|
22
22
|
import { WeaveOrchestrator } from '../../weave/orchestrator.js';
|
|
23
23
|
import { getPhaseManager } from '../../weave/phase-manager.js';
|
|
@@ -1778,8 +1778,22 @@ async function handleMap(args, basePath) {
|
|
|
1778
1778
|
async function handleInterview(args, basePath) {
|
|
1779
1779
|
const docsPath = args.docsPath || 'docs';
|
|
1780
1780
|
const lines = [];
|
|
1781
|
+
const existingStates = listInterviewStates(basePath);
|
|
1782
|
+
const canResume = existingStates.some(s => s.status === 'in_progress');
|
|
1781
1783
|
lines.push('## π¬ Interview');
|
|
1782
1784
|
lines.push('');
|
|
1785
|
+
if (canResume && !args.resumeId) {
|
|
1786
|
+
lines.push('### π Existing Interviews');
|
|
1787
|
+
lines.push('');
|
|
1788
|
+
for (const state of existingStates) {
|
|
1789
|
+
const statusIcon = state.status === 'in_progress' ? 'π' : 'β
';
|
|
1790
|
+
lines.push(`- ${statusIcon} \`${state.id}\` β ${state.rounds} rounds, **${state.status}**`);
|
|
1791
|
+
}
|
|
1792
|
+
lines.push('');
|
|
1793
|
+
lines.push('To resume an interview, use: `weave command=interview resumeId="<id>"`');
|
|
1794
|
+
lines.push('To start fresh: `weave command=interview`');
|
|
1795
|
+
lines.push('');
|
|
1796
|
+
}
|
|
1783
1797
|
const mapResult = await readMapResult(basePath);
|
|
1784
1798
|
if (mapResult) {
|
|
1785
1799
|
lines.push(`π Map available: \`${mapResult.mapPath}\``);
|
|
@@ -1797,14 +1811,56 @@ async function handleInterview(args, basePath) {
|
|
|
1797
1811
|
docsPath,
|
|
1798
1812
|
basePath,
|
|
1799
1813
|
mapResult,
|
|
1814
|
+
resumeId: args.resumeId,
|
|
1800
1815
|
});
|
|
1801
|
-
lines.push(
|
|
1816
|
+
lines.push('### π Documents Analyzed');
|
|
1802
1817
|
lines.push(`- ${interviewResult.intake.documents.length} document(s) analyzed`);
|
|
1803
1818
|
lines.push(`- ${interviewResult.intake.features.length} feature(s) identified`);
|
|
1804
1819
|
lines.push('');
|
|
1820
|
+
if (interviewResult.intake.features.length > 0) {
|
|
1821
|
+
lines.push('**Features:**');
|
|
1822
|
+
for (const feature of interviewResult.intake.features) {
|
|
1823
|
+
lines.push(` - ${feature}`);
|
|
1824
|
+
}
|
|
1825
|
+
lines.push('');
|
|
1826
|
+
}
|
|
1827
|
+
if (interviewResult.ambiguityScore) {
|
|
1828
|
+
const score = interviewResult.ambiguityScore;
|
|
1829
|
+
const milestoneIcon = score.milestone === 'ready' ? 'β
'
|
|
1830
|
+
: score.milestone === 'refined' ? 'π‘'
|
|
1831
|
+
: score.milestone === 'progress' ? 'π '
|
|
1832
|
+
: 'π΄';
|
|
1833
|
+
lines.push('### π Ambiguity Score');
|
|
1834
|
+
lines.push('');
|
|
1835
|
+
lines.push(`**${milestoneIcon} ${(score.overallScore * 100).toFixed(1)}% Ambiguity [${score.milestone.toUpperCase()}]**`);
|
|
1836
|
+
lines.push(`_${score.milestoneDescription}_`);
|
|
1837
|
+
lines.push('');
|
|
1838
|
+
lines.push('| Component | Clarity | Weight |');
|
|
1839
|
+
lines.push('|-----------|---------|--------|');
|
|
1840
|
+
const components = [
|
|
1841
|
+
score.breakdown.goalClarity,
|
|
1842
|
+
score.breakdown.constraintClarity,
|
|
1843
|
+
score.breakdown.successCriteriaClarity,
|
|
1844
|
+
];
|
|
1845
|
+
if (score.breakdown.contextClarity) {
|
|
1846
|
+
components.push(score.breakdown.contextClarity);
|
|
1847
|
+
}
|
|
1848
|
+
for (const comp of components) {
|
|
1849
|
+
const bar = 'β'.repeat(Math.round(comp.clarityScore * 10)) + 'β'.repeat(10 - Math.round(comp.clarityScore * 10));
|
|
1850
|
+
lines.push(`| ${comp.name} | ${bar} ${(comp.clarityScore * 100).toFixed(0)}% | ${(comp.weight * 100).toFixed(0)}% |`);
|
|
1851
|
+
}
|
|
1852
|
+
lines.push('');
|
|
1853
|
+
lines.push(`**Weakest area:** ${score.weakestArea}`);
|
|
1854
|
+
if (score.nextMilestone) {
|
|
1855
|
+
lines.push(`**Next milestone:** ${score.nextMilestone.label} (<= ${(score.nextMilestone.threshold * 100).toFixed(0)}%) β ${score.nextMilestone.description}`);
|
|
1856
|
+
}
|
|
1857
|
+
lines.push('');
|
|
1858
|
+
lines.push(`**Ready for Seed:** ${score.isReadyForSeed ? 'β
Yes' : 'β No (ambiguity > 20%)'}`);
|
|
1859
|
+
lines.push(`**Ready for Gherkin:** ${score.readinessForGherkin ? 'β
Yes' : 'β No (ambiguity > 30%)'}`);
|
|
1860
|
+
lines.push('');
|
|
1861
|
+
}
|
|
1805
1862
|
if (interviewResult.intake.structuralChanges && interviewResult.intake.structuralChanges.length > 0) {
|
|
1806
1863
|
lines.push('### β οΈ Structural Changes Detected');
|
|
1807
|
-
lines.push('The following structural changes were identified from codebase analysis:');
|
|
1808
1864
|
lines.push('');
|
|
1809
1865
|
for (const sc of interviewResult.intake.structuralChanges) {
|
|
1810
1866
|
const icon = sc.breaking ? 'π΄' : 'π‘';
|
|
@@ -1814,13 +1870,73 @@ async function handleInterview(args, basePath) {
|
|
|
1814
1870
|
lines.push(` - Status: ${sc.agreed ? 'β
Agreed' : 'β³ Pending approval'}`);
|
|
1815
1871
|
}
|
|
1816
1872
|
lines.push('');
|
|
1817
|
-
|
|
1873
|
+
}
|
|
1874
|
+
if (interviewResult.generatedScenarios && interviewResult.generatedScenarios.length > 0) {
|
|
1875
|
+
lines.push('### π₯ Generated Gherkin Scenarios');
|
|
1818
1876
|
lines.push('');
|
|
1877
|
+
const byFeature = new Map();
|
|
1878
|
+
for (const scenario of interviewResult.generatedScenarios) {
|
|
1879
|
+
const existing = byFeature.get(scenario.feature) || [];
|
|
1880
|
+
existing.push(scenario);
|
|
1881
|
+
byFeature.set(scenario.feature, existing);
|
|
1882
|
+
}
|
|
1883
|
+
for (const [feature, scenarios] of byFeature) {
|
|
1884
|
+
lines.push(`**Feature: ${feature}**`);
|
|
1885
|
+
lines.push('');
|
|
1886
|
+
for (const scenario of scenarios) {
|
|
1887
|
+
lines.push(` \`\`\`gherkin`);
|
|
1888
|
+
lines.push(` Scenario: ${scenario.scenario}`);
|
|
1889
|
+
for (const g of scenario.given)
|
|
1890
|
+
lines.push(` Given ${g}`);
|
|
1891
|
+
for (const w of scenario.when)
|
|
1892
|
+
lines.push(` When ${w}`);
|
|
1893
|
+
for (const t of scenario.then)
|
|
1894
|
+
lines.push(` Then ${t}`);
|
|
1895
|
+
lines.push(` \`\`\``);
|
|
1896
|
+
lines.push('');
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
if (interviewResult.intake.questions.length > 0) {
|
|
1901
|
+
lines.push('### β Questions');
|
|
1902
|
+
lines.push('');
|
|
1903
|
+
lines.push('Answer the following questions to reduce ambiguity and generate better Gherkin scenarios:');
|
|
1904
|
+
lines.push('');
|
|
1905
|
+
for (const q of interviewResult.intake.questions) {
|
|
1906
|
+
const typeLabel = q.questionType === 'gherkin-given' ? '[GIVEN]'
|
|
1907
|
+
: q.questionType === 'gherkin-when' ? '[WHEN]'
|
|
1908
|
+
: q.questionType === 'gherkin-then' ? '[THEN]'
|
|
1909
|
+
: q.questionType === 'edge-case' ? '[EDGE]'
|
|
1910
|
+
: q.questionType === 'constraint' ? '[CONSTRAINT]'
|
|
1911
|
+
: '';
|
|
1912
|
+
lines.push(`**Q${q.id.replace('Q', '')}:** ${typeLabel ? typeLabel + ' ' : ''}${q.question}`);
|
|
1913
|
+
if (q.targetFeature) {
|
|
1914
|
+
lines.push(` β Target: _${q.targetFeature}_`);
|
|
1915
|
+
}
|
|
1916
|
+
if (q.options && q.options.length > 0) {
|
|
1917
|
+
lines.push(` Options: ${q.options.join(' | ')}`);
|
|
1918
|
+
}
|
|
1919
|
+
if (q.required) {
|
|
1920
|
+
lines.push(` β οΈ _Required_`);
|
|
1921
|
+
}
|
|
1922
|
+
lines.push('');
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (interviewResult.isMultiRound) {
|
|
1926
|
+
lines.push('---');
|
|
1927
|
+
lines.push('');
|
|
1928
|
+
lines.push(`**Interview Round ${(interviewResult.interviewState?.currentRound || 1)} complete.**`);
|
|
1929
|
+
lines.push('More questions exist to refine requirements. Answer the remaining questions above and run `weave command=interview` again.');
|
|
1930
|
+
lines.push(`Interview ID: \`${interviewResult.interviewState?.interviewId}\``);
|
|
1931
|
+
lines.push('');
|
|
1932
|
+
}
|
|
1933
|
+
else if (interviewResult.satisfied) {
|
|
1934
|
+
lines.push('---');
|
|
1935
|
+
lines.push('');
|
|
1936
|
+
lines.push('β
**Requirements are clear. Ready to generate specification and plan.**');
|
|
1937
|
+
lines.push('');
|
|
1938
|
+
lines.push('Next: `weave command=design docsPath="docs/"` to create the plan with full Gherkin acceptance criteria.');
|
|
1819
1939
|
}
|
|
1820
|
-
lines.push('### Questions');
|
|
1821
|
-
lines.push('Review the features and technical requirements above.');
|
|
1822
|
-
lines.push('');
|
|
1823
|
-
lines.push('Next: `weave command=design docsPath="docs/"` to create the plan.');
|
|
1824
1940
|
}
|
|
1825
1941
|
catch (e) {
|
|
1826
1942
|
return `Error during interview: ${e.message || e}`;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { GherkinScenario } from '../types.js';
|
|
2
|
+
import type { AmbiguityScore, IntakeResult } from './intake-types.js';
|
|
3
|
+
export declare const AMBIGUITY_THRESHOLD = 0.2;
|
|
4
|
+
export declare const GHERKIN_READINESS_THRESHOLD = 0.3;
|
|
5
|
+
export declare function scoreAmbiguity(features: string[], techReqs: IntakeResult['technicalRequirements'], answers: Record<string, string>, gherkinScenarios?: GherkinScenario[], isBrownfield?: boolean, codebaseMapAvailable?: boolean): AmbiguityScore;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
export const AMBIGUITY_THRESHOLD = 0.20;
|
|
2
|
+
export const GHERKIN_READINESS_THRESHOLD = 0.30;
|
|
3
|
+
const GOAL_CLARITY_WEIGHT = 0.40;
|
|
4
|
+
const CONSTRAINT_CLARITY_WEIGHT = 0.30;
|
|
5
|
+
const SUCCESS_CRITERIA_CLARITY_WEIGHT = 0.30;
|
|
6
|
+
const BROWNFIELD_CONTEXT_WEIGHT = 0.15;
|
|
7
|
+
const BROWNFIELD_GOAL_WEIGHT = 0.35;
|
|
8
|
+
const BROWNFIELD_CONSTRAINT_WEIGHT = 0.25;
|
|
9
|
+
const BROWNFIELD_CRITERIA_WEIGHT = 0.25;
|
|
10
|
+
const MILESTONE_DEFINITIONS = [
|
|
11
|
+
{ maxScore: 1.0, label: 'initial', description: 'ν΅μ¬ μꡬμ¬νλ§ νμ
λ¨. μ μ½μ‘°κ±΄κ³Ό μ±κ³΅ κΈ°μ€μ΄ ν° κ³΅λ°±.' },
|
|
12
|
+
{ maxScore: 0.40, label: 'progress', description: 'λλΆλΆ μꡬμ¬ν νμ
. μΌλΆ μΈλΆμ¬νκ³Ό κ²½κ³ μ‘°κ±΄ λλ½.' },
|
|
13
|
+
{ maxScore: 0.30, label: 'refined', description: 'μ±κ³΅ κΈ°μ€ μΌλΆ μ μλ¨. κ²½κ³ μ‘°κ±΄κ³Ό λΉκΈ°λ₯ μꡬμ¬ν λ³΄κ° νμ.' },
|
|
14
|
+
{ maxScore: AMBIGUITY_THRESHOLD, label: 'ready', description: 'λͺ¨λ κΈ°μ€μ΄ ꡬ체μ μ΄κ³ ν
μ€νΈ κ°λ₯. Seed μμ± μ€λΉ μλ£.' },
|
|
15
|
+
];
|
|
16
|
+
function getMilestone(score) {
|
|
17
|
+
for (const m of MILESTONE_DEFINITIONS) {
|
|
18
|
+
if (score <= m.maxScore)
|
|
19
|
+
return { label: m.label, description: m.description };
|
|
20
|
+
}
|
|
21
|
+
return { label: 'initial', description: MILESTONE_DEFINITIONS[0].description };
|
|
22
|
+
}
|
|
23
|
+
function getNextMilestone(score) {
|
|
24
|
+
for (let i = MILESTONE_DEFINITIONS.length - 1; i >= 0; i--) {
|
|
25
|
+
const m = MILESTONE_DEFINITIONS[i];
|
|
26
|
+
if (score > m.maxScore) {
|
|
27
|
+
return { threshold: m.maxScore, label: m.label, description: m.description };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function buildJustification(category, score) {
|
|
33
|
+
const pct = (score * 100).toFixed(0);
|
|
34
|
+
switch (category) {
|
|
35
|
+
case 'goal':
|
|
36
|
+
if (score >= 0.70)
|
|
37
|
+
return `λͺ©νκ° λͺ
ννκ³ Gherkin μλ리μ€λ‘ λ³ν κ°λ₯ (${pct}%)`;
|
|
38
|
+
if (score >= 0.40)
|
|
39
|
+
return `νΉμ§μ΄ μλ³λμμΌλ μΈλΆμ¬ν λΆμ‘± (${pct}%)`;
|
|
40
|
+
return `λͺ©ν μ μκ° λΆμΆ©λΆ. λ λ§μ μ§λ¬Έ νμ (${pct}%)`;
|
|
41
|
+
case 'constraint':
|
|
42
|
+
if (score >= 0.60)
|
|
43
|
+
return `μ μ½μ‘°κ±΄μ΄ μ μ μλ¨ (${pct}%)`;
|
|
44
|
+
if (score >= 0.30)
|
|
45
|
+
return `κΈ°μ μ€νμ νμ
λμμΌλ κ²½κ³ μ‘°κ±΄ μ μ νμ (${pct}%)`;
|
|
46
|
+
return `μ μ½μ‘°κ±΄ μ μκ° νμν¨ (${pct}%)`;
|
|
47
|
+
case 'criteria':
|
|
48
|
+
if (score >= 0.70)
|
|
49
|
+
return `μ±κ³΅ κΈ°μ€μ΄ Gherkin μλ리μ€λ‘ μΆ©λΆν μ μλ¨ (${pct}%)`;
|
|
50
|
+
if (score >= 0.30)
|
|
51
|
+
return `μΌλΆ μΈμ 쑰건 μ‘΄μ¬νλ λ³΄κ° νμ (${pct}%)`;
|
|
52
|
+
return `μΈμ μ‘°κ±΄μ΄ μ μλμ§ μμ (${pct}%)`;
|
|
53
|
+
case 'context':
|
|
54
|
+
if (score >= 0.60)
|
|
55
|
+
return `κΈ°μ‘΄ μ½λλ² μ΄μ€ λ§₯λ½μ΄ μΆ©λΆν νμ
λ¨ (${pct}%)`;
|
|
56
|
+
if (score >= 0.30)
|
|
57
|
+
return `μ½λλ² μ΄μ€ λ§΅μ μμΌλ μΈλΆ μ΄ν΄ λΆμ‘± (${pct}%)`;
|
|
58
|
+
return `κΈ°μ‘΄ μ½λλ² μ΄μ€ λ§₯λ½ νμ
μ΄ νμν¨ (${pct}%)`;
|
|
59
|
+
default:
|
|
60
|
+
return `Clarity: ${pct}%`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function scoreAmbiguity(features, techReqs, answers, gherkinScenarios, isBrownfield = false, codebaseMapAvailable = false) {
|
|
64
|
+
const answerCount = Object.keys(answers).length;
|
|
65
|
+
const answerDepth = Object.values(answers).reduce((sum, a) => sum + Math.min(a.length / 100, 1.0), 0);
|
|
66
|
+
const hasFeatures = features.length > 0;
|
|
67
|
+
const featureDetailScore = hasFeatures
|
|
68
|
+
? Math.min(features.reduce((sum, f) => sum + Math.min(f.length / 30, 1.0), 0) / Math.max(features.length, 1), 1.0)
|
|
69
|
+
: 0;
|
|
70
|
+
let goalClarityScore = 0.0;
|
|
71
|
+
if (hasFeatures)
|
|
72
|
+
goalClarityScore += 0.25;
|
|
73
|
+
if (featureDetailScore > 0.3)
|
|
74
|
+
goalClarityScore += 0.15;
|
|
75
|
+
if (answerCount >= 3)
|
|
76
|
+
goalClarityScore += 0.15;
|
|
77
|
+
if (answerDepth >= 1.5)
|
|
78
|
+
goalClarityScore += 0.15;
|
|
79
|
+
if (gherkinScenarios && gherkinScenarios.length > 0)
|
|
80
|
+
goalClarityScore += 0.15;
|
|
81
|
+
if (answers && (answers['priority'] || Object.keys(answers).some(k => k.includes('μ°μ μμ') || k.includes('priority')))) {
|
|
82
|
+
goalClarityScore += 0.15;
|
|
83
|
+
}
|
|
84
|
+
let constraintClarityScore = 0.0;
|
|
85
|
+
const hasTechStack = (techReqs?.frontend?.length ?? 0) > 0 || (techReqs?.backend?.length ?? 0) > 0;
|
|
86
|
+
if (hasTechStack)
|
|
87
|
+
constraintClarityScore += 0.30;
|
|
88
|
+
if ((techReqs?.database?.length ?? 0) > 0)
|
|
89
|
+
constraintClarityScore += 0.15;
|
|
90
|
+
if (answerCount >= 5)
|
|
91
|
+
constraintClarityScore += 0.15;
|
|
92
|
+
if (answers && Object.keys(answers).some(k => k.includes('μμΈ') || k.includes('edge') || k.includes('μλ¬'))) {
|
|
93
|
+
constraintClarityScore += 0.20;
|
|
94
|
+
}
|
|
95
|
+
if (answers && Object.keys(answers).some(k => k.includes('μ μ½') || k.includes('constraint') || k.includes('μ ν'))) {
|
|
96
|
+
constraintClarityScore += 0.20;
|
|
97
|
+
}
|
|
98
|
+
let successCriteriaClarityScore = 0.0;
|
|
99
|
+
if (gherkinScenarios && gherkinScenarios.length > 0) {
|
|
100
|
+
const gherkinThenCount = gherkinScenarios.reduce((sum, s) => sum + s.then.length, 0);
|
|
101
|
+
successCriteriaClarityScore += Math.min(gherkinThenCount * 0.10, 0.30);
|
|
102
|
+
if (gherkinScenarios.some(s => s.given.length > 0))
|
|
103
|
+
successCriteriaClarityScore += 0.15;
|
|
104
|
+
if (gherkinScenarios.some(s => s.when.length > 0))
|
|
105
|
+
successCriteriaClarityScore += 0.15;
|
|
106
|
+
if (gherkinScenarios.length >= features.length * 2)
|
|
107
|
+
successCriteriaClarityScore += 0.15;
|
|
108
|
+
}
|
|
109
|
+
if (answerCount >= 7)
|
|
110
|
+
successCriteriaClarityScore += 0.10;
|
|
111
|
+
if (answers && Object.keys(answers).some(k => k.includes('μ±κ³΅') || k.includes('μΈ‘μ ') || k.includes('metric'))) {
|
|
112
|
+
successCriteriaClarityScore += 0.15;
|
|
113
|
+
}
|
|
114
|
+
let contextClarityScore = 0.0;
|
|
115
|
+
if (isBrownfield && codebaseMapAvailable) {
|
|
116
|
+
contextClarityScore += 0.40;
|
|
117
|
+
if (answers && Object.keys(answers).some(k => k.includes('기쑴') || k.includes('existing') || k.includes('ꡬ쑰'))) {
|
|
118
|
+
contextClarityScore += 0.30;
|
|
119
|
+
}
|
|
120
|
+
if (answers && Object.keys(answers).some(k => k.includes('λ§μ΄κ·Έλ μ΄μ
') || k.includes('migration') || k.includes('리ν©ν λ§') || k.includes('refactor'))) {
|
|
121
|
+
contextClarityScore += 0.30;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
goalClarityScore = Math.min(1.0, Math.max(0.0, goalClarityScore));
|
|
125
|
+
constraintClarityScore = Math.min(1.0, Math.max(0.0, constraintClarityScore));
|
|
126
|
+
successCriteriaClarityScore = Math.min(1.0, Math.max(0.0, successCriteriaClarityScore));
|
|
127
|
+
contextClarityScore = Math.min(1.0, Math.max(0.0, contextClarityScore));
|
|
128
|
+
const goalComponent = {
|
|
129
|
+
name: 'Goal Clarity',
|
|
130
|
+
clarityScore: goalClarityScore,
|
|
131
|
+
weight: isBrownfield ? BROWNFIELD_GOAL_WEIGHT : GOAL_CLARITY_WEIGHT,
|
|
132
|
+
justification: buildJustification('goal', goalClarityScore),
|
|
133
|
+
thresholds: [
|
|
134
|
+
{ label: 'Features identified', minScore: 0.25, description: 'νΉμ§μ΄ μλ³λμλκ°?' },
|
|
135
|
+
{ label: 'Features detailed', minScore: 0.40, description: 'νΉμ§μ μΈλΆμ¬νμ΄ μλκ°?' },
|
|
136
|
+
{ label: 'Questions answered', minScore: 0.55, description: 'μΈν°λ·° μ§λ¬Έμ λ΅νλκ°?' },
|
|
137
|
+
{ label: 'Gherkin ready', minScore: 0.70, description: 'Gherkin μλ리μ€λ‘ λ³ν κ°λ₯νκ°?' },
|
|
138
|
+
],
|
|
139
|
+
};
|
|
140
|
+
const constraintComponent = {
|
|
141
|
+
name: 'Constraint Clarity',
|
|
142
|
+
clarityScore: constraintClarityScore,
|
|
143
|
+
weight: isBrownfield ? BROWNFIELD_CONSTRAINT_WEIGHT : CONSTRAINT_CLARITY_WEIGHT,
|
|
144
|
+
justification: buildJustification('constraint', constraintClarityScore),
|
|
145
|
+
thresholds: [
|
|
146
|
+
{ label: 'Tech stack known', minScore: 0.30, description: 'κΈ°μ μ€νμ΄ νμ
λμλκ°?' },
|
|
147
|
+
{ label: 'Edge cases covered', minScore: 0.50, description: 'κ²½κ³ μ‘°κ±΄μ΄ λ€λ£¨μ΄μ‘λκ°?' },
|
|
148
|
+
{ label: 'All constraints clear', minScore: 0.80, description: 'λͺ¨λ μ μ½μ‘°κ±΄μ΄ λͺ
ννκ°?' },
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
const criteriaComponent = {
|
|
152
|
+
name: 'Success Criteria Clarity',
|
|
153
|
+
clarityScore: successCriteriaClarityScore,
|
|
154
|
+
weight: isBrownfield ? BROWNFIELD_CRITERIA_WEIGHT : SUCCESS_CRITERIA_CLARITY_WEIGHT,
|
|
155
|
+
justification: buildJustification('criteria', successCriteriaClarityScore),
|
|
156
|
+
thresholds: [
|
|
157
|
+
{ label: 'Basic AC exists', minScore: 0.30, description: 'κΈ°λ³Έ μΈμ μ‘°κ±΄μ΄ μλκ°?' },
|
|
158
|
+
{ label: 'Gherkin When/Then', minScore: 0.50, description: 'When/Then μλ리μ€κ° μλκ°?' },
|
|
159
|
+
{ label: 'Full scenario coverage', minScore: 0.75, description: 'μΆ©λΆν μλλ¦¬μ€ μ»€λ²λ¦¬μ§κ° μλκ°?' },
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
const breakdown = {
|
|
163
|
+
goalClarity: goalComponent,
|
|
164
|
+
constraintClarity: constraintComponent,
|
|
165
|
+
successCriteriaClarity: criteriaComponent,
|
|
166
|
+
};
|
|
167
|
+
if (isBrownfield && codebaseMapAvailable) {
|
|
168
|
+
breakdown.contextClarity = {
|
|
169
|
+
name: 'Context Clarity',
|
|
170
|
+
clarityScore: contextClarityScore,
|
|
171
|
+
weight: BROWNFIELD_CONTEXT_WEIGHT,
|
|
172
|
+
justification: buildJustification('context', contextClarityScore),
|
|
173
|
+
thresholds: [
|
|
174
|
+
{ label: 'Map available', minScore: 0.30, description: 'μ½λλ² μ΄μ€ λ§΅μ΄ μλκ°?' },
|
|
175
|
+
{ label: 'Patterns understood', minScore: 0.60, description: 'κΈ°μ‘΄ ν¨ν΄μ΄ νμ
λμλκ°?' },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const weightedClarity = Object.values(breakdown).reduce((sum, comp) => {
|
|
180
|
+
if (!comp)
|
|
181
|
+
return sum;
|
|
182
|
+
return sum + comp.clarityScore * comp.weight;
|
|
183
|
+
}, 0);
|
|
184
|
+
const overallScore = Math.round((1.0 - weightedClarity) * 10000) / 10000;
|
|
185
|
+
const components = [goalComponent, constraintComponent, criteriaComponent];
|
|
186
|
+
if (breakdown.contextClarity)
|
|
187
|
+
components.push(breakdown.contextClarity);
|
|
188
|
+
const weakest = components.reduce((min, c) => c.clarityScore < min.clarityScore ? c : min, components[0]);
|
|
189
|
+
const milestone = getMilestone(overallScore);
|
|
190
|
+
return {
|
|
191
|
+
overallScore,
|
|
192
|
+
breakdown,
|
|
193
|
+
isReadyForSeed: overallScore <= AMBIGUITY_THRESHOLD,
|
|
194
|
+
readinessForGherkin: overallScore <= GHERKIN_READINESS_THRESHOLD,
|
|
195
|
+
milestone: milestone.label,
|
|
196
|
+
milestoneDescription: milestone.description,
|
|
197
|
+
weakestArea: weakest.name,
|
|
198
|
+
nextMilestone: getNextMilestone(overallScore),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { InterviewState } from './intake-types.js';
|
|
2
|
+
export declare function getInterviewDir(basePath: string): string;
|
|
3
|
+
export declare function saveInterviewState(basePath: string, state: InterviewState): void;
|
|
4
|
+
export declare function loadInterviewState(basePath: string, interviewId?: string): InterviewState | null;
|
|
5
|
+
export declare function listInterviewStates(basePath: string): Array<{
|
|
6
|
+
id: string;
|
|
7
|
+
status: string;
|
|
8
|
+
rounds: number;
|
|
9
|
+
}>;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export function getInterviewDir(basePath) {
|
|
4
|
+
const dir = path.join(basePath, '.opencode', 'weave', 'interview');
|
|
5
|
+
if (!fs.existsSync(dir)) {
|
|
6
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
return dir;
|
|
9
|
+
}
|
|
10
|
+
function interviewStatePath(basePath, interviewId) {
|
|
11
|
+
return path.join(getInterviewDir(basePath), `${interviewId}.json`);
|
|
12
|
+
}
|
|
13
|
+
function getLatestInterviewId(basePath) {
|
|
14
|
+
const dir = getInterviewDir(basePath);
|
|
15
|
+
try {
|
|
16
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
17
|
+
if (files.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
return files[files.length - 1].replace('.json', '');
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function saveInterviewState(basePath, state) {
|
|
26
|
+
const filePath = interviewStatePath(basePath, state.interviewId);
|
|
27
|
+
state.updatedAt = new Date().toISOString();
|
|
28
|
+
fs.writeFileSync(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
29
|
+
}
|
|
30
|
+
export function loadInterviewState(basePath, interviewId) {
|
|
31
|
+
const id = interviewId || getLatestInterviewId(basePath);
|
|
32
|
+
if (!id)
|
|
33
|
+
return null;
|
|
34
|
+
const filePath = interviewStatePath(basePath, id);
|
|
35
|
+
if (!fs.existsSync(filePath))
|
|
36
|
+
return null;
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
39
|
+
return JSON.parse(content);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function listInterviewStates(basePath) {
|
|
46
|
+
const dir = getInterviewDir(basePath);
|
|
47
|
+
const results = [];
|
|
48
|
+
try {
|
|
49
|
+
for (const file of fs.readdirSync(dir)) {
|
|
50
|
+
if (!file.endsWith('.json'))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
54
|
+
const state = JSON.parse(content);
|
|
55
|
+
results.push({
|
|
56
|
+
id: state.interviewId,
|
|
57
|
+
status: state.status,
|
|
58
|
+
rounds: state.rounds.length,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch { }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { MapResult, GherkinScenario } from '../types.js';
|
|
2
|
+
import type { Question, IntakeResult, AmbiguityScore } from './intake-types.js';
|
|
3
|
+
export declare function generateQuestions(features: string[], techReqs: IntakeResult['technicalRequirements']): Question[];
|
|
4
|
+
export declare function generateInterviewQuestions(features: string[], techReqs: IntakeResult['technicalRequirements'], map: MapResult | null): Question[];
|
|
5
|
+
export declare function generateGherkinQuestions(features: string[], existingQuestions: Question[], existingAnswers: Record<string, string>, ambiguity: AmbiguityScore): Question[];
|
|
6
|
+
export declare function generateAcceptanceCriteriaFromAnswers(features: string[], answers: Record<string, string>, doneWhenMap: Record<string, string>): GherkinScenario[];
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
export function generateQuestions(features, techReqs) {
|
|
2
|
+
const questions = [];
|
|
3
|
+
let qId = 1;
|
|
4
|
+
if (!techReqs.frontend || techReqs.frontend.length === 0) {
|
|
5
|
+
questions.push({
|
|
6
|
+
id: `Q${qId++}`,
|
|
7
|
+
topic: 'νλ‘ νΈμλ κΈ°μ ',
|
|
8
|
+
question: 'νλ‘ νΈμλ νλ μμν¬ μ νΈλκ° μμΌμ κ°μ?',
|
|
9
|
+
options: ['React', 'Vue', 'Next.js', 'Vanilla JS', 'μκ΄μμ'],
|
|
10
|
+
required: true,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
if (!techReqs.database || techReqs.database.length === 0) {
|
|
14
|
+
questions.push({
|
|
15
|
+
id: `Q${qId++}`,
|
|
16
|
+
topic: 'λ°μ΄ν° μ μ₯',
|
|
17
|
+
question: 'λ°μ΄ν°λ₯Ό μ΄λμ μ μ₯ν κΉμ?',
|
|
18
|
+
options: ['λ‘컬 μ€ν λ¦¬μ§ (μ€νλΌμΈ)', 'μλ² DB (PostgreSQL/MySQL)', 'ν΄λΌμ°λ (Supabase/Firebase)'],
|
|
19
|
+
required: true,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
if (features.length > 3) {
|
|
23
|
+
questions.push({
|
|
24
|
+
id: `Q${qId++}`,
|
|
25
|
+
topic: 'μ°μ μμ',
|
|
26
|
+
question: 'κ°μ₯ λ¨Όμ μμ±ν΄μΌ νλ κΈ°λ₯μ 무μμΈκ°μ?',
|
|
27
|
+
options: features.slice(0, 5),
|
|
28
|
+
required: true,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return questions;
|
|
32
|
+
}
|
|
33
|
+
export function generateInterviewQuestions(features, techReqs, map) {
|
|
34
|
+
const questions = generateQuestions(features, techReqs);
|
|
35
|
+
if (map && map.structuralIssues.length > 0) {
|
|
36
|
+
const critical = map.structuralIssues.filter(i => i.severity === 'critical');
|
|
37
|
+
const warnings = map.structuralIssues.filter(i => i.severity === 'warning');
|
|
38
|
+
if (critical.length > 0) {
|
|
39
|
+
questions.unshift({
|
|
40
|
+
id: 'MAP-CRITICAL',
|
|
41
|
+
topic: 'ꡬ쑰μ λ¬Έμ ',
|
|
42
|
+
question: `μ½λλ² μ΄μ€μμ ${critical.length}κ°μ Critical μ΄μκ° λ°κ²¬λμμ΅λλ€. κ³μ μ§νν κΉμ?`,
|
|
43
|
+
options: ['μ΄μλ₯Ό λ¨Όμ ν΄κ²°', 'μ§ννλ build λ¨κ³μμ ν΄κ²°', 'μ§κΈμ 무μ'],
|
|
44
|
+
required: true,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (warnings.length > 0) {
|
|
48
|
+
questions.push({
|
|
49
|
+
id: 'MAP-WARNINGS',
|
|
50
|
+
topic: 'κΆμ₯ ꡬ쑰 λ³κ²½',
|
|
51
|
+
question: `${warnings.length}κ°μ κ²½κ³ κ° μμ΅λλ€. ꡬ쑰 λ³κ²½ κΆμ₯μ¬νμ κ²ν νμκ² μ΅λκΉ?`,
|
|
52
|
+
options: ['κ²ν ', 'λμ€μ κ²ν ', '무μ'],
|
|
53
|
+
required: false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (features.length > 0) {
|
|
58
|
+
questions.push({
|
|
59
|
+
id: 'EXISTING-CODE',
|
|
60
|
+
topic: 'κΈ°μ‘΄ μ½λ νμ©',
|
|
61
|
+
question: 'κΈ°μ‘΄ μ½λλ² μ΄μ€μ ꡬ쑰λ ν¨ν΄μ μ μ§νλ©΄μ ꡬννμκ² μ΅λκΉ?',
|
|
62
|
+
options: ['μ΅λν κΈ°μ‘΄ ꡬ쑰 μ μ§', 'νμμ ꡬ쑰 λ³κ²½', 'μλ‘ μμ±'],
|
|
63
|
+
required: true,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return questions;
|
|
67
|
+
}
|
|
68
|
+
export function generateGherkinQuestions(features, existingQuestions, existingAnswers, ambiguity) {
|
|
69
|
+
const questions = [];
|
|
70
|
+
let qId = existingQuestions.length + 1;
|
|
71
|
+
if (ambiguity.weakestArea === 'Success Criteria Clarity' || ambiguity.overallScore > 0.3) {
|
|
72
|
+
for (let i = 0; i < Math.min(features.length, 3); i++) {
|
|
73
|
+
const feature = features[i];
|
|
74
|
+
const givenAnswered = Object.keys(existingAnswers).some(k => k.includes(`given-${i}`) || k.includes(`μ μ -${i}`));
|
|
75
|
+
if (!givenAnswered) {
|
|
76
|
+
questions.push({
|
|
77
|
+
id: `Q${qId++}`,
|
|
78
|
+
topic: 'Gherkin - Given',
|
|
79
|
+
question: `"${feature}" κΈ°λ₯μ μ¬μ©νκΈ° μ μ μ΄λ€ μ μ μ‘°κ±΄μ΄ νμν©λκΉ? (μ: λ‘κ·ΈμΈ λμ΄ μμ΄μΌ νλ€, λ°μ΄ν°κ° μ‘΄μ¬ν΄μΌ νλ€)`,
|
|
80
|
+
required: true,
|
|
81
|
+
questionType: 'gherkin-given',
|
|
82
|
+
targetFeature: feature,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const whenAnswered = Object.keys(existingAnswers).some(k => k.includes(`when-${i}`) || k.includes(`νλ-${i}`));
|
|
86
|
+
if (!whenAnswered) {
|
|
87
|
+
questions.push({
|
|
88
|
+
id: `Q${qId++}`,
|
|
89
|
+
topic: 'Gherkin - When',
|
|
90
|
+
question: `"${feature}" κΈ°λ₯μ μ€ννκΈ° μν΄ μ¬μ©μκ° μ΄λ€ νλμ νλμ? (μ: λ²νΌμ ν΄λ¦νλ€, κ°μ μ
λ ₯νκ³ μ μΆνλ€)`,
|
|
91
|
+
required: true,
|
|
92
|
+
questionType: 'gherkin-when',
|
|
93
|
+
targetFeature: feature,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
const thenAnswered = Object.keys(existingAnswers).some(k => k.includes(`then-${i}`) || k.includes(`κ²°κ³Ό-${i}`));
|
|
97
|
+
if (!thenAnswered) {
|
|
98
|
+
questions.push({
|
|
99
|
+
id: `Q${qId++}`,
|
|
100
|
+
topic: 'Gherkin - Then',
|
|
101
|
+
question: `"${feature}" κΈ°λ₯ μ€ν ν μ΄λ€ κ²°κ³Όκ° λμμΌ νλμ? (μ: μ±κ³΅ λ©μμ§κ° νμλλ€, λ°μ΄ν°κ° μ μ₯λλ€, νλ©΄μ΄ κ°±μ λλ€)`,
|
|
102
|
+
required: true,
|
|
103
|
+
questionType: 'gherkin-then',
|
|
104
|
+
targetFeature: feature,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (ambiguity.weakestArea === 'Constraint Clarity' || ambiguity.breakdown.constraintClarity.clarityScore < 0.5) {
|
|
110
|
+
const edgeAnswered = Object.keys(existingAnswers).some(k => k.includes('edge') || k.includes('μμΈ') || k.includes('μλ¬'));
|
|
111
|
+
if (!edgeAnswered) {
|
|
112
|
+
questions.push({
|
|
113
|
+
id: `Q${qId++}`,
|
|
114
|
+
topic: 'κ²½κ³ μ‘°κ±΄',
|
|
115
|
+
question: 'λΉμ μμ μΈ μν©(λ€νΈμν¬ μ€λ₯, μλͺ»λ μ
λ ₯, κΆν μμ λ±)μμ μ΄λ»κ² λμν΄μΌ νλμ?',
|
|
116
|
+
required: true,
|
|
117
|
+
questionType: 'edge-case',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
const constraintAnswered = Object.keys(existingAnswers).some(k => k.includes('constraint') || k.includes('μ μ½') || k.includes('μ ν'));
|
|
121
|
+
if (!constraintAnswered) {
|
|
122
|
+
questions.push({
|
|
123
|
+
id: `Q${qId++}`,
|
|
124
|
+
topic: 'μ μ½μ‘°κ±΄',
|
|
125
|
+
question: 'κΈ°μ μ /λΉκΈ°λ₯μ μ μ½μ‘°κ±΄μ΄ μλμ? (μ§μ λΈλΌμ°μ , μ±λ₯ λͺ©ν, 보μ μꡬμ¬ν, λ°μ΄ν° μ ν λ±)',
|
|
126
|
+
required: false,
|
|
127
|
+
questionType: 'constraint',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (ambiguity.weakestArea === 'Goal Clarity' || ambiguity.breakdown.goalClarity.clarityScore < 0.5) {
|
|
132
|
+
const successAnswered = Object.keys(existingAnswers).some(k => k.includes('μ±κ³΅') || k.includes('μΈ‘μ ') || k.includes('metric'));
|
|
133
|
+
if (!successAnswered) {
|
|
134
|
+
questions.push({
|
|
135
|
+
id: `Q${qId++}`,
|
|
136
|
+
topic: 'μ±κ³΅ μ§ν',
|
|
137
|
+
question: 'μ΄ νλ‘μ νΈμ μ±κ³΅μ μ΄λ»κ² μΈ‘μ ν μ μμκΉμ? (μ: μ¬μ©μ 100λͺ
κ°μ
, νμ΄μ§ λ‘λ 2μ΄ μ΄λ΄, ν
μ€νΈ ν΅κ³Όμ¨ 95%)',
|
|
138
|
+
required: false,
|
|
139
|
+
questionType: 'clarification',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return questions;
|
|
144
|
+
}
|
|
145
|
+
function findAnswer(answers, ...keys) {
|
|
146
|
+
for (const [k, v] of Object.entries(answers)) {
|
|
147
|
+
for (const key of keys) {
|
|
148
|
+
if (k.toLowerCase().includes(key.toLowerCase()))
|
|
149
|
+
return v;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
export function generateAcceptanceCriteriaFromAnswers(features, answers, doneWhenMap) {
|
|
155
|
+
const scenarios = [];
|
|
156
|
+
for (let i = 0; i < features.length; i++) {
|
|
157
|
+
const feature = features[i];
|
|
158
|
+
const doneWhen = doneWhenMap[feature] || `μ μ κ° ${feature} κΈ°λ₯μ μ¬μ©ν μ μλ€`;
|
|
159
|
+
const givenAnswer = findAnswer(answers, `given-${i}`, `μ μ -${i}`, feature, 'given');
|
|
160
|
+
const whenAnswer = findAnswer(answers, `when-${i}`, `νλ-${i}`, feature, 'when');
|
|
161
|
+
const thenAnswer = findAnswer(answers, `then-${i}`, `κ²°κ³Ό-${i}`, feature, 'then');
|
|
162
|
+
const happyGiven = givenAnswer ? [givenAnswer] : [`${feature} κ΄λ ¨ κΈ°λ₯μ΄ κ΅¬νλμ΄ μλ€`];
|
|
163
|
+
const happyWhen = whenAnswer ? [whenAnswer] : [`μ μ κ° ${feature} κΈ°λ₯μ μ¬μ©νλ€`];
|
|
164
|
+
const happyThen = thenAnswer
|
|
165
|
+
? [thenAnswer, doneWhen].filter((v, idx, arr) => arr.indexOf(v) === idx)
|
|
166
|
+
: [doneWhen];
|
|
167
|
+
scenarios.push({
|
|
168
|
+
feature,
|
|
169
|
+
scenario: `${feature} - μ μ λμ`,
|
|
170
|
+
given: happyGiven,
|
|
171
|
+
when: happyWhen,
|
|
172
|
+
then: happyThen,
|
|
173
|
+
});
|
|
174
|
+
const edgeAnswer = findAnswer(answers, 'edge', 'μλ¬', 'μμΈ', 'edge-cases');
|
|
175
|
+
if (edgeAnswer) {
|
|
176
|
+
scenarios.push({
|
|
177
|
+
feature,
|
|
178
|
+
scenario: `${feature} - μλ¬ μ²λ¦¬`,
|
|
179
|
+
given: [`${feature} κ΄λ ¨ κΈ°λ₯μ΄ κ΅¬νλμ΄ μλ€`, 'λΉμ μμ μΈ μν©μ΄ λ°μνλ€'],
|
|
180
|
+
when: ['μ€λ₯ μ‘°κ±΄μ΄ λ°μνλ€'],
|
|
181
|
+
then: [edgeAnswer],
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
scenarios.push({
|
|
186
|
+
feature,
|
|
187
|
+
scenario: `${feature} - μλ¬ μ²λ¦¬`,
|
|
188
|
+
given: [`${feature} κ΄λ ¨ κΈ°λ₯μ΄ κ΅¬νλμ΄ μλ€`],
|
|
189
|
+
when: [`μ μ κ° ${feature} κΈ°λ₯μ λΉμ μμ μΌλ‘ μ¬μ©νλ€`],
|
|
190
|
+
then: ['μ μ ν μλ¬ λ©μμ§κ° νμλλ€'],
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return scenarios;
|
|
195
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { WeaveEvent, EnvironmentAnalysis, MapResult, StructuralChange, ConsentPrompt, GherkinScenario } from '../types.js';
|
|
2
|
+
export interface IntakeOptions {
|
|
3
|
+
docsPath: string;
|
|
4
|
+
onEvent?: (event: WeaveEvent) => void;
|
|
5
|
+
}
|
|
6
|
+
export interface IntakeResult {
|
|
7
|
+
documents: DocumentAnalysis[];
|
|
8
|
+
features: string[];
|
|
9
|
+
domainTerms: {
|
|
10
|
+
term: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
}[];
|
|
13
|
+
technicalRequirements: {
|
|
14
|
+
frontend?: string[];
|
|
15
|
+
backend?: string[];
|
|
16
|
+
database?: string[];
|
|
17
|
+
other?: string[];
|
|
18
|
+
};
|
|
19
|
+
questions: Question[];
|
|
20
|
+
similarProjects?: string[];
|
|
21
|
+
environment?: EnvironmentAnalysis;
|
|
22
|
+
codebaseMapPath?: string;
|
|
23
|
+
structuralChanges?: StructuralChange[];
|
|
24
|
+
consentPrompts?: ConsentPrompt[];
|
|
25
|
+
ambiguityScore?: AmbiguityScore;
|
|
26
|
+
generatedScenarios?: GherkinScenario[];
|
|
27
|
+
}
|
|
28
|
+
export interface DocumentAnalysis {
|
|
29
|
+
path: string;
|
|
30
|
+
title: string;
|
|
31
|
+
sections: string[];
|
|
32
|
+
keyPoints: string[];
|
|
33
|
+
}
|
|
34
|
+
export interface Question {
|
|
35
|
+
id: string;
|
|
36
|
+
topic: string;
|
|
37
|
+
question: string;
|
|
38
|
+
options?: string[];
|
|
39
|
+
required: boolean;
|
|
40
|
+
questionType?: 'clarification' | 'gherkin-given' | 'gherkin-when' | 'gherkin-then' | 'edge-case' | 'constraint' | 'priority' | 'technical';
|
|
41
|
+
targetFeature?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface AmbiguityComponent {
|
|
44
|
+
name: string;
|
|
45
|
+
clarityScore: number;
|
|
46
|
+
weight: number;
|
|
47
|
+
justification: string;
|
|
48
|
+
thresholds: {
|
|
49
|
+
label: string;
|
|
50
|
+
minScore: number;
|
|
51
|
+
description: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface AmbiguityBreakdown {
|
|
55
|
+
goalClarity: AmbiguityComponent;
|
|
56
|
+
constraintClarity: AmbiguityComponent;
|
|
57
|
+
successCriteriaClarity: AmbiguityComponent;
|
|
58
|
+
contextClarity?: AmbiguityComponent;
|
|
59
|
+
}
|
|
60
|
+
export interface AmbiguityScore {
|
|
61
|
+
overallScore: number;
|
|
62
|
+
breakdown: AmbiguityBreakdown;
|
|
63
|
+
isReadyForSeed: boolean;
|
|
64
|
+
readinessForGherkin: boolean;
|
|
65
|
+
milestone: 'initial' | 'progress' | 'refined' | 'ready';
|
|
66
|
+
milestoneDescription: string;
|
|
67
|
+
weakestArea: string;
|
|
68
|
+
nextMilestone?: {
|
|
69
|
+
threshold: number;
|
|
70
|
+
label: string;
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface InterviewRound {
|
|
75
|
+
roundNumber: number;
|
|
76
|
+
questions: Question[];
|
|
77
|
+
answers: Record<string, string>;
|
|
78
|
+
ambiguityBefore?: AmbiguityScore;
|
|
79
|
+
ambiguityAfter?: AmbiguityScore;
|
|
80
|
+
gherkinGenerated?: GherkinScenario[];
|
|
81
|
+
timestamp: string;
|
|
82
|
+
}
|
|
83
|
+
export interface InterviewState {
|
|
84
|
+
interviewId: string;
|
|
85
|
+
status: 'in_progress' | 'completed' | 'aborted';
|
|
86
|
+
initialContext: string;
|
|
87
|
+
rounds: InterviewRound[];
|
|
88
|
+
currentRound: number;
|
|
89
|
+
features: string[];
|
|
90
|
+
isBrownfield: boolean;
|
|
91
|
+
createdAt: string;
|
|
92
|
+
updatedAt: string;
|
|
93
|
+
}
|
|
94
|
+
export interface InterviewOptions {
|
|
95
|
+
docsPath: string;
|
|
96
|
+
basePath?: string;
|
|
97
|
+
mapResult?: MapResult | null;
|
|
98
|
+
onEvent?: (event: WeaveEvent) => void;
|
|
99
|
+
resumeId?: string;
|
|
100
|
+
userAnswers?: Record<string, string>;
|
|
101
|
+
skipGherkinQuestions?: boolean;
|
|
102
|
+
}
|
|
103
|
+
export interface InterviewResult {
|
|
104
|
+
intake: IntakeResult;
|
|
105
|
+
agreedStructuralChanges: StructuralChange[];
|
|
106
|
+
userAnswers: Record<string, string>;
|
|
107
|
+
satisfied: boolean;
|
|
108
|
+
ambiguityScore?: AmbiguityScore;
|
|
109
|
+
generatedScenarios?: GherkinScenario[];
|
|
110
|
+
interviewState?: InterviewState;
|
|
111
|
+
isMultiRound?: boolean;
|
|
112
|
+
}
|
|
113
|
+
export interface IntakeWithAnalysisOptions extends IntakeOptions {
|
|
114
|
+
skipEnvironmentAnalysis?: boolean;
|
|
115
|
+
warningsOnly?: boolean;
|
|
116
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,60 +1,13 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
documents: DocumentAnalysis[];
|
|
8
|
-
features: string[];
|
|
9
|
-
domainTerms: {
|
|
10
|
-
term: string;
|
|
11
|
-
description?: string;
|
|
12
|
-
}[];
|
|
13
|
-
technicalRequirements: {
|
|
14
|
-
frontend?: string[];
|
|
15
|
-
backend?: string[];
|
|
16
|
-
database?: string[];
|
|
17
|
-
other?: string[];
|
|
18
|
-
};
|
|
19
|
-
questions: Question[];
|
|
20
|
-
similarProjects?: string[];
|
|
21
|
-
environment?: EnvironmentAnalysis;
|
|
22
|
-
codebaseMapPath?: string;
|
|
23
|
-
structuralChanges?: StructuralChange[];
|
|
24
|
-
consentPrompts?: ConsentPrompt[];
|
|
25
|
-
}
|
|
26
|
-
export interface DocumentAnalysis {
|
|
27
|
-
path: string;
|
|
28
|
-
title: string;
|
|
29
|
-
sections: string[];
|
|
30
|
-
keyPoints: string[];
|
|
31
|
-
}
|
|
32
|
-
export interface Question {
|
|
33
|
-
id: string;
|
|
34
|
-
topic: string;
|
|
35
|
-
question: string;
|
|
36
|
-
options?: string[];
|
|
37
|
-
required: boolean;
|
|
38
|
-
}
|
|
1
|
+
import type { MapResult, StructuralChange, ConsentPrompt } from '../types.js';
|
|
2
|
+
export type { IntakeOptions, IntakeResult, DocumentAnalysis, Question, AmbiguityComponent, AmbiguityBreakdown, AmbiguityScore, InterviewRound, InterviewState, InterviewOptions, InterviewResult, IntakeWithAnalysisOptions, } from './intake-types.js';
|
|
3
|
+
export { scoreAmbiguity } from './intake-ambiguity.js';
|
|
4
|
+
export { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers, } from './intake-questions.js';
|
|
5
|
+
export { saveInterviewState, loadInterviewState, listInterviewStates, getInterviewDir, } from './intake-persistence.js';
|
|
6
|
+
import type { IntakeResult } from './intake-types.js';
|
|
39
7
|
export declare function injectMapContext(map: MapResult | null, features: string[]): Promise<{
|
|
40
8
|
structuralChanges: StructuralChange[];
|
|
41
9
|
consentPrompts: ConsentPrompt[];
|
|
42
10
|
}>;
|
|
43
|
-
|
|
44
|
-
docsPath: string;
|
|
45
|
-
basePath?: string;
|
|
46
|
-
mapResult?: MapResult | null;
|
|
47
|
-
onEvent?: (event: WeaveEvent) => void;
|
|
48
|
-
}
|
|
49
|
-
export interface InterviewResult {
|
|
50
|
-
intake: IntakeResult;
|
|
51
|
-
agreedStructuralChanges: StructuralChange[];
|
|
52
|
-
userAnswers: Record<string, string>;
|
|
53
|
-
satisfied: boolean;
|
|
54
|
-
}
|
|
55
|
-
export declare function interview(options: InterviewOptions): Promise<InterviewResult>;
|
|
56
|
-
export interface IntakeWithAnalysisOptions extends IntakeOptions {
|
|
57
|
-
skipEnvironmentAnalysis?: boolean;
|
|
58
|
-
warningsOnly?: boolean;
|
|
59
|
-
}
|
|
11
|
+
import type { IntakeOptions, IntakeWithAnalysisOptions, InterviewOptions, InterviewResult } from './intake-types.js';
|
|
60
12
|
export declare function intake(options: IntakeOptions | IntakeWithAnalysisOptions): Promise<IntakeResult>;
|
|
13
|
+
export declare function interview(options: InterviewOptions): Promise<InterviewResult>;
|
|
@@ -2,6 +2,9 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { analyzeEnvironment } from '../environment/index.js';
|
|
4
4
|
import { readMapResult } from './map.js';
|
|
5
|
+
export { scoreAmbiguity } from './intake-ambiguity.js';
|
|
6
|
+
export { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers, } from './intake-questions.js';
|
|
7
|
+
export { saveInterviewState, loadInterviewState, listInterviewStates, getInterviewDir, } from './intake-persistence.js';
|
|
5
8
|
const DOC_EXTENSIONS = ['.md', '.txt', '.yaml', '.yml', '.json'];
|
|
6
9
|
const INDEX_FILES = ['index.md', 'README.md', 'readme.md'];
|
|
7
10
|
function discoverDocuments(basePath) {
|
|
@@ -44,18 +47,8 @@ function analyzeDocument(filePath) {
|
|
|
44
47
|
for (const bullet of bulletMatches.slice(0, 10)) {
|
|
45
48
|
keyPoints.push(bullet.replace(/^[-*]\s+/, ''));
|
|
46
49
|
}
|
|
47
|
-
return {
|
|
48
|
-
path: filePath,
|
|
49
|
-
title,
|
|
50
|
-
sections,
|
|
51
|
-
keyPoints,
|
|
52
|
-
};
|
|
50
|
+
return { path: filePath, title, sections, keyPoints };
|
|
53
51
|
}
|
|
54
|
-
const FEATURE_PATTERNS = [
|
|
55
|
-
/(?:κΈ°λ₯|feature|functionality)[:οΌ]\s*(.+)/gi,
|
|
56
|
-
/(?:ν μ μλ€|can|should|must)\s+(.+)/gi,
|
|
57
|
-
/(?:ꡬν|implement|build|create)\s+(.+)/gi,
|
|
58
|
-
];
|
|
59
52
|
function extractFeatures(documents) {
|
|
60
53
|
const features = new Set();
|
|
61
54
|
for (const doc of documents) {
|
|
@@ -105,38 +98,6 @@ function detectTechnicalRequirements(documents) {
|
|
|
105
98
|
}
|
|
106
99
|
return result;
|
|
107
100
|
}
|
|
108
|
-
function generateQuestions(features, techReqs) {
|
|
109
|
-
const questions = [];
|
|
110
|
-
let qId = 1;
|
|
111
|
-
if (!techReqs.frontend || techReqs.frontend.length === 0) {
|
|
112
|
-
questions.push({
|
|
113
|
-
id: `Q${qId++}`,
|
|
114
|
-
topic: 'νλ‘ νΈμλ κΈ°μ ',
|
|
115
|
-
question: 'νλ‘ νΈμλ νλ μμν¬ μ νΈλκ° μμΌμ κ°μ?',
|
|
116
|
-
options: ['React', 'Vue', 'Next.js', 'Vanilla JS', 'μκ΄μμ'],
|
|
117
|
-
required: true,
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
if (!techReqs.database || techReqs.database.length === 0) {
|
|
121
|
-
questions.push({
|
|
122
|
-
id: `Q${qId++}`,
|
|
123
|
-
topic: 'λ°μ΄ν° μ μ₯',
|
|
124
|
-
question: 'λ°μ΄ν°λ₯Ό μ΄λμ μ μ₯ν κΉμ?',
|
|
125
|
-
options: ['λ‘컬 μ€ν λ¦¬μ§ (μ€νλΌμΈ)', 'μλ² DB (PostgreSQL/MySQL)', 'ν΄λΌμ°λ (Supabase/Firebase)'],
|
|
126
|
-
required: true,
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
if (features.length > 3) {
|
|
130
|
-
questions.push({
|
|
131
|
-
id: `Q${qId++}`,
|
|
132
|
-
topic: 'μ°μ μμ',
|
|
133
|
-
question: 'κ°μ₯ λ¨Όμ μμ±ν΄μΌ νλ κΈ°λ₯μ 무μμΈκ°μ?',
|
|
134
|
-
options: features.slice(0, 5),
|
|
135
|
-
required: true,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
return questions;
|
|
139
|
-
}
|
|
140
101
|
async function searchSimilarProjects(features, techStack) {
|
|
141
102
|
const results = [];
|
|
142
103
|
try {
|
|
@@ -147,33 +108,22 @@ async function searchSimilarProjects(features, techStack) {
|
|
|
147
108
|
console.log('[Intake] Memory database not initialized, skipping similar project search');
|
|
148
109
|
return results;
|
|
149
110
|
}
|
|
150
|
-
const provider = memoryModule.createProvider({
|
|
151
|
-
type: 'text-only',
|
|
152
|
-
});
|
|
111
|
+
const provider = memoryModule.createProvider({ type: 'text-only' });
|
|
153
112
|
let searchResults = [];
|
|
154
113
|
if (provider) {
|
|
155
114
|
const embeddingResult = await provider.embed([query]);
|
|
156
115
|
const embedding = embeddingResult[0];
|
|
157
|
-
searchResults = memoryModule.hybridSearch(query, embedding, {
|
|
158
|
-
limit: 5,
|
|
159
|
-
minScore: 0.3,
|
|
160
|
-
});
|
|
116
|
+
searchResults = memoryModule.hybridSearch(query, embedding, { limit: 5, minScore: 0.3 });
|
|
161
117
|
}
|
|
162
118
|
else {
|
|
163
119
|
console.log('[Intake] Provider not available, using text search only');
|
|
164
120
|
const textResults = db.searchByText(query, 5);
|
|
165
|
-
searchResults = textResults.map((r) => ({
|
|
166
|
-
chunk: r.chunk,
|
|
167
|
-
score: r.score || 0.5,
|
|
168
|
-
}));
|
|
121
|
+
searchResults = textResults.map((r) => ({ chunk: r.chunk, score: r.score || 0.5 }));
|
|
169
122
|
}
|
|
170
123
|
for (const result of searchResults) {
|
|
171
124
|
const { chunk, score } = result;
|
|
172
125
|
const pathParts = chunk.path.split(/[/\\]/);
|
|
173
|
-
const projectName = pathParts.find((p) => !p.startsWith('.') &&
|
|
174
|
-
p !== 'memory' &&
|
|
175
|
-
p !== 'daily' &&
|
|
176
|
-
!p.endsWith('.md')) || 'Previous Project';
|
|
126
|
+
const projectName = pathParts.find((p) => !p.startsWith('.') && p !== 'memory' && p !== 'daily' && !p.endsWith('.md')) || 'Previous Project';
|
|
177
127
|
const relevantFeatures = features.filter(f => chunk.text.toLowerCase().includes(f.toLowerCase().slice(0, 10)));
|
|
178
128
|
if (relevantFeatures.length > 0 || score > 0.5) {
|
|
179
129
|
results.push({
|
|
@@ -200,7 +150,7 @@ export async function injectMapContext(map, features) {
|
|
|
200
150
|
continue;
|
|
201
151
|
const area = issue.area;
|
|
202
152
|
const promptId = `consent-${area.replace(/[^a-z0-9]/gi, '-').toLowerCase()}`;
|
|
203
|
-
|
|
153
|
+
structuralChanges.push({
|
|
204
154
|
area,
|
|
205
155
|
currentState: issue.description,
|
|
206
156
|
proposedChange: issue.suggestion,
|
|
@@ -209,8 +159,7 @@ export async function injectMapContext(map, features) {
|
|
|
209
159
|
affectedFiles: issue.affectedFiles,
|
|
210
160
|
breaking: issue.severity === 'critical',
|
|
211
161
|
agreed: false,
|
|
212
|
-
};
|
|
213
|
-
structuralChanges.push(change);
|
|
162
|
+
});
|
|
214
163
|
consentPrompts.push({
|
|
215
164
|
id: promptId,
|
|
216
165
|
topic: area,
|
|
@@ -219,78 +168,15 @@ export async function injectMapContext(map, features) {
|
|
|
219
168
|
rationale: `"${issue.title}" β ${issue.description}`,
|
|
220
169
|
impact: issue.severity === 'critical' ? 'high' : 'medium',
|
|
221
170
|
breaking: issue.severity === 'critical',
|
|
222
|
-
options: [
|
|
223
|
-
'μΉμΈ β μ§κΈ μμ ',
|
|
224
|
-
'μΉμΈ β build λ¨κ³μμ μμ ',
|
|
225
|
-
'보λ₯ β μ΄ν μ¬κ²ν ',
|
|
226
|
-
'무μ β νμ¬ κ΅¬μ‘° μ μ§',
|
|
227
|
-
],
|
|
171
|
+
options: ['μΉμΈ β μ§κΈ μμ ', 'μΉμΈ β build λ¨κ³μμ μμ ', '보λ₯ β μ΄ν μ¬κ²ν ', '무μ β νμ¬ κ΅¬μ‘° μ μ§'],
|
|
228
172
|
agreed: false,
|
|
229
173
|
});
|
|
230
174
|
}
|
|
231
175
|
return { structuralChanges, consentPrompts };
|
|
232
176
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const critical = map.structuralIssues.filter(i => i.severity === 'critical');
|
|
237
|
-
const warnings = map.structuralIssues.filter(i => i.severity === 'warning');
|
|
238
|
-
if (critical.length > 0) {
|
|
239
|
-
questions.unshift({
|
|
240
|
-
id: 'MAP-CRITICAL',
|
|
241
|
-
topic: 'ꡬ쑰μ λ¬Έμ ',
|
|
242
|
-
question: `μ½λλ² μ΄μ€μμ ${critical.length}κ°μ Critical μ΄μκ° λ°κ²¬λμμ΅λλ€. κ³μ μ§νν κΉμ?`,
|
|
243
|
-
options: ['μ΄μλ₯Ό λ¨Όμ ν΄κ²°', 'μ§ννλ build λ¨κ³μμ ν΄κ²°', 'μ§κΈμ 무μ'],
|
|
244
|
-
required: true,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
if (warnings.length > 0) {
|
|
248
|
-
questions.push({
|
|
249
|
-
id: 'MAP-WARNINGS',
|
|
250
|
-
topic: 'κΆμ₯ ꡬ쑰 λ³κ²½',
|
|
251
|
-
question: `${warnings.length}κ°μ κ²½κ³ κ° μμ΅λλ€. ꡬ쑰 λ³κ²½ κΆμ₯μ¬νμ κ²ν νμκ² μ΅λκΉ?`,
|
|
252
|
-
options: ['κ²ν ', 'λμ€μ κ²ν ', '무μ'],
|
|
253
|
-
required: false,
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (features.length > 0) {
|
|
258
|
-
questions.push({
|
|
259
|
-
id: 'EXISTING-CODE',
|
|
260
|
-
topic: 'κΈ°μ‘΄ μ½λ νμ©',
|
|
261
|
-
question: 'κΈ°μ‘΄ μ½λλ² μ΄μ€μ ꡬ쑰λ ν¨ν΄μ μ μ§νλ©΄μ ꡬννμκ² μ΅λκΉ?',
|
|
262
|
-
options: ['μ΅λν κΈ°μ‘΄ ꡬ쑰 μ μ§', 'νμμ ꡬ쑰 λ³κ²½', 'μλ‘ μμ±'],
|
|
263
|
-
required: true,
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
return questions;
|
|
267
|
-
}
|
|
268
|
-
export async function interview(options) {
|
|
269
|
-
const basePath = options.basePath || process.cwd();
|
|
270
|
-
const intakeResult = await intake({
|
|
271
|
-
docsPath: options.docsPath,
|
|
272
|
-
onEvent: options.onEvent,
|
|
273
|
-
});
|
|
274
|
-
const map = options.mapResult !== undefined
|
|
275
|
-
? options.mapResult
|
|
276
|
-
: await readMapResult(basePath);
|
|
277
|
-
const { structuralChanges, consentPrompts } = await injectMapContext(map, intakeResult.features);
|
|
278
|
-
const enhancedQuestions = generateInterviewQuestions(intakeResult.features, intakeResult.technicalRequirements, map);
|
|
279
|
-
const intakeWithMap = {
|
|
280
|
-
...intakeResult,
|
|
281
|
-
codebaseMapPath: map ? map.mapPath : undefined,
|
|
282
|
-
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
283
|
-
consentPrompts: consentPrompts.length > 0 ? consentPrompts : undefined,
|
|
284
|
-
};
|
|
285
|
-
const hasUnansweredQuestions = enhancedQuestions.length > 0;
|
|
286
|
-
const hasPendingConsent = consentPrompts.length > 0 && consentPrompts.some(cp => !cp.agreed);
|
|
287
|
-
return {
|
|
288
|
-
intake: intakeWithMap,
|
|
289
|
-
agreedStructuralChanges: structuralChanges.filter(sc => sc.agreed),
|
|
290
|
-
userAnswers: {},
|
|
291
|
-
satisfied: !hasUnansweredQuestions && !hasPendingConsent,
|
|
292
|
-
};
|
|
293
|
-
}
|
|
177
|
+
import { scoreAmbiguity } from './intake-ambiguity.js';
|
|
178
|
+
import { generateQuestions, generateInterviewQuestions, generateGherkinQuestions, generateAcceptanceCriteriaFromAnswers } from './intake-questions.js';
|
|
179
|
+
import { saveInterviewState, loadInterviewState } from './intake-persistence.js';
|
|
294
180
|
export async function intake(options) {
|
|
295
181
|
const { docsPath } = options;
|
|
296
182
|
const extendedOptions = options;
|
|
@@ -347,6 +233,7 @@ export async function intake(options) {
|
|
|
347
233
|
const { structuralChanges } = mapResult
|
|
348
234
|
? await injectMapContext(mapResult, features)
|
|
349
235
|
: { structuralChanges: [] };
|
|
236
|
+
const baselineAmbiguity = scoreAmbiguity(features, technicalRequirements, {}, undefined, mapResult !== null, mapResult !== null);
|
|
350
237
|
return {
|
|
351
238
|
documents,
|
|
352
239
|
features,
|
|
@@ -357,5 +244,102 @@ export async function intake(options) {
|
|
|
357
244
|
environment,
|
|
358
245
|
codebaseMapPath: mapResult?.mapPath,
|
|
359
246
|
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
247
|
+
ambiguityScore: baselineAmbiguity,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function interview(options) {
|
|
251
|
+
const basePath = options.basePath || process.cwd();
|
|
252
|
+
const now = new Date().toISOString();
|
|
253
|
+
let interviewState = options.resumeId
|
|
254
|
+
? loadInterviewState(basePath, options.resumeId)
|
|
255
|
+
: (!options.userAnswers ? loadInterviewState(basePath) : null);
|
|
256
|
+
const intakeResult = await intake({ docsPath: options.docsPath, onEvent: options.onEvent });
|
|
257
|
+
const map = options.mapResult !== undefined
|
|
258
|
+
? options.mapResult
|
|
259
|
+
: await readMapResult(basePath);
|
|
260
|
+
const { structuralChanges, consentPrompts } = await injectMapContext(map, intakeResult.features);
|
|
261
|
+
const existingAnswers = interviewState
|
|
262
|
+
? Object.assign({}, ...interviewState.rounds.map(r => r.answers))
|
|
263
|
+
: {};
|
|
264
|
+
const roundAnswers = options.userAnswers || {};
|
|
265
|
+
const allAnswers = { ...existingAnswers, ...roundAnswers };
|
|
266
|
+
const isBrownfield = map !== null;
|
|
267
|
+
const doneWhenMap = {};
|
|
268
|
+
for (const feature of intakeResult.features) {
|
|
269
|
+
doneWhenMap[feature] = `μ μ κ° ${feature.toLowerCase().replace(/[μλ₯Όμ΄κ°μλ]/g, '')}ν μ μλ€`;
|
|
270
|
+
}
|
|
271
|
+
const generatedScenarios = Object.keys(allAnswers).length > 0
|
|
272
|
+
? generateAcceptanceCriteriaFromAnswers(intakeResult.features, allAnswers, doneWhenMap)
|
|
273
|
+
: undefined;
|
|
274
|
+
const ambiguityScore = scoreAmbiguity(intakeResult.features, intakeResult.technicalRequirements, allAnswers, generatedScenarios, isBrownfield, map !== null);
|
|
275
|
+
const baseQuestions = generateQuestions(intakeResult.features, intakeResult.technicalRequirements);
|
|
276
|
+
const mapQuestions = generateInterviewQuestions(intakeResult.features, intakeResult.technicalRequirements, map);
|
|
277
|
+
const seenIds = new Set();
|
|
278
|
+
const mergedQuestions = [...baseQuestions, ...mapQuestions].filter(q => {
|
|
279
|
+
if (seenIds.has(q.id))
|
|
280
|
+
return false;
|
|
281
|
+
seenIds.add(q.id);
|
|
282
|
+
return true;
|
|
283
|
+
});
|
|
284
|
+
let gherkinQuestions = (!options.skipGherkinQuestions && !ambiguityScore.isReadyForSeed)
|
|
285
|
+
? generateGherkinQuestions(intakeResult.features, mergedQuestions, allAnswers, ambiguityScore)
|
|
286
|
+
: [];
|
|
287
|
+
const enhancedQuestions = mergedQuestions.filter(q => !Object.keys(allAnswers).some(k => k.toLowerCase().includes(q.id.toLowerCase())
|
|
288
|
+
|| k.toLowerCase().includes((q.targetFeature || '').toLowerCase())));
|
|
289
|
+
const allQuestions = [...enhancedQuestions, ...gherkinQuestions];
|
|
290
|
+
const interviewId = interviewState?.interviewId
|
|
291
|
+
|| `interview_${new Date().toISOString().replace(/[:.]/g, '-')}`;
|
|
292
|
+
const roundNumber = interviewState ? interviewState.rounds.length + 1 : 1;
|
|
293
|
+
const currentRound = {
|
|
294
|
+
roundNumber,
|
|
295
|
+
questions: allQuestions,
|
|
296
|
+
answers: roundAnswers,
|
|
297
|
+
ambiguityBefore: interviewState?.rounds[interviewState.rounds.length - 1]?.ambiguityAfter,
|
|
298
|
+
ambiguityAfter: ambiguityScore,
|
|
299
|
+
gherkinGenerated: generatedScenarios,
|
|
300
|
+
timestamp: now,
|
|
301
|
+
};
|
|
302
|
+
if (interviewState) {
|
|
303
|
+
interviewState.rounds.push(currentRound);
|
|
304
|
+
interviewState.currentRound = roundNumber;
|
|
305
|
+
interviewState.status = ambiguityScore.isReadyForSeed ? 'completed' : 'in_progress';
|
|
306
|
+
interviewState.features = intakeResult.features;
|
|
307
|
+
interviewState.isBrownfield = isBrownfield;
|
|
308
|
+
interviewState.updatedAt = now;
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
interviewState = {
|
|
312
|
+
interviewId,
|
|
313
|
+
status: ambiguityScore.isReadyForSeed ? 'completed' : 'in_progress',
|
|
314
|
+
initialContext: intakeResult.features.join(', '),
|
|
315
|
+
rounds: [currentRound],
|
|
316
|
+
currentRound: 1,
|
|
317
|
+
features: intakeResult.features,
|
|
318
|
+
isBrownfield,
|
|
319
|
+
createdAt: now,
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
saveInterviewState(basePath, interviewState);
|
|
324
|
+
const intakeWithEverything = {
|
|
325
|
+
...intakeResult,
|
|
326
|
+
codebaseMapPath: map ? map.mapPath : undefined,
|
|
327
|
+
structuralChanges: structuralChanges.length > 0 ? structuralChanges : undefined,
|
|
328
|
+
consentPrompts: consentPrompts.length > 0 ? consentPrompts : undefined,
|
|
329
|
+
ambiguityScore,
|
|
330
|
+
generatedScenarios,
|
|
331
|
+
questions: allQuestions,
|
|
332
|
+
};
|
|
333
|
+
const hasUnansweredQuestions = allQuestions.some(q => q.required);
|
|
334
|
+
const hasPendingConsent = consentPrompts.length > 0 && consentPrompts.some(cp => !cp.agreed);
|
|
335
|
+
return {
|
|
336
|
+
intake: intakeWithEverything,
|
|
337
|
+
agreedStructuralChanges: structuralChanges.filter(sc => sc.agreed),
|
|
338
|
+
userAnswers: allAnswers,
|
|
339
|
+
satisfied: ambiguityScore.isReadyForSeed && !hasUnansweredQuestions && !hasPendingConsent,
|
|
340
|
+
ambiguityScore,
|
|
341
|
+
generatedScenarios,
|
|
342
|
+
interviewState,
|
|
343
|
+
isMultiRound: !ambiguityScore.isReadyForSeed || hasUnansweredQuestions || hasPendingConsent,
|
|
360
344
|
};
|
|
361
345
|
}
|
|
@@ -494,9 +494,19 @@ export async function plan(options) {
|
|
|
494
494
|
}
|
|
495
495
|
catch {
|
|
496
496
|
}
|
|
497
|
+
const intakeScenarios = intake.generatedScenarios;
|
|
497
498
|
for (const phase of weavePlan.phases) {
|
|
498
499
|
if (!phase.acceptanceCriteria || phase.acceptanceCriteria.length === 0) {
|
|
499
|
-
|
|
500
|
+
const matchingScenarios = intakeScenarios
|
|
501
|
+
? intakeScenarios.filter(s => s.feature.toLowerCase().includes(phase.name.toLowerCase())
|
|
502
|
+
|| phase.name.toLowerCase().includes(s.feature.toLowerCase().slice(0, 10)))
|
|
503
|
+
: [];
|
|
504
|
+
if (matchingScenarios.length > 0) {
|
|
505
|
+
phase.acceptanceCriteria = matchingScenarios;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
phase.acceptanceCriteria = generateGherkinForPhase(phase);
|
|
509
|
+
}
|
|
500
510
|
}
|
|
501
511
|
}
|
|
502
512
|
try {
|