Files
boc/SIL/architecture-policy.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

500 lines
17 KiB
JavaScript

#!/usr/bin/env node
// ═══════════════════════════════════════════════════════════════════════════
// SIL Architecture Policy — Regler som kod
// Erik-krav: "SIL kan kontrollera att arkitekturregler följs"
// ═══════════════════════════════════════════════════════════════════════════
import { readFileSync, existsSync, writeFileSync, appendFileSync } from 'fs';
import { execSync } from 'child_process';
const GRAPH_PATH = '/home/bernt/.openclaw/workspace/SYSTEM_GRAPH.json';
const POLICY_LOG = '/home/bernt/.openclaw/workspace/SIL/policy-violations.jsonl';
/**
* Arkitekturregler som kod.
* Varje regel har:
* - id: unikt nummer
* - name: beskrivande namn
* - severity: CRITICAL, HIGH, MEDIUM, LOW
* - check: funktion som returnerar true om regeln bryts
* - message: förklaring av överträdelsen
*/
const ARCHITECTURE_RULES = [
{
id: 1,
name: 'UI får aldrig anropa databasen direkt',
severity: 'CRITICAL',
category: 'layer_separation',
check: (graph, files) => {
// Kontrollera om frontend-filer innehåller databasanrop
const frontendFiles = files.filter(f =>
f.match(/\.tsx$|\.jsx$|\.ts$|\.js$/) &&
(f.includes('frontend') || f.includes('app') || f.includes('ui'))
);
for (const file of frontendFiles) {
try {
const content = readFileSync(file, 'utf8');
if (content.match(/import.*pg|import.*mysql|import.*prisma|new Pool\(|createConnection/i)) {
return { violated: true, file, evidence: 'Direkt databas-import hittad' };
}
} catch {}
}
return { violated: false };
},
message: 'Frontend-kod får inte innehålla direkta databasanrop. Använd API-lager.'
},
{
id: 2,
name: 'Domänlogik får inte ligga i API-lagret',
severity: 'HIGH',
category: 'layer_separation',
check: (graph, files) => {
// Kontrollera om API-handlers innehåller affärslogik
const apiFiles = files.filter(f =>
f.match(/handler|controller|route/i) &&
!f.includes('service')
);
for (const file of apiFiles) {
try {
const content = readFileSync(file, 'utf8');
// Enkel heuristik: om filen är >100 rader och innehåller affärslogik
const lines = content.split('\n');
if (lines.length > 100) {
const businessLogic = content.match(/if.*balance|if.*amount|calculate|validate.*payment/i);
if (businessLogic) {
return { violated: true, file, evidence: 'Affärslogik i API-handler' };
}
}
} catch {}
}
return { violated: false };
},
message: 'Affärslogik ska ligga i service-lagret, inte i API-handlers.'
},
{
id: 3,
name: 'Alla externa integrationer ska gå genom adapterlager',
severity: 'HIGH',
category: 'integration',
check: (graph, files) => {
// Kontrollera om externa anrop görs direkt från service
const serviceFiles = files.filter(f => f.includes('service'));
for (const file of serviceFiles) {
try {
const content = readFileSync(file, 'utf8');
// Om filen anropar extern API direkt utan adapter
const directCall = content.match(/fetch\(.*stripe|fetch\(.*aws|axios\(.*stripe/i);
const hasAdapter = content.match(/adapter|client|gateway/i);
if (directCall && !hasAdapter) {
return { violated: true, file, evidence: 'Direkt extern anrop utan adapter' };
}
} catch {}
}
return { violated: false };
},
message: 'Externa integrationer ska gå genom adapter/client-lager.'
},
{
id: 4,
name: 'Alla betalningar måste vara idempotenta',
severity: 'CRITICAL',
category: 'payment',
check: (graph, files) => {
// Kontrollera betalnings-relaterade filer
const paymentFiles = files.filter(f =>
f.match(/payment|stripe|wallet|payout/i)
);
for (const file of paymentFiles) {
try {
const content = readFileSync(file, 'utf8');
// Ska innehålla idempotens-kontroll
const hasIdempotency = content.match(/idempot|idempotent|duplicate.*check|already.*processed/i);
if (!hasIdempotency && content.match(/charge|payment|payout/i)) {
return { violated: true, file, evidence: 'Betalningsflöde utan idempotens-kontroll' };
}
} catch {}
}
return { violated: false };
},
message: 'Alla betalningsoperationer måste ha idempotens-kontroll.'
},
{
id: 5,
name: 'Alla KYC-flöden måste loggas',
severity: 'CRITICAL',
category: 'compliance',
check: (graph, files) => {
// Kontrollera KYC-filer
const kycFiles = files.filter(f => f.match(/kyc|identity|verify/i));
for (const file of kycFiles) {
try {
const content = readFileSync(file, 'utf8');
// Ska innehålla loggning
const hasLogging = content.match(/log\.|logger\.|audit|console\.log/i);
if (!hasLogging) {
return { violated: true, file, evidence: 'KYC-flöde utan loggning' };
}
} catch {}
}
return { violated: false };
},
message: 'Alla KYC-operationer måste loggas för compliance.'
},
{
id: 6,
name: 'Alla nya API:er ska ha integrationstester',
severity: 'HIGH',
category: 'testing',
check: (graph, files) => {
// Kontrollera om nya endpoints har tester
const newEndpoints = files.filter(f =>
f.match(/route|endpoint|handler/i) &&
!f.match(/\.test\.|\.spec\./i)
);
for (const endpoint of newEndpoints) {
const testFile = endpoint.replace(/\.(ts|js)$/, '.test.$1');
if (!existsSync(testFile)) {
return { violated: true, file: endpoint, evidence: 'Saknar integrationstest' };
}
}
return { violated: false };
},
message: 'Varje ny endpoint måste ha ett integrationstest.'
},
{
id: 7,
name: 'Kritiska tjänster ska ha circuit breaker',
severity: 'HIGH',
category: 'resilience',
check: (graph, files) => {
const criticalServices = ['wallet', 'payment', 'auth', 'kyc'];
for (const service of criticalServices) {
const serviceFiles = files.filter(f => f.includes(service));
for (const file of serviceFiles) {
try {
const content = readFileSync(file, 'utf8');
const hasCircuitBreaker = content.match(/circuit|circuitbreaker|breaker|fallback/i);
if (!hasCircuitBreaker && content.match(/fetch|axios|request/i)) {
return { violated: true, file, evidence: 'Kritisk tjänst utan circuit breaker' };
}
} catch {}
}
}
return { violated: false };
},
message: 'Kritiska tjänster måste ha circuit breaker på externa anrop.'
},
{
id: 8,
name: 'Ingen hårdkodad konfiguration i källkod',
severity: 'MEDIUM',
category: 'configuration',
check: (graph, files) => {
const configFiles = files.filter(f =>
!f.match(/\.env|config|settings|yaml|yml/i)
);
for (const file of configFiles) {
try {
const content = readFileSync(file, 'utf8');
// Hitta hårdkodade värden
const hardcoded = content.match(/api_key.*=.*['"][a-zA-Z0-9]{20,}['"]|password.*=.*['"][^'"]+['"]/i);
if (hardcoded) {
return { violated: true, file, evidence: 'Hårdkodad konfiguration hittad' };
}
} catch {}
}
return { violated: false };
},
message: 'Konfiguration ska läsas från miljövariabler eller config-filer.'
},
{
id: 9,
name: 'Databasändringar ska ha rollback-script',
severity: 'CRITICAL',
category: 'database',
check: (graph, files) => {
const migrationFiles = files.filter(f => f.match(/migration|\.sql$/i));
for (const file of migrationFiles) {
try {
const content = readFileSync(file, 'utf8');
// Ska innehålla rollback eller down-migration
const hasRollback = content.match(/rollback|down|revert|DROP.*IF.*EXISTS/i);
if (!hasRollback) {
return { violated: true, file, evidence: 'Migration utan rollback' };
}
} catch {}
}
return { violated: false };
},
message: 'Varje databasändring måste ha ett rollback-script.'
},
{
id: 10,
name: 'Alla PR:er ska uppdatera dokumentation',
severity: 'LOW',
category: 'documentation',
check: (graph, files) => {
// Kontrollera om PR innehåller API-ändringar utan doc-uppdatering
const apiChanges = files.filter(f => f.match(/openapi|swagger|\.yaml$/i));
const docChanges = files.filter(f => f.match(/README|CHANGELOG|docs/i));
if (apiChanges.length > 0 && docChanges.length === 0) {
return {
violated: true,
file: apiChanges[0],
evidence: 'API-ändring utan dokumentationsuppdatering'
};
}
return { violated: false };
},
message: 'API-ändringar ska dokumenteras.'
}
];
class ArchitecturePolicy {
constructor() {
this.graph = this.loadGraph();
this.rules = ARCHITECTURE_RULES;
}
loadGraph() {
try {
return JSON.parse(readFileSync(GRAPH_PATH, 'utf8'));
} catch {
return { nodes: [], edges: [] };
}
}
/**
* Kontrollera alla regler mot en uppsättning filer
*/
checkPR(files, prInfo = {}) {
console.log(`🔍 Kontrollerar ${files.length} filer mot ${this.rules.length} arkitekturregler...\n`);
const violations = [];
let criticalCount = 0;
let highCount = 0;
for (const rule of this.rules) {
const result = rule.check(this.graph, files);
if (result.violated) {
violations.push({
ruleId: rule.id,
ruleName: rule.name,
severity: rule.severity,
category: rule.category,
file: result.file,
evidence: result.evidence,
message: rule.message,
timestamp: new Date().toISOString()
});
if (rule.severity === 'CRITICAL') criticalCount++;
if (rule.severity === 'HIGH') highCount++;
// Logga överträdelse
appendFileSync(POLICY_LOG, JSON.stringify({
...violations[violations.length - 1],
pr: prInfo
}) + '\n', 'utf8');
}
}
const report = {
timestamp: new Date().toISOString(),
pr: prInfo,
summary: {
totalRules: this.rules.length,
violations: violations.length,
critical: criticalCount,
high: highCount,
medium: violations.filter(v => v.severity === 'MEDIUM').length,
low: violations.filter(v => v.severity === 'LOW').length
},
violations,
passed: violations.length === 0,
// Kan merge blockeras?
blockMerge: criticalCount > 0,
requiresReview: highCount > 0
};
this.printReport(report);
return report;
}
/**
* Kontrollera regler för ett repo
*/
checkRepo(repoPath) {
// Hämta alla källkodsfiler
try {
const files = execSync(
`find ${repoPath} -type f \( -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.sql" -o -name "*.yaml" -o -name "*.yml" \) | head -100`,
{ encoding: 'utf8' }
).trim().split('\n').filter(f => f);
return this.checkPR(files, { repo: repoPath, type: 'full_scan' });
} catch (e) {
console.error('Fel vid repo-scan:', e.message);
return { error: e.message };
}
}
printReport(report) {
console.log('╔═══════════════════════════════════════════════════════════════╗');
console.log('║ ARKITEKTURPOLICY-KONTROLL ║');
console.log('╚═══════════════════════════════════════════════════════════════╝');
console.log();
console.log('📊 SAMMANFATTNING\n');
console.log(` Regler kontrollerade: ${report.summary.totalRules}`);
console.log(` Överträdelser: ${report.summary.violations}`);
console.log(` 🔴 Critical: ${report.summary.critical}`);
console.log(` 🟠 High: ${report.summary.high}`);
console.log(` 🟡 Medium: ${report.summary.medium}`);
console.log(` 🟢 Low: ${report.summary.low}`);
console.log();
if (report.violations.length > 0) {
console.log('⚠️ ÖVERTRÄDELSER\n');
for (const v of report.violations) {
const icon = v.severity === 'CRITICAL' ? '🔴' :
v.severity === 'HIGH' ? '🟠' :
v.severity === 'MEDIUM' ? '🟡' : '🟢';
console.log(` ${icon} Regel ${v.ruleId}: ${v.ruleName}`);
console.log(` Fil: ${v.file}`);
console.log(` Bevis: ${v.evidence}`);
console.log(` ${v.message}`);
console.log();
}
} else {
console.log('✅ Inga överträdelser hittade!\n');
}
console.log('⚡ BESLUT\n');
if (report.blockMerge) {
console.log(' 🔴 BLOCKERA MERGE — Kritiska överträdelser måste åtgärdas');
} else if (report.requiresReview) {
console.log(' 🟠 KRÄV REVIEW — Högprioriterade överträdelser bör granskas');
} else {
console.log(' ✅ GODKÄNN — Inga blockerande överträdelser');
}
console.log();
console.log('═══════════════════════════════════════════════════════════════\n');
}
/**
* Lista alla regler
*/
listRules() {
console.log('📋 ARKITEKTURREGLER\n');
for (const rule of this.rules) {
const icon = rule.severity === 'CRITICAL' ? '🔴' :
rule.severity === 'HIGH' ? '🟠' :
rule.severity === 'MEDIUM' ? '🟡' : '🟢';
console.log(` ${icon} Regel ${rule.id}: ${rule.name}`);
console.log(` Kategori: ${rule.category}`);
console.log(` Allvarlighet: ${rule.severity}`);
console.log();
}
}
/**
* Hämta statistik över överträdelser
*/
getViolationStats() {
if (!existsSync(POLICY_LOG)) {
return { error: 'Inga överträdelser loggade ännu' };
}
const violations = readFileSync(POLICY_LOG, 'utf8')
.split('\n')
.filter(line => line.trim())
.map(line => {
try { return JSON.parse(line); } catch { return null; }
})
.filter(Boolean);
const byRule = {};
const bySeverity = {};
const byFile = {};
for (const v of violations) {
byRule[v.ruleId] = (byRule[v.ruleId] || 0) + 1;
bySeverity[v.severity] = (bySeverity[v.severity] || 0) + 1;
byFile[v.file] = (byFile[v.file] || 0) + 1;
}
return {
total: violations.length,
byRule: Object.entries(byRule).map(([id, count]) => ({
ruleId: id,
ruleName: this.rules.find(r => r.id == id)?.name || 'Unknown',
count
})).sort((a, b) => b.count - a.count),
bySeverity,
topFiles: Object.entries(byFile)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([file, count]) => ({ file, count }))
};
}
}
// ── Main ──────────────────────────────────────────────────────────────────
const policy = new ArchitecturePolicy();
const command = process.argv[2] || '--check';
if (command === '--check') {
const repoPath = process.argv[3] || '/home/bernt/repos/quixzoom.com';
policy.checkRepo(repoPath);
} else if (command === '--pr') {
// Simulera PR-kontroll med filer
const files = process.argv.slice(3);
if (files.length === 0) {
console.log('Användning: node architecture-policy.mjs --pr <fil1> <fil2> ...');
process.exit(1);
}
policy.checkPR(files);
} else if (command === '--list') {
policy.listRules();
} else if (command === '--stats') {
const stats = policy.getViolationStats();
console.log(JSON.stringify(stats, null, 2));
} else {
console.log('Användning:');
console.log(' node architecture-policy.mjs --check [repo-path] # Skanna hela repo');
console.log(' node architecture-policy.mjs --pr <fil1> <fil2> # Kontrollera specifika filer');
console.log(' node architecture-policy.mjs --list # Lista alla regler');
console.log(' node architecture-policy.mjs --stats # Visa överträdelse-statistik');
}