Task Lifecycle Transitions, Replays & Progress Tracking #12
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 explicit task lifecycle transition routes (
POST /v1/tasks/{id}/complete,POST /v1/tasks/{id}/reopen,POST /v1/tasks/{id}/canceland CLI commandslife task complete,life task reopen,life task cancel), enforcing strict domain state machine validation, RFC 5545 completion timestamps and percentage properties, mandatoryIf-MatchETag preflight, idempotent replays, and no-op suppression.Background & Technical Rationale
Explicit Domain Lifecycle State Machine:
Tasks in
life-servicedo not use arbitrary free-form status strings. VTODOSTATUSis restricted to four valid RFC 5545 states:NEEDS-ACTION,IN-PROCESS,COMPLETED, andCANCELLED. Transitions between these states must be explicitly governed by domain logic:complete):NEEDS-ACTIONorIN-PROCESS.COMPLETED.COMPLETEDtimestamp to an explicit user-reported outcome instant (occurred_at) or the confirmed action instant. SetsPERCENT-COMPLETE: 100.reopen):COMPLETEDorCANCELLED.NEEDS-ACTION.COMPLETEDtimestamp; setsPERCENT-COMPLETE: 0.DUEdeadline and commitment strength category (life-active,life-queued,life-someday) unless specifically requested.cancel):NEEDS-ACTIONorIN-PROCESS.CANCELLED.STATUS:CANCELLEDon Radicale, not deleted via HTTP DELETE.Mandatory ETag Preflight & Optimistic Concurrency:
All lifecycle mutations require the caller to supply the current resource ETag (
--etag/If-Match). If the resource has been concurrently modified on Radicale, the dispatch returnsetag_conflict(HTTP 412 / CLI exit code 4). The service must never guess or silently overwrite changes.Idempotent Replays vs No-Op Suppression (Scenario W09):
Idempotency-Key/operation_idimmediately replays the recorded outcome from the SQLiteoperationstable without re-executing remote CalDAV writes.completeon a task that is alreadyCOMPLETEDunder a NEW operation ID:changed: false.task_completedevent inlife_events!Crash Recovery & Uncertainty Handling:
If a remote write times out or the connection drops after dispatch:
'unknown'or'needs_reconciliation'.operation_unknown).life operation get <ID>.Exact Technical References & Specifications
Normative Quotations from Technical Architecture
From
docs/technical/domain-model.md(Task axes, lines 22–34):From
docs/technical/cli-api.md(Command-to-route table, lines 34–36):From
docs/technical/cli-api.md(Command payloads and results, line 84):From
docs/technical/verification.md(Workflow scenarios, Scenario W09, line 23):From
docs/technical/verification.md(Recovery scenarios, lines 103–109):From
docs/technical/cli-api.md(Complete fictional examples, lines 151–161):What to Build (Step-by-Step)
1. Lifecycle State Machine Engine (
src/domain/lifecycle.rs)TaskLifecycle:validate_transition(current_status: TaskStatus, transition: &LifecycleTransition) -> Result<TargetChanges, LifecycleError>:Ok(TargetChanges::NoOp).Complete:current_statusin[NEEDS-ACTION, IN-PROCESS].STATUS: COMPLETED,COMPLETED: <occurred_at or now>,PERCENT-COMPLETE: 100.Reopen:current_statusin[COMPLETED, CANCELLED].STATUS: NEEDS-ACTION, removeCOMPLETED,PERCENT-COMPLETE: 0.Cancel:current_statusin[NEEDS-ACTION, IN-PROCESS].STATUS: CANCELLED, removeCOMPLETED.2. Preservation-Safe iCalendar Lifecycle Patcher
VTODOcomponent:STATUS.COMPLETED.PERCENT-COMPLETE.UID,SUMMARY,DUE,DTSTART,CATEGORIES,X-LIFE-*,VALARM) untouched.3. Execution Pipeline & Two-Phase Journaling
TargetChanges::NoOp:operationswithstate: 'applied',changed: false, canonical request fingerprint, and the recorded result.changed: falseand current item.PUTand do NOT append duplicate lifecycle events tolife_events.state: 'prepared'tooperationstable with canonical fingerprint.PUTto Radicale withIf-Match: <baseline_etag>.operations.state = 'failed'and returnetag_conflict(HTTP 412, exit code 4).operations.state = 'unknown'and returnoperation_unknown(exit code 6).operations.state = 'applied', and append corresponding fact (task_completed,task_reopened, ortask_cancelled) tolife_eventswithbefore_jsonandafter_json.4. Axum Routes & CLI Handlers
POST /v1/tasks/{id}/completePOST /v1/tasks/{id}/reopenPOST /v1/tasks/{id}/cancellife task complete <ID> --etag <ETAG> --operation-id <KEY> [--input -] [--json]life task reopen <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]life task cancel <ID> --etag <ETAG> [--operation-id <KEY>] [--input -] [--json]Acceptance Criteria
life task completeupdates status toCOMPLETED, recordsPERCENT-COMPLETE: 100, setsCOMPLETEDtimestamp, and appends atask_completedevent tolife_events.life task reopenon a completed task restoresSTATUS: NEEDS-ACTION, resetsPERCENT-COMPLETE: 0, removesCOMPLETEDproperty, retains originalDUEdeadline, and appends atask_reopenedevent.life task cancelon an open task updates status toCANCELLEDand clears completion timestamp without deleting the CalDAV resource.--etag(or missingIf-Match) is rejected before remote dispatch with HTTP 400invalid_input(exit code 2); passing a stale ETag where Radicale returns HTTP 412 producesetag_conflict(HTTP 412 / exit code 4).operation_idreplays the recorded outcome without sending an additional HTTPPUTto Radicale.appliedoperation withchanged: falseinoperationswithout remotePUTor duplicate lifecycle events; subsequent same-key requests replay this outcome.operation_unknown(exit code 6) and logs the operation ID; it never reports the task completed until evidence confirms it.Notes
reopenwas a recorded no-op fromNEEDS-ACTIONbut a 422 fromIN-PROCESS; it is now refused 422 from both open states, with the no-op retained forcomplete@COMPLETEDandcancel@CANCELLED.Cargo.tomlchange was needed, so the deferred authors chore stays pending.Blocked by
Issue #012: Task Lifecycle Transitions, Replays & Progress Trackingto Task Lifecycle Transitions, Replays & Progress Trackinglife event cancel/ POST /v1/events/{id}/cancel #75