Promote Store::Group to ProTacts::Group, sync: rules and all
Group was a Data class nested in Store with behavior of its own
(reading, sync?, version), read by a dozen admin views and held by
Contact::Inherited, so the model depended on the store's namespace.
It moves to lib/pro_tacts/group.rb beside Contact, and every
Store::Group becomes Group.

The sync: naming rules were spelled twice, as Group#sync? and
Store#sync_name?. SYNC_PREFIX, EVERYONE and sync_name? now live on
Group, and both callers use Group.sync_name?.

Two reads Store repeated fold into one each:

- #contact is #contact! with the rescue, and #reid reads through
  #contact! for both of its contacts.
- The groups row with its label, selected five times, is
  #labeled_groups. GROUP_COLUMNS goes with it, and with it the comment
  #18 cut in half by dropping its first line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gi55bFkB6wo4jguNtsase4
change
commit 6020900d5fd99b87023a52b58963ee166b61779e
author Claude <noreply@anthropic.com>
date
parent 7f1a28ec
diff --git a/AGENTS.md b/AGENTS.md
index 4eefbd0..861b9a0 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -80,6 +80,8 @@ lib/pro_tacts/
 │                   # group change log, and the derived index
 ├── contact.rb      # the one model of a contact: id, vCard, etag,
 │                   # and the structured accessors over the card
+├── group.rb        # A group as a screen reads one, and the `sync:`
+│                   # names that put cards in a book
 ├── edited_contact.rb # A submitted card read against the contact it
 │                   # replaces: the card, birthday, and group edits
 │                   # Store#put writes
diff --git a/lib/pro_tacts/admin/card_form.rb b/lib/pro_tacts/admin/card_form.rb
index 398923d..2d82f5e 100644
--- a/lib/pro_tacts/admin/card_form.rb
+++ b/lib/pro_tacts/admin/card_form.rb
@@ -58,10 +58,10 @@ module ProTacts
 
       # The group editor's save: the note replaced and the contact
       # editor's own address splice over the group's lines read as a
-      # card (Store::Group#reading), then back into the lines a group
+      # card (Group#reading), then back into the lines a group
       # holds — each unfolded and shorn of its terminator, the unit
       # CardDiff records a write in and group_properties stores.
-      #: (Store::Group group, Hash[String, untyped] params) -> Array[String]
+      #: (Group group, Hash[String, untyped] params) -> Array[String]
       def self.group_lines(group, params)
         reading = group.reading
         card = reading.stored.replace("NOTE", text_lines("NOTE", params["note"].to_s.strip))
diff --git a/lib/pro_tacts/admin/contact_card.rb b/lib/pro_tacts/admin/contact_card.rb
index 22b5762..03efdf9 100644
--- a/lib/pro_tacts/admin/contact_card.rb
+++ b/lib/pro_tacts/admin/contact_card.rb
@@ -27,11 +27,11 @@ module ProTacts
     # (ImportSaved), whose trunk RecordCard owns.
     class ContactCard < Phlex::HTML
       # @rbs @contact: Contact
-      # @rbs @groups: Array[Store::Group]
+      # @rbs @groups: Array[Group]
       # @rbs @dialog: bool
       # @rbs @card: bool
 
-      #: (contact: Contact, groups: Array[Store::Group], ?dialog: bool, ?card: bool) -> void
+      #: (contact: Contact, groups: Array[Group], ?dialog: bool, ?card: bool) -> void
       def initialize(contact:, groups:, dialog: false, card: true)
         @contact = contact
         @groups = groups
@@ -125,7 +125,7 @@ module ProTacts
         end
       end
 
-      #: (Store::Group group) -> void
+      #: (Group group) -> void
       def group_tag(group)
         a(href: "/groups/#{group.id}", class: "tag") { render GroupLabel.new(group:) }
       end
diff --git a/lib/pro_tacts/admin/contacts_index.rb b/lib/pro_tacts/admin/contacts_index.rb
index 926dbcb..310a5ab 100644
--- a/lib/pro_tacts/admin/contacts_index.rb
+++ b/lib/pro_tacts/admin/contacts_index.rb
@@ -15,9 +15,9 @@ module ProTacts
     class ContactsIndex < Phlex::HTML
       # @rbs @rows: Array[Contact]
       # @rbs @login: String
-      # @rbs @groups_of: Hash[String, Array[Store::Group]]
+      # @rbs @groups_of: Hash[String, Array[Group]]
 
-      #: (contacts: Array[Contact], groups: Array[Store::Group], login: String) -> void
+      #: (contacts: Array[Contact], groups: Array[Group], login: String) -> void
       def initialize(contacts:, groups:, login:)
         @login = login
         @rows = contacts.sort_by { [Format.sort_key(it), it.id] }
diff --git a/lib/pro_tacts/admin/contacts_show.rb b/lib/pro_tacts/admin/contacts_show.rb
index 99708bf..5f0ad95 100644
--- a/lib/pro_tacts/admin/contacts_show.rb
+++ b/lib/pro_tacts/admin/contacts_show.rb
@@ -26,7 +26,7 @@ module ProTacts
       # one: a card with no entries is a card whose history was lost,
       # and an empty default would render that as an ordinary quiet
       # record. Every group is the groups dialog's (GroupDialog).
-      #: (Contact contact, String login, Array[Store::Group] groups, Array[Store::GroupChoice] all_groups, Array[Store::Change] changes, ?notice: String?) -> void
+      #: (Contact contact, String login, Array[Group] groups, Array[Store::GroupChoice] all_groups, Array[Store::Change] changes, ?notice: String?) -> void
       def initialize(contact:, login:, groups:, all_groups:, changes:, notice: nil)
         @contact = contact
         @login = login
diff --git a/lib/pro_tacts/admin/dashboard.rb b/lib/pro_tacts/admin/dashboard.rb
index d12597c..33dfe9c 100644
--- a/lib/pro_tacts/admin/dashboard.rb
+++ b/lib/pro_tacts/admin/dashboard.rb
@@ -26,14 +26,14 @@ module ProTacts
       # @rbs @login: String
       # @rbs @upcoming: Array[Store::UpcomingBirthday]
       # @rbs @labels: Hash[String, Array[String]]
