books: the table is named for the book, not for the name
`book_names` named the table after its one column, so the word for the
thing and the word for the table disagreed and every reader carried
both. A row is a book that goes by a name of its own, and the table is
`books` (010). SQLite carries a table's constraints over with it, so
009's two rules still hold and no row moves.

A word is only worth renaming if it then means one thing, so the cards
one client syncs are Store#book_cards rather than Store#book, the
reason vcard's octets are bytes. Web#book keeps its name: it is the
requester's book and no table is in scope there.

Now rather than later: nothing has dumped a books.yml yet, so this is a
migration and a pass over one class, where a deployment whose dump
history carried the old file would also need that file cleaned up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SB9PbfzjHsGjApxNNt7Qj5
change
commit 74156d02ab1f329fc8905df20f02ac07973f6831
author Claude <noreply@anthropic.com>
date
parent 67bd63c7
diff --git a/AGENTS.md b/AGENTS.md
index 7ffccef..1e8e329 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -21,9 +21,9 @@ rake contacts:reid    # Move contacts whose ids are not server-minted
                       # onto minted ones, rewriting each card's UID
 rake book:name        # Name a login's book and rename its sync group
                       # (LOGIN=login NAME=name; no NAME clears it)
-rake db:dump          # Write the cards, birthdays, groups, and book
-                      # names to data/dump and commit them to the
-                      # repository there (DUMP=path to move it)
+rake db:dump          # Write the cards, birthdays, groups, and books
+                      # to data/dump and commit them to the repository
+                      # there (DUMP=path to move it)
 rake dev              # Dev server, seeded from test/fixtures/cards into a
                       # throwaway tmpdir on every start, reloading on change
                       # (needs fd and entr)
diff --git a/README.md b/README.md
index 10a768b..ef8b63f 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ the birthdays — the two things in it that cannot be rebuilt, the
 birthdays because a partial date has no vCard 3.0 spelling — and an
 index of parsed properties, which can: `rake index:rebuild` derives
 that again from the stored cards alone. `rake db:dump` writes the
