#!/usr/bin/env node // ═══════════════════════════════════════════════════════════════════════════ // EOS Goal Graph — Vision, mål, epics, features, stories // Erik-krav: "Grafen bör också innehålla: Vision, Produktmål, Epics, Features..." // ═══════════════════════════════════════════════════════════════════════════ import { readFileSync, writeFileSync, existsSync } from 'fs'; const GOAL_GRAPH_PATH = '/home/bernt/.openclaw/workspace/EOS/goal-graph.json'; /** * Standardmål för AAMOS/LandveX */ const DEFAULT_GOALS = { vision: { text: "Landvex bygger den globala infrastrukturen för samhällsinsikt", source: "MEMORY.md#0a" }, productGoals: [ { id: 'pg-001', text: 'quiXzoom ska vara Sveriges ledande plattform för fältdatainsamling', priority: 'HIGH', status: 'active' }, { id: 'pg-002', text: 'Landvex API ska vara den mest omfattande källan för samhällsdata i Europa', priority: 'HIGH', status: 'active' } ], epics: [ { id: 'epic-001', text: 'quiXzoom Launch (Sverige, augusti 2026)', goal: 'pg-001', status: 'in_progress', progress: 65 }, { id: 'epic-002', text: 'Landvex Data Platform v1', goal: 'pg-002', status: 'in_progress', progress: 40 } ], features: [ { id: 'feat-001', text: 'Zoomer Onboarding Flow', epic: 'epic-001', status: 'done' }, { id: 'feat-002', text: 'Mission Assignment Engine', epic: 'epic-001', status: 'in_progress' }, { id: 'feat-003', text: 'Stripe Connect Integration', epic: 'epic-001', status: 'done' } ], architecturePrinciples: [ { id: 'arch-001', text: 'API:et är produkten', source: 'MEMORY.md#0a' }, { id: 'arch-002', text: 'Mobile-first, alltid', source: 'MOBILE_FIRST_STANDARD.md' }, { id: 'arch-003', text: 'Event-driven arkitektur', source: 'EOS design' } ], technicalGoals: [ { id: 'tech-001', text: 'SIL ska kunna analysera PR:er med >85% recall', status: 'in_progress' }, { id: 'tech-002', text: 'Alla tjänster ska ha <100ms p95 latency', status: 'active' } ], businessGoals: [ { id: 'biz-001', text: 'quiXzoom ska generera 100 000 USD i månadsintäkt till december 2026', status: 'active' } ] }; class GoalGraph { constructor() { this.goals = this.loadGoals(); } loadGoals() { if (existsSync(GOAL_GRAPH_PATH)) { return JSON.parse(readFileSync(GOAL_GRAPH_PATH, 'utf8')); } return DEFAULT_GOALS; } saveGoals() { writeFileSync(GOAL_GRAPH_PATH, JSON.stringify(this.goals, null, 2)); } /** * Utvärdera en föreslagen ändring mot målen */ evaluateChange(change) { const impacts = []; // Kontrollera mot varje målkategori for (const goal of this.goals.productGoals) { const impact = this.assessImpact(change, goal); if (impact) impacts.push({ type: 'product', goal, impact }); } for (const principle of this.goals.architecturePrinciples) { const impact = this.assessImpact(change, principle); if (impact) impacts.push({ type: 'architecture', principle, impact }); } for (const tech of this.goals.technicalGoals) { const impact = this.assessImpact(change, tech); if (impact) impacts.push({ type: 'technical', goal: tech, impact }); } return { change, impacts, aligned: impacts.filter(i => i.impact === 'positive').length > impacts.filter(i => i.impact === 'negative').length, recommendation: this.generateRecommendation(impacts) }; } assessImpact(change, goal) { const changeText = JSON.stringify(change).toLowerCase(); const goalText = JSON.stringify(goal).toLowerCase(); // Enkel nyckelordsmatchning const keywords = this.extractKeywords(goalText); const matches = keywords.filter(k => changeText.includes(k)); if (matches.length === 0) return null; // Bedöm om ändringen stödjer eller motverkar målet const positive = matches.length / keywords.length > 0.5; return positive ? 'positive' : 'negative'; } extractKeywords(text) { // Extrahera meningsfulla ord return text .replace(/[^a-zåäö\s]/g, '') .split(/\s+/) .filter(w => w.length > 3) .filter(w => !['skall', 'skulle', 'eller', 'och', 'men', 'för'].includes(w)); } generateRecommendation(impacts) { const positive = impacts.filter(i => i.impact === 'positive').length; const negative = impacts.filter(i => i.impact === 'negative').length; if (negative > 0 && positive === 0) { return 'REJECT — Ändringen motverkar projektets mål'; } if (negative > positive) { return 'REVIEW — Ändringen har blandad påverkan på målen'; } if (positive > 0) { return 'APPROVE — Ändringen stödjer projektets mål'; } return 'NEUTRAL — Ingen tydlig påverkan på målen'; } /** * Visa målgrafen */ showGoals() { console.log('🎯 GOAL GRAPH\n'); console.log('VISION'); console.log(` ${this.goals.vision.text}\n`); console.log('PRODUKTMÅL'); for (const goal of this.goals.productGoals) { const icon = goal.status === 'active' ? '▶️' : '✅'; console.log(` ${icon} [${goal.priority}] ${goal.text}`); } console.log(); console.log('EPICS'); for (const epic of this.goals.epics) { const bar = '█'.repeat(epic.progress / 5) + '░'.repeat(20 - epic.progress / 5); console.log(` ${epic.status === 'done' ? '✅' : '▶️'} ${epic.text}`); console.log(` [${bar}] ${epic.progress}%`); } console.log(); console.log('ARKITEKTPRINCIPER'); for (const principle of this.goals.architecturePrinciples) { console.log(` • ${principle.text}`); } console.log(); console.log('TEKNISKA MÅL'); for (const goal of this.goals.technicalGoals) { console.log(` ${goal.status === 'done' ? '✅' : '▶️'} ${goal.text}`); } console.log(); console.log('AFFÄRSMÅL'); for (const goal of this.goals.businessGoals) { console.log(` ${goal.status === 'done' ? '✅' : '▶️'} ${goal.text}`); } console.log(); } /** * Uppdatera framsteg för ett epic */ updateProgress(epicId, progress) { const epic = this.goals.epics.find(e => e.id === epicId); if (epic) { epic.progress = Math.min(100, Math.max(0, progress)); this.saveGoals(); console.log(`✅ Uppdaterat ${epicId}: ${epic.progress}%`); } } } // ── Main ────────────────────────────────────────────────────────────────── const graph = new GoalGraph(); const command = process.argv[2] || '--show'; if (command === '--show') { graph.showGoals(); } else if (command === '--evaluate') { const change = process.argv[3] || 'Lägg till Redis-cache för Wallet Service'; const result = graph.evaluateChange(change); console.log(`🔍 UTVÄRDERING: "${change}"\n`); console.log(` Rekommendation: ${result.recommendation}`); console.log(` Alignerad: ${result.aligned ? 'Ja' : 'Nej'}`); console.log(` Påverkan: ${result.impacts.length} mål påverkade`); for (const impact of result.impacts) { const icon = impact.impact === 'positive' ? '✅' : '❌'; console.log(` ${icon} [${impact.type}] ${impact.goal?.text || impact.principle?.text}`); } } else if (command === '--progress') { const epicId = process.argv[3]; const progress = parseInt(process.argv[4]); if (!epicId || isNaN(progress)) { console.log('Användning: node goal-graph.mjs --progress '); process.exit(1); } graph.updateProgress(epicId, progress); } else { console.log('Användning:'); console.log(' node goal-graph.mjs --show # Visa målgraf'); console.log(' node goal-graph.mjs --evaluate <ändring> # Utvärdera ändring'); console.log(' node goal-graph.mjs --progress # Uppdatera framsteg'); }