aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
586 lines (494 loc) • 18.4 kB
JavaScript
/**
* QA Feedback Loop - Story 12.5
*
* Receive feedback from QA results to adjust pattern confidence
* and deprecate patterns that consistently fail.
*
* Absorbed from Auto-Claude's QA_RESULT episode type.
*
* @module qa-feedback
* @version 1.0.0
*/
const fs = require('fs');
const path = require('path');
// ═══════════════════════════════════════════════════════════════════════════════════
// CONFIGURATION
// ═══════════════════════════════════════════════════════════════════════════════════
const DEFAULT_CONFIG = {
// Confidence adjustments
successBoost: 0.05, // Increase on success
failurePenalty: 0.1, // Decrease on failure
criticalPenalty: 0.2, // Decrease on critical failure
// Deprecation thresholds
deprecateAfterFailures: 3, // Consecutive failures to deprecate
minConfidenceThreshold: 0.3, // Below this = deprecated
// Feedback storage
feedbackStorePath: '.aios/qa-feedback.json',
maxFeedbackHistory: 1000,
// Suggestions
suggestAlternativeThreshold: 0.5, // Confidence below this triggers suggestion
};
// ═══════════════════════════════════════════════════════════════════════════════════
// QA FEEDBACK PROCESSOR
// ═══════════════════════════════════════════════════════════════════════════════════
class QAFeedbackProcessor {
/**
* Create a new QAFeedbackProcessor
*
* @param {Object} options - Configuration options
* @param {string} [options.rootPath] - Project root path
* @param {Object} [options.patternStore] - Pattern store instance
* @param {Object} [options.gotchaRegistry] - Gotcha registry instance
* @param {Object} [options.config] - Config overrides
*/
constructor(options = {}) {
this.rootPath = options.rootPath || process.cwd();
this.config = { ...DEFAULT_CONFIG, ...options.config };
this.storePath = path.join(this.rootPath, this.config.feedbackStorePath);
this.patternStore = options.patternStore || null;
this.gotchaRegistry = options.gotchaRegistry || null;
this._feedbackHistory = null;
}
// ─────────────────────────────────────────────────────────────────────────────────
// STORAGE
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Load feedback history
*/
loadFeedback() {
if (this._feedbackHistory) {
return this._feedbackHistory;
}
if (!fs.existsSync(this.storePath)) {
this._feedbackHistory = {
history: [],
patternStats: {},
metadata: { version: '1.0.0', lastUpdated: null },
};
return this._feedbackHistory;
}
try {
const content = fs.readFileSync(this.storePath, 'utf-8');
this._feedbackHistory = JSON.parse(content);
return this._feedbackHistory;
} catch (error) {
console.error('Failed to load feedback history:', error.message);
this._feedbackHistory = {
history: [],
patternStats: {},
metadata: { version: '1.0.0', lastUpdated: null },
};
return this._feedbackHistory;
}
}
/**
* Save feedback history
*/
saveFeedback() {
if (!this._feedbackHistory) {
return;
}
const dir = path.dirname(this.storePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Enforce max history
if (this._feedbackHistory.history.length > this.config.maxFeedbackHistory) {
this._feedbackHistory.history = this._feedbackHistory.history.slice(
-this.config.maxFeedbackHistory,
);
}
this._feedbackHistory.metadata.lastUpdated = new Date().toISOString();
fs.writeFileSync(this.storePath, JSON.stringify(this._feedbackHistory, null, 2), 'utf-8');
}
// ─────────────────────────────────────────────────────────────────────────────────
// PROCESS QA RESULT
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Process QA result and update pattern confidence
*
* @param {Object} qaResult - QA result from qa-review-build
* @param {Object} context - Context information
* @returns {Object} Feedback processing result
*/
processQAResult(qaResult, context = {}) {
this.loadFeedback();
const result = {
processed: true,
patternsAffected: [],
gotchasCreated: [],
suggestions: [],
actions: [],
};
// Extract pattern from context
const patternId = context.patternId || this._inferPattern(context);
if (!patternId) {
result.processed = false;
result.reason = 'No pattern identified';
return result;
}
// Determine outcome
const outcome = this._determineOutcome(qaResult);
// Record feedback
const feedback = {
id: this._generateId(),
patternId,
storyId: context.storyId,
outcome: outcome.status,
severity: outcome.severity,
issues: outcome.issues,
timestamp: new Date().toISOString(),
};
this._feedbackHistory.history.push(feedback);
// Update pattern stats
const stats = this._updatePatternStats(patternId, outcome);
result.patternsAffected.push({ patternId, stats });
// Process based on outcome
if (outcome.status === 'failure') {
// Check for deprecation
if (stats.consecutiveFailures >= this.config.deprecateAfterFailures) {
result.actions.push({
type: 'deprecate',
patternId,
reason: `${stats.consecutiveFailures} consecutive failures`,
});
this._deprecatePattern(patternId);
}
// Create gotcha if critical
if (outcome.severity === 'critical' && this.gotchaRegistry) {
const gotcha = this._createGotchaFromFailure(patternId, outcome, context);
if (gotcha) {
result.gotchasCreated.push(gotcha);
}
}
// Suggest alternatives
const suggestions = this._suggestAlternatives(patternId, context);
result.suggestions = suggestions;
}
// Adjust confidence in pattern store
this._adjustPatternConfidence(patternId, outcome);
this.saveFeedback();
return result;
}
/**
* Determine outcome from QA result
* @private
*/
_determineOutcome(qaResult) {
const outcome = {
status: 'success',
severity: 'none',
issues: [],
};
// Check gate decision
if (qaResult.gateDecision === 'FAIL') {
outcome.status = 'failure';
outcome.severity = 'critical';
} else if (qaResult.gateDecision === 'CONCERNS') {
outcome.status = 'partial';
outcome.severity = 'high';
}
// Check for blocking issues
if (qaResult.blockingIssues?.length > 0) {
outcome.status = 'failure';
outcome.severity = 'critical';
outcome.issues = qaResult.blockingIssues;
}
// Check security issues
if (qaResult.securityChecklist?.critical > 0) {
outcome.status = 'failure';
outcome.severity = 'critical';
outcome.issues.push('Critical security vulnerabilities');
}
// Check test failures
if (qaResult.testing?.unit?.failed > 0 || qaResult.testing?.integration?.failed > 0) {
if (outcome.status === 'success') {
outcome.status = 'partial';
outcome.severity = 'high';
}
outcome.issues.push('Test failures detected');
}
return outcome;
}
/**
* Infer pattern from context
* @private
*/
_inferPattern(context) {
if (context.sequence) {
return `sequence:${context.sequence.join('->')}`;
}
if (context.workflow) {
return `workflow:${context.workflow}`;
}
if (context.agent && context.action) {
return `${context.agent}:${context.action}`;
}
return null;
}
/**
* Update pattern statistics
* @private
*/
_updatePatternStats(patternId, outcome) {
if (!this._feedbackHistory.patternStats[patternId]) {
this._feedbackHistory.patternStats[patternId] = {
totalExecutions: 0,
successes: 0,
failures: 0,
partials: 0,
consecutiveFailures: 0,
lastOutcome: null,
lastExecuted: null,
};
}
const stats = this._feedbackHistory.patternStats[patternId];
stats.totalExecutions++;
stats.lastOutcome = outcome.status;
stats.lastExecuted = new Date().toISOString();
if (outcome.status === 'success') {
stats.successes++;
stats.consecutiveFailures = 0;
} else if (outcome.status === 'failure') {
stats.failures++;
stats.consecutiveFailures++;
} else {
stats.partials++;
// Don't reset consecutive failures for partial
}
return { ...stats };
}
/**
* Adjust pattern confidence in pattern store
* @private
*/
_adjustPatternConfidence(patternId, outcome) {
if (!this.patternStore) {
return;
}
try {
const pattern = this.patternStore.getPattern(patternId);
if (!pattern) {
return;
}
let adjustment = 0;
if (outcome.status === 'success') {
adjustment = this.config.successBoost;
} else if (outcome.status === 'failure') {
adjustment =
outcome.severity === 'critical'
? -this.config.criticalPenalty
: -this.config.failurePenalty;
} else {
adjustment = -this.config.failurePenalty / 2;
}
const newConfidence = Math.max(0, Math.min(1, pattern.confidence + adjustment));
this.patternStore.updatePattern(patternId, {
confidence: newConfidence,
status: newConfidence < this.config.minConfidenceThreshold ? 'deprecated' : pattern.status,
});
} catch (error) {
console.error('Failed to adjust pattern confidence:', error.message);
}
}
/**
* Deprecate pattern
* @private
*/
_deprecatePattern(patternId) {
if (!this.patternStore) {
return;
}
try {
this.patternStore.updatePattern(patternId, {
status: 'deprecated',
deprecatedAt: new Date().toISOString(),
deprecatedReason: 'Consecutive QA failures',
});
} catch (error) {
console.error('Failed to deprecate pattern:', error.message);
}
}
/**
* Create gotcha from failure
* @private
*/
_createGotchaFromFailure(patternId, outcome, context) {
if (!this.gotchaRegistry) {
return null;
}
try {
return this.gotchaRegistry.recordGotcha({
pattern: patternId,
context: context.agent || 'unknown',
error: outcome.issues.join('; '),
reason: `Pattern failed QA with ${outcome.severity} severity`,
keywords: [context.agent, context.action, 'qa-failure'].filter(Boolean),
source: 'qa-feedback',
});
} catch (error) {
console.error('Failed to create gotcha:', error.message);
return null;
}
}
/**
* Suggest alternatives for failing pattern
* @private
*/
_suggestAlternatives(patternId, _context) {
const suggestions = [];
// Get pattern stats
const stats = this._feedbackHistory.patternStats[patternId];
if (!stats) {
return suggestions;
}
// Calculate success rate
const successRate = stats.successes / stats.totalExecutions;
if (successRate < this.config.suggestAlternativeThreshold) {
// Look for similar successful patterns
const alternatives = this._findSuccessfulAlternatives(patternId);
for (const alt of alternatives) {
suggestions.push({
type: 'alternative_pattern',
original: patternId,
suggested: alt.patternId,
reason: `Higher success rate: ${(alt.successRate * 100).toFixed(0)}%`,
confidence: alt.successRate,
});
}
// Suggest manual review
if (stats.consecutiveFailures >= 2) {
suggestions.push({
type: 'manual_review',
reason: 'Multiple consecutive failures suggest fundamental issue',
action: 'Review pattern implementation and requirements',
});
}
}
return suggestions;
}
/**
* Find successful alternative patterns
* @private
*/
_findSuccessfulAlternatives(patternId) {
const alternatives = [];
// Extract pattern type
const [type] = patternId.split(':');
for (const [id, stats] of Object.entries(this._feedbackHistory.patternStats)) {
if (id === patternId) continue;
if (!id.startsWith(type)) continue;
const successRate = stats.successes / stats.totalExecutions;
if (successRate >= 0.8 && stats.totalExecutions >= 3) {
alternatives.push({ patternId: id, successRate, stats });
}
}
return alternatives.sort((a, b) => b.successRate - a.successRate).slice(0, 3);
}
/**
* Generate unique ID
* @private
*/
_generateId() {
const timestamp = Date.now().toString(36);
const random = Math.random().toString(36).substring(2, 8);
return `fb-${timestamp}-${random}`;
}
// ─────────────────────────────────────────────────────────────────────────────────
// STATISTICS
// ─────────────────────────────────────────────────────────────────────────────────
/**
* Get feedback statistics
*
* @returns {Object} Statistics
*/
getStatistics() {
this.loadFeedback();
const history = this._feedbackHistory.history;
const stats = this._feedbackHistory.patternStats;
if (history.length === 0) {
return {
totalFeedback: 0,
successRate: 0,
patternsTracked: 0,
deprecatedPatterns: 0,
recentTrend: 'neutral',
};
}
const outcomes = history.reduce((acc, f) => {
acc[f.outcome] = (acc[f.outcome] || 0) + 1;
return acc;
}, {});
const deprecatedCount = Object.values(stats).filter(
(s) => s.consecutiveFailures >= this.config.deprecateAfterFailures,
).length;
// Calculate recent trend (last 10 vs previous 10)
const recent = history.slice(-10);
const previous = history.slice(-20, -10);
const recentSuccessRate = recent.filter((f) => f.outcome === 'success').length / recent.length;
const previousSuccessRate =
previous.length > 0
? previous.filter((f) => f.outcome === 'success').length / previous.length
: recentSuccessRate;
let trend = 'neutral';
if (recentSuccessRate > previousSuccessRate + 0.1) trend = 'improving';
if (recentSuccessRate < previousSuccessRate - 0.1) trend = 'declining';
return {
totalFeedback: history.length,
successRate: (outcomes.success || 0) / history.length,
failureRate: (outcomes.failure || 0) / history.length,
patternsTracked: Object.keys(stats).length,
deprecatedPatterns: deprecatedCount,
recentTrend: trend,
breakdown: outcomes,
};
}
/**
* Get pattern performance report
*
* @param {string} patternId - Pattern ID
* @returns {Object} Performance report
*/
getPatternReport(patternId) {
this.loadFeedback();
const stats = this._feedbackHistory.patternStats[patternId];
if (!stats) {
return null;
}
const history = this._feedbackHistory.history
.filter((f) => f.patternId === patternId)
.slice(-20);
return {
patternId,
stats,
successRate: stats.successes / stats.totalExecutions,
recentHistory: history,
isDeprecated: stats.consecutiveFailures >= this.config.deprecateAfterFailures,
recommendation: this._getRecommendation(stats),
};
}
/**
* Get recommendation for pattern
* @private
*/
_getRecommendation(stats) {
const successRate = stats.successes / stats.totalExecutions;
if (successRate >= 0.9) {
return { action: 'keep', reason: 'High success rate' };
}
if (successRate >= 0.7) {
return { action: 'monitor', reason: 'Moderate success rate' };
}
if (successRate >= 0.5) {
return { action: 'review', reason: 'Below average success rate' };
}
return { action: 'replace', reason: 'Low success rate - consider alternatives' };
}
}
// ═══════════════════════════════════════════════════════════════════════════════════
// EXPORTS
// ═══════════════════════════════════════════════════════════════════════════════════
module.exports = {
QAFeedbackProcessor,
DEFAULT_CONFIG,
};