@wiber/ccs
Version:
Turn any codebase into an AI-aware environment. Claude launches with full context, asks smart questions, and gets better with every interaction.
307 lines (257 loc) ⢠9.82 kB
JavaScript
/**
* Feedback Loop - User feedback collection and CI/CD integration
*
* Collects feedback from users and external sources to improve ccs operations
*/
const fs = require('fs').promises;
const path = require('path');
const inquirer = require('inquirer');
class FeedbackLoop {
constructor(projectPath, learningEngine) {
this.projectPath = projectPath;
this.learningEngine = learningEngine;
this.feedbackPath = path.join(projectPath, '.ccs', 'feedback');
}
async initialize() {
await fs.mkdir(this.feedbackPath, { recursive: true });
}
/**
* Interactive feedback collection
*/
async collectInteractiveFeedback() {
console.log('\nš ccs Feedback Collection');
console.log('Help us improve by rating recent operations\n');
// Get recent executions that need feedback
const recentExecutions = await this.getExecutionsNeedingFeedback();
if (recentExecutions.length === 0) {
console.log('ā
No operations need feedback at this time');
return;
}
for (const execution of recentExecutions.slice(0, 3)) { // Limit to 3 for user experience
await this.collectExecutionFeedback(execution);
}
console.log('\nš Thank you for helping improve ccs!');
}
async collectExecutionFeedback(execution) {
console.log(`\nš Operation: ${execution.operation}`);
console.log(`ā° Time: ${new Date(execution.timestamp).toLocaleString()}`);
if (execution.duration) {
console.log(`ā” Duration: ${(execution.duration / 1000).toFixed(1)}s`);
}
const answers = await inquirer.prompt([
{
type: 'list',
name: 'rating',
message: 'How would you rate this operation?',
choices: [
{ name: 'āāāāā Excellent - Exactly what I needed', value: 5 },
{ name: 'āāāā Good - Helpful with minor issues', value: 4 },
{ name: 'āāā Okay - Somewhat useful', value: 3 },
{ name: 'āā Poor - Limited usefulness', value: 2 },
{ name: 'ā Very Poor - Not helpful', value: 1 }
]
},
{
type: 'input',
name: 'comments',
message: 'Any specific comments or suggestions? (optional)',
default: ''
},
{
type: 'checkbox',
name: 'issues',
message: 'What issues did you encounter? (if any)',
choices: [
'Too slow',
'Output not relevant',
'Missed important information',
'Too verbose',
'Too brief',
'Technical errors',
'Context not understood',
'Other'
],
when: (answers) => answers.rating <= 3
}
]);
// Record feedback
await this.learningEngine.recordFeedback(
execution.id,
answers.rating,
this.formatFeedbackComments(answers.comments, answers.issues)
);
console.log('ā
Feedback recorded');
}
/**
* Automatic feedback from CI/CD pipeline results
*/
async recordCIFeedback(executionId, ciResults) {
const feedback = {
type: 'ci_pipeline',
executionId,
results: ciResults,
timestamp: new Date().toISOString()
};
// Convert CI results to rating
let rating = 5;
if (ciResults.testFailures > 0) rating -= 1;
if (ciResults.buildFailures > 0) rating -= 1;
if (ciResults.lintErrors > 0) rating -= 0.5;
if (ciResults.deploymentFailures > 0) rating -= 2;
rating = Math.max(1, Math.min(5, Math.round(rating)));
const comments = this.formatCIComments(ciResults);
await this.learningEngine.recordFeedback(executionId, rating, comments);
// Save detailed CI feedback
const ciPath = path.join(this.feedbackPath, `ci-${executionId}.json`);
await fs.writeFile(ciPath, JSON.stringify(feedback, null, 2));
}
/**
* Monitor external metrics for automatic feedback
*/
async collectMetricsFeedback() {
try {
// Check recent ccs outputs for measurable metrics
const metrics = await this.analyzeOutputMetrics();
if (metrics.length > 0) {
for (const metric of metrics) {
await this.recordMetricFeedback(metric);
}
}
} catch (error) {
console.warn('ā ļø Could not collect metrics feedback:', error.message);
}
}
/**
* Generate feedback prompts for operations
*/
async generateFeedbackPrompt(operationName) {
const operation = this.learningEngine.learningDb.operations[operationName];
if (!operation || operation.executions.length === 0) {
return null;
}
const recentExecutions = operation.executions.slice(-5);
const needsFeedback = recentExecutions.filter(e => e.userRating === null);
if (needsFeedback.length === 0) {
return null;
}
return {
operation: operationName,
pendingFeedback: needsFeedback.length,
avgDuration: recentExecutions.reduce((sum, e) => sum + e.duration, 0) / recentExecutions.length,
successRate: recentExecutions.filter(e => e.success).length / recentExecutions.length
};
}
// Helper methods
async getExecutionsNeedingFeedback() {
const executions = [];
const cutoffTime = Date.now() - (7 * 24 * 60 * 60 * 1000); // Last 7 days
for (const [operationName, operation] of Object.entries(this.learningEngine.learningDb.operations)) {
const needsFeedback = operation.executions.filter(e =>
e.userRating === null &&
new Date(e.timestamp).getTime() > cutoffTime
);
needsFeedback.forEach(execution => {
executions.push({ ...execution, operation: operationName });
});
}
return executions.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
}
formatFeedbackComments(comments, issues) {
let formatted = comments || '';
if (issues && issues.length > 0) {
formatted += (formatted ? '\n' : '') + 'Issues: ' + issues.join(', ');
}
return formatted;
}
formatCIComments(ciResults) {
const parts = [];
if (ciResults.testFailures > 0) parts.push(`${ciResults.testFailures} test failures`);
if (ciResults.buildFailures > 0) parts.push(`${ciResults.buildFailures} build failures`);
if (ciResults.lintErrors > 0) parts.push(`${ciResults.lintErrors} lint errors`);
if (ciResults.deploymentFailures > 0) parts.push(`${ciResults.deploymentFailures} deployment failures`);
return parts.length > 0 ? 'CI Issues: ' + parts.join(', ') : 'CI: All checks passed';
}
async analyzeOutputMetrics() {
const metrics = [];
try {
// Analyze recent log files for measurable outcomes
const logsDir = path.join(this.projectPath, 'logs');
const logFiles = await fs.readdir(logsDir);
// Look for ccs output files from last 24 hours
const recent = logFiles.filter(f => {
const filePath = path.join(logsDir, f);
const stats = require('fs').statSync(filePath);
const ageHours = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60);
return ageHours < 24 && (f.includes('ccs') || f.includes('analysis'));
});
for (const file of recent.slice(0, 3)) {
const content = await fs.readFile(path.join(logsDir, file), 'utf-8');
const fileMetrics = this.extractMetricsFromContent(content, file);
metrics.push(...fileMetrics);
}
} catch (error) {
// Non-critical error
}
return metrics;
}
extractMetricsFromContent(content, filename) {
const metrics = [];
// Look for performance indicators
const performanceRegex = /(\d+)\s*(ms|seconds?)\s*(response|duration|time)/gi;
const performanceMatches = content.match(performanceRegex);
if (performanceMatches) {
metrics.push({
type: 'performance',
filename,
indicators: performanceMatches,
score: this.calculatePerformanceScore(performanceMatches)
});
}
// Look for error indicators
const errorRegex = /(error|failed|exception|timeout)/gi;
const errorMatches = content.match(errorRegex);
if (errorMatches) {
metrics.push({
type: 'errors',
filename,
count: errorMatches.length,
score: Math.max(1, 5 - Math.min(4, errorMatches.length))
});
}
return metrics;
}
calculatePerformanceScore(performanceMatches) {
// Simple scoring based on performance indicators
let score = 5;
performanceMatches.forEach(match => {
const numbers = match.match(/\d+/g);
if (numbers) {
const value = parseInt(numbers[0]);
if (match.includes('ms')) {
if (value > 5000) score -= 1; // Slow response
if (value > 10000) score -= 1; // Very slow
} else if (match.includes('second')) {
if (value > 30) score -= 1; // Slow operation
if (value > 60) score -= 1; // Very slow
}
}
});
return Math.max(1, score);
}
async recordMetricFeedback(metric) {
// Find the most recent execution that could have generated this metric
const recentExecutions = [];
for (const [operationName, operation] of Object.entries(this.learningEngine.learningDb.operations)) {
const recent = operation.executions
.filter(e => e.userRating === null)
.slice(-3);
recentExecutions.push(...recent.map(e => ({ ...e, operation: operationName })));
}
if (recentExecutions.length > 0) {
const execution = recentExecutions[0]; // Most recent
const comments = `Auto-feedback from ${metric.type}: ${JSON.stringify(metric.indicators || metric.count)}`;
await this.learningEngine.recordFeedback(execution.id, metric.score, comments);
}
}
}
module.exports = FeedbackLoop;