@elaraai/e3-types 1.0.78 → 1.0.80

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 (153) hide show
  1. package/README.md +1 -1
  2. package/dist/src/api.d.ts +569 -36
  3. package/dist/src/api.d.ts.map +1 -1
  4. package/dist/src/api.js +257 -32
  5. package/dist/src/api.js.map +1 -1
  6. package/dist/src/collection-manifest.d.ts +37 -0
  7. package/dist/src/collection-manifest.d.ts.map +1 -0
  8. package/dist/src/collection-manifest.js +37 -0
  9. package/dist/src/collection-manifest.js.map +1 -0
  10. package/dist/src/dataflow.d.ts +264 -19
  11. package/dist/src/dataflow.d.ts.map +1 -1
  12. package/dist/src/dataflow.js +156 -15
  13. package/dist/src/dataflow.js.map +1 -1
  14. package/dist/src/dataflow.spec.js +184 -27
  15. package/dist/src/dataflow.spec.js.map +1 -1
  16. package/dist/src/dataset-blob.d.ts +75 -15
  17. package/dist/src/dataset-blob.d.ts.map +1 -1
  18. package/dist/src/dataset-blob.js +95 -28
  19. package/dist/src/dataset-blob.js.map +1 -1
  20. package/dist/src/dataset-blob.spec.d.ts +6 -0
  21. package/dist/src/dataset-blob.spec.d.ts.map +1 -0
  22. package/dist/src/dataset-blob.spec.js +92 -0
  23. package/dist/src/dataset-blob.spec.js.map +1 -0
  24. package/dist/src/dataset-ref.d.ts +5 -4
  25. package/dist/src/dataset-ref.d.ts.map +1 -1
  26. package/dist/src/dataset-ref.js +5 -4
  27. package/dist/src/dataset-ref.js.map +1 -1
  28. package/dist/src/environment.d.ts +3 -2
  29. package/dist/src/environment.d.ts.map +1 -1
  30. package/dist/src/environment.js +3 -2
  31. package/dist/src/environment.js.map +1 -1
  32. package/dist/src/execution.d.ts +112 -18
  33. package/dist/src/execution.d.ts.map +1 -1
  34. package/dist/src/execution.js +174 -45
  35. package/dist/src/execution.js.map +1 -1
  36. package/dist/src/execution.spec.d.ts +6 -0
  37. package/dist/src/execution.spec.d.ts.map +1 -0
  38. package/dist/src/execution.spec.js +108 -0
  39. package/dist/src/execution.spec.js.map +1 -0
  40. package/dist/src/function.d.ts +14 -12
  41. package/dist/src/function.d.ts.map +1 -1
  42. package/dist/src/function.js +17 -32
  43. package/dist/src/function.js.map +1 -1
  44. package/dist/src/index.d.ts +16 -12
  45. package/dist/src/index.d.ts.map +1 -1
  46. package/dist/src/index.js +25 -20
  47. package/dist/src/index.js.map +1 -1
  48. package/dist/src/lock.d.ts +68 -22
  49. package/dist/src/lock.d.ts.map +1 -1
  50. package/dist/src/lock.js +33 -22
  51. package/dist/src/lock.js.map +1 -1
  52. package/dist/src/names.d.ts +21 -0
  53. package/dist/src/names.d.ts.map +1 -0
  54. package/dist/src/names.js +31 -0
  55. package/dist/src/names.js.map +1 -0
  56. package/dist/src/package.d.ts +11 -78
  57. package/dist/src/package.d.ts.map +1 -1
  58. package/dist/src/package.js +13 -66
  59. package/dist/src/package.js.map +1 -1
  60. package/dist/src/record.d.ts +582 -18
  61. package/dist/src/record.d.ts.map +1 -1
  62. package/dist/src/record.js +405 -7
  63. package/dist/src/record.js.map +1 -1
  64. package/dist/src/{stream.spec.d.ts → record.spec.d.ts} +1 -1
  65. package/dist/src/record.spec.d.ts.map +1 -0
  66. package/dist/src/record.spec.js +143 -0
  67. package/dist/src/record.spec.js.map +1 -0
  68. package/dist/src/release.d.ts +36 -0
  69. package/dist/src/release.d.ts.map +1 -0
  70. package/dist/src/release.js +71 -0
  71. package/dist/src/release.js.map +1 -0
  72. package/dist/src/release.spec.d.ts +6 -0
  73. package/dist/src/release.spec.d.ts.map +1 -0
  74. package/dist/src/release.spec.js +42 -0
  75. package/dist/src/release.spec.js.map +1 -0
  76. package/dist/src/repository.d.ts +64 -0
  77. package/dist/src/repository.d.ts.map +1 -0
  78. package/dist/src/repository.js +49 -0
  79. package/dist/src/repository.js.map +1 -0
  80. package/dist/src/runner.d.ts +16 -62
  81. package/dist/src/runner.d.ts.map +1 -1
  82. package/dist/src/runner.js +16 -79
  83. package/dist/src/runner.js.map +1 -1
  84. package/dist/src/runner.spec.js +11 -85
  85. package/dist/src/runner.spec.js.map +1 -1
  86. package/dist/src/structure.d.ts +0 -57
  87. package/dist/src/structure.d.ts.map +1 -1
  88. package/dist/src/structure.js +0 -3
  89. package/dist/src/structure.js.map +1 -1
  90. package/dist/src/task.d.ts +210 -318
  91. package/dist/src/task.d.ts.map +1 -1
  92. package/dist/src/task.js +117 -472
  93. package/dist/src/task.js.map +1 -1
  94. package/dist/src/task.spec.js +62 -154
  95. package/dist/src/task.spec.js.map +1 -1
  96. package/dist/src/transfer.d.ts +16 -22
  97. package/dist/src/transfer.d.ts.map +1 -1
  98. package/dist/src/transfer.js +16 -22
  99. package/dist/src/transfer.js.map +1 -1
  100. package/dist/src/transfer.spec.js +4 -34
  101. package/dist/src/transfer.spec.js.map +1 -1
  102. package/dist/src/unit-plan.d.ts +130 -0
  103. package/dist/src/unit-plan.d.ts.map +1 -0
  104. package/dist/src/unit-plan.js +107 -0
  105. package/dist/src/unit-plan.js.map +1 -0
  106. package/dist/src/unit-plan.spec.d.ts +6 -0
  107. package/dist/src/unit-plan.spec.d.ts.map +1 -0
  108. package/dist/src/unit-plan.spec.js +29 -0
  109. package/dist/src/unit-plan.spec.js.map +1 -0
  110. package/dist/src/workspace.d.ts +22 -11
  111. package/dist/src/workspace.d.ts.map +1 -1
  112. package/dist/src/workspace.js +10 -11
  113. package/dist/src/workspace.js.map +1 -1
  114. package/package.json +3 -3
  115. package/src/api.ts +276 -32
  116. package/src/collection-manifest.ts +53 -0
  117. package/src/dataflow.spec.ts +223 -31
  118. package/src/dataflow.ts +183 -26
  119. package/src/dataset-blob.spec.ts +117 -0
  120. package/src/dataset-blob.ts +157 -21
  121. package/src/dataset-ref.ts +5 -4
  122. package/src/environment.ts +3 -2
  123. package/src/execution.spec.ts +114 -0
  124. package/src/execution.ts +175 -51
  125. package/src/function.ts +19 -32
  126. package/src/index.ts +122 -39
  127. package/src/lock.ts +39 -22
  128. package/src/names.ts +39 -0
  129. package/src/package.ts +15 -71
  130. package/src/record.spec.ts +195 -0
  131. package/src/record.ts +489 -7
  132. package/src/release.spec.ts +47 -0
  133. package/src/release.ts +69 -0
  134. package/src/repository.ts +67 -0
  135. package/src/runner.spec.ts +11 -94
  136. package/src/runner.ts +16 -80
  137. package/src/structure.ts +0 -8
  138. package/src/task.spec.ts +67 -188
  139. package/src/task.ts +140 -522
  140. package/src/transfer.spec.ts +4 -51
  141. package/src/transfer.ts +16 -22
  142. package/src/unit-plan.spec.ts +31 -0
  143. package/src/unit-plan.ts +121 -0
  144. package/src/workspace.ts +13 -11
  145. package/dist/src/stream.d.ts +0 -97
  146. package/dist/src/stream.d.ts.map +0 -1
  147. package/dist/src/stream.js +0 -134
  148. package/dist/src/stream.js.map +0 -1
  149. package/dist/src/stream.spec.d.ts.map +0 -1
  150. package/dist/src/stream.spec.js +0 -60
  151. package/dist/src/stream.spec.js.map +0 -1
  152. package/src/stream.spec.ts +0 -88
  153. package/src/stream.ts +0 -170
