Mix aliases should only compose mix tasks. Shell tool orchestration
(shellcheck, fd) belongs in mise scripts. This removes the mix cmd
escape hatch and makes the boundary clear: mix.exs for pure mix
composition, scripts/ for cross-toolchain orchestration.
High priority:
- Auto-format on edit (.claude/hooks/format-elixir.sh): runs mix format
synchronously after every Edit/Write on .ex, .exs, and .heex files.
Ensures code is formatted before the next read or tool call.
- mix test permissions: added Bash(mix test) (bare) and Bash(mix test *)
(with path/flags) to the allow list. Previously only Bash(mix test:*)
was allowed, which didn't match common invocations.
- Slash commands: /precommit runs the full mix precommit pipeline and
reports failures with fix suggestions. /oban-status queries Oban job
state via Tidewave SQL, grouped by state and worker.
Medium priority:
- Chrome DevTools permissions: added 8 new tools (select_page,
list_network_requests, get_network_request, list_console_messages,
get_console_message, hover, press_key, wait_for) for debugging API
traffic, JS hook errors, hover states, and keyboard navigation.
- Gettext extraction hook (.claude/hooks/gettext-extract.sh): runs
mix gettext.extract --merge asynchronously after editing .heex files.
Async because it's project-wide and doesn't need to block the next edit.
- Sobelow permission: added Bash(mix sobelow:*) to the allow list for
independent security checks.
Three independent refactors of lib/music_library/listening_stats.ex,
measured against the dev DB (104k tracks) via bench/listening_stats.exs:
| Query | Baseline | After | Speedup |
|---------------------------------|-----------|-----------|---------|
| recent_activity(tz, 100) | 38.98 ms | 2.87 ms | 13.6x |
| list_tracks(page 1, 200) | 47.30 ms | 4.81 ms | 9.8x |
| get_top_artists_by_days(7) | 66.51 ms | 1.02 ms | 65x |
| get_top_artists_by_days(30) | 67.41 ms | 2.84 ms | 23.7x |
| get_top_artists_by_days(365) | 76.03 ms | 25.57 ms | 3.0x |
| get_top_albums_by_days(7) | 92.63 ms | 1.61 ms | 57.5x |
| get_top_albums_by_days(30) | 94.52 ms | 3.69 ms | 25.6x |
| get_top_albums_by_days(365) | 105.23 ms | 32.81 ms | 3.2x |
| get_top_artists(limit: 10) | 123.96 ms | 101.68 ms | 1.22x |
| get_top_albums(limit: 10) | 209.64 ms | 108.23 ms | 1.94x |
tracks_with_record_info_query/0 now uses correlated scalar subqueries
against record_releases and artist_records instead of materializing
helper subqueries on every call. The cost scales with the outer LIMIT,
not with the size of record_releases.
top_albums_base_query/0 and top_artists_base_query/0 are replaced by
aggregate_query + attach_metadata pairs. The pattern is aggregate first,
attach metadata second: GROUP BY runs against the raw track scan, then
a tiny outer SELECT attaches record_releases / artist_infos lookups for
the <= 10 result rows via correlated subqueries.
tracks_since_query/1 wraps the date-filtered inner scan with limit: -1.
SQLite cannot flatten a subquery that has a LIMIT, so it materializes
the date-bounded subset and the optimizer uses the timestamp index for
the range scan instead of the album/artist composite index. This trick
replaces SQLite's WITH ... AS MATERIALIZED (which ecto_sqlite3 doesn't
expose).
All json_extract(?, '\$.path') fragments use the canonical form rather
than the equivalent ? ->> '\$.path' shorthand. SQLite's index matcher
requires the GROUP BY expression to match the index expression
textually to use the composite index for natural ordering.
collected_releases_query/0 and wishlisted_releases_query/0 are removed
from Collection and Wishlist — they had a single internal caller that
no longer exists after the refactor.
New regression tests lock the semantics that the optimized queries
must preserve:
- count(DISTINCT scrobbled_at_uts) — 579 duplicate timestamps exist in
the dev DB from rapid Last.fm scrobbles, so replacing with count(*)
would silently change results
- :artist_id key in list_tracks result maps — ScrobbledTracksLive.Index
destructures it even though the template body never references it
Closes#148
The tooltip was using the group representative instead of the
actual record, causing all items in a group to show the same
badge regardless of their own selected_release_id.
Ecto telemetry reporter that captures executed SQL queries
to a log file with interpolated parameters and source
locations. Activated/deactivated at runtime via Tidewave
for LLM-driven query analysis workflows.
Adds context about dependency categories, major version bump
assessment, bulk vs selective update options, asset build
verification, and commit message conventions.
Remove redundant sections from production-infrastructure.md that
duplicated architecture.md (Oban queues/plugins/cron, external API
integrations, SQLite extensions, scheduled maintenance). Absorb API
rate limits into architecture.md's integration table. Replace tool
versions table with pointer to mise.toml. Add two conventions from
project history: scoped test setup and single-path shared components.