import: drop guards for a blank group, a stray slot, a lost row
import_picker allowed for an import with no group, which Staged.open
always names. Staged's paths checked a slot only constants name and an
index Roda's Integer matcher cannot make negative, and walk_on read a
row missing from its own walk as the first. The checks go, and the
missing row raises.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kqo9sKAi4DnKCQkrtYoAwM
change
commit 7637f42eb07392cbf64b44d4f0dccafe9a978698
author Claude <noreply@anthropic.com>
date
parent 8857dc4a
diff --git a/lib/pro_tacts/import/staged.rb b/lib/pro_tacts/import/staged.rb
index 9831491..c5f21c4 100644
--- a/lib/pro_tacts/import/staged.rb
+++ b/lib/pro_tacts/import/staged.rb
@@ -22,17 +22,14 @@ module ProTacts
       # them but the screens of the import that wrote them.
       DIRECTORY = "imports" #: String
 
+      # The cards' two slots, each a directory holding a file per card
+      # named by its place in the file, so a screen reads the one card
+      # it shows rather than every card the upload held.
       ORIGINAL = "original" #: String
       REVISED = "revised" #: String
+      # The walk's notes, a JSON file each.
       SAVED = "saved" #: String
       ROWS = "rows" #: String
-      # The walk's notes, a JSON file each.
-      SLOTS = [SAVED, ROWS].freeze #: Array[String]
-
-      # The cards' two slots, each a directory holding a file per card
-      # named by its place in the file, so a screen reads the one card
-      # it shows rather than every card the upload held.
-      CARDS = [ORIGINAL, REVISED].freeze #: Array[String]
 
       # One row of the walk's list as it was last read: what the row is
       # called and sorted by, off the card as it will be written, and
@@ -201,24 +198,20 @@ module ProTacts
         root / id
       end
 
-      # One slot's file. The slot is checked as narrowly as the id,
-      # for the reason the id is.
+      # One slot's file, or none for an id this never wrote. The id is
+      # the check; a slot is always one of the constants above.
       #: (String id, String slot) -> Pathname?
       def self.path(id, slot)
         directory = import_root(id)
-        return nil if directory.nil? || !SLOTS.include?(slot)
-
-        directory / file_name(slot)
+        directory / file_name(slot) if directory
       end
 
-      # One card's file in a cards slot, checked as #path checks: the
-      # place is an Integer the route matched, and never below zero.
+      # One card's file in a cards slot, #path's check: the place is
+      # the digits Roda's Integer matcher took, so never below zero.
       #: (String id, String slot, Integer index) -> Pathname?
       def self.card_path(id, slot, index)
         directory = import_root(id)
-        return nil if directory.nil? || !CARDS.include?(slot) || index.negative?
-
-        directory / slot / "#{index}.vcf"
+        directory / slot / "#{index}.vcf" if directory
       end
 
       # One slot, rewritten. Both refusals name which of the two
diff --git a/lib/pro_tacts/web/import.rb b/lib/pro_tacts/web/import.rb
index 8e88f08..d8f0a81 100644
--- a/lib/pro_tacts/web/import.rb
+++ b/lib/pro_tacts/web/import.rb
@@ -319,7 +319,7 @@ module ProTacts
     #: (String group, Array[String]? joined, Array[String]? named, ?member: Contact?, ?tags: Array[String]) -> Admin::ImportGroups
     def import_picker(group, joined, named, member: nil, tags: [])
       choices = store.group_choices
-      lot = group.empty? ? nil : choices.find { it.name == group }
+      lot = choices.find { it.name == group }
       everyone = choices.find { it.name == Group::EVERYONE }
       none = [] #: Array[String]
       was = member ? store.groups_of(member.id).map(&:id) : none
@@ -426,7 +426,7 @@ module ProTacts
                                           joined: ticked, named: wanted)
       end
 
-      edited = Admin::CardForm.contact_card(contact, first, middle, last, r.params)
+      edited = Admin::CardForm.contact_card(contact, first, middle, last, card_form_params)
       if target
         # A stored contact, whose birthday the model holds: the contact
         # editor's own save (#apply_edit), down to its one refusal.
@@ -506,7 +506,8 @@ module ProTacts
       # render.
       order = walk_order(rows)
       remaining = order.reject { |i| saved.key?(i.to_s) || i == index }
-      below = order.drop(order.index(index).to_i + 1)
+      at = order.index(index) or raise "row #{index} is not in its own walk"
+      below = order.drop(at + 1)
       r.redirect "/import/#{upload}/#{below.find { remaining.include?(it) } || remaining.fetch(0)}", 303
     end