hardness 1.0.0 → 1.1.1
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/AGENTS.md +11 -0
- package/CHANGELOG.md +36 -0
- package/README.md +62 -15
- package/node_modules/@hardness/analyzers/package.json +1 -1
- package/node_modules/@hardness/core/dist/common/paths.js +2 -2
- package/node_modules/@hardness/core/dist/common/paths.js.map +1 -1
- package/node_modules/@hardness/core/package.json +1 -1
- package/node_modules/@hardness/prompts/package.json +1 -1
- package/package.json +1 -1
- package/packages/analyzers/package.json +1 -1
- package/packages/cli/dist/commands/discover.js +47 -6
- package/packages/cli/dist/commands/discover.js.map +1 -1
- package/packages/cli/dist/commands/plan.js +39 -6
- package/packages/cli/dist/commands/plan.js.map +1 -1
- package/packages/cli/dist/commands/spec.js +34 -6
- package/packages/cli/dist/commands/spec.js.map +1 -1
- package/packages/cli/dist/dispatcher.d.ts +2 -0
- package/packages/cli/dist/dispatcher.js +63 -62
- package/packages/cli/dist/dispatcher.js.map +1 -1
- package/packages/cli/dist/generators/prd-generator.d.ts +14 -0
- package/packages/cli/dist/generators/prd-generator.js +164 -0
- package/packages/cli/dist/generators/prd-generator.js.map +1 -0
- package/packages/cli/dist/generators/spec-generator.d.ts +35 -0
- package/packages/cli/dist/generators/spec-generator.js +245 -0
- package/packages/cli/dist/generators/spec-generator.js.map +1 -0
- package/packages/cli/dist/generators/sprint-generator.d.ts +51 -0
- package/packages/cli/dist/generators/sprint-generator.js +162 -0
- package/packages/cli/dist/generators/sprint-generator.js.map +1 -0
- package/packages/cli/dist/index.js +1 -1
- package/packages/cli/dist/interview/evaluator-prompt.d.ts +9 -0
- package/packages/cli/dist/interview/evaluator-prompt.js +192 -0
- package/packages/cli/dist/interview/evaluator-prompt.js.map +1 -0
- package/packages/cli/dist/interview/evaluator.d.ts +46 -0
- package/packages/cli/dist/interview/evaluator.js +142 -0
- package/packages/cli/dist/interview/evaluator.js.map +1 -0
- package/packages/cli/dist/interview/questions.d.ts +29 -0
- package/packages/cli/dist/interview/questions.js +642 -0
- package/packages/cli/dist/interview/questions.js.map +1 -0
- package/packages/cli/dist/interview/runner.d.ts +14 -0
- package/packages/cli/dist/interview/runner.js +327 -0
- package/packages/cli/dist/interview/runner.js.map +1 -0
- package/packages/cli/dist/interview/suggestions.d.ts +6 -0
- package/packages/cli/dist/interview/suggestions.js +230 -0
- package/packages/cli/dist/interview/suggestions.js.map +1 -0
- package/packages/cli/dist/interview/types.d.ts +46 -0
- package/packages/cli/dist/interview/types.js +50 -0
- package/packages/cli/dist/interview/types.js.map +1 -0
- package/packages/cli/package.json +1 -1
- package/packages/core/dist/common/paths.js +2 -2
- package/packages/core/dist/common/paths.js.map +1 -1
- package/packages/core/package.json +1 -1
- package/packages/prompts/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
export function parseSpec(content) {
|
|
4
|
+
const lines = content.split('\n');
|
|
5
|
+
// Project name from h1
|
|
6
|
+
const h1 = lines.find((l) => l.startsWith('# SPEC —'));
|
|
7
|
+
const projectName = h1 ? h1.replace('# SPEC —', '').trim() : 'Unknown Project';
|
|
8
|
+
// Stack from section 2
|
|
9
|
+
const stackLine = lines.find((l) => l.includes('**Language / Runtime**') || l.includes('**Language**'));
|
|
10
|
+
const stack = stackLine ? stackLine.replace(/.*?—/, '').trim() : 'Node.js / TypeScript';
|
|
11
|
+
// Technical requirements from section 7
|
|
12
|
+
const sec7Start = lines.findIndex((l) => l.startsWith('## 7. Detailed Technical Requirements'));
|
|
13
|
+
const techReqs = [];
|
|
14
|
+
if (sec7Start !== -1) {
|
|
15
|
+
let idx = 1;
|
|
16
|
+
for (let i = sec7Start + 1; i < lines.length; i++) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
if (line.startsWith('## '))
|
|
19
|
+
break; // next section
|
|
20
|
+
// Match "N. text" or "N) text"
|
|
21
|
+
const match = line.match(/^\d+[.)]\s+(.+)/);
|
|
22
|
+
if (match) {
|
|
23
|
+
techReqs.push({ index: idx++, text: match[1].trim() });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return { projectName, techReqs, stack };
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Sprint builder
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/** How many features to group per sprint (2-4) */
|
|
33
|
+
const FEATURES_PER_SPRINT = 3;
|
|
34
|
+
function deriveFilesFromReq(text, projectName) {
|
|
35
|
+
// Heuristic: look for file paths in the requirement text
|
|
36
|
+
const files = [];
|
|
37
|
+
const fileMatch = text.match(/`([^`]+\.[a-z]+)`/g);
|
|
38
|
+
if (fileMatch) {
|
|
39
|
+
fileMatch.forEach((m) => files.push(m.replace(/`/g, '')));
|
|
40
|
+
}
|
|
41
|
+
if (files.length === 0) {
|
|
42
|
+
// Default: derive a plausible file from the requirement text
|
|
43
|
+
const slug = text
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
46
|
+
.split(/\s+/)
|
|
47
|
+
.slice(0, 4)
|
|
48
|
+
.join('-');
|
|
49
|
+
files.push(`src/${slug}.ts`);
|
|
50
|
+
}
|
|
51
|
+
return [...new Set(files)];
|
|
52
|
+
}
|
|
53
|
+
function buildFeature(req, featId) {
|
|
54
|
+
const files = deriveFilesFromReq(req.text, '');
|
|
55
|
+
// Determine if this is a test requirement
|
|
56
|
+
const isTest = req.text.toLowerCase().includes('test') || req.text.toLowerCase().includes('write tests');
|
|
57
|
+
const acceptance = [
|
|
58
|
+
`${req.text} is implemented correctly`,
|
|
59
|
+
'npm run build compiles without errors',
|
|
60
|
+
'npm test passes',
|
|
61
|
+
];
|
|
62
|
+
const hints = [
|
|
63
|
+
`Section 7, requirement ${req.index} in SPEC.md`,
|
|
64
|
+
];
|
|
65
|
+
if (isTest) {
|
|
66
|
+
hints.push('Use HARDNESS_ROOT env var pattern for fixture directories in tests');
|
|
67
|
+
acceptance[0] = `Tests for ${req.text.replace(/^Write tests for\s*/i, '')} cover all edge cases`;
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
id: featId,
|
|
71
|
+
title: req.text.length > 60 ? req.text.slice(0, 57) + '...' : req.text,
|
|
72
|
+
description: req.text,
|
|
73
|
+
specLines: `${req.index}`,
|
|
74
|
+
status: 'pending',
|
|
75
|
+
files,
|
|
76
|
+
acceptanceCriteria: acceptance,
|
|
77
|
+
hints,
|
|
78
|
+
verification: {
|
|
79
|
+
typecheck: true,
|
|
80
|
+
smoke: {
|
|
81
|
+
command: 'npm test',
|
|
82
|
+
executedBy: 'workflow',
|
|
83
|
+
timeoutSeconds: 60,
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function generateSprints(specContent, options = {}) {
|
|
89
|
+
const root = options.root ?? process.env.HARDNESS_ROOT ?? process.cwd();
|
|
90
|
+
const sprintsDir = options.sprintsDir ?? path.join(root, '.hardness', 'sprints');
|
|
91
|
+
const { projectName, techReqs, stack } = parseSpec(specContent);
|
|
92
|
+
if (techReqs.length === 0) {
|
|
93
|
+
throw new Error('No technical requirements found in SPEC.md section 7. ' +
|
|
94
|
+
'Make sure section 7 has numbered requirements (e.g. "1. ...").');
|
|
95
|
+
}
|
|
96
|
+
// Group requirements into sprints of FEATURES_PER_SPRINT each
|
|
97
|
+
const sprints = [];
|
|
98
|
+
const chunks = [];
|
|
99
|
+
for (let i = 0; i < techReqs.length; i += FEATURES_PER_SPRINT) {
|
|
100
|
+
chunks.push(techReqs.slice(i, i + FEATURES_PER_SPRINT));
|
|
101
|
+
}
|
|
102
|
+
const stackLabel = stack.toLowerCase().includes('node')
|
|
103
|
+
? 'TypeScript strict mode, ESM, no console.log in production code'
|
|
104
|
+
: stack.toLowerCase().includes('python')
|
|
105
|
+
? 'Python 3.10+, type hints required, no print() in production code'
|
|
106
|
+
: 'Strict types, no debug output in production code';
|
|
107
|
+
chunks.forEach((chunk, sprintIdx) => {
|
|
108
|
+
const sprintNum = String(sprintIdx + 1).padStart(2, '0');
|
|
109
|
+
const features = chunk.map((req, featIdx) => {
|
|
110
|
+
const featNum = String(sprintIdx * FEATURES_PER_SPRINT + featIdx + 1).padStart(3, '0');
|
|
111
|
+
return buildFeature(req, `feat-${featNum}`);
|
|
112
|
+
});
|
|
113
|
+
sprints.push({
|
|
114
|
+
index: sprintIdx,
|
|
115
|
+
name: `Sprint ${sprintNum} — ${chunk[0].text.slice(0, 40)}`,
|
|
116
|
+
status: 'pending',
|
|
117
|
+
description: `Technical requirements ${chunk[0].index}–${chunk[chunk.length - 1].index} from SPEC.md.`,
|
|
118
|
+
crossCutting: [
|
|
119
|
+
'All new code must have tests (vitest or equivalent)',
|
|
120
|
+
stackLabel,
|
|
121
|
+
'No new runtime dependencies without review',
|
|
122
|
+
],
|
|
123
|
+
features,
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
// Build index.json
|
|
127
|
+
const indexJson = {
|
|
128
|
+
projectName,
|
|
129
|
+
specPath: 'SPEC.md',
|
|
130
|
+
totalSprints: sprints.length,
|
|
131
|
+
schemaVersion: 1,
|
|
132
|
+
description: `Sprint plan generated by \`hardness plan\` from SPEC.md.`,
|
|
133
|
+
sprints: sprints.map((s, i) => ({
|
|
134
|
+
index: i,
|
|
135
|
+
file: `${String(i + 1).padStart(2, '0')}-${s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '')}.json`,
|
|
136
|
+
name: s.name,
|
|
137
|
+
status: 'pending',
|
|
138
|
+
featuresCount: s.features.length,
|
|
139
|
+
})),
|
|
140
|
+
};
|
|
141
|
+
const sprintFiles = indexJson.sprints.map((e) => e.file);
|
|
142
|
+
if (!options.dryRun) {
|
|
143
|
+
// Ensure sprints dir exists
|
|
144
|
+
if (!fs.existsSync(sprintsDir)) {
|
|
145
|
+
fs.mkdirSync(sprintsDir, { recursive: true });
|
|
146
|
+
}
|
|
147
|
+
// Write each sprint file
|
|
148
|
+
sprints.forEach((sprint, i) => {
|
|
149
|
+
const fileName = sprintFiles[i];
|
|
150
|
+
const filePath = path.join(sprintsDir, fileName);
|
|
151
|
+
fs.writeFileSync(filePath, JSON.stringify(sprint, null, 2), 'utf-8');
|
|
152
|
+
});
|
|
153
|
+
// Write index
|
|
154
|
+
const indexPath = path.join(sprintsDir, '00-index.json');
|
|
155
|
+
fs.writeFileSync(indexPath, JSON.stringify(indexJson, null, 2), 'utf-8');
|
|
156
|
+
// Set current.txt to first sprint
|
|
157
|
+
const currentPath = path.join(path.dirname(sprintsDir), 'current.txt');
|
|
158
|
+
fs.writeFileSync(currentPath, sprintFiles[0], 'utf-8');
|
|
159
|
+
}
|
|
160
|
+
return { sprints, indexJson, sprintFiles };
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=sprint-generator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sprint-generator.js","sourceRoot":"","sources":["../../src/generators/sprint-generator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { UserLevel } from './types.js';
|
|
2
|
+
import type { Question } from './questions.js';
|
|
3
|
+
export declare function buildTranslationPrompt(locale: string, questions: Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
choices?: string[];
|
|
7
|
+
}>): string;
|
|
8
|
+
export declare function buildEvaluationPrompt(phase: string, userLevel: UserLevel, locale: string, answers: Record<string, string | string[]>, allQuestions: Question[], detectedStack: string): string;
|
|
9
|
+
export declare function buildPrdGenerationPrompt(allAnswers: Record<string, string | string[]>, userLevel: UserLevel, locale: string, detectedStack: string, projectName: string): string;
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Evaluator prompts for the interview specialist agent.
|
|
2
|
+
// These are sent to the user's LLM (via agentCommand) for:
|
|
3
|
+
// 1. Translating questions to the user's locale
|
|
4
|
+
// 2. Evaluating phase completeness and generating follow-up questions
|
|
5
|
+
// 3. Generating the final PRD document
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Jargon-free language rules (embedded in all prompts that generate content
|
|
8
|
+
// for beginners)
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
const BEGINNER_LANGUAGE_RULES = `
|
|
11
|
+
CRITICAL LANGUAGE RULES FOR BEGINNER LEVEL:
|
|
12
|
+
- Use ZERO technical jargon. No words like: frontend, backend, API, database,
|
|
13
|
+
deploy, framework, authentication, endpoint, container, serverless, OAuth,
|
|
14
|
+
throughput, observability, metrics, tracing, schema, migration, middleware,
|
|
15
|
+
SDK, repository, runtime, dependency, package, module, component, hook,
|
|
16
|
+
state management, CI/CD, DevOps, microservice, monolith, REST, GraphQL,
|
|
17
|
+
WebSocket, token, hash, encryption, SSL, DNS, load balancer, cache.
|
|
18
|
+
- Write as if talking to someone who has never opened a code editor.
|
|
19
|
+
- Explain concepts through their purpose, not their technical name.
|
|
20
|
+
Example: say "where your system saves information" instead of "database".
|
|
21
|
+
Example: say "how people sign in" instead of "authentication strategy".
|
|
22
|
+
Example: say "where your system will run so people can access it" instead of "deployment target".
|
|
23
|
+
`;
|
|
24
|
+
const INTERMEDIATE_LANGUAGE_RULES = `
|
|
25
|
+
LANGUAGE RULES FOR INTERMEDIATE LEVEL:
|
|
26
|
+
- Basic technical terms are allowed (e.g., "database", "login", "server", "hosting").
|
|
27
|
+
- Avoid acronyms and specific tool names without context.
|
|
28
|
+
Example: say "a database like PostgreSQL or MongoDB" instead of just "PostgreSQL".
|
|
29
|
+
Example: say "hosting service like Heroku or Render" instead of "PaaS".
|
|
30
|
+
`;
|
|
31
|
+
const ADVANCED_LANGUAGE_RULES = `
|
|
32
|
+
LANGUAGE RULES FOR ADVANCED LEVEL:
|
|
33
|
+
- Use direct technical language. Name tools, frameworks, and concepts as they are.
|
|
34
|
+
- Be concise. Skip explanations of well-known concepts.
|
|
35
|
+
`;
|
|
36
|
+
function languageRulesFor(level) {
|
|
37
|
+
switch (level) {
|
|
38
|
+
case 'beginner': return BEGINNER_LANGUAGE_RULES;
|
|
39
|
+
case 'intermediate': return INTERMEDIATE_LANGUAGE_RULES;
|
|
40
|
+
case 'advanced': return ADVANCED_LANGUAGE_RULES;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// 1. Translation prompt
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
export function buildTranslationPrompt(locale, questions) {
|
|
47
|
+
return `You are a professional translator. Translate the following interview questions from English to the locale "${locale}".
|
|
48
|
+
|
|
49
|
+
RULES:
|
|
50
|
+
- Translate naturally, not word-for-word. Adapt idioms and phrasing to sound native.
|
|
51
|
+
- Keep the same tone (friendly, conversational).
|
|
52
|
+
- Preserve all placeholder markers like (?) and formatting instructions.
|
|
53
|
+
- For choice questions, translate each choice option.
|
|
54
|
+
- Do NOT translate question IDs.
|
|
55
|
+
|
|
56
|
+
INPUT (JSON array of questions):
|
|
57
|
+
${JSON.stringify(questions, null, 2)}
|
|
58
|
+
|
|
59
|
+
OUTPUT FORMAT — return ONLY a valid JSON array with the same structure:
|
|
60
|
+
[
|
|
61
|
+
{
|
|
62
|
+
"id": "<same id>",
|
|
63
|
+
"prompt": "<translated prompt>",
|
|
64
|
+
"choices": ["<translated choice 1>", "<translated choice 2>", ...]
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
Return ONLY the JSON array, no explanation, no markdown fences.`;
|
|
69
|
+
}
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// 2. Phase evaluation prompt
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
export function buildEvaluationPrompt(phase, userLevel, locale, answers, allQuestions, detectedStack) {
|
|
74
|
+
const langRules = languageRulesFor(userLevel);
|
|
75
|
+
return `You are a specialist PRD (Product Requirements Document) evaluator. Your job is to assess the quality and completeness of information collected during a product discovery interview.
|
|
76
|
+
|
|
77
|
+
## Context
|
|
78
|
+
- Current phase: "${phase}"
|
|
79
|
+
- User level: "${userLevel}" (${userLevel === 'beginner' ? 'never programmed before' : userLevel === 'intermediate' ? 'some programming experience' : 'professional developer'})
|
|
80
|
+
- User locale: "${locale}"
|
|
81
|
+
- Detected stack: "${detectedStack}"
|
|
82
|
+
|
|
83
|
+
## Answers collected so far
|
|
84
|
+
${JSON.stringify(answers, null, 2)}
|
|
85
|
+
|
|
86
|
+
## Questions asked in this phase
|
|
87
|
+
${JSON.stringify(allQuestions.filter(q => q.phase === phase).map(q => ({ id: q.id, prompt: q.prompt })), null, 2)}
|
|
88
|
+
|
|
89
|
+
## Your task
|
|
90
|
+
|
|
91
|
+
1. **Score** the completeness of the specifications collected for this phase on a scale of 0-10:
|
|
92
|
+
- 0-3: Critical information is missing
|
|
93
|
+
- 4-6: Some important details are missing
|
|
94
|
+
- 7-8: Mostly complete, minor gaps
|
|
95
|
+
- 9-10: Excellent, all important aspects covered
|
|
96
|
+
|
|
97
|
+
2. **Identify gaps**: What important information is still missing?
|
|
98
|
+
|
|
99
|
+
3. **Generate follow-up questions** (ONLY if score < 9): Create 1-3 follow-up questions to fill the gaps.
|
|
100
|
+
|
|
101
|
+
${langRules}
|
|
102
|
+
|
|
103
|
+
## Follow-up question rules
|
|
104
|
+
- Questions MUST be in the locale "${locale}" (translate if not English)
|
|
105
|
+
- Questions MUST respect the user level language rules above
|
|
106
|
+
- Each question must have a unique ID in the format "q{phase}.extra.{n}" (e.g., "q1.extra.1")
|
|
107
|
+
- Each question must have a type: "string", "multiline", or "choice"
|
|
108
|
+
- For "choice" type, provide a "choices" array
|
|
109
|
+
|
|
110
|
+
## Output format — return ONLY valid JSON:
|
|
111
|
+
{
|
|
112
|
+
"score": <number 0-10>,
|
|
113
|
+
"gaps": ["<gap description 1>", "<gap description 2>"],
|
|
114
|
+
"followUpQuestions": [
|
|
115
|
+
{
|
|
116
|
+
"id": "q${phase}.extra.1",
|
|
117
|
+
"prompt": "<question text in the user's locale>",
|
|
118
|
+
"type": "string" | "multiline" | "choice",
|
|
119
|
+
"choices": ["<option1>", "<option2>"] // only for type "choice"
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
"reasoning": "<brief explanation of the score>"
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
Return ONLY the JSON object, no explanation, no markdown fences.`;
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// 3. PRD generation prompt
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
export function buildPrdGenerationPrompt(allAnswers, userLevel, locale, detectedStack, projectName) {
|
|
131
|
+
return `You are a specialist PRD (Product Requirements Document) writer. Generate a professional PRD document in Markdown format based on the interview answers provided.
|
|
132
|
+
|
|
133
|
+
## CRITICAL RULES — FOLLOW EXACTLY
|
|
134
|
+
1. Use ONLY information from the answers below. Do NOT invent features, personas, requirements, or technical details that were not mentioned.
|
|
135
|
+
2. If information for a section is missing, write "To be determined." — do NOT make up content.
|
|
136
|
+
3. The PRD must be in English regardless of the interview language.
|
|
137
|
+
4. Follow the EXACT template structure below.
|
|
138
|
+
5. Every Functional Requirement must have a priority: (must), (should), or (could).
|
|
139
|
+
6. Non-Functional Requirements must be specific and measurable when possible.
|
|
140
|
+
|
|
141
|
+
## Interview answers
|
|
142
|
+
${JSON.stringify(allAnswers, null, 2)}
|
|
143
|
+
|
|
144
|
+
## Additional context
|
|
145
|
+
- Project name: "${projectName}"
|
|
146
|
+
- User level: "${userLevel}"
|
|
147
|
+
- Detected stack: "${detectedStack}"
|
|
148
|
+
- Interview locale: "${locale}"
|
|
149
|
+
|
|
150
|
+
## PRD Template — follow this structure EXACTLY
|
|
151
|
+
|
|
152
|
+
# PRD — ${projectName}
|
|
153
|
+
|
|
154
|
+
> Product Requirements Document for ${projectName}.
|
|
155
|
+
> Generated by \`hardness discover\`.
|
|
156
|
+
|
|
157
|
+
## 1. Overview
|
|
158
|
+
|
|
159
|
+
<Write 2-3 paragraphs based on q0.1 (free text), q1.2 (one-liner), and q1.3 (problem statement). Synthesize into a coherent overview.>
|
|
160
|
+
|
|
161
|
+
## 2. Personas
|
|
162
|
+
|
|
163
|
+
| Persona | Description | Needs |
|
|
164
|
+
|---|---|---|
|
|
165
|
+
<One row per persona based on q2.1, q2.2, q2.3. Include the "Next AI agent" persona as the last row.>
|
|
166
|
+
|
|
167
|
+
## 3. Functional Requirements
|
|
168
|
+
|
|
169
|
+
<List each feature from q3.1 as a numbered FR with priority from q3.2. Format: "- **FR-NNN** (priority): description">
|
|
170
|
+
|
|
171
|
+
## 4. Non-Functional Requirements
|
|
172
|
+
|
|
173
|
+
<Derive from q4.1 through q4.8: stack, frontend, database, auth, scale, deploy, performance, observability. Format: "- **NFR-NNN**: description">
|
|
174
|
+
|
|
175
|
+
## 5. Out of Scope
|
|
176
|
+
|
|
177
|
+
<List items from q5.1, one per bullet.>
|
|
178
|
+
|
|
179
|
+
## 6. Product Acceptance Criteria
|
|
180
|
+
|
|
181
|
+
<Derive from q3.3 and the functional requirements. Each criterion must be a checkbox: "- [ ] criterion">
|
|
182
|
+
|
|
183
|
+
## 7. Assumptions and Risks
|
|
184
|
+
|
|
185
|
+
- **Assumptions**: <from q5.2>
|
|
186
|
+
- **Risks**: <from q5.3>
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
Return ONLY the Markdown content of the PRD. No JSON wrapping, no code fences, no explanation.`;
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=evaluator-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator-prompt.js","sourceRoot":"","sources":["../../src/interview/evaluator-prompt.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,2DAA2D;AAC3D,gDAAgD;AAChD,sEAAsE;AACtE,uCAAuC;AAKvC,8EAA8E;AAC9E,4EAA4E;AAC5E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,uBAAuB,GAAG;;;;;;;;;;;;;CAa/B,CAAC;AAEF,MAAM,2BAA2B,GAAG;;;;;;CAMnC,CAAC;AAEF,MAAM,uBAAuB,GAAG;;;;CAI/B,CAAC;AAEF,SAAS,gBAAgB,CAAC,KAAgB;IACxC,QAAQ,KAAK,EAAE,CAAC;QACd,KAAK,UAAU,CAAC,CAAC,OAAO,uBAAuB,CAAC;QAChD,KAAK,cAAc,CAAC,CAAC,OAAO,2BAA2B,CAAC;QACxD,KAAK,UAAU,CAAC,CAAC,OAAO,uBAAuB,CAAC;IAClD,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,wBAAwB;AACxB,8EAA8E;AAE9E,MAAM,UAAU,sBAAsB,CACpC,MAAc,EACd,SAAoE;IAEpE,OAAO,8GAA8G,MAAM;;;;;;;;;;EAU3H,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;;;;;;;;;;;gEAW4B,CAAC;AACjE,CAAC;AAED,8EAA8E;AAC9E,6BAA6B;AAC7B,8EAA8E;AAE9E,MAAM,UAAU,qBAAqB,CACnC,KAAa,EACb,SAAoB,EACpB,MAAc,EACd,OAA0C,EAC1C,YAAwB,EACxB,aAAqB;IAErB,MAAM,SAAS,GAAG,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE9C,OAAO;;;oBAGW,KAAK;iBACR,SAAS,MAAM,SAAS,KAAK,UAAU,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,SAAS,KAAK,cAAc,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,wBAAwB;kBAC5J,MAAM;qBACH,aAAa;;;EAGhC,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;;;EAGhC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC;;;;;;;;;;;;;;EAc/G,SAAS;;;qCAG0B,MAAM;;;;;;;;;;;;gBAY3B,KAAK;;;;;;;;;iEAS4C,CAAC;AAClE,CAAC;AAED,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E,MAAM,UAAU,wBAAwB,CACtC,UAA6C,EAC7C,SAAoB,EACpB,MAAc,EACd,aAAqB,EACrB,WAAmB;IAEnB,OAAO;;;;;;;;;;;EAWP,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;;;mBAGlB,WAAW;iBACb,SAAS;qBACL,aAAa;uBACX,MAAM;;;;UAInB,WAAW;;sCAEiB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;+FAoC8C,CAAC;AAChG,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { InterviewState, PhaseEvaluation } from './types.js';
|
|
2
|
+
import type { Question } from './questions.js';
|
|
3
|
+
export interface TranslatedQuestion {
|
|
4
|
+
id: string;
|
|
5
|
+
prompt: string;
|
|
6
|
+
choices?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface EvaluatorOptions {
|
|
9
|
+
/** The agent command template from config.json (e.g. 'claude -p "$(cat {context_file})"') */
|
|
10
|
+
agentCommand: string;
|
|
11
|
+
/** Project root directory */
|
|
12
|
+
projectRoot: string;
|
|
13
|
+
/** Timeout in seconds for LLM calls */
|
|
14
|
+
timeout?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class InterviewEvaluator {
|
|
17
|
+
private agentCommand;
|
|
18
|
+
private projectRoot;
|
|
19
|
+
private timeoutMs;
|
|
20
|
+
constructor(options: EvaluatorOptions);
|
|
21
|
+
/**
|
|
22
|
+
* Sends a prompt to the LLM via agentCommand and returns the raw response.
|
|
23
|
+
* Writes the prompt to a temp file, substitutes {context_file}, and captures stdout.
|
|
24
|
+
*/
|
|
25
|
+
private invokeLlm;
|
|
26
|
+
/**
|
|
27
|
+
* Extracts JSON from LLM response, handling possible markdown fences.
|
|
28
|
+
*/
|
|
29
|
+
private extractJson;
|
|
30
|
+
/**
|
|
31
|
+
* Translate all questions to the target locale via LLM.
|
|
32
|
+
*/
|
|
33
|
+
translateQuestions(locale: string, questions: Array<{
|
|
34
|
+
id: string;
|
|
35
|
+
prompt: string;
|
|
36
|
+
choices?: string[];
|
|
37
|
+
}>): Promise<TranslatedQuestion[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the completeness of a phase and optionally generate follow-up questions.
|
|
40
|
+
*/
|
|
41
|
+
evaluatePhase(phase: string, state: InterviewState, allQuestions: Question[]): Promise<PhaseEvaluation>;
|
|
42
|
+
/**
|
|
43
|
+
* Generate the final PRD document from all collected answers.
|
|
44
|
+
*/
|
|
45
|
+
generatePrd(state: InterviewState): Promise<string>;
|
|
46
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Interview evaluator — invokes the user's LLM (via agentCommand) for:
|
|
2
|
+
// 1. Translating questions to the user's locale
|
|
3
|
+
// 2. Evaluating phase completeness and scoring
|
|
4
|
+
// 3. Generating the final PRD
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { runChecked } from '@hardness/core';
|
|
9
|
+
import { buildTranslationPrompt, buildEvaluationPrompt, buildPrdGenerationPrompt, } from './evaluator-prompt.js';
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Evaluator class
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
export class InterviewEvaluator {
|
|
14
|
+
agentCommand;
|
|
15
|
+
projectRoot;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
constructor(options) {
|
|
18
|
+
this.agentCommand = options.agentCommand;
|
|
19
|
+
this.projectRoot = options.projectRoot;
|
|
20
|
+
this.timeoutMs = (options.timeout ?? 120) * 1000;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Sends a prompt to the LLM via agentCommand and returns the raw response.
|
|
24
|
+
* Writes the prompt to a temp file, substitutes {context_file}, and captures stdout.
|
|
25
|
+
*/
|
|
26
|
+
async invokeLlm(prompt) {
|
|
27
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hardness-eval-'));
|
|
28
|
+
const promptFile = path.join(tmpDir, 'prompt.txt');
|
|
29
|
+
try {
|
|
30
|
+
fs.writeFileSync(promptFile, prompt, 'utf-8');
|
|
31
|
+
const normalizedPath = promptFile.split('\\').join('/');
|
|
32
|
+
const normalizedCwd = this.projectRoot.split('\\').join('/');
|
|
33
|
+
const command = this.agentCommand
|
|
34
|
+
.split('{context_file}').join(normalizedPath)
|
|
35
|
+
.split('{cwd}').join(normalizedCwd)
|
|
36
|
+
.split('{timeout}').join(String(Math.floor(this.timeoutMs / 1000)));
|
|
37
|
+
const result = await runChecked(command, {
|
|
38
|
+
cwd: this.projectRoot,
|
|
39
|
+
timeoutMs: this.timeoutMs,
|
|
40
|
+
});
|
|
41
|
+
if (result.error) {
|
|
42
|
+
throw new Error(`LLM invocation failed: ${result.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
if (result.code !== 0) {
|
|
45
|
+
throw new Error(`LLM exited with code ${result.code}: ${result.stderr}`);
|
|
46
|
+
}
|
|
47
|
+
return result.stdout.trim();
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// best-effort cleanup
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Extracts JSON from LLM response, handling possible markdown fences.
|
|
60
|
+
*/
|
|
61
|
+
extractJson(raw) {
|
|
62
|
+
// Strip markdown code fences if present
|
|
63
|
+
const fenceMatch = raw.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
64
|
+
if (fenceMatch) {
|
|
65
|
+
return fenceMatch[1].trim();
|
|
66
|
+
}
|
|
67
|
+
// Try to find JSON object or array directly
|
|
68
|
+
const jsonStart = raw.indexOf('{') !== -1 ? raw.indexOf('{') : raw.indexOf('[');
|
|
69
|
+
const jsonEnd = raw.lastIndexOf('}') !== -1 ? raw.lastIndexOf('}') : raw.lastIndexOf(']');
|
|
70
|
+
if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) {
|
|
71
|
+
return raw.slice(jsonStart, jsonEnd + 1);
|
|
72
|
+
}
|
|
73
|
+
return raw;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Translate all questions to the target locale via LLM.
|
|
77
|
+
*/
|
|
78
|
+
async translateQuestions(locale, questions) {
|
|
79
|
+
const prompt = buildTranslationPrompt(locale, questions);
|
|
80
|
+
const raw = await this.invokeLlm(prompt);
|
|
81
|
+
const json = this.extractJson(raw);
|
|
82
|
+
try {
|
|
83
|
+
const translated = JSON.parse(json);
|
|
84
|
+
if (!Array.isArray(translated)) {
|
|
85
|
+
throw new Error('Expected an array of translated questions');
|
|
86
|
+
}
|
|
87
|
+
return translated;
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
throw new Error(`Failed to parse translation response: ${e.message}\nRaw: ${raw.slice(0, 500)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Evaluate the completeness of a phase and optionally generate follow-up questions.
|
|
95
|
+
*/
|
|
96
|
+
async evaluatePhase(phase, state, allQuestions) {
|
|
97
|
+
const prompt = buildEvaluationPrompt(phase, state.userLevel, state.locale, state.answers, allQuestions, state.detectedStack.label);
|
|
98
|
+
const raw = await this.invokeLlm(prompt);
|
|
99
|
+
const json = this.extractJson(raw);
|
|
100
|
+
try {
|
|
101
|
+
const evaluation = JSON.parse(json);
|
|
102
|
+
if (typeof evaluation.score !== 'number' || !Array.isArray(evaluation.gaps)) {
|
|
103
|
+
throw new Error('Invalid evaluation format');
|
|
104
|
+
}
|
|
105
|
+
// Ensure followUpQuestions is always an array
|
|
106
|
+
if (!Array.isArray(evaluation.followUpQuestions)) {
|
|
107
|
+
evaluation.followUpQuestions = [];
|
|
108
|
+
}
|
|
109
|
+
// Cap at 3 follow-up questions
|
|
110
|
+
evaluation.followUpQuestions = evaluation.followUpQuestions.slice(0, 3);
|
|
111
|
+
return evaluation;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
throw new Error(`Failed to parse evaluation response: ${e.message}\nRaw: ${raw.slice(0, 500)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate the final PRD document from all collected answers.
|
|
119
|
+
*/
|
|
120
|
+
async generatePrd(state) {
|
|
121
|
+
const projectName = state.answers['q1.1'] ?? 'Untitled Project';
|
|
122
|
+
const prompt = buildPrdGenerationPrompt(state.answers, state.userLevel, state.locale, state.detectedStack.label, projectName);
|
|
123
|
+
const raw = await this.invokeLlm(prompt);
|
|
124
|
+
// The response should be raw markdown (no JSON wrapping)
|
|
125
|
+
// Strip markdown fences if present
|
|
126
|
+
let prd = raw;
|
|
127
|
+
if (prd.startsWith('```markdown')) {
|
|
128
|
+
prd = prd.slice('```markdown'.length);
|
|
129
|
+
}
|
|
130
|
+
else if (prd.startsWith('```md')) {
|
|
131
|
+
prd = prd.slice('```md'.length);
|
|
132
|
+
}
|
|
133
|
+
else if (prd.startsWith('```')) {
|
|
134
|
+
prd = prd.slice(3);
|
|
135
|
+
}
|
|
136
|
+
if (prd.endsWith('```')) {
|
|
137
|
+
prd = prd.slice(0, -3);
|
|
138
|
+
}
|
|
139
|
+
return prd.trim();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=evaluator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"evaluator.js","sourceRoot":"","sources":["../../src/interview/evaluator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UserLevel } from './types.js';
|
|
2
|
+
export type QuestionPhase = 'opening' | 'vision' | 'people' | 'features' | 'constraints' | 'boundaries' | 'review';
|
|
3
|
+
export type QuestionType = 'string' | 'multiline' | 'choice' | 'action';
|
|
4
|
+
export interface PromptByLevel {
|
|
5
|
+
beginner: string;
|
|
6
|
+
intermediate: string;
|
|
7
|
+
advanced: string;
|
|
8
|
+
}
|
|
9
|
+
export interface Question {
|
|
10
|
+
id: string;
|
|
11
|
+
phase: QuestionPhase;
|
|
12
|
+
prompt: string;
|
|
13
|
+
type: QuestionType;
|
|
14
|
+
/** Only for type === 'choice' */
|
|
15
|
+
choices?: string[];
|
|
16
|
+
/** Choices adapted per level (only for type === 'choice') */
|
|
17
|
+
choicesByLevel?: Record<UserLevel, string[]>;
|
|
18
|
+
/** Key into the SuggestionEngine (used to call suggest(suggestionId, state)) */
|
|
19
|
+
suggestionId: string;
|
|
20
|
+
/** Prompt text adapted per user level. Falls back to `prompt` if not set. */
|
|
21
|
+
promptByLevel?: PromptByLevel;
|
|
22
|
+
}
|
|
23
|
+
/** Get the prompt for a question based on user level */
|
|
24
|
+
export declare function getPromptForLevel(question: Question, level: UserLevel): string;
|
|
25
|
+
/** Get the choices for a question based on user level */
|
|
26
|
+
export declare function getChoicesForLevel(question: Question, level: UserLevel): string[] | undefined;
|
|
27
|
+
export declare const QUESTIONS: Question[];
|
|
28
|
+
/** Questions grouped by phase for easy iteration */
|
|
29
|
+
export declare function questionsByPhase(phase: QuestionPhase): Question[];
|