@codewalla_india/openspec 1.3.2 → 1.3.3
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/modify-change.js +233 -233
- package/package.json +1 -1
- package/schemas/spec-driven/templates/plan-template.md +91 -91
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module comprehension-quiz/quiz-executor
|
|
9
9
|
*/
|
|
10
|
-
import type {
|
|
10
|
+
import type { QuizResult } from './types.js';
|
|
11
|
+
import type { QuizQuestionStream } from './question-generator.js';
|
|
11
12
|
/**
|
|
12
13
|
* Quiz executor class
|
|
13
14
|
*
|
|
@@ -17,54 +18,58 @@ import type { Question, QuizResult } from './types.js';
|
|
|
17
18
|
*/
|
|
18
19
|
export declare class QuizExecutor {
|
|
19
20
|
private state;
|
|
20
|
-
private
|
|
21
|
-
private
|
|
22
|
-
private answers;
|
|
21
|
+
private totalQuestions;
|
|
22
|
+
private correctCount;
|
|
23
23
|
private questionDisplayedAt;
|
|
24
|
-
private answerReceivedAt;
|
|
25
24
|
private changeName;
|
|
26
25
|
private attemptNumber;
|
|
26
|
+
/** Minimum ms the question must be visible before ANY input is accepted (FR-4, NFR-4). */
|
|
27
|
+
private static readonly DISPLAY_LOCK_MS;
|
|
28
|
+
/** Minimum ms between question display and a valid answer (FR-9). */
|
|
29
|
+
private static readonly MIN_ANSWER_MS;
|
|
27
30
|
/**
|
|
28
|
-
* Execute quiz
|
|
31
|
+
* Execute quiz using an on-demand question stream.
|
|
29
32
|
*
|
|
30
|
-
*
|
|
33
|
+
* Questions are fetched one at a time from the stream — the next question is
|
|
34
|
+
* NOT fetched until the user has answered the current one. This means at most
|
|
35
|
+
* one Question object is in memory at any point, and its correctOption never
|
|
36
|
+
* appears in stdout.
|
|
37
|
+
*
|
|
38
|
+
* @param stream - QuizQuestionStream supplying one question per call
|
|
31
39
|
* @param changeName - Name of the change being tested
|
|
32
40
|
* @returns Quiz result with score and pass/fail status
|
|
33
41
|
*/
|
|
34
|
-
executeQuiz(
|
|
35
|
-
/**
|
|
36
|
-
* Transition to a new state
|
|
37
|
-
*
|
|
38
|
-
* @param newState - New state to transition to
|
|
39
|
-
*/
|
|
42
|
+
executeQuiz(stream: QuizQuestionStream, changeName: string): Promise<QuizResult>;
|
|
40
43
|
private transitionTo;
|
|
41
44
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
+
* Display a question to the user.
|
|
46
|
+
* Records the display timestamp for timing enforcement.
|
|
47
|
+
* correctOption is NOT printed — it stays in process memory only.
|
|
45
48
|
*/
|
|
46
|
-
private
|
|
49
|
+
private displayQuestion;
|
|
47
50
|
/**
|
|
48
|
-
*
|
|
51
|
+
* Collect a valid answer from stdin, looping until one is received.
|
|
49
52
|
*
|
|
50
|
-
* Enforces
|
|
51
|
-
*
|
|
53
|
+
* Enforces:
|
|
54
|
+
* - 4-second display lock: any input before 4s is silently discarded (FR-4 / NFR-4)
|
|
55
|
+
* - Batch input rejection: multiple newline-separated values in one write → re-prompt
|
|
56
|
+
* - Skip/show-all rejection: any non-option text → "Answer this question to continue."
|
|
57
|
+
* - 2-second minimum read time: answers faster than 2s after display → re-prompt (FR-9)
|
|
52
58
|
*
|
|
53
|
-
* @returns
|
|
59
|
+
* @returns The user's selected option number (1, 2, or 3)
|
|
54
60
|
*/
|
|
55
|
-
private
|
|
61
|
+
private collectValidAnswer;
|
|
56
62
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* @param questionIndex - Index of the question
|
|
60
|
-
* @param answer - User's answer
|
|
63
|
+
* Read a single line from stdin without echoing the prompt.
|
|
64
|
+
* Uses raw mode to capture the full write in one shot so batch detection works.
|
|
61
65
|
*/
|
|
62
|
-
private
|
|
66
|
+
private readLine;
|
|
63
67
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
68
|
+
* Show pass/fail feedback for a single answer.
|
|
69
|
+
* Does NOT reveal the correctOption number to stdout if the user was correct —
|
|
70
|
+
* only shows "Correct" or "Incorrect" with the correct option disclosed on wrong answers.
|
|
67
71
|
*/
|
|
68
|
-
private
|
|
72
|
+
private showFeedback;
|
|
73
|
+
private sleep;
|
|
69
74
|
}
|
|
70
75
|
//# sourceMappingURL=quiz-executor.d.ts.map
|
|
@@ -19,147 +19,163 @@ import { getPassRecord } from './pass-record.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export class QuizExecutor {
|
|
21
21
|
state = 'START';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
answers = [];
|
|
22
|
+
totalQuestions = 0;
|
|
23
|
+
correctCount = 0;
|
|
25
24
|
questionDisplayedAt = 0;
|
|
26
|
-
answerReceivedAt = 0;
|
|
27
25
|
changeName = '';
|
|
28
26
|
attemptNumber = 1;
|
|
27
|
+
/** Minimum ms the question must be visible before ANY input is accepted (FR-4, NFR-4). */
|
|
28
|
+
static DISPLAY_LOCK_MS = 4000;
|
|
29
|
+
/** Minimum ms between question display and a valid answer (FR-9). */
|
|
30
|
+
static MIN_ANSWER_MS = 2000;
|
|
29
31
|
/**
|
|
30
|
-
* Execute quiz
|
|
32
|
+
* Execute quiz using an on-demand question stream.
|
|
31
33
|
*
|
|
32
|
-
*
|
|
34
|
+
* Questions are fetched one at a time from the stream — the next question is
|
|
35
|
+
* NOT fetched until the user has answered the current one. This means at most
|
|
36
|
+
* one Question object is in memory at any point, and its correctOption never
|
|
37
|
+
* appears in stdout.
|
|
38
|
+
*
|
|
39
|
+
* @param stream - QuizQuestionStream supplying one question per call
|
|
33
40
|
* @param changeName - Name of the change being tested
|
|
34
41
|
* @returns Quiz result with score and pass/fail status
|
|
35
42
|
*/
|
|
36
|
-
async executeQuiz(
|
|
37
|
-
this.questions = questions;
|
|
43
|
+
async executeQuiz(stream, changeName) {
|
|
38
44
|
this.changeName = changeName;
|
|
39
|
-
this.
|
|
40
|
-
this.
|
|
45
|
+
this.totalQuestions = stream.total;
|
|
46
|
+
this.correctCount = 0;
|
|
41
47
|
this.state = 'START';
|
|
42
|
-
// Track attempt number from existing pass record
|
|
43
48
|
const existingRecord = await getPassRecord(changeName);
|
|
44
49
|
this.attemptNumber = existingRecord ? existingRecord.attemptCount + 1 : 1;
|
|
45
|
-
// Track attempt start
|
|
46
50
|
await trackComprehensionAttempt(changeName, this.attemptNumber);
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
let questionIndex = 0;
|
|
52
|
+
while (stream.hasNext) {
|
|
53
|
+
await this.transitionTo(`WAITING_FOR_QUESTION_${questionIndex + 1}`);
|
|
54
|
+
// Fetch exactly one question — only now, not before
|
|
55
|
+
const question = await stream.next();
|
|
56
|
+
if (!question)
|
|
57
|
+
break;
|
|
58
|
+
this.displayQuestion(question, questionIndex);
|
|
59
|
+
// Loop until a valid answer is received (re-prompt on any invalid/early/batch/skip input)
|
|
60
|
+
const userAnswer = await this.collectValidAnswer(question);
|
|
61
|
+
await this.transitionTo('ACCEPTING_ANSWER');
|
|
62
|
+
if (userAnswer === question.correctOption + 1) {
|
|
63
|
+
this.correctCount += 1;
|
|
56
64
|
}
|
|
65
|
+
await this.transitionTo('FEEDBACK');
|
|
66
|
+
this.showFeedback(question, userAnswer);
|
|
67
|
+
questionIndex += 1;
|
|
57
68
|
}
|
|
58
69
|
await this.transitionTo('DONE');
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
70
|
+
const scorePercent = this.totalQuestions > 0
|
|
71
|
+
? Math.round((this.correctCount / this.totalQuestions) * 100)
|
|
72
|
+
: 0;
|
|
73
|
+
const passed = scorePercent >= 80;
|
|
74
|
+
console.log(`\n${'─'.repeat(50)}`);
|
|
75
|
+
console.log(`Result: ${this.correctCount}/${this.totalQuestions} correct (${scorePercent}%)`);
|
|
76
|
+
console.log(passed ? '✓ PASS — apply is now unblocked.' : '✗ FAIL — retry to proceed.');
|
|
77
|
+
console.log('─'.repeat(50));
|
|
78
|
+
await trackComprehensionCompletion(changeName, scorePercent, passed ? 'pass' : 'fail', this.attemptNumber);
|
|
79
|
+
if (passed) {
|
|
64
80
|
await trackApplyReady(changeName);
|
|
65
81
|
}
|
|
66
|
-
return
|
|
82
|
+
return {
|
|
83
|
+
correct: this.correctCount,
|
|
84
|
+
total: this.totalQuestions,
|
|
85
|
+
scorePercent,
|
|
86
|
+
passed,
|
|
87
|
+
attemptNumber: this.attemptNumber,
|
|
88
|
+
};
|
|
67
89
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Transition to a new state
|
|
70
|
-
*
|
|
71
|
-
* @param newState - New state to transition to
|
|
72
|
-
*/
|
|
73
90
|
async transitionTo(newState) {
|
|
74
91
|
this.state = newState;
|
|
75
92
|
}
|
|
76
93
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
94
|
+
* Display a question to the user.
|
|
95
|
+
* Records the display timestamp for timing enforcement.
|
|
96
|
+
* correctOption is NOT printed — it stays in process memory only.
|
|
80
97
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log(
|
|
98
|
+
displayQuestion(question, index) {
|
|
99
|
+
console.clear();
|
|
100
|
+
console.log(`Question ${index + 1} of ${this.totalQuestions}\n`);
|
|
84
101
|
console.log(question.text);
|
|
85
102
|
console.log('');
|
|
86
|
-
console.log(
|
|
87
|
-
console.log(
|
|
88
|
-
console.log(
|
|
103
|
+
console.log(` 1) ${question.options[0]}`);
|
|
104
|
+
console.log(` 2) ${question.options[1]}`);
|
|
105
|
+
console.log(` 3) ${question.options[2]}`);
|
|
89
106
|
console.log('');
|
|
90
107
|
this.questionDisplayedAt = Date.now();
|
|
91
108
|
}
|
|
92
109
|
/**
|
|
93
|
-
*
|
|
110
|
+
* Collect a valid answer from stdin, looping until one is received.
|
|
94
111
|
*
|
|
95
|
-
* Enforces
|
|
96
|
-
*
|
|
112
|
+
* Enforces:
|
|
113
|
+
* - 4-second display lock: any input before 4s is silently discarded (FR-4 / NFR-4)
|
|
114
|
+
* - Batch input rejection: multiple newline-separated values in one write → re-prompt
|
|
115
|
+
* - Skip/show-all rejection: any non-option text → "Answer this question to continue."
|
|
116
|
+
* - 2-second minimum read time: answers faster than 2s after display → re-prompt (FR-9)
|
|
97
117
|
*
|
|
98
|
-
* @returns
|
|
118
|
+
* @returns The user's selected option number (1, 2, or 3)
|
|
99
119
|
*/
|
|
100
|
-
async
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
120
|
+
async collectValidAnswer(question) {
|
|
121
|
+
while (true) {
|
|
122
|
+
const elapsed = Date.now() - this.questionDisplayedAt;
|
|
123
|
+
if (elapsed < QuizExecutor.DISPLAY_LOCK_MS) {
|
|
124
|
+
// Still within the 4-second display lock — wait out the remainder silently
|
|
125
|
+
await this.sleep(QuizExecutor.DISPLAY_LOCK_MS - elapsed);
|
|
126
|
+
}
|
|
127
|
+
const raw = await this.readLine();
|
|
128
|
+
const answerReceivedAt = Date.now();
|
|
129
|
+
// Reject batch input: more than one non-empty line in a single write
|
|
130
|
+
if (raw.includes('\n') && raw.split('\n').filter((l) => l.trim()).length > 1) {
|
|
131
|
+
console.log('Answer this question to continue.');
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const trimmed = raw.trim();
|
|
135
|
+
// Reject empty, skip commands, show-all, or anything not "1" / "2" / "3"
|
|
136
|
+
if (trimmed === '' || !['1', '2', '3'].includes(trimmed)) {
|
|
137
|
+
console.log('Answer this question to continue.');
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// Enforce minimum 2-second read time after question was displayed
|
|
141
|
+
const timeSinceDisplay = answerReceivedAt - this.questionDisplayedAt;
|
|
142
|
+
if (timeSinceDisplay < QuizExecutor.MIN_ANSWER_MS) {
|
|
143
|
+
console.log('Answer this question to continue.');
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
return parseInt(trimmed, 10);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Read a single line from stdin without echoing the prompt.
|
|
151
|
+
* Uses raw mode to capture the full write in one shot so batch detection works.
|
|
152
|
+
*/
|
|
153
|
+
readLine() {
|
|
154
|
+
return new Promise((resolve) => {
|
|
155
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
156
|
+
rl.question('Enter 1, 2, or 3: ', (answer) => {
|
|
107
157
|
rl.close();
|
|
108
|
-
|
|
109
|
-
// Check if answer was submitted too early (less than 2 seconds after question display)
|
|
110
|
-
const timeSinceDisplay = Date.now() - this.questionDisplayedAt;
|
|
111
|
-
if (timeSinceDisplay < 2000) {
|
|
112
|
-
console.log('Answer submitted too early. Please wait at least 2 seconds before answering.');
|
|
113
|
-
resolve(-1);
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
// Validate input
|
|
117
|
-
if (trimmed === '1' || trimmed === '2' || trimmed === '3') {
|
|
118
|
-
resolve(parseInt(trimmed, 10));
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
console.log('Answer this question to continue.');
|
|
122
|
-
resolve(-1);
|
|
123
|
-
}
|
|
158
|
+
resolve(answer);
|
|
124
159
|
});
|
|
125
160
|
});
|
|
126
|
-
return answer;
|
|
127
161
|
}
|
|
128
162
|
/**
|
|
129
|
-
* Show feedback
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* @param answer - User's answer
|
|
163
|
+
* Show pass/fail feedback for a single answer.
|
|
164
|
+
* Does NOT reveal the correctOption number to stdout if the user was correct —
|
|
165
|
+
* only shows "Correct" or "Incorrect" with the correct option disclosed on wrong answers.
|
|
133
166
|
*/
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const isCorrect = answer === question.correctOption + 1;
|
|
167
|
+
showFeedback(question, userAnswer) {
|
|
168
|
+
const isCorrect = userAnswer === question.correctOption + 1;
|
|
137
169
|
if (isCorrect) {
|
|
138
|
-
console.log('✓ Correct');
|
|
170
|
+
console.log('\n✓ Correct\n');
|
|
139
171
|
}
|
|
140
172
|
else {
|
|
141
|
-
console.log('✗ Incorrect');
|
|
142
|
-
console.log(`The correct answer was ${question.correctOption + 1}`);
|
|
173
|
+
console.log('\n✗ Incorrect');
|
|
174
|
+
console.log(` The correct answer was option ${question.correctOption + 1}.\n`);
|
|
143
175
|
}
|
|
144
|
-
// Clear terminal screen
|
|
145
|
-
console.clear();
|
|
146
176
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
* @returns Quiz result with score and pass/fail status
|
|
151
|
-
*/
|
|
152
|
-
calculateResult() {
|
|
153
|
-
const correct = this.answers.filter((answer, index) => answer === this.questions[index].correctOption + 1).length;
|
|
154
|
-
const total = this.questions.length;
|
|
155
|
-
const scorePercent = Math.round((correct / total) * 100);
|
|
156
|
-
return {
|
|
157
|
-
correct,
|
|
158
|
-
total,
|
|
159
|
-
scorePercent,
|
|
160
|
-
passed: scorePercent >= 80,
|
|
161
|
-
attemptNumber: 1, // TODO: Track attempt count
|
|
162
|
-
};
|
|
177
|
+
sleep(ms) {
|
|
178
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
163
179
|
}
|
|
164
180
|
}
|
|
165
181
|
//# sourceMappingURL=quiz-executor.js.map
|
|
@@ -72,6 +72,28 @@ export interface LLMProvider {
|
|
|
72
72
|
* @returns Array of generated questions
|
|
73
73
|
*/
|
|
74
74
|
generateQuestions(artifactContent: string, questionCount: number, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks'): Promise<Question[]>;
|
|
75
|
+
/**
|
|
76
|
+
* Generate a single quiz question on-demand from artifact content.
|
|
77
|
+
*
|
|
78
|
+
* This method MUST be used during quiz execution to fetch one question at a time.
|
|
79
|
+
* The correct answer index is included in the returned Question object so the CLI
|
|
80
|
+
* process can validate the user's answer locally. The correct answer MUST NOT be
|
|
81
|
+
* included in any conversational context or stdout — it stays in process memory only.
|
|
82
|
+
*
|
|
83
|
+
* The LLM prompt used by implementors MUST:
|
|
84
|
+
* 1. Ask for exactly ONE question, not a list or array.
|
|
85
|
+
* 2. Return a JSON object with: text, options (3 strings), correctOption (0-2), artifactSource.
|
|
86
|
+
* 3. NEVER ask the LLM to explain the answer or include the answer text in any prose.
|
|
87
|
+
* 4. Use a system message that forbids the model from listing multiple questions.
|
|
88
|
+
*
|
|
89
|
+
* @param artifactContent - The content of the artifact to generate from
|
|
90
|
+
* @param artifactType - Type of artifact (proposal, design, specs, plan, tasks)
|
|
91
|
+
* @param questionIndex - 0-based index of this question in the quiz (for diversity)
|
|
92
|
+
* @param totalQuestions - Total number of questions in the quiz (for context)
|
|
93
|
+
* @param previousQuestionTexts - Texts of already-asked questions to avoid duplicates
|
|
94
|
+
* @returns A single generated question (text + options + correctOption + artifactSource)
|
|
95
|
+
*/
|
|
96
|
+
generateSingleQuestion(artifactContent: string, artifactType: 'proposal' | 'design' | 'specs' | 'plan' | 'tasks', questionIndex: number, totalQuestions: number, previousQuestionTexts: string[]): Promise<Question>;
|
|
75
97
|
/**
|
|
76
98
|
* Validate provider configuration
|
|
77
99
|
* @throws Error if configuration is invalid
|