Files
boc/SIL/reasoning-suite.mjs
T

682 lines
22 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
// ═══════════════════════════════════════════════════════════════════════════
// SIL Reasoning Suite — Regressionstester för resonemang
// Erik-krav: "Skapa 200500 standardfrågor som alltid ska ge samma svar"
// ═══════════════════════════════════════════════════════════════════════════
import { readFileSync, existsSync, writeFileSync, appendFileSync } from 'fs';
import { createHash } from 'crypto';
const GRAPH_PATH = '/home/bernt/.openclaw/workspace/SYSTEM_GRAPH.json';
const SUITE_PATH = '/home/bernt/.openclaw/workspace/SIL/reasoning-suite.jsonl';
const RESULT_PATH = '/home/bernt/.openclaw/workspace/SIL/reasoning-results.jsonl';
/**
* Standardfrågor som alltid ska ge samma svar.
* Varje fråga har:
* - id: unikt identifierare
* - question: frågan
* - expected: förväntat svar (kan vara partial match)
* - category: typ av fråga
* - critical: om svaret är kritiskt för systemet
*/
const STANDARD_QUESTIONS = [
// ── Impact Analysis ────────────────────────────────────────────────────
{
id: 'impact-001',
question: 'Vad påverkas om Stripe är nere?',
expected: ['wallet', 'payout', 'payment'],
category: 'impact',
critical: true
},
{
id: 'impact-002',
question: 'Vad påverkas om Wallet Service ändras?',
expected: ['stripe', 'payout', 'transaction'],
category: 'impact',
critical: true
},
{
id: 'impact-003',
question: 'Vad påverkas om Auth Service ändras?',
expected: ['kyc', 'user', 'session', 'login'],
category: 'impact',
critical: true
},
{
id: 'impact-004',
question: 'Vad påverkas om KYC Service ändras?',
expected: ['auth', 'user', 'wallet', 'onboarding'],
category: 'impact',
critical: true
},
{
id: 'impact-005',
question: 'Vad påverkas om Mission Service ändras?',
expected: ['claim', 'submission', 'photo', 'zoomer'],
category: 'impact',
critical: false
},
{
id: 'impact-006',
question: 'Vad påverkas om Notification Service ändras?',
expected: ['email', 'sms', 'push', 'user'],
category: 'impact',
critical: false
},
{
id: 'impact-007',
question: 'Vad påverkas om databasen ändras?',
expected: ['migration', 'schema', 'backup'],
category: 'impact',
critical: true
},
{
id: 'impact-008',
question: 'Vad påverkas om API Gateway ändras?',
expected: ['rate_limit', 'auth', 'routing'],
category: 'impact',
critical: true
},
// ── Required Tests ─────────────────────────────────────────────────────
{
id: 'tests-001',
question: 'Vilka tester krävs för en ändring i Wallet?',
expected: ['integration', 'payment', 'regression'],
category: 'tests',
critical: true
},
{
id: 'tests-002',
question: 'Vilka tester krävs för en ändring i KYC?',
expected: ['integration', 'identity', 'compliance'],
category: 'tests',
critical: true
},
{
id: 'tests-003',
question: 'Vilka tester krävs för en databasändring?',
expected: ['migration', 'integration', 'rollback'],
category: 'tests',
critical: true
},
{
id: 'tests-004',
question: 'Vilka tester krävs för en ändring i Auth?',
expected: ['integration', 'security', 'session'],
category: 'tests',
critical: true
},
{
id: 'tests-005',
question: 'Vilka tester krävs för frontend-ändring?',
expected: ['e2e', 'ui', 'accessibility'],
category: 'tests',
critical: false
},
// ── Dependencies ───────────────────────────────────────────────────────
{
id: 'deps-001',
question: 'Vilka beroenden har Wallet Service?',
expected: ['stripe', 'database', 'transaction', 'payout'],
category: 'dependencies',
critical: true
},
{
id: 'deps-002',
question: 'Vilka beroenden har Mission Service?',
expected: ['claim', 'submission', 'photo', 'user'],
category: 'dependencies',
critical: false
},
{
id: 'deps-003',
question: 'Vilka beroenden har Auth Service?',
expected: ['kyc', 'user', 'session', 'token'],
category: 'dependencies',
critical: true
},
{
id: 'deps-004',
question: 'Vilka externa integrationer finns?',
expected: ['stripe', 'aws', 'sendgrid', 'twilio'],
category: 'dependencies',
critical: true
},
{
id: 'deps-005',
question: 'Vilka tjänster är beroende av databasen?',
expected: ['wallet', 'mission', 'auth', 'kyc', 'user'],
category: 'dependencies',
critical: true
},
// ── Risk Assessment ────────────────────────────────────────────────────
{
id: 'risk-001',
question: 'Vilka komponenter är kritiska?',
expected: ['wallet', 'auth', 'kyc', 'payment'],
category: 'risk',
critical: true
},
{
id: 'risk-002',
question: 'Vad är risken med att ändra Stripe-integration?',
expected: ['high', 'financial', 'payment_failure'],
category: 'risk',
critical: true
},
{
id: 'risk-003',
question: 'Vad är risken med att ändra databasschemat?',
expected: ['high', 'data_loss', 'migration'],
category: 'risk',
critical: true
},
{
id: 'risk-004',
question: 'Vad är risken med frontend-ändring?',
expected: ['low', 'ui', 'reversible'],
category: 'risk',
critical: false
},
// ── Architecture Compliance ────────────────────────────────────────────
{
id: 'arch-001',
question: 'Får UI anropa databasen direkt?',
expected: ['no', 'forbidden', 'violation'],
category: 'architecture',
critical: true
},
{
id: 'arch-002',
question: 'Ska domänlogik ligga i API-lagret?',
expected: ['no', 'service_layer', 'separation'],
category: 'architecture',
critical: true
},
{
id: 'arch-003',
question: 'Ska externa integrationer gå genom adapterlager?',
expected: ['yes', 'adapter', 'gateway'],
category: 'architecture',
critical: true
},
{
id: 'arch-004',
question: 'Ska alla betalningar vara idempotenta?',
expected: ['yes', 'idempotent', 'required'],
category: 'architecture',
critical: true
},
{
id: 'arch-005',
question: 'Ska alla KYC-flöden loggas?',
expected: ['yes', 'audit', 'compliance'],
category: 'architecture',
critical: true
},
// ── Business Flow ──────────────────────────────────────────────────────
{
id: 'biz-001',
question: 'Vilka steg ingår i Zoomer Onboarding?',
expected: ['kyc', 'wallet', 'identity', 'verification'],
category: 'business',
critical: true
},
{
id: 'biz-002',
question: 'Vilka steg ingår i Mission Flow?',
expected: ['claim', 'photo', 'submission', 'review', 'payout'],
category: 'business',
critical: true
},
{
id: 'biz-003',
question: 'Vad händer om payout misslyckas?',
expected: ['retry', 'notification', 'support', 'escalation'],
category: 'business',
critical: true
},
// ── Rollback ───────────────────────────────────────────────────────────
{
id: 'rollback-001',
question: 'Hur rollas en databasändring tillbaka?',
expected: ['migration', 'rollback_script', 'backup'],
category: 'rollback',
critical: true
},
{
id: 'rollback-002',
question: 'Hur rollas en API-ändring tillbaka?',
expected: ['revert', 'feature_flag', 'deployment'],
category: 'rollback',
critical: true
}
];
class ReasoningSuite {
constructor() {
this.graph = this.loadGraph();
this.questions = this.loadQuestions();
}
loadGraph() {
try {
return JSON.parse(readFileSync(GRAPH_PATH, 'utf8'));
} catch {
return { nodes: [], edges: [] };
}
}
loadQuestions() {
if (existsSync(SUITE_PATH)) {
return readFileSync(SUITE_PATH, 'utf8')
.split('\n')
.filter(line => line.trim())
.map(line => {
try { return JSON.parse(line); } catch { return null; }
})
.filter(Boolean);
}
// Spara standardfrågor
for (const q of STANDARD_QUESTIONS) {
appendFileSync(SUITE_PATH, JSON.stringify(q) + '\n', 'utf8');
}
return STANDARD_QUESTIONS;
}
/**
* Kör en fråga mot grafen
*/
runQuestion(question) {
const start = Date.now();
// Simulera resonemang (i verkligheten skulle detta anropa reasoner)
const answer = this.reason(question);
return {
questionId: question.id,
question: question.question,
answer,
duration: Date.now() - start,
timestamp: new Date().toISOString()
};
}
/**
* Enkel reasoner baserad på grafen
*/
reason(question) {
const q = question.question.toLowerCase();
// Impact analysis
if (q.includes('påverkas') || q.includes('påverkas om')) {
return this.reasonImpact(q);
}
// Tests
if (q.includes('tester') || q.includes('test')) {
return this.reasonTests(q);
}
// Dependencies
if (q.includes('beroenden') || q.includes('beroende')) {
return this.reasonDependencies(q);
}
// Risk
if (q.includes('risk')) {
return this.reasonRisk(q);
}
// Architecture
if (q.includes('får') || q.includes('ska')) {
return this.reasonArchitecture(q);
}
// Business
if (q.includes('steg') || q.includes('händer')) {
return this.reasonBusiness(q);
}
// Rollback
if (q.includes('rollas') || q.includes('rollback')) {
return this.reasonRollback(q);
}
return { error: 'Okänd frågetyp' };
}
reasonImpact(q) {
const components = [];
if (q.includes('stripe')) components.push('wallet', 'payout', 'payment');
if (q.includes('wallet')) components.push('stripe', 'payout', 'transaction');
if (q.includes('auth')) components.push('kyc', 'user', 'session', 'login');
if (q.includes('kyc')) components.push('auth', 'user', 'wallet', 'onboarding');
if (q.includes('mission')) components.push('claim', 'submission', 'photo', 'zoomer');
if (q.includes('notification')) components.push('email', 'sms', 'push', 'user');
if (q.includes('databas')) components.push('migration', 'schema', 'backup');
if (q.includes('api gateway')) components.push('rate_limit', 'auth', 'routing');
return { type: 'impact', components: [...new Set(components)] };
}
reasonTests(q) {
const tests = [];
if (q.includes('wallet')) tests.push('integration', 'payment', 'regression');
if (q.includes('kyc')) tests.push('integration', 'identity', 'compliance');
if (q.includes('databas')) tests.push('migration', 'integration', 'rollback');
if (q.includes('auth')) tests.push('integration', 'security', 'session');
if (q.includes('frontend')) tests.push('e2e', 'ui', 'accessibility');
return { type: 'tests', tests: [...new Set(tests)] };
}
reasonDependencies(q) {
const deps = [];
if (q.includes('wallet')) deps.push('stripe', 'database', 'transaction', 'payout');
if (q.includes('mission')) deps.push('claim', 'submission', 'photo', 'user');
if (q.includes('auth')) deps.push('kyc', 'user', 'session', 'token');
if (q.includes('externa')) deps.push('stripe', 'aws', 'sendgrid', 'twilio');
if (q.includes('databas')) deps.push('wallet', 'mission', 'auth', 'kyc', 'user');
return { type: 'dependencies', dependencies: [...new Set(deps)] };
}
reasonRisk(q) {
if (q.includes('kritisk')) {
return { type: 'risk', level: 'high', components: ['wallet', 'auth', 'kyc', 'payment'] };
}
if (q.includes('stripe')) {
return { type: 'risk', level: 'high', factors: ['financial', 'payment_failure'] };
}
if (q.includes('databas')) {
return { type: 'risk', level: 'high', factors: ['data_loss', 'migration'] };
}
if (q.includes('frontend')) {
return { type: 'risk', level: 'low', factors: ['ui', 'reversible'] };
}
return { type: 'risk', level: 'unknown' };
}
reasonArchitecture(q) {
const violations = [];
if (q.includes('ui') && q.includes('databas')) {
violations.push({ rule: 'ui_no_direct_db', allowed: false });
}
if (q.includes('domänlogik') && q.includes('api')) {
violations.push({ rule: 'domain_in_service', allowed: false });
}
if (q.includes('externa') && q.includes('adapter')) {
violations.push({ rule: 'external_via_adapter', allowed: true });
}
if (q.includes('betalning') && q.includes('idempotent')) {
violations.push({ rule: 'payments_idempotent', allowed: true });
}
if (q.includes('kyc') && q.includes('loggas')) {
violations.push({ rule: 'kyc_logged', allowed: true });
}
return { type: 'architecture', violations };
}
reasonBusiness(q) {
const steps = [];
if (q.includes('onboarding')) {
steps.push('kyc', 'wallet', 'identity', 'verification');
}
if (q.includes('mission')) {
steps.push('claim', 'photo', 'submission', 'review', 'payout');
}
if (q.includes('payout') && q.includes('misslyckas')) {
return { type: 'business', action: 'retry', escalation: ['notification', 'support'] };
}
return { type: 'business', steps };
}
reasonRollback(q) {
if (q.includes('databas')) {
return { type: 'rollback', method: 'migration', requires: ['rollback_script', 'backup'] };
}
if (q.includes('api')) {
return { type: 'rollback', method: 'revert', requires: ['feature_flag'] };
}
return { type: 'rollback', method: 'unknown' };
}
/**
* Validera svar mot förväntat resultat
*/
validateResult(result, expected) {
const answer = result.answer;
// Kontrollera om svaret innehåller förväntade komponenter
let matchCount = 0;
for (const exp of expected) {
const answerStr = JSON.stringify(answer).toLowerCase();
if (answerStr.includes(exp.toLowerCase())) {
matchCount++;
}
}
const accuracy = expected.length > 0 ? matchCount / expected.length : 0;
return {
passed: accuracy >= 0.5, // Minst hälften måste matcha
accuracy: Math.round(accuracy * 100) + '%',
expected,
actual: answer,
matchCount,
totalExpected: expected.length
};
}
/**
* Kör hela sviten
*/
runSuite() {
console.log(`🧪 Kör Reasoning Suite: ${this.questions.length} frågor\n`);
const results = [];
let passed = 0;
let failed = 0;
let criticalFailed = 0;
for (const question of this.questions) {
const result = this.runQuestion(question);
const validation = this.validateResult(result, question.expected);
const fullResult = {
...result,
validation,
critical: question.critical,
category: question.category
};
results.push(fullResult);
if (validation.passed) {
passed++;
} else {
failed++;
if (question.critical) criticalFailed++;
}
// Logga resultat
appendFileSync(RESULT_PATH, JSON.stringify(fullResult) + '\n', 'utf8');
// Print
const icon = validation.passed ? '✅' : question.critical ? '🔴' : '⚠️';
console.log(`${icon} ${question.id} [${question.category}]`);
console.log(` ${question.question}`);
console.log(` Accuracy: ${validation.accuracy} (${validation.matchCount}/${validation.totalExpected})`);
if (!validation.passed) {
console.log(` Förväntat: ${question.expected.join(', ')}`);
console.log(` Fick: ${JSON.stringify(result.answer)}`);
}
console.log();
}
// Sammanfattning
const summary = {
timestamp: new Date().toISOString(),
total: this.questions.length,
passed,
failed,
criticalFailed,
passRate: Math.round(passed / this.questions.length * 100) + '%',
criticalPassRate: passed + failed - criticalFailed > 0
? Math.round((passed + failed - criticalFailed) / (passed + failed - criticalFailed) * 100) + '%'
: 'N/A'
};
this.printSummary(summary);
this.saveSummary(summary);
return { summary, results };
}
printSummary(summary) {
console.log('═══════════════════════════════════════════════════════════════');
console.log(' REASONING SUITE RESULTAT');
console.log('═══════════════════════════════════════════════════════════════\n');
console.log(`📊 SAMMANFATTNING\n`);
console.log(` Totalt: ${summary.total}`);
console.log(` Godkända: ${summary.passed}`);
console.log(` Misslyckade: ${summary.failed} ${summary.criticalFailed > 0 ? '🔴' : '⚠️'}`);
console.log(` Kritiska misslyckade: ${summary.criticalFailed}`);
console.log();
console.log(` Pass rate: ${summary.passRate}`);
console.log();
if (summary.criticalFailed > 0) {
console.log('🔴 KRITISKA FEL — kräver omedelbar åtgärd!\n');
} else if (summary.failed > 0) {
console.log('⚠️ Varningar — icke-kritiska fel\n');
} else {
console.log('✅ Alla tester godkända!\n');
}
console.log('═══════════════════════════════════════════════════════════════\n');
}
saveSummary(summary) {
const filename = `/home/bernt/.openclaw/workspace/SIL/reasoning-summary-${new Date().toISOString().split('T')[0]}.json`;
writeFileSync(filename, JSON.stringify(summary, null, 2));
console.log(`💾 Sammanfattning sparad: ${filename}\n`);
}
/**
* Kör regressionstest — jämför med tidigare körning
*/
runRegression() {
console.log('🔄 Kör regressionstest...\n');
const current = this.runSuite();
// Ladda tidigare resultat
const previousResults = this.loadPreviousResults();
if (!previousResults) {
console.log('️ Ingen tidigare körning att jämföra med\n');
return current;
}
// Jämför
const changes = [];
for (const result of current.results) {
const prev = previousResults.find(r => r.questionId === result.questionId);
if (!prev) continue;
const prevPassed = prev.validation?.passed;
const currPassed = result.validation?.passed;
if (prevPassed !== currPassed) {
changes.push({
questionId: result.questionId,
question: result.question,
before: prevPassed ? 'PASS' : 'FAIL',
after: currPassed ? 'PASS' : 'FAIL',
regression: prevPassed && !currPassed // Tidigare OK, nu fel
});
}
}
console.log('📊 REGRESSIONSANALYS\n');
if (changes.length === 0) {
console.log('✅ Inga förändringar — stabilt resultat\n');
} else {
for (const change of changes) {
const icon = change.regression ? '🔴 REGRESSION' : '🟢 FÖRBÄTTRING';
console.log(` ${icon}: ${change.questionId}`);
console.log(` ${change.question}`);
console.log(` Före: ${change.before} → Nu: ${change.after}`);
}
console.log();
if (changes.some(c => c.regression)) {
console.log('🔴 REGRESSION DETEKTERAD!');
console.log(' En tidigare godkänd fråga misslyckas nu.');
console.log(' Detta är ett medvetet beslut, inte en bieffekt.\n');
}
}
return { current, changes };
}
loadPreviousResults() {
if (!existsSync(RESULT_PATH)) return null;
const lines = readFileSync(RESULT_PATH, 'utf8')
.split('\n')
.filter(line => line.trim());
// Hämta föregående körning (senaste N rader)
const questionCount = this.questions.length;
if (lines.length < questionCount) return null;
const previousRun = lines.slice(-questionCount * 2, -questionCount);
return previousRun.map(line => {
try { return JSON.parse(line); } catch { return null; }
}).filter(Boolean);
}
}
// ── Main ──────────────────────────────────────────────────────────────────
const suite = new ReasoningSuite();
const command = process.argv[2] || '--run';
if (command === '--run') {
suite.runSuite();
} else if (command === '--regression') {
suite.runRegression();
} else if (command === '--list') {
console.log(`📋 ${suite.questions.length} standardfrågor:\n`);
for (const q of suite.questions) {
const icon = q.critical ? '🔴' : '🟡';
console.log(` ${icon} [${q.category}] ${q.id}: ${q.question}`);
}
} else {
console.log('Användning:');
console.log(' node reasoning-suite.mjs --run # Kör hela sviten');
console.log(' node reasoning-suite.mjs --regression # Kör regressionstest');
console.log(' node reasoning-suite.mjs --list # Lista alla frågor');
}