import: leave behind what pro-tacts will not show
Before: a property no screen here shows was kept in the card by
default, invisible but present, with dropping and the note as the two
ways out of that.

After: it does not come in. A card is meant to be the contact as this
app can show it, and a book padded with lines nothing can display is
one whose cards nobody can read. The review screen now asks a narrower
question -- of the properties leaving, which are worth saving under the
note on the way past -- and offers two choices, leaving behind by
default.

RFC 6352 section 6.3.2.2 is untouched by this: it binds this server to
keep what a client submits and does not understand, and it still does.
An import is the other direction, a person choosing what their own book
is made of.

How: Vcf.known? is the one question both the survey and the landing
ask, Vcf::KEEP is gone, and Land#decide keeps a line only when this
book reads it. That moves the orphaned-label test off the decisions and
onto the same question, so a label on a line that is coming in --
item1.ADR's, say -- comes in with it, and one on a line that is leaving
goes with it. A line that will not parse still rides along in the
card's bytes: there is no property name to have listed or decided, and
throwing it away unnamed is worse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrREeoPLXx1qsYYq7qkL8h
change
commit 44095c1fbd76f7c16068dee7232483cc20e84143
author Claude <noreply@anthropic.com>
date
parent 535eb907
diff --git a/docs/plans/2026-09-21-import-a-vcf.md b/docs/plans/2026-09-21-import-a-vcf.md
index 7e5b89c..56761a3 100644
--- a/docs/plans/2026-09-21-import-a-vcf.md
+++ b/docs/plans/2026-09-21-import-a-vcf.md
@@ -41,44 +41,61 @@ landed.
 ## Two screens, because of the question
 
 A card is stored as the card it arrived as. That is this server's whole
-posture (`2026-08-24-vcard-storage-and-groups.md`): RFC 6352 section
-6.3.2.2 requires a server to keep what it does not understand, and this
-one keeps it byte for byte. So there is no field mapping to approve on
-the way in, and nothing to confirm about N, TEL, ADR or PHOTO.
+posture (`2026-08-24-vcard-storage-and-groups.md`), and every property
+this address book reads travels through byte for byte. So there is no
+field mapping to approve on the way in, and nothing to confirm about N,
+TEL, ADR or PHOTO.
 
 There is one real question. A book exported from Contacts.app carries
 properties no screen here shows — `X-ABRELATEDNAMES`, `X-SOCIALPROFILE`,
-`X-ABADR`, `PRODID`, whatever the source thought worth writing. Kept,
-they travel with the card and come back out of it untouched; they are
-simply invisible in this app. That is the right default and it is not
-always what is wanted: some of it is the source's bookkeeping, and some
-of it is worth reading even with no field to read it in.
-
-So the importer gets three choices per unknown property:
-
-- **keep it in the card** — the default, and the one that loses nothing.
-- **drop it** — for what the source keeps and this book has no use for.
-- **write it under the note** — for a value worth reading anyway. A
-  spouse's name is worth more under the note than nowhere.
+`X-ABADR`, `PRODID`, whatever the source thought worth writing. Those
+do not come in. A card is meant to be the contact as this app can show
+it, and a book padded with lines nothing can display is one whose cards
+nobody can read — the bytes are the least of it.
+
+RFC 6352 section 6.3.2.2 is not in tension with that. It binds this
+server to keep what a *client* submits and does not understand, and it
+does (`vcard.rb`): a property macOS PUTs into a card stays in that card
+and comes back out of it untouched. An import is the other direction —
+a person choosing what their own book is made of — and there the rule
+is the opposite one.
+
+So the question is narrower than it first looks: of the properties
+leaving, which are worth saving on the way past? Two choices per
+unknown property:
+
+- **leave it behind** — the default, and what anything nobody speaks
+  for gets.
+- **save it under the note** — for a value worth reading even with no
+  field to read it in. A spouse's name is worth more under the note
+  than nowhere.
 
 The question cannot be asked until the file has been read, which is why
 this is two requests rather than one. The first stages the upload and
 surveys it; the second spends the answers. Each unknown property is
 shown with how many lines wear it and a few real values out of that very