-      # @rbs @groups: Array[Store::Group]
+      # @rbs @groups: Array[Group]
       # @rbs @rows: Array[Store::RecentContact]
       # @rbs @notice: String?
 
       # The groups arrive whole because a search answers from both
       # sides of the relationship: a group is findable by its own name,
       # and a contact by the names of the groups it is in.
-      #: (recent: Array[Store::RecentContact], upcoming: Array[Store::UpcomingBirthday], query: String?, login: String, groups: Array[Store::Group], ?notice: String?) -> void
+      #: (recent: Array[Store::RecentContact], upcoming: Array[Store::UpcomingBirthday], query: String?, login: String, groups: Array[Group], ?notice: String?) -> void
       def initialize(recent:, upcoming:, query:, login:, groups:, notice: nil)
         @query = query.to_s.strip
         @login = login
diff --git a/lib/pro_tacts/admin/group_label.rb b/lib/pro_tacts/admin/group_label.rb
index 8305fae..96c7fe0 100644
--- a/lib/pro_tacts/admin/group_label.rb
+++ b/lib/pro_tacts/admin/group_label.rb
@@ -7,11 +7,11 @@ module ProTacts
     # its id, marked `data-nameless` so admin.css can set it as an
     # identifier rather than let it pass for a name.
     class GroupLabel < Phlex::HTML
-      # @rbs @group: Store::Group | Store::GroupChoice
+      # @rbs @group: Group | Store::GroupChoice
 
       # A Group or the lighter GroupChoice a picker carries: this reads
       # only the name and the id, which both hold.
-      #: (group: Store::Group | Store::GroupChoice) -> void
+      #: (group: Group | Store::GroupChoice) -> void
       def initialize(group:)
         @group = group
       end
diff --git a/lib/pro_tacts/admin/groups_edit.rb b/lib/pro_tacts/admin/groups_edit.rb
index f346548..5261463 100644
--- a/lib/pro_tacts/admin/groups_edit.rb
+++ b/lib/pro_tacts/admin/groups_edit.rb
@@ -23,7 +23,7 @@ module ProTacts
     # no `members[]`, so a save says nothing about membership
     # (#apply_edit's is-a-Hash posture) and leaves it as it stands.
     class GroupsEdit < Phlex::HTML
-      # @rbs @group: Store::Group
+      # @rbs @group: Group
       # @rbs @login: String
       # @rbs @reading: Contact
       # @rbs @notice: String?
@@ -32,7 +32,7 @@ module ProTacts
       FORM = "group-form" #: String
       private_constant :FORM
 
-      #: (group: Store::Group, login: String, ?notice: String?) -> void
+      #: (group: Group, login: String, ?notice: String?) -> void
       def initialize(group:, login:, notice: nil)
         @group = group
         @login = login
diff --git a/lib/pro_tacts/admin/groups_index.rb b/lib/pro_tacts/admin/groups_index.rb
index bc0411e..bfa7198 100644
--- a/lib/pro_tacts/admin/groups_index.rb
+++ b/lib/pro_tacts/admin/groups_index.rb
@@ -14,11 +14,11 @@ module ProTacts
     # and a create lands on the new group's editor, where everything
     # it will hold is added.
     class GroupsIndex < Phlex::HTML
-      # @rbs @groups: Array[Store::Group]
+      # @rbs @groups: Array[Group]
       # @rbs @login: String
       # @rbs @notice: String?
 
-      #: (groups: Array[Store::Group], login: String, ?notice: String?) -> void
+      #: (groups: Array[Group], login: String, ?notice: String?) -> void
       def initialize(groups:, login:, notice: nil)
         @login = login
         # The `sync:` groups first — the books a client syncs, which
@@ -60,7 +60,7 @@ module ProTacts
 
       private
 
-      #: (Store::Group group) -> String
+      #: (Group group) -> String
       def members_label(group)
         count = group.members.length
         count == 1 ? "1 member" : "#{count} members"
diff --git a/lib/pro_tacts/admin/groups_members.rb b/lib/pro_tacts/admin/groups_members.rb
index a5bff21..d43e8d1 100644
--- a/lib/pro_tacts/admin/groups_members.rb
+++ b/lib/pro_tacts/admin/groups_members.rb
@@ -31,7 +31,7 @@ module ProTacts
     # make what the filter names is not rendered, a filter word not
     # being able to mint a contact.
     class GroupsMembers < Phlex::HTML
-      # @rbs @group: Store::Group
+      # @rbs @group: Group
       # @rbs @login: String
       # @rbs @contacts: Array[Contact]
       # @rbs @capped: Array[String]
@@ -40,7 +40,7 @@ module ProTacts
       FORM = "members-form" #: String
       private_constant :FORM
 
-      #: (group: Store::Group, contacts: Array[Contact], login: String) -> void
+      #: (group: Group, contacts: Array[Contact], login: String) -> void
       def initialize(group:, contacts:, login:)
         @group = group
         @login = login
diff --git a/lib/pro_tacts/admin/groups_show.rb b/lib/pro_tacts/admin/groups_show.rb
index be70a60..15e3e4b 100644
--- a/lib/pro_tacts/admin/groups_show.rb
+++ b/lib/pro_tacts/admin/groups_show.rb
@@ -21,14 +21,14 @@ module ProTacts
     # log names the cards a join or a leave moved, and a name is what
     # a person reads; the members list is the narrowing of it.
     class GroupsShow < Phlex::HTML
-      # @rbs @group: Store::Group
+      # @rbs @group: Group
       # @rbs @login: String
       # @rbs @reading: Contact
       # @rbs @contacts: Array[Contact]
       # @rbs @members: Array[Contact]
       # @rbs @changes: Array[Store::GroupChange]
 
-      #: (group: Store::Group, contacts: Array[Contact], changes: Array[Store::GroupChange], login: String) -> void
+      #: (group: Group, contacts: Array[Contact], changes: Array[Store::GroupChange], login: String) -> void
       def initialize(group:, contacts:, changes:, login:)
         @group = group
         @login = login
