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
This commit is contained in:
Sven (AAMOS AI)
2026-08-08 04:00:49 +07:00
parent 050c958285
commit c2cc3878dd
10 changed files with 646 additions and 43 deletions
+39
View File
@@ -6,6 +6,7 @@ import {
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from "drizzle-orm/pg-core";
import { createdAt, jobStatusEnum, jobTypeEnum, scanTypeEnum, updatedAt } from "./_shared.js";
@@ -62,6 +63,10 @@ export const aiCorrections = pgTable(
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. */
@@ -72,3 +77,37 @@ export const aiCorrections = pgTable(
},
(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),
],
);