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,266 @@
1
+ # 10. Dependencies and Reminders
2
+
3
+ ## 10.1 Purpose
4
+
5
+ This section defines canonical semantics for task dependencies (`blocked_by`) and task reminders (`reminders`).
6
+
7
+ ## 10.2 Dependencies (`blocked_by`)
8
+
9
+ ### 10.2.1 Data shape
10
+
11
+ `blocked_by` MUST be a list of dependency entries.
12
+
13
+ Each entry MUST be an object with:
14
+
15
+ - `uid` (required): reference to a blocking task (link or string)
16
+ - `reltype` (required): one of
17
+ - `FINISHTOSTART`
18
+ - `STARTTOSTART`
19
+ - `FINISHTOFINISH`
20
+ - `STARTTOFINISH`
21
+ - `gap` (optional): ISO 8601 duration string
22
+
23
+ If `reltype` is omitted on read in compatibility mode, implementations MAY treat it as `FINISHTOSTART` and SHOULD emit a warning.
24
+
25
+ ### 10.2.2 UID normalization
26
+
27
+ Implementations SHOULD normalize `uid` values to a canonical reference representation on write.
28
+
29
+ If link formats are supported, both wikilink and markdown-link forms MAY be read. Canonical write form MUST be documented.
30
+ Link parsing and resolution MUST follow §11.
31
+
32
+ ### 10.2.3 Dependency uniqueness
33
+
34
+ Within a task, dependency entries are keyed by normalized `uid`.
35
+
36
+ Policy is configuration-driven and MUST be deterministic:
37
+
38
+ - when `dependencies.enforce_unique_uid=true` (default), duplicate normalized `uid` values MUST NOT be accepted as canonical persisted state:
39
+ - strict mode: duplicates MUST fail validation/write with `duplicate_dependency_uid`;
40
+ - permissive mode: implementations MAY either fail or normalize to one entry, but MUST emit `duplicate_dependency_uid`.
41
+ - when `dependencies.enforce_unique_uid=false`, duplicates MAY be preserved as compatibility behavior.
42
+
43
+ The active policy MUST be documented in conformance claims.
44
+
45
+ ### 10.2.4 Self-dependency
46
+
47
+ A task MUST NOT depend on itself.
48
+
49
+ Self-dependency is a validation error.
50
+
51
+ ### 10.2.5 Blocking semantics (v0.1)
52
+
53
+ For v0.1, blocking evaluation MUST be status-presence based:
54
+
55
+ 1. A task is blocked if it has at least one unresolved dependency.
56
+ 2. A dependency on a resolvable task is unresolved when the referenced task is not in a completed status.
57
+ 3. Completed status MUST be determined from configured `status.completed_values`.
58
+ 4. For recurring referenced tasks in v0.1, unresolved/resolved evaluation MUST use base `status` only; `complete_instances`/`skipped_instances` are not consulted.
59
+ 5. When a referenced task is missing/unresolvable, whether that missing target contributes to blocked-state evaluation MUST follow `dependencies.treat_missing_target_as_blocked` (§9.11, default `true`).
60
+
61
+ For v0.1, `reltype` and `gap` are preserved and validated but MUST NOT change the unresolved/resolved decision.
62
+
63
+ ### 10.2.6 Missing referenced task
64
+
65
+ When `uid` cannot be resolved:
66
+
67
+ - implementations SHOULD emit `unresolved_dependency_target`.
68
+ - blocked-state contribution for this missing target MUST follow `dependencies.treat_missing_target_as_blocked` (§9.11).
69
+ - issue severity SHOULD follow `dependencies.unresolved_target_severity` (default: `warning`).
70
+ - for `blocked_by.uid`, this dependency-specific severity policy MUST take precedence over `links.unresolved_default_severity`.
71
+ - parse failures that produce `invalid_link_format` and containment violations that produce `path_traversal` remain error-severity conditions from §11.
72
+ - if `dependencies.require_resolved_uid_on_write=true`, add/update MUST fail with error.
73
+
74
+ ### 10.2.7 Cycles
75
+
76
+ Implementations MUST reject direct self-cycles.
77
+
78
+ Detection of multi-hop dependency cycles is RECOMMENDED but not required for `core-lite` or `recurrence` profiles.
79
+
80
+ ### 10.2.8 Reverse relation field
81
+
82
+ Some implementations maintain reverse relationships (for example `blocking`). Such fields are derived and non-canonical in this spec unless explicitly mapped.
83
+
84
+ If maintained, reverse updates SHOULD remain consistent with `blocked_by` edits.
85
+
86
+ ### 10.2.9 Dependency operation semantics
87
+
88
+ #### Add dependency
89
+
90
+ Add dependency MUST:
91
+
92
+ - validate entry shape,
93
+ - handle duplicates by policy (preserve, normalize, or reject),
94
+ - preserve unrelated dependency entries,
95
+ - update `date_modified` on change.
96
+
97
+ #### Remove dependency
98
+
99
+ Remove dependency MUST remove by normalized `uid` and be idempotent.
100
+
101
+ #### Replace dependencies
102
+
103
+ Replace behavior MUST be explicit operation mode (not default for patch update).
104
+
105
+ ### 10.2.10 Dependency examples
106
+
107
+ Example entry:
108
+
109
+ ```yaml
110
+ blockedBy:
111
+ - uid: "[[prepare-metrics]]"
112
+ reltype: FINISHTOSTART
113
+ gap: PT4H
114
+ ```
115
+
116
+ Example unresolved dependency (target missing):
117
+
118
+ ```yaml
119
+ blockedBy:
120
+ - uid: "[[non-existent-task]]"
121
+ reltype: FINISHTOSTART
122
+ ```
123
+
124
+ ## 10.3 Reminders (`reminders`)
125
+
126
+ ### 10.3.1 Data shape
127
+
128
+ `reminders` MUST be a list of reminder entries.
129
+
130
+ Each reminder MUST include:
131
+
132
+ - `id` (required): unique string within the task
133
+ - `type` (required): `absolute` or `relative`
134
+
135
+ Optional field:
136
+
137
+ - `description` string
138
+
139
+ Type-specific fields:
140
+
141
+ - `absolute`: requires `absoluteTime` datetime
142
+ - `relative`: requires `relatedTo` (`due` or `scheduled`) and `offset` (ISO 8601 duration)
143
+
144
+ ### 10.3.2 Reminder uniqueness
145
+
146
+ Reminder `id` values MUST be unique within the task.
147
+
148
+ If duplicates occur:
149
+
150
+ - strict mode: MUST raise `duplicate_reminder_id`.
151
+ - permissive mode: MAY normalize deterministically and emit warning.
152
+
153
+ ### 10.3.3 Relative reminder base
154
+
155
+ `relatedTo` MUST reference semantic role `due` or `scheduled`.
156
+
157
+ If the referenced role is absent, the reminder is unresolved.
158
+
159
+ ### 10.3.4 Trigger instant computation
160
+
161
+ Trigger instant MUST be computed as follows:
162
+
163
+ 1. If `type=absolute`, trigger = `absoluteTime` instant.
164
+ 2. If `type=relative`,
165
+ - resolve base field from `relatedTo`,
166
+ - if base is datetime: use that instant,
167
+ - if base is date: convert to local datetime at configured `reminders.date_only_anchor_time`,
168
+ - trigger = base + `offset`.
169
+
170
+ If `reminders.date_only_anchor_time` is not configured, implementations MUST use `00:00` local time.
171
+
172
+ ### 10.3.5 Offset semantics
173
+
174
+ `offset` MUST be a valid ISO 8601 duration with optional sign.
175
+
176
+ - negative offset = before base
177
+ - positive offset = after base
178
+ - zero offset = at base instant
179
+
180
+ ### 10.3.6 Invalid mixed fields
181
+
182
+ Entries with incompatible field combinations MUST fail validation in strict mode.
183
+
184
+ Examples:
185
+
186
+ - `type=absolute` with `relatedTo` and no `absoluteTime`
187
+ - `type=relative` with `absoluteTime` and missing `offset`
188
+
189
+ ### 10.3.7 Reminder ordering
190
+
191
+ When computing reminder schedules, implementations MUST sort by trigger instant ascending.
192
+
193
+ For identical trigger instants, implementations MUST use stable tie-break ordering by `id` ascending.
194
+
195
+ ### 10.3.8 Reminder operation semantics
196
+
197
+ #### Add reminder
198
+
199
+ Add reminder MUST validate shape and unique `id`, then update `date_modified`.
200
+
201
+ If `id` is omitted by caller and implementation supports auto-ID generation, generated IDs MUST be unique within the task.
202
+
203
+ #### Update reminder
204
+
205
+ Update reminder MUST address reminders by `id` and be patch-by-default.
206
+
207
+ #### Remove reminder
208
+
209
+ Remove reminder by `id` MUST be idempotent.
210
+
211
+ ### 10.3.9 Default reminders
212
+
213
+ If `defaults.reminders` is configured (see §9):
214
+
215
+ - create without explicit reminders MUST apply defaults.
216
+ - create with explicit reminders MUST follow `reminders.apply_defaults_when_explicit` (§9.15):
217
+ - `false` (default): explicit reminders replace defaults.
218
+ - `true`: explicit reminders are merged with defaults.
219
+
220
+ Deterministic merge rules when `apply_defaults_when_explicit=true`:
221
+
222
+ 1. Start with explicit reminders in caller order.
223
+ 2. Append each default reminder whose `id` is not already present.
224
+ 3. If explicit and default reminders share an `id`, explicit reminder MUST win.
225
+
226
+ ### 10.3.10 Reminder examples
227
+
228
+ Relative reminder:
229
+
230
+ ```yaml
231
+ reminders:
232
+ - id: due_minus_15m
233
+ type: relative
234
+ relatedTo: due
235
+ offset: -PT15M
236
+ ```
237
+
238
+ Absolute reminder:
239
+
240
+ ```yaml
241
+ reminders:
242
+ - id: call_now
243
+ type: absolute
244
+ absoluteTime: 2026-02-20T09:00:00Z
245
+ ```
246
+
247
+ ### 10.3.11 Reminder resolution error handling
248
+
249
+ If relative reminder base is unavailable (`relatedTo=due` but no `due` field):
250
+
251
+ - strict mode: validation error `unresolvable_reminder_base`.
252
+ - permissive mode: warning and reminder excluded from trigger computation.
253
+
254
+ ## 10.4 Interactions
255
+
256
+ ### 10.4.1 Rename interaction
257
+
258
+ If implementation supports rename/reference updates, dependency `uid` references SHOULD be updated consistently with other link references.
259
+
260
+ ### 10.4.2 Completion interaction
261
+
262
+ Changing task completion state does not automatically remove dependencies or reminders unless explicitly configured.
263
+
264
+ ### 10.4.3 Archive interaction
265
+
266
+ Archive operations MAY suppress reminder delivery in runtime systems, but archive MUST NOT silently delete reminder records from persisted frontmatter unless explicitly requested.
package/11-links.md ADDED
@@ -0,0 +1,373 @@
1
+ # 11. Links
2
+
3
+ ## 11.1 Purpose
4
+
5
+ This section defines link syntax, parsing, resolution, and write-format semantics for link-bearing task fields — primarily `projects` and `blocked_by.uid`.
6
+ Alignment with Obsidian and other ecosystems is informative context only; normative conformance is defined entirely by this section.
7
+
8
+ Applicability:
9
+
10
+ - Implementations claiming profile `extended` (§7.3.5) MUST conform to this section for supported link-bearing roles.
11
+ - Implementations claiming profile `materialized-occurrences` (§7.3.4) MUST conform to this section for `recurrence_parent` links, but are not required to support unrelated extended link-bearing roles solely because of that profile.
12
+ - Implementations that claim neither `extended` nor `materialized-occurrences` MAY treat link-shaped strings as opaque data and are not required to implement §11 behavior.
13
+
14
+ ---
15
+
16
+ ## 11.2 Link formats
17
+
18
+ Implementations that conform to §11 (see §11.1 applicability) MUST support three link formats.
19
+
20
+ ### 11.2.1 Wikilinks
21
+
22
+ ```
23
+ [[target]]
24
+ [[target|alias]]
25
+ [[target#anchor]]
26
+ [[target#anchor|alias]]
27
+ [[folder/target]]
28
+ [[./relative]]
29
+ [[../parent/target]]
30
+ ```
31
+
32
+ **Components:**
33
+ - **target**: The file being linked to (without extension by default)
34
+ - **alias**: Display text; does not affect resolution
35
+ - **anchor**: A heading or block reference within the target
36
+ - **path**: May be absolute (from collection root) or relative (from current file)
37
+
38
+ Examples in frontmatter:
39
+
40
+ ```yaml
41
+ projects:
42
+ - "[[home-project]]"
43
+ - "[[projects/alpha|Alpha Project]]"
44
+ blockedBy:
45
+ - uid: "[[prepare-metrics]]"
46
+ reltype: FINISHTOSTART
47
+ ```
48
+
49
+ ### 11.2.2 Markdown links
50
+
51
+ ```
52
+ [text](path.md)
53
+ [text](./relative.md)
54
+ [text](../other/file.md)
55
+ [text](path.md#anchor)
56
+ ```
57
+
58
+ The text portion is treated as an alias and does not affect resolution.
59
+
60
+ Markdown links in frontmatter require the `obsidian-frontmatter-markdown-links` Obsidian plugin. Implementations MUST NOT write markdown-format links by default unless `links.use_markdown_format=true` is configured (§11.7).
61
+
62
+ ### 11.2.3 Bare paths
63
+
64
+ ```
65
+ ./sibling.md
66
+ ../other/file.md
67
+ folder/file.md
68
+ ```
69
+
70
+ Bare paths follow the same resolution rules as markdown links (relative to containing file's directory unless starting with `/`).
71
+
72
+ ---
73
+
74
+ ## 11.3 Link parsing
75
+
76
+ When a link value is read, implementations MUST parse it into a structured representation:
77
+
78
+ | Component | Type | Description |
79
+ |---|---|---|
80
+ | `raw` | string | Original string value exactly as written |
81
+ | `target` | string | File path or identifier (without anchor or alias) |
82
+ | `alias` | string? | Display text if provided, otherwise null |
83
+ | `anchor` | string? | Heading or block reference if provided, otherwise null |
84
+ | `format` | enum | One of: `wikilink`, `markdown`, `path` |
85
+ | `is_relative` | boolean | Whether target begins with `./` or `../` |
86
+
87
+ Parsing examples:
88
+
89
+ | Input | target | alias | anchor | format | is_relative |
90
+ |---|---|---|---|---|---|
91
+ | `[[task-001]]` | `task-001` | null | null | wikilink | false |
92
+ | `[[task-001\|My Task]]` | `task-001` | `My Task` | null | wikilink | false |
93
+ | `[[docs/api#auth]]` | `docs/api` | null | `auth` | wikilink | false |
94
+ | `[[./sibling]]` | `./sibling` | null | null | wikilink | true |
95
+ | `[Link](file.md)` | `file.md` | `Link` | null | markdown | false |
96
+ | `./other.md` | `./other.md` | null | null | path | true |
97
+
98
+ ---
99
+
100
+ ## 11.4 Resolution algorithm
101
+
102
+ Resolution transforms a parsed link into an absolute path (relative to collection root) pointing to the target file.
103
+
104
+ Given a parsed link and the path of the source file containing it:
105
+
106
+ ### Step 1: Parse the link into components (target, format, is_relative)
107
+
108
+ ### Step 2: Route by format
109
+
110
+ **If format is `markdown` or `path`:**
111
+ - If target starts with `/`, resolve from collection root (strip the leading `/`)
112
+ - Otherwise, resolve relative to the source file's directory (standard markdown behavior)
113
+ - Example: link `[Docs](docs/api.md)` in `notes/meeting.md` resolves to `notes/docs/api.md`
114
+
115
+ **If format is `wikilink`:**
116
+ - If target starts with `./` or `../`, resolve relative to the source file's directory
117
+ - If target starts with `/`, resolve from collection root (strip the leading `/`)
118
+ - If target contains `/` (and is not relative), resolve from collection root
119
+ - Example: `[[docs/api]]` resolves to `docs/api`
120
+ - If simple name (no `/`, no `./` or `../`): proceed to Step 3
121
+
122
+ ### Step 3: Simple-name resolution (wikilinks only)
123
+
124
+ For simple wikilink names (no path separator):
125
+
126
+ 1. **Define the search scope:**
127
+ - For `blocked_by.uid`: scope to files matching `task_detection` (task files only)
128
+ - For `projects`: scope to all markdown files in the collection unless narrowed by explicit configuration
129
+
130
+ 2. **ID match pass:** search scoped files for semantic role `id` (§2.6.5) equal to the name.
131
+ - Implementations MAY treat literal frontmatter key `id` as compatibility input when semantic mapping for `id` is unavailable.
132
+ - Equality MUST be exact string equality.
133
+ - If exactly one match: resolve to that file
134
+ - If multiple matches: fail with `ambiguous_link`
135
+
136
+ 3. **Filename match pass:** if no ID match, search scoped markdown files by filename (without extension).
137
+ 4. Implementations MUST deduplicate normalized candidate paths before final selection.
138
+ 5. If exactly one filename candidate remains after normalization and extension handling, resolve to that file.
139
+ 6. If multiple filename candidates remain, resolve to `null` and emit `ambiguous_link`. Callers SHOULD use a path-qualified or relative target to disambiguate.
140
+
141
+ ### Step 4: Extension handling
142
+
143
+ If the target has no extension, implementations SHOULD try configured extensions in order.
144
+
145
+ Default extension order: `[".md"]`.
146
+
147
+ Example: `[[readme]]` tries `readme.md`, `readme.mdx`, etc.
148
+
149
+ ### Step 5: Path traversal check
150
+
151
+ After resolution and normalization, if the resolved path would escape the collection root, abort with `path_traversal`. See §11.5.
152
+
153
+ ### Step 6: Return result
154
+
155
+ - The normalized collection-relative path if found
156
+ - `null` if no matching file exists
157
+
158
+ ### Resolution examples
159
+
160
+ Given collection structure:
161
+ ```
162
+ /
163
+ ├── TaskNotes/
164
+ │ ├── Tasks/
165
+ │ │ ├── task-001.md
166
+ │ │ └── subtasks/
167
+ │ │ └── task-002.md
168
+ ├── notes/
169
+ │ └── meeting.md
170
+ └── projects/
171
+ └── alpha.md
172
+ ```
173
+
174
+ Resolution from `TaskNotes/Tasks/subtasks/task-002.md`:
175
+
176
+ | Link value | Resolved path | Notes |
177
+ |---|---|---|
178
+ | `[[task-001]]` | `TaskNotes/Tasks/task-001.md` | Simple-name search |
179
+ | `[[../task-001]]` | `TaskNotes/Tasks/task-001.md` | Relative wikilink |
180
+ | `[[./task-003]]` | `TaskNotes/Tasks/subtasks/task-003.md` | Relative (may not exist) |
181
+ | `[[notes/meeting]]` | `notes/meeting.md` | Absolute from root |
182
+ | `[[alpha]]` | `projects/alpha.md` | Simple-name search (projects scope) |
183
+ | `[link](../task-001.md)` | `TaskNotes/Tasks/task-001.md` | Markdown, relative |
184
+ | `../task-001.md` | `TaskNotes/Tasks/task-001.md` | Bare path, relative |
185
+
186
+ ---
187
+
188
+ ## 11.5 Path sandboxing
189
+
190
+ Link resolution MUST NOT produce paths outside the collection root.
191
+
192
+ **Rules:**
193
+ - After resolving relative paths (applying `../` segments), the resulting path MUST be within the collection root directory
194
+ - If resolution would escape the collection root, the link MUST resolve to `null` and implementations MUST emit a `path_traversal` error
195
+ - This applies to all link formats: wikilinks, markdown links, and bare paths
196
+ - Implementations MUST normalize paths (resolve `.` and `..` segments) before checking containment
197
+
198
+ **Examples** (collection rooted at `/home/user/MyVault/`):
199
+
200
+ | Link | From file | Result |
201
+ |---|---|---|
202
+ | `[[../../../etc/passwd]]` | `TaskNotes/Tasks/task.md` | `null` + `path_traversal` |
203
+ | `[[../../secrets/key]]` | `deep/nested/file.md` | `null` + `path_traversal` |
204
+ | `[[../task-001]]` | `TaskNotes/Tasks/subtasks/t.md` | Resolves normally |
205
+
206
+ ---
207
+
208
+ ## 11.6 Canonical write format
209
+
210
+ When creating new link values or changing a link target, implementations MUST use a deterministic canonical form.
211
+
212
+ ### Default (wikilink format)
213
+
214
+ By default (`links.use_markdown_format=false`), the canonical write format is a **simple wikilink**:
215
+
216
+ ```yaml
217
+ blockedBy:
218
+ - uid: "[[task-001]]"
219
+ reltype: FINISHTOSTART
220
+ projects:
221
+ - "[[projects/alpha]]"
222
+ ```
223
+
224
+ Rules:
225
+ - Use the filename without extension as the target when the file can be identified by simple-name resolution within the appropriate scope.
226
+ - Use a path-qualified target (`folder/name`) when the simple name would be ambiguous.
227
+ - Do NOT include the alias component in dependency `uid` writes.
228
+ - Do NOT include the anchor component in dependency `uid` writes.
229
+ - Preserve the alias component in `projects` entries when the alias was provided by the user.
230
+
231
+ ### Markdown link format (`links.use_markdown_format=true`)
232
+
233
+ When `links.use_markdown_format=true` is configured (requires the `obsidian-frontmatter-markdown-links` Obsidian plugin), the canonical write format for link-bearing fields is a **markdown link** using the collection-relative path:
234
+
235
+ ```yaml
236
+ blockedBy:
237
+ - uid: "[task-001](TaskNotes/Tasks/task-001.md)"
238
+ reltype: FINISHTOSTART
239
+ ```
240
+
241
+ ### Round-trip preservation
242
+
243
+ When writing an existing link field and the resolved target is unchanged, implementations MUST preserve the original format when possible:
244
+ - If the user wrote `[[task-001|My Task]]`, preserve alias for `projects` entries.
245
+ - If the user wrote a relative path, preserve relativity when possible.
246
+ - For `blocked_by.uid`, alias and anchor components MUST still be removed on canonical writes.
247
+ - If preservation is not possible (for example unresolved/ambiguous reconstruction), implementations MUST fall back to canonical write rules in this section.
248
+
249
+ ---
250
+
251
+ ## 11.7 Configuration
252
+
253
+ `links` configuration keys (see §9.12):
254
+
255
+ ```yaml
256
+ links:
257
+ extensions: [".md"] # Extension trial order for extensionless targets
258
+ use_markdown_format: false # Write markdown links instead of wikilinks
259
+ unresolved_default_severity: warning # "warning" or "error"
260
+ update_references_on_rename: true # Update link targets when files are renamed
261
+ ```
262
+
263
+ Rules:
264
+ - `extensions` MUST be a non-empty list when present
265
+ - `unresolved_default_severity` MUST be `"warning"` or `"error"`
266
+ - `update_references_on_rename=true` enables rename-time link rewrite behavior (§11.9)
267
+ - `use_markdown_format=true` requires the `obsidian-frontmatter-markdown-links` plugin in Obsidian deployments
268
+
269
+ ---
270
+
271
+ ## 11.8 Role-specific rules
272
+
273
+ ### 11.8.1 `projects`
274
+
275
+ `projects` entries SHOULD be interpreted with this section's parsing/resolution rules.
276
+
277
+ If a `projects` entry is a plain string (not a wikilink or markdown link), implementations SHOULD treat it as a bare filename or path and attempt resolution.
278
+
279
+ If unresolved:
280
+ - `links.unresolved_default_severity=error`: validation error
281
+ - otherwise: SHOULD emit `unresolved_link_target` warning
282
+
283
+ ### 11.8.2 `blocked_by.uid`
284
+
285
+ `blocked_by.uid` values MUST use this section's parsing/resolution rules.
286
+
287
+ For dependency semantics, unresolved `uid` handling follows §10.2.6.
288
+ For unresolved-target severity, `dependencies.unresolved_target_severity` controls and takes precedence over `links.unresolved_default_severity`.
289
+
290
+ The canonical write form for `uid` values is specified in §11.6.
291
+
292
+ ---
293
+
294
+ ## 11.9 Rename and reference updates
295
+
296
+ If an implementation supports reference updates on rename (i.e. claims the `rename` capability):
297
+
298
+ 1. It MUST update resolvable references in `blocked_by.uid` and `projects` link fields.
299
+ 2. It SHOULD update links in body content.
300
+ 3. It SHOULD preserve the original link format when possible.
301
+ 4. It MUST preserve alias and anchor components where valid for the target field. For `blocked_by.uid`, alias and anchor are non-canonical and MUST be removed on write (§11.6).
302
+ 5. It MUST report unresolved or ambiguous rewrite cases.
303
+
304
+ If reference updates are not supported, this limitation MUST be disclosed in conformance claims.
305
+
306
+ ---
307
+
308
+ ## 11.10 Validation
309
+
310
+ Link validation issues:
311
+
312
+ | Code | Severity | Trigger |
313
+ |---|---|---|
314
+ | `invalid_link_format` | error | Link value cannot be parsed as any supported format |
315
+ | `ambiguous_link` | warning | Simple-name resolution found multiple candidates after normalization |
316
+ | `unresolved_link_target` | warning | Link target cannot be resolved to an existing file |
317
+ | `path_traversal` | error | Resolved path escapes collection root |
318
+
319
+ `unresolved_link_target` severity may be promoted to error via `links.unresolved_default_severity=error`.
320
+ For `blocked_by.uid`, use `unresolved_dependency_target` severity policy from §10.2.6 instead of `unresolved_link_target`.
321
+
322
+ ---
323
+
324
+ ## 11.11 Examples
325
+
326
+ ### Dependency with wikilink (default)
327
+
328
+ ```yaml
329
+ ---
330
+ title: Implement API
331
+ status: open
332
+ tags: [task]
333
+ blockedBy:
334
+ - uid: "[[design-api]]"
335
+ reltype: FINISHTOSTART
336
+ - uid: "[[projects/infra/setup-server]]"
337
+ reltype: FINISHTOSTART
338
+ gap: P1D
339
+ projects:
340
+ - "[[projects/alpha]]"
341
+ dateCreated: 2026-02-20T10:00:00Z
342
+ dateModified: 2026-02-20T10:00:00Z
343
+ ---
344
+ ```
345
+
346
+ ### Dependency with markdown links (`links.use_markdown_format=true`)
347
+
348
+ ```yaml
349
+ ---
350
+ title: Implement API
351
+ status: open
352
+ tags: [task]
353
+ blockedBy:
354
+ - uid: "[design-api](TaskNotes/Tasks/design-api.md)"
355
+ reltype: FINISHTOSTART
356
+ ---
357
+ ```
358
+
359
+ ### Relative links from nested task
360
+
361
+ ```yaml
362
+ # TaskNotes/Tasks/subtasks/task-002.md
363
+ ---
364
+ title: Sub-task
365
+ status: open
366
+ tags: [task]
367
+ blockedBy:
368
+ - uid: "[[../task-001]]" # resolves to TaskNotes/Tasks/task-001.md
369
+ reltype: FINISHTOSTART
370
+ projects:
371
+ - "[[projects/alpha]]" # resolves from collection root
372
+ ---
373
+ ```
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0-draft - 2026-05-31
4
+
5
+ - add optional `materialized-occurrences` profile for recurrence occurrence notes, on-completion materialization, and occurrence-state reconciliation
6
+
7
+ ## 0.1.0-draft - 2026-02-20
8
+
9
+ Initial standalone draft of `tasknotes-spec` including:
10
+
11
+ - motivation, scope, and normative conventions
12
+ - terminology
13
+ - task model and field mapping
14
+ - temporal semantics
15
+ - recurrence semantics
16
+ - operation semantics
17
+ - validation model
18
+ - conformance profiles
19
+ - compatibility and migration policy
20
+ - collection configuration schema and provider model (`tasknotes.yaml`, TaskNotes `data.json`)
21
+ - full dependency (`blocked_by`) semantics
22
+ - full reminder (`reminders`) semantics
23
+ - explicit time-tracking management semantics (`time_entries` lifecycle, start/stop/edit/remove, and completion-triggered auto-stop configuration)
24
+ - explicit links chapter and link-resolution rules for projects/dependencies
25
+ - optional `templating` conformance profile with create-time template expansion/merge semantics