-file, so the choice is made looking at the book's own data rather than
+file, so the choice is made looking at the source's own data rather than
 at a property name.
 
 The decision is per property name, not per line: `X-SOCIALPROFILE;
 type=twitter` is not a second decision from `X-SOCIALPROFILE`. A label
 Contacts hung beside a line it names (`item3.X-ABLabel`) goes wherever
-that line goes — a label naming nothing is worse than either choice.
-Under the note, the label is what the value is called, so the note
-reads `Spouse: Jane` and not `X-ABRELATEDNAMES: Jane`.
+that line goes — a label naming nothing is worse than either choice —
+while a label on a line that is coming in, `item1.ADR`'s say, comes in
+with it. Under the note, the label is what the value is called, so the
+note reads `Spouse: Jane` and not `X-ABRELATEDNAMES: Jane`.
+
+One thing is kept that nothing will show: a line that would not parse
+at all. The parser hands it back without a property name, so there is
+nothing to have listed on the review screen and nothing for the
+importer to have decided; throwing it away unnamed is worse than
+letting it ride along in the card's bytes.
 
 "Known" is the reader's list, not the writer's: the envelope, the fields
 a screen shows (`Contact`'s own accessors), and BDAY, which the store
-takes into the model. Adding a screen for a property is what takes it
-off the list of things to decide about.
+takes into the model. Adding a screen for a property is what brings it
+in, and until then importing one would only pad the cards with what
+nobody can read.
 
 ## The file waits on the server
 
diff --git a/lib/pro_tacts/admin/import_review.rb b/lib/pro_tacts/admin/import_review.rb
index 67a6d21..ae695e2 100644
--- a/lib/pro_tacts/admin/import_review.rb
+++ b/lib/pro_tacts/admin/import_review.rb
@@ -11,14 +11,16 @@ module ProTacts
     #
     # A card is stored as the card it arrived as, so nothing here is a
     # field-by-field mapping to approve. The only question is about the
-    # properties no screen in this address book shows: keep them (they
-    # are served back untouched, just invisible here), drop them, or
-    # write them under the note where they can at least be read. Keep
-    # is checked, because it is the choice that loses nothing.
+    # properties no screen in this address book shows. Those do not
+    # come in — a book padded with what nothing can display is worse
+    # than a book without it — so the question is narrower than it
+    # looks: is this one worth saving under the note on the way past?
+    # Dropping is checked, because it is what happens to everything
+    # nobody speaks for.
     #
     # Each property is shown with how many lines wear it and a few real
     # values out of this very file, so the choice is made looking at
-    # the book's own data rather than at a property name.
+    # the source's own data rather than at a property name.
     #
     # `upload` is the staged file (Import::Staged), carried in a hidden
     # field: the bytes stay on the server, and only the ticket to them
@@ -32,12 +34,10 @@ module ProTacts
 
       # What each choice is called where it is made. The wording says
       # what happens to the line, not what the server does with it: an
-      # importer is deciding about their own address book, not about
-      # RFC 6352's rule for a property a server does not understand.
+      # importer is deciding what their own address book is made of.
       WORDING = {
-        ::ProTacts::Import::Vcf::KEEP => "keep it in the card",
-        ::ProTacts::Import::Vcf::DROP => "drop it",
-        ::ProTacts::Import::Vcf::NOTE => "write it under the note",
+        ::ProTacts::Import::Vcf::DROP => "leave it behind",
+        ::ProTacts::Import::Vcf::NOTE => "save it under the note",
       }.freeze #: Hash[String, String]
 
       #: (upload: String, file: String, cards: Integer, unknown: Array[::ProTacts::Import::Vcf::Unknown], group: String) -> void
@@ -89,7 +89,7 @@ module ProTacts
       def unknown_fields
         if @unknown.empty?
           p(class: "type-body-sm gl-muted") do
-            "Every property in this file is one this address book reads. Nothing to decide."
+            "Every property in this file is one pro-tacts reads. Nothing to decide."
           end
           return
         end
@@ -97,8 +97,8 @@ module ProTacts
         p(class: "type-body-sm") do
           "#{count(@unknown.length, "property", "properties")} here " \
             "#{@unknown.length == 1 ? "is one" : "are ones"} " \
-            "no screen in this address book shows. Kept, they travel with the card and come back out " \
-            "of it untouched; they are just not on display anywhere."
+            "no screen in pro-tacts shows, so #{@unknown.length == 1 ? "it stays" : "they stay"} " \
+            "behind. Send one under the note if the value is worth reading anyway."
         end
         @unknown.each { unknown_field(it) }
       end
@@ -115,7 +115,7 @@ module ProTacts
               # A label wrapping its own radio is Gloss's Radio.
               label do
                 input(type: "radio", name: "decide[#{property.name}]", value: choice,
-                      checked: choice == ::ProTacts::Import::Vcf::KEEP)
+                      checked: choice == ::ProTacts::Import::Vcf::DROP)
                 span { WORDING.fetch(choice) }
               end
             end
