Doku-Vollausbau: 62 Seiten (Projekt/Admin/Anwender/Referenz), Hook-Referenz mit Signaturen, Corporate-Screenshots, Coverage-Inventar
Deploy tibi-docs / build-and-deploy (push) Failing after 38s
Deploy tibi-docs / build-and-deploy (push) Failing after 38s
- Einstieg: Schnellstart + Neues Projekt aus Starter-Skill, Was-ist-Tibi mit Backend/Website-Trennung - Projekt: Sammlungen, Felder, Abfragen, Rechte, Hooks, Dateien, i18n, Suche, SSR, Pagebuilder, Jobs, Audit, Deploy, LLM, MCP - Hook-Referenz: 66 Symbole mit Signatur + Beispiel + Fehlerfall (5 Kapitel) - Admin: Ansichten, Editor, Medien, Exporte, Aktionen, Rechte, KI, Setup, Customizing - Anwender: 11 Anleitungen im professionellen Du-Ton, 30 Corporate-Screenshots - Referenz: Schemas, Token-Köpfe, Fehlerbilder, Shortcuts, OpenAPI-Platzhalter - ADR-0001 bis 0003, Coverage-Skript (282 Items) + Map, Plan-Dateien
This commit is contained in:
1 parent
acb1d32355
commit
1897bb50d9
101 files changed
+8980
-68
No files matched your search
@@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Coverage-Inventar für die Tibi-Doku.
|
||||
*
|
||||
* Liest CODE (tibi-server, tibi-types, tibi-admin-nova) und gleicht die
|
||||
* gefundenen Items mit plans/coverage-map.json ab. Reine Lückenliste:
|
||||
* Exit-Code ist immer 0, kein Fail.
|
||||
*
|
||||
* - stdout: kompakte Übersicht pro Kategorie (total, gemappt, OFFEN, max 40)
|
||||
* plus je Kategorie eine Depth-Zeile (Stufe 1/2/3/4)
|
||||
* - plans/coverage-report.json: Zusammenfassung als JSON (inkl. depth)
|
||||
*
|
||||
* Körnung: contexts und tools sind Symbole (z. B. "db.find"), alle anderen
|
||||
* Kategorien behalten ihre bisherige Körnung. Depth-Stufen je Symbol auf
|
||||
* seiner Map-Seite (src/content/docs/<slug>.md):
|
||||
* 1 = Symbol fehlt (oder Seite fehlt), 2 = nur Erwähnung,
|
||||
* 3 = Erwähnung + Fenced-Codeblock mit Symbol ≤30 Zeilen danach,
|
||||
* 4 = zusätzlich Error-Token im selben H2-Abschnitt.
|
||||
* Andere Kategorien: reiner Map-Abgleich (gemappt = Stufe 2).
|
||||
*/
|
||||
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const docsRoot = path.resolve(scriptDir, "..");
|
||||
const cmsRoot = path.dirname(docsRoot);
|
||||
|
||||
const SERVER_ROOT = path.join(cmsRoot, "tibi-server");
|
||||
const TYPES_ROOT = path.join(cmsRoot, "tibi-types");
|
||||
const ADMIN_ROOT = path.join(cmsRoot, "tibi-admin-nova");
|
||||
|
||||
const MAP_PATH = path.join(docsRoot, "plans", "coverage-map.json");
|
||||
const REPORT_PATH = path.join(docsRoot, "plans", "coverage-report.json");
|
||||
|
||||
const MAX_OPEN_STDOUT = 40;
|
||||
|
||||
function readText(p) {
|
||||
return readFileSync(p, "utf8");
|
||||
}
|
||||
|
||||
function warn(msg) {
|
||||
console.error(`WARN: ${msg}`);
|
||||
}
|
||||
|
||||
/** 1. Routen: apiGroup.GET/POST/PUT/DELETE("pfad") + adm.* + OPTIONS aus setup.go, Duplikate raus. */
|
||||
function collectRoutes() {
|
||||
const file = path.join(SERVER_ROOT, "internal", "controller", "setup.go");
|
||||
if (!existsSync(file)) {
|
||||
warn(`setup.go nicht gefunden: ${file}`);
|
||||
return [];
|
||||
}
|
||||
const src = readText(file);
|
||||
const found = new Set();
|
||||
for (const re of [
|
||||
/apiGroup\.(GET|POST|PUT|DELETE)\(\s*"([^"]+)"/g,
|
||||
/\badm\.(GET|POST|PUT|DELETE)\(\s*"([^"]+)"/g,
|
||||
/\.OPTIONS\(\s*"([^"]+)"/g,
|
||||
]) {
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) found.add(m[m.length - 1]);
|
||||
}
|
||||
return [...found].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. Hook-Symbole in Funktionskörnung: je context_*.go (ohne _test) die
|
||||
* JS-Aufrufnamen aus RegisterPackageFunctions("paket", ...{"funktion": GoIdent})
|
||||
* plus Register("name", GoIdent) (Sonderfall request). Item-Format
|
||||
* "paket.funktion" bzw. "request". Dazu je Symbol: Go-Datei, Go-Ident und
|
||||
* ob ein `args:`-Kommentar im direkt darüberstehenden Kommentarblock steht.
|
||||
*/
|
||||
function collectContextSymbols() {
|
||||
const dir = path.join(SERVER_ROOT, "internal", "hook");
|
||||
if (!existsSync(dir)) {
|
||||
warn(`hook-Verzeichnis nicht gefunden: ${dir}`);
|
||||
return [];
|
||||
}
|
||||
const out = [];
|
||||
const files = readdirSync(dir)
|
||||
.filter((f) => f.startsWith("context_") && f.endsWith(".go") && !f.endsWith("_test.go"))
|
||||
.sort();
|
||||
for (const f of files) {
|
||||
const src = readText(path.join(dir, f));
|
||||
const lines = src.split("\n");
|
||||
const pkgRe = /RegisterPackageFunctions\(\s*"([^"]+)"\s*,\s*models\.ContextPackageFunctions\{(.*?)\}\)/gs;
|
||||
let m;
|
||||
while ((m = pkgRe.exec(src)) !== null) {
|
||||
const pkg = m[1];
|
||||
const entryRe = /"([^"]+)"\s*:\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
||||
let e;
|
||||
while ((e = entryRe.exec(m[2])) !== null) {
|
||||
out.push({ symbol: `${pkg}.${e[1]}`, goFile: f, goIdent: e[2], hasArgs: hasArgsComment(lines, e[2]) });
|
||||
}
|
||||
}
|
||||
// Sonderfall Einzel-Registrierung, z. B. Register("request", FuncRequest)
|
||||
const singleRe = /\bRegister\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/g;
|
||||
while ((m = singleRe.exec(src)) !== null) {
|
||||
out.push({ symbol: m[1], goFile: f, goIdent: m[2], hasArgs: hasArgsComment(lines, m[2]) });
|
||||
}
|
||||
}
|
||||
out.sort((a, b) => (a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0));
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Enthält der zusammenhängende `//`-Block direkt über `func <ident>(` ein `args:`? */
|
||||
function hasArgsComment(lines, goIdent) {
|
||||
const idx = lines.findIndex((l) => new RegExp(`^func\\s+${goIdent}\\s*\\(`).test(l));
|
||||
if (idx < 0) return false;
|
||||
const block = [];
|
||||
let j = idx - 1;
|
||||
while (j >= 0 && /^\s*\/\//.test(lines[j])) {
|
||||
block.unshift(lines[j]);
|
||||
j--;
|
||||
}
|
||||
return block.some((l) => /(args:|JS signature:)/i.test(l));
|
||||
}
|
||||
|
||||
/** 3. MCP-Tools: Name: "..." aus internal/mcp/tools_*.go. */
|
||||
function collectTools() {
|
||||
const dir = path.join(SERVER_ROOT, "internal", "mcp");
|
||||
if (!existsSync(dir)) {
|
||||
warn(`mcp-Verzeichnis nicht gefunden: ${dir}`);
|
||||
return [];
|
||||
}
|
||||
const files = readdirSync(dir).filter((f) => f.startsWith("tools_") && f.endsWith(".go"));
|
||||
const found = new Set();
|
||||
const re = /Name:\s*"([^"]+)"/g;
|
||||
for (const f of files) {
|
||||
const src = readText(path.join(dir, f));
|
||||
let m;
|
||||
while ((m = re.exec(src)) !== null) found.add(m[1]);
|
||||
}
|
||||
return [...found].sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. Schemas: Dateinamen aus schemas/config/*.schema.json plus deren
|
||||
* Top-Level-properties-Keys als Detail (für den Report).
|
||||
*/
|
||||
function collectSchemas() {
|
||||
const dir = path.join(TYPES_ROOT, "schemas", "config");
|
||||
const detail = {};
|
||||
if (!existsSync(dir)) {
|
||||
warn(`schemas-Verzeichnis nicht gefunden: ${dir}`);
|
||||
return { items: [], detail };
|
||||
}
|
||||
const files = readdirSync(dir)
|
||||
.filter((f) => f.endsWith(".schema.json"))
|
||||
.sort();
|
||||
for (const f of files) {
|
||||
try {
|
||||
const parsed = JSON.parse(readText(path.join(dir, f)));
|
||||
const props = parsed && typeof parsed === "object" && parsed.properties && typeof parsed.properties === "object"
|
||||
? Object.keys(parsed.properties)
|
||||
: [];
|
||||
detail[f] = props.sort();
|
||||
} catch {
|
||||
warn(`Schema nicht parsbar: ${f}`);
|
||||
detail[f] = [];
|
||||
}
|
||||
}
|
||||
return { items: files, detail };
|
||||
}
|
||||
|
||||
/** 5. Admin-Komponenten: *.svelte unter frontend/src, relativ, ohne Endung. */
|
||||
function collectComponents() {
|
||||
const srcDir = path.join(ADMIN_ROOT, "frontend", "src");
|
||||
if (!existsSync(srcDir)) {
|
||||
warn(`frontend/src nicht gefunden: ${srcDir}`);
|
||||
return [];
|
||||
}
|
||||
const out = [];
|
||||
const walk = (dir) => {
|
||||
for (const entry of readdirSync(dir)) {
|
||||
const full = path.join(dir, entry);
|
||||
if (statSync(full).isDirectory()) {
|
||||
walk(full);
|
||||
} else if (entry.endsWith(".svelte")) {
|
||||
const rel = path.relative(srcDir, full).split(path.sep).join("/");
|
||||
out.push(rel.slice(0, -".svelte".length));
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(srcDir);
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
/** 6. Test-Pläne: Dateinamen aus docs/test-plans/. */
|
||||
function collectTestplans() {
|
||||
const dir = path.join(ADMIN_ROOT, "docs", "test-plans");
|
||||
if (!existsSync(dir)) {
|
||||
warn(`test-plans nicht gefunden: ${dir}`);
|
||||
return [];
|
||||
}
|
||||
return readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
|
||||
}
|
||||
|
||||
const ERROR_TOKENS = ["Error", "error", "400", "401", "403", "404", "409", "429", "500", "nil", "verweigert", "denied"];
|
||||
/** Kategorien mit Depth-Mode (Symbol-Tiefe auf der Map-Seite). */
|
||||
const DEPTH_CATS = ["contexts", "tools"];
|
||||
|
||||
const docCache = new Map();
|
||||
function loadDocLines(slug) {
|
||||
if (docCache.has(slug)) return docCache.get(slug);
|
||||
const p = path.join(docsRoot, "src", "content", "docs", `${slug}.md`);
|
||||
const lines = existsSync(p) ? readText(p).split("\n") : null;
|
||||
docCache.set(slug, lines);
|
||||
return lines;
|
||||
}
|
||||
|
||||
/** Fenced-Codeblock-Spannen als [startFence, endFence]-Paare (0-basiert). */
|
||||
function fencedSpans(lines) {
|
||||
const spans = [];
|
||||
let start = -1;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (/^\s*```/.test(lines[i])) {
|
||||
if (start < 0) start = i;
|
||||
else {
|
||||
spans.push([start, i]);
|
||||
start = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* Depth-Stufe je Symbol auf seiner Map-Seite:
|
||||
* 1 = fehlt (kein Map-Eintrag, Seite fehlt oder Symbol nicht als Text),
|
||||
* 2 = nur Erwähnung, 3 = + Fenced-Codeblock mit Symbol ≤30 Zeilen danach,
|
||||
* 4 = + Error-Token im selben H2-Abschnitt (bis zur nächsten H2).
|
||||
*/
|
||||
function symbolDepth(symbol, slug) {
|
||||
if (!slug) return 1;
|
||||
const lines = loadDocLines(slug);
|
||||
if (!lines) return 1;
|
||||
// Aliase: "paket.fn" gilt auch als "context.paket.fn" bzw. "context.fn" (Hook-Schreibweise)
|
||||
const aliases = [symbol, `context.${symbol}`];
|
||||
const dot = symbol.indexOf(".");
|
||||
if (dot > 0) aliases.push(`context.${symbol.slice(dot + 1)}`);
|
||||
const mentions = [];
|
||||
lines.forEach((l, i) => {
|
||||
if (aliases.some((a) => l.includes(a))) mentions.push(i);
|
||||
});
|
||||
if (mentions.length === 0) return 1;
|
||||
const spans = fencedSpans(lines);
|
||||
let anchor = -1;
|
||||
for (const mi of mentions) {
|
||||
const hit = spans.some(
|
||||
([s, e]) => s > mi && s <= mi + 30 && lines.slice(s, e + 1).some((l) => aliases.some((a) => l.includes(a))),
|
||||
);
|
||||
if (hit) {
|
||||
anchor = mi;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anchor < 0) return 2;
|
||||
// Abschnitt = eigene ###-Sektion (bis zum nächsten ### oder ##), nicht das ganze Paket:
|
||||
// sonst zählt der Fehler-Token fremder Symbole (Mess-Loophole).
|
||||
let start = 0;
|
||||
for (let k = anchor; k >= 0; k--) {
|
||||
if (/^####?\s/.test(lines[k])) {
|
||||
start = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let stop = lines.length;
|
||||
for (let k = anchor + 1; k < lines.length; k++) {
|
||||
if (/^####?\s/.test(lines[k])) {
|
||||
stop = k;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const section = lines.slice(start, stop).join("\n");
|
||||
// Explizit ausgewiesene Abwesenheit ("kein Fehlerfall") zählt als dokumentiert.
|
||||
if (/kein Fehlerfall/i.test(section)) return 4;
|
||||
return ERROR_TOKENS.some((t) => section.includes(t)) ? 4 : 3;
|
||||
}
|
||||
|
||||
function loadMap() {
|
||||
const empty = { routes: {}, contexts: {}, tools: {}, schemas: {}, components: {}, testplans: {} };
|
||||
if (!existsSync(MAP_PATH)) {
|
||||
warn(`keine coverage-map gefunden (${path.relative(docsRoot, MAP_PATH)}), alles OFFEN`);
|
||||
return empty;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(readText(MAP_PATH));
|
||||
for (const key of Object.keys(empty)) {
|
||||
if (parsed[key] && typeof parsed[key] === "object") empty[key] = parsed[key];
|
||||
}
|
||||
return empty;
|
||||
} catch {
|
||||
warn("coverage-map.json nicht parsbar, alles OFFEN");
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const schemas = collectSchemas();
|
||||
const ctxSymbols = collectContextSymbols();
|
||||
const ctxBySymbol = new Map(ctxSymbols.map((s) => [s.symbol, s]));
|
||||
const inventory = {
|
||||
routes: collectRoutes(),
|
||||
contexts: ctxSymbols.map((s) => s.symbol),
|
||||
tools: collectTools(),
|
||||
schemas: schemas.items,
|
||||
components: collectComponents(),
|
||||
testplans: collectTestplans(),
|
||||
};
|
||||
const map = loadMap();
|
||||
|
||||
const report = { generatedAt: new Date().toISOString(), categories: {}, depth: {} };
|
||||
let totalAll = 0;
|
||||
let openAll = 0;
|
||||
|
||||
for (const [cat, items] of Object.entries(inventory)) {
|
||||
const mapping = map[cat] ?? {};
|
||||
// "__ignore": exakte Items oder Präfixe mit "*" (Infra ohne Doku-Fläche, z. B. Mock-Daten, Tests)
|
||||
const ignores = Array.isArray(mapping.__ignore) ? mapping.__ignore : [];
|
||||
const isIgnored = (item) =>
|
||||
ignores.some((pat) => (pat.endsWith("*") ? item.startsWith(pat.slice(0, -1)) : item === pat));
|
||||
const open = items.filter((item) => !(item in mapping) && !isIgnored(item));
|
||||
totalAll += items.length;
|
||||
openAll += open.length;
|
||||
report.categories[cat] = {
|
||||
total: items.length,
|
||||
mapped: items.length - open.length,
|
||||
openCount: open.length,
|
||||
openItems: open,
|
||||
};
|
||||
|
||||
console.log(`${cat}: total=${items.length} gemappt=${items.length - open.length} OFFEN=${open.length}`);
|
||||
for (const item of open.slice(0, MAX_OPEN_STDOUT)) console.log(` OFFEN: ${item}`);
|
||||
if (open.length > MAX_OPEN_STDOUT) {
|
||||
console.log(` ... +${open.length - MAX_OPEN_STDOUT} weitere (siehe plans/coverage-report.json)`);
|
||||
}
|
||||
|
||||
// Depth-Mode: contexts/tools je Symbol auf der Map-Seite, Rest Map-Abgleich.
|
||||
const depth = { total: items.length, stufe4: 0, stufe3: 0, stufe2: 0, stufe1: 0 };
|
||||
if (DEPTH_CATS.includes(cat)) {
|
||||
const details = [];
|
||||
for (const item of items) {
|
||||
if (isIgnored(item)) {
|
||||
depth.stufe2 += 1;
|
||||
details.push({ symbol: item, page: null, stufe: 2, ignored: true });
|
||||
continue;
|
||||
}
|
||||
const page = mapping[item] ?? null;
|
||||
const stufe = symbolDepth(item, page);
|
||||
depth[`stufe${stufe}`] += 1;
|
||||
const detail = { symbol: item, page, stufe };
|
||||
if (cat === "contexts") {
|
||||
const sym = ctxBySymbol.get(item);
|
||||
detail.goFile = sym?.goFile ?? null;
|
||||
detail.hasArgs = sym?.hasArgs ?? false;
|
||||
}
|
||||
details.push(detail);
|
||||
}
|
||||
details.sort((a, b) => a.stufe - b.stufe || (a.symbol < b.symbol ? -1 : 1));
|
||||
report.categories[cat].symbols = details;
|
||||
} else {
|
||||
depth.stufe2 = items.length - open.length;
|
||||
depth.stufe1 = open.length;
|
||||
}
|
||||
report.depth[cat] = depth;
|
||||
console.log(
|
||||
`depth ${cat}: total=${depth.total} stufe4=${depth.stufe4} stufe3=${depth.stufe3} stufe2=${depth.stufe2} stufe1=${depth.stufe1}`,
|
||||
);
|
||||
if (cat === "contexts") {
|
||||
const noArgs = ctxSymbols.filter((s) => !s.hasArgs).map((s) => s.symbol);
|
||||
console.log(` ohne args-Kommentar (${noArgs.length}): ${noArgs.join(", ") || "–"}`);
|
||||
}
|
||||
}
|
||||
console.log(`GESAMT: total=${totalAll} OFFEN=${openAll}`);
|
||||
|
||||
report.schemasDetail = schemas.detail;
|
||||
writeFileSync(REPORT_PATH, JSON.stringify(report, null, 2) + "\n", "utf8");
|
||||
console.log(`Report: ${path.relative(process.cwd(), REPORT_PATH)}`);
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(`FEHLER: ${err?.message ?? err} (reine Liste, kein Fail)`);
|
||||
}
|
||||
process.exit(0);
|
||||
Reference in new issue
Block a user