Conditional Item Updates with Quoted ETag Preflight #11
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Summary
Implement conditional update routes (
PATCH /v1/tasks/{id}/life task updateandPATCH /v1/events/{id}/life event update), enforcing mandatory caller-suppliedIf-Matchquoted ETags (rejecting missing/weak ETags before network dispatch), immutable item identity, preservation-safe selective iCalendar patching, optimistic concurrency conflict handling (etag_conflict), and two-phase commit journaling.Background & Technical Rationale
Mandatory Caller ETag Preflight (Scenario R03):
A common design flaw in calendar adapters is performing a "silent read-modify-write" shortcut: if a caller omits a version token, the service silently fetches the current server ETag, merges the update, and writes it back. This creates silent data loss and overwrites concurrent external modifications.
If-Match: "\"v1\""or--etag '"v1"').W/), the request MUST be rejected immediately before any network dispatch withinvalid_input(HTTP 400 / CLI exit code 2)."\"v1\""must not be accidentally stripped or modified.Immutable Item Identity:
(collection_id, UID, optional RECURRENCE-ID).PATCHupdate. Cross-collection relocation requires creating a new item with an explicit link, followed by deactivating the original.uid,collection_id, orkindin the update payload fails validation withinvalid_input.Preservation-Safe Selective Patching:
WebDAV/CalDAV lacks native sub-resource patching; an HTTP
PUTreplaces the entire calendar object on the server. To update a single property (e.g.titleordeadline) without corrupting external data:ical.SUMMARYorDUE).VALARM), embedded timezone definitions (VTIMEZONE), and unrelated recurrence overrides.calendar_unsupported.Concurrency Conflict Detection (Scenario C10):
PUTto Radicale with the headerIf-Match: <caller_etag>.412 Precondition Failed.etag_conflict(HTTP 412 / CLI exit code 4).Two-Phase Journal Durability & Before/After Audit:
state: 'prepared'containing the target UID, baseline ETag, and intended representation.200/204from Radicale, the service commits a transaction updatingoperations.state = 'applied'and appends atask_updated(orevent_updated) fact tolife_eventswith bothbefore_json(the baseline item state) andafter_json(the newly confirmed state).Exact Technical References & Specifications
Normative Quotations from Technical Architecture
From
docs/technical/cli-api.md(Command-to-route table, lines 33, 38):From
docs/technical/cli-api.md(Idempotency and approval, line 65):From
docs/technical/cli-api.md(Command payloads and results, line 83):From
docs/technical/calendar.md(Discovery and conditional writes, lines 11–14):From
docs/technical/domain-model.md(Identity and relationships, line 45):From
docs/technical/verification.md(Recovery & Calendar scenarios, lines 44, 89):Request & Error JSON Wire Envelopes
1.
PATCH /v1/tasks/{id}Request Payload (stdin)2. Conditional CalDAV
PUTDispatch withIf-Match3. Successful Update Response Envelope (
200 OK)4. Concurrency Conflict Response (
412 Precondition Failedmapped to HTTP 412, exit code 4)5. Missing ETag Error Envelope (
400 Bad Request, exit code 2)What to Build (Step-by-Step)
1. Patch Validation Engine (
src/domain/patch.rs)TaskPatchandEventPatch:invalid_input.id,uid,collection_id, orkindthat conflicts with the target item, reject withinvalid_input.strengthis updated, ensure it is one oflife-active,life-queued,life-someday.deadlineorplanned_startare modified, verifydeadline > planned_start(strictly later).statusis modified, verify allowed domain transitions.2. ETag Preflight & Header Parsing
RequiredIfMatch:If-Matchheader.invalid_input).W/(weak ETag), reject immediately."\"...\"").--etag <ETAG>:If-Matchheader to the API.3. Preservation-Safe iCalendar Patcher (
src/calendar/preservation.rs)item_projection.raw_resource:ical::parser::ical::IcalParser.UID(andRECURRENCE-IDif an occurrence patch).SUMMARY,DUE,DTSTART,CATEGORIES,X-LIFE-*.VALARM,ATTENDEE,VTIMEZONE, custom client extensions) completely intact.4. Two-Phase Journaling & Remote Dispatch (
src/calendar/mutation.rs)state: 'prepared'inoperationstable with baseline ETag.PUTto Radicale withIf-Match: <baseline_etag>.operationstable:state = 'failed'.etag_conflict(HTTP 412, exit code 4).operations.state = 'applied'.life_eventswithkind: 'task_updated'(or'event_updated'), recordingbefore_json(baseline) andafter_json(new item state).item_projectionrow.5. CLI Presentation (
src/cli/task.rs,src/cli/event.rs)life task update <ID> --etag <ETAG> --input - [--operation-id KEY] [--json]life event update <ID> --etag <ETAG> --input - [--operation-id KEY] [--json]etag_conflict, print an explicit instruction to rerunlife item get <ID>and review differences before updating.Acceptance Criteria
life task updatewithout--etag(or HTTPPATCHwithoutIf-Match) is rejected before any network dispatch with exit code 2 (invalid_input); the service never performs a silent read-modify-write.W/"v1") fails validation before remote dispatch.uidorcollection_idin the patch payload returns validation error (HTTP 422, exit code 2).etag_conflictwith HTTP 412 / exit code 4 without retrying or fetching a fresh ETag.VALARM), custom properties, and unknown parameters without round-tripping through the new-item builder.operations.state = 'applied'and appends atask_updatedevent tolife_eventswith accuratebefore_jsonandafter_jsonsnapshots.operation_idand payload returns the confirmed applied result without sending a duplicatePUTto Radicale.Notes
icaldependency), which #14's spec explicitly permits and which is the stronger guarantee for a preservation-critical path.Cargo.tomlintentionally untouched; the corrected author landed insrc/cli.rswith the update commands.Blocked by
Issue #011: Conditional Item Updates with Quoted ETag Preflightto Conditional Item Updates with Quoted ETag Preflightlife event cancel/ POST /v1/events/{id}/cancel #75