Files
Laufzettel/scripts/pruefe-doku.mjs
T
Florian Wilde 77389a2c5f
Prüfung / pruefung (push) Canceled after 0s
Prüfung / pruefung (pull_request) Canceled after 0s
Prüfung / e2e (push) Canceled after 0s
Prüfung / e2e (pull_request) Canceled after 0s
docs(konventionen): Hexwert-Ausnahmen und LDAP-Container festhalten
Zwei Saetze in CLAUDE.md beschrieben den Code nicht genau.

"Nie auf rohe Hexwerte" hat sechs begruendete Ausnahmen, nicht null: Papier,
ein QR-Decoder und das Web-App-Manifest entscheiden nicht nach unserem
Farbschema. Sie stehen jetzt als Tabelle mit Begruendung je Zeile. Die Regel
wurde ergaenzt, nicht der Code geaendert -- eine Konstante ueber drei
unverwandte Dinge haette die Begruendung versteckt.

Das Tor vor jedem Commit nennt den LDAP-Container. Ohne ihn ueberspringt die
Suite 22 Tests -- nachgezaehlt, nicht geschaetzt: 81 mit Container, 59 ohne.
Sie sagt es auf stderr, der Lauf sieht trotzdem gruen aus. Im README fehlte
zusaetzlich format:pruefen, das CLAUDE.md seit #010 verlangt.

Damit das haelt, auch wenn niemand hinsieht: pruefe:doku vergleicht die beiden
Schranken-Bloecke und verlangt von beiden den Container. Gegenprobe gefahren.

Der Review hatte vier Hexwerte genannt und src/lib/qr.ts uebersehen, und das
Issue behauptete die Regel stehe auch in docs/architektur.md -- tut sie nicht.
Beides im Issue-Text richtiggestellt, statt einen Haken ueber falschen Text zu
setzen.

Closes #40
2026-10-04 16:11:45 +02:00

