@tashks/core 0.1.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/LICENSE +21 -0
- package/dist/src/query.d.ts +294 -0
- package/dist/src/query.d.ts.map +1 -0
- package/dist/src/query.js +303 -0
- package/dist/src/query.test.d.ts +2 -0
- package/dist/src/query.test.d.ts.map +1 -0
- package/dist/src/query.test.js +673 -0
- package/dist/src/repository.d.ts +100 -0
- package/dist/src/repository.d.ts.map +1 -0
- package/dist/src/repository.js +742 -0
- package/dist/src/repository.test.d.ts +2 -0
- package/dist/src/repository.test.d.ts.map +1 -0
- package/dist/src/repository.test.js +1439 -0
- package/dist/src/schema.d.ts +460 -0
- package/dist/src/schema.d.ts.map +1 -0
- package/dist/src/schema.js +166 -0
- package/dist/src/schema.test.d.ts +2 -0
- package/dist/src/schema.test.d.ts.map +1 -0
- package/dist/src/schema.test.js +123 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Simon W. Jackson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import { type Task, type TaskEnergy } from "./schema.js";
|
|
4
|
+
export declare const isBlocked: (task: Task, allTasks: Task[]) => boolean;
|
|
5
|
+
export declare const isUnblocked: (task: Task, allTasks: Task[]) => boolean;
|
|
6
|
+
export declare const isDueBefore: (date: string) => (task: Task) => boolean;
|
|
7
|
+
export declare const isDueThisWeek: (today: string) => (task: Task) => boolean;
|
|
8
|
+
export declare const isDeferred: (today: string) => (task: Task) => boolean;
|
|
9
|
+
export declare const hasEnergy: (level: TaskEnergy) => (task: Task) => boolean;
|
|
10
|
+
export declare const hasTag: (tag: string) => (task: Task) => boolean;
|
|
11
|
+
export declare const hasProject: (project: string) => (task: Task) => boolean;
|
|
12
|
+
export declare const isStalerThan: (
|
|
13
|
+
days: number,
|
|
14
|
+
today: string,
|
|
15
|
+
) => (task: Task) => boolean;
|
|
16
|
+
export declare const wasCompletedOn: (date: string) => (task: Task) => boolean;
|
|
17
|
+
export declare const wasCompletedBetween: (
|
|
18
|
+
start: string,
|
|
19
|
+
end: string,
|
|
20
|
+
) => (task: Task) => boolean;
|
|
21
|
+
export declare const byDueAsc: (a: Task, b: Task) => number;
|
|
22
|
+
export declare const byEnergyAsc: (a: Task, b: Task) => number;
|
|
23
|
+
export declare const byCreatedAsc: (a: Task, b: Task) => number;
|
|
24
|
+
export declare const byUpdatedDescThenTitle: (a: Task, b: Task) => number;
|
|
25
|
+
export declare const resolveRelativeDate: (
|
|
26
|
+
value: string,
|
|
27
|
+
today: string,
|
|
28
|
+
) => string | null;
|
|
29
|
+
export declare const PerspectiveFilters: Schema.Struct<{
|
|
30
|
+
status: Schema.optionalWith<
|
|
31
|
+
Schema.Literal<
|
|
32
|
+
["active", "backlog", "blocked", "done", "dropped", "on-hold"]
|
|
33
|
+
>,
|
|
34
|
+
{
|
|
35
|
+
exact: true;
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
area: Schema.optionalWith<
|
|
39
|
+
Schema.Literal<
|
|
40
|
+
[
|
|
41
|
+
"health",
|
|
42
|
+
"infrastructure",
|
|
43
|
+
"work",
|
|
44
|
+
"personal",
|
|
45
|
+
"blog",
|
|
46
|
+
"code",
|
|
47
|
+
"home",
|
|
48
|
+
"side-projects",
|
|
49
|
+
]
|
|
50
|
+
>,
|
|
51
|
+
{
|
|
52
|
+
exact: true;
|
|
53
|
+
}
|
|
54
|
+
>;
|
|
55
|
+
project: Schema.optionalWith<
|
|
56
|
+
typeof Schema.String,
|
|
57
|
+
{
|
|
58
|
+
exact: true;
|
|
59
|
+
}
|
|
60
|
+
>;
|
|
61
|
+
tags: Schema.optionalWith<
|
|
62
|
+
Schema.Array$<typeof Schema.String>,
|
|
63
|
+
{
|
|
64
|
+
exact: true;
|
|
65
|
+
}
|
|
66
|
+
>;
|
|
67
|
+
due_before: Schema.optionalWith<
|
|
68
|
+
typeof Schema.String,
|
|
69
|
+
{
|
|
70
|
+
exact: true;
|
|
71
|
+
}
|
|
72
|
+
>;
|
|
73
|
+
due_after: Schema.optionalWith<
|
|
74
|
+
typeof Schema.String,
|
|
75
|
+
{
|
|
76
|
+
exact: true;
|
|
77
|
+
}
|
|
78
|
+
>;
|
|
79
|
+
unblocked_only: Schema.optionalWith<
|
|
80
|
+
typeof Schema.Boolean,
|
|
81
|
+
{
|
|
82
|
+
exact: true;
|
|
83
|
+
}
|
|
84
|
+
>;
|
|
85
|
+
energy: Schema.optionalWith<
|
|
86
|
+
Schema.Literal<["low", "medium", "high"]>,
|
|
87
|
+
{
|
|
88
|
+
exact: true;
|
|
89
|
+
}
|
|
90
|
+
>;
|
|
91
|
+
stale_days: Schema.optionalWith<
|
|
92
|
+
typeof Schema.Number,
|
|
93
|
+
{
|
|
94
|
+
exact: true;
|
|
95
|
+
}
|
|
96
|
+
>;
|
|
97
|
+
completed_on: Schema.optionalWith<
|
|
98
|
+
typeof Schema.String,
|
|
99
|
+
{
|
|
100
|
+
exact: true;
|
|
101
|
+
}
|
|
102
|
+
>;
|
|
103
|
+
}>;
|
|
104
|
+
export type PerspectiveFilters = Schema.Schema.Type<typeof PerspectiveFilters>;
|
|
105
|
+
export declare const PerspectiveSort: typeof Schema.String;
|
|
106
|
+
export type PerspectiveSort = Schema.Schema.Type<typeof PerspectiveSort>;
|
|
107
|
+
export declare const Perspective: Schema.Struct<{
|
|
108
|
+
filters: Schema.Struct<{
|
|
109
|
+
status: Schema.optionalWith<
|
|
110
|
+
Schema.Literal<
|
|
111
|
+
["active", "backlog", "blocked", "done", "dropped", "on-hold"]
|
|
112
|
+
>,
|
|
113
|
+
{
|
|
114
|
+
exact: true;
|
|
115
|
+
}
|
|
116
|
+
>;
|
|
117
|
+
area: Schema.optionalWith<
|
|
118
|
+
Schema.Literal<
|
|
119
|
+
[
|
|
120
|
+
"health",
|
|
121
|
+
"infrastructure",
|
|
122
|
+
"work",
|
|
123
|
+
"personal",
|
|
124
|
+
"blog",
|
|
125
|
+
"code",
|
|
126
|
+
"home",
|
|
127
|
+
"side-projects",
|
|
128
|
+
]
|
|
129
|
+
>,
|
|
130
|
+
{
|
|
131
|
+
exact: true;
|
|
132
|
+
}
|
|
133
|
+
>;
|
|
134
|
+
project: Schema.optionalWith<
|
|
135
|
+
typeof Schema.String,
|
|
136
|
+
{
|
|
137
|
+
exact: true;
|
|
138
|
+
}
|
|
139
|
+
>;
|
|
140
|
+
tags: Schema.optionalWith<
|
|
141
|
+
Schema.Array$<typeof Schema.String>,
|
|
142
|
+
{
|
|
143
|
+
exact: true;
|
|
144
|
+
}
|
|
145
|
+
>;
|
|
146
|
+
due_before: Schema.optionalWith<
|
|
147
|
+
typeof Schema.String,
|
|
148
|
+
{
|
|
149
|
+
exact: true;
|
|
150
|
+
}
|
|
151
|
+
>;
|
|
152
|
+
due_after: Schema.optionalWith<
|
|
153
|
+
typeof Schema.String,
|
|
154
|
+
{
|
|
155
|
+
exact: true;
|
|
156
|
+
}
|
|
157
|
+
>;
|
|
158
|
+
unblocked_only: Schema.optionalWith<
|
|
159
|
+
typeof Schema.Boolean,
|
|
160
|
+
{
|
|
161
|
+
exact: true;
|
|
162
|
+
}
|
|
163
|
+
>;
|
|
164
|
+
energy: Schema.optionalWith<
|
|
165
|
+
Schema.Literal<["low", "medium", "high"]>,
|
|
166
|
+
{
|
|
167
|
+
exact: true;
|
|
168
|
+
}
|
|
169
|
+
>;
|
|
170
|
+
stale_days: Schema.optionalWith<
|
|
171
|
+
typeof Schema.Number,
|
|
172
|
+
{
|
|
173
|
+
exact: true;
|
|
174
|
+
}
|
|
175
|
+
>;
|
|
176
|
+
completed_on: Schema.optionalWith<
|
|
177
|
+
typeof Schema.String,
|
|
178
|
+
{
|
|
179
|
+
exact: true;
|
|
180
|
+
}
|
|
181
|
+
>;
|
|
182
|
+
}>;
|
|
183
|
+
sort: Schema.optionalWith<
|
|
184
|
+
typeof Schema.String,
|
|
185
|
+
{
|
|
186
|
+
exact: true;
|
|
187
|
+
}
|
|
188
|
+
>;
|
|
189
|
+
}>;
|
|
190
|
+
export type Perspective = Schema.Schema.Type<typeof Perspective>;
|
|
191
|
+
export declare const PerspectiveConfig: Schema.Record$<
|
|
192
|
+
typeof Schema.String,
|
|
193
|
+
Schema.Struct<{
|
|
194
|
+
filters: Schema.Struct<{
|
|
195
|
+
status: Schema.optionalWith<
|
|
196
|
+
Schema.Literal<
|
|
197
|
+
["active", "backlog", "blocked", "done", "dropped", "on-hold"]
|
|
198
|
+
>,
|
|
199
|
+
{
|
|
200
|
+
exact: true;
|
|
201
|
+
}
|
|
202
|
+
>;
|
|
203
|
+
area: Schema.optionalWith<
|
|
204
|
+
Schema.Literal<
|
|
205
|
+
[
|
|
206
|
+
"health",
|
|
207
|
+
"infrastructure",
|
|
208
|
+
"work",
|
|
209
|
+
"personal",
|
|
210
|
+
"blog",
|
|
211
|
+
"code",
|
|
212
|
+
"home",
|
|
213
|
+
"side-projects",
|
|
214
|
+
]
|
|
215
|
+
>,
|
|
216
|
+
{
|
|
217
|
+
exact: true;
|
|
218
|
+
}
|
|
219
|
+
>;
|
|
220
|
+
project: Schema.optionalWith<
|
|
221
|
+
typeof Schema.String,
|
|
222
|
+
{
|
|
223
|
+
exact: true;
|
|
224
|
+
}
|
|
225
|
+
>;
|
|
226
|
+
tags: Schema.optionalWith<
|
|
227
|
+
Schema.Array$<typeof Schema.String>,
|
|
228
|
+
{
|
|
229
|
+
exact: true;
|
|
230
|
+
}
|
|
231
|
+
>;
|
|
232
|
+
due_before: Schema.optionalWith<
|
|
233
|
+
typeof Schema.String,
|
|
234
|
+
{
|
|
235
|
+
exact: true;
|
|
236
|
+
}
|
|
237
|
+
>;
|
|
238
|
+
due_after: Schema.optionalWith<
|
|
239
|
+
typeof Schema.String,
|
|
240
|
+
{
|
|
241
|
+
exact: true;
|
|
242
|
+
}
|
|
243
|
+
>;
|
|
244
|
+
unblocked_only: Schema.optionalWith<
|
|
245
|
+
typeof Schema.Boolean,
|
|
246
|
+
{
|
|
247
|
+
exact: true;
|
|
248
|
+
}
|
|
249
|
+
>;
|
|
250
|
+
energy: Schema.optionalWith<
|
|
251
|
+
Schema.Literal<["low", "medium", "high"]>,
|
|
252
|
+
{
|
|
253
|
+
exact: true;
|
|
254
|
+
}
|
|
255
|
+
>;
|
|
256
|
+
stale_days: Schema.optionalWith<
|
|
257
|
+
typeof Schema.Number,
|
|
258
|
+
{
|
|
259
|
+
exact: true;
|
|
260
|
+
}
|
|
261
|
+
>;
|
|
262
|
+
completed_on: Schema.optionalWith<
|
|
263
|
+
typeof Schema.String,
|
|
264
|
+
{
|
|
265
|
+
exact: true;
|
|
266
|
+
}
|
|
267
|
+
>;
|
|
268
|
+
}>;
|
|
269
|
+
sort: Schema.optionalWith<
|
|
270
|
+
typeof Schema.String,
|
|
271
|
+
{
|
|
272
|
+
exact: true;
|
|
273
|
+
}
|
|
274
|
+
>;
|
|
275
|
+
}>
|
|
276
|
+
>;
|
|
277
|
+
export type PerspectiveConfig = Schema.Schema.Type<typeof PerspectiveConfig>;
|
|
278
|
+
export declare const parsePerspectiveConfig: (
|
|
279
|
+
record: unknown,
|
|
280
|
+
) => PerspectiveConfig | null;
|
|
281
|
+
export declare const resolvePerspectiveConfigRelativeDates: (
|
|
282
|
+
config: PerspectiveConfig,
|
|
283
|
+
today: string,
|
|
284
|
+
) => PerspectiveConfig | null;
|
|
285
|
+
export declare const applyPerspectiveToTasks: (
|
|
286
|
+
tasks: ReadonlyArray<Task>,
|
|
287
|
+
perspective: Perspective,
|
|
288
|
+
today?: string,
|
|
289
|
+
) => Array<Task>;
|
|
290
|
+
export declare const loadPerspectiveConfig: (
|
|
291
|
+
dataDir: string,
|
|
292
|
+
today?: string,
|
|
293
|
+
) => Effect.Effect<PerspectiveConfig, string>;
|
|
294
|
+
//# sourceMappingURL=query.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../../src/query.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Either from "effect/Either";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import YAML from "yaml";
|
|
7
|
+
import { TaskArea as TaskAreaSchema, TaskEnergy as TaskEnergySchema, TaskStatus as TaskStatusSchema, } from "./schema.js";
|
|
8
|
+
const addDays = (date, days) => {
|
|
9
|
+
const next = new Date(`${date}T00:00:00.000Z`);
|
|
10
|
+
next.setUTCDate(next.getUTCDate() + days);
|
|
11
|
+
return next.toISOString().slice(0, 10);
|
|
12
|
+
};
|
|
13
|
+
const dateToUtcMidnight = (date) => {
|
|
14
|
+
const parsed = new Date(`${date}T00:00:00.000Z`).getTime();
|
|
15
|
+
return Number.isNaN(parsed) ? Number.NaN : parsed;
|
|
16
|
+
};
|
|
17
|
+
const completionDate = (task) => {
|
|
18
|
+
if (task.completed_at === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const parsed = new Date(task.completed_at);
|
|
22
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return parsed.toISOString().slice(0, 10);
|
|
26
|
+
};
|
|
27
|
+
const perspectiveConfigFilePath = (dataDir) => join(dataDir, "perspectives.yaml");
|
|
28
|
+
const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
29
|
+
const isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
30
|
+
const relativeDaysPattern = /^\+(\d+)d$/;
|
|
31
|
+
const currentIsoDate = () => new Date().toISOString().slice(0, 10);
|
|
32
|
+
const isIsoDate = (value) => {
|
|
33
|
+
if (!isoDatePattern.test(value)) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
37
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
return parsed.toISOString().slice(0, 10) === value;
|
|
41
|
+
};
|
|
42
|
+
export const isBlocked = (task, allTasks) => {
|
|
43
|
+
if (task.blocked_by.length === 0) {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
const taskById = new Map(allTasks.map((candidate) => [candidate.id, candidate]));
|
|
47
|
+
return task.blocked_by.some((blockerId) => {
|
|
48
|
+
const blocker = taskById.get(blockerId);
|
|
49
|
+
return blocker !== undefined && blocker.status !== "done";
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
export const isUnblocked = (task, allTasks) => !isBlocked(task, allTasks);
|
|
53
|
+
export const isDueBefore = (date) => (task) => task.due !== null && task.due <= date;
|
|
54
|
+
export const isDueThisWeek = (today) => {
|
|
55
|
+
const weekEnd = addDays(today, 7);
|
|
56
|
+
return (task) => task.due !== null && task.due >= today && task.due < weekEnd;
|
|
57
|
+
};
|
|
58
|
+
export const isDeferred = (today) => (task) => task.defer_until !== null && task.defer_until > today;
|
|
59
|
+
export const hasEnergy = (level) => (task) => task.energy === level;
|
|
60
|
+
export const hasTag = (tag) => (task) => task.tags.includes(tag);
|
|
61
|
+
export const hasProject = (project) => (task) => task.project === project;
|
|
62
|
+
export const isStalerThan = (days, today) => (task) => {
|
|
63
|
+
const updated = dateToUtcMidnight(task.updated);
|
|
64
|
+
const now = dateToUtcMidnight(today);
|
|
65
|
+
if (Number.isNaN(updated) || Number.isNaN(now)) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const elapsedDays = (now - updated) / 86_400_000;
|
|
69
|
+
return elapsedDays > days;
|
|
70
|
+
};
|
|
71
|
+
export const wasCompletedOn = (date) => (task) => completionDate(task) === date;
|
|
72
|
+
export const wasCompletedBetween = (start, end) => (task) => {
|
|
73
|
+
const completed = completionDate(task);
|
|
74
|
+
if (completed === null) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return completed >= start && completed <= end;
|
|
78
|
+
};
|
|
79
|
+
const energyRank = {
|
|
80
|
+
low: 0,
|
|
81
|
+
medium: 1,
|
|
82
|
+
high: 2,
|
|
83
|
+
};
|
|
84
|
+
export const byDueAsc = (a, b) => {
|
|
85
|
+
if (a.due === null && b.due === null) {
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (a.due === null) {
|
|
89
|
+
return 1;
|
|
90
|
+
}
|
|
91
|
+
if (b.due === null) {
|
|
92
|
+
return -1;
|
|
93
|
+
}
|
|
94
|
+
return a.due.localeCompare(b.due);
|
|
95
|
+
};
|
|
96
|
+
export const byEnergyAsc = (a, b) => energyRank[a.energy] - energyRank[b.energy];
|
|
97
|
+
export const byCreatedAsc = (a, b) => a.created.localeCompare(b.created);
|
|
98
|
+
export const byUpdatedDescThenTitle = (a, b) => {
|
|
99
|
+
const byUpdatedDesc = b.updated.localeCompare(a.updated);
|
|
100
|
+
if (byUpdatedDesc !== 0) {
|
|
101
|
+
return byUpdatedDesc;
|
|
102
|
+
}
|
|
103
|
+
return a.title.localeCompare(b.title);
|
|
104
|
+
};
|
|
105
|
+
const byUpdatedAscThenTitle = (a, b) => {
|
|
106
|
+
const byUpdatedAsc = a.updated.localeCompare(b.updated);
|
|
107
|
+
if (byUpdatedAsc !== 0) {
|
|
108
|
+
return byUpdatedAsc;
|
|
109
|
+
}
|
|
110
|
+
return a.title.localeCompare(b.title);
|
|
111
|
+
};
|
|
112
|
+
const byCompletedAtDescThenTitle = (a, b) => {
|
|
113
|
+
if (a.completed_at === null && b.completed_at === null) {
|
|
114
|
+
return a.title.localeCompare(b.title);
|
|
115
|
+
}
|
|
116
|
+
if (a.completed_at === null) {
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
if (b.completed_at === null) {
|
|
120
|
+
return -1;
|
|
121
|
+
}
|
|
122
|
+
const byCompletedAtDesc = b.completed_at.localeCompare(a.completed_at);
|
|
123
|
+
if (byCompletedAtDesc !== 0) {
|
|
124
|
+
return byCompletedAtDesc;
|
|
125
|
+
}
|
|
126
|
+
return a.title.localeCompare(b.title);
|
|
127
|
+
};
|
|
128
|
+
export const resolveRelativeDate = (value, today) => {
|
|
129
|
+
const normalized = value.trim();
|
|
130
|
+
if (isIsoDate(normalized)) {
|
|
131
|
+
return normalized;
|
|
132
|
+
}
|
|
133
|
+
if (!isIsoDate(today)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (normalized === "today") {
|
|
137
|
+
return today;
|
|
138
|
+
}
|
|
139
|
+
const relativeMatch = relativeDaysPattern.exec(normalized);
|
|
140
|
+
if (relativeMatch === null) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
const days = Number.parseInt(relativeMatch[1], 10);
|
|
144
|
+
return addDays(today, days);
|
|
145
|
+
};
|
|
146
|
+
export const PerspectiveFilters = Schema.Struct({
|
|
147
|
+
status: Schema.optionalWith(TaskStatusSchema, { exact: true }),
|
|
148
|
+
area: Schema.optionalWith(TaskAreaSchema, { exact: true }),
|
|
149
|
+
project: Schema.optionalWith(Schema.String, { exact: true }),
|
|
150
|
+
tags: Schema.optionalWith(Schema.Array(Schema.String), { exact: true }),
|
|
151
|
+
due_before: Schema.optionalWith(Schema.String, { exact: true }),
|
|
152
|
+
due_after: Schema.optionalWith(Schema.String, { exact: true }),
|
|
153
|
+
unblocked_only: Schema.optionalWith(Schema.Boolean, { exact: true }),
|
|
154
|
+
energy: Schema.optionalWith(TaskEnergySchema, { exact: true }),
|
|
155
|
+
stale_days: Schema.optionalWith(Schema.Number, { exact: true }),
|
|
156
|
+
completed_on: Schema.optionalWith(Schema.String, { exact: true }),
|
|
157
|
+
});
|
|
158
|
+
export const PerspectiveSort = Schema.String;
|
|
159
|
+
export const Perspective = Schema.Struct({
|
|
160
|
+
filters: PerspectiveFilters,
|
|
161
|
+
sort: Schema.optionalWith(PerspectiveSort, { exact: true }),
|
|
162
|
+
});
|
|
163
|
+
export const PerspectiveConfig = Schema.Record({
|
|
164
|
+
key: Schema.String,
|
|
165
|
+
value: Perspective,
|
|
166
|
+
});
|
|
167
|
+
const decodePerspectiveConfigEither = Schema.decodeUnknownEither(PerspectiveConfig);
|
|
168
|
+
export const parsePerspectiveConfig = (record) => {
|
|
169
|
+
const result = decodePerspectiveConfigEither(record);
|
|
170
|
+
return Either.isRight(result) ? result.right : null;
|
|
171
|
+
};
|
|
172
|
+
export const resolvePerspectiveConfigRelativeDates = (config, today) => {
|
|
173
|
+
const resolved = {};
|
|
174
|
+
for (const [name, perspective] of Object.entries(config)) {
|
|
175
|
+
const dueBefore = perspective.filters.due_before === undefined
|
|
176
|
+
? undefined
|
|
177
|
+
: resolveRelativeDate(perspective.filters.due_before, today);
|
|
178
|
+
const dueAfter = perspective.filters.due_after === undefined
|
|
179
|
+
? undefined
|
|
180
|
+
: resolveRelativeDate(perspective.filters.due_after, today);
|
|
181
|
+
const completedOn = perspective.filters.completed_on === undefined
|
|
182
|
+
? undefined
|
|
183
|
+
: resolveRelativeDate(perspective.filters.completed_on, today);
|
|
184
|
+
if (dueBefore === null || dueAfter === null || completedOn === null) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
resolved[name] = {
|
|
188
|
+
...perspective,
|
|
189
|
+
filters: {
|
|
190
|
+
...perspective.filters,
|
|
191
|
+
...(dueBefore !== undefined ? { due_before: dueBefore } : {}),
|
|
192
|
+
...(dueAfter !== undefined ? { due_after: dueAfter } : {}),
|
|
193
|
+
...(completedOn !== undefined ? { completed_on: completedOn } : {}),
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
return resolved;
|
|
198
|
+
};
|
|
199
|
+
export const applyPerspectiveToTasks = (tasks, perspective, today = currentIsoDate()) => {
|
|
200
|
+
const taskList = Array.from(tasks);
|
|
201
|
+
const dueBeforePredicate = perspective.filters.due_before !== undefined
|
|
202
|
+
? isDueBefore(perspective.filters.due_before)
|
|
203
|
+
: null;
|
|
204
|
+
const stalePredicate = perspective.filters.stale_days !== undefined
|
|
205
|
+
? isStalerThan(perspective.filters.stale_days, today)
|
|
206
|
+
: null;
|
|
207
|
+
const completedOnPredicate = perspective.filters.completed_on !== undefined
|
|
208
|
+
? wasCompletedOn(perspective.filters.completed_on)
|
|
209
|
+
: null;
|
|
210
|
+
const filtered = taskList.filter((task) => {
|
|
211
|
+
if (perspective.filters.status !== undefined &&
|
|
212
|
+
task.status !== perspective.filters.status) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
if (perspective.filters.area !== undefined &&
|
|
216
|
+
task.area !== perspective.filters.area) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (perspective.filters.project !== undefined &&
|
|
220
|
+
task.project !== perspective.filters.project) {
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
if (perspective.filters.tags !== undefined &&
|
|
224
|
+
perspective.filters.tags.length > 0 &&
|
|
225
|
+
!perspective.filters.tags.some((tag) => task.tags.includes(tag))) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
if (perspective.filters.energy !== undefined &&
|
|
229
|
+
task.energy !== perspective.filters.energy) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
if (dueBeforePredicate !== null && !dueBeforePredicate(task)) {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
if (perspective.filters.due_after !== undefined &&
|
|
236
|
+
(task.due === null || task.due < perspective.filters.due_after)) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
if (perspective.filters.unblocked_only === true &&
|
|
240
|
+
!isUnblocked(task, taskList)) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
if (stalePredicate !== null && !stalePredicate(task)) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
if (completedOnPredicate !== null && !completedOnPredicate(task)) {
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
});
|
|
251
|
+
switch (perspective.sort) {
|
|
252
|
+
case "due_asc":
|
|
253
|
+
return filtered.sort((a, b) => {
|
|
254
|
+
const byDue = byDueAsc(a, b);
|
|
255
|
+
return byDue !== 0 ? byDue : byUpdatedDescThenTitle(a, b);
|
|
256
|
+
});
|
|
257
|
+
case "energy_asc":
|
|
258
|
+
return filtered.sort((a, b) => {
|
|
259
|
+
const byEnergy = byEnergyAsc(a, b);
|
|
260
|
+
return byEnergy !== 0 ? byEnergy : byUpdatedDescThenTitle(a, b);
|
|
261
|
+
});
|
|
262
|
+
case "created_asc":
|
|
263
|
+
return filtered.sort((a, b) => {
|
|
264
|
+
const byCreated = byCreatedAsc(a, b);
|
|
265
|
+
return byCreated !== 0 ? byCreated : a.title.localeCompare(b.title);
|
|
266
|
+
});
|
|
267
|
+
case "updated_asc":
|
|
268
|
+
return filtered.sort(byUpdatedAscThenTitle);
|
|
269
|
+
case "completed_at_desc":
|
|
270
|
+
return filtered.sort(byCompletedAtDescThenTitle);
|
|
271
|
+
case "updated_desc":
|
|
272
|
+
default:
|
|
273
|
+
return filtered.sort(byUpdatedDescThenTitle);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
export const loadPerspectiveConfig = (dataDir, today = currentIsoDate()) => Effect.tryPromise({
|
|
277
|
+
try: async () => {
|
|
278
|
+
const path = perspectiveConfigFilePath(dataDir);
|
|
279
|
+
const source = await readFile(path, "utf8").catch((error) => {
|
|
280
|
+
if (error !== null &&
|
|
281
|
+
typeof error === "object" &&
|
|
282
|
+
"code" in error &&
|
|
283
|
+
error.code === "ENOENT") {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
throw error;
|
|
287
|
+
});
|
|
288
|
+
if (source === null || source.trim().length === 0) {
|
|
289
|
+
return {};
|
|
290
|
+
}
|
|
291
|
+
const parsed = YAML.parse(source);
|
|
292
|
+
const config = parsePerspectiveConfig(parsed);
|
|
293
|
+
if (config === null) {
|
|
294
|
+
throw new Error(`Invalid perspective config in ${path}`);
|
|
295
|
+
}
|
|
296
|
+
const resolved = resolvePerspectiveConfigRelativeDates(config, today);
|
|
297
|
+
if (resolved === null) {
|
|
298
|
+
throw new Error(`Invalid perspective config in ${path}`);
|
|
299
|
+
}
|
|
300
|
+
return resolved;
|
|
301
|
+
},
|
|
302
|
+
catch: (error) => `Perspective config loader failed: ${toErrorMessage(error)}`,
|
|
303
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"query.test.d.ts","sourceRoot":"","sources":["../../src/query.test.ts"],"names":[],"mappings":""}
|