@canonical/summon 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.
Files changed (45) hide show
  1. package/README.md +439 -0
  2. package/generators/example/hello/index.ts +132 -0
  3. package/generators/example/hello/templates/README.md.ejs +20 -0
  4. package/generators/example/hello/templates/index.ts.ejs +9 -0
  5. package/generators/example/webapp/index.ts +509 -0
  6. package/generators/example/webapp/templates/ARCHITECTURE.md.ejs +180 -0
  7. package/generators/example/webapp/templates/App.tsx.ejs +86 -0
  8. package/generators/example/webapp/templates/README.md.ejs +154 -0
  9. package/generators/example/webapp/templates/app.test.ts.ejs +63 -0
  10. package/generators/example/webapp/templates/app.ts.ejs +132 -0
  11. package/generators/example/webapp/templates/feature.ts.ejs +264 -0
  12. package/generators/example/webapp/templates/index.html.ejs +20 -0
  13. package/generators/example/webapp/templates/main.tsx.ejs +43 -0
  14. package/generators/example/webapp/templates/styles.css.ejs +135 -0
  15. package/generators/init/index.ts +124 -0
  16. package/generators/init/templates/generator.ts.ejs +85 -0
  17. package/generators/init/templates/template-index.ts.ejs +9 -0
  18. package/generators/init/templates/template-test.ts.ejs +8 -0
  19. package/package.json +64 -0
  20. package/src/__tests__/combinators.test.ts +895 -0
  21. package/src/__tests__/dry-run.test.ts +927 -0
  22. package/src/__tests__/effect.test.ts +816 -0
  23. package/src/__tests__/interpreter.test.ts +673 -0
  24. package/src/__tests__/primitives.test.ts +970 -0
  25. package/src/__tests__/task.test.ts +929 -0
  26. package/src/__tests__/template.test.ts +666 -0
  27. package/src/cli-format.ts +165 -0
  28. package/src/cli-types.ts +53 -0
  29. package/src/cli.tsx +1322 -0
  30. package/src/combinators.ts +294 -0
  31. package/src/completion.ts +488 -0
  32. package/src/components/App.tsx +960 -0
  33. package/src/components/ExecutionProgress.tsx +205 -0
  34. package/src/components/FileTreePreview.tsx +97 -0
  35. package/src/components/PromptSequence.tsx +483 -0
  36. package/src/components/Spinner.tsx +36 -0
  37. package/src/components/index.ts +16 -0
  38. package/src/dry-run.ts +434 -0
  39. package/src/effect.ts +224 -0
  40. package/src/index.ts +266 -0
  41. package/src/interpreter.ts +463 -0
  42. package/src/primitives.ts +442 -0
  43. package/src/task.ts +245 -0
  44. package/src/template.ts +537 -0
  45. package/src/types.ts +453 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Primitive Operations