diff --git a/lib/pro_tacts/admin/import_upload.rb b/lib/pro_tacts/admin/import_upload.rb
index 2c4a9c0..f0d010f 100644
--- a/lib/pro_tacts/admin/import_upload.rb
+++ b/lib/pro_tacts/admin/import_upload.rb
@@ -14,9 +14,10 @@ module ProTacts
     # file that Mac already knows how to export.
     #
     # The form takes one .vcf and nothing else. What it does not ask
-    # for here is what to do with the properties this address book does
-    # not read — that question needs the file open to be worth asking,
-    # so it is the next screen's (ImportReview).
+    # for here is what to do with the properties this address book
+    # does not read — which of them are worth saving under the note
+    # rather than leaving behind. That question needs the file open to
+    # be worth asking, so it is the next screen's (ImportReview).
     #
     # `landed` is the import that just happened, rendered below the
     # form that is still there to run another.
@@ -51,7 +52,8 @@ module ProTacts
             h1(class: "type-h2", style: "margin: 0;") { "Import contacts" }
             p(class: "type-body-sm") do
               "Choose a vCard file — Contacts exports one with File, Export, Export vCard. " \
-                "The next screen says what is in it before anything lands."
+                "The next screen says what is in it before anything lands, including what " \
+                "pro-tacts cannot show and will leave behind."
             end
             form(action: "/import", method: "post", enctype: "multipart/form-data", class: "field-stack") do
               label(class: "field") do
diff --git a/lib/pro_tacts/import/land.rb b/lib/pro_tacts/import/land.rb
index 80b2665..a207857 100644
--- a/lib/pro_tacts/import/land.rb
+++ b/lib/pro_tacts/import/land.rb
@@ -10,12 +10,12 @@ module ProTacts
     # (docs/plans/2026-09-21-import-a-vcf.md).
     #
     # A card arrives a card and is stored as one: nothing here rebuilds
-    # it out of fields, so a property this address book has never heard
-    # of is kept by default and served back byte for byte (RFC 6352
-    # section 6.3.2.2, and VCard's whole posture). What the review
-    # screen collects is the exception to that — the handful of
-    # properties the importer would rather drop, or read under the
-    # note — and this is where those are spent.
+    # it out of fields, so every line this address book reads travels
+    # through byte for byte. What no screen here shows does not come in
+    # — a book padded with the source's bookkeeping is a book nobody
+    # can read the cards of — and what the review screen collects is
+    # the exception to that: the properties worth saving under the note
+    # on the way past. This is where those are spent.
     #
     # It writes through Store#put and Store#regroup rather than through
     # the routes those sit behind, so it is not a second way to write a
