# Elixir Code Smell Analysis — bDS2 > Generated by static analysis of the codebase. Do not delete; use as a reference for refactoring sprints. --- ## Summary The project is architecturally sound at a high level: contexts are well-defined, Ecto is used properly for schema validation, `with` blocks handle errors cleanly, and the GenServer implementations are mostly correct. However, **the codebase suffers from severe module bloat, excessive duplication of helper functions, risky process-dictionary usage, and several side-effect-in-transaction anti-patterns** that become maintenance liabilities in a desktop application expected to run for long sessions. --- ## Critical Anti-Patterns ### 1. Massive Module Bloat (Most Serious) Several modules are far too large and violate the single-responsibility principle. | Module | Lines | Issue | |--------|-------|-------| | `BDS.Desktop.ShellLive` | ~2,600 | Handles UI events, routing, overlays, menus, project switching, tab management, translations, task polling, and native menu integration | | `BDS.Posts` | ~1,700 | Handles CRUD, publishing, file I/O, translations, auto-translation scheduling, embeddings sync, search sync, link rebuilding, and validation | | `BDS.Generation` | ~1,500 | Site generation, validation, sitemap building, archive pagination, feed rendering, and file hashing | | `BDS.MCP` | ~670 | MCP tools, resources, proposals, post detail serialization, search, and category counting | **Why this is an anti-pattern:** Giant modules reduce compiler concurrency, make refactoring dangerous, obscure test coverage, and increase merge conflicts. Elixir/OTP best practice is to keep modules under ~400 lines and split by responsibility. **Fix:** Extract sub-domains into dedicated modules (e.g., `BDS.Posts.Publisher`, `BDS.Posts.Translations`, `BDS.Generation.SitemapBuilder`, `BDS.Desktop.ShellLive.EventRouter`). --- ### 2. Process Dictionary for I18n State In `BDS.Desktop.ShellLive`: ```elixir def render(assigns) do Process.put(:bds_ui_locale, assigns.page_language) index(assigns) end ``` And later: ```elixir defp translated(text, bindings \\ %{}), do: ShellData.translate(text, bindings, Process.get(:bds_ui_locale)) ``` **Why this is an anti-pattern:** The process dictionary is implicit global state. It makes functions harder to test in isolation and can leak state between concurrent operations in the same process. Elixir strongly discourages `Process.put/get` for business logic. **Fix:** Pass `locale` explicitly through `assigns` to all translation call sites, or use a dedicated context struct. --- ### 3. Side Effects Inside Database Transactions In `BDS.Media.import_media/1`: ```elixir Repo.transaction(fn -> media = %Media{} |> Media.changeset(...) |> Repo.insert!() :ok = File.mkdir_p(Path.dirname(destination)) :ok = File.cp(source_path, destination) :ok = write_sidecar(project, media) :ok = ensure_thumbnails(project, media) :ok = Search.sync_media(media) media end) ``` **Why this is an anti-pattern:** If the transaction retries (or the DB connection times out in SQLite), filesystem and thumbnail side effects are duplicated or orphaned. Ecto transactions should contain **only** database operations. **Fix:** Perform the DB insert in the transaction, then do filesystem work after the transaction succeeds. --- ### 4. `String.to_atom/1` on Untrusted Input In `BDS.MCP`: ```elixir defp atomize_keys(map) when is_map(map) do Map.new(map, fn {key, value} -> {String.to_atom(key), value} end) end ``` **Why this is an anti-pattern:** Atoms are not garbage collected. If MCP receives large or arbitrary JSON keys, the VM atom table can fill up and crash the BEAM. **Fix:** Use `String.to_existing_atom/1` (with a safe fallback) or keep keys as strings. --- ### 5. Bang Functions (`File.read!`) in Business Logic Without Rescue Found in `BDS.Posts`, `BDS.Media`, `BDS.MCP`, `BDS.Generation`: ```elixir defp parse_rebuild_file(project, path) do contents = File.read!(path) {:ok, %{fields: fields}} = Frontmatter.parse_document(contents) ... end ``` **Why this is an anti-pattern:** Missing files or permission errors raise unhandled exceptions that crash the caller. In a GenServer context (e.g., background tasks), this brings down the worker. **Fix:** Use `File.read/1` and handle `{:error, reason}` explicitly, returning `{:error, reason}` tuples to callers. --- ### 6. Duplicate Helper Functions Across Contexts The same private helpers are copy-pasted in `BDS.Posts`, `BDS.Media`, `BDS.Search`, `BDS.Generation`, `BDS.Publishing`, `BDS.MCP`: - `attr/2` (atom/string key normalization) - `maybe_put/3` - `blank_to_nil/1` - `progress_callback/1` - `report_rebuild_started/3`, `report_rebuild_progress/4` **Why this is an anti-pattern:** Violates DRY. When behavior needs to change (e.g., adding float progress support), every copy must be updated. **Fix:** Extract to a shared utility module such as `BDS.MapUtils`, `BDS.AttrUtils`, or `BDS.ProgressReporter`. --- ### 7. Direct Repo Access in LiveView `BDS.Desktop.ShellLive` directly queries the repo: ```elixir case Repo.get(Post, post_id) do %Post{} = post -> ... end ``` **Why this is an anti-pattern:** LiveViews are the web layer; they should call context functions (`BDS.Posts.get_post/1`). Direct Repo access leaks persistence details into the UI and makes testing harder. **Fix:** Replace all `Repo.get` calls in `ShellLive` with context function calls. --- ### 8. `String.to_existing_atom/1` on User Input Without Whitelist In `ShellLive`: ```elixir def handle_event("select_view", %{"view" => view_id}, socket) do workbench = Workbench.click_activity(socket.assigns.workbench, String.to_existing_atom(view_id)) ... end ``` **Why this is an anti-pattern:** If a client sends a non-existent atom string, it raises `ArgumentError`. While safer than `to_atom`, it is still a crash vector. **Fix:** Validate against a whitelist or map of known string-to-atom conversions before conversion. --- ### 9. Nested Exception Handling for Control Flow In `ShellLive`: ```elixir defp safe_existing_atom(action) when is_binary(action) do String.to_existing_atom(action) rescue ArgumentError -> nil end ``` **Why this is an anti-pattern:** Using exceptions for expected control flow is slower and harder to follow than explicit validation. **Fix:** Maintain an explicit mapping of allowed actions, or use a `case` with `:erlang.binary_to_existing_atom/2` and catch the error tuple. --- ### 10. `Jason.decode!` Without Error Handling In `BDS.AI.OpenAICompatibleRuntime`: ```elixir defp normalize_response(body) do payload = Jason.decode!(body) ... end ``` **Why this is an anti-pattern:** Malformed JSON from an external API will crash the caller. **Fix:** Use `Jason.decode/1` and propagate `{:error, reason}`. --- ## Moderate Concerns ### 11. Missing `@spec` Typespecs Very few public functions have `@spec` declarations. For a project of this size, this makes Dialyzer less effective and the API surface harder to understand for new contributors. ### 12. Tests Run Synchronously All tests visible use `use ExUnit.Case, async: false`. For a large suite, this unnecessarily slows CI and local feedback loops. Use `async: true` where tests are isolated. ### 13. Raw SQL Overuse While SQLite FTS necessitates some raw SQL, many queries in `BDS.Search`, `BDS.Media`, and `BDS.Posts` use `Repo.query!/2` for standard operations that Ecto could express portably (e.g., `DELETE FROM ... WHERE ...`). This reduces type safety and database portability. ### 14. Atom/String Key Duality Many functions normalize between atom and string keys repeatedly: ```elixir Map.get(assigns, :language, Map.get(assigns, "language", ...)) ``` This suggests data isn't normalized at boundaries. Prefer atoms for internal structs and strings only at external boundaries (JSON, HTTP params). ### 15. GenServer State Growth (Tasks) `BDS.Tasks` stores all tasks forever in a single map. The `clear_completed`/`clear_finished` functions exist, but if the UI doesn't trigger them regularly in a long-running desktop app, memory grows unbounded. Consider TTL-based eviction. --- ## What the Project Does Well - **Contexts are clearly separated** (`Posts`, `Media`, `Projects`, `Search`, etc.). - **Ecto changesets** are used consistently for validation. - **`with` blocks** handle multi-step error flows cleanly. - **Pattern matching** is idiomatic and pervasive. - **Task.Supervisor** is used correctly for background work. - **Atomic file writes** (`Persistence.atomic_write`) show good filesystem hygiene. - **PubSub** is used appropriately for CLI sync notifications. --- ## Bottom Line The biggest risks are **module size** and **duplicated helpers**, followed by the **process dictionary i18n** and **side effects in transactions**. Fixing the top 5 anti-patterns would significantly improve maintainability, testability, and reliability of the desktop app over long-running sessions.