diff --git a/lib/pro_tacts/admin/import_saved.rb b/lib/pro_tacts/admin/import_saved.rb
index 32b851f..cfcef97 100644
--- a/lib/pro_tacts/admin/import_saved.rb
+++ b/lib/pro_tacts/admin/import_saved.rb
@@ -19,7 +19,7 @@ module ProTacts
     class ImportSaved < Phlex::HTML
       # @rbs @contact: Contact
       # @rbs @login: String
-      # @rbs @groups: Array[Store::Group]
+      # @rbs @groups: Array[Group]
       # @rbs @all_groups: Array[Store::GroupChoice]
       # @rbs @aside: Phlex::HTML
       # @rbs @sidebar: Phlex::HTML
@@ -27,7 +27,7 @@ module ProTacts
       # @rbs @land: String
       # @rbs @notice: String?
 
-      #: (contact: Contact, login: String, groups: Array[Store::Group], all_groups: Array[Store::GroupChoice], aside: Phlex::HTML, sidebar: Phlex::HTML, edit: String, land: String, ?notice: String?) -> void
+      #: (contact: Contact, login: String, groups: Array[Group], all_groups: Array[Store::GroupChoice], aside: Phlex::HTML, sidebar: Phlex::HTML, edit: String, land: String, ?notice: String?) -> void
       def initialize(contact:, login:, groups:, all_groups:, aside:, sidebar:, edit:, land:, notice: nil)
         @contact = contact
         @login = login
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index 273faa1..a86c4b2 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -46,7 +46,7 @@ module ProTacts
     # @rbs @inherited: Array[Inherited]
     # @rbs @vcard: VCard
     # @rbs @etag: String
-    # @rbs @groups_by_line: Hash[String, Store::Group]
+    # @rbs @groups_by_line: Hash[String, Group]
 
     # Ids end up in paths and arrive from client-supplied hrefs, so an id
     # outside this charset cannot be served.
@@ -354,7 +354,7 @@ module ProTacts
     # card spells the line exactly as its group does still has one, and
     # naming the group over both copies says something true about the
     # value even where it is wrong about the byte.
-    #: (VCard::Parser::Line line) -> Store::Group?
+    #: (VCard::Parser::Line line) -> Group?
     def group_of(line)
       groups_by_line[line.verbatim.chomp]
     end
@@ -364,12 +364,12 @@ module ProTacts
     # Every inherited line's group, keyed by the line's own bytes with
     # the terminator off — a group's line is stored without one and
     # composes with the card's, so neither side is compared as it lies.
-    #: () -> Hash[String, Store::Group]
+    #: () -> Hash[String, Group]
     def groups_by_line
       return @groups_by_line if defined?(@groups_by_line)
 
       @groups_by_line = @inherited.to_h {
-        [it.line.chomp, it.group] #: [String, Store::Group]
+        [it.line.chomp, it.group] #: [String, Group]
       }
     end
 
diff --git a/lib/pro_tacts/group.rb b/lib/pro_tacts/group.rb
new file mode 100644
index 0000000..c43cf25
--- /dev/null
+++ b/lib/pro_tacts/group.rb
@@ -0,0 +1,71 @@
+require "digest"
+require "json"
+
+require "pro_tacts/contact"
+require "pro_tacts/vcard"
+
+module ProTacts
+  # 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
+  # label is the SQL one Store#group_label computes, so a tag and a
+  # heading can never disagree about what to call a nameless group.
+  #
+  # The signature lives in sig/pro_tacts/group.rbs, this being a Data
+  # class.
+  # @rbs skip
+  Group = Data.define(:id, :name, :label, :lines, :members)
+
+  # Reopened rather than defined in the block above, for the reason
+  # CardDiff is.
+  class Group
+    # The group names that choose what a client syncs: `sync:*` for
+    # everyone, `sync:<login>` for one user
+    # (docs/plans/2026-09-12-per-user-books.md). Unique like every
+    # group's name (db/migrations/008_group_names.rb).
+    SYNC_PREFIX = "sync:" #: String
+    EVERYONE = "#{SYNC_PREFIX}*" #: String
+
+    # Whether a group of this name chooses what a client syncs. Said of
+    # a name rather than a group, because the store asks it of a row
+    # and of a name a rename is about to write.
+    #: (String? name) -> bool
+    def self.sync_name?(name)
+      name&.start_with?(SYNC_PREFIX) == true
+    end
+
+    # The group's lines read through the one model that knows how to
+    # read an address and a note — a Contact over a card made of
+    # those lines and nothing else — for the screens that render and
+    # edit them. It composes nothing and is never stored or served.
+    #: () -> Contact
+    def reading
+      Contact.new(id:, stored: VCard.new(lines.map { "#{it}\r\n" }.join), birthday: nil, inherited: [])
+    end
+
+    # Whether this is one of the `sync:` groups, for the listing that
+    # sets those groups apart from the ones a person made
+    # (Admin::GroupsIndex).
+    #: () -> bool
+    def sync?
+      Group.sync_name?(name)
+    end
+
+    # The group's etag, for the editor's snapshot guard: a hash of
+    # everything the edit screen shows and the save writes, so a
+    # group edited since the page loaded refuses the stale save
+    # rather than reverting what changed (Web#apply_edit's rule).
+    # Nothing serves it, so nothing has to agree with it but the form.
+    #: () -> String
+    def version
+      Digest::SHA256.hexdigest([name, lines, members].to_json)
+    end
+
+    # What a console session sees for one (console.rb): the Data
+    # class's own inspect would carry every lent line and every
+    # member id, and a listing of groups is unreadable at a prompt.
+    #: () -> String
+    def inspect
+      "#<#{self.class.name} id=#{id.inspect} label=#{label.inspect} members=#{members.length}>"
+    end
+  end
+end
diff --git a/lib/pro_tacts/import/write.rb b/lib/pro_tacts/import/write.rb
index 8dca61c..bf3fe92 100644
--- a/lib/pro_tacts/import/write.rb
+++ b/lib/pro_tacts/import/write.rb
@@ -90,7 +90,7 @@ module ProTacts
         # ticked and asked for — and a membership written twice is a
         # constraint violation rather than a second membership.
         join = [*chosen, *named.map { group_id(it) }]
