@elaraai/e3-types 1.0.77 → 1.0.79

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 (41) hide show
  1. package/dist/src/dataflow.d.ts +10 -6
  2. package/dist/src/dataflow.d.ts.map +1 -1
  3. package/dist/src/dataflow.js.map +1 -1
  4. package/dist/src/execution.d.ts +17 -0
  5. package/dist/src/execution.d.ts.map +1 -1
  6. package/dist/src/index.d.ts +4 -3
  7. package/dist/src/index.d.ts.map +1 -1
  8. package/dist/src/index.js +4 -2
  9. package/dist/src/index.js.map +1 -1
  10. package/dist/src/runner.d.ts +35 -9
  11. package/dist/src/runner.d.ts.map +1 -1
  12. package/dist/src/runner.js +46 -13
  13. package/dist/src/runner.js.map +1 -1
  14. package/dist/src/runner.spec.js +50 -1
  15. package/dist/src/runner.spec.js.map +1 -1
  16. package/dist/src/stream.d.ts +97 -0
  17. package/dist/src/stream.d.ts.map +1 -0
  18. package/dist/src/stream.js +134 -0
  19. package/dist/src/stream.js.map +1 -0
  20. package/dist/src/stream.spec.d.ts +6 -0
  21. package/dist/src/stream.spec.d.ts.map +1 -0
  22. package/dist/src/stream.spec.js +60 -0
  23. package/dist/src/stream.spec.js.map +1 -0
  24. package/dist/src/task.d.ts +183 -17
  25. package/dist/src/task.d.ts.map +1 -1
  26. package/dist/src/task.js +302 -20
  27. package/dist/src/task.js.map +1 -1
  28. package/dist/src/task.spec.d.ts +6 -0
  29. package/dist/src/task.spec.d.ts.map +1 -0
  30. package/dist/src/task.spec.js +169 -0
  31. package/dist/src/task.spec.js.map +1 -0
  32. package/package.json +2 -2
  33. package/src/dataflow.ts +10 -6
  34. package/src/execution.ts +18 -0
  35. package/src/index.ts +21 -0
  36. package/src/runner.spec.ts +56 -1
  37. package/src/runner.ts +45 -13
  38. package/src/stream.spec.ts +88 -0
  39. package/src/stream.ts +170 -0
  40. package/src/task.spec.ts +208 -0
  41. package/src/task.ts +336 -21
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+
6
+ /**
7
+ * Partition and stream task wire helpers (issue #770): the partition
8
+ * metadata's merge fields and its triple decoder, the stream metadata's
9
+ * `merge` mode and its dual decoder, the partition plan round trip, and the
10
+ * `by` projection shapes the orchestrator evaluates by reading key fields
11
+ * instead of compiling the projection.
12
+ */
13
+
14
+ import { describe, it } from 'node:test';
15
+ import assert from 'node:assert/strict';
16
+ import { ArrayType, BlobType, BooleanType, East, IntegerType, OptionType, StringType, StructType, compareFor, encodeBeast2For, encodeEastIR, isTypeValueEqual, none, some, toEastTypeValue, variant } from '@elaraai/east';
17
+ import type { FunctionIR } from '@elaraai/east';
18
+ import {
19
+ decodePartitionPlan,
20
+ decodePartitionTaskMetadata,
21
+ decodeStreamTaskMetadata,
22
+ encodePartitionPlan,
23
+ encodePartitionTaskMetadata,
24
+ encodeStreamTaskMetadata,
25
+ partitionProjectionShape,
26
+ projectKey,
27
+ projectedKeyType,
28
+ type PartitionPlan,
29
+ type PartitionTaskMetadata,
30
+ type ProjectionShape,
31
+ type StreamTaskMetadata,
32
+ } from './task.js';
33
+ import { mergeCommandIr } from './stream.js';
34
+
35
+ describe('PartitionTaskMetadataType', () => {
36
+ const mergeIr = encodeEastIR(East.function([IntegerType, StringType, StringType], StringType, (_$, _key, acc, value) => acc.concat(value)).toIR());
37
+ const mergeCommand = encodeEastIR(mergeCommandIr(variant('east_c', { platforms: ['east-c-std'] }), 'function'));
38
+
39
+ /** The metadata with every blob as a plain Uint8Array — the decoder hands
40
+ * back Buffers, whose prototype strict deep equality would reject. */
41
+ function plain(meta: PartitionTaskMetadata): PartitionTaskMetadata {
42
+ const blob = (option: { type: string; value: unknown }) =>
43
+ option.type === 'some' ? some(new Uint8Array(option.value as Uint8Array)) : none;
44
+ return { ...meta, by: blob(meta.by), combine: blob(meta.combine), merge: blob(meta.merge), mergeCommand: blob(meta.mergeCommand) };
45
+ }
46
+
47
+ it('round-trips the merge fields and the merge command', () => {
48
+ const meta: PartitionTaskMetadata = {
49
+ partitions: 1n, by: none, combine: none, targetPartitionBytes: 1024n,
50
+ merge: some(mergeIr), mergeSets: false, mergeCommand: some(mergeCommand),
51
+ };
52
+ assert.deepEqual(plain(decodePartitionTaskMetadata(encodePartitionTaskMetadata(meta))), meta);
53
+ });
54
+
55
+ it('decodes v1.0.77 metadata, exported before merge commands, with mergeCommand none', () => {
56
+ const shape = StructType({
57
+ partitions: IntegerType, by: OptionType(BlobType), combine: OptionType(BlobType), targetPartitionBytes: IntegerType,
58
+ merge: OptionType(BlobType), mergeSets: BooleanType,
59
+ });
60
+ const legacy = encodeBeast2For(shape)({ partitions: 2n, by: none, combine: none, targetPartitionBytes: 512n, merge: some(mergeIr), mergeSets: false });
61
+ assert.deepEqual(plain(decodePartitionTaskMetadata(legacy)), {
62
+ partitions: 2n, by: none, combine: none, targetPartitionBytes: 512n, merge: some(mergeIr), mergeSets: false, mergeCommand: none,
63
+ });
64
+ });
65
+
66
+ it('decodes metadata exported before merge assembly with every merge field off', () => {
67
+ const shape = StructType({ partitions: IntegerType, by: OptionType(BlobType), combine: OptionType(BlobType), targetPartitionBytes: IntegerType });
68
+ const legacy = encodeBeast2For(shape)({ partitions: 1n, by: none, combine: none, targetPartitionBytes: 256n });
69
+ assert.deepEqual(decodePartitionTaskMetadata(legacy), {
70
+ partitions: 1n, by: none, combine: none, targetPartitionBytes: 256n, merge: none, mergeSets: false, mergeCommand: none,
71
+ });
72
+ });
73
+
74
+ it('throws for bytes of no known metadata shape', () => {
75
+ assert.throws(() => decodePartitionTaskMetadata(encodeBeast2For(IntegerType)(7n)));
76
+ });
77
+ });
78
+
79
+ describe('StreamTaskMetadataType', () => {
80
+ it('round-trips the merge mode', () => {
81
+ const meta: StreamTaskMetadata = { stream: true, emit: 'dict', merge: 'function' };
82
+ assert.deepEqual(decodeStreamTaskMetadata(encodeStreamTaskMetadata(meta)), meta);
83
+ });
84
+
85
+ it('decodes metadata exported before merge modes as merge none', () => {
86
+ const legacy = encodeBeast2For(StructType({ stream: BooleanType, emit: StringType }))({ stream: false, emit: 'set' });
87
+ assert.deepEqual(decodeStreamTaskMetadata(legacy), { stream: false, emit: 'set', merge: 'none' });
88
+ });
89
+
90
+ it('throws for bytes of no known metadata shape', () => {
91
+ assert.throws(() => decodeStreamTaskMetadata(encodeBeast2For(IntegerType)(7n)));
92
+ });
93
+ });
94
+
95
+ describe('PartitionPlanType', () => {
96
+ it('round-trips a plan, before and after its slices are carved and its merge ranges planned', () => {
97
+ const plan: PartitionPlan = {
98
+ partitions: ['a'.repeat(64), 'b'.repeat(64)],
99
+ boundaries: [0n, 4n, 9n],
100
+ splits: [[{ seg: 0n, offset: 0n }, { seg: 3n, offset: 17n }, { seg: 8n, offset: 0n }, { seg: 12n, offset: 0n }]],
101
+ slices: [],
102
+ merges: [],
103
+ };
104
+ assert.deepEqual(decodePartitionPlan(encodePartitionPlan(plan)), plan);
105
+
106
+ const carved: PartitionPlan = { ...plan, slices: [['c'.repeat(64), 'd'.repeat(64), 'e'.repeat(64)], ['f'.repeat(64), '0'.repeat(64), '1'.repeat(64)]] };
107
+ assert.deepEqual(decodePartitionPlan(encodePartitionPlan(carved)), carved);
108
+
109
+ const merged: PartitionPlan = {
110
+ ...carved,
111
+ merges: [{ partials: ['2'.repeat(64), '3'.repeat(64)], ranges: ['4'.repeat(64), '5'.repeat(64)] }],
112
+ };
113
+ assert.deepEqual(decodePartitionPlan(encodePartitionPlan(merged)), merged);
114
+ });
115
+
116
+ it('decodes a plan recorded before merge ranges existed, with none', () => {
117
+ // The pre-`merges` wire shape, encoded directly.
118
+ const PreMergesPlanType = StructType({
119
+ partitions: ArrayType(StringType),
120
+ boundaries: ArrayType(IntegerType),
121
+ splits: ArrayType(ArrayType(StructType({ seg: IntegerType, offset: IntegerType }))),
122
+ slices: ArrayType(ArrayType(StringType)),
123
+ });
124
+ const older = encodeBeast2For(PreMergesPlanType)({
125
+ partitions: ['a'.repeat(64)],
126
+ boundaries: [0n, 2n],
127
+ splits: [],
128
+ slices: [['b'.repeat(64), 'c'.repeat(64)]],
129
+ });
130
+ assert.deepEqual(decodePartitionPlan(older), {
131
+ partitions: ['a'.repeat(64)],
132
+ boundaries: [0n, 2n],
133
+ splits: [],
134
+ slices: [['b'.repeat(64), 'c'.repeat(64)]],
135
+ merges: [],
136
+ });
137
+ });
138
+ });
139
+
140
+ describe('partition projections', () => {
141
+ const AtType = StructType({ day: IntegerType, hour: IntegerType });
142
+ const KeyType = StructType({ region: StringType, at: AtType, sku: StringType });
143
+ const keyTypeValue = toEastTypeValue(KeyType);
144
+ type Key = { region: string; at: { day: bigint; hour: bigint }; sku: string };
145
+ const keys: Key[] = [
146
+ { region: 'eu', at: { day: 2n, hour: 7n }, sku: 'b' },
147
+ { region: 'eu', at: { day: 2n, hour: 7n }, sku: 'a' },
148
+ { region: 'eu', at: { day: 1n, hour: 9n }, sku: 'c' },
149
+ { region: 'ap', at: { day: 3n, hour: 0n }, sku: 'a' },
150
+ ];
151
+
152
+ /** A `by` projection over the key, compiled, and its shape. */
153
+ function projection(by: ($: any, key: any) => any): { compiled: (key: Key) => unknown; shape: ProjectionShape | null } {
154
+ const bundle = East.function([KeyType], undefined, by).toIR();
155
+ return {
156
+ compiled: bundle.compile([]) as (key: Key) => unknown,
157
+ shape: partitionProjectionShape(bundle.ir as FunctionIR),
158
+ };
159
+ }
160
+
161
+ /** Whether two comparisons agree in sign. */
162
+ const sameSign = (a: number, b: number): boolean => Math.sign(a) === Math.sign(b);
163
+
164
+ it('reads the identity, a leading field, a struct literal of leading fields and a nested first-field path', () => {
165
+ assert.deepEqual(projection(($, key) => key).shape, { kind: 'fields', names: [] });
166
+ assert.deepEqual(projection(($, key) => key.region).shape, { kind: 'fields', names: ['region'] });
167
+ assert.deepEqual(projection(($, key) => ({ region: key.region, at: key.at })).shape, { kind: 'fields', names: ['region', 'at'] });
168
+ assert.deepEqual(projection(($, key) => key.at.day).shape, { kind: 'path', names: ['at', 'day'] });
169
+ });
170
+
171
+ it('returns null for a projection that computes', () => {
172
+ assert.equal(projection(($, key) => key.region.concat('-')).shape, null);
173
+ });
174
+
175
+ it('projects the identity to the key itself', () => {
176
+ const shape: ProjectionShape = { kind: 'fields', names: [] };
177
+ assert.ok(isTypeValueEqual(projectedKeyType(shape, keyTypeValue), keyTypeValue));
178
+ assert.equal(projectKey(shape, keys[0]), keys[0]);
179
+ });
180
+
181
+ it('projects a struct literal of leading fields exactly as the compiled projection does', () => {
182
+ const { compiled, shape } = projection(($, key) => ({ region: key.region, at: key.at }));
183
+ assert.ok(isTypeValueEqual(projectedKeyType(shape!, keyTypeValue), toEastTypeValue(StructType({ region: StringType, at: AtType }))));
184
+ for (const key of keys) assert.deepEqual(projectKey(shape!, key), compiled(key));
185
+ });
186
+
187
+ it('projects a nested first-field path exactly as the compiled projection does', () => {
188
+ const { compiled, shape } = projection(($, key) => key.at.day);
189
+ assert.ok(isTypeValueEqual(projectedKeyType(shape!, keyTypeValue), toEastTypeValue(IntegerType)));
190
+ for (const key of keys) assert.equal(projectKey(shape!, key), compiled(key));
191
+ });
192
+
193
+ it('orders a single leading field as the compiled projection does', () => {
194
+ const { compiled, shape } = projection(($, key) => key.region);
195
+ const cmp = compareFor(projectedKeyType(shape!, keyTypeValue) as any) as (a: unknown, b: unknown) => number;
196
+ const cmpCompiled = compareFor(StringType);
197
+ for (const a of keys) {
198
+ for (const b of keys) {
199
+ assert.ok(sameSign(cmp(projectKey(shape!, a), projectKey(shape!, b)), cmpCompiled(compiled(a) as string, compiled(b) as string)));
200
+ }
201
+ }
202
+ });
203
+
204
+ it('throws when a shape reads a field the key type does not have', () => {
205
+ assert.throws(() => projectedKeyType({ kind: 'fields', names: ['store'] }, keyTypeValue), /field 'store'/);
206
+ assert.throws(() => projectedKeyType({ kind: 'path', names: ['at', 'minute'] }, keyTypeValue), /field 'minute'/);
207
+ });
208
+ });
package/src/task.ts CHANGED
@@ -16,7 +16,8 @@
16
16
  * specified paths - the task just references locations, not types.