package/src/task.ts CHANGED
@@ -6,566 +6,184 @@
6
6
  /**
7
7
  * Task object types for e3.
8
8
  *
9
- * A task object defines a complete executable unit: the command IR that
10
- * generates the exec args, where to read inputs from, and where to write output.
9
+ * A task object says what a task runs, on which runtime, over which datasets,
10
+ * and what its output is: every field a mode used to be implied by is typed.
11
+ * Task objects are stored in the object store and referenced by packages, so
12
+ * they are content-addressed, which is what memoizes their executions.
11
13
  *
12
- * Task objects are stored in the object store and referenced by packages.
13
- * They are content-addressed, enabling deduplication and memoization.
14
- *
15
- * Input and output types are inferred from the package's structure at the
16
- * specified paths - the task just references locations, not types.
14
+ * Input and output types are not stored here: they are the package structure's
15
+ * at the paths the task names.
17
16
  */
18
17
 
19
- import { StructType, StringType, ArrayType, BlobType, BooleanType, IntegerType, OptionType, ValueTypeOf, decodeBeast2For, encodeBeast2For, none, variant } from '@elaraai/east';
20
- import type { EastTypeValue, FunctionIR } from '@elaraai/east';
18
+ import { StructType, StringType, ArrayType, NullType, OptionType, VariantType, ValueTypeOf, decodeBeast2For } from '@elaraai/east';
21
19
  import { TreePathType } from './structure.js';
22
20
  import { RunnerType } from './runner.js';
23
21
 
