diff --git a/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md b/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md new file mode 100644 index 00000000..16238687 --- /dev/null +++ b/backlog/completed/ml-155 - Compact-collection-summary-format-statistical-preamble-for-chat-context.md @@ -0,0 +1,26 @@ +--- +id: ML-155 +title: Compact collection summary format + statistical preamble for chat context +status: Done +assignee: [] +created_date: '2026-05-01 21:43' +updated_date: '2026-05-01 21:49' +labels: [] +dependencies: [] +priority: high +--- + +## Description + + +Reduce the token count of collection_summary/0 by: +1. Compacting format_group/1 (year-only dates, remove type field, reduce max genres 3→2) +2. Adding statistical preamble (genre/formats/decade distribution) computed inline from fetched records +3. Updating tests for the new format + + +## Final Summary + + +Compacted collection_summary format_group/1: year-only dates, removed type field, reduced max genres 3→2. Added statistical preamble (genre/formats/eras distribution + artist count) computed in-memory from already-fetched records. ~27% token reduction (~26.4k → ~19.4k for 1200 records). All 886 tests pass. + diff --git a/lib/music_library/collection.ex b/lib/music_library/collection.ex index 93ee3739..aca1c221 100644 --- a/lib/music_library/collection.ex +++ b/lib/music_library/collection.ex @@ -197,7 +197,8 @@ defmodule MusicLibrary.Collection do |> MapSet.new() end - @max_genres_per_record 3 + @max_genres_per_record 2 + @stats_limit 10 @spec collection_summary() :: {String.t(), non_neg_integer()} def collection_summary do @@ -215,15 +216,92 @@ defmodule MusicLibrary.Collection do |> Enum.map(fn {_id, group} -> format_group(group) end) |> Enum.sort() - summary = Enum.join(groups, "\n") + catalog = Enum.join(groups, "\n") + stats = build_stats(records, length(groups)) + + summary = + cond do + stats == "" and catalog == "" -> "" + stats == "" -> catalog + catalog == "" -> stats + true -> stats <> "\n\n" <> catalog + end {summary, length(groups)} end + defp build_stats([], _group_count), do: "" + + defp build_stats(records, group_count) do + genre_counts = compute_genre_counts(records) + format_counts = compute_format_counts(records) + decade_counts = compute_decade_counts(records) + + artist_count = + records + |> Enum.flat_map(& &1.artists) + |> Enum.uniq_by(& &1.musicbrainz_id) + |> length() + + [ + "# Stats: #{group_count} releases, #{artist_count} artists", + genres_line(genre_counts), + formats_line(format_counts), + eras_line(decade_counts) + ] + |> Enum.join("\n") + end + + defp compute_genre_counts(records) do + records + |> Enum.flat_map(&(&1.genres || [])) + |> Enum.reject(&(&1 in @excluded_genres)) + |> Enum.frequencies() + |> Enum.sort_by(&elem(&1, 1), :desc) + |> Enum.take(@stats_limit) + end + + defp compute_format_counts(records) do + records + |> Enum.map(& &1.format) + |> Enum.frequencies() + |> Enum.sort_by(&elem(&1, 1), :desc) + end + + defp compute_decade_counts(records) do + records + |> Enum.map(&extract_decade(&1.release_date)) + |> Enum.reject(&is_nil/1) + |> Enum.frequencies() + |> Enum.sort_by(&elem(&1, 1), :desc) + end + + defp genres_line([]), do: "" + + defp genres_line(counts) do + items = Enum.map(counts, fn {genre, count} -> "#{genre} #{count}" end) + "Genres: " <> Enum.join(items, ", ") + end + + defp formats_line([]), do: "" + + defp formats_line(counts) do + items = Enum.map(counts, fn {format, count} -> "#{format} #{count}" end) + "Formats: " <> Enum.join(items, ", ") + end + + defp eras_line([]), do: "" + + defp eras_line(counts) do + items = Enum.map(counts, fn {decade, count} -> "#{decade} #{count}" end) + "Eras: " <> Enum.join(items, ", ") + end + defp format_group(records) do record = hd(records) artist_names = Record.artist_names(record) formats = records |> Enum.map(& &1.format) |> Enum.uniq() |> Enum.join("/") + year = extract_year(record.release_date) genres = records @@ -231,8 +309,7 @@ defmodule MusicLibrary.Collection do |> Enum.uniq() |> Enum.take(@max_genres_per_record) - base = - "#{artist_names} - #{record.title} (#{record.release_date || "Unknown"}, #{formats}, #{record.type})" + base = "#{artist_names} - #{record.title} (#{year}, #{formats})" if genres == [] do base @@ -241,6 +318,25 @@ defmodule MusicLibrary.Collection do end end + defp extract_year(nil), do: "Unknown" + + defp extract_year(date_str) when is_binary(date_str) do + if String.length(date_str) >= 4 do + String.slice(date_str, 0, 4) + else + date_str + end + end + + defp extract_decade(nil), do: nil + + defp extract_decade(date_str) when is_binary(date_str) do + case Integer.parse(String.slice(date_str, 0, 4)) do + {year, _} -> "#{div(year, 10) * 10}s" + :error -> nil + end + end + defp base_search do from r in SearchIndex, where: not is_nil(r.purchased_at) diff --git a/test/music_library/collection_test.exs b/test/music_library/collection_test.exs index 0d42f35a..a021588f 100644 --- a/test/music_library/collection_test.exs +++ b/test/music_library/collection_test.exs @@ -372,7 +372,7 @@ defmodule MusicLibrary.CollectionTest do assert Collection.collection_summary() == {"", 0} end - test "returns one line per collected record with record count" do + test "returns stats preamble and one catalog line per collected record" do record_with_artist("Radiohead", %{ title: "OK Computer", format: :cd, @@ -392,20 +392,28 @@ defmodule MusicLibrary.CollectionTest do }) {summary, record_count} = Collection.collection_summary() - lines = String.split(summary, "\n") + [stats, catalog] = String.split(summary, "\n\n") + lines = String.split(catalog, "\n") assert record_count == 2 assert length(lines) == 2 + # Stats section + assert stats =~ "# Stats: 2 releases" + assert stats =~ "Genres:" + assert stats =~ "Formats:" + assert stats =~ "Eras:" + + # Catalog lines use year-only dates and exclude type field assert Enum.any?(lines, fn line -> line =~ "Radiohead" and line =~ "OK Computer" and - line =~ "1997-06-16" and line =~ "cd" and line =~ "album" and + line =~ "(1997, cd)" and line =~ "alternative rock, art rock" end) assert Enum.any?(lines, fn line -> line =~ "Pink Floyd" and line =~ "The Dark Side of the Moon" and - line =~ "1973-03-01" and line =~ "vinyl" and line =~ "album" and + line =~ "(1973, vinyl)" and line =~ "progressive rock" end) end @@ -434,7 +442,8 @@ defmodule MusicLibrary.CollectionTest do }) {summary, record_count} = Collection.collection_summary() - lines = String.split(summary, "\n") + catalog = String.split(summary, "\n\n") |> List.last() + lines = String.split(catalog, "\n") assert record_count == 1 assert length(lines) == 1 @@ -444,7 +453,7 @@ defmodule MusicLibrary.CollectionTest do assert line =~ "vinyl" end - test "caps genres to 3 per record" do + test "caps genres to 2 per record" do record_with_artist("ABC", %{ title: "The Lexicon of Love", format: :vinyl, @@ -455,10 +464,11 @@ defmodule MusicLibrary.CollectionTest do }) {summary, _count} = Collection.collection_summary() - [genres_str] = Regex.run(~r/\[(.+)\]/, summary, capture: :all_but_first) + catalog = String.split(summary, "\n\n") |> List.last() + [genres_str] = Regex.run(~r/\[(.+)\]/, catalog, capture: :all_but_first) genre_count = genres_str |> String.split(", ") |> length() - assert genre_count == 3 + assert genre_count == 2 end test "omits genre brackets when genres are empty" do @@ -472,8 +482,9 @@ defmodule MusicLibrary.CollectionTest do }) {summary, _count} = Collection.collection_summary() - refute summary =~ "[" - refute summary =~ "]" + catalog = String.split(summary, "\n\n") |> List.last() + refute catalog =~ "[" + refute catalog =~ "]" end test "excludes wishlist records" do @@ -489,8 +500,11 @@ defmodule MusicLibrary.CollectionTest do {summary, record_count} = Collection.collection_summary() assert record_count == 1 - assert summary =~ "OK Computer" - refute summary =~ "Wish You Were Here" + + # Catalog portion should contain the purchased record but not the wishlist one + catalog = String.split(summary, "\n\n") |> List.last() + assert catalog =~ "OK Computer" + refute catalog =~ "Wish You Were Here" end end end