17
17
  */
18
18
 
19
- import { StructType, StringType, ArrayType, BlobType, BooleanType, IntegerType, OptionType, ValueTypeOf, decodeBeast2For, encodeBeast2For, none } from '@elaraai/east';
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';
20
21
  import { TreePathType } from './structure.js';
21
22
  import { RunnerType } from './runner.js';
22
23
 
@@ -141,6 +142,14 @@ export const TASK_KIND_PARTITION = 'partition';
141
142
  * beast2-encoded {@link StreamTaskMetadataType}. */
142
143
  export const TASK_KIND_STREAM = 'stream';
143
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
+
144
153
  /**
145
154
  * Metadata of a {@link TASK_KIND_PARTITION} task.
146
155
  *
@@ -171,17 +180,29 @@ export const PartitionTaskMetadataType = StructType({
171
180
  * `encodeEastIR` bundle of the per-key merge `(Key, Value, Value) -> Value`
172
181
  * for a Dict output; `none` otherwise.
173
182
  *
174
- * Its presence (or {@link mergeSets}) selects the SEGMENT-MERGE assembly:
175
- * partials are walked by their segment fences in the orchestrator, disjoint
176
- * segments are byte-copied exactly as a splice does, and only overlapping
177
- * ones are decoded, merged and re-encoded. Appended LAST (BEAST2 encodes
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
178
188
  * struct fields positionally) with a dual decoder — see
179
189
  * {@link decodePartitionTaskMetadata}.
180
190
  */
