contacts D6: CardDAV PUT writes, durable href aliases and ETag preconditions #797
Closed
kayg
wants to merge 12 commits from
feat/carddav-writes into dev
pull from: feat/carddav-writes
merge into: kayg:dev
kayg:main
kayg:fix/shared-row-swipes
kayg:feat/pass8
kayg:t3/paged-search-reconcile-final
kayg:feat/style-sheet-fixes
kayg:storage/wal-checkpoint-off-save-1051
kayg:feat/contrast
kayg:review/calendar-2
kayg:dev
kayg:feat/collections-db
kayg:fix/instant-keys-1056
kayg:feat/money-round
kayg:perf/stalls-1054
kayg:wip/tiering-v3-cold4
kayg:wip/tiering-v3-cold5
kayg:port/rows-actions
kayg:port/rows-drop
kayg:port/hidden-blocks
kayg:port/rows
kayg:port/port-tables
kayg:port/port-tables-2
kayg:t3/mail-1011-liveness
kayg:port/devices
kayg:port/port-attachments
kayg:port/port-attachments-2
kayg:port/port-attachments-3
kayg:docs/design-consistency
kayg:docs/engineering-standards
kayg:fix/rusqlite-0.41-rustsec-2026-0459
kayg:feat/iphone-fixes
kayg:shared/restore-and-stale-918-919
kayg:fix/phone-top-band-contrast
kayg:feat/notes-approve
kayg:perf/soak-max-gate
kayg:feat/files-s1-identity
kayg:fix/lifecycle-event-id
kayg:feat/quickfix
kayg:wip/tiering-v3-cold
kayg:perf/web-mockup-frame-fixes
kayg:port/contacts-merge
kayg:port/contacts-edit
kayg:test/shared-room-no-deleted-text
kayg:perf/first-paint-credits
kayg:fix/shared-history-after-grant
kayg:fix/phone-swipe-regressions
kayg:feat/contrast-r1
kayg:review/sharing-pr3-review
kayg:feat/tiering-v3-s2-tags
kayg:feat/folder-drop
kayg:fix/deskpills-lazy-share
kayg:feat/sharing-s1-pr3
kayg:tmp/money-round-before
kayg:fix/phone-pane-backdrop-roots
kayg:fix/palette-preload-chips
kayg:fix/invite-defaults
kayg:port/tasks-matrix
kayg:fix/restore-green-gates
kayg:fix/sharing-fast-path-hardening
kayg:t3/photos-s1
kayg:feat/tiering-v3-s1-namespace
kayg:notes-archive-hide
kayg:feat/tasks-actions
kayg:t3/photos-plan-slice-one
kayg:feat/whiteboard-s1
kayg:t3/paged-search-reconcile
kayg:t3/smooth-pane-transitions
kayg:t3/index-position-startup-scan
kayg:fix/private-staging-759b-r2
kayg:feat/dav-transport
kayg:feat/imap-notes-fetch
kayg:feat/imap-notes-projection
kayg:feat/imap-notes-readonly
kayg:feat/imap-notes-auth
kayg:feat/imap-notes-server-foundation
kayg:docs/undo-block-ids-plan
kayg:fix/undo-768
kayg:proto/block-identity-v7d
kayg:issue/carddav-read-perf-fix-r2
kayg:issue/carddav-read-r2
kayg:docs/imap-766-plan-v2
kayg:docs/imap-notes-plan
kayg:docs/quota-plan-2
kayg:fix/parallel-test-flakes
kayg:feat/contacts-companion-pure
kayg:docs/calendar-backend-v2-plan
kayg:feat/calendar-ical-converter
kayg:docs/tasks-writes-v2-plan
kayg:docs/carddav-writes-v2-plan
kayg:docs/contacts-import-v2-plan
kayg:issue/610-pm-midnight
kayg:feat/calendar-v2-s1-containment
kayg:docs/dav-recorder-v2-plan
kayg:plan/previews-865
kayg:plan/save-log-durable-entry
kayg:fix/e2e-data-on-ram
kayg:feat/appwindow3
kayg:feat/appwindow2
kayg:feat/appwindow
kayg:feat/money-p1
kayg:feat/keys-sheet
kayg:review/calendar-2-themes
kayg:feat/smooth-cursor
kayg:fix/float-style
kayg:review/calendar-2-float
kayg:release/2026-11-06-hotfix
kayg:archive/port/rows-drop-code
kayg:archive/port/rows-actions
kayg:archive/port/rows
kayg:scratch/tasks-894-ui-gates
kayg:review/calendar-2-night-wip
kayg:release/2026-11-05
kayg:archive/feat/tiering-v3-s1-namespace
kayg:archive/flaky/rows-drop-fixed
kayg:archive/flaky/rows-actions
kayg:archive/feat/tiering-v3-reader
kayg:archive/feat/tiering-v3-roots
kayg:archive/flaky/rows
kayg:sim/bm15-final
kayg:sim/bm15-fix
kayg:sim/bm15-bis-932
kayg:sim/bm15-bis-909
kayg:sim/bm15
kayg:sim/bm15-bis-887
kayg:perf/tiering-option-b
kayg:port/style-3
kayg:port/style-2
kayg:port/style
kayg:fix/sw-lazy-precache
kayg:fix/vim-w-waits-for-save
kayg:fix/signout-test-races
kayg:fp-scratch/all
kayg:sim/bm15-first-paint
kayg:archive/bm15/rows-drop
kayg:archive/bm15/rows-actions
kayg:fix/palette-snippet-contrast
kayg:archive/bm15/rows
kayg:archive/bm15/hidden
kayg:archive/plan/save-log-durable-entry
kayg:mock/money-fix2
kayg:archive/fix/e2e-data-on-ram
kayg:mock/appearance-all
kayg:mock/pill-press
kayg:archive/docs/design-consistency
kayg:mock/auto-swipes
kayg:mock/mail-actions
kayg:archive/feat/mail-p1
kayg:mock/money-static
kayg:archive/review/standards-917
kayg:archive/docs/engineering-standards
kayg:perf/tiering-761-fix
kayg:mock/type-audit
kayg:mock/third-pane
kayg:feat/contacts-import-v2-s1
kayg:perf/executor-hot-path
kayg:archive/port/hidden-blocks
kayg:archive/port/devices
kayg:scratch/executor-on-894
kayg:scratch/executor-spans2
kayg:scratch/executor-spans
kayg:train/2026-11-03-bm14c
kayg:archive/port/rows-drop
kayg:feat/tiering-761-d-scale
kayg:feat/tiering-761-c-index
kayg:feat/tiering-761-b-reader
kayg:archive/review/sharing-pr3-review-9fec3d1f
kayg:train/2026-11-03-bm14b
kayg:perf/ports-lazy-budgets
kayg:feat/tiering-761-a-migration
kayg:train/2026-11-03-bm14
kayg:train/2026-11-03-bm12
kayg:archive/review/sharing-pr3-review
kayg:archive/feat/sharing-s1-pr3
kayg:train/2026-11-03-bm11
kayg:archive/feat/imap-notes-fetch
kayg:train/2026-11-03-bm10
kayg:archive/review/sharing-pr3-review-code
kayg:archive/plan/previews-865
kayg:train/2026-11-02-bm9
kayg:archive/port/port-attachments-3
kayg:archive/port/contacts-merge
kayg:fix/motion-audit-859
kayg:train/2026-11-02-bm8
kayg:archive/scratch/bm8-prep
kayg:archive/port/port-pickers
kayg:archive/port/port-tables-2
kayg:archive/port/port-tables
kayg:archive/feat/sharing-s1-pr3-code
kayg:archive/scratch/bm7-fixed
kayg:train/2026-11-02-bm7
kayg:archive/fix/parallel-test-flakes-code-minipc01
kayg:train/2026-11-02-bm6
kayg:docs/imap-766-readonly-slice
kayg:feat/dav-recorder-v3
kayg:research/788-diagnosis
kayg:archive/fix/parallel-test-flakes
kayg:archive/perf/pr792-cold-index-edit
kayg:docs/sharing-slice-1
kayg:archive/docs/imap-766-plan-v2
kayg:feat/dav-recorder-v2
kayg:archive/docs/dav-recorder-v2-plan
kayg:spike/imap-766-s0-cases
kayg:archive/fix/undo-768
kayg:feat/dav-recorder
kayg:archive/docs/contacts-import-v2-plan
kayg:archive/feat/calendar-ical-converter
kayg:feat/contacts-import
kayg:perf/cold-growth-on-764
kayg:perf/cold-growth-on-tiering
kayg:archive/docs/tasks-writes-v2-plan
kayg:archive/docs/calendar-backend-v2-plan
kayg:archive/feat/contacts-companion-pure
kayg:feat/tasks-set-status
kayg:feat/tasks-move-reorder
kayg:archive/docs/carddav-writes-v2-plan
kayg:archive/feat/calendar-v2-s1-containment
kayg:feat/tasks-delete-promote-duplicate
kayg:archive/feat/dav-transport
kayg:archive/fix/carddav-rebuild-r1
kayg:archive/fix/carddav-retention-r1
kayg:perf/quota-on-tiering
kayg:perf/quota-simple
kayg:perf/cold-growth-on-quota-stack
kayg:feat/calendar-identity
kayg:feat/calendar-event-parser
kayg:feat/calendar-props-maps
kayg:port/glass-when-ring
kayg:feat/tiering-761-s1
kayg:wip/10by32-01-impl-759b-scratch-20261030
kayg:wip/6by32-01-bm3-webkit-pause-20261030
kayg:wip/10by32-01-bm3-chromium-pause-20261030
kayg:wip/6by32-01-tier-761-s1-pause-20261030
kayg:spike/imap-766-s0
kayg:wip/6by32-02-calternal-pause-20261030
kayg:wip/8by64-01-review-688-pause-20261030
kayg:wip/6by32-01-verify-744-pause-20261030
kayg:wip/6by32-01-verify-742-pause-20261030
kayg:fix/private-staging-759b
kayg:wip/6by32-01-review-748-pause-20261030
kayg:wip/6by32-01-review-733c-pause-20261030
kayg:wip/6by32-01-review-733b-pause-20261030
kayg:wip/6by32-01-perf-idle-8-pause-20261030
kayg:wip/6by32-01-perf767-base-pause-20261030
kayg:wip/6by32-01-perf767b-767-pause-20261030
kayg:wip/6by32-01-perf767b-764-pause-20261030
kayg:wip/6by32-01-perf767-767-pause-20261030
kayg:wip/6by32-01-perf767-764-pause-20261030
kayg:wip/6by32-01-lists-b-pause-20261030
kayg:wip/6by32-01-ids-verify5-pause-20261030
kayg:wip/6by32-01-ids-verify4-pause-20261030
kayg:review/calendar-month
kayg:wip/10by32-02-undo-768-20261030
kayg:test/ids-verify3
kayg:wip/10by32-02-bm3-chromium-20261030
kayg:wip/10by32-01-review-753-20261030
kayg:wip/10by32-01-perf-idle-2-20261030
kayg:wip/10by32-01-bm2-webkit2-20261030
kayg:plan/759b-staging
kayg:train/2026-10-30-bm2
kayg:review/758b-tests
kayg:review/758-tests
kayg:review/764d-tests
kayg:review/767b-tests
kayg:review/preview-2026-10-29
kayg:review/767-tests
kayg:docs/tiering-731-plan
kayg:wip/c6b02-when2
kayg:wip/c6b02-tasks-create-fix5
kayg:wip/c6b02-settings-frost-mock
kayg:wip/c6b02-settings-frost-base
kayg:wip/c6b02-settings-frost
kayg:wip/c6b02-ring
kayg:wip/c6b02-review-733d
kayg:wip/c6b02-bm1-full-a
kayg:wip/compute02-when-field
kayg:wip/compute02-webkit-lists-repro
kayg:wip/compute02-train-bl2-dev
kayg:wip/compute02-dev-green-pw164
kayg:docs/deploy-plan
kayg:review/764c-tests
kayg:review/764b-tests
kayg:review/748g-tests
kayg:review/764-tests
kayg:review/759b-tests
kayg:fix/storage-owner-liveness-642
kayg:fix/storage-fix-7
kayg:wip/compute02-storage-fix-7
kayg:review/759-tests
kayg:feat/tiering-731
kayg:wip/tiering-731-first-attempt
kayg:wip/compute02-storage-fix-1
kayg:review/754g-tests
kayg:perf/idle-honest-2
kayg:review/754f-tests
kayg:review/754e-tests
kayg:review/755-tests
kayg:review/754d-tests
kayg:review/754c-tests
kayg:review/754b-tests
kayg:review/754-tests
kayg:fix/storage-private-tmp-642-dev
kayg:wip/compute02-verify-750
kayg:test/ids-verify5
kayg:test/ids-verify4
kayg:fix/storage-exact-file-quarantine-642
kayg:wip/compute02-verify-740
kayg:perf/one-flush-save
kayg:review/calendar-static
kayg:wip/compute02-verify-737
kayg:review/indexing-ring
kayg:review/when-field-2
kayg:wip/compute02-review-733
kayg:proto/block-identity-2-review
kayg:proto/block-identity-review
kayg:wip/c6b02-tc-design
kayg:review/when-field
kayg:issue/tasks-create-ids
kayg:issue/tasks-create
kayg:review/combined
kayg:wip/compute02-review-combined
kayg:review/task-polish
kayg:review/top-buttons
kayg:review/q1-q5
kayg:train/2026-10-26-bl2
kayg:archive/wt-398h
kayg:archive/train-bm-proptest-seed
kayg:archive/integ325-293
kayg:archive/integ325-ci
kayg:archive/client-auth-review-baseline
kayg:archive/wt-sim-4853
kayg:archive/wt-sim-4811
kayg:archive/wt-448m
kayg:archive/wt-398a-allocator
kayg:issue/calendar-adr
kayg:archive/old/train/2026-10-23bk2
kayg:archive/old/train/2026-10-23bk
kayg:issue/469-mockup-9a
kayg:issue/smart-tasks-mockup
kayg:issue/469-menus
kayg:issue/652-signout-flake
kayg:issue/652-history-fix7
kayg:issue/652-history-fix6
kayg:archive/old/issue/spring-check-perf
kayg:issue/spring-check-perf
kayg:issue/659-rename-fast
kayg:archive/old/train/2026-10-21ar
kayg:issue/search-hardening-3
kayg:issue/652-history-fix5
kayg:issue/660-port-sheets-glass
kayg:issue/652-history-fix4
kayg:issue/652-history-fix3
kayg:issue/652-history-fix2
kayg:issue/652-history-flake
kayg:train/2026-10-21an
kayg:issue/settings-admin-port
kayg:issue/search-port
kayg:train/2026-10-20am
kayg:issue/storage-split-fix2
kayg:issue/storage-split
kayg:archive/old/issue/folder-ids-fix
kayg:train/2026-10-20al
kayg:archive/old/issue/folder-ids
kayg:train/2026-10-20ak
kayg:issue/tables-mockup
kayg:train/2026-10-19aj
kayg:train/2026-10-19ai
kayg:issue/469-merge-fixes
kayg:issue/sidebar-band
kayg:archive/old/issue/608-settings-spec
kayg:train/2026-10-19ah
kayg:issue/swipe-offscreen
kayg:train/2026-10-19ag
kayg:archive/old/issue/590-nlp-adr
kayg:archive/old/issue/587-event-after-write-2
kayg:train/2026-10-19af
kayg:archive/old/issue/587-event-after-write
kayg:issue/469-pill-room-c
kayg:issue/469-pill-room
kayg:train/2026-10-19ae
kayg:issue/469-narrow-handle
kayg:issue/397-ios-swipe-2
kayg:archive/old/issue/adrs-photos-files-tasks
kayg:issue/469-desk-fixes
kayg:archive/old/issue/auto-update-adr
kayg:issue/469-filter-gap
kayg:issue/469-port-1b
kayg:issue/442-opacity-flake
kayg:archive/397-ios-swipe-2
kayg:issue/327-web-fast-3
kayg:archive/old/issue/469-port-1
kayg:issue/469-port-1
kayg:issue/327-web-fast-2
kayg:archive/old/issue/327-web-fast
kayg:archive/old/issue/425-stt-bench-vq1
kayg:archive/old/issue/445-no-bleed
kayg:archive/old/issue/400-swipe-mockup-2
kayg:issue/426-client-review
kayg:issue/426-client-2
kayg:issue/426-client
kayg:archive/old/issue/476-safe-area-lab
kayg:archive/445-top-light-2
kayg:archive/old/issue/445-top-light
kayg:issue/471-blocks-mockup
kayg:archive/old/issue/448-links-app-2
kayg:archive/old/issue/448-links-app
kayg:archive/old/issue/398-cold-links
kayg:issue/434-442-small-2
kayg:issue/434-442-small
kayg:archive/old/issue/398-unload
kayg:archive/old/issue/445-bottom-band
kayg:issue/445-bottom-band
kayg:issue/351-rem-3
kayg:archive/312-views-mockup
kayg:issue/351-rem-2
kayg:issue/351-rem
kayg:archive/old/issue/327-first-note
kayg:issue/395-width-phone
kayg:archive/old/issue/327-typing
kayg:issue/396-ios-edges
kayg:issue/397-ios-swipe
kayg:archive/398-allocator
kayg:archive/old/issue/327-notes-index
kayg:issue/345-347-contrast-empty-2
kayg:archive/old/issue/44-arenas
kayg:issue/44-arenas
kayg:archive/integ325
kayg:archive/old/issue/327-perf-harness
kayg:archive/old/issue/44-thp
kayg:issue/44-thp
kayg:archive/old/issue/285-sticky-pills
kayg:archive/old/issue/44-trim-3
kayg:issue/44-trim-3
kayg:archive/old/issue/165-cards
kayg:archive/old/issue/165-picker-2
kayg:archive/old/issue/212-multi-select
No reviewers
Labels
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set
Reference
kayg/calternal!797
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/carddav-writes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
D6 CardDAV PUT writes
PUT creates and updates through the executor and contacts actions. canonical URLs and client aliases share a durable per-user href namespace. deleted contacts retain route tombstones: headerless replays are refused, If-Match on an absent resource returns 412, and explicit If-None-Match:* creation uses a fresh contact id. restoring the previous contact preserves a separate route if its former URL was reused.
ETag validation uses the coherent note/companion snapshot used for merging. the final room mutation compares that note version and returns 412 on any intervening ordinary note edit. conditional rename jobs check the version before changing paths or links. grouped labels belonging to retained unmodeled fields survive Apple omissions; modeled label deletions still work.
stack: #794 at
26edc6476b4b25e1a337190021f2f8dde77ca61a; head8bcb2a1e08b9fe0496812e5044af2c2d0d66325e; target dev; never merge. review round 1 fixes are implemented and ready for independent re-review (maximum 3 rounds).costs and failure handling
PUT card cap 256 KiB, transport cap 1 MiB, href cap 255 bytes. href operations are indexed O(log routes). migration visits historical ids; startup backfills handwritten contacts with an O(notes) scan. each live cached contact adds a small derived owner/route row. existing UID inventory scans and converter occurrence matching retain the previously documented scaling limits; no latency claim is added.
verification
all requested package/history tests, action contract, clippy
-D warningsand fmt passed on dev-02 through slot/Nix; builds use-j4, package tests use--test-threads=1. the final control and VM sources match across 423 Rust/Cargo/contract files. complete commands, red/green evidence, test status table, timing-test observations and inherited delivery risks are recorded in the round 1 implementation comment.native Apple capture/replay, DELETE, Lists/groups, perf-VM gates and protected recovery snapshots remain open inherited work. RFC 9110 strong/weak conditions and transformed-PUT ETag omission remain unchanged; headerless native requests remain an implementation assumption, not captured Apple evidence. scope does not include any PR merge.
WIP: contacts D6 CardDAV PUT writes and href aliasesto contacts D6: CardDAV PUT writes, durable href aliases and ETag preconditionsIndependent review, round 1
verdict: fix first. reviewed head
fd829d36f11815178ca7104d598b1feca301f0f6, D6 diff against #794 at9e95ad41. read the plan/failure table, ADRs 0063/0064/0068 and contacts decision memory. no P0 found.findings
P1: retired canonical hrefs can resurrect locally created/imported contacts.
crates/contacts-actions/src/write.rs:242–266,crates/contacts-actions/src/dav.rs:34–45. Create a contact through the ordinary contacts action, delete it, then replay a headerless PUT to its former<id>.vcf. There is no alias row for this route; create computes a different, href-derived id and checks Trash/departure against that new id. The deleted original therefore does not block creation. My regression expected refusal and got 201 Created with a new live contact id. Restoring the original can then produce two contacts claiming the same URL. Fix: durably reserve canonical routes as well as client aliases, including migration/tombstones for existing local/imported contacts; resolve retirement against the original route owner. Extend the existing Trash/restore/forget test to local/imported contacts, headerless and create-only PUTs.P1: alias uniqueness excludes canonical resource names.
crates/contacts-actions/src/write.rs:288–289,crates/contacts-actions/src/dav.rs:25–31,crates/notes/src/history/aliases.rs:9–35. With a local contact A,contacts.create {card: <different UID>, href: "<A-id>.vcf"}creates B successfully because A's implicit canonical route is absent from the alias table. Both now project to the same href. Cache upserts can replace A's wire representation with B; a later event for either contact can replace/delete the other's projection. This is reachable through the newly exposed action input, even though the normal HTTP PUT path checks the current cache first. My regression confirmed that the collision is accepted and both ids remain live. Fix: enforce one durable namespace for canonical and explicit routes under the contact writer lock, across every creation path; refuse collisions before committing a contact. Test action callers, cache rebuild and subsequent updates/deletes of both ids. This is within one user's Home; I found no cross-user route lookup.P1: the ETag recheck is detached from the snapshot being committed.
crates/contacts-actions/src/write.rs:351–375. The expected ETag is checked by oneread(), then the action awaits UID/history work and obtains a second note snapshot and companion for its merge. Ordinary note edits use the note room (crates/notes/src/lib.rs:542–564), notperson.writing. A note edit after the ETag read but before the second snapshot can therefore change a modeled field; the PUT accepts the newer snapshot as its patch base and can apply the phone-wins merge despite its now-stale If-Match. The existing test edits before entering the action and cannot exercise this window. Fix: evaluate the ETag from the exact coherent note/companion snapshot used for the merge, and bind validation to the final mutation's version check; return 412 if it changes. Add a deterministic interleaving test. This finding is from code tracing, not the three executed regressions.P2: omitted labels on unmodeled grouped fields are lost.
crates/contacts/src/merge.rs:271–275. Start withitem1.X-UNMODELED:opaqueanditem1.X-ABLabel:Private field; let Apple omit both while changing TEL. The new preservation guard retains the unknown value but explicitly excludes X-ABLabel, so the saved/exported original loses its label. My converter regression failed with exactly that output. Fix: preserve the label and associated metadata of retained unmodeled occurrences; permit intentional label deletion for modeled fields. Extend the omission regression to grouped unknown properties and labels.failure-table audit
security, protocol and Apple evidence
Credential-derived user selection and per-Home alias storage look correctly scoped. Cross-site checks run before buffering; absent browser headers remain accepted. Strong If-Match / weak If-None-Match comparison and omission of ETag after transformed PUT are appropriate (RFC 9110 §9.3.4, RFC 6352 §6.3.2).
Contacts.app Origin/Sec-Fetch behavior is not verified by this PR or this review. Fetch Metadata specifies browser Fetch context; it is not evidence that every Contacts.app/CFNetwork version omits these headers. The cited macOS duplicate report also does not establish that header claim. Headerless native compatibility is a reasonable implementation assumption, not a measured Apple result. Retain the CSRF guard, but supply a redacted real macOS Contacts PUT capture/replay with the complete request headers before claiming native compatibility. ADR 0064 explicitly makes recorded macOS writes a D6 gate; that gate remains open. DELETE is also absent and remains an explicit D6-scope gap against that ADR.
review verification
On
cua@calternal-dev-02-std, existing headfd829d36, Nix shell through~/bin/slot:Temporary regression harness reused the existing write-test setup. 0 passed, 3 failed:
review_r1_local_retired_href,review_r1_alias_canonical_collision,review_r1_unknown_group_label. These are deliberate assertions of the required safe behavior. Harness removed afterward; no production source edited, no code pushed. VM's existing Nix overlay files were left alone. Earlier broad gates are author-reported; I did not rerun them.round 1 implementation plan: re-merge #794, then replace implicit canonical URLs with reservations in the same durable per-user href namespace as aliases. backfill historical canonical ids (including departed ids); keep reservations after deletion. explicit create-only PUTs may create a fresh id at a retired route; headerless replays stay refused and If-Match stays 412. reserve routes before every contact create/import.
bind ETag validation to one note/companion snapshot and use the room version as a final compare-and-swap, including the first edit of rename jobs. a changed version fails before any rename/link mutation. retain grouped labels for omitted unmodeled fields.
cost: indexed href operations O(log routes), one-time historical backfill O(history ids); the existing UID scan and converter bounds remain. failure → effect → detection → recovery → test: retired replay → no write → durable route owner → explicit create-only/new id or restore → local/import/DAV deletion tests; namespace collision → no contact → unique(owner,href) → choose another URL → action/rebuild tests; note edit during PUT → no PUT mutation → room CAS → 412/re-GET → deterministic pause test. gates: package tests, notes regressions, contract, clippy -D warnings and fmt on dev-02 through slot/Nix. never merge.
round 1 fixes, ready for re-review
head
8bcb2a1e08b9fe0496812e5044af2c2d0d66325e; re-merged #794 at26edc6476b4b25e1a337190021f2f8dde77ca61a. this is the implementation response to review round 1, of at most 3. no new independent review was started; never merged.review_r1_local_retired_href: local/imported, normal/missing legacy reservations, headerless refusal, stale If-Match 412, explicit create-only fresh ids, Trash/restore/forgetreview_r1_alias_canonical_collision: caller refused before a second contact, reopen/rebuild, update and delete; history migration/namespace testsreview_r1_etag_and_commit_share_the_note_version: deterministic pauses after validation and before commit, same-path and rename; 412, local edit/card/path preservedreview_r1_unknown_group_labelandreview_r1_modeled_group_label_can_still_be_deletedreview_r1_simultaneous_create_only_actions_recheck_absence: one create and one BlockChanged/412review_r1_delayed_retired_events_do_not_remove_reused_href: remove old cached route by note owner, retain the new owner, move cleanup-j4buildscreate_only, checked again-D warningscargo fmt --all -- --checkall heavy commands ran on
cua@calternal-dev-02-std, worktree/home/cua/Developer/Worktrees/carddav-writes, with~/bin/slot nix develop -c bash -c .... final log~/carddav797-r1-serial.logendsGATES_EXIT=0. SHA-256 matched all 423 Rust/Cargo/contract files between the control checkout and VM. no control-host builds, tests, formatting or installs.red/green evidence: the two href regressions and grouped-label regression failed on the original implementation; the deterministic action race accepted the stale write and replaced the local phone edit before the fix. the delayed-projection regression also failed before the cache owner fix. logs:
~/carddav797-r1-red.log,~/carddav797-r1-cas-red.log,~/carddav797-r1-cache-red.log; green results are in the final package/history run.decisions and remaining risks
resource_aliases, unique by(owner, href)in each user's history. creates/imports reserve before publishing; migration backfills canonical historical/departed ids. fallback lookup also checks Trash/departure records for missing legacy reservations. reservations survive forgetting.If-None-Match: *may reuse a retired URL with a fresh contact id. if the old contact is later restored, it uses a distinct durable route, preserving both contacts. pending reservations still resume the reserved new id.ComparePatchchecks that snapshot's note version inside the room before mutation; conditional rename jobs perform this edit before any rename/link step. existing unconditional patch/merge behavior remains.reads > 400count. the complete final suite passed with--test-threads=1; neither threshold nor body-validation assertion was weakened. the ticker failure message now includes its count.Independent review, round 2
verdict: fix first. reviewed
8bcb2a1e08b9fe0496812e5044af2c2d0d66325e, against round 1 (#issuecomment-71132) and the fix report (#issuecomment-71231). no P0 found. no code pushed.round-1 status
review_r1_local_retired_hrefcovers refusal, 412, explicit fresh-id creation, restore and forget, but my run failed at its initial GET's missing ETag (write.rs:605), before completing its assertions. do not count that run as green evidence.review_r1_alias_canonical_collisionpassed: rejected action, one live id, restart, update, delete. the history namespace test also passed. these prove prevention, not repair of already-colliding v13 rows.review_r1_etag_and_commit_share_the_note_versionpassed. its deterministic pauses after validation and before commit cover both same-path and rename writes, asserting BlockChanged, retained local edit, unchanged card and path. this exercises the actual old race, unlike a pre-action stale-ETag test.new findings
P1: migration can preserve a legacy collision and disable contacts recovery for the whole Home.
crates/notes/src/history.rs:1179,crates/notes/src/history.rs:1182; recovery consequence atcrates/contacts-actions/src/write.rs:190–196.Scenario: a v13 Home contains local contact A and contact B whose client alias is
<A-id>.vcf, a state the original implementation accepted. migration copies B's alias first, thenINSERT OR IGNOREsilently drops A's canonical reservation. A has no route. startup recovery attempts to reserve A's canonical URL, encounters B, and propagates IdempotencyConflict;Person.recoveredcannot initialize, so contact operations/rebuild fail for that Home. I replayed the actual v14 migration SQL on dev-02: witha.vcf -> b, the resulting rows were onlya.vcf -> bandb.vcf -> b; A had no route. the recovery failure is established by source tracing, not an executed integration regression.Fix: explicitly reconcile conflicting existing aliases and canonical names during migration, retain every live/retired identity on a distinct durable route, and force the necessary cache resync. do not silently discard a required reservation. add a real v13 collision fixture that opens the upgraded contacts service and verifies both contacts, subsequent edits/deletes and rebuild.
P2: restoring the old contact after same-UID URL reuse creates duplicate live CardDAV UIDs.
crates/contacts-actions/src/write.rs:272–280,crates/contacts-actions/src/write.rs:689–693.Scenario: delete A with UID U; PUT
If-None-Match: *with UID U at its retired href. the new policy permits fresh contact B because A is not live. restore A: restore checks no UID inventory, so A and B become live at distinct hrefs with the same UID. distinct href allocation fixes route ownership but leaves ambiguous CardDAV identity. the restore regression uses different UIDs (local/other), so it misses this case. finding from source tracing.Fix: enforce the live UID invariant during restore under the same contact writer lock, returning an actionable conflict or explicitly reconciling identity without discarding either card. test reuse with the original UID, restore, restart and cache rebuild.
P2: permanent route/tombstone growth bypasses quota accounting with separate data/state roots.
crates/notes/src/history/aliases.rs:39–60,crates/contacts-actions/src/write.rs:135–140,crates/contacts-actions/src/write.rs:328–337.Scenario: repeatedly create, delete and forget contacts, reusing retired URLs with create-only PUTs. canonical reservations and replacement restore routes remain permanently in the state history DB. admission charges note/card growth; the separate-state provider reports only retained companion bytes, not route rows. after forgetting cards, billed usage can fall while permanent route metadata continues growing without a count/budget bound. no expiry preserves replay protection, but does not bound retained storage. finding from source tracing; no quota-load test run.
Fix: account for retained route metadata or impose an enforced per-Home metadata budget before reservation. preserve the retirement protection when that budget is exhausted; do not introduce a TTL that silently re-enables headerless resurrection. test repeated create/reuse/forget cycles with separate roots and a small quota.
concurrency and verification
review_r1_simultaneous_create_only_actions_recheck_absencepassed: one creation, one BlockChanged, one live id. the action's absence check is inside the per-user writer lock, and the route transaction has a unique(owner,href)key. this supports two-device create-only uniqueness; it is an action-level test, not two independent network connections. ordinary restore without URL reuse retains its reservation; URL reuse allocates a separate restore route, subject to the UID gap above.Executed on dev-02, isolated worktree at the reviewed SHA, Nix through
~/bin/slot, shared existing target directory:Results: 7 tests passed; retired-href regression failed during setup as described above. log:
~/review797-r2.log. two additional integration probes were prepared but their corrected run remained queued behind occupied VM slots; I cancelled my queued job and removed the temporary harness. they are not claimed as executed evidence. migration SQL replay ran separately on the VM. no control-host builds/tests, no production access, no merge. inherited Apple capture/replay and DELETE delivery gaps remain as recorded in round 1.round 2 implementation plan (P1 migration only): merged origin/feat/dav-transport at
80ec1fa3, retaining route ownership in bounded rebuilds. reconcile legacy routes transactionally: canonical owners win; competing aliases use latest committed companion sequence, then note id. each displaced alias receives a fresh durable href and a resource_route_migrations report row. keep all historical/departed identities reserved and rotate disposable cache generation. reopen is a no-op. cost O(routes log routes + history), SQLite temporary tables plus streaming collision repair.requested package/history/contract/clippy/fmt gates run on dev-02 through slot/Nix. the separate P2 UID-restore and quota findings remain follow-up scope. never merge.
round 2 P1 fixed, ready for FINAL review
head
0d55d2635d244ea87d709b700e57ebf1618802d7; re-mergedorigin/feat/dav-transportat80ec1fa3first (merge764c8daa). responds to round 2. no merge, and no additional review launched.resource_route_migrationsrow (owner, note, old/new href, version). no identity is silently discardedreview_r2_repairs_v13_and_already_upgraded_v14_collisions, including retired identities and unchanged routes/report count after reopenreview_r2_legacy_collisions_recover_and_rebuild: six real contacts; recovery, distinct advertised hrefs, GETs, four remap reports, restart, fresh cache rebuild, subsequent edits/deletes of every contact, unaffected contact preservedalias_collision_keeps_latest_committed_contact_in_either_input_order-j4; includes the previously interruptedreview_r1_local_retired_hrefregressionhistory::tests, including both migration generationscontract_matches_the_server-D warningscargo fmt --all -- --checkall gates ran on
cua@calternal-dev-02-std,/home/cua/Developer/Worktrees/carddav-writes-r2, through~/bin/slot nix develop -c bash ..., using the existing target directory. log~/carddav797-r2-gates.logendsGATES_EXIT=0. SHA-256 matched all 451 Rust/Cargo/contract files with the pushed control checkout. no control-host builds, tests, formatting or installs.red/green: the legacy integration fixture failed on the merged pre-fix migration with
UNIQUE constraint failed: resource_aliases.owner, resource_aliases.hrefand passed after repair. a separate replay of the original migration SQL reproduced the strict-v13 canonical-owner loss (a.vcf -> b). the integration fixture deliberately relaxes legacy href uniqueness to additionally exercise duplicate alias imports; the history fixtures cover collisions compatible with normal v13 uniqueness. all red and green output is in the gate log except the separate SQL probe.migration repair is transactional and uses SQLite temporary tables, indexed lookups and streamed loser rows: O(history + routes log routes), without loading card payloads. v15 also repairs Homes already upgraded by the original v14 implementation. reopening v15 does not rerun repair. transport's bounded disk rebuild now retains per-note route ownership and uses the advertised href in each batch.
scope/risks: clients perform a full resync and may see delete+add for remapped contacts, as authorized. the separate round-2 P2 same-UID restore and retained-metadata quota findings remain open follow-up scope. native Apple capture/replay, DELETE and other inherited delivery gaps remain unchanged. no new process-kill migration test or native Apple compatibility claim is made.
Independent review, round 3 (final)
verdict: stop and simplify. reviewed
0d55d2635d244ea87d709b700e57ebf1618802d7, stacked on #794 at80ec1fa3, against reviews #issuecomment-71132 / #issuecomment-71264 and fix report #issuecomment-71309. no P0 found. the ordinary legacy collision fixtures pass, but another valid collision shape still blocks the Home. this reaches kayg's final-round stopping rule. no code pushed.review_r2_legacy_collisions_recover_and_rebuildpassed independently; six contacts retained, distinct advertised hrefs, GET/edit/delete and fresh rebuildContacts::restorestill calls notes restore without a live UID check. fresh B may reuse trashed A's UID, then restoring A publishes two live resources with that UID. guard restore under the writer lock, preserving both cards on conflict; cover restart/rebuildnew findings, P0–P3
P1: the repair still omits live handwritten canonical owners, so an accepted legacy alias can disable all contact recovery.
Locations:
crates/notes/src/history/route_migration.rs:24–27;crates/contacts-actions/src/write.rs:189–196.Create handwritten contact note A (
format: calternal-contact) through notes or an outside edit, without an original-card companion/history row. In a v13 Home, contact B can have explicit alias<A-id>.vcf; this is the same legacy implicit-canonical collision class the original implementation accepted. A need not have any alias, companion, or departure row.The repair inventories only existing alias targets, contacts companions, and departures. It therefore never adds A as a canonical candidate, keeps
A.vcf -> B, and emits no displacement report for it. During recovery, A is discovered by the live-note scan;alias_for(A)is empty, soreserve_alias(A.vcf,A)returns IdempotencyConflict. That error still escapes the per-person recovery initializer, blocking contact operations and CardDAV rebuilding for the Home. v15 will not retry reconciliation on subsequent opens.Evidence: replayed the exact candidate/ranking SQL extracted from this head on dev-02, with a companion for B and legacy
a.vcf -> b. Result: onlya.vcf -> bandb.vcf -> b; A had zero routes. The recovery-equivalent insert/recheck still returned owner B. The service-wide consequence is established by source tracing, not an executed integration regression. The temporary handwritten-contact and same-UID integration probes remained queued behind occupied VM slots; I cancelled my queued job and removed their harness, so neither is claimed as run.No other new P0/P1/P2/P3 finding is asserted.
concrete simpler design
Stop adding schema-only repairs that infer a complete contact inventory from companion history. Use one authoritative advertised route per contact, retaining retired route ownership for replay protection. Resolve requests from that table; remove implicit canonical-target inference and canonical reservations for unadvertised secondary names.
Perform one reconciliation after a settled Home inventory includes live handwritten contacts, Trash, historical/departed identities and pending reservations. Preserve existing unambiguous ownership; assign each unrepresented or displaced identity its canonical href if free, otherwise a fresh href, rather than failing the whole Home. Resolve actual duplicate legacy hrefs deterministically. Commit assignments, remap reports and a route generation together; rebuild the disposable cache when that generation changes. Reopen then only reads the persisted assignments. Keep this operation metadata-only: no card rewrite or identity merge.
Before reviving the branch, require fixtures covering a handwritten owner with no companion, the existing collision matrix, URL reuse/restore, old-token rejection, exact retained card bytes and reopen stability. This is a proposed replacement design, not another request to patch and start round 4.
independent verification
dev-02, isolated checkout
/home/cua/Developer/Worktrees/review797-r3at the reviewed SHA; Nix through~/bin/slot, shared existing target directory:1 integration + 24 history tests passed, log
~/review797-r3-gates.log,EXIT=0. Exact migration SQL replay ran separately on the VM. Broader package/clippy/fmt gates remain author-reported, not independently rerun here. No control-host builds/tests, production access, code push or merge.#797 is on hold after the round-3 “stop and simplify” verdict. No fourth repair round is planned here.
Replacement plan: #807, #807
Plan: https://git.kayg.org/kayg/calternal/src/branch/docs/carddav-writes-v2-plan/docs/plans/0064-carddav-writes-v2.md
The plan chooses one authoritative route table, stable client hrefs and deterministic reconciliation from the settled Home on every startup/recovery. It covers handwritten contacts without history, collision reports, retired ownership, crash tests, metadata accounting, salvage and implementation slices. RFC 6352 and HTTP Location research does not establish Apple support for immediate href replacement after PUT, so that behavior is not assumed. This is docs only; no implementation or merge.
kayg referenced this pull request2026-11-05 23:21:11 +00:00
housekeeping #1009: needs-decision.
Explicit three-round hold. CardDAV writes v2 plan #807 replaces the route/executor direction; retain href/ETag/precondition regression coverage.
original head:
0d55d2635d244ea87d709b700e57ebf1618802d7. against devcd7f84432febfc63b788fbb9b9f9950452982d74, git cherry reports 22 unmatched and 0 equivalent patches. the landing audit lists each candidate missing commit and stable patch match inpr-797.md; unmatched patches can include superseded behavior.left open, source branch unchanged. no rebase or new test claim for this decision lane.
Superseded by #807 (CardDAV writes v2). The branch and original tips are archived under
archive/(see #1009), so nothing is lost; useful regression vectors are noted there for the active lanes. Closing.Pull request closed