import { nanoid } from "nanoid"; import type { CalibrationEvent, PerformanceSnapshot, PublicationRecord, RubricVersion, ScoringRun, } from "../calibration/types"; import type { CaseInput, CaseListFilters, CaseMetadataPatch, OptimizationCase, OptimizationCaseStatus, OptimizationCaseType, OptimizationResultVersion, ResultVersionStatus, } from "../cases/types"; import type { OptimizationFactCard, ImageInput, OptimizedArticle, PublishPlatform, QaReport, } from "../domain/types"; import { createDatabase, getDefaultDatabasePath } from "./connection"; import { initializeSchema } from "./schema"; type JsonObject = Record; export interface BrandTemplate { id: string; brand_name: string; company_full_name: string; company_short_names: string[]; product_names: string[]; target_industries: string[]; target_audience: string[]; verified_claims: string[]; forbidden_claims: string[]; tone_rules: JsonObject; created_at: string; updated_at: string; } export type NewBrandTemplate = Omit< BrandTemplate, "id" | "created_at" | "updated_at" >; export interface ArticleJob { id: string; brand_template_id: string | null; case_id: string | null; source_title: string; source_body: string; image_inputs: ImageInput[]; publish_platform: PublishPlatform; user_instructions: string; status: string; export_paths: Record; created_at: string; updated_at: string; } export interface NewArticleJob { brand_template_id?: string | null; case_id?: string | null; source_title: string; source_body: string; image_inputs: ImageInput[]; publish_platform: PublishPlatform; user_instructions: string; } interface BrandTemplateRow { id: string; brand_name: string; company_full_name: string; company_short_names: string; product_names: string; target_industries: string; target_audience: string; verified_claims: string; forbidden_claims: string; tone_rules: string; created_at: string; updated_at: string; } interface ArticleJobRow { id: string; brand_template_id: string | null; case_id: string | null; source_title: string; source_body: string; image_inputs: string; publish_platform: PublishPlatform; user_instructions: string; status: string; export_paths: string; created_at: string; updated_at: string; } interface FactCardRow { job_id: string; fact_card: string; } interface OptimizedArticleRow { job_id: string; revision: number; article: string; } interface QaReportRow { job_id: string; revision: number; report: string; } interface ScoringRunRow { id: string; result_version_id: string | null; case_type: OptimizationCaseType; job_id: string | null; revision: number | null; rubric_version_id: string; dimension_scores: string; composite_score: number; rationale: string; created_at: string; } interface PublicationRecordRow { id: string; result_version_id: string | null; job_id: string | null; revision: number | null; publish_target: string; url: string; published_at: string; status: "draft" | "published" | "archived"; notes: string; created_at: string; updated_at: string; } interface PerformanceSnapshotRow { id: string; publication_id: string; source: "manual" | `adapter:${string}`; window_label: string; metrics: string; feedback_summary: string; raw_reference: string | null; snapshot_at: string; } interface OptimizationCaseRow { id: string; case_type: OptimizationCaseType; title: string; summary: string; status: OptimizationCaseStatus; customer_name: string; brand_name: string; project_tags: string; notes: string; publish_target: string; source_excerpt: string; result_excerpt: string; latest_result_version_id: string | null; latest_version_number: number | null; last_error_stage: string | null; last_error_summary: string | null; archived_at: string | null; created_at: string; updated_at: string; } interface CaseInputRow { case_id: string; case_type: OptimizationCaseType; article_job_id: string | null; payload: string; created_at: string; updated_at: string; } interface OptimizationResultVersionRow { id: string; case_id: string; case_type: OptimizationCaseType; version: number; status: ResultVersionStatus; article_job_id: string | null; article_revision: number | null; result_summary: string; payload: string; process_summary: string; llm_audit_summary: string; error_stage: string | null; error_summary: string | null; created_at: string; } function nowIso() { return new Date().toISOString(); } function serialize(value: unknown) { return JSON.stringify(value); } function parseJson(value: string): T { return JSON.parse(value) as T; } function withDb(dbPath: string | undefined, action: (db: ReturnType) => T) { const db = createDatabase(dbPath ?? getDefaultDatabasePath()); initializeSchema(db); try { return action(db); } finally { db.close(); } } function toBrandTemplate(row: BrandTemplateRow): BrandTemplate { return { ...row, company_short_names: parseJson(row.company_short_names), product_names: parseJson(row.product_names), target_industries: parseJson(row.target_industries), target_audience: parseJson(row.target_audience), verified_claims: parseJson(row.verified_claims), forbidden_claims: parseJson(row.forbidden_claims), tone_rules: parseJson(row.tone_rules), }; } function toArticleJob(row: ArticleJobRow): ArticleJob { return { ...row, image_inputs: parseJson(row.image_inputs), export_paths: parseJson>(row.export_paths), }; } function toScoringRun(row: ScoringRunRow): ScoringRun { return { ...row, dimension_scores: parseJson>(row.dimension_scores), }; } function toPublicationRecord(row: PublicationRecordRow): PublicationRecord { return { ...row, platform: isPublishPlatform(row.publish_target) ? row.publish_target : undefined, }; } function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot { return { ...row, raw_reference: row.raw_reference ?? undefined, metrics: parseJson(row.metrics), }; } function toOptimizationCase(row: OptimizationCaseRow): OptimizationCase { return { ...row, project_tags: parseJson(row.project_tags), }; } function toCaseInput(row: CaseInputRow): CaseInput { return { ...row, payload: parseJson(row.payload), }; } function toOptimizationResultVersion( row: OptimizationResultVersionRow, ): OptimizationResultVersion { return { ...row, payload: row.payload ? parseJson(row.payload) : null, process_summary: parseJson( row.process_summary, ), llm_audit_summary: parseJson( row.llm_audit_summary, ), }; } function isPublishPlatform(value: string): value is PublishPlatform { return [ "official_site", "media_article", "comparison_review", "recommendation_list", ].includes(value); } export function createBrandTemplate( dbPath: string | undefined, input: NewBrandTemplate, ) { return withDb(dbPath, (db) => { const createdAt = nowIso(); const template: BrandTemplate = { id: `brand_${nanoid(10)}`, ...input, created_at: createdAt, updated_at: createdAt, }; db.prepare( `insert into brand_templates ( id, brand_name, company_full_name, company_short_names, product_names, target_industries, target_audience, verified_claims, forbidden_claims, tone_rules, created_at, updated_at ) values ( @id, @brand_name, @company_full_name, @company_short_names, @product_names, @target_industries, @target_audience, @verified_claims, @forbidden_claims, @tone_rules, @created_at, @updated_at )`, ).run({ ...template, company_short_names: serialize(template.company_short_names), product_names: serialize(template.product_names), target_industries: serialize(template.target_industries), target_audience: serialize(template.target_audience), verified_claims: serialize(template.verified_claims), forbidden_claims: serialize(template.forbidden_claims), tone_rules: serialize(template.tone_rules), }); return template; }); } export function listBrandTemplates(dbPath?: string) { return withDb(dbPath, (db) => db .prepare("select * from brand_templates order by updated_at desc") .all() .map((row) => toBrandTemplate(row as BrandTemplateRow)), ); } export function getBrandTemplate(dbPath: string | undefined, id: string) { return withDb(dbPath, (db) => { const row = db .prepare("select * from brand_templates where id = ?") .get(id) as BrandTemplateRow | undefined; return row ? toBrandTemplate(row) : null; }); } export function createArticleJob(dbPath: string | undefined, input: NewArticleJob) { return withDb(dbPath, (db) => { const createdAt = nowIso(); const job: ArticleJob = { id: `job_${nanoid(10)}`, brand_template_id: input.brand_template_id ?? null, case_id: input.case_id ?? null, source_title: input.source_title, source_body: input.source_body, image_inputs: input.image_inputs, publish_platform: input.publish_platform, user_instructions: input.user_instructions, status: "draft", export_paths: {}, created_at: createdAt, updated_at: createdAt, }; db.prepare( `insert into article_jobs ( id, brand_template_id, case_id, source_title, source_body, image_inputs, publish_platform, user_instructions, status, export_paths, created_at, updated_at ) values ( @id, @brand_template_id, @case_id, @source_title, @source_body, @image_inputs, @publish_platform, @user_instructions, @status, @export_paths, @created_at, @updated_at )`, ).run({ ...job, image_inputs: serialize(job.image_inputs), export_paths: serialize(job.export_paths), }); return job; }); } export function getArticleJob(dbPath: string | undefined, id: string) { return withDb(dbPath, (db) => { const row = db .prepare("select * from article_jobs where id = ?") .get(id) as ArticleJobRow | undefined; return row ? toArticleJob(row) : null; }); } export function updateArticleJob( dbPath: string | undefined, id: string, changes: Partial>, ) { return withDb(dbPath, (db) => { const existing = getArticleJob(dbPath, id); if (!existing) return null; const updated = { brand_template_id: changes.brand_template_id ?? existing.brand_template_id, status: changes.status ?? existing.status, export_paths: changes.export_paths ?? existing.export_paths, updated_at: nowIso(), }; db.prepare( `update article_jobs set brand_template_id = @brand_template_id, status = @status, export_paths = @export_paths, updated_at = @updated_at where id = @id`, ).run({ id, ...updated, export_paths: serialize(updated.export_paths), }); return getArticleJob(dbPath, id); }); } export function saveFactCard( dbPath: string | undefined, jobId: string, factCard: OptimizationFactCard, ) { return withDb(dbPath, (db) => { const timestamp = nowIso(); db.prepare( `insert into fact_cards ( job_id, source, fact_card, confirmed_by_user, created_at, updated_at ) values ( @job_id, @source, @fact_card, @confirmed_by_user, @created_at, @updated_at ) on conflict(job_id) do update set fact_card = excluded.fact_card, confirmed_by_user = excluded.confirmed_by_user, updated_at = excluded.updated_at`, ).run({ job_id: jobId, source: factCard.confirmed_by_user ? "auto_extract_then_user_confirmed" : "auto_extract_for_optimization", fact_card: serialize(factCard), confirmed_by_user: factCard.confirmed_by_user ? 1 : 0, created_at: timestamp, updated_at: timestamp, }); return { job_id: jobId, ...factCard }; }); } export function getFactCard(dbPath: string | undefined, jobId: string) { return withDb(dbPath, (db) => { const row = db .prepare("select job_id, fact_card from fact_cards where job_id = ?") .get(jobId) as FactCardRow | undefined; return row ? { job_id: row.job_id, ...parseJson(row.fact_card) } : null; }); } export function saveOptimizedArticle( dbPath: string | undefined, jobId: string, article: OptimizedArticle, ) { return withDb(dbPath, (db) => { const nextRevision = ((db .prepare( "select max(revision) as revision from optimized_articles where job_id = ?", ) .get(jobId) as { revision: number | null }).revision ?? 0) + 1; const saved = { ...article, job_id: jobId, revision: nextRevision }; db.prepare( `insert into optimized_articles (job_id, revision, article, created_at) values (?, ?, ?, ?)`, ).run(jobId, nextRevision, serialize(saved), nowIso()); return saved; }); } export function getLatestOptimizedArticle(dbPath: string | undefined, jobId: string) { return withDb(dbPath, (db) => { const row = db .prepare( `select job_id, revision, article from optimized_articles where job_id = ? order by revision desc limit 1`, ) .get(jobId) as OptimizedArticleRow | undefined; return row ? parseJson(row.article) : null; }); } export function saveQaReport( dbPath: string | undefined, jobId: string, revision: number, report: QaReport, ) { return withDb(dbPath, (db) => { const saved = { ...report, job_id: jobId, revision }; db.prepare( `insert into qa_reports (job_id, revision, report, created_at) values (?, ?, ?, ?) on conflict(job_id, revision) do update set report = excluded.report`, ).run(jobId, revision, serialize(saved), nowIso()); return saved; }); } export function getLatestQaReport(dbPath: string | undefined, jobId: string) { return withDb(dbPath, (db) => { const row = db .prepare( `select job_id, revision, report from qa_reports where job_id = ? order by revision desc limit 1`, ) .get(jobId) as QaReportRow | undefined; return row ? parseJson(row.report) : null; }); } export function createOptimizationCase( dbPath: string | undefined, input: { case_type: OptimizationCaseType; title: string; summary: string; publish_target: string; source_excerpt: string; }, ) { return withDb(dbPath, (db) => { const timestamp = nowIso(); const optimizationCase: OptimizationCase = { id: `case_${nanoid(10)}`, case_type: input.case_type, title: input.title, summary: input.summary, status: "running", customer_name: "", brand_name: "", project_tags: [], notes: "", publish_target: input.publish_target, source_excerpt: input.source_excerpt, result_excerpt: "", latest_result_version_id: null, latest_version_number: null, last_error_stage: null, last_error_summary: null, archived_at: null, created_at: timestamp, updated_at: timestamp, }; db.prepare( `insert into optimization_cases ( id, case_type, title, summary, status, customer_name, brand_name, project_tags, notes, publish_target, source_excerpt, result_excerpt, latest_result_version_id, latest_version_number, last_error_stage, last_error_summary, archived_at, created_at, updated_at ) values ( @id, @case_type, @title, @summary, @status, @customer_name, @brand_name, @project_tags, @notes, @publish_target, @source_excerpt, @result_excerpt, @latest_result_version_id, @latest_version_number, @last_error_stage, @last_error_summary, @archived_at, @created_at, @updated_at )`, ).run({ ...optimizationCase, project_tags: serialize(optimizationCase.project_tags), }); return optimizationCase; }); } export function saveCaseInput( dbPath: string | undefined, input: Omit, ) { return withDb(dbPath, (db) => { const timestamp = nowIso(); const saved: CaseInput = { ...input, created_at: timestamp, updated_at: timestamp, }; db.prepare( `insert into case_inputs ( case_id, case_type, article_job_id, payload, created_at, updated_at ) values ( @case_id, @case_type, @article_job_id, @payload, @created_at, @updated_at ) on conflict(case_id) do update set case_type = excluded.case_type, article_job_id = excluded.article_job_id, payload = excluded.payload, updated_at = excluded.updated_at`, ).run({ ...saved, payload: serialize(saved.payload), }); return saved; }); } export function listOptimizationCases( dbPath: string | undefined, filters: CaseListFilters, ) { return withDb(dbPath, (db) => { const where: string[] = []; const values: unknown[] = []; if (!filters.include_archived) { where.push("archived_at is null"); } if (filters.case_type) { where.push("case_type = ?"); values.push(filters.case_type); } if (filters.status) { where.push("status = ?"); values.push(filters.status); } if (filters.publish_target) { where.push("publish_target = ?"); values.push(filters.publish_target); } if (filters.project_tag) { where.push("project_tags like ?"); values.push(`%"${filters.project_tag}"%`); } if (filters.created_from) { where.push("created_at >= ?"); values.push(filters.created_from); } if (filters.created_to) { where.push("created_at <= ?"); values.push(filters.created_to); } if (filters.q) { where.push( "(title like ? or summary like ? or source_excerpt like ? or result_excerpt like ? or customer_name like ? or brand_name like ? or notes like ?)", ); const keyword = `%${filters.q}%`; values.push(keyword, keyword, keyword, keyword, keyword, keyword, keyword); } const clause = where.length > 0 ? `where ${where.join(" and ")}` : ""; return db .prepare(`select * from optimization_cases ${clause} order by updated_at desc`) .all(...values) .map((row) => toOptimizationCase(row as OptimizationCaseRow)); }); } export function getOptimizationCaseDetail( dbPath: string | undefined, caseId: string, ) { return withDb(dbPath, (db) => { const caseRow = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow | undefined; if (!caseRow) return null; const inputRow = db .prepare("select * from case_inputs where case_id = ?") .get(caseId) as CaseInputRow | undefined; const versionRows = db .prepare( `select * from optimization_result_versions where case_id = ? order by version desc`, ) .all(caseId) as OptimizationResultVersionRow[]; return { case: toOptimizationCase(caseRow), input: inputRow ? toCaseInput(inputRow) : null, versions: versionRows.map(toOptimizationResultVersion), }; }); } export function updateOptimizationCaseMetadata( dbPath: string | undefined, caseId: string, changes: CaseMetadataPatch, ) { return withDb(dbPath, (db) => { const existing = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow | undefined; if (!existing) return null; const updated = { title: changes.title ?? existing.title, customer_name: changes.customer_name ?? existing.customer_name, brand_name: changes.brand_name ?? existing.brand_name, project_tags: changes.project_tags === undefined ? existing.project_tags : serialize(changes.project_tags), notes: changes.notes ?? existing.notes, updated_at: nowIso(), }; db.prepare( `update optimization_cases set title = @title, customer_name = @customer_name, brand_name = @brand_name, project_tags = @project_tags, notes = @notes, updated_at = @updated_at where id = @id`, ).run({ id: caseId, ...updated }); const row = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow; return toOptimizationCase(row); }); } export function archiveOptimizationCase( dbPath: string | undefined, caseId: string, ) { return updateCaseArchiveState(dbPath, caseId, true); } export function restoreOptimizationCase( dbPath: string | undefined, caseId: string, ) { return updateCaseArchiveState(dbPath, caseId, false); } function updateCaseArchiveState( dbPath: string | undefined, caseId: string, archived: boolean, ) { return withDb(dbPath, (db) => { const existing = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow | undefined; if (!existing) return null; const timestamp = nowIso(); const restoredStatus = existing.latest_result_version_id ? "optimized" : existing.last_error_summary ? "failed" : "running"; db.prepare( `update optimization_cases set status = ?, archived_at = ?, updated_at = ? where id = ?`, ).run( archived ? "archived" : restoredStatus, archived ? timestamp : null, timestamp, caseId, ); const row = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow; return toOptimizationCase(row); }); } export function markOptimizationCaseFailed( dbPath: string | undefined, caseId: string, input: { error_stage: string; error_summary: string }, ) { return withDb(dbPath, (db) => { const timestamp = nowIso(); db.prepare( `update optimization_cases set status = 'failed', last_error_stage = ?, last_error_summary = ?, updated_at = ? where id = ?`, ).run(input.error_stage, input.error_summary, timestamp, caseId); const row = db .prepare("select * from optimization_cases where id = ?") .get(caseId) as OptimizationCaseRow | undefined; return row ? toOptimizationCase(row) : null; }); } export function createOptimizationResultVersion( dbPath: string | undefined, input: Omit, ) { return withDb(dbPath, (db) => { const nextVersion = ((db .prepare( "select max(version) as version from optimization_result_versions where case_id = ?", ) .get(input.case_id) as { version: number | null }).version ?? 0) + 1; const createdAt = nowIso(); const resultVersion: OptimizationResultVersion = { id: `ver_${nanoid(10)}`, ...input, version: nextVersion, created_at: createdAt, }; db.prepare( `insert into optimization_result_versions ( id, case_id, case_type, version, status, article_job_id, article_revision, result_summary, payload, process_summary, llm_audit_summary, error_stage, error_summary, created_at ) values ( @id, @case_id, @case_type, @version, @status, @article_job_id, @article_revision, @result_summary, @payload, @process_summary, @llm_audit_summary, @error_stage, @error_summary, @created_at )`, ).run({ ...resultVersion, payload: resultVersion.payload ? serialize(resultVersion.payload) : "", process_summary: serialize(resultVersion.process_summary), llm_audit_summary: serialize(resultVersion.llm_audit_summary), }); db.prepare( `update optimization_cases set status = @status, result_excerpt = @result_excerpt, latest_result_version_id = @latest_result_version_id, latest_version_number = @latest_version_number, last_error_stage = @last_error_stage, last_error_summary = @last_error_summary, updated_at = @updated_at where id = @case_id`, ).run({ case_id: input.case_id, status: input.status === "optimized" ? "optimized" : "failed", result_excerpt: input.result_summary, latest_result_version_id: resultVersion.id, latest_version_number: resultVersion.version, last_error_stage: input.error_stage, last_error_summary: input.error_summary, updated_at: createdAt, }); return resultVersion; }); } export function getOptimizationResultVersion( dbPath: string | undefined, versionId: string, ) { return withDb(dbPath, (db) => { const row = db .prepare("select * from optimization_result_versions where id = ?") .get(versionId) as OptimizationResultVersionRow | undefined; return row ? toOptimizationResultVersion(row) : null; }); } export function findResultVersionForArticleRevision( dbPath: string | undefined, jobId: string, revision: number, ) { return withDb(dbPath, (db) => { const row = db .prepare( `select * from optimization_result_versions where article_job_id = ? and article_revision = ? order by version desc limit 1`, ) .get(jobId, revision) as OptimizationResultVersionRow | undefined; return row ? toOptimizationResultVersion(row) : null; }); } export function saveRubricVersion( dbPath: string | undefined, rubric: RubricVersion, ) { return withDb(dbPath, (db) => { db.prepare( `insert into rubric_versions ( id, version, name, dimensions, formula, is_active, created_at ) values (?, ?, ?, ?, ?, ?, ?) on conflict(id) do update set version = excluded.version, name = excluded.name, dimensions = excluded.dimensions, formula = excluded.formula, is_active = excluded.is_active`, ).run( rubric.id, rubric.version, rubric.name, serialize(rubric.dimensions), rubric.formula, rubric.is_active ? 1 : 0, rubric.created_at, ); return rubric; }); } export function saveScoringRun(dbPath: string | undefined, run: ScoringRun) { return withDb(dbPath, (db) => { db.prepare( `insert into scoring_runs ( id, result_version_id, case_type, job_id, revision, rubric_version_id, dimension_scores, composite_score, rationale, created_at ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ).run( run.id, run.result_version_id ?? null, run.case_type ?? "article", run.job_id, run.revision, run.rubric_version_id, serialize(run.dimension_scores), run.composite_score, run.rationale, run.created_at, ); return run; }); } export function getLatestScoringRun( dbPath: string | undefined, jobId: string, revision: number, ) { return withDb(dbPath, (db) => { const row = db .prepare( `select * from scoring_runs where job_id = ? and revision = ? order by created_at desc limit 1`, ) .get(jobId, revision) as ScoringRunRow | undefined; return row ? toScoringRun(row) : null; }); } export function getLatestScoringRunForResultVersion( dbPath: string | undefined, resultVersionId: string, ) { return withDb(dbPath, (db) => { const row = db .prepare( `select * from scoring_runs where result_version_id = ? order by created_at desc limit 1`, ) .get(resultVersionId) as ScoringRunRow | undefined; return row ? toScoringRun(row) : null; }); } export function createPublicationRecord( dbPath: string | undefined, input: Omit< PublicationRecord, "id" | "created_at" | "updated_at" | "result_version_id" | "publish_target" > & { result_version_id?: string | null; publish_target?: string; }, ) { return withDb(dbPath, (db) => { const timestamp = nowIso(); const publishTarget = input.publish_target ?? input.platform ?? "未指定"; const record: PublicationRecord = { id: `pub_${nanoid(10)}`, ...input, result_version_id: input.result_version_id ?? null, publish_target: publishTarget, platform: isPublishPlatform(publishTarget) ? publishTarget : input.platform, created_at: timestamp, updated_at: timestamp, }; db.prepare( `insert into publication_records ( id, result_version_id, job_id, revision, publish_target, url, published_at, status, notes, created_at, updated_at ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ).run( record.id, record.result_version_id, record.job_id, record.revision, record.publish_target, record.url, record.published_at, record.status, record.notes, record.created_at, record.updated_at, ); return record; }); } export function listPublicationRecords(dbPath: string | undefined, jobId: string) { return withDb(dbPath, (db) => db .prepare("select * from publication_records where job_id = ? order by published_at desc") .all(jobId) .map((row) => toPublicationRecord(row as PublicationRecordRow)), ); } export function listPublicationRecordsForResultVersion( dbPath: string | undefined, resultVersionId: string, ) { return withDb(dbPath, (db) => db .prepare( "select * from publication_records where result_version_id = ? order by published_at desc", ) .all(resultVersionId) .map((row) => toPublicationRecord(row as PublicationRecordRow)), ); } export function getPublicationRecord(dbPath: string | undefined, id: string) { return withDb(dbPath, (db) => { const row = db .prepare("select * from publication_records where id = ?") .get(id) as PublicationRecordRow | undefined; return row ? toPublicationRecord(row) : null; }); } export function savePerformanceSnapshot( dbPath: string | undefined, snapshot: PerformanceSnapshot, ) { return withDb(dbPath, (db) => { db.prepare( `insert into performance_snapshots ( id, publication_id, source, window_label, metrics, feedback_summary, raw_reference, snapshot_at ) values (?, ?, ?, ?, ?, ?, ?, ?)`, ).run( snapshot.id, snapshot.publication_id, snapshot.source, snapshot.window_label, serialize(snapshot.metrics), snapshot.feedback_summary, snapshot.raw_reference ?? null, snapshot.snapshot_at, ); return snapshot; }); } export function listPerformanceSnapshots( dbPath: string | undefined, publicationId: string, ) { return withDb(dbPath, (db) => db .prepare( "select * from performance_snapshots where publication_id = ? order by snapshot_at desc", ) .all(publicationId) .map((row) => toPerformanceSnapshot(row as PerformanceSnapshotRow)), ); } export function saveCalibrationEvent( dbPath: string | undefined, event: CalibrationEvent, ) { return withDb(dbPath, (db) => { db.prepare( `insert into calibration_events ( id, publication_id, scoring_run_id, performance_snapshot_id, direction, observations, recommended_action, created_at ) values (?, ?, ?, ?, ?, ?, ?, ?)`, ).run( event.id, event.publication_id, event.scoring_run_id, event.performance_snapshot_id, event.direction, serialize(event.observations), event.recommended_action, event.created_at, ); return event; }); }