import { nanoid } from "nanoid"; import type { PerformanceSnapshot, PublicationRecord, ScoringRun, } from "../calibration/types"; import type { ConfirmedFactCard, ImageInput, OptimizedArticle, PublishPlatform, QaReport, } from "../domain/types"; import type { AppRepository } from "./repository"; import type { ArticleJob, BrandTemplate } from "./repositories"; type JsonObject = Record; 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; 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; job_id: string; revision: number; rubric_version_id: string; dimension_scores: string; composite_score: number; rationale: string; created_at: string; } interface PublicationRecordRow { id: string; job_id: string; revision: number; platform: PublishPlatform; 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; } 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 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 toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot { return { ...row, raw_reference: row.raw_reference ?? undefined, metrics: parseJson(row.metrics), }; } export function createD1Repository(db: D1Database): AppRepository { return { async createBrandTemplate(input) { const createdAt = nowIso(); const template: BrandTemplate = { id: `brand_${nanoid(10)}`, ...input, created_at: createdAt, updated_at: createdAt, }; await 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( template.id, template.brand_name, template.company_full_name, serialize(template.company_short_names), serialize(template.product_names), serialize(template.target_industries), serialize(template.target_audience), serialize(template.verified_claims), serialize(template.forbidden_claims), serialize(template.tone_rules), template.created_at, template.updated_at, ) .run(); return template; }, async listBrandTemplates() { const result = await db .prepare("select * from brand_templates order by updated_at desc") .all(); return result.results.map(toBrandTemplate); }, async getBrandTemplate(id) { const row = await db .prepare("select * from brand_templates where id = ?") .bind(id) .first(); return row ? toBrandTemplate(row) : null; }, async createArticleJob(input) { const createdAt = nowIso(); const job: ArticleJob = { id: `job_${nanoid(10)}`, brand_template_id: input.brand_template_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, }; await db .prepare( `insert into article_jobs ( id, brand_template_id, source_title, source_body, image_inputs, publish_platform, user_instructions, status, export_paths, created_at, updated_at ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( job.id, job.brand_template_id, job.source_title, job.source_body, serialize(job.image_inputs), job.publish_platform, job.user_instructions, job.status, serialize(job.export_paths), job.created_at, job.updated_at, ) .run(); return job; }, async getArticleJob(id) { const row = await db .prepare("select * from article_jobs where id = ?") .bind(id) .first(); return row ? toArticleJob(row) : null; }, async updateArticleJob(id, changes) { const existing = await this.getArticleJob(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(), }; await db .prepare( `update article_jobs set brand_template_id = ?, status = ?, export_paths = ?, updated_at = ? where id = ?`, ) .bind( updated.brand_template_id, updated.status, serialize(updated.export_paths), updated.updated_at, id, ) .run(); return this.getArticleJob(id); }, async saveFactCard(jobId, factCard) { const timestamp = nowIso(); await db .prepare( `insert into fact_cards ( job_id, source, fact_card, confirmed_by_user, created_at, updated_at ) values (?, ?, ?, ?, ?, ?) on conflict(job_id) do update set fact_card = excluded.fact_card, confirmed_by_user = excluded.confirmed_by_user, updated_at = excluded.updated_at`, ) .bind( jobId, "auto_extract_then_user_confirmed", serialize(factCard), factCard.confirmed_by_user ? 1 : 0, timestamp, timestamp, ) .run(); return { job_id: jobId, ...factCard }; }, async getFactCard(jobId) { const row = await db .prepare("select job_id, fact_card from fact_cards where job_id = ?") .bind(jobId) .first(); return row ? { job_id: row.job_id, ...parseJson(row.fact_card) } : null; }, async saveOptimizedArticle(jobId, article) { const latest = await this.getLatestOptimizedArticle(jobId); const revision = (latest?.revision ?? 0) + 1; const saved = { ...article, job_id: jobId, revision }; await db .prepare( `insert into optimized_articles (job_id, revision, article, created_at) values (?, ?, ?, ?)`, ) .bind(jobId, revision, serialize(saved), nowIso()) .run(); return saved; }, async getLatestOptimizedArticle(jobId) { const row = await db .prepare( `select job_id, revision, article from optimized_articles where job_id = ? order by revision desc limit 1`, ) .bind(jobId) .first(); return row ? parseJson(row.article) : null; }, async saveQaReport(jobId, revision, report) { const saved = { ...report, job_id: jobId, revision }; await db .prepare( `insert into qa_reports (job_id, revision, report, created_at) values (?, ?, ?, ?) on conflict(job_id, revision) do update set report = excluded.report`, ) .bind(jobId, revision, serialize(saved), nowIso()) .run(); return saved; }, async getLatestQaReport(jobId) { const row = await db .prepare( `select job_id, revision, report from qa_reports where job_id = ? order by revision desc limit 1`, ) .bind(jobId) .first(); return row ? parseJson(row.report) : null; }, async saveRubricVersion(rubric) { await 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`, ) .bind( rubric.id, rubric.version, rubric.name, serialize(rubric.dimensions), rubric.formula, rubric.is_active ? 1 : 0, rubric.created_at, ) .run(); return rubric; }, async saveScoringRun(run) { await db .prepare( `insert into scoring_runs ( id, job_id, revision, rubric_version_id, dimension_scores, composite_score, rationale, created_at ) values (?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( run.id, run.job_id, run.revision, run.rubric_version_id, serialize(run.dimension_scores), run.composite_score, run.rationale, run.created_at, ) .run(); return run; }, async getLatestScoringRun(jobId, revision) { const row = await db .prepare( `select * from scoring_runs where job_id = ? and revision = ? order by created_at desc limit 1`, ) .bind(jobId, revision) .first(); return row ? toScoringRun(row) : null; }, async createPublicationRecord(input) { const timestamp = nowIso(); const record: PublicationRecord = { id: `pub_${nanoid(10)}`, ...input, created_at: timestamp, updated_at: timestamp, }; await db .prepare( `insert into publication_records ( id, job_id, revision, platform, url, published_at, status, notes, created_at, updated_at ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( record.id, record.job_id, record.revision, record.platform, record.url, record.published_at, record.status, record.notes, record.created_at, record.updated_at, ) .run(); return record; }, async listPublicationRecords(jobId) { const result = await db .prepare( "select * from publication_records where job_id = ? order by published_at desc", ) .bind(jobId) .all(); return result.results; }, async getPublicationRecord(id) { return db .prepare("select * from publication_records where id = ?") .bind(id) .first(); }, async savePerformanceSnapshot(snapshot) { await db .prepare( `insert into performance_snapshots ( id, publication_id, source, window_label, metrics, feedback_summary, raw_reference, snapshot_at ) values (?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( snapshot.id, snapshot.publication_id, snapshot.source, snapshot.window_label, serialize(snapshot.metrics), snapshot.feedback_summary, snapshot.raw_reference ?? null, snapshot.snapshot_at, ) .run(); return snapshot; }, async listPerformanceSnapshots(publicationId) { const result = await db .prepare( "select * from performance_snapshots where publication_id = ? order by snapshot_at desc", ) .bind(publicationId) .all(); return result.results.map(toPerformanceSnapshot); }, async saveCalibrationEvent(event) { await db .prepare( `insert into calibration_events ( id, publication_id, scoring_run_id, performance_snapshot_id, direction, observations, recommended_action, created_at ) values (?, ?, ?, ?, ?, ?, ?, ?)`, ) .bind( event.id, event.publication_id, event.scoring_run_id, event.performance_snapshot_id, event.direction, serialize(event.observations), event.recommended_action, event.created_at, ) .run(); return event; }, }; }