Drop item metadata; source-specific item details get their own tables
Items keep only what any source has: name, external_id, url and
archived_at. With no JSON columns left, the hand-rolled parsing and the
json Steep library go too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LG4iASZnHb1yEM1JAASiWn
change
commit fdefbbe9dbf947390ab45f026c3c8208b696c5f2
author Claude <noreply@anthropic.com>
date
parent a66bea6e
diff --git a/Steepfile b/Steepfile
index 5f3fdfb..ae41aec 100644
--- a/Steepfile
+++ b/Steepfile
@@ -11,5 +11,4 @@ target :lib do
   signature "sig"
 
   library "securerandom"
-  library "json"
 end
diff --git a/db/migrate/004_add_list_sources.rb b/db/migrate/004_add_list_sources.rb
index 3857643..11280b4 100644
--- a/db/migrate/004_add_list_sources.rb
+++ b/db/migrate/004_add_list_sources.rb
@@ -14,7 +14,6 @@ Sequel.migration do
     alter_table(:items) do
       add_column :external_id, String
       add_column :url, String
-      add_column :metadata, String, null: false, default: "{}"
       add_column :archived_at, DateTime
       add_index [:list_id, :external_id], unique: true
     end
@@ -30,7 +29,6 @@ Sequel.migration do
       drop_index [:list_id, :external_id]
       drop_column :external_id
       drop_column :url
-      drop_column :metadata
       drop_column :archived_at
     end
 
diff --git a/lib/rank_king/models.rb b/lib/rank_king/models.rb
index 04537a6..6a23561 100644
--- a/lib/rank_king/models.rb
+++ b/lib/rank_king/models.rb
@@ -1,7 +1,5 @@
 # frozen_string_literal: true
 
-require "json"
-
 require_relative "db"
 require_relative "sources"
 
@@ -26,13 +24,6 @@ module RankKing
 
   # @rbs skip
   class Item < Sequel::Model(DB[:items])
-    # JSON by hand rather than Sequel's serialization plugin, which passes
-    # json 3.1 an option it no longer takes.
-    def metadata = JSON.parse(super)
-    def metadata=(value)
-      super(JSON.generate(value))
-    end
-
     many_to_one :list, class: "RankKing::List"
   end
 
diff --git a/lib/rank_king/sources.rb b/lib/rank_king/sources.rb
index 179827b..20c49b3 100644
--- a/lib/rank_king/sources.rb
+++ b/lib/rank_king/sources.rb
@@ -4,9 +4,11 @@ module RankKing
   module Sources
     # What every source produces: one entry per item currently in the source.
     # external_id is how sync recognizes the same item across snapshots.
+    # Only what any source has goes here; a kind's own item details belong
+    # in its own table.
     # Signed in sig/rank_king/sources.rbs: a Data class.
     # @rbs skip
-    Entry = Data.define(:external_id, :name, :url, :metadata)
+    Entry = Data.define(:external_id, :name, :url)
   end
 end
 
diff --git a/lib/rank_king/sources/text.rb b/lib/rank_king/sources/text.rb
index a784c5f..8caa4c9 100644
--- a/lib/rank_king/sources/text.rb
+++ b/lib/rank_king/sources/text.rb
@@ -14,7 +14,7 @@ module RankKing
       #: (untyped snapshot) -> Array[Entry]
       def entries(snapshot)
         snapshot.to_s.lines.map(&:strip).reject(&:empty?).uniq.map {|line|
-          Entry.new(external_id: line, name: line, url: nil, metadata: {})
+          Entry.new(external_id: line, name: line, url: nil)
         }
       end
     end
diff --git a/lib/rank_king/sync.rb b/lib/rank_king/sync.rb
index 344fb8c..e660bcf 100644
--- a/lib/rank_king/sync.rb
+++ b/lib/rank_king/sync.rb
@@ -19,7 +19,7 @@ module RankKing
         added = restored = 0
 
         entries.each do |entry|
-          attrs = { name: entry.name, url: entry.url, metadata: entry.metadata, archived_at: nil }
+          attrs = { name: entry.name, url: entry.url, archived_at: nil }
           if (item = existing.delete(entry.external_id))
             restored += 1 if item.archived_at
             item.update(**attrs)
diff --git a/sig/rank_king/models.rbs b/sig/rank_king/models.rbs
index a136203..37e0877 100644
--- a/sig/rank_king/models.rbs
+++ b/sig/rank_king/models.rbs
@@ -27,8 +27,6 @@ module RankKing
     def list: () -> List
     def external_id: () -> String?
     def url: () -> String?
-    def metadata: () -> Hash[String, untyped]
-    def metadata=: (Hash[String, untyped] value) -> void
     def archived_at: () -> Time?
   end
 
diff --git a/sig/rank_king/sources.rbs b/sig/rank_king/sources.rbs
index de9485d..aaef0ad 100644
--- a/sig/rank_king/sources.rbs
+++ b/sig/rank_king/sources.rbs
@@ -7,9 +7,8 @@ module RankKing
       attr_reader external_id: String
       attr_reader name: String
       attr_reader url: String?
-      attr_reader metadata: Hash[String, untyped]
 
-      def self.new: (external_id: String, name: String, url: String?, metadata: Hash[String, untyped]) -> instance
+      def self.new: (external_id: String, name: String, url: String?) -> instance
     end
 
     # A source instance: turns a snapshot (the pasted text for a push
diff --git a/test/test_sync.rb b/test/test_sync.rb
index fbeb68a..da88ef6 100644
--- a/test/test_sync.rb
+++ b/test/test_sync.rb
@@ -48,14 +48,13 @@ class TestSync < Minitest::Test
     assert_equal %w[A B], names
   end
 
-  def test_refreshes_name_url_and_metadata
-    Sync.call(@list, [Sources::Entry.new(external_id: "v1", name: "Old", url: nil, metadata: {})])
-    Sync.call(@list, [Sources::Entry.new(external_id: "v1", name: "New", url: "https://example.com/v1", metadata: { "thumb" => "t.jpg" })])
+  def test_refreshes_name_and_url
+    Sync.call(@list, [Sources::Entry.new(external_id: "v1", name: "Old", url: nil)])
+    Sync.call(@list, [Sources::Entry.new(external_id: "v1", name: "New", url: "https://example.com/v1")])
 
     item = Item.first(external_id: "v1")
     assert_equal "New", item.name
     assert_equal "https://example.com/v1", item.url
-    assert_equal({ "thumb" => "t.jpg" }, item.metadata)
   end
 
   def test_text_source_strips_blank_and_duplicate_lines