27 KiB
Elixir Antipatterns Audit (v2)
Incorporates reviewer corrections (v1 listed some already-moduledoced/specced modules as undocumented; mischaracterised git.ex:724; implied shell_live.ex is still monolithic when its event handlers are already delegated to OverlayManager, SidebarEvents, TitlebarMenu etc.; overstated the risk of search.ex:744 whose atom is already bounded by @stemmer_algorithms; missed reusable anchors like MapUtils, Slug, I18n). mix credo --strict passes clean — everything below is a semantic antipattern lint cannot catch.
Static review of lib/ (246 files, ~58k LOC).
H1 — Stray Logger.info leaking raw user input
lib/bds/desktop/shell_live/chat_editor.ex:93 logs socket.assigns.input at info level; :98 is a sibling stray. These two lines violate AGENTS.md and leak raw user input to production logs. Everywhere else the codebase uses Logger.debug with a swallowed convention.
Attack
- Delete
chat_editor.ex:93and:98. grep -rn "Logger\.info(" liband audit every other hit (only legitimate operational events should remain).- Optional: add a credo check banning
Logger.info(inspect(inLiveViewmodules.
Test strategy
No test changes — pure deletion.
H2 — Hardcoded user-facing language-name map
lib/bds/desktop/shell_live/overlay_components.ex:209-217:
defp language_names do
%{"en" => "English", "de" => "Deutsch", "fr" => "Francais", "it" => "Italiano", "es" => "Espanol"}
end
Violates AGENTS.md ("No hardcoded user-facing text. No exceptions.") The repo already has the right central home: BDS.I18n (lib/bds/i18n.ex:1) already holds @supported_languages, flags, format locales, and language metadata.
Chosen semantic: native autonyms
Language labels in the overlay are language self-names ("Deutsch", "Español") — they identify a language, they don't need to be translated into the UI locale. A German user seeing "Español" for Spanish is correct (it's the Spanish self-name), just as @supported_languages already lists flags per language without translating the flag.
Therefore: centralise in BDS.I18n, no Gettext needed. Do not introduce dgettext("ui", "language.<code>") for this — that would create a translation task with no real user benefit.
Attack
- Add
BDS.I18n.language_name/1returning the native autonym for each supported code:en → "English",de → "Deutsch",fr → "Français",it → "Italiano",es → "Español". Keep aligned with@supported_languages. - Replace
language_names/0inoverlay_components.ex:209with aBDS.I18nlookup (build the map fromI18n.supported_languages/0andI18n.language_name/1). - Do NOT run
mix gettext.extractfor this — no newmsgidentries.
Test strategy
Add a test asserting I18n.language_name/1 covers exactly @supported_language_codes and each value is non-empty.
H3 — Shared-helper duplication with semantic drift
Already-existing canonical anchors (reuse these, don't invent new modules first):
BDS.MapUtils.blank_to_nil/1(lib/bds/map_utils.ex:33-35) — used inmedia/file_ops.ex:10viadefdelegate. Currently only handlesniland"". Note on naming:MapUtilsis the existing home forblank_to_nil/1, but new scalar predicates (present?/1,blank?/1,truthy?/1) do not belong in a module called "MapUtils" — they operate on scalar values, not maps. Add a new moduleBDS.Values(orBDS.Booleans) for those, and haveBDS.Values.blank_to_nil/1defdelegatetoBDS.MapUtils.blank_to_nil/1to keep the existing 20 callers untouched. The existingblank_to_nil/1stays inMapUtilsto avoid churn.BDS.Slug.slugify/1(lib/bds/slug.ex:6) — used inposts.ex,templates.ex,posts/slugs.ex. Canonical slugifier.BDS.I18n(lib/bds/i18n.ex) — already centralizes supported-language metadata (see H2).
Inventory
| Helper | defs | Sites | Disagreement | Canonical source |
|---|---|---|---|---|
truthy?/1 |
13 | scripting/capabilities/util.ex:160, ai/catalog.ex:329, chat_editor.ex:584, chat_editor/tool_surfaces.ex:306, post_editor/post_metadata.ex:213, post_editor/draft_management.ex:230, settings_editor/{project,editor,managed_categories,ai}_settings.ex |
1.0? "on"? order of clauses? Adding "on" to ai/catalog.ex is a silent widening — see attack step 1. |
ADD BDS.Values.truthy?/1 (new module — scalar predicates don't belong in MapUtils) |
present?/1 |
8 | ui/sidebar.ex:1128, posts/translation_validation.ex:495, posts/auto_translation.ex:447, import_editor/taxonomy_editing.ex:287, import_editor/analysis_state.ex:313, import_editor.ex:1455, chat_editor.ex:1034, panel_renderer.ex:303 |
whitespace trim? — translation_validation, auto_translation, chat_editor trim; the other 5 do NOT |
ADD BDS.Values.present?/1 (non-trim canonical, matches current blank_to_nil/1) |
blank?/1 |
8 | ui/dashboard.ex:197, post_editor/post_metadata.ex:218, import_editor.ex:1456, chat_editor/model_selection.ex:79, ai/runtime.ex:110, ai/chat_tools.ex:708, ai/chat.ex:938 |
ditto whitespace | ADD BDS.Values.blank?/1 (negation of present?/1) |
blank_to_nil/1 |
14 local + 20 direct MapUtils.blank_to_nil/1 callers |
see grep above | most agree: nil/"" → nil; git.ex:724 adds "\n"; maintenance/diff_computation.ex:33-42 and scripting/capabilities/util.ex:166-172 trim; the rest don't. MapUtils version is non-trimming and load-bearing — do not mutate. |
REUSE BDS.MapUtils.blank_to_nil/1 as-is; add trim_or_nil/1 separately if needed |
pad2/1 |
4 | scripting/capabilities/util.ex:163, post_editor/post_metadata.ex:207, overlay_components.ex:408, rendering/links_and_languages.ex:141 |
identical bodies | NEW BDS.Strings.pad2/1 |
slugify/1 |
2 custom | rendering/filters.ex:74 (Liquex filter, leave), overlay_components.ex:410 (delete; reuse BDS.Slug.slugify/1) |
— | REUSE BDS.Slug.slugify/1 |
Required deliverable before any deletion
A table per helper of the form "site → semantic that site actually relies on → chosen canonical". Without it, deduping present?/1 silently changes whitespace behaviour at the trim-using sites (and vice versa).
⚠ WARNING — MapUtils.blank_to_nil/1 semantic change is a repo-wide migration, not a dedup
BDS.MapUtils.blank_to_nil/1 (map_utils.ex:33-35) currently does no trimming — it maps nil → nil, "" → nil, anything-else → as-is. It has 20 direct callers across tags.ex, import_editor/*, ai/chat_tools.ex, ai/chat_tool_query_helpers.ex, wxr_parser.ex, import_analysis.ex, and a defdelegate in media/file_ops.ex:10. Several of those callers already chain |> String.trim() |> BDS.MapUtils.blank_to_nil() (e.g. taxonomy_editing.ex:153,232), which proves the current non-trimming contract is load-bearing — callers compose trimming themselves.
Changing map_utils.ex:33 to trim globally would double-trim at those sites and silently alter whitespace behaviour at every other caller. This is a migration, not a canonicalization.
Attack
- Do not touch
BDS.MapUtils.blank_to_nil/1semantics. Leave its current contract (non-trimming) intact. - Create a new module
BDS.Valuesfor scalar predicates (do not add them toMapUtils— they're not map utilities). HaveBDS.Values.blank_to_nil/1defdelegatetoBDS.MapUtils.blank_to_nil/1to keep existing 20 callers untouched:BDS.Values.present?/1→blank_to_nil(v) != nil(non-trim semantic — matches the currentblank_to_nil/1contract and the 5 non-trimpresent?sites).BDS.Values.blank?/1→blank_to_nil(v) == nil(negation).BDS.Values.truthy?/1→ canonical accept settrue, "true", "on", 1, 1.0, "1".- Optional
BDS.Values.trim_or_nil/1for the 3 sites that want trim-with-nil-empty — let those call sites composeString.trim()+blank_to_nil/1explicitly (astaxonomy_editing.exalready does), do not force a new helper on them.
truthy?/1accept-set widening is a real semantic change atai/catalog.ex:329. Current local:value in [true, "true", 1, "1"]. Canonical adds"on"and1.0. Thecatalog.ex:114-116call sites feedtruthy?(BDS.MapUtils.attr(attrs, :supports_attachment))etc. whereattrscomes from external catalog config.- Verify that catalog config never produces
"on"or1.0for those fields. If it can, EITHER keepcatalog.ex:329's localdefpwith its narrower accept set, OR prove via test that widening is safe (e.g. upstream already normalizes totrue/"true"/1). - Do NOT blanket-alias
BDS.Values.truthy?/1atcatalog.exwithout that verification. The "check callers" caveat is load-bearing.
- Verify that catalog config never produces
- Migrate sites in two groups — don't blanket-delete. The local
defp's split by semantic:- Non-trim sites (matches canonical
BDS.Values.present?/1):ui/sidebar.ex:1128,import_editor/taxonomy_editing.ex:287,import_editor/analysis_state.ex:313,import_editor.ex:1455,panel_renderer.ex:303— delete localdefp,alias BDS.Values. Same forblank?/1non-trim sites:ui/dashboard.ex:197,import_editor.ex:1456,ai/runtime.ex:110,ai/chat.ex:938. - Trim sites — DO NOT migrate to
BDS.Values.present?/1without a deliberate behavior change. These rely onString.trimsemantics:posts/auto_translation.ex:447(present?/1trims)posts/translation_validation.ex:495(present?/1trims)chat_editor.ex:1034(present?/1trims)chat_editor/model_selection.ex:79(blank?/1trims)ai/chat_tools.ex:708(blank?/1trims withto_string)post_editor/post_metadata.ex:218(blank?/1defers to localblank_to_nil/1that may trim — verify) For these sites either: (a) leave the localdefpin place (safe default — no behavior change), or (b) migrate callers toString.trim(value) |> BDS.Values.blank_to_nil(value) != nilinline, with a test proving the trim behavior is preserved. Do NOT aliasBDS.Values.present?/1at these sites —present?(" ")returns true under the canonical, false under the current local. That is a silent semantics change.
truthy?/1sites — apply step 2's catalog.ex check to all 13 sites before blanket-aliasing, since each may have its own narrower accept set (e.g.ai/catalog.ex:329omits"on").
- Non-trim sites (matches canonical
- Honest framing: this is a partial dedup. After the migration, the canonical
BDS.Values.present?/1/blank?/1/truthy?/1exists, but several localdefps survive at the trim sites and at sites whose accept-set differs from canonical. The goal is consolidation where safe, not full convergence — full convergence would require touching every behavior boundary. Don't oversell the payoff — this is medium-effort/high-risk dedup of one-line predicates. - Add
BDS.Strings.pad2/1(orBDS.Formatting.pad2/1) — single 4-line implementation. Delete 4 local copies. overlay_components.ex:410localslugify/1→ replace withBDS.Slug.slugify/1.- Leave
git.ex:724,maintenance/diff_computation.ex:33,scripting/capabilities/util.ex:166as-is for now — they're internaldefps whose callers expect their specific trim/newline behavior. File a follow-up to migrate them toBDS.Values.trim_or_nil/1if convergence is desired later.
Test strategy
- New unit tests for canonical helpers covering:
nil,""," ","\n","x",0,1,1.0,true,false,"true","on","1". - Crucial:
present?(" ")andpresent?("\n")return true with the non-trim canonical (matching the currentMapUtils.blank_to_nil/1contract). A change here means every call site that expected" "to be blank needs migration — that is the migration mentioned above. - After deleting a local
defp, grep same-name to confirm no latent callers (somedefpshadowdeffrom a sibling module — careful withscripting/capabilities/util.ex:160which isdef, notdefp). mix testgreen. Test the post-field trim sites specifically (posts/auto_translation.ex:447,posts/translation_validation.ex:495) — they currently trim, and switching them to a non-trimmingpresent?/1changes their semantics on whitespace strings.
H4 — try/rescue for control flow and error swallowing
Most rescue clauses are legitimate. Categorised:
Problem (rewrite to case/with)
lib/bds/desktop/shell_live/overlay_components.ex:
:71-77,:201-205,:257-261,:296-300,:328-338,:351-361,:390-400—{:ok, _} = Metadata.get_project_metadata(...)followed by broadrescue error ->. The rescue catches aMatchErrorfrom a{:error, _}return — should becase/withover the{:ok, _} | {:error, _}shape.
Over-defensive DB rescues (remove or narrow to :error log)
lib/bds/desktop/shell_live/overlay_components.ex:
:142,:157,:170,:190,:257(theRepo.allones) — wrap plain queries in rescue → return[]/%{}. A dropped table or lock is silently swallowed. Either drop the rescue and let it propagate, or log at:error(not:warning) and return{:error, _}up the call chain.
Caller-expected fallback contracts (per function)
An agent removing a rescue needs to know what the caller expects on failure. These are the current observed fallbacks — preserve them unless the caller is also rewritten:
| Function | Line | Current fallback | Replace rescue with |
|---|---|---|---|
project_metadata/1 |
:71 |
%{main_language: "en", blog_languages: []} |
case/with returning the same map on {:error, _} |
source_language/2 (post) |
:201 |
metadata.main_language || "en" |
case returning same on error |
source_language/2 (media) |
:201 |
metadata.main_language || "en" |
same |
ai_fields/2 (post) |
:257 |
[] |
case returning [] on error |
ai_fields/2 (media) |
:296 |
[] |
case returning [] on error |
delete_details/2 (media) |
:328 |
%{title: dgettext("ui","Delete Media"), …, reference_list: []} |
case returning same on error |
delete_details/2 (tags) |
:351 |
%{title: dgettext("ui","Delete Tag"), …} |
case returning same on error |
merge_details/2 |
:390 |
%{target: "tag", count: 1, …} |
case returning same on error |
The 5 Repo.all rescues (:142, :157, :170, :190, :257) currently fall back to [] or %{}. If the live template already treats empty list as "no data" gracefully, the fallback is fine — but the failure should not be silent. Recommended: log at :error before returning the fallback so a DB/schema issue shows up in logs.
Allow-list (leave alone)
preview.ex:246—rescue Ecto.NoResultsError -> {:error, :not_found}. Genuinely narrow: single named exception type, expected control-flow case (post not found). Legitimate.shutdown.ex(10×),main_window.ex,automation.ex,deep_link.ex,shell_commands.ex:630,814— fire-and-forget teardown;Logger.debug("swallowed ...")then:ok. These explicitly tolerate:exit/:noprocduring process shutdown and re-running them is racy. Legitimate.
NOT on allow-list (re-examine)
embeddings/index.ex:348—rescue exception -> Logger.debug("swallowed embeddings index persist error..."); :ok. Broad rescue wrappingHNSWLib.Index.save_index/2+File.mkdir_p!. A persist failure is silently swallowed and:okreturned — caller believes the index was saved. Operationally tolerable only because the next reindex will retry; should at minimum log at:errorand ideally surface{:error, _}. Re-examine: not a blanket "leave alone".embeddings/index.ex:393— similar broad rescue around index load. Same caveat.embeddings/backends/neural.ex:110—rescue exception -> {:reply, {:error, Exception.message(exception)}, state}. Broad rescue aroundNx.Serving.run. Unlike the silent-default cases above, this one does surface{:error, _}to the caller (good) — it's not a silent swallow. The reason it's still on the rework list is that the broadrescue exception ->catches programmer bugs (FunctionClauseError,MatchError) and repackages them as{:error, "…"}alongside genuine Nx runtime errors, hiding the distinction. Re-examine: restrict to expected Nx/serving exception types so programmer bugs surface unmodified.
To find: grep -rn "rescue" lib.
Attack
- For each of the 7
overlay_components.ex{:ok, _} =rescues, rewrite as:Drop the rescue.case Metadata.get_project_metadata(project_id) do {:ok, metadata} -> metadata {:error, _reason} -> %{main_language: "en", blog_languages: []} end - For the 5
Repo.allrescues (overlay_components.ex:142,157,170,190,257): preserve the documented[]/%{}fallbacks — the caller templates consume them directly. LettingRepo.allpropagate is a behavior change and NOT the default repair. The correct minimal repair is:- narrow the rescue to a single named exception type if possible (
Ecto.QueryError,DBConnection.ConnectionError), AND - upgrade the log from
Logger.warningtoLogger.errorso silent DB/schema failures are visible, AND - keep the existing fallback return (
[]or%{}) per the table above. Only if a caller is also being rewritten to accept{:error, _}(and has a test for it) should the rescue be dropped in favor of propagation. Introducing a repo-wideBDS.RepoHelpers.safe_all/2is one option, not the prescribed repair — the audit does not establish enough callers want this wrapping to justify a new abstraction; keep any wrapping local unless a second use site appears.
- narrow the rescue to a single named exception type if possible (
- Keep
log_overlay_warning/2(or move into a sharedBDS.Loggingmodule — see H3 territory).
Test strategy
- Each rewritten function needs a test for the
{:error, _}branch — currently impossible because ofrescue. Use mox/mocks againstMetadataandRepo. - For over-defensive DB rescues, test the legitimate DB error path returns
{:error, _}to the caller (or surfaces it).
Rescue allow-list reference (tightened)
- Single named exception type (
Ecto.NoResultsError,File.Error,ArgumentError) — OK. - Fire-and-forget teardown in shutdown/process-kill paths with
Logger.debug("swallowed ...")+:ok— OK. - Broad
rescue exception ->(orrescue error ->) returning a silent default (e.g.:ok,[],%{}) — NOT OK, even if the operation is "background". At minimum log at:errorso programmer bugs (MatchError,FunctionClauseError) are visible.index.ex:348,393falls in this category. - Broad
rescue exception ->that does surface{:error, _}(e.g.neural.ex:110) — borderline. The failure isn't silent, but a programmer bug is indistinguishable from a genuine runtime error after repackaging. Restrict to expected exception types rather than blanket-leaving.
M1 — God modules: complete the decomposition that's already underway
Decomposition has already started. Acknowledge what exists, finish the rest.
Already extracted (don't redo):
lib/bds/desktop/shell_live/overlay_manager.ex—shell_live.ex:361-410already delegates all 15overlay_*events here.lib/bds/desktop/shell_live/sidebar_events.ex— sidebar filter events.lib/bds/desktop/shell_live/titlebar_menu.ex— titlebar menu state/logic.lib/bds/desktop/shell_live/bridges.ex,git_handler.ex,chat_editor.ex,import_editor.ex,media_editor.ex,post_editor.ex,misc_editor.ex,settings_editor.ex,script_editor.ex,template_editor.ex,tags_editor.ex,menu_editor.ex— body already lives in these.
Still routed through shell_live.ex:183-547 (~55 handle_event clauses). The remaining forwarders cluster cleanly:
- Project cluster (
shell_live.ex:449-481):toggle_project_menu,close_project_menu,select_project,create_project,import_project— extract toBDS.Desktop.ShellLive.ProjectEvents. - Language cluster (
shell_live.ex:514-518):change_ui_language,sync_ui_language— extract toBDS.Desktop.ShellLive.LocaleEvents(and tie toUILocale— see M2). - Native menu cluster (
shell_live.ex:527-547):native_menu_action,titlebar_menu_keydown,toggle_titlebar_menu,hover_titlebar_menu,close_titlebar_menu,titlebar_menu_action— extract toBDS.Desktop.ShellLive.NativeMenuEvents(already co-located withtitlebar_menu.ex). - Tab/workspace cluster (
:196-302): view/panel/tab/sidebar-item — extract toBDS.Desktop.ShellLive.WorkspaceEvents.
What stays in shell_live.ex: mount, handle_info, terminate, and a small handful of top-level routing events.
Out-of-scope decompositions (large but not urgent — touch when working nearby):
lib/bds/scripting/api_docs.ex(1554 LOC) — splitRenderer/Generator/DataStructureswhen scripting docs change next.lib/bds/ui/sidebar.ex(1129) — splitSidebar.FiltersfromSidebar.Viewopportunistically.lib/bds/ai/chat.ex(939) — orchestration; leave unless actively edited.
Attack
- Pick one cluster. Create the module, move the matching
handle_eventclauses verbatim, leave a single delegatinghandle_eventinshell_live.exper event (same pattern as:361-410). - Verify with the existing LiveView test suite after each cluster.
Test strategy
Existing LiveView tests (live/2) exercise events by name — they should pass unchanged after delegation.
M2 — Process dictionary as contained global
lib/bds/desktop/ui_locale.ex is the documented sole owner of Process.put(:bds_ui_locale, _). The module explicitly comments that direct use elsewhere is forbidden. This is not a present defect — it's a caution for future work.
Attack
- No retrofit needed.
- For NEW callers needing the locale, prefer passing it explicitly via assigns or a
%Locale{}struct rather than expandingProcess.putcallers. - Consider a dialyzer / credo custom check rejecting new
Process.put(:bds_ui_locale, _)outsideUiLocale.
Priority: Informational. No action required unless new callers appear.
M3 — Enum.at brittle index use
Real problems (rewrite)
lib/bds/import_analysis.ex:428-429:Rewrite withkey = Enum.at(captures, 1) value = Enum.at(captures, 2) || Enum.at(captures, 3) || Enum.at(captures, 4) || ""Regex.named_captures/2or pattern matching on the captured groups.lib/bds/rendering/filters.ex:241-253—match |> Enum.at(1)repeated 4× with identical shape. Hoist into a helper and pattern match.
Style only (drop from report)
titlebar_menu.ex== nilvsis_nil/1inconsistency — cosmetic.
To find: grep -rn "Enum\.at\|Enum\.find_index\|Enum\.find_value" lib.
Attack
import_analysis.ex:428-429→ switch regex to named groups, return map, pattern-match defaults.rendering/filters.ex:241-253→ introduceslug_from_match/1private helper, reuse across 4 call sites.
Test strategy
Tests for: empty captures list, regex without the expected group, full match. Existing tests should still pass.
M4 — Documentation debt
Many modules still carry @moduledoc false, including public-API modules. The reviewer correctly points out:
templates.ex:1already has a real@moduledoc """...""".tasks.ex:13is heavily@type/@spec-ed.tags.ex:18andposts.ex:64have broad public@speccoverage.
So the v1 claim "no specs" was wrong for those four. The genuine gap:
- Missing moduledoc on API modules:
posts.ex:1,media.ex:1,preview.ex:1,ai/chat.ex:1,embeddings.ex:1,generation.ex:1,git.ex:1,tags.ex:1. Verify each individually before flipping. - Missing
@specon a few remaining publicdefs inpreview.ex:1— spot-apply, don't bulk-generate. (Note:metadata.ex:1is fully specced — 9/9 public defs have@spec. Removed from the gap list.) - CI gap:
mix.exsdoes not currently includeex_doc. "Runmix docs" is therefore not a usable verification path untilex_docis wired.
Attack
- Prerequisite: add
:ex_doctomix.exsdeps(dev-only). Configuremix docs --warnings-as-errorsin CI. - Audit
grep -rn "@moduledoc false" lib. For each module that isaliased from outside its own subtree, replace@moduledoc falsewith a real@moduledoc """..."""describing intent and contract. Keepfalsefor*HTML,*Components,Web.Live.*view plumbing, and similar internal modules. - Add missing
@specto the public-API functions flagged bymix docs --warnings-as-errors. - Do NOT bulk-add specs to private helpers — knock-on churn.
Priority: Medium, after ex_doc is wired. Documentation debt, not a high-priority defect.
Test strategy
CI gate via mix docs --warnings-as-errors once ex_doc is added.
L1 — cond do ... true -> repetition
Mostly cosmetic. Example in chat_editor.ex:945-949 re-fetches Map.get(surface, :title) thrice. Opportunistic cleanup, not audit-worthy. Touch when working nearby.
Priority: Low. No dedicated task.
L2 — apply/3 with a parsed atom (informational only)
lib/bds/search.ex:743-744 already bounds the atom via Map.fetch(@stemmer_algorithms, normalize_language(language)) before apply(Stemex, algorithm, [token]). Not a dynamic-dispatch hazard in current code. Optional improvement: add @allowed_stemmer_atoms Map.values(@stemmer_algorithms) and a guard for defense-in-depth when the list changes.
Priority: Low. No dedicated task.
Suggested fix priority (re-ranked)
| # | Item | Effort | Risk |
|---|---|---|---|
| 1 | H1 delete stray Logger.info in chat_editor.ex:93,98 |
tiny | none |
| 2 | H2 externalise language_names into BDS.I18n as native autonyms (no gettext) |
small | low |
| 3 | H4 rewrite {:ok,_}=…rescue in overlay_components.ex; narrow DB rescues (preserve fallbacks) |
medium | medium |
| 4 | M1 extract remaining shell_live.ex event clusters (project / locale / native-menu / workspace) |
medium | low (delegation preserves behaviour) |
| 5 | M3 Enum.at brittle cases in import_analysis.ex:428 and rendering/filters.ex:241 |
small | low |
| 6 | M4 wire ex_doc; flip public-API @moduledoc false; close @spec gaps |
medium | none |
| 7 | H3 consolidate helpers into new BDS.Values (with semantic inventory) — last because: medium effort, high risk for one-liner predicates, partial dedup payoff |
medium | high (trim vs non-trim boundary + truthy accept-set widening at catalog.ex) |
Schedule L1/L2 opportunistically when touching those files.
Required verification after each task
Per AGENTS.md, a task is not done until all five are green:
mix test # or: xvfb-run mix test on headless Linux
mix compile --warnings-as-errors
mix credo --strict
mix deps.audit
mix dialyzer