import: say what the confirm does instead of "landing"
Before: the import's last step was a landing. `Import::Land` wrote the
cards, the staged file was `landing.vcf`, the route was
`/import/:id/land`, and the page that answered said "landed (2)". The
metaphor never said what it did: a card that lands is a card that has
arrived somewhere, and the somewhere is what the reader wanted named.

After: `Import::Write` writes the cards through the store, the staged
file is `revised.vcf` — the cards as the walk has revised them, beside
`original.vcf` — the route is `/import/:id/confirm`, which is what the
person pressed, and the page says "imported (2)".

The prose around all four followed the name, so the comments, the
screens' own words and the plan doc say the same thing in plain
English: a card comes in, is written, is imported. Nothing about how
the import behaves changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KrREeoPLXx1qsYYq7qkL8h
change
commit ca21083c7beb6423116096c4b09a5b86091d138b
author Claude <noreply@anthropic.com>
date
parent df503c47
diff --git a/AGENTS.md b/AGENTS.md
index f3be664..22d542d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -39,10 +39,10 @@ profiles, so leave those to the user.
 
 Importing is not a task at all: a .vcf is uploaded at `/import` in the
 app, and reviewed there contact by contact — the card as the file wrote
-it beside the card that is landing, with the lines no screen here shows
-struck, and this book's groups as boxes to tick beside a box for a new
-one — before the lot is landed through the store, into a group made for
-the import and whichever groups each contact was given
+it beside the card that is coming in, with the lines no screen here
+shows struck, and this book's groups as boxes to tick beside a box for a
+new one — before the lot is written through the store, into a group made
+for the import and whichever groups each contact was given
 (docs/plans/2026-09-21-import-a-vcf.md). The `import:*` tasks, the Swift
 reader they drove Contacts.app with, and the plan directories they wrote
 are gone.
@@ -67,7 +67,7 @@ lib/pro_tacts/
 ├── web/setup.rb    # The device setup screen and its profile
 ├── web/import.rb   # The import screen: a .vcf, uploaded and decided
 ├── web/api.rb      # JSON for scripts: the group list
-├── import/         # Reading an uploaded .vcf, and landing its cards
+├── import/         # Reading an uploaded .vcf, and writing its cards
 ├── admin/          # The Phlex views those screens render, plus
 │                   # card_form.rb, which reads a form back into a card
 ├── store.rb        # Sequel over SQLite: cards, change log, derived index
diff --git a/docs/plans/2026-09-21-import-a-vcf.md b/docs/plans/2026-09-21-import-a-vcf.md
index 8d54c5c..c5bfc22 100644
--- a/docs/plans/2026-09-21-import-a-vcf.md
+++ b/docs/plans/2026-09-21-import-a-vcf.md
@@ -2,7 +2,7 @@
 
 2026-09-21. Importing is one upload of one file. `/import` takes a
 `.vcf`, says what is in it, asks what to do with the properties no
-screen here shows, and lands the cards through the store. Everything
+screen here shows, and writes the cards through the store. Everything
 that came before it — `import:macos:plan`, `import:macos:finalize`,
 `import:status`, the Swift reader they drove Contacts.app with, and the
 plan directories under `data/import` — is gone.
@@ -36,7 +36,7 @@ Mac any more. Finalize's check — ask the host, contact by contact,
 whether it has the card, then delete what it has — was the safe half of
 a two-sided sync, and there is no second side now. Deleting what you
 exported is a thing to do in Contacts.app, once you have looked at what
-landed.
+came in.
 
 ## What comes in
 
@@ -102,7 +102,7 @@ So the review is a walk, four screens rather than a submission:
    properties the whole file is losing. A book is hundreds of contacts
    and only some of them will have anything worth looking at, so the
    list is what says which rows are worth opening — and a file whose
-   every loss is noise says so at the top and can be landed unread.
+   every loss is noise says so at the top and can be confirmed unread.
 3. **One contact, two cards.** On the left, the card exactly as the
    file wrote it: content lines in monospace, with every line that is
    not coming in struck in Gloss's danger color and labelled "not
@@ -113,8 +113,8 @@ So the review is a walk, four screens rather than a submission:
    spouse's name off the left card and type it
    into the note on the right, in your own words, and put the contact
    where it belongs while you are looking at it.
-4. **Confirm**, under a group for the lot, and the cards land as the
-   walk left them.
+4. **Confirm**, under a group for the lot, and the cards are written as
+   the walk left them.
 
 The right-hand card is `Admin::ContactsEdit` itself, not a copy of it.
 A second editor for imports would be a place where the two could
@@ -132,7 +132,7 @@ that cannot make the trip is one no card can spell — a year on its own,
 a month without its day (`2026-08-31-partial-birthdays.md`). A stored
 contact holds that in the model and serves a card without it; a staged
 contact is only its card, so the save says so and asks for the date
-again once the contact has landed, rather than dropping it the moment
+again once the contact is stored, rather than dropping it the moment
 it was typed.
 
 ## The import waits on the server
@@ -140,33 +140,33 @@ it was typed.
 An import is a directory under `data/imports`, keyed by a minted id
 that every screen of the walk carries in its path. Three slots in it.
 `original.vcf` is the uploaded file, written once and never again, and
-`landing.vcf` the cards as they will land — pared when the import opens
-and rewritten whole each time the editor saves one of them. A card that
+`revised.vcf` the cards as they will come in — pared when the import
+opens and rewritten whole each time the editor saves one of them. A card that
 has been edited still has to show what it arrived as, which is why
 both. `groups.json` is which groups each of those cards is joining,
 keyed by the card's place in the file: a vCard says nothing about this
-book's groups, and a line invented to hold the answer would land in the
-contact. Two maps in it, because a group this book has and a group this
+book's groups, and a line invented to hold the answer would end up in
+the contact. Two maps in it, because a group this book has and a group this
 import is about to invent are not the same answer — ids for the first,
 which is the only way to name a group that has no name of its own, and
-names for the second, made at the landing.
+names for the second, made at the confirm.
 
 On disk rather than in the browser: a book with pictures in it is tens
 of megabytes, and a form carrying it back and forth is the upload done
 once per screen. An id off a link is checked against the shape
 `ChangeId.mint` draws before it is made into a path — `../../contacts.db`
-is a filename too. The directory goes the moment the cards land, and
+is a filename too. The directory goes the moment the cards are written, and
 anything older than a day is swept on the next upload: long enough to
 work down a book over an evening, short enough that a closed window
 does not leave that book on disk for a week.
 
 A contact is named by its place in the file, there being no minted id
-until it lands. That holds because the editor's save rewrites a card in
+until it is written. That holds because the editor's save rewrites a card in
 place and never adds or removes one.
 
-## Landing
+## Writing them in
 
-`Import::Land` writes through `Store#put` and `Store#regroup` rather
+`Import::Write` writes through `Store#put` and `Store#regroup` rather
 than through the routes those two sit behind. It is not a second way to
 write a card — it is the same two writes a client's PUT and the groups
 dialog make:
