@tashks/core 0.1.0 → 0.1.2

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.
@@ -9,286 +9,127 @@ export declare const isDeferred: (today: string) => (task: Task) => boolean;
9
9
  export declare const hasEnergy: (level: TaskEnergy) => (task: Task) => boolean;
10
10
  export declare const hasTag: (tag: string) => (task: Task) => boolean;
11
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;
12
+ export declare const isStalerThan: (days: number, today: string) => (task: Task) => boolean;
16
13
  export declare const wasCompletedOn: (date: string) => (task: Task) => boolean;
17
- export declare const wasCompletedBetween: (
18
- start: string,
19
- end: string,
20
- ) => (task: Task) => boolean;
14
+ export declare const wasCompletedBetween: (start: string, end: string) => (task: Task) => boolean;
21
15
  export declare const byDueAsc: (a: Task, b: Task) => number;
22
16
  export declare const byEnergyAsc: (a: Task, b: Task) => number;
23
17
  export declare const byCreatedAsc: (a: Task, b: Task) => number;
24
18
  export declare const byUpdatedDescThenTitle: (a: Task, b: Task) => number;
25
- export declare const resolveRelativeDate: (
26
- value: string,
27
- today: string,
28
- ) => string | null;
19
+ export declare const resolveRelativeDate: (value: string, today: string) => string | null;
29
20
  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
- >;
21
+ status: Schema.optionalWith<Schema.Literal<["active", "backlog", "blocked", "done", "dropped", "on-hold"]>, {
22
+ exact: true;
23
+ }>;
24
+ area: Schema.optionalWith<Schema.Literal<["health", "infrastructure", "work", "personal", "blog", "code", "home", "side-projects"]>, {
25
+ exact: true;
26
+ }>;
27
+ project: Schema.optionalWith<typeof Schema.String, {
28
+ exact: true;
29
+ }>;
30
+ tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
31
+ exact: true;
32
+ }>;
33
+ due_before: Schema.optionalWith<typeof Schema.String, {
34
+ exact: true;
35
+ }>;
36
+ due_after: Schema.optionalWith<typeof Schema.String, {
37
+ exact: true;
38
+ }>;
39
+ unblocked_only: Schema.optionalWith<typeof Schema.Boolean, {
40
+ exact: true;
41
+ }>;
42
+ energy: Schema.optionalWith<Schema.Literal<["low", "medium", "high"]>, {
43
+ exact: true;
44
+ }>;
45
+ stale_days: Schema.optionalWith<typeof Schema.Number, {
46
+ exact: true;
47
+ }>;
48
+ completed_on: Schema.optionalWith<typeof Schema.String, {
49
+ exact: true;
50
+ }>;
103
51
  }>;
104
52
  export type PerspectiveFilters = Schema.Schema.Type<typeof PerspectiveFilters>;
105
53
  export declare const PerspectiveSort: typeof Schema.String;
106
54
  export type PerspectiveSort = Schema.Schema.Type<typeof PerspectiveSort>;
107
55
  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
- >;
56
+ filters: Schema.Struct<{
57
+ status: Schema.optionalWith<Schema.Literal<["active", "backlog", "blocked", "done", "dropped", "on-hold"]>, {
58
+ exact: true;
59
+ }>;
60
+ area: Schema.optionalWith<Schema.Literal<["health", "infrastructure", "work", "personal", "blog", "code", "home", "side-projects"]>, {
61
+ exact: true;
62
+ }>;
63
+ project: Schema.optionalWith<typeof Schema.String, {
64
+ exact: true;
65
+ }>;
66
+ tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
67
+ exact: true;
68
+ }>;
69
+ due_before: Schema.optionalWith<typeof Schema.String, {
70
+ exact: true;
71
+ }>;
72
+ due_after: Schema.optionalWith<typeof Schema.String, {
73
+ exact: true;
74
+ }>;
75
+ unblocked_only: Schema.optionalWith<typeof Schema.Boolean, {
76
+ exact: true;
77
+ }>;
78
+ energy: Schema.optionalWith<Schema.Literal<["low", "medium", "high"]>, {
79
+ exact: true;
80
+ }>;
81
+ stale_days: Schema.optionalWith<typeof Schema.Number, {
82
+ exact: true;
83
+ }>;
84
+ completed_on: Schema.optionalWith<typeof Schema.String, {
85
+ exact: true;
86
+ }>;
87
+ }>;
88
+ sort: Schema.optionalWith<typeof Schema.String, {
89
+ exact: true;
90
+ }>;
189
91
  }>;
