@codewalla_india/openspec 1.3.2 → 1.3.4
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/README.md +271 -271
- package/dist/comprehension-quiz/index.d.ts +7 -7
- package/dist/comprehension-quiz/index.js +7 -7
- package/dist/comprehension-quiz/providers/anthropic.d.ts +1 -0
- package/dist/comprehension-quiz/providers/anthropic.js +25 -9
- package/dist/comprehension-quiz/providers/interface.d.ts +42 -3
- package/dist/comprehension-quiz/providers/interface.js +102 -15
- package/dist/comprehension-quiz/providers/local.d.ts +1 -0
- package/dist/comprehension-quiz/providers/local.js +25 -9
- package/dist/comprehension-quiz/providers/ollama.d.ts +1 -0
- package/dist/comprehension-quiz/providers/ollama.js +25 -9
- package/dist/comprehension-quiz/providers/openai.d.ts +1 -0
- package/dist/comprehension-quiz/providers/openai.js +28 -9
- package/dist/comprehension-quiz/question-generator.d.ts +51 -3
- package/dist/comprehension-quiz/question-generator.js +91 -29
- package/dist/comprehension-quiz/quiz-executor.d.ts +36 -31
- package/dist/comprehension-quiz/quiz-executor.js +113 -97
- package/dist/comprehension-quiz/types.d.ts +22 -0
- package/dist/core/templates/workflows/apply-change.js +93 -3
- package/dist/core/templates/workflows/modify-change.js +233 -233
- package/package.json +1 -1
- package/schemas/spec-driven/templates/plan-template.md +91 -91
|
@@ -35,19 +35,35 @@ export class AnthropicProvider extends BaseProvider {
|
|
|
35
35
|
* @returns Array of generated questions
|
|
36
36
|
*/
|
|
37
37
|
async generateQuestions(artifactContent, questionCount, artifactType) {
|
|
38
|
-
// TODO: Implement Anthropic API call
|
|
39
|
-
// For now, return placeholder questions
|
|
40
38
|
const questions = [];
|
|
41
39
|
for (let i = 0; i < questionCount; i++) {
|
|
42
|
-
questions.
|
|
43
|
-
|
|
44
|
-
options: ['Option A', 'Option B', 'Option C'],
|
|
45
|
-
correctOption: 0,
|
|
46
|
-
artifactSource: artifactType,
|
|
47
|
-
});
|
|
40
|
+
const q = await this.generateSingleQuestion(artifactContent, artifactType, i, questionCount, questions.map((q) => q.text));
|
|
41
|
+
questions.push(q);
|
|
48
42
|
}
|
|
49
|
-
this.validateQuestions(questions);
|
|
50
43
|
return questions;
|
|
51
44
|
}
|
|
45
|
+
async generateSingleQuestion(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts) {
|
|
46
|
+
const { system, user } = this.buildSingleQuestionPrompt(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts);
|
|
47
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'x-api-key': this.config.apiKey,
|
|
51
|
+
'anthropic-version': '2023-06-01',
|
|
52
|
+
'content-type': 'application/json',
|
|
53
|
+
},
|
|
54
|
+
body: JSON.stringify({
|
|
55
|
+
model: this.config.model ?? 'claude-3-opus-20240229',
|
|
56
|
+
max_tokens: 512,
|
|
57
|
+
system,
|
|
58
|
+
messages: [{ role: 'user', content: user }],
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`Anthropic API error ${response.status}: ${await response.text()}`);
|
|
63
|
+
}
|
|
64
|
+
const data = (await response.json());
|
|
65
|
+
const raw = data.content.find((b) => b.type === 'text')?.text ?? '';
|
|
66
|
+
return this.parseSingleQuestionResponse(raw, artifactType);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
//# sourceMappingURL=anthropic.js.map
|
|
@@ -37,23 +37,62 @@ export declare abstract class BaseProvider implements LLMProvider {
|
|
|
37
37
|
* @returns Array of generated questions
|
|
38
38
|
*/
|
|
39
39
|
abstract generateQuestions(artifactContent: string, questionCount: number, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Promise<Question[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Generate a single question on-demand.
|
|
42
|
+
* Subclasses MUST call buildSingleQuestionPrompt() to get the messages and
|
|
43
|
+
* parseSingleQuestionResponse() to parse the LLM reply. This guarantees the
|
|
44
|
+
* correct-answer index never leaks into stdout or conversational context.
|
|
45
|
+
*/
|
|
46
|
+
abstract generateSingleQuestion(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): Promise<Question>;
|
|
40
47
|
/**
|
|
41
48
|
* Validate provider configuration
|
|
42
49
|
* @throws Error if configuration is invalid
|
|
43
50
|
*/
|
|
44
51
|
abstract validateConfig(): void;
|
|
52
|
+
/**
|
|
53
|
+
* Build the system + user messages for single-question generation.
|
|
54
|
+
*
|
|
55
|
+
* Contract:
|
|
56
|
+
* - System message explicitly forbids listing multiple questions.
|
|
57
|
+
* - User message asks for exactly one question (question N of total).
|
|
58
|
+
* - Response format is strict JSON only — no prose, no explanation, no answer label.
|
|
59
|
+
* - The correct answer index (correctOption) is returned as a number 0-2; its
|
|
60
|
+
* meaning is never explained in the response so the LLM cannot read it back.
|
|
61
|
+
*
|
|
62
|
+
* @returns { system, user } message pair ready to send to the LLM API
|
|
63
|
+
*/
|
|
64
|
+
protected buildSingleQuestionPrompt(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): {
|
|
65
|
+
system: string;
|
|
66
|
+
user: string;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Parse the raw LLM text response into a Question object.
|
|
70
|
+
* Strips markdown fences if present, then JSON-parses and validates.
|
|
71
|
+
*
|
|
72
|
+
* @param rawResponse - Raw text returned by the LLM API
|
|
73
|
+
* @param artifactType - Expected artifact source (used as fallback)
|
|
74
|
+
* @returns Validated Question object
|
|
75
|
+
* @throws Error if the response cannot be parsed or fails validation
|
|
76
|
+
*/
|
|
77
|
+
protected parseSingleQuestionResponse(rawResponse: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Question;
|
|
45
78
|
/**
|
|
46
79
|
* Validate generated questions before returning
|
|
47
80
|
*
|
|
48
81
|
* Ensures all questions have:
|
|
49
82
|
* - Non-empty text
|
|
50
|
-
- Exactly 3 options
|
|
51
|
-
- Valid correct option index (0-2)
|
|
52
|
-
- Valid artifact source
|
|
83
|
+
* - Exactly 3 options
|
|
84
|
+
* - Valid correct option index (0-2)
|
|
85
|
+
* - Valid artifact source
|
|
53
86
|
*
|
|
54
87
|
* @param questions - Questions to validate
|
|
55
88
|
* @throws Error if any question is invalid
|
|
56
89
|
*/
|
|
57
90
|
protected validateQuestions(questions: Question[]): void;
|
|
91
|
+
/**
|
|
92
|
+
* Validate a single question object.
|
|
93
|
+
* @param question - Question to validate
|
|
94
|
+
* @throws Error if invalid
|
|
95
|
+
*/
|
|
96
|
+
protected validateQuestion(question: Question): void;
|
|
58
97
|
}
|
|
59
98
|
//# sourceMappingURL=interface.d.ts.map
|
|
@@ -31,32 +31,119 @@ export class BaseProvider {
|
|
|
31
31
|
this.config = config;
|
|
32
32
|
this.validateConfig();
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build the system + user messages for single-question generation.
|
|
36
|
+
*
|
|
37
|
+
* Contract:
|
|
38
|
+
* - System message explicitly forbids listing multiple questions.
|
|
39
|
+
* - User message asks for exactly one question (question N of total).
|
|
40
|
+
* - Response format is strict JSON only — no prose, no explanation, no answer label.
|
|
41
|
+
* - The correct answer index (correctOption) is returned as a number 0-2; its
|
|
42
|
+
* meaning is never explained in the response so the LLM cannot read it back.
|
|
43
|
+
*
|
|
44
|
+
* @returns { system, user } message pair ready to send to the LLM API
|
|
45
|
+
*/
|
|
46
|
+
buildSingleQuestionPrompt(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts) {
|
|
47
|
+
const previousBlock = previousQuestionTexts.length > 0
|
|
48
|
+
? `\n\nAlready asked questions (do NOT repeat these):\n${previousQuestionTexts.map((t, i) => `${i + 1}. ${t}`).join('\n')}`
|
|
49
|
+
: '';
|
|
50
|
+
const system = [
|
|
51
|
+
'You are a quiz question generator for a software change review system.',
|
|
52
|
+
'STRICT RULES — violating any rule causes system failure:',
|
|
53
|
+
' 1. Output EXACTLY ONE question. Never output a list, array, or multiple questions.',
|
|
54
|
+
' 2. Output ONLY valid JSON matching the schema below. No prose before or after.',
|
|
55
|
+
' 3. Do NOT include the word "Answer", "Correct", or any hint about which option is right.',
|
|
56
|
+
' 4. Do NOT explain the options. Do NOT add notes, caveats, or additional text.',
|
|
57
|
+
' 5. The "correctOption" field is an opaque integer (0, 1, or 2). Do not label it.',
|
|
58
|
+
'',
|
|
59
|
+
'Response schema (output this JSON and nothing else):',
|
|
60
|
+
'{',
|
|
61
|
+
' "text": "<question text>",',
|
|
62
|
+
' "options": ["<option 1>", "<option 2>", "<option 3>"],',
|
|
63
|
+
' "correctOption": <0|1|2>,',
|
|
64
|
+
' "artifactSource": "<proposal|design|specs|plan|tasks>"',
|
|
65
|
+
'}',
|
|
66
|
+
].join('\n');
|
|
67
|
+
const user = [
|
|
68
|
+
`Generate question ${questionIndex + 1} of ${totalQuestions} for the "${artifactType}" artifact.`,
|
|
69
|
+
'The question must require reading the artifact to answer correctly.',
|
|
70
|
+
'Distractors must be plausible but wrong based on artifact content.',
|
|
71
|
+
previousBlock,
|
|
72
|
+
'',
|
|
73
|
+
`Artifact content:\n---\n${artifactContent}\n---`,
|
|
74
|
+
].join('\n');
|
|
75
|
+
return { system, user };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse the raw LLM text response into a Question object.
|
|
79
|
+
* Strips markdown fences if present, then JSON-parses and validates.
|
|
80
|
+
*
|
|
81
|
+
* @param rawResponse - Raw text returned by the LLM API
|
|
82
|
+
* @param artifactType - Expected artifact source (used as fallback)
|
|
83
|
+
* @returns Validated Question object
|
|
84
|
+
* @throws Error if the response cannot be parsed or fails validation
|
|
85
|
+
*/
|
|
86
|
+
parseSingleQuestionResponse(rawResponse, artifactType) {
|
|
87
|
+
let text = rawResponse.trim();
|
|
88
|
+
// Strip markdown code fences if present
|
|
89
|
+
text = text.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = JSON.parse(text);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
throw new Error(`LLM returned invalid JSON for single question: ${rawResponse.slice(0, 200)}`);
|
|
96
|
+
}
|
|
97
|
+
const q = parsed;
|
|
98
|
+
if (typeof q['text'] !== 'string' ||
|
|
99
|
+
!Array.isArray(q['options']) ||
|
|
100
|
+
typeof q['correctOption'] !== 'number' ||
|
|
101
|
+
typeof q['artifactSource'] !== 'string') {
|
|
102
|
+
throw new Error(`LLM response missing required fields: ${JSON.stringify(q)}`);
|
|
103
|
+
}
|
|
104
|
+
const question = {
|
|
105
|
+
text: q['text'],
|
|
106
|
+
options: q['options'],
|
|
107
|
+
correctOption: q['correctOption'],
|
|
108
|
+
artifactSource: q['artifactSource'] ?? artifactType,
|
|
109
|
+
};
|
|
110
|
+
this.validateQuestion(question);
|
|
111
|
+
return question;
|
|
112
|
+
}
|
|
34
113
|
/**
|
|
35
114
|
* Validate generated questions before returning
|
|
36
115
|
*
|
|
37
116
|
* Ensures all questions have:
|
|
38
117
|
* - Non-empty text
|
|
39
|
-
- Exactly 3 options
|
|
40
|
-
- Valid correct option index (0-2)
|
|
41
|
-
- Valid artifact source
|
|
118
|
+
* - Exactly 3 options
|
|
119
|
+
* - Valid correct option index (0-2)
|
|
120
|
+
* - Valid artifact source
|
|
42
121
|
*
|
|
43
122
|
* @param questions - Questions to validate
|
|
44
123
|
* @throws Error if any question is invalid
|
|
45
124
|
*/
|
|
46
125
|
validateQuestions(questions) {
|
|
47
126
|
for (const question of questions) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
127
|
+
this.validateQuestion(question);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Validate a single question object.
|
|
132
|
+
* @param question - Question to validate
|
|
133
|
+
* @throws Error if invalid
|
|
134
|
+
*/
|
|
135
|
+
validateQuestion(question) {
|
|
136
|
+
if (!question.text || question.text.trim().length === 0) {
|
|
137
|
+
throw new Error('Question text cannot be empty');
|
|
138
|
+
}
|
|
139
|
+
if (question.options.length !== 3) {
|
|
140
|
+
throw new Error('Each question must have exactly 3 options');
|
|
141
|
+
}
|
|
142
|
+
if (question.correctOption < 0 || question.correctOption > 2) {
|
|
143
|
+
throw new Error('Correct option index must be 0, 1, or 2');
|
|
144
|
+
}
|
|
145
|
+
if (!['proposal', 'design', 'specs', 'plan', 'tasks'].includes(question.artifactSource)) {
|
|
146
|
+
throw new Error('Invalid artifact source');
|
|
60
147
|
}
|
|
61
148
|
}
|
|
62
149
|
}
|
|
@@ -30,5 +30,6 @@ export declare class LocalProvider extends BaseProvider {
|
|
|
30
30
|
* @returns Array of generated questions
|
|
31
31
|
*/
|
|
32
32
|
generateQuestions(artifactContent: string, questionCount: number, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Promise<Question[]>;
|
|
33
|
+
generateSingleQuestion(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): Promise<Question>;
|
|
33
34
|
}
|
|
34
35
|
//# sourceMappingURL=local.d.ts.map
|
|
@@ -35,19 +35,35 @@ export class LocalProvider extends BaseProvider {
|
|
|
35
35
|
* @returns Array of generated questions
|
|
36
36
|
*/
|
|
37
37
|
async generateQuestions(artifactContent, questionCount, artifactType) {
|
|
38
|
-
// TODO: Implement local model inference
|
|
39
|
-
// For now, return placeholder questions
|
|
40
38
|
const questions = [];
|
|
41
39
|
for (let i = 0; i < questionCount; i++) {
|
|
42
|
-
questions.
|
|
43
|
-
|
|
44
|
-
options: ['Option A', 'Option B', 'Option C'],
|
|
45
|
-
correctOption: 0,
|
|
46
|
-
artifactSource: artifactType,
|
|
47
|
-
});
|
|
40
|
+
const q = await this.generateSingleQuestion(artifactContent, artifactType, i, questionCount, questions.map((q) => q.text));
|
|
41
|
+
questions.push(q);
|
|
48
42
|
}
|
|
49
|
-
this.validateQuestions(questions);
|
|
50
43
|
return questions;
|
|
51
44
|
}
|
|
45
|
+
async generateSingleQuestion(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts) {
|
|
46
|
+
const { system, user } = this.buildSingleQuestionPrompt(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts);
|
|
47
|
+
const endpoint = this.config.endpoint ?? 'http://localhost:11434';
|
|
48
|
+
const response = await fetch(`${endpoint}/api/chat`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model: this.config.model,
|
|
53
|
+
stream: false,
|
|
54
|
+
format: 'json',
|
|
55
|
+
messages: [
|
|
56
|
+
{ role: 'system', content: system },
|
|
57
|
+
{ role: 'user', content: user },
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`Local model API error ${response.status}: ${await response.text()}`);
|
|
63
|
+
}
|
|
64
|
+
const data = (await response.json());
|
|
65
|
+
const raw = data.message?.content ?? '';
|
|
66
|
+
return this.parseSingleQuestionResponse(raw, artifactType);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
//# sourceMappingURL=local.js.map
|
|
@@ -30,5 +30,6 @@ export declare class OllamaProvider extends BaseProvider {
|
|
|
30
30
|
* @returns Array of generated questions
|
|
31
31
|
*/
|
|
32
32
|
generateQuestions(artifactContent: string, questionCount: number, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Promise<Question[]>;
|
|
33
|
+
generateSingleQuestion(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): Promise<Question>;
|
|
33
34
|
}
|
|
34
35
|
//# sourceMappingURL=ollama.d.ts.map
|
|
@@ -35,19 +35,35 @@ export class OllamaProvider extends BaseProvider {
|
|
|
35
35
|
* @returns Array of generated questions
|
|
36
36
|
*/
|
|
37
37
|
async generateQuestions(artifactContent, questionCount, artifactType) {
|
|
38
|
-
// TODO: Implement Ollama API call
|
|
39
|
-
// For now, return placeholder questions
|
|
40
38
|
const questions = [];
|
|
41
39
|
for (let i = 0; i < questionCount; i++) {
|
|
42
|
-
questions.
|
|
43
|
-
|
|
44
|
-
options: ['Option A', 'Option B', 'Option C'],
|
|
45
|
-
correctOption: 0,
|
|
46
|
-
artifactSource: artifactType,
|
|
47
|
-
});
|
|
40
|
+
const q = await this.generateSingleQuestion(artifactContent, artifactType, i, questionCount, questions.map((q) => q.text));
|
|
41
|
+
questions.push(q);
|
|
48
42
|
}
|
|
49
|
-
this.validateQuestions(questions);
|
|
50
43
|
return questions;
|
|
51
44
|
}
|
|
45
|
+
async generateSingleQuestion(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts) {
|
|
46
|
+
const { system, user } = this.buildSingleQuestionPrompt(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts);
|
|
47
|
+
const baseUrl = this.config.endpoint ?? 'http://localhost:11434';
|
|
48
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'content-type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
model: this.config.model ?? 'llama2',
|
|
53
|
+
stream: false,
|
|
54
|
+
format: 'json',
|
|
55
|
+
messages: [
|
|
56
|
+
{ role: 'system', content: system },
|
|
57
|
+
{ role: 'user', content: user },
|
|
58
|
+
],
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok) {
|
|
62
|
+
throw new Error(`Ollama API error ${response.status}: ${await response.text()}`);
|
|
63
|
+
}
|
|
64
|
+
const data = (await response.json());
|
|
65
|
+
const raw = data.message?.content ?? '';
|
|
66
|
+
return this.parseSingleQuestionResponse(raw, artifactType);
|
|
67
|
+
}
|
|
52
68
|
}
|
|
53
69
|
//# sourceMappingURL=ollama.js.map
|
|
@@ -30,5 +30,6 @@ export declare class OpenAIProvider extends BaseProvider {
|
|
|
30
30
|
* @returns Array of generated questions
|
|
31
31
|
*/
|
|
32
32
|
generateQuestions(artifactContent: string, questionCount: number, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Promise<Question[]>;
|
|
33
|
+
generateSingleQuestion(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): Promise<Question>;
|
|
33
34
|
}
|
|
34
35
|
//# sourceMappingURL=openai.d.ts.map
|
|
@@ -35,19 +35,38 @@ export class OpenAIProvider extends BaseProvider {
|
|
|
35
35
|
* @returns Array of generated questions
|
|
36
36
|
*/
|
|
37
37
|
async generateQuestions(artifactContent, questionCount, artifactType) {
|
|
38
|
-
// TODO: Implement OpenAI API call
|
|
39
|
-
// For now, return placeholder questions
|
|
40
38
|
const questions = [];
|
|
41
39
|
for (let i = 0; i < questionCount; i++) {
|
|
42
|
-
questions.
|
|
43
|
-
|
|
44
|
-
options: ['Option A', 'Option B', 'Option C'],
|
|
45
|
-
correctOption: 0,
|
|
46
|
-
artifactSource: artifactType,
|
|
47
|
-
});
|
|
40
|
+
const q = await this.generateSingleQuestion(artifactContent, artifactType, i, questionCount, questions.map((q) => q.text));
|
|
41
|
+
questions.push(q);
|
|
48
42
|
}
|
|
49
|
-
this.validateQuestions(questions);
|
|
50
43
|
return questions;
|
|
51
44
|
}
|
|
45
|
+
async generateSingleQuestion(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts) {
|
|
46
|
+
const { system, user } = this.buildSingleQuestionPrompt(artifactContent, artifactType, questionIndex, totalQuestions, previousQuestionTexts);
|
|
47
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
51
|
+
'content-type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
model: this.config.model ?? 'gpt-4',
|
|
55
|
+
max_tokens: 512,
|
|
56
|
+
temperature: 0.7,
|
|
57
|
+
response_format: { type: 'json_object' },
|
|
58
|
+
messages: [
|
|
59
|
+
{ role: 'system', content: system },
|
|
60
|
+
{ role: 'user', content: user },
|
|
61
|
+
],
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
throw new Error(`OpenAI API error ${response.status}: ${await response.text()}`);
|
|
66
|
+
}
|
|
67
|
+
const data = (await response.json());
|
|
68
|
+
const raw = data.choices[0]?.message?.content ?? '';
|
|
69
|
+
return this.parseSingleQuestionResponse(raw, artifactType);
|
|
70
|
+
}
|
|
52
71
|
}
|
|
53
72
|
//# sourceMappingURL=openai.js.map
|
|
@@ -49,10 +49,58 @@ export declare function calculateQuestionAllocation(totalQuestions: number, arti
|
|
|
49
49
|
*/
|
|
50
50
|
export declare function generateQuestionsForArtifact(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionCount: number, provider: LLMProvider): Promise<Question[]>;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* On-demand question stream for the quiz executor.
|
|
53
53
|
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* This class resolves the artifact contents and allocation plan upfront, but
|
|
55
|
+
* NEVER pre-generates questions. Each call to `next()` fetches exactly ONE
|
|
56
|
+
* question from the LLM — only after the previous question has been answered.
|
|
57
|
+
*
|
|
58
|
+
* This enforces FR-10: questions are fetched on-demand, never pre-generated.
|
|
59
|
+
* The correct answer (correctOption) is held in process memory; it is never
|
|
60
|
+
* written to stdout, disk, or any external surface.
|
|
61
|
+
*/
|
|
62
|
+
export declare class QuizQuestionStream {
|
|
63
|
+
private provider;
|
|
64
|
+
private plan;
|
|
65
|
+
private totalQuestions;
|
|
66
|
+
private fetchedCount;
|
|
67
|
+
private askedTexts;
|
|
68
|
+
private constructor();
|
|
69
|
+
/**
|
|
70
|
+
* Create and initialise a QuizQuestionStream.
|
|
71
|
+
* Reads artifact files and builds the question plan, but does NOT generate any questions.
|
|
72
|
+
*
|
|
73
|
+
* @param artifactPaths - Object mapping artifact types to file paths
|
|
74
|
+
* @param totalQuestions - Total number of questions the quiz will ask
|
|
75
|
+
* @param provider - LLM provider (optional, uses fallback if not provided)
|
|
76
|
+
*/
|
|
77
|
+
static create(artifactPaths: {
|
|
78
|
+
proposal?: string;
|
|
79
|
+
design?: string;
|
|
80
|
+
specs?: string;
|
|
81
|
+
plan?: string;
|
|
82
|
+
tasks?: string;
|
|
83
|
+
}, totalQuestions: number, provider?: LLMProvider): Promise<QuizQuestionStream>;
|
|
84
|
+
/** Total number of questions this quiz will ask. */
|
|
85
|
+
get total(): number;
|
|
86
|
+
/** Whether there are more questions to fetch. */
|
|
87
|
+
get hasNext(): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Fetch the next question from the LLM.
|
|
90
|
+
* MUST only be called after the user has answered the current question.
|
|
91
|
+
* Only one question is held in memory at a time; this method discards the
|
|
92
|
+
* previous question before fetching the next.
|
|
93
|
+
*
|
|
94
|
+
* @returns The next Question, or null if all questions have been asked.
|
|
95
|
+
*/
|
|
96
|
+
next(): Promise<Question | null>;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Generate complete quiz question set for all artifacts.
|
|
100
|
+
*
|
|
101
|
+
* NOTE: This function pre-generates all questions and should only be used for
|
|
102
|
+
* non-interactive contexts (e.g. testing, pass recording). During live quiz
|
|
103
|
+
* execution use QuizQuestionStream.create() instead.
|
|
56
104
|
*
|
|
57
105
|
* @param artifactPaths - Object mapping artifact types to file paths
|
|
58
106
|
* @param totalQuestions - Total number of questions to generate
|
|
@@ -97,10 +97,93 @@ export async function generateQuestionsForArtifact(artifactContent, artifactType
|
|
|
97
97
|
return provider.generateQuestions(artifactContent, questionCount, artifactType);
|
|
98
98
|
}
|
|
99
99
|
/**
|
|
100
|
-
*
|
|
100
|
+
* On-demand question stream for the quiz executor.
|
|
101
101
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
102
|
+
* This class resolves the artifact contents and allocation plan upfront, but
|
|
103
|
+
* NEVER pre-generates questions. Each call to `next()` fetches exactly ONE
|
|
104
|
+
* question from the LLM — only after the previous question has been answered.
|
|
105
|
+
*
|
|
106
|
+
* This enforces FR-10: questions are fetched on-demand, never pre-generated.
|
|
107
|
+
* The correct answer (correctOption) is held in process memory; it is never
|
|
108
|
+
* written to stdout, disk, or any external surface.
|
|
109
|
+
*/
|
|
110
|
+
export class QuizQuestionStream {
|
|
111
|
+
provider;
|
|
112
|
+
plan = [];
|
|
113
|
+
totalQuestions = 0;
|
|
114
|
+
fetchedCount = 0;
|
|
115
|
+
askedTexts = [];
|
|
116
|
+
constructor(provider) {
|
|
117
|
+
this.provider = provider;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Create and initialise a QuizQuestionStream.
|
|
121
|
+
* Reads artifact files and builds the question plan, but does NOT generate any questions.
|
|
122
|
+
*
|
|
123
|
+
* @param artifactPaths - Object mapping artifact types to file paths
|
|
124
|
+
* @param totalQuestions - Total number of questions the quiz will ask
|
|
125
|
+
* @param provider - LLM provider (optional, uses fallback if not provided)
|
|
126
|
+
*/
|
|
127
|
+
static async create(artifactPaths, totalQuestions, provider) {
|
|
128
|
+
const llmProvider = provider ?? (await createProviderWithFallback());
|
|
129
|
+
const stream = new QuizQuestionStream(llmProvider);
|
|
130
|
+
// Read artifact contents
|
|
131
|
+
const artifactContents = {};
|
|
132
|
+
const artifactSizes = { proposal: 0, design: 0, specs: 0, plan: 0, tasks: 0 };
|
|
133
|
+
for (const [artifactType, filePath] of Object.entries(artifactPaths)) {
|
|
134
|
+
if (filePath) {
|
|
135
|
+
const content = await readArtifactContent(filePath);
|
|
136
|
+
artifactContents[artifactType] = content;
|
|
137
|
+
artifactSizes[artifactType] = content.length;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Build flat ordered plan: [artifactType, content] pairs for each question slot
|
|
141
|
+
const allocation = calculateQuestionAllocation(totalQuestions, artifactSizes);
|
|
142
|
+
for (const [artifactType, count] of Object.entries(allocation)) {
|
|
143
|
+
if (count > 0 && artifactContents[artifactType]) {
|
|
144
|
+
for (let i = 0; i < count; i++) {
|
|
145
|
+
stream.plan.push({
|
|
146
|
+
artifactType: artifactType,
|
|
147
|
+
content: artifactContents[artifactType],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
stream.totalQuestions = stream.plan.length;
|
|
153
|
+
return stream;
|
|
154
|
+
}
|
|
155
|
+
/** Total number of questions this quiz will ask. */
|
|
156
|
+
get total() {
|
|
157
|
+
return this.totalQuestions;
|
|
158
|
+
}
|
|
159
|
+
/** Whether there are more questions to fetch. */
|
|
160
|
+
get hasNext() {
|
|
161
|
+
return this.fetchedCount < this.totalQuestions;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Fetch the next question from the LLM.
|
|
165
|
+
* MUST only be called after the user has answered the current question.
|
|
166
|
+
* Only one question is held in memory at a time; this method discards the
|
|
167
|
+
* previous question before fetching the next.
|
|
168
|
+
*
|
|
169
|
+
* @returns The next Question, or null if all questions have been asked.
|
|
170
|
+
*/
|
|
171
|
+
async next() {
|
|
172
|
+
if (!this.hasNext)
|
|
173
|
+
return null;
|
|
174
|
+
const slot = this.plan[this.fetchedCount];
|
|
175
|
+
const question = await this.provider.generateSingleQuestion(slot.content, slot.artifactType, this.fetchedCount, this.totalQuestions, this.askedTexts);
|
|
176
|
+
this.fetchedCount += 1;
|
|
177
|
+
this.askedTexts.push(question.text);
|
|
178
|
+
return question;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Generate complete quiz question set for all artifacts.
|
|
183
|
+
*
|
|
184
|
+
* NOTE: This function pre-generates all questions and should only be used for
|
|
185
|
+
* non-interactive contexts (e.g. testing, pass recording). During live quiz
|
|
186
|
+
* execution use QuizQuestionStream.create() instead.
|
|
104
187
|
*
|
|
105
188
|
* @param artifactPaths - Object mapping artifact types to file paths
|
|
106
189
|
* @param totalQuestions - Total number of questions to generate
|
|
@@ -108,33 +191,12 @@ export async function generateQuestionsForArtifact(artifactContent, artifactType
|
|
|
108
191
|
* @returns Array of all generated questions
|
|
109
192
|
*/
|
|
110
193
|
export async function generateQuizQuestions(artifactPaths, totalQuestions, provider) {
|
|
111
|
-
|
|
112
|
-
const llmProvider = provider || await createProviderWithFallback();
|
|
113
|
-
// Read all artifact contents and calculate sizes
|
|
114
|
-
const artifactContents = {};
|
|
115
|
-
const artifactSizes = {
|
|
116
|
-
proposal: 0,
|
|
117
|
-
design: 0,
|
|
118
|
-
specs: 0,
|
|
119
|
-
plan: 0,
|
|
120
|
-
tasks: 0,
|
|
121
|
-
};
|
|
122
|
-
for (const [artifactType, filePath] of Object.entries(artifactPaths)) {
|
|
123
|
-
if (filePath) {
|
|
124
|
-
const content = await readArtifactContent(filePath);
|
|
125
|
-
artifactContents[artifactType] = content;
|
|
126
|
-
artifactSizes[artifactType] = content.length;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
// Calculate question allocation
|
|
130
|
-
const allocation = calculateQuestionAllocation(totalQuestions, artifactSizes);
|
|
131
|
-
// Generate questions for each artifact type
|
|
194
|
+
const stream = await QuizQuestionStream.create(artifactPaths, totalQuestions, provider);
|
|
132
195
|
const allQuestions = [];
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
allQuestions.push(
|
|
137
|
-
}
|
|
196
|
+
while (stream.hasNext) {
|
|
197
|
+
const q = await stream.next();
|
|
198
|
+
if (q)
|
|
199
|
+
allQuestions.push(q);
|
|
138
200
|
}
|
|
139
201
|
return allQuestions;
|
|
140
202
|
}
|