tasknotes-spec 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/00-overview.md +172 -0
  2. package/01-terminology.md +156 -0
  3. package/02-model-and-mapping.md +288 -0
  4. package/03-temporal-semantics.md +290 -0
  5. package/04-recurrence.md +398 -0
  6. package/05-operations.md +968 -0
  7. package/06-validation.md +267 -0
  8. package/07-conformance.md +292 -0
  9. package/08-compatibility-and-migrations.md +188 -0
  10. package/09-configuration.md +837 -0
  11. package/10-dependencies-and-reminders.md +266 -0
  12. package/11-links.md +373 -0
  13. package/CHANGELOG.md +25 -0
  14. package/README.md +80 -0
  15. package/conformance/README.md +31 -0
  16. package/conformance/adapters/mdbase-tasknotes.adapter.mjs +141 -0
  17. package/conformance/adapters/tasknotes-core/conformance.ts +2498 -0
  18. package/conformance/adapters/tasknotes-core/create-compat.ts +1 -0
  19. package/conformance/adapters/tasknotes-core/date.ts +12 -0
  20. package/conformance/adapters/tasknotes-core/field-mapping.ts +14 -0
  21. package/conformance/adapters/tasknotes-core/recurrence.ts +10 -0
  22. package/conformance/adapters/tasknotes-date-bridge.ts +20 -0
  23. package/conformance/adapters/tasknotes-runtime-bridge.ts +1107 -0
  24. package/conformance/adapters/tasknotes-runtime-obsidian-shim.ts +84 -0
  25. package/conformance/adapters/tasknotes-templating-bridge.ts +13 -0
  26. package/conformance/adapters/tasknotes.adapter.mjs +485 -0
  27. package/conformance/docs/ADAPTER_CONTRACT.md +245 -0
  28. package/conformance/docs/FIXTURE_FORMAT.md +247 -0
  29. package/conformance/docs/RUNNER_GUIDE.md +393 -0
  30. package/conformance/fixtures/config-schema.json +634 -0
  31. package/conformance/fixtures/config.json +18984 -0
  32. package/conformance/fixtures/conformance.json +444 -0
  33. package/conformance/fixtures/create-compat.json +18639 -0
  34. package/conformance/fixtures/date.json +25612 -0
  35. package/conformance/fixtures/dependencies.json +8647 -0
  36. package/conformance/fixtures/field-mapping.json +5406 -0
  37. package/conformance/fixtures/links.json +1127 -0
  38. package/conformance/fixtures/migrations.json +668 -0
  39. package/conformance/fixtures/operations.json +2761 -0
  40. package/conformance/fixtures/recurrence.json +22958 -0
  41. package/conformance/fixtures/reminders.json +13333 -0
  42. package/conformance/fixtures/templating.json +497 -0
  43. package/conformance/fixtures/validation.json +5308 -0
  44. package/conformance/lib/adapter-loader.mjs +23 -0
  45. package/conformance/lib/load-fixtures.mjs +43 -0
  46. package/conformance/lib/matchers.mjs +200 -0
  47. package/conformance/manifest.json +232 -0
  48. package/conformance/scripts/generate-fixtures.mjs +5239 -0
  49. package/conformance/scripts/package-fixtures.mjs +101 -0
  50. package/conformance/tests/coverage.test.mjs +213 -0
  51. package/conformance/tests/runner.test.mjs +102 -0
  52. package/conformance/tests/tasknotes-runtime-routing.test.mjs +64 -0
  53. package/package.json +49 -0