190
92
  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
- >;
93
+ export declare const PerspectiveConfig: Schema.Record$<typeof Schema.String, Schema.Struct<{
94
+ filters: Schema.Struct<{
95
+ status: Schema.optionalWith<Schema.Literal<["active", "backlog", "blocked", "done", "dropped", "on-hold"]>, {
96
+ exact: true;
97
+ }>;
98
+ area: Schema.optionalWith<Schema.Literal<["health", "infrastructure", "work", "personal", "blog", "code", "home", "side-projects"]>, {
99
+ exact: true;
100
+ }>;
101
+ project: Schema.optionalWith<typeof Schema.String, {
102
+ exact: true;
103
+ }>;
104
+ tags: Schema.optionalWith<Schema.Array$<typeof Schema.String>, {
105
+ exact: true;
106
+ }>;
107
+ due_before: Schema.optionalWith<typeof Schema.String, {
108
+ exact: true;
109
+ }>;
110
+ due_after: Schema.optionalWith<typeof Schema.String, {
111
+ exact: true;
112
+ }>;
113
+ unblocked_only: Schema.optionalWith<typeof Schema.Boolean, {
114
+ exact: true;
115
+ }>;
116
+ energy: Schema.optionalWith<Schema.Literal<["low", "medium", "high"]>, {
117
+ exact: true;
118
+ }>;
119
+ stale_days: Schema.optionalWith<typeof Schema.Number, {
120
+ exact: true;
121
+ }>;
122
+ completed_on: Schema.optionalWith<typeof Schema.String, {
123
+ exact: true;
124
+ }>;
125
+ }>;
126
+ sort: Schema.optionalWith<typeof Schema.String, {
127
+ exact: true;
128
+ }>;
129
+ }>>;
277
130
  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
131
+ export declare const parsePerspectiveConfig: (record: unknown) => PerspectiveConfig | null;
132
+ export declare const resolvePerspectiveConfigRelativeDates: (config: PerspectiveConfig, today: string) => PerspectiveConfig | null;
133
+ export declare const applyPerspectiveToTasks: (tasks: ReadonlyArray<Task>, perspective: Perspective, today?: string) => Array<Task>;
134
+ export declare const loadPerspectiveConfig: (dataDir: string, today?: string) => Effect.Effect<PerspectiveConfig, string>;
135
+ //# sourceMappingURL=query.d.ts.map
@@ -1,2 +1,2 @@
1
1
  export {};
