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,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* package-fixtures.mjs
|
|
4
|
+
*
|
|
5
|
+
* Generates fixtures then produces a distributable tarball:
|
|
6
|
+
* tasknotes-conformance-{version}.tar.gz
|
|
7
|
+
*
|
|
8
|
+
* Contents:
|
|
9
|
+
* fixtures/*.json
|
|
10
|
+
* docs/FIXTURE_FORMAT.md
|
|
11
|
+
* docs/ADAPTER_CONTRACT.md
|
|
12
|
+
* docs/RUNNER_GUIDE.md
|
|
13
|
+
* lib/matchers.mjs
|
|
14
|
+
* tests/runner.test.mjs
|
|
15
|
+
* manifest.json
|
|
16
|
+
* README.md
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
+
import { resolve, join } from "node:path";
|
|
22
|
+
|
|
23
|
+
const root = resolve(import.meta.dirname, "../..");
|
|
24
|
+
const conformanceDir = join(root, "conformance");
|
|
25
|
+
|
|
26
|
+
// Read version from package.json
|
|
27
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
28
|
+
const version = pkg.version;
|
|
29
|
+
if (!version) {
|
|
30
|
+
console.error("Error: package.json must have a 'version' field");
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const tarballName = `tasknotes-conformance-${version}.tar.gz`;
|
|
35
|
+
const tarballPath = join(root, tarballName);
|
|
36
|
+
|
|
37
|
+
console.log(`Building conformance package v${version}...`);
|
|
38
|
+
|
|
39
|
+
// Step 1: Generate fixtures
|
|
40
|
+
console.log("Generating fixtures...");
|
|
41
|
+
execSync("node conformance/scripts/generate-fixtures.mjs", {
|
|
42
|
+
cwd: root,
|
|
43
|
+
stdio: "inherit",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Step 2: Verify expected files exist
|
|
47
|
+
const required = [
|
|
48
|
+
"conformance/fixtures",
|
|
49
|
+
"conformance/docs/FIXTURE_FORMAT.md",
|
|
50
|
+
"conformance/docs/ADAPTER_CONTRACT.md",
|
|
51
|
+
"conformance/docs/RUNNER_GUIDE.md",
|
|
52
|
+
"conformance/lib/matchers.mjs",
|
|
53
|
+
"conformance/tests/runner.test.mjs",
|
|
54
|
+
"conformance/manifest.json",
|
|
55
|
+
"conformance/README.md",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
for (const rel of required) {
|
|
59
|
+
if (!existsSync(join(root, rel))) {
|
|
60
|
+
console.error(`Error: required file/directory not found: ${rel}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Step 3: Build the tarball
|
|
66
|
+
// We tar from the conformance/ directory so paths inside are relative to it
|
|
67
|
+
// (fixtures/date.json, docs/FIXTURE_FORMAT.md, etc.)
|
|
68
|
+
console.log(`Creating ${tarballName}...`);
|
|
69
|
+
|
|
70
|
+
// Read manifest to get the list of fixture files
|
|
71
|
+
const manifest = JSON.parse(
|
|
72
|
+
readFileSync(join(conformanceDir, "manifest.json"), "utf8")
|
|
73
|
+
);
|
|
74
|
+
const fixtureFiles = manifest.files.map((f) => `fixtures/${f.file}`);
|
|
75
|
+
|
|
76
|
+
const filesToInclude = [
|
|
77
|
+
...fixtureFiles,
|
|
78
|
+
"docs/FIXTURE_FORMAT.md",
|
|
79
|
+
"docs/ADAPTER_CONTRACT.md",
|
|
80
|
+
"docs/RUNNER_GUIDE.md",
|
|
81
|
+
"lib/matchers.mjs",
|
|
82
|
+
"tests/runner.test.mjs",
|
|
83
|
+
"manifest.json",
|
|
84
|
+
"README.md",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const tarArgs = [
|
|
88
|
+
"tar",
|
|
89
|
+
"-czf",
|
|
90
|
+
tarballPath,
|
|
91
|
+
"-C",
|
|
92
|
+
conformanceDir,
|
|
93
|
+
...filesToInclude,
|
|
94
|
+
].join(" ");
|
|
95
|
+
|
|
96
|
+
execSync(tarArgs, { cwd: root, stdio: "inherit" });
|
|
97
|
+
|
|
98
|
+
console.log(`\nPackage created: ${tarballName}`);
|
|
99
|
+
console.log(` Version: ${version}`);
|
|
100
|
+
console.log(` Fixtures: ${manifest.totalCases} cases across ${manifest.files.length} files`);
|
|
101
|
+
console.log(` Path: ${tarballPath}`);
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
const manifest = JSON.parse(
|
|
7
|
+
readFileSync(resolve(process.cwd(), "conformance", "manifest.json"), "utf8"),
|
|
8
|
+
);
|
|
9
|
+
const templatingFixtures = JSON.parse(
|
|
10
|
+
readFileSync(resolve(process.cwd(), "conformance", "fixtures", "templating.json"), "utf8"),
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const requiredSections = [
|
|
14
|
+
"§2",
|
|
15
|
+
"§3",
|
|
16
|
+
"§4",
|
|
17
|
+
"§5.2",
|
|
18
|
+
"§5.2.1",
|
|
19
|
+
"§5.3",
|
|
20
|
+
"§5.3.5",
|
|
21
|
+
"§5.4",
|
|
22
|
+
"§5.5",
|
|
23
|
+
"§5.6",
|
|
24
|
+
"§5.7",
|
|
25
|
+
"§5.8",
|
|
26
|
+
"§5.9",
|
|
27
|
+
"§5.10.1",
|
|
28
|
+
"§5.10.2",
|
|
29
|
+
"§5.10.3",
|
|
30
|
+
"§5.11.1",
|
|
31
|
+
"§5.11.2",
|
|
32
|
+
"§5.11.3",
|
|
33
|
+
"§5.12",
|
|
34
|
+
"§5.13",
|
|
35
|
+
"§5.14",
|
|
36
|
+
"§5.15",
|
|
37
|
+
"§5.16",
|
|
38
|
+
"§5.17",
|
|
39
|
+
"§5.18",
|
|
40
|
+
"§5.19.1",
|
|
41
|
+
"§5.19.2",
|
|
42
|
+
"§5.19.3",
|
|
43
|
+
"§5.19.4",
|
|
44
|
+
"§5.19.5",
|
|
45
|
+
"§5.19.6",
|
|
46
|
+
"§5.21",
|
|
47
|
+
"§6",
|
|
48
|
+
"§7.10",
|
|
49
|
+
"§7.11",
|
|
50
|
+
"§8.2",
|
|
51
|
+
"§8.3",
|
|
52
|
+
"§8.4",
|
|
53
|
+
"§8.5",
|
|
54
|
+
"§8.6",
|
|
55
|
+
"§8.7",
|
|
56
|
+
"§8.8",
|
|
57
|
+
"§8.9",
|
|
58
|
+
"§8.10",
|
|
59
|
+
"§8.11",
|
|
60
|
+
"§8.12",
|
|
61
|
+
"§8.13",
|
|
62
|
+
"§8.14",
|
|
63
|
+
"§9",
|
|
64
|
+
"§9.7.1",
|
|
65
|
+
"§9.2.3",
|
|
66
|
+
"§9.2.4",
|
|
67
|
+
"§9.19",
|
|
68
|
+
"§9.20",
|
|
69
|
+
"§10",
|
|
70
|
+
"§11",
|
|
71
|
+
"§11.5",
|
|
72
|
+
"§11.7",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
const sectionMinimums = {
|
|
76
|
+
"§2": 100,
|
|
77
|
+
"§3": 1200,
|
|
78
|
+
"§4": 800,
|
|
79
|
+
"§5.3": 250,
|
|
80
|
+
"§6": 50,
|
|
81
|
+
"§9": 500,
|
|
82
|
+
"§9.7.1": 10,
|
|
83
|
+
"§10": 800,
|
|
84
|
+
"§11": 10,
|
|
85
|
+
"§11.3": 20,
|
|
86
|
+
"§11.6": 2,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const operationMinimums = {
|
|
90
|
+
"date.parse_utc": 300,
|
|
91
|
+
"date.parse_local": 300,
|
|
92
|
+
"date.validate": 300,
|
|
93
|
+
"recurrence.complete": 500,
|
|
94
|
+
"recurrence.recalculate": 150,
|
|
95
|
+
"config.resolve_collection_path": 500,
|
|
96
|
+
"config.detect_task_file": 10,
|
|
97
|
+
"validation.core_evaluate": 40,
|
|
98
|
+
"dependency.validate_entry": 300,
|
|
99
|
+
"reminder.validate_entry": 500,
|
|
100
|
+
"link.parse": 20,
|
|
101
|
+
"link.resolve": 10,
|
|
102
|
+
"link.update_references_on_rename": 2,
|
|
103
|
+
"templating.expand_variables": 5,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const fixtureFileMinimums = {
|
|
107
|
+
"date.json": 1000,
|
|
108
|
+
"recurrence.json": 500,
|
|
109
|
+
"config.json": 300,
|
|
110
|
+
"operations.json": 80,
|
|
111
|
+
"dependencies.json": 200,
|
|
112
|
+
"reminders.json": 300,
|
|
113
|
+
"links.json": 30,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
test("conformance coverage: required sections represented", () => {
|
|
117
|
+
for (const section of requiredSections) {
|
|
118
|
+
const count = manifest.bySection?.[section] || 0;
|
|
119
|
+
assert.ok(count > 0, `Expected at least one fixture for ${section}`);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("conformance coverage: key section depth thresholds", () => {
|
|
124
|
+
for (const [section, minimum] of Object.entries(sectionMinimums)) {
|
|
125
|
+
const count = manifest.bySection?.[section] || 0;
|
|
126
|
+
assert.ok(
|
|
127
|
+
count >= minimum,
|
|
128
|
+
`Expected at least ${minimum} fixtures for ${section}, got ${count}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("conformance coverage: operation surface is broad", () => {
|
|
134
|
+
const operations = Object.keys(manifest.byOperation || {});
|
|
135
|
+
assert.ok(operations.length >= 70, `Expected at least 70 operations, got ${operations.length}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("conformance coverage: key operation depth thresholds", () => {
|
|
139
|
+
for (const [operation, minimum] of Object.entries(operationMinimums)) {
|
|
140
|
+
const count = manifest.byOperation?.[operation] || 0;
|
|
141
|
+
assert.ok(
|
|
142
|
+
count >= minimum,
|
|
143
|
+
`Expected at least ${minimum} fixtures for operation ${operation}, got ${count}`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("conformance coverage: key fixture file depth thresholds", () => {
|
|
149
|
+
const fileCounts = new Map((manifest.files || []).map((entry) => [entry.file, entry.cases]));
|
|
150
|
+
for (const [file, minimum] of Object.entries(fixtureFileMinimums)) {
|
|
151
|
+
const count = fileCounts.get(file) || 0;
|
|
152
|
+
assert.ok(
|
|
153
|
+
count >= minimum,
|
|
154
|
+
`Expected at least ${minimum} fixtures in ${file}, got ${count}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("conformance coverage: meta and operation-interface ops represented", () => {
|
|
160
|
+
const requiredOperations = [
|
|
161
|
+
"meta.claim",
|
|
162
|
+
"meta.has_capability",
|
|
163
|
+
"meta.has_profile",
|
|
164
|
+
"op.mutate_with_validation",
|
|
165
|
+
"op.atomic_write",
|
|
166
|
+
"op.idempotency_check",
|
|
167
|
+
"op.update_patch",
|
|
168
|
+
"op.complete_nonrecurring",
|
|
169
|
+
"op.uncomplete_nonrecurring",
|
|
170
|
+
"op.error_shape",
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
for (const operation of requiredOperations) {
|
|
174
|
+
const count = manifest.byOperation?.[operation] || 0;
|
|
175
|
+
assert.ok(count > 0, `Expected at least one fixture for operation ${operation}`);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("conformance coverage: required templating variables represented", () => {
|
|
180
|
+
const requiredVariables = [
|
|
181
|
+
"title",
|
|
182
|
+
"status",
|
|
183
|
+
"priority",
|
|
184
|
+
"dueDate",
|
|
185
|
+
"scheduledDate",
|
|
186
|
+
"details",
|
|
187
|
+
"contexts",
|
|
188
|
+
"tags",
|
|
189
|
+
"hashtags",
|
|
190
|
+
"timeEstimate",
|
|
191
|
+
"parentNote",
|
|
192
|
+
"date",
|
|
193
|
+
"time",
|
|
194
|
+
"year",
|
|
195
|
+
"month",
|
|
196
|
+
"day",
|
|
197
|
+
];
|
|
198
|
+
const covered = new Set();
|
|
199
|
+
|
|
200
|
+
for (const fixture of templatingFixtures) {
|
|
201
|
+
if (fixture.operation !== "templating.expand_variables") continue;
|
|
202
|
+
const template = String(fixture.input?.template || "");
|
|
203
|
+
for (const variable of requiredVariables) {
|
|
204
|
+
if (template.includes(`{{${variable}}}`)) {
|
|
205
|
+
covered.add(variable);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
for (const variable of requiredVariables) {
|
|
211
|
+
assert.ok(covered.has(variable), `Expected templating coverage for variable {{${variable}}}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { loadFixtures } from "../lib/load-fixtures.mjs";
|
|
5
|
+
import { loadAdapter } from "../lib/adapter-loader.mjs";
|
|
6
|
+
import { applyAssertion } from "../lib/matchers.mjs";
|
|
7
|
+
|
|
8
|
+
const fixturesDir = resolve(process.cwd(), "conformance", "fixtures");
|
|
9
|
+
const fixtures = loadFixtures(fixturesDir);
|
|
10
|
+
const adapterPath = process.env.TASKNOTES_ADAPTER;
|
|
11
|
+
const requireFullCoverage = process.env.CONFORMANCE_REQUIRE_FULL_COVERAGE === "1";
|
|
12
|
+
let adapterPromise;
|
|
13
|
+
|
|
14
|
+
function expandProfiles(claimedProfiles) {
|
|
15
|
+
const expanded = new Set(Array.isArray(claimedProfiles) ? claimedProfiles : []);
|
|
16
|
+
if (expanded.has("extended")) {
|
|
17
|
+
expanded.add("recurrence");
|
|
18
|
+
expanded.add("core-lite");
|
|
19
|
+
}
|
|
20
|
+
if (expanded.has("recurrence")) {
|
|
21
|
+
expanded.add("core-lite");
|
|
22
|
+
}
|
|
23
|
+
return expanded;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function getAdapter(t) {
|
|
27
|
+
if (!adapterPath) {
|
|
28
|
+
t.skip("Set TASKNOTES_ADAPTER to run adapter-backed conformance fixtures.");
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
if (!adapterPromise) {
|
|
32
|
+
adapterPromise = loadAdapter();
|
|
33
|
+
}
|
|
34
|
+
return adapterPromise;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
test("conformance: adapter metadata", async (t) => {
|
|
38
|
+
const adapter = await getAdapter(t);
|
|
39
|
+
if (!adapter) return;
|
|
40
|
+
const { metadata } = adapter;
|
|
41
|
+
assert.equal(typeof metadata.implementation, "string");
|
|
42
|
+
assert.equal(typeof metadata.version, "string");
|
|
43
|
+
assert.equal(Array.isArray(metadata.profiles), true);
|
|
44
|
+
assert.equal(Array.isArray(metadata.capabilities), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test(`conformance: run ${fixtures.length} fixture cases`, async (t) => {
|
|
48
|
+
const adapter = await getAdapter(t);
|
|
49
|
+
if (!adapter) return;
|
|
50
|
+
const { metadata, execute } = adapter;
|
|
51
|
+
const effectiveProfiles = expandProfiles(metadata.profiles);
|
|
52
|
+
let executedCount = 0;
|
|
53
|
+
|
|
54
|
+
for (const fixture of fixtures) {
|
|
55
|
+
await t.test(`${fixture.id} ${fixture.operation}`, async () => {
|
|
56
|
+
if (!effectiveProfiles.has(fixture.profile)) {
|
|
57
|
+
if (requireFullCoverage) {
|
|
58
|
+
assert.fail(
|
|
59
|
+
`adapter ${metadata.implementation} missing profile for fixture ${fixture.id}: ${fixture.profile}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
t.skip(
|
|
63
|
+
`adapter ${metadata.implementation} missing profile for fixture: ${fixture.profile}`,
|
|
64
|
+
);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const hasCapability = !fixture.requires
|
|
69
|
+
|| fixture.requires.every((cap) => metadata.capabilities.includes(cap));
|
|
70
|
+
|
|
71
|
+
if (!hasCapability) {
|
|
72
|
+
if (requireFullCoverage) {
|
|
73
|
+
assert.fail(
|
|
74
|
+
`adapter ${metadata.implementation} missing capability for fixture ${fixture.id}: ${fixture.requires.join(", ")}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
t.skip(`adapter ${metadata.implementation} missing capability: ${fixture.requires.join(", ")}`);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const envelope = await execute(fixture.operation, fixture.input);
|
|
82
|
+
executedCount += 1;
|
|
83
|
+
applyAssertion(fixture, envelope);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (requireFullCoverage) {
|
|
88
|
+
assert.equal(
|
|
89
|
+
executedCount,
|
|
90
|
+
fixtures.length,
|
|
91
|
+
`Expected all fixtures to execute when CONFORMANCE_REQUIRE_FULL_COVERAGE=1; executed ${executedCount}/${fixtures.length}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("conformance: adapter path is resolvable", async (t) => {
|
|
97
|
+
const adapter = await getAdapter(t);
|
|
98
|
+
if (!adapter) return;
|
|
99
|
+
const { absPath } = adapter;
|
|
100
|
+
assert.equal(typeof absPath, "string");
|
|
101
|
+
assert.equal(absPath.length > 0, true);
|
|
102
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { loadAdapter } from "../lib/adapter-loader.mjs";
|
|
5
|
+
import { loadFixtures } from "../lib/load-fixtures.mjs";
|
|
6
|
+
|
|
7
|
+
const runtimeOperations = [
|
|
8
|
+
"archive.apply",
|
|
9
|
+
"create_compat.create",
|
|
10
|
+
"dependency.validate_entry",
|
|
11
|
+
"dependency.validate_set",
|
|
12
|
+
"dependency.add",
|
|
13
|
+
"dependency.remove",
|
|
14
|
+
"dependency.replace",
|
|
15
|
+
"dependency.missing_target_behavior",
|
|
16
|
+
"op.complete_nonrecurring",
|
|
17
|
+
"op.uncomplete_nonrecurring",
|
|
18
|
+
"recurrence.complete",
|
|
19
|
+
"recurrence.recalculate",
|
|
20
|
+
"recurrence.uncomplete_instance",
|
|
21
|
+
"recurrence.skip_instance",
|
|
22
|
+
"recurrence.unskip_instance",
|
|
23
|
+
"recurrence.effective_state",
|
|
24
|
+
"rename.title_storage_interaction",
|
|
25
|
+
"reminder.validate_entry",
|
|
26
|
+
"reminder.validate_set",
|
|
27
|
+
"reminder.add",
|
|
28
|
+
"reminder.update",
|
|
29
|
+
"reminder.remove",
|
|
30
|
+
"time.start",
|
|
31
|
+
"time.stop",
|
|
32
|
+
"time.remove_entry",
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
test("tasknotes: runtime-routed operation coverage", async (t) => {
|
|
36
|
+
if (!process.env.TASKNOTES_ADAPTER) {
|
|
37
|
+
t.skip("Set TASKNOTES_ADAPTER to validate adapter-specific runtime routing.");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const adapter = await loadAdapter();
|
|
42
|
+
if (adapter.metadata?.implementation !== "tasknotes") {
|
|
43
|
+
t.skip("Runtime routing guard is only enforced for the tasknotes adapter.");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const operation of runtimeOperations) {
|
|
48
|
+
const envelope = await adapter.execute("meta.route_probe", { operation });
|
|
49
|
+
assert.equal(envelope.ok, true, `route probe failed for operation: ${operation}`);
|
|
50
|
+
assert.equal(
|
|
51
|
+
envelope.result?.runtime,
|
|
52
|
+
true,
|
|
53
|
+
`operation is no longer runtime-routed: ${operation}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const fixtures = loadFixtures(resolve(process.cwd(), "conformance", "fixtures"));
|
|
58
|
+
const runtimeFixtureCount = fixtures.filter((fixture) => runtimeOperations.includes(fixture.operation)).length;
|
|
59
|
+
assert.equal(
|
|
60
|
+
runtimeFixtureCount >= 2300,
|
|
61
|
+
true,
|
|
62
|
+
`Expected at least 2300 runtime-routed fixtures, got ${runtimeFixtureCount}`,
|
|
63
|
+
);
|
|
64
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tasknotes-spec",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Specification and conformance fixtures for TaskNotes-compatible markdown task files.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/callumalpass/tasknotes-spec.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/callumalpass/tasknotes-spec/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/callumalpass/tasknotes-spec#readme",
|
|
15
|
+
"files": [
|
|
16
|
+
"00-overview.md",
|
|
17
|
+
"01-terminology.md",
|
|
18
|
+
"02-model-and-mapping.md",
|
|
19
|
+
"03-temporal-semantics.md",
|
|
20
|
+
"04-recurrence.md",
|
|
21
|
+
"05-operations.md",
|
|
22
|
+
"06-validation.md",
|
|
23
|
+
"07-conformance.md",
|
|
24
|
+
"08-compatibility-and-migrations.md",
|
|
25
|
+
"09-configuration.md",
|
|
26
|
+
"10-dependencies-and-reminders.md",
|
|
27
|
+
"11-links.md",
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"conformance/README.md",
|
|
30
|
+
"conformance/manifest.json",
|
|
31
|
+
"conformance/adapters/*.mjs",
|
|
32
|
+
"conformance/adapters/*.ts",
|
|
33
|
+
"conformance/adapters/tasknotes-core",
|
|
34
|
+
"conformance/docs",
|
|
35
|
+
"conformance/fixtures",
|
|
36
|
+
"conformance/lib",
|
|
37
|
+
"conformance/scripts",
|
|
38
|
+
"conformance/tests"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"conformance:generate": "node conformance/scripts/generate-fixtures.mjs",
|
|
42
|
+
"conformance:test": "node --test conformance/tests/**/*.test.mjs",
|
|
43
|
+
"conformance:build:tasknotes-bridge": "mkdir -p conformance/adapters/.generated && npm --prefix ../tasknotes exec -- esbuild conformance/adapters/tasknotes-core/conformance.ts --bundle --platform=node --format=esm --outfile=conformance/adapters/.generated/tasknotes-conformance-core.mjs && npm --prefix ../tasknotes exec -- esbuild conformance/adapters/tasknotes-date-bridge.ts --bundle --platform=node --format=esm --outfile=conformance/adapters/.generated/tasknotes-date-bridge.mjs && npm --prefix ../tasknotes exec -- esbuild conformance/adapters/tasknotes-templating-bridge.ts --bundle --platform=node --format=cjs --outfile=conformance/adapters/.generated/tasknotes-templating-bridge.cjs && npm --prefix ../tasknotes exec -- esbuild conformance/adapters/tasknotes-runtime-bridge.ts --bundle --platform=node --format=cjs --alias:obsidian=./conformance/adapters/tasknotes-runtime-obsidian-shim.ts --outfile=conformance/adapters/.generated/tasknotes-runtime-bridge.cjs",
|
|
44
|
+
"conformance:test:tasknotes": "npm run conformance:generate && npm run conformance:build:tasknotes-bridge && CONFORMANCE_REQUIRE_FULL_COVERAGE=1 TASKNOTES_ADAPTER=./conformance/adapters/tasknotes.adapter.mjs npm run conformance:test",
|
|
45
|
+
"conformance:test:mdbase-tasknotes": "npm run conformance:generate && (cd ../mdbase-tasknotes && npm run build) && TASKNOTES_ADAPTER=./conformance/adapters/mdbase-tasknotes.adapter.mjs npm run conformance:test",
|
|
46
|
+
"conformance:count": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('conformance/manifest.json','utf8'));console.log(m.totalCases)\"",
|
|
47
|
+
"conformance:package": "node conformance/scripts/package-fixtures.mjs"
|
|
48
|
+
}
|
|
49
|
+
}
|