docs: rename macos-contacts.md to apple-contacts.md
It covers the iPhone too, so macOS came off the name;
five letters out, five letters in, and every wrap stayed the same.
Assisted-by: Claude Opus 5 via Claude Code
diff --git a/README.md b/README.md
index 42d0ebb..8703ee6 100644
--- a/README.md
+++ b/README.md
@@ -109,7 +109,7 @@ carry:
is its test.
Three properties are load-bearing in non-obvious ways, documented in
-`docs/macos-contacts.md`: the collection's `resourcetype` (without
+`docs/apple-contacts.md`: the collection's `resourcetype` (without
`card:addressbook` the client drops the account data), the
`sync-collection` advertisement (without it the warm sync never runs), and
`getctag` (without it no vCard is ever requested). Everything else the
@@ -118,7 +118,7 @@ me-card, principal-URL, the multiget/query advertisements — is optional.
sabre/dav's [notes on the macOS Address Book client][sabre-osx] are the best
single source of client quirks beyond this list, and they explain several
-failures that look like protocol bugs but are not. See `docs/macos-contacts.md`
+failures that look like protocol bugs but are not. See `docs/apple-contacts.md`
for the details worth keeping close, including how to turn on the client's own
debug logging.
diff --git a/docs/macos-contacts.md b/docs/apple-contacts.md
similarity index 98%
rename from docs/macos-contacts.md
rename to docs/apple-contacts.md
index 3b38266..6c71c19 100644
--- a/docs/macos-contacts.md
+++ b/docs/apple-contacts.md
@@ -1,9 +1,10 @@
-# macOS Contacts as a CardDAV client
+# Apple Contacts as a CardDAV client
-What the macOS Contacts client does, where it deviates from the RFCs, and
-which failures look like protocol bugs but are not. Most of this is
-distilled from sabre/dav's [client notes][sabre-osx] and from traffic
-captures against the servers in `servers/`.
+What the Contacts clients on macOS and iOS do, where they deviate from the
+RFCs, and which failures look like protocol bugs but are not. Most of this
+is macOS, distilled from sabre/dav's [client notes][sabre-osx] and from
+traffic captures against the servers in `servers/`; sections that cover
+iOS say so.
[sabre-osx]: https://sabre.io/dav/clients/osx-addressbook/
diff --git a/docs/plans/2026-08-12-one-card-on-macos.md b/docs/plans/2026-08-12-one-card-on-macos.md
index 1f37672..646e28d 100644
--- a/docs/plans/2026-08-12-one-card-on-macos.md
+++ b/docs/plans/2026-08-12-one-card-on-macos.md
@@ -17,7 +17,7 @@ questions into experiments: change one response, resync, observe.
It also fixes the network path early. Tailscale serve terminates TLS and
proxies to the app, which is exactly the arrangement that breaks
`/.well-known/carddav` redirects for everyone else (see
-`../macos-contacts.md`).
+`../apple-contacts.md`).
## Capabilities
@@ -48,7 +48,7 @@ Explicitly out of scope, to be built only after this milestone lands:
## Constraints
-- vCard 3.0, with an Apple-shaped `PRODID`. See `../macos-contacts.md`.
+- vCard 3.0, with an Apple-shaped `PRODID`. See `../apple-contacts.md`.
- The `.well-known` redirect must be an absolute `https://` URL. A
scheme-relative or `http://` redirect is dropped by the client without
reaching the server.
diff --git a/docs/plans/2026-08-24-corrections-from-the-first-write.md b/docs/plans/2026-08-24-corrections-from-the-first-write.md
index 57b6ae8..916210f 100644
--- a/docs/plans/2026-08-24-corrections-from-the-first-write.md
+++ b/docs/plans/2026-08-24-corrections-from-the-first-write.md
@@ -5,7 +5,7 @@ the first time, and the captures in `log/unhandled` contradict three
things in `2026-08-24-vcard-storage-and-groups.md`. That record stands
as written; this one says what the first real card changed.
-The client behavior itself is in `../macos-contacts.md`. This covers
+The client behavior itself is in `../apple-contacts.md`. This covers
only what it means for the storage design.
## Diffing a submitted card has to be semantic
diff --git a/docs/plans/2026-09-10-the-address-book-stays-writable.md b/docs/plans/2026-09-10-the-address-book-stays-writable.md
index d8ed847..107e086 100644
--- a/docs/plans/2026-09-10-the-address-book-stays-writable.md
+++ b/docs/plans/2026-09-10-the-address-book-stays-writable.md
@@ -9,7 +9,7 @@ showing nothing, so the idea is dropped.
## Every lever fails on macOS
A client learns what it may do from `DAV:current-user-privilege-set`
-(RFC 3744 section 5.4) on the collection. `docs/macos-contacts.md`
+(RFC 3744 section 5.4) on the collection. `docs/apple-contacts.md`
records what Contacts does with each form the property can take:
| Server advertises | Contacts does | Recorded under |
diff --git a/lib/pro_tacts/birthday.rb b/lib/pro_tacts/birthday.rb
index f34a153..959e545 100644
--- a/lib/pro_tacts/birthday.rb
+++ b/lib/pro_tacts/birthday.rb
@@ -38,7 +38,7 @@ module ProTacts
FULL_DATE = /\A(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])(?:T\d{2}:?\d{2}(:?\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?\z/ #: Regexp
# The no-year shape macOS writes, with 1604 standing in for the year
- # in both the parameter and the value (docs/macos-contacts.md,
+ # in both the parameter and the value (docs/apple-contacts.md,
# "Birthdays without a year"). 1604 because it is the first year of
# the Gregorian calendar; the parameter names the year to omit.
# Components in range, like FULL_DATE.
@@ -54,7 +54,7 @@ module ProTacts
# across a rewrite, by the same probe: the served shapes in any
# spelling were visible, so their absence is a deletion, and an
# unrecognized or out-of-range value is cleaner dying with the
- # rewrite than living forever (docs/macos-contacts.md, "A birthday
+ # rewrite than living forever (docs/apple-contacts.md, "A birthday
# the client cannot render is dropped from the card").
YEAR_AND_MONTH = /\A(\d{4})-(0[1-9]|1[0-2])\z/ #: Regexp
YEAR_ALONE = /\A(\d{4})\z/ #: Regexp
@@ -70,7 +70,7 @@ module ProTacts
# The reduced no-year values macOS reads on a card it did not write
# and converts to the sentinel on its own
- # (docs/macos-contacts.md, "Birthdays without a year"). Together
+ # (docs/apple-contacts.md, "Birthdays without a year"). Together
# with #from_property's two spellings this is the rendered set: a
# BDAY a client could see, and so could have deleted. Capturing
# for #from_value, like the unrendered patterns above.
diff --git a/lib/pro_tacts/card_diff.rb b/lib/pro_tacts/card_diff.rb
index 524f496..e4580b6 100644
--- a/lib/pro_tacts/card_diff.rb
+++ b/lib/pro_tacts/card_diff.rb
@@ -11,7 +11,7 @@ module ProTacts
#
# A multiset difference over the card's lines, not an edit script:
# macOS Contacts reorders properties on every card it touches
- # (docs/macos-contacts.md), so a positional diff would report a
+ # (docs/apple-contacts.md), so a positional diff would report a
# shuffle as a change to every line below the first move. Lines that
# both cards carry cancel, however far apart they sit, and what is
# left is the write. A card carrying the same line twice is two
diff --git a/lib/pro_tacts/contact.rb b/lib/pro_tacts/contact.rb
index d730bc5..af9d2b9 100644
--- a/lib/pro_tacts/contact.rb
+++ b/lib/pro_tacts/contact.rb
@@ -398,7 +398,7 @@ module ProTacts
# Every TYPE value (RFC 2426 section 3.3.1) that names a kind of
# line. `pref` ranks a line instead, and the client adds it on its
- # own (docs/macos-contacts.md, "The client rewrites every card it
+ # own (docs/apple-contacts.md, "The client rewrites every card it
# touches"); `except` holds a property's own values that name no
# kind either. Downcased: the spelling is the card's, and a screen
# shows one.
diff --git a/lib/pro_tacts/store.rb b/lib/pro_tacts/store.rb
index 5dd7669..eaa4b01 100644
--- a/lib/pro_tacts/store.rb
+++ b/lib/pro_tacts/store.rb
@@ -377,7 +377,7 @@ module ProTacts
# model emptied so nothing composes a second BDAY beside it
# (RFC 6352 section 6.3.2.2). No BDAY at all is a client's
# rewrite, and macOS Contacts drops the BDAY lines it cannot
- # render from every card it writes (docs/macos-contacts.md):
+ # render from every card it writes (docs/apple-contacts.md):
# what the stored card held in a spelling no client renders
# rides across the rewrite, what it held that a client could
# see was the user's deletion, an unseen model row survives as
@@ -1035,7 +1035,7 @@ module ProTacts
# is a question about what it says and not about its bytes: macOS
# re-serializes every card it touches, so `ADR;TYPE=home` comes
# back `ADR;type=HOME;type=pref` on an address nobody edited
- # (docs/macos-contacts.md, "The client rewrites every card it
+ # (docs/apple-contacts.md, "The client rewrites every card it
# touches"). Comparing bytes reads every such line as an edit.
#
# What a line says is its value and its types: a member who
@@ -1070,7 +1070,7 @@ module ProTacts
# The third: a type Contacts has no field for comes back moved,
# not kept — into a property group, `TYPE` parameter gone, the type
# the value of an `X-ABLabel` beside it — on an address nobody
- # touched (docs/macos-contacts.md, "An address type the client
+ # touched (docs/apple-contacts.md, "An address type the client
# cannot model becomes a custom label"). So that label counts as
# one of the line's types, and `ADR;TYPE=dom` coming back as
# `item1.ADR` with `item1.X-ABLabel:dom` is unedited.
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index d0f7773..ef506ef 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -585,7 +585,7 @@ module ProTacts
# creates with If-None-Match: * and a UUID it mints into both
# the URI and the card's UID, and updates with If-Match
# carrying the strong etag it was last served — see
- # docs/macos-contacts.md, "What a write looks like on the wire".
+ # docs/apple-contacts.md, "What a write looks like on the wire".
r.put String do |filename|
id = filename.delete_suffix(".vcf")
# A last segment that is not this server's <id>.vcf shape
diff --git a/tasks/probe.rake b/tasks/probe.rake
index cf82ed6..1a4e425 100644
--- a/tasks/probe.rake
+++ b/tasks/probe.rake
@@ -2,7 +2,7 @@
# probe is a card built to be edited by a real client and read back off
# the wire, because what Apple does with a property is empirical and
# every guess about it has been wrong at least once
-# (docs/macos-contacts.md).
+# (docs/apple-contacts.md).
#
# Seeded over HTTP rather than into the database, so the card travels
# the same route a client's would and the dev server needs no
@@ -173,7 +173,7 @@ namespace :probe do
cannot carry into a propagating write.
- 3 Untyped Way: which types did the client write where the
card carried none?
- 5. Record what happened in docs/macos-contacts.md, under "The
+ 5. Record what happened in docs/apple-contacts.md, under "The
client rewrites every card it touches".
6. Then relabel one address to a custom label and read it again:
a custom label is an itemN.ADR with an X-ABLabel beside it,
@@ -192,7 +192,7 @@ namespace :probe do
4. Compare it with the card in tasks/probe.rake: which of the five
forms came back, whether itemN was renumbered, and whether a
companion stayed with the line it was grouped with.
- 5. Record what happened in docs/macos-contacts.md. Repeat on iOS,
+ 5. Record what happened in docs/apple-contacts.md. Repeat on iOS,
which numbers item groups differently (see
test/fixtures/ios-exchange/05-put-contact-edit/request).
NEXT
@@ -213,7 +213,7 @@ namespace :probe do
item3.X-PT-GROUP follow them there, or stay at item3?
The second answers whether a companion is a binding the client
maintains or two numbers that happened to agree.
- 5. Record what happened in docs/macos-contacts.md, under "An
+ 5. Record what happened in docs/apple-contacts.md, under "An
annotation survives only on its own line".
NEXT
end
@@ -263,7 +263,7 @@ namespace :probe do
#{reading_the_result(NOTES_ID, "Notes Boole", edit: "any field other than the note").chomp}
4. Read the NOTE lines that come back: both as sent, one joined
line, or one dropped — and which.
- 5. Record what happened in docs/macos-contacts.md.
+ 5. Record what happened in docs/apple-contacts.md.
NEXT
end
@@ -280,7 +280,7 @@ namespace :probe do
#{reading_the_result(CATEGORIES_ID, "Categories Boole").chomp}
4. Read the CATEGORIES lines that come back: both as sent, merged
into one, split one value per line, or gone.
- 5. Record what happened in docs/macos-contacts.md.
+ 5. Record what happened in docs/apple-contacts.md.
NEXT
end
end
diff --git a/test/fixtures/macos-exchange/README.md b/test/fixtures/macos-exchange/README.md
index 6dec261..b639728 100644
--- a/test/fixtures/macos-exchange/README.md
+++ b/test/fixtures/macos-exchange/README.md
@@ -54,4 +54,4 @@ at 76 octets a continuation — are built synthetically in
`test/photo_card.rb`, and a later session that set pictures on the
synthetic seed contacts deliberately (2026-09-04) is seeded directly
as `test/fixtures/cards/{photo,emoji,memoji}.vcf`; see
-docs/macos-contacts.md, "Profile pictures".
+docs/apple-contacts.md, "Profile pictures".
diff --git a/test/pro_tacts/admin/test_contacts_pages.rb b/test/pro_tacts/admin/test_contacts_pages.rb
index 8347db5..36f3eb0 100644
--- a/test/pro_tacts/admin/test_contacts_pages.rb
+++ b/test/pro_tacts/admin/test_contacts_pages.rb
@@ -659,7 +659,7 @@ class AdminContactsPagesTest < Minitest::Test
# The seed book carries one fixture card per picture kind macOS
# sends — photo, emoji, memoji, promoted from real client sessions
- # (see test/fixtures/cards and docs/macos-contacts.md) — and these
+ # (see test/fixtures/cards and docs/apple-contacts.md) — and these
# tests are what keeps them exercising the path: a seed that stops
# parsing, or stops sniffing to an image, fails here rather than
# passing silently as an initials avatar. Runs against the suite's
diff --git a/test/pro_tacts/test_birthday.rb b/test/pro_tacts/test_birthday.rb
index 1b0f046..88014d8 100644
--- a/test/pro_tacts/test_birthday.rb
+++ b/test/pro_tacts/test_birthday.rb
@@ -93,7 +93,7 @@ class BirthdayTest < Minitest::Test
ProTacts::Birthday.from_property(property("1985-04-12"))
end
- # docs/macos-contacts.md, "Birthdays without a year": the verified
+ # docs/apple-contacts.md, "Birthdays without a year": the verified
# Apple form, with 1604 standing in for the year in both halves.
def test_the_apple_no_year_form_reads_into_the_model
sentinel = property("1604-04-12", parameters: [["X-APPLE-OMIT-YEAR", "1604"]])
diff --git a/test/pro_tacts/test_card_diff.rb b/test/pro_tacts/test_card_diff.rb
index d2e62b8..18983ec 100644
--- a/test/pro_tacts/test_card_diff.rb
+++ b/test/pro_tacts/test_card_diff.rb
@@ -36,7 +36,7 @@ class CardDiffTest < Minitest::Test
end
# macOS reorders properties on every card it touches
- # (docs/macos-contacts.md), so a diff that read position would call a
+ # (docs/apple-contacts.md), so a diff that read position would call a
# shuffle a rewrite of everything below the first move.
def test_reordering_a_card_is_no_change
shuffled = AIDEN.sub("FN:Aiden\r\nUID:aiden\r\n", "UID:aiden\r\nFN:Aiden\r\n")
diff --git a/test/pro_tacts/test_store.rb b/test/pro_tacts/test_store.rb
index 7d3cbdd..cfee6bd 100644
--- a/test/pro_tacts/test_store.rb
+++ b/test/pro_tacts/test_store.rb
@@ -772,7 +772,7 @@ class StoreTest < Minitest::Test
# macOS Contacts drops the lines it cannot render from every card it
# writes, so a rewrite that omits the BDAY carries them across rather
- # than reading the absence as a deletion (docs/macos-contacts.md, "A
+ # than reading the absence as a deletion (docs/apple-contacts.md, "A
# birthday the client cannot render is dropped from the card").
def test_a_birthday_no_client_renders_survives_a_rewrite_that_drops_it
["BDAY:1985-04", "BDAY:1985", "BDAY:--04", "BDAY:---12"].each do |line|
@@ -1304,7 +1304,7 @@ class StoreTest < Minitest::Test
# The group's lines as macOS returns them on a card whose address and
# note nobody edited: the parameter name lowercased, its value
# uppercased, `pref` filled in, an unmodeled parameter dropped, and
- # every value byte-identical (docs/macos-contacts.md, "The client
+ # every value byte-identical (docs/apple-contacts.md, "The client
# rewrites every card it touches" and "An annotation survives only on
# its own line").
RESERIALIZED_ADDRESS = HOUSEHOLD_ADDRESS.sub("ADR;TYPE=home:", "ADR;type=HOME;type=pref:") #: String
@@ -1439,7 +1439,7 @@ class StoreTest < Minitest::Test
end
# An untyped line comes back untyped: the client invents no type to
- # fill the gap (docs/macos-contacts.md, "An address type the client
+ # fill the gap (docs/apple-contacts.md, "An address type the client
# cannot model becomes a custom label").
def test_an_untyped_lent_line_comes_back_untyped
with_store({"aiden" => AIDEN}) do |store|