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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user