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,968 @@
1
+ # 5. Operations
2
+
3
+ ## 5.1 Purpose
4
+
5
+ This section defines normative behavior for task mutations and read-facing resolution concerns that affect persisted state.
6
+
7
+ ## 5.2 General operation rules
8
+
9
+ For all mutating operations:
10
+
11
+ 1. Validation MUST run before commit in strict mode.
12
+ 2. Writes MUST be atomic at file level (all-or-nothing per file).
13
+ 3. `date_modified` MUST be updated on successful state change.
14
+ 4. Unknown fields MUST be preserved unless explicit normalization is requested. This applies to every operation in this section: create, update, complete, uncomplete, skip, unskip, dependency mutations, reminder mutations, archive, and time-tracking operations.
15
+ 5. Operations identified as idempotent MUST remain safe under repetition.
16
+
17
+ ### 5.2.1 Target day/date resolution
18
+
19
+ Recurring instance operations act on a target day/date (`complete instance`, `uncomplete instance`, `skip`, `unskip`).
20
+ Non-recurring `complete` uses completion-day semantics for `completed_date`.
21
+
22
+ Resolution MUST be deterministic:
23
+
24
+ 1. If caller provides an explicit target date/datetime, that value is authoritative.
25
+ 2. If caller omits target for recurring instance operations, implementations MUST resolve in this order:
26
+ - task `scheduled`, if present and target-day-resolvable,
27
+ - else task `due`, if present and target-day-resolvable,
28
+ - else current local day in active runtime timezone (§3.6).
29
+ 3. Target-day resolution for fallback task fields (`scheduled` / `due`) MUST use this extraction policy:
30
+ - if stored value is canonical date (`YYYY-MM-DD`), use it as-is;
31
+ - if stored value is datetime, use the literal date token before `T` (`YYYY-MM-DD`) without timezone shifting;
32
+ - if stored value is non-canonical but accepted by compatibility policy, implementations MAY normalize to a date token and SHOULD emit a warning;
33
+ - if a candidate value is unusable, continue to the next fallback source.
34
+ 4. For non-recurring complete, if caller omits explicit completion-day input, implementations MUST use current local day in active runtime timezone.
35
+
36
+ When an explicit target is datetime:
37
+
38
+ - operations that require day semantics MUST use its calendar date part in active runtime timezone,
39
+ - for recurring `complete instance` with `recurrence_anchor=completion`, the same explicit datetime target MUST also drive `DTSTART` rewrite semantics in §4.4.3.
40
+
41
+ This explicit-target datetime rule is intentionally distinct from persisted-field fallback extraction above.
42
+
43
+ ### 5.2.2 Idempotency and `date_modified`
44
+
45
+ For operations marked idempotent:
46
+
47
+ - first application MAY change state and `date_modified`,
48
+ - repeated application with semantically equivalent input MUST leave persisted state unchanged.
49
+
50
+ When a repeated idempotent operation is a no-op, `date_modified` MUST remain unchanged.
51
+
52
+ ## 5.3 Create
53
+
54
+ ### 5.3.1 Input requirements
55
+
56
+ Create MUST provide at minimum semantic roles required by §2, directly or via defaults.
57
+
58
+ ### 5.3.2 Required behavior
59
+
60
+ Create MUST:
61
+
62
+ - apply default values,
63
+ - generate `date_created` and `date_modified` when absent,
64
+ - resolve semantic `title` and filename/path behavior according to §9.13,
65
+ - apply create-time templating when enabled and supported (§5.3.5, §9.14, §7),
66
+ - serialize canonical keys and canonical temporal formats,
67
+ - when `recurrence` is present, canonicalize `DTSTART` per §4.4.5,
68
+ - preserve caller-provided semantic `id` when present,
69
+ - fail with validation errors if required constraints are unmet,
70
+ - apply default reminders according to §10.3.9 when configured.
71
+
72
+ ### 5.3.3 Title and filename resolution on create
73
+
74
+ Create MUST produce a deterministic initial filename/path.
75
+
76
+ Rules:
77
+
78
+ - Generated filenames MUST use filename-safe sanitization and MUST avoid invalid/empty basenames.
79
+ - If `title.storage=filename`:
80
+ - basename MUST derive from semantic title,
81
+ - title changes after create are handled by update semantics (§5.4.4),
82
+ - implementations MAY also persist mapped `title` as a synchronized compatibility mirror,
83
+ - `title.filename_format` and `title.custom_filename_template` MUST be ignored.
84
+ - If `title.storage=frontmatter`, create-time basename MUST follow `title.filename_format`:
85
+ - `title`: sanitized semantic title,
86
+ - `zettel`: implementation-defined zettel pattern (MUST be documented),
87
+ - `timestamp`: implementation-defined timestamp pattern (MUST be documented),
88
+ - `custom`: `title.custom_filename_template` expansion.
89
+ - If generated path already exists, implementation MUST resolve collision deterministically (for example numeric suffixing).
90
+
91
+ ### 5.3.4 Example
92
+
93
+ Input intent:
94
+
95
+ ```yaml
96
+ title: "Pay electricity bill"
97
+ ```
98
+
99
+ Assume defaults:
100
+
101
+ - status default `open`
102
+ - priority default `normal`
103
+
104
+ Persisted frontmatter (example):
105
+
106
+ ```yaml
107
+ title: Pay electricity bill
108
+ status: open
109
+ priority: normal
110
+ dateCreated: 2026-02-20T14:00:00Z
111
+ dateModified: 2026-02-20T14:00:00Z
112
+ ```
113
+
114
+ ### 5.3.5 Optional create-time templating
115
+
116
+ This subsection is required only for implementations claiming profile `templating` (§7.3.3).
117
+
118
+ Create-time templating pipeline MUST be deterministic:
119
+
120
+ 1. Compute base create payload from explicit input, defaults (§9.8), and generated/system values (`date_created`, `date_modified`, resolved title policy).
121
+ 2. Expand template variables using that base payload and active runtime date/time context.
122
+ 3. Merge template frontmatter into base frontmatter with base payload precedence (base keys MUST win on conflict).
123
+ 4. Resolve body output:
124
+ - if expanded template body is non-empty, use it;
125
+ - otherwise use caller-provided body/details (if any).
126
+
127
+ Template parse rules:
128
+
129
+ - If template content begins with `---`, implementations MUST treat the first `--- ... ---` block as template frontmatter and the remainder as template body.
130
+ - Frontmatter variable expansion MUST run before YAML parsing of template frontmatter.
131
+
132
+ Portable variable support:
133
+
134
+ Implementations claiming `templating` MUST support all variables in the **Required** column of the table below. Implementations MAY support any variable in the **Extended** column.
135
+
136
+ **Required task-data variables:**
137
+
138
+ | Variable | Value | Format / separator |
139
+ |---|---|---|
140
+ | `{{title}}` | Semantic title | string; YAML-quoted if needed |
141
+ | `{{status}}` | Task status value | string |
142
+ | `{{priority}}` | Task priority value | string |
143
+ | `{{dueDate}}` | Due date if set | `YYYY-MM-DD` or empty |
144
+ | `{{scheduledDate}}` | Scheduled date if set | `YYYY-MM-DD` or empty |
145
+ | `{{details}}` | Caller-provided body/description | string |
146
+ | `{{contexts}}` | Contexts list | comma-separated, e.g. `"work, home"` |
147
+ | `{{tags}}` | Tags list | comma-separated, e.g. `"task, errands"` |
148
+ | `{{hashtags}}` | Tags list as hashtags | space-separated, e.g. `"#task #errands"` |
149
+ | `{{timeEstimate}}` | Time estimate in minutes | integer string or empty |
150
+ | `{{parentNote}}` | Parent note name/path | string; YAML-quoted if needed |
151
+
152
+ **Required date/time variables (expansion time):**
153
+
154
+ | Variable | Value | Format |
155
+ |---|---|---|
156
+ | `{{date}}` | Current date | `yyyy-MM-dd` |
157
+ | `{{time}}` | Current time (24h) | `HH:mm` |
158
+ | `{{year}}` | Full year | `yyyy` |
159
+ | `{{month}}` | Month number | `MM` |
160
+ | `{{day}}` | Day of month | `dd` |
161
+
162
+ **Extended variables (optional):**
163
+
164
+ | Variable | Value | Format |
165
+ |---|---|---|
166
+ | `{{dateTime}}` | Date + time | `yyyy-MM-dd-HHmm` |
167
+ | `{{timestamp}}` | Full timestamp | `yyyy-MM-dd-HHmmss` |
168
+ | `{{shortDate}}` | Short date | `yyMMdd` |
169
+ | `{{shortYear}}` | Two-digit year | `yy` |
170
+ | `{{monthName}}` | Month full name | e.g. `February` |
171
+ | `{{monthNameShort}}` | Month short name | e.g. `Feb` |
172
+ | `{{dayName}}` | Weekday full name | e.g. `Monday` |
173
+ | `{{dayNameShort}}` | Weekday short name | e.g. `Mon` |
174
+ | `{{week}}` | ISO week number | `ww` |
175
+ | `{{quarter}}` | Quarter number | `1`–`4` |
176
+ | `{{hour}}` | Hour (24h) | `HH` |
177
+ | `{{minute}}` | Minute | `mm` |
178
+ | `{{second}}` | Second | `ss` |
179
+ | `{{time12}}` | Time with AM/PM | `hh:mm a` |
180
+ | `{{time24}}` | Time (24h) | `HH:mm` |
181
+ | `{{timezone}}` | UTC offset long | e.g. `+05:30` |
182
+ | `{{utcOffset}}` | UTC offset long | e.g. `+05:30` |
183
+ | `{{unix}}` | Unix timestamp (seconds) | integer string |
184
+ | `{{unixMs}}` | Unix timestamp (ms) | integer string |
185
+ | `{{zettel}}` | Zettelkasten ID | date + seconds-since-midnight base-36 |
186
+ | `{{titleLower}}` | Title lowercase | string |
187
+ | `{{titleUpper}}` | Title uppercase | string |
188
+ | `{{titleSnake}}` | Title snake_case | string |
189
+ | `{{titleKebab}}` | Title kebab-case | string |
190
+ | `{{titleCamel}}` | Title camelCase | string |
191
+ | `{{titlePascal}}` | Title PascalCase | string |
192
+ | `{{priorityShort}}` | First letter of priority | uppercase char |
193
+ | `{{statusShort}}` | First letter of status | uppercase char |
194
+
195
+ Unknown variables (not in the above lists) MUST be handled per `templating.unknown_variable_policy` (§9.14):
196
+ - `preserve`: leave the `{{...}}` placeholder as-is.
197
+ - `empty`: replace with an empty string.
198
+
199
+ Deterministic expansion context:
200
+
201
+ - date/time variable expansion MUST use the active runtime timezone from §3.6.
202
+ - locale-sensitive variables (`monthName`, `monthNameShort`, `dayName`, `dayNameShort`, `time12`) MUST use English locale outputs for portability (`January`, `Jan`, `Monday`, `Mon`, `AM`/`PM`).
203
+ - template-generated datetime-like outputs used in frontmatter MUST normalize to second precision when written as canonical datetime roles (§3.3.2).
204
+
205
+ Failure behavior:
206
+
207
+ - `templating.failure_mode=error`: template read/parse failures MUST abort create.
208
+ - `templating.failure_mode=warning_fallback`: template read/parse failures MUST continue create using non-templated behavior (base frontmatter + caller body/details) and SHOULD emit warnings (`template_missing` or `template_parse_failed`).
209
+
210
+ ### 5.3.6 Identity handling on create
211
+
212
+ If semantic `id` is provided on create, writers MUST preserve it.
213
+ If implementation supports auto-generation of semantic `id`, generation policy MUST be documented and resulting `id` MUST remain stable after create.
214
+
215
+ ## 5.4 Update
216
+
217
+ ### 5.4.1 Patch semantics
218
+
219
+ Update MUST be patch-by-default: only targeted semantic roles are changed.
220
+
221
+ ### 5.4.2 Preservation rules
222
+
223
+ Update MUST preserve:
224
+
225
+ - unrelated known roles,
226
+ - unknown fields,
227
+ - original date/datetime granularity for untouched fields.
228
+
229
+ ### 5.4.3 Example
230
+
231
+ Before:
232
+
233
+ ```yaml
234
+ title: Weekly review
235
+ scheduled: 2026-02-20
236
+ priority: normal
237
+ customClient: ACME
238
+ ```
239
+
240
+ Operation: set priority to `high`.
241
+
242
+ After:
243
+
244
+ ```yaml
245
+ title: Weekly review
246
+ scheduled: 2026-02-20
247
+ priority: high
248
+ customClient: ACME
249
+ ```
250
+
251
+ ### 5.4.4 Title updates and filename updates
252
+
253
+ If an update changes semantic `title`, behavior MUST follow `title.storage`:
254
+
255
+ - `title.storage=filename`: implementation MUST rename file basename to match updated title (with sanitization and deterministic collision handling), preserving parent folder unless caller requested a move.
256
+ - `title.storage=frontmatter`: implementation MUST update mapped title field and MUST NOT rename file unless an explicit rename operation is requested.
257
+
258
+ If implementation keeps frontmatter title for compatibility while `title.storage=filename`, it MUST keep stored title synchronized with effective filename-derived title.
259
+
260
+ ## 5.5 Complete (non-recurring)
261
+
262
+ For non-recurring tasks, complete MUST:
263
+
264
+ 1. Set `status` to a configured completed value, unless already completed.
265
+ If multiple completed values are configured, implementation MUST choose deterministically (default: first entry in `status.completed_values`).
266
+ 2. Set `completed_date` to completion day:
267
+ - explicit completion-day input when provided,
268
+ - otherwise current local day in active runtime timezone (§5.2.1).
269
+ 3. Apply overwrite policy deterministically (`overwrite` or `preserve_if_present`).
270
+ 4. Update `date_modified` on state change.
271
+
272
+ Implementations MUST document completion-day input handling and `completed_date` overwrite policy.
273
+
274
+ Operation MUST be idempotent.
275
+
276
+ ## 5.6 Uncomplete (non-recurring)
277
+
278
+ For non-recurring tasks, uncomplete MUST:
279
+
280
+ 1. Set `status` to configured active/default status according to policy (default: `status.default`).
281
+ 2. Clear or retain `completed_date` according to policy.
282
+ 3. Update `date_modified` on state change.
283
+
284
+ Policy MUST be documented and deterministic.
285
+
286
+ Operation MUST be idempotent.
287
+
288
+ ## 5.7 Complete instance (recurring)
289
+
290
+ For recurring tasks, complete with resolved target date `D` (§5.2.1) MUST follow §4.7.
291
+
292
+ Writers MUST NOT convert recurring completion into a base status rewrite unless explicit configuration requires it.
293
+ When `recurrence_anchor=completion` and caller supplies an explicit datetime target, implementations MUST apply §4.4.3 datetime `DTSTART` rewrite behavior.
294
+ When `recurrence_anchor=scheduled` and `DTSTART` is absent, implementations MUST insert `DTSTART` deterministically per §4.4.5.
295
+ Next-occurrence recalculation in this mode MUST follow §4.4.4.
296
+
297
+ ## 5.8 Uncomplete instance (recurring)
298
+
299
+ For recurring tasks, uncomplete with resolved target date `D` (§5.2.1) MUST follow §4.8.
300
+ For `recurrence_anchor=completion`, ordinary uncomplete is intentionally non-reversible and MUST NOT roll back `DTSTART`.
301
+
302
+ ## 5.9 Skip and unskip instance
303
+
304
+ Skip/unskip for recurring tasks with resolved target date `D` (§5.2.1) MUST follow §4.9 and §4.10.
305
+
306
+ ## 5.10 Dependency operations
307
+
308
+ Dependency operations MUST follow §10.2.
309
+
310
+ ### 5.10.1 Add dependency
311
+
312
+ Add dependency MUST:
313
+
314
+ - validate dependency entry schema,
315
+ - parse and resolve `uid` per §11,
316
+ - enforce duplicate and self-reference rules,
317
+ - preserve existing non-target dependency entries,
318
+ - update `date_modified` on change.
319
+
320
+ ### 5.10.2 Remove dependency
321
+
322
+ Remove dependency by `uid` MUST be idempotent.
323
+
324
+ ### 5.10.3 Replace dependency list
325
+
326
+ Replace dependency list MUST be explicit (not default patch behavior).
327
+
328
+ ## 5.11 Reminder operations
329
+
330
+ Reminder operations MUST follow §10.3.
331
+
332
+ ### 5.11.1 Add reminder
333
+
334
+ Add reminder MUST:
335
+
336
+ - validate reminder schema,
337
+ - enforce unique `id` within task,
338
+ - update `date_modified` on change.
339
+
340
+ ### 5.11.2 Update reminder
341
+
342
+ Update reminder MUST target a reminder by `id` and be patch-by-default.
343
+
344
+ ### 5.11.3 Remove reminder
345
+
346
+ Remove reminder by `id` MUST be idempotent.
347
+
348
+ ## 5.12 Archive
349
+
350
+ Implementations MAY support archive semantics through:
351
+
352
+ - archive tag,
353
+ - archive status,
354
+ - dedicated boolean/archive field.
355
+
356
+ Archive behavior MUST be documented.
357
+
358
+ Archive MUST NOT implicitly delete the task.
359
+
360
+ ## 5.13 Delete
361
+
362
+ Delete MUST remove the task file.
363
+
364
+ Optional safety behavior:
365
+
366
+ - Implementations MAY perform backlink/dependency checks.
367
+ - If checks are enabled, implementation MUST provide a bypass option for explicit force delete.
368
+
369
+ ## 5.14 Rename
370
+
371
+ Rename operation changes file path/filename while preserving semantic record identity.
372
+
373
+ ### 5.14.1 Rename interaction with title storage mode
374
+
375
+ - If `title.storage=filename`, basename changes from rename MUST update effective semantic title.
376
+ - If `title.storage=frontmatter`, rename MUST NOT implicitly rewrite mapped `title` unless explicitly requested.
377
+ - Implementations that persist frontmatter title in `title.storage=filename` mode MUST either update that field on rename or remove it to prevent divergence.
378
+ - If semantic `id` is present, rename/move MUST preserve its value unchanged.
379
+
380
+ If implementation supports link/reference updating, it MUST:
381
+
382
+ - update resolvable references deterministically,
383
+ - report unresolved or ambiguous updates,
384
+ - update dependency `uid` and `projects` references consistently with normal links.
385
+
386
+ If implementation does not support reference updates, this limitation MUST be disclosed in conformance claim.
387
+
388
+ ## 5.15 Batch operations
389
+
390
+ Batch operations MAY be supported.
391
+
392
+ If supported, implementation MUST report per-item outcomes and summary counts:
393
+
394
+ - total
395
+ - succeeded
396
+ - failed
397
+
398
+ Partial success behavior MUST be documented.
399
+
400
+ ## 5.16 Concurrency
401
+
402
+ Implementations SHOULD provide write-conflict detection (for example based on modified timestamp or file hash).
403
+
404
+ When conflict is detected, operation MUST fail safely unless explicit overwrite is requested.
405
+
406
+ ## 5.17 Dry run
407
+
408
+ If dry run mode is supported, operation MUST:
409
+
410
+ - execute validation and transformation logic,
411
+ - report intended changes,
412
+ - perform no file write.
413
+
414
+ ## 5.18 Error model
415
+
416
+ Implementation-native operation failures MUST provide structured error information with:
417
+
418
+ - operation name,
419
+ - error code,
420
+ - message,
421
+ - optional field/path context.
422
+
423
+ For conformance adapter envelopes (§5.22), failures MAY be represented as a compact `error` string for transport compatibility, but implementations SHOULD also expose the structured fields (for example via `error_details`).
424
+
425
+ ## 5.19 Time tracking management
426
+
427
+ This subsection applies to implementations that support `time_entries`.
428
+
429
+ ### 5.19.1 Start time tracking
430
+
431
+ Start on task `T` MUST:
432
+
433
+ - fail when `T` already has an active time entry,
434
+ - append a new entry with canonical `startTime` and no `endTime`,
435
+ - MAY set a default `description`,
436
+ - update `date_modified`,
437
+ - persist canonical `time_entries` shape (including removal of legacy/stale `duration` fields when normalizing).
438
+
439
+ Recommended error code when already active: `time_tracking_already_active`.
440
+
441
+ ### 5.19.2 Stop time tracking
442
+
443
+ Stop on task `T` MUST:
444
+
445
+ - fail when `T` has no active time entry,
446
+ - set `endTime` on the active entry to canonical current datetime,
447
+ - update `date_modified`,
448
+ - preserve non-target entries and unknown frontmatter fields.
449
+
450
+ Recommended error code when no active session exists: `no_active_time_entry`.
451
+
452
+ ### 5.19.3 Edit or replace time entries
453
+
454
+ When replacing/editing `time_entries` explicitly, implementations MUST:
455
+
456
+ - validate each entry per §2.6.1 and §3.11,
457
+ - persist canonical datetime formats,
458
+ - treat `duration` as derived compatibility input and SHOULD remove it from canonical writes,
459
+ - update `date_modified` when persisted state changes.
460
+
461
+ ### 5.19.4 Remove a time entry
462
+
463
+ Remove MUST target one deterministic entry (for example by index or stable selector) and update `date_modified` on change.
464
+ Selector semantics MUST be documented.
465
+
466
+ ### 5.19.5 Completion-triggered auto-stop
467
+
468
+ If `time_tracking.auto_stop_on_complete=true` (§9.16), completion transitions MUST trigger stop behavior for the same task only.
469
+
470
+ Rules:
471
+
472
+ - non-recurring tasks: trigger when status transitions from non-completed to completed status.
473
+ - recurring tasks: trigger when `complete_instances` grows.
474
+ - materialized occurrence notes: trigger on the occurrence note's own status transition to completed; parent reconciliation SHOULD NOT trigger a second stop on the parent unless the parent itself has an active time entry and the implementation documents that policy.
475
+ - implementations MUST NOT stop active sessions on unrelated tasks.
476
+ - if `time_tracking.auto_stop_notification=true`, implementations MAY emit a user-visible notice.
477
+
478
+ ### 5.19.6 Reporting tracked time
479
+
480
+ For reporting surfaces (for example task summaries or stats), implementations MUST document whether totals use `closed_minutes` or `live_minutes` semantics from §3.11.5.
481
+
482
+ ## 5.20 Recurrence materialization operations
483
+
484
+ This subsection is required only for implementations claiming profile `materialized-occurrences` (§7.3.4).
485
+
486
+ ### 5.20.1 Materialize occurrence
487
+
488
+ Operation: `materialize occurrence` for parent recurring task `P` and target date `D`.
489
+
490
+ Conforming behavior:
491
+
492
+ 1. Resolve `P` and confirm it is recurring.
493
+ 2. If an occurrence note for `(P, D)` already exists, return that note without creating a duplicate.
494
+ 3. Create a task note using `occurrence_template` when configured, otherwise using normal create behavior.
495
+ 4. Persist `recurrence_parent` as a link to `P` and `occurrence_date` as `D`.
496
+ 5. Set `scheduled` to `D` unless caller input or template output explicitly supplies another per-occurrence scheduled value.
497
+ 6. Set initial `status` using normal create defaults unless caller input or template output explicitly supplies a status.
498
+ 7. Preserve template/body output as occurrence-owned content after creation.
499
+ 8. Update `date_created` and `date_modified` according to create semantics.
500
+
501
+ The operation MUST be idempotent for `(P, D)`.
502
+
503
+ If `D` is not generated by the current recurrence rule, implementations MAY allow materialization as an explicit user action but SHOULD report `materialization_target_not_generated`.
504
+
505
+ ### 5.20.2 Complete materialized occurrence
506
+
507
+ Completing a materialized occurrence note MUST:
508
+
509
+ 1. Complete the occurrence note using normal non-recurring completion semantics (§5.5).
510
+ 2. Reconcile the parent by adding `D` to `complete_instances` and removing `D` from `skipped_instances`.
511
+ 3. If the parent has `recurrence_anchor=completion`, advance parent progression according to §4.4.3 and §4.4.4.
512
+ 4. If `occurrence_materialization=on_completion`, compute the next occurrence from the parent after the completion transition and materialize that next occurrence note idempotently.
513
+
514
+ The occurrence note's successful completion MUST NOT be rolled back solely because next-occurrence materialization fails.
515
+ When the next note cannot be created, implementations SHOULD report `next_occurrence_materialization_failed`.
516
+
517
+ ### 5.20.3 Skip materialized occurrence
518
+
519
+ Skipping a materialized occurrence note MUST preserve the note.
520
+ Skip is a state transition, not deletion.
521
+
522
+ Conforming behavior:
523
+
524
+ 1. Set the occurrence note to the configured skipped status (`status.default_skipped`, or the first `status.skipped_values` entry when `default_skipped` is absent).
525
+ 2. Clear or retain `completed_date` according to the same documented policy used for uncompletion unless the implementation documents a skip-specific policy.
526
+ 3. Reconcile the parent by adding `D` to `skipped_instances` and removing `D` from `complete_instances`.
527
+ 4. If `occurrence_materialization=on_completion` and `occurrence_next_trigger=completion_or_skip`, compute and materialize the next occurrence note idempotently.
528
+
529
+ If no skipped status is configured, skip of a materialized occurrence MUST fail deterministically in strict mode and SHOULD report `missing_skipped_status`.
530
+
531
+ ### 5.20.4 Uncomplete and unskip materialized occurrence
532
+
533
+ Uncomplete of a materialized occurrence note MUST:
534
+
535
+ 1. Reopen the occurrence note using normal uncomplete semantics (§5.6).
536
+ 2. Remove `D` from parent `complete_instances`.
537
+ 3. For `recurrence_anchor=completion`, MUST NOT roll back parent `DTSTART`, matching §4.8.
538
+ 4. MUST NOT delete any later materialized occurrence note.
539
+
540
+ Unskip of a materialized occurrence note MUST:
541
+
542
+ 1. Reopen the occurrence note to configured active/default status.
543
+ 2. Remove `D` from parent `skipped_instances`.
544
+ 3. MUST NOT add `D` to parent `complete_instances` implicitly.
545
+ 4. MUST NOT delete any later materialized occurrence note.
546
+
547
+ ### 5.20.5 Delete and unmaterialize occurrence
548
+
549
+ Deleting a materialized occurrence note removes the task file and MUST NOT be interpreted as skip.
550
+
551
+ Implementations MAY provide an explicit `unmaterialize occurrence` operation.
552
+ If supported, it MUST document whether parent state is preserved, cleared, or reconciled.
553
+ In all cases, deleting or unmaterializing an occurrence MUST NOT silently delete the parent recurring task.
554
+
555
+ If parent instance lists still refer to a deleted occurrence note's date, validators MAY report an orphan/cache warning such as `orphan_occurrence_note`.
556
+
557
+ ## 5.21 Operation examples
558
+
559
+ ### 5.21.1 Non-recurring complete
560
+
561
+ Before:
562
+
563
+ ```yaml
564
+ title: Buy groceries
565
+ status: open
566
+ completedDate:
567
+ dateModified: 2026-02-20T09:00:00Z
568
+ ```
569
+
570
+ After complete on `2026-02-20`:
571
+
572
+ ```yaml
573
+ title: Buy groceries
574
+ status: done
575
+ completedDate: 2026-02-20
576
+ dateModified: 2026-02-20T09:05:00Z
577
+ ```
578
+
579
+ ### 5.21.2 Recurring complete instance
580
+
581
+ Before:
582
+
583
+ ```yaml
584
+ title: Weekly review
585
+ status: open
586
+ scheduled: 2026-02-20
587
+ recurrence: FREQ=WEEKLY;BYDAY=FR
588
+ completeInstances: []
589
+ skippedInstances: []
590
+ dateModified: 2026-02-20T08:00:00Z
591
+ ```
592
+
593
+ After complete instance on `2026-02-20`:
594
+
595
+ ```yaml
596
+ title: Weekly review
597
+ status: open
598
+ scheduled: 2026-02-20
599
+ recurrence: DTSTART:20260220;FREQ=WEEKLY;BYDAY=FR
600
+ completeInstances: [2026-02-20]
601
+ skippedInstances: []
602
+ dateModified: 2026-02-20T08:10:00Z
603
+ ```
604
+
605
+ ### 5.21.3 Recurring skip instance overriding completion
606
+
607
+ Before:
608
+
609
+ ```yaml
610
+ completeInstances: [2026-02-20]
611
+ skippedInstances: []
612
+ ```
613
+
614
+ After skip `2026-02-20`:
615
+
616
+ ```yaml
617
+ completeInstances: []
618
+ skippedInstances: [2026-02-20]
619
+ ```
620
+
621
+ ### 5.21.4 Preserve unknown fields on update
622
+
623
+ Before:
624
+
625
+ ```yaml
626
+ title: Plan Q2
627
+ status: open
628
+ vendorTicket: ZX-42
629
+ ```
630
+
631
+ Update status to `in-progress`.
632
+
633
+ After:
634
+
635
+ ```yaml
636
+ title: Plan Q2
637
+ status: in-progress
638
+ vendorTicket: ZX-42
639
+ ```
640
+
641
+ ### 5.21.5 Add dependency
642
+
643
+ Before:
644
+
645
+ ```yaml
646
+ blockedBy: []
647
+ ```
648
+
649
+ Add dependency:
650
+
651
+ ```yaml
652
+ uid: "[[prepare-metrics]]"
653
+ reltype: FINISHTOSTART
654
+ ```
655
+
656
+ After:
657
+
658
+ ```yaml
659
+ blockedBy:
660
+ - uid: "[[prepare-metrics]]"
661
+ reltype: FINISHTOSTART
662
+ ```
663
+
664
+ ### 5.21.6 Add reminder
665
+
666
+ Before:
667
+
668
+ ```yaml
669
+ reminders: []
670
+ ```
671
+
672
+ Add reminder:
673
+
674
+ ```yaml
675
+ id: due_minus_1h
676
+ type: relative
677
+ relatedTo: due
678
+ offset: -PT1H
679
+ ```
680
+
681
+ After:
682
+
683
+ ```yaml
684
+ reminders:
685
+ - id: due_minus_1h
686
+ type: relative
687
+ relatedTo: due
688
+ offset: -PT1H
689
+ ```
690
+
691
+ ## 5.22 Conformance operation interface
692
+
693
+ The conformance suite exercises operations via the `execute(operation, input) → envelope` interface defined in the adapter contract. This section specifies the input/output contracts for each conformance-testable operation name.
694
+
695
+ Unless otherwise noted, all operations return `{ ok: true, result: {...} }` on success and `{ ok: false, error: "..." }` on failure. Adapters MAY additionally return `error_details` with structured fields from §5.18.
696
+
697
+ ### General operations
698
+
699
+ #### `op.mutate_with_validation`
700
+
701
+ Tests §5.2 rule 1: validation runs before commit.
702
+
703
+ Input:
704
+ ```json
705
+ { "strict": true, "frontmatter": { ... } }
706
+ ```
707
+
708
+ Behavior: validate `frontmatter` as a task record. In strict mode (`strict=true`), any validation error MUST produce `ok=false`.
709
+
710
+ Output on validation error:
711
+ ```json
712
+ { "ok": false, "error": "validation|invalid_type|..." }
713
+ ```
714
+
715
+ ---
716
+
717
+ #### `op.atomic_write`
718
+
719
+ Tests §5.2 rule 2: writes are all-or-nothing.
720
+
721
+ Input:
722
+ ```json
723
+ {
724
+ "original": { ... },
725
+ "patch": { ... },
726
+ "simulateFailureAfterWrite": true
727
+ }
728
+ ```
729
+
730
+ Behavior: apply `patch` to `original`, then simulate a post-write failure. The adapter MUST report whether the write was committed and what state the record is in after recovery.
731
+
732
+ Output:
733
+ ```json
734
+ {
735
+ "ok": true,
736
+ "result": {
737
+ "committed": false,
738
+ "persisted": { ... }
739
+ }
740
+ }
741
+ ```
742
+
743
+ `committed=false` means the write was rolled back. `persisted` reflects the actual frontmatter after recovery — it MUST match `original` (not the patched version) when `simulateFailureAfterWrite=true`.
744
+
745
+ ---
746
+
747
+ #### `op.idempotency_check`
748
+
749
+ Tests §5.2 rule 5: operations are safe under repetition.
750
+
751
+ Input:
752
+ ```json
753
+ {
754
+ "operation": "complete_nonrecurring",
755
+ "first": { "status": "open", "completedDate": null },
756
+ "second": { "status": "done", "completedDate": "2026-02-20" }
757
+ }
758
+ ```
759
+
760
+ Behavior: applying the named operation to `first` state and then again to `second` state MUST produce identical persisted results (including unchanged `dateModified` on the second no-op application). The adapter checks whether this holds.
761
+
762
+ Output:
763
+ ```json
764
+ { "ok": true, "result": { "idempotent": true } }
765
+ ```
766
+
767
+ ---
768
+
769
+ #### `op.update_patch`
770
+
771
+ Tests §5.4 patch semantics and unknown-field preservation.
772
+
773
+ Input:
774
+ ```json
775
+ {
776
+ "original": { "title": "...", "vendorTicket": "ZX-42", ... },
777
+ "patch": { "priority": "high" },
778
+ "changed": true
779
+ }
780
+ ```
781
+
782
+ Behavior: apply `patch` to `original`. Return the merged frontmatter.
783
+
784
+ Output:
785
+ ```json
786
+ {
787
+ "ok": true,
788
+ "result": {
789
+ "changed": true,
790
+ "frontmatter": { "title": "...", "priority": "high", "vendorTicket": "ZX-42", ... }
791
+ }
792
+ }
793
+ ```
794
+
795
+ - `changed` indicates whether any field actually changed value.
796
+ - `frontmatter` MUST include all unknown fields from `original` unchanged.
797
+
798
+ ---
799
+
800
+ #### `op.complete_nonrecurring`
801
+
802
+ Tests §5.5 non-recurring completion.
803
+
804
+ Input:
805
+ ```json
806
+ {
807
+ "frontmatter": { "title": "...", "status": "open" },
808
+ "completedValues": ["done", "cancelled"],
809
+ "explicitDate": "2026-02-20"
810
+ }
811
+ ```
812
+
813
+ Behavior: apply a complete operation to `frontmatter` using `completedValues` (ordered list) and `explicitDate` as the completion day (omit to use current local day).
814
+
815
+ Output:
816
+ ```json
817
+ { "ok": true, "result": { "status": "done", "completedDate": "2026-02-20" } }
818
+ ```
819
+
820
+ - `status` MUST be the first entry in `completedValues`.
821
+ - `completedDate` MUST be `explicitDate` if provided; otherwise a valid `YYYY-MM-DD` date.
822
+ - If `frontmatter.status` is already a completed value, the operation MUST still succeed (idempotent).
823
+
824
+ ---
825
+
826
+ #### `op.uncomplete_nonrecurring`
827
+
828
+ Tests §5.6 non-recurring uncompletion.
829
+
830
+ Input:
831
+ ```json
832
+ {
833
+ "frontmatter": { "title": "...", "status": "done", "completedDate": "2026-02-20" },
834
+ "defaultStatus": "open",
835
+ "clearCompletedDate": true
836
+ }
837
+ ```
838
+
839
+ Output:
840
+ ```json
841
+ { "ok": true, "result": { "status": "open", "completedDate": null } }
842
+ ```
843
+
844
+ - `status` MUST be `defaultStatus`.
845
+ - If `clearCompletedDate=true`, `completedDate` MUST be `null` in the result.
846
+ - If `clearCompletedDate=false`, `completedDate` MUST be preserved.
847
+
848
+ ---
849
+
850
+ #### `op.error_shape`
851
+
852
+ Tests §5.18 error model.
853
+
854
+ Input:
855
+ ```json
856
+ { "operation": "update", "code": "invalid_type", "message": "bad value", "field": "status" }
857
+ ```
858
+
859
+ Behavior: construct and return a structured error object.
860
+
861
+ Output:
862
+ ```json
863
+ {
864
+ "ok": true,
865
+ "result": {
866
+ "operation": "update",
867
+ "code": "invalid_type",
868
+ "message": "..."
869
+ }
870
+ }
871
+ ```
872
+
873
+ The `message` field MUST be a non-empty string. The `operation` and `code` fields MUST match the input.
874
+
875
+ ---
876
+
877
+ #### `op.detect_conflict`
878
+
879
+ Tests §5.16 write-conflict detection.
880
+
881
+ Input:
882
+ ```json
883
+ { "expectedVersion": "a", "actualVersion": "b", "overwrite": false }
884
+ ```
885
+
886
+ Behavior: when `expectedVersion != actualVersion` and `overwrite=false`, the operation MUST fail.
887
+
888
+ Output on conflict:
889
+ ```json
890
+ { "ok": false, "error": "conflict|write_conflict|..." }
891
+ ```
892
+
893
+ ---
894
+
895
+ #### `op.dry_run`
896
+
897
+ Tests §5.17 dry-run mode.
898
+
899
+ Input:
900
+ ```json
901
+ { "operation": "update", "patch": { "status": "done" } }
902
+ ```
903
+
904
+ Output:
905
+ ```json
906
+ {
907
+ "ok": true,
908
+ "result": {
909
+ "wrote": false,
910
+ "plannedChanges": ["status", ...]
911
+ }
912
+ }
913
+ ```
914
+
915
+ `wrote` MUST be `false`. `plannedChanges` MUST list the field names that would have changed.
916
+
917
+ ---
918
+
919
+ ### Recurrence operations
920
+
921
+ For `recurrence.complete`, `recurrence.uncomplete_instance`, `recurrence.skip_instance`, `recurrence.unskip_instance`, and `recurrence.effective_state`, the input and output contracts are defined by the respective sections of §4 and §5.7–5.9.
922
+
923
+ Common input fields for recurrence operations:
924
+ - `targetDate` or `completionDate`: `YYYY-MM-DD` target day
925
+ - `completeInstances`: current list of completed dates
926
+ - `skippedInstances`: current list of skipped dates
927
+
928
+ `recurrence.effective_state` output:
929
+ ```json
930
+ { "ok": true, "result": { "value": "completed|skipped|open" } }
931
+ ```
932
+
933
+ ---
934
+
935
+ ### Materialized occurrence operations
936
+
937
+ For `occurrence.materialize`, `occurrence.complete`, `occurrence.skip`, `occurrence.uncomplete`, `occurrence.unskip`, and `occurrence.unmaterialize`, input and output contracts are defined by §4.18 and §5.20.
938
+
939
+ Common input fields for materialized occurrence operations:
940
+ - `parent`: frontmatter/path/link object for the recurring parent task
941
+ - `occurrence`: frontmatter/path/link object for an existing occurrence note when applicable
942
+ - `targetDate`: `YYYY-MM-DD` target day
943
+ - `config`: effective occurrence/status configuration needed by the operation
944
+
945
+ `occurrence.materialize` output:
946
+ ```json
947
+ { "ok": true, "result": { "created": true, "occurrence": { "...": "..." } } }
948
+ ```
949
+
950
+ `created` MUST be `false` when the operation returns an already-existing occurrence note for the same parent/date.
951
+
952
+ ---
953
+
954
+ ### Time-tracking operations
955
+
956
+ For `time.start`, `time.stop`, `time.replace_entries`, `time.remove_entry`, `time.auto_stop_on_complete`, and `time.report_totals`, the contracts are defined by §5.19.
957
+
958
+ Common input patterns:
959
+ - `entries`: current `time_entries` array
960
+ - `now`: ISO 8601 datetime representing current instant
961
+ - `dateModified`: current `date_modified` value
962
+
963
+ `time.report_totals` output:
964
+ ```json
965
+ { "ok": true, "result": { "closed_minutes": N, "live_minutes": M } }
966
+ ```
967
+
968
+ `live_minutes` is only present when an active (no `endTime`) entry exists.