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.
- package/00-overview.md +172 -0
- package/01-terminology.md +156 -0
- package/02-model-and-mapping.md +288 -0
- package/03-temporal-semantics.md +290 -0
- package/04-recurrence.md +398 -0
- package/05-operations.md +968 -0
- package/06-validation.md +267 -0
- package/07-conformance.md +292 -0
- package/08-compatibility-and-migrations.md +188 -0
- package/09-configuration.md +837 -0
- package/10-dependencies-and-reminders.md +266 -0
- package/11-links.md +373 -0
- package/CHANGELOG.md +25 -0
- package/README.md +80 -0
- package/conformance/README.md +31 -0
- package/conformance/adapters/mdbase-tasknotes.adapter.mjs +141 -0
- package/conformance/adapters/tasknotes-core/conformance.ts +2498 -0
- package/conformance/adapters/tasknotes-core/create-compat.ts +1 -0
- package/conformance/adapters/tasknotes-core/date.ts +12 -0
- package/conformance/adapters/tasknotes-core/field-mapping.ts +14 -0
- package/conformance/adapters/tasknotes-core/recurrence.ts +10 -0
- package/conformance/adapters/tasknotes-date-bridge.ts +20 -0
- package/conformance/adapters/tasknotes-runtime-bridge.ts +1107 -0
- package/conformance/adapters/tasknotes-runtime-obsidian-shim.ts +84 -0
- package/conformance/adapters/tasknotes-templating-bridge.ts +13 -0
- package/conformance/adapters/tasknotes.adapter.mjs +485 -0
- package/conformance/docs/ADAPTER_CONTRACT.md +245 -0
- package/conformance/docs/FIXTURE_FORMAT.md +247 -0
- package/conformance/docs/RUNNER_GUIDE.md +393 -0
- package/conformance/fixtures/config-schema.json +634 -0
- package/conformance/fixtures/config.json +18984 -0
- package/conformance/fixtures/conformance.json +444 -0
- package/conformance/fixtures/create-compat.json +18639 -0
- package/conformance/fixtures/date.json +25612 -0
- package/conformance/fixtures/dependencies.json +8647 -0
- package/conformance/fixtures/field-mapping.json +5406 -0
- package/conformance/fixtures/links.json +1127 -0
- package/conformance/fixtures/migrations.json +668 -0
- package/conformance/fixtures/operations.json +2761 -0
- package/conformance/fixtures/recurrence.json +22958 -0
- package/conformance/fixtures/reminders.json +13333 -0
- package/conformance/fixtures/templating.json +497 -0
- package/conformance/fixtures/validation.json +5308 -0
- package/conformance/lib/adapter-loader.mjs +23 -0
- package/conformance/lib/load-fixtures.mjs +43 -0
- package/conformance/lib/matchers.mjs +200 -0
- package/conformance/manifest.json +232 -0
- package/conformance/scripts/generate-fixtures.mjs +5239 -0
- package/conformance/scripts/package-fixtures.mjs +101 -0
- package/conformance/tests/coverage.test.mjs +213 -0
- package/conformance/tests/runner.test.mjs +102 -0
- package/conformance/tests/tasknotes-runtime-routing.test.mjs +64 -0
- 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.
|
package/04-recurrence.md
ADDED
|
@@ -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.
|