Files
boc/EOS/goal-graph.mjs
T
Bernt 05ed037fe8 pilot.landvex.com: HTTPS + Full Stack Verified
- 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!
2026-07-02 17:34:19 +00:00

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');
}