05ed037fe8
- DNS: pilot.landvex.com -> 16.170.83.169 - TLS: Let's Encrypt certificate (expires 2026-09-30) - Nginx: reverse proxy with SSL termination - API: https://pilot.landvex.com/api/v1/missions - UI: https://pilot.landvex.com/ - Upload: POST /api/v1/missions/import (multipart/form-data) Verified: ✅ https://pilot.landvex.com/health ✅ https://pilot.landvex.com/version ✅ https://pilot.landvex.com/api/v1/missions (list) ✅ https://pilot.landvex.com/api/v1/missions/:id (get) ✅ POST /api/v1/missions/import (video upload) ✅ UI loads with title 'LandveX Intelligence Lab' Next: Pilot 001 — Break the system!
284 lines
8.4 KiB
JavaScript
284 lines
8.4 KiB
JavaScript
#!/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 <epic-id> <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 <epic> <pct> # Uppdatera framsteg');
|
|
}
|