import { doublePrecision, index, integer, jsonb, pgTable, text, timestamp, uniqueIndex, uuid, } from "drizzle-orm/pg-core"; import { createdAt, jobStatusEnum, jobTypeEnum, scanTypeEnum, updatedAt } from "./_shared.js"; import { households } from "./households.js"; import { users } from "./users.js"; /** * Asynkrona skannings-/AI-jobb (spec §50, §54): * App → signed S3 upload → API job → worker → AAMOS → result → app. */ export const scanJobs = pgTable( "scan_jobs", { id: uuid("id").primaryKey().defaultRandom(), userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }), scanType: scanTypeEnum("scan_type").notNull(), jobType: jobTypeEnum("job_type").notNull(), status: jobStatusEnum("status").notNull().default("queued"), s3Keys: text("s3_keys").array().notNull().default([]), context: jsonb("context"), /** Strukturerat AI-resultat validerat mot ai-contracts innan lagring. */ result: jsonb("result"), error: text("error"), modelVersion: text("model_version"), promptVersion: text("prompt_version"), latencyMs: integer("latency_ms"), costUsd: doublePrecision("cost_usd"), attempts: integer("attempts").notNull().default(0), completedAt: timestamp("completed_at", { withTimezone: true }), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [ index("scan_jobs_user_idx").on(t.userId, t.createdAt), index("scan_jobs_status_idx").on(t.status), ], ); /** * Verifierade korrigeringar (spec §33): AI sa X, användaren sa Y. * Grund för träningsdata – används ENDAST enligt samtycke. */ export const aiCorrections = pgTable( "ai_corrections", { id: uuid("id").primaryKey().defaultRandom(), scanJobId: uuid("scan_job_id").references(() => scanJobs.id, { onDelete: "set null" }), userId: uuid("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), taskType: text("task_type").notNull(), aiOutput: jsonb("ai_output").notNull(), userCorrection: jsonb("user_correction").notNull(), /** The specific AI proposal item this correction refers to. */ proposal: jsonb("proposal"), /** First stored image key when image_training consent granted, otherwise null. */ imageS3Key: text("image_s3_key"), modelVersion: text("model_version"), promptVersion: text("prompt_version"), /** Snapshot av samtyckesläget när korrigeringen skapades. */ consentSnapshot: jsonb("consent_snapshot").notNull(), exportedToTraining: timestamp("exported_to_training", { withTimezone: true }), trainingBatchId: text("training_batch_id"), createdAt: createdAt(), }, (t) => [index("ai_corrections_task_idx").on(t.taskType, t.createdAt)], ); /** * App-owned, versioned training dataset (spec §33 + Skiva 1 fixrunda). * BUILD_TRAINING_SAMPLE banks eligible ai_corrections here instead of * calling external AAMOS/Gemini runTask. The bank is the consented place * for richer (image, proposal, correction) data. */ export const aiTrainingBank = pgTable( "ai_training_bank", { id: uuid("id").primaryKey().defaultRandom(), correctionId: uuid("correction_id") .notNull() .references(() => aiCorrections.id, { onDelete: "cascade" }), scanJobId: uuid("scan_job_id").references(() => scanJobs.id, { onDelete: "set null" }), version: text("version").notNull().default("v1"), taskType: text("task_type").notNull(), imageS3Key: text("image_s3_key"), proposal: jsonb("proposal").notNull(), action: text("action").notNull(), corrected: jsonb("corrected"), modelVersion: text("model_version"), promptVersion: text("prompt_version"), consentSnapshot: jsonb("consent_snapshot").notNull().default({}), exportedAt: timestamp("exported_at", { withTimezone: true }).notNull().defaultNow(), createdAt: createdAt(), }, (t) => [ uniqueIndex("ai_training_bank_correction_unique").on(t.correctionId), index("ai_training_bank_version_idx").on(t.version, t.taskType), index("ai_training_bank_scan_job_idx").on(t.scanJobId), index("ai_training_bank_created_at_idx").on(t.createdAt), ], );