30 KiB
30 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
- 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 ✅ FIXED
- Fixed: 2026-05-06
- What was done:
- Added
BDS.MapUtils.safe_atomize_key/1andBDS.MapUtils.safe_atomize_keys/1— usesString.to_existing_atom/1with rescue fallback to keep unknown keys as strings. - Replaced all 6 affected
String.to_atomcall sites:lib/bds/import_definitions.ex—atomize_keys/1→MapUtils.safe_atomize_keys/1lib/bds/import_execution.ex—normalize_report/1→MapUtils.safe_atomize_keys/1lib/bds/ai/catalog.ex—atomize_map_keys/1→MapUtils.safe_atomize_keys/1,parse_modality/1→MapUtils.safe_atomize_key/1lib/bds/ai/chat_tools.ex—metadata_attrs/2→MapUtils.safe_atomize_key/1lib/bds/desktop/automation.ex—atomize_map/1→MapUtils.safe_atomize_keys/1
- Replaced lower-risk
String.to_atomwithString.to_existing_atom/1:lib/bds/ui/menu_bar.ex— sidebar view and singleton editor command IDslib/bds/ui/workbench.ex—normalize_type/1lib/bds/desktop/shell_live/chat_editor/tool_surfaces.ex—map_value/3lib/bds/release_packaging.ex—normalize_platform/1
- Updated
test/bds/bounded_atoms_test.exsto enforce noString.to_atomon dynamic data (replaced oldString.to_existing_atomban).
- Added
CSM-002 — Search Loads Entire Tables into Memory ✅ FIXED
- Fixed: 2026-05-07
- What was done:
- Replaced
search_posts/3andsearch_media/3with SQL-level filtering and pagination. - Blank queries now use pure Ecto queries with
whereclauses for status, language, year/month, date range, tags, categories, and missing translations. - Non-blank (FTS) queries use a CTE (
WITH fts_results AS (...)) to preservebm25ordering, joined with the posts/media table, with all filters applied in SQL. - Tag and category overlap filtering uses
json_eachinEXISTSsubqueries. - Missing-translation filtering uses a
NOT EXISTScorrelated subquery. - Count uses
select count+Repo.oneinstead oflength(all_records). - Pagination uses SQL
LIMIT/OFFSETinstead ofEnum.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.
- Replaced
CSM-003 — Non-Atomic Side Effects in Post CRUD ✅ FIXED
- Fixed: 2026-05-07
- What was done:
- Replaced all 11
Repo.delete!call sites withRepo.delete+{:error, _}handling:lib/bds/posts.ex—delete_post/1lib/bds/scripts.ex—delete_script/1lib/bds/media.ex—delete_media/1,delete_media_translation/3lib/bds/templates.ex—delete_template/2,remove_orphan_templates/2lib/bds/tags.ex—delete_tag/1,merge_tags/2lib/bds/projects.ex—delete_project/1lib/bds/posts/translations.ex—delete_post_translation/1lib/bds/posts/translation_validation.ex—fix_invalid_database_row/1
- Reordered
delete_post/1to performRepo.deletefirst, 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, anddelete_post_translation/1. delete_media/1now wraps translation + media deletes in aRepo.transactionfor atomicity.- Tags and projects already used
Repo.transaction; replaced innerRepo.delete!withRepo.delete+Repo.rollbackon error. - Added tests for delete atomicity and not-found handling.
- Replaced all 11
CSM-004 — Blocking init/1 + Missing terminate/2 in Job Runner ✅ FIXED
init/1 + Missing terminate/2 in Job Runner- Fixed: 2026-05-08
- What was done:
- Moved
JobStore.attach_runner/2frominit/1to a newhandle_continue(:attach_and_start)callback, so supervisor startup is no longer blocked by the synchronous call. - Added
terminate/2callback that callsJobStore.detach_runner/2(withtry/catchfor 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_runnercalls fromhandle_call(:cancel), task result handler, and:DOWNhandler —terminate/2now handles all detach cleanup. - Changed
restart: :temporarysince job runners are one-shot processes that should not auto-restart on failure. - Added
@impl trueto allhandle_infoclauses. - Fixed pre-existing bug in
JobStore.detach_runnerhandler whereupdate_in/2macro result was incorrectly double-wrapped, corrupting state. - Added test: start a runner, kill it externally (not via cancel), assert
JobStoreno longer contains the dead PID.
- Moved
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/1andlist_media/1that loaded all records into memory. - Replaced
apply_post_filters/1andapply_media_filters/1(Elixir-side filtering) with SQLWHEREclauses using Ecto dynamic queries and SQLitejson_eachfragments. - 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) useEcto.Adapters.SQL.query!withjson_eachcross-joins,GROUP BY, andDISTINCT. - Pagination uses SQL
LIMITinstead ofEnum.take. tag_count/1replaceslist_tags/1+length/1withRepo.one(select: count(tag.id)).- Fixed
group_posts/1O(n²)acc.draft ++ [post]pattern — now usesEnum.group_by/2(also fixes CSM-024).
- Removed
- Tags (
lib/bds/tags.ex):posts_with_tag/2now usesEXISTS (SELECT 1 FROM json_each(?) WHERE value = ?)instead of loading all posts.posts_with_any_tag/2now usesjson_eachcross-join with a JSON parameter for the tag name list.post_tag_names/1now selects only thetagscolumn instead of loading full post records.
- Dashboard (
lib/bds/ui/dashboard.ex):post_statsusesGROUP BY post.status, SELECT {status, count(id)}— no longer loads all posts.media_statsusesSELECT count(id), coalesce(sum(size), 0)and a separate image count query withLIKE 'image/%'.tag_cloud_itemsandcategory_countsuse raw SQL withjson_eachcross-joins andGROUP BY.timeline_entriesuses SQLstrftime+GROUP BYfor year/month aggregation.recent_postsuses SQLORDER BY updated_at DESC LIMIT 5.
- Posts (
lib/bds/posts.ex):dashboard_stats/1usesGROUP BY post.status, SELECT {status, count(id)}instead of loading all statuses.
- Capabilities (
lib/bds/scripting/capabilities/):tag_post_ids/2usesjson_eachfragment +SELECT post.idinstead of loading all posts.names_with_counts/2uses raw SQL withjson_each+GROUP BYinstead of loading all posts.posts_by_status/2filters at SQL level instead of loading all posts and filtering in Elixir.
- Added 20 tests in
test/bds/csm005_sql_filtering_test.exscovering dashboard stats, tag cloud, sidebar page/post separation, tag/search/year-month filters, available aggregates, and media filtering.
- Sidebar (
High Severity
CSM-006 — N+1 Queries in Reindexing & Rendering ✅ FIXED
- Fixed: 2026-05-08
- What was done:
- Batch INSERT for reindexing: Replaced per-row
Repo.query!INSERT inreindex_posts/2andreindex_media/2with multi-row batch INSERTs. Rows are chunked at 166 per batch (SQLite 999-parameter limit ÷ 6 columns). Translations were already preloaded in batch; fixed O(n²)acc ++ [translation]pattern inpreload_post_translationsandpreload_media_translationsby replacing withEnum.group_by. - Rendering — preloaded post records:
PostRendering.post_assigns/2now accepts an optional:_post_recordkey in assigns, skipping theRepo.get(Post, id)re-query when the record is already available. - Generation outputs pass records:
build_page_outputsandbuild_post_outputsinoutputs.exnow pass the already-loaded post/translation records via:_post_record, eliminating per-post DB queries during generation. - ListArchive already used
load_post_records_batch(batch query) — no change needed. - Added telemetry-based query counting tests: reindex 100 posts/media and assert total query count <10.
- Batch INSERT for reindexing: Replaced per-row
CSM-007 — Monolithic State Rebuild ("God Function") ✅ FIXED
- Fixed: 2026-05-09
- What was done:
- Decomposed
reload_shell/2into four focused updaters:refresh_layout/2— No DB queries. Recomputes workbench-derived assigns (activity_buttons, panel_tabs, current_tab, status_bar, sidebar_header, editor_meta) from existing socket.assigns.refresh_sidebar/2— Queries sidebar data only, then callsrefresh_layout.refresh_content/2— Queries projects, dashboard, git badge, and sidebar data, then callsrefresh_layout.reload_shell/2— Full refresh: tab_meta sync, task status, static data, then callsrefresh_content. Kept for mount, project switch, session restore, and settings changes.
- Replaced all call sites with the minimal refresh needed:
- Layout-only (
refresh_layout): toggle_sidebar, toggle_panel, toggle_assistant_sidebar, select_panel_tab, sync_layout, resize_panel, open_tasks_panel, select_tab, close_tab, toggle_offline_mode, layout menu actions (toggle, close_tab). - Sidebar (
refresh_sidebar): select_view, all sidebar filter events, sidebar menu actions (view_posts, view_media, edit_preferences, etc.), chat/import editor tab_meta updates. - Content (
refresh_content): entity_changed (CLI sync), tags_changed, sidebar create/delete. - Full reload (
reload_shell): mount, activate_project, restore_workbench_session, set_page_language, settings_changed.
- Layout-only (
- Updated Bridges callbacks to use focused refreshers:
refresh_layoutfor toggle events and close_tab,refresh_sidebarfor view switches and tab meta updates,refresh_contentfor entity/tag changes. - Split
@local_menu_actionsinto@layout_menu_actionsand@sidebar_menu_actionsfor correct dispatch. - Fixed
false || truebug inrefresh_layoutwhereoffline_mode = assigns[:offline_mode] || trueincorrectly defaultedfalsetotrue. - Added 7 tests in
test/bds/csm007_reload_shell_test.exsusing telemetry-based query counting: toggle_sidebar (0 queries), toggle_panel (0 queries), sync_layout (0 queries), select_panel_tab (0 queries), toggle_offline_mode (1 query — settings write only), select_view (sidebar queries but no dashboard/projects), sidebar_search (no dashboard queries).
- Decomposed
CSM-008 — DB Queries During Render Path ✅ FIXED
- Fixed: 2026-05-09
- What was done:
- Panel renderer (
lib/bds/desktop/shell_live/panel_renderer.ex):render_post_linksandrender_git_logno longer call DB functions during render. Instead they read from pre-computed assigns (panel_post_links,panel_git_entries).- Renamed
post_link_entries/1→fetch_post_link_entries/1andgit_log_entries/1→fetch_git_log_entries/1, made them public for use by event handlers.
- Shell LiveView (
lib/bds/desktop/shell_live.ex):- Added
refresh_panel_data/1that fetches panel data (post links or git log) based on the active panel tab and stores results in assigns. refresh_layout/2detects whencurrent_taborpanel.active_tabchanged and callsrefresh_panel_data/1only when stale — no DB queries on re-renders.- Initialized
panel_post_linksandpanel_git_entriesassigns in mount.
- Added
- Tab meta (
lib/bds/desktop/shell_live/tab_helpers.ex):sync_tab_metanow skipsderived_tab_metaDB queries when existing meta already has both title and subtitle populated (meta_complete?/1guard).
- Added 5 tests in
test/bds/csm008_render_path_test.exs: post_links re-render (0 queries), git_log re-render (0 queries), output panel switch (0 queries), tasks panel switch (0 queries), tab meta skip for complete meta (0 queries).
- Panel renderer (
CSM-009 — Thumbnail Generation: Missing Error Handling ✅ FIXED
- Fixed: 2026-05-09
- What was done:
- Replaced all bang variants with non-bang error-tuple handling:
Image.autorotate!→Image.autorotatewith{:ok, {image, rotation_info}}destructuring.Image.thumbnail!→Image.thumbnailreturning{:ok, image}/{:error, reason}.Image.embed!→Image.embedwithwithchain.Image.flatten!→Image.flattenwithwithchain.Image.write!→Image.writewith{:ok, _}/{:error, reason}handling.
File.mkdir_presult is now checked — errors halt thumbnail generation with{:error, reason}.write_all_thumbnailsusesEnum.reduce_whileto stop on first error and return{:error, reason}.ensure_thumbnailsspec updated to:ok | {:error, term()}.regenerate_thumbnailspropagates{:error, reason}fromensure_thumbnails.regenerate_missing_thumbnailsreplacedtry/rescuewithcaseon the new error tuples.- Call sites in
BDS.Media(import_media,replace_media_binary) uselog_thumbnail_error/2— media operations succeed even if thumbnails fail, with a warning logged. - Added 6 tests in
test/bds/csm009_thumbnail_error_handling_test.exs: corrupt image returns{:error, _}, non-image returns:ok, missing source returns{:error, _}, regenerate corrupt returns{:error, _}, regenerate_missing counts failures, import succeeds despite thumbnail failure.
- Replaced all bang variants with non-bang error-tuple handling:
CSM-010 — rescue for Control Flow in Data Layer ✅ FIXED
rescue for Control Flow in Data Layer- Fixed: 2026-05-09
- What was done:
- Added
BDS.Repo.ready?/0— a lightweight probe that queriessqlite_master(parameterized) to check if core tables exist, without raising exceptions. - Replaced all 4
rescueblocks inShellData(project_snapshot/0,dashboard/1,sidebar_view/3,git_badge_count/2) with upfrontRepo.ready?()checks. - All four functions now return
{:ok, result}/{:error, :not_ready}tuples instead of silently returning defaults via rescue. - Updated callers in
ShellLive.refresh_content/2andShellLive.refresh_sidebar/2to pattern-match the new tuples and fall back to empty defaults only on{:error, :not_ready}. - Made
default_project_snapshot/0public for use by callers handling the not-ready case. - Added 10 tests in
test/bds/csm010_rescue_control_flow_test.exs:Repo.ready?returns true when DB is available, each of the 4 functions returns{:ok, _}when DB is ready and{:error, :not_ready}when the Repo is stopped.
- Added
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 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/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 — 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.
- 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
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-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)insideEnum.reduce_while(typically 3 targets, negligible impact)lib/bds/rendering/list_archive.ex:299—index < length(grouped_blocks) - 1insideEnum.maplib/bds/generation/outputs.ex:216,230,232— repeatedlength(paginated_posts)andlength(posts)inside comprehensionslib/bds/ui/sidebar.ex:556-565—acc.draft ++ [post]inEnum.reduce(also covered by CSM-024)
- Note: In
publishing.exthe O(n²) is negligible (3 targets). The real impact is inoutputs.exandlist_archive.exwith large post sets. - Fix: Bind
total = length(list)before the loop. Foracc ++ [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_idchat_messages.conversation_idembedding_keys.post_id,embedding_keys.project_iddismissed_duplicate_pairs.project_idimport_definitions.project_iddb_notifications.entity_type,db_notifications.entity_idposts.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 indexfor every foreign key and frequently filtered column. - Test: Verify with
sqlite3EXPLAIN QUERY PLANthat 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-2andPath.extname/Path.rootname. For"/#{slug}/", usePath.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 broadhandle_infoinShellLive. Each message type must be handled in the parent, creating tight coupling. - Fix: Prefer
Phoenix.LiveView.send_update/2for 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
@moduledocdescriptions for all public modules. Keep internal helpers documented or mark them@moduledoc falseonly if truly private.
CSM-019 — 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-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 -> ... endwith multiple function-head clauses.
CSM-022 — Silent Error Swallowing
- File:
lib/bds/scripting.ex:64-66 - What:
execute_macro/4returns{: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/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-024 — 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-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 (
mainLanguageand 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 theLiquex.FileSystemimpl, Z. 43-49). Between check and read the file can vanish. - Fix: Just try to read and handle
{:error, :enoent}. Remove theEnum.findexistence 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/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-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/rescuearound expected failures with non-bang functions andwithchains.
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) orMap.fetch!/2if 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.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-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/1sets process dictionary (Process.put(@key, locale)) for UI locale. Used inShellLive.render(Z. 550) andMenuBar. - 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_localeis 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 firsthandle_call(Z. 41) have@impl true. The remaininghandle_callclauses at Z. 46, 61, 71, 75 lack it. - 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.
- CSM-001: Fixed. All
String.to_atomon dynamic data replaced withMapUtils.safe_atomize_key/keysorString.to_existing_atom. - CSM-002: Fixed. Search now pushes all filtering and pagination into SQL via Ecto queries and CTEs.
- CSM-004: Fixed.
attach_runnermoved tohandle_continue,terminate/2added for cleanup,restart: :temporaryset, JobStoredetach_runnerbug fixed.
- CSM-001: Fixed. All
- All high-severity items (CSM-006 to CSM-010) have been addressed.
- CSM-006: Fixed. Batch INSERT for reindexing, preloaded post records for rendering.
- CSM-007: Fixed. Decomposed into refresh_layout, refresh_sidebar, refresh_content, reload_shell.
- CSM-008: Fixed. Panel data pre-computed in event handlers, tab meta skips DB for complete entries.
- CSM-009: Fixed. All bang Image/File variants replaced with error-tuple handling,
ensure_thumbnailsreturns{:error, _}instead of crashing. - CSM-010: Fixed. Replaced rescue blocks with
Repo.ready?/0probe and{:ok, _}/{:error, :not_ready}tuples.
- 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.mdupdated: fixed items removed, new ones added.