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
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
+ }