26 KiB
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
- 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
- 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, thenapply_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) andTags.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 viaEnum.reduce.Posts.dashboard_stats/1(posts.ex:374-394) loads all post statuses and counts in Elixir.
- Fix:
- Sidebar: Use
Repo.aggregatefor counts,whereclauses for filters,limit/offsetfor pagination. Preload tag colors separately. - Tags: Use
where: fragment("? IN (?)", ^tag_name, post.tags)or JSON functions. Forpost_tag_names, useRepo.aggregate+distinct. - Dashboard: Use
Repo.aggregate(:count)withgroup_byfor status counts, media counts, tag clouds, and category counts. No need to load full records. dashboard_stats: Replace withfrom post in Post, where: post.project_id == ^project_id, group_by: post.status, select: {post.status, count(post.id)}.
- Sidebar: Use
- 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) callsinsert_post_index/1per post insideEnum.each;insert_post_indexcallspost_index_fields/1(Z. 268) which callspost_translations/1(Z. 494) — one query per post for translations. Same for media reindexing.ListArchive(Z. 181) andPostRendering(Z. 21) callload_post_record/1per-post insideEnum.map, which doesRepo.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!/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. This function is called from mosthandle_eventandhandle_infocallbacks, 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 datarefresh_dashboard/2— only queries dashboard datarefresh_git_badge/2— only queries git statusrefresh_task_status/2— only queries task staterefresh_tab_meta/2— only syncs tab metadata Eachhandle_event/handle_infoshould 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-209—post_link_entries/1callsPostLinks.list_incoming_links/1andPosts.get_post/1per link during render.panel_renderer.ex:229-240—git_log_entries/1callsGit.file_history/2andPosts.get_post/1during render.tab_helpers.ex:120-135—derived_tab_meta/1queries 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 decomposedreload_shellupdaters 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) andImage.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) andImage.flatten!(Z. 158) are also bang variants.
- Note on scheduler blocking: The
Imagelibrary 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 uncheckedFile.mkdir_p. - Fix:
- Replace
File.mkdir_p/1withcase File.mkdir_p(dir) do :ok -> ...; {:error, reason} -> {:error, reason}. - Replace bang
Image.thumbnail!withImage.thumbnailand handle{:error, _}. - Replace bang
Image.write!withImage.writeand handle{:error, _}. - Wrap the whole
write_all_thumbnailsin a try/return-error-tuple pattern.
- Replace
- Test: Feed a corrupt image; assert
ensure_thumbnailsreturns{: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.OwnershipErrorto return empty defaults inproject_snapshot/0,dashboard/1,sidebar_view/3, andgit_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?/0check (orEcto.Adapters.SQL.query!/2with a lightweight probe) before calling the data functions. Return{:ok, result}/{:error, :not_ready}tuples from the lowest-level callers and handle them upstream. Onlyrescuefor truly unexpected DB errors. - Test: Call
project_snapshot/0with 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/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-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.