2
- //# sourceMappingURL=query.test.d.ts.map
2
+ //# sourceMappingURL=query.test.d.ts.map
@@ -0,0 +1,21 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { type Task } from "./schema.js";
3
+ export interface CompletionRecurrenceInterval {
4
+ readonly frequency: "DAILY" | "WEEKLY" | "MONTHLY" | "YEARLY";
5
+ readonly interval: number;
6
+ }
7
+ export declare const parseCompletionRecurrenceInterval: (recurrence: string) => Effect.Effect<CompletionRecurrenceInterval, string>;
8
+ export declare const addRecurrenceInterval: (date: Date, recurrenceInterval: CompletionRecurrenceInterval) => Date;
9
+ export declare const shiftIsoDateByRecurrenceInterval: (date: string, recurrenceInterval: CompletionRecurrenceInterval) => Effect.Effect<string, string>;
10
+ export declare const shiftIsoDateTimeToDateByRecurrenceInterval: (dateTime: string, recurrenceInterval: CompletionRecurrenceInterval) => Effect.Effect<string, string>;
11
+ export declare const toIsoDateTime: (value: Date, label: string) => Effect.Effect<string, string>;
12
+ export declare const parseIsoDateToUtcStart: (value: string, label: string) => Effect.Effect<Date, string>;
13
+ export declare const parseIsoDateTime: (value: string, label: string) => Effect.Effect<Date, string>;
14
+ export declare const buildCompletionRecurrenceTask: (completedTask: Task, completedAt: string) => Effect.Effect<Task | null, string>;
15
+ export declare const isClockRecurrenceDue: (task: Task, now: Date) => Effect.Effect<boolean, string>;
16
+ export declare const buildNextClockRecurrenceTask: (existingTask: Task, generatedAt: Date) => Effect.Effect<{
17
+ readonly nextTask: Task;
18
+ readonly updatedCurrent: Task | null;
19
+ readonly shouldReplaceCurrent: boolean;
20
+ }, string>;
21
+ //# sourceMappingURL=recurrence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recurrence.d.ts","sourceRoot":"","sources":["../../src/recurrence.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AAIxC,OAAO,EAAsB,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAQ5D,MAAM,WAAW,4BAA4B;IAC5C,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC1B;AAYD,eAAO,MAAM,iCAAiC,GAC7C,YAAY,MAAM,KAChB,MAAM,CAAC,MAAM,CAAC,4BAA4B,EAAE,MAAM,CA8BlD,CAAC;AAEJ,eAAO,MAAM,qBAAqB,GACjC,MAAM,IAAI,EACV,oBAAoB,4BAA4B,KAC9C,IAmBF,CAAC;AAEF,eAAO,MAAM,gCAAgC,GAC5C,MAAM,MAAM,EACZ,oBAAoB,4BAA4B,KAC9C,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAc5B,CAAC;AAEJ,eAAO,MAAM,0CAA0C,GACtD,UAAU,MAAM,EAChB,oBAAoB,4BAA4B,KAC9C,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAc5B,CAAC;AAEJ,eAAO,MAAM,aAAa,GACzB,OAAO,IAAI,EACX,OAAO,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAW5B,CAAC;AAEJ,eAAO,MAAM,sBAAsB,GAClC,OAAO,MAAM,EACb,OAAO,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAW1B,CAAC;AAEJ,eAAO,MAAM,gBAAgB,GAC5B,OAAO,MAAM,EACb,OAAO,MAAM,KACX,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAW1B,CAAC;AAEJ,eAAO,MAAM,6BAA6B,GACzC,eAAe,IAAI,EACnB,aAAa,MAAM,KACjB,MAAM,CAAC,MAAM,CAAC,IAAI,GAAG,IAAI,EAAE,MAAM,CAwCnC,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAChC,MAAM,IAAI,EACV,KAAK,IAAI,KACP,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CA8B7B,CAAC;AAEJ,eAAO,MAAM,4BAA4B,GACxC,cAAc,IAAI,EAClB,aAAa,IAAI,KACf,MAAM,CAAC,MAAM,CACf;IACC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,cAAc,EAAE,IAAI,GAAG,IAAI,CAAC;IACrC,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACvC,EACD,MAAM,CAuDJ,CAAC"}
@@ -0,0 +1,194 @@
1
+ import * as Effect from "effect/Effect";
2
+ import * as Schema from "effect/Schema";
3
+ import pkg from "rrule";
4
+ const { Frequency, RRule, rrulestr } = pkg;
5
+ import { Task as TaskSchema } from "./schema.js";
6
+ import { generateTaskId } from "./id.js";
7
+ const decodeTask = Schema.decodeUnknownSync(TaskSchema);
8
+ const toErrorMessage = (error) => error instanceof Error ? error.message : String(error);
9
+ const completionRecurrenceFrequencies = new Map([
10
+ [Frequency.DAILY, "DAILY"],
11
+ [Frequency.WEEKLY, "WEEKLY"],
12
+ [Frequency.MONTHLY, "MONTHLY"],
13
+ [Frequency.YEARLY, "YEARLY"],
14
+ ]);
15
+ export const parseCompletionRecurrenceInterval = (recurrence) => Effect.try({
16
+ try: () => {
17
+ const parsedRule = rrulestr(recurrence, { forceset: false });
18
+ if (!(parsedRule instanceof RRule)) {
19
+ throw new Error("Unsupported completion recurrence: expected a single RRULE");
20
+ }
21
+ const frequency = completionRecurrenceFrequencies.get(parsedRule.options.freq);
22
+ if (frequency === undefined) {
23
+ const frequencyLabel = Frequency[parsedRule.options.freq] ?? String(parsedRule.options.freq);
24
+ throw new Error(`Unsupported completion recurrence frequency: ${frequencyLabel}`);
25
+ }
26
+ const interval = parsedRule.options.interval;
27
+ if (!Number.isFinite(interval) || interval < 1) {
28
+ throw new Error(`Invalid recurrence interval: ${String(interval)}`);
29
+ }
30
+ return { frequency, interval };
31
+ },
32
+ catch: (error) => `TaskRepository failed to parse recurrence interval: ${toErrorMessage(error)}`,
33
+ });
34
+ export const addRecurrenceInterval = (date, recurrenceInterval) => {
35
+ const next = new Date(date.getTime());
36
+ switch (recurrenceInterval.frequency) {
37
+ case "DAILY":
38
+ next.setUTCDate(next.getUTCDate() + recurrenceInterval.interval);
39
+ break;
40
+ case "WEEKLY":
41
+ next.setUTCDate(next.getUTCDate() + recurrenceInterval.interval * 7);
42
+ break;
43
+ case "MONTHLY":
44
+ next.setUTCMonth(next.getUTCMonth() + recurrenceInterval.interval);
45
+ break;
46
+ case "YEARLY":
47
+ next.setUTCFullYear(next.getUTCFullYear() + recurrenceInterval.interval);
48
+ break;
49
+ }
50
+ return next;
51
+ };
52
+ export const shiftIsoDateByRecurrenceInterval = (date, recurrenceInterval) => Effect.try({
53
+ try: () => {
54
+ const parsed = new Date(`${date}T00:00:00.000Z`);
55
+ if (Number.isNaN(parsed.getTime())) {
56
+ throw new Error(`Invalid ISO date: ${date}`);
57
+ }
58
+ return addRecurrenceInterval(parsed, recurrenceInterval)
59
+ .toISOString()
60
+ .slice(0, 10);
61
+ },
62
+ catch: (error) => `TaskRepository failed to shift ISO date: ${toErrorMessage(error)}`,
63
+ });
64
+ export const shiftIsoDateTimeToDateByRecurrenceInterval = (dateTime, recurrenceInterval) => Effect.try({
65
+ try: () => {
66
+ const parsed = new Date(dateTime);
67
+ if (Number.isNaN(parsed.getTime())) {
68
+ throw new Error(`Invalid ISO datetime: ${dateTime}`);
69
+ }
70
+ return addRecurrenceInterval(parsed, recurrenceInterval)
71
+ .toISOString()
72
+ .slice(0, 10);
73
+ },
74
+ catch: (error) => `TaskRepository failed to shift ISO datetime: ${toErrorMessage(error)}`,
75
+ });
76
+ export const toIsoDateTime = (value, label) => Effect.try({
77
+ try: () => {
78
+ const timestamp = value.getTime();
79
+ if (Number.isNaN(timestamp)) {
80
+ throw new Error(`Invalid ${label} datetime`);
81
+ }
82
+ return value.toISOString();
83
+ },
84
+ catch: (error) => `TaskRepository failed to normalize ${label} datetime: ${toErrorMessage(error)}`,
85
+ });
86
+ export const parseIsoDateToUtcStart = (value, label) => Effect.try({
87
+ try: () => {
88
+ const parsed = new Date(`${value}T00:00:00.000Z`);
89
+ if (Number.isNaN(parsed.getTime())) {
90
+ throw new Error(`Invalid ISO date: ${value}`);
91
+ }
92
+ return parsed;
93
+ },
94
+ catch: (error) => `TaskRepository failed to parse ${label} date: ${toErrorMessage(error)}`,
95
+ });
96
+ export const parseIsoDateTime = (value, label) => Effect.try({
97
+ try: () => {
98
+ const parsed = new Date(value);
99
+ if (Number.isNaN(parsed.getTime())) {
100
+ throw new Error(`Invalid ISO datetime: ${value}`);
101
+ }
102
+ return parsed;
103
+ },
104
+ catch: (error) => `TaskRepository failed to parse ${label} datetime: ${toErrorMessage(error)}`,
105
+ });
106
+ export const buildCompletionRecurrenceTask = (completedTask, completedAt) => {
107
+ const recurrence = completedTask.recurrence;
108
+ if (recurrence === null ||
109
+ completedTask.recurrence_trigger !== "completion") {
110
+ return Effect.succeed(null);
111
+ }
112
+ return Effect.gen(function* () {
113
+ const recurrenceInterval = yield* parseCompletionRecurrenceInterval(recurrence);
114
+ const deferUntil = yield* shiftIsoDateTimeToDateByRecurrenceInterval(completedAt, recurrenceInterval);
115
+ const shiftedDue = completedTask.due === null
116
+ ? null
117
+ : yield* shiftIsoDateByRecurrenceInterval(completedTask.due, recurrenceInterval);
118
+ const completedDate = completedAt.slice(0, 10);
119
+ return decodeTask({
120
+ ...completedTask,
121
+ id: generateTaskId(completedTask.title),
122
+ status: "active",
123
+ created: completedDate,
124
+ updated: completedDate,
125
+ due: shiftedDue,
126
+ actual_minutes: null,
127
+ completed_at: null,
128
+ last_surfaced: null,
129
+ defer_until: deferUntil,
130
+ nudge_count: 0,
131
+ recurrence_last_generated: completedAt,
132
+ });
133
+ });
134
+ };
135
+ export const isClockRecurrenceDue = (task, now) => Effect.gen(function* () {
136
+ const recurrence = task.recurrence;
137
+ if (recurrence === null || task.recurrence_trigger !== "clock") {
138
+ return false;
139
+ }
140
+ const nowIso = yield* toIsoDateTime(now, "recurrence check");
141
+ const nowDate = new Date(nowIso);
142
+ const createdAt = yield* parseIsoDateToUtcStart(task.created, "task created");
143
+ const lastGeneratedAt = task.recurrence_last_generated === null
144
+ ? createdAt
145
+ : yield* parseIsoDateTime(task.recurrence_last_generated, "recurrence_last_generated");
146
+ const rule = yield* Effect.try({
147
+ try: () => rrulestr(recurrence, { dtstart: createdAt, forceset: false }),
148
+ catch: (error) => `TaskRepository failed to parse recurrence for ${task.id}: ${toErrorMessage(error)}`,
149
+ });
150
+ const nextOccurrence = rule.after(lastGeneratedAt, false);
151
+ return (nextOccurrence !== null && nextOccurrence.getTime() <= nowDate.getTime());
152
+ });
153
+ export const buildNextClockRecurrenceTask = (existingTask, generatedAt) => Effect.gen(function* () {
154
+ const recurrence = existingTask.recurrence;
155
+ if (recurrence === null) {
156
+ return yield* Effect.fail(`TaskRepository failed to generate next recurrence for ${existingTask.id}: task is not recurring`);
157
+ }
158
+ const generatedAtIso = yield* toIsoDateTime(generatedAt, "recurrence generation");
159
+ const generatedDate = generatedAtIso.slice(0, 10);
160
+ const nextTask = decodeTask({
161
+ ...existingTask,
162
+ id: generateTaskId(existingTask.title),
163
+ status: "active",
164
+ created: generatedDate,
165
+ updated: generatedDate,
166
+ actual_minutes: null,
167
+ completed_at: null,
168
+ last_surfaced: null,
169
+ defer_until: null,
170
+ nudge_count: 0,
171
+ recurrence_last_generated: generatedAtIso,
172
+ });
173
+ let updatedCurrent = null;
174
+ let shouldReplaceCurrent = false;
175
+ if (existingTask.recurrence_strategy === "replace" &&
176
+ existingTask.status !== "done" &&
177
+ existingTask.status !== "dropped") {
178
+ updatedCurrent = decodeTask({
179
+ ...existingTask,
180
+ status: "dropped",
181
+ updated: generatedDate,
182
+ recurrence_last_generated: generatedAtIso,
183
+ });
184
+ shouldReplaceCurrent = true;
185
+ }
186
+ if (existingTask.recurrence_strategy === "accumulate") {
187
+ updatedCurrent = decodeTask({
188
+ ...existingTask,
189
+ updated: generatedDate,
190
+ recurrence_last_generated: generatedAtIso,
191
+ });
192
+ }
193
+ return { nextTask, updatedCurrent, shouldReplaceCurrent };
194
+ });