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
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# tasknotes-spec
|
|
2
|
+
|
|
3
|
+
A standalone specification for representing and operating on task data stored in markdown files with YAML frontmatter.
|
|
4
|
+
|
|
5
|
+
**Version:** 0.2.0-draft
|
|
6
|
+
**Status:** Draft
|
|
7
|
+
**Canonical Source of Truth:** This specification
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
|
|
11
|
+
`tasknotes-spec` defines behavior for tools that read and write task notes in a shared vault.
|
|
12
|
+
The goal is interoperable behavior across implementations, including full-featured applications and lighter clients.
|
|
13
|
+
|
|
14
|
+
The specification focuses on:
|
|
15
|
+
|
|
16
|
+
- Task data model and field semantics
|
|
17
|
+
- Date, datetime, and timezone rules
|
|
18
|
+
- Time tracking entry and session-management semantics
|
|
19
|
+
- Recurrence, per-instance completion semantics, and optional materialized occurrence notes
|
|
20
|
+
- Link parsing and resolution semantics
|
|
21
|
+
- Dependencies and reminders semantics
|
|
22
|
+
- Optional create-time templating semantics
|
|
23
|
+
- Operation semantics and write side-effects
|
|
24
|
+
- Validation and conformance expectations
|
|
25
|
+
|
|
26
|
+
The specification does not define UI design or plugin-internal architecture.
|
|
27
|
+
|
|
28
|
+
## Why this spec exists
|
|
29
|
+
|
|
30
|
+
Task data in markdown files is useful because it remains readable and editable with standard tools.
|
|
31
|
+
However, multiple tools operating on the same files can diverge in behavior unless they share explicit rules.
|
|
32
|
+
Divergence is most costly in recurring tasks, timezone handling, and completion state transitions.
|
|
33
|
+
|
|
34
|
+
This specification provides a precise contract so independent tools can make compatible reads and writes.
|
|
35
|
+
|
|
36
|
+
## Document structure
|
|
37
|
+
|
|
38
|
+
| Section | File | Content |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| §0 | `00-overview.md` | Motivation, scope, governance, and principles |
|
|
41
|
+
| §1 | `01-terminology.md` | Normative definitions |
|
|
42
|
+
| §2 | `02-model-and-mapping.md` | Task model, canonical semantic roles, field mapping |
|
|
43
|
+
| §3 | `03-temporal-semantics.md` | Date/datetime/timezone semantics and serialization |
|
|
44
|
+
| §4 | `04-recurrence.md` | RRULE semantics, per-instance state, and optional occurrence materialization |
|
|
45
|
+
| §5 | `05-operations.md` | Create/update/complete/skip/delete/rename behaviors |
|
|
46
|
+
| §6 | `06-validation.md` | Validation rules and issue model |
|
|
47
|
+
| §7 | `07-conformance.md` | Conformance profiles and claims |
|
|
48
|
+
| §8 | `08-compatibility-and-migrations.md` | Migration and compatibility policy |
|
|
49
|
+
| §9 | `09-configuration.md` | effective configuration schema and provider model (`tasknotes.yaml`, TaskNotes `data.json`, etc.) |
|
|
50
|
+
| §10 | `10-dependencies-and-reminders.md` | Dependency and reminder semantics |
|
|
51
|
+
| §11 | `11-links.md` | Link syntax, parsing, resolution, and rename update behavior |
|
|
52
|
+
| Changelog | `CHANGELOG.md` | Spec release history |
|
|
53
|
+
|
|
54
|
+
## Conformance model
|
|
55
|
+
|
|
56
|
+
Implementations claim conformance to one or more profiles defined in §7. A conformance claim MUST include:
|
|
57
|
+
|
|
58
|
+
- specification version
|
|
59
|
+
- profile name(s)
|
|
60
|
+
- known deviations
|
|
61
|
+
|
|
62
|
+
## Executable conformance
|
|
63
|
+
|
|
64
|
+
This repository ships a reusable fixture-based conformance suite in `conformance/`.
|
|
65
|
+
|
|
66
|
+
- generate fixtures: `npm run conformance:generate`
|
|
67
|
+
- run suite: `TASKNOTES_ADAPTER=<adapter-path> npm run conformance:test`
|
|
68
|
+
- run against `../mdbase-tasknotes`: `npm run conformance:test:mdbase-tasknotes`
|
|
69
|
+
|
|
70
|
+
## Versioning
|
|
71
|
+
|
|
72
|
+
The specification uses semantic versioning.
|
|
73
|
+
|
|
74
|
+
- Patch: clarifications and non-breaking fixes
|
|
75
|
+
- Minor: additive behavior and optional features
|
|
76
|
+
- Major: breaking semantic changes
|
|
77
|
+
|
|
78
|
+
## License
|
|
79
|
+
|
|
80
|
+
MIT
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Conformance Suite
|
|
2
|
+
|
|
3
|
+
This directory contains the reusable `tasknotes-spec` conformance suite.
|
|
4
|
+
|
|
5
|
+
## Design
|
|
6
|
+
|
|
7
|
+
- Fixtures are language-neutral JSON in `conformance/fixtures`.
|
|
8
|
+
- Assertions are shared in `conformance/lib/matchers.mjs`.
|
|
9
|
+
- Implementations provide adapters per `conformance/docs/ADAPTER_CONTRACT.md`.
|
|
10
|
+
- `conformance/manifest.json` tracks total case count and section/profile coverage.
|
|
11
|
+
- `conformance/tests/coverage.test.mjs` enforces both section representation and minimum-depth thresholds for key sections/operations.
|
|
12
|
+
|
|
13
|
+
## Run
|
|
14
|
+
|
|
15
|
+
Generate fixtures:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm run conformance:generate
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Run against a specific adapter:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
TASKNOTES_ADAPTER=./conformance/adapters/mdbase-tasknotes.adapter.mjs npm run conformance:test
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Convenience command for `../mdbase-tasknotes`:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm run conformance:test:mdbase-tasknotes
|
|
31
|
+
```
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import {
|
|
2
|
+
conformanceMetadata,
|
|
3
|
+
executeConformanceOperation,
|
|
4
|
+
} from "../../../mdbase-tasknotes/dist/conformance.js";
|
|
5
|
+
export const metadata = conformanceMetadata;
|
|
6
|
+
|
|
7
|
+
function normalizeHashtagValue(value) {
|
|
8
|
+
const trimmed = String(value || "").trim();
|
|
9
|
+
const withoutHash = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
10
|
+
return withoutHash.toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function stripCodeFencesAndInlineCode(markdown) {
|
|
14
|
+
const withoutFences = String(markdown || "").replace(/```[\s\S]*?```/g, " ");
|
|
15
|
+
return withoutFences.replace(/`[^`]*`/g, " ");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function bodyHasTag(body, normalizedTag) {
|
|
19
|
+
const searchable = stripCodeFencesAndInlineCode(body);
|
|
20
|
+
const hashtagRegex = /(^|[^\w])#([A-Za-z0-9][A-Za-z0-9/_-]*)/g;
|
|
21
|
+
let match;
|
|
22
|
+
while ((match = hashtagRegex.exec(searchable)) != null) {
|
|
23
|
+
if (match[2].toLowerCase() === normalizedTag) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function frontmatterHasTag(frontmatter, normalizedTag) {
|
|
31
|
+
const tagsValue = frontmatter?.tags;
|
|
32
|
+
const entries = Array.isArray(tagsValue)
|
|
33
|
+
? tagsValue
|
|
34
|
+
: (typeof tagsValue === "string" ? [tagsValue] : []);
|
|
35
|
+
return entries.some((entry) => typeof entry === "string" && normalizeHashtagValue(entry) === normalizedTag);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizeExcludedFolders(value) {
|
|
39
|
+
const normalizePath = (entry) =>
|
|
40
|
+
String(entry || "")
|
|
41
|
+
.replace(/\\/g, "/")
|
|
42
|
+
.replace(/^\/+/, "")
|
|
43
|
+
.replace(/\/+$/, "")
|
|
44
|
+
.trim();
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map(normalizePath).filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
return value.split(",").map(normalizePath).filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pathExcluded(filePath, excludedFolders) {
|
|
55
|
+
const normalizedPath = String(filePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
56
|
+
return excludedFolders.some((folder) =>
|
|
57
|
+
normalizedPath === folder || normalizedPath.startsWith(`${folder}/`));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function detectTaskFile(input) {
|
|
61
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
62
|
+
const detection = payload.taskDetection && typeof payload.taskDetection === "object"
|
|
63
|
+
? payload.taskDetection
|
|
64
|
+
: {};
|
|
65
|
+
const frontmatter = payload.frontmatter && typeof payload.frontmatter === "object"
|
|
66
|
+
? payload.frontmatter
|
|
67
|
+
: {};
|
|
68
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
69
|
+
const filePath = typeof payload.filePath === "string" ? payload.filePath : "";
|
|
70
|
+
|
|
71
|
+
const excludedFolders = normalizeExcludedFolders(detection.excluded_folders);
|
|
72
|
+
if (filePath && pathExcluded(filePath, excludedFolders)) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const methods = Array.isArray(detection.methods)
|
|
77
|
+
? detection.methods.filter((entry) => typeof entry === "string")
|
|
78
|
+
: (typeof detection.method === "string" ? [detection.method] : []);
|
|
79
|
+
const normalizedMethods = methods.map((method) => method.trim().toLowerCase()).filter(Boolean);
|
|
80
|
+
const effectiveMethods = normalizedMethods.length > 0
|
|
81
|
+
? normalizedMethods
|
|
82
|
+
: (typeof detection.tag === "string" ? ["tag"] : []);
|
|
83
|
+
|
|
84
|
+
const evaluations = [];
|
|
85
|
+
for (const method of effectiveMethods) {
|
|
86
|
+
if (method === "tag") {
|
|
87
|
+
const configuredTag = typeof detection.tag === "string" ? detection.tag : "task";
|
|
88
|
+
const normalizedTag = normalizeHashtagValue(configuredTag);
|
|
89
|
+
evaluations.push(
|
|
90
|
+
normalizedTag.length > 0
|
|
91
|
+
&& (frontmatterHasTag(frontmatter, normalizedTag) || bodyHasTag(body, normalizedTag)),
|
|
92
|
+
);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (method === "property") {
|
|
97
|
+
const propertyName = typeof detection.property_name === "string"
|
|
98
|
+
? detection.property_name.trim()
|
|
99
|
+
: "";
|
|
100
|
+
const propertyValue = typeof detection.property_value === "string"
|
|
101
|
+
? detection.property_value
|
|
102
|
+
: "";
|
|
103
|
+
if (!propertyName || !Object.prototype.hasOwnProperty.call(frontmatter, propertyName)) {
|
|
104
|
+
evaluations.push(false);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
evaluations.push(propertyValue.length === 0 || String(frontmatter[propertyName]) === propertyValue);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (evaluations.length === 0) return false;
|
|
112
|
+
const combine = detection.combine === "and" ? "and" : "or";
|
|
113
|
+
return combine === "and" ? evaluations.every(Boolean) : evaluations.some(Boolean);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function execute(operation, input) {
|
|
117
|
+
if (operation === "config.detect_task_file") {
|
|
118
|
+
return { ok: true, result: { value: detectTaskFile(input) } };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (operation === "dependency.missing_target_behavior") {
|
|
122
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
123
|
+
const severity = payload.unresolvedTargetSeverity === "error" ? "error" : "warning";
|
|
124
|
+
const requireResolvedUidOnWrite = payload.requireResolvedUidOnWrite === true;
|
|
125
|
+
const treatMissingTargetAsBlocked = payload.treatMissingTargetAsBlocked !== false;
|
|
126
|
+
const onWrite = payload.onWrite === true;
|
|
127
|
+
if (requireResolvedUidOnWrite && onWrite) {
|
|
128
|
+
return { ok: false, error: "unresolved_dependency_target require_resolved_uid_on_write" };
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
ok: true,
|
|
132
|
+
result: {
|
|
133
|
+
blocked: treatMissingTargetAsBlocked,
|
|
134
|
+
issue: "unresolved_dependency_target",
|
|
135
|
+
severity,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return executeConformanceOperation(operation, input);
|
|
141
|
+
}
|