196 lines
7.0 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Prueft, dass Dokumentation und Code nicht auseinanderlaufen. Laeuft in CI und
* vor jedem Commit. Geprueft wird:
*
* 1. Die Version aus package.json hat einen Abschnitt im Changelog.
* 2. Jede Issue-Datei steht in der Uebersicht – und umgekehrt.
* 3. Geschaetzte und tatsaechliche Zeiten stimmen zwischen Datei und Uebersicht.
* 4. Alle in der Anleitung verlinkten Bilder sind vorhanden.
* 5. Kein Markdown verweist auf eine Datei, die es nicht gibt.
* 6. Die Schranken-Befehle in CLAUDE.md und README.md stimmen ueberein.
*/
import { readFileSync, existsSync, readdirSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
const wurzel = resolve(import.meta.dirname, "..");
const befunde = [];
function melde(text) {
befunde.push(text);
}
function lies(pfad) {
return readFileSync(join(wurzel, pfad), "utf8");
}
function feld(text, name) {
const treffer = text.match(new RegExp(`\\*\\*${name}:\\*\\*\\s*(.+?)(?=\\s*\\*\\*|$)`, "m"));
return treffer?.[1]?.trim() ?? "";
}
// ---- 1. Version im Changelog ----------------------------------------------
{
const { version } = JSON.parse(lies("package.json"));
const changelog = lies("CHANGELOG.md");
const veroeffentlicht = changelog.includes(`## [${version}]`);
const unveroeffentlicht = /^## \[Unveröffentlicht\]/m.test(changelog);
if (!veroeffentlicht && !unveroeffentlicht) {
melde(
`Version ${version} hat keinen Abschnitt im Changelog und es gibt keinen Abschnitt ` +
`[Unveröffentlicht].`,
);
}
}
// ---- 2. und 3. Issue-Dateien gegen die Uebersicht --------------------------
{
const verz = "docs/issues";
const dateien = readdirSync(join(wurzel, verz))
.filter((n) => /^\d{3}-.+\.md$/.test(n))
.sort();
const uebersicht = lies(join(verz, "README.md"));
for (const name of dateien) {
if (!uebersicht.includes(`(${name})`)) {
melde(`${verz}/${name} fehlt in ${verz}/README.md.`);
continue;
}
const text = lies(join(verz, name));
const kopf = text.match(/^#\s*#(\d+)\s+(.+)$/m);
if (!kopf) {
melde(`${verz}/${name} hat keine Kopfzeile der Form "# #007 Titel".`);
continue;
}
const [, nummer, titel] = kopf;
if (Number(nummer) !== Number(name.slice(0, 3))) {
melde(`${verz}/${name} nennt #${nummer}, der Dateiname sagt ${name.slice(0, 3)}.`);
}
const zeile = uebersicht.split("\n").find((z) => z.includes(`(${name})`));
for (const [beschriftung, wert] of [
["Status", feld(text, "Status")],
["Geschätzt", feld(text, "Geschätzt")],
["Tatsächlich", feld(text, "Tatsächlich")],
]) {
if (wert && !zeile.includes(`| ${wert} |`)) {
melde(`${verz}/${name}: "${beschriftung}: ${wert}" weicht von der Übersicht ab.`);
}
}
if (!zeile.includes(titel)) {
melde(`${verz}/${name}: Titel "${titel}" weicht von der Übersicht ab.`);
}
}
for (const treffer of uebersicht.matchAll(/\((\d{3}-[^)]+\.md)\)/g)) {
if (!dateien.includes(treffer[1])) {
melde(`${verz}/README.md verweist auf ${treffer[1]}, die Datei fehlt.`);
}
}
}
// ---- 4. Verlinkte Bilder in der Anleitung ----------------------------------
{
const verz = join(wurzel, "docs/anleitung");
if (existsSync(verz)) {
const sammeln = (ordner) =>
readdirSync(ordner, { withFileTypes: true }).flatMap((eintrag) => {
const pfad = join(ordner, eintrag.name);
if (eintrag.isDirectory()) return sammeln(pfad);
return eintrag.name.endsWith(".md") ? [pfad] : [];
});
for (const datei of sammeln(verz)) {
const text = readFileSync(datei, "utf8");
for (const treffer of text.matchAll(/!\[[^\]]*\]\(([^)]+)\)/g)) {
const ziel = treffer[1];
if (/^https?:/.test(ziel)) continue;
if (!existsSync(resolve(dirname(datei), ziel))) {
melde(`${datei.replace(`${wurzel}/`, "")} verlinkt das fehlende Bild ${ziel}.`);
}
}
}
}
}
// ---- 5. Verweise zwischen Markdown-Dateien --------------------------------
{
const sammeln = (ordner) =>
readdirSync(ordner, { withFileTypes: true }).flatMap((eintrag) => {
if (["node_modules", ".git", ".next"].includes(eintrag.name)) return [];
const pfad = join(ordner, eintrag.name);
if (eintrag.isDirectory()) return sammeln(pfad);
return eintrag.name.endsWith(".md") ? [pfad] : [];
});
for (const datei of sammeln(wurzel)) {
const text = readFileSync(datei, "utf8");
for (const treffer of text.matchAll(/(?<!!)\[[^\]]*\]\(([^)]+)\)/g)) {
const ziel = treffer[1];
if (/^(https?:|mailto:|#)/.test(ziel)) continue;
const ohneAnker = ziel.split("#")[0];
if (!ohneAnker) continue;
if (!existsSync(resolve(dirname(datei), ohneAnker))) {
melde(`${datei.replace(`${wurzel}/`, "")} verweist auf ${ohneAnker}, das es nicht gibt.`);
}
}
}
}
// ---- 6. Die Schranken in CLAUDE.md und README.md -------------------------
//
// Zwei Dateien beschreiben dasselbe Tor, und sie sind auseinandergelaufen: im
// README fehlte `format:pruefen`, und beide nannten den LDAP-Container nicht, ohne
// den die Suite 22 Tests still ueberspringt. Beides fiel erst bei der Abnahme zu
// #019 auf -- ein gruener Lauf sieht genauso aus. Geprueft wird deshalb der
// Befehlsblock selbst, nicht seine Umgebung (#040).
{
/** Der erste eingezaeunte Block nach einer Ueberschrift. */
const blockNach = (text, ueberschrift) => {
const ab = text.indexOf(ueberschrift);
if (ab < 0) return null;
const treffer = text.slice(ab).match(/```(?:bash|sh)?\n([\s\S]*?)```/);
return treffer?.[1] ?? null;
};
const skripte = (block) => [...block.matchAll(/pnpm ([a-z:]+)/g)].map((m) => m[1]).sort();
const quellen = [
["CLAUDE.md", "## Vor jedem Commit"],
["README.md", "### Qualitätsschranken"],
];
const bloecke = quellen.map(([datei, ueberschrift]) => {
const block = blockNach(lies(datei), ueberschrift);
if (block === null) melde(`${datei}: kein Befehlsblock unter "${ueberschrift}" gefunden.`);
return [datei, block];
});
if (bloecke.every(([, block]) => block !== null)) {
const [[dateiA, blockA], [dateiB, blockB]] = bloecke;
const a = skripte(blockA);
const b = skripte(blockB);
if (a.join(" ") !== b.join(" ")) {
melde(
`Die Schranken laufen auseinander: ${dateiA} nennt [${a.join(", ")}], ` +
`${dateiB} nennt [${b.join(", ")}].`,
);
}
for (const [datei, block] of bloecke) {
if (!/docker compose -f docker-compose\.test\.yml up .*ldap/.test(block)) {
melde(
`${datei}: der Schranken-Block nennt den LDAP-Container nicht. ` +
`Ohne ihn ueberspringt die Suite den Verzeichnis-Abgleich, und der Lauf sieht gruen aus.`,
);
}
}
}
}
// ---- Ergebnis --------------------------------------------------------------
if (befunde.length > 0) {
console.error(`\nDokumentation und Code laufen auseinander (${befunde.length}):\n`);
for (const b of befunde) console.error(` - ${b}`);
console.error("");
process.exit(1);
}
console.log("Dokumentation stimmt.");