| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { and, count, desc, eq, gte, sql } from "drizzle-orm"; |
| import { |
| db, |
| networkEvolutionEvents, |
| networkVersionMetrics, |
| toolNetworks, |
| } from "@workspace/db"; |
| import { rollingFitness, type FitnessSummary } from "./fitness"; |
| import { recordEvent, type EvolutionEventKind } from "./events"; |
|
|
| const DEFAULT_CADENCE_N = 30; |
| const DEFAULT_REGRESSION_FLOOR = 0.6; |
| const DEFAULT_COVERAGE_K = 5; |
| const DEFAULT_MAX_RETRIES = 2; |
|
|
| export interface TriggerThresholds { |
| cadenceN: number; |
| regressionFloor: number; |
| coverageK: number; |
| } |
|
|
| export interface TriggerSignal { |
| kind: EvolutionEventKind; |
| fired: boolean; |
| payload: Record<string, unknown>; |
| } |
|
|
| export interface TriggerEvaluation { |
| networkId: string; |
| activeVersionId: string | null; |
| problemClassPath: string; |
| fitness: FitnessSummary | null; |
| thresholds: TriggerThresholds; |
| signals: TriggerSignal[]; |
| |
| anyFired: boolean; |
| } |
|
|
| |
| |
| |
| |
| |
| function readThresholds( |
| config: Record<string, unknown> | null, |
| ): TriggerThresholds { |
| const c = (config ?? {}) as Record<string, unknown>; |
| const evo = (c.evolution as Record<string, unknown>) ?? {}; |
| |
| |
| |
| |
| const usingDefaults = !c.evolution; |
| if (usingDefaults) { |
| void import("../quarantine/index.ts").then((q) => |
| q.recordQuarantineHit("CONT-006", { |
| gate: "trigger_thresholds_default", |
| site: "evolution/triggers.ts:readThresholds", |
| defaults: { |
| cadenceN: DEFAULT_CADENCE_N, |
| regressionFloor: DEFAULT_REGRESSION_FLOOR, |
| coverageK: DEFAULT_COVERAGE_K, |
| }, |
| }), |
| ); |
| } |
| const num = (v: unknown, d: number): number => { |
| const n = typeof v === "number" ? v : Number(v); |
| return Number.isFinite(n) && n > 0 ? n : d; |
| }; |
| return { |
| cadenceN: num(evo.cadenceN, DEFAULT_CADENCE_N), |
| regressionFloor: clamp01(num(evo.regressionFloor, DEFAULT_REGRESSION_FLOOR)), |
| coverageK: num(evo.coverageK, DEFAULT_COVERAGE_K), |
| }; |
| } |
|
|
| function clamp01(n: number): number { |
| if (n < 0) return 0; |
| if (n > 1) return 1; |
| return n; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export async function evaluateTriggers( |
| networkId: string, |
| ): Promise<TriggerEvaluation> { |
| const netRow = ( |
| await db.select().from(toolNetworks).where(eq(toolNetworks.id, networkId)).limit(1) |
| )[0]; |
| if (!netRow) throw new Error(`network ${networkId} not found`); |
| const thresholds = readThresholds(netRow.config as Record<string, unknown>); |
| const activeVersionId = netRow.activeVariantId; |
| if (!activeVersionId) { |
| return { |
| networkId, |
| activeVersionId: null, |
| problemClassPath: netRow.problemClassPath, |
| fitness: null, |
| thresholds, |
| signals: [], |
| anyFired: false, |
| }; |
| } |
| const fitness = await rollingFitness( |
| networkId, |
| activeVersionId, |
| netRow.problemClassPath, |
| ); |
|
|
| |
| const lastCadenceEvent = ( |
| await db |
| .select() |
| .from(networkEvolutionEvents) |
| .where( |
| and( |
| eq(networkEvolutionEvents.networkId, networkId), |
| eq(networkEvolutionEvents.kind, "cadence_trigger"), |
| ), |
| ) |
| .orderBy(desc(networkEvolutionEvents.createdAt)) |
| .limit(1) |
| )[0]; |
| const cadenceWhere = and( |
| eq(networkVersionMetrics.networkId, networkId), |
| eq(networkVersionMetrics.versionId, activeVersionId), |
| lastCadenceEvent |
| ? gte(networkVersionMetrics.createdAt, lastCadenceEvent.createdAt) |
| : sql`true`, |
| ); |
| const sinceCount = Number( |
| ( |
| await db |
| .select({ c: count() }) |
| .from(networkVersionMetrics) |
| .where(cadenceWhere) |
| )[0]?.c ?? 0, |
| ); |
| const cadenceFired = sinceCount >= thresholds.cadenceN; |
|
|
| |
| const regressionFired = |
| fitness.sampleCount >= 10 && fitness.weightedMean < thresholds.regressionFloor; |
|
|
| |
| const coverageRows = await db |
| .select({ |
| retries: networkVersionMetrics.retries, |
| budgetExceeded: networkVersionMetrics.budgetExceeded, |
| }) |
| .from(networkVersionMetrics) |
| .where( |
| and( |
| eq(networkVersionMetrics.networkId, networkId), |
| eq(networkVersionMetrics.versionId, activeVersionId), |
| gte( |
| networkVersionMetrics.createdAt, |
| new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), |
| ), |
| ), |
| ); |
| const coverageMisses = coverageRows.filter( |
| (r) => r.budgetExceeded || (r.retries ?? 0) >= DEFAULT_MAX_RETRIES, |
| ).length; |
| const coverageFired = coverageMisses >= thresholds.coverageK; |
|
|
| |
| |
| |
| |
| |
| |
| |
| const lastBackfill = ( |
| await db |
| .select() |
| .from(networkEvolutionEvents) |
| .where( |
| and( |
| eq(networkEvolutionEvents.networkId, networkId), |
| eq(networkEvolutionEvents.kind, "external_truth_backfilled"), |
| ), |
| ) |
| .orderBy(desc(networkEvolutionEvents.createdAt)) |
| .limit(1) |
| )[0]; |
| const lastTruthConsume = ( |
| await db |
| .select() |
| .from(networkEvolutionEvents) |
| .where( |
| and( |
| eq(networkEvolutionEvents.networkId, networkId), |
| eq(networkEvolutionEvents.kind, "external_truth_trigger"), |
| ), |
| ) |
| .orderBy(desc(networkEvolutionEvents.createdAt)) |
| .limit(1) |
| )[0]; |
| const externalTruthFired = |
| !!lastBackfill && |
| (!lastTruthConsume || lastBackfill.createdAt > lastTruthConsume.createdAt); |
|
|
| const signals: TriggerSignal[] = [ |
| { |
| kind: "cadence_trigger", |
| fired: cadenceFired, |
| payload: { |
| sampleCountSinceLast: sinceCount, |
| threshold: thresholds.cadenceN, |
| sinceEventId: lastCadenceEvent?.id ?? null, |
| }, |
| }, |
| { |
| kind: "regression_trigger", |
| fired: regressionFired, |
| payload: { |
| weightedMean: fitness.weightedMean, |
| floor: thresholds.regressionFloor, |
| sampleCount: fitness.sampleCount, |
| ciLower: fitness.ciLower, |
| }, |
| }, |
| { |
| kind: "coverage_trigger", |
| fired: coverageFired, |
| payload: { |
| misses: coverageMisses, |
| threshold: thresholds.coverageK, |
| windowDays: 7, |
| }, |
| }, |
| { |
| kind: "external_truth_trigger", |
| fired: externalTruthFired, |
| payload: { |
| latestBackfillId: lastBackfill?.id ?? null, |
| latestBackfillAt: lastBackfill?.createdAt |
| ? new Date(lastBackfill.createdAt).toISOString() |
| : null, |
| sinceTriggerId: lastTruthConsume?.id ?? null, |
| }, |
| }, |
| ]; |
|
|
| for (const s of signals) { |
| if (!s.fired) continue; |
| await recordEvent({ |
| networkId, |
| kind: s.kind, |
| variantId: activeVersionId, |
| payload: s.payload, |
| }); |
| } |
|
|
| return { |
| networkId, |
| activeVersionId, |
| problemClassPath: netRow.problemClassPath, |
| fitness, |
| thresholds, |
| signals, |
| anyFired: signals.some((s) => s.fired), |
| }; |
| } |
|
|