@iservu-inc/adf-cli 0.3.0 → 0.4.12
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/.project/chats/{current → complete}/2025-10-03_AGENTS-MD-AND-TOOL-GENERATORS.md +82 -17
- package/.project/chats/complete/2025-10-03_AI-PROVIDER-INTEGRATION.md +568 -0
- package/.project/chats/complete/2025-10-03_FRAMEWORK-UPDATE-SYSTEM.md +497 -0
- package/.project/chats/complete/2025-10-04_CONFIG-COMMAND.md +503 -0
- package/.project/chats/current/2025-10-04_PHASE-4-1-SMART-FILTERING.md +381 -0
- package/.project/chats/current/SESSION-STATUS.md +168 -0
- package/.project/docs/AI-PROVIDER-INTEGRATION.md +600 -0
- package/.project/docs/FRAMEWORK-UPDATE-INTEGRATION.md +421 -0
- package/.project/docs/FRAMEWORK-UPDATE-SYSTEM.md +832 -0
- package/.project/docs/PHASE-4-2-LEARNING-SYSTEM.md +881 -0
- package/.project/docs/PROJECT-STRUCTURE-EXPLANATION.md +500 -0
- package/.project/docs/SMART-FILTERING-SYSTEM.md +385 -0
- package/.project/docs/architecture/SYSTEM-DESIGN.md +122 -1
- package/.project/docs/goals/PROJECT-VISION.md +61 -34
- package/CHANGELOG.md +257 -1
- package/README.md +476 -292
- package/bin/adf.js +7 -0
- package/lib/ai/ai-client.js +328 -0
- package/lib/ai/ai-config.js +398 -0
- package/lib/analyzers/project-analyzer.js +380 -0
- package/lib/commands/config.js +221 -0
- package/lib/commands/init.js +56 -10
- package/lib/filters/question-filter.js +480 -0
- package/lib/frameworks/interviewer.js +271 -12
- package/lib/frameworks/progress-tracker.js +8 -1
- package/lib/learning/learning-manager.js +447 -0
- package/lib/learning/pattern-detector.js +376 -0
- package/lib/learning/rule-generator.js +304 -0
- package/lib/learning/skip-tracker.js +260 -0
- package/lib/learning/storage.js +296 -0
- package/package.json +70 -57
- package/tests/learning-storage.test.js +184 -0
- package/tests/pattern-detector.test.js +297 -0
- package/tests/project-analyzer.test.js +221 -0
- package/tests/question-filter.test.js +297 -0
- package/tests/skip-tracker.test.js +198 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
const storage = require('./storage');
|
|
2
|
+
const { v4: uuidv4 } = require('uuid');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Pattern Detector - Analyzes skip/answer history to detect patterns
|
|
6
|
+
*
|
|
7
|
+
* Detects:
|
|
8
|
+
* - Consistent skip patterns (same question skipped repeatedly)
|
|
9
|
+
* - Category skip patterns (user skips most questions in a category)
|
|
10
|
+
* - Framework-specific patterns (framework + question combinations)
|
|
11
|
+
* - User preferences (answer style, detail level)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class PatternDetector {
|
|
15
|
+
constructor(skipHistory, answerHistory, config = {}) {
|
|
16
|
+
this.skipHistory = skipHistory;
|
|
17
|
+
this.answerHistory = answerHistory;
|
|
18
|
+
this.config = {
|
|
19
|
+
minSessionsForPattern: config.minSessionsForPattern || 3,
|
|
20
|
+
minConfidenceForAutoFilter: config.minConfidenceForAutoFilter || 75,
|
|
21
|
+
...config
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detect all patterns
|
|
27
|
+
* @returns {Object} All detected patterns
|
|
28
|
+
*/
|
|
29
|
+
detectAllPatterns() {
|
|
30
|
+
return {
|
|
31
|
+
consistentSkips: this.detectConsistentSkips(),
|
|
32
|
+
categoryPatterns: this.detectCategoryPatterns(),
|
|
33
|
+
frameworkPatterns: this.detectFrameworkPatterns(),
|
|
34
|
+
userPreferences: this.detectUserPreferences()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Detect consistent skip patterns
|
|
40
|
+
* @returns {Array} Consistent skip patterns
|
|
41
|
+
*/
|
|
42
|
+
detectConsistentSkips() {
|
|
43
|
+
const patterns = [];
|
|
44
|
+
const questionSkipData = this.aggregateQuestionSkips();
|
|
45
|
+
|
|
46
|
+
for (const [questionId, data] of Object.entries(questionSkipData)) {
|
|
47
|
+
const totalSessions = this.skipHistory.sessions.length;
|
|
48
|
+
const skipCount = data.skipCount;
|
|
49
|
+
const confidence = Math.round((skipCount / totalSessions) * 100);
|
|
50
|
+
|
|
51
|
+
// Pattern threshold: skipped in 75%+ of sessions with minimum sessions
|
|
52
|
+
if (skipCount >= this.config.minSessionsForPattern && confidence >= 75) {
|
|
53
|
+
patterns.push({
|
|
54
|
+
id: `pattern_skip_${questionId}`,
|
|
55
|
+
type: 'consistent_skip',
|
|
56
|
+
questionId,
|
|
57
|
+
questionText: data.text,
|
|
58
|
+
category: data.category,
|
|
59
|
+
confidence,
|
|
60
|
+
sessionsAnalyzed: totalSessions,
|
|
61
|
+
skipCount,
|
|
62
|
+
recommendation: confidence >= this.config.minConfidenceForAutoFilter
|
|
63
|
+
? 'Auto-filter this question'
|
|
64
|
+
: 'Monitor for more sessions',
|
|
65
|
+
status: 'detected',
|
|
66
|
+
userApproved: false
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return patterns.sort((a, b) => b.confidence - a.confidence);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Detect category skip patterns
|
|
76
|
+
* @returns {Array} Category skip patterns
|
|
77
|
+
*/
|
|
78
|
+
detectCategoryPatterns() {
|
|
79
|
+
const patterns = [];
|
|
80
|
+
const categoryData = this.aggregateCategoryData();
|
|
81
|
+
|
|
82
|
+
for (const [category, data] of Object.entries(categoryData)) {
|
|
83
|
+
if (data.totalQuestions === 0) continue;
|
|
84
|
+
|
|
85
|
+
const skipRate = Math.round((data.skipCount / data.totalQuestions) * 100);
|
|
86
|
+
|
|
87
|
+
// Pattern threshold: 70%+ skip rate in category with minimum appearances
|
|
88
|
+
if (data.totalQuestions >= 5 && skipRate >= 70) {
|
|
89
|
+
patterns.push({
|
|
90
|
+
id: `pattern_category_${category}`,
|
|
91
|
+
type: 'category_skip',
|
|
92
|
+
category,
|
|
93
|
+
confidence: skipRate,
|
|
94
|
+
totalQuestions: data.totalQuestions,
|
|
95
|
+
skipCount: data.skipCount,
|
|
96
|
+
recommendation: skipRate >= this.config.minConfidenceForAutoFilter
|
|
97
|
+
? `Reduce relevance for ${category} questions`
|
|
98
|
+
: 'Monitor category skip behavior',
|
|
99
|
+
status: 'detected',
|
|
100
|
+
userApproved: false
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return patterns.sort((a, b) => b.confidence - a.confidence);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Detect framework-specific skip patterns
|
|
110
|
+
* @returns {Array} Framework skip patterns
|
|
111
|
+
*/
|
|
112
|
+
detectFrameworkPatterns() {
|
|
113
|
+
const patterns = [];
|
|
114
|
+
const frameworkSkipData = this.aggregateFrameworkSkips();
|
|
115
|
+
|
|
116
|
+
for (const [framework, questions] of Object.entries(frameworkSkipData)) {
|
|
117
|
+
for (const [questionId, data] of Object.entries(questions)) {
|
|
118
|
+
const frameworkSessions = this.skipHistory.sessions.filter(
|
|
119
|
+
s => s.frameworks && s.frameworks.includes(framework)
|
|
120
|
+
).length;
|
|
121
|
+
|
|
122
|
+
if (frameworkSessions < this.config.minSessionsForPattern) continue;
|
|
123
|
+
|
|
124
|
+
const confidence = Math.round((data.skipCount / frameworkSessions) * 100);
|
|
125
|
+
|
|
126
|
+
// Pattern threshold: 75%+ skip rate for framework + question combo
|
|
127
|
+
if (confidence >= 75) {
|
|
128
|
+
patterns.push({
|
|
129
|
+
id: `pattern_framework_${framework}_${questionId}`,
|
|
130
|
+
type: 'framework_skip',
|
|
131
|
+
framework,
|
|
132
|
+
questionId,
|
|
133
|
+
questionText: data.text,
|
|
134
|
+
category: data.category,
|
|
135
|
+
confidence,
|
|
136
|
+
sessionsAnalyzed: frameworkSessions,
|
|
137
|
+
skipCount: data.skipCount,
|
|
138
|
+
recommendation: `Skip "${data.text}" for ${framework} projects`,
|
|
139
|
+
status: 'detected',
|
|
140
|
+
userApproved: false
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return patterns.sort((a, b) => b.confidence - a.confidence);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Detect user preference patterns
|
|
151
|
+
* @returns {Array} User preference patterns
|
|
152
|
+
*/
|
|
153
|
+
detectUserPreferences() {
|
|
154
|
+
const patterns = [];
|
|
155
|
+
|
|
156
|
+
if (!this.answerHistory || !this.answerHistory.sessions) {
|
|
157
|
+
return patterns;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Analyze answer length by category
|
|
161
|
+
const categoryAnswerData = {};
|
|
162
|
+
|
|
163
|
+
for (const session of this.answerHistory.sessions) {
|
|
164
|
+
for (const answer of session.answers || []) {
|
|
165
|
+
if (!answer.category) continue;
|
|
166
|
+
|
|
167
|
+
if (!categoryAnswerData[answer.category]) {
|
|
168
|
+
categoryAnswerData[answer.category] = {
|
|
169
|
+
totalAnswers: 0,
|
|
170
|
+
totalWords: 0,
|
|
171
|
+
avgWords: 0
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
categoryAnswerData[answer.category].totalAnswers++;
|
|
176
|
+
categoryAnswerData[answer.category].totalWords += answer.wordCount || 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Detect brief answer preferences
|
|
181
|
+
for (const [category, data] of Object.entries(categoryAnswerData)) {
|
|
182
|
+
if (data.totalAnswers < 3) continue;
|
|
183
|
+
|
|
184
|
+
data.avgWords = Math.round(data.totalWords / data.totalAnswers);
|
|
185
|
+
|
|
186
|
+
// Brief answers: <30 words on average
|
|
187
|
+
if (data.avgWords < 30) {
|
|
188
|
+
patterns.push({
|
|
189
|
+
id: `pattern_pref_brief_${category}`,
|
|
190
|
+
type: 'user_preference',
|
|
191
|
+
category,
|
|
192
|
+
preference: 'brief_answers',
|
|
193
|
+
avgWordCount: data.avgWords,
|
|
194
|
+
confidence: 70,
|
|
195
|
+
recommendation: `User prefers brief answers for ${category} questions`,
|
|
196
|
+
status: 'detected',
|
|
197
|
+
userApproved: false
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
// Detailed answers: >100 words on average
|
|
201
|
+
else if (data.avgWords > 100) {
|
|
202
|
+
patterns.push({
|
|
203
|
+
id: `pattern_pref_detailed_${category}`,
|
|
204
|
+
type: 'user_preference',
|
|
205
|
+
category,
|
|
206
|
+
preference: 'detailed_answers',
|
|
207
|
+
avgWordCount: data.avgWords,
|
|
208
|
+
confidence: 70,
|
|
209
|
+
recommendation: `User prefers detailed answers for ${category} questions`,
|
|
210
|
+
status: 'detected',
|
|
211
|
+
userApproved: false
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return patterns;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Aggregate question skip data
|
|
221
|
+
* @returns {Object} Question skip data
|
|
222
|
+
*/
|
|
223
|
+
aggregateQuestionSkips() {
|
|
224
|
+
const questionData = {};
|
|
225
|
+
|
|
226
|
+
for (const session of this.skipHistory.sessions) {
|
|
227
|
+
const sessionQuestions = new Set(); // Track unique questions per session
|
|
228
|
+
|
|
229
|
+
for (const skip of session.skips || []) {
|
|
230
|
+
// Only count manual skips for pattern detection
|
|
231
|
+
if (skip.reason !== 'manual') continue;
|
|
232
|
+
|
|
233
|
+
if (!questionData[skip.questionId]) {
|
|
234
|
+
questionData[skip.questionId] = {
|
|
235
|
+
text: skip.text,
|
|
236
|
+
category: skip.category,
|
|
237
|
+
skipCount: 0,
|
|
238
|
+
sessions: []
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Only count once per session
|
|
243
|
+
if (!sessionQuestions.has(skip.questionId)) {
|
|
244
|
+
questionData[skip.questionId].skipCount++;
|
|
245
|
+
questionData[skip.questionId].sessions.push(session.sessionId);
|
|
246
|
+
sessionQuestions.add(skip.questionId);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return questionData;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Aggregate category data (total questions vs skips)
|
|
256
|
+
* @returns {Object} Category data
|
|
257
|
+
*/
|
|
258
|
+
aggregateCategoryData() {
|
|
259
|
+
const categoryData = {};
|
|
260
|
+
|
|
261
|
+
for (const session of this.skipHistory.sessions) {
|
|
262
|
+
// Count all questions by category (skipped + answered)
|
|
263
|
+
const sessionCategories = {};
|
|
264
|
+
|
|
265
|
+
for (const skip of session.skips || []) {
|
|
266
|
+
if (!skip.category) continue;
|
|
267
|
+
|
|
268
|
+
if (!sessionCategories[skip.category]) {
|
|
269
|
+
sessionCategories[skip.category] = { total: 0, skipped: 0 };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
sessionCategories[skip.category].total++;
|
|
273
|
+
if (skip.reason === 'manual') {
|
|
274
|
+
sessionCategories[skip.category].skipped++;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Aggregate across sessions
|
|
279
|
+
for (const [category, counts] of Object.entries(sessionCategories)) {
|
|
280
|
+
if (!categoryData[category]) {
|
|
281
|
+
categoryData[category] = { totalQuestions: 0, skipCount: 0 };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
categoryData[category].totalQuestions += counts.total;
|
|
285
|
+
categoryData[category].skipCount += counts.skipped;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return categoryData;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Aggregate framework-specific skip data
|
|
294
|
+
* @returns {Object} Framework skip data
|
|
295
|
+
*/
|
|
296
|
+
aggregateFrameworkSkips() {
|
|
297
|
+
const frameworkData = {};
|
|
298
|
+
|
|
299
|
+
for (const session of this.skipHistory.sessions) {
|
|
300
|
+
const frameworks = session.frameworks || [];
|
|
301
|
+
|
|
302
|
+
for (const framework of frameworks) {
|
|
303
|
+
if (!frameworkData[framework]) {
|
|
304
|
+
frameworkData[framework] = {};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const skip of session.skips || []) {
|
|
308
|
+
if (skip.reason !== 'manual') continue;
|
|
309
|
+
|
|
310
|
+
if (!frameworkData[framework][skip.questionId]) {
|
|
311
|
+
frameworkData[framework][skip.questionId] = {
|
|
312
|
+
text: skip.text,
|
|
313
|
+
category: skip.category,
|
|
314
|
+
skipCount: 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
frameworkData[framework][skip.questionId].skipCount++;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return frameworkData;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Detect patterns from project history
|
|
329
|
+
* @param {string} projectPath - Project root path
|
|
330
|
+
* @returns {Promise<Object>} Detected patterns
|
|
331
|
+
*/
|
|
332
|
+
async function detectPatterns(projectPath) {
|
|
333
|
+
const skipHistory = await storage.getSkipHistory(projectPath);
|
|
334
|
+
const answerHistory = await storage.getAnswerHistory(projectPath);
|
|
335
|
+
const config = await storage.getLearningConfig(projectPath);
|
|
336
|
+
|
|
337
|
+
const detector = new PatternDetector(skipHistory, answerHistory, config);
|
|
338
|
+
return detector.detectAllPatterns();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get pattern summary statistics
|
|
343
|
+
* @param {Object} patterns - Detected patterns
|
|
344
|
+
* @returns {Object} Summary statistics
|
|
345
|
+
*/
|
|
346
|
+
function getPatternSummary(patterns) {
|
|
347
|
+
const allPatterns = [
|
|
348
|
+
...(patterns.consistentSkips || []),
|
|
349
|
+
...(patterns.categoryPatterns || []),
|
|
350
|
+
...(patterns.frameworkPatterns || []),
|
|
351
|
+
...(patterns.userPreferences || [])
|
|
352
|
+
];
|
|
353
|
+
|
|
354
|
+
const highConfidence = allPatterns.filter(p => p.confidence >= 80).length;
|
|
355
|
+
const mediumConfidence = allPatterns.filter(p => p.confidence >= 60 && p.confidence < 80).length;
|
|
356
|
+
const lowConfidence = allPatterns.filter(p => p.confidence < 60).length;
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
total: allPatterns.length,
|
|
360
|
+
highConfidence,
|
|
361
|
+
mediumConfidence,
|
|
362
|
+
lowConfidence,
|
|
363
|
+
byType: {
|
|
364
|
+
consistentSkips: patterns.consistentSkips?.length || 0,
|
|
365
|
+
categoryPatterns: patterns.categoryPatterns?.length || 0,
|
|
366
|
+
frameworkPatterns: patterns.frameworkPatterns?.length || 0,
|
|
367
|
+
userPreferences: patterns.userPreferences?.length || 0
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = {
|
|
373
|
+
PatternDetector,
|
|
374
|
+
detectPatterns,
|
|
375
|
+
getPatternSummary
|
|
376
|
+
};
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
const storage = require('./storage');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rule Generator - Converts detected patterns into filtering rules
|
|
5
|
+
*
|
|
6
|
+
* Generates rules like:
|
|
7
|
+
* - Skip specific questions
|
|
8
|
+
* - Reduce relevance for categories
|
|
9
|
+
* - Framework-specific adjustments
|
|
10
|
+
* - User preference adjustments
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate filtering rules from patterns
|
|
15
|
+
* @param {Object} patterns - Detected patterns
|
|
16
|
+
* @param {Object} config - Learning configuration
|
|
17
|
+
* @returns {Array} Generated rules
|
|
18
|
+
*/
|
|
19
|
+
function generateRulesFromPatterns(patterns, config = {}) {
|
|
20
|
+
const rules = [];
|
|
21
|
+
const minConfidence = config.minConfidenceForAutoFilter || 75;
|
|
22
|
+
|
|
23
|
+
// Rules from consistent skip patterns
|
|
24
|
+
for (const pattern of patterns.consistentSkips || []) {
|
|
25
|
+
if (pattern.confidence >= minConfidence) {
|
|
26
|
+
rules.push({
|
|
27
|
+
id: `rule_${pattern.id}`,
|
|
28
|
+
type: 'skip_question',
|
|
29
|
+
questionId: pattern.questionId,
|
|
30
|
+
adjustment: -100, // Completely filter out
|
|
31
|
+
reason: `Consistently skipped (${pattern.confidence}% confidence, ${pattern.skipCount}/${pattern.sessionsAnalyzed} sessions)`,
|
|
32
|
+
confidence: pattern.confidence,
|
|
33
|
+
patternId: pattern.id,
|
|
34
|
+
appliedSince: new Date().toISOString(),
|
|
35
|
+
enabled: true
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Rules from category patterns
|
|
41
|
+
for (const pattern of patterns.categoryPatterns || []) {
|
|
42
|
+
if (pattern.confidence >= minConfidence) {
|
|
43
|
+
// Reduce relevance but don't completely filter
|
|
44
|
+
rules.push({
|
|
45
|
+
id: `rule_${pattern.id}`,
|
|
46
|
+
type: 'reduce_relevance',
|
|
47
|
+
category: pattern.category,
|
|
48
|
+
adjustment: -30, // Reduce score by 30 points
|
|
49
|
+
reason: `Category skip rate: ${pattern.confidence}% (${pattern.skipCount}/${pattern.totalQuestions} questions)`,
|
|
50
|
+
confidence: pattern.confidence,
|
|
51
|
+
patternId: pattern.id,
|
|
52
|
+
appliedSince: new Date().toISOString(),
|
|
53
|
+
enabled: true
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Rules from framework patterns
|
|
59
|
+
for (const pattern of patterns.frameworkPatterns || []) {
|
|
60
|
+
if (pattern.confidence >= minConfidence) {
|
|
61
|
+
rules.push({
|
|
62
|
+
id: `rule_${pattern.id}`,
|
|
63
|
+
type: 'framework_skip',
|
|
64
|
+
framework: pattern.framework,
|
|
65
|
+
questionId: pattern.questionId,
|
|
66
|
+
adjustment: -50, // Strong reduction for framework-specific skips
|
|
67
|
+
reason: `${pattern.framework} users skip this (${pattern.confidence}% confidence)`,
|
|
68
|
+
confidence: pattern.confidence,
|
|
69
|
+
patternId: pattern.id,
|
|
70
|
+
appliedSince: new Date().toISOString(),
|
|
71
|
+
enabled: true
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Rules from user preferences (informational, lower priority)
|
|
77
|
+
for (const pattern of patterns.userPreferences || []) {
|
|
78
|
+
if (pattern.preference === 'brief_answers' && pattern.avgWordCount < 20) {
|
|
79
|
+
// User gives very brief answers to this category
|
|
80
|
+
rules.push({
|
|
81
|
+
id: `rule_${pattern.id}`,
|
|
82
|
+
type: 'preference_brief',
|
|
83
|
+
category: pattern.category,
|
|
84
|
+
adjustment: -15, // Slight reduction
|
|
85
|
+
reason: `User prefers brief answers (avg ${pattern.avgWordCount} words)`,
|
|
86
|
+
confidence: pattern.confidence,
|
|
87
|
+
patternId: pattern.id,
|
|
88
|
+
appliedSince: new Date().toISOString(),
|
|
89
|
+
enabled: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return rules;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Apply learned rules to a question's relevance score
|
|
99
|
+
* @param {Object} question - Question object
|
|
100
|
+
* @param {Object} projectContext - Project context
|
|
101
|
+
* @param {Array} rules - Learned rules
|
|
102
|
+
* @returns {Object} Adjustment details
|
|
103
|
+
*/
|
|
104
|
+
function applyLearnedRules(question, projectContext, rules) {
|
|
105
|
+
let totalAdjustment = 0;
|
|
106
|
+
const appliedRules = [];
|
|
107
|
+
|
|
108
|
+
for (const rule of rules) {
|
|
109
|
+
if (!rule.enabled) continue;
|
|
110
|
+
|
|
111
|
+
let applies = false;
|
|
112
|
+
|
|
113
|
+
switch (rule.type) {
|
|
114
|
+
case 'skip_question':
|
|
115
|
+
if (question.id === rule.questionId) {
|
|
116
|
+
applies = true;
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case 'reduce_relevance':
|
|
121
|
+
if (question.category === rule.category) {
|
|
122
|
+
applies = true;
|
|
123
|
+
}
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'framework_skip':
|
|
127
|
+
if (
|
|
128
|
+
question.id === rule.questionId &&
|
|
129
|
+
projectContext.frameworks &&
|
|
130
|
+
projectContext.frameworks.includes(rule.framework)
|
|
131
|
+
) {
|
|
132
|
+
applies = true;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
|
|
136
|
+
case 'preference_brief':
|
|
137
|
+
if (question.category === rule.category) {
|
|
138
|
+
applies = true;
|
|
139
|
+
}
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (applies) {
|
|
144
|
+
totalAdjustment += rule.adjustment;
|
|
145
|
+
appliedRules.push({
|
|
146
|
+
ruleId: rule.id,
|
|
147
|
+
type: rule.type,
|
|
148
|
+
adjustment: rule.adjustment,
|
|
149
|
+
reason: rule.reason,
|
|
150
|
+
confidence: rule.confidence
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
adjustment: totalAdjustment,
|
|
157
|
+
appliedRules,
|
|
158
|
+
hasLearning: appliedRules.length > 0
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get rule explanations for user display
|
|
164
|
+
* @param {Array} rules - Learned rules
|
|
165
|
+
* @returns {Array} Formatted rule explanations
|
|
166
|
+
*/
|
|
167
|
+
function getRuleExplanations(rules) {
|
|
168
|
+
const explanations = [];
|
|
169
|
+
|
|
170
|
+
// Group by type
|
|
171
|
+
const byType = {
|
|
172
|
+
skip_question: [],
|
|
173
|
+
reduce_relevance: [],
|
|
174
|
+
framework_skip: [],
|
|
175
|
+
preference_brief: []
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (const rule of rules) {
|
|
179
|
+
if (rule.enabled && byType[rule.type]) {
|
|
180
|
+
byType[rule.type].push(rule);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Skip question rules
|
|
185
|
+
if (byType.skip_question.length > 0) {
|
|
186
|
+
explanations.push({
|
|
187
|
+
type: 'skip_question',
|
|
188
|
+
count: byType.skip_question.length,
|
|
189
|
+
message: `Skip ${byType.skip_question.length} question${byType.skip_question.length > 1 ? 's' : ''} (consistently skipped)`,
|
|
190
|
+
details: byType.skip_question.map(r => r.reason)
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Category reduction rules
|
|
195
|
+
if (byType.reduce_relevance.length > 0) {
|
|
196
|
+
const categories = byType.reduce_relevance.map(r => r.category).join(', ');
|
|
197
|
+
explanations.push({
|
|
198
|
+
type: 'reduce_relevance',
|
|
199
|
+
count: byType.reduce_relevance.length,
|
|
200
|
+
message: `Reduce relevance for: ${categories}`,
|
|
201
|
+
details: byType.reduce_relevance.map(r => r.reason)
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Framework skip rules
|
|
206
|
+
if (byType.framework_skip.length > 0) {
|
|
207
|
+
explanations.push({
|
|
208
|
+
type: 'framework_skip',
|
|
209
|
+
count: byType.framework_skip.length,
|
|
210
|
+
message: `Framework-specific filtering (${byType.framework_skip.length} rule${byType.framework_skip.length > 1 ? 's' : ''})`,
|
|
211
|
+
details: byType.framework_skip.map(r => r.reason)
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Preference rules
|
|
216
|
+
if (byType.preference_brief.length > 0) {
|
|
217
|
+
explanations.push({
|
|
218
|
+
type: 'preference_brief',
|
|
219
|
+
count: byType.preference_brief.length,
|
|
220
|
+
message: `User preference adjustments (${byType.preference_brief.length} categor${byType.preference_brief.length > 1 ? 'ies' : 'y'})`,
|
|
221
|
+
details: byType.preference_brief.map(r => r.reason)
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return explanations;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Update learned rules based on new patterns
|
|
230
|
+
* @param {string} projectPath - Project root path
|
|
231
|
+
* @param {Object} patterns - Newly detected patterns
|
|
232
|
+
*/
|
|
233
|
+
async function updateLearnedRules(projectPath, patterns) {
|
|
234
|
+
const config = await storage.getLearningConfig(projectPath);
|
|
235
|
+
const currentRules = await storage.getLearnedRules(projectPath);
|
|
236
|
+
|
|
237
|
+
// Generate new rules from patterns
|
|
238
|
+
const newRules = generateRulesFromPatterns(patterns, config);
|
|
239
|
+
|
|
240
|
+
// Merge with existing rules (don't duplicate)
|
|
241
|
+
const existingRuleIds = new Set(currentRules.rules.map(r => r.id));
|
|
242
|
+
const rulesToAdd = newRules.filter(r => !existingRuleIds.has(r.id));
|
|
243
|
+
|
|
244
|
+
// Add new rules
|
|
245
|
+
currentRules.rules.push(...rulesToAdd);
|
|
246
|
+
|
|
247
|
+
// Save updated rules
|
|
248
|
+
await storage.saveLearnedRules(projectPath, currentRules);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
added: rulesToAdd.length,
|
|
252
|
+
total: currentRules.rules.length
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get active learned rules
|
|
258
|
+
* @param {string} projectPath - Project root path
|
|
259
|
+
* @returns {Promise<Array>} Active rules
|
|
260
|
+
*/
|
|
261
|
+
async function getActiveRules(projectPath) {
|
|
262
|
+
const rulesData = await storage.getLearnedRules(projectPath);
|
|
263
|
+
return rulesData.rules.filter(r => r.enabled);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Enable or disable a specific rule
|
|
268
|
+
* @param {string} projectPath - Project root path
|
|
269
|
+
* @param {string} ruleId - Rule ID
|
|
270
|
+
* @param {boolean} enabled - Enable/disable
|
|
271
|
+
*/
|
|
272
|
+
async function toggleRule(projectPath, ruleId, enabled) {
|
|
273
|
+
const rulesData = await storage.getLearnedRules(projectPath);
|
|
274
|
+
const rule = rulesData.rules.find(r => r.id === ruleId);
|
|
275
|
+
|
|
276
|
+
if (rule) {
|
|
277
|
+
rule.enabled = enabled;
|
|
278
|
+
await storage.saveLearnedRules(projectPath, rulesData);
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Remove a learned rule
|
|
287
|
+
* @param {string} projectPath - Project root path
|
|
288
|
+
* @param {string} ruleId - Rule ID
|
|
289
|
+
*/
|
|
290
|
+
async function removeRule(projectPath, ruleId) {
|
|
291
|
+
const rulesData = await storage.getLearnedRules(projectPath);
|
|
292
|
+
rulesData.rules = rulesData.rules.filter(r => r.id !== ruleId);
|
|
293
|
+
await storage.saveLearnedRules(projectPath, rulesData);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
module.exports = {
|
|
297
|
+
generateRulesFromPatterns,
|
|
298
|
+
applyLearnedRules,
|
|
299
|
+
getRuleExplanations,
|
|
300
|
+
updateLearnedRules,
|
|
301
|
+
getActiveRules,
|
|
302
|
+
toggleRule,
|
|
303
|
+
removeRule
|
|
304
|
+
};
|