24
- /**
25
- * Task object stored in the object store.
26
- *
27
- * A task is a complete executable unit that reads from input dataset paths
28
- * and writes to an output dataset path. The commandIr is evaluated at runtime
29
- * to produce the exec args.
30
- *
31
- * @remarks
32
- * - `commandIr`: Hash of East IR object that produces exec args
33
- * - IR signature: (inputs: Array<String>, output: String) -> Array<String>
34
- * - `inputs` are paths to staged input .beast2 files
35
- * - `output` is the path where output should be written
36
- * - Returns array of strings to exec (e.g., ["sh", "-c", "python ..."])
37
- * - `inputs`: Paths to input datasets in the data tree
38
- * - `output`: Path to the output dataset in the data tree
39
- *
40
- * Types are not stored in the task - they are inferred from the package's
41
- * structure at the specified paths. This keeps tasks simple and avoids
42
- * redundant type information.
43
- *
44
- * @example
45
- * ```ts
46
- * import { variant } from '@elaraai/east';
47
- *
48
- * // Task with command IR that generates: ["sh", "-c", "python script.py <input> <output>"]
49
- * const task: TaskObject = {
50
- * commandIr: '5e7a3b...', // hash of compiled IR
51
- * inputs: [
52
- * [variant('field', 'inputs'), variant('field', 'sales')],
53
- * ],
54
- * output: [variant('field', 'tasks'), variant('field', 'train'), variant('field', 'output')],
55
- * };
56
- * ```
57
- */
58
- export const TaskObjectType = StructType({
59
- /** Hash of East IR that generates exec args: (inputs, output) -> Array<String> */
60
- commandIr: StringType,
61
- /** Input paths: where to read each input dataset from the data tree */
62
- inputs: ArrayType(TreePathType),
63
- /** Output path: where to write the output dataset in the data tree */
64
- output: TreePathType,
65
- /** Task kind: "data" (default), "ui", or future extensions. None for old packages. */
66
- kind: OptionType(StringType),
67
- /** Opaque extension metadata (beast2-encoded). Interpreted by the kind-specific consumer. */
68
- metadata: OptionType(BlobType),
69
- /**
70
- * The task's runner, as routing metadata (symmetric with
71
- * FunctionObject.runner). For `custom` runners, `commandIr` remains
72
- * authoritative for execution — the wire command is informational.
73
- *
74
- * NOTE: added as a hard cutover (no dual decoder) — packages exported by
75
- * older SDKs must be re-exported.
76
- */
77
- runner: RunnerType,
78
- /**
79
- * Hash of an {@link EnvironmentSpecType} object the task executes in;
80
- * `none` ⇒ the stock runtime image. Appended LAST (BEAST2 encodes struct
81
- * fields positionally) with a legacy dual decoder — see
82
- * {@link decodeTaskObject}.
83
- */
84
- environment: OptionType(StringType),
85
- });
86
- export type TaskObjectType = typeof TaskObjectType;
87
-
88
- export type TaskObject = ValueTypeOf<typeof TaskObjectType>;
22
+ /** The kind tag every task object carries, which the garbage collector
23
+ * dispatches on. */
24
+ export const TASK_OBJECT_KIND = '$task';
89
25
 
90
26
  /**
91
- * The pre-`environment` task object wire shape, kept only so
92
- * {@link decodeTaskObject} can read tasks exported before execution
93
- * environments existed.
27
+ * What a task runs.
28
+ *
29
+ * - `east`: an East program, which a stock runner executes as a unit
30
+ * (`exec`): `program` is the hash of its IR bundle.
31
+ * - `command`: a command, which the custom runtime spawns. `commandIr` is the
32
+ * hash of the IR bundle of the East function that builds its argv,
33
+ * `(inputs: Array<String>, output: String) -> Array<String>`.
94
34
  */
