import: show what a refused field held
A refusal now gives a few distinct values each unknown field held, beside the card that held it,
so a branch is written from what the error says, without opening a backup to read it.
A folded line is joined back together, and anything past a hundred characters is cut,
so a photo's base64 gives its length instead of filling the terminal up.

Assisted-by: Claude Opus 5 via Claude Code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Issue: rtqvmzvp
change omlplqkykmryryuxzqxrurxstrzlrrqs
commit 5e48891d4b34c3a60420544983936a9bd3837fec
author Alpha Chen <alpha@kejadlen.dev>
date
parent olqlkysn
diff --git a/docs/plans/2026-09-16-importing-from-macos.md b/docs/plans/2026-09-16-importing-from-macos.md
index 332be67..3bcf1e5 100644
--- a/docs/plans/2026-09-16-importing-from-macos.md
+++ b/docs/plans/2026-09-16-importing-from-macos.md
@@ -132,7 +132,8 @@ data, which the vCard leaves out, are unknown until a branch takes them
 too. Each branch
 names the parameters it accepts, and a parameter outside that list is
 unknown too. Once every contact in the batch is read, any unknown fails
-the build with every unknown name, its count, and a few source ids, and
+the build with every unknown name, its count, and a few distinct values
+it held, each beside its source id and cut short when long, and
 no plan is written. A small batch needs only the branches its own
 contacts use.
 
diff --git a/lib/pro_tacts/import/macos.rb b/lib/pro_tacts/import/macos.rb
index e2cbab8..701f6cc 100644
--- a/lib/pro_tacts/import/macos.rb
+++ b/lib/pro_tacts/import/macos.rb
@@ -19,19 +19,22 @@ module ProTacts
       # Every field no branch handles, across every contact read, so one
       # build names all of them.
       class Unknown < StandardError
-        # @rbs @seen: Hash[String, Array[String]]
+        # @rbs @seen: Hash[String, Array[[String, String]]]
 
         EXAMPLES = 3 #: Integer
 
+        # Long enough for a line's parameters, short of a photo's payload.
+        EXAMPLE_LENGTH = 100 #: Integer
+
         #: () -> void
         def initialize
           @seen = Hash.new { |seen, field| seen[field] = [] }
           super
         end
 
-        #: (String field, String source_id) -> void
-        def add(field, source_id)
-          @seen[field] << source_id
+        #: (String field, String source_id, String example) -> void
+        def add(field, source_id, example)
+          @seen[field] << [source_id, example]
         end
 
         #: () -> bool
@@ -39,11 +42,23 @@ module ProTacts
 
         #: () -> String
         def message
-          lines = @seen.sort.map do |field, source_ids|
-            "  #{field}: #{source_ids.size}, e.g. #{source_ids.uniq.first(EXAMPLES).join(", ")}"
+          lines = @seen.sort.flat_map do |field, seen|
+            examples = seen.uniq(&:last).first(EXAMPLES).map do |source_id, example|
+              "    #{source_id}: #{truncate(example).inspect}"
+            end
+            ["  #{field}: #{seen.size}", *examples]
           end
           "no branch handles these fields, so no plan was written:\n#{lines.join("\n")}"
         end
+
+        private
+
+        #: (String example) -> String
+        def truncate(example)
+          return example if example.length <= EXAMPLE_LENGTH
+
+          "#{example[0, EXAMPLE_LENGTH]}… (#{example.length} characters)"
+        end
       end
 
       # The reader's output, one parsed object per contact.
@@ -76,8 +91,9 @@ module ProTacts
 
         body = VCard::Parser.lines(vcard).filter_map { line(it, source_id, unknown) }
         # Beyond the vCard: what the serializer leaves out.
-        unknown.add("note", source_id) if note
-        unknown.add("imageData", source_id) if contact.fetch("imageData")
+        unknown.add("note", source_id, note) if note
+        image = contact.fetch("imageData") #: String?
+        unknown.add("imageData", source_id, image) if image
 
         backup = {"original.vcf" => vcard, "contact.json" => JSON.pretty_generate(contact)}
         backup["note.txt"] = note if note
@@ -95,8 +111,9 @@ module ProTacts
       #: (VCard::Parser::Line line, String source_id, Unknown unknown) -> String?
       def self.line(line, source_id, unknown)
         property = line.property
+        example = VCard::Parser.unfold(line.verbatim).chomp
         if property.nil?
-          unknown.add("unreadable line", source_id) unless line.verbatim.strip.empty?
+          unknown.add("unreadable line", source_id, example) unless example.strip.empty?
           return
         end
 
@@ -104,10 +121,10 @@ module ProTacts
         when "BEGIN", "END"
           nil
         when "VERSION"
-          unknown.add("VERSION:#{property.value}", source_id) unless property.value == "3.0"
+          unknown.add("VERSION:#{property.value}", source_id, example) unless property.value == "3.0"
           nil
         else
-          unknown.add(property.name.upcase, source_id)
+          unknown.add(property.name.upcase, source_id, example)
           nil
         end
       end
diff --git a/test/pro_tacts/import/test_macos.rb b/test/pro_tacts/import/test_macos.rb
index cec792a..a7720d1 100644
--- a/test/pro_tacts/import/test_macos.rb
+++ b/test/pro_tacts/import/test_macos.rb
@@ -58,14 +58,42 @@ class ImportMacosTest < Minitest::Test
 
       error = assert_raises(Macos::Unknown) { Macos.plan(dir, records, created_at: CREATED_AT) }
 
-      assert_includes error.message, "FN: 2, e.g. A:ABPerson, B:ABPerson"
-      assert_includes error.message, "N: 1, e.g. A:ABPerson"
-      assert_includes error.message, "note: 1, e.g. A:ABPerson"
-      assert_includes error.message, "imageData: 1, e.g. B:ABPerson"
+      assert_equal <<~MESSAGE.chomp, error.message
+        no branch handles these fields, so no plan was written:
+          FN: 2
+            A:ABPerson: "FN:Ada Lovelace"
+            B:ABPerson: "FN:Mary Boole"
+          N: 1
+            A:ABPerson: "N:Lovelace;Ada;;;"
+          imageData: 1
+            B:ABPerson: "/9j/"
+          note: 1
+            A:ABPerson: "Analyst."
+      MESSAGE
       refute dir.exist?
     end
   end
 
+  def test_examples_are_distinct_unfolded_and_cut_short
+    in_tmpdir do |dir|
+      records = %w[A B C D].map { record("#{it}:ABPerson", lines: ["PHOTO;ENCODING=b:#{"A" * 60}\r\n #{"A" * 60}"]) }
+      records << record("E:ABPerson", lines: ["NOTE:one\\ntwo"], note: "one\ntwo")
+      records << record("F:ABPerson", lines: ["NOTE:one\\ntwo"])
+
+      error = assert_raises(Macos::Unknown) { Macos.plan(dir, records, created_at: CREATED_AT) }
+
+      assert_equal <<~MESSAGE.chomp, error.message
+        no branch handles these fields, so no plan was written:
+          NOTE: 2
+            E:ABPerson: "NOTE:one\\\\ntwo"
+          PHOTO: 4
+            A:ABPerson: "PHOTO;ENCODING=b:#{"A" * 83}… (137 characters)"
+          note: 1
+            E:ABPerson: "one\\ntwo"
+      MESSAGE
+    end
+  end
+
   def test_a_card_that_is_not_version_3_is_refused
     in_tmpdir do |dir|
       source = record("A:ABPerson")