@@ -42,8 +42,9 @@ module ProTacts
       end
 
       # `decisions` is Vcf::CHOICES by property name, upper case, as
-      # Vcf#unknown names them; a property missing from it is kept.
-      # `group` is the group every arrival joins, or none.
+      # Vcf#unknown names them; a property missing from it is dropped,
+      # the choice nothing has to be said to get. `group` is the group
+      # every arrival joins, or none.
       #: (Store store, Array[VCard] cards, decisions: Hash[String, String], ?group: String?) -> Array[Contact]
       def self.call(store, cards, decisions:, group: nil)
         new(store, decisions:, group:).call(cards)
@@ -98,11 +99,10 @@ module ProTacts
         rest.insert(["UID:#{id}\r\n"])
       end
 
-      # The card with the importer's decisions spent on it: the lines
-      # they dropped gone, the lines they sent to the note gone from
-      # where they were and written under it, and everything else —
-      # every known property, and every unknown one they kept — exactly
-      # as it arrived, bytes and all.
+      # The card as this book will hold it: every property it reads,
+      # byte for byte as it arrived; nothing it does not read; and,
+      # under the note, the values the importer asked to save on the
+      # way past.
       #: (VCard card) -> VCard
       def decide(card)
         lines = card.lines
@@ -112,17 +112,19 @@ module ProTacts
 
         lines.each do |line|
           property = line.property
-          # A blank line, or one that would not read: there is no field
-          # to have made a decision about, and the bytes travel
-          # (VCard::Parser's own posture).
+          # A blank line, or one that would not read: no property name
+          # to have decided anything about, so the bytes travel rather
+          # than being thrown away unnamed (VCard::Parser's posture).
           if property.nil?
             kept << line.verbatim
             next
           end
 
-          choice = choice_for(property)
-          notes << noted(property, lines) if choice == Vcf::NOTE
-          next unless choice == Vcf::KEEP
+          unless Vcf.known?(property.name)
+            notes << noted(property, lines) if @decisions[property.name.upcase] == Vcf::NOTE
+            next
+          end
+
           # A label whose line is gone names nothing, so it goes too.
           next if orphaned.include?(property.group)
 
@@ -132,20 +134,13 @@ module ProTacts
         noted_into(VCard.new(kept.join), notes)
       end
 
-      # What the importer asked for this property. Anything they were
-      # not asked about — every known property — is kept, which is also
-      # what an unknown one gets when the form sent no answer for it
-      # (Web#decisions_in).
-      #: (VCard::Parser::Property property) -> String
-      def choice_for(property)
-        @decisions.fetch(property.name.upcase, Vcf::KEEP)
-      end
-
-      # The group prefixes (`item1.`) whose every line but the label is
-      # leaving. Apple hangs a label off the line it names rather than
-      # inside it, so dropping `item3.X-ABRELATEDNAMES` without this
-      # would leave `item3.X-ABLabel:_$!<Spouse>!$_` behind, naming a
-      # line that is no longer there.
+      # The group prefixes (`item1.`) with nothing left in them but a
+      # label. Apple hangs a label off the line it names rather than
+      # inside it, so bringing in `item3.X-ABLabel:_$!<Spouse>!$_`
+      # 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.
       #: (Array[VCard::Parser::Line] lines) -> Array[String]
       def orphaned_groups(lines)
         anchored = {} #: Hash[String, bool]
@@ -156,7 +151,7 @@ module ProTacts
           group = property.group
           next if group.nil? || property.name.casecmp?(Vcf::LABEL)
 
-          anchored[group] = anchored.fetch(group, false) || choice_for(property) == Vcf::KEEP
+          anchored[group] = anchored.fetch(group, false) || Vcf.known?(property.name)
         end
         anchored.reject { |_group, kept| kept }.keys
       end
