docs: enterprise refactor plan with ralph specs

This commit is contained in:
debian
2026-03-04 16:17:03 -05:00
parent 4c92712d20
commit f8191133c8
204 changed files with 32722 additions and 422 deletions

View File

@@ -0,0 +1,71 @@
"use strict";
/**
* AIEnrichmentService — selects AI provider and runs enrichment asynchronously.
* Triggered manually (POST /api/anomalies/:id/enrich) or automatically for high/critical.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIEnrichmentService = void 0;
const ClaudeProvider_1 = require("./ClaudeProvider");
const OpenAIProvider_1 = require("./OpenAIProvider");
const OllamaProvider_1 = require("./OllamaProvider");
const logger_1 = require("../logger");
class AIEnrichmentService {
constructor(emitter) {
this.emitter = emitter;
this.autoEnrich = process.env['ABE_AI_AUTO_ENRICH'] === 'true';
const minSev = process.env['ABE_AI_MIN_SEVERITY'] ?? 'high';
this.minSeverityRank = AIEnrichmentService.SEVERITY_RANK[minSev] ?? 2;
this.provider = this.createProvider();
}
createProvider() {
const providerName = process.env['ABE_AI_PROVIDER'] ?? 'none';
const model = process.env['ABE_AI_MODEL'];
if (providerName === 'claude') {
const key = process.env['ABE_AI_API_KEY'];
if (!key)
return null;
return new ClaudeProvider_1.ClaudeProvider(key, model);
}
if (providerName === 'openai') {
const key = process.env['ABE_OPENAI_API_KEY'];
if (!key)
return null;
return new OpenAIProvider_1.OpenAIProvider(key, model);
}
if (providerName === 'ollama') {
const url = process.env['ABE_OLLAMA_URL'] ?? 'http://localhost:11434';
return new OllamaProvider_1.OllamaProvider(url, model);
}
return null;
}
/** Check if auto-enrichment should run for this anomaly. */
shouldAutoEnrich(anomaly) {
if (!this.autoEnrich || !this.provider)
return false;
const rank = AIEnrichmentService.SEVERITY_RANK[anomaly.severity] ?? 0;
return rank >= this.minSeverityRank;
}
/** Enrich an anomaly asynchronously and emit WebSocket event when done. */
async enrich(anomaly, context) {
if (!this.provider) {
logger_1.log.warn({ anomalyId: anomaly.id }, 'No AI provider configured');
return;
}
try {
const enrichment = await this.provider.enrich(anomaly, context);
anomaly.aiEnrichment = enrichment;
this.emitter('anomaly:enriched', { anomalyId: anomaly.id, enrichment });
logger_1.log.info({ anomalyId: anomaly.id, provider: this.provider.name }, 'Anomaly enriched');
}
catch (err) {
logger_1.log.error({ anomalyId: anomaly.id, err: err instanceof Error ? err.message : String(err) }, 'AI enrichment failed');
}
}
hasProvider() {
return this.provider !== null;
}
}
exports.AIEnrichmentService = AIEnrichmentService;
AIEnrichmentService.SEVERITY_RANK = {
low: 0, medium: 1, high: 2, critical: 3,
};

View File

@@ -0,0 +1,88 @@
"use strict";
/**
* ClaudeProvider — AI enrichment using Anthropic API.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClaudeProvider = void 0;
const DEFAULT_MODEL = 'claude-haiku-4-5-20251001';
class ClaudeProvider {
constructor(apiKey, model = DEFAULT_MODEL) {
this.name = 'claude';
this.apiKey = apiKey;
this.model = model;
}
async enrich(anomaly, context) {
const prompt = buildPrompt(anomaly, context);
const res = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': this.apiKey,
'anthropic-version': '2023-06-01',
},
body: JSON.stringify({
model: this.model,
max_tokens: 1024,
messages: [{ role: 'user', content: prompt }],
}),
});
if (!res.ok) {
throw new Error(`Anthropic API error: ${res.status} ${await res.text()}`);
}
const data = await res.json();
const text = data.content.find((c) => c.type === 'text')?.text ?? '';
return parseEnrichment(text, this.name, this.model);
}
}
exports.ClaudeProvider = ClaudeProvider;
function buildPrompt(anomaly, context) {
return `You are a senior software engineer analyzing a bug report from an automated web testing tool.
Bug Report:
- Type: ${anomaly.type}
- Severity: ${anomaly.severity}
- Description: ${anomaly.description}
- URL: ${context.url}
- Page Title: ${context.pageTitle}
- Action Trace: ${JSON.stringify(anomaly.actionTrace.slice(-5), null, 2)}
${context.httpLog.length > 0 ? `- HTTP Log: ${JSON.stringify(context.httpLog.slice(-3), null, 2)}` : ''}
${context.consoleErrors.length > 0 ? `- Console Errors: ${context.consoleErrors.slice(-3).join('\n')}` : ''}
Please provide a concise analysis in exactly this JSON format:
{
"rootCause": "One sentence explaining the likely root cause",
"userImpact": "One sentence describing the impact on users",
"suggestedFix": "One to two sentences with a concrete fix suggestion",
"confidence": "low|medium|high"
}`;
}
function parseEnrichment(text, provider, model) {
const debugPrompt = `Bug analysis:\n${text}`;
try {
const match = text.match(/\{[\s\S]*\}/);
if (match) {
const parsed = JSON.parse(match[0]);
return {
rootCause: parsed.rootCause ?? 'Unknown root cause',
userImpact: parsed.userImpact ?? 'Unknown impact',
suggestedFix: parsed.suggestedFix ?? 'No fix suggested',
debugPrompt,
confidence: parsed.confidence ?? 'medium',
generatedAt: Date.now(),
provider,
model,
};
}
}
catch { /* fallback below */ }
return {
rootCause: text.slice(0, 200) || 'Could not parse root cause',
userImpact: 'See full response',
suggestedFix: 'See full response',
debugPrompt,
confidence: 'low',
generatedAt: Date.now(),
provider,
model,
};
}

