From d5532d9c3d255aa434a25f24eab442980eaf062d Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Tue, 28 Apr 2026 00:01:44 -0700 Subject: [PATCH 1/2] feat: Drizzle schema + zod companions for uncertainty engine (#98) Two schema files matching the 0002_uncertainty_engine.sql migration exactly: - uncertainty_prediction: full emit/witness lifecycle with state enum (emitted | witnessed | orphaned | retired) and 0-1 bounds on claimedConfidence and outcomeCorrectness - uncertainty_calibration_snapshot: materialized reliability per cohort with 0-1 bounds on all confidence/correctness/brier columns Both tables get index declarations mirroring the migration. State and outcome_label are constrained via zod enums; drizzle-zod bridges the Drizzle table types to insert/select schemas and inferred TypeScript types. Closes #98. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/db/schema/uncertainty.ts | 55 +++++++++++++++++++++++ apps/web/src/db/schema/uncertainty.zod.ts | 55 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 apps/web/src/db/schema/uncertainty.ts create mode 100644 apps/web/src/db/schema/uncertainty.zod.ts diff --git a/apps/web/src/db/schema/uncertainty.ts b/apps/web/src/db/schema/uncertainty.ts new file mode 100644 index 0000000..0e5b1ec --- /dev/null +++ b/apps/web/src/db/schema/uncertainty.ts @@ -0,0 +1,55 @@ +import { index, integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { uuidv7 } from "uuidv7"; + +export const uncertaintyPrediction = sqliteTable( + "uncertainty_prediction", + { + id: text("id") + .primaryKey() + .$defaultFn(() => uuidv7()), + surface: text("surface").notNull(), + featureKey: text("feature_key").notNull(), + inputFingerprint: text("input_fingerprint").notNull(), + model: text("model").notNull(), + modelVersion: text("model_version").notNull(), + claimedConfidence: real("claimed_confidence").notNull(), + predictionPayload: text("prediction_payload").notNull(), + state: text("state").notNull(), + outcomeLabel: text("outcome_label"), + outcomePayload: text("outcome_payload"), + outcomeCorrectness: real("outcome_correctness"), + createdAt: integer("created_at", { mode: "timestamp_ms" }) + .notNull() + .$defaultFn(() => new Date()), + witnessedAt: integer("witnessed_at", { mode: "timestamp_ms" }), + orphanAfter: integer("orphan_after", { mode: "timestamp_ms" }).notNull(), + cohortKey: text("cohort_key").notNull(), + }, + (table) => [ + index("idx_uncertainty_prediction_cohort").on(table.cohortKey), + index("idx_uncertainty_prediction_surface").on(table.surface), + index("idx_uncertainty_prediction_state").on(table.state), + index("idx_uncertainty_prediction_orphan_sweep").on(table.state, table.orphanAfter), + ], +); + +export const uncertaintyCalibrationSnapshot = sqliteTable( + "uncertainty_calibration_snapshot", + { + id: text("id") + .primaryKey() + .$defaultFn(() => uuidv7()), + cohortKey: text("cohort_key").notNull(), + bucketLower: real("bucket_lower").notNull(), + bucketUpper: real("bucket_upper").notNull(), + claimedConfidence: real("claimed_confidence").notNull(), + actualCorrectness: real("actual_correctness").notNull(), + predictionCount: integer("prediction_count").notNull(), + orphanCount: integer("orphan_count").notNull(), + brierScore: real("brier_score").notNull(), + computedAt: integer("computed_at", { mode: "timestamp_ms" }) + .notNull() + .$defaultFn(() => new Date()), + }, + (table) => [index("idx_uncertainty_calibration_snapshot_cohort").on(table.cohortKey)], +); diff --git a/apps/web/src/db/schema/uncertainty.zod.ts b/apps/web/src/db/schema/uncertainty.zod.ts new file mode 100644 index 0000000..da8663d --- /dev/null +++ b/apps/web/src/db/schema/uncertainty.zod.ts @@ -0,0 +1,55 @@ +import { createInsertSchema, createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { uncertaintyCalibrationSnapshot, uncertaintyPrediction } from "./uncertainty"; + +const predictionState = z.enum(["emitted", "witnessed", "orphaned", "retired"]); +const outcomeLabel = z.enum(["accepted", "rejected", "edited", "ignored", "custom"]); + +export const insertUncertaintyPredictionSchema = createInsertSchema(uncertaintyPrediction, { + state: predictionState, + outcomeLabel: outcomeLabel.nullable().optional(), + claimedConfidence: z.number().min(0).max(1), + outcomeCorrectness: z.number().min(0).max(1).nullable().optional(), +}); + +export const selectUncertaintyPredictionSchema = createSelectSchema(uncertaintyPrediction, { + state: predictionState, + outcomeLabel: outcomeLabel.nullable(), + claimedConfidence: z.number().min(0).max(1), + outcomeCorrectness: z.number().min(0).max(1).nullable(), +}); + +export const insertUncertaintyCalibrationSnapshotSchema = createInsertSchema( + uncertaintyCalibrationSnapshot, + { + bucketLower: z.number().min(0).max(1), + bucketUpper: z.number().min(0).max(1), + claimedConfidence: z.number().min(0).max(1), + actualCorrectness: z.number().min(0).max(1), + predictionCount: z.number().int().nonnegative(), + orphanCount: z.number().int().nonnegative(), + brierScore: z.number().min(0).max(1), + }, +); + +export const selectUncertaintyCalibrationSnapshotSchema = createSelectSchema( + uncertaintyCalibrationSnapshot, + { + bucketLower: z.number().min(0).max(1), + bucketUpper: z.number().min(0).max(1), + claimedConfidence: z.number().min(0).max(1), + actualCorrectness: z.number().min(0).max(1), + predictionCount: z.number().int().nonnegative(), + orphanCount: z.number().int().nonnegative(), + brierScore: z.number().min(0).max(1), + }, +); + +export type InsertUncertaintyPrediction = z.infer; +export type SelectUncertaintyPrediction = z.infer; +export type InsertUncertaintyCalibrationSnapshot = z.infer< + typeof insertUncertaintyCalibrationSnapshotSchema +>; +export type SelectUncertaintyCalibrationSnapshot = z.infer< + typeof selectUncertaintyCalibrationSnapshotSchema +>; From 8370d2853e096d396059a45fdc054db84b81a648 Mon Sep 17 00:00:00 2001 From: Sean Silvius Date: Tue, 28 Apr 2026 00:03:03 -0700 Subject: [PATCH 2/2] refactor: extract calibration snapshot overrides and unitInterval constant Eliminates the duplicated 7-field override object between insert and select schemas for uncertainty_calibration_snapshot, and collapses the repeated z.number().min(0).max(1) pattern to a named unitInterval constant. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/db/schema/uncertainty.zod.ts | 39 ++++++++++------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/apps/web/src/db/schema/uncertainty.zod.ts b/apps/web/src/db/schema/uncertainty.zod.ts index da8663d..6eb4ef0 100644 --- a/apps/web/src/db/schema/uncertainty.zod.ts +++ b/apps/web/src/db/schema/uncertainty.zod.ts @@ -4,45 +4,40 @@ import { uncertaintyCalibrationSnapshot, uncertaintyPrediction } from "./uncerta const predictionState = z.enum(["emitted", "witnessed", "orphaned", "retired"]); const outcomeLabel = z.enum(["accepted", "rejected", "edited", "ignored", "custom"]); +const unitInterval = z.number().min(0).max(1); + +const calibrationSnapshotOverrides = { + bucketLower: unitInterval, + bucketUpper: unitInterval, + claimedConfidence: unitInterval, + actualCorrectness: unitInterval, + predictionCount: z.number().int().nonnegative(), + orphanCount: z.number().int().nonnegative(), + brierScore: unitInterval, +}; export const insertUncertaintyPredictionSchema = createInsertSchema(uncertaintyPrediction, { state: predictionState, outcomeLabel: outcomeLabel.nullable().optional(), - claimedConfidence: z.number().min(0).max(1), - outcomeCorrectness: z.number().min(0).max(1).nullable().optional(), + claimedConfidence: unitInterval, + outcomeCorrectness: unitInterval.nullable().optional(), }); export const selectUncertaintyPredictionSchema = createSelectSchema(uncertaintyPrediction, { state: predictionState, outcomeLabel: outcomeLabel.nullable(), - claimedConfidence: z.number().min(0).max(1), - outcomeCorrectness: z.number().min(0).max(1).nullable(), + claimedConfidence: unitInterval, + outcomeCorrectness: unitInterval.nullable(), }); export const insertUncertaintyCalibrationSnapshotSchema = createInsertSchema( uncertaintyCalibrationSnapshot, - { - bucketLower: z.number().min(0).max(1), - bucketUpper: z.number().min(0).max(1), - claimedConfidence: z.number().min(0).max(1), - actualCorrectness: z.number().min(0).max(1), - predictionCount: z.number().int().nonnegative(), - orphanCount: z.number().int().nonnegative(), - brierScore: z.number().min(0).max(1), - }, + calibrationSnapshotOverrides, ); export const selectUncertaintyCalibrationSnapshotSchema = createSelectSchema( uncertaintyCalibrationSnapshot, - { - bucketLower: z.number().min(0).max(1), - bucketUpper: z.number().min(0).max(1), - claimedConfidence: z.number().min(0).max(1), - actualCorrectness: z.number().min(0).max(1), - predictionCount: z.number().int().nonnegative(), - orphanCount: z.number().int().nonnegative(), - brierScore: z.number().min(0).max(1), - }, + calibrationSnapshotOverrides, ); export type InsertUncertaintyPrediction = z.infer;