Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-route-inventory.mjs
T

173 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
const FILENAME = fileURLToPath(import.meta.url);
const REPO_ROOT = path.resolve(FILENAME, "..", "..");
const ROUTES_DIR = path.join(REPO_ROOT, "apps", "web", "app", "routes");
// WF-P8 adds three reviewed tenant routes: the S04 board settings
// (admin.pipelines.$pipelineId._index), the S05 field template
// (admin.pipelines.$pipelineId.fields) and the S02 contact-search resource
// (projects.$id.contact-search). BILL-13 adds the reviewed tenant-bound product
// acquisition return route. Tenant 178 → 181, public/system 76 → 77 and total
// 563 → 567.
//
// P4 adds one reviewed tenant resource route: the link-target search behind the
// file detail's "Datei verknüpfen" dialog (files.link-targets), which replaced
// the raw-id field. Tenant 181 → 182 and total 567 → 568.
// BILL-14 adds one public, read-only registration plan-advice resource. Public
// system 77 → 78 and total 568 → 569.
// The appearance settings page adds one reviewed tenant route. Tenant 182 →
// 183 and total 569 → 570.
export const ROUTE_INVENTORY_BUDGETS = Object.freeze({
admin: 159,
tenant: 184,
public_help_docs_internal: 78,
portal: 14,
optional: 136,
total: 571,
});
const OPTIONAL_ROUTE_PATTERNS = Object.freeze([
/^help(\/|$)/,
/^mcp(\/|$)/,
/^api\.embed\.forms\./,
/^b\.\$tenant\.\$slug(\.|$)/,
/^e\.[co]\.\$outboundEmailId(\.|$)/,
/^embed\.forms\./,
/^f\.\$tenant\.\$slug(\.|$)/,
/^integrations\.aircall\.oauth\./,
/^integrations\.kwp\./,
/^sign\.\$token(\.|$)/,
/^webhooks\.telephony\.aircall(\.|$)/,
/^internal\/api\/__workflows(\/|$)/,
/^admin\/?__workflows(\/|$)/,
/^admin\/ai(\/|$)/,
/^admin\/chat(\/|$)/,
/^admin\/crm(\.|\/|$)/,
/^admin\/email-marketing(\.|\/|$)/,
/^admin\/help-desk(\.|\/|$)/,
/^admin\/knowledge-base(\.|\/|$)/,
/^admin\/prompts(\.|\/|$)/,
/^app\.\$tenant\/bookings(\.|\/|$)/,
/^app\.\$tenant\/calendar(\.|\/|$)/,
/^app\.\$tenant\/calls(\.|\/|$)/,
/^app\.\$tenant\/forms(\.|\/|$)/,
/^app\.\$tenant\/hr(\.|\/|$)/,
/^app\.\$tenant\/settings\/integrations\.calendar(\.|\/|$)/,
/^app\.\$tenant\/settings\/chat(\.|\/|$)/,
/^app\.\$tenant\/settings\.integrations\.telephony(\.|\/|$)/,
/^app\.\$tenant\/telephony(\.|\/|$)/,
/^app\.\$tenant\/__widgets(\/|$)/,
/^app\.\$tenant\/__workflows(\/|$)/,
/^app\.\$tenant\/chat(\.|\/|$)/,
/^app\.\$tenant\/contacts(\.|\/|$)/,
/^app\.\$tenant\/crm(\.|\/|$)/,
/^app\.\$tenant\/email-marketing(\.|\/|$)/,
/^app\.\$tenant\/internal\/ai(\/|$)/,
/^app\.\$tenant\/__portals(\/|$)/,
/^app\.\$tenant\/sites(\/|$)/,
]);
function isRouteFile(fileName) {
return /\.(ts|tsx)$/.test(fileName) && !/\.test\.(ts|tsx)$/.test(fileName) && !/\.spec\.(ts|tsx)$/.test(fileName) && !/\.server\.(ts|tsx)$/.test(fileName);
}
async function walkRoutes(dir = ROUTES_DIR) {
const entries = await fs.readdir(dir, { withFileTypes: true });
const files = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...(await walkRoutes(fullPath)));
} else if (entry.isFile() && isRouteFile(entry.name)) {
files.push(path.relative(ROUTES_DIR, fullPath).split(path.sep).join("/"));
}
}
return files.sort((left, right) => left.localeCompare(right));
}
export function classifyRouteSurface(relativePath) {
if (/^customer-portal(\/|$)/.test(relativePath)) {
return "portal";
}
if (OPTIONAL_ROUTE_PATTERNS.some((pattern) => pattern.test(relativePath))) {
return "optional";
}
if (relativePath.startsWith("admin") || relativePath.startsWith("admin/")) {
return "admin";
}
if (relativePath.startsWith("app.$tenant")) {
return "tenant";
}
return "public_help_docs_internal";
}
export function classifyRouteModule(relativePath) {
const surface = classifyRouteSurface(relativePath);
if (surface === "portal") {
return "portal";
}
if (surface === "optional") {
return "optional";
}
if (surface === "admin" || surface === "tenant") {
return "default-enabled";
}
return "public-system";
}
export async function scanRouteInventory() {
const files = await walkRoutes();
const surfaces = { admin: 0, tenant: 0, public_help_docs_internal: 0, portal: 0, optional: 0, total: files.length };
const classifications = { "public-system": 0, "default-enabled": 0, portal: 0, optional: 0 };
for (const file of files) {
surfaces[classifyRouteSurface(file)] += 1;
classifications[classifyRouteModule(file)] += 1;
}
return { files, surfaces, classifications };
}
export function evaluateRouteInventory(inventory, budgets = ROUTE_INVENTORY_BUDGETS) {
return Object.entries(budgets).map(([surface, budget]) => {
const count = inventory.surfaces[surface] ?? 0;
return { surface, count, budget, pass: count <= budget };
});
}
export async function runRouteInventoryGate({ logger = console } = {}) {
const inventory = await scanRouteInventory();
const results = evaluateRouteInventory(inventory);
const failures = results.filter((result) => !result.pass);
for (const result of results) {
const writer = result.pass ? logger.log : logger.error;
writer(`${result.pass ? "PASS" : "FAIL"} route_inventory surface=${result.surface} count=${result.count} budget=${result.budget}`);
}
logger.log(
`Physical classifications: public-system=${inventory.classifications["public-system"]} default-enabled=${inventory.classifications["default-enabled"]} portal=${inventory.classifications.portal} optional=${inventory.classifications.optional}`
);
logger.log(`Summary: ${results.length - failures.length} passed, ${failures.length} failed`);
return { inventory, results, failures };
}
async function main() {
const outcome = await runRouteInventoryGate();
if (outcome.failures.length > 0) {
process.exit(1);
}
}
const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === FILENAME;
if (invokedDirectly) {
main().catch((error) => {
// eslint-disable-next-line no-console -- CLI gate must surface fatal errors on stderr.
console.error(error);
process.exit(1);
});
}