Files
bDS2/CODESMELL.md

26 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

  • What: String.to_atom/1 on user-controlled data creates atoms that are never GC'd. A malicious payload can exhaust the ~1M atom limit and crash the VM.
  • Affected files (all must be fixed):
    • lib/bds/import_definitions.ex:101atomize_keys/1 converts all JSON keys from import analysis data
    • lib/bds/import_execution.ex:541normalize_report/1 converts import execution report keys
    • lib/bds/ai/catalog.ex:215Enum.into(map, %{}, fn {k,v} -> {String.to_atom(k), v}) on catalog data
    • lib/bds/ai/catalog.ex:316parse_modality/1 on provider data
    • lib/bds/ai/chat_tools.ex:912maybe_put(acc, String.to_atom(key), arguments[key]) on tool arguments
    • lib/bds/desktop/automation.ex:380normalized_key on automation event data
  • NOT affected: lib/bds/bounded_atoms.ex uses safe string-only matching, no String.to_atom. lib/bds/ui/menu_bar.ex:124,134 and lib/bds/ui/workbench.ex:270 convert internal IDs (controlled vocabulary), which is lower risk but should still use String.to_existing_atom/1.
  • Fix: Replace all String.to_atom(key) with String.to_existing_atom(key) or keep keys as strings. For the import/analysis results, keep keys as strings since the consumer code can use Map.get(map, "key") or Access.key/2. For internal IDs (menu_bar, workbench), ensure the atoms exist in the module attribute allow-lists before conversion.
  • Test: For each file: create a payload with 100k unique string keys; verify atom count (:erlang.system_info(:atom_count)) does not increase.

CSM-002 — Search Loads Entire Tables into Memory

  • File: lib/bds/search.ex:111-149
  • What: search_posts/3 and search_media/3 load all candidate IDs via candidate_post_ids, then all matching records via load_posts_in_order, then filter in Elixir (filter_posts), then paginate in Elixir (paginate).
  • Why it's bad: For a project with thousands of posts, every search fetches the entire table into the BEAM process. Filtering (status, tags, categories, language, year, month, date range, missing translations) and pagination all happen in Elixir instead of SQL.
  • Fix:
    • Push where clauses for status, language, year/month, and date range into the Ecto query.
    • Use Repo.aggregate(:count) for total instead of length(posts).
    • Push limit/offset into SQL for pagination.
    • For tag/category overlap filtering, use a JOIN or subquery on the FTS index.
    • filter_posts (Z. 348-371) should become SQL where-clauses, not an Enum.filter.
  • Test: Create a project with 5,000 posts; run a search; assert memory stays below 50MB and query completes in <200ms.

CSM-003 — Non-Atomic Side Effects in Post CRUD

  • File: lib/bds/posts.ex
  • What: create_post/1 (Z. 65-108), update_post/2 (Z. 112-151), publish_post/1 (Z. 155-195), delete_post/1 (Z. 298-320) mix DB writes with filesystem/search/embeddings side effects.
  • Why it's bad:
    • If a side effect fails after the DB commit, the system is inconsistent (DB says one thing, filesystem another).
    • delete_post/1 (Z. 312-318) deletes files (delete_post_file), removes embeddings, deletes post links — all before Repo.delete!(post). If Repo.delete! raises, the files are gone but the row remains.
    • Repo.delete! (bang) crashes instead of returning {:error, _}.
    • Same pattern with Repo.delete! in tags.ex:153,232, scripts.ex:133, media.ex:184,250, projects.ex:190, templates.ex:216,535, posts/translations.ex:101, posts/translation_validation.ex:399.
  • Fix:
    • Use Repo.transaction or Ecto.Multi to group DB writes atomically.
    • Perform filesystem side effects after the DB commit succeeds (use Repo.transaction callback or Ecto.Multi with run/5).
    • Replace all Repo.delete! with Repo.delete and handle {:error, _} tuples.
    • In delete_post/1: reorder to Repo.delete first, then clean up files/embeddings/search.
  • Test: Mock a file-delete failure in delete_post/1; assert the post row still exists.

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-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.
  • 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.