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,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
|