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,84 @@
|
|
|
1
|
+
import { parse as parseYamlImpl, stringify as stringifyYamlImpl } from "../../../tasknotes/node_modules/yaml/dist/index.js";
|
|
2
|
+
|
|
3
|
+
export class App {}
|
|
4
|
+
|
|
5
|
+
export class Vault {}
|
|
6
|
+
|
|
7
|
+
export class TAbstractFile {
|
|
8
|
+
path: string;
|
|
9
|
+
|
|
10
|
+
constructor(path: string) {
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get name(): string {
|
|
15
|
+
return this.path.split("/").pop() || "";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class TFolder extends TAbstractFile {
|
|
20
|
+
children: TAbstractFile[] = [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class TFile extends TAbstractFile {
|
|
24
|
+
parent: { path: string } | null = null;
|
|
25
|
+
vault: unknown = null;
|
|
26
|
+
|
|
27
|
+
get basename(): string {
|
|
28
|
+
const name = this.name;
|
|
29
|
+
const dot = name.lastIndexOf(".");
|
|
30
|
+
return dot > 0 ? name.slice(0, dot) : name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get extension(): string {
|
|
34
|
+
const name = this.name;
|
|
35
|
+
const dot = name.lastIndexOf(".");
|
|
36
|
+
return dot > 0 ? name.slice(dot + 1) : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get stat() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
return { ctime: now, mtime: now, size: 0 };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class Notice {
|
|
46
|
+
message: string;
|
|
47
|
+
|
|
48
|
+
constructor(message: string) {
|
|
49
|
+
this.message = String(message || "");
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function normalizePath(input: string): string {
|
|
54
|
+
return String(input || "")
|
|
55
|
+
.replace(/\\/g, "/")
|
|
56
|
+
.replace(/\/+/g, "/")
|
|
57
|
+
.replace(/^\//, "")
|
|
58
|
+
.replace(/\/$/, "");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseYaml(source: string): unknown {
|
|
62
|
+
return parseYamlImpl(String(source || ""));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function stringifyYaml(value: unknown): string {
|
|
66
|
+
return stringifyYamlImpl(value as Record<string, unknown>);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function parseLinktext(linktext: string): { path: string; subpath: string } {
|
|
70
|
+
const raw = String(linktext || "").trim();
|
|
71
|
+
if (!raw) {
|
|
72
|
+
return { path: "", subpath: "" };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const noAlias = raw.split("|", 1)[0] || raw;
|
|
76
|
+
const hashIndex = noAlias.indexOf("#");
|
|
77
|
+
if (hashIndex >= 0) {
|
|
78
|
+
return {
|
|
79
|
+
path: noAlias.slice(0, hashIndex),
|
|
80
|
+
subpath: noAlias.slice(hashIndex),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return { path: noAlias, subpath: "" };
|
|
84
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
processTemplate,
|
|
3
|
+
mergeTemplateFrontmatter,
|
|
4
|
+
parseTemplateSections,
|
|
5
|
+
processTemplateVariables,
|
|
6
|
+
} from "../../../tasknotes/src/utils/templateProcessor.ts";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
processTemplate,
|
|
10
|
+
mergeTemplateFrontmatter,
|
|
11
|
+
parseTemplateSections,
|
|
12
|
+
processTemplateVariables,
|
|
13
|
+
};
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import {
|
|
3
|
+
conformanceMetadata as baseMetadata,
|
|
4
|
+
executeConformanceOperation as baseExecute,
|
|
5
|
+
} from "./.generated/tasknotes-conformance-core.mjs";
|
|
6
|
+
import * as dateBridge from "./.generated/tasknotes-date-bridge.mjs";
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const { version } = require("../../../tasknotes/package.json");
|
|
10
|
+
const templatingBridge = require("./.generated/tasknotes-templating-bridge.cjs");
|
|
11
|
+
const runtimeBridge = require("./.generated/tasknotes-runtime-bridge.cjs");
|
|
12
|
+
|
|
13
|
+
function uniqueStrings(values) {
|
|
14
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const metadata = {
|
|
18
|
+
...baseMetadata,
|
|
19
|
+
implementation: "tasknotes",
|
|
20
|
+
version,
|
|
21
|
+
profiles: uniqueStrings([...(baseMetadata.profiles || []), "templating"]),
|
|
22
|
+
capabilities: uniqueStrings([...(baseMetadata.capabilities || []), "templating"]),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function envelopeOk(result) {
|
|
26
|
+
return { ok: true, result };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function envelopeErr(error) {
|
|
30
|
+
return { ok: false, error: String(error?.message || error || "unknown_error") };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateDateString(value) {
|
|
34
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
35
|
+
throw new Error(`Invalid date "${value}". Expected YYYY-MM-DD.`);
|
|
36
|
+
}
|
|
37
|
+
dateBridge.parseDateToUTC(value);
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveOperationTargetDate(explicitDate, scheduled, due) {
|
|
42
|
+
const extractValidDatePart = (input) => {
|
|
43
|
+
if (typeof input !== "string" || input.trim().length === 0) return undefined;
|
|
44
|
+
try {
|
|
45
|
+
const part = dateBridge.getDatePart(input.trim());
|
|
46
|
+
return validateDateString(part);
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (typeof explicitDate === "string" && explicitDate.trim().length > 0) {
|
|
53
|
+
return validateDateString(explicitDate.trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const scheduledPart = extractValidDatePart(scheduled);
|
|
57
|
+
if (scheduledPart) return scheduledPart;
|
|
58
|
+
|
|
59
|
+
const duePart = extractValidDatePart(due);
|
|
60
|
+
if (duePart) return duePart;
|
|
61
|
+
|
|
62
|
+
return dateBridge.getCurrentDateString();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function dayInTimezone(instant, timezone) {
|
|
66
|
+
const date = dateBridge.parseDateToUTC(String(instant || ""));
|
|
67
|
+
const zone = typeof timezone === "string" ? timezone.trim() : "";
|
|
68
|
+
if (!zone) {
|
|
69
|
+
throw new Error("timezone missing");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
73
|
+
timeZone: zone,
|
|
74
|
+
year: "numeric",
|
|
75
|
+
month: "2-digit",
|
|
76
|
+
day: "2-digit",
|
|
77
|
+
});
|
|
78
|
+
const parts = formatter.formatToParts(date);
|
|
79
|
+
const year = parts.find((part) => part.type === "year")?.value;
|
|
80
|
+
const month = parts.find((part) => part.type === "month")?.value;
|
|
81
|
+
const day = parts.find((part) => part.type === "day")?.value;
|
|
82
|
+
if (!year || !month || !day) {
|
|
83
|
+
throw new Error("timezone conversion failed");
|
|
84
|
+
}
|
|
85
|
+
return `${year}-${month}-${day}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isPlainObject(value) {
|
|
89
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toStringArray(value) {
|
|
93
|
+
if (Array.isArray(value)) {
|
|
94
|
+
return value.map((item) => String(item ?? "").trim()).filter(Boolean);
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === "string") {
|
|
97
|
+
return value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function toTaskTemplateData(rawValues) {
|
|
103
|
+
const values = isPlainObject(rawValues) ? rawValues : {};
|
|
104
|
+
const toStr = (value) => (value == null ? "" : String(value));
|
|
105
|
+
const timeEstimateRaw = toStr(values.timeEstimate).trim();
|
|
106
|
+
const parsedEstimate = Number.parseFloat(timeEstimateRaw);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
title: toStr(values.title),
|
|
110
|
+
priority: toStr(values.priority),
|
|
111
|
+
status: toStr(values.status),
|
|
112
|
+
contexts: toStringArray(values.contexts),
|
|
113
|
+
tags: toStringArray(values.tags),
|
|
114
|
+
timeEstimate: Number.isFinite(parsedEstimate) ? parsedEstimate : 0,
|
|
115
|
+
dueDate: toStr(values.dueDate),
|
|
116
|
+
scheduledDate: toStr(values.scheduledDate),
|
|
117
|
+
details: toStr(values.details),
|
|
118
|
+
parentNote: toStr(values.parentNote),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function deriveTemplateNow(rawValues) {
|
|
123
|
+
const values = isPlainObject(rawValues) ? rawValues : {};
|
|
124
|
+
const date =
|
|
125
|
+
typeof values.date === "string" && /^\d{4}-\d{2}-\d{2}$/.test(values.date)
|
|
126
|
+
? values.date
|
|
127
|
+
: undefined;
|
|
128
|
+
if (!date) return undefined;
|
|
129
|
+
const time =
|
|
130
|
+
typeof values.time === "string" && /^\d{2}:\d{2}$/.test(values.time)
|
|
131
|
+
? values.time
|
|
132
|
+
: "00:00";
|
|
133
|
+
const candidate = new Date(`${date}T${time}:00`);
|
|
134
|
+
if (Number.isNaN(candidate.getTime())) return undefined;
|
|
135
|
+
return candidate;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function withFrozenNow(nowDate, fn) {
|
|
139
|
+
if (!(nowDate instanceof Date) || Number.isNaN(nowDate.getTime())) {
|
|
140
|
+
return fn();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const RealDate = Date;
|
|
144
|
+
function MockDate(...args) {
|
|
145
|
+
if (args.length === 0) {
|
|
146
|
+
return new RealDate(nowDate.getTime());
|
|
147
|
+
}
|
|
148
|
+
return new RealDate(...args);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
MockDate.UTC = RealDate.UTC;
|
|
152
|
+
MockDate.parse = RealDate.parse;
|
|
153
|
+
MockDate.now = () => nowDate.getTime();
|
|
154
|
+
MockDate.prototype = RealDate.prototype;
|
|
155
|
+
|
|
156
|
+
globalThis.Date = MockDate;
|
|
157
|
+
try {
|
|
158
|
+
return fn();
|
|
159
|
+
} finally {
|
|
160
|
+
globalThis.Date = RealDate;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function executeDateOperation(operation, input) {
|
|
165
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
166
|
+
|
|
167
|
+
if (operation === "date.parse_utc") {
|
|
168
|
+
const parsed = dateBridge.parseDateToUTC(String(payload.value || ""));
|
|
169
|
+
return envelopeOk({ date: parsed.toISOString().slice(0, 10), isoDate: parsed.toISOString().slice(0, 10) });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (operation === "date.parse_local") {
|
|
173
|
+
const parsed = dateBridge.parseDateToLocal(String(payload.value || ""));
|
|
174
|
+
const localDate = `${parsed.getFullYear()}-${String(parsed.getMonth() + 1).padStart(2, "0")}-${String(parsed.getDate()).padStart(2, "0")}`;
|
|
175
|
+
return envelopeOk({ localDate, isoDate: parsed.toISOString().slice(0, 10) });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (operation === "date.validate") {
|
|
179
|
+
return envelopeOk({ value: validateDateString(String(payload.value || "")) });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (operation === "date.get_part") {
|
|
183
|
+
return envelopeOk({ value: dateBridge.getDatePart(String(payload.value || "")) });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (operation === "date.has_time") {
|
|
187
|
+
return envelopeOk({ value: dateBridge.hasTimeComponent(String(payload.value || "")) });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (operation === "date.is_same") {
|
|
191
|
+
return envelopeOk({
|
|
192
|
+
value: dateBridge.isSameDateSafe(String(payload.a || ""), String(payload.b || "")),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (operation === "date.is_before") {
|
|
197
|
+
return envelopeOk({
|
|
198
|
+
value: dateBridge.isBeforeDateSafe(String(payload.a || ""), String(payload.b || "")),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (operation === "date.resolve_operation_target") {
|
|
203
|
+
return envelopeOk({
|
|
204
|
+
value: resolveOperationTargetDate(payload.explicitDate, payload.scheduled, payload.due),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (operation === "date.day_in_timezone") {
|
|
209
|
+
return envelopeOk({ value: dayInTimezone(payload.instant, payload.timezone) });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
throw new Error(`unsupported_operation:${operation}`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function executeTemplatingOperation(operation, input) {
|
|
216
|
+
const payload = isPlainObject(input) ? input : {};
|
|
217
|
+
|
|
218
|
+
if (operation === "templating.parse_sections") {
|
|
219
|
+
const sections = templatingBridge.parseTemplateSections(String(payload.templateText || ""));
|
|
220
|
+
return envelopeOk({
|
|
221
|
+
frontmatterRaw: sections.frontmatter || "",
|
|
222
|
+
body: sections.body,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (operation === "templating.expand_variables") {
|
|
227
|
+
const unknownVariablePolicy =
|
|
228
|
+
typeof payload.unknownVariablePolicy === "string" && payload.unknownVariablePolicy.length > 0
|
|
229
|
+
? payload.unknownVariablePolicy
|
|
230
|
+
: "preserve";
|
|
231
|
+
if (unknownVariablePolicy !== "preserve" && unknownVariablePolicy !== "empty") {
|
|
232
|
+
throw new Error("templating.unknown_variable_policy invalid");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const taskData = toTaskTemplateData(payload.values);
|
|
236
|
+
const nowDate = deriveTemplateNow(payload.values);
|
|
237
|
+
let value = withFrozenNow(nowDate, () =>
|
|
238
|
+
templatingBridge.processTemplateVariables(String(payload.template || ""), taskData),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
if (unknownVariablePolicy === "empty") {
|
|
242
|
+
value = value.replace(/\{\{[^{}]+\}\}/g, "");
|
|
243
|
+
}
|
|
244
|
+
return envelopeOk({ value });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (operation === "templating.tokenize") {
|
|
248
|
+
const template = String(payload.template || "");
|
|
249
|
+
const tokenRegex = /\{\{([^{}]+)\}\}/g;
|
|
250
|
+
const tokens = [];
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = tokenRegex.exec(template)) != null) {
|
|
253
|
+
const token = String(match[1] || "").trim();
|
|
254
|
+
if (token.length > 0) {
|
|
255
|
+
tokens.push(token);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return envelopeOk({ tokens });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (operation === "templating.merge_frontmatter") {
|
|
262
|
+
const baseFrontmatter = isPlainObject(payload.baseFrontmatter) ? payload.baseFrontmatter : {};
|
|
263
|
+
const templateFrontmatter = isPlainObject(payload.templateFrontmatter) ? payload.templateFrontmatter : {};
|
|
264
|
+
return envelopeOk({
|
|
265
|
+
value: templatingBridge.mergeTemplateFrontmatter(baseFrontmatter, templateFrontmatter),
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (operation === "templating.create_pipeline") {
|
|
270
|
+
const baseFrontmatter = isPlainObject(payload.baseFrontmatter) ? payload.baseFrontmatter : {};
|
|
271
|
+
const templateFrontmatter = isPlainObject(payload.templateFrontmatter) ? payload.templateFrontmatter : {};
|
|
272
|
+
const callerBody = String(payload.callerBody || "");
|
|
273
|
+
const templateBody = String(payload.templateBody || "");
|
|
274
|
+
|
|
275
|
+
return envelopeOk({
|
|
276
|
+
frontmatter: templatingBridge.mergeTemplateFrontmatter(baseFrontmatter, templateFrontmatter),
|
|
277
|
+
body: templateBody.trim().length > 0 ? templateBody : callerBody,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (operation === "templating.handle_failure") {
|
|
282
|
+
const failureMode = String(payload.failureMode || "warning_fallback");
|
|
283
|
+
const errorCode = String(payload.errorCode || "template_error");
|
|
284
|
+
if (failureMode === "error") {
|
|
285
|
+
throw new Error(errorCode);
|
|
286
|
+
}
|
|
287
|
+
if (failureMode === "warning_fallback") {
|
|
288
|
+
return envelopeOk({ mode: "fallback" });
|
|
289
|
+
}
|
|
290
|
+
throw new Error("templating.failure_mode invalid");
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (operation === "templating.config_defaults") {
|
|
294
|
+
return envelopeOk({
|
|
295
|
+
failure_mode: "warning_fallback",
|
|
296
|
+
unknown_variable_policy: "preserve",
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (operation === "templating.profile_claim_requirements") {
|
|
301
|
+
const supportsCreateTimeTemplating = payload.supports_create_time_templating === true;
|
|
302
|
+
const supportsFailureMode = payload.supports_failure_mode === true;
|
|
303
|
+
const supportsVariableSet = payload.supports_variable_set === true;
|
|
304
|
+
if (supportsCreateTimeTemplating && supportsFailureMode && supportsVariableSet) {
|
|
305
|
+
return envelopeOk({ value: "claim_valid" });
|
|
306
|
+
}
|
|
307
|
+
throw new Error("templating.claim_requirements_failed");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw new Error(`unsupported_operation:${operation}`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeHashtagValue(value) {
|
|
314
|
+
const trimmed = String(value || "").trim();
|
|
315
|
+
const withoutHash = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
316
|
+
return withoutHash.toLowerCase();
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function stripCodeFencesAndInlineCode(markdown) {
|
|
320
|
+
const withoutFences = String(markdown || "").replace(/```[\s\S]*?```/g, " ");
|
|
321
|
+
return withoutFences.replace(/`[^`]*`/g, " ");
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function bodyHasTag(body, normalizedTag) {
|
|
325
|
+
const searchable = stripCodeFencesAndInlineCode(body);
|
|
326
|
+
const hashtagRegex = /(^|[^\w])#([A-Za-z0-9][A-Za-z0-9/_-]*)/g;
|
|
327
|
+
let match;
|
|
328
|
+
while ((match = hashtagRegex.exec(searchable)) != null) {
|
|
329
|
+
if (match[2].toLowerCase() === normalizedTag) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function frontmatterHasTag(frontmatter, normalizedTag) {
|
|
337
|
+
const tagsValue = frontmatter?.tags;
|
|
338
|
+
const entries = Array.isArray(tagsValue)
|
|
339
|
+
? tagsValue
|
|
340
|
+
: (typeof tagsValue === "string" ? [tagsValue] : []);
|
|
341
|
+
return entries.some((entry) => typeof entry === "string" && normalizeHashtagValue(entry) === normalizedTag);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function normalizeExcludedFolders(value) {
|
|
345
|
+
const normalizePath = (entry) =>
|
|
346
|
+
String(entry || "")
|
|
347
|
+
.replace(/\\/g, "/")
|
|
348
|
+
.replace(/^\/+/, "")
|
|
349
|
+
.replace(/\/+$/, "")
|
|
350
|
+
.trim();
|
|
351
|
+
if (Array.isArray(value)) {
|
|
352
|
+
return value.map(normalizePath).filter(Boolean);
|
|
353
|
+
}
|
|
354
|
+
if (typeof value === "string") {
|
|
355
|
+
return value.split(",").map(normalizePath).filter(Boolean);
|
|
356
|
+
}
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function pathExcluded(filePath, excludedFolders) {
|
|
361
|
+
const normalizedPath = String(filePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
362
|
+
return excludedFolders.some((folder) =>
|
|
363
|
+
normalizedPath === folder || normalizedPath.startsWith(`${folder}/`));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function detectTaskFile(input) {
|
|
367
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
368
|
+
const detection = payload.taskDetection && typeof payload.taskDetection === "object"
|
|
369
|
+
? payload.taskDetection
|
|
370
|
+
: {};
|
|
371
|
+
const frontmatter = payload.frontmatter && typeof payload.frontmatter === "object"
|
|
372
|
+
? payload.frontmatter
|
|
373
|
+
: {};
|
|
374
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
375
|
+
const filePath = typeof payload.filePath === "string" ? payload.filePath : "";
|
|
376
|
+
|
|
377
|
+
const excludedFolders = normalizeExcludedFolders(detection.excluded_folders);
|
|
378
|
+
if (filePath && pathExcluded(filePath, excludedFolders)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const methods = Array.isArray(detection.methods)
|
|
383
|
+
? detection.methods.filter((entry) => typeof entry === "string")
|
|
384
|
+
: (typeof detection.method === "string" ? [detection.method] : []);
|
|
385
|
+
const normalizedMethods = methods.map((method) => method.trim().toLowerCase()).filter(Boolean);
|
|
386
|
+
const effectiveMethods = normalizedMethods.length > 0
|
|
387
|
+
? normalizedMethods
|
|
388
|
+
: (typeof detection.tag === "string" ? ["tag"] : []);
|
|
389
|
+
|
|
390
|
+
const evaluations = [];
|
|
391
|
+
for (const method of effectiveMethods) {
|
|
392
|
+
if (method === "tag") {
|
|
393
|
+
const configuredTag = typeof detection.tag === "string" ? detection.tag : "task";
|
|
394
|
+
const normalizedTag = normalizeHashtagValue(configuredTag);
|
|
395
|
+
evaluations.push(
|
|
396
|
+
normalizedTag.length > 0
|
|
397
|
+
&& (frontmatterHasTag(frontmatter, normalizedTag) || bodyHasTag(body, normalizedTag)),
|
|
398
|
+
);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (method === "property") {
|
|
403
|
+
const propertyName = typeof detection.property_name === "string"
|
|
404
|
+
? detection.property_name.trim()
|
|
405
|
+
: "";
|
|
406
|
+
const propertyValue = typeof detection.property_value === "string"
|
|
407
|
+
? detection.property_value
|
|
408
|
+
: "";
|
|
409
|
+
if (!propertyName || !Object.prototype.hasOwnProperty.call(frontmatter, propertyName)) {
|
|
410
|
+
evaluations.push(false);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
evaluations.push(propertyValue.length === 0 || String(frontmatter[propertyName]) === propertyValue);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (evaluations.length === 0) return false;
|
|
418
|
+
const combine = detection.combine === "and" ? "and" : "or";
|
|
419
|
+
return combine === "and" ? evaluations.every(Boolean) : evaluations.some(Boolean);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export async function execute(operation, input) {
|
|
423
|
+
try {
|
|
424
|
+
if (operation === "meta.claim") {
|
|
425
|
+
return envelopeOk({
|
|
426
|
+
implementation: metadata.implementation,
|
|
427
|
+
version: metadata.version,
|
|
428
|
+
spec_version: "0.2.0-draft",
|
|
429
|
+
profiles: [...metadata.profiles],
|
|
430
|
+
capabilities: [...metadata.capabilities],
|
|
431
|
+
validation_modes: ["strict"],
|
|
432
|
+
known_deviations: ["tasknotes-bridge-date-semantics"],
|
|
433
|
+
compatibility_mode: "bridge",
|
|
434
|
+
configuration_providers: [
|
|
435
|
+
"tasknotes_plugin_settings",
|
|
436
|
+
"bridge_defaults",
|
|
437
|
+
],
|
|
438
|
+
configuration_fallback: "bridge_defaults",
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (operation === "meta.has_capability") {
|
|
443
|
+
const capability =
|
|
444
|
+
input && typeof input === "object" && typeof input.capability === "string"
|
|
445
|
+
? input.capability
|
|
446
|
+
: "";
|
|
447
|
+
return envelopeOk({ value: metadata.capabilities.includes(capability) });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (operation === "meta.has_profile") {
|
|
451
|
+
const profile =
|
|
452
|
+
input && typeof input === "object" && typeof input.profile === "string"
|
|
453
|
+
? input.profile
|
|
454
|
+
: "";
|
|
455
|
+
return envelopeOk({ value: metadata.profiles.includes(profile) });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (operation === "meta.route_probe") {
|
|
459
|
+
const target =
|
|
460
|
+
input && typeof input === "object" && typeof input.operation === "string"
|
|
461
|
+
? input.operation
|
|
462
|
+
: "";
|
|
463
|
+
return envelopeOk({ runtime: runtimeBridge.canHandleRuntimeOperation(target) });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (operation.startsWith("date.")) {
|
|
467
|
+
return await executeDateOperation(operation, input);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (operation === "config.detect_task_file") {
|
|
471
|
+
return envelopeOk({ value: detectTaskFile(input) });
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (operation.startsWith("templating.")) {
|
|
475
|
+
return await executeTemplatingOperation(operation, input);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (runtimeBridge.canHandleRuntimeOperation(operation)) {
|
|
479
|
+
return await runtimeBridge.executeRuntimeOperation(operation, input);
|
|
480
|
+
}
|
|
481
|
+
return await baseExecute(operation, input);
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return envelopeErr(error);
|
|
484
|
+
}
|
|
485
|
+
}
|