import: a label stays beside a line that will not read
Vcf.read keeps a line that will not read, but orphaned_groups skipped
it, so a group whose only other line was unreadable looked empty and
its label was dropped. Such a line now anchors its group, read off
its bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VAiyEheRBdzbBjZUDWZUPk
change
commit 86ee08ceab8212233d6af078b5c66416cafe6281
author Claude <noreply@anthropic.com>
date
parent 90dd3ebc
diff --git a/lib/pro_tacts/import/vcf.rb b/lib/pro_tacts/import/vcf.rb
index cc98d8a..db1faf4 100644
--- a/lib/pro_tacts/import/vcf.rb
+++ b/lib/pro_tacts/import/vcf.rb
@@ -185,20 +185,20 @@ module ProTacts
       # without its `item3.X-ABRELATEDNAMES` would leave a name for a
       # line that is not there. A grouped property this book reads —
       # `item1.ADR`, say — anchors its own group, because it is coming
-      # in.
+      # in, and so does a line that will not read, which comes in
+      # whatever it is (#read): its group is read off its bytes, there
+      # being no property to read it from.
       #: (Array[VCard::Parser::Line] lines) -> Array[String]
       def self.orphaned_groups(lines)
         anchored = {} #: Hash[String, bool]
         lines.each do |line|
           property = line.property
-          next if property.nil?
-
-          group = property.group
+          group = property ? property.group : VCard::Parser.unfold(line.verbatim)[/\A([A-Za-z0-9-]+)\./, 1]
           next if group.nil?
 
           # A label is in its group without anchoring it, so a group
           # holding nothing else is one with nothing but a label in it.
-          anchors = !property.name.casecmp?(LABEL) && known?(property.name)
+          anchors = property.nil? || (!property.name.casecmp?(LABEL) && known?(property.name))
           # Without case, as a property name compares and as
           # EditedContact#label_of and Merge pair a label with its
           # line: `ITEM1.X-ABLabel` names `item1.TEL`'s group.
diff --git a/test/pro_tacts/import/test_vcf.rb b/test/pro_tacts/import/test_vcf.rb
index ca1c1f9..1bdc33d 100644
--- a/test/pro_tacts/import/test_vcf.rb
+++ b/test/pro_tacts/import/test_vcf.rb
@@ -301,6 +301,7 @@ class ImportVcfTest < Minitest::Test
     "item1.X-ABLabel:_$!<Mobile>!$_", "item2.X-ABRELATEDNAMES:Sam", "item2.X-ABLabel:_$!<Spouse>!$_",
     "item3.X-ABLabel:orphan", "item4.ADR:;;1 Main St;;;;", "item4.X-ABRELATEDNAMES:Pat",
     "item4.X-ABLabel:_$!<Home>!$_", "X-SOCIALPROFILE:x", "PRODID:-//Apple", "CATEGORIES:Family",
+    "item5.TEL;=:+1 555 0102", "item5.X-ABLabel:unreadable",
   ].freeze #: Array[String]
 
   # Each line's group spelt in either case, as a client may write it
@@ -339,10 +340,15 @@ class ImportVcfTest < Minitest::Test
   end
 
   # The groups a reading's card keeps a label in, and the groups it
-  # keeps anything else in.
-  def labeled_groups(card) = card.properties.select { it.name.casecmp?(Vcf::LABEL) }.map { it.group&.downcase }.uniq
+  # keeps anything else in — a line that will not read included, its
+  # group read off its bytes.
+  def group_of(line) = (line.property&.group || line.verbatim[/\A([A-Za-z0-9-]+)\./, 1])&.downcase
 
-  def anchored_groups(card) = card.properties.reject { it.name.casecmp?(Vcf::LABEL) }.filter_map { it.group&.downcase }.uniq
+  def label?(line) = line.property&.name&.casecmp?(Vcf::LABEL)
+
+  def labeled_groups(card) = card.lines.select { label?(it) }.map { group_of(it) }.uniq
+
+  def anchored_groups(card) = card.lines.reject { label?(it) }.filter_map { group_of(it) }.uniq
 
   # A label names the line beside it, so none comes in without one.
   def test_no_label_comes_in_without_a_line_to_name