fix: preserve decision diary summary

Keep diary summary as an independent Decision Center field when body content is supplied from --body-file, and cover the CLI/backend contract.
This commit is contained in:
AgentRun Artificer
2026-06-11 17:35:35 +08:00
parent 49c3145439
commit fc3a095858
5 changed files with 195 additions and 15 deletions
@@ -102,6 +102,7 @@ interface DiaryEntryRow {
entry_date: Date | string;
month: string;
title: string;
summary: string;
body: string;
source_file: string;
markdown_path: string;
@@ -714,6 +715,7 @@ async function ensureSchema(): Promise<void> {
entry_date DATE NOT NULL,
month TEXT NOT NULL,
title TEXT NOT NULL,
summary TEXT NOT NULL DEFAULT '',
body TEXT NOT NULL DEFAULT '',
source_file TEXT NOT NULL DEFAULT '',
markdown_path TEXT NOT NULL,
@@ -727,6 +729,7 @@ async function ensureSchema(): Promise<void> {
CONSTRAINT decision_center_diary_entries_source_date_unique UNIQUE (source_file, entry_date)
)
`;
await sql`ALTER TABLE decision_center_diary_entries ADD COLUMN IF NOT EXISTS summary TEXT NOT NULL DEFAULT ''`;
await sql`CREATE INDEX IF NOT EXISTS idx_decision_center_diary_entries_month_date ON decision_center_diary_entries(month DESC, entry_date DESC)`;
await sql`CREATE INDEX IF NOT EXISTS idx_decision_center_diary_entries_updated ON decision_center_diary_entries(updated_at DESC)`;
}, { retryRead: true });
@@ -1310,7 +1313,7 @@ function diaryEntryFromRow(row: DiaryEntryRow, options: { includeBody?: boolean
date: dateOnly(row.entry_date),
month: row.month,
title: row.title,
summary: summaryFromBody(body),
summary: row.summary || summaryFromBody(body),
body: includeBody ? body : "",
sourceFile: row.source_file,
markdownPath: row.markdown_path,
@@ -1322,6 +1325,11 @@ function diaryEntryFromRow(row: DiaryEntryRow, options: { includeBody?: boolean
};
}
function normalizeDiarySummary(value: unknown, body: string): string {
const explicit = asText(value).replace(/\s+/gu, " ").trim();
return (explicit || summaryFromBody(body)).slice(0, 280);
}
function splitDiaryMarkdown(markdown: string): DiaryDraft[] {
const normalized = markdown.replace(/\r\n?/gu, "\n");
const lines = normalized.split("\n");
@@ -1410,20 +1418,22 @@ function diaryTitleFor(date: string, body: string, rawTitle: unknown): string {
async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: string[]): Promise<{ entry: DiaryEntry; action: "created" | "updated" | "unchanged" }> {
if (draft.body.length > 300_000) throw new HttpError(400, "diary day body must be at most 300000 characters", { date: draft.date, length: draft.body.length });
const hash = contentHash(draft.body);
const summary = summaryFromBody(draft.body);
const rows = await withDatabaseRecovery("upsert_diary_entry", () => sql<DiaryUpsertRow[]>`
WITH existing AS (
SELECT content_hash
SELECT content_hash, summary
FROM decision_center_diary_entries
WHERE source_file = ${sourceFile} AND entry_date = ${draft.date}::date
),
upsert AS (
INSERT INTO decision_center_diary_entries (
id, entry_date, month, title, body, source_file, markdown_path, tags, content_hash
id, entry_date, month, title, summary, body, source_file, markdown_path, tags, content_hash
) VALUES (
${`diary_${randomUUID()}`},
${draft.date}::date,
${draft.month},
${draft.title},
${summary},
${draft.body},
${sourceFile},
${draft.markdownPath},
@@ -1434,13 +1444,16 @@ async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: str
SET
month = EXCLUDED.month,
title = EXCLUDED.title,
summary = EXCLUDED.summary,
body = EXCLUDED.body,
markdown_path = EXCLUDED.markdown_path,
tags = EXCLUDED.tags,
content_hash = EXCLUDED.content_hash,
imported_at = now(),
updated_at = CASE
WHEN decision_center_diary_entries.content_hash IS DISTINCT FROM EXCLUDED.content_hash THEN now()
WHEN decision_center_diary_entries.content_hash IS DISTINCT FROM EXCLUDED.content_hash
OR decision_center_diary_entries.summary IS DISTINCT FROM EXCLUDED.summary
THEN now()
ELSE decision_center_diary_entries.updated_at
END
RETURNING *
@@ -1448,7 +1461,7 @@ async function upsertDiaryEntry(draft: DiaryDraft, sourceFile: string, tags: str
SELECT
upsert.*,
NOT EXISTS(SELECT 1 FROM existing) AS inserted,
EXISTS(SELECT 1 FROM existing WHERE existing.content_hash IS DISTINCT FROM upsert.content_hash) AS changed
EXISTS(SELECT 1 FROM existing WHERE existing.content_hash IS DISTINCT FROM upsert.content_hash OR existing.summary IS DISTINCT FROM upsert.summary) AS changed
FROM upsert
`);
const row = rows[0];
@@ -1513,6 +1526,7 @@ async function listDiaryEntries(url: URL): Promise<DiaryEntry[]> {
entry_date,
month,
title,
summary,
CASE WHEN ${includeBody}::boolean THEN body ELSE left(body, 4000) END AS body,
source_file,
markdown_path,
@@ -1569,6 +1583,7 @@ async function getDiaryEntry(key: string, options: { sourceFile?: string } = {})
async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>): Promise<JsonRecord> {
const keyDate = /^\d{4}-\d{2}-\d{2}$/u.test(key) ? validDateFilter(key, "date") : "";
const bodyProvided = "body" in input || "markdown" in input;
const summaryProvided = "summary" in input;
const titleProvided = "title" in input;
const tagsProvided = "tags" in input;
const sourceFileProvided = "sourceFile" in input || "sourcePath" in input || "source" in input;
@@ -1592,6 +1607,11 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
const date = existing === undefined ? keyDate : dateOnly(existing.entry_date);
const body = bodyProvided ? asText(input.body ?? input.markdown) : existing?.body ?? defaultDiaryBody(date);
if (body.length > 300_000) throw new HttpError(400, "diary day body must be at most 300000 characters", { date, length: body.length });
const summary = summaryProvided
? normalizeDiarySummary(input.summary, body)
: bodyProvided || existing === undefined
? summaryFromBody(body)
: existing.summary || summaryFromBody(body);
const title = titleProvided || existing === undefined ? diaryTitleFor(date, body, input.title) : existing.title;
const tags = tagsProvided ? asStringArray(input.tags, "tags") : Array.isArray(existing?.tags) ? existing.tags.map(String) : [];
const finalSourceFile = sourceFileProvided ? sourceFile : validateDiarySourceFile(existing?.source_file ?? "manual");
@@ -1602,12 +1622,13 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
const rows = existing === undefined
? await withDatabaseRecovery("create_diary_entry_by_date", () => sql<DiaryEntryRow[]>`
INSERT INTO decision_center_diary_entries (
id, entry_date, month, title, body, source_file, markdown_path, tags, content_hash, imported_at, updated_at
id, entry_date, month, title, summary, body, source_file, markdown_path, tags, content_hash, imported_at, updated_at
) VALUES (
${`diary_${randomUUID()}`},
${date}::date,
${month},
${title},
${summary},
${body},
${finalSourceFile},
${markdownPath},
@@ -1622,6 +1643,7 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
UPDATE decision_center_diary_entries
SET
title = ${title},
summary = ${summary},
body = ${body},
source_file = ${finalSourceFile},
markdown_path = ${markdownPath},
@@ -1631,6 +1653,7 @@ async function upsertDiaryEntryByKey(key: string, input: Record<string, unknown>
updated_at = CASE
WHEN content_hash IS DISTINCT FROM ${hash}
OR title IS DISTINCT FROM ${title}
OR summary IS DISTINCT FROM ${summary}
OR source_file IS DISTINCT FROM ${finalSourceFile}
OR tags IS DISTINCT FROM ${sql.json(tags)}::jsonb
THEN now()