@@ -181,7 +181,7 @@ dialog make:
 3. `Store#regroup` into whichever groups the walk settled on. Two
    questions, asked in the two places they belong. The review screen
    names one group for the lot, `import-<timestamp>` by default — what
-   landed together can be found together, and undone together — and it
+   arrived together can be found together, and undone together — and it
    is emptiable, a name this server already has joining that group
    rather than colliding with it. Beside each contact's own card, every
    group this book already has as a box to tick: an import is as often
@@ -206,5 +206,5 @@ dialog make:
 Importing is not idempotent and cannot be. A `.vcf` carries no id this
 server minted, so a second upload of the same file is a second set of
 contacts. The review screen is what stands in for the `If-None-Match:
-*` the old PUT sent: it says how many contacts are about to land, and
+*` the old PUT sent: it says how many contacts are about to come in, and
 under what group name, before any of them do.
diff --git a/lib/pro_tacts/admin/import_groups.rb b/lib/pro_tacts/admin/import_groups.rb
index 7e2a610..c21baa6 100644
--- a/lib/pro_tacts/admin/import_groups.rb
+++ b/lib/pro_tacts/admin/import_groups.rb
@@ -19,7 +19,7 @@ module ProTacts
     # A box per group this book has, then a box per group this import
     # has been told to make and has not made yet, then somewhere to
     # name another. A named group is not created here: it is made at
-    # the landing (Import::Land#group_id), because a group created
+    # the confirm (Import::Write#group_id), because a group created
     # while the walk is still going is a group left behind by an
     # import that was abandoned. So it rides as a name until then, and
     # shows as a ticked box like any other so that it can be unticked.
@@ -64,7 +64,7 @@ module ProTacts
           # and taken off again.
           label(class: "field") do
             span { "New group" }
-            input(type: "text", name: "new", placeholder: "made when the import lands")
+            input(type: "text", name: "new", placeholder: "made when the import is confirmed")
           end
         end
       end
diff --git a/lib/pro_tacts/admin/import_original.rb b/lib/pro_tacts/admin/import_original.rb
index 470e259..93ff325 100644
--- a/lib/pro_tacts/admin/import_original.rb
+++ b/lib/pro_tacts/admin/import_original.rb
@@ -9,7 +9,7 @@ module ProTacts
     #
     # Content lines rather than a rendered contact, and in the
     # monospace the bytes are actually in. The card beside this one is
-    # the rendered reading — it is the editor, over what is landing —
+    # the rendered reading — it is the editor, over what is coming in —
     # so a second rendering here would show the same thing twice and
     # answer neither question this card is for: what did the source
     # actually say, and what of it is not coming with it.
diff --git a/lib/pro_tacts/admin/import_review.rb b/lib/pro_tacts/admin/import_review.rb
index 23da0ba..26cd583 100644
--- a/lib/pro_tacts/admin/import_review.rb
+++ b/lib/pro_tacts/admin/import_review.rb
@@ -6,8 +6,8 @@ require "pro_tacts/admin/list_item"
 
 module ProTacts
   module Admin
-    # The contacts an uploaded .vcf holds, before any of them land
-    # (docs/plans/2026-09-21-import-a-vcf.md).
+    # The contacts an uploaded .vcf holds, before any of them are in
+    # the address book (docs/plans/2026-09-21-import-a-vcf.md).
     #
     # A list rather than every card laid open at once: a book is
     # hundreds of contacts and only some of them will have anything
@@ -30,9 +30,9 @@ module ProTacts
       # @rbs @group: String
       # @rbs @notice: String?
 
-      # `rows` is the contact each card will land as, how many of its
-      # original's lines are not coming with it, and the groups the
-      # walk has put it in so far. `upload` is the staged import every
+      # `rows` is the contact each card will be written as, how many
+      # of its original's lines are not coming with it, and the groups
+      # the walk has put it in so far. `upload` is the staged import every
       # link and the confirm carry, and `group` the name of the group
       # this import offers to make for the lot of them.
       #: (upload: String, rows: Array[[::ProTacts::Contact, Integer, Array[String]]], unknown: Array[::ProTacts::Import::Vcf::Unknown], group: String, ?notice: String?) -> void
@@ -64,13 +64,13 @@ module ProTacts
           div(class: "card-body") do
             h1(class: "type-h2", style: "margin: 0;") { count(@rows.length, "contact") }
             losses
-            form(action: "/import/#{@upload}/land", method: "post", class: "field-stack") do
+            form(action: "/import/#{@upload}/confirm", method: "post", class: "field-stack") do
               # A group made for this import and taking in every
               # contact, named for the moment by default so one import
               # can be found — or undone — apart from the next.
               # Editable, and emptiable; a name this book already uses
               # joins that group rather than making a second one by the
-              # same name (Import::Land#group_id). Which of this book's
+              # same name (Import::Write#group_id). Which of this book's
               # own groups a contact joins is that contact's question,
               # asked beside its card (Admin::ImportGroups).
               label(class: "field") do
@@ -94,7 +94,7 @@ module ProTacts
           # noise is not read either (Vcf::NOISE), it is only not
           # worth a sentence. What is true in both cases is that
           # there is nothing in this file to stop over.
-          p(class: "type-body-sm gl-muted") { "Nothing in this file needs reading before it lands." }
+          p(class: "type-body-sm gl-muted") { "Nothing in this file needs reading before it comes in." }
           return
         end
 
diff --git a/lib/pro_tacts/admin/import_upload.rb b/lib/pro_tacts/admin/import_upload.rb
index c4ae1cf..c588e24 100644
--- a/lib/pro_tacts/admin/import_upload.rb
+++ b/lib/pro_tacts/admin/import_upload.rb
@@ -8,7 +8,7 @@ require "pro_tacts/admin/list_item"
 module ProTacts
   module Admin
     # GET /import — the screen an address book arrives on, and the page
-    # a landing answers with (docs/plans/2026-09-21-import-a-vcf.md).
+    # a confirm answers with (docs/plans/2026-09-21-import-a-vcf.md).
     # Until this existed an import meant rake tasks reading Contacts.app
     # on a Mac and carrying a plan to a host over HTTP; now it is the
     # file that Mac already knows how to export.
@@ -17,15 +17,15 @@ module ProTacts
     # in it needs the file open, so that is the next screen's
     # (ImportReview, and the pair of cards a row there opens).
     #
-    # `landed` is the import that just happened, rendered below the
+    # `imported` is the import that just happened, rendered below the
     # form that is still there to run another.
     class ImportUpload < Phlex::HTML
-      # @rbs @landed: Array[Contact]?
+      # @rbs @imported: Array[Contact]?
       # @rbs @notice: String?
 
-      #: (?landed: Array[Contact]?, ?notice: String?) -> void
-      def initialize(landed: nil, notice: nil)
-        @landed = landed
+      #: (?imported: Array[Contact]?, ?notice: String?) -> void
+      def initialize(imported: nil, notice: nil)
+        @imported = imported
         @notice = notice
       end
 
@@ -36,7 +36,7 @@ module ProTacts
               a(href: "/", class: "type-label") { "‹ contacts" }
             end
             upload_card
-            landed_list if @landed
+            imported_list if @imported
           end
         end
       end
@@ -50,7 +50,7 @@ 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, including what " \
+                "The next screen says what is in it before anything is added, including what " \
                 "pro-tacts cannot show and will leave behind."
             end
             form(action: "/import", method: "post", enctype: "multipart/form-data", class: "field-stack") do
@@ -64,19 +64,19 @@ module ProTacts
         end
       end
 
-      # What landed, contact by contact, each linked to the record it
+      # What came in, contact by contact, each linked to the record it
       # became — so the next thing to do, reading one or fixing one, is
       # a click away.
       #: () -> void
-      def landed_list
-        landed = @landed or return
+      def imported_list
+        imported = @imported or return
 
         section do
           div(class: "section-head") do
-            h2(class: "type-label") { "landed (#{landed.length})" }
+            h2(class: "type-label") { "imported (#{imported.length})" }
           end
           ul(class: "card") do
-            landed.each do |contact|
+            imported.each do |contact|
               render ListItem.new(
                 href: "/contacts/#{contact.id}",
                 avatar: Avatar.new(contact:, size: "lg"),
diff --git a/lib/pro_tacts/import/staged.rb b/lib/pro_tacts/import/staged.rb
index 80565b9..260df88 100644
--- a/lib/pro_tacts/import/staged.rb
+++ b/lib/pro_tacts/import/staged.rb
@@ -15,12 +15,13 @@ module ProTacts
     # Three slots per import. ORIGINAL is the uploaded file, written
     # once and never again: it is what the left-hand card renders, and
     # a card that has been edited still has to show what it arrived
-    # as. LANDING is the cards as they will land — pared to what this
-    # book reads when the import opens (Vcf.read), and rewritten whole
-    # each time the editor saves one of them. GROUPS is which groups
-    # each of those cards is joining, a decision the cards themselves
-    # cannot carry: a vCard says nothing about this book's groups, and
-    # a line invented to hold the answer would land in the contact.
+    # as. REVISED is the cards as they will be written — pared to what
+    # this book reads when the import opens (Vcf.read), and rewritten
+    # whole each time the editor saves one of them. GROUPS is which
+    # groups each of those cards is joining, a decision the cards
+    # themselves cannot carry: a vCard says nothing about this book's
+    # groups, and a line invented to hold the answer would end up in
+    # the contact.
     #
     # On disk rather than in the browser: a book with pictures in it is
     # tens of megabytes, and a form carrying it back and forth is the
@@ -35,9 +36,9 @@ module ProTacts
       DIRECTORY = "imports" #: String
 
       ORIGINAL = "original" #: String
-      LANDING = "landing" #: String
+      REVISED = "revised" #: String
       GROUPS = "groups" #: String
-      SLOTS = [ORIGINAL, LANDING, GROUPS].freeze #: Array[String]
+      SLOTS = [ORIGINAL, REVISED, GROUPS].freeze #: Array[String]
 
       # The two halves of the groups slot: the ids of groups this
       # book has, and the names of groups it does not have yet.
@@ -54,14 +55,14 @@ module ProTacts
       # it carries. Minted, so the id a link or a form hands back is
       # checked against the one shape this writes (#path) rather than
       # trusted as a filename.
-      #: (original: String, landing: String) -> String
-      def self.open(original:, landing:)
+      #: (original: String, revised: String) -> String
+      def self.open(original:, revised:)
         sweep
         id = ChangeId.mint(ChangeId::CONTACT_LENGTH)
         directory = root / id
         FileUtils.mkdir_p(directory)
         File.binwrite(directory / file_name(ORIGINAL), original)
-        File.binwrite(directory / file_name(LANDING), landing)
+        File.binwrite(directory / file_name(REVISED), revised)
         # Empty rather than absent, so every slot of an import that
         # exists is a file that exists and a write can say which of
         # the two it found (#write).
@@ -71,7 +72,7 @@ module ProTacts
       end
 
       # One slot's bytes, or none — an import swept out from under a
-      # screen left open overnight, or one already landed, its second
+      # screen left open overnight, or one already written, its second
       # confirm finding what the first removed. Ordinary enough for
       # the screen to say so and ask for the file again.
       #: (String id, String slot) -> String?
@@ -86,20 +87,20 @@ module ProTacts
         File.read(file.to_s, encoding: Encoding::UTF_8)
       end
 
-      # The cards as they will land, rewritten: the editor's save, and
-      # the only write this takes after the import opens. Whole rather
-      # than a card at a time, because the file is the unit a re-read
-      # splits (Vcf.cards) and a card's index in it is how a screen
-      # names one.
-      #: (String id, String landing) -> void
-      def self.update(id, landing)
-        write(id, LANDING, landing)
+      # The cards as they will be written, rewritten: the editor's
+      # save, and the only write this takes after the import opens.
+      # Whole rather than a card at a time, because the file is the
+      # unit a re-read splits (Vcf.cards) and a card's index in it is
+      # how a screen names one.
+      #: (String id, String revised) -> void
+      def self.update(id, revised)
+        write(id, REVISED, revised)
       end
 
       # Which groups each contact joins, by its place in the file —
       # the name every screen of the walk calls a contact by, there
-      # being no minted id until it lands. Rewritten whole like the
-      # cards beside it, and for the same reason: one save is one
+      # being no minted id until it is written. Rewritten whole like
+      # the cards beside it, and for the same reason: one save is one
       # state of the whole import.
       #
       # Two maps rather than one, because a group this book has and a
@@ -107,9 +108,9 @@ module ProTacts
       # `chosen` holds ids, the only way to name a group that has no
       # name of its own (db/migrations/005_group_identity.rb).
       # `named` holds names of groups that do not exist yet: made at
-      # the landing and not before, because a group created while the
+      # the confirm and not before, because a group created while the
       # walk is still going is a group left behind by an import that
-      # was abandoned (Import::Land#group_id).
+      # was abandoned (Import::Write#group_id).
       #: (String id, Hash[String, Array[String]] chosen, Hash[String, Array[String]] named) -> void
       def self.update_groups(id, chosen, named)
         write(id, GROUPS, JSON.generate({CHOSEN => chosen, NAMED => named}))
@@ -134,7 +135,7 @@ module ProTacts
         [lists(parsed[CHOSEN]), lists(parsed[NAMED])]
       end
 
-      # The import, gone: the last step of a landing, and what keeps
+      # The import, gone: the last step of a confirm, and what keeps
       # the ordinary case from waiting on the sweep.
       #: (String id) -> void
       def self.close(id)
diff --git a/lib/pro_tacts/import/vcf.rb b/lib/pro_tacts/import/vcf.rb
index be34d79..fa6eccd 100644
--- a/lib/pro_tacts/import/vcf.rb
+++ b/lib/pro_tacts/import/vcf.rb
@@ -19,7 +19,7 @@ module ProTacts
     # per card the split #read makes: what is coming in, and what is
     # being left behind. The review screen shows both at once — the
     # card as it was exported with the doomed lines struck, beside the
-    # card that is landing, open in the editor — so nothing is lost
+    # card that is coming in, open in the editor — so nothing is lost
     # without having been shown first (Admin::ImportOriginal).
     module Vcf
       # A file that is not a set of vCards.
@@ -65,7 +65,7 @@ module ProTacts
       # What an import makes of one card: the card as it will come in,
       # and the lines it leaves behind. Both, because the review
       # screen shows them side by side — the original with its doomed
-      # lines struck, and the card that is actually landing, open in
+      # lines struck, and the card that is actually coming in, open in
       # the editor (Admin::ImportOriginal). Signed in
       # sig/pro_tacts/import.rbs, being a Data class.
       # @rbs skip
@@ -87,8 +87,8 @@ module ProTacts
       # The cards a file holds, in the order it wrote them, each as the
       # bytes it arrived in. A card is what lies from BEGIN to END, and
       # the lines between cards are blank or the file is not a set of
-      # vCards — refused whole rather than partly imported, `execute`'s
-      # own rule that a source which will not read lands nothing.
+      # vCards — refused whole rather than partly imported: a source
+      # that will not read brings in nothing.
       #: (String bytes) -> Array[VCard]
       def self.cards(bytes)
         cards = [] #: Array[VCard]
@@ -150,7 +150,7 @@ module ProTacts
 
       # Whether a property is one this address book reads, asked of a
       # name as a line spells it — the one question the survey, the
-      # review screen and the landing all ask.
+      # review screen and the confirm all ask.
       #: (String name) -> bool
       def self.known?(name)
         KNOWN.include?(name.upcase)
diff --git a/lib/pro_tacts/import/land.rb b/lib/pro_tacts/import/write.rb
similarity index 87%
rename from lib/pro_tacts/import/land.rb
rename to lib/pro_tacts/import/write.rb
index ae5fa62..0d8aa9d 100644
--- a/lib/pro_tacts/import/land.rb
+++ b/lib/pro_tacts/import/write.rb
@@ -3,8 +3,8 @@ require "pro_tacts/vcard"
 
 module ProTacts
   module Import
-    # Lands the cards an import has settled on in this server's store
-    # (docs/plans/2026-09-21-import-a-vcf.md).
+    # Writes the cards an import has settled on into this server's
+    # store (docs/plans/2026-09-21-import-a-vcf.md).
     #
     # What arrives here is already what is coming in: the review screen
     # pared each card to the properties this book reads and let the
@@ -17,8 +17,8 @@ module ProTacts
     # Not idempotent, and cannot be: a .vcf carries no id this server
     # minted, so a second upload of the same file is a second set of
     # cards. The review screen is what stands in for that — it says how
-    # many contacts are about to land before any of them do.
-    class Land
+    # many contacts are about to be written before any of them are.
+    class Write
       # @rbs @store: Store
       # @rbs @group: String?
       # @rbs @joins: Array[Array[String]]
@@ -26,7 +26,7 @@ module ProTacts
       # @rbs @group_ids: Hash[String, String]
 
       # The group an import offers to put its arrivals in, named for
-      # when it happened: what landed together can be found together,
+      # when it happened: what came in together can be found together,
       # and undone together, without anything having to be recorded
       # about the file it came from.
       #: (?Time now) -> String
@@ -43,7 +43,8 @@ module ProTacts
       # for the groups the walk asked for that do not exist yet —
       # made here and not while the walk was going, an abandoned
       # import being the case that argues for waiting. Any of them, or
-      # none: a card in no group is still in everyone's book (#land).
+      # none: a card in no group is still in everyone's book
+      # (#contact).
       #: (Store store, Array[VCard] cards, ?group: String?, ?joins: Array[Array[String]], ?named: Array[Array[String]]) -> Array[Contact]
       def self.call(store, cards, group: nil, joins: [], named: [])
         new(store, group:, joins:, named:).call(cards)
@@ -61,17 +62,17 @@ module ProTacts
       #: (Array[VCard] cards) -> Array[Contact]
       def call(cards)
         cards.each_with_index.map { |card, index|
-          land(card, @joins.fetch(index, []), @named.fetch(index, []))
+          contact(card, @joins.fetch(index, []), @named.fetch(index, []))
         }
       end
 
       private
 
-      # A card, then its groups, the order a create writes them in:
-      # the put makes the contact, and the memberships that follow put
-      # it somewhere to be found.
+      # One card, written as a contact: then its groups, the order a
+      # create writes them in — the put makes the contact, and the
+      # memberships that follow put it somewhere to be found.
       #: (VCard card, Array[String] chosen, Array[String] named) -> Contact
-      def land(card, chosen, named)
+      def contact(card, chosen, named)
         id = ChangeId.mint(ChangeId::CONTACT_LENGTH)
         # `client: true` for what a client's PUT means here: a card this
         # creates joins `sync:*`, or it would be on the server and in
@@ -94,7 +95,7 @@ module ProTacts
         # above moved what the card serves. Nothing can have taken it
         # away in between, so a miss here is a broken assumption rather
         # than a case to handle.
-        @store.contact(id) || raise("#{id} was landed and is not stored")
+        @store.contact(id) || raise("#{id} was written and is not stored")
       end
 
       # The card under the id it is stored as. The source's UID goes
diff --git a/lib/pro_tacts/web/import.rb b/lib/pro_tacts/web/import.rb
index f92632f..52d8dc5 100644
--- a/lib/pro_tacts/web/import.rb
+++ b/lib/pro_tacts/web/import.rb
@@ -5,14 +5,15 @@ require "pro_tacts/admin/import_original"
 require "pro_tacts/admin/import_review"
 require "pro_tacts/admin/import_upload"
 require "pro_tacts/birthday"
-require "pro_tacts/import/land"
+require "pro_tacts/import/write"
 require "pro_tacts/import/staged"
 require "pro_tacts/import/vcf"
 
 module ProTacts
   class Web < Roda
     # The import screens: a .vcf, read, looked over contact by
-    # contact, and landed (docs/plans/2026-09-21-import-a-vcf.md).
+    # contact, and written into the store
+    # (docs/plans/2026-09-21-import-a-vcf.md).
     # They replace the rake 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
@@ -43,8 +44,8 @@ module ProTacts
         # Before the index below, and distinguishable from one: a
         # path segment is what says which request this is, not which
         # fields a form happened to carry.
-        r.post "land" do
-          land_upload(r, upload)
+        r.post "confirm" do
+          confirm_upload(r, upload)
         end
 
         r.is do
@@ -54,7 +55,7 @@ module ProTacts
         end
 
         # The contact's place in the file is its name here. There is
-        # no minted id until it lands, and the file's own order is
+        # no minted id until it is written, and the file's own order is
         # the one thing about a card that cannot change under the
         # walk: the editor's save rewrites a card in place and never
         # adds or removes one.
@@ -76,7 +77,7 @@ module ProTacts
 
     # The upload's own POST: the file read and judged whole before
     # anything is staged, the old `execute`'s rule that a source which
-    # will not read lands nothing. What it stages is both readings —
+    # will not read brings in nothing. What it stages is both readings —
     # the file as it arrived, and the cards pared to what this book
     # shows — and then it is the review screen's walk.
     #: (untyped r) -> untyped
@@ -100,7 +101,7 @@ module ProTacts
 
       id = Import::Staged.open(
         original: bytes,
-        landing: joined(cards.map { Import::Vcf.read(it).card }),
+        revised: joined(cards.map { Import::Vcf.read(it).card }),
       )
       # A 303, the other writes' answer, because what follows is a
       # walk: every screen of it is a GET a back button can revisit.
@@ -113,7 +114,7 @@ module ProTacts
       staged = staged_cards(upload)
       return expired_screen if staged.nil?
 
-      originals, landing = staged
+      originals, revised = staged
       chosen, named = Import::Staged.groups(upload)
       # By name, because the row is read rather than submitted: a
       # group the walk ticked and someone then deleted is simply not
@@ -123,7 +124,7 @@ module ProTacts
         # otherwise, and an inline annotation needs its own line.
         [group.id, group.label] #: [String, String]
       }
-      rows = landing.each_with_index.map { |card, index|
+      rows = revised.each_with_index.map { |card, index|
         # A literal of several elements is an Array until something
         # says otherwise, and an inline annotation needs its own line.
         dropped = Import::Vcf.read(originals.fetch(index)).dropped
@@ -136,13 +137,13 @@ module ProTacts
         upload:,
         rows:,
         unknown: Import::Vcf.unknown(originals),
-        group: Import::Land.default_group,
+        group: Import::Write.default_group,
         notice:,
       )
     end
 
     # One contact, the card it arrived as beside the card that is
-    # landing. The right-hand half is the contact editor itself, not a
+    # coming in. The right-hand half is the contact editor itself, not a
     # copy of it: a field the two disagreed about would be a field an
     # import writes and an edit cannot undo.
     #: (String upload, Integer index, ?notice: String?) -> String?
@@ -150,9 +151,9 @@ module ProTacts
       staged = staged_cards(upload)
       return expired_screen if staged.nil?
 
-      originals, landing = staged
+      originals, revised = staged
       original = originals[index]
-      card = landing[index]
+      card = revised[index]
       # An index past the end of the file is the empty-body 404 the
       # not_found handler fills in, the same as a contact id nobody
       # has.
@@ -177,8 +178,8 @@ module ProTacts
       staged = staged_cards(upload)
       return expired_screen if staged.nil?
 
-      _originals, landing = staged
-      card = landing[index]
+      _originals, revised = staged
+      card = revised[index]
       return nil if card.nil?
 
       contact = import_contact(card, index)
@@ -214,7 +215,7 @@ module ProTacts
 
       edited = Admin::CardForm.contact_card(contact, first, middle, last, r.params)
       # The birthday goes back into the card, an import having no
-      # model to hold one until it lands. Skipped for a card carrying
+      # model to hold one until it is written. Skipped for a card carrying
       # a BDAY spelling the model does not read: that line stayed in
       # the card (#import_contact, Store#put's own rule), no row
       # rendered for it, and a replace here would delete it.
@@ -228,14 +229,14 @@ module ProTacts
         # what it is, and writing it without the date would lose the
         # date the moment it was typed. Say so instead.
         if birthday && line.nil?
-          return card_screen(upload, index, notice: "A card cannot hold a birthday that partial until it lands. Land the contact, then add it on its own page.")
+          return card_screen(upload, index, notice: "A card cannot hold a birthday that partial. Import the contact, then add the date on its own page.")
         end
 
         edited = edited.replace("BDAY", line ? [line] : [])
       end
 
-      landing[index] = edited
-      Import::Staged.update(upload, joined(landing))
+      revised[index] = edited
+      Import::Staged.update(upload, joined(revised))
       # The groups ride in the same form and save with it
       # (Admin::ImportGroups): a card the walk has looked at has been
       # decided about in both ways at once. Only ids naming a group,
@@ -255,13 +256,14 @@ module ProTacts
       r.redirect "/import/#{upload}", 303
     end
 
-    # The confirm: the cards as the walk left them, landed.
+    # The confirm: the cards as the walk left them, written into the
+    # store.
     #: (untyped r, String upload) -> String
-    def land_upload(r, upload)
+    def confirm_upload(r, upload)
       staged = staged_cards(upload)
       return expired_screen if staged.nil?
 
-      _originals, landing = staged
+      _originals, revised = staged
       group = r.params["group"].to_s.strip
       # Filtered again here, and not only where the walk wrote them
       # (#save_card): a group can be deleted between the tick and the
@@ -269,12 +271,12 @@ module ProTacts
       # stale id would be a 500 rather than the nothing it means.
       known = store.all_groups.map(&:id)
       chosen, named = Import::Staged.groups(upload)
-      joins = landing.each_index.map { chosen.fetch(it.to_s, []) & known }
-      makes = landing.each_index.map { named.fetch(it.to_s, []) }
-      landed = Import::Land.call(store, landing, group: group.empty? ? nil : group, joins:, named: makes)
+      joins = revised.each_index.map { chosen.fetch(it.to_s, []) & known }
+      makes = revised.each_index.map { named.fetch(it.to_s, []) }
+      imported = Import::Write.call(store, revised, group: group.empty? ? nil : group, joins:, named: makes)
       Import::Staged.close(upload)
 
-      import_screen(landed:)
+      import_screen(imported:)
     end
 
     # The groups half of one contact's screen: what this book has,
@@ -292,7 +294,7 @@ module ProTacts
     end
 
     # The import's two readings, or none when it is gone — swept out
-    # from under a screen left open overnight, or landed already, a
+    # from under a screen left open overnight, or written already, a
     # second confirm finding what the first removed. Ordinary enough
     # for the screen to say so and ask for the file again.
     #
@@ -303,15 +305,16 @@ module ProTacts
     #: (String upload) -> [Array[VCard], Array[VCard]]?
     def staged_cards(upload)
       original = Import::Staged.read(upload, Import::Staged::ORIGINAL)
-      landing = Import::Staged.read(upload, Import::Staged::LANDING)
-      return nil if original.nil? || landing.nil?
+      revised = Import::Staged.read(upload, Import::Staged::REVISED)
+      return nil if original.nil? || revised.nil?
 
-      [Import::Vcf.cards(original), Import::Vcf.cards(landing)]
+      [Import::Vcf.cards(original), Import::Vcf.cards(revised)]
     end
 
     # A staged card read as the contact the editor edits. The id is
-    # its place in the file, there being no minted one until it lands,
-    # and no group lends an unlanded card a line.
+    # its place in the file, there being no minted one until it is
+    # written, and no group lends a card that is not stored yet a
+    # line.
     #
     # The birthday comes out of the card and into the model, which is
     # the split Store#put makes on the way in
@@ -352,15 +355,15 @@ module ProTacts
       param if param.is_a?(Hash) && param[:tempfile]
     end
 
-    # The screen a file is chosen on, and the page a landing answers
-    # with. The landing answers with the result rather than a 303, and
+    # The screen a file is chosen on, and the page a confirm answers
+    # with. The confirm answers with the result rather than a 303, and
     # has to: the staged import is gone, so the re-submission a back
-    # button offers has nothing to land, and there is no other page
+    # button offers has nothing to import, and there is no other page
     # holding what just arrived.
-    #: (?landed: Array[Contact]?, ?notice: String?) -> String
-    def import_screen(landed: nil, notice: nil)
+    #: (?imported: Array[Contact]?, ?notice: String?) -> String
+    def import_screen(imported: nil, notice: nil)
       response["Content-Type"] = "text/html; charset=utf-8"
-      Admin::ImportUpload.call(landed:, notice:)
+      Admin::ImportUpload.call(imported:, notice:)
     end
 
     #: () -> String
diff --git a/sig/pro_tacts/admin.rbs b/sig/pro_tacts/admin.rbs
index 48074fb..77667cd 100644
--- a/sig/pro_tacts/admin.rbs
+++ b/sig/pro_tacts/admin.rbs
@@ -42,7 +42,7 @@ module ProTacts
     end
 
     class ImportUpload
-      def self.call: (?landed: Array[Contact]?, ?notice: String?) -> String
+      def self.call: (?imported: Array[Contact]?, ?notice: String?) -> String
     end
 
     class ImportReview
diff --git a/test/pro_tacts/admin/test_import_pages.rb b/test/pro_tacts/admin/test_import_pages.rb
index 20d5ad6..1ae2a66 100644
--- a/test/pro_tacts/admin/test_import_pages.rb
+++ b/test/pro_tacts/admin/test_import_pages.rb
@@ -70,7 +70,7 @@ class AdminImportPagesTest < Minitest::Test
     assert_includes last_response.body, %(<input type="file" name="vcf" accept=".vcf,text/vcard">)
   end
 
-  def test_an_upload_is_looked_over_before_anything_lands
+  def test_an_upload_is_looked_over_before_anything_is_written
     with_contacts({}) do |store|
       id = upload(JANE + PLAIN)
 
@@ -116,7 +116,7 @@ class AdminImportPagesTest < Minitest::Test
       upload(PLAIN)
       follow_redirect!
 
-      assert_includes last_response.body, "Nothing in this file needs reading before it lands."
+      assert_includes last_response.body, "Nothing in this file needs reading before it comes in."
       refute_includes last_response.body, "left behind"
     end
   end
@@ -129,7 +129,7 @@ class AdminImportPagesTest < Minitest::Test
       upload(PLAIN.sub("VERSION:3.0\r\n", "VERSION:3.0\r\nPRODID:-//Apple Inc.//macOS 15.0//EN\r\n"))
       follow_redirect!
 
-      assert_includes last_response.body, "Nothing in this file needs reading before it lands."
+      assert_includes last_response.body, "Nothing in this file needs reading before it comes in."
       refute_includes last_response.body, "left behind"
       refute_includes last_response.body, "PRODID"
     end
@@ -147,14 +147,14 @@ class AdminImportPagesTest < Minitest::Test
       assert_includes last_response.body,
                       %(<li data-dropped><span>PRODID:-//Apple Inc.//macOS 15.0//EN</span>)
 
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
 
       refute_includes store.contacts.fetch(0).vcard.to_s, "PRODID"
     end
   end
 
   # The two cards: the contact as the file wrote it, and the contact
-  # that is landing, open in the editor.
+  # that is coming in, open in the editor.
   def test_a_contact_opens_beside_the_card_it_arrived_as
     with_contacts({}) do |_store|
       id = upload(JANE)
@@ -188,7 +188,7 @@ class AdminImportPagesTest < Minitest::Test
 
   # The point of the pair: read what is being left behind on the
   # left, and put what matters into the card on the right.
-  def test_an_edit_is_held_against_the_contact_until_the_import_lands
+  def test_an_edit_is_held_against_the_contact_until_the_import_is_confirmed
     with_contacts({}) do |store|
       id = upload(JANE)
       get "/import/#{id}/0"
@@ -200,7 +200,7 @@ class AdminImportPagesTest < Minitest::Test
       assert_equal 303, last_response.status
       assert_empty store.changes
 
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
 
       assert_includes store.contacts.fetch(0).vcard.to_s, "NOTE:Spouse: Sam Booles"
     end
@@ -224,8 +224,8 @@ class AdminImportPagesTest < Minitest::Test
 
   # The birthday is the one field the editor holds in the model
   # rather than in the card, and a staged contact has no model: the
-  # save writes it back as the BDAY line it will land as.
-  def test_a_birthday_typed_on_the_import_lands_with_the_contact
+  # save writes it back as the BDAY line it will come in as.
+  def test_a_birthday_typed_on_the_import_comes_in_with_the_contact
     with_contacts({}) do |store|
       id = upload(JANE)
       get "/import/#{id}/0"
@@ -237,7 +237,7 @@ class AdminImportPagesTest < Minitest::Test
 
       assert_equal 303, last_response.status
 
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
 
       assert_equal ProTacts::Birthday.new(year: 1985, month: 4, day: 12), store.contacts.fetch(0).birthday
     end
@@ -245,8 +245,8 @@ class AdminImportPagesTest < Minitest::Test
 
   # And the shape no card can spell is refused rather than dropped:
   # the model that holds a partial birthday does not exist until the
-  # contact lands (docs/plans/2026-08-31-partial-birthdays.md).
-  def test_a_birthday_no_card_can_spell_is_refused_until_the_contact_lands
+  # contact is stored (docs/plans/2026-08-31-partial-birthdays.md).
+  def test_a_birthday_no_card_can_spell_is_refused_until_the_contact_is_stored
     with_contacts({}) do |_store|
       id = upload(JANE)
       get "/import/#{id}/0"
@@ -257,7 +257,7 @@ class AdminImportPagesTest < Minitest::Test
            "birthday" => { "year" => "1985", "month" => "", "day" => "" }
 
       assert_equal 200, last_response.status
-      assert_includes last_response.body, "A card cannot hold a birthday that partial until it lands."
+      assert_includes last_response.body, "A card cannot hold a birthday that partial."
     end
   end
 
@@ -294,10 +294,10 @@ class AdminImportPagesTest < Minitest::Test
   end
 
   # A group that does not exist yet is named beside the contact and
-  # made at the landing, not before: a group created while the walk
+  # made at the confirm, not before: a group created while the walk
   # is still going is one left behind by an import that was
   # abandoned.
-  def test_a_group_named_during_the_walk_waits_for_the_landing
+  def test_a_group_named_during_the_walk_waits_for_the_confirm
     with_contacts({}) do |store|
       id = upload(JANE)
       get "/import/#{id}/0"
@@ -316,14 +316,14 @@ class AdminImportPagesTest < Minitest::Test
       assert_includes last_response.body,
                       %(<input type="checkbox" name="named[]" value="Clarks" checked>)
 
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
       clarks = store.all_groups.find { it.name == "Clarks" }
 
       assert_equal [store.contacts.fetch(0).id], clarks.members
     end
   end
 
-  # Contact by contact, so the cards do not all land alike. The id
+  # Contact by contact, so the cards do not all come in alike. The id
   # that names no group is dropped rather than carried into
   # Store#add_member, which reads a group with `sole` and would
   # answer bad input with a 500.
@@ -337,7 +337,7 @@ class AdminImportPagesTest < Minitest::Test
            "etag" => etag, "first" => "Jane", "middle" => "", "last" => "Booles",
            "nickname" => "", "note" => "", "groups" => [school, "zzzz"]
 
-      post "/import/#{id}/land", "group" => "import-20260921T031655Z"
+      post "/import/#{id}/confirm", "group" => "import-20260921T031655Z"
 
       assert_equal 200, last_response.status
       jane = store.contacts.find { it.name == "Jane Booles" }
@@ -364,21 +364,21 @@ class AdminImportPagesTest < Minitest::Test
            "nickname" => "", "note" => "", "groups" => [school]
       store.delete_group(school)
 
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
 
       assert_equal 200, last_response.status
       assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
     end
   end
 
-  def test_confirming_lands_the_contacts_as_they_stand
+  def test_confirming_writes_the_contacts_as_they_stand
     with_contacts({}) do |store|
       id = upload(JANE + PLAIN)
 
-      post "/import/#{id}/land", "group" => "import-20260921T031655Z"
+      post "/import/#{id}/confirm", "group" => "import-20260921T031655Z"
 
       assert_equal 200, last_response.status
-      assert_includes last_response.body, "landed (2)"
+      assert_includes last_response.body, "imported (2)"
       assert_equal 2, store.contacts.length
       assert_includes store.all_groups.map(&:name), "import-20260921T031655Z"
 
@@ -393,12 +393,12 @@ class AdminImportPagesTest < Minitest::Test
 
   # Swept out from under a screen left open, or a confirm submitted
   # twice: the import is gone and only the person has another copy.
-  def test_a_confirm_with_no_staged_import_lands_nothing
+  def test_a_confirm_with_no_staged_import_writes_nothing
     with_contacts({}) do |store|
       id = upload(JANE)
 
-      post "/import/#{id}/land", "group" => ""
-      post "/import/#{id}/land", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
+      post "/import/#{id}/confirm", "group" => ""
 
       assert_includes last_response.body, "That import is no longer here."
       assert_equal 1, store.contacts.length
@@ -411,7 +411,7 @@ class AdminImportPagesTest < Minitest::Test
     with_contacts({}) do |store|
       refute ProTacts::ChangeId.minted?("notminted")
 
-      post "/import/notminted/land", "group" => ""
+      post "/import/notminted/confirm", "group" => ""
 
       assert_includes last_response.body, "That import is no longer here."
       assert_empty store.changes
diff --git a/test/pro_tacts/import/test_vcf.rb b/test/pro_tacts/import/test_vcf.rb
index df1b17d..7c7d9b4 100644
--- a/test/pro_tacts/import/test_vcf.rb
+++ b/test/pro_tacts/import/test_vcf.rb
@@ -152,14 +152,14 @@ class ImportVcfTest < Minitest::Test
   # One card as this book will hold it: the lines it reads, in the
   # bytes and the order they arrived.
   def test_reading_a_card_keeps_what_a_screen_here_shows
-    landing = Vcf.read(Vcf.cards(JANE).fetch(0)).card.to_s
+    revised = Vcf.read(Vcf.cards(JANE).fetch(0)).card.to_s
 
-    assert_includes landing, "N:Booles;Jane;;;\r\n"
-    assert_includes landing, "TEL;type=CELL;type=VOICE;type=pref:+1 555 0100\r\n"
-    assert_includes landing, "item1.ADR;type=HOME:;;1 Main St;Springfield;;;\r\n"
-    assert_includes landing, "UID:ABC-123\r\n"
+    assert_includes revised, "N:Booles;Jane;;;\r\n"
+    assert_includes revised, "TEL;type=CELL;type=VOICE;type=pref:+1 555 0100\r\n"
+    assert_includes revised, "item1.ADR;type=HOME:;;1 Main St;Springfield;;;\r\n"
+    assert_includes revised, "UID:ABC-123\r\n"
     # Still a card: the envelope is read like anything else.
-    assert Vcf.cards(landing).fetch(0).card?
+    assert Vcf.cards(revised).fetch(0).card?
   end
 
   def test_reading_a_card_leaves_behind_what_no_screen_shows
diff --git a/test/pro_tacts/import/test_land.rb b/test/pro_tacts/import/test_write.rb
similarity index 65%
rename from test/pro_tacts/import/test_land.rb
rename to test/pro_tacts/import/test_write.rb
index a365f63..211900a 100644
--- a/test/pro_tacts/import/test_land.rb
+++ b/test/pro_tacts/import/test_write.rb
@@ -1,14 +1,14 @@
 require_relative "../../test_helper"
 
-require "pro_tacts/import/land"
+require "pro_tacts/import/write"
 require "pro_tacts/import/vcf"
 
-# Landing the cards an import has settled on
+# Writing the cards an import has settled on
 # (docs/plans/2026-09-21-import-a-vcf.md).
-class ImportLandTest < Minitest::Test
+class ImportWriteTest < Minitest::Test
   include ThrowawayContacts
 
-  Land = ProTacts::Import::Land
+  Write = ProTacts::Import::Write
   Vcf = ProTacts::Import::Vcf
 
   JANE = <<~CARD.gsub("\n", "\r\n")
@@ -21,17 +21,17 @@ class ImportLandTest < Minitest::Test
     END:VCARD
   CARD
 
-  def land(cards: Vcf.cards(JANE), group: nil)
+  def write_cards(cards: Vcf.cards(JANE), group: nil)
     with_contacts({}) do |store|
-      yield Land.call(store, cards, group:), store
+      yield Write.call(store, cards, group:), store
     end
   end
 
   # What arrives here is already what is coming in, so nothing is
   # pared, rewritten or rebuilt but the card's identity.
-  def test_the_card_lands_as_it_was_handed_over
-    land do |landed, _store|
-      card = landed.fetch(0).vcard.to_s
+  def test_the_card_comes_in_as_it_was_handed_over
+    write_cards do |imported, _store|
+      card = imported.fetch(0).vcard.to_s
 
       assert_includes card, "N:Booles;Jane;;;\r\n"
       assert_includes card, "FN:Jane Booles\r\n"
@@ -41,13 +41,13 @@ class ImportLandTest < Minitest::Test
 
   # A minted id, and the UID spelling it: the source's own UID named a
   # record in a book this is not (rake contacts:reid's whole subject).
-  def test_the_card_lands_under_a_minted_id_its_uid_spells
-    land do |landed, store|
-      id = landed.fetch(0).id
+  def test_the_card_is_stored_under_a_minted_id_its_uid_spells
+    write_cards do |imported, store|
+      id = imported.fetch(0).id
 
       assert ProTacts::ChangeId.minted?(id)
-      refute_includes landed.fetch(0).vcard.to_s, "ABC-123"
-      assert_includes landed.fetch(0).vcard.to_s, "UID:#{id}\r\n"
+      refute_includes imported.fetch(0).vcard.to_s, "ABC-123"
+      assert_includes imported.fetch(0).vcard.to_s, "UID:#{id}\r\n"
       assert_equal id, store.card_id_with_uid(id)
     end
   end
@@ -55,8 +55,8 @@ class ImportLandTest < Minitest::Test
   # A card this creates joins everyone's book the way a client's
   # create does, and the import's own group besides.
   def test_arrivals_join_the_import_group_and_everyones_book
-    land(cards: Vcf.cards(JANE + JANE), group: "import-20260921T031655Z") do |landed, store|
-      ids = landed.map(&:id)
+    write_cards(cards: Vcf.cards(JANE + JANE), group: "import-20260921T031655Z") do |imported, store|
+      ids = imported.map(&:id)
 
       assert_equal 2, ids.uniq.length
       groups = store.all_groups.select { it.members.sort == ids.sort }
@@ -70,13 +70,13 @@ class ImportLandTest < Minitest::Test
   # is what creates `sync:*` — so a list read once up front would send
   # the next lookup to create a group that now exists
   # (db/migrations/008_group_names.rb).
-  def test_landing_into_a_group_that_already_exists_joins_it
+  def test_writing_into_a_group_that_already_exists_joins_it
     with_contacts({}) do |store|
       existing = store.create_group(name: "import-20260921T031655Z")
 
-      landed = Land.call(store, Vcf.cards(JANE + JANE), group: "import-20260921T031655Z")
+      imported = Write.call(store, Vcf.cards(JANE + JANE), group: "import-20260921T031655Z")
 
-      assert_equal landed.map(&:id).sort, store.group(existing).members.sort
+      assert_equal imported.map(&:id).sort, store.group(existing).members.sort
       assert_equal 1, store.all_groups.count { it.name == "import-20260921T031655Z" }
     end
   end
@@ -88,8 +88,8 @@ class ImportLandTest < Minitest::Test
     with_contacts({}) do |store|
       school = store.create_group(name: "school")
 
-      landed = Land.call(store, Vcf.cards(JANE), group: "import-20260921T031655Z", joins: [[school]])
-      id = landed.fetch(0).id
+      imported = Write.call(store, Vcf.cards(JANE), group: "import-20260921T031655Z", joins: [[school]])
+      id = imported.fetch(0).id
 
       assert_equal [id], store.group(school).members
       assert_equal ["import-20260921T031655Z", "school", ProTacts::Store::EVERYONE],
@@ -97,15 +97,15 @@ class ImportLandTest < Minitest::Test
     end
   end
 
-  # A contact at a time means the cards do not all land alike: the
+  # A contact at a time means the cards do not all come in alike: the
   # choices are read by the card's own place in the file.
   def test_each_arrival_joins_its_own_groups
     with_contacts({}) do |store|
       school = store.create_group(name: "school")
       work = store.create_group(name: "work")
 
-      landed = Land.call(store, Vcf.cards(JANE + JANE + JANE), joins: [[school], [], [work, school]])
-      first, second, third = landed.map(&:id)
+      imported = Write.call(store, Vcf.cards(JANE + JANE + JANE), joins: [[school], [], [work, school]])
+      first, second, third = imported.map(&:id)
 
       assert_equal [first, third].sort, store.group(school).members.sort
       assert_equal [third], store.group(work).members
@@ -117,12 +117,12 @@ class ImportLandTest < Minitest::Test
   # A group the walk asked for by name is made here and not before:
   # a group created while the walk was still going is one left behind
   # by an import that was abandoned.
-  def test_a_group_named_during_the_walk_is_made_at_the_landing
+  def test_a_group_named_during_the_walk_is_made_at_the_confirm
     with_contacts({}) do |store|
-      landed = Land.call(store, Vcf.cards(JANE + JANE), named: [["Clarks"], ["Clarks"]])
+      imported = Write.call(store, Vcf.cards(JANE + JANE), named: [["Clarks"], ["Clarks"]])
       clarks = store.all_groups.find { it.name == "Clarks" }
 
-      assert_equal landed.map(&:id).sort, clarks.members.sort
+      assert_equal imported.map(&:id).sort, clarks.members.sort
       assert_equal 1, store.all_groups.count { it.name == "Clarks" }
     end
   end
@@ -131,8 +131,8 @@ class ImportLandTest < Minitest::Test
   # a constraint violation rather than a second membership.
   def test_a_name_that_is_the_imports_own_group_joins_it_once
     with_contacts({}) do |store|
-      landed = Land.call(store, Vcf.cards(JANE), group: "Clarks", named: [["Clarks"]])
-      id = landed.fetch(0).id
+      imported = Write.call(store, Vcf.cards(JANE), group: "Clarks", named: [["Clarks"]])
+      id = imported.fetch(0).id
 
       assert_equal 1, store.all_groups.count { it.name == "Clarks" }
       assert_equal [id], store.all_groups.find { it.name == "Clarks" }.members
@@ -145,23 +145,23 @@ class ImportLandTest < Minitest::Test
     with_contacts({}) do |store|
       school = store.create_group(name: "school")
 
-      landed = Land.call(store, Vcf.cards(JANE), joins: [[school]])
+      imported = Write.call(store, Vcf.cards(JANE), joins: [[school]])
 
-      assert_equal landed.map(&:id), store.group(school).members
+      assert_equal imported.map(&:id), store.group(school).members
       assert_equal %w[school], store.all_groups.map(&:name).reject { it == ProTacts::Store::EVERYONE }
     end
   end
 
-  def test_no_group_name_lands_the_cards_in_everyones_book_alone
-    land do |landed, store|
+  def test_no_group_name_puts_the_cards_in_everyones_book_alone
+    write_cards do |imported, store|
       assert_equal [ProTacts::Store::EVERYONE], store.all_groups.map(&:name)
-      assert_equal landed.map(&:id), store.all_groups.fetch(0).members
+      assert_equal imported.map(&:id), store.all_groups.fetch(0).members
     end
   end
 
-  # Named for when it happened, so what landed together can be found
+  # Named for when it happened, so what imported together can be found
   # together.
   def test_the_default_group_is_named_for_the_moment
-    assert_equal "import-20260921T031655Z", Land.default_group(Time.utc(2026, 9, 21, 3, 16, 55))
+    assert_equal "import-20260921T031655Z", Write.default_group(Time.utc(2026, 9, 21, 3, 16, 55))
   end
 end