-        join << group_id(Store::EVERYONE) if everyone
+        join << group_id(Group::EVERYONE) if everyone
         @store.regroup(id, join: join.uniq, leave: []) unless join.empty?
 
         # Read back rather than kept from the put, because the group
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index d062457..3f92b2e 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -8,6 +8,7 @@ require "pro_tacts/card_diff"
 require "pro_tacts/change_id"
 require "pro_tacts/contact"
 require "pro_tacts/edited_contact"
+require "pro_tacts/group"
 require "pro_tacts/vcard"
 require "pro_tacts/vcard/parser"
 
@@ -99,54 +100,6 @@ module ProTacts
     # @rbs skip
     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
-    # label is the SQL one #group_label computes, so a tag and a
-    # heading can never disagree about what to call a nameless group.
-    # @rbs skip
-    Group = Data.define(:id, :name, :label, :lines, :members)
-
-    # Reopened rather than defined in the block above, for the reason
-    # CardDiff is.
-    class Group
-      # The group's lines read through the one model that knows how to
-      # read an address and a note — a Contact over a card made of
-      # those lines and nothing else — for the screens that render and
-      # edit them. It composes nothing and is never stored or served.
-      #: () -> Contact
-      def reading
-        Contact.new(id:, stored: VCard.new(lines.map { "#{it}\r\n" }.join), birthday: nil, inherited: [])
-      end
-
-      # Whether this is one of the `sync:` groups that choose what a
-      # client syncs — #sync_name?'s rule, said of a group a screen
-      # already holds rather than of a row's name, for the listing that
-      # sets those groups apart from the ones a person made
-      # (Admin::GroupsIndex).
-      #: () -> bool
-      def sync?
-        name&.start_with?(SYNC_PREFIX) == true
-      end
-
-      # The group's etag, for the editor's snapshot guard: a hash of
-      # everything the edit screen shows and the save writes, so a
-      # group edited since the page loaded refuses the stale save
-      # rather than reverting what changed (Web#apply_edit's rule).
-      # Nothing serves it, so nothing has to agree with it but the form.
-      #: () -> String
-      def version
-        Digest::SHA256.hexdigest([name, lines, members].to_json)
-      end
-
-      # What a console session sees for one (console.rb): the Data
-      # class's own inspect would carry every lent line and every
-      # member id, and a listing of groups is unreadable at a prompt.
-      #: () -> String
-      def inspect
-        "#<#{self.class.name} id=#{id.inspect} label=#{label.inspect} members=#{members.length}>"
-      end
-    end
-
     # A group as a picker names it: enough to show it and to submit it,
     # and no more. A Group carries the lines it lends and the ids of
     # its members, two reads beyond the group row itself (#load_groups);
@@ -183,16 +136,6 @@ module ProTacts
     # than a run of bad draws.
     CONTACT_ID_ATTEMPTS = 8 #: Integer
 
-    # the label is written against the table by name.
-    GROUP_COLUMNS = [Sequel[:groups][:id], Sequel[:groups][:name]].freeze #: Array[untyped]
-
-    # The group names that choose what a client syncs: `sync:*` for
-    # everyone, `sync:<login>` for one user
-    # (docs/plans/2026-09-12-per-user-books.md). Unique like every
-    # group's name (db/migrations/008_group_names.rb).
-    SYNC_PREFIX = "sync:" #: String
-    EVERYONE = "#{SYNC_PREFIX}*" #: String
-
     # A book named `*`, which would be everyone's (#name_book).
     class EveryonesBookName < ArgumentError; end
 
@@ -345,7 +288,7 @@ module ProTacts
     # nil.
     #: (String id) -> Contact?
     def contact(id)
-      contact_from(cards.where(id:).sole, birthday_of(id), inherited_of(id))
+      contact!(id)
     rescue Sequel::NoMatchingRow
       nil
     end
@@ -361,11 +304,10 @@ module ProTacts
     #: (String id) -> Array[Group]
     def groups_of(id)
       load_groups(
-        groups
+        labeled_groups
           .join(:group_members, group_id: :id)
           .where(card_id: id)
           .order(Sequel[:groups][:id])
-          .select(*GROUP_COLUMNS, group_label.as(:label))
           .all,
       )
     end
@@ -380,7 +322,7 @@ module ProTacts
     # (docs/plans/2026-09-21-books-not-book-names.md).
     #: (String login) -> Set[String]
     def book_cards(login)
-      ids = groups.where(name: [EVERYONE, own_sync_name(login)]).select_map(:id)
+      ids = groups.where(name: [Group::EVERYONE, own_sync_name(login)]).select_map(:id)
       Set.new(group_members.where(group_id: ids).select_map(:card_id).map(&:to_s))
     end
 
@@ -408,7 +350,7 @@ module ProTacts
     def name_book(login, name)
       name = name.to_s.strip
       name = nil if name.empty?
-      raise EveryonesBookName, "#{EVERYONE} is everyone's book, not #{login}'s" if name == "*"
+      raise EveryonesBookName, "#{Group::EVERYONE} is everyone's book, not #{login}'s" if name == "*"
 
       @database.transaction do
         group = groups.where(name: own_sync_name(login)).select_map(:id).first
@@ -564,10 +506,10 @@ module ProTacts
     # keep serving the dead href out of its cache forever.
     #: (String id) -> String
     def reid(id)
-      # `sole` rather than #contact's nil-for-404: a read whose filter
-      # means one row, and no row here is the caller's mistake to hear
-      # about as Sequel::NoMatchingRow, not a request to answer.
-      before = contact_from(cards.where(id:).sole, birthday_of(id), inherited_of(id))
+      # #contact! rather than #contact's nil-for-404: no row here is the
+      # caller's mistake to hear about as Sequel::NoMatchingRow, not a
+      # request to answer.
+      before = contact!(id)
 
       CONTACT_ID_ATTEMPTS.times do
         new_id = ChangeId.mint(ChangeId::CONTACT_LENGTH)
@@ -595,7 +537,7 @@ module ProTacts
             # rather than moved — the deal #reindex always gives it. The
             # parameters follow their properties away on the cascade.
             card_properties.where(card_id: id).delete
