/*
RFC-0840: materialize.config.validate command handler — workspace-scope validator
that checks OPERATOR_CONFIG_FILES list stays in sync with actual workpiece and
cache clone files. Emits MAT-CONFIG-01 (warning: unrecognized operator file) and
MAT-CONFIG-02 (error: dead entry in OPERATOR_CONFIG_FILES).
OPTIONAL_OPERATOR_CONFIG_FILES (m000170) whitelists MAT-CONFIG-01 but is not
covered by MAT-CONFIG-02 — optional files legitimately exist on no site.
- Do not validate file contents — only presence/absence.
- Do not validate .env* files — those are handled by RFC-0822.
- Do not validate generated files — those are regenerated by build.prepare.
- RFC-0840: initial materialize.config.validate command handler.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- warpgogol-m000170: OPTIONAL_OPERATOR_CONFIG_FILES joined into the MAT-CONFIG-01 whitelist; MAT-CONFIG-02 intentionally still scans only the required OPERATOR_CONFIG_FILES.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
Diagnostic,
CheckResult,
} from "@warpgogol/werkstatt-engine/kernel";
import { OPERATOR_CONFIG_FILES, OPTIONAL_OPERATOR_CONFIG_FILES } from "./operator-config-files.ts";
import { resolveCacheClonePath } from "../sternsystem/registry-io.ts";
import { discoverSystems } from "../sternsystem/registry-io.ts";
const _STERNSYSTEM_DATA_PATHS = ["src/content", "public", "provenance"];
const BOILERPLATE_FILES = new Set([
".gitignore",
"package.json",
"astro.config.mjs",
"tsconfig.json",
"wrangler.jsonc",
"postcss.config.cjs",
"system.pin.json",
"behavior.snapshot.generated.yaml",
"system-config.yaml",
"system-state.yaml",
".changelog-system",
"CHANGELOG.md",
"AGENTS.md",
"README.md",
"content.config.ts",
"env.d.ts",
"middleware.ts",
]);
const GENERATED_PATTERNS = [
/\.generated\.ya?ml$/,
/\.generated\.json$/,
/^content-ref-index\.generated\.yaml$/,
];
function isGenerated(name: string): boolean {
return GENERATED_PATTERNS.some((p) => p.test(name));
}
function isEnvFile(name: string): boolean {
return name.startsWith(".env");
}
function isBoilerplate(name: string): boolean {
return BOILERPLATE_FILES.has(name);
}
function isOperatorConfigEntry(name: string): boolean {
return [...OPERATOR_CONFIG_FILES, ...OPTIONAL_OPERATOR_CONFIG_FILES].some(
(entry) => entry === name || entry.endsWith(`/${name}`),
);
}
export async function runMaterializeConfigValidate(
_input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const diagnostics: Diagnostic[] = [];
const root = context.workspaceRoot;
const missionsDir = join(root, "missions");
const workpieceRoots: string[] = [];
if (await context.io.exists(missionsDir)) {
const missionEntries = await context.io.readdir(missionsDir);
for (const entry of missionEntries) {
if (!entry.isDirectory) continue;
if (entry.name === "archive") continue;
const wp = join(missionsDir, entry.name, "workpiece");
if (await context.io.exists(wp)) {
workpieceRoots.push(wp);
}
}
}
const cacheCloneDirs: string[] = [];
try {
const { systems } = await discoverSystems(root);
for (const sys of systems) {
try {
const cacheDir = resolveCacheClonePath(root, sys.id);
if (await context.io.exists(cacheDir)) {
cacheCloneDirs.push(cacheDir);
}
} catch {
// skip
}
}
} catch {
// no systems registered
}
for (const wp of workpieceRoots) {
const rootEntries = await context.io.readdir(wp);
for (const entry of rootEntries) {
if (!entry.isFile) continue;
const name = entry.name;
if (isBoilerplate(name)) continue;
if (isEnvFile(name)) continue;
if (isGenerated(name)) continue;
if (isOperatorConfigEntry(name)) continue;
diagnostics.push({
ruleId: "MAT-CONFIG-01",
severity: "warning",
message: `Unrecognized root-level file "${name}" in workpiece — if this is an operator config file, add it to OPERATOR_CONFIG_FILES via a superseding RFC.`,
file: `${wp}/${name}`,
fixHint: `Add "${name}" to OPERATOR_CONFIG_FILES in packages/werkstatt/src/mission/operator-config-files.ts`,
});
}
const srcDir = join(wp, "src");
if (await context.io.exists(srcDir)) {
const srcEntries = await context.io.readdir(srcDir);
for (const entry of srcEntries) {
if (!entry.isFile) continue;
if (entry.name === "system.md") continue;
if (isGenerated(entry.name)) continue;
const srcPath = `src/${entry.name}`;
if (
[...OPERATOR_CONFIG_FILES, ...OPTIONAL_OPERATOR_CONFIG_FILES].includes(srcPath as never)
)
continue;
if (isBoilerplate(entry.name)) continue;
if (entry.name.endsWith(".generated.mjs")) continue;
diagnostics.push({
ruleId: "MAT-CONFIG-01",
severity: "warning",
message: `Unrecognized file "src/${entry.name}" in workpiece — if this is an operator config file, add it to OPERATOR_CONFIG_FILES via a superseding RFC.`,
file: `${srcDir}/${entry.name}`,
fixHint: `Add "src/${entry.name}" to OPERATOR_CONFIG_FILES in packages/werkstatt/src/mission/operator-config-files.ts`,
});
}
}
}
for (const entry of OPERATOR_CONFIG_FILES) {
const foundInWorkpiece = (
await Promise.all(workpieceRoots.map((wp) => context.io.exists(join(wp, entry))))
).some(Boolean);
const foundInCacheClone = (
await Promise.all(cacheCloneDirs.map((cc) => context.io.exists(join(cc, entry))))
).some(Boolean);
if (!foundInWorkpiece && !foundInCacheClone) {
diagnostics.push({
ruleId: "MAT-CONFIG-02",
severity: "error",
message: `OPERATOR_CONFIG_FILES entry "${entry}" not found in any active workpiece or cache clone — dead entry.`,
fixHint: `Remove "${entry}" from OPERATOR_CONFIG_FILES in packages/werkstatt/src/mission/operator-config-files.ts`,
});
}
}
const summary = {
error: diagnostics.filter((d) => d.severity === "error").length,
warning: diagnostics.filter((d) => d.severity === "warning").length,
info: diagnostics.filter((d) => d.severity === "info").length,
};
const status: CheckResult["status"] =
summary.error > 0 ? "fail" : summary.warning > 0 ? "warn" : "pass";
return {
data: {
command: "materialize.config.validate",
status,
diagnostics,
summary,
},
exitCode: summary.error > 0 ? 1 : 0,
summary: `[materialize.config.validate] ${summary.error} error${summary.error === 1 ? "" : "s"}, ${summary.warning} warning${summary.warning === 1 ? "" : "s"}`,
};
}