@arclabs561/ai-visual-test 0.5.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.
Files changed (93) hide show
  1. package/.secretsignore.example +20 -0
  2. package/CHANGELOG.md +360 -0
  3. package/CONTRIBUTING.md +63 -0
  4. package/DEPLOYMENT.md +80 -0
  5. package/LICENSE +22 -0
  6. package/README.md +142 -0
  7. package/SECURITY.md +108 -0
  8. package/api/health.js +34 -0
  9. package/api/validate.js +252 -0
  10. package/index.d.ts +1221 -0
  11. package/package.json +112 -0
  12. package/public/index.html +149 -0
  13. package/src/batch-optimizer.mjs +451 -0
  14. package/src/bias-detector.mjs +370 -0
  15. package/src/bias-mitigation.mjs +233 -0
  16. package/src/cache.mjs +433 -0
  17. package/src/config.mjs +268 -0
  18. package/src/constants.mjs +80 -0
  19. package/src/context-compressor.mjs +350 -0
  20. package/src/convenience.mjs +617 -0
  21. package/src/cost-tracker.mjs +257 -0
  22. package/src/cross-modal-consistency.mjs +170 -0
  23. package/src/data-extractor.mjs +232 -0
  24. package/src/dynamic-few-shot.mjs +140 -0
  25. package/src/dynamic-prompts.mjs +361 -0
  26. package/src/ensemble/index.mjs +53 -0
  27. package/src/ensemble-judge.mjs +366 -0
  28. package/src/error-handler.mjs +67 -0
  29. package/src/errors.mjs +167 -0
  30. package/src/experience-propagation.mjs +128 -0
  31. package/src/experience-tracer.mjs +487 -0
  32. package/src/explanation-manager.mjs +299 -0
  33. package/src/feedback-aggregator.mjs +248 -0
  34. package/src/game-goal-prompts.mjs +478 -0
  35. package/src/game-player.mjs +548 -0
  36. package/src/hallucination-detector.mjs +155 -0
  37. package/src/helpers/playwright.mjs +80 -0
  38. package/src/human-validation-manager.mjs +516 -0
  39. package/src/index.mjs +364 -0
  40. package/src/judge.mjs +929 -0
  41. package/src/latency-aware-batch-optimizer.mjs +192 -0
  42. package/src/load-env.mjs +159 -0
  43. package/src/logger.mjs +55 -0
  44. package/src/metrics.mjs +187 -0
  45. package/src/model-tier-selector.mjs +221 -0
  46. package/src/multi-modal/index.mjs +36 -0
  47. package/src/multi-modal-fusion.mjs +190 -0
  48. package/src/multi-modal.mjs +524 -0
  49. package/src/natural-language-specs.mjs +1071 -0
  50. package/src/pair-comparison.mjs +277 -0
  51. package/src/persona/index.mjs +42 -0
  52. package/src/persona-enhanced.mjs +200 -0
  53. package/src/persona-experience.mjs +572 -0
  54. package/src/position-counterbalance.mjs +140 -0
  55. package/src/prompt-composer.mjs +375 -0
  56. package/src/render-change-detector.mjs +583 -0
  57. package/src/research-enhanced-validation.mjs +436 -0
  58. package/src/retry.mjs +152 -0
  59. package/src/rubrics.mjs +231 -0
  60. package/src/score-tracker.mjs +277 -0
  61. package/src/smart-validator.mjs +447 -0
  62. package/src/spec-config.mjs +106 -0
  63. package/src/spec-templates.mjs +347 -0
  64. package/src/specs/index.mjs +38 -0
  65. package/src/temporal/index.mjs +102 -0
  66. package/src/temporal-adaptive.mjs +163 -0
  67. package/src/temporal-batch-optimizer.mjs +222 -0
  68. package/src/temporal-constants.mjs +69 -0
  69. package/src/temporal-context.mjs +49 -0
  70. package/src/temporal-decision-manager.mjs +271 -0
  71. package/src/temporal-decision.mjs +669 -0
  72. package/src/temporal-errors.mjs +58 -0
  73. package/src/temporal-note-pruner.mjs +173 -0
  74. package/src/temporal-preprocessor.mjs +543 -0
  75. package/src/temporal-prompt-formatter.mjs +219 -0
  76. package/src/temporal-validation.mjs +159 -0
  77. package/src/temporal.mjs +415 -0
  78. package/src/type-guards.mjs +311 -0
  79. package/src/uncertainty-reducer.mjs +470 -0
  80. package/src/utils/index.mjs +175 -0
  81. package/src/validation-framework.mjs +321 -0
  82. package/src/validation-result-normalizer.mjs +64 -0
  83. package/src/validation.mjs +243 -0
  84. package/src/validators/accessibility-programmatic.mjs +345 -0
  85. package/src/validators/accessibility-validator.mjs +223 -0
  86. package/src/validators/batch-validator.mjs +143 -0
  87. package/src/validators/hybrid-validator.mjs +268 -0
  88. package/src/validators/index.mjs +34 -0
  89. package/src/validators/prompt-builder.mjs +218 -0
  90. package/src/validators/rubric.mjs +85 -0
  91. package/src/validators/state-programmatic.mjs +260 -0
  92. package/src/validators/state-validator.mjs +291 -0
  93. package/vercel.json +27 -0
