Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-first-approved-small-feature-cut.mjs
T

146 lines
4.7 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const root = path.resolve(process.argv[2] ?? process.cwd());
const failures = [];
const passes = [];
const removedBlogApiPath = path.join("apps", "web", "app", "utils", "api", ".server", "BlogApi.ts");
const blogModulePath = path.join("apps", "web", "app", "modules", "blog");
const inventoryPath = path.join("docs", "feature-inventory.md");
const removeCandidatesPath = path.join("docs", "feature-manifests", "remove-candidates.json");
const appPath = path.join("apps", "web", "app");
const runtimeExtensions = new Set([".ts", ".tsx"]);
const allowedBlogModuleRuntimeBasenames = new Set(["README.ts", "README.tsx"]);
function abs(relativePath) {
return path.join(root, relativePath);
}
function pass(message) {
passes.push(message);
}
function fail(message) {
failures.push(message);
}
function relativeFromRoot(filePath) {
return path.relative(root, filePath).split(path.sep).join("/");
}
function exists(relativePath) {
return fs.existsSync(abs(relativePath));
}
function readText(relativePath) {
try {
return fs.readFileSync(abs(relativePath), "utf8");
} catch {
fail(`required file is missing or unreadable: ${relativePath}`);
return "";
}
}
function walkFiles(relativePath) {
const start = abs(relativePath);
if (!fs.existsSync(start)) return [];
const files = [];
const stack = [start];
while (stack.length > 0) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name === "node_modules" || entry.name === ".git") continue;
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) stack.push(entryPath);
else if (entry.isFile()) files.push(entryPath);
}
}
return files.sort((a, b) => relativeFromRoot(a).localeCompare(relativeFromRoot(b)));
}
function appRuntimeFiles() {
return walkFiles(appPath).filter((filePath) => runtimeExtensions.has(path.extname(filePath)));
}
function lineNumberFor(text, index) {
return text.slice(0, index).split(/\r?\n/).length;
}
function checkRemovedBlogApiFile() {
if (exists(removedBlogApiPath)) fail(`removed Blog API file still exists: ${removedBlogApiPath}`);
else pass(`removed Blog API file is absent: ${removedBlogApiPath}`);
}
function checkAppReferences() {
const offenders = [];
for (const filePath of appRuntimeFiles()) {
const relativePath = relativeFromRoot(filePath);
let text;
try {
text = fs.readFileSync(filePath, "utf8");
} catch {
fail(`could not read source file while scanning app references: ${relativePath}`);
continue;
}
for (const pattern of ["~/modules/blog", "BlogApi"]) {
const index = text.indexOf(pattern);
if (index !== -1) offenders.push(`${relativePath}:${lineNumberFor(text, index)} references ${pattern}`);
}
}
if (offenders.length > 0) {
for (const offender of offenders) fail(`stale Blog source reference: ${offender}`);
} else {
pass("app runtime sources contain no ~/modules/blog imports or BlogApi references");
}
}
function checkBlogModuleHasNoRuntimeFiles() {
const runtimeFiles = walkFiles(blogModulePath)
.filter((filePath) => runtimeExtensions.has(path.extname(filePath)))
.map(relativeFromRoot)
.filter((relativePath) => !allowedBlogModuleRuntimeBasenames.has(path.basename(relativePath)));
if (runtimeFiles.length > 0) {
for (const runtimeFile of runtimeFiles) fail(`blog module runtime file remains: ${runtimeFile}`);
} else {
pass("apps/web/app/modules/blog contains no runtime .ts/.tsx files");
}
}
function checkDocNote(relativePath, label) {
const text = readText(relativePath);
if (!text) return;
const requiredTokens = ["Feature Decision/S06", "BlogApi", "remove-from-core"];
const missing = requiredTokens.filter((token) => !text.includes(token));
if (missing.length === 0) pass(`${label} records the Feature Decision/S06 BlogApi remove-from-core note`);
else fail(`${label} must record an Feature Decision/S06 BlogApi remove-from-core note; missing: ${missing.join(", ")}`);
}
checkRemovedBlogApiFile();
checkAppReferences();
checkBlogModuleHasNoRuntimeFiles();
checkDocNote(inventoryPath, "feature inventory");
checkDocNote(removeCandidatesPath, "remove candidates manifest");
if (failures.length > 0) {
process.stderr.write(["FAIL first approved small feature cut verification", ...failures.map((failure) => `- ${failure}`), `Summary: ${passes.length} passed, ${failures.length} failed`].join("\n") + "\n");
process.exit(1);
}
process.stdout.write(`PASS first approved small feature cut verification\nSummary: ${passes.length} passed, 0 failed\n`);