#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const repoRoot = path.resolve(__dirname, '..'); const vaultRoot = path.join(repoRoot, 'obsidian-vault'); const manifestPath = path.join(vaultRoot, '00 Start', 'Kildemanifest.tsv'); const archivePrefixes = ['_archive/', 'archive/', 'docs/archived/', 'tmp/', '_temp/']; const configurationPrefixes = ['.claude/', '.codex/', '.github/']; const errors = []; const warnings = []; const destinations = new Set(); const toRepoPath = value => path.join(repoRoot, value); const normalizeTarget = target => { let value = target.trim().replace(/^<|>$/g, ''); value = value.split(/\s+["']/)[0]; value = value.split('#')[0]; try { return decodeURIComponent(value); } catch (_error) { return value; } }; const isIgnoredLink = target => ( !target || target.startsWith('#') || target.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(target) || /[{}$]/.test(target) ); if (!fs.existsSync(manifestPath)) { console.error(`Manifest mangler: ${manifestPath}`); process.exit(1); } const rows = fs.readFileSync(manifestPath, 'utf8') .trimEnd() .split('\n') .slice(1) .map(line => line.split('\t')); for (const [source, vaultPath] of rows) { if (!source || !vaultPath) { errors.push(`Ugyldig manifestpost: ${source || ''}`); continue; } if (destinations.has(vaultPath)) { errors.push(`Dobbelt vault-destination: ${vaultPath}`); } destinations.add(vaultPath); const sourcePath = toRepoPath(source); const copyPath = path.join(vaultRoot, vaultPath); if (!fs.existsSync(sourcePath)) { errors.push(`Kilde mangler: ${source}`); continue; } if (!fs.existsSync(copyPath)) { errors.push(`Vault-kopi mangler: ${vaultPath}`); continue; } if (!fs.readFileSync(sourcePath).equals(fs.readFileSync(copyPath))) { errors.push(`Vault-kopi afviger fra kilde: ${source}`); } const archived = archivePrefixes.some(prefix => source.startsWith(prefix)); const configuration = configurationPrefixes.some(prefix => source.startsWith(prefix)); const content = fs.readFileSync(sourcePath, 'utf8'); if (!archived && !configuration && content.trim() === '') { errors.push(`Aktivt dokument er tomt: ${source}`); } if (!archived && !configuration && !/^#\s+\S/m.test(content)) { warnings.push(`Aktivt dokument mangler H1: ${source}`); } if (archived || configuration) continue; const linkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g; for (const match of content.matchAll(linkPattern)) { const rawTarget = match[1].trim(); if (isIgnoredLink(rawTarget)) continue; const target = normalizeTarget(rawTarget); if (!target) continue; const candidates = [ path.resolve(path.dirname(sourcePath), target), path.resolve(repoRoot, target) ]; if (!candidates.some(candidate => fs.existsSync(candidate))) { errors.push(`Brudt link i ${source}: ${rawTarget}`); } } } for (const indexName of ['Aktiv dokumentation.md', 'Dokumentindeks.md']) { const indexPath = path.join(vaultRoot, '00 Start', indexName); if (!fs.existsSync(indexPath)) { errors.push(`Vault-indeks mangler: 00 Start/${indexName}`); continue; } const content = fs.readFileSync(indexPath, 'utf8'); const wikiPattern = /\[\[([^\]|#]+)(?:#[^\]|]+)?(?:\|[^\]]+)?\]\]/g; for (const match of content.matchAll(wikiPattern)) { const target = match[1].trim(); const candidates = [ path.join(vaultRoot, `${target}.md`), path.resolve(path.dirname(indexPath), `${target}.md`) ]; if (!candidates.some(candidate => fs.existsSync(candidate))) { errors.push(`Brudt Obsidian-link i ${indexName}: ${target}`); } } } console.log(`Kontrollerede ${rows.length} manifestposter.`); console.log(`Fejl: ${errors.length}. Advarsler: ${warnings.length}.`); for (const warning of warnings) console.log(`ADVARSEL: ${warning}`); for (const error of errors) console.error(`FEJL: ${error}`); process.exit(errors.length > 0 ? 1 : 0);