Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-crm-decision-report.mjs
T

209 lines
6.1 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const failures = [];
const passes = [];
function rel(...parts) {
return path.join(...parts);
}
function abs(relativePath) {
return path.join(root, relativePath);
}
function pass(message) {
passes.push(message);
}
function fail(message) {
failures.push(message);
}
function exists(relativePath) {
return fs.existsSync(abs(relativePath));
}
function isNonEmptyFile(relativePath) {
try {
return fs.statSync(abs(relativePath)).isFile() && fs.readFileSync(abs(relativePath), "utf8").trim().length > 0;
} catch {
return false;
}
}
function hasDirectoryEntries(relativePath) {
try {
const stat = fs.statSync(abs(relativePath));
return stat.isDirectory() && fs.readdirSync(abs(relativePath)).length > 0;
} catch {
return false;
}
}
function requireReportText(report, text) {
if (report.includes(text)) {
pass(`report references ${text}`);
} else {
fail(`report must reference ${text}`);
}
}
function requirePath(relativePath) {
if (exists(relativePath)) {
pass(`path exists: ${relativePath}`);
} else {
fail(`missing path: ${relativePath}`);
}
}
function requireRouteGlob(globText, directoryPath) {
if (hasDirectoryEntries(directoryPath)) {
pass(`glob resolves: ${globText} -> ${directoryPath}`);
} else {
fail(`glob does not resolve to a non-empty directory: ${globText} -> ${directoryPath}`);
}
}
const reportPath = rel("docs", "feature-decisions", "crm-decision-report.md");
const manifestPath = rel("docs", "feature-manifests", "plugin-candidates.json");
let report = "";
if (isNonEmptyFile(reportPath)) {
report = fs.readFileSync(abs(reportPath), "utf8");
pass(`report exists and is non-empty: ${reportPath}`);
} else {
fail(`report is missing or empty: ${reportPath}`);
}
if (report) {
const requiredHeadings = [
"## Decision",
"## Scope",
"## Current Code Surfaces",
"## Couplings",
"## Schema and Template Impact",
"## Sidebar Command Docs Impact",
"## Execution Plan",
"## Verification Plan",
"## Risks",
];
for (const heading of requiredHeadings) {
if (report.includes(heading)) {
pass(`heading present: ${heading}`);
} else {
fail(`missing required heading: ${heading}`);
}
}
if (report.includes("extract-plugin")) {
pass("decision token present: extract-plugin");
} else {
fail("report must contain decision token: extract-plugin");
}
const placeholderPattern = /\b(?:TODO|TBD)\b/i;
const placeholderMatch = report
.split(/\r?\n/)
.filter((line) => !line.includes("No TODO, TBD, or unresolved placeholder text exists"))
.find((line) => placeholderPattern.test(line));
if (placeholderMatch) {
fail(`report contains unresolved placeholder line: ${placeholderMatch}`);
} else {
pass("report has no TODO/TBD placeholders outside the verifier requirement");
}
const requiredReportReferences = [
"apps/web/app/modules/crm",
"apps/web/app/routes/admin/crm.tsx",
"apps/web/app/routes/admin/crm/*",
"apps/web/app/routes/app.$tenant/crm.tsx",
"apps/web/app/routes/app.$tenant/crm/*",
"apps/web/app/modules/templates/defaultEntityTemplates.ts",
"apps/web/app/utils/api/.server/EntitiesApi.ts",
"apps/web/app/services/command-registry/commandRegistry.ts",
"apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts",
"apps/web/app/utils/db/users.db.server.ts",
"apps/web/public/locales/en/translations.json",
"apps/web/public/locales/de/translations.json",
"docs/feature-manifests/plugin-candidates.json",
];
for (const reference of requiredReportReferences) {
requireReportText(report, reference);
}
}
const requiredConcretePaths = [
"apps/web/app/modules/crm",
"apps/web/app/routes/admin/crm.tsx",
"apps/web/app/routes/app.$tenant/crm.tsx",
"apps/web/app/modules/templates/defaultEntityTemplates.ts",
"apps/web/app/utils/api/.server/EntitiesApi.ts",
"apps/web/app/services/command-registry/commandRegistry.ts",
"apps/web/app/modules/emailMarketing/services/EmailMarketingService.ts",
"apps/web/app/utils/db/users.db.server.ts",
"apps/web/public/locales/en/translations.json",
"apps/web/public/locales/de/translations.json",
manifestPath,
];
for (const requiredPath of requiredConcretePaths) {
requirePath(requiredPath);
}
requireRouteGlob("apps/web/app/routes/admin/crm/*", "apps/web/app/routes/admin/crm");
requireRouteGlob("apps/web/app/routes/app.$tenant/crm/*", "apps/web/app/routes/app.$tenant/crm");
if (isNonEmptyFile(manifestPath)) {
try {
const manifest = JSON.parse(fs.readFileSync(abs(manifestPath), "utf8"));
const crm = Array.isArray(manifest.components)
? manifest.components.find((component) => component && component.id === "crm")
: undefined;
if (crm) {
pass('manifest contains component id "crm"');
} else {
fail('manifest must contain component id "crm"');
}
const codeAreas = Array.isArray(crm?.codeAreas) ? crm.codeAreas : [];
for (const codeArea of ["apps/web/app/modules/crm", "apps/web/app/routes/admin/crm*", "apps/web/app/routes/app.$tenant/crm*"]) {
if (codeAreas.includes(codeArea)) {
pass(`manifest CRM code area present: ${codeArea}`);
} else {
fail(`manifest CRM component missing code area: ${codeArea}`);
}
}
if (manifest.pruning?.action === "extract-plugin") {
pass("manifest pruning.action is extract-plugin");
} else {
fail(`manifest pruning.action must be extract-plugin, got ${JSON.stringify(manifest.pruning?.action)}`);
}
} catch (error) {
fail(`manifest JSON parse failed: ${error.message}`);
}
} else {
fail(`manifest is missing or empty: ${manifestPath}`);
}
if (failures.length > 0) {
process.stderr.write(
[
"FAIL crm decision report verification",
...failures.map((failure) => `- ${failure}`),
`Summary: ${passes.length} passed, ${failures.length} failed`,
].join("\n") + "\n",
);
process.exit(1);
}
process.stdout.write(
`PASS crm decision report verification\nSummary: ${passes.length} passed, 0 failed\n`,
);