95
- const PreEnvironmentTaskObjectType = StructType({
96
- commandIr: StringType,
97
- inputs: ArrayType(TreePathType),
98
- output: TreePathType,
99
- kind: OptionType(StringType),
100
- metadata: OptionType(BlobType),
101
- runner: RunnerType,
35
+ export const TaskBodyType = VariantType({
36
+ east: StructType({ program: StringType }),
37
+ command: StructType({ commandIr: StringType }),
102
38
  });
103
-
104
- const decodeCurrentTask = decodeBeast2For(TaskObjectType);
105
- const decodePreEnvironmentTask = decodeBeast2For(PreEnvironmentTaskObjectType);
39
+ export type TaskBodyType = typeof TaskBodyType;
40
+ export type TaskBody = ValueTypeOf<typeof TaskBodyType>;
106
41
 
107
42
  /**
108
- * Decode a `TaskObject` from BEAST2 bytes, tolerating the pre-`environment`
109
- * wire format (dual-decode migration, like {@link decodePackageObject}).
110
- *
111
- * Every task-read path — local AND cloud — must use this instead of
112
- * `decodeBeast2For(TaskObjectType)` directly. Older bytes decode with
113
- * `environment` defaulted to `none`.
43
+ * An input the work may be split over: `by` names the leading key fields
44
+ * whose equal values are never split across pieces — a field, or a
45
+ * first-field path written `a.b`. Empty when any key may start a piece.
114
46
  */
115
- export function decodeTaskObject(data: Uint8Array): TaskObject {
116
- try {
117
- return decodeCurrentTask(data);
118
- } catch (err) {
119
- try {
120
- const legacy = decodePreEnvironmentTask(data);
121
- return { ...legacy, environment: none };
122
- } catch {
123
- throw err; // no known shape — surface the current-format error
124
- }
125
- }
126
- }
47
+ export const TaskPartitionType = StructType({ by: ArrayType(StringType) });
48
+ export type TaskPartitionType = typeof TaskPartitionType;
49
+ export type TaskPartition = ValueTypeOf<typeof TaskPartitionType>;
127
50
 
128
- // =============================================================================
129
- // Partition / stream task kinds
130
- // =============================================================================
131
-
132
- /** Task kind of a partitioned task — the orchestrator carves its partitioned
133
- * input(s) into key-range slices, runs each slice as an ordinary
134
- * content-addressed execution, and assembles the output by splice or by
135
- * combining partials. The spec rides {@link TaskObjectType}'s `metadata`
136
- * slot as a beast2-encoded {@link PartitionTaskMetadataType}. */
137
- export const TASK_KIND_PARTITION = 'partition';
138
-
139
- /** Task kind of a streaming task — one execution whose runner feeds the
140
- * stream input lazily and writes the output incrementally through an `emit`
141
- * capability. The spec rides {@link TaskObjectType}'s `metadata` slot as a
142
- * beast2-encoded {@link StreamTaskMetadataType}. */
143
- export const TASK_KIND_STREAM = 'stream';
144
-
145
- /** Task kind of a merge unit — the runner's `merge` command over sorted
146
- * partials of a partitioned task's keyed output (issue #770). The
147
- * orchestrator writes one such task per partitioned task from the
148
- * package's {@link PartitionTaskMetadataType} `mergeCommand`, and runs it
149
- * as an ordinary content-addressed execution per group of partials. It
150
- * carries no metadata. */
151
- export const TASK_KIND_MERGE = 'merge';
152
-
153
- /**
154
- * Metadata of a {@link TASK_KIND_PARTITION} task.
155
- *
156
- * The task's wire `inputs` are laid out `[function_ir, ...partitions,
157
- * ...inputs]`, so `partitions` counts how many entries after the function IR
158
- * are partitioned datasets; the rest are ordinary (broadcast) inputs that
159
- * hash into every partition execution's identity.
160
- *
161
- * `by` and `combine` are carried as `encodeEastIR` bundles (capture-free IR +
162
- * its source map), not as FunctionType values or object-store hashes: IR is
163
- * how executable code travels everywhere on the e3 wire (`commandIr`,
164
- * `function_ir`, `bodyIr`), a FunctionType value could smuggle captures the
165
- * orchestrator must not evaluate, and inline bytes stay reachable where a
166
- * hash inside an opaque metadata blob would be invisible to GC.
167
- */
168
- export const PartitionTaskMetadataType = StructType({
169
- /** Number of partitioned inputs (wire input indices `1..1+partitions`). */
170
- partitions: IntegerType,
171
- /** `encodeEastIR` bundle of the boundary-alignment projection
172
- * `(Key) -> Projection`; `none` when partitioning is free per row/segment. */
173
- by: OptionType(BlobType),
174
- /** `encodeEastIR` bundle of the associative fold `(Out, Out) -> Out`;
175
- * `none` in splice mode (shards concatenate). */
176
- combine: OptionType(BlobType),
177
- /** Target carved-slice size in wire bytes — the only sizing knob. */
178
- targetPartitionBytes: IntegerType,
179
- /**
180
- * `encodeEastIR` bundle of the per-key merge `(Key, Value, Value) -> Value`
181
- * for a Dict output; `none` otherwise.
182
- *
183
- * Its presence (or {@link mergeSets}) selects the MERGE-TREE assembly:
184
- * partials whose key ranges overlap are merged by the task's own runner, in
185
- * a tree of merge executions ({@link mergeCommand}) folding equal keys with
186
- * this function; disjoint partials are spliced; the orchestrator never
187
- * decodes a partial. Appended after `targetPartitionBytes` (BEAST2 encodes
188
- * struct fields positionally) with a dual decoder — see
189
- * {@link decodePartitionTaskMetadata}.
190
- */
191
- merge: OptionType(BlobType),
192
- /** Whether a Set output assembles by the merge tree, keeping one of equal
193
- * elements (`--union`). The Set twin of {@link merge}, which needs no
194
- * function. */
195
- mergeSets: BooleanType,
196
- /**
197
- * `encodeEastIR` bundle of the merge command `(input_paths, output_path) ->
198
- * argv` — `mergeCommandIr` over the task's runner, in `function` mode with
199
- * {@link merge} and `union` mode with {@link mergeSets} — that the
200
- * orchestrator's merge units execute; `none` in splice and combine modes.
201
- * Built at export, so the merge unit task is a package object like any
202
- * other and nothing is synthesized at run time. Appended LAST with a
203
- * triple decoder — see {@link decodePartitionTaskMetadata}.
204
- */
205
- mergeCommand: OptionType(BlobType),
51
+ /** A dataset a task reads, and whether the work may be split over it. */
52
+ export const TaskInputType = StructType({
53
+ path: TreePathType,
54
+ partition: OptionType(TaskPartitionType),
206
55
  });
207
- export type PartitionTaskMetadataType = typeof PartitionTaskMetadataType;
208
-
209
- export type PartitionTaskMetadata = ValueTypeOf<typeof PartitionTaskMetadataType>;
210
-
211
- /**
212
- * The pre-`merge` partition metadata wire shape, kept only so
213
- * {@link decodePartitionTaskMetadata} can read tasks exported before the
214
- * segment-merge assembly existed.
215
- */
216
- const PreMergePartitionTaskMetadataType = StructType({
217
- partitions: IntegerType,
218
- by: OptionType(BlobType),
219
- combine: OptionType(BlobType),
220
- targetPartitionBytes: IntegerType,
56
+ export type TaskInputType = typeof TaskInputType;
57
+ export type TaskInput = ValueTypeOf<typeof TaskInputType>;
58
+
59
+ /**
60
+ * How a task's output is made, which fixes how the parts of it that units
61
+ * write combine.
62
+ *
63
+ * - `value`: the program returns it.
64
+ * - `array`: emitted elements, concatenated in emission order.
65
+ * - `set`: emitted elements, in any order, unioned.
66
+ * - `dict`: emitted entries, in any order; equal keys fold with `merge`, the
67
+ * hash of a `(K, V, V) -> V` IR bundle, and without it are refused.
68
+ * - `fold`: emitted values folded with `combine`, the hash of a `(T, T) -> T`
69
+ * IR bundle, starting at `zero`, the hash of a stored value.
70
+ */
71
+ export const TaskOutputKindType = VariantType({
72
+ value: NullType,
73
+ array: NullType,
74
+ set: NullType,
75
+ dict: StructType({ merge: OptionType(StringType) }),
76
+ fold: StructType({ zero: StringType, combine: StringType }),
221
77
  });
78
+ export type TaskOutputKindType = typeof TaskOutputKindType;
79
+ export type TaskOutputKind = ValueTypeOf<typeof TaskOutputKindType>;
222
80
 
223
- /**
224
- * The v1.0.77 partition metadata wire shape — `merge` and `mergeSets` but no
225
- * `mergeCommand` — kept only so {@link decodePartitionTaskMetadata} can read
226
- * tasks exported by that SDK; their merge units cannot run.
227
- */
228
- const PreMergeCommandPartitionTaskMetadataType = StructType({
229
- partitions: IntegerType,
230
- by: OptionType(BlobType),
231
- combine: OptionType(BlobType),
232
- targetPartitionBytes: IntegerType,
233
- merge: OptionType(BlobType),
234
- mergeSets: BooleanType,
81
+ /** Where a task's output goes, and how it is made. */
82
+ export const TaskOutputType = StructType({
83
+ path: TreePathType,
84
+ kind: TaskOutputKindType,
235
85
  });
236
-
237
- /** Encode a {@link PartitionTaskMetadataType} value for `TaskObject.metadata`. */
238
- export const encodePartitionTaskMetadata: (value: PartitionTaskMetadata) => Uint8Array =
239
- encodeBeast2For(PartitionTaskMetadataType);
240
-
241
- const decodeCurrentPartitionMetadata = decodeBeast2For(PartitionTaskMetadataType);
242
- const decodePreMergeCommandPartitionMetadata = decodeBeast2For(PreMergeCommandPartitionTaskMetadataType);
243
- const decodePreMergePartitionMetadata = decodeBeast2For(PreMergePartitionTaskMetadataType);
244
-
245
- /**
246
- * Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_PARTITION} task,
247
- * tolerating the two older wire formats: the v1.0.77 shape without
248
- * `mergeCommand`, and the pre-`merge` shape.
249
- *
250
- * @param data - the metadata blob
251
- * @returns the decoded metadata, with `mergeCommand` (and, for the oldest
252
- * bytes, `merge`/`mergeSets`) defaulted off
253
- */
254
- export function decodePartitionTaskMetadata(data: Uint8Array): PartitionTaskMetadata {
255
- try {
256
- return decodeCurrentPartitionMetadata(data);
257
- } catch (err) {
258
- try {
259
- return { ...decodePreMergeCommandPartitionMetadata(data), mergeCommand: none };
260
- } catch {
261
- try {
262
- return { ...decodePreMergePartitionMetadata(data), merge: none, mergeSets: false, mergeCommand: none };
263
- } catch {
264
- throw err; // no known shape — surface the current-format error
265
- }
266
- }
267
- }
268
- }
269
-
270
- /**
271
- * Metadata of a {@link TASK_KIND_STREAM} task.
272
- *
273
- * The task's wire `inputs` are laid out `[function_ir, merge_ir?, stream?,
274
- * ...inputs]` — `merge_ir` only in `function` merge mode; the compiled body
275
- * takes one trailing `emit` parameter beyond the wire inputs, and the runner
276
- * writes the `-o` file from the emit sink instead of the body's (Null) return
277
- * value.
278
- */
279
- export const StreamTaskMetadataType = StructType({
280
- /** Whether the first input after the IRs is the streamed input (producer
281
- * tasks have no streamed input). */
282
- stream: BooleanType,
283
- /** The output collection kind the emit sink writes: `"array"`, `"set"`, or
284
- * `"dict"` — element/key/value types come from the body IR's emit
285
- * parameter. */
286
- emit: StringType,
287
- /**
288
- * How the emit sink treats equal keys, a `StreamMergeMode`: `"none"` —
289
- * a duplicate key is an error; `"function"` — wire input 1 is the merge IR
290
- * `(K, V, V) -> V`, folding equal Dict keys; `"union"` — equal Set elements
291
- * collapse. Appended LAST (BEAST2 encodes struct fields positionally) with a
292
- * dual decoder — see {@link decodeStreamTaskMetadata}.
293
- */
294
- merge: StringType,
86
+ export type TaskOutputType = typeof TaskOutputType;
87
+ export type TaskOutput = ValueTypeOf<typeof TaskOutputType>;
88
+
89
+ /**
90
+ * What a ui task binds as it renders.
91
+ *
92
+ * @property paths - Dataset paths it reads or writes through `Data.bind`,
93
+ * including each bound record's own path, so the record's current value is
94
+ * preloaded and polled like any dataset.
95
+ * @property functions - Package functions it calls through `Func.bind`.
96
+ * @property records - Records it binds through `Record.bind`.
97
+ * @property pages - Dataset paths it reads a window at a time through
98
+ * `Data.bindPaged`: declared apart from `paths` because they are never
99
+ * preloaded or polled whole.
100
+ */
101
+ export const DataManifestType = StructType({
102
+ paths: ArrayType(TreePathType),
103
+ functions: ArrayType(StringType),
104
+ records: ArrayType(StringType),
105
+ pages: ArrayType(TreePathType),
295
106
  });
296
- export type StreamTaskMetadataType = typeof StreamTaskMetadataType;
297
-
298
- export type StreamTaskMetadata = ValueTypeOf<typeof StreamTaskMetadataType>;
107
+ export type DataManifestType = typeof DataManifestType;
108
+ export type DataManifest = ValueTypeOf<typeof DataManifestType>;
299
109
 
300
110
  /**
301
- * The pre-`merge` stream metadata wire shape, kept only so
302
- * {@link decodeStreamTaskMetadata} can read tasks exported before folding
303
- * emit sinks existed.
111
+ * What a task's output is for: `data`, or a `ui` whose output is a component
112
+ * tree rendered against the datasets, functions and records it binds.
304
113
  */
305
- const PreMergeStreamTaskMetadataType = StructType({
306
- stream: BooleanType,
307
- emit: StringType,
114
+ export const TaskRoleType = VariantType({
115
+ data: NullType,
116
+ ui: DataManifestType,
308
117
  });
309
-
310
- /** Encode a {@link StreamTaskMetadataType} value for `TaskObject.metadata`. */
311
- export const encodeStreamTaskMetadata: (value: StreamTaskMetadata) => Uint8Array =
312
- encodeBeast2For(StreamTaskMetadataType);
313
-
314
- const decodeCurrentStreamMetadata = decodeBeast2For(StreamTaskMetadataType);
315
- const decodePreMergeStreamMetadata = decodeBeast2For(PreMergeStreamTaskMetadataType);
316
-
317
- /**
318
- * Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_STREAM} task,
319
- * tolerating the pre-`merge` wire format (dual-decode migration).
320
- *
321
- * @param data - the metadata blob
322
- * @returns the decoded metadata, with `merge` defaulted to `"none"` for older
323
- * bytes
324
- */
325
- export function decodeStreamTaskMetadata(data: Uint8Array): StreamTaskMetadata {
326
- try {
327
- return decodeCurrentStreamMetadata(data);
328
- } catch (err) {
329
- try {
330
- return { ...decodePreMergeStreamMetadata(data), merge: 'none' };
331
- } catch {
332
- throw err; // no known shape — surface the current-format error
333
- }
334
- }
335
- }
336
-
337
- // =============================================================================
338
- // Partition plan
339
- // =============================================================================
340
-
341
- /** A carve position: a segment index and an element offset within it. */
342
- const SplitPointType = StructType({ seg: IntegerType, offset: IntegerType });
118
+ export type TaskRoleType = typeof TaskRoleType;
119
+ export type TaskRole = ValueTypeOf<typeof TaskRoleType>;
343
120
 
344
121
  /**
345
- * The plan of one merged component's fan-in (issue #770): the partials whose
346
- * key ranges overlap, and the key ranges its merge units run over, as the
347
- * hashes of the range blobs the units take as their input.
122
+ * Task object stored in the object store.
348
123
  *
349
124
  * @remarks
350
- * The ranges are a pure function of the partials and the task's
351
- * `targetPartitionBytes` — the boundary keys are fences of the largest
352
- * partial — so a re-run whose partials are the same objects plans the same
353
- * ranges; recording them lets it skip the planning probes, and because
354
- * every unit's inputs are then the same objects, every merge unit
355
- * cache-hits.
356
- */
357
- export const MergeRangePlanType = StructType({
358
- /** The component's partial hashes, in partition order. */
359
- partials: ArrayType(StringType),
360
- /** The hashes of the component's range blobs — `Struct{from: Option<K>,
361
- * to: Option<K>}` over the output's key type — in key order; one open
362
- * range for a component that merges whole. */
363
- ranges: ArrayType(StringType),
364
- });
365
- export type MergeRangePlanType = typeof MergeRangePlanType;
366
-
367
- export type MergeRangePlan = ValueTypeOf<typeof MergeRangePlanType>;
368
-
369
- /**
370
- * The plan of a partitioned execution (issue #770): the partitioned inputs,
371
- * where each partition starts in every one of them, the carved slices, and
372
- * the ranged fan-in of each merged component.
125
+ * A package-borne wire, so it changes by hard cutover: a package exported by
126
+ * an older SDK is re-exported, and {@link decodeTaskObject} says so.
373
127
  *
374
- * @remarks
375
- * The step interpreter writes it to the object store as the run goes —
376
- * after the map step with the carved slices, and again after the reduce
377
- * step has planned each component's merge ranges — and points the `plan`
378
- * sidecar of the execution's `(taskHash, inputsHash)` directory at it, so a
379
- * re-plan or a resume that computes the same `partitions`/`boundaries`/
380
- * `splits` reuses the slices instead of carving them again, partition by
381
- * partition, and a component of the same partials reuses its ranges.
382
- */
383
- export const PartitionPlanType = StructType({
384
- /** Partitioned input hashes, wire order. */
385
- partitions: ArrayType(StringType),
386
- /** First segment index of each partition of the primary; boundaries[0] = 0. */
387
- boundaries: ArrayType(IntegerType),
388
- /** Per secondary (partitions[1..]): the split point of every partition plus the end; length boundaries.length + 1. */
389
- splits: ArrayType(ArrayType(SplitPointType)),
390
- /** slices[input][partition] object hashes; empty until carved. */
391
- slices: ArrayType(ArrayType(StringType)),
392
- /** The ranged fan-in of each merged component, once its ranges are
393
- * planned. Appended LAST (BEAST2 encodes struct fields positionally) with
394
- * a dual decoder — see {@link decodePartitionPlan}. */
395
- merges: ArrayType(MergeRangePlanType),
396
- });
397
- export type PartitionPlanType = typeof PartitionPlanType;
398
-
399
- export type PartitionPlan = ValueTypeOf<typeof PartitionPlanType>;
400
-
401
- /**
402
- * The pre-`merges` plan wire shape, kept only so {@link decodePartitionPlan}
403
- * can read plans a run recorded before the fan-in ran per key range.
128
+ * @example
129
+ * ```ts
130
+ * import { none, variant } from '@elaraai/east';
131
+ *
132
+ * const task: TaskObject = {
133
+ * kind: TASK_OBJECT_KIND,
134
+ * body: variant('east', { program: '5e7a3b...' }),
135
+ * runner: variant('east_node', { platforms: ['@elaraai/east-node-std'] }),
136
+ * inputs: [{ path: [variant('field', 'inputs'), variant('field', 'sales')], partition: none }],
137
+ * output: { path: [variant('field', 'tasks'), variant('field', 'totals'), variant('field', 'output')], kind: variant('value', null) },
138
+ * role: variant('data', null),
139
+ * environment: none,
140
+ * };
141
+ * ```
404
142
  */
405
- const PreMergesPartitionPlanType = StructType({
406
- partitions: ArrayType(StringType),
407
- boundaries: ArrayType(IntegerType),
408
- splits: ArrayType(ArrayType(SplitPointType)),
409
- slices: ArrayType(ArrayType(StringType)),
143
+ export const TaskObjectType = StructType({
144
+ /** Always {@link TASK_OBJECT_KIND}. */
145
+ kind: StringType,
146
+ /** What the task runs. */
147
+ body: TaskBodyType,
148
+ /** The runtime it runs on. A `custom` runner's command is informational:
149
+ * the body's command IR is what spawns. */
150
+ runner: RunnerType,
151
+ /** The datasets it reads, in the order its program takes them. */
152
+ inputs: ArrayType(TaskInputType),
153
+ /** Where its output goes, and how it is made. */
154
+ output: TaskOutputType,
155
+ /** What its output is for. */
156
+ role: TaskRoleType,
157
+ /** Hash of the {@link EnvironmentSpecType} object it executes in; `none`
158
+ * for the stock runtime image. */
159
+ environment: OptionType(StringType),
410
160
  });
161
+ export type TaskObjectType = typeof TaskObjectType;
411
162
 
412
- /** Encode a {@link PartitionPlanType} value for the object store. */
413
- export const encodePartitionPlan: (value: PartitionPlan) => Uint8Array =
414
- encodeBeast2For(PartitionPlanType);
163
+ export type TaskObject = ValueTypeOf<typeof TaskObjectType>;
415
164
 
416
- const decodeCurrentPlan = decodeBeast2For(PartitionPlanType);
417
- const decodePreMergesPlan = decodeBeast2For(PreMergesPartitionPlanType);
165
+ const decodeCurrentTask = decodeBeast2For(TaskObjectType);
418
166
 
419
167
  /**
420
- * Decode a {@link PartitionPlanType} object, tolerating the pre-`merges`
421
- * wire shape (dual-decode migration): an older plan decodes with no recorded
422
- * merge ranges, so its partition slices are reused and its ranges planned.
168
+ * Decode a `TaskObject` from BEAST2 bytes.
423
169
  *
424
- * @param data - the plan object's bytes
425
- * @returns the decoded plan
170
+ * @param data - the stored bytes
171
+ * @returns the task object
172
+ * @throws {Error} When the bytes are not a current task object — a package
173
+ * exported by an older SDK, which is re-exported with the current one.
426
174
  */
427
- export function decodePartitionPlan(data: Uint8Array): PartitionPlan {
175
+ export function decodeTaskObject(data: Uint8Array): TaskObject {
176
+ let task: TaskObject;
428
177
  try {
429
- return decodeCurrentPlan(data);
178
+ task = decodeCurrentTask(data);
430
179
  } catch (err) {
431
- try {
432
- return { ...decodePreMergesPlan(data), merges: [] };
433
- } catch {
434
- throw err; // no known shape — surface the current-format error
435
- }
436
- }
437
- }
438
-
439
- // =============================================================================
440
- // Partition `by` projections
441
- // =============================================================================
442
-
443
- /**
444
- * The shape a partition task's `by` projection reads, as extracted from its
445
- * IR by {@link partitionProjectionShape}.
446
- *
447
- * - `fields`: the identity (`names: []`), or a leading prefix of the key's
448
- * top-level fields — one field, or a struct literal of fields in declared
449
- * order. Validated against each dataset's top-level key field order.
450
- * - `path`: a nested leading-field path (`key.a.b`, two or more steps).
451
- * Validated per step: each must read the FIRST field of its level's
452
- * struct, which is what keeps the projection monotone in canonical key
453
- * order.
454
- */
455
- export interface ProjectionShape {
456
- /** Top-level fields (or the identity), or a nested first-field path. */
457
- kind: 'fields' | 'path';
458
- /** The fields read, in order; empty for the identity. */
459
- names: string[];
460
- }
461
-
462
- /**
463
- * Extracts the shape a `by` projection reads — see {@link ProjectionShape} —
464
- * or `null` for any other (unaccepted) shape.
465
- *
466
- * @param ir - the projection's function IR, `(Key) -> Projection`
467
- * @returns the shape, or `null` when the body is not an accepted projection
468
- */
469
- export function partitionProjectionShape(ir: FunctionIR): ProjectionShape | null {
470
- const param = ir.value.parameters[0]?.value.name;
471
- if (param === undefined) return null;
472
-
473
- // Chase wrappers to the expression the body evaluates to.
474
- const unwrap = (node: any): any => {
475
- for (;;) {
476
- if (node.type === 'Block') {
477
- const statements = node.value.statements;
478
- if (statements.length === 0) return node;
479
- node = statements[statements.length - 1];
480
- } else if (node.type === 'Return') {
481
- node = node.value.value;
482
- } else if (node.type === 'As' || node.type === 'WrapRecursive' || node.type === 'UnwrapRecursive') {
483
- node = node.value.value;
484
- } else {
485
- return node;
486
- }
487
- }
488
- };
489
-
490
- const isParam = (node: any): boolean => node.type === 'Variable' && node.value.name === param;
491
- const fieldOf = (node: any): string | null =>
492
- node.type === 'GetField' && isParam(unwrap(node.value.struct)) ? node.value.field as string : null;
493
- // Walks a GetField chain down to the parameter: `key.a.b` → ['a', 'b'].
494
- const pathOf = (node: any): string[] | null => {
495
- const path: string[] = [];
496
- let cur = node;
497
- while (cur.type === 'GetField') {
498
- path.unshift(cur.value.field as string);
499
- cur = unwrap(cur.value.struct);
500
- }
501
- return isParam(cur) && path.length > 0 ? path : null;
502
- };
503
-
504
- const body = unwrap(ir.value.body as any);
505
- if (isParam(body)) return { kind: 'fields', names: [] };
506
- const chain = pathOf(body);
507
- if (chain !== null) {
508
- // A one-step chain is a top-level field read — the `fields` family.
509
- return chain.length === 1 ? { kind: 'fields', names: chain } : { kind: 'path', names: chain };
510
- }
511
- if (body.type === 'Struct') {
512
- const fields: string[] = [];
513
- for (const { value } of body.value.fields) {
514
- const f = fieldOf(unwrap(value));
515
- if (f === null) return null;
516
- fields.push(f);
517
- }
518
- return { kind: 'fields', names: fields };
180
+ throw new Error(
181
+ `the task object does not decode: the package was exported by an older e3 SDK — re-export it with the current one ` +
182
+ `(${err instanceof Error ? err.message : String(err)})`,
183
+ );
519
184
  }
520
- return null;
521
- }
522
-
523
- /**
524
- * The East type of a projected key — the type {@link projectKey} values have,
525
- * and the one to build their comparator for: the key type itself for the
526
- * identity, a struct of the named leading fields in declared order, or the
527
- * type at the end of a nested first-field path.
528
- *
529
- * @param shape - the projection's shape
530
- * @param keyType - the partitioned dataset's key type
531
- * @returns the projected key type
532
- * @throws {Error} When the shape reads a field the key type does not have.
533
- *
534
- * @example
535
- * ```ts
536
- * const cmp = compareFor(projectedKeyType(shape, keyTypeValue));
537
- * cmp(projectKey(shape, a), projectKey(shape, b));
538
- * ```
539
- */
540
- export function projectedKeyType(shape: ProjectionShape, keyType: EastTypeValue): EastTypeValue {
541
- if (shape.names.length === 0) return keyType;
542
- const fieldType = (level: EastTypeValue, name: string): EastTypeValue => {
543
- const field = level.type === 'Struct' ? level.value.find((f) => f.name === name) : undefined;
544
- if (field === undefined) {
545
- throw new Error(`partition projection reads field '${name}' of a key level that has no such field`);
546
- }
547
- return field.type as EastTypeValue;
548
- };
549
- if (shape.kind === 'path') return shape.names.reduce(fieldType, keyType);
550
- return variant('Struct', shape.names.map((name) => ({ name, type: fieldType(keyType, name) })));
551
- }
552
-
553
- /**
554
- * Projects a partition key through a `by` shape, without compiling the
555
- * projection: the key itself for the identity, a struct of the named leading
556
- * fields built field by field in declared order, or the value at the end of a
557
- * nested first-field path.
558
- *
559
- * @param shape - the projection's shape
560
- * @param key - a key of the partitioned dataset
561
- * @returns the projected key, a value of {@link projectedKeyType}
562
- */
563
- export function projectKey(shape: ProjectionShape, key: unknown): unknown {
564
- if (shape.names.length === 0) return key;
565
- if (shape.kind === 'path') {
566
- return shape.names.reduce((level, name) => (level as Record<string, unknown>)[name], key);
185
+ if (task.kind !== TASK_OBJECT_KIND) {
186
+ throw new Error(`the object is not a task object: its kind is '${task.kind}', not '${TASK_OBJECT_KIND}'`);
567
187
  }
568
- const projected: Record<string, unknown> = {};
569
- for (const name of shape.names) projected[name] = (key as Record<string, unknown>)[name];
570
- return projected;
188
+ return task;
571
189
  }