@@ -0,0 +1,290 @@
1
+ # 3. Temporal Semantics
2
+
3
+ ## 3.1 Purpose
4
+
5
+ This section defines how implementations MUST interpret, compare, and serialize task-related date and datetime values.
6
+
7
+ ## 3.2 Supported temporal value classes
8
+
9
+ Implementations MUST support:
10
+
11
+ 1. **Date values**: calendar day without time or timezone.
12
+ 2. **Datetime values**: instant in time.
13
+
14
+ ## 3.3 Canonical serialization
15
+
16
+ ### 3.3.1 Date
17
+
18
+ Canonical date serialization is:
19
+
20
+ ```text
21
+ YYYY-MM-DD
22
+ ```
23
+
24
+ ### 3.3.2 Datetime
25
+
26
+ Canonical datetime serialization is UTC ISO 8601 with `Z`, for example:
27
+
28
+ ```text
29
+ 2026-02-20T13:45:00Z
30
+ ```
31
+
32
+ Canonical datetime writes MUST use second precision (`YYYY-MM-DDTHH:MM:SSZ`) and MUST NOT include fractional seconds.
33
+ Implementations MAY accept alternative inbound datetime forms but MUST normalize outbound canonical writes.
34
+ If an accepted inbound datetime includes fractional seconds, writers MUST normalize deterministically by truncating fractional precision to whole seconds.
35
+
36
+ ## 3.4 Parsing requirements
37
+
38
+ ### 3.4.1 Date parsing
39
+
40
+ Date parsing MUST reject invalid calendar dates.
41
+
42
+ - valid: `2026-02-28`
43
+ - invalid: `2026-02-30`
44
+
45
+ ### 3.4.2 Datetime parsing
46
+
47
+ Datetime parsing MUST reject malformed values.
48
+ In strict mode, datetime parsing MUST reject ambiguous local datetimes without offset (for example `2026-02-20T09:00:00` with no timezone offset).
49
+ In permissive mode, implementations MAY accept such values only under an explicitly documented compatibility policy and SHOULD emit a warning.
50
+
51
+ ### 3.4.3 Mixed input tolerance
52
+
53
+ Implementations MAY accept both date and datetime values for roles like `due` and `scheduled` if configured to do so. The accepted form MUST be documented.
54
+
55
+ ### 3.4.4 Inbound acceptance matrix
56
+
57
+ To avoid ambiguity, conforming parsers MUST follow this matrix.
58
+
59
+ Date forms:
60
+
61
+ | Form | Example | Strict | Permissive |
62
+ |---|---|---|---|
63
+ | Canonical date | `2026-02-20` | accept | accept |
64
+ | Basic date (no separators) | `20260220` | reject (`invalid_date_value`) | MAY accept only under documented compatibility policy; SHOULD emit warning |
65
+
66
+ Datetime forms:
67
+
68
+ | Form | Example | Strict | Permissive |
69
+ |---|---|---|---|
70
+ | Canonical UTC | `2026-02-20T09:00:00Z` | accept | accept |
71
+ | ISO with explicit offset | `2026-02-20T09:00:00+10:00` | accept | accept |
72
+ | ISO with fractional seconds | `2026-02-20T09:00:00.250Z` | accept (normalize to second precision on write) | accept (normalize to second precision on write) |
73
+ | Offset-less local datetime | `2026-02-20T09:00:00` | reject (`invalid_datetime_value`) | MAY accept only under documented compatibility policy; SHOULD emit warning |
74
+ | Space-separated datetime | `2026-02-20 09:00:00` | reject (`invalid_datetime_value`) | MAY accept only under documented compatibility policy; SHOULD emit warning |
75
+ | Basic datetime (no separators) | `20260220T090000Z` | reject (`invalid_datetime_value`) | MAY accept only under documented compatibility policy; SHOULD emit warning |
76
+
77
+ When permissive-mode compatibility accepts a non-canonical form, canonical writes MUST still follow §3.3.
78
+
79
+ ## 3.5 Day semantics vs instant semantics
80
+
81
+ ### 3.5.1 Date roles
82
+
83
+ Date roles represent days, not instants, and MUST NOT be timezone-shifted.
84
+
85
+ ### 3.5.2 Datetime roles
86
+
87
+ Datetime roles represent instants and MUST preserve instant equality through normalization.
88
+
89
+ Example:
90
+
91
+ - input `2026-02-20T08:00:00-05:00`
92
+ - canonical write `2026-02-20T13:00:00Z`
93
+
94
+ ### 3.5.3 Implementation guidance: UTC-anchor strategy (non-normative)
95
+
96
+ Because date-only fields are human-readable (`YYYY-MM-DD`) while implementations often compute using datetime objects, a common strategy is to use a UTC-midnight anchor for date-only internals.
97
+
98
+ Recommended approach:
99
+
100
+ 1. Parse date-only values to a UTC-midnight anchor instant for internal computations.
101
+ 2. Format date-only values from UTC calendar components when writing `YYYY-MM-DD`.
102
+ 3. Keep date-only roles as date-only on write unless an explicit conversion operation is requested.
103
+ 4. Evaluate user-facing day semantics (today, overdue, day grouping) using local calendar-day boundaries per §3.6.
104
+
105
+ Implementations MAY use different internal representations as long as all normative requirements in this section are preserved.
106
+
107
+ ## 3.6 Local calendar-day evaluation
108
+
109
+ ### 3.6.1 Active runtime timezone
110
+
111
+ For day-level semantics, the active runtime timezone is:
112
+
113
+ 1. configured collection `runtime_timezone` if provided, otherwise
114
+ 2. the process/system local timezone.
115
+
116
+ Implementations MUST make the effective timezone discoverable.
117
+
118
+ ### 3.6.2 Local calendar-day rules
119
+
120
+ When calculating day-level concepts (for example overdue/day grouping/calendar day cells), implementations MUST evaluate against local calendar-day boundaries of the active runtime timezone.
121
+
122
+ This requirement prevents off-by-one day drift in positive and negative UTC offsets.
123
+
124
+ ## 3.7 Comparison rules
125
+
126
+ ### 3.7.1 Date-to-date
127
+
128
+ Compare by calendar day ordering.
129
+
130
+ ### 3.7.2 Datetime-to-datetime
131
+
132
+ Compare by instant ordering.
133
+
134
+ ### 3.7.3 Date-to-datetime
135
+
136
+ If compared, implementations MUST document coercion policy. Recommended policy:
137
+
138
+ - convert date to local start-of-day for day-level comparisons,
139
+ - avoid implicit coercion for instant-level comparisons.
140
+
141
+ ## 3.8 due and scheduled semantics
142
+
143
+ `due` and `scheduled` MAY be either date or datetime by configuration.
144
+
145
+ Implementations MUST:
146
+
147
+ - preserve stored granularity unless explicit conversion is requested,
148
+ - avoid silently converting date to datetime during unrelated writes,
149
+ - require documented explicit policy for date-to-datetime conversion operations,
150
+ - apply consistent coercion policy in filters and status calculations.
151
+
152
+ ## 3.9 Completion date semantics
153
+
154
+ `completed_date` is a date role.
155
+
156
+ For non-recurring completion, writers MUST set `completed_date` using §5 semantics:
157
+
158
+ - explicit completion-day input when provided,
159
+ - otherwise current local day in active runtime timezone.
160
+
161
+ ## 3.10 Created/modified timestamps
162
+
163
+ `date_created` and `date_modified` are datetime roles.
164
+
165
+ Writers MUST:
166
+
167
+ - set both on create,
168
+ - update `date_modified` on successful mutating operations that change persisted state,
169
+ - preserve `date_created` unless explicit migration/edit operation changes it.
170
+
171
+ ## 3.11 time_entries semantics
172
+
173
+ ### 3.11.1 Timestamp format
174
+
175
+ `time_entries.startTime` and `time_entries.endTime` MUST be datetime instants in canonical form on write.
176
+
177
+ ### 3.11.2 Range validity
178
+
179
+ If both times exist, `endTime` MUST be greater than or equal to `startTime`.
180
+
181
+ ### 3.11.3 Duration handling
182
+
183
+ If duration is present, implementations SHOULD treat it as derived and MAY rewrite or remove stale duration values during normalization.
184
+
185
+ ### 3.11.4 Active session semantics
186
+
187
+ An entry with `startTime` and no `endTime` represents an active/running session.
188
+
189
+ Implementations that support time-tracking management operations (§5.19) MUST enforce at most one active session per task at commit time.
190
+
191
+ ### 3.11.5 Derived time calculations
192
+
193
+ For interoperability, implementations SHOULD expose both derived views when reporting tracked time:
194
+
195
+ - `closed_minutes`: sum of entries where both `startTime` and `endTime` exist.
196
+ - `live_minutes`: `closed_minutes` plus elapsed minutes of active entries (if any).
197
+
198
+ Implementations MUST document which derived view is used by each reporting surface (for example task cards, stats, APIs).
199
+
200
+ ## 3.12 reminder temporal semantics
201
+
202
+ Reminder time fields are governed by §10.3 and MUST follow canonical datetime and duration formats from this section.
203
+
204
+ Rules:
205
+
206
+ - `reminders[].absoluteTime` MUST be a canonical datetime on write.
207
+ - `reminders[].offset` MUST be a valid ISO 8601 duration string.
208
+ - relative reminder base conversion for date-only values MUST use `reminders.date_only_anchor_time` from §9, or `00:00` local time if unset.
209
+
210
+ ## 3.13 Example: local-day overdue evaluation
211
+
212
+ Assume local timezone `America/Los_Angeles` and local date `2026-02-20`.
213
+
214
+ Task A:
215
+
216
+ ```yaml
217
+ due: 2026-02-19
218
+ status: open
219
+ ```
220
+
221
+ Task A is overdue.
222
+
223
+ Task B:
224
+
225
+ ```yaml
226
+ due: 2026-02-20
227
+ status: open
228
+ ```
229
+
230
+ Task B is not overdue at start of day; it becomes overdue on `2026-02-21` local day.
231
+
232
+ ## 3.14 Example: preserving granularity
233
+
234
+ Before update:
235
+
236
+ ```yaml
237
+ due: 2026-02-20
238
+ priority: normal
239
+ ```
240
+
241
+ Operation: update priority only.
242
+
243
+ After update (conforming):
244
+
245
+ ```yaml
246
+ due: 2026-02-20
247
+ priority: high
248
+ ```
249
+
250
+ Non-conforming behavior would silently rewrite `due` to a datetime.
251
+
252
+ ## 3.15 Example: canonical datetime write
253
+
254
+ Input:
255
+
256
+ ```yaml
257
+ timeEntries:
258
+ - startTime: 2026-02-20T09:30:00+01:00
259
+ endTime: 2026-02-20T10:00:00+01:00
260
+ ```
261
+
262
+ Canonical write:
263
+
264
+ ```yaml
265
+ timeEntries:
266
+ - startTime: 2026-02-20T08:30:00Z
267
+ endTime: 2026-02-20T09:00:00Z
268
+ ```
269
+
270
+ ## 3.16 Example: UTC-anchor roundtrip for date-only field
271
+
272
+ Input:
273
+
274
+ ```yaml
275
+ due: 2026-02-20
276
+ ```
277
+
278
+ Internal computation anchor:
279
+
280
+ ```text
281
+ 2026-02-20T00:00:00Z
282
+ ```
283
+
284
+ After unrelated update, conforming write:
285
+
286
+ ```yaml
287
+ due: 2026-02-20
288
+ ```
289
+
290
+ Non-conforming behavior would rewrite `due` as a datetime or shift it to another day based on local offset.
@@ -0,0 +1,398 @@
1
+ # 4. Recurrence
2
+
3
+ ## 4.1 Purpose
4
+
5
+ This section defines recurrence semantics, including rule representation, anchor behavior, and per-instance completion/skip state.
6
+
7
+ ## 4.2 Recurrence applicability
8
+
9
+ A task is recurring when semantic role `recurrence` contains a valid tasknotes recurrence string.
10
+
11
+ If `recurrence` is absent or empty, the task is non-recurring.
12
+
13
+ ## 4.3 Rule format
14
+
15
+ ### 4.3.1 Required format
16
+
17
+ `recurrence` MUST be a tasknotes recurrence string.
18
+ This syntax is RRULE-derived but is not a full RFC 5545 content line or multi-line iCalendar fragment.
19
+ The RRULE parameter portion MUST use RFC 5545 RRULE property syntax.
20
+ It MAY include a leading inline `DTSTART` segment.
21
+
22
+ If `DTSTART` is present, it MUST use one of:
23
+
24
+ - `DTSTART:YYYYMMDD` (date-only)
25
+ - `DTSTART:YYYYMMDDTHHMMSSZ` (UTC datetime)
26
+
27
+ Canonical combined form is:
28
+
29
+ - `DTSTART:...;FREQ=...`
30
+
31
+ Implementations MAY accept inbound `RRULE:` prefixes or multi-line iCalendar-like inputs for compatibility, but canonical writes SHOULD use the combined single-field form above.
32
+
33
+ Examples:
34
+
35
+ - `FREQ=DAILY`
36
+ - `FREQ=WEEKLY;BYDAY=MO,WE,FR`
37
+ - `FREQ=MONTHLY;BYMONTHDAY=1`
38
+ - `DTSTART:20260220;FREQ=WEEKLY;BYDAY=FR`
39
+
40
+ ### 4.3.2 Invalid rule handling
41
+
42
+ Invalid recurrence rules MUST produce a validation error in strict mode and SHOULD produce at least a warning in permissive mode.
43
+
44
+ ## 4.4 Recurrence anchor
45
+
46
+ `recurrence_anchor` controls progression semantics and MUST be one of:
47
+
48
+ - `scheduled`
49
+ - `completion`
50
+
51
+ If missing, implementations SHOULD default to `scheduled` unless collection configuration defines another default.
52
+
53
+ ### 4.4.1 Recurrence seed precedence
54
+
55
+ When recurrence generation requires a seed/start date, implementations MUST resolve it in this order:
56
+
57
+ 1. `DTSTART` embedded in `recurrence`,
58
+ 2. semantic `scheduled`,
59
+ 3. semantic `date_created`.
60
+
61
+ If no seed can be resolved, recurrence materialization MUST fail deterministically and validation MUST report an error.
62
+
63
+ ### 4.4.2 Anchor progression behavior
64
+
65
+ For `recurrence_anchor=scheduled`, progression is based on the scheduled chain and `DTSTART` MUST remain fixed after it is set.
66
+
67
+ For `recurrence_anchor=completion`, complete-instance operations MUST advance progression by updating `DTSTART` to the completion target (date or datetime per §4.4.3).
68
+
69
+ ### 4.4.3 `DTSTART` update semantics
70
+
71
+ When `recurrence_anchor=completion` and instance completion succeeds for resolved target day `D`:
72
+
73
+ 1. Instance-list state (`complete_instances`, `skipped_instances`) MUST always use day `D`.
74
+ 2. If the caller provided an explicit datetime target, `DTSTART` MUST be rewritten as `DTSTART:YYYYMMDDTHHMMSSZ` using that target instant normalized to UTC.
75
+ 3. Otherwise, `DTSTART` MUST be rewritten as `DTSTART:YYYYMMDD` for day `D`.
76
+ 4. RRULE components other than `DTSTART` MUST be preserved unless an explicit recurrence-edit operation changes them.
77
+ 5. If `DTSTART` is absent, completion-anchor progression MUST insert it before RRULE parameters.
78
+
79
+ ### 4.4.4 Recalculation semantics for `recurrence_anchor=completion`
80
+
81
+ When calculating the next candidate occurrence for `recurrence_anchor=completion`:
82
+
83
+ 1. Implementations MUST treat `DTSTART` progression as the consumed-history mechanism.
84
+ 2. `complete_instances` MUST NOT be used as an exclusion set for future occurrence selection in this mode.
85
+ 3. `skipped_instances` MUST still exclude matching candidate dates.
86
+ 4. Candidate selection MUST choose the first RRULE occurrence strictly after the current `DTSTART` anchor (or resolved seed if `DTSTART` is absent).
87
+
88
+ This matches the completion-anchor progression model where each completion advances the chain by rewriting `DTSTART`.
89
+ This model is intentionally not fully reversible via ordinary `uncomplete instance`; progression history lives in `DTSTART`, not only in `complete_instances`.
90
+
91
+ ### 4.4.5 `DTSTART` canonicalization on recurring writes
92
+
93
+ To ensure stable recurrence materialization across implementations, writers that persist recurring tasks MUST ensure `DTSTART` is present after create or successful recurring-instance completion.
94
+
95
+ Rules:
96
+
97
+ 1. If persisted `recurrence` already contains `DTSTART`, keep it unless an explicit operation updates it per §4.4.3.
98
+ 2. If persisted `recurrence` omits `DTSTART`, writers MUST resolve a seed using §4.4.1 and insert `DTSTART` before RRULE parameters.
99
+ 3. For `recurrence_anchor=scheduled`, inserted `DTSTART` becomes the fixed progression baseline and MUST NOT be rewritten by later scheduled-anchor completions.
100
+ 4. If seed resolution fails, operation MUST fail deterministically and emit `missing_recurrence_seed`.
101
+
102
+ ## 4.5 Instance state fields
103
+
104
+ Per-instance state is represented by:
105
+
106
+ - `complete_instances`: list of date values
107
+ - `skipped_instances`: list of date values
108
+
109
+ These lists represent day-level instance outcomes.
110
+
111
+ ## 4.6 Invariants
112
+
113
+ Implementations MUST enforce:
114
+
115
+ 1. Items in instance lists are valid date values.
116
+ 2. No date appears in both lists simultaneously.
117
+ 3. Duplicate dates are normalized (set semantics) or rejected deterministically.
118
+ 4. Validators MUST NOT reject an instance date solely because it is not an RRULE-generated occurrence.
119
+
120
+ ## 4.7 Instance completion semantics
121
+
122
+ Operation: `complete instance` for target date `D`.
123
+
124
+ Conforming behavior:
125
+
126
+ 1. Add `D` to `complete_instances`.
127
+ 2. Remove `D` from `skipped_instances` if present.
128
+ 3. Leave base task status unchanged unless explicit policy states otherwise.
129
+ 4. Update `date_modified` when state changes.
130
+
131
+ Operation MUST be idempotent.
132
+
133
+ ## 4.8 Instance uncompletion semantics
134
+
135
+ Operation: `uncomplete instance` for target date `D`.
136
+
137
+ Conforming behavior:
138
+
139
+ 1. Remove `D` from `complete_instances` if present.
140
+ 2. Do not add `D` to `skipped_instances` implicitly.
141
+ 3. For `recurrence_anchor=completion`, uncomplete MUST NOT rewrite or roll back `DTSTART`; restoring prior progression requires an explicit recurrence edit or implementation-specific rewind operation.
142
+ 4. Update `date_modified` when state changes.
143
+
144
+ Operation MUST be idempotent.
145
+
146
+ ## 4.9 Instance skip semantics
147
+
148
+ Operation: `skip instance` for target date `D`.
149
+
150
+ Conforming behavior:
151
+
152
+ 1. Add `D` to `skipped_instances`.
153
+ 2. Remove `D` from `complete_instances` if present.
154
+ 3. Update `date_modified` when state changes.
155
+
156
+ Operation MUST be idempotent.
157
+
158
+ ## 4.10 Instance unskip semantics
159
+
160
+ Operation: `unskip instance` for target date `D`.
161
+
162
+ Conforming behavior:
163
+
164
+ 1. Remove `D` from `skipped_instances` if present.
165
+ 2. Do not add to `complete_instances` implicitly.
166
+ 3. Update `date_modified` when state changes.
167
+
168
+ Operation MUST be idempotent.
169
+
170
+ ## 4.11 Effective instance state
171
+
172
+ For target date `D`, effective state MUST be resolved in this order:
173
+
174
+ 1. If `D` in `complete_instances`: state is `completed`.
175
+ 2. Else if `D` in `skipped_instances`: state is `skipped`.
176
+ 3. Else: state is unresolved/default for that instance.
177
+
178
+ Because overlap is invalid, this ordering is deterministic.
179
+
180
+ ## 4.12 Interaction with base status
181
+
182
+ For recurring tasks:
183
+
184
+ - Base `status` is task-level metadata and MUST NOT be forcibly rewritten to a completed status on instance completion unless explicitly configured.
185
+ - Instance state determines completion for recurrence-aware views and operations.
186
+ - Dependency blocked/unblocked evaluation for v0.1 follows §10.2.5 and is not recurrence-instance-aware unless explicitly extended by implementation policy.
187
+
188
+ For non-recurring tasks, completion uses base status semantics (§5).
189
+
190
+ ## 4.13 Completed-status configuration
191
+
192
+ Implementations MUST define which status values are treated as complete for non-recurring tasks.
193
+
194
+ The completed-status list MUST be configurable or schema-driven and MUST NOT rely on a hardcoded single literal.
195
+
196
+ ## 4.14 Example: complete then skip same day
197
+
198
+ Initial:
199
+
200
+ ```yaml
201
+ recurrence: FREQ=DAILY
202
+ completeInstances: [2026-02-20]
203
+ skippedInstances: []
204
+ ```
205
+
206
+ Skip target date `2026-02-20`:
207
+
208
+ ```yaml
209
+ recurrence: FREQ=DAILY
210
+ completeInstances: []
211
+ skippedInstances: [2026-02-20]
212
+ ```
213
+
214
+ ## 4.15 Example: idempotent complete
215
+
216
+ Initial:
217
+
218
+ ```yaml
219
+ completeInstances: [2026-02-20]
220
+ skippedInstances: []
221
+ ```
222
+
223
+ Complete `2026-02-20` again:
224
+
225
+ - persisted instance lists remain unchanged,
226
+ - operation succeeds without duplicate entries.
227
+
228
+ ## 4.16 Example: anchor semantics
229
+
230
+ Task:
231
+
232
+ ```yaml
233
+ recurrence: FREQ=WEEKLY;BYDAY=FR
234
+ recurrenceAnchor: completion
235
+ ```
236
+
237
+ If implementation supports next-occurrence materialization, it MUST compute next occurrence relative to completion progression when anchor is `completion`, not solely by scheduled chain.
238
+ Implementations using `DTSTART` progression MUST update `DTSTART` to the completion date for this mode.
239
+
240
+ Worked example (`recurrence_anchor=completion`):
241
+
242
+ ```yaml
243
+ recurrence: DTSTART:20260220;FREQ=DAILY
244
+ recurrence_anchor: completion
245
+ complete_instances: [2026-02-20, 2026-02-21]
246
+ skipped_instances: [2026-02-23]
247
+ ```
248
+
249
+ Conforming recalculation:
250
+
251
+ - does **not** exclude `2026-02-22` because it is absent from `skipped_instances`,
252
+ - does exclude `2026-02-23` because it is skipped,
253
+ - ignores `complete_instances` for future exclusion in this mode (§4.4.4).
254
+
255
+ ## 4.17 Validation examples
256
+
257
+ Invalid overlap:
258
+
259
+ ```yaml
260
+ completeInstances: [2026-02-20]
261
+ skippedInstances: [2026-02-20]
262
+ ```
263
+
264
+ Result: validation error `instance_state_overlap`.
265
+
266
+ Invalid date in list:
267
+
268
+ ```yaml
269
+ completeInstances: [2026-02-30]
270
+ ```
271
+
272
+ Result: validation error `invalid_date_value`.
273
+
274
+ ## 4.18 Recurrence materialization
275
+
276
+ This subsection is required only for implementations claiming profile `materialized-occurrences` (§7.3.4).
277
+
278
+ Recurrence materialization creates ordinary task files for specific target dates of a recurring parent task.
279
+ It is additive behavior: implementations that do not claim `materialized-occurrences` continue to use the instance-list model in §4.5-§4.12.
280
+ Materialized occurrence notes MUST be task files according to the collection's task-detection rules (§9.7).
281
+
282
+ ### 4.18.1 Parent and occurrence ownership
283
+
284
+ The parent recurring task owns series-level semantics:
285
+
286
+ - `recurrence`
287
+ - `recurrence_anchor`
288
+ - recurrence generation and `DTSTART` progression
289
+ - parent instance-list compatibility state (`complete_instances`, `skipped_instances`)
290
+ - materialization policy fields (`occurrence_materialization`, `occurrence_next_trigger`, `occurrence_template`, `occurrence_past_horizon`, `occurrence_future_horizon`)
291
+
292
+ A materialized occurrence note owns per-date task semantics for its `occurrence_date`, including:
293
+
294
+ - `status`
295
+ - `completed_date`
296
+ - body content and checklists
297
+ - `time_entries`
298
+ - per-occurrence `due`, `scheduled`, `reminders`, `contexts`, `projects`, and unknown fields
299
+
300
+ ### 4.18.2 Occurrence identity and parent reference
301
+
302
+ A materialized occurrence note MUST store:
303
+
304
+ - `recurrence_parent`: a link-or-string reference to the parent recurring task
305
+ - `occurrence_date`: the target date represented by the occurrence note
306
+
307
+ The preferred Obsidian-compatible representation for `recurrence_parent` is a wikilink, for example:
308
+
309
+ ```yaml
310
+ recurrence_parent: "[[Weekly Review]]"
311
+ occurrence_date: 2026-02-27
312
+ ```
313
+
314
+ Implementations claiming `materialized-occurrences` MUST resolve `recurrence_parent` using §11 link semantics for this role, even if they do not claim full `extended`.
315
+ If the parent cannot be resolved, validation SHOULD report `invalid_recurrence_parent`.
316
+
317
+ For one resolved parent, there SHOULD be at most one materialized occurrence note per `occurrence_date`.
318
+ Duplicate detected occurrence notes SHOULD report `duplicate_occurrence_note` and MUST NOT be chosen nondeterministically for writes.
319
+
320
+ ### 4.18.3 Effective state precedence
321
+
322
+ For target date `D`, recurrence-aware effective state MUST be resolved in this order:
323
+
324
+ 1. If a materialized occurrence note exists for `(parent, D)`, derive state from that note's own task state:
325
+ - `completed` when its `status` is in `status.completed_values`;
326
+ - `skipped` when its `status` is in `status.skipped_values`;
327
+ - otherwise unresolved/default for that instance.
328
+ 2. Else if `D` is in parent `complete_instances`: state is `completed`.
329
+ 3. Else if `D` is in parent `skipped_instances`: state is `skipped`.
330
+ 4. Else: state is unresolved/default for that instance.
331
+
332
+ If a materialized occurrence note disagrees with the parent instance lists, the materialized occurrence note wins for `D`.
333
+ Validators SHOULD report `occurrence_state_conflict`, and repair/normalization operations MAY update parent lists to match occurrence notes.
334
+
335
+ ### 4.18.4 Parent instance lists as compatibility state
336
+
337
+ When materialized occurrence notes are supported, parent `complete_instances` and `skipped_instances` remain part of canonical persisted state.
338
+ They serve as interoperability state for unmaterialized occurrences and as a compatibility index for clients that do not read occurrence notes.
339
+
340
+ Conforming writers SHOULD reconcile parent lists after materialized occurrence completion, skip, uncompletion, or unskip:
341
+
342
+ - completed child state: add `D` to `complete_instances`, remove `D` from `skipped_instances`;
343
+ - skipped child state: add `D` to `skipped_instances`, remove `D` from `complete_instances`;
344
+ - active/unresolved child state after uncompletion/unskip: remove `D` from the relevant parent list.
345
+
346
+ Reconciliation failure MUST NOT silently change the occurrence note state.
347
+ Implementations SHOULD surface a recoverable error or warning.
348
+
349
+ ### 4.18.5 Materialization modes
350
+
351
+ `occurrence_materialization` controls when materialized occurrence notes are created.
352
+ Allowed values:
353
+
354
+ - `manual`: occurrence notes are created only by explicit user or API action.
355
+ - `on_completion`: completing a materialized occurrence note creates the next occurrence note.
356
+ - `rolling`: the implementation maintains a bounded materialized window, such as today through the next 14 days.
357
+
358
+ If absent, `occurrence_materialization` MUST default to `manual`.
359
+
360
+ `on_completion` does not by itself create the first occurrence note in a series.
361
+ The first materialized occurrence MUST be created by explicit materialization, rolling materialization, or a documented create-time option.
362
+
363
+ Conforming implementations MUST NOT create occurrence notes merely as a side effect of read-only browsing.
364
+
365
+ ### 4.18.6 Next-occurrence trigger
366
+
367
+ `occurrence_next_trigger` controls whether skip/cancel transitions also create the next occurrence note when `occurrence_materialization=on_completion`.
368
+ Allowed values:
369
+
370
+ - `completion`: only completion creates the next occurrence note.
371
+ - `completion_or_skip`: completion and skip/cancel create the next occurrence note.
372
+
373
+ If absent, `occurrence_next_trigger` MUST default to `completion`.
374
+
375
+ The next occurrence MUST be computed from the parent recurring task after applying the triggering state transition and §4.4 anchor semantics.
376
+ Materializing the next occurrence MUST be idempotent: if the next occurrence note already exists, the operation MUST return or reuse it rather than creating a duplicate.
377
+
378
+ ### 4.18.7 Rolling materialization bounds
379
+
380
+ For `occurrence_materialization=rolling`, implementations MUST enforce finite bounds.
381
+ Unbounded future materialization is non-conformant.
382
+
383
+ Bounds SHOULD be expressed with ISO 8601 durations such as:
384
+
385
+ ```yaml
386
+ occurrence_past_horizon: P0D
387
+ occurrence_future_horizon: P14D
388
+ ```
389
+
390
+ If bounds are absent and rolling mode is enabled, implementations MUST use documented finite defaults.
391
+
392
+ ### 4.18.8 Recurrence edits and historical notes
393
+
394
+ Materialized occurrence notes are durable task files.
395
+ Editing the parent recurrence rule MUST NOT silently delete, rewrite, or move existing materialized occurrence notes.
396
+
397
+ If an existing occurrence note's `occurrence_date` is no longer generated by the current parent recurrence rule, validators MAY report `materialization_target_not_generated` as a warning.
398
+ This condition MUST NOT be treated as an error by default because recurrence edits can make historical notes intentionally non-generated.