181
191
  merge: OptionType(BlobType),
182
- /** Whether a Set output assembles by segment merge (union). The Set twin of
183
- * {@link merge}, which needs no function. Appended LAST. */
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. */
184
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),
185
206
  });
186
207
  export type PartitionTaskMetadataType = typeof PartitionTaskMetadataType;
187
208
 
@@ -199,29 +220,49 @@ const PreMergePartitionTaskMetadataType = StructType({
199
220
  targetPartitionBytes: IntegerType,
200
221
  });
201
222
 
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,
235
+ });
236
+
202
237
  /** Encode a {@link PartitionTaskMetadataType} value for `TaskObject.metadata`. */
203
238
  export const encodePartitionTaskMetadata: (value: PartitionTaskMetadata) => Uint8Array =
204
239
  encodeBeast2For(PartitionTaskMetadataType);
205
240
 
206
241
  const decodeCurrentPartitionMetadata = decodeBeast2For(PartitionTaskMetadataType);
242
+ const decodePreMergeCommandPartitionMetadata = decodeBeast2For(PreMergeCommandPartitionTaskMetadataType);
207
243
  const decodePreMergePartitionMetadata = decodeBeast2For(PreMergePartitionTaskMetadataType);
208
244
 
209
245
  /**
210
246
  * Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_PARTITION} task,
211
- * tolerating the pre-`merge` wire format (dual-decode migration).
247
+ * tolerating the two older wire formats: the v1.0.77 shape without
248
+ * `mergeCommand`, and the pre-`merge` shape.
212
249
  *
213
250
  * @param data - the metadata blob
214
- * @returns the decoded metadata, with `merge`/`mergeSets` defaulted off for
215
- * older bytes
251
+ * @returns the decoded metadata, with `mergeCommand` (and, for the oldest
252
+ * bytes, `merge`/`mergeSets`) defaulted off
216
253
  */
