Conditional Task & Event Creation with Prepared Intent #10
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 creation for tasks (
POST /v1/tasks/life task add) and calendar events (POST /v1/events/life event add), enforcing two-phase commit journaling (local prepared intent committed before network dispatch), CalDAVIf-None-Match: *preconditions against Radicale, mandatory task commitment strength categories, and atomic timeline event logging.Background & Technical Rationale
Two-Phase Commit Protocol for Remote Writes:
Directly sending a network write without a durable local record invites split-brain states and phantom updates if the client crashes or network drops mid-flight. To guarantee operational durability and crash recovery, item creation follows a strict two-phase commit pattern:
operationstable withstate: 'prepared'andintent_json. No network transmission occurs while this database transaction is open.PUTto Radicale withIf-None-Match: *. This ensures that if a resource with the generated UID already exists, Radicale rejects the write with HTTP412 Precondition Failed.201 Createdwith a confirmed resourceETag, the service commits a local transaction that:operations.stateto'applied'and storesresult_json.life_eventswithkind: 'task_created'(or'event_created'),evidence_class: 'confirmed_service', and the confirmed ETag.item_projection.201 Createdwith the confirmed item, ETag, and operation ID.Task Domain Invariants & Commitment Strengths:
CATEGORIES: exactly one oflife-active,life-queued, orlife-someday. New actionable tasks without a valid strength category MUST be rejected withinvalid_input(HTTP 422 / CLI exit code 2).STATUS:NEEDS-ACTION.deadlinemaps to iCalendarDUE.planned_startmaps toDTSTART. If both are provided,DUEmust be strictly later thanDTSTARTas required by RFC 5545 anddocs/technical/domain-model.md:35; equal or inverted values are rejected withinvalid_input. Incompatible types (e.g. comparing a date-only value with an instant without timezone context) are rejected. Undated tasks must not have arbitrary default dates invented.estimated_minutes->X-LIFE-ESTIMATED-MINUTES(integer)energy->X-LIFE-ENERGY(low,medium,high)project_id->X-LIFE-PROJECT-ID(string)Event Domain Invariants:
startandendinstants or dates;endmust be strictly afterstart.statusisCONFIRMEDorTENTATIVE.transparencyisOPAQUE(default) orTRANSPARENT.related_task_idmaps to iCalendarRELATED-TO.Concurrency & Conflict Semantics:
If Radicale returns HTTP 412, the service maps it to
etag_conflict(HTTP 409 / CLI exit code 4). The service must never automatically retry by fabricating a new UID or overwriting existing data without caller intervention.Exact Technical References & Specifications
Normative Quotations from Technical Architecture
From
docs/technical/cli-api.md(Command-to-route table, lines 32, 37):From
docs/technical/cli-api.md(Idempotency and approval, lines 61–65):From
docs/technical/calendar.md(Discovery and conditional writes, lines 11–12):From
docs/technical/domain-model.md(Task axes, lines 22–25):From
docs/technical/timeline-consistency.md(Operation state machine, line 129):From
docs/technical/verification.md(Workflow scenarios, Scenario W01, line 15):Request & Response Wire Envelopes
1.
POST /v1/tasksRequest Payload (stdin)2. Generated iCalendar Object Transmitted via CalDAV
PUT3. Successful HTTP 201 Response Envelope
4. Precondition Conflict (HTTP 412 mapped to HTTP 409 / Exit code 4)
What to Build (Step-by-Step)
1. Domain Validation & Serialization (
src/domain/task.rs,src/domain/event.rs)titleis non-empty after trimming.strengthis provided and is one oflife-active,life-queued,life-someday.CATEGORIEScontains the strength tag, merging with any provided area tags.deadline(DUE) andplanned_start(DTSTART) are both present, verifydeadline > planned_start(strictly later).icalendar:VCALENDARwithVERSION:2.0.VTODOcomponent withUID,DTSTAMP,SUMMARY,STATUS:NEEDS-ACTION,CATEGORIES,DUE, and customX-LIFE-*properties.start < end.VEVENTwithDTSTART,DTEND,SUMMARY,STATUS:CONFIRMED(orTENTATIVE), andTRANSP:OPAQUE(orTRANSPARENT).2. Two-Phase Journal Coordination (
src/db/operations.rs,src/db/events.rs)operations:(operation_id, request_hash, schema_version=1, state='prepared', actor, prepared_at=now, updated_at=now, intent_json, baseline_json=NULL, result_json=NULL).operation_idalready exists:idempotency_conflict(HTTP 409, exit code 4).3. Conditional Remote Dispatch (
src/calendar/mutation.rs)PUTto/{collection_url}/{uid}.ics:If-None-Match: *.ETagheader. If missing, perform a single boundedHEAD/PROPFINDread to obtain the confirmed ETag.operationstostate: 'failed'.etag_conflicterror.4. Result Transaction Commit
operationsrecord:state = 'applied',result_json = <serialized response>.life_events:event_id: Generated UUID / CUID.dedupe_key:${operation_id}:applied:create.kind:'task_created'(or'event_created').evidence_class:'confirmed_service'.after_json: Serialized normalizedItem.operation_id: Reference to parent operation.item_projection.5. API Handlers & CLI Commands (
src/api/tasks.rs,src/cli/task.rs)POST /v1/tasksandPOST /v1/events:Idempotency-Keyheader; reject with HTTP 400invalid_inputif absent. All database writes go through the dedicated blocking worker.life task add --input - [--operation-id KEY] [--json]life event add --input - [--operation-id KEY] [--json]Acceptance Criteria
life-active,life-queued,life-someday) returns validation error (HTTP 422, exit code 2).PUTrequest to Radicale containsIf-None-Match: *.operationstable contains a'prepared'row before network dispatch; upon HTTP 201 from Radicale,operations.stateis updated to'applied'and an event is appended tolife_events.etag_conflict(HTTP 409 / exit code 4) and setsoperations.state = 'failed'without retrying with a new UID.operation_idand payload returns the previously recorded result withchanged: false(or original applied response) without sending a secondPUTto Radicale.operation_idbut a modified payload returnsidempotency_conflict(HTTP 409, exit code 4).estimated_minutes,energy, andproject_idare written asX-LIFE-ESTIMATED-MINUTES,X-LIFE-ENERGY, andX-LIFE-PROJECT-IDin the generated iCalendar resource.POST /v1/tasksorPOST /v1/eventswithout anIdempotency-Keyheader returns HTTP 400invalid_input(exit code 2).Notes
error.operation_id),deny_unknown_fieldson create payloads, and an explicit method guard.Blocked by
Issue #010: Conditional Task & Event Creation with Prepared Intentto Conditional Task & Event Creation with Prepared Intent