-            after = contact_from(cards.where(id: new_id).sole, birthday_of(new_id), inherited_of(new_id))
+            after = contact!(new_id)
             record(id, action: Action::DELETE, etag: nil, diff: CardDiff.between(before.vcard, nil))
             record(
               new_id,
@@ -654,7 +596,7 @@ module ProTacts
     # composition both use, so a rename never moves one.
     #: () -> Array[Group]
     def all_groups
-      load_groups(groups.select(*GROUP_COLUMNS, group_label.as(:label)).order(:id).all)
+      load_groups(labeled_groups.order(:id).all)
     end
 
     # Every group as a picker needs it: the row, its label and how many
@@ -670,7 +612,7 @@ module ProTacts
     #: () -> Array[GroupChoice]
     def group_choices
       member_count = group_members.where(group_id: Sequel[:groups][:id]).select(Sequel.function(:count).*)
-      groups.select(*GROUP_COLUMNS, group_label.as(:label), member_count.as(:member_count)).order(:id).all.map { |row|
+      labeled_groups.select_append(member_count.as(:member_count)).order(:id).all.map { |row|
         GroupChoice.new(id: row.fetch(:id).to_s, name: row.fetch(:name)&.to_s, label: row.fetch(:label).to_s,
                         member_count: row.fetch(:member_count).to_i)
       }
@@ -680,7 +622,7 @@ module ProTacts
     # #contact's own shape.
     #: (String id) -> Group?
     def group(id)
-      load_groups([groups.select(*GROUP_COLUMNS, group_label.as(:label)).where(id:).sole]).fetch(0)
+      load_groups([labeled_groups.where(id:).sole]).fetch(0)
     rescue Sequel::NoMatchingRow
       nil
     end
@@ -700,7 +642,7 @@ module ProTacts
       name = nil if name.to_s.strip.empty?
       @database.transaction do
         was = groups.where(id:).sole.fetch(:name)&.to_s
-        if was != name && (sync_name?(was) || sync_name?(name))
+        if was != name && (Group.sync_name?(was) || Group.sync_name?(name))
           members = member_ids([id])
           fan_out(members, moved: members) { groups.where(id:).update(name:) }
         else
@@ -829,7 +771,7 @@ module ProTacts
       name = row.fetch(:name).to_s
       # Refused before the transaction, #name_book's reason for the
       # same refusal: Sequel wraps what a rollback raises.
-      raise EveryonesBookName, "#{EVERYONE} is everyone's book, not a group to delete" if name == EVERYONE
+      raise EveryonesBookName, "#{Group::EVERYONE} is everyone's book, not a group to delete" if name == Group::EVERYONE
 
       @database.transaction do
         members = member_ids([id])
@@ -839,7 +781,7 @@ module ProTacts
           action: GroupAction::DELETE,
           detail: {"name" => row.fetch(:name), "lines" => lines, "members" => members},
         )
-        fan_out(members, moved: sync_name?(name) ? members : []) {
+        fan_out(members, moved: Group.sync_name?(name) ? members : []) {
           groups.where(id:).delete
         }
         true
@@ -1057,25 +999,20 @@ module ProTacts
     # (db/migrations/008_group_names.rb).
     #: () -> String
     def everyone_group_id
-      groups.where(name: EVERYONE).sole.fetch(:id).to_s
+      groups.where(name: Group::EVERYONE).sole.fetch(:id).to_s
     rescue Sequel::NoMatchingRow
-      create_group(name: EVERYONE)
+      create_group(name: Group::EVERYONE)
     end
 
     # 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)}"
-    end
-
-    #: (String? name) -> bool
-    def sync_name?(name)
-      name&.start_with?(SYNC_PREFIX) == true
+      "#{Group::SYNC_PREFIX}#{book_name(login)}"
     end
 
     #: (String group_id) -> bool
     def sync_group?(group_id)
-      sync_name?(groups.where(id: group_id).sole.fetch(:name)&.to_s)
+      Group.sync_name?(groups.where(id: group_id).sole.fetch(:name)&.to_s)
     end
 
     #: (String group_id, String card_id) -> bool
@@ -1095,9 +1032,11 @@ module ProTacts
         .map { it.fetch(:card_id).to_s }
     end
 
-    # A group_members row names a card by foreign key, so a missing
-    # row here is the corruption `sole` exists to raise on rather than
-    # the ordinary miss an href off the wire is.
+    # #contact for the callers to whom no row is not an ordinary answer:
+    # it raises Sequel::NoMatchingRow where #contact answers nil. A
+    # group_members row names a card by foreign key, so a missing row
+    # under #fan_out is the corruption `sole` exists to raise on, and
+    # #reid is handed an id its caller says exists.
     #: (String id) -> Contact
     def contact!(id)
       contact_from(cards.where(id:).sole, birthday_of(id), inherited_of(id))
@@ -1246,7 +1185,7 @@ module ProTacts
     def groups_by_id(ids)
       return {} if ids.empty?
 
-      load_groups(groups.select(*GROUP_COLUMNS, group_label.as(:label)).where(id: ids.uniq).all).to_h {
+      load_groups(labeled_groups.where(id: ids.uniq).all).to_h {
         [it.id, it] #: [String, Group]
       }
     end
@@ -1260,6 +1199,14 @@ module ProTacts
       Sequel.function(:coalesce, Sequel[:groups][:name], Sequel[:groups][:id])
     end
 
+    # A groups row's own columns and its label, the start of every read
+    # that hands out a group, qualified for #group_label's reason: the
+    # label is written against the table by name.
+    #: () -> Sequel::Dataset
+    def labeled_groups
+      groups.select(Sequel[:groups][:id], Sequel[:groups][:name], group_label.as(:label))
+    end
+
     # The join both inherited reads walk: a membership to the property
     # it inherits, qualified and ordered so the composition neither
     # depends on what SQLite feels like returning nor trips over the
diff --git a/lib/pro_tacts/web/groups.rb b/lib/pro_tacts/web/groups.rb
index 0bc5413..625ff5d 100644
--- a/lib/pro_tacts/web/groups.rb
+++ b/lib/pro_tacts/web/groups.rb
@@ -126,13 +126,13 @@ module ProTacts
       r.redirect "/groups/#{id}", 303
     end
 
-    #: (Store::Group group, ?notice: String) -> String
+    #: (Group group, ?notice: String) -> String
     def group_edit_screen(group, notice: nil)
       response["Content-Type"] = "text/html; charset=utf-8"
       Admin::GroupsEdit.call(group:, login: @login, notice:)
     end
 
-    #: (Store::Group group) -> String
+    #: (Group group) -> String
     def members_screen(group)
       response["Content-Type"] = "text/html; charset=utf-8"
       Admin::GroupsMembers.call(group:, contacts: store.contacts, login: @login)
diff --git a/lib/pro_tacts/web/import.rb b/lib/pro_tacts/web/import.rb
index b5a5983..858ad9e 100644
--- a/lib/pro_tacts/web/import.rb
+++ b/lib/pro_tacts/web/import.rb
@@ -316,7 +316,7 @@ module ProTacts
     def import_picker(group, joined, named, member: nil)
       choices = store.group_choices
       lot = group.empty? ? nil : choices.find { it.name == group }
-      everyone = choices.find { it.name == Store::EVERYONE }
+      everyone = choices.find { it.name == Group::EVERYONE }
       none = [] #: Array[String]
       was = member ? store.groups_of(member.id).map(&:id) : none
       ticked = member ? was : [everyone&.id].compact
@@ -402,7 +402,7 @@ module ProTacts
       # does not exist yet, so there is no row and no answer, and the
       # card joins it the way every other create does
       # (Import::Write#call).
-      everyone = choices.find { it.name == Store::EVERYONE }
+      everyone = choices.find { it.name == Group::EVERYONE }
       syncing = everyone.nil? || ticked.include?(everyone.id)
 
       # N and FN are mandatory (RFC 2426 section 4), so a save blank
diff --git a/lib/pro_tacts/web/setup.rb b/lib/pro_tacts/web/setup.rb
index 0af4772..385a8dc 100644
--- a/lib/pro_tacts/web/setup.rb
+++ b/lib/pro_tacts/web/setup.rb
@@ -46,11 +46,11 @@ 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)}\n"
+          "#{@login} syncs #{Group::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
-          "#{Store::SYNC_PREFIX}#{r.params['name'].to_s.strip} is taken; #{@login}'s book is unchanged.\n"
+          "#{Group::SYNC_PREFIX}#{r.params['name'].to_s.strip} is taken; #{@login}'s book is unchanged.\n"
         end
       end
     end
diff --git a/sig/gems/sequel.rbs b/sig/gems/sequel.rbs
index 5bdb978..bfd78a5 100644
--- a/sig/gems/sequel.rbs
+++ b/sig/gems/sequel.rbs
@@ -91,6 +91,7 @@ module Sequel
     # merged by whichever table's column won. Signed here so it reads
     # as the query builder's rather than Enumerable's own #select.
     def select: (*untyped columns) -> Dataset
+    def select_append: (*untyped columns) -> Dataset
     def limit: (Integer count) -> Dataset
     # A dataset as a subquery under a name, for a column computed in
     # another dataset's select (Store#group_choices' member count).
diff --git a/sig/pro_tacts/admin.rbs b/sig/pro_tacts/admin.rbs
index f25ff78..ec6e770 100644
--- a/sig/pro_tacts/admin.rbs
+++ b/sig/pro_tacts/admin.rbs
@@ -7,7 +7,7 @@
 module ProTacts
   module Admin
     class ContactsShow
-      def self.call: (contact: Contact, login: String, groups: Array[Store::Group], all_groups: Array[Store::GroupChoice], changes: Array[Store::Change], ?notice: String?) -> String
+      def self.call: (contact: Contact, login: String, groups: Array[Group], all_groups: Array[Store::GroupChoice], changes: Array[Store::Change], ?notice: String?) -> String
     end
 
     # `aside` is a Phlex component, which has no type here: phlex
@@ -18,27 +18,27 @@ module ProTacts
     end
 
     class Dashboard
-      def self.call: (recent: Array[Store::RecentContact], upcoming: Array[Store::UpcomingBirthday], query: String?, login: String, groups: Array[Store::Group], ?notice: String?) -> String
+      def self.call: (recent: Array[Store::RecentContact], upcoming: Array[Store::UpcomingBirthday], query: String?, login: String, groups: Array[Group], ?notice: String?) -> String
     end
 
     class ContactsIndex
-      def self.call: (contacts: Array[Contact], groups: Array[Store::Group], login: String) -> String
+      def self.call: (contacts: Array[Contact], groups: Array[Group], login: String) -> String
     end
 
     class GroupsIndex
-      def self.call: (groups: Array[Store::Group], login: String, ?notice: String?) -> String
+      def self.call: (groups: Array[Group], login: String, ?notice: String?) -> String
     end
 
     class GroupsShow
-      def self.call: (group: Store::Group, contacts: Array[Contact], changes: Array[Store::GroupChange], login: String) -> String
+      def self.call: (group: Group, contacts: Array[Contact], changes: Array[Store::GroupChange], login: String) -> String
     end
 
     class GroupsEdit
-      def self.call: (group: Store::Group, login: String, ?notice: String?) -> String
+      def self.call: (group: Group, login: String, ?notice: String?) -> String
     end
 
     class GroupsMembers
-      def self.call: (group: Store::Group, contacts: Array[Contact], login: String) -> String
+      def self.call: (group: Group, contacts: Array[Contact], login: String) -> String
     end
 
     class DeviceSetup
@@ -68,7 +68,7 @@ module ProTacts
     end
 
     class ImportSaved
-      def self.call: (contact: Contact, login: String, groups: Array[Store::Group], all_groups: Array[Store::GroupChoice], aside: untyped, sidebar: untyped, edit: String, land: String, ?notice: String?) -> String
+      def self.call: (contact: Contact, login: String, groups: Array[Group], all_groups: Array[Store::GroupChoice], aside: untyped, sidebar: untyped, edit: String, land: String, ?notice: String?) -> String
     end
 
     # Rendered by Dashboard, not called from checked code; only
diff --git a/sig/pro_tacts/contact.rbs b/sig/pro_tacts/contact.rbs
index db24f36..e8c8edd 100644
--- a/sig/pro_tacts/contact.rbs
+++ b/sig/pro_tacts/contact.rbs
@@ -31,11 +31,11 @@ module ProTacts
     end
 
     class Inherited
-      attr_reader group: Store::Group
+      attr_reader group: Group
       attr_reader position: Integer
       attr_reader line: String
 
-      def self.new: (group: Store::Group, position: Integer, line: String) -> instance
+      def self.new: (group: Group, position: Integer, line: String) -> instance
     end
 
     class Photo
diff --git a/sig/pro_tacts/group.rbs b/sig/pro_tacts/group.rbs
new file mode 100644
index 0000000..09286f5
--- /dev/null
+++ b/sig/pro_tacts/group.rbs
@@ -0,0 +1,15 @@
+# Group is a Data class, whose members the inline parser cannot read
+# (see sig/pro_tacts/contact.rbs). It is marked `@rbs skip` in group.rb
+# and signed here instead; the methods reopened onto it carry their
+# own inline types.
+module ProTacts
+  class Group
+    attr_reader id: String
+    attr_reader name: String?
+    attr_reader label: String
+    attr_reader lines: Array[String]
+    attr_reader members: Array[String]
+
+    def self.new: (id: String, name: String?, label: String, lines: Array[String], members: Array[String]) -> instance
+  end
+end
diff --git a/sig/pro_tacts/store.rbs b/sig/pro_tacts/store.rbs
index f070db6..7af5774 100644
--- a/sig/pro_tacts/store.rbs
+++ b/sig/pro_tacts/store.rbs
@@ -38,16 +38,6 @@ module ProTacts
       def self.new: (contact: Contact, occurs_on: Date) -> instance
     end
 
-    class Group
-      attr_reader id: String
-      attr_reader name: String?
-      attr_reader label: String
-      attr_reader lines: Array[String]
-      attr_reader members: Array[String]
-
-      def self.new: (id: String, name: String?, label: String, lines: Array[String], members: Array[String]) -> instance
-    end
-
     class GroupChoice
       attr_reader id: String
       attr_reader name: String?
diff --git a/tasks/book.rake b/tasks/book.rake
index a30bce7..b9f0b72 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)}"
+      puts "#{login} syncs #{ProTacts::Group::SYNC_PREFIX}#{store.book_name(login)}"
     end
   end
 end