-cards, birthdays, groups, and book names out as plain files, into
+cards, birthdays, groups, and books out as plain files, into
 `data/dump` unless `DUMP` names another directory, and commits them to
 that directory's own repository, so a snapshot is something to revert
 to (`docs/plans/2026-09-12-database-dump.md`,
diff --git a/db/migrations/010_books.rb b/db/migrations/010_books.rb
new file mode 100644
index 0000000..dbc6e40
--- /dev/null
+++ b/db/migrations/010_books.rb
@@ -0,0 +1,14 @@
+# The table is named for what a row is rather than for its one column.
+# A book is what a login syncs (Store#book_cards), and a row here is
+# one that goes by a name of its own; `book_names` named the table
+# after the `name` column, leaving the word for the thing and the word
+# for the table at odds (docs/plans/2026-09-21-books-not-book-names.md).
+#
+# A rename and nothing else. SQLite carries a table's own constraints
+# over with it, so 009's two rules — a login holds one row, a name is
+# one login's — still hold, and no row moves.
+Sequel.migration do
+  change do
+    rename_table :book_names, :books
+  end
+end
diff --git a/docs/plans/2026-09-19-book-names-in-the-dump.md b/docs/plans/2026-09-19-books-in-the-dump.md
similarity index 58%
rename from docs/plans/2026-09-19-book-names-in-the-dump.md
rename to docs/plans/2026-09-19-books-in-the-dump.md
index e94f3f5..220f80a 100644
--- a/docs/plans/2026-09-19-book-names-in-the-dump.md
+++ b/docs/plans/2026-09-19-books-in-the-dump.md
@@ -1,16 +1,19 @@
-# Book names in the dump
+# Books in the dump
 
-2026-09-19. `rake db:dump` writes `book_names.yml` beside the cards,
-the birthdays, and the groups: every login that has a book name, and
+2026-09-19. `rake db:dump` writes `books.yml` beside the cards, the
+birthdays, and the groups: every login whose book goes by a name, and
 the name it goes by.
 
+Written as `book_names.yml` on the day, off the table's name then;
+`2026-09-21-books-not-book-names.md` renamed both.
+
 ## Why it belongs there
 
 The dump holds what the store cannot rebuild
-(`2026-09-12-database-dump.md`), and a book name is exactly that. Nothing
-re-derives a `book_names` row — the migration says so in as many words
-(db/migrations/009_book_names.rb) — and it is not carried by any card, so
-it went out of a dump the way the birthdays would have.
+(`2026-09-12-database-dump.md`), and a book's name is exactly that.
+Nothing re-derives one of those rows — the migration says so in as many
+words (db/migrations/009_book_names.rb) — and no card carries it, so it
+went out of a dump the way the birthdays would have.
 
 Losing one is not a cosmetic loss either. A login whose name is gone
 goes back to syncing `sync:<login>`, and the `sync:<name>` group the dump
@@ -23,23 +26,23 @@ down.
 
 | Path | Holds |
 |---|---|
-| `book_names.yml` | The name each login's book goes by, keyed by login |
+| `books.yml` | The name each named book goes by, keyed by login |
 
 One file rather than a directory, the birthdays' shape: a row is a login
 and a string, and there is one per user of the deployment. Written every
 dump, `--- {}` for a deployment that has named no book, so the file is
 the answer rather than its absence being one.
 
-Logins are ordered in `Store#all_book_names` rather than by the task, the
+Logins are ordered in `Store#all_books` rather than by the task, the
 birthdays' arrangement reversed. `Store#birthdays_by_id` is shared with
 the listing reads, which do not care, so `tasks/db.rake` sorts what it
-writes; nothing but the dump reads all the book names, so the order the
-file needs lives with the read.
+writes; nothing but the dump reads all the books, so the order the file
+needs lives with the read.
 
 ## Still not read back
 
 Nothing reads a dump back, this no more than the rest. A restore would
 need an importer, and `Store#name_book` is the writer it would call —
 which renames the `sync:` group as it goes, so an importer replaying
-`book_names.yml` after the groups would rename what it had just restored.
+`books.yml` after the groups would rename what it had just restored.
 The order is the importer's problem to solve when one is written.
diff --git a/docs/plans/2026-09-21-books-not-book-names.md b/docs/plans/2026-09-21-books-not-book-names.md
new file mode 100644
index 0000000..f02231b
--- /dev/null
+++ b/docs/plans/2026-09-21-books-not-book-names.md
@@ -0,0 +1,45 @@
+# Books, not book names
+
+2026-09-21. The `book_names` table is `books`
+(db/migrations/010_books.rb), and the word `book` inside `Store` now
+names one thing.
+
+## The table was named for its column
+
+A book is what a login syncs. `book_names` named the table after the
+`name` column instead, so the table and the thing it held went by
+different words, and every reader had to hold both. A row here is a book
+that goes by a name of its own; a login with no row has a book all the
+same, which is the state the table exists to record a departure from.
+
+The rename is a rename and nothing else. SQLite carries a table's own
+constraints over with it, so 009's two rules — a login holds one row, a
+name is one login's — still hold, and no row moves.
+
+## What the word costs elsewhere
+
+`books` cannot mean a table of names while `book` means a set of cards.
+`Store#book(login)` answered with the card ids one client syncs, so it
+is `Store#book_cards(login)` now, for the same reason `vcard` names a
+`VCard` and a card's octets are `bytes` (AGENTS.md). `Web#book` keeps
+its name: it is the requester's book, and no table is in scope there.
+
+The pairs that stay, because each already said which half it meant:
+
+| Reads | Answers |
+|---|---|
+| `Store#books` | The table |
+| `Store#all_books` | Every named book, by login — `groups`/`all_groups`'s arrangement |
+| `Store#book_name(login)` | The name, or nil for a book that goes by its login |
+| `Store#name_book(login, name)` | Sets it, and renames the `sync:` group with it |
+| `Store#book_cards(login)` | The card ids that client syncs |
+
+`Snapshot#books` and the dump's `books.yml` follow the table
+(`2026-09-19-books-in-the-dump.md`).
+
+## Why now
+
+The feature is five days old (`2026-09-16-book-names.md`) and the dump
+was about to write the name into a file that version control would keep.
+A rename costs a migration and a pass over one class today; it costs a
+dump migration once a deployment's history carries the old file.
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 9afa6ba..f5c0280 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -89,9 +89,10 @@ module ProTacts
 
     # Everything the store cannot rebuild, read at one moment: the
     # stored cards by id, the birthdays by card id, the groups, and the
-    # book names by login. What `rake db:dump` writes (tasks/db.rake).
+    # books — the name each named one goes by, keyed by login. What
+    # `rake db:dump` writes (tasks/db.rake).
     # @rbs skip
-    Snapshot = Data.define(:cards, :birthdays, :groups, :book_names)
+    Snapshot = Data.define(:cards, :birthdays, :groups, :books)
 
     # A group as the admin screens read one: its own row, the lines it
     # lends in the order it lends them, and its members' card ids. The
@@ -268,7 +269,7 @@ module ProTacts
           },
           birthdays: birthdays_by_id,
           groups: all_groups,
