/* 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"}`, }; }