/** * CRUD operations for notification decisions. * * Each row records the routing decision made by the decision engine for * a given notification event: whether to notify, which channels, and the * reasoning behind it. This provides a full audit trail of how signals * were routed. */ import { desc, eq } from "drizzle-orm"; import { getDb } from "../persistence/db-connection.js"; import { notificationDecisions } from "../persistence/schema/index.js"; export interface NotificationDecisionRow { id: string; notificationEventId: string; shouldNotify: boolean; selectedChannels: string; // JSON array reasoningSummary: string; confidence: number; fallbackUsed: boolean; promptVersion: string | null; validationResults: string | null; // JSON createdAt: number; } export interface CreateDecisionParams { id: string; notificationEventId: string; shouldNotify: boolean; selectedChannels: string[]; // will be serialised to JSON reasoningSummary: string; confidence: number; fallbackUsed: boolean; promptVersion?: string; validationResults?: Record; } /** Insert a new decision record. */ export function createDecision( params: CreateDecisionParams, ): NotificationDecisionRow { const db = getDb(); const now = Date.now(); const row = { id: params.id, notificationEventId: params.notificationEventId, shouldNotify: params.shouldNotify ? 1 : 0, selectedChannels: JSON.stringify(params.selectedChannels), reasoningSummary: params.reasoningSummary, confidence: params.confidence, fallbackUsed: params.fallbackUsed ? 1 : 0, promptVersion: params.promptVersion ?? null, validationResults: params.validationResults ? JSON.stringify(params.validationResults) : null, createdAt: now, }; db.insert(notificationDecisions).values(row).run(); return { ...row, shouldNotify: params.shouldNotify, fallbackUsed: params.fallbackUsed, }; } export interface UpdateDecisionParams { selectedChannels?: string[]; reasoningSummary?: string; validationResults?: Record; } /** Update an existing decision row (e.g. after routing intent enforcement). */ export function updateDecision(id: string, params: UpdateDecisionParams): void { const db = getDb(); const updates: Record = {}; if (params.selectedChannels !== undefined) { updates.selectedChannels = JSON.stringify(params.selectedChannels); } if (params.reasoningSummary !== undefined) { updates.reasoningSummary = params.reasoningSummary; } if (params.validationResults !== undefined) { updates.validationResults = JSON.stringify(params.validationResults); } if (Object.keys(updates).length === 0) { return; } db.update(notificationDecisions) .set(updates) .where(eq(notificationDecisions.id, id)) .run(); } /** * Return the most recent decision for a given notification event, or * `undefined` if none exists. The decision engine writes exactly one * decision per event today, but ordering by createdAt DESC keeps this * stable if that ever changes (e.g. re-decisions on retry). */ export function findLatestDecisionByEventId( eventId: string, ): NotificationDecisionRow | undefined { const db = getDb(); const row = db .select() .from(notificationDecisions) .where(eq(notificationDecisions.notificationEventId, eventId)) .orderBy(desc(notificationDecisions.createdAt)) .get(); if (!row) { return undefined; } return { id: row.id, notificationEventId: row.notificationEventId, shouldNotify: row.shouldNotify === 1, selectedChannels: row.selectedChannels, reasoningSummary: row.reasoningSummary, confidence: row.confidence, fallbackUsed: row.fallbackUsed === 1, promptVersion: row.promptVersion, validationResults: row.validationResults, createdAt: row.createdAt, }; }