ML-155: Compact collection summary and add stats preamble
This commit is contained in:
+26
@@ -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
|
||||||
|
|
||||||
|
<!-- SECTION:DESCRIPTION:BEGIN -->
|
||||||
|
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
|
||||||
|
<!-- SECTION:DESCRIPTION:END -->
|
||||||
|
|
||||||
|
## Final Summary
|
||||||
|
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
|
||||||
|
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.
|
||||||
|
<!-- SECTION:FINAL_SUMMARY:END -->
|
||||||
@@ -197,7 +197,8 @@ defmodule MusicLibrary.Collection do
|
|||||||
|> MapSet.new()
|
|> MapSet.new()
|
||||||
end
|
end
|
||||||
|
|
||||||
@max_genres_per_record 3
|
@max_genres_per_record 2
|
||||||
|
@stats_limit 10
|
||||||
|
|
||||||
@spec collection_summary() :: {String.t(), non_neg_integer()}
|
@spec collection_summary() :: {String.t(), non_neg_integer()}
|
||||||
def collection_summary do
|
def collection_summary do
|
||||||
@@ -215,15 +216,92 @@ defmodule MusicLibrary.Collection do
|
|||||||
|> Enum.map(fn {_id, group} -> format_group(group) end)
|
|> Enum.map(fn {_id, group} -> format_group(group) end)
|
||||||
|> Enum.sort()
|
|> 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)}
|
{summary, length(groups)}
|
||||||
end
|
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
|
defp format_group(records) do
|
||||||
record = hd(records)
|
record = hd(records)
|
||||||
artist_names = Record.artist_names(record)
|
artist_names = Record.artist_names(record)
|
||||||
formats = records |> Enum.map(& &1.format) |> Enum.uniq() |> Enum.join("/")
|
formats = records |> Enum.map(& &1.format) |> Enum.uniq() |> Enum.join("/")
|
||||||
|
year = extract_year(record.release_date)
|
||||||
|
|
||||||
genres =
|
genres =
|
||||||
records
|
records
|
||||||
@@ -231,8 +309,7 @@ defmodule MusicLibrary.Collection do
|
|||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|> Enum.take(@max_genres_per_record)
|
|> Enum.take(@max_genres_per_record)
|
||||||
|
|
||||||
base =
|
base = "#{artist_names} - #{record.title} (#{year}, #{formats})"
|
||||||
"#{artist_names} - #{record.title} (#{record.release_date || "Unknown"}, #{formats}, #{record.type})"
|
|
||||||
|
|
||||||
if genres == [] do
|
if genres == [] do
|
||||||
base
|
base
|
||||||
@@ -241,6 +318,25 @@ defmodule MusicLibrary.Collection do
|
|||||||
end
|
end
|
||||||
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
|
defp base_search do
|
||||||
from r in SearchIndex,
|
from r in SearchIndex,
|
||||||
where: not is_nil(r.purchased_at)
|
where: not is_nil(r.purchased_at)
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
assert Collection.collection_summary() == {"", 0}
|
assert Collection.collection_summary() == {"", 0}
|
||||||
end
|
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", %{
|
record_with_artist("Radiohead", %{
|
||||||
title: "OK Computer",
|
title: "OK Computer",
|
||||||
format: :cd,
|
format: :cd,
|
||||||
@@ -392,20 +392,28 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
})
|
})
|
||||||
|
|
||||||
{summary, record_count} = Collection.collection_summary()
|
{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 record_count == 2
|
||||||
assert length(lines) == 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 ->
|
assert Enum.any?(lines, fn line ->
|
||||||
line =~ "Radiohead" and line =~ "OK Computer" and
|
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"
|
line =~ "alternative rock, art rock"
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(lines, fn line ->
|
assert Enum.any?(lines, fn line ->
|
||||||
line =~ "Pink Floyd" and line =~ "The Dark Side of the Moon" and
|
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"
|
line =~ "progressive rock"
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -434,7 +442,8 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
})
|
})
|
||||||
|
|
||||||
{summary, record_count} = Collection.collection_summary()
|
{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 record_count == 1
|
||||||
assert length(lines) == 1
|
assert length(lines) == 1
|
||||||
@@ -444,7 +453,7 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
assert line =~ "vinyl"
|
assert line =~ "vinyl"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "caps genres to 3 per record" do
|
test "caps genres to 2 per record" do
|
||||||
record_with_artist("ABC", %{
|
record_with_artist("ABC", %{
|
||||||
title: "The Lexicon of Love",
|
title: "The Lexicon of Love",
|
||||||
format: :vinyl,
|
format: :vinyl,
|
||||||
@@ -455,10 +464,11 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
})
|
})
|
||||||
|
|
||||||
{summary, _count} = Collection.collection_summary()
|
{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()
|
genre_count = genres_str |> String.split(", ") |> length()
|
||||||
|
|
||||||
assert genre_count == 3
|
assert genre_count == 2
|
||||||
end
|
end
|
||||||
|
|
||||||
test "omits genre brackets when genres are empty" do
|
test "omits genre brackets when genres are empty" do
|
||||||
@@ -472,8 +482,9 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
})
|
})
|
||||||
|
|
||||||
{summary, _count} = Collection.collection_summary()
|
{summary, _count} = Collection.collection_summary()
|
||||||
refute summary =~ "["
|
catalog = String.split(summary, "\n\n") |> List.last()
|
||||||
refute summary =~ "]"
|
refute catalog =~ "["
|
||||||
|
refute catalog =~ "]"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "excludes wishlist records" do
|
test "excludes wishlist records" do
|
||||||
@@ -489,8 +500,11 @@ defmodule MusicLibrary.CollectionTest do
|
|||||||
|
|
||||||
{summary, record_count} = Collection.collection_summary()
|
{summary, record_count} = Collection.collection_summary()
|
||||||
assert record_count == 1
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user