Break up SRP violations in Templates.do_update_template/2 and Capabilities.for_project/2 by extracting domain-specific builder functions and single-responsibility private helpers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
37 KiB
37 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 ✅ FIXED
- Fixed: 2026-05-09
- What was done:
mount/3now reads?view=and?tab=<type>:<id>query params and applies them to the initial workbench state, enabling deep linking on page load.- Added
push_url_state/1— after state-changing events (select_view,select_tab,close_tab,open_sidebar_item, sidebar menu actions, project switch), pushes aurl-stateevent to the client with the serialized URL. - Added JS handler in the
AppShellhook that callshistory.replaceStateto update the browser URL without triggering navigation. - URL encoding:
?view=<sidebar_view>(omitted whenposts, the default) and?tab=<type>:<id>(omitted when no tab is active). Invalid or unknown params are silently ignored. - Used
push_event+history.replaceStateinstead ofpush_patch/handle_paramsto maintain compatibility with existinglive_isolatedtests. - Added 10 tests in
test/bds/csm011_url_state_test.exs: mount with?view=media, mount with default, mount with invalid view, mount with?tab=post:<id>, mount with both params,select_viewpushes url-state,select_viewposts pushes clean URL,select_tabpushes url-state,close_tabremoves tab from URL,open_sidebar_itempushes url-state.
CSM-012 — Desktop File Dialog Blocks Event Handler ✅ FIXED
- Fixed: 2026-05-09
- What was done:
- Replaced synchronous
FilePicker.choose_file/1call inSidebarCreate.create/4for the "media" kind withTask.async, storing the task ref in a newfile_picker_tasksocket assign. - Added
handle_file_picker_result/2private function inShellLivewith clauses for{:ok, _media},:cancel,{:error, %{message: _}}, and{:error, reason}. - Extended the existing
handle_info({ref, result}, socket)andhandle_info({:DOWN, ref, ...}, socket)handlers to match onfile_picker_taskref. - Added
BDS_DESKTOP_AUTOMATIONguard toFilePicker.choose_file/1— returns:cancelimmediately in automation/test mode, preventing native dialogs from opening during tests. - Initialized
file_picker_task: nilassign in mount. - Added 5 tests in
test/bds/csm012_file_picker_async_test.exs: event handler returns within 100ms, LiveView handles other events while task is pending, task completion doesn't crash LiveView, cancel is handled gracefully, error results don't crash LiveView.
- Replaced synchronous
CSM-013 — Bang Functions in Rendering Pipelines ✅ FIXED
- Fixed: 2026-05-09
- What was done:
lib/bds/rendering/filters.ex—render_macro_template:- Replaced
Liquex.parse!withLiquex.parse(non-bang) andcasematch on{:ok, ast}/{:error, reason, line}. - Wrapped
Liquex.render!intry/rescuecatchingLiquex.Errorspecifically (no non-bangrenderexists in Liquex). - Removed broad
rescue _error -> ""— errors now log viaLogger.warningwith template path and reason before returning"".
- Replaced
lib/bds/rendering/template_selection.ex—render_template:Liquex.parsewas already non-bang; addedelseclause to normalize the 3-tuple{:error, reason, line}into{:error, "reason at line N"}.- Wrapped
Liquex.render!intry/rescuecatchingLiquex.Errorspecifically, returning{:error, message}. - Removed broad
rescue error -> {:error, error}.
lib/bds/rendering/post_rendering.ex—post_data_json_value:- Replaced
Jason.encode!withJason.encodeandcasematch — returns"{}"on encode failure instead of crashing.
- Replaced
- Added 5 tests in
test/bds/csm013_bang_rendering_test.exs: template syntax error returns{:error, _}fromrender_template, broken template inrender_post_pagereturns{:error, _},{% break %}render error returns{:error, _}, normal post context produces valid JSON, non-encodable data returns"{}"fallback.
CSM-014 — O(n²) Loops from length/1 Inside Iteration ✅ FIXED
length/1 Inside Iteration- Fixed: 2026-05-09
- What was done:
lib/bds/generation/outputs.ex—build_category_outputs:- Bound
total_pages = length(paginated_posts)andtotal_items = length(posts)before the nested loop. Previously calledlength/14 times per page × language iteration.
- Bound
lib/bds/generation/outputs.ex—build_root_outputs:- Bound
total_items = length(posts)before the loop, reused bypagination_for_page. Previously calledlength(posts)on every page iteration.
- Bound
lib/bds/generation/outputs.ex—build_paginated_archive_outputs:- Bound
total_items = length(posts)before the loop. Previously calledlength(posts)inside the nested page × language loop.
- Bound
lib/bds/rendering/list_archive.ex—build_day_blocks:- Bound
last_index = length(grouped_blocks) - 1before theEnum.map. Previously calledlength(grouped_blocks)on every iteration.
- Bound
lib/bds/publishing.ex—run_upload:- Bound
target_count = max(length(targets), 1)before theEnum.reduce_while. Negligible impact (3 targets) but fixed for consistency.
- Bound
lib/bds/ui/sidebar.exacc.draft ++ [post]was already fixed by CSM-005 (replaced withEnum.group_by).- Added 3 tests in
test/bds/csm014_length_in_loop_test.exs: multi-page pagination correctness, single-page pagination correctness, 1000-post linear time completion.
CSM-015 — Missing DB Indexes on Foreign Keys ✅ FIXED
- Fixed: 2026-05-09
- What was done:
- Added migration
20260509145208_add_missing_indexes.exswith indexes for all missing foreign keys and frequently filtered columns:- FK indexes:
media.project_id,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,publish_jobs.project_id. - Filter indexes:
posts.status,posts.published_at,posts.language. - Composite index:
db_notifications(entity_type, entity_id).
- FK indexes:
- Added 12 tests in
test/bds/csm015_missing_indexes_test.exsverifying viaEXPLAIN QUERY PLANthat all indexed columns use index lookups.
- Added migration
CSM-016 — String Concatenation for Paths ✅ FIXED
- Fixed: 2026-05-09
- What was done:
lib/bds/rendering/file_system.ex— Extractedensure_liquid_ext/1usingPath.extname/1to check before appending.liquid, preventing double-extension bugs (e.g."header.liquid.liquid").lib/bds/rendering/metadata.ex—menu_item_hreffor:pagekind now appliesURI.encode/1to the slug (matching the existing:category_archivepattern).href_for_language/1now usesString.trim_trailing(prefix, "/")before appending/to prevent double trailing slashes.lib/bds/rendering/metadata.ex— Addedmenu_items_from_raw/1public function for testability.lib/bds/rendering/links_and_languages.ex—post_path/2fornillanguage now usesPath.join(["/", year, month, day, slug]) <> "/"instead of building withindex.htmlthen stripping it. Language-prefix clause usesString.trim_trailing/2to prevent double slashes.canonical_media_path_by_source_path/1usesPath.join("/", media.file_path)instead of"/" <> file_path.lib/bds/publishing.ex—ensure_trailing_slash/1made public for testability (implementation already correct).- Added 17 tests in
test/bds/csm016_path_concatenation_test.exs: FileSystem extension handling (bare name, double extension, nested paths),href_for_language(empty, with/without trailing slash), menu item href encoding (special chars, plain slugs, category slugs), post_path construction (leading/trailing slashes, no double slashes, language prefix),language_prefix(same/nil/different language),ensure_trailing_slash(without/with trailing slash, empty string).
CSM-017 — send(self(), ...) Component Chatter ✅ FIXED
send(self(), ...) Component Chatter- Fixed: 2026-05-09
- What was done:
- Created
BDS.Desktop.ShellLive.Notify— a single dispatch module that standardizes all parent communication from LiveComponent editors. Provides typed functions:output/3,output/4,tab_meta/4,tab_meta_merge/3,close_tab/2,reload/0,dirty/3,command/2,open_sidebar_item/2, andparent/1(escape hatch for chat-specific messages). - Replaced all 25+
send(self(), ...)calls across 11 editor components withNotify.*calls:post_editor.ex— 13 calls (dirty, tab_meta, close_tab, output)media_editor.ex— 7 calls (dirty, tab_meta, output)chat_editor.ex— 15 calls (output, tab_meta, open_sidebar_item, plus chat-specific viaNotify.parent)template_editor.ex— 3 calls (close_tab, output, reload)script_editor.ex— 3 calls (close_tab, output, reload)misc_editor.ex— 4 calls (command, output, tab_meta_merge, open_sidebar_item)settings_editor.ex— 2 calls (output, parent)tags_editor.ex— 2 calls (output, parent)menu_editor.ex— 1 call (output)import_editor.ex— 2 calls (tab_meta, output)overlay_manager.ex— 3 calls (parent for cross-component routing)
- Consolidated Bridges from 30+ editor-specific
handle_infoclauses to 4 generic handlers:{:editor_output, ...},{:editor_tab_meta, ...},{:editor_dirty, ...},{:editor_command, ...}. - Removed 18 editor-specific message atoms from Bridges (
:post_editor_output,:media_editor_output,:post_editor_dirty,:media_editor_dirty,:post_editor_tab_meta, etc.). - Kept chat-specific messages (
{:chat_editor_task_started, ...},{:chat_editor_toggle_sidebar}, etc.) and cross-component routing ({:post_editor_insert_content, ...}) in Bridges since they originate from AI streaming or overlay actions, not from editor self-notification. - Added 24 tests in
test/bds/csm017_component_chatter_test.exs: 11 source-level tests asserting nosend(self(), ...)in any editor file, 1 aggregate test verifying all shell_livesend(self(), ...)calls are innotify.ex, 2 Bridges tests verifying old patterns are gone and new generic handlers exist, 10 Notify API tests verifying each function sends the correct message.
- Created
Low Severity / Code Quality
CSM-018 — @moduledoc false Epidemic ✅ FIXED
@moduledoc false Epidemic- Fixed: 2026-05-10
- What was done:
- Replaced
@moduledoc falsewith descriptive@moduledocstrings in all 12 listed public modules:lib/bds/i18n.ex— language support, locale resolution, flag emoji mappinglib/bds/map_utils.ex— mixed-key map utilities and safe atom conversionlib/bds/bounded_atoms.ex— allow-list-based dynamic atom conversionlib/bds/document_fields.ex— frontmatter field access with key aliaseslib/bds/import_definitions.ex— CRUD for WXR import configurationslib/bds/publishing.ex— GenServer for site upload job coordinationlib/bds/settings.ex— global key-value settings persistencelib/bds/templates.ex— Liquid template lifecycle managementlib/bds/ai.ex— AI endpoint config, secrets, and inference dispatchlib/bds/mcp.ex— MCP server facade for external AI agentslib/bds/scripting/capabilities.ex— Lua scripting capability map builderlib/bds/scripting/api_docs.ex— machine-readable Lua API documentation
- Replaced
CSM-019 — Missing @spec on Public Functions ✅ FIXED
@spec on Public Functions- Fixed: 2026-05-10
- What was done:
- Added
@specannotations to every public function across 25 files in rendering, generation, publishing, UI, and scripting modules. - Added
@type t :: %__MODULE__{}toworkbench.exandfile_system.exto support struct-based specs. - Rendering:
post_rendering.ex,links_and_languages.ex,labels.ex,metadata.ex,file_system.ex,filters.ex,list_archive.ex,template_selection.ex - Generation:
generated_file_hash.ex - Publishing:
publishing.ex - UI:
registry.ex,session.ex,sidebar.ex,menu_bar.ex,commands.ex,dashboard.ex,workbench.ex - Scripting:
job_store.ex,job_runner.ex,job_supervisor.ex,capabilities.ex,capabilities/util.ex,api_docs.ex - Dialyzer passes with 0 errors; all 619 tests pass.
- Added
CSM-020 — Deeply Nested case Instead of with ✅ FIXED
case Instead of with- Fixed: 2026-05-10
- What was done:
lib/bds/import_definitions.ex—delete_definition/1: Replaced nestedcasepiped into anothercasewith a flatwithchain:Repo.get→Repo.delete→{:ok, :deleted}, withelseclauses forniland{:error, _}.lib/bds/publishing.ex—handle_call({:update_job, ...}): Replacedcase Repo.getwithwith %PublishJob{} = job <- Repo.get(...). Also replacedRepo.update!()withRepo.update()to avoid crashes on changeset errors.lib/bds/templates.ex—update_template/2: Replaced outercase Repo.getwithwith+ extracteddo_update_template/2private function. Collapsed three levels of nestedcase(Repo.get → transaction_result → sync_side_effects) into a single flatwithchain.- Added 7 tests in
test/bds/csm020_nested_case_test.exs: delete_definition success and not-found, update_template success and not-found, source-level assertions that all three files usewithinstead of nestedcase.
CSM-021 — cond Where Pattern Matching Suffices ✅ FIXED
cond Where Pattern Matching Suffices- Fixed: 2026-05-11
- What was done:
lib/bds/ai.ex—get_endpoint/2: Replacedcond do is_nil(x) and ...; true -> ... endwith a simpleif/elsesince there are only two branches.lib/bds/scripting/api_docs.ex—example_response_value/1: Extracted"nil"literal match into a separate function head. Replaced remainingcondwithcaseon a tuple of guard results.lib/bds/scripting/api_docs.ex—example_field_value/1: Replacedcondwithcaseon a tuple ofString.contains?/String.ends_with?results.- Added 2 source-level tests in
test/bds/csm021_cond_pattern_match_test.exsasserting nocond doblocks remain in either file.
CSM-022 — Silent Error Swallowing ✅ FIXED
- Fixed: 2026-05-11
- What was done:
execute_macro/4now returns{:error, reason}instead of{:ok, ""}when the underlying script execution fails.- Added
Logger.warning/1call that logs the project ID and error reason before returning the error tuple. - Updated test in
api_test.exsto assert{:error, _reason}instead of{:ok, ""}for failing macros.
CSM-023 — SRP Violations ✅ FIXED
- Fixed: 2026-05-11
- What was done:
lib/bds/templates.ex—do_update_template/2:- Extracted
resolve_next_slug/2— determines slug from attrs or keeps current. - Extracted
content_changed?/2— checks if content attr differs from effective content. - Extracted
resolve_next_status/2— pattern-matched function heads for status transition (published + content change → draft). - Extracted
build_update_attrs/5— assembles the changeset map from resolved values. - Extracted
commit_update_transaction/4— runs the Repo transaction with cascade logic. do_update_template/2is now a concise pipeline: resolve → build → commit → sync.
- Extracted
lib/bds/scripting/capabilities.ex—for_project/2:- Extracted 13 domain-specific builder functions:
app_capabilities/2,project_capabilities/1,meta_capabilities/1,post_capabilities/1,media_capabilities/1,script_capabilities/1,template_capabilities/1,tag_capabilities/1,task_capabilities/0,sync_capabilities/2,publish_capabilities/2,chat_capabilities/1,embedding_capabilities/1. for_project/2is now a 15-line dispatch map.
- Extracted 13 domain-specific builder functions:
- Added 5 tests in
test/bds/csm023_srp_violations_test.exs: source-level assertions for helper extraction in templates, delegation in do_update_template, builder function presence in capabilities, concise for_project body (≤20 lines), no inline capability definitions in for_project.
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.