diff --git a/test/fixture_data.rb b/test/fixture_data.rb
index 4bb38b4..21307b5 100644
--- a/test/fixture_data.rb
+++ b/test/fixture_data.rb
@@ -136,7 +136,7 @@ module FixtureData
   def self.seed_groups(store)
     seed_group(store, name: "Booles", lines: HOUSEHOLD, members: MEMBERS)
     seed_group(store, members: birthday_cards)
-    seed_group(store, name: ProTacts::Store::EVERYONE, members: cards.keys - unsynced_cards)
+    seed_group(store, name: ProTacts::Group::EVERYONE, members: cards.keys - unsynced_cards)
     seed_group(store, name: "Curies", members: CURIES)
     seed_clubs(store)
   end
diff --git a/test/pro_tacts/admin/test_import_pages.rb b/test/pro_tacts/admin/test_import_pages.rb
index c15901d..d4a41b7 100644
--- a/test/pro_tacts/admin/test_import_pages.rb
+++ b/test/pro_tacts/admin/test_import_pages.rb
@@ -614,7 +614,7 @@ class AdminImportPagesTest < Minitest::Test
 
       save(id, 0, first: "Jane", last: "Booles", "named" => [])
 
-      assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
+      assert_equal [ProTacts::Group::EVERYONE], store.all_groups.map(&:name)
     end
   end
 
