books: a book goes by its login until it is named
#book_name answered with nil for a login with no row, and each of its
three callers undid that with `|| login` — the same fallback spelled
out three times, for a name the method's own comment already said was
the login's. A login with no row has a book all the same, so the method
answers with the login and the fallback goes.

Nothing asked the other question. No screen, task or route branches on
whether a book was ever named: the two writers set a name or clear one
and then report what the book goes by, which is the same sentence
either way. The tests that said this as an absence now say it as the
name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SB9PbfzjHsGjApxNNt7Qj5
change
commit 417a51bb34b1e72255263781ea5e884699a37137
author Claude <noreply@anthropic.com>
date
parent 74156d02
diff --git a/docs/plans/2026-09-21-books-not-book-names.md b/docs/plans/2026-09-21-books-not-book-names.md
index f02231b..c60cdf3 100644
--- a/docs/plans/2026-09-21-books-not-book-names.md
+++ b/docs/plans/2026-09-21-books-not-book-names.md
@@ -30,13 +30,29 @@ The pairs that stay, because each already said which half it meant:
 |---|---|
 | `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#book_name(login)` | The name it goes by, which is the login until one is set |
 | `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`).
 
+## A book has a name whether or not it has a row
+
+`book_name` answered with nil for a login with no row, and all three of
+its callers wrote `book_name(login) || login` — the fallback spelled out
+again each time, and a name the method's own comment had already said was
+the login's. A login with no row has a book all the same, and it goes by
+the login, so that is what the method answers now and the `|| login`
+goes.
+
+Nothing asked the other question. Whether a book was ever named is not a
+thing any screen, task or route branches on: the two writers
+(`rake book:name` and the setup POST) set a name or clear one, and both
+report what the book goes by afterwards either way. A reader that needs
+the distinction can ask the table, but none does, and a nil that every
+caller had to undo was not that reader.
+
 ## Why now
 
 The feature is five days old (`2026-09-16-book-names.md`) and the dump
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index f5c0280..9cf96d0 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -393,13 +393,17 @@ module ProTacts
       Set.new(group_members.where(group_id: ids).select_map(:card_id).map(&:to_s))
     end
 
-    # The name a login's book goes by in place of the login, or nil for
-    # the login itself.
-    #: (String login) -> String?
+    # The name a login's book goes by. A login with no row has a book
+    # all the same and it goes by the login, so that is the answer
+    # rather than a nil for every caller to spell the fallback out
+    # again (docs/plans/2026-09-21-books-not-book-names.md). Whether a
+    # book was named is a question nothing asks; #name_book is how one
+    # is set either way.
+    #: (String login) -> String
     def book_name(login)
       books.where(login:).sole.fetch(:name).to_s
     rescue Sequel::NoMatchingRow
-      nil
+      login
     end
 
     # Sets the name a login's book goes by, or with nil or a blank goes
@@ -1010,7 +1014,7 @@ module ProTacts
     # The group name that puts cards in this login's book alone.
     #: (String login) -> String
     def own_sync_name(login)
-      "#{SYNC_PREFIX}#{book_name(login) || login}"
+      "#{SYNC_PREFIX}#{book_name(login)}"
     end
 
     #: (String? name) -> bool
diff --git a/lib/pro_tacts/web/setup.rb b/lib/pro_tacts/web/setup.rb
index 424186a..57a4d63 100644
--- a/lib/pro_tacts/web/setup.rb
+++ b/lib/pro_tacts/web/setup.rb
@@ -46,7 +46,7 @@ module ProTacts
         response["Content-Type"] = "text/plain"
         begin
           store.name_book(@login, r.params["name"]&.to_s)
-          "#{@login} syncs #{Store::SYNC_PREFIX}#{store.book_name(@login) || @login}\n"
+          "#{@login} syncs #{Store::SYNC_PREFIX}#{store.book_name(@login)}\n"
         rescue Sequel::UniqueConstraintViolation, Store::EveryonesBookName
           # Another login's name, a group's already, or everyone's.
           response.status = 409
diff --git a/tasks/book.rake b/tasks/book.rake
index 113bae2..a30bce7 100644
--- a/tasks/book.rake
+++ b/tasks/book.rake
@@ -10,7 +10,7 @@ namespace :book do
     login = ENV.fetch("LOGIN")
     ProTacts::Store.connect(ProTacts.config.database_path) do |store|
       store.name_book(login, ENV.fetch("NAME", nil))
-      puts "#{login} syncs #{ProTacts::Store::SYNC_PREFIX}#{store.book_name(login) || login}"
+      puts "#{login} syncs #{ProTacts::Store::SYNC_PREFIX}#{store.book_name(login)}"
     end
   end
 end
diff --git a/test/pro_tacts/admin/test_device_setup.rb b/test/pro_tacts/admin/test_device_setup.rb
index 2fdd066..53b7435 100644
--- a/test/pro_tacts/admin/test_device_setup.rb
+++ b/test/pro_tacts/admin/test_device_setup.rb
@@ -113,7 +113,7 @@ class AdminDeviceSetupTest < Minitest::Test
       post "/setup/book"
 
       assert_equal "test@example.com syncs sync:test@example.com\n", last_response.body
-      assert_nil store.book_name("test@example.com")
+      assert_equal "test@example.com", store.book_name("test@example.com")
     end
   end
 
@@ -125,7 +125,7 @@ class AdminDeviceSetupTest < Minitest::Test
 
       assert_equal 409, last_response.status
       assert_equal "sync:zoe is taken; test@example.com's book is unchanged.\n", last_response.body
-      assert_nil store.book_name("test@example.com")
+      assert_equal "test@example.com", store.book_name("test@example.com")
 
       post "/setup/book", name: "*"
 
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 0f379e3..9187c4b 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -1524,7 +1524,7 @@ class StoreTest < Minitest::Test
       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 "alpha@example.com", store.book_name("alpha@example.com")
       assert_equal Set["aiden"], store.book_cards("alpha@example.com")
     end
   end
@@ -1546,7 +1546,7 @@ class StoreTest < Minitest::Test
 
       assert_raises(Sequel::UniqueConstraintViolation) { store.name_book("zoe@example.com", "Alpha Chen") }
       assert_raises(ProTacts::Store::EveryonesBookName) { store.name_book("zoe@example.com", "*") }
-      assert_nil store.book_name("zoe@example.com")
+      assert_equal "zoe@example.com", store.book_name("zoe@example.com")
     end
   end