diff --git a/lib/pro_tacts/import/vcf.rb b/lib/pro_tacts/import/vcf.rb
index 75e49cc..f3a9331 100644
--- a/lib/pro_tacts/import/vcf.rb
+++ b/lib/pro_tacts/import/vcf.rb
@@ -7,27 +7,33 @@ module ProTacts
     # server does not recognise in them
     # (docs/plans/2026-09-21-import-a-vcf.md).
     #
-    # Nothing here rebuilds a card out of fields. A contact arrives as a
-    # vCard and is stored as one — RFC 6352 section 6.3.2.2 requires a
-    # server to keep what it does not understand, and this one does
-    # (vcard.rb) — so the reading this module owes is the split into
-    # cards and the question the importer is asked: of the properties
-    # no screen here shows, which are worth keeping?
+    # Nothing here rebuilds a card out of fields: a contact arrives as a
+    # vCard and is stored as one, and every line this address book reads
+    # travels through untouched. What an import does not do is carry in
+    # what nothing here will ever show. RFC 6352 section 6.3.2.2 binds
+    # this server to keep what a client submits and does not understand,
+    # and it does (vcard.rb); an import is the other direction, a person
+    # choosing what their own book is made of. So the reading this
+    # module owes is the split into cards and the question the importer
+    # is asked: of the properties no screen here shows, which are worth
+    # saving under the note on the way past?
     module Vcf
       # A file that is not a set of vCards.
       class Invalid < StandardError; end
 
-      # The properties this address book reads. The envelope (BEGIN,
-      # END, VERSION, UID), the fields a screen shows (Contact's own
-      # readers: N, FN, NICKNAME, TEL, EMAIL, ADR, NOTE, PHOTO, and the
-      # X-ABLabel that names a row), and BDAY, which the store takes
-      # into the model on the way in
+      # The properties this address book reads, and so the properties
+      # an import brings in. The envelope (BEGIN, END, VERSION, UID),
+      # the fields a screen shows (Contact's own readers: N, FN,
+      # NICKNAME, TEL, EMAIL, ADR, NOTE, PHOTO, and the X-ABLabel that
+      # names a row), and BDAY, which the store takes into the model on
+      # the way in
       # (docs/plans/2026-09-11-every-birthday-in-the-model.md).
       #
-      # Everything else is unknown, which here means "nothing here will
-      # show it", not "nothing here can hold it": a kept line is served
-      # back byte for byte like any other. That is why the list is the
-      # reader's and not the writer's.
+      # Everything else is unknown, which here means "no screen will
+      # show it". The list is the reader's, not the writer's: adding a
+      # screen for a property is what brings it in, and until then
+      # importing one would only pad the cards with what nobody can
+      # read.
       # Apple's own name for a row, carried on a line of its own beside
       # the one it names (`item1.X-ABLabel:_$!<Spouse>!$_`). Known, and
       # named apart from the rest because a decision about the line it
@@ -40,21 +46,20 @@ module ProTacts
         LABEL,
       ].freeze #: Array[String]
 
-      # What the importer can ask for an unknown property. Keeping is
-      # the default because it loses nothing: the line is stored as it
-      # arrived and served back untouched, invisible to these screens
-      # rather than gone. Dropping is for what the source keeps and
-      # this book has no use for. The note is for a value worth reading
-      # even with no field to read it in — a spouse's name is worth
-      # more under the note than nowhere.
-      KEEP = "keep" #: String
+      # What the importer can ask for an unknown property. Dropping is
+      # the default, and what happens to anything not spoken for: a
+      # line no screen here shows is a line this book has no use for,
+      # and carrying it in would leave every card padded with the
+      # source's bookkeeping. The note is the way out for a value worth
+      # reading even with no field to read it in — a spouse's name is
+      # worth more under the note than nowhere.
       DROP = "drop" #: String
       NOTE = "note" #: String
