Files
Cibello-app/packages/database/src/schema/scans.ts
T
Sven (AAMOS AI) c2cc3878dd fix(skiva-1): spara accept, bildref, lokal träningsbank, lärande-loop-dok
- confirm-loop sparar nu även action=accept som positivt exempel i ai_corrections
- imageS3Key sparas vid image_training-samtycke, annars null
- ai_corrections.proposal lagrar det specifika AI-förslaget per item
- BUILD_TRAINING_SAMPLE bankar lokalt till ai_training_bank (ej externt runTask)
- Jobbet kastar aldrig i AAMOS_MODE=gemini
- Migration 0020: ai_corrections.image_s3_key/proposal + ai_training_bank
- docs/28-lärande-loop.md: datakontrakt, samtycke, retention, GDPR-radering
- Tester: accept + bildref (ja/nej) + lokal bank i gemini-läge
- REQUIRE_REAL=1 är AI-fokuserat i gemini-läge; staging mail/S3 får vara mock
2026-08-08 04:00:49 +07:00

114 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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),
],
);