-          book_names: all_book_names,
+          books: all_books,
         )
       end
     end
@@ -383,8 +384,11 @@ module ProTacts
     # where the name is the login's book name or the login itself
     # (docs/plans/2026-09-16-book-names.md). Matched as spelled: a login
     # whose case reads badly gets a name rather than a folded match.
+    # Named for the cards rather than for the book, because `books` is
+    # the table of the books that have names
+    # (docs/plans/2026-09-21-books-not-book-names.md).
     #: (String login) -> Set[String]
-    def book(login)
+    def book_cards(login)
       ids = groups.where(name: [EVERYONE, own_sync_name(login)]).select_map(:id)
       Set.new(group_members.where(group_id: ids).select_map(:card_id).map(&:to_s))
     end
@@ -393,7 +397,7 @@ module ProTacts
     # the login itself.
     #: (String login) -> String?
     def book_name(login)
-      book_names.where(login:).sole.fetch(:name).to_s
+      books.where(login:).sole.fetch(:name).to_s
     rescue Sequel::NoMatchingRow
       nil
     end
@@ -413,8 +417,8 @@ module ProTacts
 
       @database.transaction do
         group = groups.where(name: own_sync_name(login)).select_map(:id).first
-        book_names.where(login:).delete
-        book_names.insert(login:, name:) unless name.nil?
+        books.where(login:).delete
+        books.insert(login:, name:) unless name.nil?
         rename_group(group.to_s, name: own_sync_name(login)) unless group.nil?
       end
     end
@@ -898,8 +902,8 @@ module ProTacts
     end
 
     #: () -> Sequel::Dataset
-    def book_names
-      @database[:book_names]
+    def books
+      @database[:books]
     end
 
     #: () -> Sequel::Dataset
@@ -1451,14 +1455,15 @@ module ProTacts
       end
     end
 
-    # Every book name, keyed by login, for the dump — the one read that
-    # wants them all (docs/plans/2026-09-19-book-names-in-the-dump.md).
-    # Ordered here rather than by the caller, the birthdays' arrangement
-    # reversed: nothing else reads this, so the order a dumped file
-    # needs belongs with the read.
+    # Every named book, the name keyed by the login whose book it is,
+    # for the dump — the one read that wants them all
+    # (docs/plans/2026-09-19-books-in-the-dump.md). Ordered here
+    # rather than by the caller, the birthdays' arrangement reversed:
+    # nothing else reads this, so the order a dumped file needs belongs
+    # with the read.
     #: () -> Hash[String, String]
