/* observability.alerts.validate — offline lint for the alert rule projection (RFC-0342). Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file. RFC-0342: initial implementation. */ import { join } from "node:path"; import type { CheckResult, Diagnostic, KernelCommandInput, KernelCommandResult, KernelRuntimeContext, } from "@warpgogol/werkstatt-engine/kernel"; import { diagnosticsResult } from "@warpgogol/werkstatt-shared/checks"; import { WARPGOGOL_METRIC_REGISTRY } from "@warpgogol/werkstatt-shared/observability"; import { parse as yamlParse, stringify as yamlStringify } from "yaml"; import { ALERT_RULES, NOTIFICATION_CHANNELS } from "../alert-rules.ts"; const METRIC_NAME_REGEX = /warpgogol_[a-z0-9_]+/g; export async function runObservabilityAlertsValidate( _input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const diagnostics: Diagnostic[] = []; const root = context.workspaceRoot; const projectionPath = join(root, "docs", "observability", "alerts.generated.yaml"); // OBS-ALR-01: check file exists and is fresh if (!(await context.io.exists(projectionPath))) { diagnostics.push({ ruleId: "OBS-ALR-01", severity: "error", file: "docs/observability/alerts.generated.yaml", message: "alerts.generated.yaml does not exist — run observability.alerts.generate.", fixHint: "Run: pnpm exec werkstatt run observability.alerts.generate", }); return diagnosticsResult("observability.alerts.validate", diagnostics); } // Check freshness by comparing source-sorted rules with the projection const expectedRules = [...ALERT_RULES].sort((a, b) => a.id.localeCompare(b.id)); const expectedChannels = [...NOTIFICATION_CHANNELS].sort((a, b) => a.id.localeCompare(b.id)); const expectedProjection = { schemaVersion: 1, generatedBy: "observability.alerts.generate", rules: expectedRules, channels: expectedChannels, }; const rawText = await context.io.readFile(projectionPath); let projection: typeof expectedProjection; try { projection = yamlParse(rawText) as typeof expectedProjection; } catch { diagnostics.push({ ruleId: "OBS-ALR-01", severity: "error", file: "docs/observability/alerts.generated.yaml", message: "alerts.generated.yaml is not valid YAML.", fixHint: "Regenerate: pnpm exec werkstatt run observability.alerts.generate", }); return diagnosticsResult("observability.alerts.validate", diagnostics); } const expectedJson = yamlStringify(expectedProjection); const actualJson = yamlStringify({ schemaVersion: projection.schemaVersion, generatedBy: projection.generatedBy, rules: projection.rules, channels: projection.channels, }); if (expectedJson !== actualJson) { diagnostics.push({ ruleId: "OBS-ALR-01", severity: "error", file: "docs/observability/alerts.generated.yaml", message: "alerts.generated.yaml is stale — source rules/channels differ from projection.", fixHint: "Regenerate: pnpm exec werkstatt run observability.alerts.generate", }); } // OBS-ALR-02: duplicate rule ids and promql/builder exclusivity const seenIds = new Set(); const declaredMetricNames = new Set(WARPGOGOL_METRIC_REGISTRY.map((m) => m.name)); const channelIds = new Set(NOTIFICATION_CHANNELS.map((c) => c.id)); for (const rule of ALERT_RULES) { if (seenIds.has(rule.id)) { diagnostics.push({ ruleId: "OBS-ALR-02", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Duplicate alert rule id: "${rule.id}".`, fixHint: "Rename one of the duplicate rule ids.", }); } seenIds.add(rule.id); if (rule.promql && rule.builder) { diagnostics.push({ ruleId: "OBS-ALR-02", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Rule "${rule.id}" has both promql and builder — exactly one is required.`, fixHint: "Remove either promql or builder from the rule.", }); } if (!rule.promql && !rule.builder) { diagnostics.push({ ruleId: "OBS-ALR-02", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Rule "${rule.id}" has neither promql nor builder — exactly one is required.`, fixHint: "Add a promql expression or a builder query to the rule.", }); } // OBS-ALR-03: promql references undeclared metrics if (rule.promql) { const matches = rule.promql.matchAll(METRIC_NAME_REGEX); for (const match of matches) { const metricName = match[0]; if (!declaredMetricNames.has(metricName)) { diagnostics.push({ ruleId: "OBS-ALR-03", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Rule "${rule.id}" references undeclared metric "${metricName}" — not in WARPGOGOL_METRIC_REGISTRY.`, fixHint: "Add the metric to the registry or fix the promql expression.", }); } } } // OBS-ALR-04: undeclared channel references for (const chId of rule.channels) { if (!channelIds.has(chId)) { diagnostics.push({ ruleId: "OBS-ALR-04", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Rule "${rule.id}" references undeclared channel "${chId}".`, fixHint: "Add the channel to NOTIFICATION_CHANNELS or fix the rule.", }); } } // OBS-ALR-05: description must include a runbook hint if (!rule.description.includes("runbook:")) { diagnostics.push({ ruleId: "OBS-ALR-05", severity: "warning", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Rule "${rule.id}" description lacks a runbook hint (no "runbook:" substring).`, fixHint: 'Add "runbook: ..." to the description so each alert is actionable.', }); } } // OBS-ALR-04: webhook channels must use env var names, not literal URLs for (const channel of NOTIFICATION_CHANNELS) { if (channel.kind === "webhook") { for (const target of channel.target) { if (target.startsWith("http://") || target.startsWith("https://")) { diagnostics.push({ ruleId: "OBS-ALR-04", severity: "error", file: "packages/os/site-kernel-observability/src/alert-rules.ts", message: `Webhook channel "${channel.id}" embeds a literal URL — use an env var name instead.`, fixHint: "Replace the URL with the env var name that holds the URL at runtime.", }); } } } } return diagnosticsResult("observability.alerts.validate", diagnostics); }