Scoped Recurring Mutations (Series vs Occurrence) #15
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 mutation scoping (
scope: seriesvsscope: occurrence) for recurring items, supporting detached occurrence updates and completing individual recurring task occurrences without mutating the master series.Background & Technical Rationale
In calendar and task management systems supporting recurrence (RFC 5545), mutating an item that belongs to a recurring series presents an inherent semantic ambiguity:
If an automation system guesses this intent, it risks destructive consequences—either completing an entire recurring task series when the user only finished today's instance, or inadvertently modifying only a single occurrence when the user intended to reschedule their recurring weekly sync.
To guarantee determinism and user agency, Life enforces strict mutation scoping rules:
Whenever a mutation (
update,complete,reopen,cancel) targets an item with recurrence rules (RRULE,RDATE) or an existing recurrence identity, the caller MUST explicitly supplyscope: seriesorscope: occurrence.If a mutation on a recurring item omits
--scope(or thescopefield in JSON request payloads), Life MUST NOT guess or default to series or occurrence. It must halt execution and returnneeds_clarification(HTTP 200 with clarification questions / CLI exit code 3).In CalDAV (RFC 4791), a recurring series and all of its detached overrides reside in a single resource (
.icsfile) identified by one URL href and protected by one ETag:scope: series, the masterVEVENTorVTODOcomponent is patched, and unrelated detached overrides are preserved.scope: occurrence, the system targets the specificRECURRENCE-ID. If a detached component already exists, it is patched. If no detached component exists yet, a new detached component is cloned from the master, assigned the targetRECURRENCE-ID, patched with the requested changes, and injected into the same resource.PUTusing the single resource ETag withIf-Match.Completing a single occurrence of a recurring task (
life task complete <id> --etag <etag> --scope occurrence) must complete only that occurrence. The detached component receivesSTATUS: COMPLETEDand aCOMPLETEDtimestamp, while the masterVTODOMUST remain inSTATUS: NEEDS-ACTION. If the underlying CalDAV server does not support task recurrence conformance, the operation must returncalendar_unsupportedrather than corrupting the series.Complex range operations such as
RANGE=THISANDFUTURE(which split an ongoing series into two separate UID series) are error-prone and inconsistently supported across CalDAV backends. Life explicitly rejects split-series operations withcalendar_unsupported.Exact Technical References & Specifications
Recurrence Mutation Contract
From
docs/technical/calendar.md(lines 35–36):Domain Model Recurrence Rules
From
docs/technical/domain-model.md(lines 49–50):Verification Matrix Scenario C12
From
docs/technical/verification.md(lines 46, 50):CLI & API Envelopes and Exit Codes
From
docs/technical/cli-api.md:life task update ID --etag ETAG --input -(PATCH/v1/tasks/{id}): acceptsscope: series|occurrence.life task complete ID --etag ETAG --input -(POST/v1/tasks/{id}/complete): acceptsscope: series|occurrence.life event update ID --etag ETAG --input -(PATCH/v1/events/{id}): acceptsscope: series|occurrence.life event cancel ID --etag ETAG --input -(POST/v1/events/{id}/cancel): acceptsscope: series|occurrence.needs_clarificationresponse when--scopeis omitted:Section Citations
docs/technical/calendar.md: Section Recurrence and mutation (lines 35–36), Section Conformance gates (line 49).docs/technical/domain-model.md: Section Identity and relationships (lines 49–50).docs/technical/verification.md: Section Calendar scenarios (lines 46, 50, Scenario C12).docs/technical/cli-api.md: Section Transport and common contract (lines 9–15), Section Command-to-route table (lines 33–36, 83–86), Section Error identifiers (line 113).What to Build
1. Scoped Mutation Resolver (
src/calendar/scoped_mutation.rs)validate_mutation_scope(item: &Item, raw_resource: &str, requested_scope: Option<MutationScope>) -> Result<MutationScope, ScopeResolutionError>:RRULE,RDATE) or existing detached overrides (RECURRENCE-ID).requested_scopeis provided asSeriesorOccurrence, accept or ignore safely.requested_scopeisNone: returnScopeResolutionError::NeedsClarificationcontaining the structured question envelope.requested_scopeisSome(scope): proceed with the chosen scope.2. Series Mutation Processor
scope == MutationScope::Series:RECURRENCE-ID).ICalendarPatcher.UIDare not removed or corrupted.CalendarError::Unsupported("calendar_unsupported").3. Occurrence Mutation & Task Completion Processor
scope == MutationScope::Occurrence:RECURRENCE-IDfrom the request or the targeted item projection.VCALENDARstream.RECURRENCE-ID == target_recurrence_id.RRULEorRDATEproperties from the cloned component.RECURRENCE-IDproperty matching the target occurrence timestamp and timezone.DTSTART,DTEND, and/orDUEto the target occurrence's scheduled instant/date, preserving the series duration and time-of-day.VCALENDARcomponent list.life task complete <id> --etag <etag> --scope occurrence:STATUS: COMPLETEDon the detached component.COMPLETED: <timestamp>on the detached component.VTODOMUST remain untouched withSTATUS: NEEDS-ACTION.PUTwithIf-Match: <resource_etag>.4. CLI Flags and HTTP Route Handlers
src/cli.rs:--scope <series|occurrence>argument to:life task updatelife task completelife task reopenlife task cancellife event updatelife event cancelsrc/api.rs):scopefrom request JSON body.ScopeResolutionError::NeedsClarificationis returned, respond with HTTP 200 and envelopestatus: "needs_clarification".needs_clarification.5. Integration Test Suite (
tests/calendar_scoped_mutation_tests.rs)--scope.status: "needs_clarification"and clarification options["series", "occurrence"].20300410T090000Zof a recurring task with--scope occurrence.VTODOexists withRECURRENCE-ID: 20300410T090000ZandSTATUS: COMPLETED.VTODOexists withSTATUS: NEEDS-ACTION.SUMMARYof recurring series with--scope series.SUMMARYis updated while unrelated detached overrides retain their identities.RANGE=THISANDFUTURE).calendar_unsupported.Acceptance Criteria
--scopereturnsneeds_clarification(HTTP 200, CLI exit code 3) with structured questions (Scenario C12).--scope occurrenceupdates only that occurrence component toSTATUS: COMPLETEDwhile the masterVTODOremains inSTATUS: NEEDS-ACTION(Scenario C12).RECURRENCE-IDand preserves all other occurrences and master properties.--scope seriesmutates the master component while leaving unrelated existing detached overrides intact.RANGE=THISANDFUTURE) and split-series mutations are rejected with explicitcalendar_unsupportederror (Exit code 2 / 5).If-Match.Notes
mainand re-verified against the jev and hindsight suites (374 tests). No code change was required.Cargo.tomlauthors chore stays pending.Blocked by
Issue #015: Scoped Recurring Mutations (Series vs Occurrence)to Scoped Recurring Mutations (Series vs Occurrence)life event cancel/ POST /v1/events/{id}/cancel #75