#!/usr/bin/env node /** * Prüft chartTheme.ts gegen docs/carbon-referenz.json. * * Warum das existiert: „Wir halten uns an IBM Carbon" ist eine Behauptung, * solange sie niemand nachrechnet. Farbwerte driften beim Kopieren, und die * Carbon-Dokumentation widerspricht stellenweise dem ausgelieferten Code. * Dieser Prüfer macht aus der Behauptung eine Zusage. * * Aufruf: npm run verify:carbon */ import { readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { dirname, join } from "node:path"; const wurzel = join(dirname(fileURLToPath(import.meta.url)), ".."); const referenz = JSON.parse(readFileSync(join(wurzel, "docs/carbon-referenz.json"), "utf8")); const theme = readFileSync(join(wurzel, "features/dashboard/utils/chartTheme.ts"), "utf8"); const fehler = []; const hinweise = []; /** Liest ein exportiertes Array von Hex-Werten aus dem Theme. */ function hexArray(name) { const block = theme.match(new RegExp(`export const ${name}[^=]*=\\s*\\[([\\s\\S]*?)\\]`)); if (!block) return null; return [...block[1].matchAll(/#[0-9a-fA-F]{6}/g)].map((m) => m[0].toLowerCase()); } /** Liest einen einzelnen Hex-Wert aus einem Objektfeld im Theme. */ function hexFeld(objekt, feld) { const block = theme.match(new RegExp(`export const ${objekt}[^=]*=\\s*\\{([\\s\\S]*?)\\}`)); if (!block) return null; const treffer = block[1].match(new RegExp(`${feld}\\s*:\\s*"(#[0-9a-fA-F]{6})"`)); return treffer ? treffer[1].toLowerCase() : null; } /* -------------------------------------------------- Kategoriale Palette */ const palette = hexArray("KATEGORIE_FARBEN"); const soll = referenz.kategorialePalette.werte.map((w) => w.toLowerCase()); if (!palette) { fehler.push("KATEGORIE_FARBEN nicht gefunden"); } else if (palette.length !== soll.length) { fehler.push(`Palette hat ${palette.length} Farben, Carbon hat ${soll.length}`); } else { palette.forEach((farbe, i) => { if (farbe !== soll[i]) { fehler.push(`Palette Position ${i + 1}: ${farbe}: Carbon: ${soll[i]}`); } }); } /* ------------------------------------------------------- Neutrale Werte */ const neutraleZuordnung = { grid: "gitterlinie", achse: "achsenlinie", textSekundaer: "achsenbeschriftung", text: "textPrimaer", }; for (const [feld, refName] of Object.entries(neutraleZuordnung)) { const ist = hexFeld("CHART", feld); const sollWert = referenz.neutraleWerte[refName].wert.toLowerCase(); if (ist === null) fehler.push(`CHART.${feld} nicht gefunden`); else if (ist !== sollWert) { fehler.push( `CHART.${feld} = ${ist}: Carbon ${referenz.neutraleWerte[refName].token}: ${sollWert}` ); } } /* ---------------------------------------------------------------- Tooltip */ const tooltipBg = hexFeld("CHART", "tooltipBg"); const tooltipFg = hexFeld("CHART", "tooltipFg"); if (tooltipBg !== referenz.tooltip.hintergrund.wert.toLowerCase()) { fehler.push( `CHART.tooltipBg = ${tooltipBg}: Carbon ${referenz.tooltip.hintergrund.token}: ${referenz.tooltip.hintergrund.wert}` ); } if (tooltipFg !== referenz.tooltip.text.wert.toLowerCase()) { fehler.push(`CHART.tooltipFg = ${tooltipFg}: Carbon: ${referenz.tooltip.text.wert}`); } /* ------------------------------------------------- Achsen-Tickanzahl */ const raster = theme.match(/nettesRaster\(\s*\n?\s*max: number,\s*\n?\s*maxIntervalle = (\d+)/); if (raster && Number(raster[1]) !== referenz.achsen.ticksYAchse) { fehler.push( `nettesRaster maxIntervalle = ${raster[1]}: Carbon numberOfTicks (y): ${referenz.achsen.ticksYAchse}` ); } /* -------------------------------------------- Bewusste Abweichungen melden */ const balken = theme.match(/maxBreite:\s*(\d+)/); const abwBalken = referenz.bewusstAbweichend.balkenMaxBreitePx; if (balken) { if (Number(balken[1]) !== abwBalken.hier) { fehler.push( `BALKEN.maxBreite = ${balken[1]}, dokumentiert ist ${abwBalken.hier}. ` + `Entweder den Wert zurücksetzen oder docs/carbon-referenz.json nachziehen.` ); } else { hinweises(`Balkenbreite ${abwBalken.hier} px statt Carbon ${abwBalken.carbon} px`, abwBalken.grund); } } function hinweises(titel, grund) { hinweise.push({ titel, grund }); } hinweises( "Feste Farbe je Kategorie statt n-Serien-Palette", referenz.bewusstAbweichend.festeKategorieFarben.grund ); /* ------------------------------------------------------------- Ausgabe */ console.log(`Carbon-Abgleich gegen ${referenz.quelle.version} (${referenz.quelle.geprueftAm})\n`); if (hinweise.length) { console.log("Bewusste Abweichungen (dokumentiert, kein Fehler):"); for (const h of hinweise) console.log(` · ${h.titel}\n ${h.grund}\n`); } if (fehler.length) { console.error(`${fehler.length} Abweichung(en) von Carbon:`); for (const f of fehler) console.error(` ✗ ${f}`); console.error("\nEntweder chartTheme.ts korrigieren oder die Abweichung in"); console.error("docs/carbon-referenz.json unter 'bewusstAbweichend' begründen."); process.exit(1); } console.log("✓ Palette, neutrale Werte, Tooltip und Achsenraster stimmen mit Carbon überein.");