217
254
  export function decodePartitionTaskMetadata(data: Uint8Array): PartitionTaskMetadata {
218
255
  try {
219
256
  return decodeCurrentPartitionMetadata(data);
220
257
  } catch (err) {
221
258
  try {
222
- return { ...decodePreMergePartitionMetadata(data), merge: none, mergeSets: false };
259
+ return { ...decodePreMergeCommandPartitionMetadata(data), mergeCommand: none };
223
260
  } catch {
224
- throw err; // no known shape — surface the current-format error
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
+ }
225
266
  }
226
267
  }
227
268
  }
@@ -229,28 +270,302 @@ export function decodePartitionTaskMetadata(data: Uint8Array): PartitionTaskMeta
229
270
  /**
230
271
  * Metadata of a {@link TASK_KIND_STREAM} task.
231
272
  *
232
- * The task's wire `inputs` are laid out `[function_ir, stream?, ...inputs]`;
233
- * the compiled body takes one trailing `emit` parameter beyond the wire
234
- * inputs, and the runner writes the `-o` file from the emit sink instead of
235
- * the body's (Null) return value.
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.
236
278
  */
237
279
  export const StreamTaskMetadataType = StructType({
238
- /** Whether wire input index 1 is the streamed input (producer tasks have
239
- * no streamed input). */
280
+ /** Whether the first input after the IRs is the streamed input (producer
281
+ * tasks have no streamed input). */
240
282
  stream: BooleanType,
241
283
  /** The output collection kind the emit sink writes: `"array"`, `"set"`, or
242
284
  * `"dict"` — element/key/value types come from the body IR's emit
243
285
  * parameter. */
244
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,
245
295
  });