-      CHOICES = [KEEP, DROP, NOTE].freeze #: Array[String]
+      CHOICES = [DROP, NOTE].freeze #: Array[String]
 
       # One property the file carries that KNOWN does not name, across
       # every card in it: how many lines wear it, and a few of their
-      # real values, so a choice is made looking at this book's own
+      # real values, so a choice is made looking at the source's own
       # data rather than at a property name. Signed in
       # sig/pro_tacts/import.rbs, being a Data class.
       # @rbs skip
@@ -106,9 +111,10 @@ module ProTacts
       # `X-SOCIALPROFILE`. Ordered by name, a list to work down.
       #
       # An unreadable line is not among them and asks nothing: the
-      # parser hands one back without a property, the card is served
-      # from its bytes regardless, and there is no field it could be
-      # dropped out of (vcard/parser.rb's own posture).
+      # parser hands one back without a property, so there is no
+      # property name to decide about and nothing to say about it on
+      # a screen. It travels with the card rather than being thrown
+      # away unnamed (vcard/parser.rb's own posture).
       #: (Array[VCard] cards) -> Array[Unknown]
       def self.unknown(cards)
         seen = {} #: Hash[String, Array[String]]
@@ -118,7 +124,7 @@ module ProTacts
             next if property.nil?
 
             name = property.name.upcase
-            next if KNOWN.include?(name)
+            next if known?(name)
 
             (seen[name] ||= []) << example(line)
           end
@@ -128,6 +134,14 @@ module ProTacts
         }
       end
 
+      # Whether a property is one this address book reads, asked of a
+      # name as a line spells it — the one question both the survey
+      # and the landing ask (Land#decide).
+      #: (String name) -> bool
+      def self.known?(name)
+        KNOWN.include?(name.upcase)
+      end
+
       # A line unfolded, without its terminator, cut short of a
       # payload nobody wants to read.
       #: (VCard::Parser::Line line) -> String
diff --git a/lib/pro_tacts/web/import.rb b/lib/pro_tacts/web/import.rb
index d8f6869..166209e 100644
--- a/lib/pro_tacts/web/import.rb
+++ b/lib/pro_tacts/web/import.rb
@@ -11,7 +11,9 @@ module ProTacts
     # tasks that read Contacts.app on a Mac, built a plan directory,
     # and carried it to a host over HTTP — a .vcf is what every
     # address book on earth already exports, and the machine holding
-    # it is whichever one the browser is on.
+    # it is whichever one the browser is on. What comes in is what
+    # this book can show; the rest is dropped, or saved under the note
+    # where the review screen says so.
     #
     # Two requests, because the question this screen exists to ask
     # cannot be asked until the file has been read: the first stages
@@ -109,13 +111,13 @@ module ProTacts
     # What the form said to do with each unknown property, read against
     # the survey rather than trusted: the names are the ones this run
     # found, and anything else a post carries is not a property of this
