209 lines
6.1 KiB
JavaScript
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`,
|
|
);
|