/*
RFC-1027: emits docs/remediation-catalog.generated.yaml from REMEDIATION_CATALOG,
cross-referencing validator rules[] declarations to identify uncovered ruleIds.
- Do not own the catalog — that lives in @warpgogol/werkstatt-shared/remediation.
- RFC-1027: initial implementation.
- RFC-1254: drop unused io-migration leftovers
*/
import { dirname, join } from "node:path";
import { stringify as yamlStringify } from "yaml";
import { buildGeneratedHeader } from "../kernel/generated-marker.ts";
import { writeFileAtomic } from "@warpgogol/werkstatt-shared/kernel";
import { REMEDIATION_CATALOG } from "@warpgogol/werkstatt-shared/remediation";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-shared/kernel";
const CATALOG_RELATIVE_PATH = join("docs", "remediation-catalog.generated.yaml");
export interface RemediationCatalogEntry {
ruleId: string;
action: string;
template?: string;
docRef?: string;
targetFiles?: string[];
}
export interface RemediationCatalogFile {
meta: {
schemaVersion: 1;
deterministic: true;
generatedAt: null;
};
entries: RemediationCatalogEntry[];
uncoveredRuleIds: string[];
}
export interface RemediationCatalogGenerateResult {
command: "remediation.catalog.generate";
written: boolean;
entryCount: number;
uncoveredCount: number;
}
export async function runRemediationCatalogGenerate(
_input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const workspaceRoot = context.workspaceRoot;
const catalogPath = join(workspaceRoot, CATALOG_RELATIVE_PATH);
const entries: RemediationCatalogEntry[] = [...REMEDIATION_CATALOG.values()].map((p) => {
const entry: RemediationCatalogEntry = { ruleId: p.ruleId, action: p.action };
if (p.template !== undefined) entry.template = p.template;
if (p.docRef !== undefined) entry.docRef = p.docRef;
if (p.targetFiles !== undefined) entry.targetFiles = p.targetFiles;
return entry;
});
const catalogRuleIds = new Set(REMEDIATION_CATALOG.keys());
const validatorRuleIds = collectValidatorRuleIds(context);
const uncoveredRuleIds = validatorRuleIds.filter((id) => !catalogRuleIds.has(id));
const file: RemediationCatalogFile = {
meta: {
schemaVersion: 1,
deterministic: true,
generatedAt: null,
},
entries,
uncoveredRuleIds,
};
const header = buildGeneratedHeader({
filePath: catalogPath,
ownerCommand: "remediation.catalog.generate",
});
const yaml = yamlStringify(file, { sortMapEntries: false });
await context.io.mkdir(dirname(catalogPath));
await writeFileAtomic(catalogPath, `${header}\n${yaml}`);
return {
data: {
command: "remediation.catalog.generate",
written: true,
entryCount: entries.length,
uncoveredCount: uncoveredRuleIds.length,
},
exitCode: 0,
summary: `[remediation.catalog.generate] ${entries.length} entries, ${uncoveredRuleIds.length} uncovered ruleIds`,
};
}
function collectValidatorRuleIds(context: KernelRuntimeContext): string[] {
const ruleIds = new Set();
for (const cmd of context.actualState.commands.values()) {
if (cmd.rules) {
for (const rule of cmd.rules) {
ruleIds.add(rule);
}
}
}
return [...ruleIds].sort();
}