-    def all_book_names
-      book_names.order(:login).all.to_h {
+    def all_books
+      books.order(:login).all.to_h {
         [it.fetch(:login).to_s, it.fetch(:name).to_s] #: [String, String]
       }
     end
diff --git a/lib/pro_tacts/web/dav.rb b/lib/pro_tacts/web/dav.rb
index 74fd8ba..1cd17b9 100644
--- a/lib/pro_tacts/web/dav.rb
+++ b/lib/pro_tacts/web/dav.rb
@@ -322,7 +322,7 @@ module ProTacts
 
     #: () -> Set[String]
     def book
-      @book ||= store.book(@login)
+      @book ||= store.book_cards(@login)
     end
 
     # One member of the requester's book, or nil for a card outside it
diff --git a/sig/pro_tacts/store.rbs b/sig/pro_tacts/store.rbs
index 7a4fb89..0b42bb8 100644
--- a/sig/pro_tacts/store.rbs
+++ b/sig/pro_tacts/store.rbs
@@ -50,9 +50,9 @@ module ProTacts
       attr_reader cards: Hash[String, String]
       attr_reader birthdays: Hash[String, Birthday]
       attr_reader groups: Array[Group]
-      attr_reader book_names: Hash[String, String]
+      attr_reader books: Hash[String, String]
 
-      def self.new: (cards: Hash[String, String], birthdays: Hash[String, Birthday], groups: Array[Group], book_names: Hash[String, String]) -> instance
+      def self.new: (cards: Hash[String, String], birthdays: Hash[String, Birthday], groups: Array[Group], books: Hash[String, String]) -> instance
     end
 
     class GroupEdit
diff --git a/tasks/db.rake b/tasks/db.rake
index a4872e5..799ef0d 100644
--- a/tasks/db.rake
+++ b/tasks/db.rake
@@ -1,7 +1,7 @@
 # A readable copy of what the store cannot rebuild: each stored card as
-# its bytes, the birthdays, the groups, and the book names, split the
-# way the store splits them (docs/plans/2026-09-12-database-dump.md,
-# docs/plans/2026-09-19-book-names-in-the-dump.md). cards/ and
+# its bytes, the birthdays, the groups, and the books, split the way
+# the store splits them (docs/plans/2026-09-12-database-dump.md,
+# docs/plans/2026-09-19-books-in-the-dump.md). cards/ and
 # groups/ are made to hold exactly what the store holds, so a dump kept
 # in version control shows deletions too; anything else in the
 # directory is left alone.
@@ -24,7 +24,7 @@ directory cards.to_s
 directory groups.to_s
 
 namespace :db do
-  desc "Dump the cards, birthdays, groups, and book names into #{dump} and commit them there (DUMP=path to move it)"
+  desc "Dump the cards, birthdays, groups, and books into #{dump} and commit them there (DUMP=path to move it)"
   task dump: [cards.to_s, groups.to_s] do
     require "yaml"
     require "pro_tacts/store"
@@ -44,7 +44,7 @@ namespace :db do
         ["#{group.id}.yml", YAML.dump({"name" => group.name, "lines" => group.lines, "members" => group.members})]
       })
 
-      dump_file(dump / "book_names.yml", YAML.dump(snapshot.book_names))
+      dump_file(dump / "books.yml", YAML.dump(snapshot.books))
     end
     commit_dump(dump)
     puts "dumped #{database} into #{dump}"
diff --git a/test/pro_tacts/import/test_finalize.rb b/test/pro_tacts/import/test_finalize.rb
index 25dcc67..c369054 100644
--- a/test/pro_tacts/import/test_finalize.rb
+++ b/test/pro_tacts/import/test_finalize.rb
@@ -128,7 +128,7 @@ class ImportFinalizeTest < Minitest::Test
   # user's book, which a card in no `sync:` group is outside of.
   def test_a_contact_whose_card_is_in_no_book_still_leaves_the_mac
     with_landed_plan(edit: ->(yaml) { yaml.sub("- sync:*\n", "") }) do |plan, store, client|
-      refute_includes store.book("test@example.com"), IDS.first
+      refute_includes store.book_cards("test@example.com"), IDS.first
       mac = Mac.new(IDS.to_h { [source_id(it), record(it)] })
 
       result = Finalize.call(plan, client:, mac:)
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index a35b989..0f379e3 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -1490,8 +1490,8 @@ class StoreTest < Minitest::Test
       FixtureData.seed_group(store, name: "sync:Alpha Chen", members: %w[aiden znorth])
       FixtureData.seed_group(store, name: "Household", members: ["yuki"])
 
-      assert_equal Set["aiden", "znorth"], store.book("Alpha Chen")
-      assert_equal Set["aiden"], store.book("Zoë Chen")
+      assert_equal Set["aiden", "znorth"], store.book_cards("Alpha Chen")
+      assert_equal Set["aiden"], store.book_cards("Zoë Chen")
     end
   end
 
@@ -1503,7 +1503,7 @@ class StoreTest < Minitest::Test
       FixtureData.seed_group(store, name: "sync:alpha chen", members: ["znorth"])
       FixtureData.seed_group(store, name: "SYNC:Alpha Chen", members: ["yuki"])
 
-      assert_equal Set["aiden"], store.book("Alpha Chen")
+      assert_equal Set["aiden"], store.book_cards("Alpha Chen")
     end
   end
 
@@ -1518,14 +1518,14 @@ class StoreTest < Minitest::Test
 
       assert_equal "sync:Alpha Chen", store.group(id).name
       assert_equal "Alpha Chen", store.book_name("alpha@example.com")