3
+ *
4
+ * This module provides primitive task-returning functions for common operations.
5
+ * These are the building blocks for constructing generators.
6
+ */
7
+
8
+ import {
9
+ appendFileEffect,
10
+ copyDirectoryEffect,
11
+ copyFileEffect,
12
+ deleteDirectoryEffect,
13
+ deleteFileEffect,
14
+ execEffect,
15
+ existsEffect,
16
+ globEffect,
17
+ logEffect,
18
+ makeDirEffect,
19
+ promptEffect,
20
+ readContextEffect,
21
+ readFileEffect,
22
+ writeContextEffect,
23
+ writeFileEffect,
24
+ } from "./effect.js";
25
+ import { effect, flatMap, pure } from "./task.js";
26
+ import type { ExecResult, LogLevel, PromptQuestion, Task } from "./types.js";
27
+
28
+ // =============================================================================
29
+ // File System Primitives
30
+ // =============================================================================
31
+
32
+ /**
33
+ * Read a file and return its contents as a string.
34
+ */
35
+ export const readFile = (path: string): Task<string> =>
36
+ effect(readFileEffect(path));
37
+
38
+ /**
39
+ * Write content to a file.
40
+ */
41
+ export const writeFile = (path: string, content: string): Task<void> =>
42
+ effect(writeFileEffect(path, content));
43
+
44
+ /**
45
+ * Append content to a file.
46
+ * @param path - Path to the file
47
+ * @param content - Content to append
48
+ * @param createIfMissing - Create the file if it doesn't exist (default: true)
49
+ */
50
+ export const appendFile = (
51
+ path: string,
52
+ content: string,
53
+ createIfMissing = true,
54
+ ): Task<void> => effect(appendFileEffect(path, content, createIfMissing));
55
+
56
+ /**
57
+ * Copy a file from source to destination.
58
+ */
59
+ export const copyFile = (source: string, dest: string): Task<void> =>
60
+ effect(copyFileEffect(source, dest));
61
+
62
+ /**
63
+ * Copy a directory recursively from source to destination.
64
+ */
65
+ export const copyDirectory = (source: string, dest: string): Task<void> =>
66
+ effect(copyDirectoryEffect(source, dest));
67
+
68
+ /**
69
+ * Delete a file.
70
+ */
71
+ export const deleteFile = (path: string): Task<void> =>
72
+ effect(deleteFileEffect(path));
73
+
74
+ /**
75
+ * Delete a directory recursively.
76
+ */
77
+ export const deleteDirectory = (path: string): Task<void> =>
78
+ effect(deleteDirectoryEffect(path));
79
+
80
+ /**
81
+ * Create a directory (recursively by default).
82
+ */
83
+ export const mkdir = (path: string, recursive = true): Task<void> =>
84
+ effect(makeDirEffect(path, recursive));
85
+
86
+ /**
87
+ * Check if a file or directory exists.
88
+ */
89
+ export const exists = (path: string): Task<boolean> =>
90
+ effect(existsEffect(path));
91
+
92
+ /**
93
+ * Find files matching a glob pattern.
94
+ */
95
+ export const glob = (pattern: string, cwd: string): Task<string[]> =>
96
+ effect(globEffect(pattern, cwd));
97
+
98
+ // =============================================================================
99
+ // File Transformation Primitives
100
+ // =============================================================================
101
+
102
+ /**
103
+ * Options for sorting file lines.
104
+ */
105
+ export interface SortFileLinesOptions {
106
+ /**
107
+ * Custom comparator function for sorting.
108
+ * Defaults to locale-aware string comparison.
109
+ */
110
+ compare?: (a: string, b: string) => number;
111
+
112
+ /**
113
+ * If true, remove duplicate lines after sorting.
114
+ * @default false
115
+ */
116
+ unique?: boolean;
117
+
118
+ /**
119
+ * Lines matching this pattern are considered "header" lines and will
120
+ * be kept at the top of the file, unsorted.
121
+ * Useful for preserving file headers, comments, or specific imports.
122
+ *
123
+ * @example /^\/\// - Keep lines starting with // at the top
124
+ * @example /^import.*from ["']react["']/ - Keep React imports at top
125
+ */
126
+ headerPattern?: RegExp;
127
+
128
+ /**
129
+ * Lines matching this pattern are considered "footer" lines and will
130
+ * be kept at the bottom of the file, unsorted.
131
+ */
132
+ footerPattern?: RegExp;
133
+
134
+ /**
135
+ * If true, preserve blank lines in their relative positions within
136
+ * the sorted content. If false, blank lines are sorted with other lines.
137
+ * @default false
138
+ */
139
+ preserveBlankLines?: boolean;
140
+ }
141
+
142
+ /**
143
+ * Sort the lines of a file.
144
+ *
145
+ * This is useful for maintaining sorted barrel files (index.ts with exports),
146
+ * sorted import lists, or any file where line order should be alphabetical.
147
+ *
148
+ * @param path - Path to the file to sort
149
+ * @param options - Sorting options
150
+ *
151
+ * @example
152
+ * // Sort a barrel file alphabetically
153
+ * sortFileLines("src/index.ts")
154
+ *
155
+ * @example
156
+ * // Sort exports, keeping the header comment
157
+ * sortFileLines("src/index.ts", {
158
+ * headerPattern: /^\/\//, // Keep comment lines at top
159
+ * })
160
+ *
161
+ * @example
162
+ * // Sort and deduplicate
163
+ * sortFileLines("src/exports.ts", { unique: true })
164
+ *
165
+ * @example
166
+ * // Custom sort (case-insensitive)
167
+ * sortFileLines("src/index.ts", {
168
+ * compare: (a, b) => a.toLowerCase().localeCompare(b.toLowerCase())
169
+ * })
170
+ */
171
+ export const sortFileLines = (
172
+ path: string,
173
+ options: SortFileLinesOptions = {},
174
+ ): Task<void> => {
175
+ const {
176
+ compare = (a, b) => a.localeCompare(b),
177
+ unique = false,
178
+ headerPattern,
179
+ footerPattern,
180
+ preserveBlankLines = false,
181
+ } = options;
182
+
183
+ return flatMap(readFile(path), (content) => {
184
+ const lines = content.split("\n");
185
+
186
+ // Separate header, body, and footer lines
187
+ const headerLines: string[] = [];
188
+ const footerLines: string[] = [];
189
+ const bodyLines: string[] = [];
190
+ const blankLineIndices: number[] = [];
191
+
192
+ // First pass: identify header lines (consecutive matches at start)
193
+ let inHeader = true;
194
+ let bodyStartIndex = 0;
195
+
196
+ if (headerPattern) {
197
+ for (let i = 0; i < lines.length; i++) {
198
+ if (inHeader && headerPattern.test(lines[i])) {
199
+ headerLines.push(lines[i]);
200
+ bodyStartIndex = i + 1;
201
+ } else {
202
+ inHeader = false;
203
+ break;
204
+ }
205
+ }
206
+ }
207
+
208
+ // Second pass: identify footer lines (consecutive matches at end)
209
+ let footerStartIndex = lines.length;
210
+
211
+ if (footerPattern) {
212
+ for (let i = lines.length - 1; i >= bodyStartIndex; i--) {
213
+ if (footerPattern.test(lines[i])) {
214
+ footerLines.unshift(lines[i]);
215
+ footerStartIndex = i;
216
+ } else {
217
+ break;
218
+ }
219
+ }
220
+ }
221
+
222
+ // Third pass: collect body lines
223
+ for (let i = bodyStartIndex; i < footerStartIndex; i++) {
224
+ const line = lines[i];
225
+ if (preserveBlankLines && line.trim() === "") {
226
+ blankLineIndices.push(bodyLines.length);
227
+ bodyLines.push(line);
228
+ } else {
229
+ bodyLines.push(line);
230
+ }
231
+ }
232
+
233
+ // Sort body lines (excluding blank lines if preserving them)
234
+ let sortedBody: string[];
235
+
236
+ if (preserveBlankLines) {
237
+ // Extract non-blank lines, sort them, then reinsert blanks
238
+ const nonBlankLines = bodyLines.filter((line) => line.trim() !== "");
239
+ const sortedNonBlank = unique
240
+ ? [...new Set(nonBlankLines)].sort(compare)
241
+ : nonBlankLines.sort(compare);
242
+
243
+ // Rebuild with blank lines in relative positions
244
+ sortedBody = [];
245
+ let nonBlankIdx = 0;
246
+ for (let i = 0; i < bodyLines.length; i++) {
247
+ if (blankLineIndices.includes(i)) {
248
+ sortedBody.push("");
249
+ } else if (nonBlankIdx < sortedNonBlank.length) {
250
+ sortedBody.push(sortedNonBlank[nonBlankIdx++]);
251
+ }
252
+ }
253
+ } else {
254
+ sortedBody = unique
255
+ ? [...new Set(bodyLines)].sort(compare)
256
+ : bodyLines.sort(compare);
257
+ }
258
+
259
+ // Reassemble the file
260
+ const sortedContent = [...headerLines, ...sortedBody, ...footerLines].join(
261
+ "\n",
262
+ );
263
+
264
+ return writeFile(path, sortedContent);
265
+ });
266
+ };
267
+
268
+ // =============================================================================
269
+ // Process Primitives
270
+ // =============================================================================
271
+
272
+ /**
273
+ * Execute a command with arguments.
274
+ */
275
+ export const exec = (
276
+ command: string,
277
+ args: string[],
278
+ cwd?: string,
279
+ ): Task<ExecResult> => effect(execEffect(command, args, cwd));
280
+
281
+ /**
282
+ * Execute a simple command string (split on spaces).
283
+ */
284
+ export const execSimple = (
285
+ commandLine: string,
286
+ cwd?: string,
287
+ ): Task<ExecResult> => {
288
+ const parts = commandLine.split(" ");
289
+ const [command, ...args] = parts;
290
+ return exec(command, args, cwd);
291
+ };
292
+
293
+ // =============================================================================
294
+ // Prompt Primitives
295
+ // =============================================================================
296
+
297
+ /**
298
+ * Prompt the user with a question.
299
+ */
300
+ export const prompt = <T = unknown>(question: PromptQuestion): Task<T> =>
301
+ effect(promptEffect(question));
302
+
303
+ /**
304
+ * Prompt for text input.
305
+ */
306
+ export const promptText = (
307
+ name: string,
308
+ message: string,
309
+ defaultValue?: string,
310
+ ): Task<string> =>
311
+ prompt({
312
+ type: "text",
313
+ name,
314
+ message,
315
+ default: defaultValue,
316
+ });
317
+
318
+ /**
319
+ * Prompt for confirmation.
320
+ */
321
+ export const promptConfirm = (
322
+ name: string,
323
+ message: string,
324
+ defaultValue = false,
325
+ ): Task<boolean> =>
326
+ prompt({
327
+ type: "confirm",
328
+ name,
329
+ message,
330
+ default: defaultValue,
331
+ });
332
+
333
+ /**
334
+ * Prompt for selection from a list.
335
+ */
336
+ export const promptSelect = (
337
+ name: string,
338
+ message: string,
339
+ choices: Array<{ label: string; value: string }>,
340
+ defaultValue?: string,
341
+ ): Task<string> =>
342
+ prompt({
343
+ type: "select",
344
+ name,
345
+ message,
346
+ choices,
347
+ default: defaultValue,
348
+ });
349
+
350
+ /**
351
+ * Prompt for multiple selections from a list.
352
+ */
353
+ export const promptMultiselect = (
354
+ name: string,
355
+ message: string,
356
+ choices: Array<{ label: string; value: string }>,
357
+ defaultValue?: string[],
358
+ ): Task<string[]> =>
359
+ prompt({
360
+ type: "multiselect",
361
+ name,
362
+ message,
363
+ choices,
364
+ default: defaultValue,
365
+ });
366
+
367
+ // =============================================================================
368
+ // Logging Primitives
369
+ // =============================================================================
370
+
371
+ /**
372
+ * Log a message at a specific level.
373
+ */
374
+ export const log = (level: LogLevel, message: string): Task<void> =>
375
+ effect(logEffect(level, message));
376
+
377
+ /**
378
+ * Log a debug message.
379
+ */
380
+ export const debug = (message: string): Task<void> => log("debug", message);
381
+
382
+ /**
383
+ * Log an info message.
384
+ */
385
+ export const info = (message: string): Task<void> => log("info", message);
386
+
387
+ /**
388
+ * Log a warning message.
389
+ */
390
+ export const warn = (message: string): Task<void> => log("warn", message);
391
+
392
+ /**
393
+ * Log an error message.
394
+ */
395
+ export const error = (message: string): Task<void> => log("error", message);
396
+
397
+ // =============================================================================
398
+ // Context Primitives
399
+ // =============================================================================
400
+
401
+ /**
402
+ * Read a value from the context.
403
+ */
404
+ export const getContext = <T = unknown>(key: string): Task<T | undefined> =>
405
+ effect(readContextEffect(key));
406
+
407
+ /**
408
+ * Write a value to the context.
409
+ */
410
+ export const setContext = (key: string, value: unknown): Task<void> =>
411
+ effect(writeContextEffect(key, value));
412
+
413
+ /**
414
+ * Execute a task with a temporary context value.
415
+ */
416
+ export const withContext = <A>(
417
+ key: string,
418
+ value: unknown,
419
+ task: Task<A>,
420
+ ): Task<A> => {
421
+ // This is a higher-level pattern that will be handled specially by interpreters
422
+ // For now, we implement it as set -> run -> restore
423
+ return {
424
+ _tag: "Effect",
425
+ effect: writeContextEffect(key, value),
426
+ cont: () => task,
427
+ };
428
+ };
429
+
430
+ // =============================================================================
431
+ // Pure Primitives
432
+ // =============================================================================
433
+
434
+ /**
435
+ * A task that does nothing and returns void.
436
+ */
437
+ export const noop: Task<void> = pure(undefined);
438
+
439
+ /**
440
+ * A task that returns the given value.
441
+ */
442
+ export const succeed = <A>(value: A): Task<A> => pure(value);
package/src/task.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Task Monad Implementation
3
+ *
4
+ * The Task monad is the core abstraction for building composable, testable generators.
5
+ * Tasks represent computations that may perform effects and can fail.
6
+ *
7
+ * Key properties:
8
+ * - Pure: Tasks return effect descriptions, not actual effects
9
+ * - Composable: Tasks can be sequenced with flatMap/chain
10
+ * - Testable: Effects can be collected without execution
11
+ * - Type-safe: Full TypeScript inference for composed tasks
12
+ */
13
+
14
+ import type { Effect, Task, TaskError } from "./types.js";
15
+
16
+ // =============================================================================
17
+ // Core Constructors
18
+ // =============================================================================
19
+
20
+ /**
21
+ * Lift a pure value into a Task.
22
+ */
23
+ export const pure = <A>(value: A): Task<A> => ({
24
+ _tag: "Pure",
25
+ value,
26
+ });
27
+
28
+ /**
29
+ * Create a Task from an Effect.
30
+ * The continuation receives the result of executing the effect.
31
+ */
32
+ export const effect = <A>(eff: Effect): Task<A> => ({
33
+ _tag: "Effect",
34
+ effect: eff,
35
+ cont: (result) => pure(result as A),
36
+ });
37
+
38
+ /**
39
+ * Create a failed Task with an error.
40
+ */
41
+ export const fail = <A = never>(error: TaskError): Task<A> => ({
42
+ _tag: "Fail",
43
+ error,
44
+ });
45
+
46
+ /**
47
+ * Create a failed Task from an error code and message.
48
+ */
49
+ export const failWith = <A = never>(code: string, message: string): Task<A> =>
50
+ fail({
51
+ code,
52
+ message,
53
+ });
54
+
55
+ // =============================================================================
56
+ // Monad Operations
57
+ // =============================================================================
58
+
59
+ /**
60
+ * Monadic bind (flatMap).
61
+ * Sequences two tasks, passing the result of the first to produce the second.
62
+ */
63
+ export const flatMap = <A, B>(task: Task<A>, f: (a: A) => Task<B>): Task<B> => {
64
+ switch (task._tag) {
65
+ case "Pure":
66
+ return f(task.value);
67
+ case "Fail":
68
+ return task as unknown as Task<B>;
69
+ case "Effect":
70
+ return {
71
+ _tag: "Effect",
72
+ effect: task.effect,
73
+ cont: (result) => flatMap(task.cont(result), f),
74
+ };
75
+ }
76
+ };
77
+
78
+ /**
79
+ * Functor map.
80
+ * Transform the result of a task with a pure function.
81
+ */
82
+ export const map = <A, B>(task: Task<A>, f: (a: A) => B): Task<B> =>
83
+ flatMap(task, (a) => pure(f(a)));
84
+
85
+ /**
86
+ * Apply a function wrapped in a Task to a value wrapped in a Task.
87
+ */
88
+ export const ap = <A, B>(taskF: Task<(a: A) => B>, taskA: Task<A>): Task<B> =>
89
+ flatMap(taskF, (f) => map(taskA, f));
90
+
91
+ /**
92
+ * Error recovery.
93
+ * If the task fails, the handler can produce a new task.
94
+ */
95
+ export const recover = <A>(
96
+ task: Task<A>,
97
+ handler: (error: TaskError) => Task<A>,
98
+ ): Task<A> => {
99
+ switch (task._tag) {
100
+ case "Pure":
101
+ return task;
102
+ case "Fail":
103
+ return handler(task.error);
104
+ case "Effect":
105
+ return {
106
+ _tag: "Effect",
107
+ effect: task.effect,
108
+ cont: (result) => recover(task.cont(result), handler),
109
+ };
110
+ }
111
+ };
112
+
113
+ /**
114
+ * Map over the error of a failed task.
115
+ */
116
+ export const mapError = <A>(
117
+ task: Task<A>,
118
+ f: (error: TaskError) => TaskError,
119
+ ): Task<A> => {
120
+ switch (task._tag) {
121
+ case "Pure":
122
+ return task;
123
+ case "Fail":
124
+ return fail(f(task.error));
125
+ case "Effect":
126
+ return {
127
+ _tag: "Effect",
128
+ effect: task.effect,
129
+ cont: (result) => mapError(task.cont(result), f),
130
+ };
131
+ }
132
+ };
133
+
134
+ // =============================================================================
135
+ // Fluent Builder API
136
+ // =============================================================================
137
+
138
+ /**
139
+ * Fluent builder for composing tasks.
140
+ * Provides a chainable API for common task operations.
141
+ */
142
+ export class TaskBuilder<A> {
143
+ constructor(private readonly _task: Task<A>) {}
144
+
145
+ /**
146
+ * Transform the result with a pure function.
147
+ */
148
+ map<B>(f: (a: A) => B): TaskBuilder<B> {
149
+ return new TaskBuilder(map(this._task, f));
150
+ }
151
+
152
+ /**
153
+ * Chain with another task-producing function.
154
+ */
155
+ flatMap<B>(f: (a: A) => Task<B>): TaskBuilder<B> {
156
+ return new TaskBuilder(flatMap(this._task, f));
157
+ }
158
+
159
+ /**
160
+ * Chain with another TaskBuilder-producing function.
161
+ */
162
+ chain<B>(f: (a: A) => TaskBuilder<B>): TaskBuilder<B> {
163
+ return new TaskBuilder(flatMap(this._task, (a) => f(a).unwrap()));
164
+ }
165
+
166
+ /**
167
+ * Recover from errors.
168
+ */
169
+ recover(f: (error: TaskError) => Task<A>): TaskBuilder<A> {
170
+ return new TaskBuilder(recover(this._task, f));
171
+ }
172
+
173
+ /**
174
+ * Map over errors.
175
+ */
176
+ mapError(f: (error: TaskError) => TaskError): TaskBuilder<A> {
177
+ return new TaskBuilder(mapError(this._task, f));
178
+ }
179
+
180
+ /**
181
+ * Execute a side effect without changing the value.
182
+ */
183
+ tap(f: (a: A) => Task<unknown>): TaskBuilder<A> {
184
+ return new TaskBuilder(flatMap(this._task, (a) => map(f(a), () => a)));
185
+ }
186
+
187
+ /**
188
+ * Sequence with another task, discarding the current value.
189
+ * Useful for chaining void tasks.
190
+ *
191
+ * @example
192
+ * task(mkdir("output"))
193
+ * .andThen(writeFile("output/a.txt", "A"))
194
+ * .andThen(writeFile("output/b.txt", "B"))
195
+ * .andThen(info("Done!"))
196
+ */
197
+ andThen<B>(next: Task<B>): TaskBuilder<B> {
198
+ return new TaskBuilder(flatMap(this._task, () => next));
199
+ }
200
+
201
+ /**
202
+ * Extract the underlying Task.
203
+ */
204
+ unwrap(): Task<A> {
205
+ return this._task;
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Create a TaskBuilder from a Task.
211
+ */
212
+ export const task = <A>(t: Task<A>): TaskBuilder<A> => new TaskBuilder(t);
213
+
214
+ /**
215
+ * Create a TaskBuilder from a pure value.
216
+ */
217
+ export const of = <A>(value: A): TaskBuilder<A> => task(pure(value));
218
+
219
+ // =============================================================================
220
+ // Utility Functions
221
+ // =============================================================================
222
+
223
+ /**
224
+ * Check if a task is pure (no effects).
225
+ */
226
+ export const isPure = <A>(t: Task<A>): t is { _tag: "Pure"; value: A } =>
227
+ t._tag === "Pure";
228
+
229
+ /**
230
+ * Check if a task has failed.
231
+ */
232
+ export const isFailed = <A>(
233
+ t: Task<A>,
234
+ ): t is { _tag: "Fail"; error: TaskError } => t._tag === "Fail";
235
+
236
+ /**
237
+ * Check if a task has effects.
238
+ */
239
+ export const hasEffects = <A>(
240
+ t: Task<A>,
241
+ ): t is {
242
+ _tag: "Effect";
243
+ effect: Effect;
244
+ cont: (result: unknown) => Task<A>;
245
+ } => t._tag === "Effect";