246
296
  export type StreamTaskMetadataType = typeof StreamTaskMetadataType;
247
297
 
248
298
  export type StreamTaskMetadata = ValueTypeOf<typeof StreamTaskMetadataType>;
249
299
 
300
+ /**
301
+ * The pre-`merge` stream metadata wire shape, kept only so
302
+ * {@link decodeStreamTaskMetadata} can read tasks exported before folding
303
+ * emit sinks existed.
304
+ */
305
+ const PreMergeStreamTaskMetadataType = StructType({
306
+ stream: BooleanType,
307
+ emit: StringType,
308
+ });
309
+
250
310
  /** Encode a {@link StreamTaskMetadataType} value for `TaskObject.metadata`. */
251
311
  export const encodeStreamTaskMetadata: (value: StreamTaskMetadata) => Uint8Array =
252
312
  encodeBeast2For(StreamTaskMetadataType);
253
313
 
254
- /** Decode a `TaskObject.metadata` blob of a {@link TASK_KIND_STREAM} task. */
255
- export const decodeStreamTaskMetadata: (data: Uint8Array) => StreamTaskMetadata =
256
- decodeBeast2For(StreamTaskMetadataType);
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 });
343
+
344
+ /**
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.
348
+ *
349
+ * @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.
373
+ *
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.
404
+ */
405
+ const PreMergesPartitionPlanType = StructType({
406
+ partitions: ArrayType(StringType),
407
+ boundaries: ArrayType(IntegerType),
408
+ splits: ArrayType(ArrayType(SplitPointType)),
409
+ slices: ArrayType(ArrayType(StringType)),
410
+ });
411
+
412
+ /** Encode a {@link PartitionPlanType} value for the object store. */
413
+ export const encodePartitionPlan: (value: PartitionPlan) => Uint8Array =
414
+ encodeBeast2For(PartitionPlanType);
415
+
416
+ const decodeCurrentPlan = decodeBeast2For(PartitionPlanType);
417
+ const decodePreMergesPlan = decodeBeast2For(PreMergesPartitionPlanType);
418
+
419
+ /**
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.
423
+ *
424
+ * @param data - the plan object's bytes
425
+ * @returns the decoded plan
426
+ */
427
+ export function decodePartitionPlan(data: Uint8Array): PartitionPlan {
428
+ try {
429
+ return decodeCurrentPlan(data);
430
+ } 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 };
519
+ }
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);
567
+ }
568
+ const projected: Record<string, unknown> = {};
569
+ for (const name of shape.names) projected[name] = (key as Record<string, unknown>)[name];
570
+ return projected;
571
+ }