contact: a lent line that will not read raises
composed_note and the contact card skipped a lent NOTE that would not
read, a case every write's own read rules out, and joined a member's
several NOTEs, which migration 012 and Store#assert_one_note rule out.
Contact.lent_text raises for the first, and the member's one NOTE is
read as one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Kqo9sKAi4DnKCQkrtYoAwM
change
commit 8857dc4a77a86ff03260c6f6ba1f180a2129eef0
author Claude <noreply@anthropic.com>
date
parent 2ce450ab
diff --git a/lib/pro_tacts/admin/contact_card.rb b/lib/pro_tacts/admin/contact_card.rb
index d1ef160..9a56a03 100644
--- a/lib/pro_tacts/admin/contact_card.rb
+++ b/lib/pro_tacts/admin/contact_card.rb
@@ -119,9 +119,7 @@ module ProTacts
         @contact.inherited.each do |lent|
           next unless lent.line[/\A[^;:]*/].to_s.casecmp?("NOTE") == true
 
-          text = VCard.new(lent.line).lines.fetch(0).property&.text
-          next if text.nil?
-
+          text = Contact.lent_text(lent)
           dt(class: "type-label") { "notes" }
           dd(class: "type-body-sm") do
             div(style: "white-space: pre-wrap;") { text }
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index e1c7762..296d241 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -449,6 +449,17 @@ module ProTacts
       @lent_at
     end
 
+    # A lent line's text. Every write that stores one reads it first
+    # (#composed_note's reason), so one that will not read raises
+    # rather than being skipped as though the group lent nothing.
+    #: (Inherited row) -> String
+    def self.lent_text(row)
+      property = VCard.new(row.line).lines.fetch(0).property
+      raise "group #{row.group.id} lends a line that will not read" if property.nil?
+
+      property.text
+    end
+
     # Whether a lent line is a NOTE, read off its bytes the way
     # EditedContact reads a property name off them: the group schema
     # admits no `item1.` prefix to strip (db/migrations/004_groups.rb).
@@ -465,22 +476,17 @@ module ProTacts
     # hold no note serves its own NOTE's bytes as they lie, whatever
     # they say, the editor's whole safety.
     #
-    # A member's own two NOTE lines — a card older than the one-note
-    # migration, or a bug — join as the member's text, which serves
-    # the card this shape means to serve and converges on it at the
-    # next write. A lent line that will not read has no text to
-    # become and is skipped, unreachable through every write the
-    # schema and #shareable? gate.
+    # A member holds at most one NOTE: migration 012 joined the
+    # stored ones and Store#assert_one_note refuses a write of two. A
+    # lent line that will not read is a broken assumption too, every
+    # write the schema and EditedContact#shareable? gate reading it
+    # first, so it raises (Contact.lent_text).
     #: (Array[Inherited] rows) -> String?
     def composed_note(rows)
       return if rows.empty?
 
-      sections = rows.filter_map { |row|
-        text = VCard.new(row.line).lines.fetch(0).property&.text
-        NoteSections::Section.new(label: row.group.label, text:) if text
-      }
-      member = @stored.lines.select { it.names?("NOTE") }
-        .filter_map { it.property&.text }.join("\n\n")
+      sections = rows.map { NoteSections::Section.new(label: it.group.label, text: Contact.lent_text(it)) }
+      member = @stored.property("NOTE")&.text.to_s
       "NOTE:#{VCard.escape(NoteSections.join(member, sections))}"
     end
 
diff --git a/test/pro_tacts/test_contact.rb b/test/pro_tacts/test_contact.rb
index 3ef1a7c..3b45804 100644
--- a/test/pro_tacts/test_contact.rb
+++ b/test/pro_tacts/test_contact.rb
@@ -437,6 +437,15 @@ class ContactTest < Minitest::Test
     )
   end
 
+  # Every write that stores a lent line reads it first, so one that
+  # will not read is a broken assumption to raise on rather than a
+  # section to skip as though the group lent nothing.
+  def test_a_lent_note_that_will_not_read_raises
+    error = assert_raises(RuntimeError) { contact(STRUCTURED, inherited: ["NOTE;=;:"]).vcard }
+
+    assert_equal "group nous lends a line that will not read", error.message
+  end
+
   # A member with no note of its own serves the section alone, the
   # one NOTE inserted before END:VCARD the way a lent line is.
   def test_a_member_with_no_note_of_its_own_serves_the_section_alone