Files
bDS2/CODESMELL.md
2026-05-08 20:09:02 +02:00

27 KiB

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.exatomize_keys/1MapUtils.safe_atomize_keys/1
      • lib/bds/import_execution.exnormalize_report/1MapUtils.safe_atomize_keys/1
      • lib/bds/ai/catalog.exatomize_map_keys/1MapUtils.safe_atomize_keys/1, parse_modality/1MapUtils.safe_atomize_key/1
      • lib/bds/ai/chat_tools.exmetadata_attrs/2MapUtils.safe_atomize_key/1
      • lib/bds/desktop/automation.exatomize_map/1MapUtils.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.exnormalize_type/1
      • lib/bds/desktop/shell_live/chat_editor/tool_surfaces.exmap_value/3
      • lib/bds/release_packaging.exnormalize_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.exdelete_post/1
      • lib/bds/scripts.exdelete_script/1
      • lib/bds/media.exdelete_media/1, delete_media_translation/3
      • lib/bds/templates.exdelete_template/2, remove_orphan_templates/2
      • lib/bds/tags.exdelete_tag/1, merge_tags/2
      • lib/bds/projects.exdelete_project/1
      • lib/bds/posts/translations.exdelete_post_translation/1
      • lib/bds/posts/translation_validation.exfix_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 FIXED

  • Fixed: 2026-05-08
  • What was done:
    • Moved JobStore.attach_runner/2 from init/1 to a new handle_continue(:attach_and_start) callback, so supervisor startup is no longer blocked by the synchronous call.
    • Added terminate/2 callback that calls JobStore.detach_runner/2 (with try/catch for shutdown safety), centralizing cleanup that was previously scattered across individual exit paths.
    • Added handle_info({:EXIT, _pid, _reason}) clause to handle trapped exit signals from linked processes.
    • Removed redundant inline detach_runner calls from handle_call(:cancel), task result handler, and :DOWN handler — terminate/2 now handles all detach cleanup.
    • Changed restart: :temporary since job runners are one-shot processes that should not auto-restart on failure.
    • Added @impl true to all handle_info clauses.
    • Fixed pre-existing bug in JobStore.detach_runner handler where update_in/2 macro result was incorrectly double-wrapped, corrupting state.
    • Added test: start a runner, kill it externally (not via cancel), assert JobStore no longer contains the dead PID.

CSM-005 — Client-Side Filtering of Entire Tables FIXED

  • Fixed: 2026-05-08
  • What was done:
    • Sidebar (lib/bds/ui/sidebar.ex):
      • Removed list_posts/1 and list_media/1 that loaded all records into memory.
      • Replaced apply_post_filters/1 and apply_media_filters/1 (Elixir-side filtering) with SQL WHERE clauses using Ecto dynamic queries and SQLite json_each fragments.
      • Page/non-page split now uses EXISTS (SELECT 1 FROM json_each(categories) WHERE lower(value) = 'page') in SQL.
      • Search, year/month, tag, and category filters all push to SQL via maybe_where_search, maybe_where_year, maybe_where_month, maybe_where_all_tags, maybe_where_all_categories.
      • Aggregate queries (year_month_counts, available_tags, available_categories) use Ecto.Adapters.SQL.query! with json_each cross-joins, GROUP BY, and DISTINCT.
      • Pagination uses SQL LIMIT instead of Enum.take.
      • tag_count/1 replaces list_tags/1 + length/1 with Repo.one(select: count(tag.id)).
      • Fixed group_posts/1 O(n²) acc.draft ++ [post] pattern — now uses Enum.group_by/2 (also fixes CSM-024).
    • Tags (lib/bds/tags.ex):
      • posts_with_tag/2 now uses EXISTS (SELECT 1 FROM json_each(?) WHERE value = ?) instead of loading all posts.
      • posts_with_any_tag/2 now uses json_each cross-join with a JSON parameter for the tag name list.
      • post_tag_names/1 now selects only the tags column instead of loading full post records.
    • Dashboard (lib/bds/ui/dashboard.ex):
      • post_stats uses GROUP BY post.status, SELECT {status, count(id)} — no longer loads all posts.
      • media_stats uses SELECT count(id), coalesce(sum(size), 0) and a separate image count query with LIKE 'image/%'.
      • tag_cloud_items and category_counts use raw SQL with json_each cross-joins and GROUP BY.
      • timeline_entries uses SQL strftime + GROUP BY for year/month aggregation.
      • recent_posts uses SQL ORDER BY updated_at DESC LIMIT 5.
    • Posts (lib/bds/posts.ex):
      • dashboard_stats/1 uses GROUP BY post.status, SELECT {status, count(id)} instead of loading all statuses.
    • Capabilities (lib/bds/scripting/capabilities/):
      • tag_post_ids/2 uses json_each fragment + SELECT post.id instead of loading all posts.
      • names_with_counts/2 uses raw SQL with json_each + GROUP BY instead of loading all posts.
      • posts_by_status/2 filters at SQL level instead of loading all posts and filtering in Elixir.
    • Added 20 tests in test/bds/csm005_sql_filtering_test.exs covering dashboard stats, tag cloud, sidebar page/post separation, tag/search/year-month filters, available aggregates, and media filtering.

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-209post_link_entries/1 calls PostLinks.list_incoming_links/1 and Posts.get_post/1 per link during render.
    • panel_renderer.ex:229-240git_log_entries/1 calls Git.file_history/2 and Posts.get_post/1 during render.
    • tab_helpers.ex:120-135derived_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:127length(targets) inside Enum.reduce_while (typically 3 targets, negligible impact)
    • lib/bds/rendering/list_archive.ex:299index < 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-565acc.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:112prefix <> "/"
    • lib/bds/publishing.ex:284String.trim_trailing(path, "/") <> "/"
    • lib/bds/rendering/file_system.ex:29normalized_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:
    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-163update_template/2 does slug changes, content changes, status transitions, file paths, transactions, cascades, and filesystem sync.
    • lib/bds/scripting/capabilities.ex:22-248for_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

  • 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.
    • CSM-004: Fixed. attach_runner moved to handle_continue, terminate/2 added for cleanup, restart: :temporary set, JobStore detach_runner bug fixed.
  • All high-severity items (CSM-006 to CSM-010) have been addressed.
  • CSM-001 fix covers ALL 6 affected files, not just import_definitions.ex.
  • 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).
  • Tests were written before implementation changes (Red → Green → Refactor).
  • Full test suite passes: mix test.
  • Dialyzer passes cleanly: mix dialyzer (zero warnings).
  • Build succeeds: mix compile.
  • No external JS/CSS referenced in preview/generated HTML (per AGENTS.md).
  • All UI strings use gettext / i18n, no hardcoded text.
  • API docs (API.md) updated if any API changes were made.
  • Metadata diff tool and rebuild-from-database updated if metadata changed.
  • Specs in specs/ folder updated and validated if behavior changed.
  • Unused code (including tests for removed features) has been deleted.
  • This CODESMELL.md updated: fixed items removed, new ones added.