ai planning
change qpovkvqxwqrywzwotolkrlnltsuolnst
commit 9b7bdd087031d33d740e465e3c99ed3fb6a2453b
author Alpha Chen <alpha@kejadlen.dev>
date
parent kumwusuw
diff --git a/docs/plans/2026-01-12-carddav-architecture.md b/docs/plans/2026-01-12-carddav-architecture.md
new file mode 100644
index 0000000..18ca8b3
--- /dev/null
+++ b/docs/plans/2026-01-12-carddav-architecture.md
@@ -0,0 +1,173 @@
+# CardDAV Server Architecture
+
+Phase 1: Read-only CardDAV server for Apple Contacts.
+
+## RFCs
+
+| RFC | Name | Purpose |
+|-----|------|---------|
+| [RFC 4918](https://datatracker.ietf.org/doc/html/rfc4918) | WebDAV | HTTP extensions for distributed authoring |
+| [RFC 6352](https://datatracker.ietf.org/doc/html/rfc6352) | CardDAV | vCard extensions to WebDAV |
+| [RFC 6350](https://datatracker.ietf.org/doc/html/rfc6350) | vCard 4.0 | Contact data format |
+| [RFC 3253](https://datatracker.ietf.org/doc/html/rfc3253) | REPORT | Required for addressbook-multiget |
+
+## HTTP Methods
+
+### Phase 1 (read-only)
+
+| Method | Purpose |
+|--------|---------|
+| `OPTIONS` | Advertise capabilities |
+| `PROPFIND` | Discovery + collection listing |
+| `GET` | Retrieve individual vCards |
+| `REPORT` | addressbook-multiget (batch fetch) |
+
+### Deferred
+
+| Method | Purpose |
+|--------|---------|
+| `PUT` | Create/update vCards |
+| `DELETE` | Remove vCards |
+| `MKCOL` | Create address books |
+| `PROPPATCH` | Modify properties |
+| `addressbook-query` | Search/filter contacts |
+
+## URL Structure
+
+```
+/.well-known/carddav        → redirect to /principal/
+/principal/                 → user's principal resource
+/addressbooks/              → addressbook-home-set
+/addressbooks/contacts/     → the address book collection
+/addressbooks/contacts/{id}.vcf → individual vCard
+```
+
+User identity from auth, not URL path.
+
+## Discovery Flow
+
+```
+1. OPTIONS /.well-known/carddav
+   → 301 Redirect to /principal/
+
+2. OPTIONS /principal/
+   → 200 OK
+   → DAV: 1, 3, addressbook
+
+3. PROPFIND /principal/ (Depth: 0)
+   → current-user-principal: /principal/
+   → addressbook-home-set: /addressbooks/
+
+4. PROPFIND /addressbooks/ (Depth: 1)
+   → Lists /addressbooks/contacts/ as type addressbook
+
+5. REPORT /addressbooks/contacts/ (addressbook-multiget)
+   → Returns vCards
+```
+
+### Key Properties
+
+| Property | Location | Value |
+|----------|----------|-------|
+| `current-user-principal` | `/principal/` | `/principal/` |
+| `addressbook-home-set` | `/principal/` | `/addressbooks/` |
+| `resourcetype` | `/addressbooks/contacts/` | collection, addressbook |
+| `displayname` | `/addressbooks/contacts/` | "Contacts" |
+| `getctag` | `/addressbooks/contacts/` | change tag for sync |
+
+## Storage
+
+### Directory Structure
+
+```
+data/
+└── contacts/
+    ├── kqmtnwpxlrvszoyp.kdl
+    ├── nzxwvtslqpomkrny.kdl
+    └── plokmnzxwvtsrqky.kdl
+```
+
+### Contact File Format (KDL)
+
+```kdl
+contact {
+    name "John Smith"
+    phone "+1-555-1234" type="mobile"
+    email "john@example.com"
+}
+```
+
+### Contact IDs
+
+- 16 characters using k-z (16 letters)
+- 64 bits of entropy
+- Filename is the ID: `{id}.kdl`
+- Maps to vCard UID
+
+### Change Detection
+
+- `getctag`: hash of directory listing or newest mtime
+- `getetag`: mtime or content hash of .kdl file
+
+### Deferred
+
+- Per-user addressbook files (`addressbooks/{user}.kdl`)
+- Groups with shared attributes
+- Database migration
+
+## Roda Architecture
+
+### File Structure
+
+```
+lib/
+└── pro_tacts/
+    └── app.rb
+```
+
+Extract classes when needed.
+
+### Routing
+
+```ruby
+module ProTacts
+  class App < Roda
+    plugin :all_verbs
+
+    route do |r|
+      r.on ".well-known/carddav" do
+        r.redirect "/principal/"
+      end
+
+      r.on "principal" do
+        r.is do
+          r.options { dav_options }
+          r.propfind { principal_propfind(r) }
+        end
+      end
+
+      r.on "addressbooks" do
+        r.is do
+          r.propfind { home_propfind(r) }
+        end
+
+        r.on "contacts" do
+          r.is do
+            r.options { dav_options }
+            r.propfind { collection_propfind(r) }
+            r.report { addressbook_multiget(r) }
+          end
+
+          r.on String do |id|
+            r.get { serve_vcard(id) }
+          end
+        end
+      end
+    end
+  end
+end
+```
+
+## Translation
+
+KDL → vCard on read. Server parses KDL contact files and generates vCard 4.0 output.
diff --git a/docs/plans/2026-01-12-carddav-reference.md b/docs/plans/2026-01-12-carddav-reference.md
new file mode 100644
index 0000000..2aee9bf
--- /dev/null
+++ b/docs/plans/2026-01-12-carddav-reference.md
@@ -0,0 +1,295 @@
+# CardDAV Protocol Reference
+
+Examples and formats from the RFCs for implementation reference.
+
+## XML Namespaces
+
+```xml
+xmlns:D="DAV:"
+xmlns:C="urn:ietf:params:xml:ns:carddav"
+```
+
+## OPTIONS
+
+**Request:**
+```http
+OPTIONS /principal/ HTTP/1.1
+Host: carddav.example.com
+```
+
+**Response:**
+```http
+HTTP/1.1 200 OK
+DAV: 1, 3, addressbook
+Allow: OPTIONS, GET, PROPFIND, REPORT
+```
+
+## PROPFIND
+
+### Request Format
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:propfind xmlns:D="DAV:">
+  <D:prop>
+    <D:displayname/>
+    <D:resourcetype/>
+  </D:prop>
+</D:propfind>
+```
+
+### Request with CardDAV Properties
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:propfind xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:prop>
+    <D:current-user-principal/>
+    <C:addressbook-home-set/>
+  </D:prop>
+</D:propfind>
+```
+
+### 207 Multi-Status Response
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+  <D:response>
+    <D:href>/principal/</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>Alice</D:displayname>
+        <D:resourcetype/>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
+</D:multistatus>
+```
+
+### Response with Multiple Properties (some missing)
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:">
+  <D:response>
+    <D:href>/principal/</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:displayname>Alice</D:displayname>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+    <D:propstat>
+      <D:prop>
+        <D:getcontentlength/>
+      </D:prop>
+      <D:status>HTTP/1.1 404 Not Found</D:status>
+    </D:propstat>
+  </D:response>
+</D:multistatus>
+```
+
+## Discovery Properties
+
+### current-user-principal
+
+```xml
+<D:current-user-principal xmlns:D="DAV:">
+  <D:href>/principal/</D:href>
+</D:current-user-principal>
+```
+
+### addressbook-home-set
+
+```xml
+<C:addressbook-home-set xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:href>/addressbooks/</D:href>
+</C:addressbook-home-set>
+```
+
+### resourcetype (for address book collection)
+
+```xml
+<D:resourcetype xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:collection/>
+  <C:addressbook/>
+</D:resourcetype>
+```
+
+### supported-address-data
+
+```xml
+<C:supported-address-data xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <C:address-data-type content-type="text/vcard" version="4.0"/>
+</C:supported-address-data>
+```
+
+## REPORT: addressbook-multiget
+
+### Request
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:prop>
+    <D:getetag/>
+    <C:address-data/>
+  </D:prop>
+  <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+  <D:href>/addressbooks/contacts/nzxwvtslqpomkrny.vcf</D:href>
+</C:addressbook-multiget>
+```
+
+### Request with Specific vCard Properties
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:prop>
+    <D:getetag/>
+    <C:address-data>
+      <C:prop name="VERSION"/>
+      <C:prop name="UID"/>
+      <C:prop name="FN"/>
+      <C:prop name="EMAIL"/>
+    </C:address-data>
+  </D:prop>
+  <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+</C:addressbook-multiget>
+```
+
+### Response
+
+```xml
+<?xml version="1.0" encoding="utf-8" ?>
+<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
+  <D:response>
+    <D:href>/addressbooks/contacts/kqmtnwpxlrvszoyp.vcf</D:href>
+    <D:propstat>
+      <D:prop>
+        <D:getetag>"abc123"</D:getetag>
+        <C:address-data>BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+EMAIL:john@example.com
+END:VCARD
+</C:address-data>
+      </D:prop>
+      <D:status>HTTP/1.1 200 OK</D:status>
+    </D:propstat>
+  </D:response>
+</D:multistatus>
+```
+
+## vCard 4.0 Format
+
+### Minimal vCard
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+END:VCARD
+```
+
+### Full Example
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+N:Smith;John;;;
+TEL;TYPE=mobile:+1-555-1234
+TEL;TYPE=work:+1-555-5678
+EMAIL;TYPE=home:john@example.com
+EMAIL;TYPE=work:jsmith@work.com
+ADR;TYPE=home:;;123 Main St;Springfield;IL;62701;USA
+END:VCARD
+```
+
+### Property Reference
+
+| Property | Required | Format |
+|----------|----------|--------|
+| `BEGIN` | Yes | `BEGIN:VCARD` |
+| `VERSION` | Yes | `VERSION:4.0` |
+| `UID` | Yes | Unique identifier |
+| `FN` | Yes | Formatted name (display name) |
+| `END` | Yes | `END:VCARD` |
+| `N` | No | `family;given;additional;prefix;suffix` |
+| `TEL` | No | Phone with optional TYPE |
+| `EMAIL` | No | Email with optional TYPE |
+| `ADR` | No | `pobox;ext;street;city;region;postal;country` |
+
+### TYPE Values
+
+- TEL: `work`, `home`, `mobile`, `fax`, `pager`
+- EMAIL: `work`, `home`
+- ADR: `work`, `home`
+
+## KDL to vCard Mapping
+
+### KDL Input
+
+```kdl
+contact {
+    name "John Smith"
+    phone "+1-555-1234" type="mobile"
+    phone "+1-555-5678" type="work"
+    email "john@example.com" type="home"
+    address type="home" {
+        street "123 Main St"
+        city "Springfield"
+        state "IL"
+        zip "62701"
+        country "USA"
+    }
+}
+```
+
+### vCard Output
+
+```
+BEGIN:VCARD
+VERSION:4.0
+UID:kqmtnwpxlrvszoyp
+FN:John Smith
+TEL;TYPE=mobile:+1-555-1234
+TEL;TYPE=work:+1-555-5678
+EMAIL;TYPE=home:john@example.com
+ADR;TYPE=home:;;123 Main St;Springfield;IL;62701;USA
+END:VCARD
+```
+
+## HTTP Headers
+
+### Request Headers
+
+| Header | Value | When |
+|--------|-------|------|
+| `Depth` | `0`, `1`, `infinity` | PROPFIND |
+| `Content-Type` | `application/xml; charset=utf-8` | PROPFIND, REPORT |
+
+### Response Headers
+
+| Header | Value | When |
+|--------|-------|------|
+| `DAV` | `1, 3, addressbook` | OPTIONS |
+| `Content-Type` | `application/xml; charset=utf-8` | 207 responses |
+| `Content-Type` | `text/vcard; charset=utf-8` | GET vCard |
+| `ETag` | `"hash"` | GET, in propstat |
+
+## Status Codes
+
+| Code | Meaning |
+|------|---------|
+| 200 | OK |
+| 207 | Multi-Status (contains per-resource status) |
+| 301 | Redirect (for .well-known) |
+| 404 | Not Found |
+| 405 | Method Not Allowed |