/* RFC-1210: closed-loop remediation contracts — the closed `remediationClasses` catalog, the `RemediationSignal` intake record, the per-site `remediation` autonomy block (`system-state.yaml`), and the default signal source allowlist. The catalog is closed by policy: promoting a class to autoFixable requires a follow-up RFC, not an ad-hoc edit. Does not route or ingest signals — that is the engine's remediation.signal.ingest command. Does not own the remediation hint catalog (REMEDIATION_CATALOG in remediation-catalog.ts) — this module classifies defect classes, that module maps ruleIds to fix text. Does not open missions — the open step is an internal composition inside the ingest command. RFC-1210: initial contracts — signal schema, autonomy config, closed autoFixable catalog (generated-drift, workpiece-deps, typography) with real registered fixCommands, default signal-source allowlist. RFC-1210: closed-loop signal ingest to bounded missions (RFC-1210) */ import { z } from "zod"; // --------------------------------------------------------------------------- // RemediationSignal — normalized intake record (gogol.remediation-signal/1) // --------------------------------------------------------------------------- export const REMEDIATION_SIGNAL_SCHEMA = "gogol.remediation-signal/1" as const; export const remediationSignalSchema = z.object({ schema: z.literal(REMEDIATION_SIGNAL_SCHEMA), /** Sternsystem id the signal targets. */ siteId: z.string().min(1), /** Key into the closed remediationClasses catalog. */ class: z.string().min(1), /** Invoker id — MUST be in the site's remediation.signalSources allowlist. */ source: z.string().min(1), /** Detector kind — provenance metadata, not an authorization input. */ detectedBy: z.enum(["signoz-alert", "fleet-probe", "check-runner", "validator", "manual"]), /** File paths, probe run ids, alert urls — carried into mission evidence. */ evidenceRefs: z.array(z.string().min(1)).default([]), detectedAt: z.string().datetime(), }); export type RemediationSignal = z.infer; // --------------------------------------------------------------------------- // RemediationAutonomyConfig — `remediation:` block in system-state.yaml // --------------------------------------------------------------------------- /** * Default signal-source allowlist (AUDIT-RFC-1210-01 + operator decision): * the trust boundary for ingest — an invoker not listed here cannot trigger * any route, not even a deduped one. */ export const DEFAULT_REMEDIATION_SIGNAL_SOURCES = [ "operator", "leitstand", "observability-bridge", ] as const; export const remediationAutonomyConfigSchema = z.object({ /** off = ingest produces action-packs only; bounded = autoFixable classes open bounded missions. */ autonomy: z.enum(["off", "bounded"]).default("off"), /** Allowlist of invoker ids accepted by remediation.signal.ingest. */ signalSources: z.array(z.string().min(1)).default([...DEFAULT_REMEDIATION_SIGNAL_SOURCES]), }); export type RemediationAutonomyConfig = z.infer; // --------------------------------------------------------------------------- // RemediationClassEntry — closed catalog of defect classes // --------------------------------------------------------------------------- export const remediationClassEntrySchema = z .object({ /** Defect class key, e.g. "generated-drift". */ class: z.string().min(1), /** true only when the class's fix is a proven deterministic command. */ autoFixable: z.boolean(), /** Registered kernel/site command the auto-mission runs — required iff autoFixable. */ fixCommand: z.string().min(1).optional(), /** Per-site rate cap on auto-missions for this class per rolling day (0 = never — meaningful for non-autoFixable entries). */ maxAutoMissionsPerDay: z.number().int().min(0), /** Minimum signal severity for this class to route (informational floor). */ severityFloor: z.enum(["warning", "error"]), }) .refine((e) => !e.autoFixable || typeof e.fixCommand === "string", { message: "autoFixable entries require fixCommand", path: ["fixCommand"], }); export type RemediationClassEntry = z.infer; /** * RFC-1210 closed v1 catalog. autoFixable classes map to real registered * commands (verified by remediation.autonomy.validate against the workspace * registry): * * - `generated-drift` → `mission.build` — a workpiece build re-runs every * build.post generator, deterministically re-emitting drifted artifacts. * - `workpiece-deps` → `config.regenerate` — regenerates the workpiece * package.json from WORKPIECE_DEPENDENCY_MANIFEST (the fixHint * template.deps.drift already emits). * - `typography` → `typography.fix` — mechanical whitelisted typography fixes. * * Extending this catalog requires a follow-up RFC (closed-by-policy). */ export const REMEDIATION_CLASSES: readonly RemediationClassEntry[] = [ { class: "generated-drift", autoFixable: true, fixCommand: "mission.build", maxAutoMissionsPerDay: 1, severityFloor: "warning", }, { class: "workpiece-deps", autoFixable: true, fixCommand: "config.regenerate", maxAutoMissionsPerDay: 1, severityFloor: "warning", }, { class: "typography", autoFixable: true, fixCommand: "typography.fix", maxAutoMissionsPerDay: 2, severityFloor: "warning", }, { class: "content-regression", autoFixable: false, maxAutoMissionsPerDay: 0, severityFloor: "warning", }, { class: "agent-surface", autoFixable: false, maxAutoMissionsPerDay: 0, severityFloor: "warning", }, { class: "runtime-health", autoFixable: false, maxAutoMissionsPerDay: 0, severityFloor: "error", }, ]; /** * Resolve a catalog entry by class key. Returns undefined for unknown classes — * callers MUST reject unknown classes (no ad-hoc classes, RFC-1210). */ export function lookupRemediationClass(name: string): RemediationClassEntry | undefined { return REMEDIATION_CLASSES.find((e) => e.class === name); } /** * Catalog integrity check — parse every entry and assert the autoFixable→ * fixCommand invariant. Returns the list of structural issues (empty = valid). */ export function validateRemediationClasses(): string[] { const issues: string[] = []; const seen = new Set(); for (const entry of REMEDIATION_CLASSES) { const parsed = remediationClassEntrySchema.safeParse(entry); if (!parsed.success) { issues.push( `class "${entry.class}": ${parsed.error.issues.map((i) => i.message).join("; ")}`, ); continue; } if (seen.has(entry.class)) { issues.push(`duplicate class key "${entry.class}"`); } seen.add(entry.class); if (entry.autoFixable && entry.maxAutoMissionsPerDay < 1) { issues.push(`class "${entry.class}" is autoFixable but has a zero rate cap`); } } return issues; }