View File

@@ -0,0 +1,63 @@
"use strict";
/**
* OllamaProvider — AI enrichment using local Ollama API.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OllamaProvider = void 0;
const DEFAULT_MODEL = 'llama3.2';
const DEFAULT_URL = 'http://localhost:11434';
function buildPrompt(anomaly, context) {
return `Analyze this bug and respond ONLY with JSON {"rootCause":"...","userImpact":"...","suggestedFix":"...","confidence":"low|medium|high"}.
Bug: ${anomaly.type} (${anomaly.severity}) at ${context.url}
Description: ${anomaly.description}
Last actions: ${anomaly.actionTrace.slice(-3).map((a) => `${a.type} ${a.selector ?? a.url ?? ''}`).join(' → ')}`;
}
class OllamaProvider {
constructor(baseUrl = DEFAULT_URL, model = DEFAULT_MODEL) {
this.name = 'ollama';
this.baseUrl = baseUrl;
this.model = model;
}
async enrich(anomaly, context) {
const prompt = buildPrompt(anomaly, context);
const res = await fetch(`${this.baseUrl}/api/generate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model: this.model, prompt, stream: false }),
});
if (!res.ok) {
throw new Error(`Ollama API error: ${res.status}`);
}
const data = await res.json();
const text = data.response ?? '';
try {
const match = text.match(/\{[\s\S]*\}/);
if (match) {
const p = JSON.parse(match[0]);
return {
rootCause: p['rootCause'] ?? 'Unknown',
userImpact: p['userImpact'] ?? 'Unknown',
suggestedFix: p['suggestedFix'] ?? 'None',
debugPrompt: text,
confidence: p['confidence'] ?? 'low',
generatedAt: Date.now(),
provider: 'ollama',
model: this.model,
};
}
}
catch { /* fallback */ }
return {
rootCause: text.slice(0, 200),
userImpact: 'See response',
suggestedFix: 'See response',
debugPrompt: text,
confidence: 'low',
generatedAt: Date.now(),
provider: 'ollama',
model: this.model,
};
}
}
exports.OllamaProvider = OllamaProvider;

View File

@@ -0,0 +1,81 @@
"use strict";
/**
* OpenAIProvider — AI enrichment using OpenAI API.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.OpenAIProvider = void 0;
const DEFAULT_MODEL = 'gpt-4o-mini';
function buildPrompt(anomaly, context) {
return `You are a senior software engineer analyzing a bug report.
Bug:
- Type: ${anomaly.type}
- Severity: ${anomaly.severity}
- Description: ${anomaly.description}
- URL: ${context.url}
- Actions: ${JSON.stringify(anomaly.actionTrace.slice(-5))}
${context.httpLog.length > 0 ? `- HTTP: ${JSON.stringify(context.httpLog.slice(-3))}` : ''}
${context.consoleErrors.length > 0 ? `- Errors: ${context.consoleErrors.slice(-3).join('; ')}` : ''}
Respond ONLY with JSON:
{"rootCause":"...","userImpact":"...","suggestedFix":"...","confidence":"low|medium|high"}`;
}
function parseResponse(text, model) {
try {
const match = text.match(/\{[\s\S]*\}/);
if (match) {
const p = JSON.parse(match[0]);
return {
rootCause: p['rootCause'] ?? 'Unknown',
userImpact: p['userImpact'] ?? 'Unknown',
suggestedFix: p['suggestedFix'] ?? 'None',
debugPrompt: text,
confidence: p['confidence'] ?? 'medium',
generatedAt: Date.now(),
provider: 'openai',
model,
};
}
}
catch { /* fallback */ }
return {
rootCause: text.slice(0, 200),
userImpact: 'See response',
suggestedFix: 'See response',
debugPrompt: text,
confidence: 'low',
generatedAt: Date.now(),
provider: 'openai',
model,
};
}
class OpenAIProvider {
constructor(apiKey, model = DEFAULT_MODEL) {
this.name = 'openai';
this.apiKey = apiKey;
this.model = model;
}
async enrich(anomaly, context) {
const prompt = buildPrompt(anomaly, context);
const res = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.apiKey}`,
},
body: JSON.stringify({
model: this.model,
messages: [{ role: 'user', content: prompt }],
max_tokens: 512,
response_format: { type: 'json_object' },
}),
});
if (!res.ok) {
throw new Error(`OpenAI API error: ${res.status} ${await res.text()}`);
}
const data = await res.json();
const text = data.choices[0]?.message?.content ?? '';
return parseResponse(text, this.model);
}
}
exports.OpenAIProvider = OpenAIProvider;