19 KiB
19 KiB
bDS2 Elixir Anti-Pattern & Best-Practice Audit
Generated: 2026-05-05 Scope: Elixir application, Phoenix LiveView UI, Ecto DB layer, Desktop (wx) integration, Rendering/Generation pipelines
How to use this file
- Pick a section.
- Search the codebase for the file/line references.
- Write a failing test that reproduces the issue.
- Fix the code.
- Run the full test suite and
mix dialyzer. - Delete the item from this file.
Critical (Fix Immediately)
CSM-001 — Atom Table Exhaustion Vulnerability
- File:
lib/bds/import_definitions.ex:98-108 - What:
atomize_keys/1callsString.to_atom(key)on all JSON keys from user-controlled import data. - Why it's bad: Atoms are never garbage-collected. A malicious import can exhaust the ~1M atom limit and crash the VM.
- Fix: Use
String.to_existing_atom/1or keep keys as strings. Add a test with a JSON payload containing 100k unique string keys to verify it no longer creates atoms. - Related: Also check
lib/bds/bounded_atoms.exfor any runtimeString.to_atomon external data.
CSM-002 — Search Loads Entire Tables into Memory
- File:
lib/bds/search.ex:111-149 - What:
search_posts/3andsearch_media/3load all candidate IDs, then all matching records, filter in Elixir, and slice. - Why it's bad: For a project with thousands of posts, every search fetches the entire table into the BEAM process.
- Fix: Push pagination, filtering, and sorting into SQL (
limit,offset,where). UseRepo.aggregatefor counts. - 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,update_post/2,publish_post/1,delete_post/1mix DB writes with filesystem/search/embeddings side effects. - Why it's bad: If a side effect fails, the DB is already committed and the system is inconsistent.
delete_post/1deletes files/embeddings before the DB row, so ifRepo.delete!raises, the files are gone but the row remains. - Fix:
- Use
Repo.transactionorEcto.Multito group DB + side effects atomically. - Use non-bang
Repo.delete/1and return{:error, _}tuples. - Delete files after the DB delete succeeds, or roll back on file-delete failure.
- Use
- Test: Mock a file-delete failure in
delete_post/1; assert the post row still exists.
CSM-004 — Blocking init/1 + Broken trap_exit in Job Runner
- File:
lib/bds/scripting/job_runner.ex:28-35 - What:
- Synchronous
GenServer.calltoJobStore.attach_runner/2insideinit/1blocks the supervisor. Process.flag(:trap_exit, true)is set but there is noterminate/2and no{:EXIT, ...}handler.
- Synchronous
- Why it's bad:
- Supervisor child startup hangs if
JobStoreis slow. - Dead runner PIDs leak in
JobStore.runners, causing futureGenServer.calltimeouts.
- Supervisor child startup hangs if
- Fix:
- Move
attach_runnertohandle_continue/2. - Remove
trap_exit(the task usesasync_nolink, so links aren't the issue) OR addterminate/2to callJobStore.detach_runner/2.
- Move
- Test: Start a runner, stop it, assert
JobStore.list_runners()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/1loads every post for a project, then filters in Elixir (apply_post_filters).Tags.posts_with_tag/2/post_tag_names/1load all posts to extract tags.UI.Dashboard.snapshot/1loads ALL posts and ALL media for counts.
- Why it's bad: O(n) memory usage for the whole table, scales linearly with blog size.
- Fix: Move filtering, pagination, and aggregation into Ecto queries (
where,group_by,limit,Repo.aggregate). - 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/1callsinsert_post_index/1insideEnum.each; that helper queriespost_translations/1per post.- Same pattern for media reindexing.
ListArchiveandPostRenderingcallload_post_record/1per-post insideEnum.map.
- Fix: Preload translations in a single query before the loop, or batch-insert with a multi-insert statement.
- Test: Reindex 100 posts; assert the total query count is <5 (use
Ecto.Adapters.SQL.query!/2hook or logger capture).
CSM-007 — Monolithic State Rebuild ("God Function")
- File:
lib/bds/desktop/shell_live.ex:554-616 - What:
reload_shell/2rebuilds sidebar, dashboard, git badge, tasks, status bar, tab meta, and panel data on almost every event. - Why it's bad: Even a simple sidebar toggle triggers 5+ unrelated DB queries.
- Fix: Decompose into focused updaters:
refresh_sidebar/2refresh_dashboard/2refresh_git_badge/2refresh_task_status/2Only call the relevant one from eachhandle_event.
- Test: Toggle sidebar; assert no dashboard or git queries are executed.
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:
post_link_entries/1callsPostLinks.list_incoming_links/1andPosts.get_post/1during render.git_log_entries/1callsGit.file_history/2during render.tab_helpers.exqueries posts/media from withinderived_tab_meta/1.
- Fix: Move all data fetching into event handlers (
handle_event,handle_params) or the decomposedreload_shellupdaters. 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 Blocks Scheduler
- File:
lib/bds/media/thumbnails.ex:107-165 - What:
Image.open,Image.thumbnail!,Image.write!run CPU-intensive image decode/encode in the current process. - Why it's bad: Blocks the BEAM scheduler; the entire app freezes for large images.
- Fix: Run in
Task.async/Task.awaitor aTask.Supervisorworker pool. Also handleFile.mkdir_presult. - Test: Generate a thumbnail from a 20MB image; assert the total scheduler-blocking time is <50ms (use
:erlang.system_info(:scheduler_wall_time)).
CSM-010 — rescue for Control Flow in Data Layer
- File:
lib/bds/desktop/shell_data.ex:64-74, 81-103 - What:
rescue Exqlite.Error,rescue DBConnection.OwnershipErrorto return empty defaults. - Why it's bad: Exceptions are for exceptional conditions, not expected states like "DB not ready yet."
- Fix: Return
{:ok, _}/{:error, _}tuples from the lowest-level callers and handle them upstream. - Test: Call
project_snapshot/0with no DB connection; assert it returns{:error, _}instead of an empty 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/3orpush_patch/2is used. - Why it's bad: Users cannot bookmark or use the back button. Harder to debug.
- Fix: Add
handle_params/3for at least?tab=and?view=params. Usepush_patchon state changes. - Test: Click a tab; assert the URL updates to
/?tab=posts; refresh; assert the same tab is active.
CSM-012 — Memory Pressure from Large Assigns
- File:
lib/bds/desktop/shell_live.ex - What: Output entries, sidebar lists, media lists, and post lists live forever in socket assigns.
- Fix: Use
temporary_assignsfor append-only or large lists:socket = stream(socket, :posts, posts) # or socket = assign(socket, :output_entries, output_entries, temporary: true) - Test: Load 1,000 sidebar items; assert socket assigns size stays below 100KB.
CSM-013 — Desktop File Dialog Blocks Event Handler
- File:
lib/bds/desktop/shell_live/sidebar_create.ex:44-69 - What:
FilePicker.choose_file/1is called directly insidehandle_event. - Why it's bad: Can freeze the socket process while the native dialog is open.
- Fix: Spawn a short-lived
Taskor 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-014 — Bang Functions in Rendering Pipelines
- Files:
lib/bds/rendering/post_rendering.ex:151,lib/bds/rendering/filters.ex:125,lib/bds/rendering/template_selection.ex: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-015 — O(n²) Loops
- Files:
lib/bds/publishing.ex:127—length(targets)insideEnum.reduce_whilelib/bds/rendering/list_archive.ex:299—index < length(grouped_blocks) - 1insideEnum.maplib/bds/generation/outputs.ex:208-323— repeatedlength(posts)inside comprehensions
- Fix: Bind
total = length(list)outside the loop. - Test: Run the function with a large list; assert it completes in linear time.
CSM-016 — Missing @derive Jason.Encoder on Schemas
- Files: All schema modules (
Post,Media,Tag,Template,Script,Project,Setting,ChatMessage,ChatConversation,Embedding.Key, etc.) - What: No JSON derivation. Passing a struct to
Jason.encode!/1crashes. - Fix: Add to schemas that cross API boundaries:
@derive {Jason.Encoder, except: [:__meta__]} schema "posts" do - Test: Encode each schema struct to JSON; assert success.
CSM-017 — Missing DB Indexes on Foreign Keys
- Files:
priv/repo/migrations/ - What: Many FKs lack dedicated indexes:
media.project_id,post_media.post_id,post_media.media_id,chat_messages.conversation_id,embedding_keys.post_id,embedding_keys.project_id,publish_jobs.project_id, etc. - Fix: Add
create indexfor every foreign key and frequently filtered column (posts.status,posts.published_at,posts.language). - Test: Verify with
sqlite3.indexesor Ecto introspection.
CSM-018 — Missing on_delete in Schema Associations
- Files: Most schema modules
- What:
belongs_toandhas_manylackon_deleteat the Ecto schema level. - Why it's bad: Ecto preloads and reflection behave differently; cascading deletes may not work as expected at the application level.
- Fix: Add
on_delete: :delete_all/:nilify_allto match the DB-level constraints in migrations.
CSM-019 — String Concatenation for Paths
- Files:
lib/bds/rendering/metadata.ex,lib/bds/rendering/links_and_languages.ex,lib/bds/publishing.ex,lib/bds/rendering/file_system.ex:29 - What:
"/#{slug}/",String.trim_trailing(path, "/") <> "/",normalized_path <> ".liquid" - Fix: Use
Path.join/1-2andPath.extname/Path.rootname.
CSM-020 — send(self(), ...) Component Chatter
- Files:
lib/bds/desktop/shell_live/script_editor.ex,lib/bds/desktop/shell_live/chat_editor.ex,lib/bds/desktop/shell_live/overlay_manager.ex - What: Components send messages to the parent via
send(self(), ...), forcing a broadhandle_infoinShellLive. - Fix: Prefer
Phoenix.LiveView.send_update/2for targeted updates, or delegate through a single dispatch module.
Low Severity / Code Quality
CSM-021 — @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
@moduledocdescriptions for all public modules. Keep internal helpers documented or mark them@moduledoc falseonly if truly private.
CSM-022 — Missing @spec on Public Functions
- Files: Widespread across rendering, generation, publishing, UI, and scripting modules.
- Fix: Add
@specto every public function. This is a Dialyzer prerequisite (the project already runs Dialyzer; the report notes it should be clean).
CSM-023 — 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-024 — 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 -> ... endwith multiple function-head clauses.
CSM-025 — Silent Error Swallowing
- File:
lib/bds/scripting.ex:64-66 - What: Macro execution errors return
{:ok, ""}with no logging. - Fix: Return the actual error tuple or at least log the failure with
Logger.error/1.
CSM-026 — Missing terminate/2 in Stateful GenServers
- Files:
lib/bds/scripting/job_runner.ex,lib/bds/scripting/job_store.ex - Fix: Add
terminate/2to clean up runner mappings, timers, or temporary resources.
CSM-027 — SRP Violations
- Files:
lib/bds/templates.ex:86-163—update_template/2does slug changes, content changes, status transitions, file paths, transactions, cascades, and filesystem sync.lib/bds/scripting/capabilities.ex:22-248—for_project/2returns a 200+ line map literal.
- Fix: Decompose into smaller private pipelines or domain-specific builder functions.
CSM-028 — Enum.reduce with acc.draft ++ [post] (O(n²))
- File:
lib/bds/ui/sidebar.ex:556-565 - Fix: Use
Enum.group_by/3or reverse-accumulate andEnum.reverse.
CSM-029 — Hardcoded Language Prefixes
- File:
lib/bds/generation/pagefind.ex:48-54 - What:
["de/", "fr/", "it/", "es/"]hardcoded. - Fix: Derive from project settings (
mainLanguageand supported languages).
CSM-030 — 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. Between check and read the file can vanish. - Fix: Just try to read and handle
{:error, :enoent}.
CSM-031 — if result == :ok Instead of Pattern Matching
- File:
lib/bds/templates.ex:445 - Fix: Use
case result do :ok -> ...; _ -> ... end.
CSM-032 — 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-033 — 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-034 — 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/1is discarded.File.mkdir_p!/1inrelease_packagingcan crash on permission errors. - Fix: Pattern-match
File.mkdir_p/1or usewith; replace bang variants with non-bang and handle errors.
CSM-035 — try/rescue Instead of with and Error Tuples
- Files:
lib/bds/rendering/filters.ex,lib/bds/templates.ex,lib/bds/desktop/shell_data.ex - Fix: Replace
try/rescuearound expected failures with non-bang functions andwithchains.
CSM-036 — 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) orMap.fetch!/2if the key is required.
CSM-037 — Enum.each with Side Effects That Should Be Enum.map / Comprehensions
- Files:
lib/bds/search.ex:174-177,lib/bds/embeddings.ex - What:
Enum.eachused for inserting records. The side-effect pattern is fine, butEnum.map+Repo.insert_allwould be much faster for bulk inserts. - Fix: Use
Repo.insert_allfor batch inserts instead ofEnum.each+Repo.insert.
CSM-038 — 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-039 — Process Dictionary (Process.get/put) Usage
- Files: Check
lib/bds/desktop/menu_bar.ex:25-34, any otherProcess.put - What:
UILocale.put/1sets process dictionary in the MenuBar process. - Fix: This is isolated to the MenuBar process so it's low-risk, but document it explicitly.
CSM-040 — Missing @impl true on GenServer Callbacks
- File:
lib/bds/publishing.ex:46,61,71,75 - Fix: Add
@impl truebefore everyhandle_call,handle_cast,handle_info, andterminate.
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.
- 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.mdupdated: fixed items removed, new ones added.