-      assert_equal Set["aiden"], store.book("alpha@example.com")
+      assert_equal Set["aiden"], store.book_cards("alpha@example.com")
       assert_equal %w[group put], store.changes_of("aiden").map(&:action)
 
       store.name_book("alpha@example.com", " ")
 
       assert_equal "sync:alpha@example.com", store.group(id).name
       assert_nil store.book_name("alpha@example.com")
-      assert_equal Set["aiden"], store.book("alpha@example.com")
+      assert_equal Set["aiden"], store.book_cards("alpha@example.com")
     end
   end
 
@@ -1534,7 +1534,7 @@ class StoreTest < Minitest::Test
       store.name_book("alpha@example.com", "Alpha Chen")
       FixtureData.seed_group(store, name: "sync:Alpha Chen", members: ["aiden"])
 
-      assert_equal Set["aiden"], store.book("alpha@example.com")
+      assert_equal Set["aiden"], store.book_cards("alpha@example.com")
     end
   end
 
@@ -1557,7 +1557,7 @@ class StoreTest < Minitest::Test
       store.put("aiden", vcard(AIDEN), client: true)
       store.put("znorth", vcard(ZED), client: true)
 
-      assert_equal Set["aiden", "znorth"], store.book("Zoë Chen")
+      assert_equal Set["aiden", "znorth"], store.book_cards("Zoë Chen")
       assert_equal ["sync:*"], store.all_groups.map(&:name)
       assert_equal ["put"], store.changes_of("aiden").map(&:action)
     end
@@ -1567,7 +1567,7 @@ class StoreTest < Minitest::Test
     with_store({"aiden" => AIDEN}) do |store|
       store.put("aiden", vcard(AIDEN), client: true)
 
-      assert_empty store.book("Alpha Chen")
+      assert_empty store.book_cards("Alpha Chen")
     end
   end
 
@@ -2137,7 +2137,7 @@ class StoreTest < Minitest::Test
       assert_equal({"aiden" => AIDEN}, snapshot.cards)
       assert_equal({"aiden" => ProTacts::Birthday.new(year: 1985, month: 4, day: 12)}, snapshot.birthdays)
       assert_equal [["Booles", ["aiden"]]], snapshot.groups.map { [it.name, it.members] }
-      assert_equal({"alpha@example.com" => "Alpha Chen"}, snapshot.book_names)
+      assert_equal({"alpha@example.com" => "Alpha Chen"}, snapshot.books)
     end
   end
 
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index f79fead..9d96483 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -1024,8 +1024,8 @@ class WebTest < Minitest::Test
       put_request "new", card("new", "New"), "CONTENT_TYPE" => VCARD, "HTTP_IF_NONE_MATCH" => "*"
 
       assert_equal 201, last_response.status
-      assert_equal Set["new"], store.book("test@example.com")
-      assert_equal Set["new"], store.book("zoë@example.com")
+      assert_equal Set["new"], store.book_cards("test@example.com")
+      assert_equal Set["new"], store.book_cards("zoë@example.com")
     end
   end
 
diff --git a/test/tasks/test_db.rb b/test/tasks/test_db.rb
index 432008f..49ebc81 100644
--- a/test/tasks/test_db.rb
+++ b/test/tasks/test_db.rb
@@ -30,7 +30,7 @@ class DbTasksTest < Minitest::Test
       assert_equal({"aiden" => {"year" => 1985, "month" => 4}}, YAML.safe_load((root / "dump" / "birthdays.yml").read))
       assert_equal({"name" => "Booles", "lines" => ["NOTE:a household"], "members" => ["aiden"]},
         YAML.safe_load((root / "dump" / "groups" / "#{group}.yml").read))
-      assert_equal({"alpha@example.com" => "Alpha Chen"}, YAML.safe_load((root / "dump" / "book_names.yml").read))
+      assert_equal({"alpha@example.com" => "Alpha Chen"}, YAML.safe_load((root / "dump" / "books.yml").read))
     end
   end
 
@@ -42,7 +42,7 @@ class DbTasksTest < Minitest::Test
 
       dump(root)
 
-      assert_equal({}, YAML.safe_load((root / "dump" / "book_names.yml").read))
+      assert_equal({}, YAML.safe_load((root / "dump" / "books.yml").read))
     end
   end