@@ -0,0 +1,299 @@
1
+ /**
2
+ * Explanation Manager
3
+ *
4
+ * Provides late interaction capabilities for explaining VLLM judgments.
5
+ * Allows humans to ask questions about judgments after they've been made.
6
+ */
7
+
8
+ import { VLLMJudge } from './judge.mjs';
9
+ import { getCached, setCached } from './cache.mjs';
10
+ import { log, warn } from './logger.mjs';
11
+ import { formatNotesForPrompt } from './temporal.mjs';
12
+
13
+ /**
14
+ * Explanation Manager
15
+ *
16
+ * Manages interactive explanations of VLLM judgments
17
+ */
18
+ export class ExplanationManager {
19
+ constructor(options = {}) {
20
+ this.judge = options.judge || new VLLMJudge(options);
21
+ this.cacheEnabled = options.cacheEnabled !== false;
22
+ this.explanations = new Map(); // In-memory cache of explanations
23
+ }
24
+
25
+ /**
26
+ * Get explanation for a judgment
27
+ *
28
+ * @param {Object} vllmJudgment - VLLM judgment to explain
29
+ * @param {string} question - Question about the judgment (optional)
30
+ * @param {Object} options - Explanation options
31
+ * @returns {Promise<Object>} Explanation response
32
+ */
33
+ async explainJudgment(vllmJudgment, question = null, options = {}) {
34
+ const {
35
+ screenshotPath = vllmJudgment.screenshot,
36
+ prompt = vllmJudgment.prompt,
37
+ context = vllmJudgment.context || {},
38
+ useCache = true,
39
+ // NEW: Temporal and experience context
40
+ temporalNotes = vllmJudgment.temporalNotes || context.temporalNotes || null,
41
+ aggregatedNotes = vllmJudgment.aggregatedNotes || context.aggregatedNotes || null,
42
+ experienceTrace = vllmJudgment.experienceTrace || context.experienceTrace || null
43
+ } = options;
44
+
45
+ // Build explanation prompt with temporal context
46
+ const explanationPrompt = this._buildExplanationPrompt(
47
+ vllmJudgment,
48
+ question,
49
+ { temporalNotes, aggregatedNotes, experienceTrace }
50
+ );
51
+
52
+ // Check cache
53
+ if (useCache && this.cacheEnabled) {
54
+ const cacheKey = `explain-${vllmJudgment.id}-${question || 'default'}`;
55
+ const cached = getCached(cacheKey, explanationPrompt, context);
56
+ if (cached) {
57
+ return cached;
58
+ }
59
+ }
60
+
61
+ // Get explanation from VLLM
62
+ try {
63
+ const result = await this.judge.judgeScreenshot(
64
+ screenshotPath,
65
+ explanationPrompt,
66
+ {
67
+ ...context,
68
+ useCache: false, // Don't cache explanation requests
69
+ enableHumanValidation: false // Don't collect explanations for validation
70
+ }
71
+ );
72
+
73
+ const explanation = {
74
+ question: question || 'Why did you score this the way you did?',
75
+ answer: result.reasoning || result.assessment || 'No explanation available',
76
+ confidence: this._extractConfidence(result),
77
+ timestamp: new Date().toISOString(),
78
+ judgmentId: vllmJudgment.id
79
+ };
80
+
81
+ // Cache explanation
82
+ if (useCache && this.cacheEnabled) {
83
+ const cacheKey = `explain-${vllmJudgment.id}-${question || 'default'}`;
84
+ setCached(cacheKey, explanationPrompt, context, explanation);
85
+ }
86
+
87
+ this.explanations.set(vllmJudgment.id, explanation);
88
+ return explanation;
89
+ } catch (error) {
90
+ warn('Failed to get explanation:', error.message);
91
+ return {
92
+ question: question || 'Why did you score this the way you did?',
93
+ answer: 'Unable to generate explanation at this time.',
94
+ error: error.message,
95
+ timestamp: new Date().toISOString()
96
+ };
97
+ }
98
+ }
99
+
100
+ /**
101
+ * Build explanation prompt with temporal and experience context
102
+ *
103
+ * @param {Object} vllmJudgment - VLLM judgment
104
+ * @param {string|null} question - Optional question
105
+ * @param {Object} temporalContext - Temporal and experience context
106
+ * @param {Object|null} temporalContext.temporalNotes - Raw temporal notes
107
+ * @param {Object|null} temporalContext.aggregatedNotes - Aggregated temporal notes
108
+ * @param {Object|null} temporalContext.experienceTrace - Experience trace data
109
+ */
110
+ _buildExplanationPrompt(vllmJudgment, question, temporalContext = {}) {
111
+ const { temporalNotes, aggregatedNotes, experienceTrace } = temporalContext;
112
+
113
+ let prompt = '';
114
+
115
+ // Base judgment context
116
+ if (question) {
117
+ prompt = `You previously evaluated this screenshot and gave it a score of ${vllmJudgment.vllmScore}/10.
118
+
119
+ Your previous judgment:
120
+ - Score: ${vllmJudgment.vllmScore}/10
121
+ - Issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}
122
+ - Reasoning: ${vllmJudgment.vllmReasoning}
123
+
124
+ Question: ${question}
125
+
126
+ Please provide a clear, detailed explanation addressing this question.`;
127
+ } else {
128
+ prompt = `You previously evaluated this screenshot and gave it a score of ${vllmJudgment.vllmScore}/10.
129
+
130
+ Your previous judgment:
131
+ - Score: ${vllmJudgment.vllmScore}/10
132
+ - Issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}
133
+ - Reasoning: ${vllmJudgment.vllmReasoning}
134
+
135
+ Please provide a detailed explanation of:
136
+ 1. Why you scored it ${vllmJudgment.vllmScore}/10
137
+ 2. What specific evidence in the screenshot led to this score
138
+ 3. What the main issues are and why they matter
139
+ 4. What would need to change to improve the score
140
+
141
+ Be specific and reference visual elements in the screenshot.`;
142
+ }
143
+
144
+ // Add temporal context if available
145
+ if (aggregatedNotes) {
146
+ prompt += `\n\nTEMPORAL CONTEXT:\n`;
147
+ prompt += formatNotesForPrompt(aggregatedNotes);
148
+ prompt += `\n\nWhen explaining, reference specific time points, trends, and temporal relationships (before/after/during).`;
149
+ prompt += ` Explain how the current judgment relates to previous observations and temporal patterns.`;
150
+ } else if (temporalNotes && Array.isArray(temporalNotes) && temporalNotes.length > 0) {
151
+ // Format raw temporal notes if aggregated notes not available
152
+ prompt += `\n\nTEMPORAL CONTEXT:\n`;
153
+ const recentNotes = temporalNotes.slice(-5);
154
+ recentNotes.forEach((note, i) => {
155
+ const time = note.elapsed ? `${(note.elapsed / 1000).toFixed(1)}s` : `step ${i + 1}`;
156
+ prompt += ` ${time}: ${note.observation || note.step || 'step'}\n`;
157
+ if (note.score !== null && note.score !== undefined) {
158
+ prompt += ` Score: ${note.score}/10\n`;
159
+ }
160
+ });
161
+ prompt += `\n\nWhen explaining, reference these temporal observations and explain how they relate to the current judgment.`;
162
+ }
163
+
164
+ // Add experience trace context if available
165
+ if (experienceTrace) {
166
+ prompt += `\n\nEXPERIENCE TRACE CONTEXT:\n`;
167
+ prompt += `Session ID: ${experienceTrace.sessionId || 'unknown'}\n`;
168
+
169
+ if (experienceTrace.persona) {
170
+ prompt += `Persona: ${experienceTrace.persona.name || 'unknown'}\n`;
171
+ if (experienceTrace.persona.goals) {
172
+ prompt += `Persona Goals: ${experienceTrace.persona.goals.join(', ')}\n`;
173
+ }
174
+ }
175
+
176
+ if (experienceTrace.events && experienceTrace.events.length > 0) {
177
+ prompt += `\nRecent Events (last 5):\n`;
178
+ const recentEvents = experienceTrace.events.slice(-5);
179
+ recentEvents.forEach(event => {
180
+ const time = event.elapsed ? `${(event.elapsed / 1000).toFixed(1)}s` : 'unknown';
181
+ prompt += ` ${time}: ${event.type} - ${event.data.observation || event.data.action || ''}\n`;
182
+ });
183
+ }
184
+
185
+ if (experienceTrace.validations && experienceTrace.validations.length > 0) {
186
+ prompt += `\nPrevious Validations (last 3):\n`;
187
+ const recentValidations = experienceTrace.validations.slice(-3);
188
+ recentValidations.forEach(validation => {
189
+ const time = validation.elapsed ? `${(validation.elapsed / 1000).toFixed(1)}s` : 'unknown';
190
+ prompt += ` ${time}: Score ${validation.validation.score}/10 - ${validation.validation.reasoning?.substring(0, 100) || ''}\n`;
191
+ });
192
+ }
193
+
194
+ prompt += `\n\nWhen explaining, consider the user's journey, previous states, and how the current judgment fits into the overall experience.`;
195
+ }
196
+
197
+ // Add VLLM-specific guidance for temporal explanations
198
+ if (aggregatedNotes || temporalNotes || experienceTrace) {
199
+ prompt += `\n\nIMPORTANT: As a Vision-Language Model, when explaining temporal aspects:
200
+ 1. Visual citations: Reference specific image regions (coordinates, descriptions) when mentioning visual evidence
201
+ 2. Temporal citations: Reference specific time points (e.g., "at t=5s", "after 12 seconds")
202
+ 3. Temporal relationships: Explain before/after/during relationships and transitions
203
+ 4. Experience context: Reference the user's journey and how previous states influenced the judgment
204
+ 5. Trends: Explain improvement/decline trends and temporal coherence`;
205
+ }
206
+
207
+ return prompt;
208
+ }
209
+
210
+ /**
211
+ * Extract confidence from result
212
+ */
213
+ _extractConfidence(result) {
214
+ // Try to extract confidence from various sources
215
+ if (result.semantic?.confidence !== undefined) {
216
+ return result.semantic.confidence;
217
+ }
218
+ if (result.raw?.confidence !== undefined) {
219
+ return result.raw.confidence;
220
+ }
221
+ // Estimate from uncertainty if available
222
+ if (result.uncertainty !== undefined) {
223
+ return 1.0 - result.uncertainty;
224
+ }
225
+ return null;
226
+ }
227
+
228
+ /**
229
+ * Get confidence breakdown for different aspects
230
+ */
231
+ async getConfidenceBreakdown(vllmJudgment) {
232
+ const questions = [
233
+ 'How confident are you in the overall score?',
234
+ 'How confident are you in the issues you identified?',
235
+ 'Are there any aspects you are uncertain about?'
236
+ ];
237
+
238
+ const breakdown = {
239
+ overall: null,
240
+ issues: null,
241
+ uncertainty: null,
242
+ aspects: []
243
+ };
244
+
245
+ // Get explanations for each question
246
+ for (const question of questions) {
247
+ const explanation = await this.explainJudgment(vllmJudgment, question, { useCache: true });
248
+ // Parse explanation to extract confidence info
249
+ // This is a simplified version - could be enhanced with structured output
250
+ breakdown.aspects.push({
251
+ question,
252
+ explanation: explanation.answer
253
+ });
254
+ }
255
+
256
+ return breakdown;
257
+ }
258
+
259
+ /**
260
+ * Explain disagreement between human and VLLM
261
+ */
262
+ async explainDisagreement(vllmJudgment, humanJudgment) {
263
+ const question = `A human reviewer scored this ${humanJudgment.humanScore}/10, but you scored it ${vllmJudgment.vllmScore}/10.
264
+ The human identified these issues: ${humanJudgment.humanIssues.join(', ') || 'None'}.
265
+ You identified these issues: ${vllmJudgment.vllmIssues.join(', ') || 'None'}.
266
+
267
+ Please explain:
268
+ 1. Why there might be a difference in scores
269
+ 2. Whether you think the human's assessment is valid
270
+ 3. What you might have missed or over-emphasized
271
+ 4. How this could help improve future evaluations`;
272
+
273
+ return await this.explainJudgment(vllmJudgment, question);
274
+ }
275
+
276
+ /**
277
+ * Get cached explanation if available
278
+ */
279
+ getCachedExplanation(judgmentId, question = null) {
280
+ const key = question ? `${judgmentId}-${question}` : judgmentId;
281
+ return this.explanations.get(key) || null;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Global explanation manager instance
287
+ */
288
+ let globalExplanationManager = null;
289
+
290
+ /**
291
+ * Get or create global explanation manager
292
+ */
293
+ export function getExplanationManager(options = {}) {
294
+ if (!globalExplanationManager) {
295
+ globalExplanationManager = new ExplanationManager(options);
296
+ }
297
+ return globalExplanationManager;
298
+ }
299
+
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Feedback Aggregator
3
+ *
4
+ * Aggregates judge feedback across multiple tests for iterative improvement.
5
+ *
6
+ * General-purpose utility - no domain-specific logic.
7
+ */
8
+
9
+ /**
10
+ * Aggregate judge feedback from multiple test runs
11
+ *
12
+ * @param {import('./index.mjs').ValidationResult[]} judgeResults - Array of validation results
13
+ * @returns {import('./index.mjs').AggregatedFeedback} Aggregated feedback with statistics and recommendations
14
+ */
15
+ export function aggregateFeedback(judgeResults) {
16
+ const aggregated = {
17
+ scores: [],
18
+ issues: {},
19
+ recommendations: {},
20
+ strengths: {},
21
+ weaknesses: {},
22
+ actionableItems: {},
23
+ categories: {
24
+ visual: [],
25
+ functional: [],
26
+ performance: [],
27
+ accessibility: [],
28
+ gameplay: [],
29
+ ux: [],
30
+ other: []
31
+ },
32
+ priority: {
33
+ critical: [],
34
+ high: [],
35
+ medium: [],
36
+ low: []
37
+ },
38
+ trends: {
39
+ score: [],
40
+ issues: [],
41
+ recommendations: []
42
+ }
43
+ };
44
+
45
+ judgeResults.forEach(result => {
46
+ // Aggregate scores
47
+ if (result.score !== null && result.score !== undefined) {
48
+ aggregated.scores.push(result.score);
49
+ }
50
+
51
+ // Aggregate semantic information
52
+ if (result.semantic) {
53
+ const sem = result.semantic;
54
+
55
+ // Aggregate issues by frequency
56
+ if (sem.issues) {
57
+ sem.issues.forEach(issue => {
58
+ aggregated.issues[issue] = (aggregated.issues[issue] || 0) + 1;
59
+ });
60
+ }
61
+
62
+ // Aggregate recommendations
63
+ if (sem.recommendations) {
64
+ sem.recommendations.forEach(rec => {
65
+ aggregated.recommendations[rec] = (aggregated.recommendations[rec] || 0) + 1;
66
+ });
67
+ }
68
+
69
+ // Aggregate strengths
70
+ if (sem.strengths) {
71
+ sem.strengths.forEach(strength => {
72
+ aggregated.strengths[strength] = (aggregated.strengths[strength] || 0) + 1;
73
+ });
74
+ }
75
+
76
+ // Aggregate weaknesses
77
+ if (sem.weaknesses) {
78
+ sem.weaknesses.forEach(weakness => {
79
+ aggregated.weaknesses[weakness] = (aggregated.weaknesses[weakness] || 0) + 1;
80
+ });
81
+ }
82
+
83
+ // Aggregate actionable items
84
+ if (sem.actionableItems) {
85
+ sem.actionableItems.forEach(item => {
86
+ aggregated.actionableItems[item] = (aggregated.actionableItems[item] || 0) + 1;
87
+ });
88
+ }
89
+
90
+ // Aggregate by category
91
+ if (sem.semanticCategories) {
92
+ Object.entries(sem.semanticCategories).forEach(([category, items]) => {
93
+ if (items && items.length > 0) {
94
+ aggregated.categories[category] = aggregated.categories[category] || [];
95
+ aggregated.categories[category].push(...items);
96
+ }
97
+ });
98
+ }
99
+
100
+ // Aggregate by priority
101
+ if (sem.priority) {
102
+ Object.entries(sem.priority).forEach(([level, items]) => {
103
+ if (items && items.length > 0) {
104
+ aggregated.priority[level] = aggregated.priority[level] || [];
105
+ aggregated.priority[level].push(...items);
106
+ }
107
+ });
108
+ }
109
+ }
110
+ });
111
+
112
+ // Calculate statistics
113
+ const stats = {
114
+ totalJudgments: judgeResults.length,
115
+ averageScore: aggregated.scores.length > 0
116
+ ? aggregated.scores.reduce((a, b) => a + b, 0) / aggregated.scores.length
117
+ : null,
118
+ minScore: aggregated.scores.length > 0 ? Math.min(...aggregated.scores) : null,
119
+ maxScore: aggregated.scores.length > 0 ? Math.max(...aggregated.scores) : null,
120
+ mostCommonIssues: Object.entries(aggregated.issues)
121
+ .sort((a, b) => b[1] - a[1])
122
+ .slice(0, 10)
123
+ .map(([issue, count]) => ({ issue, count })),
124
+ mostCommonRecommendations: Object.entries(aggregated.recommendations)
125
+ .sort((a, b) => b[1] - a[1])
126
+ .slice(0, 10)
127
+ .map(([rec, count]) => ({ rec, count })),
128
+ mostCommonStrengths: Object.entries(aggregated.strengths)
129
+ .sort((a, b) => b[1] - a[1])
130
+ .slice(0, 10)
131
+ .map(([strength, count]) => ({ strength, count })),
132
+ mostCommonWeaknesses: Object.entries(aggregated.weaknesses)
133
+ .sort((a, b) => b[1] - a[1])
134
+ .slice(0, 10)
135
+ .map(([weakness, count]) => ({ weakness, count })),
136
+ mostCommonActionableItems: Object.entries(aggregated.actionableItems)
137
+ .sort((a, b) => b[1] - a[1])
138
+ .slice(0, 10)
139
+ .map(([item, count]) => ({ item, count })),
140
+ categoryCounts: Object.entries(aggregated.categories)
141
+ .map(([category, items]) => ({ category, count: items.length }))
142
+ .sort((a, b) => b.count - a.count),
143
+ priorityCounts: Object.entries(aggregated.priority)
144
+ .map(([level, items]) => ({ level, count: items.length }))
145
+ .sort((a, b) => {
146
+ const order = { critical: 0, high: 1, medium: 2, low: 3 };
147
+ return (order[a.level] || 99) - (order[b.level] || 99);
148
+ })
149
+ };
150
+
151
+ return {
152
+ aggregated,
153
+ stats,
154
+ summary: generateSummary(aggregated, stats)
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Generate human-readable summary
160
+ */
161
+ function generateSummary(aggregated, stats) {
162
+ const parts = [];
163
+
164
+ parts.push(`Aggregated ${stats.totalJudgments} judge results.`);
165
+
166
+ if (stats.averageScore !== null) {
167
+ parts.push(`Average score: ${stats.averageScore.toFixed(1)}/10 (range: ${stats.minScore}-${stats.maxScore}).`);
168
+ }
169
+
170
+ if (stats.mostCommonIssues.length > 0) {
171
+ parts.push(`Most common issues: ${stats.mostCommonIssues.slice(0, 3).map(i => i.issue).join(', ')}.`);
172
+ }
173
+
174
+ if (stats.mostCommonRecommendations.length > 0) {
175
+ parts.push(`Most common recommendations: ${stats.mostCommonRecommendations.slice(0, 3).map(r => r.rec).join(', ')}.`);
176
+ }
177
+
178
+ if (stats.priorityCounts.length > 0) {
179
+ const critical = stats.priorityCounts.find(p => p.level === 'critical');
180
+ if (critical && critical.count > 0) {
181
+ parts.push(`Critical issues: ${critical.count}.`);
182
+ }
183
+ }
184
+
185
+ return parts.join(' ');
186
+ }
187
+
188
+ /**
189
+ * Generate recommendations from aggregated feedback
190
+ *
191
+ * @param {import('./index.mjs').AggregatedFeedback} aggregated - Aggregated feedback
192
+ * @returns {string[]} Array of recommendation strings
193
+ */
194
+ export function generateRecommendations(aggregated) {
195
+ const recommendations = [];
196
+
197
+ // Critical priority items
198
+ if (aggregated.priority.critical && aggregated.priority.critical.length > 0) {
199
+ recommendations.push({
200
+ priority: 'critical',
201
+ category: 'all',
202
+ items: aggregated.priority.critical.slice(0, 5),
203
+ description: 'Critical issues that must be addressed immediately'
204
+ });
205
+ }
206
+
207
+ // High priority items
208
+ if (aggregated.priority.high && aggregated.priority.high.length > 0) {
209
+ recommendations.push({
210
+ priority: 'high',
211
+ category: 'all',
212
+ items: aggregated.priority.high.slice(0, 5),
213
+ description: 'High priority issues that should be addressed soon'
214
+ });
215
+ }
216
+
217
+ // Category-specific recommendations
218
+ Object.entries(aggregated.categories).forEach(([category, items]) => {
219
+ if (items && items.length > 0) {
220
+ const uniqueItems = [...new Set(items)];
221
+ if (uniqueItems.length > 0) {
222
+ recommendations.push({
223
+ priority: 'medium',
224
+ category,
225
+ items: uniqueItems.slice(0, 5),
226
+ description: `${category} improvements`
227
+ });
228
+ }
229
+ }
230
+ });
231
+
232
+ // Most common actionable items
233
+ const actionableEntries = Object.entries(aggregated.actionableItems || {})
234
+ .sort((a, b) => b[1] - a[1])
235
+ .slice(0, 10);
236
+
237
+ if (actionableEntries.length > 0) {
238
+ recommendations.push({
239
+ priority: 'medium',
240
+ category: 'actionable',
241
+ items: actionableEntries.map(([item, count]) => `${item} (mentioned ${count} times)`),
242
+ description: 'Most frequently mentioned actionable improvements'
243
+ });
244
+ }
245
+
246
+ return recommendations;
247
+ }
248
+