-    # file. An answer missing or unrecognized is a keep, the choice
-    # that loses nothing.
+    # file. An answer missing or unrecognized is a drop, which is what
+    # a property nobody spoke for gets either way (Import::Land#decide).
     #: (untyped param, Array[Import::Vcf::Unknown] unknown) -> Hash[String, String]
     def decisions_in(param, unknown)
       unknown.to_h { |property|
         answer = param.is_a?(Hash) ? param[property.name].to_s : ""
-        choice = Import::Vcf::CHOICES.include?(answer) ? answer : Import::Vcf::KEEP
+        choice = Import::Vcf::CHOICES.include?(answer) ? answer : Import::Vcf::DROP
         # A two-element literal is an Array until something says
         # otherwise, and an inline annotation needs its own line.
         [property.name, choice] #: [String, String]
diff --git a/test/pro_tacts/admin/test_import_pages.rb b/test/pro_tacts/admin/test_import_pages.rb
index bcb339c..209db18 100644
--- a/test/pro_tacts/admin/test_import_pages.rb
+++ b/test/pro_tacts/admin/test_import_pages.rb
@@ -82,19 +82,22 @@ class AdminImportPagesTest < Minitest::Test
     assert_includes last_response.body, "X-ABRELATEDNAMES (1 line)"
     assert_includes last_response.body, "X-SOCIALPROFILE (1 line)"
     assert_includes last_response.body, "item1.X-ABRELATEDNAMES:Sam Booles"
-    %w[keep drop note].each do |choice|
+    %w[drop note].each do |choice|
       assert_includes last_response.body,
                       %(<input type="radio" name="decide[X-SOCIALPROFILE]" value="#{choice}")
     end
-    # Keeping loses nothing, so it is what an unread form sends.
+    # Nothing here will show it, so leaving it behind is what an
+    # unread form sends, and there is no third choice that keeps it.
     assert_includes last_response.body,
-                    %(name="decide[X-SOCIALPROFILE]" value="keep" checked>)
+                    %(name="decide[X-SOCIALPROFILE]" value="drop" checked>)
+    refute_includes last_response.body, %(value="keep")
   end
 
   def test_a_file_this_book_reads_whole_has_nothing_to_decide
     with_contacts({}) { upload(PLAIN) }
 
     assert_includes last_response.body, "Nothing to decide."
+    refute_includes last_response.body, "stays behind"
     refute_includes last_response.body, "decide["
   end
 
@@ -105,7 +108,7 @@ class AdminImportPagesTest < Minitest::Test
       post "/import/land",
            "upload" => staged,
            "group" => "import-20260921T031655Z",
-           "decide" => {"X-SOCIALPROFILE" => "drop", "X-ABRELATEDNAMES" => "note"}
+           "decide" => {"X-ABRELATEDNAMES" => "note"}
 
       assert_equal 200, last_response.status
       assert_includes last_response.body, "landed (1)"
@@ -120,15 +123,19 @@ class AdminImportPagesTest < Minitest::Test
     end
   end
 
-  # The default is the choice that loses nothing, so a form submitted
-  # without touching a radio keeps everything.
-  def test_confirming_with_no_answers_keeps_everything
+  # Leaving it behind is what nobody has to ask for, so a form
+  # submitted without touching a radio brings in only what shows.
+  def test_confirming_with_no_answers_leaves_the_unknown_behind
     with_contacts({}) do |store|
       upload(JANE)
 
       post "/import/land", "upload" => staged, "group" => ""
 
-      assert_includes store.contacts.fetch(0).vcard.to_s, "X-SOCIALPROFILE"
+      card = store.contacts.fetch(0).vcard.to_s
+
+      refute_includes card, "X-SOCIALPROFILE"
+      refute_includes card, "X-ABRELATEDNAMES"
+      assert_includes card, "FN:Jane Booles"
       assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
     end
   end
diff --git a/test/pro_tacts/import/test_land.rb b/test/pro_tacts/import/test_land.rb
index 0b2b99e..09e5adf 100644
--- a/test/pro_tacts/import/test_land.rb
+++ b/test/pro_tacts/import/test_land.rb
@@ -17,8 +17,10 @@ class ImportLandTest < Minitest::Test
     N:Booles;Jane;;;
     FN:Jane Booles
     NOTE:Met at work
-    item1.X-ABRELATEDNAMES:Sam Booles
-    item1.X-ABLabel:_$!<Spouse>!$_
+    item1.ADR;type=HOME:;;1 Main St;Springfield;;;
+    item1.X-ABLabel:_$!<Home>!$_
+    item2.X-ABRELATEDNAMES:Sam Booles
+    item2.X-ABLabel:_$!<Spouse>!$_
     X-SOCIALPROFILE;type=twitter:https://twitter.com/jb
     UID:ABC-123
     END:VCARD
@@ -31,54 +33,55 @@ class ImportLandTest < Minitest::Test
     end
   end
 
-  # Nothing rebuilds a card out of fields: the bytes that arrived are
-  # the bytes that are stored, minus the id this server gives it.
-  def test_a_kept_property_travels_with_the_card
-    land({"X-SOCIALPROFILE" => Vcf::KEEP}) do |landed, _store|
+  # Nothing rebuilds a card out of fields: every line this book reads
+  # is the bytes that arrived, minus the id this server gives it.
+  def test_what_this_book_reads_travels_with_the_card
+    land({}) do |landed, _store|
       card = landed.fetch(0).vcard.to_s
 
-      assert_includes card, "X-SOCIALPROFILE;type=twitter:https://twitter.com/jb\r\n"
-      assert_includes card, "item1.X-ABRELATEDNAMES:Sam Booles\r\n"
+      assert_includes card, "N:Booles;Jane;;;\r\n"
+      assert_includes card, "FN:Jane Booles\r\n"
+      assert_includes card, "NOTE:Met at work\r\n"
+      assert_includes card, "item1.ADR;type=HOME:;;1 Main St;Springfield;;;\r\n"
     end
   end
 
-  # A property nobody was asked about is kept, which is every known
-  # one and any unknown one the form said nothing about.
-  def test_a_property_nobody_decided_is_kept
+  # A grouped property this book reads keeps the label that names it:
+  # the row is coming in, so the name for it is too.
+  def test_a_label_on_a_line_that_comes_in_comes_with_it
     land({}) do |landed, _store|
-      assert_includes landed.fetch(0).vcard.to_s, "X-SOCIALPROFILE"
+      assert_includes landed.fetch(0).vcard.to_s, "item1.X-ABLabel:_$!<Home>!$_\r\n"
     end
   end
 
-  def test_a_dropped_property_is_gone
-    land({"X-SOCIALPROFILE" => Vcf::DROP}) do |landed, _store|
+  # Nobody has to say so: a property no screen here shows is not
+  # carried in, and the label that named it goes with it.
+  def test_a_property_no_screen_shows_does_not_come_in
+    land({}) do |landed, _store|
       card = landed.fetch(0).vcard.to_s
 
       refute_includes card, "X-SOCIALPROFILE"
-      assert_includes card, "FN:Jane Booles\r\n"
+      refute_includes card, "X-ABRELATEDNAMES"
+      refute_includes card, "_$!<Spouse>!$_"
     end
   end
 
-  # Apple hangs a row's label off the line it names rather than inside
-  # it, so a label whose line is dropped would be left naming nothing.
-  def test_a_dropped_line_takes_its_label_with_it
-    land({"X-ABRELATEDNAMES" => Vcf::DROP}) do |landed, _store|
-      card = landed.fetch(0).vcard.to_s
-
-      refute_includes card, "X-ABRELATEDNAMES"
-      refute_includes card, "X-ABLabel"
+  # Saying so explicitly is the same thing.
+  def test_dropping_is_what_a_drop_asks_for
+    land({"X-SOCIALPROFILE" => Vcf::DROP}) do |landed, _store|
+      refute_includes landed.fetch(0).vcard.to_s, "X-SOCIALPROFILE"
     end
   end
 
-  # The note is where a value goes when it is worth reading and there
-  # is no field to read it in. It is called what the row was called,
-  # not what the property was named.
+  # The note is the way out for a value worth reading even with no
+  # field to read it in. It is called what the row was called, not
+  # what the property was named.
   def test_a_noted_property_is_written_under_the_note_by_its_label
     land({"X-ABRELATEDNAMES" => Vcf::NOTE}) do |landed, _store|
       card = landed.fetch(0).vcard.to_s
 
       refute_includes card, "X-ABRELATEDNAMES"
-      refute_includes card, "X-ABLabel"
+      refute_includes card, "_$!<Spouse>!$_"
       # One NOTE, the card's own and the addition, joined by the
       # escape a line break inside a text value is written as.
       assert_includes card, "NOTE:Met at work\\nSpouse: Sam Booles\r\n"