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!
528 lines
17 KiB
JavaScript
528 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// EOS Engineering Readiness v2 — 10 områden med nuvarande/mål/blockerare/åtgärder
|
|
// Erik-krav: "Varje område bör ha: nuvarande poäng, mål, blockerande problem, rekommenderade åtgärder"
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
import { execSync } from 'child_process';
|
|
import { existsSync, writeFileSync } from 'fs';
|
|
|
|
const READINESS_PATH = '/home/bernt/.openclaw/workspace/EOS/readiness-report-v2.json';
|
|
|
|
/**
|
|
* 10 områden med detaljerad mätning
|
|
*/
|
|
const READINESS_AREAS = [
|
|
{
|
|
id: 'git',
|
|
name: 'Git',
|
|
weight: 1.0,
|
|
target: 90,
|
|
measures: [
|
|
{
|
|
name: 'Ocommitade filer',
|
|
check: () => {
|
|
try {
|
|
const status = execSync('git status --short', {
|
|
cwd: '/home/bernt/repos/quixzoom.com',
|
|
encoding: 'utf8'
|
|
});
|
|
const uncommitted = status.trim().split('\n').filter(l => l.trim()).length;
|
|
const score = Math.max(0, 100 - uncommitted * 2);
|
|
return {
|
|
score,
|
|
evidence: `${uncommitted} ocommitade filer`,
|
|
blocker: uncommitted > 10,
|
|
action: uncommitted > 0 ? `Commita eller .gitignore ${uncommitted} filer` : null
|
|
};
|
|
} catch {
|
|
return { score: 0, evidence: 'Kunde inte köra git status', blocker: true, action: 'Kontrollera git-konfiguration' };
|
|
}
|
|
}
|
|
},
|
|
{
|
|
name: 'Git-konfiguration',
|
|
check: () => {
|
|
const hasGit = existsSync('/home/bernt/repos/quixzoom.com/.git');
|
|
return {
|
|
score: hasGit ? 100 : 0,
|
|
evidence: hasGit ? 'Git-repo hittat' : 'Inget Git-repo',
|
|
blocker: !hasGit,
|
|
action: !hasGit ? 'Initiera git-repo' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Branch-skydd',
|
|
check: () => {
|
|
// Kan inte kontrollera direkt, antar att det saknas
|
|
return {
|
|
score: 0,
|
|
evidence: 'Branch-skydd ej verifierat',
|
|
blocker: true,
|
|
action: 'Konfigurera branch-skydd på GitHub/GitLab'
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'cicd',
|
|
name: 'CI/CD',
|
|
weight: 1.0,
|
|
target: 90,
|
|
measures: [
|
|
{
|
|
name: 'CI/CD pipeline',
|
|
check: () => {
|
|
const hasCI = existsSync('/home/bernt/repos/quixzoom.com/.github/workflows') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/.gitlab-ci.yml');
|
|
return {
|
|
score: hasCI ? 100 : 0,
|
|
evidence: hasCI ? 'CI/CD hittad' : 'Ingen CI/CD',
|
|
blocker: !hasCI,
|
|
action: !hasCI ? 'Sätt upp GitHub Actions eller GitLab CI' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Automatiserade tester i pipeline',
|
|
check: () => {
|
|
// Kan inte verifiera utan att läsa CI-config
|
|
return {
|
|
score: 0,
|
|
evidence: 'Ej verifierat',
|
|
blocker: true,
|
|
action: 'Lägg till test-steg i CI-pipeline'
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'infrastructure',
|
|
name: 'Infrastructure as Code',
|
|
weight: 1.0,
|
|
target: 80,
|
|
measures: [
|
|
{
|
|
name: 'IaC-verktyg',
|
|
check: () => {
|
|
const hasIaC = existsSync('/home/bernt/repos/quixzoom.com/terraform') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/cloudformation') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/cdk');
|
|
return {
|
|
score: hasIaC ? 100 : 0,
|
|
evidence: hasIaC ? 'IaC hittad' : 'Ingen IaC',
|
|
blocker: !hasIaC,
|
|
action: !hasIaC ? 'Skapa Terraform/CloudFormation för AWS-resurser' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Miljökonfiguration',
|
|
check: () => {
|
|
const hasEnv = existsSync('/home/bernt/repos/quixzoom.com/.env.example');
|
|
return {
|
|
score: hasEnv ? 100 : 0,
|
|
evidence: hasEnv ? '.env.example hittad' : 'Ingen .env.example',
|
|
blocker: false,
|
|
action: !hasEnv ? 'Skapa .env.example' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'api-contracts',
|
|
name: 'API Contracts',
|
|
weight: 0.8,
|
|
target: 80,
|
|
measures: [
|
|
{
|
|
name: 'OpenAPI-spec',
|
|
check: () => {
|
|
const hasOpenAPI = existsSync('/home/bernt/repos/quixzoom.com/openapi.yaml') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/openapi.json');
|
|
return {
|
|
score: hasOpenAPI ? 100 : 0,
|
|
evidence: hasOpenAPI ? 'OpenAPI hittad' : 'Ingen OpenAPI',
|
|
blocker: !hasOpenAPI,
|
|
action: !hasOpenAPI ? 'Skapa OpenAPI-spec för alla endpoints' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'API-dokumentation',
|
|
check: () => {
|
|
const hasDocs = existsSync('/home/bernt/repos/quixzoom.com/API.md') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/docs/api.md');
|
|
return {
|
|
score: hasDocs ? 100 : 0,
|
|
evidence: hasDocs ? 'API-docs hittad' : 'Ingen API-dokumentation',
|
|
blocker: false,
|
|
action: !hasDocs ? 'Skapa API-dokumentation' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'database',
|
|
name: 'Database Migrations',
|
|
weight: 1.0,
|
|
target: 90,
|
|
measures: [
|
|
{
|
|
name: 'Migrationer',
|
|
check: () => {
|
|
const hasMigrations = existsSync('/home/bernt/repos/quixzoom.com/migrations') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/prisma');
|
|
return {
|
|
score: hasMigrations ? 100 : 0,
|
|
evidence: hasMigrations ? 'Migrationer hittade' : 'Inga migrationer',
|
|
blocker: !hasMigrations,
|
|
action: !hasMigrations ? 'Sätt upp Prisma eller liknande migrationsverktyg' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Migration i CI',
|
|
check: () => {
|
|
return {
|
|
score: 0,
|
|
evidence: 'Ej verifierat',
|
|
blocker: true,
|
|
action: 'Lägg till migration-steg i CI-pipeline'
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'testing',
|
|
name: 'Testing',
|
|
weight: 1.0,
|
|
target: 80,
|
|
measures: [
|
|
{
|
|
name: 'Tester',
|
|
check: () => {
|
|
const hasTests = existsSync('/home/bernt/repos/quixzoom.com/tests') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/__tests__');
|
|
return {
|
|
score: hasTests ? 100 : 0,
|
|
evidence: hasTests ? 'Tester hittade' : 'Inga tester',
|
|
blocker: !hasTests,
|
|
action: !hasTests ? 'Skapa första testerna' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Test-konfiguration',
|
|
check: () => {
|
|
const hasConfig = existsSync('/home/bernt/repos/quixzoom.com/jest.config.js') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/vitest.config.ts');
|
|
return {
|
|
score: hasConfig ? 100 : 0,
|
|
evidence: hasConfig ? 'Test-config hittad' : 'Ingen test-config',
|
|
blocker: !hasConfig,
|
|
action: !hasConfig ? 'Konfigurera Jest/Vitest' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'observability',
|
|
name: 'Observability',
|
|
weight: 0.8,
|
|
target: 70,
|
|
measures: [
|
|
{
|
|
name: 'Loggning',
|
|
check: () => {
|
|
const hasLogging = existsSync('/home/bernt/repos/quixzoom.com/src/lib/logger.ts') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/src/utils/logger.js');
|
|
return {
|
|
score: hasLogging ? 100 : 0,
|
|
evidence: hasLogging ? 'Logger hittad' : 'Ingen logger',
|
|
blocker: false,
|
|
action: !hasLogging ? 'Skapa central logger' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Health checks',
|
|
check: () => {
|
|
const hasHealth = existsSync('/home/bernt/repos/quixzoom.com/src/routes/health.ts') ||
|
|
existsSync('/home/bernt/repos/quixzoom.com/src/api/health.js');
|
|
return {
|
|
score: hasHealth ? 100 : 0,
|
|
evidence: hasHealth ? 'Health endpoint hittad' : 'Ingen health endpoint',
|
|
blocker: false,
|
|
action: !hasHealth ? 'Skapa /health endpoint' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'backup',
|
|
name: 'Backup & Rollback',
|
|
weight: 0.9,
|
|
target: 80,
|
|
measures: [
|
|
{
|
|
name: 'Backup-procedur',
|
|
check: () => {
|
|
const hasBackup = existsSync('/home/bernt/repos/quixzoom.com/scripts/backup.sh');
|
|
return {
|
|
score: hasBackup ? 100 : 0,
|
|
evidence: hasBackup ? 'Backup-script hittat' : 'Inget backup-script',
|
|
blocker: !hasBackup,
|
|
action: !hasBackup ? 'Skapa backup-script för databas' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Rollback-procedur',
|
|
check: () => {
|
|
const hasRollback = existsSync('/home/bernt/repos/quixzoom.com/scripts/rollback.sh');
|
|
return {
|
|
score: hasRollback ? 100 : 0,
|
|
evidence: hasRollback ? 'Rollback-script hittat' : 'Inget rollback-script',
|
|
blocker: !hasRollback,
|
|
action: !hasRollback ? 'Skapa rollback-script' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'security',
|
|
name: 'Security',
|
|
weight: 0.9,
|
|
target: 70,
|
|
measures: [
|
|
{
|
|
name: 'Dependency scanning',
|
|
check: () => {
|
|
return {
|
|
score: 0,
|
|
evidence: 'Ej verifierat',
|
|
blocker: false,
|
|
action: 'Lägg till npm audit i CI'
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Secret-hantering',
|
|
check: () => {
|
|
const hasEnv = existsSync('/home/bernt/repos/quixzoom.com/.env.example');
|
|
return {
|
|
score: hasEnv ? 50 : 0,
|
|
evidence: hasEnv ? '.env.example finns' : 'Ingen secret-hantering',
|
|
blocker: false,
|
|
action: 'Använd AWS Secrets Manager eller liknande'
|
|
};
|
|
}
|
|
}
|
|
]
|
|
},
|
|
{
|
|
id: 'agent-safety',
|
|
name: 'Agent Safety',
|
|
weight: 1.0,
|
|
target: 100,
|
|
measures: [
|
|
{
|
|
name: 'EOS-kontrakt',
|
|
check: () => {
|
|
const hasContract = existsSync('/home/bernt/.openclaw/workspace/EOS/engineering-contract.mjs');
|
|
return {
|
|
score: hasContract ? 100 : 0,
|
|
evidence: hasContract ? 'EOS-kontrakt hittat' : 'Inget EOS-kontrakt',
|
|
blocker: !hasContract,
|
|
action: !hasContract ? 'Skapa EOS-kontrakt' : null
|
|
};
|
|
}
|
|
},
|
|
{
|
|
name: 'Immutable Truth',
|
|
check: () => {
|
|
const hasTruth = existsSync('/home/bernt/.openclaw/workspace/EOS/immutable-truth.mjs');
|
|
return {
|
|
score: hasTruth ? 100 : 0,
|
|
evidence: hasTruth ? 'Immutable Truth hittad' : 'Ingen Immutable Truth',
|
|
blocker: !hasTruth,
|
|
action: !hasTruth ? 'Skapa Immutable Truth-validering' : null
|
|
};
|
|
}
|
|
}
|
|
]
|
|
}
|
|
];
|
|
|
|
class EngineeringReadinessV2 {
|
|
constructor() {
|
|
this.areas = READINESS_AREAS;
|
|
}
|
|
|
|
calculate() {
|
|
console.log('📊 Beräknar Engineering Readiness v2...\n');
|
|
|
|
const results = [];
|
|
let totalScore = 0;
|
|
let totalWeight = 0;
|
|
let totalBlockers = 0;
|
|
|
|
for (const area of this.areas) {
|
|
const areaResult = this.calculateArea(area);
|
|
results.push(areaResult);
|
|
|
|
totalScore += areaResult.score * area.weight;
|
|
totalWeight += area.weight;
|
|
totalBlockers += areaResult.blockers.length;
|
|
}
|
|
|
|
const overallScore = Math.round(totalScore / totalWeight);
|
|
|
|
const report = {
|
|
timestamp: new Date().toISOString(),
|
|
overall: overallScore,
|
|
totalBlockers,
|
|
areas: results,
|
|
thresholds: {
|
|
critical: 60,
|
|
warning: 80,
|
|
ready: 90
|
|
},
|
|
status: overallScore >= 90 ? 'READY' :
|
|
overallScore >= 60 ? 'WARNING' : 'CRITICAL'
|
|
};
|
|
|
|
this.saveReport(report);
|
|
this.printReport(report);
|
|
|
|
return report;
|
|
}
|
|
|
|
calculateArea(area) {
|
|
let areaScore = 0;
|
|
const measures = [];
|
|
const blockers = [];
|
|
const actions = [];
|
|
|
|
for (const measure of area.measures) {
|
|
const result = measure.check();
|
|
measures.push({
|
|
name: measure.name,
|
|
score: result.score,
|
|
evidence: result.evidence,
|
|
blocker: result.blocker,
|
|
action: result.action
|
|
});
|
|
areaScore += result.score;
|
|
|
|
if (result.blocker) {
|
|
blockers.push({
|
|
measure: measure.name,
|
|
evidence: result.evidence,
|
|
action: result.action
|
|
});
|
|
}
|
|
|
|
if (result.action) {
|
|
actions.push(result.action);
|
|
}
|
|
}
|
|
|
|
const avgScore = Math.round(areaScore / area.measures.length);
|
|
|
|
return {
|
|
id: area.id,
|
|
name: area.name,
|
|
weight: area.weight,
|
|
target: area.target,
|
|
score: avgScore,
|
|
gap: area.target - avgScore,
|
|
measures,
|
|
blockers,
|
|
actions,
|
|
status: avgScore >= area.target ? 'READY' :
|
|
avgScore >= 60 ? 'WARNING' : 'CRITICAL'
|
|
};
|
|
}
|
|
|
|
saveReport(report) {
|
|
writeFileSync(READINESS_PATH, JSON.stringify(report, null, 2));
|
|
}
|
|
|
|
printReport(report) {
|
|
console.log('╔═══════════════════════════════════════════════════════════════╗');
|
|
console.log('║ ENGINEERING READINESS v2 ║');
|
|
console.log('║ Sprint: Engineering Readiness v1 ║');
|
|
console.log('╚═══════════════════════════════════════════════════════════════╝');
|
|
console.log();
|
|
|
|
const overallIcon = report.status === 'READY' ? '✅' :
|
|
report.status === 'WARNING' ? '⚠️' : '🔴';
|
|
console.log(`${overallIcon} ÖVERGRIPANDE: ${report.overall}%`);
|
|
console.log(` Status: ${report.status}`);
|
|
console.log(` Blockerare: ${report.totalBlockers}`);
|
|
console.log();
|
|
|
|
console.log('📋 PER OMRÅDE\n');
|
|
for (const area of report.areas) {
|
|
const icon = area.status === 'READY' ? '✅' :
|
|
area.status === 'WARNING' ? '⚠️' : '🔴';
|
|
const bar = '█'.repeat(area.score / 5) + '░'.repeat(20 - area.score / 5);
|
|
console.log(`${icon} ${area.name}`);
|
|
console.log(` [${bar}] ${area.score}% (mål: ${area.target}%)`);
|
|
console.log(` Gap: ${area.gap > 0 ? '+' : ''}${area.gap}%`);
|
|
|
|
if (area.blockers.length > 0) {
|
|
console.log(` 🔴 Blockerare:`);
|
|
for (const blocker of area.blockers) {
|
|
console.log(` • ${blocker.measure}: ${blocker.evidence}`);
|
|
console.log(` Åtgärd: ${blocker.action}`);
|
|
}
|
|
}
|
|
|
|
if (area.actions.length > 0 && area.blockers.length === 0) {
|
|
console.log(` 💡 Rekommendationer:`);
|
|
for (const action of area.actions) {
|
|
console.log(` • ${action}`);
|
|
}
|
|
}
|
|
|
|
console.log();
|
|
}
|
|
|
|
console.log('🎯 TRÖSKLAR\n');
|
|
console.log(` Critical: <${report.thresholds.critical}%`);
|
|
console.log(` Warning: <${report.thresholds.warning}%`);
|
|
console.log(` Ready: ≥${report.thresholds.ready}%`);
|
|
console.log();
|
|
|
|
console.log('💡 REKOMMENDATION\n');
|
|
if (report.status === 'CRITICAL') {
|
|
console.log(' 🔴 STOP — Agenter får INTE utföra ändringar');
|
|
console.log(` Åtgärda ${report.totalBlockers} blockerare innan fortsatt arbete`);
|
|
} else if (report.status === 'WARNING') {
|
|
console.log(' ⚠️ WARNING — Begränsad agent-aktivitet');
|
|
} else {
|
|
console.log(' ✅ READY — Agenter kan arbeta normalt');
|
|
}
|
|
console.log();
|
|
|
|
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
}
|
|
}
|
|
|
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
|
|
const readiness = new EngineeringReadinessV2();
|
|
readiness.calculate();
|