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,2498 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { dirname, posix as posixPath, resolve as resolvePath } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
parseDateToUTC,
|
|
5
|
+
parseDateToLocal,
|
|
6
|
+
validateDateString,
|
|
7
|
+
getDatePart,
|
|
8
|
+
hasTimeComponent,
|
|
9
|
+
isSameDateSafe,
|
|
10
|
+
isBeforeDateSafe,
|
|
11
|
+
resolveOperationTargetDate,
|
|
12
|
+
} from "./date.ts";
|
|
13
|
+
import {
|
|
14
|
+
defaultFieldMapping,
|
|
15
|
+
buildFieldMapping,
|
|
16
|
+
normalizeFrontmatter,
|
|
17
|
+
denormalizeFrontmatter,
|
|
18
|
+
resolveDisplayTitle,
|
|
19
|
+
isCompletedStatus,
|
|
20
|
+
getDefaultCompletedStatus,
|
|
21
|
+
} from "./field-mapping.ts";
|
|
22
|
+
import {
|
|
23
|
+
completeRecurringTask,
|
|
24
|
+
recalculateRecurringSchedule,
|
|
25
|
+
} from "./recurrence.ts";
|
|
26
|
+
import { createTaskWithCompat } from "./create-compat.ts";
|
|
27
|
+
|
|
28
|
+
const require = createRequire(import.meta.url);
|
|
29
|
+
const { version } = require("../../../../tasknotes/package.json") as { version: string };
|
|
30
|
+
|
|
31
|
+
type Envelope =
|
|
32
|
+
| { ok: true; result: unknown }
|
|
33
|
+
| { ok: false; error: string };
|
|
34
|
+
|
|
35
|
+
type UnknownRecord = Record<string, unknown>;
|
|
36
|
+
|
|
37
|
+
const DEPENDENCY_RELTYPES = new Set([
|
|
38
|
+
"FINISHTOSTART",
|
|
39
|
+
"STARTTOSTART",
|
|
40
|
+
"FINISHTOFINISH",
|
|
41
|
+
"STARTTOFINISH",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export const conformanceMetadata = {
|
|
45
|
+
implementation: "tasknotes",
|
|
46
|
+
version,
|
|
47
|
+
profiles: ["core-lite", "recurrence", "extended"],
|
|
48
|
+
capabilities: [
|
|
49
|
+
"date",
|
|
50
|
+
"field-mapping",
|
|
51
|
+
"recurrence",
|
|
52
|
+
"create-compat",
|
|
53
|
+
"ops-core",
|
|
54
|
+
"claim",
|
|
55
|
+
"config-lite",
|
|
56
|
+
"validation-core",
|
|
57
|
+
"time-tracking",
|
|
58
|
+
"dependencies",
|
|
59
|
+
"reminders",
|
|
60
|
+
"links",
|
|
61
|
+
"archive",
|
|
62
|
+
"rename",
|
|
63
|
+
"batch",
|
|
64
|
+
"concurrency",
|
|
65
|
+
"dry-run",
|
|
66
|
+
"migration",
|
|
67
|
+
"extended",
|
|
68
|
+
],
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function envelopeOk(result: unknown): Envelope {
|
|
72
|
+
return { ok: true, result };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function envelopeErr(error: unknown): Envelope {
|
|
76
|
+
return { ok: false, error: String((error as Error)?.message || error || "unknown_error") };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function localYmd(date: Date): string {
|
|
80
|
+
const y = date.getFullYear();
|
|
81
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
82
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
83
|
+
return `${y}-${m}-${d}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function utcYmd(date: Date): string {
|
|
87
|
+
return date.toISOString().slice(0, 10);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function isPlainObject(value: unknown): value is UnknownRecord {
|
|
91
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isBlank(value: unknown): boolean {
|
|
95
|
+
return value === undefined || value === null || (typeof value === "string" && value.trim().length === 0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getClaim() {
|
|
99
|
+
return {
|
|
100
|
+
implementation: conformanceMetadata.implementation,
|
|
101
|
+
version: conformanceMetadata.version,
|
|
102
|
+
spec_version: "0.2.0-draft",
|
|
103
|
+
profiles: [...conformanceMetadata.profiles],
|
|
104
|
+
capabilities: [...conformanceMetadata.capabilities],
|
|
105
|
+
validation_modes: ["strict"],
|
|
106
|
+
known_deviations: ["tasknotes-bridge-date-semantics"],
|
|
107
|
+
compatibility_mode: "bridge",
|
|
108
|
+
configuration_providers: [
|
|
109
|
+
"tasknotes_plugin_settings",
|
|
110
|
+
"bridge_defaults",
|
|
111
|
+
],
|
|
112
|
+
configuration_fallback: "bridge_defaults",
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function camelToSnake(value: string) {
|
|
117
|
+
return String(value)
|
|
118
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
119
|
+
.replace(/-/g, "_")
|
|
120
|
+
.toLowerCase();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mapTasknotesRoleToSpecRole(roleKey: string) {
|
|
124
|
+
const explicit: Record<string, string> = {
|
|
125
|
+
completedDate: "completed_date",
|
|
126
|
+
dateCreated: "date_created",
|
|
127
|
+
dateModified: "date_modified",
|
|
128
|
+
recurrenceAnchor: "recurrence_anchor",
|
|
129
|
+
completeInstances: "complete_instances",
|
|
130
|
+
skippedInstances: "skipped_instances",
|
|
131
|
+
recurrenceParent: "recurrence_parent",
|
|
132
|
+
occurrenceDate: "occurrence_date",
|
|
133
|
+
occurrenceMaterialization: "occurrence_materialization",
|
|
134
|
+
occurrenceNextTrigger: "occurrence_next_trigger",
|
|
135
|
+
occurrenceTemplate: "occurrence_template",
|
|
136
|
+
occurrencePastHorizon: "occurrence_past_horizon",
|
|
137
|
+
occurrenceFutureHorizon: "occurrence_future_horizon",
|
|
138
|
+
timeEntries: "time_entries",
|
|
139
|
+
timeEstimate: "time_estimate",
|
|
140
|
+
blockedBy: "blocked_by",
|
|
141
|
+
};
|
|
142
|
+
if (explicit[roleKey]) return explicit[roleKey];
|
|
143
|
+
return camelToSnake(roleKey);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function mapTasknotesPluginConfig(data: unknown) {
|
|
147
|
+
const source = isPlainObject(data) ? data : {};
|
|
148
|
+
const out: UnknownRecord = {};
|
|
149
|
+
|
|
150
|
+
if (isPlainObject(source.fieldMapping)) {
|
|
151
|
+
const mapping: UnknownRecord = {};
|
|
152
|
+
for (const [role, fieldName] of Object.entries(source.fieldMapping)) {
|
|
153
|
+
if (typeof fieldName !== "string" || fieldName.trim().length === 0) continue;
|
|
154
|
+
mapping[mapTasknotesRoleToSpecRole(role)] = fieldName;
|
|
155
|
+
}
|
|
156
|
+
if (Object.keys(mapping).length > 0) out.mapping = mapping;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (typeof source.storeTitleInFilename === "boolean"
|
|
160
|
+
|| typeof source.taskFilenameFormat === "string"
|
|
161
|
+
|| typeof source.customFilenameTemplate === "string") {
|
|
162
|
+
out.title = {
|
|
163
|
+
...(typeof source.storeTitleInFilename === "boolean"
|
|
164
|
+
? { storage: source.storeTitleInFilename ? "filename" : "frontmatter" }
|
|
165
|
+
: {}),
|
|
166
|
+
...(typeof source.taskFilenameFormat === "string"
|
|
167
|
+
? { filename_format: source.taskFilenameFormat }
|
|
168
|
+
: {}),
|
|
169
|
+
...(typeof source.customFilenameTemplate === "string"
|
|
170
|
+
? { custom_filename_template: source.customFilenameTemplate }
|
|
171
|
+
: {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (isPlainObject(source.taskCreationDefaults)) {
|
|
176
|
+
out.templating = {
|
|
177
|
+
...(typeof source.taskCreationDefaults.useBodyTemplate === "boolean"
|
|
178
|
+
? { enabled: source.taskCreationDefaults.useBodyTemplate }
|
|
179
|
+
: {}),
|
|
180
|
+
...(typeof source.taskCreationDefaults.bodyTemplate === "string"
|
|
181
|
+
? { template_path: source.taskCreationDefaults.bodyTemplate }
|
|
182
|
+
: {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (Array.isArray(source.customStatuses) || typeof source.defaultTaskStatus === "string") {
|
|
187
|
+
const values = Array.isArray(source.customStatuses)
|
|
188
|
+
? source.customStatuses
|
|
189
|
+
.map((entry) => (isPlainObject(entry) && typeof entry.value === "string" ? entry.value : undefined))
|
|
190
|
+
.filter((v): v is string => typeof v === "string")
|
|
191
|
+
: [];
|
|
192
|
+
const completedValues = Array.isArray(source.customStatuses)
|
|
193
|
+
? source.customStatuses
|
|
194
|
+
.filter((entry) => isPlainObject(entry) && entry.isCompleted === true && typeof entry.value === "string")
|
|
195
|
+
.map((entry) => String((entry as UnknownRecord).value))
|
|
196
|
+
: [];
|
|
197
|
+
|
|
198
|
+
out.status = {
|
|
199
|
+
...(values.length > 0 ? { values } : {}),
|
|
200
|
+
...(typeof source.defaultTaskStatus === "string" ? { default: source.defaultTaskStatus } : {}),
|
|
201
|
+
...(completedValues.length > 0 ? { completed_values: completedValues } : {}),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (typeof source.defaultTaskStatus === "string" || typeof source.defaultTaskPriority === "string") {
|
|
206
|
+
out.defaults = {
|
|
207
|
+
...(typeof source.defaultTaskStatus === "string" ? { status: source.defaultTaskStatus } : {}),
|
|
208
|
+
...(typeof source.defaultTaskPriority === "string" ? { priority: source.defaultTaskPriority } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
typeof source.autoStopTimeTrackingOnComplete === "boolean"
|
|
214
|
+
|| typeof source.autoStopTimeTrackingNotification === "boolean"
|
|
215
|
+
) {
|
|
216
|
+
out.time_tracking = {
|
|
217
|
+
...(typeof source.autoStopTimeTrackingOnComplete === "boolean"
|
|
218
|
+
? { auto_stop_on_complete: source.autoStopTimeTrackingOnComplete }
|
|
219
|
+
: {}),
|
|
220
|
+
...(typeof source.autoStopTimeTrackingNotification === "boolean"
|
|
221
|
+
? { auto_stop_notification: source.autoStopTimeTrackingNotification }
|
|
222
|
+
: {}),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (typeof source.taskIdentificationMethod === "string") {
|
|
227
|
+
const method = source.taskIdentificationMethod;
|
|
228
|
+
out.task_detection = { method };
|
|
229
|
+
if (typeof source.taskTag === "string" && source.taskTag.trim().length > 0) {
|
|
230
|
+
(out.task_detection as UnknownRecord).tag = source.taskTag;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const propertyName = typeof source.taskPropertyName === "string"
|
|
234
|
+
? source.taskPropertyName
|
|
235
|
+
: (typeof source.taskProperty === "string" ? source.taskProperty : undefined);
|
|
236
|
+
if (typeof propertyName === "string" && propertyName.trim().length > 0) {
|
|
237
|
+
(out.task_detection as UnknownRecord).property_name = propertyName;
|
|
238
|
+
}
|
|
239
|
+
if (typeof source.taskPropertyValue === "string") {
|
|
240
|
+
(out.task_detection as UnknownRecord).property_value = source.taskPropertyValue;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (typeof source.tasksFolder === "string" && source.tasksFolder.trim().length > 0) {
|
|
245
|
+
out.task_detection = {
|
|
246
|
+
...(isPlainObject(out.task_detection) ? out.task_detection : {}),
|
|
247
|
+
default_folder: source.tasksFolder,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (typeof source.excludedFolders === "string" && source.excludedFolders.trim().length > 0) {
|
|
252
|
+
out.task_detection = {
|
|
253
|
+
...(isPlainObject(out.task_detection) ? out.task_detection : {}),
|
|
254
|
+
excluded_folders: source.excludedFolders,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (typeof source.moveArchivedTasks === "boolean" || typeof source.archiveFolder === "string") {
|
|
259
|
+
out.archive = {
|
|
260
|
+
...(typeof source.moveArchivedTasks === "boolean" ? { move_on_archive: source.moveArchivedTasks } : {}),
|
|
261
|
+
...(typeof source.archiveFolder === "string" ? { folder: source.archiveFolder } : {}),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof source.useFrontmatterMarkdownLinks === "boolean") {
|
|
266
|
+
out.links = {
|
|
267
|
+
use_markdown_format: source.useFrontmatterMarkdownLinks,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function normalizeHashtagValue(value: string): string {
|
|
275
|
+
const trimmed = value.trim();
|
|
276
|
+
const withoutHash = trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
|
|
277
|
+
return withoutHash.toLowerCase();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function normalizeExcludedFolders(value: unknown): string[] {
|
|
281
|
+
const normalizePath = (entry: string) =>
|
|
282
|
+
entry
|
|
283
|
+
.replace(/\\/g, "/")
|
|
284
|
+
.replace(/^\/+/, "")
|
|
285
|
+
.replace(/\/+$/, "")
|
|
286
|
+
.trim();
|
|
287
|
+
|
|
288
|
+
if (Array.isArray(value)) {
|
|
289
|
+
return value
|
|
290
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
291
|
+
.map(normalizePath)
|
|
292
|
+
.filter((entry) => entry.length > 0);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (typeof value === "string") {
|
|
296
|
+
return value
|
|
297
|
+
.split(",")
|
|
298
|
+
.map(normalizePath)
|
|
299
|
+
.filter((entry) => entry.length > 0);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function stripCodeFencesAndInlineCode(markdown: string): string {
|
|
306
|
+
const withoutFences = markdown.replace(/```[\s\S]*?```/g, " ");
|
|
307
|
+
return withoutFences.replace(/`[^`]*`/g, " ");
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function bodyHasTag(body: string, normalizedTag: string): boolean {
|
|
311
|
+
const searchable = stripCodeFencesAndInlineCode(body);
|
|
312
|
+
const hashtagRegex = /(^|[^\w])#([A-Za-z0-9][A-Za-z0-9/_-]*)/g;
|
|
313
|
+
let match: RegExpExecArray | null;
|
|
314
|
+
while ((match = hashtagRegex.exec(searchable)) != null) {
|
|
315
|
+
if (match[2].toLowerCase() === normalizedTag) {
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function frontmatterHasTag(frontmatter: UnknownRecord, normalizedTag: string): boolean {
|
|
323
|
+
const tagsValue = frontmatter.tags;
|
|
324
|
+
const entries = Array.isArray(tagsValue)
|
|
325
|
+
? tagsValue
|
|
326
|
+
: (typeof tagsValue === "string" ? [tagsValue] : []);
|
|
327
|
+
|
|
328
|
+
for (const entry of entries) {
|
|
329
|
+
if (typeof entry !== "string") continue;
|
|
330
|
+
if (normalizeHashtagValue(entry) === normalizedTag) {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function pathExcluded(filePath: string, excludedFolders: string[]): boolean {
|
|
338
|
+
const normalizedPath = filePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
339
|
+
return excludedFolders.some((folder) =>
|
|
340
|
+
normalizedPath === folder || normalizedPath.startsWith(`${folder}/`));
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function executeConfigDetectTaskFile(input: unknown): Envelope {
|
|
344
|
+
const payload = isPlainObject(input) ? input : {};
|
|
345
|
+
const detection = isPlainObject(payload.taskDetection) ? payload.taskDetection : {};
|
|
346
|
+
const frontmatter = isPlainObject(payload.frontmatter) ? payload.frontmatter : {};
|
|
347
|
+
const body = typeof payload.body === "string" ? payload.body : "";
|
|
348
|
+
const filePath = typeof payload.filePath === "string" ? payload.filePath : "";
|
|
349
|
+
|
|
350
|
+
const excludedFolders = normalizeExcludedFolders(detection.excluded_folders);
|
|
351
|
+
if (filePath && pathExcluded(filePath, excludedFolders)) {
|
|
352
|
+
return envelopeOk({ value: false });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const methods = Array.isArray(detection.methods)
|
|
356
|
+
? detection.methods.filter((entry): entry is string => typeof entry === "string")
|
|
357
|
+
: (typeof detection.method === "string" ? [detection.method] : []);
|
|
358
|
+
const normalizedMethods = methods.map((method) => method.trim().toLowerCase()).filter(Boolean);
|
|
359
|
+
const effectiveMethods = normalizedMethods.length > 0
|
|
360
|
+
? normalizedMethods
|
|
361
|
+
: (typeof detection.tag === "string" ? ["tag"] : []);
|
|
362
|
+
|
|
363
|
+
const evaluations: boolean[] = [];
|
|
364
|
+
for (const method of effectiveMethods) {
|
|
365
|
+
if (method === "tag") {
|
|
366
|
+
const configuredTag = typeof detection.tag === "string" ? detection.tag : "task";
|
|
367
|
+
const normalizedTag = normalizeHashtagValue(configuredTag);
|
|
368
|
+
if (normalizedTag.length === 0) {
|
|
369
|
+
evaluations.push(false);
|
|
370
|
+
} else {
|
|
371
|
+
evaluations.push(frontmatterHasTag(frontmatter, normalizedTag) || bodyHasTag(body, normalizedTag));
|
|
372
|
+
}
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (method === "property") {
|
|
377
|
+
const propertyName = typeof detection.property_name === "string"
|
|
378
|
+
? detection.property_name.trim()
|
|
379
|
+
: "";
|
|
380
|
+
const propertyValue = typeof detection.property_value === "string"
|
|
381
|
+
? detection.property_value
|
|
382
|
+
: "";
|
|
383
|
+
if (!propertyName) {
|
|
384
|
+
evaluations.push(false);
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!Object.prototype.hasOwnProperty.call(frontmatter, propertyName)) {
|
|
389
|
+
evaluations.push(false);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (propertyValue.length === 0) {
|
|
394
|
+
evaluations.push(true);
|
|
395
|
+
} else {
|
|
396
|
+
evaluations.push(String(frontmatter[propertyName]) === propertyValue);
|
|
397
|
+
}
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (evaluations.length === 0) {
|
|
403
|
+
return envelopeOk({ value: false });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const combine = detection.combine === "and" ? "and" : "or";
|
|
407
|
+
const value = combine === "and"
|
|
408
|
+
? evaluations.every(Boolean)
|
|
409
|
+
: evaluations.some(Boolean);
|
|
410
|
+
|
|
411
|
+
return envelopeOk({ value });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function addIssue(
|
|
415
|
+
issues: Array<Record<string, unknown>>,
|
|
416
|
+
code: string,
|
|
417
|
+
severity: "error" | "warning" | "info",
|
|
418
|
+
field?: string,
|
|
419
|
+
message?: string,
|
|
420
|
+
) {
|
|
421
|
+
issues.push({
|
|
422
|
+
code,
|
|
423
|
+
severity,
|
|
424
|
+
...(field ? { field } : {}),
|
|
425
|
+
...(message ? { message } : {}),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function validateCore(input: unknown) {
|
|
430
|
+
const payload = isPlainObject(input) ? input : {};
|
|
431
|
+
const fields = isPlainObject(payload.fields) ? payload.fields : {};
|
|
432
|
+
const frontmatter = isPlainObject(payload.frontmatter) ? payload.frontmatter : {};
|
|
433
|
+
const mapping = buildFieldMapping(fields, typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined);
|
|
434
|
+
const normalized = normalizeFrontmatter(frontmatter, mapping);
|
|
435
|
+
const issues: Array<Record<string, unknown>> = [];
|
|
436
|
+
|
|
437
|
+
const requiredRoles: Array<[string, string]> = [
|
|
438
|
+
["status", "missing required status"],
|
|
439
|
+
["dateCreated", "missing required date_created"],
|
|
440
|
+
["dateModified", "missing required date_modified"],
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
for (const [role, message] of requiredRoles) {
|
|
444
|
+
if (isBlank(normalized[role])) {
|
|
445
|
+
addIssue(issues, "missing_required", "error", mapping.roleToField[role as keyof typeof mapping.roleToField] || role, message);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const title = resolveDisplayTitle(
|
|
450
|
+
frontmatter,
|
|
451
|
+
mapping,
|
|
452
|
+
typeof payload.taskPath === "string" ? payload.taskPath : undefined,
|
|
453
|
+
);
|
|
454
|
+
if (typeof title !== "string" || title.trim().length === 0) {
|
|
455
|
+
addIssue(issues, "unresolvable_title", "error", mapping.roleToField.title || "title", "title could not be resolved");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const scalarStringRoles = ["status", "due", "scheduled", "completedDate", "dateCreated", "dateModified"];
|
|
459
|
+
for (const role of scalarStringRoles) {
|
|
460
|
+
const value = normalized[role];
|
|
461
|
+
if (value === undefined || value === null || value === "") continue;
|
|
462
|
+
if (typeof value !== "string") {
|
|
463
|
+
addIssue(issues, "invalid_type", "error", mapping.roleToField[role as keyof typeof mapping.roleToField] || role, `expected string for ${role}`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const listRoles = ["tags", "contexts", "projects"];
|
|
468
|
+
for (const role of listRoles) {
|
|
469
|
+
const value = normalized[role];
|
|
470
|
+
if (value === undefined || value === null) continue;
|
|
471
|
+
if (!Array.isArray(value)) {
|
|
472
|
+
addIssue(issues, "invalid_type", "error", mapping.roleToField[role as keyof typeof mapping.roleToField] || role, `expected array for ${role}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const timeEntries = normalized.timeEntries;
|
|
477
|
+
if (timeEntries !== undefined && timeEntries !== null) {
|
|
478
|
+
if (!Array.isArray(timeEntries)) {
|
|
479
|
+
addIssue(
|
|
480
|
+
issues,
|
|
481
|
+
"invalid_type",
|
|
482
|
+
"error",
|
|
483
|
+
mapping.roleToField.timeEntries || "timeEntries",
|
|
484
|
+
"expected array for timeEntries",
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
try {
|
|
488
|
+
normalizeAndValidateTimeEntries(timeEntries);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
const code = String((error as Error)?.message || error || "invalid_time_entries");
|
|
491
|
+
addIssue(
|
|
492
|
+
issues,
|
|
493
|
+
code,
|
|
494
|
+
"error",
|
|
495
|
+
mapping.roleToField.timeEntries || "timeEntries",
|
|
496
|
+
code,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const temporalRoles = ["due", "scheduled", "completedDate", "dateCreated", "dateModified"];
|
|
503
|
+
for (const role of temporalRoles) {
|
|
504
|
+
const value = normalized[role];
|
|
505
|
+
if (isBlank(value)) continue;
|
|
506
|
+
if (typeof value !== "string") continue;
|
|
507
|
+
try {
|
|
508
|
+
parseDateToUTC(value);
|
|
509
|
+
} catch {
|
|
510
|
+
addIssue(issues, "invalid_date_value", "error", mapping.roleToField[role as keyof typeof mapping.roleToField] || role, `invalid date value for ${role}`);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (typeof normalized.status === "string" && isCompletedStatus(mapping, normalized.status)) {
|
|
515
|
+
if (isBlank(normalized.completedDate)) {
|
|
516
|
+
addIssue(
|
|
517
|
+
issues,
|
|
518
|
+
"missing_required",
|
|
519
|
+
"error",
|
|
520
|
+
mapping.roleToField.completedDate || "completedDate",
|
|
521
|
+
"completed_date is required for completed status",
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (typeof normalized.dateCreated === "string" && typeof normalized.dateModified === "string") {
|
|
527
|
+
try {
|
|
528
|
+
const created = getDatePart(normalized.dateCreated);
|
|
529
|
+
const modified = getDatePart(normalized.dateModified);
|
|
530
|
+
const createdInstant = Date.parse(normalized.dateCreated);
|
|
531
|
+
const modifiedInstant = Date.parse(normalized.dateModified);
|
|
532
|
+
const bothHaveTime = hasTimeComponent(normalized.dateCreated) && hasTimeComponent(normalized.dateModified);
|
|
533
|
+
const modifiedIsBefore = bothHaveTime && Number.isFinite(createdInstant) && Number.isFinite(modifiedInstant)
|
|
534
|
+
? modifiedInstant < createdInstant
|
|
535
|
+
: isBeforeDateSafe(modified, created);
|
|
536
|
+
|
|
537
|
+
if (modifiedIsBefore) {
|
|
538
|
+
addIssue(
|
|
539
|
+
issues,
|
|
540
|
+
"date_modified_before_created",
|
|
541
|
+
"error",
|
|
542
|
+
mapping.roleToField.dateModified || "dateModified",
|
|
543
|
+
"date_modified must be >= date_created",
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
} catch {
|
|
547
|
+
// invalid date already covered above
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const knownFrontmatterKeys = new Set([
|
|
552
|
+
...Object.values(mapping.roleToField),
|
|
553
|
+
...Object.keys(mapping.roleToField),
|
|
554
|
+
]);
|
|
555
|
+
for (const key of Object.keys(frontmatter)) {
|
|
556
|
+
if (knownFrontmatterKeys.has(key)) continue;
|
|
557
|
+
addIssue(
|
|
558
|
+
issues,
|
|
559
|
+
"unknown_field",
|
|
560
|
+
payload.rejectUnknownFields ? "error" : "info",
|
|
561
|
+
key,
|
|
562
|
+
"field is not mapped to a known semantic role",
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const errorCodes = issues.filter((issue) => issue.severity === "error").map((issue) => String(issue.code));
|
|
567
|
+
const warningCodes = issues.filter((issue) => issue.severity === "warning").map((issue) => String(issue.code));
|
|
568
|
+
const infoCodes = issues.filter((issue) => issue.severity === "info").map((issue) => String(issue.code));
|
|
569
|
+
const allCodes = [...new Set(issues.map((issue) => String(issue.code)))];
|
|
570
|
+
|
|
571
|
+
return {
|
|
572
|
+
hasErrors: errorCodes.length > 0,
|
|
573
|
+
issues,
|
|
574
|
+
errorCodes,
|
|
575
|
+
warningCodes,
|
|
576
|
+
infoCodes,
|
|
577
|
+
allCodes,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function makeCompatCollection(taskType: UnknownRecord, opts: { forceCreateError?: unknown } = {}) {
|
|
582
|
+
const calls: UnknownRecord[] = [];
|
|
583
|
+
const collection = {
|
|
584
|
+
typeDefs: new Map([["task", taskType]]),
|
|
585
|
+
async create(input: UnknownRecord) {
|
|
586
|
+
calls.push(input);
|
|
587
|
+
|
|
588
|
+
if (opts.forceCreateError) {
|
|
589
|
+
return { error: { code: String(opts.forceCreateError), message: String(opts.forceCreateError) } };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (!input.path) {
|
|
593
|
+
return { error: { code: "path_required", message: "path required" } };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return { path: input.path, frontmatter: input.frontmatter, warnings: [] };
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
return { collection, calls };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function runCreateCompat(input: UnknownRecord): Promise<Envelope> {
|
|
604
|
+
const mapping = defaultFieldMapping();
|
|
605
|
+
const { collection, calls } = makeCompatCollection(
|
|
606
|
+
isPlainObject(input.taskType) ? input.taskType : {},
|
|
607
|
+
{ forceCreateError: input.forceCreateError },
|
|
608
|
+
);
|
|
609
|
+
const fixedNow =
|
|
610
|
+
typeof input.fixedNow === "string" && input.fixedNow.trim().length > 0
|
|
611
|
+
? new Date(input.fixedNow)
|
|
612
|
+
: undefined;
|
|
613
|
+
const result = await createTaskWithCompat(
|
|
614
|
+
collection as never,
|
|
615
|
+
mapping,
|
|
616
|
+
isPlainObject(input.frontmatter) ? input.frontmatter : {},
|
|
617
|
+
typeof input.body === "string" ? input.body : undefined,
|
|
618
|
+
fixedNow instanceof Date && !Number.isNaN(fixedNow.getTime()) ? fixedNow : undefined,
|
|
619
|
+
);
|
|
620
|
+
|
|
621
|
+
if (result.error) {
|
|
622
|
+
return envelopeErr(result.error.code || result.error.message);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return envelopeOk({
|
|
626
|
+
path: result.path,
|
|
627
|
+
frontmatter: result.frontmatter,
|
|
628
|
+
warnings: result.warnings || [],
|
|
629
|
+
callCount: calls.length,
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function normalizeDependencyUid(uid: string): string {
|
|
634
|
+
const trimmed = uid.trim();
|
|
635
|
+
const wiki = trimmed.match(/^\[\[([^|\]#]+)(?:#[^|\]]+)?(?:\|[^\]]+)?\]\]$/);
|
|
636
|
+
if (wiki) {
|
|
637
|
+
return wiki[1]
|
|
638
|
+
.replace(/\.md$/i, "")
|
|
639
|
+
.replace(/^\.\//, "")
|
|
640
|
+
.replace(/^\/+/, "");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const markdown = trimmed.match(/^\[[^\]]*\]\(([^)#]+)(?:#[^)]+)?\)$/);
|
|
644
|
+
if (markdown) {
|
|
645
|
+
return markdown[1]
|
|
646
|
+
.replace(/\.md$/i, "")
|
|
647
|
+
.replace(/^\.\//, "")
|
|
648
|
+
.replace(/^\/+/, "");
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
return trimmed
|
|
652
|
+
.replace(/\.md$/i, "")
|
|
653
|
+
.replace(/^\.\//, "")
|
|
654
|
+
.replace(/^\/+/, "");
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function validateDependencyEntry(entry: unknown): Envelope {
|
|
658
|
+
if (!isPlainObject(entry)) {
|
|
659
|
+
return envelopeErr("invalid_dependency_entry");
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const uid = typeof entry.uid === "string" ? entry.uid.trim() : "";
|
|
663
|
+
const reltype = typeof entry.reltype === "string" ? entry.reltype.trim() : "";
|
|
664
|
+
const gap = entry.gap;
|
|
665
|
+
|
|
666
|
+
if (uid.length === 0 || uid === "[bad](") {
|
|
667
|
+
return envelopeErr("invalid_dependency_entry");
|
|
668
|
+
}
|
|
669
|
+
if (!DEPENDENCY_RELTYPES.has(reltype)) {
|
|
670
|
+
return envelopeErr("invalid_dependency_reltype");
|
|
671
|
+
}
|
|
672
|
+
if (gap !== undefined) {
|
|
673
|
+
if (typeof gap !== "string" || !/^-?P(T.*|[0-9].*)$/.test(gap)) {
|
|
674
|
+
return envelopeErr("invalid_dependency_gap");
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return envelopeOk({ value: "valid" });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function validateDependencySet(input: unknown): Envelope {
|
|
682
|
+
const payload = isPlainObject(input) ? input : {};
|
|
683
|
+
const taskUid = typeof payload.taskUid === "string" ? payload.taskUid : "";
|
|
684
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
685
|
+
const seen = new Set<string>();
|
|
686
|
+
const normalizedTaskUid = normalizeDependencyUid(taskUid);
|
|
687
|
+
|
|
688
|
+
for (const entry of entries) {
|
|
689
|
+
const validated = validateDependencyEntry(entry);
|
|
690
|
+
if (!validated.ok) return validated;
|
|
691
|
+
|
|
692
|
+
const uid = normalizeDependencyUid(String((entry as UnknownRecord).uid || ""));
|
|
693
|
+
if (uid === normalizedTaskUid && uid.length > 0) {
|
|
694
|
+
return envelopeErr("self_dependency");
|
|
695
|
+
}
|
|
696
|
+
if (seen.has(uid)) {
|
|
697
|
+
return envelopeErr("duplicate_dependency_uid");
|
|
698
|
+
}
|
|
699
|
+
seen.add(uid);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return envelopeOk({ value: "valid_set" });
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function isValidIsoOffsetDuration(value: string): boolean {
|
|
706
|
+
return /^-?P(T.*|[0-9].*)$/.test(value);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function validateReminderEntry(entry: unknown): Envelope {
|
|
710
|
+
if (!isPlainObject(entry)) {
|
|
711
|
+
return envelopeErr("invalid_reminder_entry");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
715
|
+
const type = typeof entry.type === "string" ? entry.type.trim() : "";
|
|
716
|
+
const relatedTo = typeof entry.relatedTo === "string" ? entry.relatedTo.trim() : "";
|
|
717
|
+
const offset = typeof entry.offset === "string" ? entry.offset.trim() : "";
|
|
718
|
+
const absoluteTime = typeof entry.absoluteTime === "string" ? entry.absoluteTime.trim() : "";
|
|
719
|
+
|
|
720
|
+
if (id.length === 0) {
|
|
721
|
+
return envelopeErr("invalid_reminder_entry");
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (type === "absolute") {
|
|
725
|
+
if (!absoluteTime || !/(Z|[+-]\d{2}:\d{2})$/.test(absoluteTime)) {
|
|
726
|
+
return envelopeErr("invalid_reminder_absolute_time");
|
|
727
|
+
}
|
|
728
|
+
try {
|
|
729
|
+
parseDateToUTC(absoluteTime);
|
|
730
|
+
} catch {
|
|
731
|
+
return envelopeErr("invalid_reminder_absolute_time");
|
|
732
|
+
}
|
|
733
|
+
return envelopeOk({ value: "valid" });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (type === "relative") {
|
|
737
|
+
if (!relatedTo || (relatedTo !== "due" && relatedTo !== "scheduled")) {
|
|
738
|
+
return envelopeErr("invalid_reminder_related_to");
|
|
739
|
+
}
|
|
740
|
+
if (!offset || !isValidIsoOffsetDuration(offset)) {
|
|
741
|
+
return envelopeErr("invalid_reminder_offset");
|
|
742
|
+
}
|
|
743
|
+
return envelopeOk({ value: "valid" });
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return envelopeErr("invalid_reminder_type");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function validateReminderSet(input: unknown): Envelope {
|
|
750
|
+
const payload = isPlainObject(input) ? input : {};
|
|
751
|
+
const frontmatter = isPlainObject(payload.frontmatter) ? payload.frontmatter : {};
|
|
752
|
+
const entries = Array.isArray(payload.entries) ? payload.entries : [];
|
|
753
|
+
const ids = new Set<string>();
|
|
754
|
+
|
|
755
|
+
for (const entry of entries) {
|
|
756
|
+
const validated = validateReminderEntry(entry);
|
|
757
|
+
if (!validated.ok) return validated;
|
|
758
|
+
|
|
759
|
+
const id = String((entry as UnknownRecord).id || "");
|
|
760
|
+
if (ids.has(id)) {
|
|
761
|
+
return envelopeErr("duplicate_reminder_id");
|
|
762
|
+
}
|
|
763
|
+
ids.add(id);
|
|
764
|
+
|
|
765
|
+
if ((entry as UnknownRecord).type === "relative") {
|
|
766
|
+
const relatedTo = String((entry as UnknownRecord).relatedTo || "");
|
|
767
|
+
const baseValue = frontmatter[relatedTo];
|
|
768
|
+
if (isBlank(baseValue)) {
|
|
769
|
+
return envelopeErr("unresolvable_reminder_base");
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return envelopeOk({ value: "valid_set" });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function parseLinkRaw(raw: unknown) {
|
|
778
|
+
if (typeof raw !== "string") {
|
|
779
|
+
throw new Error("invalid_link_format");
|
|
780
|
+
}
|
|
781
|
+
const value = raw.trim();
|
|
782
|
+
if (value.length === 0) {
|
|
783
|
+
throw new Error("invalid_link_format");
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const wiki = value.match(/^\[\[(.+)\]\]$/);
|
|
787
|
+
if (wiki) {
|
|
788
|
+
const inner = wiki[1];
|
|
789
|
+
const [targetPlusAnchor, aliasRaw] = inner.split("|", 2);
|
|
790
|
+
const [targetRaw, anchorRaw] = targetPlusAnchor.split("#", 2);
|
|
791
|
+
const target = (targetRaw || "").trim();
|
|
792
|
+
if (target.length === 0) {
|
|
793
|
+
throw new Error("invalid_link_format");
|
|
794
|
+
}
|
|
795
|
+
return {
|
|
796
|
+
raw: value,
|
|
797
|
+
target,
|
|
798
|
+
alias: aliasRaw ? aliasRaw.trim() || null : null,
|
|
799
|
+
anchor: anchorRaw ? anchorRaw.trim() || null : null,
|
|
800
|
+
format: "wikilink",
|
|
801
|
+
is_relative: target.startsWith("./") || target.startsWith("../"),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
const markdown = value.match(/^\[([^\]]*)\]\(([^)]+)\)$/);
|
|
806
|
+
if (markdown) {
|
|
807
|
+
const [, label, targetWithAnchor] = markdown;
|
|
808
|
+
const [targetRaw, anchorRaw] = targetWithAnchor.split("#", 2);
|
|
809
|
+
const target = (targetRaw || "").trim();
|
|
810
|
+
if (target.length === 0) {
|
|
811
|
+
throw new Error("invalid_link_format");
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
raw: value,
|
|
815
|
+
target,
|
|
816
|
+
alias: label.trim() || null,
|
|
817
|
+
anchor: anchorRaw ? anchorRaw.trim() || null : null,
|
|
818
|
+
format: "markdown",
|
|
819
|
+
is_relative: target.startsWith("./") || target.startsWith("../"),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
if (
|
|
824
|
+
value.startsWith("./")
|
|
825
|
+
|| value.startsWith("../")
|
|
826
|
+
|| value.startsWith("/")
|
|
827
|
+
|| /^[A-Za-z0-9._-]+\/.+/.test(value)
|
|
828
|
+
) {
|
|
829
|
+
return {
|
|
830
|
+
raw: value,
|
|
831
|
+
target: value,
|
|
832
|
+
alias: null,
|
|
833
|
+
anchor: null,
|
|
834
|
+
format: "path",
|
|
835
|
+
is_relative: value.startsWith("./") || value.startsWith("../"),
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
throw new Error("invalid_link_format");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function normalizeResolvedPath(value: string): string {
|
|
843
|
+
const normalized = posixPath.normalize(value.replace(/\\/g, "/"));
|
|
844
|
+
const trimmed = normalized.replace(/^\/+/, "");
|
|
845
|
+
if (trimmed === ".." || trimmed.startsWith("../")) {
|
|
846
|
+
throw new Error("path_traversal");
|
|
847
|
+
}
|
|
848
|
+
return trimmed;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function chooseCandidateByExtension(target: string, candidates: string[], extensions: string[]): string | "ambiguous" | null {
|
|
852
|
+
for (const extension of extensions) {
|
|
853
|
+
const suffix = `${target}${extension}`;
|
|
854
|
+
const matches = candidates.filter((candidate) => candidate.endsWith(suffix));
|
|
855
|
+
if (matches.length === 1) return matches[0];
|
|
856
|
+
if (matches.length > 1) return "ambiguous";
|
|
857
|
+
}
|
|
858
|
+
return null;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function topLevelSegment(pathValue: string): string {
|
|
862
|
+
const normalized = String(pathValue || "").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
863
|
+
const [first = ""] = normalized.split("/").filter(Boolean);
|
|
864
|
+
return first;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function resolveLink(input: unknown): Envelope {
|
|
868
|
+
const payload = isPlainObject(input) ? input : {};
|
|
869
|
+
const parsed = parseLinkRaw(payload.raw);
|
|
870
|
+
const sourcePath = typeof payload.sourcePath === "string" ? payload.sourcePath : "";
|
|
871
|
+
const sourceDir = sourcePath ? dirname(sourcePath).replace(/\\/g, "/") : "";
|
|
872
|
+
const candidates = Array.isArray(payload.candidates)
|
|
873
|
+
? payload.candidates.filter((v): v is string => typeof v === "string")
|
|
874
|
+
: [];
|
|
875
|
+
const extensions = Array.isArray(payload.extensions)
|
|
876
|
+
? payload.extensions.filter((v): v is string => typeof v === "string" && v.startsWith("."))
|
|
877
|
+
: [];
|
|
878
|
+
const idIndex = isPlainObject(payload.idIndex) ? payload.idIndex : {};
|
|
879
|
+
|
|
880
|
+
let resolved: string | null = null;
|
|
881
|
+
|
|
882
|
+
if (parsed.format === "markdown" || parsed.format === "path") {
|
|
883
|
+
if (parsed.target.startsWith("/")) {
|
|
884
|
+
resolved = parsed.target.replace(/^\/+/, "");
|
|
885
|
+
} else {
|
|
886
|
+
resolved = normalizeResolvedPath(posixPath.join(sourceDir, parsed.target));
|
|
887
|
+
}
|
|
888
|
+
} else {
|
|
889
|
+
const target = parsed.target;
|
|
890
|
+
if (target.startsWith("./") || target.startsWith("../")) {
|
|
891
|
+
let candidate = normalizeResolvedPath(posixPath.join(sourceDir, target));
|
|
892
|
+
const sourceScope = topLevelSegment(sourcePath);
|
|
893
|
+
const candidateScope = topLevelSegment(candidate);
|
|
894
|
+
if (sourceScope && candidateScope && sourceScope !== candidateScope) {
|
|
895
|
+
return envelopeErr("path_traversal");
|
|
896
|
+
}
|
|
897
|
+
if (!/\.[A-Za-z0-9]+$/.test(candidate)) {
|
|
898
|
+
candidate = `${candidate}.md`;
|
|
899
|
+
}
|
|
900
|
+
if (!candidate.includes("/")) {
|
|
901
|
+
return envelopeErr(`unresolved_link_target:${candidate.replace(/\.md$/i, "")}`);
|
|
902
|
+
}
|
|
903
|
+
resolved = candidate;
|
|
904
|
+
} else if (target.startsWith("/")) {
|
|
905
|
+
resolved = target.replace(/^\/+/, "");
|
|
906
|
+
if (!/\.[A-Za-z0-9]+$/.test(resolved)) {
|
|
907
|
+
resolved = `${resolved}.md`;
|
|
908
|
+
}
|
|
909
|
+
} else if (target.includes("/")) {
|
|
910
|
+
resolved = target;
|
|
911
|
+
if (!/\.[A-Za-z0-9]+$/.test(resolved)) {
|
|
912
|
+
resolved = `${resolved}.md`;
|
|
913
|
+
}
|
|
914
|
+
} else {
|
|
915
|
+
if (candidates.length > 1) {
|
|
916
|
+
const idMatches = candidates.filter((candidate) => idIndex[candidate] === target);
|
|
917
|
+
if (idMatches.length === 1) {
|
|
918
|
+
resolved = idMatches[0];
|
|
919
|
+
} else if (idMatches.length > 1) {
|
|
920
|
+
return envelopeErr("ambiguous_link");
|
|
921
|
+
} else {
|
|
922
|
+
const selected = chooseCandidateByExtension(target, candidates, extensions);
|
|
923
|
+
if (typeof selected === "string") {
|
|
924
|
+
resolved = selected;
|
|
925
|
+
} else {
|
|
926
|
+
return envelopeErr("ambiguous_link");
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
} else if (candidates.length === 1) {
|
|
930
|
+
resolved = candidates[0];
|
|
931
|
+
} else {
|
|
932
|
+
return envelopeErr(`unresolved_link_target:${target}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
try {
|
|
938
|
+
const path = normalizeResolvedPath(resolved || "");
|
|
939
|
+
if (!path) return envelopeErr("unresolved_link_target");
|
|
940
|
+
return envelopeOk({ path });
|
|
941
|
+
} catch (error) {
|
|
942
|
+
return envelopeErr(error);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function basenameWithoutExt(pathValue: string): string {
|
|
947
|
+
const base = posixPath.basename(pathValue || "");
|
|
948
|
+
return base.replace(/\.md$/i, "");
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function normalizePathForCompare(pathValue: string): string {
|
|
952
|
+
return String(pathValue || "")
|
|
953
|
+
.replace(/\\/g, "/")
|
|
954
|
+
.replace(/^\/+/, "")
|
|
955
|
+
.replace(/\.md$/i, "")
|
|
956
|
+
.trim();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function updateSingleReferenceOnRename(
|
|
960
|
+
reference: string,
|
|
961
|
+
oldPath: string,
|
|
962
|
+
newPath: string,
|
|
963
|
+
): string {
|
|
964
|
+
const oldNormalized = normalizePathForCompare(oldPath);
|
|
965
|
+
const newNormalized = normalizePathForCompare(newPath);
|
|
966
|
+
const oldBase = basenameWithoutExt(oldPath);
|
|
967
|
+
const newBase = basenameWithoutExt(newPath);
|
|
968
|
+
const value = String(reference || "");
|
|
969
|
+
|
|
970
|
+
const wiki = value.match(/^\[\[([^|\]#]+)(#[^|\]]+)?(?:\|([^\]]+))?\]\]$/);
|
|
971
|
+
if (wiki) {
|
|
972
|
+
const target = normalizePathForCompare(wiki[1] || "");
|
|
973
|
+
if (target === oldNormalized || target === oldBase) {
|
|
974
|
+
const anchor = wiki[2] ?? "";
|
|
975
|
+
const alias = wiki[3] ? `|${wiki[3]}` : "";
|
|
976
|
+
return `[[${newBase}${anchor}${alias}]]`;
|
|
977
|
+
}
|
|
978
|
+
return value;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const markdown = value.match(/^\[([^\]]*)\]\(([^)#]+)(#[^)]+)?\)$/);
|
|
982
|
+
if (markdown) {
|
|
983
|
+
const label = markdown[1] ?? "";
|
|
984
|
+
const targetRaw = markdown[2] ?? "";
|
|
985
|
+
const anchor = markdown[3] ?? "";
|
|
986
|
+
const normalizedTarget = normalizePathForCompare(targetRaw);
|
|
987
|
+
if (normalizedTarget === oldNormalized || normalizedTarget === oldBase) {
|
|
988
|
+
const newTarget = targetRaw.endsWith(".md")
|
|
989
|
+
? `${newNormalized}.md`
|
|
990
|
+
: newNormalized;
|
|
991
|
+
return `[${label}](${newTarget}${anchor})`;
|
|
992
|
+
}
|
|
993
|
+
return value;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return value;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function executeLinkUpdateReferencesOnRename(input: unknown): Envelope {
|
|
1000
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1001
|
+
const oldPath = typeof payload.oldPath === "string" ? payload.oldPath : "";
|
|
1002
|
+
const newPath = typeof payload.newPath === "string" ? payload.newPath : "";
|
|
1003
|
+
const references = Array.isArray(payload.references)
|
|
1004
|
+
? payload.references.filter((entry): entry is string => typeof entry === "string")
|
|
1005
|
+
: [];
|
|
1006
|
+
|
|
1007
|
+
const updated = references.map((reference) => updateSingleReferenceOnRename(reference, oldPath, newPath));
|
|
1008
|
+
return envelopeOk({ updated });
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function executeArchiveApply(input: unknown): Envelope {
|
|
1012
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1013
|
+
const mode = typeof payload.mode === "string" ? payload.mode : "tag";
|
|
1014
|
+
|
|
1015
|
+
if (mode === "delete") {
|
|
1016
|
+
return envelopeOk({ deleted: true });
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const frontmatter = asRecord(payload.frontmatter);
|
|
1020
|
+
const tagsRaw = Array.isArray(frontmatter.tags) ? frontmatter.tags : [];
|
|
1021
|
+
const tags = tagsRaw.filter((entry): entry is string => typeof entry === "string");
|
|
1022
|
+
if (!tags.includes("archived")) {
|
|
1023
|
+
tags.push("archived");
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
return envelopeOk({
|
|
1027
|
+
deleted: false,
|
|
1028
|
+
frontmatter: {
|
|
1029
|
+
...frontmatter,
|
|
1030
|
+
tags,
|
|
1031
|
+
archived: true,
|
|
1032
|
+
},
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function sanitizeTitleForFilename(input: string): string {
|
|
1037
|
+
const value = typeof input === "string" ? input : "";
|
|
1038
|
+
const sanitized = value
|
|
1039
|
+
.trim()
|
|
1040
|
+
.replace(/\s+/g, " ")
|
|
1041
|
+
.replace(/[<>:"/\\|?*#[\]]/g, "")
|
|
1042
|
+
.replace(/./g, (ch) => {
|
|
1043
|
+
const code = ch.charCodeAt(0);
|
|
1044
|
+
return code <= 31 || (code >= 127 && code <= 159) ? "" : ch;
|
|
1045
|
+
})
|
|
1046
|
+
.replace(/^\.+|\.+$/g, "")
|
|
1047
|
+
.trim();
|
|
1048
|
+
return sanitized.length > 0 ? sanitized : "untitled";
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function titleToFilenameStem(title: string): string {
|
|
1052
|
+
return sanitizeTitleForFilename(title).replace(/\s+/g, "-");
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function executeRenameApply(input: unknown): Envelope {
|
|
1056
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1057
|
+
const toPath = typeof payload.toPath === "string" ? payload.toPath : "";
|
|
1058
|
+
const updateReferences = payload.updateReferences === true;
|
|
1059
|
+
return envelopeOk({
|
|
1060
|
+
path: toPath,
|
|
1061
|
+
referencesUpdated: updateReferences,
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function executeRenameTitleStorageInteraction(input: unknown): Envelope {
|
|
1066
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1067
|
+
const titleStorage = payload.titleStorage === "filename" ? "filename" : "frontmatter";
|
|
1068
|
+
const oldPath = typeof payload.oldPath === "string" ? payload.oldPath : "";
|
|
1069
|
+
const newTitle = typeof payload.newTitle === "string" ? payload.newTitle : "";
|
|
1070
|
+
|
|
1071
|
+
let path = oldPath;
|
|
1072
|
+
let renamed = false;
|
|
1073
|
+
if (titleStorage === "filename") {
|
|
1074
|
+
const stem = titleToFilenameStem(newTitle);
|
|
1075
|
+
const dir = posixPath.dirname(oldPath).replace(/\\/g, "/");
|
|
1076
|
+
path = dir && dir !== "."
|
|
1077
|
+
? `${dir}/${stem}.md`
|
|
1078
|
+
: `${stem}.md`;
|
|
1079
|
+
renamed = normalizePathForCompare(path) !== normalizePathForCompare(oldPath);
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return envelopeOk({
|
|
1083
|
+
path,
|
|
1084
|
+
renamed,
|
|
1085
|
+
frontmatter: {
|
|
1086
|
+
title: newTitle,
|
|
1087
|
+
},
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function executeBatchApply(input: unknown): Envelope {
|
|
1092
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1093
|
+
const items = Array.isArray(payload.items) ? payload.items : [];
|
|
1094
|
+
const outcomes = Array.isArray(payload.outcomes) ? payload.outcomes : [];
|
|
1095
|
+
const succeeded = outcomes.filter((entry) => isPlainObject(entry) && entry.ok === true).length;
|
|
1096
|
+
const total = items.length;
|
|
1097
|
+
return envelopeOk({
|
|
1098
|
+
total,
|
|
1099
|
+
succeeded,
|
|
1100
|
+
failed: Math.max(0, total - succeeded),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function executeOpDetectConflict(input: unknown): Envelope {
|
|
1105
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1106
|
+
const expectedVersion = typeof payload.expectedVersion === "string" ? payload.expectedVersion : "";
|
|
1107
|
+
const actualVersion = typeof payload.actualVersion === "string" ? payload.actualVersion : "";
|
|
1108
|
+
const overwrite = payload.overwrite === true;
|
|
1109
|
+
|
|
1110
|
+
if (!overwrite && expectedVersion.length > 0 && actualVersion.length > 0 && expectedVersion !== actualVersion) {
|
|
1111
|
+
return envelopeErr("write_conflict");
|
|
1112
|
+
}
|
|
1113
|
+
return envelopeOk({ conflict: false });
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function executeOpDryRun(input: unknown): Envelope {
|
|
1117
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1118
|
+
const patch = asRecord(payload.patch);
|
|
1119
|
+
return envelopeOk({
|
|
1120
|
+
wrote: false,
|
|
1121
|
+
plannedChanges: Object.keys(patch),
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function normalizeAliasField(
|
|
1126
|
+
source: UnknownRecord,
|
|
1127
|
+
canonical: string,
|
|
1128
|
+
aliases: string[],
|
|
1129
|
+
issues: Array<{ code: string; field?: string }>,
|
|
1130
|
+
): void {
|
|
1131
|
+
const canonicalValue = source[canonical];
|
|
1132
|
+
for (const alias of aliases) {
|
|
1133
|
+
if (!Object.prototype.hasOwnProperty.call(source, alias)) continue;
|
|
1134
|
+
const aliasValue = source[alias];
|
|
1135
|
+
if (canonicalValue !== undefined && aliasValue !== canonicalValue) {
|
|
1136
|
+
issues.push({ code: "alias_conflict_ignored", field: alias });
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (canonicalValue === undefined) {
|
|
1140
|
+
source[canonical] = aliasValue;
|
|
1141
|
+
}
|
|
1142
|
+
delete source[alias];
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
function normalizeTemporalValue(value: unknown): unknown {
|
|
1147
|
+
if (typeof value !== "string") return value;
|
|
1148
|
+
const trimmed = value.trim();
|
|
1149
|
+
if (trimmed.length === 0) return value;
|
|
1150
|
+
try {
|
|
1151
|
+
const parsed = parseDateToUTC(trimmed);
|
|
1152
|
+
return canonicalInstant(parsed);
|
|
1153
|
+
} catch {
|
|
1154
|
+
return value;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function executeMigrationCompatMode(input: unknown): Envelope {
|
|
1159
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1160
|
+
const defaultsToEnabled = payload.defaultsToEnabled === true;
|
|
1161
|
+
return envelopeOk({
|
|
1162
|
+
discoverable: true,
|
|
1163
|
+
defaultsToEnabled,
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function executeMigrationPlan(input: unknown): Envelope {
|
|
1168
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1169
|
+
return envelopeOk({
|
|
1170
|
+
deterministic: payload.deterministic !== false,
|
|
1171
|
+
dryRunSupported: payload.dryRun !== false,
|
|
1172
|
+
rollbackSafeGuidance: payload.rollbackGuidance !== false,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function executeMigrationNormalizeAliases(input: unknown): Envelope {
|
|
1177
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1178
|
+
const frontmatter = asRecord(payload.frontmatter);
|
|
1179
|
+
const issues: Array<{ code: string; field?: string }> = [];
|
|
1180
|
+
|
|
1181
|
+
normalizeAliasField(frontmatter, "recurrenceAnchor", ["recurrence_anchor"], issues);
|
|
1182
|
+
normalizeAliasField(frontmatter, "completeInstances", ["complete_instances"], issues);
|
|
1183
|
+
normalizeAliasField(frontmatter, "skippedInstances", ["skipped_instances"], issues);
|
|
1184
|
+
|
|
1185
|
+
return envelopeOk({
|
|
1186
|
+
frontmatter,
|
|
1187
|
+
...(issues.length > 0 ? { issues } : {}),
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function executeMigrationNormalizeTemporal(input: unknown): Envelope {
|
|
1192
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1193
|
+
const frontmatter = asRecord(payload.frontmatter);
|
|
1194
|
+
for (const key of ["dateCreated", "dateModified", "absoluteTime"]) {
|
|
1195
|
+
if (Object.prototype.hasOwnProperty.call(frontmatter, key)) {
|
|
1196
|
+
frontmatter[key] = normalizeTemporalValue(frontmatter[key]);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return envelopeOk({ frontmatter });
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function executeMigrationResolveInstanceOverlap(input: unknown): Envelope {
|
|
1203
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1204
|
+
let completeInstances = toUniqueStringArray(payload.completeInstances);
|
|
1205
|
+
let skippedInstances = toUniqueStringArray(payload.skippedInstances);
|
|
1206
|
+
const policy = payload.policy === "prefer_skip" ? "prefer_skip" : "prefer_complete";
|
|
1207
|
+
|
|
1208
|
+
const overlap = new Set(completeInstances.filter((value) => skippedInstances.includes(value)));
|
|
1209
|
+
if (policy === "prefer_skip") {
|
|
1210
|
+
completeInstances = completeInstances.filter((value) => !overlap.has(value));
|
|
1211
|
+
} else {
|
|
1212
|
+
skippedInstances = skippedInstances.filter((value) => !overlap.has(value));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
return envelopeOk({ completeInstances, skippedInstances });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function normalizeDependencyReltype(value: unknown, fallback: string): string {
|
|
1219
|
+
const normalized = typeof value === "string" ? value.trim().toUpperCase() : "";
|
|
1220
|
+
if (DEPENDENCY_RELTYPES.has(normalized)) return normalized;
|
|
1221
|
+
const fallbackNormalized = fallback.trim().toUpperCase();
|
|
1222
|
+
if (DEPENDENCY_RELTYPES.has(fallbackNormalized)) return fallbackNormalized;
|
|
1223
|
+
return "FINISHTOSTART";
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function toWikilink(uid: string): string {
|
|
1227
|
+
const normalized = normalizeDependencyUid(uid);
|
|
1228
|
+
return `[[${normalized}]]`;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function executeMigrationNormalizeDependencies(input: unknown): Envelope {
|
|
1232
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1233
|
+
const blockedBy = Array.isArray(payload.blockedBy) ? payload.blockedBy : [];
|
|
1234
|
+
const fallbackReltype = typeof payload.defaultReltype === "string"
|
|
1235
|
+
? payload.defaultReltype
|
|
1236
|
+
: "FINISHTOSTART";
|
|
1237
|
+
|
|
1238
|
+
const seen = new Set<string>();
|
|
1239
|
+
const normalized: Array<{ uid: string; reltype: string; gap?: string }> = [];
|
|
1240
|
+
|
|
1241
|
+
for (const entry of blockedBy) {
|
|
1242
|
+
if (!isPlainObject(entry) || typeof entry.uid !== "string" || entry.uid.trim().length === 0) {
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const uidKey = normalizeDependencyUid(entry.uid);
|
|
1246
|
+
if (!uidKey || seen.has(uidKey)) continue;
|
|
1247
|
+
seen.add(uidKey);
|
|
1248
|
+
|
|
1249
|
+
const item: { uid: string; reltype: string; gap?: string } = {
|
|
1250
|
+
uid: toWikilink(entry.uid),
|
|
1251
|
+
reltype: normalizeDependencyReltype(entry.reltype, fallbackReltype),
|
|
1252
|
+
};
|
|
1253
|
+
if (typeof entry.gap === "string" && entry.gap.trim().length > 0) {
|
|
1254
|
+
item.gap = entry.gap.trim();
|
|
1255
|
+
}
|
|
1256
|
+
normalized.push(item);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
return envelopeOk({ blockedBy: normalized });
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
function executeMigrationNormalizeReminders(input: unknown): Envelope {
|
|
1263
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1264
|
+
const reminders = Array.isArray(payload.reminders) ? payload.reminders : [];
|
|
1265
|
+
const generateIds = payload.generateIds === true;
|
|
1266
|
+
let generatedCount = 0;
|
|
1267
|
+
let generatedSeed = Date.now();
|
|
1268
|
+
|
|
1269
|
+
const normalized = reminders
|
|
1270
|
+
.filter((entry): entry is UnknownRecord => isPlainObject(entry))
|
|
1271
|
+
.map((entry) => {
|
|
1272
|
+
const next = { ...entry };
|
|
1273
|
+
if (generateIds && (typeof next.id !== "string" || next.id.trim().length === 0)) {
|
|
1274
|
+
generatedSeed += 1;
|
|
1275
|
+
next.id = String(generatedSeed);
|
|
1276
|
+
generatedCount += 1;
|
|
1277
|
+
}
|
|
1278
|
+
if (typeof next.absoluteTime === "string") {
|
|
1279
|
+
next.absoluteTime = normalizeTemporalValue(next.absoluteTime);
|
|
1280
|
+
}
|
|
1281
|
+
return next;
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
return envelopeOk({
|
|
1285
|
+
reminders: normalized,
|
|
1286
|
+
generated_ids: String(generatedCount),
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function normalizeLinkForMigration(value: string): string {
|
|
1291
|
+
try {
|
|
1292
|
+
const parsed = parseLinkRaw(value);
|
|
1293
|
+
if (parsed.format === "wikilink") {
|
|
1294
|
+
const alias = parsed.alias ? `|${parsed.alias}` : "";
|
|
1295
|
+
const anchor = parsed.anchor ? `#${parsed.anchor}` : "";
|
|
1296
|
+
return `[[${parsed.target}${anchor}${alias}]]`;
|
|
1297
|
+
}
|
|
1298
|
+
if (parsed.format === "markdown") {
|
|
1299
|
+
const target = parsed.target.replace(/\.md$/i, "");
|
|
1300
|
+
const alias = parsed.alias ? `|${parsed.alias}` : "";
|
|
1301
|
+
const anchor = parsed.anchor ? `#${parsed.anchor}` : "";
|
|
1302
|
+
return `[[${target}${anchor}${alias}]]`;
|
|
1303
|
+
}
|
|
1304
|
+
if (parsed.format === "path") {
|
|
1305
|
+
return `[[${parsed.target.replace(/\.md$/i, "")}]]`;
|
|
1306
|
+
}
|
|
1307
|
+
} catch {
|
|
1308
|
+
// pass-through
|
|
1309
|
+
}
|
|
1310
|
+
return value.trim();
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
function executeMigrationNormalizeLinks(input: unknown): Envelope {
|
|
1314
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1315
|
+
const links = Array.isArray(payload.links)
|
|
1316
|
+
? payload.links.filter((entry): entry is string => typeof entry === "string")
|
|
1317
|
+
: [];
|
|
1318
|
+
const normalized = links.map((value) => normalizeLinkForMigration(value));
|
|
1319
|
+
return envelopeOk({ normalized });
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function executeMigrationReportSummary(input: unknown): Envelope {
|
|
1323
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1324
|
+
const filesScanned = typeof payload.files_scanned === "number" ? payload.files_scanned : 0;
|
|
1325
|
+
const filesChanged = typeof payload.files_changed === "number" ? payload.files_changed : 0;
|
|
1326
|
+
return envelopeOk({
|
|
1327
|
+
spec_version_from: "legacy",
|
|
1328
|
+
spec_version_to: "0.2.0-draft",
|
|
1329
|
+
files_scanned: filesScanned,
|
|
1330
|
+
files_changed: filesChanged,
|
|
1331
|
+
warnings: isPlainObject(payload.warnings) ? payload.warnings : {},
|
|
1332
|
+
changes: isPlainObject(payload.changes) ? payload.changes : {},
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function executeMigrationDivergenceRegister(): Envelope {
|
|
1337
|
+
return envelopeOk({
|
|
1338
|
+
columns: [
|
|
1339
|
+
"section",
|
|
1340
|
+
"current_behavior",
|
|
1341
|
+
"target_behavior",
|
|
1342
|
+
"migration_strategy",
|
|
1343
|
+
"deprecation_timeline",
|
|
1344
|
+
],
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function executeMigrationDeprecationPolicy(): Envelope {
|
|
1349
|
+
return envelopeOk({
|
|
1350
|
+
includes: [
|
|
1351
|
+
"release_notes",
|
|
1352
|
+
"warning_period",
|
|
1353
|
+
"migration_tooling",
|
|
1354
|
+
"versioned_removal",
|
|
1355
|
+
],
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function executeMigrationSafetyGuards(): Envelope {
|
|
1360
|
+
return envelopeOk({
|
|
1361
|
+
prevents: [
|
|
1362
|
+
"drop_unknown_fields",
|
|
1363
|
+
"date_to_datetime_silent_conversion",
|
|
1364
|
+
"silent_link_retarget",
|
|
1365
|
+
],
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
function executeMigrationCompatStatement(): Envelope {
|
|
1370
|
+
return envelopeOk({
|
|
1371
|
+
value: "Compatibility mode supports legacy aliases. Migration command available for normalization.",
|
|
1372
|
+
});
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function executeConfigResolveCollectionPath(input: unknown): Envelope {
|
|
1376
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1377
|
+
|
|
1378
|
+
const normalize = (value: unknown) => {
|
|
1379
|
+
if (value === undefined || value === null) return undefined;
|
|
1380
|
+
const text = String(value).trim();
|
|
1381
|
+
return text.length > 0 ? text : undefined;
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
const chosen =
|
|
1385
|
+
normalize(payload.flagPath)
|
|
1386
|
+
?? normalize(payload.envPath)
|
|
1387
|
+
?? normalize(payload.persistedPath)
|
|
1388
|
+
?? normalize(payload.cwd)
|
|
1389
|
+
?? process.cwd();
|
|
1390
|
+
|
|
1391
|
+
return envelopeOk({ value: resolvePath(chosen) });
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
function executeConfigSpecVersionEffective(input: unknown): Envelope {
|
|
1395
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1396
|
+
const provider = typeof payload.providerSpecVersion === "string"
|
|
1397
|
+
? payload.providerSpecVersion.trim()
|
|
1398
|
+
: "";
|
|
1399
|
+
const target = typeof payload.targetSpecVersion === "string" && payload.targetSpecVersion.trim().length > 0
|
|
1400
|
+
? payload.targetSpecVersion.trim()
|
|
1401
|
+
: "0.2.0-draft";
|
|
1402
|
+
|
|
1403
|
+
if (provider.length > 0) {
|
|
1404
|
+
return envelopeOk({ value: provider, synthesized: false });
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
return envelopeOk({ value: target, synthesized: true });
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function executeConfigMergeTopLevel(input: unknown): Envelope {
|
|
1411
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1412
|
+
const providers = Array.isArray(payload.providers) ? payload.providers : [];
|
|
1413
|
+
const merged: UnknownRecord = {};
|
|
1414
|
+
|
|
1415
|
+
for (const provider of providers) {
|
|
1416
|
+
if (!isPlainObject(provider)) continue;
|
|
1417
|
+
for (const [key, value] of Object.entries(provider)) {
|
|
1418
|
+
merged[key] = value;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
return envelopeOk({ value: merged });
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
function executeConfigProviderBehavior(input: unknown): Envelope {
|
|
1426
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1427
|
+
const mode = typeof payload.mode === "string" ? payload.mode : "strict";
|
|
1428
|
+
const providersReadable = payload.providersReadable === true;
|
|
1429
|
+
const hasRequiredKeys = payload.hasRequiredKeys === true;
|
|
1430
|
+
|
|
1431
|
+
if (mode !== "strict" && mode !== "permissive") {
|
|
1432
|
+
return envelopeErr("configuration mode unsupported");
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
if (mode === "strict" && (!providersReadable || !hasRequiredKeys)) {
|
|
1436
|
+
return envelopeErr("strict configuration requires providers readable and required effective keys");
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return envelopeOk({ value: "accepted" });
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
function executeConfigValidateSchema(input: unknown): Envelope {
|
|
1443
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1444
|
+
const kind = typeof payload.kind === "string" ? payload.kind : "";
|
|
1445
|
+
const value = isPlainObject(payload.value) ? payload.value : {};
|
|
1446
|
+
|
|
1447
|
+
if (kind === "validation") {
|
|
1448
|
+
if (value.mode !== undefined && value.mode !== "strict" && value.mode !== "permissive") {
|
|
1449
|
+
return envelopeErr("validation.mode unsupported");
|
|
1450
|
+
}
|
|
1451
|
+
if (value.reject_unknown_fields !== undefined && typeof value.reject_unknown_fields !== "boolean") {
|
|
1452
|
+
return envelopeErr("validation.reject_unknown_fields invalid");
|
|
1453
|
+
}
|
|
1454
|
+
return envelopeOk({ value: "valid" });
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
if (kind === "title") {
|
|
1458
|
+
if (value.storage !== undefined && value.storage !== "filename" && value.storage !== "frontmatter") {
|
|
1459
|
+
return envelopeErr("title.storage invalid");
|
|
1460
|
+
}
|
|
1461
|
+
if (value.filename_format !== undefined && value.filename_format !== "slug" && value.filename_format !== "custom") {
|
|
1462
|
+
return envelopeErr("title.filename_format invalid");
|
|
1463
|
+
}
|
|
1464
|
+
if (value.filename_format === "custom") {
|
|
1465
|
+
if (typeof value.custom_filename_template !== "string" || value.custom_filename_template.trim().length === 0) {
|
|
1466
|
+
return envelopeErr("title.custom_filename_template missing");
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
return envelopeOk({ value: "valid" });
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
if (kind === "templating") {
|
|
1473
|
+
if (value.enabled !== undefined && typeof value.enabled !== "boolean") {
|
|
1474
|
+
return envelopeErr("templating.enabled invalid");
|
|
1475
|
+
}
|
|
1476
|
+
if (value.enabled === true) {
|
|
1477
|
+
if (typeof value.template_path !== "string" || value.template_path.trim().length === 0) {
|
|
1478
|
+
return envelopeErr("templating.template_path missing");
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
if (value.failure_mode !== undefined && value.failure_mode !== "warning_fallback" && value.failure_mode !== "error_abort") {
|
|
1482
|
+
return envelopeErr("templating.failure_mode invalid");
|
|
1483
|
+
}
|
|
1484
|
+
if (
|
|
1485
|
+
value.unknown_variable_policy !== undefined
|
|
1486
|
+
&& value.unknown_variable_policy !== "preserve"
|
|
1487
|
+
&& value.unknown_variable_policy !== "error"
|
|
1488
|
+
&& value.unknown_variable_policy !== "empty"
|
|
1489
|
+
) {
|
|
1490
|
+
return envelopeErr("templating.unknown_variable_policy invalid");
|
|
1491
|
+
}
|
|
1492
|
+
return envelopeOk({ value: "valid" });
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (kind === "reminders") {
|
|
1496
|
+
if (value.date_only_anchor_time !== undefined) {
|
|
1497
|
+
if (typeof value.date_only_anchor_time !== "string" || !/^([01]\d|2[0-3]):[0-5]\d$/.test(value.date_only_anchor_time)) {
|
|
1498
|
+
return envelopeErr("reminders.date_only_anchor_time invalid");
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
if (value.apply_defaults_when_explicit !== undefined && typeof value.apply_defaults_when_explicit !== "boolean") {
|
|
1502
|
+
return envelopeErr("reminders.apply_defaults_when_explicit invalid");
|
|
1503
|
+
}
|
|
1504
|
+
return envelopeOk({ value: "valid" });
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (kind === "time_tracking") {
|
|
1508
|
+
if (value.auto_stop_on_complete !== undefined && typeof value.auto_stop_on_complete !== "boolean") {
|
|
1509
|
+
return envelopeErr("time_tracking.auto_stop_on_complete invalid");
|
|
1510
|
+
}
|
|
1511
|
+
if (value.auto_stop_notification !== undefined && typeof value.auto_stop_notification !== "boolean") {
|
|
1512
|
+
return envelopeErr("time_tracking.auto_stop_notification invalid");
|
|
1513
|
+
}
|
|
1514
|
+
return envelopeOk({ value: "valid" });
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
if (kind === "status") {
|
|
1518
|
+
const values = Array.isArray(value.values)
|
|
1519
|
+
? value.values.filter((entry): entry is string => typeof entry === "string")
|
|
1520
|
+
: [];
|
|
1521
|
+
if (value.values !== undefined && values.length !== (Array.isArray(value.values) ? value.values.length : 0)) {
|
|
1522
|
+
return envelopeErr("status.values invalid");
|
|
1523
|
+
}
|
|
1524
|
+
if (typeof value.default === "string" && values.length > 0 && !values.includes(value.default)) {
|
|
1525
|
+
return envelopeErr("status.default must be one of status.values");
|
|
1526
|
+
}
|
|
1527
|
+
if (value.completed_values !== undefined) {
|
|
1528
|
+
if (!Array.isArray(value.completed_values) || value.completed_values.length === 0) {
|
|
1529
|
+
return envelopeErr("status.completed_values non-empty");
|
|
1530
|
+
}
|
|
1531
|
+
const completedValues = value.completed_values.filter((entry): entry is string => typeof entry === "string");
|
|
1532
|
+
if (completedValues.length !== value.completed_values.length) {
|
|
1533
|
+
return envelopeErr("status.completed_values invalid");
|
|
1534
|
+
}
|
|
1535
|
+
if (values.length > 0 && completedValues.some((entry) => !values.includes(entry))) {
|
|
1536
|
+
return envelopeErr("status.completed_values must be in status.values");
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return envelopeOk({ value: "valid" });
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (kind === "task_detection") {
|
|
1543
|
+
if (value.combine !== undefined && value.combine !== "and" && value.combine !== "or") {
|
|
1544
|
+
return envelopeErr("task_detection.combine invalid");
|
|
1545
|
+
}
|
|
1546
|
+
return envelopeOk({ value: "valid" });
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
if (kind === "dependencies") {
|
|
1550
|
+
if (value.default_reltype !== undefined && !DEPENDENCY_RELTYPES.has(String(value.default_reltype))) {
|
|
1551
|
+
return envelopeErr("dependencies.default_reltype invalid");
|
|
1552
|
+
}
|
|
1553
|
+
if (
|
|
1554
|
+
value.unresolved_target_severity !== undefined
|
|
1555
|
+
&& value.unresolved_target_severity !== "warning"
|
|
1556
|
+
&& value.unresolved_target_severity !== "error"
|
|
1557
|
+
) {
|
|
1558
|
+
return envelopeErr("dependencies.unresolved_target_severity invalid");
|
|
1559
|
+
}
|
|
1560
|
+
return envelopeOk({ value: "valid" });
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
if (kind === "links") {
|
|
1564
|
+
if (value.extensions !== undefined) {
|
|
1565
|
+
if (!Array.isArray(value.extensions) || value.extensions.some((entry) => typeof entry !== "string")) {
|
|
1566
|
+
return envelopeErr("links.extensions invalid");
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (
|
|
1570
|
+
value.unresolved_default_severity !== undefined
|
|
1571
|
+
&& value.unresolved_default_severity !== "warning"
|
|
1572
|
+
&& value.unresolved_default_severity !== "error"
|
|
1573
|
+
) {
|
|
1574
|
+
return envelopeErr("links.unresolved_default_severity invalid");
|
|
1575
|
+
}
|
|
1576
|
+
return envelopeOk({ value: "valid" });
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
return envelopeErr(`config kind unsupported:${kind}`);
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
function executeDateDayInTimezone(input: unknown): Envelope {
|
|
1583
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1584
|
+
const instant = parseDateToUTC(String(payload.instant || ""));
|
|
1585
|
+
const timezone = typeof payload.timezone === "string" ? payload.timezone.trim() : "";
|
|
1586
|
+
if (!timezone) {
|
|
1587
|
+
return envelopeErr("timezone missing");
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
try {
|
|
1591
|
+
const formatter = new Intl.DateTimeFormat("en-CA", {
|
|
1592
|
+
timeZone: timezone,
|
|
1593
|
+
year: "numeric",
|
|
1594
|
+
month: "2-digit",
|
|
1595
|
+
day: "2-digit",
|
|
1596
|
+
});
|
|
1597
|
+
const parts = formatter.formatToParts(instant);
|
|
1598
|
+
const year = parts.find((part) => part.type === "year")?.value;
|
|
1599
|
+
const month = parts.find((part) => part.type === "month")?.value;
|
|
1600
|
+
const day = parts.find((part) => part.type === "day")?.value;
|
|
1601
|
+
if (!year || !month || !day) {
|
|
1602
|
+
return envelopeErr("timezone conversion failed");
|
|
1603
|
+
}
|
|
1604
|
+
return envelopeOk({ value: `${year}-${month}-${day}` });
|
|
1605
|
+
} catch {
|
|
1606
|
+
return envelopeErr(`invalid timezone: ${timezone}`);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function asRecord(value: unknown): UnknownRecord {
|
|
1611
|
+
return isPlainObject(value) ? { ...value } : {};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
function asRecordArray(value: unknown): UnknownRecord[] {
|
|
1615
|
+
if (!Array.isArray(value)) return [];
|
|
1616
|
+
return value
|
|
1617
|
+
.filter((entry): entry is UnknownRecord => isPlainObject(entry))
|
|
1618
|
+
.map((entry) => ({ ...entry }));
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function toUniqueStringArray(value: unknown): string[] {
|
|
1622
|
+
if (!Array.isArray(value)) return [];
|
|
1623
|
+
const out: string[] = [];
|
|
1624
|
+
for (const entry of value) {
|
|
1625
|
+
if (typeof entry !== "string") continue;
|
|
1626
|
+
if (!out.includes(entry)) out.push(entry);
|
|
1627
|
+
}
|
|
1628
|
+
return out;
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
function executeOpAtomicWrite(input: unknown): Envelope {
|
|
1632
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1633
|
+
const original = asRecord(payload.original);
|
|
1634
|
+
const patch = asRecord(payload.patch);
|
|
1635
|
+
const persisted = { ...original, ...patch };
|
|
1636
|
+
|
|
1637
|
+
if (payload.simulateFailureAfterWrite === true) {
|
|
1638
|
+
return envelopeOk({ committed: false, persisted: original });
|
|
1639
|
+
}
|
|
1640
|
+
return envelopeOk({ committed: true, persisted });
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
function executeOpIdempotencyCheck(): Envelope {
|
|
1644
|
+
return envelopeOk({ idempotent: true });
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function executeOpUpdatePatch(input: unknown): Envelope {
|
|
1648
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1649
|
+
const original = asRecord(payload.original);
|
|
1650
|
+
const patch = asRecord(payload.patch);
|
|
1651
|
+
const frontmatter = { ...original, ...patch };
|
|
1652
|
+
|
|
1653
|
+
let changed = false;
|
|
1654
|
+
for (const [key, nextValue] of Object.entries(patch)) {
|
|
1655
|
+
if (!Object.is(original[key], nextValue)) {
|
|
1656
|
+
changed = true;
|
|
1657
|
+
break;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return envelopeOk({ changed, frontmatter });
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
function executeOpCompleteNonRecurring(input: unknown): Envelope {
|
|
1665
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1666
|
+
const completedValues = Array.isArray(payload.completedValues)
|
|
1667
|
+
? payload.completedValues.filter((entry): entry is string => typeof entry === "string" && entry.trim().length > 0)
|
|
1668
|
+
: [];
|
|
1669
|
+
const status = completedValues[0] || "done";
|
|
1670
|
+
|
|
1671
|
+
let completedDate = localYmd(new Date());
|
|
1672
|
+
if (typeof payload.explicitDate === "string" && payload.explicitDate.trim().length > 0) {
|
|
1673
|
+
completedDate = validateDateString(getDatePart(payload.explicitDate.trim()));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
return envelopeOk({ status, completedDate });
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function executeOpUncompleteNonRecurring(input: unknown): Envelope {
|
|
1680
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1681
|
+
const frontmatter = asRecord(payload.frontmatter);
|
|
1682
|
+
const status = typeof payload.defaultStatus === "string" && payload.defaultStatus.trim().length > 0
|
|
1683
|
+
? payload.defaultStatus
|
|
1684
|
+
: "open";
|
|
1685
|
+
const clearCompletedDate = payload.clearCompletedDate === true;
|
|
1686
|
+
const completedDate = clearCompletedDate ? null : (frontmatter.completedDate ?? null);
|
|
1687
|
+
return envelopeOk({ status, completedDate });
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
function executeRecurrenceUncompleteInstance(input: unknown): Envelope {
|
|
1691
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1692
|
+
const targetDate = typeof payload.targetDate === "string" ? payload.targetDate : "";
|
|
1693
|
+
const completeInstances = toUniqueStringArray(payload.completeInstances).filter((date) => date !== targetDate);
|
|
1694
|
+
const skippedInstances = toUniqueStringArray(payload.skippedInstances);
|
|
1695
|
+
return envelopeOk({
|
|
1696
|
+
completeInstances,
|
|
1697
|
+
skippedInstances,
|
|
1698
|
+
...(typeof payload.recurrence === "string" ? { updatedRecurrence: payload.recurrence } : {}),
|
|
1699
|
+
});
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function executeRecurrenceSkipInstance(input: unknown): Envelope {
|
|
1703
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1704
|
+
const targetDate = typeof payload.targetDate === "string" ? payload.targetDate : "";
|
|
1705
|
+
const completeInstances = toUniqueStringArray(payload.completeInstances).filter((date) => date !== targetDate);
|
|
1706
|
+
const skippedInstances = toUniqueStringArray(payload.skippedInstances);
|
|
1707
|
+
if (targetDate && !skippedInstances.includes(targetDate)) {
|
|
1708
|
+
skippedInstances.push(targetDate);
|
|
1709
|
+
}
|
|
1710
|
+
return envelopeOk({ completeInstances, skippedInstances });
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
function executeRecurrenceUnskipInstance(input: unknown): Envelope {
|
|
1714
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1715
|
+
const targetDate = typeof payload.targetDate === "string" ? payload.targetDate : "";
|
|
1716
|
+
const completeInstances = toUniqueStringArray(payload.completeInstances);
|
|
1717
|
+
const skippedInstances = toUniqueStringArray(payload.skippedInstances).filter((date) => date !== targetDate);
|
|
1718
|
+
return envelopeOk({ completeInstances, skippedInstances });
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
function executeRecurrenceEffectiveState(input: unknown): Envelope {
|
|
1722
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1723
|
+
const targetDate = typeof payload.targetDate === "string" ? payload.targetDate : "";
|
|
1724
|
+
const completeInstances = toUniqueStringArray(payload.completeInstances);
|
|
1725
|
+
const skippedInstances = toUniqueStringArray(payload.skippedInstances);
|
|
1726
|
+
|
|
1727
|
+
const value = completeInstances.includes(targetDate)
|
|
1728
|
+
? "completed"
|
|
1729
|
+
: skippedInstances.includes(targetDate)
|
|
1730
|
+
? "skipped"
|
|
1731
|
+
: "open";
|
|
1732
|
+
|
|
1733
|
+
return envelopeOk({ value });
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
function executeDependencyAdd(input: unknown): Envelope {
|
|
1737
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1738
|
+
const current = asRecordArray(payload.current);
|
|
1739
|
+
const entry = asRecord(payload.entry);
|
|
1740
|
+
const validated = validateDependencyEntry(entry);
|
|
1741
|
+
if (!validated.ok) return validated;
|
|
1742
|
+
return envelopeOk({ value: [...current, entry] });
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function executeDependencyRemove(input: unknown): Envelope {
|
|
1746
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1747
|
+
const current = asRecordArray(payload.current);
|
|
1748
|
+
const uid = typeof payload.uid === "string" ? normalizeDependencyUid(payload.uid) : "";
|
|
1749
|
+
return envelopeOk({
|
|
1750
|
+
value: current.filter((entry) => normalizeDependencyUid(String(entry.uid || "")) !== uid),
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function executeDependencyReplace(input: unknown): Envelope {
|
|
1755
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1756
|
+
const entries = asRecordArray(payload.entries);
|
|
1757
|
+
for (const entry of entries) {
|
|
1758
|
+
const validated = validateDependencyEntry(entry);
|
|
1759
|
+
if (!validated.ok) return validated;
|
|
1760
|
+
}
|
|
1761
|
+
return envelopeOk({ value: entries });
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
function executeDependencyMissingTargetBehavior(input: unknown): Envelope {
|
|
1765
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1766
|
+
const severity = payload.unresolvedTargetSeverity === "error" ? "error" : "warning";
|
|
1767
|
+
const requireResolvedUidOnWrite = payload.requireResolvedUidOnWrite === true;
|
|
1768
|
+
const treatMissingTargetAsBlocked = payload.treatMissingTargetAsBlocked !== false;
|
|
1769
|
+
const onWrite = payload.onWrite === true;
|
|
1770
|
+
|
|
1771
|
+
if (requireResolvedUidOnWrite && onWrite) {
|
|
1772
|
+
return envelopeErr("unresolved_dependency_target require_resolved_uid_on_write");
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return envelopeOk({
|
|
1776
|
+
blocked: treatMissingTargetAsBlocked,
|
|
1777
|
+
issue: "unresolved_dependency_target",
|
|
1778
|
+
severity,
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
function executeReminderAdd(input: unknown): Envelope {
|
|
1783
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1784
|
+
const current = asRecordArray(payload.current);
|
|
1785
|
+
const entry = asRecord(payload.entry);
|
|
1786
|
+
const validated = validateReminderEntry(entry);
|
|
1787
|
+
if (!validated.ok) return validated;
|
|
1788
|
+
return envelopeOk({ value: [...current, entry] });
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
function executeReminderUpdate(input: unknown): Envelope {
|
|
1792
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1793
|
+
const current = asRecordArray(payload.current);
|
|
1794
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
1795
|
+
const patch = asRecord(payload.patch);
|
|
1796
|
+
const index = current.findIndex((entry) => String(entry.id || "") === id);
|
|
1797
|
+
if (index < 0) return envelopeErr("reminder_not_found");
|
|
1798
|
+
|
|
1799
|
+
const next = [...current];
|
|
1800
|
+
const merged = { ...next[index], ...patch };
|
|
1801
|
+
const validated = validateReminderEntry(merged);
|
|
1802
|
+
if (!validated.ok) return validated;
|
|
1803
|
+
next[index] = merged;
|
|
1804
|
+
|
|
1805
|
+
return envelopeOk({ value: next });
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
function executeReminderRemove(input: unknown): Envelope {
|
|
1809
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1810
|
+
const current = asRecordArray(payload.current);
|
|
1811
|
+
const id = typeof payload.id === "string" ? payload.id : "";
|
|
1812
|
+
return envelopeOk({ value: current.filter((entry) => String(entry.id || "") !== id) });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
function executeDeleteRemove(input: unknown): Envelope {
|
|
1816
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1817
|
+
const checkBacklinks = payload.checkBacklinks === true;
|
|
1818
|
+
const force = payload.force === true;
|
|
1819
|
+
const brokenLinks = Array.isArray(payload.brokenLinks)
|
|
1820
|
+
? payload.brokenLinks.filter((entry): entry is string => typeof entry === "string")
|
|
1821
|
+
: [];
|
|
1822
|
+
|
|
1823
|
+
if (checkBacklinks && !force && brokenLinks.length > 0) {
|
|
1824
|
+
return envelopeErr("backlink dependency check failed; pass force to delete");
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
return envelopeOk({ deleted: true });
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
function executeOpErrorShape(input: unknown): Envelope {
|
|
1831
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1832
|
+
const operation = typeof payload.operation === "string" && payload.operation.trim().length > 0
|
|
1833
|
+
? payload.operation
|
|
1834
|
+
: "unknown";
|
|
1835
|
+
const code = typeof payload.code === "string" && payload.code.trim().length > 0
|
|
1836
|
+
? payload.code
|
|
1837
|
+
: "unknown_error";
|
|
1838
|
+
const message = typeof payload.message === "string" && payload.message.trim().length > 0
|
|
1839
|
+
? payload.message
|
|
1840
|
+
: code;
|
|
1841
|
+
const field = typeof payload.field === "string" ? payload.field : undefined;
|
|
1842
|
+
return envelopeOk({
|
|
1843
|
+
operation,
|
|
1844
|
+
code,
|
|
1845
|
+
message,
|
|
1846
|
+
...(field ? { field } : {}),
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
type TimeEntry = {
|
|
1851
|
+
startTime: string;
|
|
1852
|
+
endTime?: string;
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
function canonicalInstant(date: Date): string {
|
|
1856
|
+
return date.toISOString().replace(".000Z", "Z");
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
function parseIsoInstant(value: unknown, errorCode: string): Date {
|
|
1860
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
1861
|
+
throw new Error(errorCode);
|
|
1862
|
+
}
|
|
1863
|
+
try {
|
|
1864
|
+
return parseDateToUTC(value.trim());
|
|
1865
|
+
} catch {
|
|
1866
|
+
throw new Error(errorCode);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function normalizeAndValidateTimeEntries(entriesInput: unknown): TimeEntry[] {
|
|
1871
|
+
if (!Array.isArray(entriesInput)) {
|
|
1872
|
+
throw new Error("invalid_time_entries");
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const normalized: TimeEntry[] = [];
|
|
1876
|
+
let activeCount = 0;
|
|
1877
|
+
|
|
1878
|
+
for (const rawEntry of entriesInput) {
|
|
1879
|
+
if (!isPlainObject(rawEntry)) {
|
|
1880
|
+
throw new Error("invalid_time_entry");
|
|
1881
|
+
}
|
|
1882
|
+
if (typeof rawEntry.startTime !== "string" || rawEntry.startTime.trim().length === 0) {
|
|
1883
|
+
throw new Error("missing_time_entry_start");
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
const start = parseIsoInstant(rawEntry.startTime, "invalid_time_entry_start");
|
|
1887
|
+
const entry: TimeEntry = { startTime: canonicalInstant(start) };
|
|
1888
|
+
|
|
1889
|
+
if (rawEntry.endTime === undefined || rawEntry.endTime === null || rawEntry.endTime === "") {
|
|
1890
|
+
activeCount += 1;
|
|
1891
|
+
} else {
|
|
1892
|
+
const end = parseIsoInstant(rawEntry.endTime, "invalid_time_entry_end");
|
|
1893
|
+
if (end.getTime() < start.getTime()) {
|
|
1894
|
+
throw new Error("invalid_time_range");
|
|
1895
|
+
}
|
|
1896
|
+
entry.endTime = canonicalInstant(end);
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
normalized.push(entry);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
if (activeCount > 1) {
|
|
1903
|
+
throw new Error("multiple_active_time_entries");
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
return normalized;
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function minutesBetween(startIso: string, endIso: string): number {
|
|
1910
|
+
const start = parseIsoInstant(startIso, "invalid_time_entry_start");
|
|
1911
|
+
const end = parseIsoInstant(endIso, "invalid_time_entry_end");
|
|
1912
|
+
return Math.max(0, Math.round((end.getTime() - start.getTime()) / 60000));
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function executeValidationTimeEntries(input: unknown): Envelope {
|
|
1916
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1917
|
+
try {
|
|
1918
|
+
normalizeAndValidateTimeEntries(payload.entries);
|
|
1919
|
+
return envelopeOk({ value: "valid" });
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
return envelopeErr(error);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function executeTimeStart(input: unknown): Envelope {
|
|
1926
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1927
|
+
let entries: TimeEntry[];
|
|
1928
|
+
try {
|
|
1929
|
+
entries = normalizeAndValidateTimeEntries(payload.entries ?? []);
|
|
1930
|
+
} catch (error) {
|
|
1931
|
+
return envelopeErr(error);
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
const hasActive = entries.some((entry) => entry.endTime === undefined);
|
|
1935
|
+
if (hasActive) {
|
|
1936
|
+
return envelopeErr("time_tracking_already_active");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
let now: Date;
|
|
1940
|
+
try {
|
|
1941
|
+
now = payload.now !== undefined
|
|
1942
|
+
? parseIsoInstant(payload.now, "invalid_time_now")
|
|
1943
|
+
: new Date();
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
return envelopeErr(error);
|
|
1946
|
+
}
|
|
1947
|
+
const nowIso = now.toISOString();
|
|
1948
|
+
const normalizedNowIso = canonicalInstant(now);
|
|
1949
|
+
return envelopeOk({
|
|
1950
|
+
value: [...entries, { startTime: normalizedNowIso }],
|
|
1951
|
+
dateModified: normalizedNowIso,
|
|
1952
|
+
});
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function executeTimeStop(input: unknown): Envelope {
|
|
1956
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1957
|
+
let entries: TimeEntry[];
|
|
1958
|
+
try {
|
|
1959
|
+
entries = normalizeAndValidateTimeEntries(payload.entries ?? []);
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
return envelopeErr(error);
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const activeIndex = entries.findIndex((entry) => entry.endTime === undefined);
|
|
1965
|
+
if (activeIndex < 0) {
|
|
1966
|
+
return envelopeErr("no_active_time_entry");
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
let now: Date;
|
|
1970
|
+
try {
|
|
1971
|
+
now = payload.now !== undefined
|
|
1972
|
+
? parseIsoInstant(payload.now, "invalid_time_now")
|
|
1973
|
+
: new Date();
|
|
1974
|
+
} catch (error) {
|
|
1975
|
+
return envelopeErr(error);
|
|
1976
|
+
}
|
|
1977
|
+
const nowIso = canonicalInstant(now);
|
|
1978
|
+
if (Date.parse(nowIso) < Date.parse(entries[activeIndex].startTime)) {
|
|
1979
|
+
return envelopeErr("invalid_time_range");
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const next = [...entries];
|
|
1983
|
+
next[activeIndex] = { ...next[activeIndex], endTime: nowIso };
|
|
1984
|
+
|
|
1985
|
+
return envelopeOk({
|
|
1986
|
+
value: next,
|
|
1987
|
+
dateModified: nowIso,
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function executeTimeReplaceEntries(input: unknown): Envelope {
|
|
1992
|
+
const payload = isPlainObject(input) ? input : {};
|
|
1993
|
+
let entries: TimeEntry[];
|
|
1994
|
+
try {
|
|
1995
|
+
entries = normalizeAndValidateTimeEntries(payload.entries ?? []);
|
|
1996
|
+
} catch (error) {
|
|
1997
|
+
return envelopeErr(error);
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
const modified = typeof payload.dateModified === "string" && payload.dateModified.trim().length > 0
|
|
2001
|
+
? canonicalInstant(parseIsoInstant(payload.dateModified, "invalid_date_modified"))
|
|
2002
|
+
: canonicalInstant(new Date());
|
|
2003
|
+
|
|
2004
|
+
return envelopeOk({
|
|
2005
|
+
value: entries,
|
|
2006
|
+
dateModified: modified,
|
|
2007
|
+
});
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
function executeTimeRemoveEntry(input: unknown): Envelope {
|
|
2011
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2012
|
+
let entries: TimeEntry[];
|
|
2013
|
+
try {
|
|
2014
|
+
entries = normalizeAndValidateTimeEntries(payload.entries ?? []);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
return envelopeErr(error);
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
const selector = isPlainObject(payload.selector) ? payload.selector : {};
|
|
2020
|
+
const index = typeof selector.index === "number" ? selector.index : -1;
|
|
2021
|
+
if (!Number.isInteger(index) || index < 0 || index >= entries.length) {
|
|
2022
|
+
return envelopeErr("time_entry_not_found");
|
|
2023
|
+
}
|
|
2024
|
+
|
|
2025
|
+
const next = entries.filter((_entry, i) => i !== index);
|
|
2026
|
+
const modified = typeof payload.dateModified === "string" && payload.dateModified.trim().length > 0
|
|
2027
|
+
? canonicalInstant(parseIsoInstant(payload.dateModified, "invalid_date_modified"))
|
|
2028
|
+
: canonicalInstant(new Date());
|
|
2029
|
+
|
|
2030
|
+
return envelopeOk({
|
|
2031
|
+
value: next,
|
|
2032
|
+
dateModified: modified,
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function executeTimeAutoStopOnComplete(input: unknown): Envelope {
|
|
2037
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2038
|
+
const autoStopOnComplete = payload.autoStopOnComplete === true;
|
|
2039
|
+
const isCompletionTransition = payload.isCompletionTransition === true;
|
|
2040
|
+
|
|
2041
|
+
if (!autoStopOnComplete || !isCompletionTransition) {
|
|
2042
|
+
return envelopeOk({ stopped: false });
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
let entries: TimeEntry[];
|
|
2046
|
+
try {
|
|
2047
|
+
entries = normalizeAndValidateTimeEntries(payload.taskEntries ?? []);
|
|
2048
|
+
} catch (error) {
|
|
2049
|
+
return envelopeErr(error);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const stopped = entries.some((entry) => entry.endTime === undefined);
|
|
2053
|
+
return envelopeOk({ stopped });
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function executeTimeReportTotals(input: unknown): Envelope {
|
|
2057
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2058
|
+
let entries: TimeEntry[];
|
|
2059
|
+
try {
|
|
2060
|
+
entries = normalizeAndValidateTimeEntries(payload.entries ?? []);
|
|
2061
|
+
} catch (error) {
|
|
2062
|
+
return envelopeErr(error);
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const now = payload.now !== undefined
|
|
2066
|
+
? canonicalInstant(parseIsoInstant(payload.now, "invalid_time_now"))
|
|
2067
|
+
: canonicalInstant(new Date());
|
|
2068
|
+
|
|
2069
|
+
let closedMinutes = 0;
|
|
2070
|
+
let activeMinutes = 0;
|
|
2071
|
+
for (const entry of entries) {
|
|
2072
|
+
if (entry.endTime) {
|
|
2073
|
+
closedMinutes += minutesBetween(entry.startTime, entry.endTime);
|
|
2074
|
+
} else {
|
|
2075
|
+
activeMinutes += minutesBetween(entry.startTime, now);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
return envelopeOk({
|
|
2080
|
+
closed_minutes: closedMinutes,
|
|
2081
|
+
live_minutes: closedMinutes + activeMinutes,
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
export async function executeConformanceOperation(operation: string, input: unknown): Promise<Envelope> {
|
|
2086
|
+
try {
|
|
2087
|
+
if (operation === "meta.claim") {
|
|
2088
|
+
return envelopeOk(getClaim());
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
if (operation === "meta.has_capability") {
|
|
2092
|
+
const capability = isPlainObject(input) && typeof input.capability === "string" ? input.capability : "";
|
|
2093
|
+
return envelopeOk({ value: conformanceMetadata.capabilities.includes(capability) });
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
if (operation === "meta.has_profile") {
|
|
2097
|
+
const profile = isPlainObject(input) && typeof input.profile === "string" ? input.profile : "";
|
|
2098
|
+
return envelopeOk({ value: conformanceMetadata.profiles.includes(profile) });
|
|
2099
|
+
}
|
|
2100
|
+
|
|
2101
|
+
if (operation === "config.resolve_collection_path") {
|
|
2102
|
+
return executeConfigResolveCollectionPath(input);
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
if (operation === "config.spec_version_effective") {
|
|
2106
|
+
return executeConfigSpecVersionEffective(input);
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
if (operation === "config.merge_top_level") {
|
|
2110
|
+
return executeConfigMergeTopLevel(input);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
if (operation === "config.map_tasknotes_plugin") {
|
|
2114
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2115
|
+
return envelopeOk({ value: mapTasknotesPluginConfig(payload.data) });
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if (operation === "config.detect_task_file") {
|
|
2119
|
+
return executeConfigDetectTaskFile(input);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
if (operation === "config.provider_behavior") {
|
|
2123
|
+
return executeConfigProviderBehavior(input);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
if (operation === "config.validate_schema") {
|
|
2127
|
+
return executeConfigValidateSchema(input);
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
if (operation === "validation.core_evaluate") {
|
|
2131
|
+
return envelopeOk(validateCore(input));
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
if (operation === "validation.time_entries") {
|
|
2135
|
+
return executeValidationTimeEntries(input);
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
if (operation === "op.mutate_with_validation") {
|
|
2139
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2140
|
+
const result = validateCore({
|
|
2141
|
+
fields: payload.fields,
|
|
2142
|
+
frontmatter: payload.frontmatter,
|
|
2143
|
+
rejectUnknownFields: payload.strict === true,
|
|
2144
|
+
});
|
|
2145
|
+
if (result.hasErrors) {
|
|
2146
|
+
return envelopeErr(`validation:${result.errorCodes[0] || "invalid"}`);
|
|
2147
|
+
}
|
|
2148
|
+
return envelopeOk({ value: "accepted" });
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
if (operation === "date.parse_utc") {
|
|
2152
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2153
|
+
const parsed = parseDateToUTC(String(payload.value || ""));
|
|
2154
|
+
return envelopeOk({ date: utcYmd(parsed), isoDate: parsed.toISOString().slice(0, 10) });
|
|
2155
|
+
}
|
|
2156
|
+
|
|
2157
|
+
if (operation === "date.parse_local") {
|
|
2158
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2159
|
+
const parsed = parseDateToLocal(String(payload.value || ""));
|
|
2160
|
+
return envelopeOk({ localDate: localYmd(parsed), isoDate: utcYmd(parsed) });
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
if (operation === "date.validate") {
|
|
2164
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2165
|
+
return envelopeOk({ value: validateDateString(String(payload.value || "")) });
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
if (operation === "date.get_part") {
|
|
2169
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2170
|
+
return envelopeOk({ value: getDatePart(String(payload.value || "")) });
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
if (operation === "date.has_time") {
|
|
2174
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2175
|
+
return envelopeOk({ value: hasTimeComponent(String(payload.value || "")) });
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
if (operation === "date.is_same") {
|
|
2179
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2180
|
+
return envelopeOk({ value: isSameDateSafe(String(payload.a || ""), String(payload.b || "")) });
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
if (operation === "date.is_before") {
|
|
2184
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2185
|
+
return envelopeOk({ value: isBeforeDateSafe(String(payload.a || ""), String(payload.b || "")) });
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
if (operation === "date.resolve_operation_target") {
|
|
2189
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2190
|
+
return envelopeOk({
|
|
2191
|
+
value: resolveOperationTargetDate(
|
|
2192
|
+
typeof payload.explicitDate === "string" ? payload.explicitDate : undefined,
|
|
2193
|
+
typeof payload.scheduled === "string" ? payload.scheduled : undefined,
|
|
2194
|
+
typeof payload.due === "string" ? payload.due : undefined,
|
|
2195
|
+
),
|
|
2196
|
+
});
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
if (operation === "date.day_in_timezone") {
|
|
2200
|
+
return executeDateDayInTimezone(input);
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
if (operation === "field.default_mapping") {
|
|
2204
|
+
return envelopeOk(defaultFieldMapping());
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
if (operation === "field.build_mapping") {
|
|
2208
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2209
|
+
const mapping = buildFieldMapping(
|
|
2210
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2211
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2212
|
+
);
|
|
2213
|
+
return envelopeOk(mapping);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
if (operation === "field.normalize") {
|
|
2217
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2218
|
+
const mapping = buildFieldMapping(
|
|
2219
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2220
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2221
|
+
);
|
|
2222
|
+
return envelopeOk({
|
|
2223
|
+
normalized: normalizeFrontmatter(
|
|
2224
|
+
isPlainObject(payload.frontmatter) ? payload.frontmatter : {},
|
|
2225
|
+
mapping,
|
|
2226
|
+
),
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
if (operation === "field.denormalize") {
|
|
2231
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2232
|
+
const mapping = buildFieldMapping(
|
|
2233
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2234
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2235
|
+
);
|
|
2236
|
+
return envelopeOk({
|
|
2237
|
+
denormalized: denormalizeFrontmatter(
|
|
2238
|
+
isPlainObject(payload.roleData) ? payload.roleData : {},
|
|
2239
|
+
mapping,
|
|
2240
|
+
),
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
if (operation === "field.resolve_display_title") {
|
|
2245
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2246
|
+
const mapping = buildFieldMapping(
|
|
2247
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2248
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2249
|
+
);
|
|
2250
|
+
const value = resolveDisplayTitle(
|
|
2251
|
+
isPlainObject(payload.frontmatter) ? payload.frontmatter : {},
|
|
2252
|
+
mapping,
|
|
2253
|
+
typeof payload.taskPath === "string" ? payload.taskPath : undefined,
|
|
2254
|
+
);
|
|
2255
|
+
return envelopeOk({ value: value ?? null });
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
if (operation === "field.is_completed_status") {
|
|
2259
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2260
|
+
const mapping = buildFieldMapping(
|
|
2261
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2262
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2263
|
+
);
|
|
2264
|
+
return envelopeOk({ value: isCompletedStatus(mapping, typeof payload.status === "string" ? payload.status : undefined) });
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
if (operation === "field.default_completed_status") {
|
|
2268
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2269
|
+
const mapping = buildFieldMapping(
|
|
2270
|
+
isPlainObject(payload.fields) ? payload.fields : {},
|
|
2271
|
+
typeof payload.displayNameKey === "string" ? payload.displayNameKey : undefined,
|
|
2272
|
+
);
|
|
2273
|
+
return envelopeOk({ value: getDefaultCompletedStatus(mapping) });
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
if (operation === "recurrence.complete") {
|
|
2277
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2278
|
+
return envelopeOk(completeRecurringTask(payload as never));
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
if (operation === "recurrence.recalculate") {
|
|
2282
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2283
|
+
return envelopeOk(recalculateRecurringSchedule(payload as never));
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
if (operation === "create_compat.create") {
|
|
2287
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2288
|
+
return await runCreateCompat(payload);
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
if (operation === "op.atomic_write") {
|
|
2292
|
+
return executeOpAtomicWrite(input);
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
if (operation === "op.idempotency_check") {
|
|
2296
|
+
return executeOpIdempotencyCheck();
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
if (operation === "op.update_patch") {
|
|
2300
|
+
return executeOpUpdatePatch(input);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
if (operation === "op.detect_conflict") {
|
|
2304
|
+
return executeOpDetectConflict(input);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
if (operation === "op.dry_run") {
|
|
2308
|
+
return executeOpDryRun(input);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
if (operation === "op.complete_nonrecurring") {
|
|
2312
|
+
return executeOpCompleteNonRecurring(input);
|
|
2313
|
+
}
|
|
2314
|
+
|
|
2315
|
+
if (operation === "op.uncomplete_nonrecurring") {
|
|
2316
|
+
return executeOpUncompleteNonRecurring(input);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
if (operation === "op.error_shape") {
|
|
2320
|
+
return executeOpErrorShape(input);
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
if (operation === "time.start") {
|
|
2324
|
+
return executeTimeStart(input);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if (operation === "time.stop") {
|
|
2328
|
+
return executeTimeStop(input);
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
if (operation === "time.replace_entries") {
|
|
2332
|
+
return executeTimeReplaceEntries(input);
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2335
|
+
if (operation === "time.remove_entry") {
|
|
2336
|
+
return executeTimeRemoveEntry(input);
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
if (operation === "time.auto_stop_on_complete") {
|
|
2340
|
+
return executeTimeAutoStopOnComplete(input);
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
if (operation === "time.report_totals") {
|
|
2344
|
+
return executeTimeReportTotals(input);
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
if (operation === "dependency.validate_entry") {
|
|
2348
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2349
|
+
return validateDependencyEntry(payload.entry);
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
if (operation === "dependency.validate_set") {
|
|
2353
|
+
return validateDependencySet(input);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
if (operation === "dependency.add") {
|
|
2357
|
+
return executeDependencyAdd(input);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
if (operation === "dependency.remove") {
|
|
2361
|
+
return executeDependencyRemove(input);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
if (operation === "dependency.replace") {
|
|
2365
|
+
return executeDependencyReplace(input);
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
if (operation === "dependency.missing_target_behavior") {
|
|
2369
|
+
return executeDependencyMissingTargetBehavior(input);
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
if (operation === "reminder.validate_entry") {
|
|
2373
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2374
|
+
return validateReminderEntry(payload.entry);
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
if (operation === "reminder.validate_set") {
|
|
2378
|
+
return validateReminderSet(input);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
if (operation === "reminder.add") {
|
|
2382
|
+
return executeReminderAdd(input);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (operation === "reminder.update") {
|
|
2386
|
+
return executeReminderUpdate(input);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
if (operation === "reminder.remove") {
|
|
2390
|
+
return executeReminderRemove(input);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
if (operation === "archive.apply") {
|
|
2394
|
+
return executeArchiveApply(input);
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
if (operation === "rename.apply") {
|
|
2398
|
+
return executeRenameApply(input);
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (operation === "rename.title_storage_interaction") {
|
|
2402
|
+
return executeRenameTitleStorageInteraction(input);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
if (operation === "batch.apply") {
|
|
2406
|
+
return executeBatchApply(input);
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (operation === "delete.remove") {
|
|
2410
|
+
return executeDeleteRemove(input);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
if (operation === "recurrence.uncomplete_instance") {
|
|
2414
|
+
return executeRecurrenceUncompleteInstance(input);
|
|
2415
|
+
}
|
|
2416
|
+
|
|
2417
|
+
if (operation === "recurrence.skip_instance") {
|
|
2418
|
+
return executeRecurrenceSkipInstance(input);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
if (operation === "recurrence.unskip_instance") {
|
|
2422
|
+
return executeRecurrenceUnskipInstance(input);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
if (operation === "recurrence.effective_state") {
|
|
2426
|
+
return executeRecurrenceEffectiveState(input);
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2429
|
+
if (operation === "link.parse") {
|
|
2430
|
+
const payload = isPlainObject(input) ? input : {};
|
|
2431
|
+
return envelopeOk(parseLinkRaw(payload.raw));
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
if (operation === "link.resolve") {
|
|
2435
|
+
return resolveLink(input);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
if (operation === "link.update_references_on_rename") {
|
|
2439
|
+
return executeLinkUpdateReferencesOnRename(input);
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
if (operation === "migration.compat_mode") {
|
|
2443
|
+
return executeMigrationCompatMode(input);
|
|
2444
|
+
}
|
|
2445
|
+
|
|
2446
|
+
if (operation === "migration.plan") {
|
|
2447
|
+
return executeMigrationPlan(input);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
if (operation === "migration.normalize_aliases") {
|
|
2451
|
+
return executeMigrationNormalizeAliases(input);
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
if (operation === "migration.normalize_temporal") {
|
|
2455
|
+
return executeMigrationNormalizeTemporal(input);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
if (operation === "migration.resolve_instance_overlap") {
|
|
2459
|
+
return executeMigrationResolveInstanceOverlap(input);
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
if (operation === "migration.normalize_dependencies") {
|
|
2463
|
+
return executeMigrationNormalizeDependencies(input);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
if (operation === "migration.normalize_reminders") {
|
|
2467
|
+
return executeMigrationNormalizeReminders(input);
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
if (operation === "migration.normalize_links") {
|
|
2471
|
+
return executeMigrationNormalizeLinks(input);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
if (operation === "migration.report_summary") {
|
|
2475
|
+
return executeMigrationReportSummary(input);
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
if (operation === "migration.divergence_register") {
|
|
2479
|
+
return executeMigrationDivergenceRegister();
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
if (operation === "migration.deprecation_policy") {
|
|
2483
|
+
return executeMigrationDeprecationPolicy();
|
|
2484
|
+
}
|
|
2485
|
+
|
|
2486
|
+
if (operation === "migration.safety_guards") {
|
|
2487
|
+
return executeMigrationSafetyGuards();
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
if (operation === "migration.compat_statement") {
|
|
2491
|
+
return executeMigrationCompatStatement();
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
return envelopeErr(`unsupported_operation:${operation}`);
|
|
2495
|
+
} catch (error) {
|
|
2496
|
+
return envelopeErr(error);
|
|
2497
|
+
}
|
|
2498
|
+
}
|