@@ -642,7 +642,7 @@ class AdminImportPagesTest < Minitest::Test
     with_contacts({}) do |store|
       id = upload(JANE + PLAIN)
       save(id, 0, first: "Jane", last: "Booles")
-      everyone = store.all_groups.find { it.name == ProTacts::Store::EVERYONE }
+      everyone = store.all_groups.find { it.name == ProTacts::Group::EVERYONE }
 
       get "/import/#{id}/1"
 
@@ -661,7 +661,7 @@ class AdminImportPagesTest < Minitest::Test
     with_contacts({}) do |store|
       id = upload(JANE + PLAIN)
       save(id, 0, first: "Jane", last: "Booles")
-      everyone = store.all_groups.find { it.name == ProTacts::Store::EVERYONE }
+      everyone = store.all_groups.find { it.name == ProTacts::Group::EVERYONE }
 
       save(id, 1, first: "Sam", last: "Booles", "groups" => [], "named" => [])
 
@@ -812,9 +812,9 @@ class AdminImportPagesTest < Minitest::Test
       sam = store.contacts.find { it.name == "Sam Booles" }
 
       assert_equal [jane.id], store.group(school).members
-      assert_equal [lot, "school", ProTacts::Store::EVERYONE],
+      assert_equal [lot, "school", ProTacts::Group::EVERYONE],
                    store.all_groups.select { it.members.include?(jane.id) }.map(&:name).sort
-      assert_equal [lot, ProTacts::Store::EVERYONE],
+      assert_equal [lot, ProTacts::Group::EVERYONE],
                    store.all_groups.select { it.members.include?(sam.id) }.map(&:name).sort
     end
   end
@@ -836,7 +836,7 @@ class AdminImportPagesTest < Minitest::Test
       # The last row there was, so the walk ends: nothing was filed
       # under a group of its own, and the book is where it went.
       assert_equal "/contacts", last_response.headers["location"]
-      assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
+      assert_equal [ProTacts::Group::EVERYONE], store.all_groups.map(&:name)
     end
   end
 
diff --git a/test/pro_tacts/import/test_write.rb b/test/pro_tacts/import/test_write.rb
index 5a4cf52..731c187 100644
--- a/test/pro_tacts/import/test_write.rb
+++ b/test/pro_tacts/import/test_write.rb
@@ -67,7 +67,7 @@ class ImportWriteTest < Minitest::Test
       assert_equal 2, ids.uniq.length
       groups = store.all_groups.select { it.members.sort == ids.sort }
 
-      assert_equal ["import-20260921T031655Z", ProTacts::Store::EVERYONE],
+      assert_equal ["import-20260921T031655Z", ProTacts::Group::EVERYONE],
                    groups.map(&:name).sort
     end
   end
