接入发布校准仓储接口
This commit is contained in:
@@ -62,4 +62,37 @@ describe("createD1Repository", () => {
|
|||||||
export_paths: {},
|
export_paths: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("saves a manual performance snapshot using D1 prepare and bind", async () => {
|
||||||
|
const run = vi.fn().mockResolvedValue({ success: true });
|
||||||
|
const bind = vi.fn().mockReturnValue({ run });
|
||||||
|
const prepare = vi.fn().mockReturnValue({ bind });
|
||||||
|
const db = { prepare } as unknown as D1Database;
|
||||||
|
|
||||||
|
const repository = createD1Repository(db);
|
||||||
|
|
||||||
|
await repository.savePerformanceSnapshot({
|
||||||
|
id: "perf_123",
|
||||||
|
publication_id: "pub_123",
|
||||||
|
source: "manual",
|
||||||
|
window_label: "T+7d",
|
||||||
|
metrics: { views: 1200 },
|
||||||
|
feedback_summary: "用户追问案例依据",
|
||||||
|
snapshot_at: "2026-07-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(prepare).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("insert into performance_snapshots"),
|
||||||
|
);
|
||||||
|
expect(bind).toHaveBeenCalledWith(
|
||||||
|
"perf_123",
|
||||||
|
"pub_123",
|
||||||
|
"manual",
|
||||||
|
"T+7d",
|
||||||
|
'{"views":1200}',
|
||||||
|
"用户追问案例依据",
|
||||||
|
null,
|
||||||
|
"2026-07-01T12:00:00.000Z",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,4 +36,79 @@ describe("createSqliteRepository", () => {
|
|||||||
export_paths: {},
|
export_paths: {},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("persists scoring, publication, performance, and calibration event", async () => {
|
||||||
|
const repository = createSqliteRepository(dbPath);
|
||||||
|
const job = await repository.createArticleJob({
|
||||||
|
source_title: "Title",
|
||||||
|
source_body: "Body",
|
||||||
|
image_inputs: [],
|
||||||
|
publish_platform: "official_site",
|
||||||
|
user_instructions: "",
|
||||||
|
});
|
||||||
|
const article = await repository.saveOptimizedArticle(job.id, {
|
||||||
|
title: "Optimized",
|
||||||
|
summary: "Summary",
|
||||||
|
body_markdown: "Body",
|
||||||
|
image_suggestions: [],
|
||||||
|
changed_sections: [],
|
||||||
|
requires_user_confirmation: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await repository.saveRubricVersion({
|
||||||
|
id: "rubric_geo_v1",
|
||||||
|
version: "v1",
|
||||||
|
name: "GEO rubric",
|
||||||
|
dimensions: [],
|
||||||
|
formula: "weighted_average_0_to_10",
|
||||||
|
is_active: true,
|
||||||
|
created_at: "2026-06-24T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
const scoringRun = await repository.saveScoringRun({
|
||||||
|
id: "score_1",
|
||||||
|
job_id: job.id,
|
||||||
|
revision: article.revision ?? 1,
|
||||||
|
rubric_version_id: "rubric_geo_v1",
|
||||||
|
dimension_scores: { readability: 4 },
|
||||||
|
composite_score: 8,
|
||||||
|
rationale: "Readable",
|
||||||
|
created_at: "2026-06-24T00:00:00.000Z",
|
||||||
|
});
|
||||||
|
const publication = await repository.createPublicationRecord({
|
||||||
|
job_id: job.id,
|
||||||
|
revision: article.revision ?? 1,
|
||||||
|
platform: "official_site",
|
||||||
|
url: "https://example.com/article",
|
||||||
|
published_at: "2026-06-24T12:00:00.000Z",
|
||||||
|
status: "published",
|
||||||
|
notes: "官网首发",
|
||||||
|
});
|
||||||
|
const snapshot = await repository.savePerformanceSnapshot({
|
||||||
|
id: "perf_1",
|
||||||
|
publication_id: publication.id,
|
||||||
|
source: "manual",
|
||||||
|
window_label: "T+7d",
|
||||||
|
metrics: { views: 1200 },
|
||||||
|
feedback_summary: "用户追问案例依据",
|
||||||
|
snapshot_at: "2026-07-01T12:00:00.000Z",
|
||||||
|
});
|
||||||
|
const event = await repository.saveCalibrationEvent({
|
||||||
|
id: "cal_1",
|
||||||
|
publication_id: publication.id,
|
||||||
|
scoring_run_id: scoringRun.id,
|
||||||
|
performance_snapshot_id: snapshot.id,
|
||||||
|
direction: "better_than_expected",
|
||||||
|
observations: ["表现高于预期"],
|
||||||
|
recommended_action: "继续积累样本",
|
||||||
|
created_at: "2026-07-01T12:10:00.000Z",
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(repository.listPublicationRecords(job.id)).resolves.toHaveLength(1);
|
||||||
|
await expect(repository.getLatestScoringRun(job.id, article.revision ?? 1))
|
||||||
|
.resolves.toMatchObject({ id: scoringRun.id, composite_score: 8 });
|
||||||
|
await expect(repository.listPerformanceSnapshots(publication.id)).resolves.toEqual([
|
||||||
|
expect.objectContaining({ id: snapshot.id }),
|
||||||
|
]);
|
||||||
|
expect(event.observations).toEqual(["表现高于预期"]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
PerformanceSnapshot,
|
||||||
|
PublicationRecord,
|
||||||
|
ScoringRun,
|
||||||
|
} from "../calibration/types";
|
||||||
import type {
|
import type {
|
||||||
ConfirmedFactCard,
|
ConfirmedFactCard,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
@@ -58,6 +63,41 @@ interface QaReportRow {
|
|||||||
report: string;
|
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() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -91,6 +131,21 @@ function toArticleJob(row: ArticleJobRow): ArticleJob {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toScoringRun(row: ScoringRunRow): ScoringRun {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
dimension_scores: parseJson<Record<string, number>>(row.dimension_scores),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
raw_reference: row.raw_reference ?? undefined,
|
||||||
|
metrics: parseJson<PerformanceSnapshot["metrics"]>(row.metrics),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createD1Repository(db: D1Database): AppRepository {
|
export function createD1Repository(db: D1Database): AppRepository {
|
||||||
return {
|
return {
|
||||||
async createBrandTemplate(input) {
|
async createBrandTemplate(input) {
|
||||||
@@ -299,5 +354,159 @@ export function createD1Repository(db: D1Database): AppRepository {
|
|||||||
.first<QaReportRow>();
|
.first<QaReportRow>();
|
||||||
return row ? parseJson<QaReport>(row.report) : null;
|
return row ? parseJson<QaReport>(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<ScoringRunRow>();
|
||||||
|
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<PublicationRecordRow>();
|
||||||
|
return result.results;
|
||||||
|
},
|
||||||
|
async getPublicationRecord(id) {
|
||||||
|
return db
|
||||||
|
.prepare("select * from publication_records where id = ?")
|
||||||
|
.bind(id)
|
||||||
|
.first<PublicationRecordRow>();
|
||||||
|
},
|
||||||
|
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<PerformanceSnapshotRow>();
|
||||||
|
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;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CalibrationEvent,
|
||||||
|
PerformanceSnapshot,
|
||||||
|
PublicationRecord,
|
||||||
|
RubricVersion,
|
||||||
|
ScoringRun,
|
||||||
|
} from "../calibration/types";
|
||||||
import type {
|
import type {
|
||||||
ConfirmedFactCard,
|
ConfirmedFactCard,
|
||||||
ImageInput,
|
ImageInput,
|
||||||
@@ -102,6 +109,41 @@ interface QaReportRow {
|
|||||||
report: string;
|
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() {
|
function nowIso() {
|
||||||
return new Date().toISOString();
|
return new Date().toISOString();
|
||||||
}
|
}
|
||||||
@@ -145,6 +187,21 @@ function toArticleJob(row: ArticleJobRow): ArticleJob {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toScoringRun(row: ScoringRunRow): ScoringRun {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
dimension_scores: parseJson<Record<string, number>>(row.dimension_scores),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function toPerformanceSnapshot(row: PerformanceSnapshotRow): PerformanceSnapshot {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
raw_reference: row.raw_reference ?? undefined,
|
||||||
|
metrics: parseJson<PerformanceSnapshot["metrics"]>(row.metrics),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function createBrandTemplate(
|
export function createBrandTemplate(
|
||||||
dbPath: string | undefined,
|
dbPath: string | undefined,
|
||||||
input: NewBrandTemplate,
|
input: NewBrandTemplate,
|
||||||
@@ -380,3 +437,183 @@ export function getLatestQaReport(dbPath: string | undefined, jobId: string) {
|
|||||||
return row ? parseJson<QaReport>(row.report) : null;
|
return row ? parseJson<QaReport>(row.report) : 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, job_id, revision, rubric_version_id, dimension_scores,
|
||||||
|
composite_score, rationale, created_at
|
||||||
|
) values (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
).run(
|
||||||
|
run.id,
|
||||||
|
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 createPublicationRecord(
|
||||||
|
dbPath: string | undefined,
|
||||||
|
input: Omit<PublicationRecord, "id" | "created_at" | "updated_at">,
|
||||||
|
) {
|
||||||
|
return withDb(dbPath, (db) => {
|
||||||
|
const timestamp = nowIso();
|
||||||
|
const record: PublicationRecord = {
|
||||||
|
id: `pub_${nanoid(10)}`,
|
||||||
|
...input,
|
||||||
|
created_at: timestamp,
|
||||||
|
updated_at: timestamp,
|
||||||
|
};
|
||||||
|
db.prepare(
|
||||||
|
`insert into publication_records (
|
||||||
|
id, job_id, revision, platform, url, published_at, status,
|
||||||
|
notes, created_at, updated_at
|
||||||
|
) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
).run(
|
||||||
|
record.id,
|
||||||
|
record.job_id,
|
||||||
|
record.revision,
|
||||||
|
record.platform,
|
||||||
|
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) => 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 ?? 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,10 @@
|
|||||||
|
import type {
|
||||||
|
CalibrationEvent,
|
||||||
|
PerformanceSnapshot,
|
||||||
|
PublicationRecord,
|
||||||
|
RubricVersion,
|
||||||
|
ScoringRun,
|
||||||
|
} from "../calibration/types";
|
||||||
import type { ConfirmedFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
import type { ConfirmedFactCard, OptimizedArticle, QaReport } from "../domain/types";
|
||||||
import type {
|
import type {
|
||||||
ArticleJob,
|
ArticleJob,
|
||||||
@@ -28,6 +35,17 @@ export interface AppRepository {
|
|||||||
getLatestOptimizedArticle(jobId: string): Promise<OptimizedArticle | null>;
|
getLatestOptimizedArticle(jobId: string): Promise<OptimizedArticle | null>;
|
||||||
saveQaReport(jobId: string, revision: number, report: QaReport): Promise<QaReport>;
|
saveQaReport(jobId: string, revision: number, report: QaReport): Promise<QaReport>;
|
||||||
getLatestQaReport(jobId: string): Promise<QaReport | null>;
|
getLatestQaReport(jobId: string): Promise<QaReport | null>;
|
||||||
|
saveRubricVersion(rubric: RubricVersion): Promise<RubricVersion>;
|
||||||
|
saveScoringRun(run: ScoringRun): Promise<ScoringRun>;
|
||||||
|
getLatestScoringRun(jobId: string, revision: number): Promise<ScoringRun | null>;
|
||||||
|
createPublicationRecord(
|
||||||
|
input: Omit<PublicationRecord, "id" | "created_at" | "updated_at">,
|
||||||
|
): Promise<PublicationRecord>;
|
||||||
|
listPublicationRecords(jobId: string): Promise<PublicationRecord[]>;
|
||||||
|
getPublicationRecord(id: string): Promise<PublicationRecord | null>;
|
||||||
|
savePerformanceSnapshot(snapshot: PerformanceSnapshot): Promise<PerformanceSnapshot>;
|
||||||
|
listPerformanceSnapshots(publicationId: string): Promise<PerformanceSnapshot[]>;
|
||||||
|
saveCalibrationEvent(event: CalibrationEvent): Promise<CalibrationEvent>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RuntimeRepositoryOptions {
|
interface RuntimeRepositoryOptions {
|
||||||
|
|||||||
@@ -3,15 +3,24 @@ import type { AppRepository } from "./repository";
|
|||||||
import {
|
import {
|
||||||
createArticleJob,
|
createArticleJob,
|
||||||
createBrandTemplate,
|
createBrandTemplate,
|
||||||
|
createPublicationRecord,
|
||||||
getArticleJob,
|
getArticleJob,
|
||||||
getBrandTemplate,
|
getBrandTemplate,
|
||||||
getFactCard,
|
getFactCard,
|
||||||
getLatestOptimizedArticle,
|
getLatestOptimizedArticle,
|
||||||
getLatestQaReport,
|
getLatestQaReport,
|
||||||
|
getLatestScoringRun,
|
||||||
|
getPublicationRecord,
|
||||||
listBrandTemplates,
|
listBrandTemplates,
|
||||||
|
listPerformanceSnapshots,
|
||||||
|
listPublicationRecords,
|
||||||
|
saveCalibrationEvent,
|
||||||
saveFactCard,
|
saveFactCard,
|
||||||
saveOptimizedArticle,
|
saveOptimizedArticle,
|
||||||
|
savePerformanceSnapshot,
|
||||||
saveQaReport,
|
saveQaReport,
|
||||||
|
saveRubricVersion,
|
||||||
|
saveScoringRun,
|
||||||
updateArticleJob,
|
updateArticleJob,
|
||||||
type ArticleJob,
|
type ArticleJob,
|
||||||
type NewArticleJob,
|
type NewArticleJob,
|
||||||
@@ -59,5 +68,32 @@ export function createSqliteRepository(dbPath?: string): AppRepository {
|
|||||||
getLatestQaReport(jobId: string) {
|
getLatestQaReport(jobId: string) {
|
||||||
return Promise.resolve(getLatestQaReport(dbPath, jobId));
|
return Promise.resolve(getLatestQaReport(dbPath, jobId));
|
||||||
},
|
},
|
||||||
|
saveRubricVersion(rubric) {
|
||||||
|
return Promise.resolve(saveRubricVersion(dbPath, rubric));
|
||||||
|
},
|
||||||
|
saveScoringRun(run) {
|
||||||
|
return Promise.resolve(saveScoringRun(dbPath, run));
|
||||||
|
},
|
||||||
|
getLatestScoringRun(jobId, revision) {
|
||||||
|
return Promise.resolve(getLatestScoringRun(dbPath, jobId, revision));
|
||||||
|
},
|
||||||
|
createPublicationRecord(input) {
|
||||||
|
return Promise.resolve(createPublicationRecord(dbPath, input));
|
||||||
|
},
|
||||||
|
listPublicationRecords(jobId) {
|
||||||
|
return Promise.resolve(listPublicationRecords(dbPath, jobId));
|
||||||
|
},
|
||||||
|
getPublicationRecord(id) {
|
||||||
|
return Promise.resolve(getPublicationRecord(dbPath, id));
|
||||||
|
},
|
||||||
|
savePerformanceSnapshot(snapshot) {
|
||||||
|
return Promise.resolve(savePerformanceSnapshot(dbPath, snapshot));
|
||||||
|
},
|
||||||
|
listPerformanceSnapshots(publicationId) {
|
||||||
|
return Promise.resolve(listPerformanceSnapshots(dbPath, publicationId));
|
||||||
|
},
|
||||||
|
saveCalibrationEvent(event) {
|
||||||
|
return Promise.resolve(saveCalibrationEvent(dbPath, event));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user