/** * Resolution verification gate (Sprint 22). * * verifyResolution queries the configured observability MCP provider for the * named metric over the named window, applies the comparison per-sample (lt is * strict <, lte is <=, gt is strict >, gte is >=), and returns verified=true * ONLY when EVERY sample meets the threshold. One outlier = not verified. * * Evidence persistence: every call (verified=true OR false) writes raw samples * to `.bober/incidents//resolution-evidence/.json` for * audit and postmortem reconstruction. Presence of the evidence file does * NOT imply resolution — only the VerifyResult.verified boolean does. * * No provider configured: returns verified=false, reason='NO_PROVIDER', hint. * Override path: handled by setIncidentStatus in src/incident/timeline.ts, * not here. This module ONLY measures. * * Sprint 22 — src/incident/resolution-verify.ts */ import { z } from "zod"; import type { ObservabilityProvider } from "../config/schema.js"; import type { IncidentId } from "./types.js"; export declare const ComparisonSchema: z.ZodEnum<["lt", "gt", "lte", "gte"]>; export type Comparison = z.infer; export declare const BaselineComparisonSchema: z.ZodEnum<["absolute", "percent-of-baseline"]>; export type BaselineComparison = z.infer; export declare const ResolutionCriteriaSchema: z.ZodObject<{ metricName: z.ZodString; threshold: z.ZodNumber; comparison: z.ZodEnum<["lt", "gt", "lte", "gte"]>; windowMinutes: z.ZodNumber; provider: z.ZodString; baselineComparison: z.ZodOptional>; }, "strip", z.ZodTypeAny, { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }, { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }>; export type ResolutionCriteria = z.infer; export declare const MetricSampleSchema: z.ZodObject<{ timestamp: z.ZodString; value: z.ZodNumber; }, "strip", z.ZodTypeAny, { value: number; timestamp: string; }, { value: number; timestamp: string; }>; export type MetricSample = z.infer; export declare const VerifyResultSchema: z.ZodObject<{ verified: z.ZodBoolean; /** Worst-case sample value (the one that failed, else last sample). */ observedValue: z.ZodOptional; /** ISO timestamp of the witness sample. */ sampledAt: z.ZodOptional; /** Path to the evidence file: .bober/incidents//resolution-evidence/.json */ evidencePath: z.ZodOptional; reason: z.ZodOptional>; hint: z.ZodOptional; }, "strip", z.ZodTypeAny, { verified: boolean; hint?: string | undefined; observedValue?: number | undefined; sampledAt?: string | undefined; evidencePath?: string | undefined; reason?: "OK" | "OUTLIER" | "NO_PROVIDER" | "NOT_IMPLEMENTED" | "MCP_ERROR" | undefined; }, { verified: boolean; hint?: string | undefined; observedValue?: number | undefined; sampledAt?: string | undefined; evidencePath?: string | undefined; reason?: "OK" | "OUTLIER" | "NO_PROVIDER" | "NOT_IMPLEMENTED" | "MCP_ERROR" | undefined; }>; export type VerifyResult = z.infer; export declare const ResolutionEvidenceSchema: z.ZodObject<{ incidentId: z.ZodString; verifiedAt: z.ZodString; criteria: z.ZodObject<{ metricName: z.ZodString; threshold: z.ZodNumber; comparison: z.ZodEnum<["lt", "gt", "lte", "gte"]>; windowMinutes: z.ZodNumber; provider: z.ZodString; baselineComparison: z.ZodOptional>; }, "strip", z.ZodTypeAny, { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }, { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }>; samples: z.ZodArray, "many">; allSamplesPassed: z.ZodBoolean; }, "strip", z.ZodTypeAny, { incidentId: string; verifiedAt: string; criteria: { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }; samples: { value: number; timestamp: string; }[]; allSamplesPassed: boolean; }, { incidentId: string; verifiedAt: string; criteria: { provider: string; metricName: string; threshold: number; comparison: "lt" | "gt" | "lte" | "gte"; windowMinutes: number; baselineComparison?: "absolute" | "percent-of-baseline" | undefined; }; samples: { value: number; timestamp: string; }[]; allSamplesPassed: boolean; }>; export type ResolutionEvidence = z.infer; export interface MetricQueryArgs { name: string; timeRange: { start: string; end: string; }; step?: string; } export interface MetricQueryClient { /** Call obs____query_metric and return parsed dataPoints. */ queryMetric(provider: string, args: MetricQueryArgs): Promise; } export interface VerifyResolutionDeps { projectRoot: string; providers: readonly ObservabilityProvider[]; /** Injected for tests; default = real spawn via mergeObsTools. */ client?: MetricQueryClient; /** Injected clock for tests. Default = () => new Date(). */ now?: () => Date; } /** * Apply the comparison per the criteria. Boundary semantics: * - 'lt' : value < threshold (strict; value === threshold FAILS) * - 'lte' : value <= threshold (inclusive; value === threshold PASSES) * - 'gt' : value > threshold (strict) * - 'gte' : value >= threshold (inclusive) */ export declare function sampleMeetsThreshold(value: number, threshold: number, comparison: Comparison): boolean; export declare function verifyResolution(incidentId: IncidentId, criteria: ResolutionCriteria, deps: VerifyResolutionDeps): Promise; //# sourceMappingURL=resolution-verify.d.ts.map