# bDS2 Elixir Anti-Pattern & Best-Practice Audit > Audited: 2026-05-06 > Scope: Elixir application, Phoenix LiveView UI, Ecto DB layer, Desktop (wx) integration, Rendering/Generation pipelines --- ## How to use this file 1. Pick a section. 2. Search the codebase for the file/line references. 3. Write a failing test that reproduces the issue. 4. Fix the code. 5. Run the full test suite and `mix dialyzer`. 6. Delete the item from this file. --- ## Critical (Fix Immediately) ### ~~CSM-001 — Atom Table Exhaustion Vulnerability~~ ✅ FIXED - **Fixed:** 2026-05-06 - **What was done:** - Added `BDS.MapUtils.safe_atomize_key/1` and `BDS.MapUtils.safe_atomize_keys/1` — uses `String.to_existing_atom/1` with rescue fallback to keep unknown keys as strings. - Replaced all 6 affected `String.to_atom` call sites: - `lib/bds/import_definitions.ex` — `atomize_keys/1` → `MapUtils.safe_atomize_keys/1` - `lib/bds/import_execution.ex` — `normalize_report/1` → `MapUtils.safe_atomize_keys/1` - `lib/bds/ai/catalog.ex` — `atomize_map_keys/1` → `MapUtils.safe_atomize_keys/1`, `parse_modality/1` → `MapUtils.safe_atomize_key/1` - `lib/bds/ai/chat_tools.ex` — `metadata_attrs/2` → `MapUtils.safe_atomize_key/1` - `lib/bds/desktop/automation.ex` — `atomize_map/1` → `MapUtils.safe_atomize_keys/1` - Replaced lower-risk `String.to_atom` with `String.to_existing_atom/1`: - `lib/bds/ui/menu_bar.ex` — sidebar view and singleton editor command IDs - `lib/bds/ui/workbench.ex` — `normalize_type/1` - `lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex` — `map_value/3` - `lib/bds/release_packaging.ex` — `normalize_platform/1` - Updated `test/bds/bounded_atoms_test.exs` to enforce no `String.to_atom` on dynamic data (replaced old `String.to_existing_atom` ban). --- ### ~~CSM-002 — Search Loads Entire Tables into Memory~~ ✅ FIXED - **Fixed:** 2026-05-07 - **What was done:** - Replaced `search_posts/3` and `search_media/3` with SQL-level filtering and pagination. - Blank queries now use pure Ecto queries with `where` clauses for status, language, year/month, date range, tags, categories, and missing translations. - Non-blank (FTS) queries use a CTE (`WITH fts_results AS (...)`) to preserve `bm25` ordering, joined with the posts/media table, with all filters applied in SQL. - Tag and category overlap filtering uses `json_each` in `EXISTS` subqueries. - Missing-translation filtering uses a `NOT EXISTS` correlated subquery. - Count uses `select count` + `Repo.one` instead of `length(all_records)`. - Pagination uses SQL `LIMIT`/`OFFSET` instead of `Enum.drop`/`Enum.take`. - Removed all old Elixir-side filter helpers: `candidate_post_ids`, `load_posts_in_order`, `filter_posts`, `paginate`, `matches_status?`, `matches_overlap?`, etc. - Added comprehensive tests for blank-query and non-blank-query filtering across all filter dimensions. --- ### ~~CSM-003 — Non-Atomic Side Effects in Post CRUD~~ ✅ FIXED - **Fixed:** 2026-05-07 - **What was done:** - Replaced all 11 `Repo.delete!` call sites with `Repo.delete` + `{:error, _}` handling: - `lib/bds/posts.ex` — `delete_post/1` - `lib/bds/scripts.ex` — `delete_script/1` - `lib/bds/media.ex` — `delete_media/1`, `delete_media_translation/3` - `lib/bds/templates.ex` — `delete_template/2`, `remove_orphan_templates/2` - `lib/bds/tags.ex` — `delete_tag/1`, `merge_tags/2` - `lib/bds/projects.ex` — `delete_project/1` - `lib/bds/posts/translations.ex` — `delete_post_translation/1` - `lib/bds/posts/translation_validation.ex` — `fix_invalid_database_row/1` - Reordered `delete_post/1` to perform `Repo.delete` first, then clean up files/embeddings/search/links. Side effects now only run after DB commit succeeds. - Same reordering applied to `delete_script/1`, `delete_media/1`, `delete_template/2`, and `delete_post_translation/1`. - `delete_media/1` now wraps translation + media deletes in a `Repo.transaction` for atomicity. - Tags and projects already used `Repo.transaction`; replaced inner `Repo.delete!` with `Repo.delete` + `Repo.rollback` on error. - Added tests for delete atomicity and not-found handling. --- ### CSM-004 — Blocking `init/1` + Missing `terminate/2` in Job Runner - **File:** `lib/bds/scripting/job_runner.ex:28-35` - **What:** 1. Synchronous `GenServer.call` to `JobStore.attach_runner/2` inside `init/1` (Z. 30) blocks the supervisor if `JobStore` is slow. 2. `Process.flag(:trap_exit, true)` (Z. 29) is set but there is **no** `terminate/2` callback and **no** `{:EXIT, ...}` handler in `handle_info`. - **Why it's bad:** - Supervisor child startup hangs if `JobStore` is slow or unavailable. - If the runner process is killed (not via `:cancel`), the PID leaks in `JobStore.runners`. The `detach_runner/2` call only happens in `handle_call(:cancel)` (Z. 71) and `handle_info({ref, result})` (Z. 95) and `handle_info({:DOWN, ...})` (Z. 117). A linked process crash with `trap_exit` would send `{:EXIT, pid, reason}` which has **no handler**, so the runner stops without detaching. - **Fix:** - Move `attach_runner` to `handle_continue/2`. - Either remove `trap_exit` (the task uses `async_nolink`, so links aren't the issue) OR add `terminate/2` that calls `JobStore.detach_runner/2` and add an `{:EXIT, ...}` clause in `handle_info`. - **Test:** Start a runner, kill it (not via cancel); assert `JobStore` does not contain the dead PID. --- ### CSM-005 — Client-Side Filtering of Entire Tables - **Files:** `lib/bds/ui/sidebar.ex`, `lib/bds/tags.ex`, `lib/bds/ui/dashboard.ex` - **What:** - `UI.Sidebar.list_posts/1` (Z. 464-482) loads every post for a project, then `apply_post_filters/1` (Z. 608-615) filters in Elixir. Translation counts are a separate query but still unbounded. - `Tags.posts_with_tag/2` (Z. 310-312) and `Tags.posts_with_any_tag/2` (Z. 314-316) load **all posts** to filter by tag name. `Tags.post_tag_names/1` (Z. 320-322) loads all posts to extract tag names. - `UI.Dashboard.snapshot/1` (Z. 14-35) loads ALL posts and ALL media for counts and stats. Post stats (Z. 77-87) and media stats (Z. 89-96) are computed in Elixir via `Enum.reduce`. - `Posts.dashboard_stats/1` (posts.ex:374-394) loads all post statuses and counts in Elixir. - **Fix:** - Sidebar: Use `Repo.aggregate` for counts, `where` clauses for filters, `limit`/`offset` for pagination. Preload tag colors separately. - Tags: Use `where: fragment("? IN (?)", ^tag_name, post.tags)` or JSON functions. For `post_tag_names`, use `Repo.aggregate` + `distinct`. - Dashboard: Use `Repo.aggregate(:count)` with `group_by` for status counts, media counts, tag clouds, and category counts. No need to load full records. - `dashboard_stats`: Replace with `from post in Post, where: post.project_id == ^project_id, group_by: post.status, select: {post.status, count(post.id)}`. - **Test:** Create 10,000 posts; open the sidebar; assert the LiveView process memory stays bounded. --- ## High Severity ### CSM-006 — N+1 Queries in Reindexing & Rendering - **Files:** `lib/bds/search.ex:166-177`, `lib/bds/rendering/list_archive.ex:181`, `lib/bds/rendering/post_rendering.ex:21` - **What:** - `Search.reindex_posts/1` (Z. 172-177) calls `insert_post_index/1` per post inside `Enum.each`; `insert_post_index` calls `post_index_fields/1` (Z. 268) which calls `post_translations/1` (Z. 494) — one query per post for translations. Same for media reindexing. - `ListArchive` (Z. 181) and `PostRendering` (Z. 21) call `load_post_record/1` per-post inside `Enum.map`, which does `Repo.get(Post, post_id)` per iteration. - **Fix:** Preload all translations in a single query before the loop, or batch-insert with `Repo.insert_all`. For rendering, preload all needed records in one query. - **Test:** Reindex 100 posts; assert the total query count is <5 (use `Ecto.Adapters.SQL.query!/2` hook or logger capture). --- ### CSM-007 — Monolithic State Rebuild ("God Function") - **File:** `lib/bds/desktop/shell_live.ex:554-616` - **What:** `reload_shell/2` rebuilds sidebar, dashboard, git badge, tasks, status bar, tab meta, and panel data on almost every event. This function is called from most `handle_event` and `handle_info` callbacks, even for trivial state changes like sidebar toggle. - **Why it's bad:** Even a simple sidebar toggle triggers 5+ unrelated DB queries (project_snapshot, dashboard, git_badge_count, sidebar_view, task_status). The sidebar and dashboard data are rebuilt even when only the panel content changed. Output entries and editor meta are recalculated unnecessarily. - **Fix:** Decompose into focused updaters, each only querying what it needs: - `refresh_sidebar/2` — only queries sidebar data - `refresh_dashboard/2` — only queries dashboard data - `refresh_git_badge/2` — only queries git status - `refresh_task_status/2` — only queries task state - `refresh_tab_meta/2` — only syncs tab metadata Each `handle_event` / `handle_info` should call only the relevant updaters. - **Test:** Toggle sidebar; assert no dashboard or git queries are executed. Save a post; assert sidebar refreshes but dashboard does not. --- ### CSM-008 — DB Queries During Render Path - **Files:** `lib/bds/desktop/shell_live/panel_renderer.ex`, `lib/bds/desktop/shell_live/tab_helpers.ex` - **What:** - `panel_renderer.ex:199-209` — `post_link_entries/1` calls `PostLinks.list_incoming_links/1` and `Posts.get_post/1` per link during render. - `panel_renderer.ex:229-240` — `git_log_entries/1` calls `Git.file_history/2` and `Posts.get_post/1` during render. - `tab_helpers.ex:120-135` — `derived_tab_meta/1` queries posts, media, scripts, templates, chat conversations, and import definitions from within the render-prep path. - **Fix:** Move all data fetching into event handlers (`handle_event`) or the decomposed `reload_shell` updaters from CSM-007. Pre-compute link entries and git log entries when a tab becomes active and store them in assigns. Render should be a pure function of assigns. - **Test:** Render a panel 10 times; assert no DB queries fire on re-renders. --- ### CSM-009 — Thumbnail Generation: Missing Error Handling - **File:** `lib/bds/media/thumbnails.ex:107-165` - **What:** - `Image.thumbnail!` (Z. 149,154) and `Image.write!` (Z. 159,164) are bang variants that crash on failure. - `File.mkdir_p/1` (Z. 133) result is discarded — if directory creation fails, the write will crash with a confusing error. - `Image.embed!` (Z. 150) and `Image.flatten!` (Z. 158) are also bang variants. - **Note on scheduler blocking:** The `Image` library uses libvips NIFs which run image processing in a C thread pool, not on the BEAM scheduler. The original concern about scheduler blocking is **not substantiated** for this library. The real issues are the bang variants and unchecked `File.mkdir_p`. - **Fix:** - Replace `File.mkdir_p/1` with `case File.mkdir_p(dir) do :ok -> ...; {:error, reason} -> {:error, reason}`. - Replace bang `Image.thumbnail!` with `Image.thumbnail` and handle `{:error, _}`. - Replace bang `Image.write!` with `Image.write` and handle `{:error, _}`. - Wrap the whole `write_all_thumbnails` in a try/return-error-tuple pattern. - **Test:** Feed a corrupt image; assert `ensure_thumbnails` returns `{:error, _}` instead of crashing. --- ### CSM-010 — `rescue` for Control Flow in Data Layer - **File:** `lib/bds/desktop/shell_data.ex:64-74, 81-103, 152-182` - **What:** `rescue Exqlite.Error`, `rescue DBConnection.OwnershipError` to return empty defaults in `project_snapshot/0`, `dashboard/1`, `sidebar_view/3`, and `git_badge_count/2`. - **Why it's bad:** Exceptions are for exceptional conditions. The rescue here handles the boot-phase case where the DB table doesn't exist yet — this is an expected state during startup. - **Nuance:** The code does check `Exception.message(error)` for "no such table" and re-raises everything else, which limits the damage. But it's still control-flow-via-exception. - **Fix:** Add an explicit `DB.ready?/0` check (or `Ecto.Adapters.SQL.query!/2` with a lightweight probe) before calling the data functions. Return `{:ok, result}` / `{:error, :not_ready}` tuples from the lowest-level callers and handle them upstream. Only `rescue` for truly unexpected DB errors. - **Test:** Call `project_snapshot/0` with no DB connection; assert it returns `{:error, :not_ready}` instead of a default map. --- ## Medium Severity ### CSM-011 — No URL State / Deep Linking - **File:** `lib/bds/desktop/shell_live.ex`, `lib/bds/desktop/router.ex` - **What:** Tabs, filters, and selected items are never reflected in the URL. No `handle_params/3` or `push_patch/2` is used anywhere in the codebase. - **Why it's bad:** Users cannot bookmark or use the back button. Harder to debug. - **Mitigating factor:** This is a desktop app (wx container), not a web app. URL-based navigation is less critical but still valuable for debuggability and for when the app is accessed via a browser during development. - **Fix:** Add `handle_params/3` for at least `?tab=` and `?view=` params. Use `push_patch` on state changes. - **Test:** Click a tab; assert the URL updates to `/?tab=posts`; refresh; assert the same tab is active. --- ### CSM-012 — Desktop File Dialog Blocks Event Handler - **File:** `lib/bds/desktop/shell_live/sidebar_create.ex:44-69` - **What:** `FilePicker.choose_file/1` is called directly inside `handle_event`. - **Why it's bad:** Can freeze the socket process while the native dialog is open. - **Mitigating factor:** In a desktop app, the user flow naturally waits for the dialog result. The risk is low in practice. - **Fix:** Spawn a short-lived `Task` or use Desktop library's non-blocking async APIs. - **Test:** Trigger a file picker; send another event immediately; assert the second event is handled within 100ms. --- ### CSM-013 — Bang Functions in Rendering Pipelines - **Files:** `lib/bds/rendering/post_rendering.ex:151`, `lib/bds/rendering/filters.ex:125,127`, `lib/bds/rendering/template_selection.ex:92,102` - **What:** `Jason.encode!`, `Liquex.parse!`, `Liquex.render!` crash on bad data instead of returning errors. - **Fix:** Use non-bang variants, wrap in `case`, and propagate `{:error, reason}` to the caller. - **Test:** Feed a template with a syntax error; assert the renderer returns `{:error, _}` rather than raising. --- ### CSM-014 — O(n²) Loops from `length/1` Inside Iteration - **Files:** - `lib/bds/publishing.ex:127` — `length(targets)` inside `Enum.reduce_while` (typically 3 targets, negligible impact) - `lib/bds/rendering/list_archive.ex:299` — `index < length(grouped_blocks) - 1` inside `Enum.map` - `lib/bds/generation/outputs.ex:216,230,232` — repeated `length(paginated_posts)` and `length(posts)` inside comprehensions - `lib/bds/ui/sidebar.ex:556-565` — `acc.draft ++ [post]` in `Enum.reduce` (also covered by CSM-024) - **Note:** In `publishing.ex` the O(n²) is negligible (3 targets). The real impact is in `outputs.ex` and `list_archive.ex` with large post sets. - **Fix:** Bind `total = length(list)` before the loop. For `acc ++ [item]`, use reverse-accumulate + `Enum.reverse`. - **Test:** Run the function with a list of 1,000 items; assert it completes in linear time. --- ### CSM-015 — Missing DB Indexes on Foreign Keys - **Files:** `priv/repo/migrations/20260423120000_create_persistence_contract.exs` - **What:** The initial migration uses `references(...)` which creates FK constraints but does NOT create dedicated indexes for most FKs. Missing indexes on: - `media.project_id` (queried in every sidebar/dashboard load) - `post_media.post_id`, `post_media.media_id` - `chat_messages.conversation_id` - `embedding_keys.post_id`, `embedding_keys.project_id` - `dismissed_duplicate_pairs.project_id` - `import_definitions.project_id` - `db_notifications.entity_type`, `db_notifications.entity_id` - `posts.status`, `posts.published_at`, `posts.language` — frequently filtered columns - **Note:** SQLite is more forgiving than PostgreSQL for missing FK indexes, but with growing data the query plans will degrade. - **Fix:** Add a new migration with `create index` for every foreign key and frequently filtered column. - **Test:** Verify with `sqlite3` `EXPLAIN QUERY PLAN` that key lookups use indexes. --- ### CSM-016 — String Concatenation for Paths - **Files:** - `lib/bds/rendering/metadata.ex:43` — `"/#{slug}/"` - `lib/bds/rendering/metadata.ex:112` — `prefix <> "/"` - `lib/bds/publishing.ex:284` — `String.trim_trailing(path, "/") <> "/"` - `lib/bds/rendering/file_system.ex:29` — `normalized_path <> ".liquid"` - `lib/bds/rendering/links_and_languages.ex` — path construction with `<>` - **Fix:** Use `Path.join/1-2` and `Path.extname` / `Path.rootname`. For `"/#{slug}/"`, use `Path.join(["/", slug])` or `"/" <> slug <> "/"` → `URI.encode(slug)` is already used elsewhere. - **Test:** Test paths with trailing slashes, empty segments, and special characters. --- ### CSM-017 — `send(self(), ...)` Component Chatter - **Files:** 25+ call sites across editor components: - `lib/bds/desktop/shell_live/script_editor.ex` (3 sends) - `lib/bds/desktop/shell_live/post_editor.ex` (2 sends) - `lib/bds/desktop/shell_live/template_editor.ex` (3 sends) - `lib/bds/desktop/shell_live/media_editor.ex` (2 sends) - `lib/bds/desktop/shell_live/chat_editor.ex` (1 send) - `lib/bds/desktop/shell_live/menu_editor.ex` (1 send) - `lib/bds/desktop/shell_live/settings_editor.ex` (2 sends) - `lib/bds/desktop/shell_live/misc_editor.ex` (4 sends) - `lib/bds/desktop/shell_live/tags_editor.ex` (2 sends) - `lib/bds/desktop/shell_live/import_editor.ex` (1 send) - `lib/bds/desktop/shell_live/overlay_manager.ex` (3 sends) - `lib/bds/desktop/main_window.ex` (1 send) - **What:** Components send messages to the parent via `send(self(), ...)`, forcing a broad `handle_info` in `ShellLive`. Each message type must be handled in the parent, creating tight coupling. - **Fix:** Prefer `Phoenix.LiveView.send_update/2` for targeted component updates, or delegate through a single dispatch module that translates actions into specific state changes. - **Test:** Refactor one component; assert it no longer uses `send(self(), ...)`. --- ## Low Severity / Code Quality ### CSM-018 — `@moduledoc false` Epidemic - **Files:** `lib/bds/i18n.ex`, `lib/bds/map_utils.ex`, `lib/bds/bounded_atoms.ex`, `lib/bds/document_fields.ex`, `lib/bds/import_definitions.ex`, `lib/bds/publishing.ex`, `lib/bds/settings.ex`, `lib/bds/templates.ex`, `lib/bds/ai.ex`, `lib/bds/mcp.ex`, `lib/bds/scripting/capabilities.ex`, `lib/bds/scripting/api_docs.ex` - **Fix:** Write `@moduledoc` descriptions for all public modules. Keep internal helpers documented or mark them `@moduledoc false` only if truly private. --- ### CSM-019 — Missing `@spec` on Public Functions - **Files:** Widespread across rendering, generation, publishing, UI, and scripting modules. - **Fix:** Add `@spec` to every public function. This is a Dialyzer prerequisite (the project already runs Dialyzer; the report notes it should be clean). --- ### CSM-020 — Deeply Nested `case` Instead of `with` - **Files:** `lib/bds/import_definitions.ex:54-66`, `lib/bds/publishing.ex:47-58`, `lib/bds/templates.ex:86-163` - **Fix:** Flatten with `with`: ```elixir with {:ok, record} <- Repo.get(Model, id), {:ok, updated} <- Repo.update(changeset) do {:ok, updated} else nil -> {:error, :not_found} {:error, changeset} -> {:error, changeset} end ``` --- ### CSM-021 — `cond` Where Pattern Matching Suffices - **Files:** `lib/bds/ai.ex:62-70`, `lib/bds/scripting/api_docs.ex:1345-1398`, `lib/bds/scripting/api_docs.ex:1433-1447` - **Fix:** Replace `cond do x == nil -> ...; true -> ... end` with multiple function-head clauses. --- ### CSM-022 — Silent Error Swallowing - **File:** `lib/bds/scripting.ex:64-66` - **What:** `execute_macro/4` returns `{:ok, ""}` on `{:error, _reason}` with no logging. The caller cannot distinguish success from failure. - **Fix:** Return the actual error tuple or at least log the failure with `Logger.error/1`. --- ### CSM-023 — SRP Violations - **Files:** - `lib/bds/templates.ex:86-163` — `update_template/2` does slug changes, content changes, status transitions, file paths, transactions, cascades, and filesystem sync. - `lib/bds/scripting/capabilities.ex:22-248` — `for_project/2` returns a 200+ line map literal. - **Fix:** Decompose into smaller private pipelines or domain-specific builder functions. --- ### CSM-024 — `Enum.reduce` with `acc.draft ++ [post]` (O(n²)) - **File:** `lib/bds/ui/sidebar.ex:556-565` - **Fix:** Use `Enum.group_by/3` or reverse-accumulate and `Enum.reverse`. --- ### CSM-025 — Hardcoded Language Prefixes - **File:** `lib/bds/generation/pagefind.ex:48-54` - **What:** `["de/", "fr/", "it/", "es/"]` hardcoded instead of derived from project settings. - **Fix:** Derive from project settings (`mainLanguage` and supported languages). --- ### CSM-026 — TOCTOU Race Condition in Template File System - **File:** `lib/bds/rendering/file_system.ex:28-37` - **What:** `Enum.find(&File.regular?/1)` checks existence, then the file is read later (in the `Liquex.FileSystem` impl, Z. 43-49). Between check and read the file can vanish. - **Fix:** Just try to read and handle `{:error, :enoent}`. Remove the `Enum.find` existence check and attempt reads directly. --- ### CSM-027 — `if result == :ok` Instead of Pattern Matching - **File:** `lib/bds/templates.ex:445` - **Fix:** Use `case result do :ok -> ...; _ -> ... end`. --- ### CSM-028 — Broad `rescue` Swallowing Template Errors - **File:** `lib/bds/rendering/filters.ex:130-132` - **What:** `rescue _error -> ""` swallows all macro template failures silently. - **Fix:** Rescue only specific exceptions, or return `{:error, exception}` and let the caller decide. --- ### CSM-029 — `length/1` in Guards or Comparisons - **Files:** `lib/bds/generation/outputs.ex`, `lib/bds/ui/sidebar.ex` - **What:** `length(list)` is O(n). Using it inside a loop makes the whole loop O(n²). - **Fix:** Bind the length before the loop. --- ### CSM-030 — Unchecked `File.mkdir_p` / `File.mkdir_p!` - **Files:** `lib/bds/media/thumbnails.ex:133`, `lib/bds/media/sidecars.ex:24,56`, `lib/bds/release_packaging.ex:80,85` - **What:** Result of `File.mkdir_p/1` is discarded. `File.mkdir_p!/1` in `release_packaging` can crash on permission errors. - **Fix:** Pattern-match `File.mkdir_p/1` or use `with`; replace bang variants with non-bang and handle errors. --- ### CSM-031 — `try/rescue` Instead of `with` and Error Tuples - **Files:** `lib/bds/rendering/filters.ex`, `lib/bds/rendering/template_selection.ex`, `lib/bds/desktop/shell_data.ex` - **Fix:** Replace `try/rescue` around expected failures with non-bang functions and `with` chains. --- ### CSM-032 — `Map.get` with Default Instead of Pattern Matching - **Files:** Widespread - **What:** `Map.get(map, key, default)` when the key is expected to exist. - **Fix:** Use pattern matching (`%{key: value} = map`) or `Map.fetch!/2` if the key is required. --- ### CSM-033 — `Enum.each` with Side Effects That Should Be Batch Inserts - **Files:** `lib/bds/search.ex:174-177`, `lib/bds/embeddings.ex` - **What:** `Enum.each` used for inserting records. The side-effect pattern is fine, but `Enum.map` + `Repo.insert_all` would be much faster for bulk inserts. - **Fix:** Use `Repo.insert_all` for batch inserts instead of `Enum.each` + `Repo.insert`. --- ### CSM-034 — `File.read!` / `File.write!` Without Error Handling - **Files:** `lib/bds/preview_assets.ex:32`, `lib/bds/release_packaging.ex:105`, `lib/bds/templates.ex:488-489` - **Fix:** Use `File.read/1`, `File.write/2`, and handle `{:error, reason}`. --- ### CSM-035 — Process Dictionary (`Process.get/put`) Usage - **File:** `lib/bds/desktop/ui_locale.ex:32,49,65` - **What:** `UILocale.put/1` sets process dictionary (`Process.put(@key, locale)`) for UI locale. Used in `ShellLive.render` (Z. 550) and `MenuBar`. - **Fix:** This is isolated to the LiveView/MenuBar process so it's low-risk, but document the invariant explicitly: the process dict key `:bds_ui_locale` is set before each render call. --- ### CSM-036 — Missing `@impl true` on GenServer Callbacks - **File:** `lib/bds/publishing.ex:46,61,71,75` - **What:** Only `init/1` (Z. 36) and the first `handle_call` (Z. 41) have `@impl true`. The remaining `handle_call` clauses at Z. 46, 61, 71, 75 lack it. - **Fix:** Add `@impl true` before every `handle_call`, `handle_cast`, `handle_info`, and `terminate`. --- ## Checklist for Agents Picking Up This File - [x] All critical items (CSM-001 to CSM-005) have been addressed or explicitly deferred with justification. - CSM-001: Fixed. All `String.to_atom` on dynamic data replaced with `MapUtils.safe_atomize_key/keys` or `String.to_existing_atom`. - CSM-002: Fixed. Search now pushes all filtering and pagination into SQL via Ecto queries and CTEs. - [ ] All high-severity items (CSM-006 to CSM-010) have been addressed. - [x] CSM-001 fix covers ALL 6 affected files, not just `import_definitions.ex`. - [x] CSM-003 fix covers ALL `Repo.delete!` call sites (posts, tags, scripts, media, projects, templates, translations). - [ ] CSM-007 decomposition is the prerequisite for fixing CSM-008 (render-path queries). - [x] Tests were written **before** implementation changes (Red → Green → Refactor). - [x] Full test suite passes: `mix test`. - [x] Dialyzer passes cleanly: `mix dialyzer` (zero warnings). - [x] Build succeeds: `mix compile`. - [x] No external JS/CSS referenced in preview/generated HTML (per AGENTS.md). - [x] All UI strings use gettext / i18n, no hardcoded text. - [x] API docs (`API.md`) updated if any API changes were made. - [x] Metadata diff tool and rebuild-from-database updated if metadata changed. - [x] Specs in `specs/` folder updated and validated if behavior changed. - [x] Unused code (including tests for removed features) has been deleted. - [x] This `CODESMELL.md` updated: fixed items removed, new ones added.