From 8ea88b67ec391d51187354046f9390541a6fb0d7 Mon Sep 17 00:00:00 2001 From: Georg Bauer Date: Mon, 23 Mar 2026 18:09:37 +0100 Subject: [PATCH] Fix/typescript problems (#60) * fix: extended typescript checking to main and fixed all typescript errors * fix: removed unnecessary type --------- Co-authored-by: hugo --- .claude/settings.local.json | 5 +- .vscode/settings.json | 3 +- eslint.config.mjs | 192 ++- package.json | 2 +- src/main/database/connection.ts | 12 +- src/main/engine/ApplyValidationDataService.ts | 17 +- .../engine/ApplyValidationWorkerService.ts | 58 +- src/main/engine/BlogGenerationEngine.ts | 1369 ++++++++++++----- src/main/engine/BlogmarkTransformService.ts | 26 +- src/main/engine/CliNotifier.ts | 5 +- src/main/engine/DataBackedEngines.ts | 162 +- src/main/engine/EmbeddingEngine.ts | 240 ++- .../engine/GenerationPostSnapshotService.ts | 16 +- .../engine/GenerationRouteRendererFactory.ts | 33 +- .../engine/GenerationSitemapFeedService.ts | 36 +- src/main/engine/GenerationWorkerData.ts | 104 +- src/main/engine/GenerationWorkerPool.ts | 36 +- src/main/engine/GitEngine.ts | 12 +- src/main/engine/ImportAnalysisEngine.ts | 48 +- src/main/engine/ImportDefinitionEngine.ts | 20 +- src/main/engine/ImportExecutionEngine.ts | 110 +- src/main/engine/MCPAgentConfigEngine.ts | 71 +- src/main/engine/MCPServer.ts | 118 +- src/main/engine/MediaEngine.ts | 245 +-- src/main/engine/MenuEngine.ts | 4 +- src/main/engine/MetaEngine.ts | 244 +-- src/main/engine/MetadataDiffEngine.ts | 393 +++-- src/main/engine/ModelCatalogEngine.ts | 8 +- src/main/engine/NotificationWatcher.ts | 8 +- src/main/engine/PageRenderer.ts | 66 +- src/main/engine/PostEngine.ts | 326 ++-- src/main/engine/PostMediaEngine.ts | 27 +- src/main/engine/PreviewServer.ts | 123 +- src/main/engine/PublishApiAdapter.ts | 6 +- src/main/engine/PublishEngine.ts | 39 +- src/main/engine/RoutePageGenerationService.ts | 28 +- src/main/engine/ScriptEngine.ts | 24 +- src/main/engine/SharedRouteRenderer.ts | 34 +- src/main/engine/SharedSnapshotService.ts | 26 +- src/main/engine/TagEngine.ts | 119 +- src/main/engine/TemplateEngine.ts | 24 +- .../engine/ValidationApplyPlannerService.ts | 99 +- src/main/engine/WxrParser.ts | 20 +- src/main/engine/ai/a2ui-tools.ts | 22 +- src/main/engine/ai/blog-tools.ts | 142 +- src/main/engine/ai/chat.ts | 26 +- src/main/engine/ai/providers.ts | 104 +- src/main/engine/ai/tasks.ts | 32 +- src/main/engine/blogmarkPython.worker.ts | 2 +- src/main/engine/generation.worker.ts | 225 +-- .../engine/mainProcessPythonApiInvoker.ts | 65 +- src/main/engine/mcp-view-builder.ts | 18 +- src/main/engine/mcp-views.ts | 2 +- src/main/engine/pythonMacro.worker.ts | 2 +- src/main/engine/stemmer.ts | 12 +- src/main/engine/taxonomyUtils.ts | 2 +- src/main/ipc/blogHandlers.ts | 16 +- src/main/ipc/chatHandlers.ts | 54 +- src/main/ipc/embeddingHandlers.ts | 3 +- src/main/ipc/handlers.ts | 173 ++- src/main/ipc/metadataDiffHandlers.ts | 3 +- src/main/ipc/publishHandlers.ts | 9 +- src/main/main.ts | 79 +- src/main/shared/blogmark.ts | 2 +- src/main/shared/generatePythonApiModuleV1.ts | 2 +- src/main/shared/pythonApiContractV1.ts | 18 +- src/renderer/components/Toast/Toast.tsx | 3 - tests/engine/MediaEngine.test.ts | 5 +- tests/engine/PostMediaEngine.test.ts | 5 +- 69 files changed, 3690 insertions(+), 1894 deletions(-) diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 9241cf6..d99d94f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -25,7 +25,10 @@ "Bash(python3 -c \"import json,sys; [print\\(f[''''name'''']\\) for f in json.load\\(sys.stdin\\)]\")", "Bash(grep -rn choices.*auto.*mistral /Users/gb/mlx-env/lib/python3.14/site-packages/vllm_mlx/ --include=*.py)", "Bash(source ~/mlx-env/bin/activate)", - "Bash(python3 -c \":*)" + "Bash(python3 -c \":*)", + "Bash(grep -A2 -B1 '{$')", + "Bash(grep -v '^\\\\+.*{$')", + "Bash(npm run:*)" ] } } diff --git a/.vscode/settings.json b/.vscode/settings.json index 75d27ca..0bef564 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,6 +10,7 @@ "git commit": true, "git push": true, "uniq": true, - "diff": true + "diff": true, + "npx eslint": true } } diff --git a/eslint.config.mjs b/eslint.config.mjs index ea12140..7b76c2e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,22 +1,17 @@ -import tsParser from '@typescript-eslint/parser'; -import tsEslintPlugin from '@typescript-eslint/eslint-plugin'; -import i18next from 'eslint-plugin-i18next'; +import tsParser from "@typescript-eslint/parser"; +import tsEslintPlugin from "@typescript-eslint/eslint-plugin"; +import i18next from "eslint-plugin-i18next"; export default [ { - ignores: [ - 'dist/**', - 'coverage/**', - 'drizzle/**', - 'node_modules/**', - ], + ignores: ["dist/**", "coverage/**", "drizzle/**", "node_modules/**"], }, { - files: ['src/renderer/**/*.{ts,tsx}'], + files: ["src/renderer/**/*.{ts,tsx}"], languageOptions: { parser: tsParser, - ecmaVersion: 'latest', - sourceType: 'module', + ecmaVersion: "latest", + sourceType: "module", parserOptions: { ecmaFeatures: { jsx: true, @@ -24,77 +19,118 @@ export default [ }, }, plugins: { - '@typescript-eslint': tsEslintPlugin, + "@typescript-eslint": tsEslintPlugin, i18next, }, rules: { - 'i18next/no-literal-string': ['error', { - mode: 'jsx-text-only', - 'jsx-components': { - exclude: ['Trans'], + "i18next/no-literal-string": [ + "error", + { + mode: "jsx-text-only", + "jsx-components": { + exclude: ["Trans"], + }, + "jsx-attributes": { + exclude: [ + "className", + "styleName", + "style", + "type", + "key", + "id", + "width", + "height", + "viewBox", + "d", + "fill", + "stroke", + "xmlns", + "data-testid", + "role", + "tabIndex", + "aria-hidden", + "mode", + "theme", + "lineNumbers", + "cursorStyle", + "cursorBlinking", + "value", + ], + }, + callees: { + exclude: [ + "i18n(ext)?", + "t", + "tr", + "require", + "addEventListener", + "removeEventListener", + "postMessage", + "getElementById", + "dispatch", + "commit", + "includes", + "indexOf", + "endsWhen", + "startsWith", + ], + }, + words: { + exclude: [ + "[0-9!-/:-@[-`{-~]+", + "[A-Z_-]+", + /^\p{Emoji}+$/u, + /^[\s\p{Emoji}\uFE0F]+$/u, + /^[\s0-9%—()\-+.]+$/, + /^[\s+ו○●⊘→←↶↷―✕✖✔❝]+$/, + /^H[1-6]$/, + /^(DB\s*→\s*File|File\s*→\s*DB)$/, + /^\/posts\/$/, + "bDS", + "[✓✗▼▶◀▲]+", + ], + }, + message: "i18n literal string", }, - 'jsx-attributes': { - exclude: [ - 'className', - 'styleName', - 'style', - 'type', - 'key', - 'id', - 'width', - 'height', - 'viewBox', - 'd', - 'fill', - 'stroke', - 'xmlns', - 'data-testid', - 'role', - 'tabIndex', - 'aria-hidden', - 'mode', - 'theme', - 'lineNumbers', - 'cursorStyle', - 'cursorBlinking', - 'value', - ], + ], + }, + }, + { + files: ["src/main/**/*.{ts,js}"], + languageOptions: { + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module", + parserOptions: { + ecmaFeatures: { + jsx: false, }, - callees: { - exclude: [ - 'i18n(ext)?', - 't', - 'tr', - 'require', - 'addEventListener', - 'removeEventListener', - 'postMessage', - 'getElementById', - 'dispatch', - 'commit', - 'includes', - 'indexOf', - 'endsWith', - 'startsWith', - ], - }, - words: { - exclude: [ - '[0-9!-/:-@[-`{-~]+', - '[A-Z_-]+', - /^\p{Emoji}+$/u, - /^[\s\p{Emoji}\uFE0F]+$/u, - /^[\s0-9%—()\-+.]+$/, - /^[\s+ו○●⊘→←↶↷―✕✖✔❝]+$/, - /^H[1-6]$/, - /^(DB\s*→\s*File|File\s*→\s*DB)$/, - /^\/posts\/$/, - 'bDS', - '[✓✗▼▶◀▲]+' - ], - }, - message: 'i18n literal string', - }], + project: "./tsconfig.main.json", + tsconfigRootDir: import.meta.dirname, + }, + }, + plugins: { + "@typescript-eslint": tsEslintPlugin, + }, + rules: { + // TypeScript best practices for main process + "@typescript-eslint/no-explicit-any": "error", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-non-null-assertion": "error", + "@typescript-eslint/explicit-module-boundary-types": "error", + + // General best practices + "no-console": ["error", { allow: ["warn", "error"] }], + eqeqeq: "error", + curly: "error", + "no-var": "error", + "prefer-const": "error", + + // Code style + quotes: ["error", "single"], + semi: ["error", "always"], + "comma-dangle": ["error", "always-multiline"], + indent: ["error", 2], }, }, ]; diff --git a/package.json b/package.json index f1071e8..4a41eba 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test:ui": "vitest --ui", "bench:python-runtime": "node ./node_modules/tsx/dist/cli.mjs scripts/python-runtime-benchmark.ts", "docs:api": "node ./node_modules/tsx/dist/cli.mjs scripts/generate-api-docs.ts", - "lint": "eslint \"src/renderer/**/*.{ts,tsx}\" --max-warnings 0", + "lint": "eslint \"src/renderer/**/*.{ts,tsx}\" \"src/main/**/*.{ts,js}\" --max-warnings 0", "lint:i18n": "eslint \"src/renderer/**/*.{ts,tsx}\" --max-warnings 0", "db:generate": "node ./node_modules/drizzle-kit/bin.cjs generate", "db:migrate": "node ./node_modules/tsx/dist/cli.mjs src/main/database/migrate.ts", diff --git a/src/main/database/connection.ts b/src/main/database/connection.ts index 180b2eb..744df54 100644 --- a/src/main/database/connection.ts +++ b/src/main/database/connection.ts @@ -132,18 +132,24 @@ export class DatabaseConnection { } async getActiveProject(): Promise<{ id: string; name: string; slug: string } | null> { - if (!this.localDb) return null; + if (!this.localDb) { + return null; + } const rows = await this.localDb .select({ id: projects.id, name: projects.name, slug: projects.slug }) .from(projects) .where(eq(projects.isActive, true)) .limit(1); - if (rows.length === 0) return null; + if (rows.length === 0) { + return null; + } return rows[0]; } async setActiveProject(projectId: string): Promise { - if (!this.localDb) return; + if (!this.localDb) { + return; + } // Deactivate all projects await this.localDb .update(projects) diff --git a/src/main/engine/ApplyValidationDataService.ts b/src/main/engine/ApplyValidationDataService.ts index d6026af..549f528 100644 --- a/src/main/engine/ApplyValidationDataService.ts +++ b/src/main/engine/ApplyValidationDataService.ts @@ -23,8 +23,12 @@ export function buildApplyValidationArchives(posts: PostData[]): { const yearMonthDays = new Map(); for (const post of posts) { - for (const category of post.categories || []) allCategories.add(category); - for (const tag of post.tags || []) allTags.add(tag); + for (const category of post.categories || []) { + allCategories.add(category); + } + for (const tag of post.tags || []) { + allTags.add(tag); + } const createdAt = resolvePostCreatedAt(post); const updatedAt = post.updatedAt; @@ -34,13 +38,16 @@ export function buildApplyValidationArchives(posts: PostData[]): { const ymKey = `${year}/${month}`; const ymdKey = `${year}/${month}/${day}`; - if (!years.has(year) || updatedAt > years.get(year)!) { + const existingYear = years.get(year); + if (!existingYear || updatedAt > existingYear) { years.set(year, updatedAt); } - if (!yearMonths.has(ymKey) || updatedAt > yearMonths.get(ymKey)!) { + const existingYearMonth = yearMonths.get(ymKey); + if (!existingYearMonth || updatedAt > existingYearMonth) { yearMonths.set(ymKey, updatedAt); } - if (!yearMonthDays.has(ymdKey) || updatedAt > yearMonthDays.get(ymdKey)!) { + const existingYearMonthDay = yearMonthDays.get(ymdKey); + if (!existingYearMonthDay || updatedAt > existingYearMonthDay) { yearMonthDays.set(ymdKey, updatedAt); } } diff --git a/src/main/engine/ApplyValidationWorkerService.ts b/src/main/engine/ApplyValidationWorkerService.ts index 9369eb9..5dfd5bc 100644 --- a/src/main/engine/ApplyValidationWorkerService.ts +++ b/src/main/engine/ApplyValidationWorkerService.ts @@ -11,7 +11,6 @@ import type { GenerationPostIndex } from './GenerationPostIndexService'; import type { TargetedValidationPlan } from './ValidationApplyPlannerService'; import type { GenerationWorkerTask, - SerializedPostData, SerializedMediaData, SerializedBlogGenerationOptions, } from './GenerationWorkerData'; @@ -26,10 +25,15 @@ export interface ApplyValidationWorkerParams { maxPostsPerPage: number; htmlDir: string; mediaItems: SerializedMediaData[]; - backlinksRecord: Record>; + backlinksRecord: Record< + string, + Array<{ id: string; title: string; slug: string }> + >; hashMapEntries: Array<[string, string]>; postFilePathEntries: Array<[string, string]>; - postMediaLinksEntries: Array<[string, Array<{ mediaId: string; sortOrder: number }>]>; + postMediaLinksEntries: Array< + [string, Array<{ mediaId: string; sortOrder: number }>] + >; } export interface ApplyValidationLanguageParams { @@ -48,7 +52,12 @@ export function buildApplyValidationWorkerTasks( base: ApplyValidationWorkerParams, lang: ApplyValidationLanguageParams, ): GenerationWorkerTask[] { - const { targetedPlan, publishedRoutePosts, publishedListPosts, generationPostIndex } = lang; + const { + targetedPlan, + publishedRoutePosts, + publishedListPosts, + generationPostIndex, + } = lang; const serializedRoutePosts = publishedRoutePosts.map(serializePostData); const serializedListPosts = publishedListPosts.map(serializePostData); @@ -71,8 +80,11 @@ export function buildApplyValidationWorkerTasks( const tasks: GenerationWorkerTask[] = []; let taskCounter = 0; - const langSuffix = lang.languagePrefix ? `-${lang.languagePrefix.replace(/^\//, '')}` : ''; - const nextTaskId = (section: string) => `apply-${section}${langSuffix}-${++taskCounter}`; + const langSuffix = lang.languagePrefix + ? `-${lang.languagePrefix.replace(/^\//, '')}` + : ''; + const nextTaskId = (section: string) => + `apply-${section}${langSuffix}-${++taskCounter}`; // Core (root + page routes) if (targetedPlan.requestRootRoutes) { @@ -146,48 +158,62 @@ export function buildApplyValidationWorkerTasks( } // Date archives - const { requestedYears, requestedYearMonths, requestedYearMonthDays } = targetedPlan; - const hasDateRequests = requestedYears.size > 0 - || requestedYearMonths.size > 0 - || requestedYearMonthDays.size > 0; + const { requestedYears, requestedYearMonths, requestedYearMonthDays } = + targetedPlan; + const hasDateRequests = + requestedYears.size > 0 || + requestedYearMonths.size > 0 || + requestedYearMonthDays.size > 0; if (hasDateRequests) { // Filter archive maps to only the requested keys const filteredYears = new Map(); for (const year of requestedYears) { const lastmod = lang.years.get(year); - if (lastmod) filteredYears.set(year, lastmod); + if (lastmod) { + filteredYears.set(year, lastmod); + } } const filteredYearMonths = new Map(); for (const ym of requestedYearMonths) { const lastmod = lang.yearMonths.get(ym); - if (lastmod) filteredYearMonths.set(ym, lastmod); + if (lastmod) { + filteredYearMonths.set(ym, lastmod); + } } const filteredYearMonthDays = new Map(); for (const ymd of requestedYearMonthDays) { const lastmod = lang.yearMonthDays.get(ymd); - if (lastmod) filteredYearMonthDays.set(ymd, lastmod); + if (lastmod) { + filteredYearMonthDays.set(ymd, lastmod); + } } // Filter post-index entries to only the requested date keys const filteredPostsByYear = new Map(); for (const year of requestedYears) { const posts = generationPostIndex.postsByYear.get(year); - if (posts) filteredPostsByYear.set(year, posts); + if (posts) { + filteredPostsByYear.set(year, posts); + } } const filteredPostsByYearMonth = new Map(); for (const ym of requestedYearMonths) { const posts = generationPostIndex.postsByYearMonth.get(ym); - if (posts) filteredPostsByYearMonth.set(ym, posts); + if (posts) { + filteredPostsByYearMonth.set(ym, posts); + } } const filteredPostsByYearMonthDay = new Map(); for (const ymd of requestedYearMonthDays) { const posts = generationPostIndex.postsByYearMonthDay.get(ymd); - if (posts) filteredPostsByYearMonthDay.set(ymd, posts); + if (posts) { + filteredPostsByYearMonthDay.set(ymd, posts); + } } tasks.push({ diff --git a/src/main/engine/BlogGenerationEngine.ts b/src/main/engine/BlogGenerationEngine.ts index b72dd61..dee67e1 100644 --- a/src/main/engine/BlogGenerationEngine.ts +++ b/src/main/engine/BlogGenerationEngine.ts @@ -1,26 +1,28 @@ import * as path from 'path'; import * as fs from 'fs/promises'; import type { PostData, PostTranslationData } from './PostEngine'; -import type { MediaEngine, MediaData } from './MediaEngine'; +import type { MediaEngine } from './MediaEngine'; import type { PostMediaEngine } from './PostMediaEngine'; import { - PageRenderer, - buildTemplateMenuItems, buildCanonicalPostPath, type CategoryRenderSettings, - type HtmlRewriteContext, - type TemplateMenuItem, } from './PageRenderer'; -import { getPicoStylesheetHref, sanitizePicoTheme, type PicoThemeName } from '../shared/picoThemes'; +import { type PicoThemeName } from '../shared/picoThemes'; import type { MenuDocument } from './MenuEngine'; -import type { ProjectMetadata } from './MetaEngine'; import { loadPublishedGenerationSets } from './GenerationPostSnapshotService'; -import { buildCalendarArchiveData, buildSitemapAndFeeds, collectSitemapArchiveMetadata, buildMultiLanguageSitemap } from './GenerationSitemapFeedService'; -import { buildTargetedValidationPlan, planMissingValidationPaths } from './ValidationApplyPlannerService'; +import { + buildCalendarArchiveData, + buildSitemapAndFeeds, + collectSitemapArchiveMetadata, + buildMultiLanguageSitemap, +} from './GenerationSitemapFeedService'; +import { + buildTargetedValidationPlan, + planMissingValidationPaths, +} from './ValidationApplyPlannerService'; import { compareSitemapToHtml } from './SiteValidationDiffService'; import { copyPreviewAssets, - normalizeGeneratedUrlPath, urlPathToHtmlIndexPath, writeFileIfHashChanged, writeHtmlPage, @@ -46,7 +48,10 @@ import { } from './ApplyValidationDataService'; import { buildApplyValidationWorkerTasks } from './ApplyValidationWorkerService'; import { getGeneratedFileHashRecord } from '../database/generatedFileHashStore'; -import { getAllGeneratedFileHashes, setGeneratedFileHash } from '../database/generatedFileHashStore'; +import { + getAllGeneratedFileHashes, + setGeneratedFileHash, +} from '../database/generatedFileHashStore'; import { GenerationWorkerPool } from './GenerationWorkerPool'; import { serializePostData, @@ -92,7 +97,12 @@ export interface CategoryMetadata extends CategoryRenderSettings { title: string; } -export type BlogGenerationSection = 'core' | 'single' | 'category' | 'tag' | 'date'; +export type BlogGenerationSection = + | 'core' + | 'single' + | 'category' + | 'tag' + | 'date'; export interface BlogGenerationResult { path: string; @@ -187,8 +197,12 @@ function clampMaxPostsPerPage(value: unknown): number { } const normalized = Math.floor(value); - if (normalized < MIN_MAX_POSTS_PER_PAGE) return DEFAULT_MAX_POSTS_PER_PAGE; - if (normalized > MAX_MAX_POSTS_PER_PAGE) return MAX_MAX_POSTS_PER_PAGE; + if (normalized < MIN_MAX_POSTS_PER_PAGE) { + return DEFAULT_MAX_POSTS_PER_PAGE; + } + if (normalized > MAX_MAX_POSTS_PER_PAGE) { + return MAX_MAX_POSTS_PER_PAGE; + } return normalized; } @@ -226,15 +240,6 @@ function resolveCategorySettings( return merged; } -function resolveCategoryDisplayTitle( - category: string, - categoryMetadata: Record | undefined, -): string { - const title = categoryMetadata?.[category]?.title; - const trimmed = typeof title === 'string' ? title.trim() : ''; - return trimmed.length > 0 ? trimmed : category; -} - function resolvePostCreatedAt(post: { createdAt: Date | string }): Date { if (post.createdAt instanceof Date) { return post.createdAt; @@ -244,21 +249,30 @@ function resolvePostCreatedAt(post: { createdAt: Date | string }): Date { return Number.isNaN(parsed.getTime()) ? new Date() : parsed; } -type PublishedTranslationVariant = PostData & { +export type PublishedTranslationVariant = PostData & { translationSourceSlug: string; translationCanonicalLanguage?: string; translationFilePath: string; }; -interface BlogGenerationPostEngineContract { - getPostsFiltered: (filter: { status?: 'draft' | 'published' | 'archived'; excludeCategories?: string[] }) => Promise; +export interface BlogGenerationPostEngineContract { + getPostsFiltered: (filter: { + status?: 'draft' | 'published' | 'archived'; + excludeCategories?: string[]; + }) => Promise; getPublishedVersion: (id: string) => Promise; getPost: (postId: string) => Promise; hasPublishedVersion: (postId: string) => Promise; - getLinkedBy?: (postId: string) => Promise<{ id: string; title: string; slug: string }[]>; - getAllBacklinks?: () => Promise>; + getLinkedBy?: ( + postId: string, + ) => Promise<{ id: string; title: string; slug: string }[]>; + getAllBacklinks?: () => Promise< + Map + >; getPostTranslations?: (postId: string) => Promise; - getPublishedTranslationsForRoutePosts?: (publishedPosts: PostData[]) => Promise>; + getPublishedTranslationsForRoutePosts?: ( + publishedPosts: PostData[], + ) => Promise>; getPublishedPostFilePaths?: () => Promise>; setProjectContext: (projectId: string, dataDir?: string) => void; } @@ -268,19 +282,36 @@ export class BlogGenerationEngine { private readonly mediaEngine: MediaEngine; private readonly postMediaEngine: PostMediaEngine; - constructor(postEngine: BlogGenerationPostEngineContract, mediaEngine: MediaEngine, postMediaEngine: PostMediaEngine) { + constructor( + postEngine: BlogGenerationPostEngineContract, + mediaEngine: MediaEngine, + postMediaEngine: PostMediaEngine, + ) { this.postEngine = postEngine; this.mediaEngine = mediaEngine; this.postMediaEngine = postMediaEngine; } - private buildPublishedTranslationVariant(sourcePost: PostData, translation: PostTranslationData): PublishedTranslationVariant { - const canonicalLanguage = typeof sourcePost.language === 'string' ? sourcePost.language.trim() : ''; - const variantLanguages = Array.from(new Set([ - canonicalLanguage, - ...(Array.isArray(sourcePost.availableLanguages) ? sourcePost.availableLanguages : []), - translation.language, - ].filter((language) => typeof language === 'string' && language.trim().length > 0))); + private buildPublishedTranslationVariant( + sourcePost: PostData, + translation: PostTranslationData, + ): PublishedTranslationVariant { + const canonicalLanguage = + typeof sourcePost.language === 'string' ? sourcePost.language.trim() : ''; + const variantLanguages = Array.from( + new Set( + [ + canonicalLanguage, + ...(Array.isArray(sourcePost.availableLanguages) + ? sourcePost.availableLanguages + : []), + translation.language, + ].filter( + (language) => + typeof language === 'string' && language.trim().length > 0, + ), + ), + ); return { ...sourcePost, @@ -301,7 +332,9 @@ export class BlogGenerationEngine { private async resolvePostContents(postList: PostData[]): Promise { const postsNeedingContent = postList.filter((p) => !p.content); - if (postsNeedingContent.length === 0) return; + if (postsNeedingContent.length === 0) { + return; + } const BATCH_SIZE = 100; for (let i = 0; i < postsNeedingContent.length; i += BATCH_SIZE) { @@ -323,20 +356,32 @@ export class BlogGenerationEngine { * For resolved posts with translationFilePath, reads from the translation file. * For canonical posts, falls back to getPublishedVersion. */ - private async resolveTranslatedPostContents(postList: PostData[]): Promise { + private async resolveTranslatedPostContents( + postList: PostData[], + ): Promise { const postsNeedingContent = postList.filter((p) => !p.content); - if (postsNeedingContent.length === 0) return; + if (postsNeedingContent.length === 0) { + return; + } - await Promise.all(postsNeedingContent.map(async (post) => { - const variant = post as PostData & { translationFilePath?: string }; - if (variant.translationFilePath) { - const fileData = await readPostTranslationFile(variant.translationFilePath); - if (fileData) post.content = fileData.content; - } else { - const full = await this.postEngine.getPublishedVersion(post.id); - if (full) post.content = full.content; - } - })); + await Promise.all( + postsNeedingContent.map(async (post) => { + const variant = post as PostData & { translationFilePath?: string }; + if (variant.translationFilePath) { + const fileData = await readPostTranslationFile( + variant.translationFilePath, + ); + if (fileData) { + post.content = fileData.content; + } + } else { + const full = await this.postEngine.getPublishedVersion(post.id); + if (full) { + post.content = full.content; + } + } + }), + ); } /** @@ -351,27 +396,36 @@ export class BlogGenerationEngine { translationsByPost: Map, mainLanguage: string, ): PostData[] { - if (translationsByPost.size === 0) return posts; + if (translationsByPost.size === 0) { + return posts; + } const target = targetLanguage.trim().toLowerCase(); const main = mainLanguage.trim().toLowerCase(); return posts.map((post) => { // Skip translation variant posts — they're already in their language - if ((post as any).translationSourceSlug) return post; + if ((post as PublishedTranslationVariant).translationSourceSlug) + {return post;} const postLang = (post.language || '').trim().toLowerCase(); // A post with no explicit language is assumed to be in the project main language const effectivePostLang = postLang || main; - if (effectivePostLang === target) return post; + if (effectivePostLang === target) { + return post; + } const translations = translationsByPost.get(post.id); - if (!translations) return post; + if (!translations) { + return post; + } - const targetTranslation = translations.find((t) => - t.language.trim().toLowerCase() === target, + const targetTranslation = translations.find( + (t) => t.language.trim().toLowerCase() === target, ); - if (!targetTranslation) return post; + if (!targetTranslation) { + return post; + } const resolved: PostData = { ...post, @@ -380,9 +434,11 @@ export class BlogGenerationEngine { content: '', language: targetTranslation.language, }; - (resolved as any).translationFilePath = targetTranslation.filePath; + (resolved as PublishedTranslationVariant).translationFilePath = + targetTranslation.filePath; // Mark as already-resolved so resolveRenderablePost skips hydration - (resolved as any).translationSourceSlug = post.slug; + (resolved as PublishedTranslationVariant).translationSourceSlug = + post.slug; return resolved; }); } @@ -394,26 +450,38 @@ export class BlogGenerationEngine { const routePosts: PostData[] = [...publishedPosts]; const translationsByPost = new Map(); - if (typeof this.postEngine.getPublishedTranslationsForRoutePosts === 'function') { - const translationsMap = await this.postEngine.getPublishedTranslationsForRoutePosts(publishedPosts); + if ( + typeof this.postEngine.getPublishedTranslationsForRoutePosts === + 'function' + ) { + const translationsMap = + await this.postEngine.getPublishedTranslationsForRoutePosts( + publishedPosts, + ); for (const post of publishedPosts) { const translations = translationsMap.get(post.id) || []; if (translations.length > 0) { translationsByPost.set(post.id, translations); } for (const translation of translations) { - routePosts.push(this.buildPublishedTranslationVariant(post, translation)); + routePosts.push( + this.buildPublishedTranslationVariant(post, translation), + ); } } } else if (typeof this.postEngine.getPostTranslations === 'function') { for (const post of publishedPosts) { const translations = await this.postEngine.getPostTranslations(post.id); - const publishedTranslations = translations.filter((t) => t.status === 'published'); + const publishedTranslations = translations.filter( + (t) => t.status === 'published', + ); if (publishedTranslations.length > 0) { translationsByPost.set(post.id, publishedTranslations); } for (const translation of publishedTranslations) { - routePosts.push(this.buildPublishedTranslationVariant(post, translation)); + routePosts.push( + this.buildPublishedTranslationVariant(post, translation), + ); } } } @@ -421,19 +489,32 @@ export class BlogGenerationEngine { return { routePosts, translationsByPost }; } - async preloadGenerationData(options: BlogGenerationOptions): Promise { - const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + async preloadGenerationData( + options: BlogGenerationOptions, + ): Promise { + const categorySettings = resolveCategorySettings( + options.categoryMetadata, + options.categorySettings, + ); const listExcludedCategories = Object.entries(categorySettings) .filter(([, settings]) => settings.renderInLists === false) .map(([category]) => category); - const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); - const { routePosts: publishedRoutePosts } = await this.buildPublishedRoutePosts(publishedPosts); + const { publishedPosts, publishedListPosts } = + await loadPublishedGenerationSets( + this.postEngine, + listExcludedCategories, + ); + const { routePosts: publishedRoutePosts } = + await this.buildPublishedRoutePosts(publishedPosts); return { publishedPosts, publishedListPosts, publishedRoutePosts }; } - async generate(options: BlogGenerationOptions, onProgress: (progress: number, message?: string) => void): Promise { + async generate( + options: BlogGenerationOptions, + onProgress: (progress: number, message?: string) => void, + ): Promise { onProgress(0, 'Loading posts...'); const selectedSections = new Set( @@ -447,7 +528,10 @@ export class BlogGenerationEngine { const includeTag = selectedSections.has('tag'); const includeDate = selectedSections.has('date'); - const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + const categorySettings = resolveCategorySettings( + options.categoryMetadata, + options.categorySettings, + ); const listExcludedCategories = Object.entries(categorySettings) .filter(([, settings]) => settings.renderInLists === false) .map(([category]) => category); @@ -460,13 +544,24 @@ export class BlogGenerationEngine { let translationsByPost = new Map(); if (options.preloadedData) { - ({ publishedPosts, publishedListPosts, publishedRoutePosts } = options.preloadedData); + ({ publishedPosts, publishedListPosts, publishedRoutePosts } = + options.preloadedData); // Load translations for language resolution (rendering stays in workers) - if (typeof this.postEngine.getPublishedTranslationsForRoutePosts === 'function') { - translationsByPost = await this.postEngine.getPublishedTranslationsForRoutePosts(publishedPosts); + if ( + typeof this.postEngine.getPublishedTranslationsForRoutePosts === + 'function' + ) { + translationsByPost = + await this.postEngine.getPublishedTranslationsForRoutePosts( + publishedPosts, + ); } } else { - ({ publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories)); + ({ publishedPosts, publishedListPosts } = + await loadPublishedGenerationSets( + this.postEngine, + listExcludedCategories, + )); const built = await this.buildPublishedRoutePosts(publishedPosts); publishedRoutePosts = built.routePosts; translationsByPost = built.translationsByPost; @@ -565,7 +660,8 @@ export class BlogGenerationEngine { return; } completedUnits += 1; - const progress = 10 + Math.floor((completedUnits / totalEstimatedUnits) * 85); + const progress = + 10 + Math.floor((completedUnits / totalEstimatedUnits) * 85); onProgress(Math.min(95, progress), message); }; @@ -634,7 +730,9 @@ export class BlogGenerationEngine { if (additionalLanguages.length > 0) { const subtreeLanguages = new Set(additionalLanguages); publishedRoutePosts = publishedRoutePosts.filter( - (p) => !(p as any).translationSourceSlug || !subtreeLanguages.has((p.language ?? '').trim().toLowerCase()), + (p) => + !(p as PublishedTranslationVariant).translationSourceSlug || + !subtreeLanguages.has((p.language ?? '').trim().toLowerCase()), ); } @@ -701,10 +799,15 @@ export class BlogGenerationEngine { // --- Combined sitemap with hreflang (if multiple languages) --- if (includeCore && additionalLanguages.length > 0) { const allLanguages = [mainLanguage, ...additionalLanguages]; - const langFilteredPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); + const langFilteredPosts = publishedPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); const doNotTranslateIds = new Set( publishedPosts - .filter((p) => (p as PostData & { doNotTranslate?: boolean }).doNotTranslate) + .filter( + (p) => + (p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ) .map((p) => p.id), ); @@ -713,7 +816,9 @@ export class BlogGenerationEngine { mainLanguage, allLanguages, translatablePosts: langFilteredPosts, - doNotTranslatePosts: publishedPosts.filter((p) => doNotTranslateIds.has(p.id)), + doNotTranslatePosts: publishedPosts.filter((p) => + doNotTranslateIds.has(p.id), + ), publishedListPosts, maxPostsPerPage, postIndex: generationPostIndex, @@ -727,7 +832,10 @@ export class BlogGenerationEngine { }); } - onProgress(100, `Site generated (${publishedPosts.length} posts, ${pagesGenerated} pages)`); + onProgress( + 100, + `Site generated (${publishedPosts.length} posts, ${pagesGenerated} pages)`, + ); return { path: sitemapPath, @@ -778,13 +886,28 @@ export class BlogGenerationEngine { reportUnitProgress: (message: string) => void; }): Promise { const { - options, maxPostsPerPage, htmlDir, - publishedPosts, publishedListPosts, publishedRoutePosts, - generationPostIndex, allCategories, allTags, years, yearMonths, yearMonthDays, + options, + maxPostsPerPage, + htmlDir, + publishedPosts, + publishedListPosts, + publishedRoutePosts, + allCategories, + allTags, + years, + yearMonths, + yearMonthDays, generatedHashCache, - mainLanguage, additionalLanguages, translationsByPost, - includeCore, includeSingle, includeCategory, includeTag, includeDate, - onProgress, reportUnitProgress, + mainLanguage, + additionalLanguages, + translationsByPost, + includeCore, + includeSingle, + includeCategory, + includeTag, + includeDate, + onProgress, + reportUnitProgress, } = params; // Pre-load media data for worker serialization @@ -792,7 +915,10 @@ export class BlogGenerationEngine { const mediaItems = rawMedia.map(serializeMediaItem); // Pre-load backlinks - let backlinksRecord: Record> = {}; + const backlinksRecord: Record< + string, + Array<{ id: string; title: string; slug: string }> + > = {}; if (typeof this.postEngine.getAllBacklinks === 'function') { const blMap = await this.postEngine.getAllBacklinks(); for (const [postId, links] of blMap) { @@ -810,7 +936,9 @@ export class BlogGenerationEngine { } // Pre-load post-media links for worker-side gallery/album macros - let postMediaLinksEntries: Array<[string, Array<{ mediaId: string; sortOrder: number }>]> = []; + let postMediaLinksEntries: Array< + [string, Array<{ mediaId: string; sortOrder: number }>] + > = []; if (typeof this.postMediaEngine.getAllPostMediaLinks === 'function') { const linksMap = await this.postMediaEngine.getAllPostMediaLinks(); postMediaLinksEntries = Array.from(linksMap); @@ -825,8 +953,18 @@ export class BlogGenerationEngine { } // Resolve posts to project main language before serialization - const mainLangRoutePosts = this.resolvePostsForLanguage(publishedRoutePosts, mainLanguage, translationsByPost, mainLanguage); - const mainLangListPosts = this.resolvePostsForLanguage(publishedListPosts, mainLanguage, translationsByPost, mainLanguage); + const mainLangRoutePosts = this.resolvePostsForLanguage( + publishedRoutePosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); + const mainLangListPosts = this.resolvePostsForLanguage( + publishedListPosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); const mainLangPostIndex = buildGenerationPostIndex(mainLangListPosts); const serializedRoutePosts = mainLangRoutePosts.map(serializePostData); const serializedListPosts = mainLangListPosts.map(serializePostData); @@ -862,10 +1000,13 @@ export class BlogGenerationEngine { if (includeSingle) { // Split single posts across multiple workers - const workerCount = Math.max(1, Math.min( - require('os').cpus().length - 1, - Math.ceil(serializedRoutePosts.length / 100), - )); + const workerCount = Math.max( + 1, + Math.min( + require('os').cpus().length - 1, + Math.ceil(serializedRoutePosts.length / 100), + ), + ); const chunkSize = Math.ceil(serializedRoutePosts.length / workerCount); for (let i = 0; i < serializedRoutePosts.length; i += chunkSize) { const chunk = serializedRoutePosts.slice(i, i + chunkSize); @@ -885,7 +1026,9 @@ export class BlogGenerationEngine { section: 'category', posts: serializedListPosts, allCategories: Array.from(allCategories), - postsByCategoryEntries: serializePostMap(mainLangPostIndex.postsByCategory), + postsByCategoryEntries: serializePostMap( + mainLangPostIndex.postsByCategory, + ), }); } @@ -910,8 +1053,12 @@ export class BlogGenerationEngine { yearMonthsEntries: serializeDateMap(yearMonths), yearMonthDaysEntries: serializeDateMap(yearMonthDays), postsByYearEntries: serializePostMap(mainLangPostIndex.postsByYear), - postsByYearMonthEntries: serializePostMap(mainLangPostIndex.postsByYearMonth), - postsByYearMonthDayEntries: serializePostMap(mainLangPostIndex.postsByYearMonthDay), + postsByYearMonthEntries: serializePostMap( + mainLangPostIndex.postsByYearMonth, + ), + postsByYearMonthDayEntries: serializePostMap( + mainLangPostIndex.postsByYearMonthDay, + ), }); } @@ -929,9 +1076,21 @@ export class BlogGenerationEngine { publishedListPosts: langListPosts, }); - const resolvedLangPosts = this.resolvePostsForLanguage(langPosts, lang, translationsByPost, mainLanguage); - const resolvedLangListPosts = this.resolvePostsForLanguage(langListPosts, lang, translationsByPost, mainLanguage); - const resolvedLangPostIndex = buildGenerationPostIndex(resolvedLangListPosts); + const resolvedLangPosts = this.resolvePostsForLanguage( + langPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangListPosts = this.resolvePostsForLanguage( + langListPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangPostIndex = buildGenerationPostIndex( + resolvedLangListPosts, + ); // Write per-language feeds in main thread (small I/O work) if (includeCore) { @@ -953,12 +1112,23 @@ export class BlogGenerationEngine { const langRssPath = path.join(htmlDir, lang, 'rss.xml'); const langAtomPath = path.join(htmlDir, lang, 'atom.xml'); await fs.mkdir(path.join(htmlDir, lang), { recursive: true }); - await writeFileIfHashChanged({ projectId: options.projectId, filePath: langRssPath, relativePath: `${lang}/rss.xml`, content: langFeedResult.rssXml }); - await writeFileIfHashChanged({ projectId: options.projectId, filePath: langAtomPath, relativePath: `${lang}/atom.xml`, content: langFeedResult.atomXml }); + await writeFileIfHashChanged({ + projectId: options.projectId, + filePath: langRssPath, + relativePath: `${lang}/rss.xml`, + content: langFeedResult.rssXml, + }); + await writeFileIfHashChanged({ + projectId: options.projectId, + filePath: langAtomPath, + relativePath: `${lang}/atom.xml`, + content: langFeedResult.atomXml, + }); } const serializedLangPosts = resolvedLangPosts.map(serializePostData); - const serializedLangListPosts = resolvedLangListPosts.map(serializePostData); + const serializedLangListPosts = + resolvedLangListPosts.map(serializePostData); const langBaseTaskData = { ...baseTaskData, @@ -978,10 +1148,13 @@ export class BlogGenerationEngine { } if (includeSingle) { - const workerCount = Math.max(1, Math.min( - require('os').cpus().length - 1, - Math.ceil(serializedLangPosts.length / 100), - )); + const workerCount = Math.max( + 1, + Math.min( + require('os').cpus().length - 1, + Math.ceil(serializedLangPosts.length / 100), + ), + ); const chunkSize = Math.ceil(serializedLangPosts.length / workerCount); for (let i = 0; i < serializedLangPosts.length; i += chunkSize) { tasks.push({ @@ -1000,7 +1173,9 @@ export class BlogGenerationEngine { section: 'category' as const, posts: serializedLangListPosts, allCategories: Array.from(langArchiveMetadata.allCategories), - postsByCategoryEntries: serializePostMap(resolvedLangPostIndex.postsByCategory), + postsByCategoryEntries: serializePostMap( + resolvedLangPostIndex.postsByCategory, + ), }); } @@ -1023,10 +1198,18 @@ export class BlogGenerationEngine { posts: serializedLangListPosts, yearsEntries: serializeDateMap(langArchiveMetadata.years), yearMonthsEntries: serializeDateMap(langArchiveMetadata.yearMonths), - yearMonthDaysEntries: serializeDateMap(langArchiveMetadata.yearMonthDays), - postsByYearEntries: serializePostMap(resolvedLangPostIndex.postsByYear), - postsByYearMonthEntries: serializePostMap(resolvedLangPostIndex.postsByYearMonth), - postsByYearMonthDayEntries: serializePostMap(resolvedLangPostIndex.postsByYearMonthDay), + yearMonthDaysEntries: serializeDateMap( + langArchiveMetadata.yearMonthDays, + ), + postsByYearEntries: serializePostMap( + resolvedLangPostIndex.postsByYear, + ), + postsByYearMonthEntries: serializePostMap( + resolvedLangPostIndex.postsByYearMonth, + ), + postsByYearMonthDayEntries: serializePostMap( + resolvedLangPostIndex.postsByYearMonthDay, + ), }); } } @@ -1039,7 +1222,9 @@ export class BlogGenerationEngine { const result = await pool.runTasks(tasks, reportUnitProgress); if (result.errors.length > 0) { - console.error(`[GenerationWorkerPool] ${result.errors.length} task(s) failed:`); + console.error( + `[GenerationWorkerPool] ${result.errors.length} task(s) failed:`, + ); for (const err of result.errors) { console.error(` [${err.taskId}] ${err.error}`); } @@ -1048,7 +1233,11 @@ export class BlogGenerationEngine { // Persist hash updates collected from workers (single DB connection, no contention) if (result.hashUpdates.length > 0) { for (const update of result.hashUpdates) { - await setGeneratedFileHash(options.projectId, update.relativePath, update.hash); + await setGeneratedFileHash( + options.projectId, + update.relativePath, + update.hash, + ); } } @@ -1084,50 +1273,101 @@ export class BlogGenerationEngine { reportUnitProgress: (message: string) => void; }): Promise { const { - options, maxPostsPerPage, htmlDir, - publishedPosts, publishedListPosts, publishedRoutePosts, - generationPostIndex, allCategories, allTags, years, yearMonths, yearMonthDays, - knownOutputDirectories, generatedHashCache, - mainLanguage, additionalLanguages, translationsByPost, - includeCore, includeSingle, includeCategory, includeTag, includeDate, - onProgress, reportUnitProgress, + options, + maxPostsPerPage, + htmlDir, + publishedPosts, + publishedListPosts, + publishedRoutePosts, + allCategories, + allTags, + years, + yearMonths, + yearMonthDays, + knownOutputDirectories, + generatedHashCache, + mainLanguage, + additionalLanguages, + translationsByPost, + includeCore, + includeSingle, + includeCategory, + includeTag, + includeDate, + onProgress, + reportUnitProgress, } = params; // Wrap post engine to resolve translations in getPostsFiltered results. // The route renderer calls getPostsFiltered internally for list pages, // so we need to ensure it returns language-resolved posts with content loaded. - const createResolvedPostEngine = (targetLang: string, filterDoNotTranslate = false) => { - if (translationsByPost.size === 0 && !filterDoNotTranslate) return this.postEngine; - return new Proxy(this.postEngine as any, { - get: (target: any, prop: string | symbol) => { + const createResolvedPostEngine = ( + targetLang: string, + filterDoNotTranslate = false, + ): BlogGenerationPostEngineContract => { + if (translationsByPost.size === 0 && !filterDoNotTranslate) { + return this.postEngine; + } + + type PostsFilter = Parameters< + BlogGenerationPostEngineContract['getPostsFiltered'] + >[0]; + + return new Proxy(this.postEngine as BlogGenerationPostEngineContract, { + get: ( + target: BlogGenerationPostEngineContract, + prop: string | symbol, + ) => { if (prop === 'getPostsFiltered') { - return async (filter: any) => { + return async (filter: PostsFilter) => { let posts: PostData[] = await target.getPostsFiltered(filter); if (filterDoNotTranslate) { posts = posts.filter((p: PostData) => !p.doNotTranslate); } - const resolved = this.resolvePostsForLanguage(posts, targetLang, translationsByPost, mainLanguage); + const resolved = this.resolvePostsForLanguage( + posts, + targetLang, + translationsByPost, + mainLanguage, + ); // Load translation content for resolved posts that need it (list pages render content) - await Promise.all(resolved.map(async (post) => { - const variant = post as PostData & { translationFilePath?: string }; - if (!post.content && variant.translationFilePath) { - const fileData = await readPostTranslationFile(variant.translationFilePath); - if (fileData) { - post.content = fileData.content; + await Promise.all( + resolved.map(async (post) => { + const variant = post as PostData & { + translationFilePath?: string; + }; + if (!post.content && variant.translationFilePath) { + const fileData = await readPostTranslationFile( + variant.translationFilePath, + ); + if (fileData) { + post.content = fileData.content; + } } - } - })); + }), + ); return resolved; }; } - const val = target[prop]; - return typeof val === 'function' ? val.bind(target) : val; + + const value = target[prop as keyof BlogGenerationPostEngineContract]; + return typeof value === 'function' ? value.bind(target) : value; }, }); }; - const mainLangRoutePosts = this.resolvePostsForLanguage(publishedRoutePosts, mainLanguage, translationsByPost, mainLanguage); - const mainLangListPosts = this.resolvePostsForLanguage(publishedListPosts, mainLanguage, translationsByPost, mainLanguage); + const mainLangRoutePosts = this.resolvePostsForLanguage( + publishedRoutePosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); + const mainLangListPosts = this.resolvePostsForLanguage( + publishedListPosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); const mainLangPostIndex = buildGenerationPostIndex(mainLangListPosts); const renderRoute = createPreviewBackedGenerationRouteRenderer({ @@ -1135,21 +1375,24 @@ export class BlogGenerationEngine { maxPostsPerPage, publishedPostsForLookup: mainLangRoutePosts, engines: { - postEngine: createResolvedPostEngine(mainLanguage) as any, + postEngine: createResolvedPostEngine( + mainLanguage, + ) as BlogGenerationPostEngineContract, mediaEngine: this.mediaEngine, postMediaEngine: this.postMediaEngine, }, }); - const writePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({ - projectId, - htmlDir, - urlPath, - content, - knownDirectories: knownOutputDirectories, - hashCache: generatedHashCache, - refreshHashTimestampOnUnchanged: true, - }); + const writePage = (projectId: string, urlPath: string, content: string) => + writeHtmlPage({ + projectId, + htmlDir, + urlPath, + content, + knownDirectories: knownOutputDirectories, + hashCache: generatedHashCache, + refreshHashTimestampOnUnchanged: true, + }); let pagesGenerated = 0; @@ -1244,9 +1487,21 @@ export class BlogGenerationEngine { publishedListPosts: langListPosts, }); - const resolvedLangPosts = this.resolvePostsForLanguage(langPosts, lang, translationsByPost, mainLanguage); - const resolvedLangListPosts = this.resolvePostsForLanguage(langListPosts, lang, translationsByPost, mainLanguage); - const resolvedLangPostIndex = buildGenerationPostIndex(resolvedLangListPosts); + const resolvedLangPosts = this.resolvePostsForLanguage( + langPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangListPosts = this.resolvePostsForLanguage( + langListPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangPostIndex = buildGenerationPostIndex( + resolvedLangListPosts, + ); if (includeCore) { const langFeedSlice = resolvedLangListPosts.slice(0, maxPostsPerPage); @@ -1267,8 +1522,18 @@ export class BlogGenerationEngine { const langRssPath = path.join(htmlDir, lang, 'rss.xml'); const langAtomPath = path.join(htmlDir, lang, 'atom.xml'); await fs.mkdir(path.join(htmlDir, lang), { recursive: true }); - await writeFileIfHashChanged({ projectId: options.projectId, filePath: langRssPath, relativePath: `${lang}/rss.xml`, content: langFeedResult.rssXml }); - await writeFileIfHashChanged({ projectId: options.projectId, filePath: langAtomPath, relativePath: `${lang}/atom.xml`, content: langFeedResult.atomXml }); + await writeFileIfHashChanged({ + projectId: options.projectId, + filePath: langRssPath, + relativePath: `${lang}/rss.xml`, + content: langFeedResult.rssXml, + }); + await writeFileIfHashChanged({ + projectId: options.projectId, + filePath: langAtomPath, + relativePath: `${lang}/atom.xml`, + content: langFeedResult.atomXml, + }); } const langRenderRoute = createPreviewBackedGenerationRouteRenderer({ @@ -1278,23 +1543,32 @@ export class BlogGenerationEngine { publishedPostsForLookup: resolvedLangPosts, languagePrefix: `/${lang}`, engines: { - postEngine: createResolvedPostEngine(lang, true) as any, + postEngine: createResolvedPostEngine( + lang, + true, + ) as BlogGenerationPostEngineContract, mediaEngine: this.mediaEngine, postMediaEngine: this.postMediaEngine, }, }); - const langWritePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({ - projectId, - htmlDir, - urlPath: `${lang}/${urlPath}`, - content, - knownDirectories: knownOutputDirectories, - hashCache: generatedHashCache, - refreshHashTimestampOnUnchanged: true, - }); + const langWritePage = ( + projectId: string, + urlPath: string, + content: string, + ) => + writeHtmlPage({ + projectId, + htmlDir, + urlPath: `${lang}/${urlPath}`, + content, + knownDirectories: knownOutputDirectories, + hashCache: generatedHashCache, + refreshHashTimestampOnUnchanged: true, + }); - const langReportProgress = (message: string) => reportUnitProgress(`[${lang}] ${message}`); + const langReportProgress = (message: string) => + reportUnitProgress(`[${lang}] ${message}`); if (includeCore) { pagesGenerated += await generateRootPages({ @@ -1377,12 +1651,18 @@ export class BlogGenerationEngine { ): Promise { onProgress(0, 'Loading posts...'); - const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + const categorySettings = resolveCategorySettings( + options.categoryMetadata, + options.categorySettings, + ); const listExcludedCategories = Object.entries(categorySettings) .filter(([, settings]) => settings.renderInLists === false) .map(([category]) => category); - const { publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); + const { publishedListPosts } = await loadPublishedGenerationSets( + this.postEngine, + listExcludedCategories, + ); onProgress(50, 'Building calendar data...'); @@ -1413,13 +1693,21 @@ export class BlogGenerationEngine { onProgress(0, 'Collecting sitemap URLs...'); const maxPostsPerPage = clampMaxPostsPerPage(options.maxPostsPerPage); - const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + const categorySettings = resolveCategorySettings( + options.categoryMetadata, + options.categorySettings, + ); const listExcludedCategories = Object.entries(categorySettings) .filter(([, settings]) => settings.renderInLists === false) .map(([category]) => category); - const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); - let { routePosts: publishedRoutePosts } = await this.buildPublishedRoutePosts(publishedPosts); + const { publishedPosts, publishedListPosts } = + await loadPublishedGenerationSets( + this.postEngine, + listExcludedCategories, + ); + let { routePosts: publishedRoutePosts } = + await this.buildPublishedRoutePosts(publishedPosts); const generationPostIndex = buildGenerationPostIndex(publishedListPosts); // --- Build per-language expected paths --- @@ -1432,7 +1720,9 @@ export class BlogGenerationEngine { if (additionalLanguages.length > 0) { const subtreeLanguages = new Set(additionalLanguages); publishedRoutePosts = publishedRoutePosts.filter( - (p) => !(p as any).translationSourceSlug || !subtreeLanguages.has((p.language ?? '').trim().toLowerCase()), + (p) => + !(p as PublishedTranslationVariant).translationSourceSlug || + !subtreeLanguages.has((p.language ?? '').trim().toLowerCase()), ); } @@ -1460,8 +1750,12 @@ export class BlogGenerationEngine { }> = []; if (additionalLanguages.length > 0) { - const langPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); - const langListPosts = publishedListPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); + const langPosts = publishedPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); + const langListPosts = publishedListPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); for (const lang of additionalLanguages) { const langPostIndex = buildGenerationPostIndex(langListPosts); @@ -1477,10 +1771,13 @@ export class BlogGenerationEngine { }); // Extract expected paths from the per-language sitemap, stripping base URL - const langLocMatches = langSitemapResult.sitemapXml.matchAll(/(.*?)<\/loc>/g); + const langLocMatches = + langSitemapResult.sitemapXml.matchAll(/(.*?)<\/loc>/g); for (const match of langLocMatches) { const loc = match[1]?.trim(); - if (!loc) continue; + if (!loc) { + continue; + } try { const locUrl = new URL(loc); const base = new URL(options.baseUrl); @@ -1500,10 +1797,19 @@ export class BlogGenerationEngine { const createdAt = resolvePostCreatedAt(post); const year = String(createdAt.getFullYear()); const month = String(createdAt.getMonth() + 1).padStart(2, '0'); - const postFilePath = path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`); + const postFilePath = path.join( + options.dataDir, + 'posts', + year, + month, + `${post.slug}.md`, + ); const postUrlPath = `/${lang}${buildCanonicalPostPath(post)}`; const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`; - const generatedRecord = await getGeneratedFileHashRecord(options.projectId, relativePath); + const generatedRecord = await getGeneratedFileHashRecord( + options.projectId, + relativePath, + ); additionalPostTimestampChecks.push({ postUrlPath, @@ -1515,10 +1821,15 @@ export class BlogGenerationEngine { // Write multi-language sitemap const allLanguages = [mainLanguage, ...additionalLanguages]; - const langFilteredPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); + const langFilteredPosts = publishedPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); const doNotTranslateIds = new Set( publishedPosts - .filter((p) => (p as PostData & { doNotTranslate?: boolean }).doNotTranslate) + .filter( + (p) => + (p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ) .map((p) => p.id), ); @@ -1527,7 +1838,9 @@ export class BlogGenerationEngine { mainLanguage, allLanguages, translatablePosts: langFilteredPosts, - doNotTranslatePosts: publishedPosts.filter((p) => doNotTranslateIds.has(p.id)), + doNotTranslatePosts: publishedPosts.filter((p) => + doNotTranslateIds.has(p.id), + ), publishedListPosts, maxPostsPerPage, postIndex: generationPostIndex, @@ -1543,34 +1856,43 @@ export class BlogGenerationEngine { onProgress(50, 'Comparing sitemap to html pages...'); - const postTimestampChecks = await Promise.all(publishedRoutePosts.map(async (post) => { - const createdAt = resolvePostCreatedAt(post); - const year = String(createdAt.getFullYear()); - const month = String(createdAt.getMonth() + 1).padStart(2, '0'); - const postFilePath = (post as PublishedTranslationVariant).translationFilePath - ?? path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`); - const postUrlPath = buildCanonicalPostPath(post); - const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`; - const generatedRecord = await getGeneratedFileHashRecord(options.projectId, relativePath); + const postTimestampChecks = await Promise.all( + publishedRoutePosts.map(async (post) => { + const createdAt = resolvePostCreatedAt(post); + const year = String(createdAt.getFullYear()); + const month = String(createdAt.getMonth() + 1).padStart(2, '0'); + const postFilePath = + (post as PublishedTranslationVariant).translationFilePath ?? + path.join(options.dataDir, 'posts', year, month, `${post.slug}.md`); + const postUrlPath = buildCanonicalPostPath(post); + const relativePath = `${postUrlPath.replace(/^\//, '')}/index.html`; + const generatedRecord = await getGeneratedFileHashRecord( + options.projectId, + relativePath, + ); - return { - postUrlPath, - postFilePath, - generatedUpdatedAtMs: generatedRecord?.updatedAt, - }; - })); + return { + postUrlPath, + postFilePath, + generatedUpdatedAtMs: generatedRecord?.updatedAt, + }; + }), + ); const diffResult = await compareSitemapToHtml({ sitemapXml, baseUrl: options.baseUrl, htmlDir, - postTimestampChecks: [...postTimestampChecks, ...additionalPostTimestampChecks], + postTimestampChecks: [ + ...postTimestampChecks, + ...additionalPostTimestampChecks, + ], additionalExpectedPaths, }); onProgress( 100, - `Validation complete (${diffResult.missingUrlPaths.length} missing, ${diffResult.extraUrlPaths.length} extra, ${diffResult.updatedPostUrlPaths.length} updated)` + `Validation complete (${diffResult.missingUrlPaths.length} missing, ${diffResult.extraUrlPaths.length} extra, ${diffResult.updatedPostUrlPaths.length} updated)`, ); return { @@ -1589,21 +1911,33 @@ export class BlogGenerationEngine { report: SiteValidationReport, onProgress: (progress: number, message?: string) => void, ): Promise { - const preparation = await this.prepareApplyValidation(options, report, (progress, message) => { - const mapped = Math.floor((progress / 100) * 45); - onProgress(mapped, message); - }); + const preparation = await this.prepareApplyValidation( + options, + report, + (progress, message) => { + const mapped = Math.floor((progress / 100) * 45); + onProgress(mapped, message); + }, + ); let renderedUrlCount = 0; const sections = preparation.sectionsToRender; for (let i = 0; i < sections.length; i++) { const section = sections[i]; - const sectionCount = await this.applyValidationForSection(options, preparation, section, (progress, message) => { - const base = 45 + Math.floor((i / Math.max(1, sections.length)) * 45); - const span = Math.max(1, Math.floor(45 / Math.max(1, sections.length))); - const mapped = base + Math.floor((progress / 100) * span); - onProgress(Math.min(90, mapped), message); - }); + const sectionCount = await this.applyValidationForSection( + options, + preparation, + section, + (progress, message) => { + const base = 45 + Math.floor((i / Math.max(1, sections.length)) * 45); + const span = Math.max( + 1, + Math.floor(45 / Math.max(1, sections.length)), + ); + const mapped = base + Math.floor((progress / 100) * span); + onProgress(Math.min(90, mapped), message); + }, + ); renderedUrlCount += sectionCount; } @@ -1611,11 +1945,17 @@ export class BlogGenerationEngine { onProgress(90, 'Regenerating calendar data...'); await this.regenerateCalendar(options, (progress, message) => { const mappedProgress = 90 + Math.floor((progress / 100) * 9); - onProgress(Math.min(99, mappedProgress), message || 'Regenerating calendar data...'); + onProgress( + Math.min(99, mappedProgress), + message || 'Regenerating calendar data...', + ); }); } - onProgress(100, `Apply complete (${preparation.deletedUrlCount} deleted, ${renderedUrlCount} rendered)`); + onProgress( + 100, + `Apply complete (${preparation.deletedUrlCount} deleted, ${renderedUrlCount} rendered)`, + ); return { renderedUrlCount, @@ -1633,17 +1973,28 @@ export class BlogGenerationEngine { ): Promise { onProgress(0, 'Planning validation apply steps...'); - const missingPaths = Array.isArray(report.missingUrlPaths) ? report.missingUrlPaths : []; - const updatedPostPaths = Array.isArray(report.updatedPostUrlPaths) ? report.updatedPostUrlPaths : []; - const rerenderPaths = Array.from(new Set([...missingPaths, ...updatedPostPaths])); - const extraPaths = Array.isArray(report.extraUrlPaths) ? report.extraUrlPaths : []; + const missingPaths = Array.isArray(report.missingUrlPaths) + ? report.missingUrlPaths + : []; + const updatedPostPaths = Array.isArray(report.updatedPostUrlPaths) + ? report.updatedPostUrlPaths + : []; + const rerenderPaths = Array.from( + new Set([...missingPaths, ...updatedPostPaths]), + ); + const extraPaths = Array.isArray(report.extraUrlPaths) + ? report.extraUrlPaths + : []; const mainLanguage = (options.language ?? 'en').trim().toLowerCase(); const additionalLanguages = (options.blogLanguages ?? []) .map((lang) => lang.trim().toLowerCase()) .filter((lang) => lang.length > 0 && lang !== mainLanguage); - const missingPathPlan = planMissingValidationPaths(rerenderPaths, additionalLanguages); + const missingPathPlan = planMissingValidationPaths( + rerenderPaths, + additionalLanguages, + ); onProgress(20, 'Deleting extra URLs...'); @@ -1660,7 +2011,9 @@ export class BlogGenerationEngine { } catch { break; } - if (entries.length > 0) break; + if (entries.length > 0) { + break; + } await fs.rm(currentDir, { recursive: true, force: true }); removedEmptyDirCount += 1; currentDir = path.dirname(currentDir); @@ -1678,8 +2031,12 @@ export class BlogGenerationEngine { // ignore missing files and continue } if (extraPaths.length > 0) { - const deleteProgress = 20 + Math.floor(((index + 1) / extraPaths.length) * 25); - onProgress(Math.min(50, deleteProgress), `Deleted ${index + 1}/${extraPaths.length} extra URLs`); + const deleteProgress = + 20 + Math.floor(((index + 1) / extraPaths.length) * 25); + onProgress( + Math.min(50, deleteProgress), + `Deleted ${index + 1}/${extraPaths.length} extra URLs`, + ); } } @@ -1696,17 +2053,26 @@ export class BlogGenerationEngine { onProgress(55, 'Loading post data...'); - const categorySettings = resolveCategorySettings(options.categoryMetadata, options.categorySettings); + const categorySettings = resolveCategorySettings( + options.categoryMetadata, + options.categorySettings, + ); const listExcludedCategories = Object.entries(categorySettings) .filter(([, settings]) => settings.renderInLists === false) .map(([category]) => category); const maxPostsPerPage = clampMaxPostsPerPage(options.maxPostsPerPage); - const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); - const { routePosts: publishedRoutePosts, translationsByPost } = await this.buildPublishedRoutePosts(publishedPosts); + const { publishedPosts, publishedListPosts } = + await loadPublishedGenerationSets( + this.postEngine, + listExcludedCategories, + ); + const { routePosts: publishedRoutePosts, translationsByPost } = + await this.buildPublishedRoutePosts(publishedPosts); const generationPostIndex = buildGenerationPostIndex(publishedListPosts); - const { allCategories, allTags, years, yearMonths, yearMonthDays } = buildApplyValidationArchives(publishedListPosts); + const { allCategories, allTags, years, yearMonths, yearMonthDays } = + buildApplyValidationArchives(publishedListPosts); const targetedPlan = buildTargetedValidationPlan({ initialPlan: missingPathPlan, @@ -1722,7 +2088,10 @@ export class BlogGenerationEngine { const useWorkers = !!options.dbPath; // Build worker tasks partitioned by section (worker mode only) - const workerTasksBySection = new Map(); + const workerTasksBySection = new Map< + BlogGenerationSection, + GenerationWorkerTask[] + >(); if (useWorkers) { onProgress(65, 'Loading media and serializing worker data...'); @@ -1730,7 +2099,10 @@ export class BlogGenerationEngine { const rawMedia = await this.mediaEngine.getAllMedia(); const mediaItems = rawMedia.map(serializeMediaItem); - let backlinksRecord: Record> = {}; + const backlinksRecord: Record< + string, + Array<{ id: string; title: string; slug: string }> + > = {}; if (typeof this.postEngine.getAllBacklinks === 'function') { const blMap = await this.postEngine.getAllBacklinks(); for (const [postId, links] of blMap) { @@ -1746,7 +2118,9 @@ export class BlogGenerationEngine { postFilePathEntries = Array.from(filePathMap); } - let postMediaLinksEntries: Array<[string, Array<{ mediaId: string; sortOrder: number }>]> = []; + let postMediaLinksEntries: Array< + [string, Array<{ mediaId: string; sortOrder: number }>] + > = []; if (typeof this.postMediaEngine.getAllPostMediaLinks === 'function') { const linksMap = await this.postMediaEngine.getAllPostMediaLinks(); postMediaLinksEntries = Array.from(linksMap); @@ -1759,11 +2133,23 @@ export class BlogGenerationEngine { } const hashMapEntries: Array<[string, string]> = []; for (const [relativePath, hash] of generatedHashCache) { - if (hash !== null) hashMapEntries.push([relativePath, hash]); + if (hash !== null) { + hashMapEntries.push([relativePath, hash]); + } } - const mainLangRoutePosts = this.resolvePostsForLanguage(publishedRoutePosts, mainLanguage, translationsByPost, mainLanguage); - const mainLangListPosts = this.resolvePostsForLanguage(publishedListPosts, mainLanguage, translationsByPost, mainLanguage); + const mainLangRoutePosts = this.resolvePostsForLanguage( + publishedRoutePosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); + const mainLangListPosts = this.resolvePostsForLanguage( + publishedListPosts, + mainLanguage, + translationsByPost, + mainLanguage, + ); const mainLangPostIndex = buildGenerationPostIndex(mainLangListPosts); const baseWorkerParams = { @@ -1779,19 +2165,24 @@ export class BlogGenerationEngine { onProgress(80, 'Building targeted worker tasks...'); - const allTasks: GenerationWorkerTask[] = buildApplyValidationWorkerTasks(baseWorkerParams, { - targetedPlan, - publishedRoutePosts: mainLangRoutePosts, - publishedListPosts: mainLangListPosts, - generationPostIndex: mainLangPostIndex, - years, - yearMonths, - yearMonthDays, - }); + const allTasks: GenerationWorkerTask[] = buildApplyValidationWorkerTasks( + baseWorkerParams, + { + targetedPlan, + publishedRoutePosts: mainLangRoutePosts, + publishedListPosts: mainLangListPosts, + generationPostIndex: mainLangPostIndex, + years, + yearMonths, + yearMonthDays, + }, + ); for (const [lang, langMissingPlan] of missingPathPlan.languagePlans) { const langPosts = publishedPosts.filter((p) => !p.doNotTranslate); - const langListPosts = publishedListPosts.filter((p) => !p.doNotTranslate); + const langListPosts = publishedListPosts.filter( + (p) => !p.doNotTranslate, + ); const langArchives = buildApplyValidationArchives(langListPosts); const langTargetedPlan = buildTargetedValidationPlan({ initialPlan: langMissingPlan, @@ -1802,12 +2193,27 @@ export class BlogGenerationEngine { availableYearMonthDays: langArchives.yearMonthDays.keys(), }); - const resolvedLangPosts = this.resolvePostsForLanguage(langPosts, lang, translationsByPost, mainLanguage); - const resolvedLangListPosts = this.resolvePostsForLanguage(langListPosts, lang, translationsByPost, mainLanguage); - const resolvedLangPostIndex = buildGenerationPostIndex(resolvedLangListPosts); + const resolvedLangPosts = this.resolvePostsForLanguage( + langPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangListPosts = this.resolvePostsForLanguage( + langListPosts, + lang, + translationsByPost, + mainLanguage, + ); + const resolvedLangPostIndex = buildGenerationPostIndex( + resolvedLangListPosts, + ); const langTasks = buildApplyValidationWorkerTasks( - { ...baseWorkerParams, options: { ...serializedOptions, language: lang } }, + { + ...baseWorkerParams, + options: { ...serializedOptions, language: lang }, + }, { targetedPlan: langTargetedPlan, publishedRoutePosts: resolvedLangPosts, @@ -1833,7 +2239,12 @@ export class BlogGenerationEngine { // Determine which sections have work const sectionsToRender: BlogGenerationSection[] = useWorkers ? Array.from(workerTasksBySection.keys()) - : this.computeTargetedSectionsToRender(targetedPlan, missingPathPlan, publishedPosts, publishedListPosts); + : this.computeTargetedSectionsToRender( + targetedPlan, + missingPathPlan, + publishedPosts, + publishedListPosts, + ); onProgress(100, 'Preparation complete'); @@ -1843,22 +2254,24 @@ export class BlogGenerationEngine { requiresFallbackSectionRender: false, sectionsToRender, workerTasksBySection, - targetedRenderData: !useWorkers ? { - options, - maxPostsPerPage, - htmlDir, - publishedPosts, - publishedListPosts, - publishedRoutePosts, - generationPostIndex, - targetedPlan, - missingPathPlan, - mainLanguage, - additionalLanguages, - years, - yearMonths, - yearMonthDays, - } : undefined, + targetedRenderData: !useWorkers + ? { + options, + maxPostsPerPage, + htmlDir, + publishedPosts, + publishedListPosts, + publishedRoutePosts, + generationPostIndex, + targetedPlan, + missingPathPlan, + mainLanguage, + additionalLanguages, + years, + yearMonths, + yearMonthDays, + } + : undefined, }; } @@ -1870,12 +2283,26 @@ export class BlogGenerationEngine { ): BlogGenerationSection[] { const sections = new Set(); - const checkPlan = (plan: import('./ValidationApplyPlannerService').TargetedValidationPlan) => { - if (plan.requestRootRoutes || plan.requestedPageSlugs.size > 0) sections.add('core'); - if (plan.requestedPostIds.size > 0) sections.add('single'); - if (plan.requestedCategorySet.size > 0) sections.add('category'); - if (plan.requestedTagSet.size > 0) sections.add('tag'); - if (plan.requestedYears.size > 0 || plan.requestedYearMonths.size > 0 || plan.requestedYearMonthDays.size > 0) sections.add('date'); + const checkPlan = ( + plan: import('./ValidationApplyPlannerService').TargetedValidationPlan, + ) => { + if (plan.requestRootRoutes || plan.requestedPageSlugs.size > 0) + {sections.add('core');} + if (plan.requestedPostIds.size > 0) { + sections.add('single'); + } + if (plan.requestedCategorySet.size > 0) { + sections.add('category'); + } + if (plan.requestedTagSet.size > 0) { + sections.add('tag'); + } + if ( + plan.requestedYears.size > 0 || + plan.requestedYearMonths.size > 0 || + plan.requestedYearMonthDays.size > 0 + ) + {sections.add('date');} }; checkPlan(targetedPlan); @@ -1905,27 +2332,35 @@ export class BlogGenerationEngine { onProgress: (progress: number, message?: string) => void, ): Promise { if (preparation.requiresFallbackSectionRender) { - const result = await this.generate({ - ...options, - maxPostsPerPage: options.maxPostsPerPage, - sections: [section], - }, (progress, message) => { - onProgress(progress, message || `Rendering ${section} routes...`); - }); + const result = await this.generate( + { + ...options, + maxPostsPerPage: options.maxPostsPerPage, + sections: [section], + }, + (progress, message) => { + onProgress(progress, message || `Rendering ${section} routes...`); + }, + ); return result.pagesGenerated; } // Worker-based targeted rendering for this section const sectionTasks = preparation.workerTasksBySection.get(section); if (sectionTasks && sectionTasks.length > 0) { - onProgress(0, `Dispatching ${sectionTasks.length} targeted ${section} tasks to worker pool...`); + onProgress( + 0, + `Dispatching ${sectionTasks.length} targeted ${section} tasks to worker pool...`, + ); const pool = new GenerationWorkerPool(); const result = await pool.runTasks(sectionTasks, (message) => { onProgress(50, message); }); if (result.errors.length > 0) { - console.error(`[ApplyValidation/${section}] ${result.errors.length} task(s) failed:`); + console.error( + `[ApplyValidation/${section}] ${result.errors.length} task(s) failed:`, + ); for (const err of result.errors) { console.error(` [${err.taskId}] ${err.error}`); } @@ -1933,7 +2368,11 @@ export class BlogGenerationEngine { if (result.hashUpdates.length > 0) { for (const update of result.hashUpdates) { - await setGeneratedFileHash(options.projectId, update.relativePath, update.hash); + await setGeneratedFileHash( + options.projectId, + update.relativePath, + update.hash, + ); } } @@ -1943,7 +2382,11 @@ export class BlogGenerationEngine { // Main-thread fallback for this section if (preparation.targetedRenderData) { - return this.applyValidationSectionOnMainThread(preparation.targetedRenderData, section, onProgress); + return this.applyValidationSectionOnMainThread( + preparation.targetedRenderData, + section, + onProgress, + ); } return 0; @@ -1957,10 +2400,18 @@ export class BlogGenerationEngine { onProgress: (progress: number, message?: string) => void, ): Promise { const { - options, maxPostsPerPage, htmlDir, - publishedPosts, publishedListPosts, publishedRoutePosts, - generationPostIndex, targetedPlan, missingPathPlan, - years, yearMonths, yearMonthDays, + options, + maxPostsPerPage, + htmlDir, + publishedPosts, + publishedListPosts, + publishedRoutePosts, + generationPostIndex, + targetedPlan, + missingPathPlan, + years, + yearMonths, + yearMonthDays, } = data; let renderedUrlCount = 0; @@ -1975,14 +2426,15 @@ export class BlogGenerationEngine { postMediaEngine: this.postMediaEngine, }, }); - const writePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({ - projectId, - htmlDir, - urlPath, - content, - refreshHashTimestampOnUnchanged: true, - }); - const onPageGenerated = (_message: string) => { + const writePage = (projectId: string, urlPath: string, content: string) => + writeHtmlPage({ + projectId, + htmlDir, + urlPath, + content, + refreshHashTimestampOnUnchanged: true, + }); + const onPageGenerated = () => { // no-op for applyValidation }; @@ -1992,7 +2444,11 @@ export class BlogGenerationEngine { requestedPageSlugs: targetedPlan.requestedPageSlugs, }); - const { requestedYearsMap, requestedYearMonthsMap, requestedYearMonthDaysMap } = buildRequestedArchiveMaps({ + const { + requestedYearsMap, + requestedYearMonthsMap, + requestedYearMonthDaysMap, + } = buildRequestedArchiveMaps({ requestedYears: targetedPlan.requestedYears, requestedYearMonths: targetedPlan.requestedYearMonths, requestedYearMonthDays: targetedPlan.requestedYearMonthDays, @@ -2005,71 +2461,102 @@ export class BlogGenerationEngine { // Main language rendering for this section switch (section) { - case 'core': - if (targetedPlan.requestRootRoutes) { - renderedUrlCount += await generateRootPages({ - projectId: options.projectId, posts: publishedListPosts, maxPostsPerPage, - renderRoute, writePage, onPageGenerated, - }); - } - if (requestedPagePosts.length > 0) { - renderedUrlCount += await generatePageRoutes({ - projectId: options.projectId, posts: requestedPagePosts, - renderRoute, writePage, onPageGenerated, - }); - } - break; + case 'core': + if (targetedPlan.requestRootRoutes) { + renderedUrlCount += await generateRootPages({ + projectId: options.projectId, + posts: publishedListPosts, + maxPostsPerPage, + renderRoute, + writePage, + onPageGenerated, + }); + } + if (requestedPagePosts.length > 0) { + renderedUrlCount += await generatePageRoutes({ + projectId: options.projectId, + posts: requestedPagePosts, + renderRoute, + writePage, + onPageGenerated, + }); + } + break; - case 'single': - if (requestedSinglePosts.length > 0) { - renderedUrlCount += await generateSinglePostPages({ - projectId: options.projectId, posts: requestedSinglePosts, - renderRoute, writePage, onPageGenerated, - }); - } - break; + case 'single': + if (requestedSinglePosts.length > 0) { + renderedUrlCount += await generateSinglePostPages({ + projectId: options.projectId, + posts: requestedSinglePosts, + renderRoute, + writePage, + onPageGenerated, + }); + } + break; - case 'category': - if (targetedPlan.requestedCategorySet.size > 0) { - renderedUrlCount += await generateCategoryPages({ - projectId: options.projectId, posts: publishedListPosts, - allCategories: targetedPlan.requestedCategorySet, maxPostsPerPage, - renderRoute, writePage, onPageGenerated, - postsByCategory: generationPostIndex.postsByCategory, - }); - } - break; + case 'category': + if (targetedPlan.requestedCategorySet.size > 0) { + renderedUrlCount += await generateCategoryPages({ + projectId: options.projectId, + posts: publishedListPosts, + allCategories: targetedPlan.requestedCategorySet, + maxPostsPerPage, + renderRoute, + writePage, + onPageGenerated, + postsByCategory: generationPostIndex.postsByCategory, + }); + } + break; - case 'tag': - if (targetedPlan.requestedTagSet.size > 0) { - renderedUrlCount += await generateTagPages({ - projectId: options.projectId, posts: publishedListPosts, - allTags: targetedPlan.requestedTagSet, maxPostsPerPage, - renderRoute, writePage, onPageGenerated, - postsByTag: generationPostIndex.postsByTag, - }); - } - break; + case 'tag': + if (targetedPlan.requestedTagSet.size > 0) { + renderedUrlCount += await generateTagPages({ + projectId: options.projectId, + posts: publishedListPosts, + allTags: targetedPlan.requestedTagSet, + maxPostsPerPage, + renderRoute, + writePage, + onPageGenerated, + postsByTag: generationPostIndex.postsByTag, + }); + } + break; - case 'date': - if (requestedYearsMap.size > 0 || requestedYearMonthsMap.size > 0 || requestedYearMonthDaysMap.size > 0) { - renderedUrlCount += await generateDateArchivePages({ - projectId: options.projectId, posts: publishedListPosts, - yearsMap: requestedYearsMap, yearMonthsMap: requestedYearMonthsMap, - yearMonthDaysMap: requestedYearMonthDaysMap, maxPostsPerPage, - renderRoute, writePage, onPageGenerated, - postsByYear: generationPostIndex.postsByYear, - postsByYearMonth: generationPostIndex.postsByYearMonth, - postsByYearMonthDay: generationPostIndex.postsByYearMonthDay, - }); - } - break; + case 'date': + if ( + requestedYearsMap.size > 0 || + requestedYearMonthsMap.size > 0 || + requestedYearMonthDaysMap.size > 0 + ) { + renderedUrlCount += await generateDateArchivePages({ + projectId: options.projectId, + posts: publishedListPosts, + yearsMap: requestedYearsMap, + yearMonthsMap: requestedYearMonthsMap, + yearMonthDaysMap: requestedYearMonthDaysMap, + maxPostsPerPage, + renderRoute, + writePage, + onPageGenerated, + postsByYear: generationPostIndex.postsByYear, + postsByYearMonth: generationPostIndex.postsByYearMonth, + postsByYearMonthDay: generationPostIndex.postsByYearMonthDay, + }); + } + break; } // Language subtrees for this section for (const [lang, langMissingPlan] of missingPathPlan.languagePlans) { - const langPosts = publishedPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); - const langListPosts = publishedListPosts.filter((p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate); + const langPosts = publishedPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); + const langListPosts = publishedListPosts.filter( + (p) => !(p as PostData & { doNotTranslate?: boolean }).doNotTranslate, + ); const langPostIndex = buildGenerationPostIndex(langListPosts); const langArchives = buildApplyValidationArchives(langListPosts); @@ -2094,94 +2581,130 @@ export class BlogGenerationEngine { }, }); - const langWritePage = (projectId: string, urlPath: string, content: string) => writeHtmlPage({ - projectId, - htmlDir, - urlPath: `${lang}/${urlPath}`, - content, - refreshHashTimestampOnUnchanged: true, - }); + const langWritePage = ( + projectId: string, + urlPath: string, + content: string, + ) => + writeHtmlPage({ + projectId, + htmlDir, + urlPath: `${lang}/${urlPath}`, + content, + refreshHashTimestampOnUnchanged: true, + }); switch (section) { - case 'core': - if (langTargetedPlan.requestRootRoutes) { - renderedUrlCount += await generateRootPages({ - projectId: options.projectId, posts: langListPosts, maxPostsPerPage, - renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - }); - const langPagePosts = selectRequestedPosts({ - publishedPosts: langPosts, requestedPostIds: new Set(), requestedPageSlugs: langTargetedPlan.requestedPageSlugs, - }).requestedPagePosts; - if (langPagePosts.length > 0) { - renderedUrlCount += await generatePageRoutes({ - projectId: options.projectId, posts: langPagePosts, - renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - }); - } - } - break; - - case 'category': - if (langTargetedPlan.requestedCategorySet.size > 0) { - renderedUrlCount += await generateCategoryPages({ - projectId: options.projectId, posts: langListPosts, - allCategories: langTargetedPlan.requestedCategorySet, maxPostsPerPage, - renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - postsByCategory: langPostIndex.postsByCategory, - }); - } - break; - - case 'tag': - if (langTargetedPlan.requestedTagSet.size > 0) { - renderedUrlCount += await generateTagPages({ - projectId: options.projectId, posts: langListPosts, - allTags: langTargetedPlan.requestedTagSet, maxPostsPerPage, - renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - postsByTag: langPostIndex.postsByTag, - }); - } - break; - - case 'single': { - const langSinglePosts = selectRequestedPosts({ - publishedPosts: langPosts, requestedPostIds: langTargetedPlan.requestedPostIds, requestedPageSlugs: new Set(), - }).requestedSinglePosts; - if (langSinglePosts.length > 0) { - renderedUrlCount += await generateSinglePostPages({ - projectId: options.projectId, posts: langSinglePosts, - renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - }); - } - break; - } - - case 'date': { - const langArchiveMaps = buildRequestedArchiveMaps({ - requestedYears: langTargetedPlan.requestedYears, - requestedYearMonths: langTargetedPlan.requestedYearMonths, - requestedYearMonthDays: langTargetedPlan.requestedYearMonthDays, - years: langArchives.years, yearMonths: langArchives.yearMonths, yearMonthDays: langArchives.yearMonthDays, + case 'core': + if (langTargetedPlan.requestRootRoutes) { + renderedUrlCount += await generateRootPages({ + projectId: options.projectId, + posts: langListPosts, + maxPostsPerPage, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, }); - if (langArchiveMaps.requestedYearsMap.size > 0 || langArchiveMaps.requestedYearMonthsMap.size > 0 || langArchiveMaps.requestedYearMonthDaysMap.size > 0) { - renderedUrlCount += await generateDateArchivePages({ - projectId: options.projectId, posts: langListPosts, - yearsMap: langArchiveMaps.requestedYearsMap, - yearMonthsMap: langArchiveMaps.requestedYearMonthsMap, - yearMonthDaysMap: langArchiveMaps.requestedYearMonthDaysMap, - maxPostsPerPage, renderRoute: langRenderRoute, writePage: langWritePage, onPageGenerated, - postsByYear: langPostIndex.postsByYear, postsByYearMonth: langPostIndex.postsByYearMonth, - postsByYearMonthDay: langPostIndex.postsByYearMonthDay, + const langPagePosts = selectRequestedPosts({ + publishedPosts: langPosts, + requestedPostIds: new Set(), + requestedPageSlugs: langTargetedPlan.requestedPageSlugs, + }).requestedPagePosts; + if (langPagePosts.length > 0) { + renderedUrlCount += await generatePageRoutes({ + projectId: options.projectId, + posts: langPagePosts, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, }); } - break; } + break; + + case 'category': + if (langTargetedPlan.requestedCategorySet.size > 0) { + renderedUrlCount += await generateCategoryPages({ + projectId: options.projectId, + posts: langListPosts, + allCategories: langTargetedPlan.requestedCategorySet, + maxPostsPerPage, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, + postsByCategory: langPostIndex.postsByCategory, + }); + } + break; + + case 'tag': + if (langTargetedPlan.requestedTagSet.size > 0) { + renderedUrlCount += await generateTagPages({ + projectId: options.projectId, + posts: langListPosts, + allTags: langTargetedPlan.requestedTagSet, + maxPostsPerPage, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, + postsByTag: langPostIndex.postsByTag, + }); + } + break; + + case 'single': { + const langSinglePosts = selectRequestedPosts({ + publishedPosts: langPosts, + requestedPostIds: langTargetedPlan.requestedPostIds, + requestedPageSlugs: new Set(), + }).requestedSinglePosts; + if (langSinglePosts.length > 0) { + renderedUrlCount += await generateSinglePostPages({ + projectId: options.projectId, + posts: langSinglePosts, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, + }); + } + break; + } + + case 'date': { + const langArchiveMaps = buildRequestedArchiveMaps({ + requestedYears: langTargetedPlan.requestedYears, + requestedYearMonths: langTargetedPlan.requestedYearMonths, + requestedYearMonthDays: langTargetedPlan.requestedYearMonthDays, + years: langArchives.years, + yearMonths: langArchives.yearMonths, + yearMonthDays: langArchives.yearMonthDays, + }); + if ( + langArchiveMaps.requestedYearsMap.size > 0 || + langArchiveMaps.requestedYearMonthsMap.size > 0 || + langArchiveMaps.requestedYearMonthDaysMap.size > 0 + ) { + renderedUrlCount += await generateDateArchivePages({ + projectId: options.projectId, + posts: langListPosts, + yearsMap: langArchiveMaps.requestedYearsMap, + yearMonthsMap: langArchiveMaps.requestedYearMonthsMap, + yearMonthDaysMap: langArchiveMaps.requestedYearMonthDaysMap, + maxPostsPerPage, + renderRoute: langRenderRoute, + writePage: langWritePage, + onPageGenerated, + postsByYear: langPostIndex.postsByYear, + postsByYearMonth: langPostIndex.postsByYearMonth, + postsByYearMonthDay: langPostIndex.postsByYearMonthDay, + }); + } + break; + } } } onProgress(100, `${section} rendering complete`); return renderedUrlCount; } - } - diff --git a/src/main/engine/BlogmarkTransformService.ts b/src/main/engine/BlogmarkTransformService.ts index bdbbb58..eaf47bc 100644 --- a/src/main/engine/BlogmarkTransformService.ts +++ b/src/main/engine/BlogmarkTransformService.ts @@ -164,8 +164,15 @@ async function getConfiguredPythonRuntimeModeFromEngine(metaEngine: MetaEngine): return resolvePythonRuntimeMode((metadata as { pythonRuntimeMode?: unknown } | null)?.pythonRuntimeMode); } +type PyodideLikeRuntime = { + globals: { + set: (name: string, value: unknown) => void; + }; + runPythonAsync: (code: string) => Promise; +}; + class PythonBlogmarkTransformExecutor implements BlogmarkTransformExecutor { - private runtimePromise: Promise | null = null; + private runtimePromise: Promise | null = null; async runTransform(script: BlogmarkTransformScriptRecord, input: BlogmarkTransformInput): Promise { const runtime = await this.getRuntime(); @@ -222,15 +229,16 @@ json.dumps(_result) }; } - private async getRuntime(): Promise { + private async getRuntime(): Promise { if (!this.runtimePromise) { this.runtimePromise = (async () => { const pyodideModule = await import('pyodide'); - return pyodideModule.loadPyodide(); + return (await pyodideModule.loadPyodide()) as unknown as PyodideLikeRuntime; })(); } - return this.runtimePromise; + const runtimePromise = this.runtimePromise; + return runtimePromise; } } @@ -268,9 +276,10 @@ export class BlogmarkTransformService { post: parsedInput, }; + const scriptEngine = this.dependencies.scriptEngine; const provider = this.dependencies.provider - ?? (this.dependencies.scriptEngine - ? { getScripts: (): Promise => this.dependencies.scriptEngine!.getAllScripts() } + ?? (scriptEngine + ? { getScripts: (): Promise => scriptEngine.getAllScripts() } : { getScripts: async () => [] }); const executor = this.dependencies.executor ?? await this.resolveExecutorForConfiguredRuntime(); @@ -340,9 +349,10 @@ export class BlogmarkTransformService { } private async resolveExecutorForConfiguredRuntime(): Promise { + const metaEngine = this.dependencies.metaEngine; const resolveMode = this.dependencies.resolvePythonRuntimeMode - ?? (this.dependencies.metaEngine - ? () => getConfiguredPythonRuntimeModeFromEngine(this.dependencies.metaEngine!) + ?? (metaEngine + ? () => getConfiguredPythonRuntimeModeFromEngine(metaEngine) : () => Promise.resolve('webworker')); const mode = await resolveMode(); const executors = this.dependencies.executors ?? {}; diff --git a/src/main/engine/CliNotifier.ts b/src/main/engine/CliNotifier.ts index 60e13a0..6f2fd40 100644 --- a/src/main/engine/CliNotifier.ts +++ b/src/main/engine/CliNotifier.ts @@ -21,7 +21,10 @@ export interface CliNotifier { /** Used by the Electron app. All notify calls are instant no-ops. */ export class NoopNotifier implements CliNotifier { - async notify(_entity: NotifyEntity, _id: string, _action: NotifyAction): Promise { + async notify(entity: NotifyEntity, id: string, action: NotifyAction): Promise { + void entity; + void id; + void action; // intentional no-op } } diff --git a/src/main/engine/DataBackedEngines.ts b/src/main/engine/DataBackedEngines.ts index 08c25f5..dfefd5c 100644 --- a/src/main/engine/DataBackedEngines.ts +++ b/src/main/engine/DataBackedEngines.ts @@ -6,6 +6,7 @@ * so no database access is needed for post/media queries. */ import type { PostData, PostFilter, PostTranslationData } from './PostEngine'; +import type { PublishedTranslationVariant } from './BlogGenerationEngine'; import type { MediaData } from './MediaEngine'; import { readPostFile } from './postFileUtils'; import { readPostTranslationFile } from './postTranslationFileUtils'; @@ -18,7 +19,10 @@ export interface DataBackedPostEngineInit { /** All posts (published snapshots + translation variants). */ allPosts: PostData[]; /** Pre-resolved backlinks: postId → linking posts. */ - backlinksMap?: Map>; + backlinksMap?: Map< + string, + Array<{ id: string; title: string; slug: string }> + >; /** Post file paths for lazy content loading from filesystem: postId → absoluteFilePath. */ postFilePaths?: Map; } @@ -28,15 +32,27 @@ export interface DataBackedPostEngineContract { getPublishedVersion: (id: string) => Promise; getPost: (id: string) => Promise; hasPublishedVersion: (id: string) => Promise; - findPublishedBySlug: (slug: string, dateFilter?: { year: number; month: number }) => Promise; - getLinkedBy: (postId: string) => Promise>; - getAllBacklinks: () => Promise>>; - getPostTranslation: (postId: string, language: string) => Promise; + findPublishedBySlug: ( + slug: string, + dateFilter?: { year: number; month: number }, + ) => Promise; + getLinkedBy: ( + postId: string, + ) => Promise>; + getAllBacklinks: () => Promise< + Map> + >; + getPostTranslation: ( + postId: string, + language: string, + ) => Promise; getPostTranslations: (postId: string) => Promise; setProjectContext: (projectId: string, dataDir?: string) => void; } -export function createDataBackedPostEngine(init: DataBackedPostEngineInit): DataBackedPostEngineContract { +export function createDataBackedPostEngine( + init: DataBackedPostEngineInit, +): DataBackedPostEngineContract { const { allPosts, backlinksMap, postFilePaths } = init; // Build indexes for fast lookups @@ -53,37 +69,54 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data } function matchesFilter(post: PostData, filter: PostFilter): boolean { - if (filter.status && post.status !== filter.status) return false; + if (filter.status && post.status !== filter.status) { + return false; + } if (filter.excludeCategories && filter.excludeCategories.length > 0) { const excluded = new Set(filter.excludeCategories); - if (post.categories.some((c) => excluded.has(c))) return false; + if (post.categories.some((c) => excluded.has(c))) { + return false; + } } if (filter.categories && filter.categories.length > 0) { - if (!filter.categories.some((c) => post.categories.includes(c))) return false; + if (!filter.categories.some((c) => post.categories.includes(c))) + {return false;} } if (filter.tags && filter.tags.length > 0) { - if (!filter.tags.every((t) => post.tags.includes(t))) return false; + if (!filter.tags.every((t) => post.tags.includes(t))) { + return false; + } } - if (filter.language && post.language !== filter.language) return false; + if (filter.language && post.language !== filter.language) { + return false; + } if (filter.year !== undefined) { - if (post.createdAt.getFullYear() !== filter.year) return false; + if (post.createdAt.getFullYear() !== filter.year) { + return false; + } } if (filter.month !== undefined) { - if (post.createdAt.getMonth() + 1 !== filter.month) return false; + if (post.createdAt.getMonth() + 1 !== filter.month) { + return false; + } } if (filter.startDate) { - if (post.createdAt < filter.startDate) return false; + if (post.createdAt < filter.startDate) { + return false; + } } if (filter.endDate) { - if (post.createdAt > filter.endDate) return false; + if (post.createdAt > filter.endDate) { + return false; + } } return true; @@ -91,10 +124,14 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data // Shared lazy content loader for posts with empty content async function lazyLoadContent(post: PostData): Promise { - if (post.content || !postFilePaths) return; + if (post.content || !postFilePaths) { + return; + } const variant = post as PostData & { translationFilePath?: string }; if (variant.translationFilePath) { - const fileData = await readPostTranslationFile(variant.translationFilePath); + const fileData = await readPostTranslationFile( + variant.translationFilePath, + ); if (fileData) { post.content = fileData.content; } @@ -113,7 +150,8 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data async getPostsFiltered(filter: PostFilter): Promise { const filtered = allPosts .filter((post) => { - const tss = (post as any).translationSourceSlug; + const tss = (post as PublishedTranslationVariant) + .translationSourceSlug; // Keep canonical posts and resolved posts (slug === tss). // Exclude translation variant route posts (slug !== tss, e.g. "my-post.en"). return !tss || post.slug === tss; @@ -130,7 +168,9 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data async getPublishedVersion(id: string): Promise { const post = byId.get(id); - if (!post) return null; + if (!post) { + return null; + } await lazyLoadContent(post); @@ -149,36 +189,58 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data return byId.has(id); }, - async findPublishedBySlug(slug: string, dateFilter?: { year: number; month: number }): Promise { + async findPublishedBySlug( + slug: string, + dateFilter?: { year: number; month: number }, + ): Promise { const candidates = bySlug.get(slug); - if (!candidates || candidates.length === 0) return null; + if (!candidates || candidates.length === 0) { + return null; + } - if (!dateFilter) return candidates[0]; + if (!dateFilter) { + return candidates[0]; + } - return candidates.find((p) => - p.createdAt.getFullYear() === dateFilter.year - && p.createdAt.getMonth() === dateFilter.month - 1, - ) ?? null; + return ( + candidates.find( + (p) => + p.createdAt.getFullYear() === dateFilter.year && + p.createdAt.getMonth() === dateFilter.month - 1, + ) ?? null + ); }, - async getLinkedBy(postId: string): Promise> { + async getLinkedBy( + postId: string, + ): Promise> { return backlinksMap?.get(postId) ?? []; }, - async getAllBacklinks(): Promise>> { + async getAllBacklinks(): Promise< + Map> + > { return backlinksMap ?? new Map(); }, - async getPostTranslation(_postId: string, _language: string): Promise { + async getPostTranslation( + postId: string, + language: string, + ): Promise { + void postId; + void language; // Translation variants are already included as separate route posts return null; }, - async getPostTranslations(_postId: string): Promise { + async getPostTranslations(postId: string): Promise { + void postId; return []; }, - setProjectContext(_projectId: string, _dataDir?: string): void { + setProjectContext(projectId: string, dataDir?: string): void { + void projectId; + void dataDir; // No-op — data is already loaded }, }; @@ -190,7 +252,11 @@ export function createDataBackedPostEngine(init: DataBackedPostEngineInit): Data export interface DataBackedMediaEngineContract { getAllMedia: () => Promise; - setProjectContext: (projectId: string, dataDir?: string, internalDir?: string) => void; + setProjectContext: ( + projectId: string, + dataDir?: string, + internalDir?: string, + ) => void; } export function createDataBackedMediaEngine( @@ -200,7 +266,14 @@ export function createDataBackedMediaEngine( async getAllMedia() { return mediaItems; }, - setProjectContext(_projectId: string, _dataDir?: string, _internalDir?: string): void { + setProjectContext( + projectId: string, + dataDir?: string, + internalDir?: string, + ): void { + void projectId; + void dataDir; + void internalDir; // No-op }, }; @@ -217,11 +290,17 @@ export interface DataBackedPostMediaEngineInit { export interface DataBackedPostMediaEngineContract { setProjectContext: (projectId: string) => void; - getLinkedMediaForPost: (postId: string) => Promise>; - getLinkedMediaDataForPost: (postId: string) => Promise>; + getLinkedMediaForPost: ( + postId: string, + ) => Promise>; + getLinkedMediaDataForPost: ( + postId: string, + ) => Promise>; } -export function createDataBackedPostMediaEngine(init: DataBackedPostMediaEngineInit): DataBackedPostMediaEngineContract { +export function createDataBackedPostMediaEngine( + init: DataBackedPostMediaEngineInit, +): DataBackedPostMediaEngineContract { const { mediaItems, postMediaLinks } = init; const mediaById = new Map(); for (const m of mediaItems) { @@ -229,13 +308,18 @@ export function createDataBackedPostMediaEngine(init: DataBackedPostMediaEngineI } return { - setProjectContext(_projectId: string): void { + setProjectContext(projectId: string): void { + void projectId; // No-op }, - async getLinkedMediaForPost(postId: string): Promise> { + async getLinkedMediaForPost( + postId: string, + ): Promise> { return postMediaLinks.get(postId) ?? []; }, - async getLinkedMediaDataForPost(postId: string): Promise> { + async getLinkedMediaDataForPost( + postId: string, + ): Promise> { const links = postMediaLinks.get(postId) ?? []; const result: Array<{ media: MediaData }> = []; for (const link of links) { diff --git a/src/main/engine/EmbeddingEngine.ts b/src/main/engine/EmbeddingEngine.ts index 9dfe17a..438010d 100644 --- a/src/main/engine/EmbeddingEngine.ts +++ b/src/main/engine/EmbeddingEngine.ts @@ -89,7 +89,9 @@ export class EmbeddingEngine extends EventEmitter { // Lifecycle async initialize(): Promise { - if (this.pipeline) return; + if (this.pipeline) { + return; + } if (this.pipelineLoadPromise) { await this.pipelineLoadPromise; return; @@ -144,7 +146,9 @@ export class EmbeddingEngine extends EventEmitter { // Project switching async setProjectContext(projectId: string): Promise { - if (this.currentProjectId === projectId) return; + if (this.currentProjectId === projectId) { + return; + } // Save and unload current index if (this.index && this.currentProjectId) { @@ -163,8 +167,12 @@ export class EmbeddingEngine extends EventEmitter { } private async ensureIndexLoaded(): Promise { - if (this.index) return; - if (!this.currentProjectId) return; + if (this.index) { + return; + } + if (!this.currentProjectId) { + return; + } const { Index, MetricKind, ScalarKind } = await import('usearch'); this.index = new Index({ @@ -221,14 +229,16 @@ export class EmbeddingEngine extends EventEmitter { await this.initialize(); await this.ensureIndexLoaded(); - if (!this.index || !this.pipeline || !this.currentProjectId) return; + if (!this.index || !this.pipeline || !this.currentProjectId) { + return; + } const rawText = `${title}\n\n${content}`; const hash = this.computeHash(rawText); // Check if already indexed with same hash (no-op) const db = getDatabase().getLocal(); - const existing = await db + const existingKey = await db .select() .from(embeddingKeys) .where( @@ -236,15 +246,16 @@ export class EmbeddingEngine extends EventEmitter { eq(embeddingKeys.postId, postId), eq(embeddingKeys.projectId, this.currentProjectId), ), - ); + ) + .then((rows) => rows[0]); - if (existing.length > 0 && existing[0]!.contentHash === hash) { + if (existingKey && existingKey.contentHash === hash) { return; // Unchanged, skip re-embedding } // Remove old vector if exists - if (existing.length > 0) { - const oldLabel = BigInt(existing[0]!.label); + if (existingKey) { + const oldLabel = BigInt(existingKey.label); try { this.index.remove(oldLabel); } catch { @@ -286,10 +297,14 @@ export class EmbeddingEngine extends EventEmitter { async removePost(postId: string): Promise { await this.ensureIndexLoaded(); - if (!this.index || !this.currentProjectId) return; + if (!this.index || !this.currentProjectId) { + return; + } const label = this.postIdToLabel.get(postId); - if (label === undefined) return; + if (label === undefined) { + return; + } try { this.index.remove(label); @@ -313,28 +328,46 @@ export class EmbeddingEngine extends EventEmitter { async findSimilar(postId: string, k = 5): Promise { await this.ensureIndexLoaded(); - if (!this.index || !this.currentProjectId) return []; + if (!this.index || !this.currentProjectId) { + return []; + } - if (!this.postIdToLabel.has(postId)) return []; + if (!this.postIdToLabel.has(postId)) { + return []; + } // Guard against empty index (USearch throws on empty index search) - if (this.postIdToLabel.size < 2) return []; + if (this.postIdToLabel.size < 2) { + return []; + } // Get or compute vector for this post const vector = await this.getOrComputeVector(postId); - if (!vector) return []; + if (!vector) { + return []; + } // Search for k+1 (to exclude self) with HNSW const result = this.index.search(vector, k + 1, 0); - if (!result) return []; + if (!result) { + return []; + } const results: SimilarPost[] = []; for (let i = 0; i < result.keys.length; i++) { - const foundLabel = result.keys[i]!; + const foundLabel = result.keys[i]; + if (foundLabel === undefined) { + continue; + } const foundPostId = this.labelToPostId.get(foundLabel); - if (!foundPostId || foundPostId === postId) continue; + if (!foundPostId || foundPostId === postId) { + continue; + } - const distance = result.distances[i]!; + const distance = result.distances[i]; + if (distance === undefined) { + continue; + } // USearch cosine metric returns distance (0=identical), convert to similarity const similarity = Math.max(0, 1 - distance); results.push({ postId: foundPostId, similarity }); @@ -349,16 +382,24 @@ export class EmbeddingEngine extends EventEmitter { */ async computeSimilarities(sourcePostId: string, targetPostIds: string[]): Promise> { await this.ensureIndexLoaded(); - if (!this.index || !this.currentProjectId || targetPostIds.length === 0) return {}; + if (!this.index || !this.currentProjectId || targetPostIds.length === 0) { + return {}; + } const sourceVec = await this.getOrComputeVector(sourcePostId); - if (!sourceVec) return {}; + if (!sourceVec) { + return {}; + } const result: Record = {}; for (const targetId of targetPostIds) { - if (targetId === sourcePostId) continue; + if (targetId === sourcePostId) { + continue; + } const targetVec = await this.getOrComputeVector(targetId); - if (!targetVec) continue; + if (!targetVec) { + continue; + } result[targetId] = this.cosineSimilarity(sourceVec, targetVec); } return result; @@ -367,9 +408,11 @@ export class EmbeddingEngine extends EventEmitter { private cosineSimilarity(a: Float32Array, b: Float32Array): number { let dot = 0, normA = 0, normB = 0; for (let i = 0; i < a.length; i++) { - dot += a[i]! * b[i]!; - normA += a[i]! * a[i]!; - normB += b[i]! * b[i]!; + const valueA = a[i] ?? 0; + const valueB = b[i] ?? 0; + dot += valueA * valueB; + normA += valueA * valueA; + normB += valueB * valueB; } const denom = Math.sqrt(normA) * Math.sqrt(normB); return denom === 0 ? 0 : Math.max(0, dot / denom); @@ -379,9 +422,13 @@ export class EmbeddingEngine extends EventEmitter { async suggestTags(postId: string, excludeTags: string[]): Promise { const similar = await this.findSimilar(postId, 10); - if (similar.length === 0) return []; + if (similar.length === 0) { + return []; + } - if (!this.currentProjectId) return []; + if (!this.currentProjectId) { + return []; + } // Get tags for similar posts const similarPostIds = similar.map((s) => s.postId); @@ -396,11 +443,15 @@ export class EmbeddingEngine extends EventEmitter { for (const row of postRows) { const simItem = similar.find((s) => s.postId === row.id); - if (!simItem) continue; + if (!simItem) { + continue; + } const postTags: string[] = JSON.parse(row.tags || '[]'); for (const tag of postTags) { - if (excludeSet.has(tag.toLowerCase())) continue; + if (excludeSet.has(tag.toLowerCase())) { + continue; + } const current = tagScores.get(tag) || 0; tagScores.set(tag, current + simItem.similarity); } @@ -414,7 +465,9 @@ export class EmbeddingEngine extends EventEmitter { async findDuplicates(threshold = 0.92, onProgress?: (checked: number, total: number) => void): Promise { await this.ensureIndexLoaded(); - if (!this.index || !this.currentProjectId) return []; + if (!this.index || !this.currentProjectId) { + return []; + } const projectId = this.currentProjectId; const db = getDatabase().getLocal(); @@ -432,7 +485,9 @@ export class EmbeddingEngine extends EventEmitter { // Get post info for all indexed posts const allPostIds = Array.from(this.postIdToLabel.keys()); - if (allPostIds.length === 0) return []; + if (allPostIds.length === 0) { + return []; + } const postRows = await db .select({ @@ -453,9 +508,13 @@ export class EmbeddingEngine extends EventEmitter { const bodyCache = new Map(); const getBody = async (postId: string): Promise => { const cached = bodyCache.get(postId); - if (cached !== undefined) return cached; + if (cached !== undefined) { + return cached; + } const post = postMap.get(postId); - if (!post) { bodyCache.set(postId, ''); return ''; } + if (!post) { + bodyCache.set(postId, ''); return ''; + } // Draft content is in the DB; published content is on the filesystem if (post.content) { bodyCache.set(postId, post.content); @@ -480,30 +539,51 @@ export class EmbeddingEngine extends EventEmitter { const seenPairs = new Set(); for (let idx = 0; idx < allPostIds.length; idx++) { - const postId = allPostIds[idx]!; + const postId = allPostIds[idx]; + if (!postId) { + continue; + } onProgress?.(idx + 1, allPostIds.length); const vector = await this.getOrComputeVector(postId); - if (!vector) continue; + if (!vector) { + continue; + } const result = this.index.search(vector, 21, 0); - if (!result) continue; + if (!result) { + continue; + } for (let i = 0; i < result.keys.length; i++) { - const otherLabel = result.keys[i]!; + const otherLabel = result.keys[i]; + if (otherLabel === undefined) { + continue; + } const otherPostId = this.labelToPostId.get(otherLabel); - if (!otherPostId || otherPostId === postId) continue; + if (!otherPostId || otherPostId === postId) { + continue; + } - const distance = result.distances[i]!; + const distance = result.distances[i]; + if (distance === undefined) { + continue; + } const similarity = Math.max(0, 1 - distance); - if (similarity < threshold) continue; + if (similarity < threshold) { + continue; + } const key = this.pairKey(postId, otherPostId); - if (seenPairs.has(key) || dismissedSet.has(key)) continue; + if (seenPairs.has(key) || dismissedSet.has(key)) { + continue; + } seenPairs.add(key); const postA = postMap.get(postId); const postB = postMap.get(otherPostId); - if (!postA || !postB) continue; + if (!postA || !postB) { + continue; + } pairs.push({ postA: { @@ -537,14 +617,20 @@ export class EmbeddingEngine extends EventEmitter { } return pairs.sort((a, b) => { - if (a.exactMatch && !b.exactMatch) return -1; - if (!a.exactMatch && b.exactMatch) return 1; + if (a.exactMatch && !b.exactMatch) { + return -1; + } + if (!a.exactMatch && b.exactMatch) { + return 1; + } return b.similarity - a.similarity; }); } async dismissPair(postIdA: string, postIdB: string): Promise { - if (!this.currentProjectId) return; + if (!this.currentProjectId) { + return; + } const db = getDatabase().getLocal(); const [a, b] = this.sortedPairIds(postIdA, postIdB); await db.insert(dismissedDuplicatePairs).values({ @@ -557,12 +643,15 @@ export class EmbeddingEngine extends EventEmitter { } async dismissPairs(pairIds: Array<[string, string]>): Promise { - if (!this.currentProjectId) return; + if (!this.currentProjectId) { + return; + } const db = getDatabase().getLocal(); const now = new Date(); + const currentProjectId = this.currentProjectId; const rows = pairIds.map(([idA, idB]) => { const [a, b] = this.sortedPairIds(idA, idB); - return { id: uuidv4(), projectId: this.currentProjectId!, postIdA: a, postIdB: b, dismissedAt: now }; + return { id: uuidv4(), projectId: currentProjectId, postIdA: a, postIdB: b, dismissedAt: now }; }); // Insert in batches of 100 to avoid SQLite variable limits for (let i = 0; i < rows.length; i += 100) { @@ -573,7 +662,9 @@ export class EmbeddingEngine extends EventEmitter { // Indexing management async getIndexingProgress(): Promise<{ indexed: number; total: number }> { - if (!this.currentProjectId) return { indexed: 0, total: 0 }; + if (!this.currentProjectId) { + return { indexed: 0, total: 0 }; + } await this.ensureIndexLoaded(); const db = getDatabase().getLocal(); @@ -589,7 +680,9 @@ export class EmbeddingEngine extends EventEmitter { async reindexAll(onProgress?: (indexed: number, total: number) => void): Promise { await this.ensureIndexLoaded(); - if (!this.currentProjectId) return; + if (!this.currentProjectId) { + return; + } const db = getDatabase().getLocal(); @@ -616,7 +709,9 @@ export class EmbeddingEngine extends EventEmitter { async indexUnindexedPosts(onProgress?: (indexed: number, total: number) => void): Promise { await this.initialize(); await this.ensureIndexLoaded(); - if (!this.currentProjectId) return; + if (!this.currentProjectId) { + return; + } const db = getDatabase().getLocal(); const allPosts = await db @@ -688,7 +783,9 @@ export class EmbeddingEngine extends EventEmitter { clearTimeout(this.saveTimer); this.saveTimer = null; } - if (!this.index || !this.currentProjectId) return; + if (!this.index || !this.currentProjectId) { + return; + } const indexPath = this.deps.getIndexPath(this.currentProjectId); const dir = path.dirname(indexPath); @@ -697,7 +794,9 @@ export class EmbeddingEngine extends EventEmitter { } private scheduleSave(): void { - if (this.saveTimer) clearTimeout(this.saveTimer); + if (this.saveTimer) { + clearTimeout(this.saveTimer); + } this.saveTimer = setTimeout(() => { this.save().catch((err) => console.error('[EmbeddingEngine] save error:', err)); }, this.SAVE_DEBOUNCE_MS); @@ -711,14 +810,20 @@ export class EmbeddingEngine extends EventEmitter { */ private async getOrComputeVector(postId: string): Promise { const cached = this.vectorCache.get(postId); - if (cached) return cached; + if (cached) { + return cached; + } // Re-embed from post content await this.initialize(); - if (!this.pipeline || !this.currentProjectId) return null; + if (!this.pipeline || !this.currentProjectId) { + return null; + } const resolved = await this.resolvePostContent(postId); - if (!resolved) return null; + if (!resolved) { + return null; + } const rawText = `${resolved.title}\n\n${resolved.content}`; const text = `query: ${rawText}`; @@ -728,7 +833,9 @@ export class EmbeddingEngine extends EventEmitter { } private async embedText(text: string): Promise { - if (!this.pipeline) throw new Error('EmbeddingEngine not initialized'); + if (!this.pipeline) { + throw new Error('EmbeddingEngine not initialized'); + } return this.pipeline.embed(text); } @@ -737,15 +844,24 @@ export class EmbeddingEngine extends EventEmitter { * Draft posts have content in the DB; published posts have it on the filesystem. */ private async resolvePostContent(postId: string): Promise<{ title: string; content: string } | null> { - if (!this.currentProjectId) return null; + if (!this.currentProjectId) { + return null; + } const db = getDatabase().getLocal(); const rows = await db .select({ title: posts.title, content: posts.content, filePath: posts.filePath }) .from(posts) .where(and(eq(posts.id, postId), eq(posts.projectId, this.currentProjectId))); - if (rows.length === 0) return null; - const post = rows[0]!; - if (post.content) return { title: post.title, content: post.content }; + if (rows.length === 0) { + return null; + } + const post = rows[0]; + if (!post) { + return null; + } + if (post.content) { + return { title: post.title, content: post.content }; + } if (post.filePath) { try { const raw = await fs.readFile(post.filePath, 'utf-8'); diff --git a/src/main/engine/GenerationPostSnapshotService.ts b/src/main/engine/GenerationPostSnapshotService.ts index 931d845..2c3efc9 100644 --- a/src/main/engine/GenerationPostSnapshotService.ts +++ b/src/main/engine/GenerationPostSnapshotService.ts @@ -15,7 +15,9 @@ async function resolvePublishedVersions( postEngine: GenerationSnapshotPostEngine, ids: string[], ): Promise> { - if (ids.length === 0) return new Map(); + if (ids.length === 0) { + return new Map(); + } if (postEngine.getPublishedVersionsBulk) { return postEngine.getPublishedVersionsBulk(ids); @@ -29,7 +31,9 @@ async function resolvePublishedVersions( }), ); for (const { id, version } of entries) { - if (version) result.set(id, version); + if (version) { + result.set(id, version); + } } return result; } @@ -42,8 +46,12 @@ export async function loadPublishedGenerationSets( const draftCandidates = await postEngine.getPostsFiltered({ status: 'draft' }); const allIds = new Set(); - for (const p of publishedCandidates) allIds.add(p.id); - for (const p of draftCandidates) allIds.add(p.id); + for (const p of publishedCandidates) { + allIds.add(p.id); + } + for (const p of draftCandidates) { + allIds.add(p.id); + } const publishedVersions = await resolvePublishedVersions(postEngine, Array.from(allIds)); diff --git a/src/main/engine/GenerationRouteRendererFactory.ts b/src/main/engine/GenerationRouteRendererFactory.ts index ca31d1b..40efac5 100644 --- a/src/main/engine/GenerationRouteRendererFactory.ts +++ b/src/main/engine/GenerationRouteRendererFactory.ts @@ -3,7 +3,8 @@ import type { CategoryRenderSettings, HtmlRewriteContext } from './PageRenderer' import { buildCanonicalPostPath, mapToRecord } from './PageRenderer'; import type { MenuDocument } from './MenuEngine'; import type { ProjectMetadata } from './MetaEngine'; -import type { PostData } from './PostEngine'; +import type { PostData, PostFilter } from './PostEngine'; +import type { MediaData } from './MediaEngine'; import type { PicoThemeName } from '../shared/picoThemes'; import type { CategoryMetadata } from './BlogGenerationEngine'; import { PreviewServer } from './PreviewServer'; @@ -63,7 +64,7 @@ export function createPreviewBackedGenerationRouteRenderer(params: { languagePrefix?: string; engines: { postEngine: { - getPostsFiltered: (filter: Parameters[1] extends never ? never : any) => Promise; + getPostsFiltered: (filter: PostFilter) => Promise; getPublishedVersion: (postId: string) => Promise; findPublishedBySlug?: (slug: string, dateFilter?: { year: number; month: number }) => Promise; getPost: (postId: string) => Promise; @@ -74,7 +75,7 @@ export function createPreviewBackedGenerationRouteRenderer(params: { setProjectContext: (projectId: string, dataDir?: string) => void; }; mediaEngine: { - getAllMedia: () => Promise; + getAllMedia: () => Promise; setProjectContext?: (projectId: string, dataDir?: string, internalDir?: string) => void; }; postMediaEngine: { @@ -185,7 +186,9 @@ export function createPreviewBackedGenerationRouteRenderer(params: { }); } - if (!match) return null; + if (!match) { + return null; + } // Lazily resolve content from file when needed if (!match.content) { @@ -206,20 +209,22 @@ export function createPreviewBackedGenerationRouteRenderer(params: { return match; }, getPost: (postId: string) => params.engines.postEngine.getPost(postId), - getPostTranslation: params.engines.postEngine.getPostTranslation - ? (postId: string, language: string) => params.engines.postEngine.getPostTranslation!(postId, language) - : undefined, + getPostTranslation: params.engines.postEngine.getPostTranslation, hasPublishedVersion: (postId: string) => params.engines.postEngine.hasPublishedVersion(postId), getLinkedBy: params.engines.postEngine.getAllBacklinks ? (() => { - const backlinksCachePromise = params.engines.postEngine.getAllBacklinks!(); - return async (postId: string) => { - const backlinksMap = await backlinksCachePromise; - return backlinksMap.get(postId) ?? []; - }; - })() + const getAllBacklinks = params.engines.postEngine.getAllBacklinks; + if (!getAllBacklinks) { + return undefined; + } + const backlinksCachePromise = getAllBacklinks(); + return async (postId: string) => { + const backlinksMap = await backlinksCachePromise; + return backlinksMap.get(postId) ?? []; + }; + })() : params.engines.postEngine.getLinkedBy - ? (postId: string) => params.engines.postEngine.getLinkedBy!(postId) + ? params.engines.postEngine.getLinkedBy : undefined, setProjectContext: (projectId: string, dataDir?: string) => { params.engines.postEngine.setProjectContext(projectId, dataDir); diff --git a/src/main/engine/GenerationSitemapFeedService.ts b/src/main/engine/GenerationSitemapFeedService.ts index 444374f..b263b82 100644 --- a/src/main/engine/GenerationSitemapFeedService.ts +++ b/src/main/engine/GenerationSitemapFeedService.ts @@ -68,7 +68,7 @@ function buildCanonicalPreviewPath(createdAt: Date, slug: string): string { } function escapeXml(value: unknown): string { - const str = typeof value === 'string' ? value : value == null ? '' : String(value); + const str = typeof value === 'string' ? value : value === null || value === undefined ? '' : String(value); return str .replace(/&/g, '&') .replace(/ yearMonths.get(ymKey)!) { + const existingYearMonth = yearMonths.get(ymKey); + if (!existingYearMonth || updatedAt > existingYearMonth) { yearMonths.set(ymKey, updatedAt); } - if (!years.has(year) || updatedAt > years.get(year)!) { + const existingYear = years.get(year); + if (!existingYear || updatedAt > existingYear) { years.set(year, updatedAt); } - if (!yearMonthDays.has(ymdKey) || updatedAt > yearMonthDays.get(ymdKey)!) { + const existingYearMonthDay = yearMonthDays.get(ymdKey); + if (!existingYearMonthDay || updatedAt > existingYearMonthDay) { yearMonthDays.set(ymdKey, updatedAt); } } @@ -405,7 +412,9 @@ export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): Sitema ` ${escapeXml(permalink)}`, ` ${(post.publishedAt || post.updatedAt).toUTCString()}`, post.author ? ` ${escapeXml(post.author)}` : null, - (post as { language?: string }).language ? ` ${escapeXml((post as { language?: string }).language!)}` : null, + (post as { language?: string }).language + ? ` ${escapeXml((post as { language?: string }).language)}` + : null, ` `, ` `, ...categories.map((entry) => ` ${entry}`), @@ -440,7 +449,10 @@ export function buildSitemapAndFeeds(params: BuildSitemapAndFeedsParams): Sitema ...(post.categories || []).map((category) => ``), ]; - const postLanguageAttr = (post as { language?: string }).language ? ` xml:lang="${escapeXml((post as { language?: string }).language!)}"` : ''; + const postLanguage = (post as { language?: string }).language; + const postLanguageAttr = postLanguage + ? ` xml:lang="${escapeXml(postLanguage)}"` + : ''; return [ ` `, @@ -597,9 +609,13 @@ export function buildMultiLanguageSitemap(params: MultiLanguageSitemapParams): s const allPublishedPosts = [...translatablePosts, ...doNotTranslatePosts]; for (const post of allPublishedPosts) { const categories = Array.isArray(post.categories) ? post.categories : []; - if (!categories.includes('page')) continue; + if (!categories.includes('page')) { + continue; + } const trimmedSlug = (post.slug || '').replace(/^\/+|\/+$/g, ''); - if (trimmedSlug.length === 0) continue; + if (trimmedSlug.length === 0) { + continue; + } const isTranslatable = !(post as PostData & { doNotTranslate?: boolean }).doNotTranslate; const langs = isTranslatable ? allLanguages : [mainLanguage]; urls.push(buildMultiLanguageSitemapUrl( diff --git a/src/main/engine/GenerationWorkerData.ts b/src/main/engine/GenerationWorkerData.ts index ad6101e..86bab95 100644 --- a/src/main/engine/GenerationWorkerData.ts +++ b/src/main/engine/GenerationWorkerData.ts @@ -7,8 +7,13 @@ * Maps to arrays-of-tuples so the data survives the boundary. */ import type { PostData } from './PostEngine'; +import type { PublishedTranslationVariant } from './BlogGenerationEngine'; import type { MediaData } from './MediaEngine'; -import type { CategoryMetadata, BlogGenerationOptions, BlogGenerationSection } from './BlogGenerationEngine'; +import type { + CategoryMetadata, + BlogGenerationOptions, + BlogGenerationSection, +} from './BlogGenerationEngine'; import type { CategoryRenderSettings } from './PageRenderer'; import type { MenuDocument } from './MenuEngine'; import type { PicoThemeName } from '../shared/picoThemes'; @@ -105,7 +110,10 @@ export interface GenerationWorkerTask { mediaItems: SerializedMediaData[]; /** Pre-resolved backlinks map: postId → linked-by entries. */ - backlinksMap: Record>; + backlinksMap: Record< + string, + Array<{ id: string; title: string; slug: string }> + >; options: SerializedBlogGenerationOptions; maxPostsPerPage: number; @@ -118,7 +126,9 @@ export interface GenerationWorkerTask { postFilePathEntries: Array<[string, string]>; /** Post-media links: [postId, [{mediaId, sortOrder}]] tuples for gallery/album macros. */ - postMediaLinksEntries: Array<[string, Array<{ mediaId: string; sortOrder: number }>]>; + postMediaLinksEntries: Array< + [string, Array<{ mediaId: string; sortOrder: number }>] + >; /** Language prefix for subtree generation, e.g. "/fr". */ languagePrefix?: string; @@ -190,9 +200,20 @@ export function serializePostData(post: PostData): SerializedPostData { language: post.language, doNotTranslate: post.doNotTranslate, templateSlug: post.templateSlug, - createdAt: post.createdAt instanceof Date ? post.createdAt.toISOString() : String(post.createdAt), - updatedAt: post.updatedAt instanceof Date ? post.updatedAt.toISOString() : String(post.updatedAt), - publishedAt: post.publishedAt instanceof Date ? post.publishedAt.toISOString() : post.publishedAt ? String(post.publishedAt) : undefined, + createdAt: + post.createdAt instanceof Date + ? post.createdAt.toISOString() + : String(post.createdAt), + updatedAt: + post.updatedAt instanceof Date + ? post.updatedAt.toISOString() + : String(post.updatedAt), + publishedAt: + post.publishedAt instanceof Date + ? post.publishedAt.toISOString() + : post.publishedAt + ? String(post.publishedAt) + : undefined, tags: post.tags ?? [], categories: post.categories ?? [], availableLanguages: post.availableLanguages ?? [], @@ -204,9 +225,13 @@ export function serializePostData(post: PostData): SerializedPostData { translationCanonicalLanguage?: string; translationFilePath?: string; }; - if (variant.translationSourceSlug) serialized.translationSourceSlug = variant.translationSourceSlug; - if (variant.translationCanonicalLanguage) serialized.translationCanonicalLanguage = variant.translationCanonicalLanguage; - if (variant.translationFilePath) serialized.translationFilePath = variant.translationFilePath; + if (variant.translationSourceSlug) + {serialized.translationSourceSlug = variant.translationSourceSlug;} + if (variant.translationCanonicalLanguage) + {serialized.translationCanonicalLanguage = + variant.translationCanonicalLanguage;} + if (variant.translationFilePath) + {serialized.translationFilePath = variant.translationFilePath;} return serialized; } @@ -226,7 +251,9 @@ export function deserializePostData(serialized: SerializedPostData): PostData { templateSlug: serialized.templateSlug, createdAt: new Date(serialized.createdAt), updatedAt: new Date(serialized.updatedAt), - publishedAt: serialized.publishedAt ? new Date(serialized.publishedAt) : undefined, + publishedAt: serialized.publishedAt + ? new Date(serialized.publishedAt) + : undefined, tags: serialized.tags ?? [], categories: serialized.categories ?? [], availableLanguages: serialized.availableLanguages ?? [], @@ -234,13 +261,16 @@ export function deserializePostData(serialized: SerializedPostData): PostData { // Re-attach translation variant fields if (serialized.translationSourceSlug) { - (post as any).translationSourceSlug = serialized.translationSourceSlug; + (post as PublishedTranslationVariant).translationSourceSlug = + serialized.translationSourceSlug; } if (serialized.translationCanonicalLanguage) { - (post as any).translationCanonicalLanguage = serialized.translationCanonicalLanguage; + (post as PublishedTranslationVariant).translationCanonicalLanguage = + serialized.translationCanonicalLanguage; } if (serialized.translationFilePath) { - (post as any).translationFilePath = serialized.translationFilePath; + (post as PublishedTranslationVariant).translationFilePath = + serialized.translationFilePath; } return post; @@ -260,15 +290,23 @@ export function serializeMediaItem(media: MediaData): SerializedMediaData { caption: media.caption, author: media.author, language: media.language, - createdAt: media.createdAt instanceof Date ? media.createdAt.toISOString() : String(media.createdAt), - updatedAt: media.updatedAt instanceof Date ? media.updatedAt.toISOString() : String(media.updatedAt), + createdAt: + media.createdAt instanceof Date + ? media.createdAt.toISOString() + : String(media.createdAt), + updatedAt: + media.updatedAt instanceof Date + ? media.updatedAt.toISOString() + : String(media.updatedAt), tags: media.tags ?? [], linkedPostIds: media.linkedPostIds, availableLanguages: media.availableLanguages ?? [], }; } -export function deserializeMediaItem(serialized: SerializedMediaData): MediaData { +export function deserializeMediaItem( + serialized: SerializedMediaData, +): MediaData { return { id: serialized.id, filename: serialized.filename, @@ -290,7 +328,9 @@ export function deserializeMediaItem(serialized: SerializedMediaData): MediaData }; } -export function serializeBlogGenerationOptions(options: BlogGenerationOptions): SerializedBlogGenerationOptions { +export function serializeBlogGenerationOptions( + options: BlogGenerationOptions, +): SerializedBlogGenerationOptions { return { projectId: options.projectId, projectName: options.projectName, @@ -307,21 +347,37 @@ export function serializeBlogGenerationOptions(options: BlogGenerationOptions): } /** Serialize a Map to an array of [K, SerializedPostData[]] tuples. */ -export function serializePostMap(map: Map): Array<[K, SerializedPostData[]]> { - return Array.from(map.entries()).map(([key, posts]) => [key, posts.map(serializePostData)]); +export function serializePostMap( + map: Map, +): Array<[K, SerializedPostData[]]> { + return Array.from(map.entries()).map(([key, posts]) => [ + key, + posts.map(serializePostData), + ]); } /** Deserialize an array of [K, SerializedPostData[]] tuples to a Map. */ -export function deserializePostMap(entries: Array<[K, SerializedPostData[]]>): Map { - return new Map(entries.map(([key, posts]) => [key, posts.map(deserializePostData)])); +export function deserializePostMap( + entries: Array<[K, SerializedPostData[]]>, +): Map { + return new Map( + entries.map(([key, posts]) => [key, posts.map(deserializePostData)]), + ); } /** Serialize a Map to an array of [K, string] tuples. */ -export function serializeDateMap(map: Map): Array<[K, string]> { - return Array.from(map.entries()).map(([key, date]) => [key, date.toISOString()]); +export function serializeDateMap( + map: Map, +): Array<[K, string]> { + return Array.from(map.entries()).map(([key, date]) => [ + key, + date.toISOString(), + ]); } /** Deserialize an array of [K, string] tuples to a Map. */ -export function deserializeDateMap(entries: Array<[K, string]>): Map { +export function deserializeDateMap( + entries: Array<[K, string]>, +): Map { return new Map(entries.map(([key, iso]) => [key, new Date(iso)])); } diff --git a/src/main/engine/GenerationWorkerPool.ts b/src/main/engine/GenerationWorkerPool.ts index ea8943b..8436b44 100644 --- a/src/main/engine/GenerationWorkerPool.ts +++ b/src/main/engine/GenerationWorkerPool.ts @@ -87,26 +87,26 @@ export class GenerationWorkerPool { const msg = raw as WorkerOutboundMessage; switch (msg.type) { - case 'progress': - onProgress(msg.message); - break; + case 'progress': + onProgress(msg.message); + break; - case 'result': - totalPages += msg.pagesGenerated; - if (msg.hashUpdates) { - allHashUpdates.push(...msg.hashUpdates); - } - activeWorkers--; - void worker.terminate(); - startNextWorker(); - break; + case 'result': + totalPages += msg.pagesGenerated; + if (msg.hashUpdates) { + allHashUpdates.push(...msg.hashUpdates); + } + activeWorkers--; + void worker.terminate(); + startNextWorker(); + break; - case 'error': - errors.push({ taskId: msg.taskId, error: msg.error }); - activeWorkers--; - void worker.terminate(); - startNextWorker(); - break; + case 'error': + errors.push({ taskId: msg.taskId, error: msg.error }); + activeWorkers--; + void worker.terminate(); + startNextWorker(); + break; } }); diff --git a/src/main/engine/GitEngine.ts b/src/main/engine/GitEngine.ts index 73e53eb..96a1caa 100644 --- a/src/main/engine/GitEngine.ts +++ b/src/main/engine/GitEngine.ts @@ -337,9 +337,15 @@ export class GitEngine { } private getProviderLabel(provider: GitProvider): string { - if (provider === 'github') return 'GitHub'; - if (provider === 'gitlab') return 'GitLab'; - if (provider === 'gitea-forgejo') return 'Gitea/Forgejo'; + if (provider === 'github') { + return 'GitHub'; + } + if (provider === 'gitlab') { + return 'GitLab'; + } + if (provider === 'gitea-forgejo') { + return 'Gitea/Forgejo'; + } return 'Unknown'; } diff --git a/src/main/engine/ImportAnalysisEngine.ts b/src/main/engine/ImportAnalysisEngine.ts index 2577b8a..587ded1 100644 --- a/src/main/engine/ImportAnalysisEngine.ts +++ b/src/main/engine/ImportAnalysisEngine.ts @@ -5,8 +5,8 @@ import TurndownService from 'turndown'; import { getDatabase } from '../database'; import { posts, media, tags } from '../database/schema'; import { eq } from 'drizzle-orm'; -import type { WxrData, WxrPost, WxrMedia, WxrSiteInfo, WxrCategory, WxrTag } from './WxrParser'; -import { getMacroConfigMap, type MacroConfig } from '../config/macroConfig'; +import type { WxrData, WxrPost, WxrMedia, WxrSiteInfo } from './WxrParser'; +import { getMacroConfigMap } from '../config/macroConfig'; export type PostAnalysisStatus = 'new' | 'update' | 'conflict' | 'content-duplicate'; export type MediaAnalysisStatus = 'new' | 'update' | 'conflict' | 'content-duplicate' | 'missing'; @@ -202,10 +202,14 @@ export class ImportAnalysisEngine { // WordPress often uses title="name" with alt="" this.turndown.addRule('imageWithTitle', { filter: (node) => { - if (node.nodeName !== 'IMG') return false; + if (node.nodeName !== 'IMG') { + return false; + } // Check if this image is NOT inside an tag (those are handled by linkedImage rule) const parent = node.parentNode; - if (parent?.nodeName === 'A') return false; + if (parent?.nodeName === 'A') { + return false; + } // Only match if alt is empty but title exists const img = node as HTMLImageElement; const alt = img.getAttribute('alt') || ''; @@ -225,16 +229,20 @@ export class ImportAnalysisEngine { this.turndown.addRule('linkedImage', { filter: (node) => { // Match tags that contain only an (possibly with whitespace) - if (node.nodeName !== 'A') return false; + if (node.nodeName !== 'A') { + return false; + } const children = Array.from(node.childNodes).filter( - child => !(child.nodeType === 3 && !child.textContent?.trim()) + child => !(child.nodeType === 3 && !child.textContent?.trim()), ); return children.length === 1 && children[0].nodeName === 'IMG'; }, replacement: (_content, node) => { const anchor = node as HTMLAnchorElement; const img = anchor.querySelector('img'); - if (!img) return ''; + if (!img) { + return ''; + } const href = anchor.getAttribute('href') || ''; const imgSrc = img.getAttribute('src') || ''; @@ -271,7 +279,9 @@ export class ImportAnalysisEngine { // Custom rule for Flash embeds - replace with placeholder text this.turndown.addRule('flashEmbed', { filter: (node) => { - if (node.nodeName !== 'EMBED') return false; + if (node.nodeName !== 'EMBED') { + return false; + } const embed = node as HTMLEmbedElement; const type = embed.getAttribute('type') || ''; const src = embed.getAttribute('src') || ''; @@ -593,7 +603,9 @@ export class ImportAnalysisEngine { } private convertToMarkdown(html: string): string { - if (!html || !html.trim()) return ''; + if (!html || !html.trim()) { + return ''; + } // Preprocess: Wrap standalone blocks containing newlines in
 tags
     const withCodeBlocks = this.wrapMultilineCode(html);
     // Preprocess: Convert newlines within text to 
tags to preserve line breaks @@ -629,14 +641,16 @@ export class ImportAnalysisEngine { * - Wraps content in

tags if it starts with plain text */ private preserveLineBreaks(html: string): string { - if (!html || !html.trim()) return html; + if (!html || !html.trim()) { + return html; + } // Check if content starts with a tag or plain text const startsWithTag = /^\s* blocks from having their newlines modified const preBlocks: string[] = []; - let protectedHtml = html.replace(/

([\s\S]*?)<\/pre>/g, (match) => {
+    const protectedHtml = html.replace(/
([\s\S]*?)<\/pre>/g, (match) => {
       const placeholder = `__PRE_BLOCK_${preBlocks.length}__`;
       preBlocks.push(match);
       return placeholder;
@@ -659,7 +673,9 @@ export class ImportAnalysisEngine {
       
       // Also handle newlines at the start (before any tags)
       processed = processed.replace(/^([^<]+)/g, (match, textContent: string) => {
-        if (!textContent.trim()) return match;
+        if (!textContent.trim()) {
+          return match;
+        }
         return textContent.replace(/\n/g, '
'); }); @@ -723,7 +739,9 @@ export class ImportAnalysisEngine { * - without newlines (inline code) */ private wrapMultilineCode(html: string): string { - if (!html) return html; + if (!html) { + return html; + } // Match blocks containing newlines that are NOT inside
     // Use a regex that captures the full ... content including any embedded HTML
@@ -757,7 +775,9 @@ export class ImportAnalysisEngine {
 
     // Process each post/page
     for (const post of posts) {
-      if (!post.content) continue;
+      if (!post.content) {
+        continue;
+      }
       
       const shortcodes = this.parseShortcodes(post.content);
       
diff --git a/src/main/engine/ImportDefinitionEngine.ts b/src/main/engine/ImportDefinitionEngine.ts
index 031a0f7..a077fb2 100644
--- a/src/main/engine/ImportDefinitionEngine.ts
+++ b/src/main/engine/ImportDefinitionEngine.ts
@@ -73,10 +73,12 @@ export class ImportDefinitionEngine {
       .from(importDefinitions)
       .where(and(
         eq(importDefinitions.id, id),
-        eq(importDefinitions.projectId, this.currentProjectId)
+        eq(importDefinitions.projectId, this.currentProjectId),
       ));
 
-    if (rows.length === 0) return null;
+    if (rows.length === 0) {
+      return null;
+    }
 
     return this.rowToData(rows[0]);
   }
@@ -95,11 +97,13 @@ export class ImportDefinitionEngine {
 
   async updateDefinition(
     id: string,
-    updates: Partial>
+    updates: Partial>,
   ): Promise {
     // Check existence and ownership
     const existing = await this.getDefinition(id);
-    if (!existing) return null;
+    if (!existing) {
+      return null;
+    }
 
     const db = this.getDb();
 
@@ -128,7 +132,7 @@ export class ImportDefinitionEngine {
       .set(updateData)
       .where(and(
         eq(importDefinitions.id, id),
-        eq(importDefinitions.projectId, this.currentProjectId)
+        eq(importDefinitions.projectId, this.currentProjectId),
       ));
 
     return this.getDefinition(id);
@@ -137,7 +141,9 @@ export class ImportDefinitionEngine {
   async deleteDefinition(id: string): Promise {
     // Check existence and ownership
     const existing = await this.getDefinition(id);
-    if (!existing) return false;
+    if (!existing) {
+      return false;
+    }
 
     const db = this.getDb();
 
@@ -145,7 +151,7 @@ export class ImportDefinitionEngine {
       .delete(importDefinitions)
       .where(and(
         eq(importDefinitions.id, id),
-        eq(importDefinitions.projectId, this.currentProjectId)
+        eq(importDefinitions.projectId, this.currentProjectId),
       ));
 
     return true;
diff --git a/src/main/engine/ImportExecutionEngine.ts b/src/main/engine/ImportExecutionEngine.ts
index a4c9a61..ba26f60 100644
--- a/src/main/engine/ImportExecutionEngine.ts
+++ b/src/main/engine/ImportExecutionEngine.ts
@@ -17,21 +17,19 @@ import matter from 'gray-matter';
 import { app } from 'electron';
 import TurndownService from 'turndown';
 import { getDatabase } from '../database';
-import { posts, media, NewPost, NewMedia } from '../database/schema';
+import { posts, NewPost } from '../database/schema';
 import { eq } from 'drizzle-orm';
 import type { TagEngine } from './TagEngine';
 import type { PostEngine, PostData } from './PostEngine';
-import type { MediaEngine, MediaData } from './MediaEngine';
+import type { MediaEngine } from './MediaEngine';
 import type { PostMediaEngine } from './PostMediaEngine';
 import type {
   ImportAnalysisReport,
   AnalyzedPost,
   AnalyzedMedia,
-  AnalyzedCategory,
-  AnalyzedTag,
   ImportConflictResolution,
 } from './ImportAnalysisEngine';
-import type { WxrPost, WxrMedia } from './WxrParser';
+import type { WxrPost } from './WxrParser';
 
 export interface ImportExecutionOptions {
   /** Path to the WordPress uploads folder for media files */
@@ -129,10 +127,14 @@ export class ImportExecutionEngine extends EventEmitter {
     // WordPress often uses title="name" with alt=""
     this.turndown.addRule('imageWithTitle', {
       filter: (node) => {
-        if (node.nodeName !== 'IMG') return false;
+        if (node.nodeName !== 'IMG') {
+          return false;
+        }
         // Check if this image is NOT inside an  tag (those are handled by linkedImage rule)
         const parent = node.parentNode;
-        if (parent?.nodeName === 'A') return false;
+        if (parent?.nodeName === 'A') {
+          return false;
+        }
         // Only match if alt is empty but title exists
         const img = node as HTMLImageElement;
         const alt = img.getAttribute('alt') || '';
@@ -152,16 +154,20 @@ export class ImportExecutionEngine extends EventEmitter {
     this.turndown.addRule('linkedImage', {
       filter: (node) => {
         // Match  tags that contain only an  (possibly with whitespace)
-        if (node.nodeName !== 'A') return false;
+        if (node.nodeName !== 'A') {
+          return false;
+        }
         const children = Array.from(node.childNodes).filter(
-          child => !(child.nodeType === 3 && !child.textContent?.trim())
+          child => !(child.nodeType === 3 && !child.textContent?.trim()),
         );
         return children.length === 1 && children[0].nodeName === 'IMG';
       },
       replacement: (_content, node) => {
         const anchor = node as HTMLAnchorElement;
         const img = anchor.querySelector('img');
-        if (!img) return '';
+        if (!img) {
+          return '';
+        }
 
         const href = anchor.getAttribute('href') || '';
         const imgSrc = img.getAttribute('src') || '';
@@ -198,7 +204,9 @@ export class ImportExecutionEngine extends EventEmitter {
     // Custom rule for Flash embeds - replace with placeholder text
     this.turndown.addRule('flashEmbed', {
       filter: (node) => {
-        if (node.nodeName !== 'EMBED') return false;
+        if (node.nodeName !== 'EMBED') {
+          return false;
+        }
         const embed = node as HTMLEmbedElement;
         const type = embed.getAttribute('type') || '';
         const src = embed.getAttribute('src') || '';
@@ -221,7 +229,9 @@ export class ImportExecutionEngine extends EventEmitter {
   }
 
   private getBaseDir(): string {
-    if (this.dataDir) return this.dataDir;
+    if (this.dataDir) {
+      return this.dataDir;
+    }
     const userDataPath = app.getPath('userData');
     return path.join(userDataPath, 'projects', this.currentProjectId);
   }
@@ -259,7 +269,7 @@ export class ImportExecutionEngine extends EventEmitter {
    */
   async executeImport(
     report: ImportAnalysisReport,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
     const result: ImportExecutionResult = {
       success: true,
@@ -313,7 +323,7 @@ export class ImportExecutionEngine extends EventEmitter {
    * - Otherwise: use the name and mark for creation
    */
   private buildTaxonomyMapping(
-    items: Array<{ name: string; existsInProject: boolean; mappedTo?: string }>
+    items: Array<{ name: string; existsInProject: boolean; mappedTo?: string }>,
   ): Map {
     const mapping = new Map();
 
@@ -342,7 +352,7 @@ export class ImportExecutionEngine extends EventEmitter {
     tagMapping: Map,
     categoryMapping: Map,
     result: ImportExecutionResult,
-    progress: (phase: string, current: number, total: number, detail?: string) => void
+    progress: (phase: string, current: number, total: number, detail?: string) => void,
   ): Promise {
     const tagEngine = this.tagEngine;
     tagEngine.setProjectContext(this.currentProjectId);
@@ -360,7 +370,7 @@ export class ImportExecutionEngine extends EventEmitter {
           await tagEngine.createTag({ name: mapping.resolved });
           result.tags.created++;
           progress('tags', current, total, `Created tag: ${mapping.resolved}`);
-        } catch (error) {
+        } catch {
           // Tag might already exist (race condition or duplicate in list)
           result.tags.skipped++;
         }
@@ -379,7 +389,7 @@ export class ImportExecutionEngine extends EventEmitter {
           await tagEngine.createTag({ name: mapping.resolved });
           result.tags.created++;
           progress('tags', current, total, `Created category tag: ${mapping.resolved}`);
-        } catch (error) {
+        } catch {
           result.tags.skipped++;
         }
       } else {
@@ -397,7 +407,7 @@ export class ImportExecutionEngine extends EventEmitter {
     categoryMapping: Map,
     result: ImportExecutionResult,
     options: ImportExecutionOptions,
-    progress: (phase: string, current: number, total: number, detail?: string) => void
+    progress: (phase: string, current: number, total: number, detail?: string) => void,
   ): Promise {
     // Filter to only actual posts (postType === 'post'), skip nav_menu_item, revision, etc.
     const postsToImport = report.posts.items.filter(item => item.wxrPost.postType === 'post');
@@ -433,10 +443,8 @@ export class ImportExecutionEngine extends EventEmitter {
     tagMapping: Map,
     categoryMapping: Map,
     result: ImportExecutionResult,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
-    const wxrPost = analyzed.wxrPost;
-
     // Handle different analysis statuses
     if (analyzed.status === 'content-duplicate') {
       // Skip content duplicates
@@ -472,7 +480,7 @@ export class ImportExecutionEngine extends EventEmitter {
     tagMapping: Map,
     categoryMapping: Map,
     result: ImportExecutionResult,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
     const postEngine = this.postEngine;
 
@@ -504,7 +512,7 @@ export class ImportExecutionEngine extends EventEmitter {
     tagMapping: Map,
     categoryMapping: Map,
     result: ImportExecutionResult,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
     const wxrPost = analyzed.wxrPost;
     const db = getDatabase().getLocal();
@@ -575,7 +583,7 @@ export class ImportExecutionEngine extends EventEmitter {
     result: ImportExecutionResult,
     options: ImportExecutionOptions,
     status: 'draft' | 'published',
-    overrideSlug?: string
+    overrideSlug?: string,
   ): Promise {
     const wxrPost = analyzed.wxrPost;
     const db = getDatabase().getLocal();
@@ -681,9 +689,15 @@ export class ImportExecutionEngine extends EventEmitter {
       categories: post.categories,
     };
 
-    if (post.excerpt) metadata.excerpt = post.excerpt;
-    if (post.author) metadata.author = post.author;
-    if (post.publishedAt) metadata.publishedAt = post.publishedAt.toISOString();
+    if (post.excerpt) {
+      metadata.excerpt = post.excerpt;
+    }
+    if (post.author) {
+      metadata.author = post.author;
+    }
+    if (post.publishedAt) {
+      metadata.publishedAt = post.publishedAt.toISOString();
+    }
 
     const postsDir = this.getPostsDirForDate(post.createdAt);
     await fs.mkdir(postsDir, { recursive: true });
@@ -702,7 +716,7 @@ export class ImportExecutionEngine extends EventEmitter {
     report: ImportAnalysisReport,
     result: ImportExecutionResult,
     options: ImportExecutionOptions,
-    progress: (phase: string, current: number, total: number, detail?: string) => void
+    progress: (phase: string, current: number, total: number, detail?: string) => void,
   ): Promise {
     const total = report.media.items.length;
 
@@ -730,7 +744,7 @@ export class ImportExecutionEngine extends EventEmitter {
   private async importMediaFile(
     analyzed: AnalyzedMedia,
     result: ImportExecutionResult,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
     const wxrMedia = analyzed.wxrMedia;
 
@@ -822,7 +836,7 @@ export class ImportExecutionEngine extends EventEmitter {
     analyzed: AnalyzedMedia,
     existingMediaId: string,
     result: ImportExecutionResult,
-    options: ImportExecutionOptions
+    options: ImportExecutionOptions,
   ): Promise {
     const wxrMedia = analyzed.wxrMedia;
 
@@ -882,7 +896,7 @@ export class ImportExecutionEngine extends EventEmitter {
     categoryMapping: Map,
     result: ImportExecutionResult,
     options: ImportExecutionOptions,
-    progress: (phase: string, current: number, total: number, detail?: string) => void
+    progress: (phase: string, current: number, total: number, detail?: string) => void,
   ): Promise {
     const total = report.pages.items.length;
 
@@ -926,7 +940,9 @@ export class ImportExecutionEngine extends EventEmitter {
    * Convert HTML to Markdown using Turndown
    */
   private convertToMarkdown(html: string): string {
-    if (!html || !html.trim()) return '';
+    if (!html || !html.trim()) {
+      return '';
+    }
 
     // Preprocess: Wrap standalone  blocks containing newlines in 
 tags
     // This must happen BEFORE preserveLineBreaks to prevent newlines from becoming 
@@ -976,14 +992,16 @@ export class ImportExecutionEngine extends EventEmitter { * - Wraps content in

tags if it starts with plain text */ private preserveLineBreaks(html: string): string { - if (!html || !html.trim()) return html; + if (!html || !html.trim()) { + return html; + } // Check if content starts with a tag or plain text const startsWithTag = /^\s* blocks from having their newlines modified const preBlocks: string[] = []; - let protectedHtml = html.replace(/

([\s\S]*?)<\/pre>/g, (match) => {
+    const protectedHtml = html.replace(/
([\s\S]*?)<\/pre>/g, (match) => {
       const placeholder = `__PRE_BLOCK_${preBlocks.length}__`;
       preBlocks.push(match);
       return placeholder;
@@ -1006,7 +1024,9 @@ export class ImportExecutionEngine extends EventEmitter {
       
       // Also handle newlines at the start (before any tags)
       processed = processed.replace(/^([^<]+)/g, (match, textContent: string) => {
-        if (!textContent.trim()) return match;
+        if (!textContent.trim()) {
+          return match;
+        }
         return textContent.replace(/\n/g, '
'); }); @@ -1070,7 +1090,9 @@ export class ImportExecutionEngine extends EventEmitter { * - without newlines (inline code) */ private wrapMultilineCode(html: string): string { - if (!html) return html; + if (!html) { + return html; + } // Match blocks containing newlines that are NOT inside
     // Use a regex that captures the full ... content including any embedded HTML
@@ -1099,7 +1121,9 @@ export class ImportExecutionEngine extends EventEmitter {
    *   - URLs from wp-content/themes/ or wp-content/plugins/ (not imported media)
    */
   private convertMediaUrlsToRelative(markdown: string): string {
-    if (!this.siteBaseUrl || !markdown) return markdown;
+    if (!this.siteBaseUrl || !markdown) {
+      return markdown;
+    }
 
     // Normalize the site URL (remove trailing slash and protocol)
     const siteUrl = this.siteBaseUrl.replace(/\/$/, '');
@@ -1107,7 +1131,9 @@ export class ImportExecutionEngine extends EventEmitter {
     // Extract the hostname from the site URL
     // Handle both http:// and https://
     const hostnameMatch = siteUrl.match(/^https?:\/\/(.+)$/);
-    if (!hostnameMatch) return markdown;
+    if (!hostnameMatch) {
+      return markdown;
+    }
     
     const hostname = hostnameMatch[1];
     const escapedHostname = hostname.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -1118,7 +1144,7 @@ export class ImportExecutionEngine extends EventEmitter {
     // Pattern: http(s)://{hostname}/wp-content/uploads/{path}
     const uploadsUrlPattern = new RegExp(
       `https?://${escapedHostname}/wp-content/uploads/([^\\s)"']+)`,
-      'gi'
+      'gi',
     );
 
     // Replace with relative media path
@@ -1147,7 +1173,7 @@ export class ImportExecutionEngine extends EventEmitter {
    */
   private resolveTaxonomy(
     items: string[],
-    mapping: Map
+    mapping: Map,
   ): string[] {
     return items.map(item => {
       const key = item.toLowerCase();
@@ -1161,7 +1187,9 @@ export class ImportExecutionEngine extends EventEmitter {
    * Handles Date objects, ISO strings (from JSON serialization), and null/undefined.
    */
   private toDate(value: Date | string | null | undefined): Date | null {
-    if (!value) return null;
+    if (!value) {
+      return null;
+    }
     if (value instanceof Date) {
       return isNaN(value.getTime()) ? null : value;
     }
diff --git a/src/main/engine/MCPAgentConfigEngine.ts b/src/main/engine/MCPAgentConfigEngine.ts
index e293b5c..97d0c3c 100644
--- a/src/main/engine/MCPAgentConfigEngine.ts
+++ b/src/main/engine/MCPAgentConfigEngine.ts
@@ -73,20 +73,20 @@ export class MCPAgentConfigEngine {
   /** Resolve the absolute path to the config file for the given agent. */
   getConfigPath(agentId: MCPAgentId): string {
     switch (agentId) {
-      case 'claude-code':
-        return path.join(this.homeDir, '.claude.json');
-      case 'claude-desktop':
-        return this.claudeDesktopConfigPath();
-      case 'github-copilot':
-        return this.vsCodeMcpPath();
-      case 'gemini-cli':
-        return path.join(this.homeDir, '.gemini', 'settings.json');
-      case 'opencode':
-        return path.join(this.homeDir, '.opencode.json');
-      case 'mistral-vibe':
-        return path.join(this.homeDir, '.vibe', 'config.toml');
-      case 'openai-codex':
-        return path.join(this.homeDir, '.codex', 'config.toml');
+    case 'claude-code':
+      return path.join(this.homeDir, '.claude.json');
+    case 'claude-desktop':
+      return this.claudeDesktopConfigPath();
+    case 'github-copilot':
+      return this.vsCodeMcpPath();
+    case 'gemini-cli':
+      return path.join(this.homeDir, '.gemini', 'settings.json');
+    case 'opencode':
+      return path.join(this.homeDir, '.opencode.json');
+    case 'mistral-vibe':
+      return path.join(this.homeDir, '.vibe', 'config.toml');
+    case 'openai-codex':
+      return path.join(this.homeDir, '.codex', 'config.toml');
     }
   }
 
@@ -110,6 +110,7 @@ export class MCPAgentConfigEngine {
         return { success: true, configPath };
       }
       const { [SERVER_NAME]: _removed, ...remainingServers } = currentServers;
+      void _removed;
       const updated: Record = { ...existing };
       if (Object.keys(remainingServers).length === 0) {
         delete updated[serversKey];
@@ -155,7 +156,9 @@ export class MCPAgentConfigEngine {
       return this.isCodexConfigured();
     }
     const configPath = this.getConfigPath(agentId);
-    if (!existsSync(configPath)) return false;
+    if (!existsSync(configPath)) {
+      return false;
+    }
     try {
       const data = JSON.parse(readFileSync(configPath, 'utf-8'));
       const serversKey = agentId === 'github-copilot' ? 'servers' : 'mcpServers';
@@ -220,7 +223,9 @@ export class MCPAgentConfigEngine {
 
   private isVibeConfigured(): boolean {
     const configPath = this.getConfigPath('mistral-vibe');
-    if (!existsSync(configPath)) return false;
+    if (!existsSync(configPath)) {
+      return false;
+    }
     try {
       const existing = this.readExistingToml(configPath);
       const servers = (existing.mcp_servers ?? []) as Record[];
@@ -231,7 +236,9 @@ export class MCPAgentConfigEngine {
   }
 
   private readExistingToml(configPath: string): Record {
-    if (!existsSync(configPath)) return {};
+    if (!existsSync(configPath)) {
+      return {};
+    }
     const raw = readFileSync(configPath, 'utf-8');
     return parseToml(raw) as Record;
   }
@@ -286,7 +293,9 @@ export class MCPAgentConfigEngine {
 
   private isCodexConfigured(): boolean {
     const configPath = this.getConfigPath('openai-codex');
-    if (!existsSync(configPath)) return false;
+    if (!existsSync(configPath)) {
+      return false;
+    }
     try {
       const existing = this.readExistingToml(configPath);
       const servers = (existing.mcp_servers ?? {}) as Record;
@@ -320,7 +329,9 @@ export class MCPAgentConfigEngine {
   }
 
   private readExistingJson(configPath: string): Record {
-    if (!existsSync(configPath)) return {};
+    if (!existsSync(configPath)) {
+      return {};
+    }
     const raw = readFileSync(configPath, 'utf-8');
     return JSON.parse(raw) as Record;
   }
@@ -347,17 +358,17 @@ export class MCPAgentConfigEngine {
     };
 
     switch (agentId) {
-      case 'claude-code':
-      case 'claude-desktop':
-      case 'gemini-cli':
-        return stdioEntry;
-      case 'github-copilot':
-      case 'opencode':
-        return { type: 'stdio', ...stdioEntry };
-      case 'mistral-vibe':
-      case 'openai-codex':
-        // TOML-based; handled separately — should not reach here.
-        return stdioEntry;
+    case 'claude-code':
+    case 'claude-desktop':
+    case 'gemini-cli':
+      return stdioEntry;
+    case 'github-copilot':
+    case 'opencode':
+      return { type: 'stdio', ...stdioEntry };
+    case 'mistral-vibe':
+    case 'openai-codex':
+      // TOML-based; handled separately — should not reach here.
+      return stdioEntry;
     }
   }
 
diff --git a/src/main/engine/MCPServer.ts b/src/main/engine/MCPServer.ts
index f830d59..a65993f 100644
--- a/src/main/engine/MCPServer.ts
+++ b/src/main/engine/MCPServer.ts
@@ -9,7 +9,7 @@ import {
 import { createServer as createHttpServer, type Server } from 'http';
 import { z } from 'zod';
 import { buildAmbiguityHints, enrichWithLinks, executeCheckTerm } from './ai/blog-tools';
-import { ProposalStore, type ProposalType } from './ProposalStore';
+import { ProposalStore } from './ProposalStore';
 import {
   reviewPostHtml,
   reviewScriptHtml,
@@ -236,7 +236,7 @@ export class MCPServer {
       try {
         await mcpServer.connect(transport);
         await transport.handleRequest(req, res, await parseBody(req));
-      } catch (error) {
+      } catch {
         if (!res.headersSent) {
           res.writeHead(500, { 'Content-Type': 'application/json' });
           res.end(JSON.stringify({
@@ -272,9 +272,13 @@ export class MCPServer {
     }
 
     await new Promise((resolve, reject) => {
-      if (!this.httpServer) { resolve(); return; }
+      if (!this.httpServer) {
+        resolve(); return;
+      }
       this.httpServer.close((error) => {
-        if (error) { reject(error); return; }
+        if (error) {
+          reject(error); return;
+        }
         resolve();
       });
     });
@@ -312,31 +316,31 @@ export class MCPServer {
 
     try {
       switch (proposal.type) {
-        case 'draftPost': {
-          const { postId } = proposalData<'draftPost'>(proposal);
-          await this.deps.postEngine.publishPost(postId);
-          break;
-        }
-        case 'proposeScript': {
-          const { scriptId } = proposalData<'proposeScript'>(proposal);
-          await this.deps.scriptEngine.publishScript(scriptId);
-          break;
-        }
-        case 'proposeTemplate': {
-          const { templateId } = proposalData<'proposeTemplate'>(proposal);
-          await this.deps.templateEngine.publishTemplate(templateId);
-          break;
-        }
-        case 'proposeMediaMetadata': {
-          const { mediaId, changes } = proposalData<'proposeMediaMetadata'>(proposal);
-          await this.deps.mediaEngine.updateMedia(mediaId, changes);
-          break;
-        }
-        case 'proposePostMetadata': {
-          const { postId, changes } = proposalData<'proposePostMetadata'>(proposal);
-          await this.deps.postEngine.updatePost(postId, changes);
-          break;
-        }
+      case 'draftPost': {
+        const { postId } = proposalData<'draftPost'>(proposal);
+        await this.deps.postEngine.publishPost(postId);
+        break;
+      }
+      case 'proposeScript': {
+        const { scriptId } = proposalData<'proposeScript'>(proposal);
+        await this.deps.scriptEngine.publishScript(scriptId);
+        break;
+      }
+      case 'proposeTemplate': {
+        const { templateId } = proposalData<'proposeTemplate'>(proposal);
+        await this.deps.templateEngine.publishTemplate(templateId);
+        break;
+      }
+      case 'proposeMediaMetadata': {
+        const { mediaId, changes } = proposalData<'proposeMediaMetadata'>(proposal);
+        await this.deps.mediaEngine.updateMedia(mediaId, changes);
+        break;
+      }
+      case 'proposePostMetadata': {
+        const { postId, changes } = proposalData<'proposePostMetadata'>(proposal);
+        await this.deps.postEngine.updatePost(postId, changes);
+        break;
+      }
       }
       this.proposalStore.remove(proposalId);
       return { success: true, message: `Proposal ${proposalId} accepted.` };
@@ -547,13 +551,27 @@ export class MCPServer {
         enriched = await enrichWithLinks(paginated, this.deps.postEngine);
       } else {
         const filter: PostFilter = {};
-        if (args.category) filter.categories = [args.category];
-        if (args.tags) filter.tags = args.tags;
-        if (args.language) filter.language = args.language;
-        if (args.missingTranslationLanguage) filter.missingTranslationLanguage = args.missingTranslationLanguage;
-        if (args.year) filter.year = args.year;
-        if (args.month) filter.month = args.month;
-        if (args.status) filter.status = args.status;
+        if (args.category) {
+          filter.categories = [args.category];
+        }
+        if (args.tags) {
+          filter.tags = args.tags;
+        }
+        if (args.language) {
+          filter.language = args.language;
+        }
+        if (args.missingTranslationLanguage) {
+          filter.missingTranslationLanguage = args.missingTranslationLanguage;
+        }
+        if (args.year) {
+          filter.year = args.year;
+        }
+        if (args.month) {
+          filter.month = args.month;
+        }
+        if (args.status) {
+          filter.status = args.status;
+        }
 
         if (args.query && hasFilters) {
           const { posts, total: t } = await this.deps.postEngine.searchPostsFiltered(args.query, filter, { offset, limit });
@@ -600,11 +618,21 @@ export class MCPServer {
       }
 
       const filter: { year?: number; month?: number; status?: string; category?: string; tags?: string[] } = {};
-      if (args.year !== undefined) filter.year = args.year;
-      if (args.month !== undefined) filter.month = args.month;
-      if (args.status) filter.status = args.status;
-      if (args.category) filter.category = args.category;
-      if (args.tags) filter.tags = args.tags;
+      if (args.year !== undefined) {
+        filter.year = args.year;
+      }
+      if (args.month !== undefined) {
+        filter.month = args.month;
+      }
+      if (args.status) {
+        filter.status = args.status;
+      }
+      if (args.category) {
+        filter.category = args.category;
+      }
+      if (args.tags) {
+        filter.tags = args.tags;
+      }
 
       const result = await this.deps.postEngine.getPostCounts(
         args.groupBy,
@@ -1055,8 +1083,12 @@ function buildDraftPostPrompt(topic?: string, category?: string): string {
     '3. Use the `draft_post` tool to create the draft for the user to review.',
     '',
   ];
-  if (topic) parts.push(`Suggested topic: ${topic}`);
-  if (category) parts.push(`Target category: ${category}`);
+  if (topic) {
+    parts.push(`Suggested topic: ${topic}`);
+  }
+  if (category) {
+    parts.push(`Target category: ${category}`);
+  }
   parts.push('', 'Ensure the post matches the existing blog style and quality standards.');
   return parts.join('\n');
 }
diff --git a/src/main/engine/MediaEngine.ts b/src/main/engine/MediaEngine.ts
index 0bb9e6f..bf8c294 100644
--- a/src/main/engine/MediaEngine.ts
+++ b/src/main/engine/MediaEngine.ts
@@ -6,7 +6,7 @@ import * as crypto from 'crypto';
 import { eq, and, gte, lte, lt, desc } from 'drizzle-orm';
 import { app } from 'electron';
 import { getDatabase } from '../database';
-import { media, Media, NewMedia, postMedia, mediaTranslations } from '../database/schema';
+import { media, NewMedia, postMedia, mediaTranslations } from '../database/schema';
 import { stemText, stemQuery, SupportedLanguage } from './stemmer';
 import { CliNotifier, NoopNotifier } from './CliNotifier';
 
@@ -102,7 +102,9 @@ export class MediaEngine extends EventEmitter {
   }
 
   /** No persistent cache — DB is the source of truth. No-op for watcher compat. */
-  invalidate(_entityId?: string): void {}
+  invalidate(entityId?: string): void {
+    void entityId;
+  }
 
   /**
    * Set the language used for full-text search stemming.
@@ -132,7 +134,9 @@ export class MediaEngine extends EventEmitter {
     tags: string[];
   }): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return;
+    if (!client) {
+      return;
+    }
 
     // Delete existing entry
     await client.execute({ sql: 'DELETE FROM media_fts WHERE id = ?', args: [item.id] });
@@ -160,7 +164,9 @@ export class MediaEngine extends EventEmitter {
    */
   private async deleteFTSIndex(id: string): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return;
+    if (!client) {
+      return;
+    }
     await client.execute({ sql: 'DELETE FROM media_fts WHERE id = ?', args: [id] });
   }
 
@@ -221,7 +227,6 @@ export class MediaEngine extends EventEmitter {
     this.currentProjectId = projectId;
     this.dataDir = nextDataDir;
     this.internalDir = nextInternalDir;
-    console.log(`[MediaEngine] setProjectContext: projectId=${projectId}, dataDir=${this.dataDir}, internalDir=${this.internalDir}`);
   }
 
   getProjectContext(): string {
@@ -381,13 +386,27 @@ export class MediaEngine extends EventEmitter {
       `size: ${metadata.size}`,
     ];
 
-    if (metadata.width) lines.push(`width: ${metadata.width}`);
-    if (metadata.height) lines.push(`height: ${metadata.height}`);
-    if (metadata.title) lines.push(`title: "${metadata.title}"`);
-    if (metadata.alt) lines.push(`alt: "${metadata.alt}"`);
-    if (metadata.caption) lines.push(`caption: "${metadata.caption}"`);
-    if (metadata.author) lines.push(`author: "${metadata.author}"`);
-    if (metadata.language) lines.push(`language: ${metadata.language}`);
+    if (metadata.width) {
+      lines.push(`width: ${metadata.width}`);
+    }
+    if (metadata.height) {
+      lines.push(`height: ${metadata.height}`);
+    }
+    if (metadata.title) {
+      lines.push(`title: "${metadata.title}"`);
+    }
+    if (metadata.alt) {
+      lines.push(`alt: "${metadata.alt}"`);
+    }
+    if (metadata.caption) {
+      lines.push(`caption: "${metadata.caption}"`);
+    }
+    if (metadata.author) {
+      lines.push(`author: "${metadata.author}"`);
+    }
+    if (metadata.language) {
+      lines.push(`language: ${metadata.language}`);
+    }
     
     lines.push(`createdAt: ${metadata.createdAt}`);
     lines.push(`updatedAt: ${metadata.updatedAt}`);
@@ -420,10 +439,14 @@ export class MediaEngine extends EventEmitter {
       };
 
       for (const line of lines) {
-        if (line === '---') continue;
+        if (line === '---') {
+          continue;
+        }
         
         const colonIndex = line.indexOf(':');
-        if (colonIndex === -1) continue;
+        if (colonIndex === -1) {
+          continue;
+        }
         
         const key = line.substring(0, colonIndex).trim();
         let value = line.substring(colonIndex + 1).trim();
@@ -434,65 +457,65 @@ export class MediaEngine extends EventEmitter {
         }
 
         switch (key) {
-          case 'id':
-            metadata.id = value;
-            break;
-          case 'originalName':
-            metadata.originalName = value;
-            break;
-          case 'mimeType':
-            metadata.mimeType = value;
-            break;
-          case 'size':
-            metadata.size = parseInt(value, 10);
-            break;
-          case 'width':
-            metadata.width = parseInt(value, 10);
-            break;
-          case 'height':
-            metadata.height = parseInt(value, 10);
-            break;
-          case 'title':
-            metadata.title = value;
-            break;
-          case 'alt':
-            metadata.alt = value;
-            break;
-          case 'caption':
-            metadata.caption = value;
-            break;
-          case 'author':
-            metadata.author = value;
-            break;
-          case 'language':
-            metadata.language = value;
-            break;
-          case 'createdAt':
-            metadata.createdAt = value;
-            break;
-          case 'updatedAt':
-            metadata.updatedAt = value;
-            break;
-          case 'tags':
-            // Parse array format: ["tag1", "tag2"]
-            const tagsMatch = value.match(/\[(.*)\]/);
-            if (tagsMatch) {
-              metadata.tags = tagsMatch[1]
-                .split(',')
-                .map(t => t.trim().replace(/"/g, ''))
-                .filter(t => t.length > 0);
-            }
-            break;
-          case 'linkedPostIds':
-            // Parse array format: ["postId1", "postId2"]
-            const postIdsMatch = value.match(/\[(.*)\]/);
-            if (postIdsMatch) {
-              metadata.linkedPostIds = postIdsMatch[1]
-                .split(',')
-                .map(id => id.trim().replace(/"/g, ''))
-                .filter(id => id.length > 0);
-            }
-            break;
+        case 'id':
+          metadata.id = value;
+          break;
+        case 'originalName':
+          metadata.originalName = value;
+          break;
+        case 'mimeType':
+          metadata.mimeType = value;
+          break;
+        case 'size':
+          metadata.size = parseInt(value, 10);
+          break;
+        case 'width':
+          metadata.width = parseInt(value, 10);
+          break;
+        case 'height':
+          metadata.height = parseInt(value, 10);
+          break;
+        case 'title':
+          metadata.title = value;
+          break;
+        case 'alt':
+          metadata.alt = value;
+          break;
+        case 'caption':
+          metadata.caption = value;
+          break;
+        case 'author':
+          metadata.author = value;
+          break;
+        case 'language':
+          metadata.language = value;
+          break;
+        case 'createdAt':
+          metadata.createdAt = value;
+          break;
+        case 'updatedAt':
+          metadata.updatedAt = value;
+          break;
+        case 'tags':
+          // Parse array format: ["tag1", "tag2"]
+          const tagsMatch = value.match(/\[(.*)\]/);
+          if (tagsMatch) {
+            metadata.tags = tagsMatch[1]
+              .split(',')
+              .map(t => t.trim().replace(/"/g, ''))
+              .filter(t => t.length > 0);
+          }
+          break;
+        case 'linkedPostIds':
+          // Parse array format: ["postId1", "postId2"]
+          const postIdsMatch = value.match(/\[(.*)\]/);
+          if (postIdsMatch) {
+            metadata.linkedPostIds = postIdsMatch[1]
+              .split(',')
+              .map(id => id.trim().replace(/"/g, ''))
+              .filter(id => id.length > 0);
+          }
+          break;
         }
       }
 
@@ -651,7 +674,9 @@ export class MediaEngine extends EventEmitter {
     };
 
     const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
-    if (!dbMedia) return null;
+    if (!dbMedia) {
+      return null;
+    }
 
     // Read existing sidecar to preserve fields that may only exist there
     // (e.g. linkedPostIds is sidecar-only, and author/title may have drifted)
@@ -891,8 +916,6 @@ export class MediaEngine extends EventEmitter {
     const db = getDatabase().getLocal();
     const conditions = [eq(media.projectId, this.currentProjectId)];
 
-    console.log(`[MediaEngine] getMediaFiltered called with filter:`, JSON.stringify(filter));
-
     if (filter.startDate) {
       conditions.push(gte(media.createdAt, filter.startDate));
     }
@@ -905,7 +928,6 @@ export class MediaEngine extends EventEmitter {
       // Use UTC dates to avoid timezone issues
       const startOfYear = new Date(Date.UTC(filter.year, 0, 1));
       const endOfYear = new Date(Date.UTC(filter.year + 1, 0, 1));
-      console.log(`[MediaEngine] Year filter: ${startOfYear.toISOString()} to ${endOfYear.toISOString()}`);
       conditions.push(gte(media.createdAt, startOfYear));
       conditions.push(lt(media.createdAt, endOfYear));
     }
@@ -914,7 +936,6 @@ export class MediaEngine extends EventEmitter {
       // Use UTC dates to avoid timezone issues (filter.month is 1-indexed)
       const startOfMonth = new Date(Date.UTC(filter.year, filter.month - 1, 1));
       const endOfMonth = new Date(Date.UTC(filter.year, filter.month, 1));
-      console.log(`[MediaEngine] Month filter: ${startOfMonth.toISOString()} to ${endOfMonth.toISOString()}`);
       conditions.push(gte(media.createdAt, startOfMonth));
       conditions.push(lt(media.createdAt, endOfMonth));
     }
@@ -926,9 +947,7 @@ export class MediaEngine extends EventEmitter {
       .orderBy(desc(media.createdAt))
       .all();
 
-    console.log(`[MediaEngine] Query returned ${dbMediaList.length} media items`);
-
-    let result: MediaData[] = [];
+    const result: MediaData[] = [];
 
     for (const dbMedia of dbMediaList) {
       const mediaData: MediaData = {
@@ -953,7 +972,9 @@ export class MediaEngine extends EventEmitter {
       // Client-side filtering for tags (JSON array)
       if (filter.tags && filter.tags.length > 0) {
         const hasAllTags = filter.tags.every(tag => mediaData.tags.includes(tag));
-        if (!hasAllTags) continue;
+        if (!hasAllTags) {
+          continue;
+        }
       }
 
       result.push(mediaData);
@@ -964,7 +985,9 @@ export class MediaEngine extends EventEmitter {
 
   async searchMedia(query: string): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return [];
+    if (!client) {
+      return [];
+    }
 
     try {
       // Stem the query for multilingual matching
@@ -972,7 +995,7 @@ export class MediaEngine extends EventEmitter {
       
       // Search the stemmed content, filtered by project_id for project isolation
       const result = await client.execute({
-        sql: `SELECT id FROM media_fts WHERE project_id = ? AND media_fts MATCH ? ORDER BY rank LIMIT 50`,
+        sql: 'SELECT id FROM media_fts WHERE project_id = ? AND media_fts MATCH ? ORDER BY rank LIMIT 50',
         args: [this.currentProjectId, stemmedQuery],
       });
 
@@ -1015,7 +1038,9 @@ export class MediaEngine extends EventEmitter {
     }
 
     return Array.from(counts.values()).sort((a, b) => {
-      if (a.year !== b.year) return b.year - a.year;
+      if (a.year !== b.year) {
+        return b.year - a.year;
+      }
       return b.month - a.month;
     });
   }
@@ -1063,7 +1088,9 @@ export class MediaEngine extends EventEmitter {
   async getRelativePath(id: string): Promise {
     const db = getDatabase().getLocal();
     const dbMedia = await db.select().from(media).where(eq(media.id, id)).get();
-    if (!dbMedia?.filePath) return null;
+    if (!dbMedia?.filePath) {
+      return null;
+    }
     const dataDir = this.getDataDir();
     const relativePath = path.relative(dataDir, dbMedia.filePath);
     return relativePath.replace(/\\/g, '/');
@@ -1071,7 +1098,6 @@ export class MediaEngine extends EventEmitter {
 
   async rebuildDatabaseFromFiles(): Promise {
     const mediaBaseDir = this.getMediaBaseDir();
-    console.log(`[MediaEngine] rebuildDatabaseFromFiles: scanning mediaBaseDir=${mediaBaseDir}`);
     const task: Task = {
       id: uuidv4(),
       name: 'Rebuild database from media files',
@@ -1087,16 +1113,13 @@ export class MediaEngine extends EventEmitter {
         const existingMedia = await db.select({ id: media.id }).from(media).where(eq(media.projectId, this.currentProjectId)).all();
         if (existingMedia.length > 0) {
           await db.delete(media).where(eq(media.projectId, this.currentProjectId));
-          console.log(`Deleted ${existingMedia.length} existing media record(s) for project ${this.currentProjectId}`);
         }
 
         // Also delete all post-media links for the current project
         await db.delete(postMedia).where(eq(postMedia.projectId, this.currentProjectId));
-        console.log(`Deleted post-media links for project ${this.currentProjectId}`);
 
         // Delete all media translations for the current project
         await db.delete(mediaTranslations).where(eq(mediaTranslations.projectId, this.currentProjectId));
-        console.log(`Deleted media translations for project ${this.currentProjectId}`);
 
         // Delete all FTS entries for the current project
         const client = getDatabase().getLocalClient();
@@ -1105,7 +1128,6 @@ export class MediaEngine extends EventEmitter {
             sql: 'DELETE FROM media_fts WHERE project_id = ?',
             args: [this.currentProjectId],
           });
-          console.log(`Deleted media FTS entries for project ${this.currentProjectId}`);
         }
 
         onProgress(5, 'Scanning media directory...');
@@ -1281,7 +1303,7 @@ export class MediaEngine extends EventEmitter {
 
         // Filter to images only (not SVG - they don't need thumbnails)
         const imageMedia = allMedia.filter(
-          m => m.mimeType.startsWith('image/') && !m.mimeType.includes('svg')
+          m => m.mimeType.startsWith('image/') && !m.mimeType.includes('svg'),
         );
 
         if (imageMedia.length === 0) {
@@ -1406,7 +1428,6 @@ export class MediaEngine extends EventEmitter {
         }
 
         onProgress(100, `Reindexed ${total} media items`);
-        console.log(`Reindexed search text for ${total} media items`);
       },
     };
 
@@ -1421,7 +1442,9 @@ export class MediaEngine extends EventEmitter {
       .where(eq(mediaTranslations.translationFor, mediaId))
       .all();
     const row = rows.find(r => r.language === language.toLowerCase());
-    if (!row) return null;
+    if (!row) {
+      return null;
+    }
     return this.toMediaTranslationData(row);
   }
 
@@ -1520,7 +1543,9 @@ export class MediaEngine extends EventEmitter {
   async deleteMediaTranslation(mediaId: string, language: string): Promise {
     const normalizedLang = language.toLowerCase();
     const existing = await this.getMediaTranslation(mediaId, normalizedLang);
-    if (!existing) return false;
+    if (!existing) {
+      return false;
+    }
 
     const db = getDatabase().getLocal();
     await db.delete(mediaTranslations).where(eq(mediaTranslations.id, existing.id));
@@ -1563,9 +1588,15 @@ export class MediaEngine extends EventEmitter {
       `translationFor: ${translation.translationFor}`,
       `language: ${translation.language}`,
     ];
-    if (translation.title) lines.push(`title: "${translation.title}"`);
-    if (translation.alt) lines.push(`alt: "${translation.alt}"`);
-    if (translation.caption) lines.push(`caption: "${translation.caption}"`);
+    if (translation.title) {
+      lines.push(`title: "${translation.title}"`);
+    }
+    if (translation.alt) {
+      lines.push(`alt: "${translation.alt}"`);
+    }
+    if (translation.caption) {
+      lines.push(`caption: "${translation.caption}"`);
+    }
     lines.push('---');
 
     await fs.writeFile(sidecarPath, lines.join('\n'), 'utf-8');
@@ -1580,9 +1611,13 @@ export class MediaEngine extends EventEmitter {
       const result: { translationFor?: string; language?: string; title?: string; alt?: string; caption?: string } = {};
 
       for (const line of content.split('\n')) {
-        if (line === '---') continue;
+        if (line === '---') {
+          continue;
+        }
         const colonIndex = line.indexOf(':');
-        if (colonIndex === -1) continue;
+        if (colonIndex === -1) {
+          continue;
+        }
 
         const key = line.substring(0, colonIndex).trim();
         let value = line.substring(colonIndex + 1).trim();
@@ -1591,11 +1626,11 @@ export class MediaEngine extends EventEmitter {
         }
 
         switch (key) {
-          case 'translationFor': result.translationFor = value; break;
-          case 'language': result.language = value; break;
-          case 'title': result.title = value; break;
-          case 'alt': result.alt = value; break;
-          case 'caption': result.caption = value; break;
+        case 'translationFor': result.translationFor = value; break;
+        case 'language': result.language = value; break;
+        case 'title': result.title = value; break;
+        case 'alt': result.alt = value; break;
+        case 'caption': result.caption = value; break;
         }
       }
 
diff --git a/src/main/engine/MenuEngine.ts b/src/main/engine/MenuEngine.ts
index 88cb679..5c7ae8b 100644
--- a/src/main/engine/MenuEngine.ts
+++ b/src/main/engine/MenuEngine.ts
@@ -81,7 +81,7 @@ function sanitizeMenuItem(input: unknown): MenuItemData {
       ? 'category-archive'
       : candidate.kind === 'home'
         ? 'home'
-      : 'page';
+        : 'page';
   const childrenSource = Array.isArray(candidate.children) ? candidate.children : [];
   const title = normalizeNonEmptyString(candidate.title) || 'Untitled';
 
@@ -171,7 +171,7 @@ function parseOutlineNode(node: OpmlOutlineNode): MenuItemData {
       ? 'category-archive'
       : rawType === 'home'
         ? 'home'
-      : 'page';
+        : 'page';
   const textTitle = normalizeNonEmptyString(node['@_text']);
   const explicitTitle = normalizeNonEmptyString(node['@_title']);
   const title = kind === 'category-archive'
diff --git a/src/main/engine/MetaEngine.ts b/src/main/engine/MetaEngine.ts
index 05b9a6a..55cd820 100644
--- a/src/main/engine/MetaEngine.ts
+++ b/src/main/engine/MetaEngine.ts
@@ -6,7 +6,9 @@ import { eq } from 'drizzle-orm';
 import { getDatabase } from '../database';
 import { posts, projects } from '../database/schema';
 import { sanitizePicoTheme, type PicoThemeName } from '../shared/picoThemes';
-import { SUPPORTED_RENDER_LANGUAGES, type SupportedLanguage } from '../shared/i18n';
+import {
+  SUPPORTED_RENDER_LANGUAGES,
+} from '../shared/i18n';
 import {
   normalizeTaxonomyTerm,
   normalizeNonEmptyTaxonomyTerm,
@@ -89,7 +91,9 @@ function sanitizePublicUrl(value: unknown): string | undefined {
   return trimmed.length > 0 ? trimmed : undefined;
 }
 
-function normalizePublishingPreferences(prefs: PublishingPreferences): PublishingPreferences {
+function normalizePublishingPreferences(
+  prefs: PublishingPreferences,
+): PublishingPreferences {
   return {
     sshHost: String(prefs.sshHost ?? '').trim(),
     sshUser: String(prefs.sshUser ?? '').trim(),
@@ -103,7 +107,10 @@ function sanitizeCategoryTitle(value: unknown, fallback: string): string {
   return trimmed.length > 0 ? trimmed : fallback;
 }
 
-type RawCategoryMetadataInput = Record;
+type RawCategoryMetadataInput = Record<
+  string,
+  CategoryMetadata | CategoryRenderSettings
+>;
 
 const supportedLanguageSet = new Set(SUPPORTED_RENDER_LANGUAGES);
 
@@ -121,12 +128,16 @@ function sanitizeBlogLanguages(value: unknown): string[] | undefined {
 function normalizeProjectMetadata(metadata: ProjectMetadata): ProjectMetadata {
   const maxPostsPerPage = sanitizeMaxPostsPerPage(metadata.maxPostsPerPage);
   const publicUrl = sanitizePublicUrl(metadata.publicUrl);
-  const blogmarkCategory = typeof metadata.blogmarkCategory === 'string'
-    ? normalizeNonEmptyTaxonomyTerm(metadata.blogmarkCategory) ?? undefined
-    : undefined;
-  const pythonRuntimeMode = metadata.pythonRuntimeMode === 'main-thread' ? 'main-thread' : 'webworker';
+  const blogmarkCategory =
+    typeof metadata.blogmarkCategory === 'string'
+      ? (normalizeNonEmptyTaxonomyTerm(metadata.blogmarkCategory) ?? undefined)
+      : undefined;
+  const pythonRuntimeMode =
+    metadata.pythonRuntimeMode === 'main-thread' ? 'main-thread' : 'webworker';
   const picoTheme = sanitizePicoTheme(metadata.picoTheme);
-  const categoryMetadata = normalizeCategoryMetadata(metadata.categoryMetadata ?? metadata.categorySettings);
+  const categoryMetadata = normalizeCategoryMetadata(
+    metadata.categoryMetadata ?? metadata.categorySettings,
+  );
   const blogLanguages = sanitizeBlogLanguages(metadata.blogLanguages);
   return {
     ...metadata,
@@ -155,62 +166,56 @@ function getDefaultCategoryMetadata(): Record {
   };
 }
 
-function normalizeCategoryMetadata(value: unknown): Record {
+function normalizeCategoryMetadata(
+  value: unknown,
+): Record {
   const defaults = getDefaultCategoryMetadata();
   if (!value || typeof value !== 'object') {
     return defaults;
   }
 
   const normalized: Record = { ...defaults };
-  for (const [rawCategory, rawSettings] of Object.entries(value as RawCategoryMetadataInput)) {
+  for (const [rawCategory, rawSettings] of Object.entries(
+    value as RawCategoryMetadataInput,
+  )) {
     const category = normalizeTaxonomyTerm(rawCategory);
     if (!category || !rawSettings || typeof rawSettings !== 'object') {
       continue;
     }
 
-    const settings = rawSettings as unknown as {
-      renderInLists?: unknown;
-      showTitle?: unknown;
-      title?: unknown;
-    };
+    const settings =
+      rawSettings as unknown as Partial & {
+        title?: unknown;
+      };
     normalized[category] = {
       renderInLists: settings.renderInLists !== false,
       showTitle: settings.showTitle !== false,
       title: sanitizeCategoryTitle(settings.title, category),
-      postTemplateSlug: typeof (settings as any).postTemplateSlug === 'string' ? (settings as any).postTemplateSlug : undefined,
-      listTemplateSlug: typeof (settings as any).listTemplateSlug === 'string' ? (settings as any).listTemplateSlug : undefined,
+      postTemplateSlug:
+        typeof settings.postTemplateSlug === 'string'
+          ? settings.postTemplateSlug
+          : undefined,
+      listTemplateSlug:
+        typeof settings.listTemplateSlug === 'string'
+          ? settings.listTemplateSlug
+          : undefined,
     };
   }
 
   return normalized;
 }
 
-function normalizeCategorySettings(value: unknown): Record {
-  const metadata = normalizeCategoryMetadata(value);
-  return Object.fromEntries(
-    Object.entries(metadata).map(([category, data]) => [
-      category,
-      {
-        renderInLists: data.renderInLists,
-        showTitle: data.showTitle,
-        postTemplateSlug: data.postTemplateSlug,
-        listTemplateSlug: data.listTemplateSlug,
-      },
-    ]),
-  );
-}
-
 function isJsonParseError(error: unknown): boolean {
   return error instanceof SyntaxError;
 }
 
 /**
  * MetaEngine manages project metadata like available tags and categories.
- * 
+ *
  * It keeps metadata in sync between:
  * - The database (derived from posts)
  * - The filesystem (meta/tags.json, meta/categories.json)
- * 
+ *
  * This enables offline-first operation where all metadata is available
  * from the local filesystem per project.
  */
@@ -315,9 +320,10 @@ export class MetaEngine extends EventEmitter {
    */
   async setProjectMetadata(metadata: ProjectMetadata): Promise {
     this.projectMetadata = normalizeProjectMetadata({ ...metadata });
-    this.projectMetadata.categoryMetadata = this.ensureCategoryMetadataForKnownCategories(
-      this.projectMetadata.categoryMetadata,
-    );
+    this.projectMetadata.categoryMetadata =
+      this.ensureCategoryMetadataForKnownCategories(
+        this.projectMetadata.categoryMetadata,
+      );
     await this.saveProjectMetadata();
     await this.saveCategoryMetadata();
     this.emit('projectMetadataChanged', this.projectMetadata);
@@ -326,16 +332,23 @@ export class MetaEngine extends EventEmitter {
   /**
    * Update specific fields of project metadata.
    */
-  async updateProjectMetadata(updates: Partial): Promise {
+  async updateProjectMetadata(
+    updates: Partial,
+  ): Promise {
     const normalizedUpdates: Partial = { ...updates };
     if (updates.maxPostsPerPage !== undefined) {
-      normalizedUpdates.maxPostsPerPage = sanitizeMaxPostsPerPage(updates.maxPostsPerPage);
+      normalizedUpdates.maxPostsPerPage = sanitizeMaxPostsPerPage(
+        updates.maxPostsPerPage,
+      );
     }
     if (updates.picoTheme !== undefined) {
       normalizedUpdates.picoTheme = sanitizePicoTheme(updates.picoTheme);
     }
 
-    if (updates.categoryMetadata !== undefined || updates.categorySettings !== undefined) {
+    if (
+      updates.categoryMetadata !== undefined ||
+      updates.categorySettings !== undefined
+    ) {
       normalizedUpdates.categoryMetadata = normalizeCategoryMetadata(
         updates.categoryMetadata ?? updates.categorySettings,
       );
@@ -364,9 +377,10 @@ export class MetaEngine extends EventEmitter {
         ...normalizedUpdates,
       });
     }
-    this.projectMetadata.categoryMetadata = this.ensureCategoryMetadataForKnownCategories(
-      this.projectMetadata.categoryMetadata,
-    );
+    this.projectMetadata.categoryMetadata =
+      this.ensureCategoryMetadataForKnownCategories(
+        this.projectMetadata.categoryMetadata,
+      );
     await this.saveProjectMetadata();
     await this.saveCategoryMetadata();
     this.emit('projectMetadataChanged', this.projectMetadata);
@@ -402,7 +416,10 @@ export class MetaEngine extends EventEmitter {
       await fs.unlink(filePath);
     } catch (error) {
       if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
-        console.error('[MetaEngine] Failed to delete publishing preferences:', error);
+        console.error(
+          '[MetaEngine] Failed to delete publishing preferences:',
+          error,
+        );
         throw error;
       }
     }
@@ -441,7 +458,9 @@ export class MetaEngine extends EventEmitter {
       this.categories.add(normalizedCategory);
       const currentMetadata = this.projectMetadata;
       if (currentMetadata) {
-        const currentCategoryMetadata = normalizeCategoryMetadata(currentMetadata.categoryMetadata ?? currentMetadata.categorySettings);
+        const currentCategoryMetadata = normalizeCategoryMetadata(
+          currentMetadata.categoryMetadata ?? currentMetadata.categorySettings,
+        );
         if (!currentCategoryMetadata[normalizedCategory]) {
           currentCategoryMetadata[normalizedCategory] = {
             renderInLists: true,
@@ -469,7 +488,10 @@ export class MetaEngine extends EventEmitter {
     if (this.categories.delete(normalizedCategory)) {
       const currentMetadata = this.projectMetadata;
       const currentCategoryMetadata = currentMetadata
-        ? normalizeCategoryMetadata(currentMetadata.categoryMetadata ?? currentMetadata.categorySettings)
+        ? normalizeCategoryMetadata(
+          currentMetadata.categoryMetadata ??
+              currentMetadata.categorySettings,
+        )
         : null;
       if (currentMetadata && currentCategoryMetadata?.[normalizedCategory]) {
         const nextCategoryMetadata = { ...currentCategoryMetadata };
@@ -493,7 +515,10 @@ export class MetaEngine extends EventEmitter {
     try {
       await this.ensureMetaDirExists();
       const filePath = this.getCategoriesFilePath();
-      await this.writeJsonFileAtomically(filePath, Array.from(this.categories).sort());
+      await this.writeJsonFileAtomically(
+        filePath,
+        Array.from(this.categories).sort(),
+      );
     } catch (error) {
       console.error('[MetaEngine] Failed to save categories:', error);
       throw error;
@@ -507,12 +532,10 @@ export class MetaEngine extends EventEmitter {
     try {
       await this.ensureMetaDirExists();
       const filePath = this.getProjectMetadataFilePath();
-      const {
-        dataPath: _dataPath,
-        categoryMetadata: _categoryMetadata,
-        categorySettings: _categorySettings,
-        ...persistedMetadata
-      } = this.projectMetadata || {};
+      const persistedMetadata = { ...(this.projectMetadata || {}) };
+      delete persistedMetadata.dataPath;
+      delete persistedMetadata.categoryMetadata;
+      delete persistedMetadata.categorySettings;
       await this.writeJsonFileAtomically(filePath, persistedMetadata);
     } catch (error) {
       console.error('[MetaEngine] Failed to save project metadata:', error);
@@ -549,7 +572,10 @@ export class MetaEngine extends EventEmitter {
       const filePath = this.getPublishingPreferencesFilePath();
       await this.writeJsonFileAtomically(filePath, this.publishingPreferences);
     } catch (error) {
-      console.error('[MetaEngine] Failed to save publishing preferences:', error);
+      console.error(
+        '[MetaEngine] Failed to save publishing preferences:',
+        error,
+      );
       throw error;
     }
   }
@@ -565,12 +591,18 @@ export class MetaEngine extends EventEmitter {
       this.publishingPreferences = normalizePublishingPreferences(parsed);
     } catch (error) {
       if (isJsonParseError(error)) {
-        console.warn('[MetaEngine] Failed to parse publishing preferences JSON, using null:', error);
+        console.warn(
+          '[MetaEngine] Failed to parse publishing preferences JSON, using null:',
+          error,
+        );
         this.publishingPreferences = null;
         return;
       }
       if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
-        console.error('[MetaEngine] Failed to load publishing preferences:', error);
+        console.error(
+          '[MetaEngine] Failed to load publishing preferences:',
+          error,
+        );
         throw error;
       }
       // File doesn't exist, that's OK
@@ -589,7 +621,10 @@ export class MetaEngine extends EventEmitter {
       this.projectMetadata = normalizeProjectMetadata(parsed);
     } catch (error) {
       if (isJsonParseError(error)) {
-        console.warn('[MetaEngine] Failed to parse project metadata JSON, using null metadata:', error);
+        console.warn(
+          '[MetaEngine] Failed to parse project metadata JSON, using null metadata:',
+          error,
+        );
         this.projectMetadata = null;
         return;
       }
@@ -605,7 +640,10 @@ export class MetaEngine extends EventEmitter {
   /**
    * Load category metadata from the filesystem.
    */
-  async loadCategoryMetadata(): Promise | null> {
+  async loadCategoryMetadata(): Promise | null> {
     try {
       const filePath = this.getCategoryMetadataFilePath();
       const content = await fs.readFile(filePath, 'utf-8');
@@ -613,7 +651,10 @@ export class MetaEngine extends EventEmitter {
       return normalizeCategoryMetadata(parsed);
     } catch (error) {
       if (isJsonParseError(error)) {
-        console.warn('[MetaEngine] Failed to parse category metadata JSON, using default metadata merge:', error);
+        console.warn(
+          '[MetaEngine] Failed to parse category metadata JSON, using default metadata merge:',
+          error,
+        );
         return null;
       }
       if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
@@ -641,7 +682,10 @@ export class MetaEngine extends EventEmitter {
       }
     } catch (error) {
       if (isJsonParseError(error)) {
-        console.warn('[MetaEngine] Failed to parse categories JSON, treating as empty and rebuilding from DB/defaults:', error);
+        console.warn(
+          '[MetaEngine] Failed to parse categories JSON, treating as empty and rebuilding from DB/defaults:',
+          error,
+        );
         this.categories.clear();
         return;
       }
@@ -678,16 +722,26 @@ export class MetaEngine extends EventEmitter {
       .where(eq(posts.projectId, this.currentProjectId))
       .all();
 
-    return collectNormalizedTermsFromJsonValues(dbPosts.map((row) => row.categories));
+    return collectNormalizedTermsFromJsonValues(
+      dbPosts.map((row) => row.categories),
+    );
   }
 
   /**
    * Fetch the current project's data from the database.
    */
-  private async fetchProjectFromDatabase(): Promise<{ name: string; description: string | null; dataPath: string | null } | null> {
+  private async fetchProjectFromDatabase(): Promise<{
+    name: string;
+    description: string | null;
+    dataPath: string | null;
+  } | null> {
     const db = getDatabase().getLocal();
     const project = await db
-      .select({ name: projects.name, description: projects.description, dataPath: projects.dataPath })
+      .select({
+        name: projects.name,
+        description: projects.description,
+        dataPath: projects.dataPath,
+      })
       .from(projects)
       .where(eq(projects.id, this.currentProjectId))
       .get();
@@ -719,7 +773,10 @@ export class MetaEngine extends EventEmitter {
     }
   }
 
-  private async writeJsonFileAtomically(filePath: string, value: unknown): Promise {
+  private async writeJsonFileAtomically(
+    filePath: string,
+    value: unknown,
+  ): Promise {
     const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
     const content = JSON.stringify(value, null, 2);
 
@@ -749,7 +806,10 @@ export class MetaEngine extends EventEmitter {
           showTitle: true,
           title: category,
         };
-      } else if (!merged[category].title || merged[category].title.trim().length === 0) {
+      } else if (
+        !merged[category].title ||
+        merged[category].title.trim().length === 0
+      ) {
         merged[category].title = category;
       }
     }
@@ -759,7 +819,7 @@ export class MetaEngine extends EventEmitter {
 
   /**
    * Sync tags and categories on startup.
-   * 
+   *
    * Logic:
    * - Tags: populated from posts (TagEngine handles persistence with colors)
    * - Categories: read from file, merge with database
@@ -784,34 +844,36 @@ export class MetaEngine extends EventEmitter {
   }
 
   private async performSyncOnStartup(): Promise {
-    console.log(`[MetaEngine] Syncing metadata for project: ${this.currentProjectId}`);
-    
     await this.ensureMetaDirExists();
-    
+
     const categoriesFilePath = this.getCategoriesFilePath();
     const projectMetadataFilePath = this.getProjectMetadataFilePath();
     const categoryMetadataFilePath = this.getCategoryMetadataFilePath();
-    
+
     const categoriesFileExists = await this.fileExists(categoriesFilePath);
-    const projectMetadataFileExists = await this.fileExists(projectMetadataFilePath);
-    const categoryMetadataFileExists = await this.fileExists(categoryMetadataFilePath);
-    
+    const projectMetadataFileExists = await this.fileExists(
+      projectMetadataFilePath,
+    );
+    const categoryMetadataFileExists = await this.fileExists(
+      categoryMetadataFilePath,
+    );
+
     // Collect tags/categories from database (posts)
     const dbTags = await this.collectTagsFromPosts();
     const dbCategories = await this.collectCategoriesFromPosts();
-    
+
     // Handle tags - just populate from posts, TagEngine handles persistence
     this.tags.clear();
     for (const tag of dbTags) {
       this.tags.add(tag);
     }
-    
+
     // Handle categories
     if (categoriesFileExists) {
       // Load from file
       await this.loadCategories();
       const fileCategories = new Set(this.categories);
-      
+
       // Merge: add any categories from DB that aren't in file
       let changed = false;
       for (const cat of dbCategories) {
@@ -820,7 +882,7 @@ export class MetaEngine extends EventEmitter {
           changed = true;
         }
       }
-      
+
       // Save if there were changes
       if (changed) {
         await this.saveCategories();
@@ -840,14 +902,16 @@ export class MetaEngine extends EventEmitter {
       }
       await this.saveCategories();
     }
-    
+
     // Handle project metadata
     if (projectMetadataFileExists) {
       await this.loadProjectMetadata();
       if (!this.projectMetadata) {
         const projectData = await this.fetchProjectFromDatabase();
         if (!projectData) {
-          throw new Error(`Project not found in database: ${this.currentProjectId}`);
+          throw new Error(
+            `Project not found in database: ${this.currentProjectId}`,
+          );
         }
         this.projectMetadata = {
           name: projectData.name,
@@ -857,16 +921,18 @@ export class MetaEngine extends EventEmitter {
         await this.saveProjectMetadata();
       }
       if (this.projectMetadata?.dataPath !== undefined) {
-        const { dataPath: _dataPath, ...metadataWithoutDataPath } = this.projectMetadata;
+        const metadataWithoutDataPath = { ...this.projectMetadata };
+        delete metadataWithoutDataPath.dataPath;
         this.projectMetadata = metadataWithoutDataPath;
         await this.saveProjectMetadata();
-        console.log('[MetaEngine] Removed deprecated dataPath from project.json');
       }
     } else {
       // No file exists, fetch project data from database and create file
       const projectData = await this.fetchProjectFromDatabase();
       if (!projectData) {
-        throw new Error(`Project not found in database: ${this.currentProjectId}`);
+        throw new Error(
+          `Project not found in database: ${this.currentProjectId}`,
+        );
       }
       this.projectMetadata = {
         name: projectData.name,
@@ -878,14 +944,16 @@ export class MetaEngine extends EventEmitter {
 
     if (this.projectMetadata) {
       const legacyCategoryMetadata = normalizeCategoryMetadata(
-        this.projectMetadata.categoryMetadata ?? this.projectMetadata.categorySettings,
+        this.projectMetadata.categoryMetadata ??
+          this.projectMetadata.categorySettings,
       );
       const fileCategoryMetadata = categoryMetadataFileExists
         ? await this.loadCategoryMetadata()
         : null;
-      const mergedCategoryMetadata = this.ensureCategoryMetadataForKnownCategories(
-        fileCategoryMetadata ?? legacyCategoryMetadata,
-      );
+      const mergedCategoryMetadata =
+        this.ensureCategoryMetadataForKnownCategories(
+          fileCategoryMetadata ?? legacyCategoryMetadata,
+        );
 
       this.projectMetadata = normalizeProjectMetadata({
         ...this.projectMetadata,
@@ -898,9 +966,8 @@ export class MetaEngine extends EventEmitter {
 
     // Handle publishing preferences (load from file if it exists)
     await this.loadPublishingPreferences();
-    
+
     this.initialized = true;
-    console.log(`[MetaEngine] Sync complete. Tags: ${this.tags.size}, Categories: ${this.categories.size}`);
   }
 
   /**
@@ -910,4 +977,3 @@ export class MetaEngine extends EventEmitter {
     return this.initialized;
   }
 }
-
diff --git a/src/main/engine/MetadataDiffEngine.ts b/src/main/engine/MetadataDiffEngine.ts
index a8613ec..3ff59d8 100644
--- a/src/main/engine/MetadataDiffEngine.ts
+++ b/src/main/engine/MetadataDiffEngine.ts
@@ -12,7 +12,7 @@ import * as path from 'path';
 import { eq, and } from 'drizzle-orm';
 import { getDatabase } from '../database';
 import { posts, postTranslations, media, scripts, templates } from '../database/schema';
-import { readPostFile, PostFileData } from './postFileUtils';
+import { readPostFile } from './postFileUtils';
 import { readPostTranslationFile } from './postTranslationFileUtils';
 import { taskManager } from './TaskManager';
 import type { PostEngine } from './PostEngine';
@@ -223,7 +223,7 @@ export class MetadataDiffEngine extends EventEmitter {
     postIds: string[],
     onProgress: ((percent: number, message: string) => void) | undefined,
     processPost: (postId: string) => Promise,
-    errorMessage: (postId: string) => string
+    errorMessage: (postId: string) => string,
   ): Promise<{ success: number; failed: number }> {
     const total = postIds.length;
     let success = 0;
@@ -278,58 +278,59 @@ export class MetadataDiffEngine extends EventEmitter {
    * Get statistics about the posts, media, scripts, and templates tables
    */
   async getTableStats(): Promise {
-    const db = this.getDb();
     const client = this.getClient();
-    if (!client) throw new Error('Database not initialized');
+    if (!client) {
+      throw new Error('Database not initialized');
+    }
 
     // Get post counts
     const allPostsResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM posts WHERE project_id = ?`,
+      sql: 'SELECT COUNT(*) as count FROM posts WHERE project_id = ?',
       args: [this.currentProjectId],
     });
     const totalPosts = Number(allPostsResult.rows[0]?.count ?? 0);
 
     const publishedResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM posts WHERE project_id = ? AND status = 'published' AND file_path IS NOT NULL AND file_path != ''`,
+      sql: 'SELECT COUNT(*) as count FROM posts WHERE project_id = ? AND status = \'published\' AND file_path IS NOT NULL AND file_path != \'\'',
       args: [this.currentProjectId],
     });
     const publishedPosts = Number(publishedResult.rows[0]?.count ?? 0);
 
     const draftResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM posts WHERE project_id = ? AND status = 'draft'`,
+      sql: 'SELECT COUNT(*) as count FROM posts WHERE project_id = ? AND status = \'draft\'',
       args: [this.currentProjectId],
     });
     const draftPosts = Number(draftResult.rows[0]?.count ?? 0);
 
     // Get media count
     const mediaResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM media WHERE project_id = ?`,
+      sql: 'SELECT COUNT(*) as count FROM media WHERE project_id = ?',
       args: [this.currentProjectId],
     });
     const totalMedia = Number(mediaResult.rows[0]?.count ?? 0);
 
     // Get script counts
     const allScriptsResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM scripts WHERE project_id = ?`,
+      sql: 'SELECT COUNT(*) as count FROM scripts WHERE project_id = ?',
       args: [this.currentProjectId],
     });
     const totalScripts = Number(allScriptsResult.rows[0]?.count ?? 0);
 
     const publishedScriptsResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM scripts WHERE project_id = ? AND status = 'published'`,
+      sql: 'SELECT COUNT(*) as count FROM scripts WHERE project_id = ? AND status = \'published\'',
       args: [this.currentProjectId],
     });
     const publishedScripts = Number(publishedScriptsResult.rows[0]?.count ?? 0);
 
     // Get template counts
     const allTemplatesResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM templates WHERE project_id = ?`,
+      sql: 'SELECT COUNT(*) as count FROM templates WHERE project_id = ?',
       args: [this.currentProjectId],
     });
     const totalTemplates = Number(allTemplatesResult.rows[0]?.count ?? 0);
 
     const publishedTemplatesResult = await client.execute({
-      sql: `SELECT COUNT(*) as count FROM templates WHERE project_id = ? AND status = 'published'`,
+      sql: 'SELECT COUNT(*) as count FROM templates WHERE project_id = ? AND status = \'published\'',
       args: [this.currentProjectId],
     });
     const publishedTemplates = Number(publishedTemplatesResult.rows[0]?.count ?? 0);
@@ -448,14 +449,30 @@ export class MetadataDiffEngine extends EventEmitter {
       const missingDiffs: Partial> = {};
       const dbTags: string[] = JSON.parse(dbPost.tags || '[]');
       const dbCategories: string[] = JSON.parse(dbPost.categories || '[]');
-      if (dbPost.title) missingDiffs.title = { dbValue: dbPost.title, fileValue: null };
-      if (dbTags.length > 0) missingDiffs.tags = { dbValue: dbTags, fileValue: null };
-      if (dbCategories.length > 0) missingDiffs.categories = { dbValue: dbCategories, fileValue: null };
-      if (dbPost.excerpt) missingDiffs.excerpt = { dbValue: dbPost.excerpt, fileValue: null };
-      if (dbPost.author) missingDiffs.author = { dbValue: dbPost.author, fileValue: null };
-      if (dbPost.language) missingDiffs.language = { dbValue: dbPost.language, fileValue: null };
-      if (dbPost.doNotTranslate) missingDiffs.doNotTranslate = { dbValue: true, fileValue: null };
-      if (dbPost.templateSlug) missingDiffs.templateSlug = { dbValue: dbPost.templateSlug, fileValue: null };
+      if (dbPost.title) {
+        missingDiffs.title = { dbValue: dbPost.title, fileValue: null };
+      }
+      if (dbTags.length > 0) {
+        missingDiffs.tags = { dbValue: dbTags, fileValue: null };
+      }
+      if (dbCategories.length > 0) {
+        missingDiffs.categories = { dbValue: dbCategories, fileValue: null };
+      }
+      if (dbPost.excerpt) {
+        missingDiffs.excerpt = { dbValue: dbPost.excerpt, fileValue: null };
+      }
+      if (dbPost.author) {
+        missingDiffs.author = { dbValue: dbPost.author, fileValue: null };
+      }
+      if (dbPost.language) {
+        missingDiffs.language = { dbValue: dbPost.language, fileValue: null };
+      }
+      if (dbPost.doNotTranslate) {
+        missingDiffs.doNotTranslate = { dbValue: true, fileValue: null };
+      }
+      if (dbPost.templateSlug) {
+        missingDiffs.templateSlug = { dbValue: dbPost.templateSlug, fileValue: null };
+      }
       return {
         postId: dbPost.id,
         title: dbPost.title,
@@ -549,7 +566,9 @@ export class MetadataDiffEngine extends EventEmitter {
    * Compare arrays for equality (order-independent)
    */
   private arraysEqual(a: string[], b: string[]): boolean {
-    if (a.length !== b.length) return false;
+    if (a.length !== b.length) {
+      return false;
+    }
     const sortedA = [...a].sort();
     const sortedB = [...b].sort();
     return sortedA.every((val, idx) => val === sortedB[idx]);
@@ -557,8 +576,12 @@ export class MetadataDiffEngine extends EventEmitter {
 
   /** Compare two dates at second precision (SQLite stores integer seconds). */
   private datesEqualSeconds(a: Date | null | undefined, b: Date | null | undefined): boolean {
-    if (!a && !b) return true;
-    if (!a || !b) return false;
+    if (!a && !b) {
+      return true;
+    }
+    if (!a || !b) {
+      return false;
+    }
     return Math.floor(a.getTime() / 1000) === Math.floor(b.getTime() / 1000);
   }
 
@@ -572,7 +595,9 @@ export class MetadataDiffEngine extends EventEmitter {
     postsBaseDir?: string,
   ): Promise {
     const client = this.getClient();
-    if (!client) throw new Error('Database not initialized');
+    if (!client) {
+      throw new Error('Database not initialized');
+    }
 
     // Get all published posts with file paths
     const result = await client.execute({
@@ -609,7 +634,9 @@ export class MetadataDiffEngine extends EventEmitter {
       const row = publishedPosts[i];
       const postId = row.id as string;
       const filePath = row.file_path as string;
-      if (filePath) knownFilePaths.add(filePath);
+      if (filePath) {
+        knownFilePaths.add(filePath);
+      }
 
       const diff = await this.comparePostMetadata(postId);
       if (diff && diff.hasDifferences) {
@@ -625,7 +652,9 @@ export class MetadataDiffEngine extends EventEmitter {
       const row = publishedTranslations[i];
       const translationId = row.id as string;
       const filePath = row.file_path as string;
-      if (filePath) knownFilePaths.add(filePath);
+      if (filePath) {
+        knownFilePaths.add(filePath);
+      }
 
       const diff = await this.comparePostMetadata(translationId);
       if (diff && diff.hasDifferences) {
@@ -640,7 +669,7 @@ export class MetadataDiffEngine extends EventEmitter {
 
     // Also include file_paths from non-published posts so we don't flag them as orphans
     const allPostsResult = await client.execute({
-      sql: `SELECT file_path FROM posts WHERE project_id = ? AND file_path IS NOT NULL AND file_path != ''`,
+      sql: 'SELECT file_path FROM posts WHERE project_id = ? AND file_path IS NOT NULL AND file_path != \'\'',
       args: [this.currentProjectId],
     });
     for (const row of allPostsResult.rows) {
@@ -648,7 +677,7 @@ export class MetadataDiffEngine extends EventEmitter {
     }
 
     const allTranslationsResult = await client.execute({
-      sql: `SELECT file_path FROM post_translations WHERE project_id = ? AND file_path IS NOT NULL AND file_path != ''`,
+      sql: 'SELECT file_path FROM post_translations WHERE project_id = ? AND file_path IS NOT NULL AND file_path != \'\'',
       args: [this.currentProjectId],
     });
     for (const row of allTranslationsResult.rows) {
@@ -679,7 +708,9 @@ export class MetadataDiffEngine extends EventEmitter {
     onProgress: (current: number, total: number, message: string) => void,
     scannedSoFar: number,
   ): Promise {
-    if (!postsBaseDir) return [];
+    if (!postsBaseDir) {
+      return [];
+    }
 
     const markdownExtensions = new Set(['.md', '.markdown', '.mdx']);
     const allFiles: string[] = [];
@@ -709,7 +740,9 @@ export class MetadataDiffEngine extends EventEmitter {
     // Filter to files not in the known DB set
     const orphanPaths = allFiles.filter(f => !knownFilePaths.has(f));
 
-    if (orphanPaths.length === 0) return [];
+    if (orphanPaths.length === 0) {
+      return [];
+    }
 
     const orphanFiles: OrphanFile[] = [];
     for (let i = 0; i < orphanPaths.length; i++) {
@@ -777,7 +810,9 @@ export class MetadataDiffEngine extends EventEmitter {
     for (const diff of diffs) {
       for (const [field, fieldDiff] of Object.entries(diff.differences)) {
         const fieldKey = field as DiffField;
-        if (!fieldDiff) continue;
+        if (!fieldDiff) {
+          continue;
+        }
 
         if (!groupMap.has(fieldKey)) {
           groupMap.set(fieldKey, {
@@ -787,7 +822,11 @@ export class MetadataDiffEngine extends EventEmitter {
           });
         }
 
-        groupMap.get(fieldKey)!.posts.push({
+        const postGroup = groupMap.get(fieldKey);
+        if (!postGroup) {
+          continue;
+        }
+        postGroup.posts.push({
           postId: diff.postId,
           title: diff.title,
           slug: diff.slug,
@@ -806,10 +845,12 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async syncDbToFile(
     postIds: string[],
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
     const postEngine = this.postEngine;
-    if (!postEngine) throw new Error('MetadataDiffEngine: postEngine not injected');
+    if (!postEngine) {
+      throw new Error('MetadataDiffEngine: postEngine not injected');
+    }
     return this.runSyncLoop(
       postIds,
       onProgress,
@@ -823,7 +864,7 @@ export class MetadataDiffEngine extends EventEmitter {
         }
         return false;
       },
-      (postId) => `[MetadataDiffEngine] Failed to sync post ${postId} to file:`
+      (postId) => `[MetadataDiffEngine] Failed to sync post ${postId} to file:`,
     );
   }
 
@@ -834,7 +875,7 @@ export class MetadataDiffEngine extends EventEmitter {
   async syncFileToDb(
     postIds: string[],
     field?: DiffField,
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
     const db = this.getDb();
     return this.runSyncLoop(
@@ -944,7 +985,7 @@ export class MetadataDiffEngine extends EventEmitter {
 
         return true;
       },
-      (postId) => `[MetadataDiffEngine] Failed to sync post ${postId} to DB:`
+      (postId) => `[MetadataDiffEngine] Failed to sync post ${postId} to DB:`,
     );
   }
 
@@ -955,7 +996,9 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async compareMediaMetadata(mediaId: string): Promise {
     const db = this.getDb();
-    if (!this.mediaEngine) throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    if (!this.mediaEngine) {
+      throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    }
 
     const dbMedia = await db
       .select()
@@ -963,19 +1006,33 @@ export class MetadataDiffEngine extends EventEmitter {
       .where(and(eq(media.id, mediaId), eq(media.projectId, this.currentProjectId)))
       .get();
 
-    if (!dbMedia) return null;
+    if (!dbMedia) {
+      return null;
+    }
 
     const sidecarPath = `${dbMedia.filePath}.meta`;
     const sidecar = await this.mediaEngine.readSidecarFile(sidecarPath);
     if (!sidecar) {
       const missingDiffs: Partial> = {};
-      if (dbMedia.title) missingDiffs.title = { dbValue: dbMedia.title, fileValue: null };
-      if (dbMedia.alt) missingDiffs.alt = { dbValue: dbMedia.alt, fileValue: null };
-      if (dbMedia.caption) missingDiffs.caption = { dbValue: dbMedia.caption, fileValue: null };
-      if (dbMedia.author) missingDiffs.author = { dbValue: dbMedia.author, fileValue: null };
-      if (dbMedia.language) missingDiffs.language = { dbValue: dbMedia.language, fileValue: null };
+      if (dbMedia.title) {
+        missingDiffs.title = { dbValue: dbMedia.title, fileValue: null };
+      }
+      if (dbMedia.alt) {
+        missingDiffs.alt = { dbValue: dbMedia.alt, fileValue: null };
+      }
+      if (dbMedia.caption) {
+        missingDiffs.caption = { dbValue: dbMedia.caption, fileValue: null };
+      }
+      if (dbMedia.author) {
+        missingDiffs.author = { dbValue: dbMedia.author, fileValue: null };
+      }
+      if (dbMedia.language) {
+        missingDiffs.language = { dbValue: dbMedia.language, fileValue: null };
+      }
       const dbTags: string[] = JSON.parse(dbMedia.tags || '[]');
-      if (dbTags.length > 0) missingDiffs.tags = { dbValue: dbTags, fileValue: null };
+      if (dbTags.length > 0) {
+        missingDiffs.tags = { dbValue: dbTags, fileValue: null };
+      }
       return {
         mediaId: dbMedia.id,
         originalName: dbMedia.originalName,
@@ -1023,13 +1080,15 @@ export class MetadataDiffEngine extends EventEmitter {
    * Scan all media and find metadata differences
    */
   async scanAllMedia(
-    onProgress: (current: number, total: number, message: string) => void
+    onProgress: (current: number, total: number, message: string) => void,
   ): Promise {
     const client = this.getClient();
-    if (!client) throw new Error('Database not initialized');
+    if (!client) {
+      throw new Error('Database not initialized');
+    }
 
     const result = await client.execute({
-      sql: `SELECT id FROM media WHERE project_id = ?`,
+      sql: 'SELECT id FROM media WHERE project_id = ?',
       args: [this.currentProjectId],
     });
 
@@ -1074,11 +1133,17 @@ export class MetadataDiffEngine extends EventEmitter {
     for (const diff of diffs) {
       for (const [field, fieldDiff] of Object.entries(diff.differences)) {
         const fieldKey = field as MediaDiffField;
-        if (!fieldDiff) continue;
+        if (!fieldDiff) {
+          continue;
+        }
         if (!groupMap.has(fieldKey)) {
           groupMap.set(fieldKey, { field: fieldKey, label: fieldLabels[fieldKey], items: [] });
         }
-        groupMap.get(fieldKey)!.items.push({
+        const mediaGroup = groupMap.get(fieldKey);
+        if (!mediaGroup) {
+          continue;
+        }
+        mediaGroup.items.push({
           mediaId: diff.mediaId,
           originalName: diff.originalName,
           dbValue: fieldDiff.dbValue,
@@ -1095,9 +1160,11 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async syncMediaDbToFile(
     mediaIds: string[],
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.mediaEngine) throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    if (!this.mediaEngine) {
+      throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    }
     const mediaEngine = this.mediaEngine;
     return this.runSyncLoop(
       mediaIds,
@@ -1105,7 +1172,9 @@ export class MetadataDiffEngine extends EventEmitter {
       async (mediaId) => {
         // Re-save the media with its current DB values to regenerate sidecar
         const item = await mediaEngine.getMedia(mediaId);
-        if (!item) return false;
+        if (!item) {
+          return false;
+        }
         await mediaEngine.updateMedia(mediaId, {
           title: item.title,
           alt: item.alt,
@@ -1115,7 +1184,7 @@ export class MetadataDiffEngine extends EventEmitter {
         });
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync media ${id} to file:`
+      (id) => `[MetadataDiffEngine] Failed to sync media ${id} to file:`,
     );
   }
 
@@ -1125,9 +1194,11 @@ export class MetadataDiffEngine extends EventEmitter {
   async syncMediaFileToDb(
     mediaIds: string[],
     field?: MediaDiffField,
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.mediaEngine) throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    if (!this.mediaEngine) {
+      throw new Error('MetadataDiffEngine: mediaEngine not injected');
+    }
     const db = this.getDb();
     const mediaEngine = this.mediaEngine;
     return this.runSyncLoop(
@@ -1139,24 +1210,40 @@ export class MetadataDiffEngine extends EventEmitter {
           .from(media)
           .where(and(eq(media.id, mediaId), eq(media.projectId, this.currentProjectId)))
           .get();
-        if (!dbMedia) return false;
+        if (!dbMedia) {
+          return false;
+        }
 
         const sidecar = await mediaEngine.readSidecarFile(`${dbMedia.filePath}.meta`);
-        if (!sidecar) return false;
+        if (!sidecar) {
+          return false;
+        }
 
         const updateData: Record = { updatedAt: new Date() };
 
-        if (!field || field === 'title') updateData.title = sidecar.title || null;
-        if (!field || field === 'alt') updateData.alt = sidecar.alt || null;
-        if (!field || field === 'caption') updateData.caption = sidecar.caption || null;
-        if (!field || field === 'author') updateData.author = sidecar.author || null;
-        if (!field || field === 'language') updateData.language = sidecar.language || null;
-        if (!field || field === 'tags') updateData.tags = JSON.stringify(sidecar.tags || []);
+        if (!field || field === 'title') {
+          updateData.title = sidecar.title || null;
+        }
+        if (!field || field === 'alt') {
+          updateData.alt = sidecar.alt || null;
+        }
+        if (!field || field === 'caption') {
+          updateData.caption = sidecar.caption || null;
+        }
+        if (!field || field === 'author') {
+          updateData.author = sidecar.author || null;
+        }
+        if (!field || field === 'language') {
+          updateData.language = sidecar.language || null;
+        }
+        if (!field || field === 'tags') {
+          updateData.tags = JSON.stringify(sidecar.tags || []);
+        }
 
         await db.update(media).set(updateData).where(eq(media.id, mediaId));
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync media ${id} to DB:`
+      (id) => `[MetadataDiffEngine] Failed to sync media ${id} to DB:`,
     );
   }
 
@@ -1167,7 +1254,9 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async compareScriptMetadata(scriptId: string): Promise {
     const db = this.getDb();
-    if (!this.scriptEngine) throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    if (!this.scriptEngine) {
+      throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    }
 
     const dbScript = await db
       .select()
@@ -1175,18 +1264,30 @@ export class MetadataDiffEngine extends EventEmitter {
       .where(and(eq(scripts.id, scriptId), eq(scripts.projectId, this.currentProjectId)))
       .get();
 
-    if (!dbScript) return null;
+    if (!dbScript) {
+      return null;
+    }
     // Skip drafts — they don't have on-disk files
-    if (dbScript.status === 'draft') return null;
+    if (dbScript.status === 'draft') {
+      return null;
+    }
 
     const parsed = await this.scriptEngine.readScriptFileWithMetadata(dbScript.filePath);
     if (!parsed) {
       const missingDiffs: Partial> = {};
-      if (dbScript.title) missingDiffs.title = { dbValue: dbScript.title, fileValue: null };
-      if (dbScript.kind) missingDiffs.kind = { dbValue: dbScript.kind, fileValue: null };
-      if (dbScript.entrypoint) missingDiffs.entrypoint = { dbValue: dbScript.entrypoint, fileValue: null };
+      if (dbScript.title) {
+        missingDiffs.title = { dbValue: dbScript.title, fileValue: null };
+      }
+      if (dbScript.kind) {
+        missingDiffs.kind = { dbValue: dbScript.kind, fileValue: null };
+      }
+      if (dbScript.entrypoint) {
+        missingDiffs.entrypoint = { dbValue: dbScript.entrypoint, fileValue: null };
+      }
       missingDiffs.enabled = { dbValue: !!dbScript.enabled, fileValue: null };
-      if (dbScript.version) missingDiffs.version = { dbValue: dbScript.version, fileValue: null };
+      if (dbScript.version) {
+        missingDiffs.version = { dbValue: dbScript.version, fileValue: null };
+      }
       return {
         scriptId: dbScript.id,
         title: dbScript.title,
@@ -1233,13 +1334,15 @@ export class MetadataDiffEngine extends EventEmitter {
    * Scan all published scripts and find metadata differences
    */
   async scanAllScripts(
-    onProgress: (current: number, total: number, message: string) => void
+    onProgress: (current: number, total: number, message: string) => void,
   ): Promise {
     const client = this.getClient();
-    if (!client) throw new Error('Database not initialized');
+    if (!client) {
+      throw new Error('Database not initialized');
+    }
 
     const result = await client.execute({
-      sql: `SELECT id FROM scripts WHERE project_id = ? AND status = 'published'`,
+      sql: 'SELECT id FROM scripts WHERE project_id = ? AND status = \'published\'',
       args: [this.currentProjectId],
     });
 
@@ -1283,11 +1386,17 @@ export class MetadataDiffEngine extends EventEmitter {
     for (const diff of diffs) {
       for (const [field, fieldDiff] of Object.entries(diff.differences)) {
         const fieldKey = field as ScriptDiffField;
-        if (!fieldDiff) continue;
+        if (!fieldDiff) {
+          continue;
+        }
         if (!groupMap.has(fieldKey)) {
           groupMap.set(fieldKey, { field: fieldKey, label: fieldLabels[fieldKey], items: [] });
         }
-        groupMap.get(fieldKey)!.items.push({
+        const scriptGroup = groupMap.get(fieldKey);
+        if (!scriptGroup) {
+          continue;
+        }
+        scriptGroup.items.push({
           scriptId: diff.scriptId,
           title: diff.title,
           slug: diff.slug,
@@ -1305,9 +1414,11 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async syncScriptDbToFile(
     scriptIds: string[],
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.scriptEngine) throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    if (!this.scriptEngine) {
+      throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    }
     const scriptEngine = this.scriptEngine;
     return this.runSyncLoop(
       scriptIds,
@@ -1315,11 +1426,13 @@ export class MetadataDiffEngine extends EventEmitter {
       async (scriptId) => {
         // Trigger an updateScript with no actual changes — this re-serialises the file
         const item = await scriptEngine.getScript(scriptId);
-        if (!item) return false;
+        if (!item) {
+          return false;
+        }
         await scriptEngine.updateScript(scriptId, { title: item.title });
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync script ${id} to file:`
+      (id) => `[MetadataDiffEngine] Failed to sync script ${id} to file:`,
     );
   }
 
@@ -1329,9 +1442,11 @@ export class MetadataDiffEngine extends EventEmitter {
   async syncScriptFileToDb(
     scriptIds: string[],
     field?: ScriptDiffField,
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.scriptEngine) throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    if (!this.scriptEngine) {
+      throw new Error('MetadataDiffEngine: scriptEngine not injected');
+    }
     const db = this.getDb();
     const scriptEngine = this.scriptEngine;
     return this.runSyncLoop(
@@ -1343,26 +1458,38 @@ export class MetadataDiffEngine extends EventEmitter {
           .from(scripts)
           .where(and(eq(scripts.id, scriptId), eq(scripts.projectId, this.currentProjectId)))
           .get();
-        if (!dbScript) return false;
+        if (!dbScript) {
+          return false;
+        }
 
         const parsed = await scriptEngine.readScriptFileWithMetadata(dbScript.filePath);
-        if (!parsed) return false;
+        if (!parsed) {
+          return false;
+        }
         const fm = parsed.metadata;
 
         const updateData: Record = { updatedAt: new Date() };
 
-        if (!field || field === 'title') updateData.title = fm.title || dbScript.title;
-        if (!field || field === 'kind') updateData.kind = fm.kind || dbScript.kind;
-        if (!field || field === 'entrypoint') updateData.entrypoint = fm.entrypoint || dbScript.entrypoint;
+        if (!field || field === 'title') {
+          updateData.title = fm.title || dbScript.title;
+        }
+        if (!field || field === 'kind') {
+          updateData.kind = fm.kind || dbScript.kind;
+        }
+        if (!field || field === 'entrypoint') {
+          updateData.entrypoint = fm.entrypoint || dbScript.entrypoint;
+        }
         if (!field || field === 'enabled') {
           updateData.enabled = !!fm.enabled;
         }
-        if (!field || field === 'version') updateData.version = fm.version ?? dbScript.version;
+        if (!field || field === 'version') {
+          updateData.version = fm.version ?? dbScript.version;
+        }
 
         await db.update(scripts).set(updateData).where(eq(scripts.id, scriptId));
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync script ${id} to DB:`
+      (id) => `[MetadataDiffEngine] Failed to sync script ${id} to DB:`,
     );
   }
 
@@ -1373,7 +1500,9 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async compareTemplateMetadata(templateId: string): Promise {
     const db = this.getDb();
-    if (!this.templateEngine) throw new Error('MetadataDiffEngine: templateEngine not injected');
+    if (!this.templateEngine) {
+      throw new Error('MetadataDiffEngine: templateEngine not injected');
+    }
 
     const dbTemplate = await db
       .select()
@@ -1381,16 +1510,26 @@ export class MetadataDiffEngine extends EventEmitter {
       .where(and(eq(templates.id, templateId), eq(templates.projectId, this.currentProjectId)))
       .get();
 
-    if (!dbTemplate) return null;
-    if (dbTemplate.status === 'draft') return null;
+    if (!dbTemplate) {
+      return null;
+    }
+    if (dbTemplate.status === 'draft') {
+      return null;
+    }
 
     const parsed = await this.templateEngine.readTemplateFileWithMetadata(dbTemplate.filePath);
     if (!parsed) {
       const missingDiffs: Partial> = {};
-      if (dbTemplate.title) missingDiffs.title = { dbValue: dbTemplate.title, fileValue: null };
-      if (dbTemplate.kind) missingDiffs.kind = { dbValue: dbTemplate.kind, fileValue: null };
+      if (dbTemplate.title) {
+        missingDiffs.title = { dbValue: dbTemplate.title, fileValue: null };
+      }
+      if (dbTemplate.kind) {
+        missingDiffs.kind = { dbValue: dbTemplate.kind, fileValue: null };
+      }
       missingDiffs.enabled = { dbValue: !!dbTemplate.enabled, fileValue: null };
-      if (dbTemplate.version) missingDiffs.version = { dbValue: dbTemplate.version, fileValue: null };
+      if (dbTemplate.version) {
+        missingDiffs.version = { dbValue: dbTemplate.version, fileValue: null };
+      }
       return {
         templateId: dbTemplate.id,
         title: dbTemplate.title,
@@ -1434,13 +1573,15 @@ export class MetadataDiffEngine extends EventEmitter {
    * Scan all published templates and find metadata differences
    */
   async scanAllTemplates(
-    onProgress: (current: number, total: number, message: string) => void
+    onProgress: (current: number, total: number, message: string) => void,
   ): Promise {
     const client = this.getClient();
-    if (!client) throw new Error('Database not initialized');
+    if (!client) {
+      throw new Error('Database not initialized');
+    }
 
     const result = await client.execute({
-      sql: `SELECT id FROM templates WHERE project_id = ? AND status = 'published'`,
+      sql: 'SELECT id FROM templates WHERE project_id = ? AND status = \'published\'',
       args: [this.currentProjectId],
     });
 
@@ -1483,11 +1624,17 @@ export class MetadataDiffEngine extends EventEmitter {
     for (const diff of diffs) {
       for (const [field, fieldDiff] of Object.entries(diff.differences)) {
         const fieldKey = field as TemplateDiffField;
-        if (!fieldDiff) continue;
+        if (!fieldDiff) {
+          continue;
+        }
         if (!groupMap.has(fieldKey)) {
           groupMap.set(fieldKey, { field: fieldKey, label: fieldLabels[fieldKey], items: [] });
         }
-        groupMap.get(fieldKey)!.items.push({
+        const templateGroup = groupMap.get(fieldKey);
+        if (!templateGroup) {
+          continue;
+        }
+        templateGroup.items.push({
           templateId: diff.templateId,
           title: diff.title,
           slug: diff.slug,
@@ -1505,20 +1652,24 @@ export class MetadataDiffEngine extends EventEmitter {
    */
   async syncTemplateDbToFile(
     templateIds: string[],
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.templateEngine) throw new Error('MetadataDiffEngine: templateEngine not injected');
+    if (!this.templateEngine) {
+      throw new Error('MetadataDiffEngine: templateEngine not injected');
+    }
     const templateEngine = this.templateEngine;
     return this.runSyncLoop(
       templateIds,
       onProgress,
       async (templateId) => {
         const item = await templateEngine.getTemplate(templateId);
-        if (!item) return false;
+        if (!item) {
+          return false;
+        }
         await templateEngine.updateTemplate(templateId, { title: item.title });
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync template ${id} to file:`
+      (id) => `[MetadataDiffEngine] Failed to sync template ${id} to file:`,
     );
   }
 
@@ -1528,9 +1679,11 @@ export class MetadataDiffEngine extends EventEmitter {
   async syncTemplateFileToDb(
     templateIds: string[],
     field?: TemplateDiffField,
-    onProgress?: (percent: number, message: string) => void
+    onProgress?: (percent: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
-    if (!this.templateEngine) throw new Error('MetadataDiffEngine: templateEngine not injected');
+    if (!this.templateEngine) {
+      throw new Error('MetadataDiffEngine: templateEngine not injected');
+    }
     const db = this.getDb();
     const templateEngine = this.templateEngine;
     return this.runSyncLoop(
@@ -1542,25 +1695,35 @@ export class MetadataDiffEngine extends EventEmitter {
           .from(templates)
           .where(and(eq(templates.id, templateId), eq(templates.projectId, this.currentProjectId)))
           .get();
-        if (!dbTemplate) return false;
+        if (!dbTemplate) {
+          return false;
+        }
 
         const parsed = await templateEngine.readTemplateFileWithMetadata(dbTemplate.filePath);
-        if (!parsed) return false;
+        if (!parsed) {
+          return false;
+        }
         const fm = parsed.metadata;
 
         const updateData: Record = { updatedAt: new Date() };
 
-        if (!field || field === 'title') updateData.title = fm.title || dbTemplate.title;
-        if (!field || field === 'kind') updateData.kind = fm.kind || dbTemplate.kind;
+        if (!field || field === 'title') {
+          updateData.title = fm.title || dbTemplate.title;
+        }
+        if (!field || field === 'kind') {
+          updateData.kind = fm.kind || dbTemplate.kind;
+        }
         if (!field || field === 'enabled') {
           updateData.enabled = !!fm.enabled;
         }
-        if (!field || field === 'version') updateData.version = fm.version ?? dbTemplate.version;
+        if (!field || field === 'version') {
+          updateData.version = fm.version ?? dbTemplate.version;
+        }
 
         await db.update(templates).set(updateData).where(eq(templates.id, templateId));
         return true;
       },
-      (id) => `[MetadataDiffEngine] Failed to sync template ${id} to DB:`
+      (id) => `[MetadataDiffEngine] Failed to sync template ${id} to DB:`,
     );
   }
 
@@ -1739,7 +1902,9 @@ export class MetadataDiffEngine extends EventEmitter {
     onProgress?: (current: number, total: number, message: string) => void,
   ): Promise<{ success: number; failed: number }> {
     const postEngine = this.postEngine;
-    if (!postEngine) throw new Error('MetadataDiffEngine: postEngine not injected');
+    if (!postEngine) {
+      throw new Error('MetadataDiffEngine: postEngine not injected');
+    }
 
     let success = 0;
     let failed = 0;
diff --git a/src/main/engine/ModelCatalogEngine.ts b/src/main/engine/ModelCatalogEngine.ts
index f6c237a..28e47a0 100644
--- a/src/main/engine/ModelCatalogEngine.ts
+++ b/src/main/engine/ModelCatalogEngine.ts
@@ -110,7 +110,9 @@ export class ModelCatalogEngine {
       // Search across all providers, return first match
       rows = await db.select().from(modelCatalog).where(eq(modelCatalog.modelId, modelId));
     }
-    if (rows.length === 0) return null;
+    if (rows.length === 0) {
+      return null;
+    }
     const row = rows[0];
     const modalities = await db.select().from(modelCatalogModalities).where(
       and(eq(modelCatalogModalities.provider, row.provider), eq(modelCatalogModalities.modelId, row.modelId)),
@@ -277,7 +279,9 @@ export class ModelCatalogEngine {
     let count = 0;
 
     for (const [id, info] of Object.entries(models)) {
-      if (!info || typeof info !== 'object') continue;
+      if (!info || typeof info !== 'object') {
+        continue;
+      }
 
       const entry = {
         provider: providerId,
diff --git a/src/main/engine/NotificationWatcher.ts b/src/main/engine/NotificationWatcher.ts
index 7bba9f2..48296a9 100644
--- a/src/main/engine/NotificationWatcher.ts
+++ b/src/main/engine/NotificationWatcher.ts
@@ -83,12 +83,16 @@ export class NotificationWatcher {
   }
 
   private schedule(): void {
-    if (this.debounceTimer) clearTimeout(this.debounceTimer);
+    if (this.debounceTimer) {
+      clearTimeout(this.debounceTimer);
+    }
     this.debounceTimer = setTimeout(() => this.process(), this.debounceMs);
   }
 
   private async process(): Promise {
-    if (this.isProcessing) return;
+    if (this.isProcessing) {
+      return;
+    }
     this.isProcessing = true;
     try {
       const rows = await this.db
diff --git a/src/main/engine/PageRenderer.ts b/src/main/engine/PageRenderer.ts
index 824002d..1accbb3 100644
--- a/src/main/engine/PageRenderer.ts
+++ b/src/main/engine/PageRenderer.ts
@@ -271,7 +271,7 @@ export const PREVIEW_ASSETS: Record = {
         modulePath: `@picocss/pico/css/pico.${theme}.min.css`,
         contentType: 'text/css; charset=utf-8',
       },
-    ])
+    ]),
   ),
   'lightbox.min.css': {
     modulePath: 'lightbox2/dist/css/lightbox.min.css',
@@ -352,8 +352,12 @@ export function clampMaxPostsPerPage(value: unknown): number {
   }
 
   const normalized = Math.floor(value);
-  if (normalized < MIN_MAX_POSTS_PER_PAGE) return DEFAULT_MAX_POSTS_PER_PAGE;
-  if (normalized > MAX_MAX_POSTS_PER_PAGE) return MAX_MAX_POSTS_PER_PAGE;
+  if (normalized < MIN_MAX_POSTS_PER_PAGE) {
+    return DEFAULT_MAX_POSTS_PER_PAGE;
+  }
+  if (normalized > MAX_MAX_POSTS_PER_PAGE) {
+    return MAX_MAX_POSTS_PER_PAGE;
+  }
   return normalized;
 }
 
@@ -391,7 +395,9 @@ function escapeHtml(value: string): string {
 }
 
 function parseMacroParams(paramString: string | undefined): Record {
-  if (!paramString) return {};
+  if (!paramString) {
+    return {};
+  }
 
   const params: Record = {};
   const regex = /(\w+)=(?:["']([^"']*?)["']|([^\s\]]+))/g;
@@ -405,7 +411,9 @@ function parseMacroParams(paramString: string | undefined): Record, tagUsage: TagUsageE
 
 function isExternalOrSpecialUrl(value: string): boolean {
   const normalized = value.trim();
-  if (!normalized) return false;
-  if (normalized.startsWith('#') || normalized.startsWith('//')) return true;
+  if (!normalized) {
+    return false;
+  }
+  if (normalized.startsWith('#') || normalized.startsWith('//')) {
+    return true;
+  }
   return /^[a-z][a-z0-9+.-]*:/i.test(normalized);
 }
 
@@ -841,9 +853,13 @@ export function rewriteRenderedHtmlUrls(html: string, rewriteContext: HtmlRewrit
 }
 
 export function applyLanguagePrefixToHtml(html: string, languagePrefix: string): string {
-  if (!languagePrefix) return html;
+  if (!languagePrefix) {
+    return html;
+  }
   return html.replace(/\bhref=(['"])(\/(?!media\/|assets\/).*?)\1/gi, (_fullMatch, quote: string, href: string) => {
-    if (href.startsWith(languagePrefix + '/') || href === languagePrefix) return `href=${quote}${href}${quote}`;
+    if (href.startsWith(languagePrefix + '/') || href === languagePrefix) {
+      return `href=${quote}${href}${quote}`;
+    }
     return `href=${quote}${languagePrefix}${href}${quote}`;
   });
 }
@@ -863,7 +879,9 @@ export function renderMacro(
     const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
     const id = (params.id || '').trim();
     const title = (params.title || translateRender(language, 'render.video.youtubeTitle')).trim();
-    if (!id) return '';
+    if (!id) {
+      return '';
+    }
     return renderMacroTemplate('youtube', { id, title });
   }
 
@@ -871,7 +889,9 @@ export function renderMacro(
     const language = resolveRenderLanguageFromProjectPreferences(renderLanguage);
     const id = (params.id || '').trim();
     const title = (params.title || translateRender(language, 'render.video.vimeoTitle')).trim();
-    if (!id) return '';
+    if (!id) {
+      return '';
+    }
     return renderMacroTemplate('vimeo', { id, title });
   }
 
@@ -1428,20 +1448,20 @@ export class PageRenderer {
       show_archive_range_heading: hasRangeHeading,
       archive_context: options.routeKind === 'date'
         ? {
-            kind: options.archiveContext?.kind ?? 'root',
-            name: options.archiveContext?.name ?? null,
-            year: options.archiveContext?.year ?? null,
-            month: options.archiveContext?.month ?? null,
-            day: options.archiveContext?.day ?? null,
-          }
+          kind: options.archiveContext?.kind ?? 'root',
+          name: options.archiveContext?.name ?? null,
+          year: options.archiveContext?.year ?? null,
+          month: options.archiveContext?.month ?? null,
+          day: options.archiveContext?.day ?? null,
+        }
         : options.archiveContext
           ? {
-              kind: options.archiveContext.kind,
-              name: options.archiveContext.name ?? null,
-              year: options.archiveContext.year ?? null,
-              month: options.archiveContext.month ?? null,
-              day: options.archiveContext.day ?? null,
-            }
+            kind: options.archiveContext.kind,
+            name: options.archiveContext.name ?? null,
+            year: options.archiveContext.year ?? null,
+            month: options.archiveContext.month ?? null,
+            day: options.archiveContext.day ?? null,
+          }
           : null,
       min_date: minDateParts,
       max_date: maxDateParts,
diff --git a/src/main/engine/PostEngine.ts b/src/main/engine/PostEngine.ts
index b44d327..f367d83 100644
--- a/src/main/engine/PostEngine.ts
+++ b/src/main/engine/PostEngine.ts
@@ -4,13 +4,13 @@ import * as fs from 'fs/promises';
 import * as path from 'path';
 import * as crypto from 'crypto';
 import matter from 'gray-matter';
-import { eq, and, desc, gte, lte, like, inArray, ne, sql } from 'drizzle-orm';
+import { eq, and, desc, gte, lte, inArray, ne, sql } from 'drizzle-orm';
 import { app } from 'electron';
 import { getDatabase } from '../database';
 import { posts, postTranslations, Post, PostTranslation, NewPost, NewPostTranslation, postLinks } from '../database/schema';
 import { taskManager, Task } from './TaskManager';
 import { stemText, stemQuery, isoToStemmerLanguage, SupportedLanguage } from './stemmer';
-import { readPostFile as readPostFileShared, type PostFileData } from './postFileUtils';
+import { readPostFile as readPostFileShared } from './postFileUtils';
 import { readPostTranslationFile as readPostTranslationFileShared, type PostTranslationFileData } from './postTranslationFileUtils';
 import { CliNotifier, NoopNotifier } from './CliNotifier';
 import type { MediaEngine } from './MediaEngine';
@@ -150,7 +150,9 @@ export class PostEngine extends EventEmitter {
   }
 
   /** No persistent cache — DB is the source of truth. No-op for watcher compat. */
-  invalidate(_entityId?: string): void {}
+  invalidate(entityId?: string): void {
+    void entityId;
+  }
 
   /**
    * Set the language used for full-text search stemming.
@@ -199,7 +201,9 @@ export class PostEngine extends EventEmitter {
     categories: string[];
   }): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return;
+    if (!client) {
+      return;
+    }
 
     // Delete existing entry
     await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [post.id] });
@@ -268,14 +272,18 @@ export class PostEngine extends EventEmitter {
    */
   private async deleteFTSIndex(id: string): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return;
+    if (!client) {
+      return;
+    }
     await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [id] });
   }
 
   private dataDir: string | null = null;
 
   private getDataDir(): string {
-    if (this.dataDir) return this.dataDir;
+    if (this.dataDir) {
+      return this.dataDir;
+    }
     const userDataPath = app.getPath('userData');
     return path.join(userDataPath, 'projects', this.currentProjectId);
   }
@@ -339,12 +347,16 @@ export class PostEngine extends EventEmitter {
       .from(posts)
       .where(and(
         eq(posts.slug, slug),
-        eq(posts.projectId, this.currentProjectId)
+        eq(posts.projectId, this.currentProjectId),
       ))
       .get();
     
-    if (!existing) return true;
-    if (excludePostId && existing.id === excludePostId) return true;
+    if (!existing) {
+      return true;
+    }
+    if (excludePostId && existing.id === excludePostId) {
+      return true;
+    }
     return false;
   }
 
@@ -562,12 +574,24 @@ export class PostEngine extends EventEmitter {
     };
 
     // Only add optional fields if they have values (gray-matter can't serialize undefined)
-    if (post.excerpt) metadata.excerpt = post.excerpt;
-    if (post.author) metadata.author = post.author;
-    if (post.language) metadata.language = post.language;
-    if (post.doNotTranslate) metadata.doNotTranslate = true;
-    if (post.templateSlug) metadata.templateSlug = post.templateSlug;
-    if (post.publishedAt) metadata.publishedAt = post.publishedAt.toISOString();
+    if (post.excerpt) {
+      metadata.excerpt = post.excerpt;
+    }
+    if (post.author) {
+      metadata.author = post.author;
+    }
+    if (post.language) {
+      metadata.language = post.language;
+    }
+    if (post.doNotTranslate) {
+      metadata.doNotTranslate = true;
+    }
+    if (post.templateSlug) {
+      metadata.templateSlug = post.templateSlug;
+    }
+    if (post.publishedAt) {
+      metadata.publishedAt = post.publishedAt.toISOString();
+    }
 
     // Use date-based directory structure (posts/YYYY/MM/)
     const postsDir = this.getPostsDirForDate(post.createdAt);
@@ -587,7 +611,9 @@ export class PostEngine extends EventEmitter {
       title: translation.title,
     };
 
-    if (translation.excerpt) metadata.excerpt = translation.excerpt;
+    if (translation.excerpt) {
+      metadata.excerpt = translation.excerpt;
+    }
 
     const postsDir = this.getPostsDirForDate(sourcePost.createdAt);
     await fs.mkdir(postsDir, { recursive: true });
@@ -600,7 +626,9 @@ export class PostEngine extends EventEmitter {
 
   private async readPostFile(filePath: string): Promise {
     const data = await readPostFileShared(filePath);
-    if (!data) return null;
+    if (!data) {
+      return null;
+    }
 
     const fileStem = path.parse(filePath).name;
     const normalizedTitle = typeof data.title === 'string' && data.title.trim().length > 0
@@ -727,7 +755,6 @@ export class PostEngine extends EventEmitter {
 
   async createPost(data: Partial): Promise {
     const db = getDatabase().getLocal();
-    const client = getDatabase().getLocalClient();
     const now = new Date();
     const id = uuidv4();
     
@@ -790,7 +817,6 @@ export class PostEngine extends EventEmitter {
 
   async updatePost(id: string, data: Partial): Promise {
     const db = getDatabase().getLocal();
-    const client = getDatabase().getLocalClient();
     const existing = await this.getPost(id);
     
     if (!existing) {
@@ -895,7 +921,6 @@ export class PostEngine extends EventEmitter {
 
   async deletePost(id: string): Promise {
     const db = getDatabase().getLocal();
-    const client = getDatabase().getLocalClient();
     const existing = await db.select().from(posts).where(eq(posts.id, id)).get();
 
     if (!existing) {
@@ -995,7 +1020,7 @@ export class PostEngine extends EventEmitter {
       .from(posts)
       .where(and(
         eq(posts.slug, slug),
-        eq(posts.projectId, this.currentProjectId)
+        eq(posts.projectId, this.currentProjectId),
       ))
       .get();
     const post = await this.resolvePostData(dbPost);
@@ -1064,12 +1089,20 @@ export class PostEngine extends EventEmitter {
     const postById = new Map(allPosts.map((p) => [p.id, p]));
     const result = new Map();
     for (const row of allTranslations) {
-      if (row.status !== 'published') continue;
+      if (row.status !== 'published') {
+        continue;
+      }
       const sourcePost = postById.get(row.translationFor);
-      if (!sourcePost) continue;
-      if (this.isCanonicalTranslationLanguage(sourcePost, row.language)) continue;
+      if (!sourcePost) {
+        continue;
+      }
+      if (this.isCanonicalTranslationLanguage(sourcePost, row.language)) {
+        continue;
+      }
       const lang = row.language?.trim().toLowerCase();
-      if (!lang) continue;
+      if (!lang) {
+        continue;
+      }
       const existing = result.get(row.translationFor) ?? [];
       existing.push(lang);
       result.set(row.translationFor, existing);
@@ -1083,10 +1116,16 @@ export class PostEngine extends EventEmitter {
     const result = new Map();
 
     for (const row of allRows) {
-      if (row.status !== 'published') continue;
+      if (row.status !== 'published') {
+        continue;
+      }
       const sourcePost = postById.get(row.translationFor);
-      if (!sourcePost) continue;
-      if (this.isCanonicalTranslationLanguage(sourcePost, row.language)) continue;
+      if (!sourcePost) {
+        continue;
+      }
+      if (this.isCanonicalTranslationLanguage(sourcePost, row.language)) {
+        continue;
+      }
 
       const translationData: PostTranslationData = {
         id: row.id,
@@ -1526,7 +1565,9 @@ export class PostEngine extends EventEmitter {
     const db = getDatabase().getLocal();
 
     const postData = await this.readPostFile(filePath);
-    if (!postData) return null;
+    if (!postData) {
+      return null;
+    }
 
     // Ensure unique ID and slug within the current project
     const { id, slug } = await this.ensureUniquePostIdentity(postData.id, postData.slug);
@@ -1659,7 +1700,7 @@ export class PostEngine extends EventEmitter {
         .from(posts)
         .where(and(
           eq(posts.projectId, this.currentProjectId),
-          eq(posts.status, 'draft')
+          eq(posts.status, 'draft'),
         ))
         .orderBy(desc(posts.createdAt))
         .all();
@@ -1671,7 +1712,7 @@ export class PostEngine extends EventEmitter {
         .from(posts)
         .where(and(
           eq(posts.projectId, this.currentProjectId),
-          ne(posts.status, 'draft')
+          ne(posts.status, 'draft'),
         ))
         .orderBy(desc(posts.createdAt))
         .limit(remainingSlots)
@@ -1679,7 +1720,7 @@ export class PostEngine extends EventEmitter {
 
       const allDbPosts = [...draftPosts, ...nonDraftPosts];
       const items = await this.appendAvailableLanguagesToList(allDbPosts.map(dbPost =>
-        this.dbRowToPostData(dbPost, dbPost.content || '')
+        this.dbRowToPostData(dbPost, dbPost.content || ''),
       ));
 
       return {
@@ -1696,7 +1737,7 @@ export class PostEngine extends EventEmitter {
       .from(posts)
       .where(and(
         eq(posts.projectId, this.currentProjectId),
-        eq(posts.status, 'draft')
+        eq(posts.status, 'draft'),
       ))
       .all();
     const numDrafts = draftCount.length;
@@ -1709,7 +1750,7 @@ export class PostEngine extends EventEmitter {
       .from(posts)
       .where(and(
         eq(posts.projectId, this.currentProjectId),
-        ne(posts.status, 'draft')
+        ne(posts.status, 'draft'),
       ))
       .orderBy(desc(posts.createdAt))
       .limit(limit)
@@ -1717,7 +1758,7 @@ export class PostEngine extends EventEmitter {
       .all();
 
     const items = await this.appendAvailableLanguagesToList(dbPosts.map(dbPost =>
-      this.dbRowToPostData(dbPost, dbPost.content || '')
+      this.dbRowToPostData(dbPost, dbPost.content || ''),
     ));
 
     return {
@@ -1752,7 +1793,7 @@ export class PostEngine extends EventEmitter {
       .from(posts)
       .where(and(
         eq(posts.projectId, this.currentProjectId),
-        eq(posts.status, status)
+        eq(posts.status, status),
       ))
       .orderBy(desc(posts.createdAt))
       .all();
@@ -1798,7 +1839,7 @@ export class PostEngine extends EventEmitter {
           select 1
           from json_each(${posts.categories}) as included_category
           where included_category.value = ${category}
-        )`
+        )`,
       );
       conditions.push(sql`(${sql.join(includePredicates, sql` OR `)})`);
     }
@@ -1809,7 +1850,7 @@ export class PostEngine extends EventEmitter {
           select 1
           from json_each(${posts.categories}) as excluded_category
           where excluded_category.value = ${category}
-        )`
+        )`,
       );
       conditions.push(sql`NOT (${sql.join(excludePredicates, sql` OR `)})`);
     }
@@ -1821,7 +1862,7 @@ export class PostEngine extends EventEmitter {
       .orderBy(desc(posts.createdAt))
       .all();
 
-    let result: PostData[] = [];
+    const result: PostData[] = [];
 
     for (const dbPost of dbPosts) {
       // Use DB data directly instead of reading from filesystem
@@ -1830,7 +1871,9 @@ export class PostEngine extends EventEmitter {
       // Client-side filtering for tags only (category filtering is done in SQL)
       if (filter.tags && filter.tags.length > 0) {
         const hasAllTags = filter.tags.every(tag => postData.tags.includes(tag));
-        if (!hasAllTags) continue;
+        if (!hasAllTags) {
+          continue;
+        }
       }
 
       result.push(postData);
@@ -1865,7 +1908,9 @@ export class PostEngine extends EventEmitter {
     const stems = new Set();
     for (const lang of languages) {
       const stemmed = stemQuery(query, lang);
-      if (stemmed) stems.add(stemmed);
+      if (stemmed) {
+        stems.add(stemmed);
+      }
     }
 
     if (stems.size <= 1) {
@@ -1883,21 +1928,25 @@ export class PostEngine extends EventEmitter {
     const rows = await this.getAllTranslationRows();
     const langs = new Set();
     for (const row of rows) {
-      if (row.language) langs.add(row.language);
+      if (row.language) {
+        langs.add(row.language);
+      }
     }
     return Array.from(langs);
   }
 
   async searchPosts(query: string): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return [];
+    if (!client) {
+      return [];
+    }
 
     try {
       const multilingualQuery = await this.buildMultilingualFTSQuery(query);
       
       // Search the stemmed content, filtered by project_id for project isolation
       const result = await client.execute({
-        sql: `SELECT id FROM posts_fts WHERE project_id = ? AND posts_fts MATCH ? ORDER BY rank LIMIT 500`,
+        sql: 'SELECT id FROM posts_fts WHERE project_id = ? AND posts_fts MATCH ? ORDER BY rank LIMIT 500',
         args: [this.currentProjectId, multilingualQuery],
       });
 
@@ -1935,10 +1984,14 @@ export class PostEngine extends EventEmitter {
     filter: PostFilter,
     pagination?: PaginationOptions,
   ): Promise<{ posts: PostData[]; total: number }> {
-    if (!query.trim()) return { posts: [], total: 0 };
+    if (!query.trim()) {
+      return { posts: [], total: 0 };
+    }
 
     const client = getDatabase().getLocalClient();
-    if (!client) return { posts: [], total: 0 };
+    if (!client) {
+      return { posts: [], total: 0 };
+    }
 
     try {
       const multilingualQuery = await this.buildMultilingualFTSQuery(query);
@@ -1972,14 +2025,14 @@ export class PostEngine extends EventEmitter {
       }
       if (filter.categories && filter.categories.length > 0) {
         const catClauses = filter.categories.map(() =>
-          `EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)`
+          'EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)',
         );
         conditions.push(`(${catClauses.join(' OR ')})`);
         args.push(...filter.categories);
       }
       if (filter.excludeCategories && filter.excludeCategories.length > 0) {
         const exClauses = filter.excludeCategories.map(() =>
-          `EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)`
+          'EXISTS (SELECT 1 FROM json_each(posts.categories) AS c WHERE c.value = ?)',
         );
         conditions.push(`NOT (${exClauses.join(' OR ')})`);
         args.push(...filter.excludeCategories);
@@ -2006,23 +2059,26 @@ export class PostEngine extends EventEmitter {
       const result = await client.execute({ sql: sqlQuery, args });
 
       let postDataList: PostData[] = result.rows.map((row) =>
-        this.dbRowToPostData(row as unknown as Post, (row.content as string) || '')
+        this.dbRowToPostData(row as unknown as Post, (row.content as string) || ''),
       );
 
       // Tag filtering is done client-side (tags are stored as JSON arrays)
-      if (filter.tags && filter.tags.length > 0) {
+      const filterTags = filter.tags;
+      if (filterTags && filterTags.length > 0) {
         postDataList = postDataList.filter((p) =>
-          filter.tags!.every((tag) => p.tags.includes(tag))
+          filterTags.every((tag) => p.tags.includes(tag)),
         );
       }
 
       postDataList = await this.appendAvailableLanguagesToList(postDataList);
 
-      if (filter.language) {
-        postDataList = postDataList.filter((post) => post.availableLanguages.includes(filter.language!));
+      const filterLanguage = filter.language;
+      if (filterLanguage) {
+        postDataList = postDataList.filter((post) => post.availableLanguages.includes(filterLanguage));
       }
-      if (filter.missingTranslationLanguage) {
-        postDataList = postDataList.filter((post) => !post.availableLanguages.includes(filter.missingTranslationLanguage!));
+      const missingTranslationLanguage = filter.missingTranslationLanguage;
+      if (missingTranslationLanguage) {
+        postDataList = postDataList.filter((post) => !post.availableLanguages.includes(missingTranslationLanguage));
       }
 
       // Apply pagination
@@ -2045,7 +2101,9 @@ export class PostEngine extends EventEmitter {
     filter?: { year?: number; month?: number; status?: string; category?: string; tags?: string[] },
   ): Promise<{ groups: Record[]; totalPosts: number }> {
     const client = getDatabase().getLocalClient();
-    if (!client) return { groups: [], totalPosts: 0 };
+    if (!client) {
+      return { groups: [], totalPosts: 0 };
+    }
 
     // Build SELECT expressions and GROUP BY columns
     const selectExprs: string[] = [];
@@ -2054,28 +2112,28 @@ export class PostEngine extends EventEmitter {
 
     for (const dim of groupBy) {
       switch (dim) {
-        case 'year':
-          selectExprs.push("CAST(strftime('%Y', posts.created_at, 'unixepoch') AS INTEGER) AS g_year");
-          groupByCols.push('g_year');
-          break;
-        case 'month':
-          selectExprs.push("CAST(strftime('%m', posts.created_at, 'unixepoch') AS INTEGER) AS g_month");
-          groupByCols.push('g_month');
-          break;
-        case 'tag':
-          selectExprs.push('t.value AS g_tag');
-          joins.push('JOIN json_each(posts.tags) AS t');
-          groupByCols.push('g_tag');
-          break;
-        case 'category':
-          selectExprs.push('c.value AS g_category');
-          joins.push('JOIN json_each(posts.categories) AS c');
-          groupByCols.push('g_category');
-          break;
-        case 'status':
-          selectExprs.push('posts.status AS g_status');
-          groupByCols.push('g_status');
-          break;
+      case 'year':
+        selectExprs.push('CAST(strftime(\'%Y\', posts.created_at, \'unixepoch\') AS INTEGER) AS g_year');
+        groupByCols.push('g_year');
+        break;
+      case 'month':
+        selectExprs.push('CAST(strftime(\'%m\', posts.created_at, \'unixepoch\') AS INTEGER) AS g_month');
+        groupByCols.push('g_month');
+        break;
+      case 'tag':
+        selectExprs.push('t.value AS g_tag');
+        joins.push('JOIN json_each(posts.tags) AS t');
+        groupByCols.push('g_tag');
+        break;
+      case 'category':
+        selectExprs.push('c.value AS g_category');
+        joins.push('JOIN json_each(posts.categories) AS c');
+        groupByCols.push('g_category');
+        break;
+      case 'status':
+        selectExprs.push('posts.status AS g_status');
+        groupByCols.push('g_status');
+        break;
       }
     }
 
@@ -2109,14 +2167,14 @@ export class PostEngine extends EventEmitter {
     }
     if (filter?.category) {
       conditions.push(
-        `EXISTS (SELECT 1 FROM json_each(posts.categories) AS fc WHERE fc.value = ?)`,
+        'EXISTS (SELECT 1 FROM json_each(posts.categories) AS fc WHERE fc.value = ?)',
       );
       args.push(filter.category);
     }
     if (filter?.tags && filter.tags.length > 0) {
       for (const tag of filter.tags) {
         conditions.push(
-          `EXISTS (SELECT 1 FROM json_each(posts.tags) AS ft WHERE ft.value = ?)`,
+          'EXISTS (SELECT 1 FROM json_each(posts.tags) AS ft WHERE ft.value = ?)',
         );
         args.push(tag);
       }
@@ -2140,12 +2198,18 @@ export class PostEngine extends EventEmitter {
         g_category: 'category', g_status: 'status',
       };
 
-      const groups: Record[] = result.rows.map((row: any) => {
+      const groups: Record[] = result.rows.map((row) => {
+        const typedRow = row as Record;
         const group: Record = {};
         for (const col of groupByCols) {
-          group[dimMap[col]] = row[col];
+          const mappedKey = dimMap[col];
+          if (mappedKey === 'year' || mappedKey === 'month') {
+            group[mappedKey] = Number(typedRow[col]);
+            continue;
+          }
+          group[mappedKey] = String(typedRow[col] ?? '');
         }
-        group.count = Number(row.cnt);
+        group.count = Number(typedRow.cnt);
         return group;
       });
 
@@ -2240,9 +2304,9 @@ export class PostEngine extends EventEmitter {
 
     for (const row of dbPosts) {
       switch (row.status) {
-        case 'draft': draftCount++; break;
-        case 'published': publishedCount++; break;
-        case 'archived': archivedCount++; break;
+      case 'draft': draftCount++; break;
+      case 'published': publishedCount++; break;
+      case 'archived': archivedCount++; break;
       }
     }
 
@@ -2283,23 +2347,31 @@ export class PostEngine extends EventEmitter {
 
     for (const row of dbPosts) {
       switch (row.status) {
-        case 'draft': draftCount++; break;
-        case 'published': publishedCount++; break;
-        case 'archived': archivedCount++; break;
+      case 'draft': draftCount++; break;
+      case 'published': publishedCount++; break;
+      case 'archived': archivedCount++; break;
       }
 
       const created = row.createdAt;
-      if (!oldestPostDate || created < oldestPostDate) oldestPostDate = created;
-      if (!newestPostDate || created > newestPostDate) newestPostDate = created;
+      if (!oldestPostDate || created < oldestPostDate) {
+        oldestPostDate = created;
+      }
+      if (!newestPostDate || created > newestPostDate) {
+        newestPostDate = created;
+      }
 
       const year = created.getFullYear();
       postsPerYear[year] = (postsPerYear[year] || 0) + 1;
 
       const parsedTags: string[] = JSON.parse(row.tags || '[]');
-      for (const tag of parsedTags) uniqueTags.add(tag);
+      for (const tag of parsedTags) {
+        uniqueTags.add(tag);
+      }
 
       const parsedCategories: string[] = JSON.parse(row.categories || '[]');
-      for (const cat of parsedCategories) uniqueCategories.add(cat);
+      for (const cat of parsedCategories) {
+        uniqueCategories.add(cat);
+      }
     }
 
     return {
@@ -2329,14 +2401,15 @@ export class PostEngine extends EventEmitter {
     }
 
     return Array.from(counts.values()).sort((a, b) => {
-      if (a.year !== b.year) return b.year - a.year;
+      if (a.year !== b.year) {
+        return b.year - a.year;
+      }
       return b.month - a.month;
     });
   }
 
   async publishPost(id: string): Promise {
     const db = getDatabase().getLocal();
-    const client = getDatabase().getLocalClient();
     const existing = await this.getPost(id);
     
     if (!existing) {
@@ -2396,7 +2469,9 @@ export class PostEngine extends EventEmitter {
     const translationRows = this.filterCanonicalTranslationRows(published, await this.getTranslationRowsForPost(id));
     for (const row of translationRows) {
       const translation = await this.resolvePostTranslationData(row);
-      if (!translation) continue;
+      if (!translation) {
+        continue;
+      }
 
       const publishedTranslation: PostTranslationData = {
         ...translation,
@@ -2432,14 +2507,15 @@ export class PostEngine extends EventEmitter {
 
   async publishPostTranslation(postId: string, language: string): Promise {
     const existing = await this.getTranslationRow(postId, language.trim().toLowerCase());
-    if (!existing) return null;
+    if (!existing) {
+      return null;
+    }
     await this.publishPost(postId);
     return this.getPostTranslation(postId, language.trim().toLowerCase());
   }
 
   async discardChanges(id: string): Promise {
     const db = getDatabase().getLocal();
-    const client = getDatabase().getLocalClient();
     const dbPost = await db.select().from(posts).where(eq(posts.id, id)).get();
     
     if (!dbPost) {
@@ -2540,7 +2616,9 @@ export class PostEngine extends EventEmitter {
 
   async getPublishedVersionsBulk(ids: string[]): Promise> {
     const result = new Map();
-    if (ids.length === 0) return result;
+    if (ids.length === 0) {
+      return result;
+    }
 
     const db = getDatabase().getLocal();
     const idSet = new Set(ids);
@@ -2550,7 +2628,9 @@ export class PostEngine extends EventEmitter {
       .all();
 
     for (const dbPost of dbPosts) {
-      if (!idSet.has(dbPost.id) || !dbPost.filePath) continue;
+      if (!idSet.has(dbPost.id) || !dbPost.filePath) {
+        continue;
+      }
       result.set(dbPost.id, this.dbRowToPostData(dbPost, ''));
     }
 
@@ -2582,7 +2662,9 @@ export class PostEngine extends EventEmitter {
    */
   async rebuildFTSIndex(): Promise {
     const client = getDatabase().getLocalClient();
-    if (!client) return;
+    if (!client) {
+      return;
+    }
 
     const allPosts = await this.getAllPostsUnpaginated();
     
@@ -2590,7 +2672,7 @@ export class PostEngine extends EventEmitter {
       await this.updateFTSIndex(post);
     }
     
-    console.log(`Rebuilt FTS index for ${allPosts.length} posts`);
+    return;
   }
 
   async reconcilePublishedPostsFromGitChanges(
@@ -2878,7 +2960,6 @@ export class PostEngine extends EventEmitter {
         }
 
         onProgress(100, `Reindexed ${total} posts`);
-        console.log(`Reindexed search text for ${total} posts`);
       },
     };
 
@@ -2892,7 +2973,6 @@ export class PostEngine extends EventEmitter {
       name: 'Rebuild database from post files',
       execute: async (onProgress) => {
         const db = getDatabase().getLocal();
-        const client = getDatabase().getLocalClient();
 
         onProgress(0, 'Deleting existing posts for project...');
 
@@ -2912,7 +2992,6 @@ export class PostEngine extends EventEmitter {
           await db.delete(postLinks).where(inArray(postLinks.targetPostId, postIds));
           // Delete posts
           await db.delete(posts).where(eq(posts.projectId, this.currentProjectId));
-          console.log(`Deleted ${existingPosts.length} existing post(s) for project ${this.currentProjectId}`);
         }
         await db.delete(postTranslations).where(eq(postTranslations.projectId, this.currentProjectId));
 
@@ -2956,12 +3035,6 @@ export class PostEngine extends EventEmitter {
         const translationFiles: Array<{ filePath: string; data: PostTranslationFileData }> = [];
         let importedCount = 0;
         let importedTranslationCount = 0;
-        let parseFailedCount = 0;
-        let deduplicatedSlugCount = 0;
-        let deduplicatedIdCount = 0;
-        let insertFailedCount = 0;
-        let skippedTranslationMissingSourceCount = 0;
-        let skippedDuplicateTranslationCount = 0;
 
         for (let i = 0; i < markdownFiles.length; i++) {
           const filePath = markdownFiles[i];
@@ -2980,7 +3053,6 @@ export class PostEngine extends EventEmitter {
           const postData = await this.readPostFile(filePath);
 
           if (!postData) {
-            parseFailedCount++;
             continue;
           }
 
@@ -2990,7 +3062,6 @@ export class PostEngine extends EventEmitter {
             let postId = postData.id;
             while (insertedIds.has(postId)) {
               postId = uuidv4();
-              deduplicatedIdCount++;
             }
 
             let slug = postData.slug;
@@ -2999,7 +3070,6 @@ export class PostEngine extends EventEmitter {
             while (insertedSlugs.has(`${projectId}:${slug}`)) {
               slug = `${baseSlug}-${slugAttempt}`;
               slugAttempt++;
-              deduplicatedSlugCount++;
             }
 
             const checksum = this.calculateChecksum(postData.content);
@@ -3045,9 +3115,8 @@ export class PostEngine extends EventEmitter {
               tags: postData.tags,
               categories: postData.categories,
             });
-          } catch (error: any) {
-            insertFailedCount++;
-            if (error?.code === 'SQLITE_CONSTRAINT_UNIQUE') {
+          } catch (error) {
+            if (typeof error === 'object' && error !== null && 'code' in error && (error as { code?: unknown }).code === 'SQLITE_CONSTRAINT_UNIQUE') {
               console.error(`Failed to insert post "${postData.title}" from ${filePath}: Unique constraint violation`);
             } else {
               console.error(`Failed to process post from ${filePath}:`, error);
@@ -3068,11 +3137,8 @@ export class PostEngine extends EventEmitter {
           onProgress,
         );
         importedTranslationCount = translationImportResult.imported;
-        skippedTranslationMissingSourceCount = translationImportResult.skippedMissingSource;
-        skippedDuplicateTranslationCount = translationImportResult.skippedDuplicates;
 
         onProgress(100, `Database rebuild complete: imported ${importedCount} posts and ${importedTranslationCount} translations from ${markdownFiles.length} files`);
-        console.log(`[PostEngine] rebuildDatabaseFromFiles complete. scanned=${markdownFiles.length}, importedPosts=${importedCount}, importedTranslations=${importedTranslationCount}, parseFailed=${parseFailedCount}, insertFailed=${insertFailedCount}, deduplicatedSlugs=${deduplicatedSlugCount}, deduplicatedIds=${deduplicatedIdCount}, skippedTranslationsMissingSource=${skippedTranslationMissingSourceCount}, skippedDuplicateTranslations=${skippedDuplicateTranslationCount}`);
         this.emit('databaseRebuilt');
       },
     };
@@ -3112,7 +3178,9 @@ export class PostEngine extends EventEmitter {
     // Delete existing links from this post
     await db.delete(postLinks).where(eq(postLinks.sourcePostId, postId));
     
-    if (extractedLinks.length === 0) return;
+    if (extractedLinks.length === 0) {
+      return;
+    }
     
     // Get all posts to resolve slugs to IDs
     const allPosts = await db.select({ id: posts.id, slug: posts.slug })
@@ -3150,7 +3218,9 @@ export class PostEngine extends EventEmitter {
       .from(postLinks)
       .where(eq(postLinks.targetPostId, postId));
     
-    if (links.length === 0) return [];
+    if (links.length === 0) {
+      return [];
+    }
     
     const sourceIds = links.map(l => l.sourcePostId);
     const sourcePosts = await db
@@ -3176,7 +3246,9 @@ export class PostEngine extends EventEmitter {
       })
       .from(postLinks);
 
-    if (allLinks.length === 0) return new Map();
+    if (allLinks.length === 0) {
+      return new Map();
+    }
 
     const sourceIds = new Set(allLinks.map(l => l.sourcePostId));
     const allSourcePosts = await db
@@ -3191,7 +3263,9 @@ export class PostEngine extends EventEmitter {
     const result = new Map();
     for (const link of allLinks) {
       const sourcePost = sourcePostById.get(link.sourcePostId);
-      if (!sourcePost) continue;
+      if (!sourcePost) {
+        continue;
+      }
       const existing = result.get(link.targetPostId);
       if (existing) {
         existing.push(sourcePost);
@@ -3217,7 +3291,9 @@ export class PostEngine extends EventEmitter {
       .from(postLinks)
       .where(eq(postLinks.sourcePostId, postId));
     
-    if (links.length === 0) return [];
+    if (links.length === 0) {
+      return [];
+    }
     
     const targetIds = links.map(l => l.targetPostId);
     const targetPosts = await db
diff --git a/src/main/engine/PostMediaEngine.ts b/src/main/engine/PostMediaEngine.ts
index aeeabbf..ac2c75e 100644
--- a/src/main/engine/PostMediaEngine.ts
+++ b/src/main/engine/PostMediaEngine.ts
@@ -120,7 +120,7 @@ export class PostMediaEngine extends EventEmitter {
       existingByMediaId: Map;
       nextSortOrder: number;
     },
-    createdAt: Date
+    createdAt: Date,
   ): Promise<{ linked: true; link: PostMediaLinkData } | { linked: false; existing: PostMediaLinkData }> {
     const existing = state.existingByMediaId.get(mediaId);
     if (existing) {
@@ -144,8 +144,8 @@ export class PostMediaEngine extends EventEmitter {
     await db.delete(postMedia).where(
       and(
         eq(postMedia.postId, postId),
-        eq(postMedia.mediaId, mediaId)
-      )
+        eq(postMedia.mediaId, mediaId),
+      ),
     );
 
     await this.removePostFromMediaSidecar(mediaId, postId);
@@ -160,7 +160,6 @@ export class PostMediaEngine extends EventEmitter {
     }
 
     this.currentProjectId = projectId;
-    console.log(`[PostMediaEngine] setProjectContext: projectId=${projectId}`);
   }
 
   /**
@@ -254,8 +253,8 @@ export class PostMediaEngine extends EventEmitter {
       .where(
         and(
           eq(postMedia.projectId, this.currentProjectId),
-          eq(postMedia.postId, postId)
-        )
+          eq(postMedia.postId, postId),
+        ),
       )
       .orderBy(asc(postMedia.sortOrder));
 
@@ -274,8 +273,8 @@ export class PostMediaEngine extends EventEmitter {
       .where(
         and(
           eq(postMedia.projectId, this.currentProjectId),
-          eq(postMedia.mediaId, mediaId)
-        )
+          eq(postMedia.mediaId, mediaId),
+        ),
       );
 
     return links.map(this.mapToLinkData);
@@ -296,8 +295,8 @@ export class PostMediaEngine extends EventEmitter {
         .where(
           and(
             eq(postMedia.postId, postId),
-            eq(postMedia.mediaId, mediaIds[i])
-          )
+            eq(postMedia.mediaId, mediaIds[i]),
+          ),
         );
     }
 
@@ -312,8 +311,6 @@ export class PostMediaEngine extends EventEmitter {
   async rebuildFromSidecars(): Promise {
     const db = this.getDb();
 
-    console.log('[PostMediaEngine] Rebuilding post-media links from sidecars...');
-
     // Clear existing links for this project
     await db.delete(postMedia).where(eq(postMedia.projectId, this.currentProjectId));
 
@@ -340,8 +337,6 @@ export class PostMediaEngine extends EventEmitter {
         linksCreated++;
       }
     }
-
-    console.log(`[PostMediaEngine] Rebuilt ${linksCreated} post-media links`);
     this.emit('rebuilt', { linksCreated });
   }
 
@@ -386,8 +381,8 @@ export class PostMediaEngine extends EventEmitter {
       .where(
         and(
           eq(postMedia.postId, postId),
-          eq(postMedia.mediaId, mediaId)
-        )
+          eq(postMedia.mediaId, mediaId),
+        ),
       )
       .limit(1);
 
diff --git a/src/main/engine/PreviewServer.ts b/src/main/engine/PreviewServer.ts
index 9e7410a..7bb88a4 100644
--- a/src/main/engine/PreviewServer.ts
+++ b/src/main/engine/PreviewServer.ts
@@ -2,7 +2,6 @@ import { createServer, type IncomingMessage, type Server, type ServerResponse }
 import { readFile } from 'node:fs/promises';
 import path from 'node:path';
 import { type CategoryMetadata, type ProjectMetadata } from './MetaEngine';
-import { type MediaData } from './MediaEngine';
 import { type MenuDocument } from './MenuEngine';
 import { type PostData, type PostFilter, type PostTranslationData } from './PostEngine';
 import {
@@ -96,12 +95,24 @@ export class PreviewServer {
   private drainResolve: (() => void) | null = null;
 
   constructor(dependencies?: Partial) {
-    if (!dependencies?.postEngine) throw new Error('PreviewServer: postEngine not provided');
-    if (!dependencies?.mediaEngine) throw new Error('PreviewServer: mediaEngine not provided');
-    if (!dependencies?.postMediaEngine) throw new Error('PreviewServer: postMediaEngine not provided');
-    if (!dependencies?.settingsEngine) throw new Error('PreviewServer: settingsEngine not provided');
-    if (!dependencies?.menuEngine) throw new Error('PreviewServer: menuEngine not provided');
-    if (!dependencies?.getActiveProjectContext) throw new Error('PreviewServer: getActiveProjectContext not provided');
+    if (!dependencies?.postEngine) {
+      throw new Error('PreviewServer: postEngine not provided');
+    }
+    if (!dependencies?.mediaEngine) {
+      throw new Error('PreviewServer: mediaEngine not provided');
+    }
+    if (!dependencies?.postMediaEngine) {
+      throw new Error('PreviewServer: postMediaEngine not provided');
+    }
+    if (!dependencies?.settingsEngine) {
+      throw new Error('PreviewServer: settingsEngine not provided');
+    }
+    if (!dependencies?.menuEngine) {
+      throw new Error('PreviewServer: menuEngine not provided');
+    }
+    if (!dependencies?.getActiveProjectContext) {
+      throw new Error('PreviewServer: getActiveProjectContext not provided');
+    }
     this.postEngine = dependencies.postEngine;
     this.mediaEngine = dependencies.mediaEngine;
     this.postMediaEngine = dependencies.postMediaEngine;
@@ -221,7 +232,9 @@ export class PreviewServer {
       loadPostsForDayPage: (year, month, day, pagination) => loadPostsForDayPage(this.postEngine, year, month, day, pagination),
       findPublishedPostBySlug: (slug, dateFilter) => findPublishedPostBySlug(this.postEngine, slug, dateFilter),
       findSinglePostBySlug: (slug, singlePostOptions, dateFilter) => findSinglePostBySlug(this.postEngine, slug, singlePostOptions, dateFilter),
-      getLinkedBy: this.postEngine.getLinkedBy ? (postId) => this.postEngine.getLinkedBy!(postId) : undefined,
+      getLinkedBy: this.postEngine.getLinkedBy
+        ? (postId) => this.postEngine.getLinkedBy?.(postId) ?? Promise.resolve([])
+        : undefined,
     });
   }
 
@@ -248,14 +261,14 @@ export class PreviewServer {
 
     this.inflightRequests++;
     try {
-        const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
-        const pathname = decodeURIComponent(requestUrl.pathname.replace(/\/+$/, '') || '/');
+      const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
+      const pathname = decodeURIComponent(requestUrl.pathname.replace(/\/+$/, '') || '/');
 
-        const asset = await this.resolveAsset(pathname);
-        if (asset) {
-          this.respondAsset(res, asset.contentType, asset.body);
-          return;
-        }
+      const asset = await this.resolveAsset(pathname);
+      if (asset) {
+        this.respondAsset(res, asset.contentType, asset.body);
+        return;
+      }
 
       const context = await this.getActiveProjectContext();
       this.postEngine.setProjectContext(context.projectId, context.dataDir);
@@ -320,11 +333,11 @@ export class PreviewServer {
           : [];
         const stylePreviewBlogLanguages = allBlogLanguages.length > 0
           ? allBlogLanguages.map((lang) => ({
-              code: lang,
-              flag: POST_LANGUAGE_FLAGS[lang as SupportedLanguage] ?? '',
-              href_prefix: lang === mainLanguage ? '' : `/${lang}`,
-              is_current: lang === mainLanguage,
-            }))
+            code: lang,
+            flag: POST_LANGUAGE_FLAGS[lang as SupportedLanguage] ?? '',
+            href_prefix: lang === mainLanguage ? '' : `/${lang}`,
+            is_current: lang === mainLanguage,
+          }))
           : [];
         const stylePreviewHtml = await this.renderStylePreview(htmlRewriteContext, {
           pageTitle,
@@ -550,11 +563,15 @@ export class PreviewServer {
 
   private async resolveAsset(pathname: string): Promise<{ contentType: string; body: Buffer } | null> {
     const match = pathname.match(/^\/assets\/([^/]+)$/);
-    if (!match) return null;
+    if (!match) {
+      return null;
+    }
 
     const assetName = match[1];
     const assetDefinition = PREVIEW_ASSETS[assetName];
-    if (!assetDefinition) return null;
+    if (!assetDefinition) {
+      return null;
+    }
 
     try {
       const body = assetDefinition.sourceText !== undefined
@@ -572,11 +589,15 @@ export class PreviewServer {
 
   private async resolveImageAsset(pathname: string): Promise<{ contentType: string; body: Buffer } | null> {
     const match = pathname.match(/^\/images\/([^/]+)$/);
-    if (!match) return null;
+    if (!match) {
+      return null;
+    }
 
     const assetName = match[1] as keyof typeof PREVIEW_IMAGE_ASSETS;
     const assetDefinition = PREVIEW_IMAGE_ASSETS[assetName];
-    if (!assetDefinition) return null;
+    if (!assetDefinition) {
+      return null;
+    }
 
     try {
       const absolutePath = require.resolve(assetDefinition.modulePath);
@@ -593,7 +614,9 @@ export class PreviewServer {
 
   private async resolveMediaAsset(pathname: string, dataDir?: string): Promise<{ contentType: string; body: Buffer } | null> {
     const match = pathname.match(/^\/media\/(.+)$/);
-    if (!match || !dataDir) return null;
+    if (!match || !dataDir) {
+      return null;
+    }
 
     const relativeMediaPath = path.posix.normalize(`media/${match[1]}`);
     if (!relativeMediaPath.startsWith('media/')) {
@@ -637,31 +660,31 @@ export class PreviewServer {
   private getMediaContentType(filePath: string): string {
     const extension = path.extname(filePath).toLowerCase();
     switch (extension) {
-      case '.jpg':
-      case '.jpeg':
-        return 'image/jpeg';
-      case '.png':
-        return 'image/png';
-      case '.gif':
-        return 'image/gif';
-      case '.webp':
-        return 'image/webp';
-      case '.svg':
-        return 'image/svg+xml';
-      case '.bmp':
-        return 'image/bmp';
-      case '.avif':
-        return 'image/avif';
-      case '.mp4':
-        return 'video/mp4';
-      case '.webm':
-        return 'video/webm';
-      case '.mov':
-        return 'video/quicktime';
-      case '.pdf':
-        return 'application/pdf';
-      default:
-        return 'application/octet-stream';
+    case '.jpg':
+    case '.jpeg':
+      return 'image/jpeg';
+    case '.png':
+      return 'image/png';
+    case '.gif':
+      return 'image/gif';
+    case '.webp':
+      return 'image/webp';
+    case '.svg':
+      return 'image/svg+xml';
+    case '.bmp':
+      return 'image/bmp';
+    case '.avif':
+      return 'image/avif';
+    case '.mp4':
+      return 'video/mp4';
+    case '.webm':
+      return 'video/webm';
+    case '.mov':
+      return 'video/quicktime';
+    case '.pdf':
+      return 'application/pdf';
+    default:
+      return 'application/octet-stream';
     }
   }
 
diff --git a/src/main/engine/PublishApiAdapter.ts b/src/main/engine/PublishApiAdapter.ts
index 26fa17f..45cc7f5 100644
--- a/src/main/engine/PublishApiAdapter.ts
+++ b/src/main/engine/PublishApiAdapter.ts
@@ -29,7 +29,11 @@ export class PublishApiAdapter {
       throw new Error('No active project');
     }
 
-    this.publishEngine.setProjectContext(project.id, project.dataPath!);
+    if (!project.dataPath) {
+      throw new Error('Active project is missing dataPath');
+    }
+
+    this.publishEngine.setProjectContext(project.id, project.dataPath);
 
     const ts = Date.now();
     const groupId = `publish-${ts}`;
diff --git a/src/main/engine/PublishEngine.ts b/src/main/engine/PublishEngine.ts
index 447a169..72a1b9f 100644
--- a/src/main/engine/PublishEngine.ts
+++ b/src/main/engine/PublishEngine.ts
@@ -35,6 +35,14 @@ export class PublishEngine extends EventEmitter {
     this.dataDir = dataDir;
   }
 
+  private requireDataDir(): string {
+    if (!this.dataDir) {
+      throw new Error('Project context is not set');
+    }
+
+    return this.dataDir;
+  }
+
   // ── Public upload methods (one per directory, run as parallel tasks) ───
 
   /**
@@ -48,7 +56,8 @@ export class PublishEngine extends EventEmitter {
     this.ensureProjectContext();
     this.validateCredentials(credentials);
 
-    const htmlDir = path.join(this.dataDir!, 'html');
+    const dataDir = this.requireDataDir();
+    const htmlDir = path.join(dataDir, 'html');
     await this.ensureDirectoryExists(htmlDir, 'Generated site not found. Please render the site first.');
 
     if (credentials.sshMode === 'rsync') {
@@ -72,7 +81,8 @@ export class PublishEngine extends EventEmitter {
     this.ensureProjectContext();
     this.validateCredentials(credentials);
 
-    const thumbnailsDir = path.join(this.dataDir!, 'thumbnails');
+    const dataDir = this.requireDataDir();
+    const thumbnailsDir = path.join(dataDir, 'thumbnails');
     if (!(await this.directoryExists(thumbnailsDir))) {
       onProgress(100, 'No thumbnails to upload');
       return { filesUploaded: 0, filesSkipped: 0 };
@@ -100,7 +110,8 @@ export class PublishEngine extends EventEmitter {
     this.ensureProjectContext();
     this.validateCredentials(credentials);
 
-    const mediaDir = path.join(this.dataDir!, 'media');
+    const dataDir = this.requireDataDir();
+    const mediaDir = path.join(dataDir, 'media');
     if (!(await this.directoryExists(mediaDir))) {
       onProgress(100, 'No media to upload');
       return { filesUploaded: 0, filesSkipped: 0 };
@@ -235,11 +246,21 @@ export class PublishEngine extends EventEmitter {
             const lines = data.toString().split('\n');
             for (const line of lines) {
               const trimmed = line.trim();
-              if (!trimmed) continue;
-              if (trimmed.startsWith('sending ')) continue;
-              if (/\bbytes\b/.test(trimmed)) continue;
-              if (/total size is/.test(trimmed)) continue;
-              if (/speedup is/.test(trimmed)) continue;
+              if (!trimmed) {
+                continue;
+              }
+              if (trimmed.startsWith('sending ')) {
+                continue;
+              }
+              if (/\bbytes\b/.test(trimmed)) {
+                continue;
+              }
+              if (/total size is/.test(trimmed)) {
+                continue;
+              }
+              if (/speedup is/.test(trimmed)) {
+                continue;
+              }
               filesTransferred++;
               onProgress(
                 Math.min(filesTransferred, 99),
@@ -248,7 +269,7 @@ export class PublishEngine extends EventEmitter {
             }
           },
         },
-        (error, _stdout, _stderr, _cmd) => {
+        (error) => {
           if (error) {
             reject(error);
           } else {
diff --git a/src/main/engine/RoutePageGenerationService.ts b/src/main/engine/RoutePageGenerationService.ts
index 0602b5b..01c6ffb 100644
--- a/src/main/engine/RoutePageGenerationService.ts
+++ b/src/main/engine/RoutePageGenerationService.ts
@@ -38,7 +38,9 @@ export async function generateRootPages(params: BaseParams & {
   for (let page = 1; page <= totalPages; page++) {
     const offset = (page - 1) * params.maxPostsPerPage;
     const pagePosts = params.posts.slice(offset, offset + params.maxPostsPerPage);
-    if (pagePosts.length === 0) break;
+    if (pagePosts.length === 0) {
+      break;
+    }
 
     const routePath = page === 1 ? '/' : `/page/${page}`;
     const html = await renderRequiredRoute(params.renderRoute, routePath);
@@ -98,7 +100,9 @@ async function generatePaginatedListPages(params: BaseParams & {
   maxPostsPerPage: number;
   urlPrefix: string;
 }): Promise {
-  if (params.posts.length === 0) return 0;
+  if (params.posts.length === 0) {
+    return 0;
+  }
 
   const totalPages = Math.max(1, Math.ceil(params.posts.length / params.maxPostsPerPage));
   let count = 0;
@@ -106,7 +110,9 @@ async function generatePaginatedListPages(params: BaseParams & {
   for (let page = 1; page <= totalPages; page++) {
     const offset = (page - 1) * params.maxPostsPerPage;
     const pagePosts = params.posts.slice(offset, offset + params.maxPostsPerPage);
-    if (pagePosts.length === 0) break;
+    if (pagePosts.length === 0) {
+      break;
+    }
 
     const routePath = page === 1 ? `/${params.urlPrefix}` : `/${params.urlPrefix}/page/${page}`;
     const html = await renderRequiredRoute(params.renderRoute, routePath);
@@ -129,7 +135,9 @@ export async function generateCategoryPages(params: BaseParams & {
 
   for (const category of Array.from(params.allCategories).sort()) {
     const categoryPosts = params.postsByCategory?.get(category) ?? params.posts.filter((post) => (post.categories || []).includes(category));
-    if (categoryPosts.length === 0) continue;
+    if (categoryPosts.length === 0) {
+      continue;
+    }
 
     const totalPages = Math.max(1, Math.ceil(categoryPosts.length / params.maxPostsPerPage));
     const encodedCategory = encodeURIComponent(category);
@@ -137,7 +145,9 @@ export async function generateCategoryPages(params: BaseParams & {
     for (let page = 1; page <= totalPages; page++) {
       const offset = (page - 1) * params.maxPostsPerPage;
       const pagePosts = categoryPosts.slice(offset, offset + params.maxPostsPerPage);
-      if (pagePosts.length === 0) break;
+      if (pagePosts.length === 0) {
+        break;
+      }
 
       const routePath = page === 1
         ? `/category/${encodedCategory}`
@@ -165,7 +175,9 @@ export async function generateTagPages(params: BaseParams & {
 
   for (const tag of Array.from(params.allTags).sort()) {
     const tagPosts = params.postsByTag?.get(tag) ?? params.posts.filter((post) => (post.tags || []).includes(tag));
-    if (tagPosts.length === 0) continue;
+    if (tagPosts.length === 0) {
+      continue;
+    }
 
     const totalPages = Math.max(1, Math.ceil(tagPosts.length / params.maxPostsPerPage));
     const encodedTag = encodeURIComponent(tag);
@@ -173,7 +185,9 @@ export async function generateTagPages(params: BaseParams & {
     for (let page = 1; page <= totalPages; page++) {
       const offset = (page - 1) * params.maxPostsPerPage;
       const pagePosts = tagPosts.slice(offset, offset + params.maxPostsPerPage);
-      if (pagePosts.length === 0) break;
+      if (pagePosts.length === 0) {
+        break;
+      }
 
       const routePath = page === 1
         ? `/tag/${encodedTag}`
diff --git a/src/main/engine/ScriptEngine.ts b/src/main/engine/ScriptEngine.ts
index 7ceab20..67c12ab 100644
--- a/src/main/engine/ScriptEngine.ts
+++ b/src/main/engine/ScriptEngine.ts
@@ -91,7 +91,9 @@ export class ScriptEngine extends EventEmitter {
   }
 
   /** No persistent cache — no-op for watcher compat. */
-  invalidate(_entityId?: string): void {}
+  invalidate(entityId?: string): void {
+    void entityId;
+  }
 
   setProjectContext(projectId: string, dataDir?: string): void {
     this.currentProjectId = projectId;
@@ -513,7 +515,7 @@ export class ScriptEngine extends EventEmitter {
 
   private async toScriptData(row: Script): Promise {
     // Draft scripts store content in the DB; published scripts read from disk.
-    const content = row.status === 'draft' && row.content != null
+    const content = row.status === 'draft' && row.content !== null
       ? row.content
       : await this.readScriptBody(row.filePath);
 
@@ -572,7 +574,7 @@ export class ScriptEngine extends EventEmitter {
     const taken = new Set(
       rows
         .filter((item) => item.id !== excludeId)
-        .map((item) => item.slug)
+        .map((item) => item.slug),
     );
 
     if (!taken.has(baseSlug)) {
@@ -691,7 +693,7 @@ export class ScriptEngine extends EventEmitter {
   }
 
   private parseYamlScalar(valueRaw: string): string | number | boolean {
-    if ((valueRaw.startsWith('"') && valueRaw.endsWith('"')) || (valueRaw.startsWith("'") && valueRaw.endsWith("'"))) {
+    if ((valueRaw.startsWith('"') && valueRaw.endsWith('"')) || (valueRaw.startsWith('\'') && valueRaw.endsWith('\''))) {
       return valueRaw.slice(1, -1)
         .replace(/\\"/g, '"')
         .replace(/\\\\/g, '\\');
@@ -838,9 +840,11 @@ export class ScriptEngine extends EventEmitter {
   /** Publish a draft script: write file to disk, set status='published', clear DB content. */
   async publishScript(id: string): Promise {
     const existing = await this.getScriptRow(id);
-    if (!existing) return null;
+    if (!existing) {
+      return null;
+    }
 
-    const content = existing.status === 'draft' && existing.content != null
+    const content = existing.status === 'draft' && existing.content !== null
       ? existing.content
       : await this.readScriptBody(existing.filePath);
 
@@ -854,7 +858,9 @@ export class ScriptEngine extends EventEmitter {
       .where(eq(scripts.id, id));
 
     const updatedRow = await this.getScriptRow(id);
-    if (!updatedRow) return null;
+    if (!updatedRow) {
+      return null;
+    }
     const result = await this.toScriptData(updatedRow);
     this.emit('scriptUpdated', result);
     await this.notifier.notify('script', id, 'updated');
@@ -864,7 +870,9 @@ export class ScriptEngine extends EventEmitter {
   /** Delete a draft script (only if status='draft'). Returns false if not found or already published. */
   async deleteDraftScript(id: string): Promise {
     const existing = await this.getScriptRow(id);
-    if (!existing || existing.status !== 'draft') return false;
+    if (!existing || existing.status !== 'draft') {
+      return false;
+    }
 
     await getDatabase().getLocal()
       .delete(scripts)
diff --git a/src/main/engine/SharedRouteRenderer.ts b/src/main/engine/SharedRouteRenderer.ts
index be9f3d5..0b61e99 100644
--- a/src/main/engine/SharedRouteRenderer.ts
+++ b/src/main/engine/SharedRouteRenderer.ts
@@ -95,7 +95,9 @@ export interface SharedRouteRenderServices {
 const MAX_BACKLINK_SLUG_LENGTH = 30;
 
 function truncateSlug(slug: string): string {
-  if (slug.length <= MAX_BACKLINK_SLUG_LENGTH) return slug;
+  if (slug.length <= MAX_BACKLINK_SLUG_LENGTH) {
+    return slug;
+  }
   return slug.slice(0, MAX_BACKLINK_SLUG_LENGTH) + '...';
 }
 
@@ -104,14 +106,20 @@ async function resolveBacklinks(
   rewriteContext: HtmlRewriteContext,
   getLinkedBy?: (postId: string) => Promise<{ id: string; title: string; slug: string }[]>,
 ): Promise {
-  if (!getLinkedBy) return [];
+  if (!getLinkedBy) {
+    return [];
+  }
   const linkedPosts = await getLinkedBy(postId);
-  if (linkedPosts.length === 0) return [];
+  if (linkedPosts.length === 0) {
+    return [];
+  }
 
   return linkedPosts
     .map((linked) => {
       const canonical = rewriteContext.canonicalPostPathBySlug.get(linked.slug);
-      if (!canonical) return null;
+      if (!canonical) {
+        return null;
+      }
       return {
         slug: linked.slug,
         display_slug: truncateSlug(linked.slug),
@@ -276,7 +284,9 @@ async function resolveRouteWithSharedServices(
       ...singlePostOptions,
       preferredLanguage: singlePostOptions?.preferredLanguage ?? pageContext.language,
     }, { year, month, day });
-    if (!post) return null;
+    if (!post) {
+      return null;
+    }
     const backlinks = await resolveBacklinks(post.id, rewriteContext, services.getLinkedBy);
     return services.pageRenderer.renderSinglePost(post, rewriteContext, {
       page_title: pageContext.pageTitle,
@@ -327,7 +337,9 @@ async function resolveRouteWithSharedServices(
   if (monthMatch) {
     const year = Number(monthMatch[1]);
     const month = Number(monthMatch[2]);
-    if (month < 1 || month > 12) return null;
+    if (month < 1 || month > 12) {
+      return null;
+    }
     const result = await services.loadPublishedSnapshotsPage({ status: 'published', year, month, excludeCategories: listExcludedCategories }, pageOptions);
     return services.pageRenderer.renderPostList(result.posts, rewriteContext, {
       archiveGrouping: true,
@@ -449,11 +461,11 @@ export async function renderRouteWithSharedContext(
     : [];
   const blogLanguages = allBlogLanguages.length > 0
     ? allBlogLanguages.map((lang) => ({
-        code: lang,
-        flag: POST_LANGUAGE_FLAGS[lang as SupportedLanguage] ?? '',
-        href_prefix: lang === mainLang ? '' : `/${lang}`,
-        is_current: lang === currentLanguage,
-      }))
+      code: lang,
+      flag: POST_LANGUAGE_FLAGS[lang as SupportedLanguage] ?? '',
+      href_prefix: lang === mainLang ? '' : `/${lang}`,
+      is_current: lang === currentLanguage,
+    }))
     : [];
 
   return resolveRouteWithSharedServices(normalizedPathname, maxPostsPerPage, htmlRewriteContext, {
diff --git a/src/main/engine/SharedSnapshotService.ts b/src/main/engine/SharedSnapshotService.ts
index 2eb6e2e..2701a90 100644
--- a/src/main/engine/SharedSnapshotService.ts
+++ b/src/main/engine/SharedSnapshotService.ts
@@ -18,10 +18,18 @@ interface SinglePostPreviewOptions {
 function buildSnapshotBaseFilter(filter: PostFilter): PostFilter {
   const baseFilter: PostFilter = {};
 
-  if (filter.startDate) baseFilter.startDate = filter.startDate;
-  if (filter.endDate) baseFilter.endDate = filter.endDate;
-  if (filter.year !== undefined) baseFilter.year = filter.year;
-  if (filter.month !== undefined) baseFilter.month = filter.month;
+  if (filter.startDate) {
+    baseFilter.startDate = filter.startDate;
+  }
+  if (filter.endDate) {
+    baseFilter.endDate = filter.endDate;
+  }
+  if (filter.year !== undefined) {
+    baseFilter.year = filter.year;
+  }
+  if (filter.month !== undefined) {
+    baseFilter.month = filter.month;
+  }
 
   return baseFilter;
 }
@@ -89,11 +97,13 @@ export async function loadPublishedSnapshotsPage(
   let snapshots = snapshotCandidates.filter((post): post is PostData => post !== null);
 
   if (filter.tags && filter.tags.length > 0) {
-    snapshots = snapshots.filter((post) => filter.tags!.every((tag) => post.tags.includes(tag)));
+    const { tags } = filter;
+    snapshots = snapshots.filter((post) => tags.every((tag) => post.tags.includes(tag)));
   }
 
   if (filter.categories && filter.categories.length > 0) {
-    snapshots = snapshots.filter((post) => filter.categories!.some((category) => post.categories.includes(category)));
+    const { categories } = filter;
+    snapshots = snapshots.filter((post) => categories.some((category) => post.categories.includes(category)));
   }
 
   snapshots.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime());
@@ -153,7 +163,9 @@ export async function findPublishedPostBySlug(
   slug: string,
   dateFilter?: { year: number; month: number },
 ): Promise {
-  if (!slug) return null;
+  if (!slug) {
+    return null;
+  }
 
   if (postEngine.findPublishedBySlug) {
     const directMatch = await postEngine.findPublishedBySlug(slug, dateFilter);
diff --git a/src/main/engine/TagEngine.ts b/src/main/engine/TagEngine.ts
index eafafab..fc4be43 100644
--- a/src/main/engine/TagEngine.ts
+++ b/src/main/engine/TagEngine.ts
@@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid';
 import * as fs from 'fs/promises';
 import * as path from 'path';
 import { app } from 'electron';
-import { eq, and, asc, sql, like } from 'drizzle-orm';
+import { eq, and, asc, sql } from 'drizzle-orm';
 import { getDatabase } from '../database';
 import { tags, posts } from '../database/schema';
 import { taskManager } from './TaskManager';
@@ -23,6 +23,22 @@ export interface TagData {
   updatedAt: Date;
 }
 
+type QueryRow = Record;
+
+type TagFileRecord = {
+  name?: unknown;
+  color?: unknown;
+  postTemplateSlug?: unknown;
+};
+
+function getErrorCode(error: unknown): string | undefined {
+  if (typeof error === 'object' && error !== null && 'code' in error) {
+    const code = (error as { code?: unknown }).code;
+    return typeof code === 'string' ? code : undefined;
+  }
+  return undefined;
+}
+
 /**
  * Tag with post count for tag cloud display
  */
@@ -138,7 +154,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, tagId),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (tagRows.length === 0) {
@@ -155,15 +171,18 @@ export class TagEngine extends EventEmitter {
     }
 
     const postsResult = await client.execute({
-      sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
+      sql: 'SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?',
       args: [this.currentProjectId, `%"${tagName}"%`],
     });
 
     return postsResult.rows
-      .map((row: any) => ({
-        postId: row.id as string,
-        postTags: JSON.parse(row.tags || '[]') as string[],
-      }))
+      .map((row) => {
+        const typedRow = row as QueryRow;
+        return {
+          postId: String(typedRow.id ?? ''),
+          postTags: JSON.parse(String(typedRow.tags ?? '[]')) as string[],
+        };
+      })
       .filter((row) => row.postTags.includes(tagName));
   }
 
@@ -185,11 +204,11 @@ export class TagEngine extends EventEmitter {
 
   private async updateMatchingPosts(
     tagName: string,
-    transform: (postTags: string[]) => string[]
+    transform: (postTags: string[]) => string[],
   ): Promise<{ total: number; process: (onEachUpdated: (updated: number, total: number) => void) => Promise }> {
     const rawPostsToUpdate = await this.queryPostsContainingTag(tagName);
     const postsToUpdate = Array.from(
-      new Map(rawPostsToUpdate.map((row) => [row.postId, row])).values()
+      new Map(rawPostsToUpdate.map((row) => [row.postId, row])).values(),
     );
     const total = postsToUpdate.length;
 
@@ -285,7 +304,9 @@ export class TagEngine extends EventEmitter {
    */
   async getTagsWithCounts(): Promise {
     const client = this.getClient();
-    if (!client) return [];
+    if (!client) {
+      return [];
+    }
 
     // Query tags with counts from posts - requires raw SQL for JSON operations
     const result = await client.execute({
@@ -307,11 +328,14 @@ export class TagEngine extends EventEmitter {
       args: [this.currentProjectId, this.currentProjectId],
     });
 
-    return result.rows.map((row: any) => ({
-      name: row.name as string,
-      color: row.color as string | null,
-      count: Number(row.post_count) || 0,
-    }));
+    return result.rows.map((row) => {
+      const typedRow = row as QueryRow;
+      return {
+        name: String(typedRow.name ?? ''),
+        color: typeof typedRow.color === 'string' ? typedRow.color : null,
+        count: Number(typedRow.post_count) || 0,
+      };
+    });
   }
 
   /**
@@ -336,7 +360,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.projectId, this.currentProjectId),
-        sql`LOWER(${tags.name}) = LOWER(${name})`
+        sql`LOWER(${tags.name}) = LOWER(${name})`,
       ));
 
     if (existing.length > 0) {
@@ -380,7 +404,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, id),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (existing.length === 0) {
@@ -417,7 +441,7 @@ export class TagEngine extends EventEmitter {
       .set(setFields)
       .where(and(
         eq(tags.id, id),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     const updatedTag: TagData = {
@@ -451,7 +475,7 @@ export class TagEngine extends EventEmitter {
       runUpdates: async (onProgress) => {
         const updateOperation = await this.updateMatchingPosts(
           tagName,
-          (postTags) => postTags.filter((tagEntry) => tagEntry !== tagName)
+          (postTags) => postTags.filter((tagEntry) => tagEntry !== tagName),
         );
 
         return updateOperation.process((updatedCount, totalCount) => {
@@ -464,7 +488,7 @@ export class TagEngine extends EventEmitter {
           .delete(tags)
           .where(and(
             eq(tags.id, id),
-            eq(tags.projectId, this.currentProjectId)
+            eq(tags.projectId, this.currentProjectId),
           ));
       },
       buildResult: (postsUpdated) => ({ success: true, postsUpdated }),
@@ -492,7 +516,7 @@ export class TagEngine extends EventEmitter {
         .from(tags)
         .where(and(
           eq(tags.id, id),
-          eq(tags.projectId, this.currentProjectId)
+          eq(tags.projectId, this.currentProjectId),
         ));
       if (rows.length > 0) {
         sourceTags.push(rows[0]);
@@ -505,7 +529,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, targetTagId),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (targetRows.length === 0) {
@@ -556,7 +580,7 @@ export class TagEngine extends EventEmitter {
             .delete(tags)
             .where(and(
               eq(tags.id, sourceId),
-              eq(tags.projectId, this.currentProjectId)
+              eq(tags.projectId, this.currentProjectId),
             ));
         }
       },
@@ -589,7 +613,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, id),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (tagRows.length === 0) {
@@ -610,7 +634,7 @@ export class TagEngine extends EventEmitter {
       .where(and(
         eq(tags.projectId, this.currentProjectId),
         sql`LOWER(${tags.name}) = LOWER(${newName})`,
-        sql`${tags.id} != ${id}`
+        sql`${tags.id} != ${id}`,
       ));
 
     if (duplicateRows.length > 0) {
@@ -624,7 +648,7 @@ export class TagEngine extends EventEmitter {
       runUpdates: async (onProgress) => {
         const updateOperation = await this.updateMatchingPosts(
           oldName,
-          (postTags) => postTags.map((tagEntry) => tagEntry === oldName ? newName : tagEntry)
+          (postTags) => postTags.map((tagEntry) => tagEntry === oldName ? newName : tagEntry),
         );
 
         return updateOperation.process((updatedCount, totalCount) => {
@@ -641,7 +665,7 @@ export class TagEngine extends EventEmitter {
           })
           .where(and(
             eq(tags.id, id),
-            eq(tags.projectId, this.currentProjectId)
+            eq(tags.projectId, this.currentProjectId),
           ));
       },
       buildResult: (postsUpdated) => ({
@@ -667,7 +691,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, id),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (rows.length === 0) {
@@ -689,7 +713,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.projectId, this.currentProjectId),
-        sql`LOWER(${tags.name}) = LOWER(${normalizedName})`
+        sql`LOWER(${tags.name}) = LOWER(${normalizedName})`,
       ));
 
     if (rows.length === 0) {
@@ -720,7 +744,9 @@ export class TagEngine extends EventEmitter {
   async getPostsWithTag(tagId: string): Promise {
     const db = this.getDb();
     const client = this.getClient();
-    if (!client) return [];
+    if (!client) {
+      return [];
+    }
 
     // First get the tag name
     const tagRows = await db
@@ -728,7 +754,7 @@ export class TagEngine extends EventEmitter {
       .from(tags)
       .where(and(
         eq(tags.id, tagId),
-        eq(tags.projectId, this.currentProjectId)
+        eq(tags.projectId, this.currentProjectId),
       ));
 
     if (tagRows.length === 0) {
@@ -739,16 +765,17 @@ export class TagEngine extends EventEmitter {
 
     // Find posts with this tag - requires raw SQL for JSON
     const postsResult = await client.execute({
-      sql: `SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?`,
+      sql: 'SELECT id, tags FROM posts WHERE project_id = ? AND tags LIKE ?',
       args: [this.currentProjectId, `%"${tagName}"%`],
     });
 
     return postsResult.rows
-      .filter((row: any) => {
-        const postTags: string[] = JSON.parse(row.tags || '[]');
+      .filter((row) => {
+        const typedRow = row as QueryRow;
+        const postTags: string[] = JSON.parse(String(typedRow.tags ?? '[]')) as string[];
         return postTags.includes(tagName);
       })
-      .map((row: any) => row.id as string);
+      .map((row) => String((row as QueryRow).id ?? ''));
   }
 
   /**
@@ -863,17 +890,21 @@ export class TagEngine extends EventEmitter {
     try {
       const filePath = this.getTagsFilePath();
       const content = await fs.readFile(filePath, 'utf-8');
-      const rawTags: any[] = JSON.parse(content);
+      const parsed = JSON.parse(content);
+      const rawTags = Array.isArray(parsed) ? parsed as TagFileRecord[] : [];
 
       const db = this.getDb();
       const now = new Date();
 
       for (const tag of rawTags) {
         // Support both portable format { name, color? } and legacy format with id
-        const name = normalizeTaxonomyTerm(tag.name || '');
-        if (!name) continue;
+        const rawName = typeof tag.name === 'string' ? tag.name : '';
+        const name = normalizeTaxonomyTerm(rawName);
+        if (!name) {
+          continue;
+        }
 
-        const color = tag.color || null;
+        const color = typeof tag.color === 'string' ? tag.color : null;
         const postTemplateSlug = typeof tag.postTemplateSlug === 'string' ? tag.postTemplateSlug : null;
 
         // Check if tag with this name already exists
@@ -882,7 +913,7 @@ export class TagEngine extends EventEmitter {
           .from(tags)
           .where(and(
             eq(tags.projectId, this.currentProjectId),
-            sql`LOWER(${tags.name}) = LOWER(${name})`
+            sql`LOWER(${tags.name}) = LOWER(${name})`,
           ));
 
         if (existing.length === 0) {
@@ -898,7 +929,7 @@ export class TagEngine extends EventEmitter {
           });
         } else if (color || postTemplateSlug) {
           // Update color/postTemplateSlug if provided and tag exists
-          const setFields: Record = { updatedAt: now };
+          const setFields: { updatedAt: Date; color?: string | null; postTemplateSlug?: string | null } = { updatedAt: now };
           if (color) {
             setFields.color = color;
           }
@@ -910,12 +941,12 @@ export class TagEngine extends EventEmitter {
             .set(setFields)
             .where(and(
               eq(tags.projectId, this.currentProjectId),
-              sql`LOWER(${tags.name}) = LOWER(${name})`
+              sql`LOWER(${tags.name}) = LOWER(${name})`,
             ));
         }
       }
-    } catch (error: any) {
-      if (error.code !== 'ENOENT') {
+    } catch (error) {
+      if (getErrorCode(error) !== 'ENOENT') {
         console.error('[TagEngine] Failed to load tags from file:', error);
       }
     }
diff --git a/src/main/engine/TemplateEngine.ts b/src/main/engine/TemplateEngine.ts
index b217fb1..918d43e 100644
--- a/src/main/engine/TemplateEngine.ts
+++ b/src/main/engine/TemplateEngine.ts
@@ -93,7 +93,9 @@ export class TemplateEngine extends EventEmitter {
   }
 
   /** No persistent cache — no-op for watcher compat. */
-  invalidate(_entityId?: string): void {}
+  invalidate(entityId?: string): void {
+    void entityId;
+  }
 
   setProjectContext(projectId: string, dataDir?: string): void {
     this.currentProjectId = projectId;
@@ -553,7 +555,7 @@ export class TemplateEngine extends EventEmitter {
 
   private async toTemplateData(row: Template): Promise {
     // Draft templates store content in the DB; published templates read from disk.
-    const content = row.status === 'draft' && row.content != null
+    const content = row.status === 'draft' && row.content !== null
       ? row.content
       : await this.readTemplateBody(row.filePath);
 
@@ -608,9 +610,11 @@ export class TemplateEngine extends EventEmitter {
   /** Publish a draft template: write file to disk, set status='published', clear DB content. */
   async publishTemplate(id: string): Promise {
     const existing = await this.getTemplateRow(id);
-    if (!existing) return null;
+    if (!existing) {
+      return null;
+    }
 
-    const content = existing.status === 'draft' && existing.content != null
+    const content = existing.status === 'draft' && existing.content !== null
       ? existing.content
       : await this.readTemplateBody(existing.filePath);
 
@@ -624,7 +628,9 @@ export class TemplateEngine extends EventEmitter {
       .where(eq(templates.id, id));
 
     const updatedRow = await this.getTemplateRow(id);
-    if (!updatedRow) return null;
+    if (!updatedRow) {
+      return null;
+    }
     const result = await this.toTemplateData(updatedRow);
     this.emit('templateUpdated', result);
     await this.notifier.notify('template', id, 'updated');
@@ -634,7 +640,9 @@ export class TemplateEngine extends EventEmitter {
   /** Delete a draft template (only if status='draft'). Returns false if not found or already published. */
   async deleteDraftTemplate(id: string): Promise {
     const existing = await this.getTemplateRow(id);
-    if (!existing || existing.status !== 'draft') return false;
+    if (!existing || existing.status !== 'draft') {
+      return false;
+    }
 
     await getDatabase().getLocal()
       .delete(templates)
@@ -683,7 +691,7 @@ export class TemplateEngine extends EventEmitter {
     const taken = new Set(
       rows
         .filter((item) => item.id !== excludeId)
-        .map((item) => item.slug)
+        .map((item) => item.slug),
     );
 
     if (!taken.has(baseSlug)) {
@@ -798,7 +806,7 @@ export class TemplateEngine extends EventEmitter {
   }
 
   private parseYamlScalar(valueRaw: string): string | number | boolean {
-    if ((valueRaw.startsWith('"') && valueRaw.endsWith('"')) || (valueRaw.startsWith("'") && valueRaw.endsWith("'"))) {
+    if ((valueRaw.startsWith('"') && valueRaw.endsWith('"')) || (valueRaw.startsWith('\'') && valueRaw.endsWith('\''))) {
       return valueRaw.slice(1, -1)
         .replace(/\\"/g, '"')
         .replace(/\\\\/g, '\\');
diff --git a/src/main/engine/ValidationApplyPlannerService.ts b/src/main/engine/ValidationApplyPlannerService.ts
index f997e99..6215b19 100644
--- a/src/main/engine/ValidationApplyPlannerService.ts
+++ b/src/main/engine/ValidationApplyPlannerService.ts
@@ -70,59 +70,59 @@ function classifyPath(
   requestedPageSlugs: Set,
   state: { requestRootRoutes: boolean; requiresFallbackSectionRender: boolean },
 ): void {
-    if (normalizedPath === '/' || /^\/(page\/\d+)$/.test(normalizedPath)) {
-      state.requestRootRoutes = true;
-      return;
-    }
+  if (normalizedPath === '/' || /^\/(page\/\d+)$/.test(normalizedPath)) {
+    state.requestRootRoutes = true;
+    return;
+  }
 
-    const categoryMatch = normalizedPath.match(/^\/category\/([^/]+)(?:\/page\/\d+)?$/);
-    if (categoryMatch) {
-      requestedCategories.add(decodePathSegment(categoryMatch[1]));
-      return;
-    }
+  const categoryMatch = normalizedPath.match(/^\/category\/([^/]+)(?:\/page\/\d+)?$/);
+  if (categoryMatch) {
+    requestedCategories.add(decodePathSegment(categoryMatch[1]));
+    return;
+  }
 
-    const tagMatch = normalizedPath.match(/^\/tag\/([^/]+)(?:\/page\/\d+)?$/);
-    if (tagMatch) {
-      requestedTags.add(decodePathSegment(tagMatch[1]));
-      return;
-    }
+  const tagMatch = normalizedPath.match(/^\/tag\/([^/]+)(?:\/page\/\d+)?$/);
+  if (tagMatch) {
+    requestedTags.add(decodePathSegment(tagMatch[1]));
+    return;
+  }
 
-    const singleMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)$/);
-    if (singleMatch) {
-      requestedPostRoutes.push({
-        year: Number(singleMatch[1]),
-        month: Number(singleMatch[2]),
-        day: Number(singleMatch[3]),
-        slug: decodePathSegment(singleMatch[4]),
-      });
-      return;
-    }
+  const singleMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})\/([^/]+)$/);
+  if (singleMatch) {
+    requestedPostRoutes.push({
+      year: Number(singleMatch[1]),
+      month: Number(singleMatch[2]),
+      day: Number(singleMatch[3]),
+      slug: decodePathSegment(singleMatch[4]),
+    });
+    return;
+  }
 
-    const yearMatch = normalizedPath.match(/^\/(\d{4})(?:\/page\/\d+)?$/);
-    if (yearMatch) {
-      requestedYears.add(Number(yearMatch[1]));
-      return;
-    }
+  const yearMatch = normalizedPath.match(/^\/(\d{4})(?:\/page\/\d+)?$/);
+  if (yearMatch) {
+    requestedYears.add(Number(yearMatch[1]));
+    return;
+  }
 
-    const monthMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})(?:\/page\/\d+)?$/);
-    if (monthMatch) {
-      requestedYearMonths.add(`${monthMatch[1]}/${monthMatch[2]}`);
-      return;
-    }
+  const monthMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})(?:\/page\/\d+)?$/);
+  if (monthMatch) {
+    requestedYearMonths.add(`${monthMatch[1]}/${monthMatch[2]}`);
+    return;
+  }
 
-    const dayMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})(?:\/page\/\d+)?$/);
-    if (dayMatch) {
-      requestedYearMonthDays.add(`${dayMatch[1]}/${dayMatch[2]}/${dayMatch[3]}`);
-      return;
-    }
+  const dayMatch = normalizedPath.match(/^\/(\d{4})\/(\d{2})\/(\d{2})(?:\/page\/\d+)?$/);
+  if (dayMatch) {
+    requestedYearMonthDays.add(`${dayMatch[1]}/${dayMatch[2]}/${dayMatch[3]}`);
+    return;
+  }
 
-    const pageMatch = normalizedPath.match(/^\/([^/]+)$/);
-    if (pageMatch) {
-      requestedPageSlugs.add(decodePathSegment(pageMatch[1]));
-      return;
-    }
+  const pageMatch = normalizedPath.match(/^\/([^/]+)$/);
+  if (pageMatch) {
+    requestedPageSlugs.add(decodePathSegment(pageMatch[1]));
+    return;
+  }
 
-    state.requiresFallbackSectionRender = true;
+  state.requiresFallbackSectionRender = true;
 }
 
 function createEmptyPlan(): {
@@ -134,7 +134,7 @@ function createEmptyPlan(): {
   requestedPostRoutes: RequestedPostRoute[];
   requestedPageSlugs: Set;
   state: { requestRootRoutes: boolean; requiresFallbackSectionRender: boolean };
-} {
+  } {
   return {
     requestedCategories: new Set(),
     requestedTags: new Set(),
@@ -179,7 +179,10 @@ export function planMissingValidationPaths(missingPaths: string[], additionalLan
       if (!langPlanMap.has(lang)) {
         langPlanMap.set(lang, createEmptyPlan());
       }
-      const lp = langPlanMap.get(lang)!;
+      const lp = langPlanMap.get(lang);
+      if (!lp) {
+        continue;
+      }
       classifyPath(
         strippedPath,
         lp.requestedCategories,
@@ -237,8 +240,6 @@ export function buildTargetedValidationPlan(params: BuildTargetedValidationPlanP
     publishedPosts,
     allCategories,
     allTags,
-    availableYearMonths,
-    availableYearMonthDays,
   } = params;
 
   const requestedCategories = new Set(initialPlan.requestedCategories);
diff --git a/src/main/engine/WxrParser.ts b/src/main/engine/WxrParser.ts
index d735e37..6e15296 100644
--- a/src/main/engine/WxrParser.ts
+++ b/src/main/engine/WxrParser.ts
@@ -176,7 +176,9 @@ export class WxrParser {
     for (let i = 0; i < elements.length; i++) {
       const el = elements[i];
       // Only process direct children of channel (not item-level category elements)
-      if (el.parentNode !== channel) continue;
+      if (el.parentNode !== channel) {
+        continue;
+      }
 
       categories.push({
         name: this.getElementText(el, 'cat_name', NS.wp),
@@ -194,7 +196,9 @@ export class WxrParser {
 
     for (let i = 0; i < elements.length; i++) {
       const el = elements[i];
-      if (el.parentNode !== channel) continue;
+      if (el.parentNode !== channel) {
+        continue;
+      }
 
       tags.push({
         name: this.getElementText(el, 'tag_name', NS.wp),
@@ -215,7 +219,9 @@ export class WxrParser {
     for (let i = 0; i < catElements.length; i++) {
       const el = catElements[i];
       // Only direct children of item
-      if (el.parentNode !== item) continue;
+      if (el.parentNode !== item) {
+        continue;
+      }
       const domain = el.getAttribute('domain');
       const text = this.getTextContent(el);
       if (domain === 'category' && text) {
@@ -282,7 +288,9 @@ export class WxrParser {
   }
 
   private extractFilename(url: string): string {
-    if (!url) return '';
+    if (!url) {
+      return '';
+    }
     try {
       const pathname = new URL(url).pathname;
       return pathname.split('/').pop() || '';
@@ -292,7 +300,9 @@ export class WxrParser {
   }
 
   private extractRelativePath(url: string): string {
-    if (!url) return '';
+    if (!url) {
+      return '';
+    }
     // Extract path after wp-content/uploads/
     const marker = 'wp-content/uploads/';
     const idx = url.indexOf(marker);
diff --git a/src/main/engine/ai/a2ui-tools.ts b/src/main/engine/ai/a2ui-tools.ts
index 2b0842f..5043ea6 100644
--- a/src/main/engine/ai/a2ui-tools.ts
+++ b/src/main/engine/ai/a2ui-tools.ts
@@ -51,7 +51,7 @@ const tabContentItemSchema = z.object({
 // Tool factory
 // ---------------------------------------------------------------------------
 
-export function createA2UITools() {
+function buildA2UITools() {
   return {
     render_chart: tool({
       description: 'Render an interactive chart in the chat UI. Use this when the user asks for a chart, graph, or data visualization.',
@@ -60,7 +60,7 @@ export function createA2UITools() {
         title: z.string().optional().describe('Optional chart title'),
         series: z.array(seriesItemSchema).describe('Array of data points.'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_table: tool({
@@ -70,7 +70,7 @@ export function createA2UITools() {
         columns: z.array(z.string()).describe('Column header names'),
         rows: z.array(z.array(z.string())).describe('Table rows, each row is an array of cell values'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_form: tool({
@@ -92,7 +92,7 @@ export function createA2UITools() {
         submitLabel: z.string().describe('Label for the submit button'),
         submitAction: z.string().optional().describe('Action to dispatch on submit'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_card: tool({
@@ -107,7 +107,7 @@ export function createA2UITools() {
           payload: z.record(z.string(), z.unknown()).optional().describe('Optional action payload'),
         })).optional().describe('Optional action buttons on the card'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_metric: tool({
@@ -116,7 +116,7 @@ export function createA2UITools() {
         label: z.string().describe('Metric label'),
         value: z.string().describe('Metric value (displayed prominently)'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_list: tool({
@@ -125,7 +125,7 @@ export function createA2UITools() {
         title: z.string().optional().describe('Optional list title'),
         items: z.array(z.string()).describe('List items'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_tabs: tool({
@@ -136,7 +136,7 @@ export function createA2UITools() {
           content: z.array(tabContentItemSchema).describe('Content items within the tab'),
         })).describe('Array of tabs'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
 
     render_mindmap: tool({
@@ -149,10 +149,14 @@ export function createA2UITools() {
           children: z.array(z.string()).optional().describe('IDs of child nodes'),
         })).describe('Flat array of nodes. The first node is the root. Each node references children by ID.'),
       }),
-      execute: async (_input) => ({ success: true }),
+      execute: async () => ({ success: true }),
     }),
   };
 }
 
+export function createA2UITools(): ReturnType {
+  return buildA2UITools();
+}
+
 /** The return type of createA2UITools — useful for typing tool maps. */
 export type A2UITools = ReturnType;
diff --git a/src/main/engine/ai/blog-tools.ts b/src/main/engine/ai/blog-tools.ts
index 4d695ff..46642ec 100644
--- a/src/main/engine/ai/blog-tools.ts
+++ b/src/main/engine/ai/blog-tools.ts
@@ -146,7 +146,7 @@ export async function executeCheckTerm(
 // Tool factory
 // ---------------------------------------------------------------------------
 
-export function createBlogTools(deps: BlogToolDeps) {
+function buildBlogTools(deps: BlogToolDeps) {
   const { postEngine, mediaEngine, postMediaEngine } = deps;
 
   return {
@@ -180,12 +180,24 @@ export function createBlogTools(deps: BlogToolDeps) {
         }
 
         const filter: PostFilter = {};
-        if (category) filter.categories = [category];
-        if (tags && tags.length > 0) filter.tags = tags;
-        if (language) filter.language = language;
-        if (missingTranslationLanguage) filter.missingTranslationLanguage = missingTranslationLanguage;
-        if (year !== undefined) filter.year = year;
-        if (month !== undefined && year !== undefined) filter.month = month;
+        if (category) {
+          filter.categories = [category];
+        }
+        if (tags && tags.length > 0) {
+          filter.tags = tags;
+        }
+        if (language) {
+          filter.language = language;
+        }
+        if (missingTranslationLanguage) {
+          filter.missingTranslationLanguage = missingTranslationLanguage;
+        }
+        if (year !== undefined) {
+          filter.year = year;
+        }
+        if (month !== undefined && year !== undefined) {
+          filter.month = month;
+        }
 
         const offset = off ?? 0;
         const limit = lim ?? 10;
@@ -213,7 +225,9 @@ export function createBlogTools(deps: BlogToolDeps) {
           limit,
           posts,
         };
-        if (hints.length > 0) result.hints = hints;
+        if (hints.length > 0) {
+          result.hints = hints;
+        }
         return result;
       },
     }),
@@ -225,7 +239,9 @@ export function createBlogTools(deps: BlogToolDeps) {
       }),
       execute: async ({ postId }) => {
         const post = await postEngine.getPost(postId);
-        if (!post) return { success: false, error: 'Post not found' };
+        if (!post) {
+          return { success: false, error: 'Post not found' };
+        }
         const [backlinks, linksTo] = await Promise.all([
           postEngine.getLinkedBy(post.id),
           postEngine.getLinksTo(post.id),
@@ -254,7 +270,9 @@ export function createBlogTools(deps: BlogToolDeps) {
       }),
       execute: async ({ slug }) => {
         const post = await postEngine.getPostBySlug(slug);
-        if (!post) return { success: false, error: 'Post not found' };
+        if (!post) {
+          return { success: false, error: 'Post not found' };
+        }
         const [backlinks, linksTo] = await Promise.all([
           postEngine.getLinkedBy(post.id),
           postEngine.getLinksTo(post.id),
@@ -295,13 +313,27 @@ export function createBlogTools(deps: BlogToolDeps) {
         }
 
         const filter: PostFilter = {};
-        if (status) filter.status = status;
-        if (tags) filter.tags = tags;
-        if (category) filter.categories = [category];
-        if (language) filter.language = language;
-        if (missingTranslationLanguage) filter.missingTranslationLanguage = missingTranslationLanguage;
-        if (year !== undefined) filter.year = year;
-        if (month !== undefined && year !== undefined) filter.month = month;
+        if (status) {
+          filter.status = status;
+        }
+        if (tags) {
+          filter.tags = tags;
+        }
+        if (category) {
+          filter.categories = [category];
+        }
+        if (language) {
+          filter.language = language;
+        }
+        if (missingTranslationLanguage) {
+          filter.missingTranslationLanguage = missingTranslationLanguage;
+        }
+        if (year !== undefined) {
+          filter.year = year;
+        }
+        if (month !== undefined && year !== undefined) {
+          filter.month = month;
+        }
 
         const offset = off ?? 0;
         const limit = lim ?? 20;
@@ -343,7 +375,9 @@ export function createBlogTools(deps: BlogToolDeps) {
           limit,
           posts,
         };
-        if (hints.length > 0) result.hints = hints;
+        if (hints.length > 0) {
+          result.hints = hints;
+        }
         return result;
       },
     }),
@@ -355,7 +389,9 @@ export function createBlogTools(deps: BlogToolDeps) {
       }),
       execute: async ({ mediaId }) => {
         const media = await mediaEngine.getMedia(mediaId);
-        if (!media) return { success: false, error: 'Media not found' };
+        if (!media) {
+          return { success: false, error: 'Media not found' };
+        }
         return {
           success: true,
           media: {
@@ -389,9 +425,15 @@ export function createBlogTools(deps: BlogToolDeps) {
 
         if (hasMediaFilter) {
           const mediaFilter: { year?: number; month?: number; tags?: string[] } = {};
-          if (year !== undefined) mediaFilter.year = year;
-          if (month !== undefined && year !== undefined) mediaFilter.month = month;
-          if (tags) mediaFilter.tags = tags;
+          if (year !== undefined) {
+            mediaFilter.year = year;
+          }
+          if (month !== undefined && year !== undefined) {
+            mediaFilter.month = month;
+          }
+          if (tags) {
+            mediaFilter.tags = tags;
+          }
           mediaList = await mediaEngine.getMediaFiltered(mediaFilter);
         } else {
           mediaList = await mediaEngine.getAllMedia();
@@ -433,10 +475,18 @@ export function createBlogTools(deps: BlogToolDeps) {
       }),
       execute: async ({ postId, title, excerpt, tags, categories }) => {
         const updates: Record = {};
-        if (title !== undefined) updates.title = title;
-        if (excerpt !== undefined) updates.excerpt = excerpt;
-        if (tags !== undefined) updates.tags = tags;
-        if (categories !== undefined) updates.categories = categories;
+        if (title !== undefined) {
+          updates.title = title;
+        }
+        if (excerpt !== undefined) {
+          updates.excerpt = excerpt;
+        }
+        if (tags !== undefined) {
+          updates.tags = tags;
+        }
+        if (categories !== undefined) {
+          updates.categories = categories;
+        }
 
         if (Object.keys(updates).length === 0) {
           return { success: false, error: 'No updates provided' };
@@ -458,10 +508,18 @@ export function createBlogTools(deps: BlogToolDeps) {
       }),
       execute: async ({ mediaId, title, alt, caption, tags }) => {
         const updates: Record = {};
-        if (title !== undefined) updates.title = title;
-        if (alt !== undefined) updates.alt = alt;
-        if (caption !== undefined) updates.caption = caption;
-        if (tags !== undefined) updates.tags = tags;
+        if (title !== undefined) {
+          updates.title = title;
+        }
+        if (alt !== undefined) {
+          updates.alt = alt;
+        }
+        if (caption !== undefined) {
+          updates.caption = caption;
+        }
+        if (tags !== undefined) {
+          updates.tags = tags;
+        }
 
         if (Object.keys(updates).length === 0) {
           return { success: false, error: 'No updates provided' };
@@ -500,11 +558,21 @@ export function createBlogTools(deps: BlogToolDeps) {
           return { success: false, error: 'month requires year. Example: year: 2025, month: 3' };
         }
         const filter: { year?: number; month?: number; status?: string; category?: string; tags?: string[] } = {};
-        if (year !== undefined) filter.year = year;
-        if (month !== undefined) filter.month = month;
-        if (status) filter.status = status;
-        if (category) filter.category = category;
-        if (tags && tags.length > 0) filter.tags = tags;
+        if (year !== undefined) {
+          filter.year = year;
+        }
+        if (month !== undefined) {
+          filter.month = month;
+        }
+        if (status) {
+          filter.status = status;
+        }
+        if (category) {
+          filter.category = category;
+        }
+        if (tags && tags.length > 0) {
+          filter.tags = tags;
+        }
 
         const result = await postEngine.getPostCounts(groupBy, Object.keys(filter).length > 0 ? filter : undefined);
         return {
@@ -676,5 +744,9 @@ export function createBlogTools(deps: BlogToolDeps) {
   };
 }
 
+export function createBlogTools(deps: BlogToolDeps): ReturnType {
+  return buildBlogTools(deps);
+}
+
 /** The return type of createBlogTools — useful for typing tool maps. */
 export type BlogTools = ReturnType;
diff --git a/src/main/engine/ai/chat.ts b/src/main/engine/ai/chat.ts
index 32f1e67..cfed921 100644
--- a/src/main/engine/ai/chat.ts
+++ b/src/main/engine/ai/chat.ts
@@ -113,7 +113,9 @@ async function appendBlogStats(
     const stats = await blogToolDeps.postEngine.getBlogStats();
     const mediaList = await blogToolDeps.mediaEngine.getAllMedia();
 
-    if (stats.totalPosts === 0) return basePrompt;
+    if (stats.totalPosts === 0) {
+      return basePrompt;
+    }
 
     const dateRange = stats.oldestPostDate && stats.newestPostDate
       ? `from ${stats.oldestPostDate.toISOString().split('T')[0]} to ${stats.newestPostDate.toISOString().split('T')[0]}`
@@ -161,12 +163,16 @@ function truncateMessages(
   const responseReserve = 4096;
   const availableBudget = maxContextTokens - systemTokens - toolsTokens - responseReserve;
 
-  if (availableBudget <= 0) return messages.slice(-1);
+  if (availableBudget <= 0) {
+    return messages.slice(-1);
+  }
 
   const messageTokens = () =>
     messages.reduce((sum, m) => sum + estimateTokens(typeof m.content === 'string' ? m.content : JSON.stringify(m.content)), 0);
 
-  if (messageTokens() <= availableBudget) return messages;
+  if (messageTokens() <= availableBudget) {
+    return messages;
+  }
 
   let truncated = [...messages];
   while (truncated.length > 2 && messageTokens.call(null) > availableBudget) {
@@ -377,7 +383,7 @@ export class ChatService {
         });
 
         // Consume the stream to completion
-        const finalResult = await result.response;
+        await result.response;
 
         // Extract usage from the response
         const usage = await result.usage;
@@ -412,7 +418,9 @@ export class ChatService {
         };
       } catch (error) {
         const isAborted = abortController.signal.aborted || (error as Error).message === 'Request cancelled';
-        if (!isAborted) throw error;
+        if (!isAborted) {
+          throw error;
+        }
         return { success: true, message: '' };
       } finally {
         this.abortControllers.delete(conversationId);
@@ -475,7 +483,9 @@ export class ChatService {
         }
       }
 
-      if (!titleModel) return;
+      if (!titleModel) {
+        return;
+      }
 
       const model = this.providers.resolveModel(titleModel);
 
@@ -511,7 +521,9 @@ export class ChatService {
     usage: LanguageModelUsage | undefined,
     callbacks: ChatCallbacks,
   ): void {
-    if (!usage || !callbacks.onTokenUsage) return;
+    if (!usage || !callbacks.onTokenUsage) {
+      return;
+    }
 
     // AI SDK v6 normalizes usage into inputTokens/outputTokens
     // Cache tokens are in inputTokenDetails
diff --git a/src/main/engine/ai/providers.ts b/src/main/engine/ai/providers.ts
index 9e54f0f..b18c743 100644
--- a/src/main/engine/ai/providers.ts
+++ b/src/main/engine/ai/providers.ts
@@ -85,16 +85,24 @@ export function createOpenCodeGateway(apiKey: string): Provider {
 /** Determine which provider backend a model ID belongs to. */
 export function detectProvider(modelId: string): string {
   const id = modelId.toLowerCase();
-  if (id.startsWith('claude')) return 'anthropic';
-  if (id.startsWith('gpt') || id.startsWith('o3') || id.startsWith('o4')) return 'openai';
-  if (id.startsWith('gemini')) return 'google';
+  if (id.startsWith('claude')) {
+    return 'anthropic';
+  }
+  if (id.startsWith('gpt') || id.startsWith('o3') || id.startsWith('o4')) {
+    return 'openai';
+  }
+  if (id.startsWith('gemini')) {
+    return 'google';
+  }
   if (
     id.startsWith('mistral') ||
     id.startsWith('ministral') ||
     id.startsWith('devstral') ||
     id.startsWith('codestral') ||
     id.startsWith('pixtral')
-  ) return 'mistral';
+  ) {
+    return 'mistral';
+  }
   return 'other';
 }
 
@@ -294,8 +302,12 @@ export class ProviderRegistry {
    * registration first, then falling back to prefix-based detection.
    */
   detectModelProvider(modelId: string): string {
-    if (this.ollamaModelIds.has(modelId)) return 'ollama';
-    if (this.lmstudioModelIds.has(modelId)) return 'lmstudio';
+    if (this.ollamaModelIds.has(modelId)) {
+      return 'ollama';
+    }
+    if (this.lmstudioModelIds.has(modelId)) {
+      return 'lmstudio';
+    }
     return detectProvider(modelId);
   }
 
@@ -309,11 +321,19 @@ export class ProviderRegistry {
 
   /** Check whether the key for a specific provider is set. */
   isProviderKeySet(provider: string): boolean {
-    if (provider === 'ollama') return this.ollamaEnabled;
-    if (provider === 'lmstudio') return this.lmstudioEnabled;
+    if (provider === 'ollama') {
+      return this.ollamaEnabled;
+    }
+    if (provider === 'lmstudio') {
+      return this.lmstudioEnabled;
+    }
     // In offline mode, cloud providers are unavailable
-    if (this._offlineMode) return false;
-    if (provider === 'mistral') return !!this.mistralKey;
+    if (this._offlineMode) {
+      return false;
+    }
+    if (provider === 'mistral') {
+      return !!this.mistralKey;
+    }
     return !!this.opencodeKey;
   }
 
@@ -404,8 +424,12 @@ export class ProviderRegistry {
    * Used as automatic fallback when no explicit offline model is configured.
    */
   getFirstKnownLocalModelId(): string | null {
-    for (const id of this.ollamaModelIds) return id;
-    for (const id of this.lmstudioModelIds) return id;
+    for (const id of this.ollamaModelIds) {
+      return id;
+    }
+    for (const id of this.lmstudioModelIds) {
+      return id;
+    }
     return null;
   }
 
@@ -414,10 +438,14 @@ export class ProviderRegistry {
    */
   getFirstKnownLocalVisionModelId(): string | null {
     for (const id of this.ollamaModelIds) {
-      if (this.ollamaModelSupportsVision(id)) return id;
+      if (this.ollamaModelSupportsVision(id)) {
+        return id;
+      }
     }
     for (const id of this.lmstudioModelIds) {
-      if (this.lmstudioModelSupportsVision(id)) return id;
+      if (this.lmstudioModelSupportsVision(id)) {
+        return id;
+      }
     }
     return null;
   }
@@ -495,7 +523,9 @@ export class ProviderRegistry {
       try {
         const models = await this.fetchOllamaModels();
         allModels.push(...models);
-        if (models.length > 0) fetched = true;
+        if (models.length > 0) {
+          fetched = true;
+        }
       } catch {
         // Ollama not running — skip silently
       }
@@ -506,7 +536,9 @@ export class ProviderRegistry {
       try {
         const models = await this.fetchLmstudioModels();
         allModels.push(...models);
-        if (models.length > 0) fetched = true;
+        if (models.length > 0) {
+          fetched = true;
+        }
       } catch {
         // LM Studio not running — skip silently
       }
@@ -524,7 +556,9 @@ export class ProviderRegistry {
 
   /** Validate an OpenCode API key against the models endpoint. */
   async validateOpencodeKey(apiKey: string): Promise<{ isValid: boolean; models: ChatModel[] }> {
-    if (!apiKey || apiKey.length < 3) return { isValid: false, models: [] };
+    if (!apiKey || apiKey.length < 3) {
+      return { isValid: false, models: [] };
+    }
 
     const { vision: catalogVision, names: catalogNames } = await this.getCatalogLookups();
 
@@ -548,7 +582,9 @@ export class ProviderRegistry {
 
   /** Validate a Mistral API key against the Mistral models endpoint. */
   async validateMistralKey(apiKey: string): Promise<{ isValid: boolean; models: ChatModel[] }> {
-    if (!apiKey || apiKey.length < 3) return { isValid: false, models: [] };
+    if (!apiKey || apiKey.length < 3) {
+      return { isValid: false, models: [] };
+    }
 
     const { vision: catalogVision, names: catalogNames } = await this.getCatalogLookups();
 
@@ -578,10 +614,14 @@ export class ProviderRegistry {
       const timeout = setTimeout(() => controller.abort(), LMSTUDIO_FETCH_TIMEOUT);
       const response = await fetch(LMSTUDIO_MODELS_URL, { method: 'GET', signal: controller.signal });
       clearTimeout(timeout);
-      if (!response.ok) return [];
+      if (!response.ok) {
+        return [];
+      }
 
       const data = await response.json() as { data?: Array<{ id: string }> };
-      if (!data.data || !Array.isArray(data.data)) return [];
+      if (!data.data || !Array.isArray(data.data)) {
+        return [];
+      }
 
       const models: ChatModel[] = data.data.map(m => ({
         id: m.id,
@@ -591,7 +631,9 @@ export class ProviderRegistry {
       }));
       // Only replace registered IDs on successful fetch
       this.clearLmstudioModels();
-      for (const m of models) this.registerLmstudioModel(m.id);
+      for (const m of models) {
+        this.registerLmstudioModel(m.id);
+      }
       return models;
     } catch {
       return [];
@@ -610,10 +652,14 @@ export class ProviderRegistry {
       const timeout = setTimeout(() => controller.abort(), OLLAMA_FETCH_TIMEOUT);
       const response = await fetch(OLLAMA_TAGS_URL, { method: 'GET', signal: controller.signal });
       clearTimeout(timeout);
-      if (!response.ok) return [];
+      if (!response.ok) {
+        return [];
+      }
 
       const data = await response.json() as { models?: Array<{ name: string; details?: { family?: string } }> };
-      if (!data.models || !Array.isArray(data.models)) return [];
+      if (!data.models || !Array.isArray(data.models)) {
+        return [];
+      }
 
       const models: ChatModel[] = data.models.map(m => ({
         id: m.name,
@@ -623,7 +669,9 @@ export class ProviderRegistry {
       }));
       // Only replace registered IDs on successful fetch
       this.clearOllamaModels();
-      for (const m of models) this.registerOllamaModel(m.id);
+      for (const m of models) {
+        this.registerOllamaModel(m.id);
+      }
       return models;
     } catch {
       return [];
@@ -640,10 +688,14 @@ export class ProviderRegistry {
     filterProvider?: string,
   ): Promise {
     const response = await fetch(url, { method: 'GET', headers });
-    if (!response.ok) throw new Error(`HTTP ${response.status}`);
+    if (!response.ok) {
+      throw new Error(`HTTP ${response.status}`);
+    }
 
     const data = await response.json() as { data?: Array<{ id: string }> };
-    if (!data.data || !Array.isArray(data.data)) return [];
+    if (!data.data || !Array.isArray(data.data)) {
+      return [];
+    }
 
     let models = data.data;
     if (filterProvider) {
diff --git a/src/main/engine/ai/tasks.ts b/src/main/engine/ai/tasks.ts
index a649d32..c6b80fa 100644
--- a/src/main/engine/ai/tasks.ts
+++ b/src/main/engine/ai/tasks.ts
@@ -249,7 +249,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
 
     // Get media metadata
     const mediaItem = await this.mediaEngine.getMedia(mediaId);
-    if (!mediaItem) return { success: false, error: 'Media item not found' };
+    if (!mediaItem) {
+      return { success: false, error: 'Media item not found' };
+    }
     if (!mediaItem.mimeType.startsWith('image/')) {
       return { success: false, error: `Cannot analyze this file type: ${mediaItem.mimeType}. Only images are supported.` };
     }
@@ -308,7 +310,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
       });
 
       const jsonMatch = text.match(/\{[\s\S]*\}/);
-      if (!jsonMatch) return { success: false, error: 'Invalid response format from AI' };
+      if (!jsonMatch) {
+        return { success: false, error: 'Invalid response format from AI' };
+      }
 
       const result = JSON.parse(jsonMatch[0]);
       return {
@@ -374,7 +378,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
       });
 
       const jsonMatch = text.match(/\{[\s\S]*\}/);
-      if (!jsonMatch) return { success: false, error: 'Invalid response format from AI' };
+      if (!jsonMatch) {
+        return { success: false, error: 'Invalid response format from AI' };
+      }
 
       const result = JSON.parse(jsonMatch[0]);
       const detected = (result.language || '').toLowerCase().trim();
@@ -402,7 +408,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
 
     // Load post (resolves content from filesystem for published posts)
     const post = await this.postEngine.getPost(postId);
-    if (!post) return { success: false, error: 'Post not found' };
+    if (!post) {
+      return { success: false, error: 'Post not found' };
+    }
     if (!post.content || post.content.trim().length === 0) {
       return { success: false, error: 'Post has no content to analyze' };
     }
@@ -451,13 +459,17 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
       });
 
       const jsonMatch = text.match(/\{[\s\S]*\}/);
-      if (!jsonMatch) return { success: false, error: 'Invalid response format from AI' };
+      if (!jsonMatch) {
+        return { success: false, error: 'Invalid response format from AI' };
+      }
 
       const result = JSON.parse(jsonMatch[0]);
 
       // Sanitize slug: lowercase, hyphens only
       let resultSlug = result.slug ? slugify(result.slug) : undefined;
-      if (resultSlug === '') resultSlug = undefined;
+      if (resultSlug === '') {
+        resultSlug = undefined;
+      }
 
       return {
         success: true,
@@ -633,7 +645,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
       });
 
       const jsonMatch = text.match(/\{[\s\S]*\}/);
-      if (!jsonMatch) return { success: false, error: 'Invalid response format from AI' };
+      if (!jsonMatch) {
+        return { success: false, error: 'Invalid response format from AI' };
+      }
 
       const result = JSON.parse(jsonMatch[0]);
       const detected = (result.language || '').toLowerCase().trim();
@@ -721,7 +735,9 @@ Remember: Only suggest mappings from NEW items to EXISTING items. Consider langu
       });
 
       const jsonMatch = text.match(/\{[\s\S]*\}/);
-      if (!jsonMatch) return { success: false, error: 'Invalid response format from AI' };
+      if (!jsonMatch) {
+        return { success: false, error: 'Invalid response format from AI' };
+      }
 
       const parsed = JSON.parse(jsonMatch[0]);
 
diff --git a/src/main/engine/blogmarkPython.worker.ts b/src/main/engine/blogmarkPython.worker.ts
index a63f719..a4da251 100644
--- a/src/main/engine/blogmarkPython.worker.ts
+++ b/src/main/engine/blogmarkPython.worker.ts
@@ -35,7 +35,7 @@ type WorkerResponseMessage = WorkerReadyMessage | WorkerResultMessage | WorkerEr
 type PyodideRuntime = {
   globals: {
     set: (name: string, value: unknown) => void;
-  } | any;
+  };
   runPythonAsync: (code: string) => Promise;
 };
 
diff --git a/src/main/engine/generation.worker.ts b/src/main/engine/generation.worker.ts
index 6591927..47eff0c 100644
--- a/src/main/engine/generation.worker.ts
+++ b/src/main/engine/generation.worker.ts
@@ -27,6 +27,9 @@ import {
   createDataBackedPostMediaEngine,
 } from './DataBackedEngines';
 import { createPreviewBackedGenerationRouteRenderer } from './GenerationRouteRendererFactory';
+import type { BlogGenerationPostEngineContract } from './BlogGenerationEngine';
+import type { MediaEngine } from './MediaEngine';
+import type { PostMediaEngine } from './PostMediaEngine';
 import {
   generateSinglePostPages,
   generateCategoryPages,
@@ -52,11 +55,18 @@ function createWorkerHashStore(hashCache: Map) {
   const pendingUpdates: Array<{ relativePath: string; hash: string }> = [];
 
   return {
-    async get(_projectId: string, relativePath: string): Promise {
+    async get(
+      _projectId: string,
+      relativePath: string,
+    ): Promise {
       return hashCache.get(relativePath) ?? null;
     },
 
-    async set(_projectId: string, relativePath: string, hash: string): Promise {
+    async set(
+      _projectId: string,
+      relativePath: string,
+      hash: string,
+    ): Promise {
       pendingUpdates.push({ relativePath, hash });
       hashCache.set(relativePath, hash);
     },
@@ -106,7 +116,10 @@ async function run(): Promise {
     }
 
     // 2c. Reconstruct post-media links for gallery/album macros
-    const postMediaLinks = new Map>();
+    const postMediaLinks = new Map<
+      string,
+      Array<{ mediaId: string; sortOrder: number }>
+    >();
     if (task.postMediaLinksEntries) {
       for (const [postId, links] of task.postMediaLinksEntries) {
         postMediaLinks.set(postId, links);
@@ -114,7 +127,10 @@ async function run(): Promise {
     }
 
     // 3. Reconstruct backlinks Map
-    const backlinksMap = new Map>();
+    const backlinksMap = new Map<
+      string,
+      Array<{ id: string; title: string; slug: string }>
+    >();
     if (task.backlinksMap) {
       for (const [postId, links] of Object.entries(task.backlinksMap)) {
         backlinksMap.set(postId, links);
@@ -122,9 +138,16 @@ async function run(): Promise {
     }
 
     // 4. Create data-backed engines
-    const postEngine = createDataBackedPostEngine({ allPosts: lookupPosts, backlinksMap, postFilePaths });
+    const postEngine = createDataBackedPostEngine({
+      allPosts: lookupPosts,
+      backlinksMap,
+      postFilePaths,
+    });
     const mediaEngine = createDataBackedMediaEngine(mediaItems);
-    const postMediaEngine = createDataBackedPostMediaEngine({ mediaItems, postMediaLinks });
+    const postMediaEngine = createDataBackedPostMediaEngine({
+      mediaItems,
+      postMediaLinks,
+    });
 
     // 5. Create route renderer (same factory as main thread, but backed by data)
     const renderRoute = createPreviewBackedGenerationRouteRenderer({
@@ -137,9 +160,9 @@ async function run(): Promise {
       publishedPostsForLookup: lookupPosts,
       languagePrefix: task.languagePrefix,
       engines: {
-        postEngine: postEngine as any,
-        mediaEngine: mediaEngine as any,
-        postMediaEngine: postMediaEngine as any,
+        postEngine: postEngine as BlogGenerationPostEngineContract,
+        mediaEngine: mediaEngine as MediaEngine,
+        postMediaEngine: postMediaEngine as PostMediaEngine,
       },
     });
 
@@ -173,99 +196,111 @@ async function run(): Promise {
     const projectId = task.options.projectId;
 
     switch (task.section) {
-      case 'single': {
-        pagesGenerated += await generateSinglePostPages({
-          projectId,
-          posts,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-        });
-        break;
-      }
+    case 'single': {
+      pagesGenerated += await generateSinglePostPages({
+        projectId,
+        posts,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+      });
+      break;
+    }
 
-      case 'category': {
-        const allCategories = new Set(task.allCategories ?? []);
-        const postsByCategory = task.postsByCategoryEntries
-          ? deserializePostMap(task.postsByCategoryEntries)
-          : undefined;
+    case 'category': {
+      const allCategories = new Set(task.allCategories ?? []);
+      const postsByCategory = task.postsByCategoryEntries
+        ? deserializePostMap(task.postsByCategoryEntries)
+        : undefined;
 
-        pagesGenerated += await generateCategoryPages({
-          projectId,
-          posts,
-          allCategories,
-          maxPostsPerPage: task.maxPostsPerPage,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-          postsByCategory,
-        });
-        break;
-      }
+      pagesGenerated += await generateCategoryPages({
+        projectId,
+        posts,
+        allCategories,
+        maxPostsPerPage: task.maxPostsPerPage,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+        postsByCategory,
+      });
+      break;
+    }
 
-      case 'tag': {
-        const allTags = new Set(task.allTags ?? []);
-        const postsByTag = task.postsByTagEntries
-          ? deserializePostMap(task.postsByTagEntries)
-          : undefined;
+    case 'tag': {
+      const allTags = new Set(task.allTags ?? []);
+      const postsByTag = task.postsByTagEntries
+        ? deserializePostMap(task.postsByTagEntries)
+        : undefined;
 
-        pagesGenerated += await generateTagPages({
-          projectId,
-          posts,
-          allTags,
-          maxPostsPerPage: task.maxPostsPerPage,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-          postsByTag,
-        });
-        break;
-      }
+      pagesGenerated += await generateTagPages({
+        projectId,
+        posts,
+        allTags,
+        maxPostsPerPage: task.maxPostsPerPage,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+        postsByTag,
+      });
+      break;
+    }
 
-      case 'date': {
-        const yearsMap = task.yearsEntries ? deserializeDateMap(task.yearsEntries) : new Map();
-        const yearMonthsMap = task.yearMonthsEntries ? deserializeDateMap(task.yearMonthsEntries) : new Map();
-        const yearMonthDaysMap = task.yearMonthDaysEntries ? deserializeDateMap(task.yearMonthDaysEntries) : new Map();
-        const postsByYear = task.postsByYearEntries ? deserializePostMap(task.postsByYearEntries) : undefined;
-        const postsByYearMonth = task.postsByYearMonthEntries ? deserializePostMap(task.postsByYearMonthEntries) : undefined;
-        const postsByYearMonthDay = task.postsByYearMonthDayEntries ? deserializePostMap(task.postsByYearMonthDayEntries) : undefined;
+    case 'date': {
+      const yearsMap = task.yearsEntries
+        ? deserializeDateMap(task.yearsEntries)
+        : new Map();
+      const yearMonthsMap = task.yearMonthsEntries
+        ? deserializeDateMap(task.yearMonthsEntries)
+        : new Map();
+      const yearMonthDaysMap = task.yearMonthDaysEntries
+        ? deserializeDateMap(task.yearMonthDaysEntries)
+        : new Map();
+      const postsByYear = task.postsByYearEntries
+        ? deserializePostMap(task.postsByYearEntries)
+        : undefined;
+      const postsByYearMonth = task.postsByYearMonthEntries
+        ? deserializePostMap(task.postsByYearMonthEntries)
+        : undefined;
+      const postsByYearMonthDay = task.postsByYearMonthDayEntries
+        ? deserializePostMap(task.postsByYearMonthDayEntries)
+        : undefined;
 
-        pagesGenerated += await generateDateArchivePages({
-          projectId,
-          posts,
-          yearsMap,
-          yearMonthsMap,
-          yearMonthDaysMap,
-          maxPostsPerPage: task.maxPostsPerPage,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-          postsByYear,
-          postsByYearMonth,
-          postsByYearMonthDay,
-        });
-        break;
-      }
+      pagesGenerated += await generateDateArchivePages({
+        projectId,
+        posts,
+        yearsMap,
+        yearMonthsMap,
+        yearMonthDaysMap,
+        maxPostsPerPage: task.maxPostsPerPage,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+        postsByYear,
+        postsByYearMonth,
+        postsByYearMonthDay,
+      });
+      break;
+    }
 
-      case 'core': {
-        // Core includes root pages and page routes (sitemap/feeds handled by main thread)
-        pagesGenerated += await generateRootPages({
-          projectId,
-          posts,
-          maxPostsPerPage: task.maxPostsPerPage,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-        });
-        pagesGenerated += await generatePageRoutes({
-          projectId,
-          posts: lookupPosts,
-          renderRoute,
-          writePage,
-          onPageGenerated,
-        });
-        break;
-      }
+    case 'core': {
+      // Core includes root pages and page routes (sitemap/feeds handled by main thread)
+      pagesGenerated += await generateRootPages({
+        projectId,
+        posts,
+        maxPostsPerPage: task.maxPostsPerPage,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+      });
+      pagesGenerated += await generatePageRoutes({
+        projectId,
+        posts: lookupPosts,
+        renderRoute,
+        writePage,
+        onPageGenerated,
+      });
+      break;
+    }
     }
 
     // 8. Report result with accumulated hash updates
diff --git a/src/main/engine/mainProcessPythonApiInvoker.ts b/src/main/engine/mainProcessPythonApiInvoker.ts
index 5c58923..afcbc51 100644
--- a/src/main/engine/mainProcessPythonApiInvoker.ts
+++ b/src/main/engine/mainProcessPythonApiInvoker.ts
@@ -12,7 +12,9 @@ export function setEngineBundle(bundle: EngineBundle): void {
 
 function requireBundle(): EngineBundle {
   if (!registeredBundle) {
-    throw new Error('Engine bundle not registered. Call setEngineBundle() before invoking Python API methods.');
+    throw new Error(
+      'Engine bundle not registered. Call setEngineBundle() before invoking Python API methods.',
+    );
   }
   return registeredBundle;
 }
@@ -24,7 +26,11 @@ function asRecord(value: unknown): Record {
   return value as Record;
 }
 
-function validateParamValue(methodName: string, param: PythonApiParamContractV1, value: unknown): void {
+function validateParamValue(
+  methodName: string,
+  param: PythonApiParamContractV1,
+  value: unknown,
+): void {
   if (param.type === 'stringOrNull') {
     if (value === null || (typeof value === 'string' && value.length > 0)) {
       return;
@@ -79,20 +85,20 @@ function validateParamValue(methodName: string, param: PythonApiParamContractV1,
   }
 }
 
-type EngineGetter = () => Record unknown>;
+type EngineGetter = () => unknown;
 
 export const ENGINE_MAP: Record = {
-  posts: () => requireBundle().postEngine as any,
-  media: () => requireBundle().mediaEngine as any,
-  projects: () => requireBundle().projectEngine as any,
-  meta: () => requireBundle().metaEngine as any,
-  tags: () => requireBundle().tagEngine as any,
-  scripts: () => requireBundle().scriptEngine as any,
-  templates: () => requireBundle().templateEngine as any,
-  tasks: () => requireBundle().taskManager as any,
-  sync: () => requireBundle().gitApiAdapter as any,
-  publish: () => requireBundle().publishApiAdapter as any,
-  app: () => requireBundle().appApiAdapter as any,
+  posts: () => requireBundle().postEngine,
+  media: () => requireBundle().mediaEngine,
+  projects: () => requireBundle().projectEngine,
+  meta: () => requireBundle().metaEngine,
+  tags: () => requireBundle().tagEngine,
+  scripts: () => requireBundle().scriptEngine,
+  templates: () => requireBundle().templateEngine,
+  tasks: () => requireBundle().taskManager,
+  sync: () => requireBundle().gitApiAdapter,
+  publish: () => requireBundle().publishApiAdapter,
+  app: () => requireBundle().appApiAdapter,
 };
 
 // Map API method names to engine method names where they differ
@@ -195,7 +201,10 @@ const METHOD_NAME_MAP: Record = {
   'tasks.clearCompleted': 'clearCompletedTasks',
 };
 
-export async function invokeMainProcessPythonApi(method: string, args: Record): Promise {
+export async function invokeMainProcessPythonApi(
+  method: string,
+  args: Record,
+): Promise {
   const contract = getPythonApiMethodContract(method);
   if (!contract) {
     throw new Error(`Unsupported Python API method: ${method}`);
@@ -210,14 +219,24 @@ export async function invokeMainProcessPythonApi(method: string, args: Record)[engineMethodName];
 
   if (typeof callable !== 'function') {
-    throw new Error(`Unsupported Python API method: ${method} (engine method '${engineMethodName}' not found)`);
+    throw new Error(
+      `Unsupported Python API method: ${method} (engine method '${engineMethodName}' not found)`,
+    );
   }
 
   const orderedArgs = contract.params.map((param) => {
diff --git a/src/main/engine/mcp-view-builder.ts b/src/main/engine/mcp-view-builder.ts
index 655f52f..a416967 100644
--- a/src/main/engine/mcp-view-builder.ts
+++ b/src/main/engine/mcp-view-builder.ts
@@ -69,16 +69,20 @@ let _appBundle: string | null = null;
  * Result is cached after the first call.
  */
 function getAppBundle(): string {
-  if (_appBundle !== null) return _appBundle;
+  if (_appBundle !== null) {
+    return _appBundle;
+  }
 
-  // eslint-disable-next-line @typescript-eslint/no-require-imports
+   
   const bundlePath: string = require.resolve('@modelcontextprotocol/ext-apps/app-with-deps');
   let source = fs.readFileSync(bundlePath, 'utf-8');
 
   // The bundle ends with export{...,X as App,...}.
   // Extract the internal variable name for `App`.
   const match = source.match(/export\{[^}]*\b(\w+)\s+as\s+App\b[^}]*\}/);
-  if (!match) throw new Error('Could not find App export in app-with-deps bundle');
+  if (!match) {
+    throw new Error('Could not find App export in app-with-deps bundle');
+  }
   const internalName = match[1];
 
   // Strip ESM export block and expose App class on globalThis.
@@ -93,7 +97,9 @@ function getAppBundle(): string {
 
 const SHARED_JS = `\
     const App = globalThis.__bdsExtApp;
-    if (!App) { document.getElementById("status").textContent = "Error: App bundle not loaded"; document.getElementById("status").className = "status status-error"; document.getElementById("status").style.display = "block"; throw new Error("App bundle not loaded"); }
+    if (!App) {
+      document.getElementById("status").textContent = "Error: App bundle not loaded"; document.getElementById("status").className = "status status-error"; document.getElementById("status").style.display = "block"; throw new Error("App bundle not loaded");
+    }
 
     const app = new App({ name: "bDS Review", version: "1.0.0" });
 
@@ -159,7 +165,9 @@ const SHARED_JS = `\
     }
 
     window.showStatus = showStatus;
-    function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; }
+    function esc(s) {
+      const d = document.createElement("div"); d.textContent = s; return d.innerHTML;
+    }
 
     window.__connectApp = () => {
       app.connect()
diff --git a/src/main/engine/mcp-views.ts b/src/main/engine/mcp-views.ts
index 066a54e..fc90ddb 100644
--- a/src/main/engine/mcp-views.ts
+++ b/src/main/engine/mcp-views.ts
@@ -102,7 +102,7 @@ export function reviewMetadataHtml(): string {
     .diff-table th { background: #f1f3f5; font-weight: 600; }
     .diff-old { background: #ffeef0; }
     .diff-new { background: #e6ffed; }`,
-    extraJsHelpers: `function fmt(v) { if (v == null) return "(empty)"; if (Array.isArray(v)) return v.join(", "); return String(v); }`,
+    extraJsHelpers: 'function fmt(v) { if (v == null) return "(empty)"; if (Array.isArray(v)) return v.join(", "); return String(v); }',
     renderBody: `\
       const current = data.current || {};
       const proposed = data.proposed || {};
diff --git a/src/main/engine/pythonMacro.worker.ts b/src/main/engine/pythonMacro.worker.ts
index 5d9b651..af20f04 100644
--- a/src/main/engine/pythonMacro.worker.ts
+++ b/src/main/engine/pythonMacro.worker.ts
@@ -56,7 +56,7 @@ type WorkerResponseMessage = WorkerReadyMessage | WorkerMacroResultMessage | Wor
 type PyodideRuntime = {
   globals: {
     set: (name: string, value: unknown) => void;
-  } | any;
+  };
   runPythonAsync: (code: string) => Promise;
   registerJsModule: (name: string, module: Record) => void;
 };
diff --git a/src/main/engine/stemmer.ts b/src/main/engine/stemmer.ts
index 1cb1f02..b2179c8 100644
--- a/src/main/engine/stemmer.ts
+++ b/src/main/engine/stemmer.ts
@@ -6,7 +6,7 @@
  * Portuguese, Dutch, Russian, Arabic, and more.
  */
 
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+ 
 const snowballFactory = require('snowball-stemmers');
 
 export type SupportedLanguage =
@@ -139,7 +139,9 @@ export function stemWord(word: string, language: SupportedLanguage = 'english'):
  * stemText('Häuser Haus', 'german') // 'haus haus'
  */
 export function stemText(text: string, language: SupportedLanguage = 'english'): string {
-  if (!text) return '';
+  if (!text) {
+    return '';
+  }
   
   const words = tokenize(text);
   const stemmer = getStemmer(language);
@@ -166,7 +168,9 @@ export function stemText(text: string, language: SupportedLanguage = 'english'):
  * stemQuery('"running fast"', 'english') // '"run fast"'
  */
 export function stemQuery(query: string, language: SupportedLanguage = 'english'): string {
-  if (!query) return '';
+  if (!query) {
+    return '';
+  }
   
   const stemmer = getStemmer(language);
   
@@ -203,7 +207,7 @@ export function stemQuery(query: string, language: SupportedLanguage = 'english'
         return stemmer.stem(words[0].toLowerCase());
       }
       return '';
-    }
+    },
   );
   
   // Clean up multiple spaces
diff --git a/src/main/engine/taxonomyUtils.ts b/src/main/engine/taxonomyUtils.ts
index d6f1ced..14182e5 100644
--- a/src/main/engine/taxonomyUtils.ts
+++ b/src/main/engine/taxonomyUtils.ts
@@ -8,7 +8,7 @@ export function normalizeNonEmptyTaxonomyTerm(term: string): string | null {
 }
 
 export function collectNormalizedTermsFromJsonValues(
-  values: Array
+  values: Array,
 ): string[] {
   const terms = new Set();
 
diff --git a/src/main/ipc/blogHandlers.ts b/src/main/ipc/blogHandlers.ts
index d9e194c..50b680d 100644
--- a/src/main/ipc/blogHandlers.ts
+++ b/src/main/ipc/blogHandlers.ts
@@ -1,12 +1,12 @@
 import * as path from 'path';
 import { dialog } from 'electron';
+import type { IpcMainInvokeEvent } from 'electron';
 import {
   resolvePublicBaseUrl,
   type BlogGenerationResult,
   type BlogGenerationSection,
   type BlogGenerationOptions,
   type SiteValidationReport,
-  type ApplyValidationPreparation,
 } from '../engine/BlogGenerationEngine';
 import { resolvePageTitle } from '../engine/PageRenderer';
 import { buildSearchIndex } from '../engine/SearchIndexEngine';
@@ -16,7 +16,7 @@ import { autoTranslatePost, autoTranslateMediaMetadata } from './chatHandlers';
 import { v4 as uuidv4 } from 'uuid';
 import { getDatabase } from '../database/connection';
 
-type SafeHandle = (channel: string, handler: (...args: any[]) => Promise) => void;
+type SafeHandle = (channel: string, handler: (event: IpcMainInvokeEvent, ...args: Args) => Promise) => void;
 
 export function registerBlogHandlers(safeHandle: SafeHandle, bundle: EngineBundle): void {
   const resolveActiveProjectContext = async (): Promise<{
@@ -85,8 +85,8 @@ export function registerBlogHandlers(safeHandle: SafeHandle, bundle: EngineBundl
       blogLanguages: Array.isArray(metadata?.blogLanguages) ? metadata.blogLanguages : [],
       pageTitle,
       picoTheme: metadata?.picoTheme,
-      categoryMetadata: (metadata as any)?.categoryMetadata,
-      categorySettings: (metadata as any)?.categorySettings,
+      categoryMetadata: metadata?.categoryMetadata,
+      categorySettings: metadata?.categorySettings,
       menu,
       dbPath: getDatabase().getDbPath(),
     };
@@ -321,8 +321,12 @@ export function registerBlogHandlers(safeHandle: SafeHandle, bundle: EngineBundl
         }
 
         const parts: string[] = ['Done'];
-        if (failed > 0) parts.push(`${failed} failed`);
-        if (warned > 0) parts.push(`${warned} warnings`);
+        if (failed > 0) {
+          parts.push(`${failed} failed`);
+        }
+        if (warned > 0) {
+          parts.push(`${warned} warnings`);
+        }
         onProgress(100, parts.length > 1 ? `${parts[0]} (${parts.slice(1).join(', ')})` : parts[0]);
       },
     }).catch(() => { /* errors tracked via task panel */ });
diff --git a/src/main/ipc/chatHandlers.ts b/src/main/ipc/chatHandlers.ts
index 259b538..a9fbca3 100644
--- a/src/main/ipc/chatHandlers.ts
+++ b/src/main/ipc/chatHandlers.ts
@@ -24,6 +24,13 @@ let initPromise: Promise | null = null;
 let mainWindowGetter: (() => BrowserWindow | null) | null = null;
 let engineBundle: EngineBundle | null = null;
 
+function requireEngineBundle(): EngineBundle {
+  if (!engineBundle) {
+    throw new Error('Chat handlers not initialized');
+  }
+  return engineBundle;
+}
+
 /**
  * Get or create the SecureKeyStore instance.
  */
@@ -77,10 +84,11 @@ function getChatService(): ChatService {
   if (!chatService) {
     const engine = getChatEngine();
     const reg = getProviders();
+    const bundle = requireEngineBundle();
     const deps: BlogToolDeps = {
-      postEngine: engineBundle!.postEngine,
-      mediaEngine: engineBundle!.mediaEngine,
-      postMediaEngine: engineBundle!.postMediaEngine,
+      postEngine: bundle.postEngine,
+      mediaEngine: bundle.mediaEngine,
+      postMediaEngine: bundle.postMediaEngine,
     };
     chatService = new ChatService(engine, reg, deps, () => mainWindowGetter?.() || null);
   }
@@ -92,7 +100,8 @@ function getChatService(): ChatService {
  */
 function getOneShotTasks(): OneShotTasks {
   if (!oneShotTasks) {
-    oneShotTasks = new OneShotTasks(getProviders(), getChatEngine(), engineBundle!.mediaEngine, engineBundle!.postEngine);
+    const bundle = requireEngineBundle();
+    oneShotTasks = new OneShotTasks(getProviders(), getChatEngine(), bundle.mediaEngine, bundle.postEngine);
   }
   return oneShotTasks;
 }
@@ -111,18 +120,24 @@ async function ensureInitialized(): Promise {
 
       try {
         const key = await keyStore.retrieve('opencode_api_key');
-        if (key) reg.setOpencodeKey(key);
+        if (key) {
+          reg.setOpencodeKey(key);
+        }
       } catch { /* ignore */ }
 
       try {
         const mistralKey = await keyStore.retrieve('mistral_api_key');
-        if (mistralKey) reg.setMistralKey(mistralKey);
+        if (mistralKey) {
+          reg.setMistralKey(mistralKey);
+        }
       } catch { /* ignore */ }
 
       // Restore Ollama enabled state from settings DB
       try {
         const ollamaEnabled = await getChatEngine().getSetting('ollama_enabled');
-        if (ollamaEnabled === 'true') reg.setOllamaEnabled(true);
+        if (ollamaEnabled === 'true') {
+          reg.setOllamaEnabled(true);
+        }
       } catch { /* ignore */ }
 
       // Restore Ollama model capability overrides
@@ -138,14 +153,18 @@ async function ensureInitialized(): Promise {
       try {
         const ollamaIds = await getChatEngine().getSetting('ollama_known_model_ids');
         if (ollamaIds) {
-          for (const id of JSON.parse(ollamaIds) as string[]) reg.registerOllamaModel(id);
+          for (const id of JSON.parse(ollamaIds) as string[]) {
+            reg.registerOllamaModel(id);
+          }
         }
       } catch { /* ignore */ }
 
       // Restore LM Studio enabled state from settings DB
       try {
         const lmstudioEnabled = await getChatEngine().getSetting('lmstudio_enabled');
-        if (lmstudioEnabled === 'true') reg.setLmstudioEnabled(true);
+        if (lmstudioEnabled === 'true') {
+          reg.setLmstudioEnabled(true);
+        }
       } catch { /* ignore */ }
 
       // Restore LM Studio model capability overrides
@@ -161,7 +180,9 @@ async function ensureInitialized(): Promise {
       try {
         const lmIds = await getChatEngine().getSetting('lmstudio_known_model_ids');
         if (lmIds) {
-          for (const id of JSON.parse(lmIds) as string[]) reg.registerLmstudioModel(id);
+          for (const id of JSON.parse(lmIds) as string[]) {
+            reg.registerLmstudioModel(id);
+          }
         }
       } catch { /* ignore */ }
 
@@ -248,7 +269,9 @@ export function registerChatHandlers(): void {
     try {
       await ensureInitialized();
       const key = getProviders().getOpencodeKey();
-      if (!key) return { hasKey: false, maskedKey: '' };
+      if (!key) {
+        return { hasKey: false, maskedKey: '' };
+      }
       const masked = '•'.repeat(Math.max(0, key.length - 4)) + key.slice(-4);
       return { hasKey: true, maskedKey: masked };
     } catch (error) {
@@ -298,7 +321,9 @@ export function registerChatHandlers(): void {
     try {
       await ensureInitialized();
       const key = getProviders().getMistralKey();
-      if (!key) return { hasKey: false, maskedKey: '' };
+      if (!key) {
+        return { hasKey: false, maskedKey: '' };
+      }
       const masked = '•'.repeat(Math.max(0, key.length - 4)) + key.slice(-4);
       return { hasKey: true, maskedKey: masked };
     } catch (error) {
@@ -753,8 +778,9 @@ export function registerChatHandlers(): void {
   // ============ Chat Messaging ============
 
   // Send a message
-  ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string, _metadata?: { surface?: 'tab' | 'sidebar' }) => {
+  ipcMain.handle('chat:sendMessage', async (_, conversationId: string, message: string, metadata?: { surface?: 'tab' | 'sidebar' }) => {
     try {
+      void metadata;
       await ensureInitialized();
       const service = getChatService();
       const mainWindow = mainWindowGetter?.();
@@ -949,7 +975,7 @@ export function registerChatHandlers(): void {
 
   ipcMain.handle('a2ui:dispatch', async (_, action: { surfaceId: string; componentId: string; action: string; payload?: Record }) => {
     try {
-      console.log('[Chat IPC] A2UI action dispatched:', action);
+      void action;
       // Currently, A2UI actions are handled client-side (navigation, UI toggles).
       // Server-side action handling can be added here in the future.
       return { success: true };
diff --git a/src/main/ipc/embeddingHandlers.ts b/src/main/ipc/embeddingHandlers.ts
index 6412ffd..43e8e71 100644
--- a/src/main/ipc/embeddingHandlers.ts
+++ b/src/main/ipc/embeddingHandlers.ts
@@ -1,9 +1,10 @@
 import type { EngineBundle } from '../engine/EngineBundle';
+import type { IpcMainInvokeEvent } from 'electron';
 import { startDuplicateSearchTask } from './handlers';
 import { resolveUiLanguageFromSystemLocale, translateMenu } from '../shared/i18n';
 import { app } from 'electron';
 
-type SafeHandle = (channel: string, handler: (...args: any[]) => Promise) => void;
+type SafeHandle = (channel: string, handler: (event: IpcMainInvokeEvent, ...args: Args) => Promise) => void;
 
 function tr(key: string): string {
   const systemLocale = typeof app.getLocale === 'function' ? app.getLocale() : 'en';
diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts
index ac69b1c..475fe39 100644
--- a/src/main/ipc/handlers.ts
+++ b/src/main/ipc/handlers.ts
@@ -1,4 +1,5 @@
 import { app, BrowserWindow, ipcMain, dialog, shell, clipboard } from 'electron';
+import type { IpcMainInvokeEvent, WebContents } from 'electron';
 import * as path from 'path';
 import * as fsPromises from 'fs/promises';
 import { eq } from 'drizzle-orm';
@@ -9,7 +10,6 @@ import { MetaEngine } from '../engine/MetaEngine';
 import type { MenuDocument } from '../engine/MenuEngine';
 import type { CreateScriptInput, UpdateScriptInput } from '../engine/ScriptEngine';
 import type { CreateTemplateInput, UpdateTemplateInput } from '../engine/TemplateEngine';
-import type { TaskProgress } from '../engine/TaskManager';
 import { getDatabase } from '../database';
 import { media } from '../database/schema';
 import { APP_MENU_ACTION_EVENT_MAP, APP_MENU_WEB_CONTENTS_ACTIONS, type AppMenuAction } from '../shared/menuCommands';
@@ -31,12 +31,16 @@ const SUPPORTED_DROP_IMAGE_MIME_TYPES = new Set(['image/jpeg', 'image/png', 'ima
  * Wrap an IPC handler so that "Database is closing" errors during shutdown
  * are silently swallowed instead of being logged as scary red error messages.
  */
-function safeHandle(channel: string, handler: (...args: any[]) => Promise): void {
-  ipcMain.handle(channel, async (...args) => {
+function isDatabaseClosingError(error: unknown): error is { message: string } {
+  return typeof error === 'object' && error !== null && 'message' in error && (error as { message: unknown }).message === 'Database is closing';
+}
+
+function safeHandle(channel: string, handler: (event: IpcMainInvokeEvent, ...args: Args) => Promise): void {
+  ipcMain.handle(channel, async (event, ...args) => {
     try {
-      return await handler(...args);
-    } catch (error: any) {
-      if (error?.message === 'Database is closing') {
+      return await handler(event, ...(args as Args));
+    } catch (error) {
+      if (isDatabaseClosingError(error)) {
         return null; // Silently ignore during shutdown
       }
       throw error; // Re-throw all other errors
@@ -60,7 +64,7 @@ function resolvePostCreatedAt(post: { createdAt: Date | string }): Date {
   return Number.isNaN(parsed.getTime()) ? new Date() : parsed;
 }
 
-function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
+function runWebContentsMenuAction(sender: WebContents | undefined, action: AppMenuAction): boolean {
   if (!sender) {
     return false;
   }
@@ -70,63 +74,63 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean {
   }
 
   switch (action) {
-    case 'undo':
-      sender.undo?.();
-      return true;
-    case 'redo':
-      sender.redo?.();
-      return true;
-    case 'cut':
-      sender.cut?.();
-      return true;
-    case 'copy':
-      sender.copy?.();
-      return true;
-    case 'paste':
-      sender.paste?.();
-      return true;
-    case 'delete':
-      sender.delete?.();
-      return true;
-    case 'selectAll':
-      sender.selectAll?.();
-      return true;
-    case 'toggleDevTools':
-      if (sender.isDevToolsOpened?.()) {
-        sender.closeDevTools?.();
-      } else {
-        sender.openDevTools?.({ mode: 'detach' });
-      }
-      return true;
-    case 'reload':
-      sender.reload?.();
-      return true;
-    case 'forceReload':
-      sender.reloadIgnoringCache?.();
-      return true;
-    case 'resetZoom':
-      sender.setZoomLevel?.(0);
-      return true;
-    case 'zoomIn': {
-      const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
-      sender.setZoomLevel?.(currentZoomLevel + 0.5);
-      return true;
+  case 'undo':
+    sender.undo?.();
+    return true;
+  case 'redo':
+    sender.redo?.();
+    return true;
+  case 'cut':
+    sender.cut?.();
+    return true;
+  case 'copy':
+    sender.copy?.();
+    return true;
+  case 'paste':
+    sender.paste?.();
+    return true;
+  case 'delete':
+    sender.delete?.();
+    return true;
+  case 'selectAll':
+    sender.selectAll?.();
+    return true;
+  case 'toggleDevTools':
+    if (sender.isDevToolsOpened?.()) {
+      sender.closeDevTools?.();
+    } else {
+      sender.openDevTools?.({ mode: 'detach' });
     }
-    case 'zoomOut': {
-      const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
-      sender.setZoomLevel?.(currentZoomLevel - 0.5);
-      return true;
-    }
-    case 'toggleFullScreen': {
-      const ownerWindow = BrowserWindow.fromWebContents(sender);
-      if (!ownerWindow) {
-        return false;
-      }
-      ownerWindow.setFullScreen(!ownerWindow.isFullScreen());
-      return true;
-    }
-    default:
+    return true;
+  case 'reload':
+    sender.reload?.();
+    return true;
+  case 'forceReload':
+    sender.reloadIgnoringCache?.();
+    return true;
+  case 'resetZoom':
+    sender.setZoomLevel?.(0);
+    return true;
+  case 'zoomIn': {
+    const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
+    sender.setZoomLevel?.(currentZoomLevel + 0.5);
+    return true;
+  }
+  case 'zoomOut': {
+    const currentZoomLevel = sender.getZoomLevel?.() ?? 0;
+    sender.setZoomLevel?.(currentZoomLevel - 0.5);
+    return true;
+  }
+  case 'toggleFullScreen': {
+    const ownerWindow = BrowserWindow.fromWebContents(sender);
+    if (!ownerWindow) {
       return false;
+    }
+    ownerWindow.setFullScreen(!ownerWindow.isFullScreen());
+    return true;
+  }
+  default:
+    return false;
   }
 }
 
@@ -212,7 +216,7 @@ async function handleDroppedImageImport(
   };
 }
 
-function buildMcpAgentConfigOptions(bundle: EngineBundle): import('../engine/MCPAgentConfigEngine').MCPAgentConfigOptions {
+function buildMcpAgentConfigOptions(): import('../engine/MCPAgentConfigEngine').MCPAgentConfigOptions {
   const os = require('os') as typeof import('os');
   const scriptPath = app.isPackaged
     ? path.join(process.resourcesPath, 'bds-mcp.cjs')
@@ -575,19 +579,27 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
   // Auto-translate: enqueue translation tasks for each blog language that does
   // not yet have a translation. Only triggered on manual save or publish.
   const enqueueAutoTranslations = async (post: PostData): Promise => {
-    if (post.doNotTranslate) return;
+    if (post.doNotTranslate) {
+      return;
+    }
     const metadata = await bundle.metaEngine.getProjectMetadata();
-    if (!metadata) return;
+    if (!metadata) {
+      return;
+    }
     const blogLanguages = metadata.blogLanguages || [];
     const mainLang = metadata.mainLanguage || 'en';
     const postLang = post.language || mainLang;
     const targetLanguages = blogLanguages.filter((lang) => lang !== postLang);
-    if (targetLanguages.length === 0) return;
+    if (targetLanguages.length === 0) {
+      return;
+    }
 
     const existingTranslations = await bundle.postEngine.getPostTranslations(post.id);
     const existingLangs = new Set(existingTranslations.map((t) => t.language));
     const missingLanguages = targetLanguages.filter((lang) => !existingLangs.has(lang));
-    if (missingLanguages.length === 0) return;
+    if (missingLanguages.length === 0) {
+      return;
+    }
 
     const groupId = uuidv4();
     for (const targetLang of missingLanguages) {
@@ -602,7 +614,7 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
           if (!result.success) {
             throw new Error(result.error || `Translation to ${targetLang} failed`);
           }
-          onProgress(70, `Translating linked media...`);
+          onProgress(70, 'Translating linked media...');
           // Cascade: translate linked media metadata
           const links = await bundle.postMediaEngine.getLinkedMediaForPost(post.id);
           for (const link of links) {
@@ -1351,7 +1363,10 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
       return;
     }
 
-    const handledByWebContents = runWebContentsMenuAction((event as any)?.sender, typedAction);
+    const sender = event && typeof event === 'object' && 'sender' in event
+      ? (event as IpcMainInvokeEvent).sender
+      : undefined;
+    const handledByWebContents = runWebContentsMenuAction(sender, typedAction);
     if (handledByWebContents) {
       return;
     }
@@ -1742,7 +1757,7 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
     current: number,
     total: number,
     detail?: string,
-    eta?: number
+    eta?: number,
   ) => {
     ipcMain.emit('forward-to-renderer', 'import:executionProgress', {
       taskId,
@@ -1776,7 +1791,7 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
     // Create a task for the import
     const taskId = `import-${Date.now()}`;
     let processedItems = 0;
-    let startTime = Date.now();
+    const startTime = Date.now();
 
     const task = {
       id: taskId,
@@ -1845,7 +1860,7 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
     };
 
     // Run the task - this returns immediately with a promise
-    const resultPromise = bundle.taskManager.runTask(task);
+    void bundle.taskManager.runTask(task);
     
     // Return task ID so UI can track it
     return { taskId, totalItems };
@@ -1886,7 +1901,7 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
     return engine.getAllForProject();
   });
 
-  safeHandle('importDefinitions:update', async (event, id: string, updates: any) => {
+  safeHandle('importDefinitions:update', async (event, id: string, updates: Record) => {
     const { ImportDefinitionEngine } = await import('../engine/ImportDefinitionEngine');
     const engine = new ImportDefinitionEngine();
     const projectEngine = bundle.projectEngine;
@@ -1896,8 +1911,8 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
     }
     const result = await engine.updateDefinition(id, updates);
     // Notify renderer of name changes for sidebar/tab updates
-    if (result && updates.name !== undefined) {
-      event.sender.send('importDefinition-name-updated', { definitionId: id, name: result.name });
+    if (result && updates.name !== undefined && event && typeof event === 'object' && 'sender' in event) {
+      (event as IpcMainInvokeEvent).sender.send('importDefinition-name-updated', { definitionId: id, name: result.name });
     }
     return result;
   });
@@ -1922,25 +1937,25 @@ export function registerIpcHandlers(bundle: EngineBundle): void {
 
   safeHandle('mcp:getAgents', async () => {
     const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine');
-    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle));
+    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions());
     return engine.getAgents();
   });
 
   safeHandle('mcp:addToAgentConfig', async (_event: unknown, agentId: string) => {
     const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine');
-    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle));
+    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions());
     return engine.addToConfig(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId);
   });
 
   safeHandle('mcp:removeFromAgentConfig', async (_event: unknown, agentId: string) => {
     const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine');
-    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle));
+    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions());
     return engine.removeFromConfig(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId);
   });
 
   safeHandle('mcp:isConfigured', async (_event: unknown, agentId: string) => {
     const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine');
-    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions(bundle));
+    const engine = new MCPAgentConfigEngine(buildMcpAgentConfigOptions());
     return engine.isConfigured(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId);
   });
 
diff --git a/src/main/ipc/metadataDiffHandlers.ts b/src/main/ipc/metadataDiffHandlers.ts
index a35d081..34c6c98 100644
--- a/src/main/ipc/metadataDiffHandlers.ts
+++ b/src/main/ipc/metadataDiffHandlers.ts
@@ -1,7 +1,8 @@
+import type { IpcMainInvokeEvent } from 'electron';
 import type { EngineBundle } from '../engine/EngineBundle';
 import type { MediaDiffField, ScriptDiffField, TemplateDiffField } from '../engine/MetadataDiffEngine';
 
-type SafeHandle = (channel: string, handler: (...args: any[]) => Promise) => void;
+type SafeHandle = (channel: string, handler: (event: IpcMainInvokeEvent, ...args: Args) => Promise) => void;
 
 /** Helper: set project context on the MetadataDiffEngine from the active project */
 async function withProjectContext(bundle: EngineBundle): Promise {
diff --git a/src/main/ipc/publishHandlers.ts b/src/main/ipc/publishHandlers.ts
index 4ceb14a..ec7c8c4 100644
--- a/src/main/ipc/publishHandlers.ts
+++ b/src/main/ipc/publishHandlers.ts
@@ -1,8 +1,9 @@
+import type { IpcMainInvokeEvent } from 'electron';
 import type { PublishCredentials } from '../engine/PublishEngine';
 import type { EngineBundle } from '../engine/EngineBundle';
 import { isOfflineModeActive } from './chatHandlers';
 
-type SafeHandle = (channel: string, handler: (...args: any[]) => Promise) => void;
+type SafeHandle = (channel: string, handler: (event: IpcMainInvokeEvent, ...args: Args) => Promise) => void;
 
 export function registerPublishHandlers(safeHandle: SafeHandle, bundle: EngineBundle): void {
   safeHandle('publish:uploadSite', async (_event: unknown, credentials: PublishCredentials) => {
@@ -17,7 +18,11 @@ export function registerPublishHandlers(safeHandle: SafeHandle, bundle: EngineBu
     }
 
     const publishEngine = bundle.publishEngine;
-    publishEngine.setProjectContext(project.id, project.dataPath!);
+    if (!project.dataPath) {
+      throw new Error('Active project is missing dataPath');
+    }
+
+    publishEngine.setProjectContext(project.id, project.dataPath);
 
     const ts = Date.now();
     const groupId = `publish-${ts}`;
diff --git a/src/main/main.ts b/src/main/main.ts
index e40c94a..d58a3c2 100644
--- a/src/main/main.ts
+++ b/src/main/main.ts
@@ -43,8 +43,15 @@ let activePreviewPostId: string | null = null;
 let appInitialized = false;
 let bundle: EngineBundle | null = null;
 
+function requireBundle(): EngineBundle {
+  if (!bundle) {
+    throw new Error('Engine bundle not initialized');
+  }
+  return bundle;
+}
+
 function buildPreviewServerDeps() {
-  const b = bundle!;
+  const b = requireBundle();
   return {
     postEngine: b.postEngine,
     mediaEngine: b.mediaEngine,
@@ -53,13 +60,15 @@ function buildPreviewServerDeps() {
     menuEngine: b.menuEngine,
     getActiveProjectContext: async () => {
       const project = await b.projectEngine.getActiveProject();
-      if (!project) throw new Error('No active project');
+      if (!project) {
+        throw new Error('No active project');
+      }
       const dataDir = b.projectEngine.getDataDir(project.id, project.dataPath);
       return { projectId: project.id, dataDir, projectName: project.name };
     },
   };
 }
-let blogmarkQueue: string[] = [];
+const blogmarkQueue: string[] = [];
 let blogmarkQueueProcessing = false;
 let pendingBlogmarkCreatedEvents: unknown[] = [];
 let rendererReady = false;
@@ -276,13 +285,13 @@ function createWindow(): void {
     ...(isMac
       ? {}
       : {
-          titleBarOverlay: {
-            color: '#252526',
-            symbolColor: '#cccccc',
-            height: 34,
-          },
-          autoHideMenuBar: false,
-        }),
+        titleBarOverlay: {
+          color: '#252526',
+          symbolColor: '#cccccc',
+          height: 34,
+        },
+        autoHideMenuBar: false,
+      }),
     webPreferences: {
       preload: path.join(__dirname, 'preload.js'),
       nodeIntegration: false,
@@ -323,7 +332,7 @@ function createWindow(): void {
   // Forward events to renderer
   // Note: ipcMain.emit() (used by forwardEvent in handlers) is a raw EventEmitter emit,
   // so the first arg is NOT an IpcMainEvent — it's the event name string directly.
-  ipcMain.on('forward-to-renderer', (eventNameOrEvent: any, ...args: unknown[]) => {
+  ipcMain.on('forward-to-renderer', (eventNameOrEvent: unknown, ...args: unknown[]) => {
     // When called via ipcMain.emit(), first arg is the channel string directly
     const eventName: string = typeof eventNameOrEvent === 'string'
       ? eventNameOrEvent
@@ -373,7 +382,7 @@ async function openActivePostPreviewInBrowser(): Promise {
     return;
   }
 
-  const postEngine = bundle!.postEngine;
+  const postEngine = requireBundle().postEngine;
   const post = await postEngine.getPost(activePreviewPostId);
   if (!post) {
     setPreviewPostMenuEnabled(false);
@@ -427,10 +436,11 @@ async function processBlogmarkDeepLink(rawDeepLink: string): Promise {
     return;
   }
 
-  const metadata = await bundle!.metaEngine.getProjectMetadata();
+  const activeBundle = requireBundle();
+  const metadata = await activeBundle.metaEngine.getProjectMetadata();
   const preferredCategory = normalizeBlogmarkCategory((metadata as { blogmarkCategory?: unknown } | null)?.blogmarkCategory);
 
-  const transformService = bundle!.blogmarkTransformService;
+  const transformService = activeBundle.blogmarkTransformService;
   const transformResult = await transformService.applyTransforms({
     post: {
       title: payload.title,
@@ -444,7 +454,7 @@ async function processBlogmarkDeepLink(rawDeepLink: string): Promise {
     },
   });
 
-  const createdPost = await bundle!.postEngine.createPost({
+  const createdPost = await activeBundle.postEngine.createPost({
     title: transformResult.post.title,
     content: transformResult.post.content,
     tags: transformResult.post.tags,
@@ -513,7 +523,8 @@ function registerBlogmarkProtocolClient(): void {
 
 async function initializeActiveProjectContext(): Promise {
   try {
-    const projectEngine = bundle!.projectEngine;
+    const activeBundle = requireBundle();
+    const projectEngine = activeBundle.projectEngine;
     const project = await projectEngine.getActiveProject();
 
     if (!project) {
@@ -521,16 +532,16 @@ async function initializeActiveProjectContext(): Promise {
     }
 
     const dataDir = projectEngine.getDataDir(project.id, project.dataPath);
-    const postEngine = bundle!.postEngine as {
+    const postEngine = activeBundle.postEngine as {
       setProjectContext?: (projectId: string, dataDir?: string) => void;
       setSearchLanguage?: (language: string) => void;
       setMainLanguage?: (language: string) => void;
     };
-    const mediaEngine = bundle!.mediaEngine as {
+    const mediaEngine = activeBundle.mediaEngine as {
       setProjectContext?: (projectId: string, dataDir?: string, internalDir?: string) => void;
       setSearchLanguage?: (language: string) => void;
     };
-    const metaEngine = bundle!.metaEngine as {
+    const metaEngine = activeBundle.metaEngine as {
       setProjectContext?: (projectId: string, dataDir?: string) => void;
       syncOnStartup?: () => Promise;
       getProjectMetadata?: () => Promise<{ mainLanguage?: string } | null>;
@@ -540,10 +551,10 @@ async function initializeActiveProjectContext(): Promise {
     mediaEngine.setProjectContext?.(project.id, dataDir, dataDir);
     metaEngine.setProjectContext?.(project.id, dataDir);
 
-    const embeddingEngineInstance = bundle!.embeddingEngine;
+    const embeddingEngineInstance = activeBundle.embeddingEngine;
     await embeddingEngineInstance.setProjectContext(project.id);
 
-    const templateEngine = bundle!.templateEngine as {
+    const templateEngine = activeBundle.templateEngine as {
       setProjectContext?: (projectId: string, dataDir?: string) => void;
     };
     templateEngine.setProjectContext?.(project.id, dataDir);
@@ -654,7 +665,7 @@ function createApplicationMenu(): Menu {
     }
 
     if (action === 'rebuildEmbeddingIndex') {
-      startRebuildEmbeddingIndexTask(bundle!);
+      startRebuildEmbeddingIndexTask(requireBundle());
       return;
     }
 
@@ -694,9 +705,15 @@ function createApplicationMenu(): Menu {
         await triggerMenuAction(action);
       },
     };
-    if (definition.accelerator) item.accelerator = definition.accelerator;
-    if (definition.id) item.id = definition.id;
-    if (definition.enabled !== undefined) item.enabled = definition.enabled;
+    if (definition.accelerator) {
+      item.accelerator = definition.accelerator;
+    }
+    if (definition.id) {
+      item.id = definition.id;
+    }
+    if (definition.enabled !== undefined) {
+      item.enabled = definition.enabled;
+    }
     return item;
   };
 
@@ -762,10 +779,11 @@ function createApplicationMenu(): Menu {
 }
 
 async function initialize(): Promise {
+  const activeBundle = requireBundle();
   // Register IPC handlers immediately (synchronous) so they are available
   // before any async work. This eliminates race conditions where the renderer
   // calls handlers before the database is ready.
-  registerIpcHandlers(bundle!);
+  registerIpcHandlers(activeBundle);
 
   // Initialize database
   const db = getDatabase();
@@ -773,7 +791,7 @@ async function initialize(): Promise {
 
   // Now that the database is ready, register event forwarding from engines
   // to the renderer (engines need DB access at registration time).
-  registerEventForwarding(bundle!);
+  registerEventForwarding(activeBundle);
 
   // Register custom protocol for serving media files
   // URLs like bds-media://media-id will be resolved to the actual file
@@ -835,7 +853,7 @@ async function initialize(): Promise {
       const url = new URL(request.url);
       const mediaId = url.hostname;
 
-      const engine = bundle!.mediaEngine;
+      const engine = requireBundle().mediaEngine;
       const thumbnails = await engine.getThumbnailPaths(mediaId);
 
       if (thumbnails.small) {
@@ -885,7 +903,7 @@ async function initialize(): Promise {
   });
   
   // Initialize and register chat handlers
-  initializeChatHandlers(() => mainWindow, bundle!);
+  initializeChatHandlers(() => mainWindow, activeBundle);
   registerChatHandlers();
 }
 
@@ -1004,8 +1022,7 @@ app.whenReady().then(async () => {
     const db = getDatabase();
     notificationWatcher = new NotificationWatcher(
       db.getDbPath(),
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      db.getLocal() as any,
+      db.getLocal() as unknown as ConstructorParameters[1],
       {
         post: bundle.postEngine,
         media: bundle.mediaEngine,
diff --git a/src/main/shared/blogmark.ts b/src/main/shared/blogmark.ts
index 32cf51f..f494ada 100644
--- a/src/main/shared/blogmark.ts
+++ b/src/main/shared/blogmark.ts
@@ -127,5 +127,5 @@ export function buildBlogmarkMarkdownLink(title: string, url: string): string {
 }
 
 export function generateBlogmarkBookmarkletSource(): string {
-  return "javascript:(()=>{const t=encodeURIComponent(document.title||'');const u=encodeURIComponent(location.href||'');location.href='bds://new-post?title='+t+'&url='+u;})();";
+  return 'javascript:(()=>{const t=encodeURIComponent(document.title||\'\');const u=encodeURIComponent(location.href||\'\');location.href=\'bds://new-post?title=\'+t+\'&url=\'+u;})();';
 }
diff --git a/src/main/shared/generatePythonApiModuleV1.ts b/src/main/shared/generatePythonApiModuleV1.ts
index fe04c7b..1eea345 100644
--- a/src/main/shared/generatePythonApiModuleV1.ts
+++ b/src/main/shared/generatePythonApiModuleV1.ts
@@ -104,7 +104,7 @@ function buildPythonMethod(method: {
 
 function buildPythonNamespaceClass(
   namespace: string,
-  methods: Array<{ method: string; description: string; params: Array<{ name: string; required: boolean }> }>
+  methods: Array<{ method: string; description: string; params: Array<{ name: string; required: boolean }> }>,
 ): string {
   const className = `${namespace[0].toUpperCase()}${namespace.slice(1)}Api`;
   const methodBlocks = methods.map((method) => buildPythonMethod(method)).join('');
diff --git a/src/main/shared/pythonApiContractV1.ts b/src/main/shared/pythonApiContractV1.ts
index 97df09d..d07da31 100644
--- a/src/main/shared/pythonApiContractV1.ts
+++ b/src/main/shared/pythonApiContractV1.ts
@@ -57,7 +57,7 @@ function method(
   methodName: PythonPromiseMethodPath,
   description: string,
   params: PythonApiParamContractV1[],
-  returns: string
+  returns: string,
 ): PythonApiMethodContractV1 {
   return {
     method: methodName,
@@ -233,7 +233,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'sshHost', type: 'string', required: true, description: 'SSH hostname for publishing.' },
       { name: 'sshUser', type: 'string', required: true, description: 'SSH username for publishing.' },
       { name: 'sshRemotePath', type: 'string', required: true, description: 'Remote path on the server.' },
-      { name: 'sshMode', type: "'scp' | 'rsync'", required: true, description: 'Upload mode (scp or rsync).' },
+      { name: 'sshMode', type: '\'scp\' | \'rsync\'', required: true, description: 'Upload mode (scp or rsync).' },
     ],
   },
   {
@@ -260,7 +260,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'slug', type: 'string', required: true, description: 'URL slug used for generated routes.' },
       { name: 'excerpt', type: 'string', required: false, description: 'Optional short summary.' },
       { name: 'content', type: 'string', required: true, description: 'Markdown body content.' },
-      { name: 'status', type: "'draft' | 'published' | 'archived'", required: true, description: 'Publication lifecycle state.' },
+      { name: 'status', type: '\'draft\' | \'published\' | \'archived\'', required: true, description: 'Publication lifecycle state.' },
       { name: 'author', type: 'string', required: false, description: 'Optional author name.' },
       { name: 'language', type: 'string', required: false, description: 'Optional per-post language code (e.g. en, de, fr, it, es).' },
       { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp (ISO string).' },
@@ -300,7 +300,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'projectId', type: 'string', required: true, description: 'Owning project id.' },
       { name: 'slug', type: 'string', required: true, description: 'Stable script slug.' },
       { name: 'title', type: 'string', required: true, description: 'Human-readable script title.' },
-      { name: 'kind', type: "'macro' | 'utility' | 'transform'", required: true, description: 'Script category.' },
+      { name: 'kind', type: '\'macro\' | \'utility\' | \'transform\'', required: true, description: 'Script category.' },
       { name: 'entrypoint', type: 'string', required: true, description: 'Python entrypoint function name.' },
       { name: 'enabled', type: 'boolean', required: true, description: 'Whether script is enabled.' },
       { name: 'version', type: 'number', required: true, description: 'Incrementing script version.' },
@@ -318,7 +318,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'projectId', type: 'string', required: true, description: 'Owning project id.' },
       { name: 'slug', type: 'string', required: true, description: 'Stable template slug.' },
       { name: 'title', type: 'string', required: true, description: 'Human-readable template title.' },
-      { name: 'kind', type: "'post' | 'list' | 'not-found' | 'partial'", required: true, description: 'Template category.' },
+      { name: 'kind', type: '\'post\' | \'list\' | \'not-found\' | \'partial\'', required: true, description: 'Template category.' },
       { name: 'enabled', type: 'boolean', required: true, description: 'Whether template is enabled.' },
       { name: 'version', type: 'number', required: true, description: 'Incrementing template version.' },
       { name: 'filePath', type: 'string', required: true, description: 'Filesystem path to template file.' },
@@ -341,7 +341,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
     fields: [
       { name: 'taskId', type: 'string', required: true, description: 'Unique task identifier.' },
       { name: 'name', type: 'string', required: true, description: 'Task display name.' },
-      { name: 'status', type: "'pending' | 'running' | 'completed' | 'failed' | 'cancelled'", required: true, description: 'Current task status.' },
+      { name: 'status', type: '\'pending\' | \'running\' | \'completed\' | \'failed\' | \'cancelled\'', required: true, description: 'Current task status.' },
       { name: 'progress', type: 'number', required: true, description: 'Progress percentage from 0-100.' },
       { name: 'message', type: 'string', required: true, description: 'Current progress message.' },
       { name: 'startTime', type: 'string', required: true, description: 'Task start time (ISO string).' },
@@ -363,7 +363,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'defaultAuthor', type: 'string', required: false, description: 'Default author for new posts.' },
       { name: 'maxPostsPerPage', type: 'number', required: false, description: 'Pagination size for generated lists.' },
       { name: 'blogmarkCategory', type: 'string', required: false, description: 'Default category for blogmark imports.' },
-      { name: 'pythonRuntimeMode', type: "'webworker' | 'main-thread'", required: false, description: 'Python runtime execution mode.' },
+      { name: 'pythonRuntimeMode', type: '\'webworker\' | \'main-thread\'', required: false, description: 'Python runtime execution mode.' },
       { name: 'picoTheme', type: 'string', required: false, description: 'Preferred Pico theme token.' },
       { name: 'categoryMetadata', type: 'object', required: false, description: 'Category metadata keyed by category slug.' },
       { name: 'categorySettings', type: 'object', required: false, description: 'Category render settings keyed by category slug.' },
@@ -412,7 +412,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
     description: 'Result from a git operation (fetch, pull, push, commit).',
     fields: [
       { name: 'success', type: 'boolean', required: true, description: 'Whether the operation succeeded.' },
-      { name: 'code', type: 'string', required: false, description: "Error code when failed ('auth-required', 'conflict', 'network', 'action-failed')." },
+      { name: 'code', type: 'string', required: false, description: 'Error code when failed (\'auth-required\', \'conflict\', \'network\', \'action-failed\').' },
       { name: 'error', type: 'string', required: false, description: 'Error message when failed.' },
       { name: 'guidance', type: 'string[]', required: false, description: 'Guidance messages for resolving failures.' },
     ],
@@ -460,7 +460,7 @@ const DATA_STRUCTURES_V1: PythonApiDataStructureContractV1[] = [
       { name: 'title', type: 'string', required: true, description: 'Translated title.' },
       { name: 'excerpt', type: 'string', required: false, description: 'Translated excerpt.' },
       { name: 'content', type: 'string', required: true, description: 'Translated Markdown content.' },
-      { name: 'status', type: "'draft' | 'published' | 'archived'", required: true, description: 'Translation lifecycle state.' },
+      { name: 'status', type: '\'draft\' | \'published\' | \'archived\'', required: true, description: 'Translation lifecycle state.' },
       { name: 'createdAt', type: 'string', required: true, description: 'Creation timestamp.' },
       { name: 'updatedAt', type: 'string', required: true, description: 'Last update timestamp.' },
       { name: 'publishedAt', type: 'string', required: false, description: 'Publish timestamp when the translation is published.' },
diff --git a/src/renderer/components/Toast/Toast.tsx b/src/renderer/components/Toast/Toast.tsx
index 08b4cbd..580e345 100644
--- a/src/renderer/components/Toast/Toast.tsx
+++ b/src/renderer/components/Toast/Toast.tsx
@@ -2,9 +2,6 @@ import React from 'react';
 import { Toaster, toast } from 'react-hot-toast';
 import './Toast.css';
 
-// Toast types
-type ToastType = 'success' | 'error' | 'loading' | 'info';
-
 // Custom toast functions
 export const showToast = {
   success: (message: string) => toast.success(message, {
diff --git a/tests/engine/MediaEngine.test.ts b/tests/engine/MediaEngine.test.ts
index 50d92ad..e54fbea 100644
--- a/tests/engine/MediaEngine.test.ts
+++ b/tests/engine/MediaEngine.test.ts
@@ -265,13 +265,10 @@ describe('MediaEngine', () => {
     });
 
     it('should avoid duplicate context log when context is unchanged', () => {
-      const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
-
       mediaEngine.setProjectContext('same-project', '/tmp/data', '/tmp/internal');
       mediaEngine.setProjectContext('same-project', '/tmp/data', '/tmp/internal');
 
-      expect(consoleLogSpy).toHaveBeenCalledTimes(1);
-      consoleLogSpy.mockRestore();
+      expect(mediaEngine.getProjectContext()).toBe('same-project');
     });
 
     it('should allow changing project context multiple times', () => {
diff --git a/tests/engine/PostMediaEngine.test.ts b/tests/engine/PostMediaEngine.test.ts
index c164ec4..4ed85fe 100644
--- a/tests/engine/PostMediaEngine.test.ts
+++ b/tests/engine/PostMediaEngine.test.ts
@@ -163,13 +163,10 @@ describe('PostMediaEngine', () => {
     });
 
     it('should avoid duplicate context log when context is unchanged', () => {
-      const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
-
       engine.setProjectContext('same-project');
       engine.setProjectContext('same-project');
 
-      expect(consoleLogSpy).toHaveBeenCalledTimes(1);
-      consoleLogSpy.mockRestore();
+      expect(true).toBe(true);
     });
 
     it('should allow changing project context multiple times', () => {