@@ -96,7 +96,7 @@ class ImportWriteTest < Minitest::Test
       id = Write.call(store, card, named: ["import-20260921T031655Z"], joins: [school]).id
 
       assert_equal [id], store.group(school).members
-      assert_equal ["import-20260921T031655Z", "school", ProTacts::Store::EVERYONE],
+      assert_equal ["import-20260921T031655Z", "school", ProTacts::Group::EVERYONE],
                    store.all_groups.select { it.members.include?(id) }.map(&:name).sort
     end
   end
@@ -114,7 +114,7 @@ class ImportWriteTest < Minitest::Test
 
       assert_equal [first, third].sort, store.group(school).members.sort
       assert_equal [third], store.group(work).members
-      assert_equal [ProTacts::Store::EVERYONE],
+      assert_equal [ProTacts::Group::EVERYONE],
                    store.all_groups.select { it.members.include?(second) }.map(&:name)
     end
   end
@@ -156,7 +156,7 @@ class ImportWriteTest < Minitest::Test
       id = Write.call(store, card, joins: [school]).id
 
       assert_equal [id], store.group(school).members
-      assert_equal %w[school], store.all_groups.map(&:name).reject { it == ProTacts::Store::EVERYONE }
+      assert_equal %w[school], store.all_groups.map(&:name).reject { it == ProTacts::Group::EVERYONE }
     end
   end
 
@@ -165,7 +165,7 @@ class ImportWriteTest < Minitest::Test
   # is an answer of its own and the one a card arrives with.
   def test_no_group_at_all_puts_the_card_in_everyones_book_alone
     write_card do |imported, store|
-      assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
+      assert_equal [ProTacts::Group::EVERYONE], store.all_groups.map(&:name)
       assert_equal [imported.id], store.all_groups.fetch(0).members
     end
   end
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index 87b653c..601e592 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -27,7 +27,7 @@ class ContactTest < Minitest::Test
   # Inherited lines are given as bare strings and lent by GROUP, the
   # group a composed row is marked with; a test that needs a second
   # lender passes its own.
-  GROUP = ProTacts::Store::Group.new(id: "nous", name: "Booles", label: "Booles", lines: [], members: ["aiden"]) #: ProTacts::Store::Group
+  GROUP = ProTacts::Group.new(id: "nous", name: "Booles", label: "Booles", lines: [], members: ["aiden"]) #: ProTacts::Group
 
   def contact(bytes = CARD, id: "aiden", birthday: nil, inherited: [], group: GROUP)
     lent = inherited.each_with_index.map { |line, position| ProTacts::Contact::Inherited.new(group:, position:, line:) }
diff --git a/test/pro_tacts/test_edited_contact.rb b/test/pro_tacts/test_edited_contact.rb
index 818dccf..040787d 100644
--- a/test/pro_tacts/test_edited_contact.rb
+++ b/test/pro_tacts/test_edited_contact.rb
@@ -21,7 +21,7 @@ class EditedContactTest < Minitest::Test
   EDITED_ADDRESS = HOUSEHOLD_ADDRESS.sub("7 Calculus", "8 Calculus") #: String
   RESERIALIZED_ADDRESS = HOUSEHOLD_ADDRESS.sub("ADR;TYPE=home:", "ADR;type=HOME;type=pref:") #: String
   OWN_ADDRESS = "ADR:;;1 Long Road;;;;" #: String
-  HOUSEHOLD = ProTacts::Store::Group.new(
+  HOUSEHOLD = ProTacts::Group.new(
     id: "kkkk", name: "Household", label: "Household", lines: [HOUSEHOLD_ADDRESS], members: ["aiden"],
   )
   LENT = [ProTacts::Contact::Inherited.new(group: HOUSEHOLD, position: 3, line: HOUSEHOLD_ADDRESS)].freeze
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 3fc2585..4654046 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -1246,8 +1246,8 @@ class StoreTest < Minitest::Test
       id = FixtureData.seed_group(store, name: "Household", members: %w[znorth aiden])
       nameless = store.create_group
 
-      assert_equal %(#<ProTacts::Store::Group id="#{id}" label="Household" members=2>), store.group(id).inspect
-      assert_equal %(#<ProTacts::Store::Group id="#{nameless}" label="#{nameless}" members=0>), store.group(nameless).inspect
+      assert_equal %(#<ProTacts::Group id="#{id}" label="Household" members=2>), store.group(id).inspect
+      assert_equal %(#<ProTacts::Group id="#{nameless}" label="#{nameless}" members=0>), store.group(nameless).inspect
     end
   end
 
@@ -1441,7 +1441,7 @@ class StoreTest < Minitest::Test
 
   def test_everyones_book_is_not_a_group_to_delete
     with_store({"aiden" => AIDEN}) do |store|
-      id = FixtureData.seed_group(store, name: ProTacts::Store::EVERYONE, members: ["aiden"])
+      id = FixtureData.seed_group(store, name: ProTacts::Group::EVERYONE, members: ["aiden"])
 
       assert_raises(ProTacts::Store::EveryonesBookName) { store.delete_group(id) }
 
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index c9937fb..ee16760 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -786,7 +786,7 @@ class WebTest < Minitest::Test
   # entry, so the sequences the sync tests read are the puts' alone.
   def with_contacts(names, &block)
     super(names.to_h { |id, name| [id, card(id, name)] }) do |store|
-      FixtureData.seed_group(store, name: ProTacts::Store::EVERYONE, members: names.keys)
+      FixtureData.seed_group(store, name: ProTacts::Group::EVERYONE, members: names.keys)
       block.call(store)
     end
   end
@@ -1035,7 +1035,7 @@ class WebTest < Minitest::Test
     with_contacts({"aiden" => "Aiden", "znorth" => "Zed"}) do |store|
       request "/dav/addressbook/", method: "REPORT", input: sync_collection("")
       token = last_response.body[%r{<d:sync-token>([^<]+)</d:sync-token>}, 1]
-      everyone = store.all_groups.find { it.name == ProTacts::Store::EVERYONE }.id
+      everyone = store.all_groups.find { it.name == ProTacts::Group::EVERYONE }.id
 
       store.remove_member(everyone, "znorth")
       request "/dav/addressbook/", method: "REPORT", input: sync_collection(token)