@elaraai/east-claude-plugin 1.0.98 → 1.0.100

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 (37) hide show
  1. package/.build/daemon/lsp.js +40 -3
  2. package/.build/daemon/server.js +40 -3
  3. package/.build/mcp/server.js +3 -2
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/index.json +13340 -482
  6. package/package.json +1 -1
  7. package/skills/e3/SKILL.md +107 -676
  8. package/skills/e3/reference/browser.md +93 -0
  9. package/skills/e3/reference/cli.md +120 -0
  10. package/skills/e3/reference/from-code.md +48 -0
  11. package/skills/e3/reference/rack.md +139 -0
  12. package/skills/e3/reference/record-indexes.md +326 -0
  13. package/skills/e3/reference/record-migrations.md +63 -0
  14. package/skills/e3/reference/repository.md +25 -0
  15. package/skills/e3-ui/SKILL.md +426 -259
  16. package/skills/east/SKILL.md +33 -395
  17. package/skills/east/reference/beast2.md +71 -0
  18. package/skills/east/reference/cross-language-and-json-schema.md +87 -0
  19. package/skills/east/reference/queries.md +46 -0
  20. package/skills/east/reference/runners-and-ir.md +148 -0
  21. package/skills/east/reference/trees-and-recursion.md +56 -0
  22. package/skills/east/reference/values-in-typescript.md +62 -0
  23. package/skills/east-contribute/SKILL.md +0 -1
  24. package/skills/east-py/SKILL.md +36 -1613
  25. package/skills/east-py/reference/beast2.md +108 -0
  26. package/skills/east-py/reference/cli.md +51 -0
  27. package/skills/east-py/reference/columns-and-platform-functions.md +87 -0
  28. package/skills/east-py/reference/control-flow-and-queries.md +147 -0
  29. package/skills/east-py/reference/cross-language.md +70 -0
  30. package/skills/east-py/reference/ir-and-diagnostics.md +150 -0
  31. package/skills/east-py/reference/patterns.md +369 -0
  32. package/skills/east-py/reference/performance.md +70 -0
  33. package/skills/east-py/reference/value-methods.md +306 -0
  34. package/skills/east-py/reference/values-construction-and-json.md +88 -0
  35. package/skills/east-py/reference/writing-east-functions.md +275 -0
  36. package/skills/east-py-datascience/SKILL.md +11 -28
  37. package/skills/east-ui/SKILL.md +5 -5
@@ -11,9 +11,10 @@ in-process, or on the east-node, east-c and east-py runners.
11
11
 
12
12
  ## Before writing code — search the example index
13
13
 
14
- Every East API has a tested example in the plugin's index — the index IS the
15
- API reference, printed from each example's IR in TypeScript or python. Before
16
- writing or changing East code:
14
+ Every East API has a tested example in the plugin's index — the index is the
15
+ API reference, printed from each example's IR in TypeScript or python, which
16
+ is why this skill lists methods without their signatures. Before writing or
17
+ changing East code:
17
18
 
18
19
  1. Call `mcp__plugin_east_east__search_east_examples` for each capability you
19
20
  are about to use — `language: "python"` for east-py, `"typescript"`
@@ -26,7 +27,10 @@ writing or changing East code:
26
27
  programs, exact and far cheaper, and the signatures omit the runtime rules
27
28
  that make East code correct.
28
29
 
29
- Nothing is injected for you; the search is the step.
30
+ Nothing is injected for you; the search is the step. (Python code working
31
+ with East values directly — eager methods, not an East function — is a
32
+ separate set, searched with `package: "east-values"`; the `east-py` skill
33
+ covers it.)
30
34
 
31
35
  ## Quick Start
32
36
 
@@ -108,7 +112,7 @@ Task → What do you need?
108
112
  │ ├─ Blob → .size(), .getUint8(), .decodeUtf8(), .decodeUtf16(), .decodeBeast(T),
109
113
  │ │ .decodeCsv(rowType, { nullStrings (default [] — an empty field is an empty string; opt in for none),
110
114
  │ │ defaults (per-column fallbacks), skipShortRows (drop ragged rows rather than fail), trimFields, … }),
111
- │ │ .openBeast(ArrayType(T) | SetType(K) | DictType(K, V)) — a huge collection, opened lazily (below)
115
+ │ │ .openBeast(ArrayType(T) | SetType(K) | DictType(K, V)) — a huge collection, opened lazily (reference/beast2.md)
112
116
  │ ├─ Array
113
117
  │ │ ├─ Read → .size(), .length(), .has(), .get(), .at(), .tryGet(), .getKeys()
114
118
  │ │ ├─ Mutate → .update(), .pushLast(), .popLast(), .pushFirst(), .popFirst(), .append(), .prepend(),
@@ -120,7 +124,8 @@ Task → What do you need?
120
124
  │ │ ├─ Convert → .stringJoin(), .toSet(), .toDict(), .flattenToSet(), .flattenToDict(), .encodeCsv()
121
125
  │ │ ├─ Group → .groupReduce(), .groupSize(), .groupSum(), .groupMean(), .groupMinimum(), .groupMaximum(),
122
126
  │ │ │ .groupToArrays(), .groupToSets(), .groupToDicts(), .groupEvery(), .groupSome()
123
- │ │ └─ Tree → .toTree(Node, key, parent, build) — flat parent-keyed rows to nested nodes (below)
127
+ │ │ └─ Tree → .toTree(Node, key, parent, build) — flat parent-keyed rows to nested nodes;
128
+ │ │ .flattenTree(children) — nested nodes back to { path, node } rows, depth first (reference/trees-and-recursion.md)
124
129
  │ ├─ Set
125
130
  │ │ ├─ Read and mutate → .size(), .has(), .insert(), .tryInsert(), .delete(), .tryDelete(), .clear(), .unionInPlace()
126
131
  │ │ ├─ Set ops → .copy(), .union(), .intersection(), .difference(), .symmetricDifference(), .isSubsetOf(),
@@ -129,6 +134,7 @@ Task → What do you need?
129
134
  │ │ reduce .reduce(), .scan(), .every(), .some(), .sum(), .mean();
130
135
  │ │ convert .toArray(), .toSet(), .toDict(), .flattenToArray(), .flattenToSet(), .flattenToDict(),
131
136
  │ │ .toTree(Node, parent, build) (an element / a key is its own key; Dict callbacks take (value, key));
137
+ │ │ Dict only: .flattenTree(children), the nodes back again (reference/trees-and-recursion.md);
132
138
  │ │ group .groupReduce(), .groupSize(), .groupSum(), .groupMean(), .groupToArrays(), .groupToSets(),
133
139
  │ │ .groupToDicts(), .groupEvery(), .groupSome()
134
140
  │ ├─ Dict → .size(), .has(), .get(), .tryGet(), .keys(), .getKeys(), .insert(), .insertOrUpdate(), .update(),
@@ -151,7 +157,7 @@ Task → What do you need?
151
157
  │ ├─ Integer → East.Integer.printCommaSeperated(), .roundNearest(), .printOrdinal()
152
158
  │ ├─ Float → East.Float.roundToDecimals(), .printCurrency(), .printCompact()
153
159
  │ ├─ DateTime → East.DateTime.fromComponents(), .roundDownDay(), .parseFormatted()
154
- │ │ (fromComponents NORMALISES an out-of-range component into the next — (2024, 2, 31) is 2024-03-02 —
160
+ │ │ (fromComponents normalises an out-of-range component into the next — (2024, 2, 31) is 2024-03-02 —
155
161
  │ │ so validate by round-tripping getMonth(), not by range-checking)
156
162
  │ ├─ Array → East.Array.range(), .linspace(), .generate() · Set → East.Set.generate() · Dict → East.Dict.generate()
157
163
  │ ├─ Blob → East.Blob.encodeBeast() · String → East.String.printJson(), East.String.printError()
@@ -161,21 +167,24 @@ Task → What do you need?
161
167
  │
162
168
  ├─ Compare and convert (East.*) → East.equal(), East.notEqual(), East.less(), East.greater(), East.lessEqual(),
163
169
  │ East.greaterEqual() (and their aliases), East.min(), East.max(), East.clamp(), East.print(expr);
164
- │ East.is() is identity for mutable collections — FROZEN ones (task inputs) compare by value
170
+ │ East.is() is identity for mutable collections — frozen ones (task inputs) compare by value
165
171
  ├─ Patches (East.*) → East.diff(before, after), East.applyPatch(value, patch),
166
172
  │ East.composePatch(first, second, type), East.invertPatch(patch, type)
173
+ ├─ Build East over types you are handed (a library, a generator) → East.field(v, name, T?), East.fields(v, names?),
174
+ │ East.struct(T, fields), East.withField(v, name, x), East.inCase(T, name, x), East.patchesFor(T): each name
175
+ │ checked as the program builds → reference/values-in-typescript.md
167
176
  │
168
177
  ├─ Ask a question of a value in jq → East.jq(input, program, resultType), checked when the program
169
- │ builds → "Queries (typed jq)"
170
- ├─ Work with East values from TypeScript → "Values in TypeScript" (comparators, SortedMap, codecs)
171
- ├─ Run a program from the shell → "Runners" (east-node / east-c / east-py run, exec, east-c ir)
178
+ │ builds → reference/queries.md
179
+ ├─ Work with East values from TypeScript → reference/values-in-typescript.md (comparators, SortedMap, codecs)
180
+ ├─ Run a program from the shell → reference/runners-and-ir.md (east-node / east-c / east-py run, exec, east-c ir)
172
181
  ├─ Run a runner-protocol unit in a host of your own (a browser's worker) → executeUnit(unit, io, { platforms })
173
- │ over an InMemoryUnitIO ("Units in a host of your own")
182
+ │ over an InMemoryUnitIO (reference/runners-and-ir.md)
174
183
  ├─ IR ↔ source → fn.toIR(), encodeEastIR, East.toSource(fn), east-node transpile
175
184
  ├─ A function written in python (or another package) → East.importFunction; yours for python → East.exportFunctions
176
- ├─ Check that a module BUILDS → east-node check (tsc cannot see the build's own errors)
185
+ ├─ Check that a module builds → east-node check (tsc cannot see the build's own errors)
177
186
  ├─ A JSON contract for a type → jsonSchemaFor(T) · back from a vendored schema → typeFromJsonSchema(schema)
178
- └─ Data larger than memory → "Binary serialization (beast2)"; inside a body blob.openBeast(T),
187
+ └─ Data larger than memory → reference/beast2.md; inside a body blob.openBeast(T),
179
188
  and a file on the runner's disk FileSystem.openBeast(T, path) (east-node-std / east-c-std / east-py-std)
180
189
  ```
181
190
 
@@ -249,33 +258,6 @@ $.try($ => {
249
258
  }).finally($ => { /* cleanup */ });
250
259
  ```
251
260
 
252
- ### Trees from flat rows, and recursion
253
-
254
- `toTree` builds nested data — any depth, your own node type — from rows that
255
- name their key and their parent's key. `build` runs once per row, children
256
- first; the result is the roots in source order.
257
-
258
- ```typescript
259
- const Row = StructType({ id: StringType, parent: OptionType(StringType), name: StringType });
260
- const Node = RecursiveType(self => StructType({ name: StringType, children: ArrayType(self) }));
261
-
262
- const nest = East.function([ArrayType(Row)], ArrayType(Node), ($, rows) =>
263
- rows.toTree(Node, ($, r) => r.id, ($, r) => r.parent, ($, r, _i, children) => ({ name: r.name, children })));
264
- ```
265
-
266
- - A row whose parent is `none` is a root, and so is an orphan (its parent key
267
- is not in the rows). A repeated key ❗ `toTree: duplicate key <key>`; a cycle
268
- ❗ `toTree: cycle through key <key>` — both before any `build`.
269
- - A non-recursive `Node` folds bottom-up: `IntegerType` with
270
- `children.sum().add(1n)` is each root's subtree size.
271
- - **Recursion** — a function calling itself through a captured variable
272
- (`$.let(fn, FunctionType(...))`, then `$.assign(fn, East.function(...))`) —
273
- is for shallow depths, in the hundreds. Deeper, a call ❗
274
- `call stack exhausted: East calls nested too deeply`, a catchable error on
275
- every runtime. Walk deep data with `$.while` or `toTree`, which is iterative;
276
- the C and Python runtimes still bound a recursive VALUE's depth to tens of
277
- thousands of levels.
278
-
279
261
  ### Platform functions
280
262
 
281
263
  An implementation is a plain TypeScript function over the `ValueTypeOf` of each
@@ -312,362 +294,18 @@ implementation, python's included), and list the package in the task's runner,
312
294
  wiring is **east-project**'s; the task runner is **e3**'s; python's
313
295
  `@platform_function` is **east-py**'s.
314
296
 
315
- ## Values in TypeScript
316
-
317
- Host-side functions for East values — build time, no expression, no IR. Each
318
- takes the type and returns a function (`compareFor(T)(a, b)`).
319
-
320
- | Signature | Description |
321
- |-----------|-------------|
322
- | **Comparing** |
323
- | `compareFor(T)(a, b): -1 \| 0 \| 1` | East's total order — the order sets, dicts and `.sort()` use (NaN last) |
324
- | `equalFor(T)`, `notEqualFor(T)`, `lessFor(T)`, `lessEqualFor(T)`, `greaterFor(T)`, `greaterEqualFor(T)` | The comparisons as East defines them: by value, NaN equal to itself and −0 apart from 0, variants by case name. Never `===`, `<` or a comparator of your own |
325
- | `isFor(T)(a, b)` | East's `Is`: value for immutable types, identity for mutable containers — but two frozen collections by value, and a `Ref` always by identity |
326
- | `new SortedMap(entries?, compareFor(K))` · `new SortedSet(values?, compareFor(K))` | `Map` / `Set` in East order — what decoders return |
327
- | **Types** |
328
- | `isValueOf(value, T)` | Whether a TypeScript value is a value of `T` |
329
- | `printType(T)` · `isTypeEqual(a, b)` · `isSubtype(a, b)` | Print a type in East text; compare types |
330
- | `diffTypes(actual, expected)` → `renderTypeDiff(diffs)` | The differences between two types, each with its path |
331
- | `defaultValue(T)` · `minimalValue(T)` | A value of `T`: its default, and its least |
332
- | **Codecs** — each `…For(T)` builds a codec for one type |
333
- | `printFor(T)(value)` · `parseFor(T)(text)` | East text, the literal syntax `.east` files hold. `parseFor` returns `{ success, value \| error, position }`; `parseInferred(text)` returns `[type, value]` |
334
- | `toJSONFor(T)(value)` · `fromJSONFor(T)(json)` | East JSON as a JavaScript value (`encodeJSONFor` / `decodeJSONFor` as bytes): lossless — an Integer a quoted decimal, a Blob hex, a flat Option (see JSON Schema) |
335
- | `encodeCsvFor(StructT, config?)(rows)` · `decodeCsvFor(StructT, config?)(blob)` | An array of structs ↔ CSV bytes |
336
- | `encodeBeast2For(T)(value)` · `decodeBeast2For(T)(blob)` | beast2, the binary form every runtime and e3 store — see below |
337
-
338
- ## Queries (typed jq)
339
-
340
- `East.jq` asks a question of a value in jq, inside an East body. The program is
341
- parsed, type-checked against the input's East type and translated to ordinary
342
- IR when the function is built, so it runs on every runner; a mistake is a
343
- `QueryError` at build time, with the checker's sentence, span and fixes. The
344
- language is jq 1.8 over East values: `libs/east/devdocs/QUERY.md` in the East
345
- repository says what each builtin does, and every place East differs from jq.
346
-
347
- ```typescript
348
- const Order = StructType({ id: IntegerType, total: FloatType });
349
-
350
- const bigOrders = East.function([ArrayType(Order)], ArrayType(IntegerType), ($, orders) =>
351
- East.jq(orders, "[.[] | select(.total > 1000) | .id]", ArrayType(IntegerType)));
352
-
353
- // An object of inputs is read as an e3 root: `.orders` is that input alone, so a lazy one stays lazy.
354
- const revenue = East.function([ArrayType(Order)], FloatType, ($, orders) =>
355
- East.jq({ orders }, ".orders | map(.total) | add", FloatType));
356
- ```
357
-
358
- | Signature | Description | Example |
359
- |-----------|-------------|---------|
360
- | **In an East body** |
361
- | `East.jq<T>(input: Expr \| { [name: string]: Expr }, program: string, resultType: T): ExprType<T>` **❗** | The query's result, an expression of `resultType`: the outputs' type `T` for a query that gives exactly one output, `Option<T>` for at most one, `Array<T>` for any number. Throws `QueryError` when the query does not check, or checks to another type | `East.jq(orders, "map(.total)", ArrayType(FloatType)).sum()` |
362
- | **Host-side** |
363
- | `checkJq(program: string, input: EastType, options?: { root?: boolean, tooling?: boolean }): CheckJqResult` | The checker: `program` (the program as written, a `JqType` tree, or `null` when it does not check), `inputType`, `diagnostics` (each a code, a span, one sentence, suggestions and fixes), `elementType`, `multiplicity` (`"one"`, `"maybe"` or `"many"`). A query's types are never stored: check the program against its data wherever they are needed | `checkJq(".orders[0].totl", Root).diagnostics[0].message` |
364
- | `completeJq(text: string, offset: number, input: EastType, options?): JqCompletions \| null` | The completions at a cursor, from the type there: fields, cases, variables, builtins | `completeJq(".orders[0].to", 13, Root)` |
365
- | `printJq(program: JqNode, options?: { layout?: "line" \| "pipeline" }): PrintedJq` | A program's canonical text, and its nodes' spans; `pipeline` puts each top-level stage on a line of its own | `printJq(checked.program!).text` |
366
- | `translateJq(checked: CheckJqResult, options?: { maxOutputs?: number, tooling?: boolean }): JqTranslation` **❗** | The translation: `fn()`, an East function of the inputs the query reads (`inputs`), and its `resultType` | `East.compile(translateJq(checked).fn(), [])` |
367
- | `evaluateJq(program: string \| CheckJqResult, input: unknown, options?: { inputType?, root?, tooling?, platform? }): unknown` **❗** | Checks, translates, compiles (cached) and runs a query over a TypeScript value. Throws `QueryError`: the checker's diagnostics, or one `runtime` diagnostic at the node that raised | `evaluateJq("[.[] \| .id]", orders, { inputType: ArrayType(Order) })` |
368
- | `splitJq(checked: CheckJqResult, options?: { maxOutputs?: number }): JqSplit` | A query checked as an e3 root, split over one dataset's pieces for an e3 split call: `piece()` each piece runs, the `output` kind its outputs combine by (`array`, `set`, `dict` with `merge()`, `fold` with `zero` and `combine()`), and `then()` over the result; or, `kind: "whole"`, why it runs as one unit. QUERY.md §17.1 | `splitJq(checkJq(".orders \| group_by(.region) \| map(length)", Root, { root: true })).output.kind` |
369
-
370
- - **The values are East's.** A Dict is an object keyed by its key type, so
371
- `.byId[1035]` looks up an Integer key; a variant reads as `{type, value}`,
372
- and `select(.status.type == "shipped")` narrows it; an Option is `null` or
373
- its value. `.name` on a struct that lacks the field is an error (`.name?`
374
- gives `null`), and so is comparing values of two types.
375
- - **A lookup can miss**, so `.[i]` and `.[k]` give an `Option`, and
376
- `first(f)` gives at most one output, so its result type is `Option<T>`.
377
- - **In code a query is a call of the `Query` builtin**, which carries the
378
- program as written beside its translation: every runtime runs it as any
379
- builtin, and `East.toSource` prints it back as `East.jq(…)`.
380
-
381
- ## Runners: running a program from the command line
382
-
383
- A program's IR runs on any of three runners, which take the same arguments.
384
- Write it with `writeFileSync("prog.beast2", encodeEastIR(fn.toIR()))` — which
385
- keeps its source map, so errors name your lines — or `fn.toIR().toJSON()`.
386
-
387
- ```bash
388
- east-node run prog.beast2 -p @elaraai/east-node-std -i a.beast2 -i b.beast2 -o out.beast2 # -v: timing and peak memory
389
- east-c run prog.beast2 -p east-c-std -i a.beast2 -o out.beast2 [--profile] # native; --profile: time per function
390
- east-py run prog.beast2 -p east-py-std -i a.beast2 -o out.beast2 [--profile] # python (the east-py skill)
391
- ```
392
-
393
- - `--profile` (east-c and east-py, `run` and `exec`) prints every East
394
- function and platform function the work called, by self time, with its
395
- calls and where it is defined and called. `EAST_PROFILE=1` profiles without
396
- the flag — e3 hands its environment to the runners, so a task's report
397
- lands in its log — and `EAST_PROFILE_INTERVAL=N` prints the profile so far
398
- every N seconds while the work runs.
399
- - Each `-i` is one argument, in parameter order, and each `-p` loads one
400
- platform package (east-node needs at least one); `-o` writes the result,
401
- else it is printed. IR and values may be `.beast2`, `.beast`, `.east` or
402
- `.json`.
403
- - `exec <unit>` is the runner protocol e3 speaks: it runs one unit file — a
404
- program over its inputs, a merge of an output's parts, or the intake of a
405
- delivered collection — writes the output by its kind and records a result;
406
- exit 0 when it succeeded, 1 when it failed.
407
- - `version [-p <package>…]` prints the runner's version and each package's.
408
- - east-c also has `convert <file> [-o <file>] [--type <east-type>]`, a value
409
- from one format to another, and the IR toolbox: `ir normalize` (the canonical
410
- form — the round-trip equality contract), `ir diff a b` (the first structural
411
- difference) and `ir convert` (JSON ↔ beast2, the source map kept).
412
- - east-node also has `transpile`, `check`, `lsp` and `export-functions`, below;
413
- east-py has the python twins, and `lint`.
414
-
415
- ### Units in a host of your own: `executeUnit` and `UnitIO`
416
-
417
- east-node's `exec` is a file wrapper around `executeUnit`, the one TypeScript
418
- unit runner, which does a unit's work over a `UnitIO` rather than a file
419
- system — so a host with none runs units too: e3-web runs them in a browser's
420
- Web Workers. east-c has the same shape (`east/unit.h`).
421
-
422
- | Signature | Description |
423
- |-----------|-------------|
424
- | `executeUnit(unit: Unit, io: UnitIO, options: ExecuteUnitOptions): Promise<UnitResult>` | Does the unit's work — a run, a merge or an intake — over `io`, writing its output there by its kind, and returns the result, which the host writes where `unit.result` says if it keeps it. A failure is the result's `outcome`, `failed` with its message and source locations, never a throw |
425
- | `options.platforms(name: string): readonly PlatformFunction[] \| Promise<…>` | Each package the unit lists, asked once, in the unit's order, before the work begins; a throw fails the unit with its message |
426
- | `options.resident?: () => number` · `options.peakBytes?: () => bigint` | The host's memory gauges. Without `resident`, what reading a lazy input whole weighs and what decoding an input whole added both come to 0; without `peakBytes`, the result's `peakBytes` is 0, as a browser's is |
427
- | `options.report?: UnitRunReport` | Hears how a run unit's inputs open and what reading them came to: the account `exec -v` prints |
428
- | `UnitIO` | The unit's files, by the paths it names: `read`, `size`, `readRange`, `segment(path, fetch)`, `list`, `makeDirectory`, `write` — every one synchronous, since compiled East reads a lazy input without awaiting |
429
- | `new InMemoryUnitIO(files?: Iterable<[path: string, bytes: Uint8Array]>)` | A `UnitIO` in memory, for a host with no file system: given every file the unit names, segments included; `.files` then holds them and every file the unit wrote |
430
- | `UnitType` · `UnitResultType` | The unit and its result as East types, for a host that keeps them as beast2 |
431
-
432
- - The unit's thread grant (`threads`) caps the frame pool for the rest of the
433
- process: a grant of one frames every output inline.
434
- - A unit whose host places segments as they are read (`fetch: true`) has
435
- `io.segment` asked for each segment of a staged manifest before it is read.
436
- `InMemoryUnitIO` holds every segment already, so its unit says `fetch: false`,
437
- and a segment it does not hold fails at once, as any absent file does.
438
-
439
- ```typescript
440
- import { East, IntegerType, InMemoryUnitIO, decodeBeast2For, encodeBeast2For, encodeEastIR, executeUnit, variant, type Unit } from "@elaraai/east";
441
-
442
- const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
443
-
444
- // A unit names its files by path: here, the program, its one input and its output.
445
- const unit: Unit = {
446
- work: variant("run", {
447
- program: "program.beast2",
448
- inputs: ["x.beast2"],
449
- output: variant("value", "out.beast2"),
450
- decode: variant("lazy", null),
451
- }),
452
- platforms: [], // the platform packages it needs, each resolved by `options.platforms`
453
- threads: 1n,
454
- fetch: false, // every segment it reads is in the IO already
455
- result: "result.beast2",
456
- };
457
- const io = new InMemoryUnitIO([
458
- ["program.beast2", encodeEastIR(double.toIR())],
459
- ["x.beast2", encodeBeast2For(IntegerType)(21n)],
460
- ]);
461
- const result = await executeUnit(unit, io, { platforms: () => [] });
462
- // result.outcome: ok, or failed with its message and source locations; a failure is never thrown
463
- decodeBeast2For(IntegerType)(io.read("out.beast2")); // 42n
464
- ```
465
-
466
- ## IR → source: `East.toSource` and `east-node transpile`
467
-
468
- Any IR — yours, or one exported from python — prints back as a module whose
469
- `East.function(...)` rebuilds the same IR (equal under `east-c ir normalize`).
470
-
471
- ```typescript
472
- const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
473
- console.log(East.toSource(double));
474
- // import { East, … IntegerType } from "@elaraai/east";
475
- // export const main = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
476
- ```
477
-
478
- ```bash
479
- east-node transpile double.beast2 -o double.ts # any runtime's IR
480
- east-node transpile ./ir/ -o ./ts/ --rebuild ./rebuilt/ # a directory, and the IR each module builds back
481
- east-py transpile double.beast2 -o double.py # the python twin
482
- ```
297
+ ## Reference files
483
298
 
484
- - Statements print as the `$`-forms, a bound value as `$.let(value, T)`, an
485
- Option as `some(v)` / `none`, other values as `East.value(v, T)`, builtins as
486
- their methods; one with no method spelling prints as
487
- `East.builtin(name, [T...], [args], out)`, which rebuilds. `East.as(v, T)` and
488
- `East.wrapRecursive(v, T)` spell the `As` and `WrapRecursive` nodes.
489
- - A platform call prints as its library spells it when you pass the library:
490
- `East.toSource(fn, { libraries: { "@elaraai/east-node-io": await import("@elaraai/east-node-io") } })`.
491
- Without it the call hoists to a declaration named after the function
492
- (`const tar_create = East.asyncPlatform("tar_create", …)`). The printer finds
493
- the handles in the module's exports — each handle `East.platform` returns
494
- carries its `name`, `inputs`, `output` and `async`, and
495
- `isPlatformDeclaration(x)` tells one from a function.
496
- - Variables keep the names you wrote, read by the TypeScript compiler (an
497
- optional peer); without it, or in a browser, they print as `_N`, as does a
498
- slot the body did not name (`($, x) =>` for a `(value, index)` callback).
499
- - The contract and the round-trip suites: `docs/conventions/EAST_CODEGEN.md`.
500
-
501
- ## Checking that a module builds: `east-node check`
502
-
503
- `tsc` type-checks the TypeScript around a program, not the program: East's own
504
- refusals — a body whose type differs from the declared output, a capture the
505
- builder refuses, an `IRAnalysisError` — happen when `East.function` runs, at
506
- import.
507
-
508
- ```bash
509
- east-node check dist/index.js # every broken function, at its line
510
- east-node check dist/index.js --format json # the records `east-py check --format json` emits
511
- east-node lsp # the diagnostics over stdio (needs @elaraai/east-diagnostics)
512
- ```
513
-
514
- It reports **every** broken function, not the first. Importing a module runs
515
- it, so `EAST_CHECK=1` is set meanwhile, and a module should skip import-time
516
- work when it sees it. Edit-time idiom checks are separate:
517
- `@elaraai/tsserver-plugin-east` in the editor, `@elaraai/eslint-plugin-east`'s
518
- `east/east-rules` in `make lint`.
519
-
520
- ## Cross-language functions: `East.importFunction` / `East.exportFunctions`
521
-
522
- A function authored in python (with its `East.function`) is called from a
523
- TypeScript body by name, and the other way round; the deployed program is pure
524
- IR. Functions travel as a **manifest** — their IR, declared types and platform
525
- dependencies. In an e3 project `e3.export` builds and links manifests itself
526
- (see **e3**); by hand:
527
-
528
- ```bash
529
- east-py export-functions pricing -o pricing.functions.beast2 -p east-py-std # python side
530
- east-node export-functions dist/maths.js -o maths.functions.beast2 # reads the module's `eastFunctions`
531
- ```
532
-
533
- ```typescript
534
- const score = East.importFunction("pricing", "score", FunctionType([RowType], FloatType)); // typed, callable
535
- const total = East.function([ArrayType(RowType)], FloatType, ($, rows) => rows.map(($, r) => score(r)).sum());
536
-
537
- // in-process: link, then compile — linkImports returns a sync or async function's IR
538
- const { ir } = East.linkImports(total, [East.decodeFunctionManifest(readFileSync("pricing.functions.beast2"))]);
539
- new EastIR(ir as FunctionIR).compile([])(rows);
540
-
541
- // export TypeScript functions for python to import
542
- writeFileSync("maths.functions.beast2", East.encodeFunctionManifest(East.exportFunctions("maths", "1.0.0", { double, halve })));
543
- ```
544
-
545
- - The declared type must equal the exported type **exactly** — a mismatch is a
546
- build error naming both. Unlinked, the reference is a `Platform` node named
547
- `east.importFunction`, so compiling it without linking fails loudly.
548
- - Only closed values export: no captures, and no unresolved imports of their own.
549
- - A manifest carries each function's source map: `linkImports` adds its
550
- locations to the importer's map, which it returns as `sourceMap`, so an
551
- error or a profile names the exporter's source — the python line included.
552
- - The imported function's platform calls must be provided by the consuming
553
- runner (stock families count across runtimes: `east-py-std` ≡
554
- `@elaraai/east-node-std` ≡ `east-c-std`). See `docs/conventions/EAST_CODEGEN.md`.
555
-
556
- ## A JSON contract for a type: `jsonSchemaFor` / `typeFromJsonSchema`
557
-
558
- `East.String.printJson` writes East JSON, lossless and self-describing.
559
- `jsonSchemaFor(T)` emits the JSON Schema of exactly that encoding, so a third
560
- party can validate a payload before sending it; `typeFromJsonSchema` goes the
561
- other way. Both are host-side, like `compareFor`.
562
-
563
- ```typescript
564
- writeFileSync("contract.schema.json", JSON.stringify(jsonSchemaFor(ArrayType(ReadingType), { draft: "draft-07" }), null, 2));
565
- const T = typeFromJsonSchema(JSON.parse(readFileSync("partner.schema.json", "utf-8")));
566
- ```
567
-
568
- | Signature | Description |
569
- |-----------|-------------|
570
- | `jsonSchemaFor(type, options?: { draft }): JsonSchema` | The schema of `type`'s East JSON. `draft`: `"2020-12"` (default), `"draft-07"` or `"openapi-3.0"`. Throws on `Never`, `Function`, `AsyncFunction` |
571
- | `typeFromJsonSchema(schema): EastType` | The type a schema describes. Throws `JsonSchemaUnsupportedError`, with the RFC 6901 pointer, on a keyword East cannot express (`allOf`, `not`, `if`/`then`/`else`, `anyOf`, an open record, …) |
572
- | `EAST_JSON_PATTERNS` | The lexical forms of East JSON's scalars (`integer`, `blob`, `floatSpecials`), for a reader enforcing exactly what the schema says |
573
-
574
- - **An `Option<T>` is `null` or `T`'s own encoding** wherever `T` can never
575
- encode as `null` (`none` → `null`, `some(7n)` → `"7"`; a struct field is
576
- `"note": null` or `"note": "x"`, and an absent key is still an error). Only
577
- `Option<Null>` and `Option<Option<T>>` keep the tagged
578
- `{"type": …, "value": …}` object. A `Recursive` payload is judged by what it
579
- wraps. The rule is a total function of the type, the same on every runtime,
580
- and the tagged object under a flat Option is refused by the payload's decoder
581
- (`expected string, got {"type":"none","value":null}`).
582
- - **A `DateTime` is `format: "date-time"`**: every decoder reads any RFC 3339
583
- date-time — `Z` or any offset (read as UTC), `t`/`z` in either case, any
584
- fractional digits (past the millisecond dropped, never rounded), a leap second
585
- as the Unix time its fields add up to — in years 0001–9999. `printJson`
586
- writes one form: UTC, three fractional digits, `+00:00`.
587
- - **Other scalars are described as the encoder emits them**: an `Integer` a
588
- quoted i64 decimal, a `Blob` lowercase hex (`parseJson` also takes uppercase,
589
- which the contract does not), so a producer that validates never sends what
590
- the strict reader rejects. The patterns spell digits `[0-9]`, so python and
591
- JavaScript validators accept the same strings.
592
- - **The document is deterministic and identical across languages** (east-py's
593
- `json_schema_for` emits the same bytes), and its `x-east-type` annotations
594
- make the inverse exact: `Set` vs `Array`, `Dict` vs an array of pairs, a flat
595
- Option vs any `oneOf`. A foreign schema without them converts by a
596
- documented structural mapping; `nullable: true`, `["string", "null"]` and a
597
- `oneOf` of null and one schema read as an Option, and a
598
- `format: "date-time"` string as a `DateTime`.
599
- - **Recursion binds one `RecursiveType` per cycle group**; definitions that
600
- need two binders are refused, naming them.
601
- - To READ a JSON document larger than memory under a contract, use `Json.open`
602
- / `Json.next` from **east-node-std** (and its east-py-std and east-c-std twins).
603
-
604
- ## Binary serialization (beast2)
605
-
606
- Inside a body the builtins are `East.Blob.encodeBeast(value, 'v2')` and
607
- `blob.decodeBeast(type, 'v2')`; the functions below are the **host-side**
608
- TypeScript API for the same format. Blobs are self-describing, and a decoder
609
- reads any container version its type was written in.
610
-
611
- | Signature | Description |
612
- |-----------|-------------|
613
- | **Whole values** |
614
- | `encodeBeast2For(T, opts?): (value) => Uint8Array` | `opts.version` (`5` default, `4` for legacy readers); `opts.codec` (`"deflate"` \| `"none"`) and `opts.index` for v5 |
615
- | `decodeBeast2For(T, opts?): (blob) => ValueTypeOf<T>` | Any container version, in every runtime and browser. The blob's header type must be `T`, or a subtype whose variant cases line up with `T`'s (a `none` reads as any `Option`); any other is refused — `beast2: cannot decode a blob of type … as …`, the words east-c and east-py use. `opts.platform` for decoded functions |
616
- | `decodeBeast2(blob)` → `{ type, value }` | Without knowing the type |
617
- | `decodeBeast2ForAsync(T)` | The same, decompressing through the platform's `DecompressionStream` — faster for large blobs in a browser |
618
- | `encodeEastIR(ir)` · `decodeEastIR(blob)` | A program with its source map |
619
- | **Collections larger than memory** (v5) |
620
- | `new Beast2ElementWriter(T, sink, opts?)` | The canonical writer: `.add(element)` one Array/Set element or Dict `[key, value]` — Set elements and Dict keys strictly ascending in East order — and the content-defined cut rule places the segments, so the bytes are a function of the value, identical from east-c and east-py, and a one-row edit re-cuts only the segments around it. Memory is one segment; `.finish()` writes the last, the terminator and the index. `sink` is `(bytes) => void`, or `{ segment(s) }` for one standalone segment blob at a time (`s`: `{ count, fence, logicalBytes, blob }`, under `writer.header`) |
621
- | `encodeBeast2PagedFor(T)(value)` | One value in memory through the element writer; plain `Map`/`Set` inputs are sorted first |
622
- | `new Beast2RunSorter(T, openRun, { merge } \| { union })` | Elements in ANY order in, sorted canonical runs out (a key added twice folds with `merge(key, acc, value)`, or `union` for a Set; without one it throws); then `mergeBeast2For(T, { merge } \| { union })(runs, sink)` writes the value. Memory is one run, then one segment per run. A run closes at `RUN_MAX_COUNT` elements or `RUN_MAX_BYTES`, platform constants, so every runtime writes the same runs |
623
- | `mergeBeast2For(T, opts?)(sources, sink)` | Sorted collections — blobs, range readers or manifests — into their union's blob or manifest, segment by segment; a key several hold folds in input order; `opts.from` / `opts.to` merge a key range |
624
- | `recutBeast2For(T)(pieces, sink)` | A collection in pieces, some already written, into its canonical segments: a segment the whole shares with its piece is carried unread (`sink.carried`), the rest re-cut (`sink.written`) |
625
- | `intakeBeast2For(T)(delivery, sink, opts?)` | A collection from outside — a `Beast2SyncRangeReader` over a v4 or v5 blob whose header names exactly `T` — through the Writer into a `Beast2ManifestSink`, a segment at a time: each row is walked by its type, the Writer's own bytes go on as they stand, and any other row is decoded and written again. `opts.segments` takes in the index's segments `[from, to)`, a piece of a large delivery. Refuses another type, a malformed segment or one over `RUN_MAX_BYTES`, a row that does not decode, and keys that do not ascend, with `Beast2IntakeError` in the words every runner uses. What e3's intake units run, on every runner |
626
- | `new Beast2Writer(T, sink)` · `encodeBeast2SegmentsFor(T)(batches)` | Segments of your own choosing: one per non-empty batch, Set/Dict batches in ascending order |
627
- | `iterBeast2SegmentsFor(T)(blob)` | One decoded segment at a time |
628
- | `openBeast2PagesFor(T)(source)` | Random access: `.elementCount` and `.segmentCount` in O(1); `.segment(i)`, `.element(row)`, `.slice(offset, limit)` and `.get(key)` decode only the segments they touch |
629
- | `openBeast2LazyFor(T, { frozen? })(source)` | An ordinary `SortedMap` / `SortedSet` / array served from the index, a segment at a time — what `blob.openBeast` and `FileSystem.openBeast` return, frozen. `isBeast2LazySafe(T, { frozen })`: only `Ref`- and function-bearing shapes decode whole |
630
- | `Beast2SyncRangeReader` — `{ size, read(offset, length) }` | Positioned reads, wherever a blob goes, so a file's bytes never sit on the heap whole; `readBeast2Extents` / `readBeast2ExtentsRanged` (HTTP, S3) read the geometry |
631
- | **Segment manifests** — one object per segment plus a manifest naming them: how e3 stores a collection |
632
- | `new Beast2ManifestWriter(T, { object(hash, bytes), manifest(bytes) })` | The canonical manifest directory: the header, each segment as it is cut (named by its SHA-256, `<manifest>.segments/<hash>.beast2` on disk), then the manifest; east-c and east-py write the same |
633
- | `readBeast2Manifest(source)` | The manifest a blob holds (`{ kind, level, type, rule, header, entries: [{ hash, fence, count, bytes }] }`), or `null`; `manifestElementCount` / `manifestByteSize` total it |
634
- | `{ manifest, segment(i) }` (`Beast2ManifestSource`) | A manifest-held collection wherever a blob goes; `segment(i)` is called only for a segment a read decodes |
635
- | `spliceBeast2Segments(head, segments)` | A manifest's segments spliced back into one blob, a chunk at a time, decoding nothing and holding only the segment in hand — how e3-api-client downloads a collection. Throws on a segment that is not a segmented, indexed v5 collection, or was written under another header |
636
- | `sha256Hex(bytes)` | SHA-256 in lowercase hex, pure TypeScript |
637
-
638
- ```typescript
639
- import { ArrayType, StructType, StringType, IntegerType,
640
- Beast2ElementWriter, iterBeast2SegmentsFor, openBeast2PagesFor, decodeBeast2For } from "@elaraai/east";
641
-
642
- const Rows = ArrayType(StructType({ id: IntegerType, name: StringType }));
643
-
644
- const out = createWriteStream("rows.beast2");
645
- const writer = new Beast2ElementWriter(Rows, bytes => out.write(bytes));
646
- for (const row of rows) writer.add(row); // never whole in memory
647
- writer.finish();
648
-
649
- const blob = new Uint8Array(readFileSync("rows.beast2"));
650
- decodeBeast2For(Rows)(blob); // whole: segments concatenate
651
- for (const segment of iterBeast2SegmentsFor(Rows)(blob)) consume(segment); // a segment at a time
652
- openBeast2PagesFor(Rows)(blob).element(4_000_000); // one row: decodes ONE segment
653
- ```
299
+ Read the one a task needs; each is complete on its subject.
654
300
 
655
- - Set and Dict segments hold the canonical value — strictly ascending,
656
- disjoint, no duplicate keys — so writers refuse out-of-order elements and
657
- decoders refuse a non-canonical blob as corrupt.
658
- - **One encoding per value**: the element writer and `encodeBeast2PagedFor` cut
659
- by the same rule in every runtime, so equal values are equal bytes and share
660
- segments in a content-addressed store. `Beast2Writer` writes the batches you
661
- give it, which every reader also reads.
662
- - Streaming and paging are v5 only. The writer's segments are self-contained,
663
- with an index, which paging and parallel decode need: pass
664
- `{ selfContained: false }` only when aliasing must span segments. `write()`
665
- skips an empty batch, so a segment count is never zero.
666
- - Inside a body, `blob.openBeast(T)` and `FileSystem.openBeast(T, path)` hand
667
- back the frozen lazy value a runner gives a large task input: keyed reads and
668
- `$.for` cost one segment, mutation is refused (`.copy()` first), and the
669
- header type is checked against `T` first. `East.Blob.encodeBeast` writes no
670
- index, so a paged blob comes from the writers above or a runner's output.
301
+ | File | Holds |
302
+ |---|---|
303
+ | [reference/values-in-typescript.md](reference/values-in-typescript.md) | Comparators (`compareFor`, `equalFor`, …), `SortedMap`/`SortedSet`, type utilities, the East-text, JSON, CSV and beast2 codecs; building programs over names and types held as values (`East.field`, `East.withField`, `East.patchesFor`) |
304
+ | [reference/trees-and-recursion.md](reference/trees-and-recursion.md) | `toTree`, `flattenTree`, rollups, and the depth limits of recursion |
305
+ | [reference/queries.md](reference/queries.md) | `East.jq` in a body, and `checkJq` / `evaluateJq` / `splitJq` on the host |
306
+ | [reference/runners-and-ir.md](reference/runners-and-ir.md) | `east-node` / `east-c` / `east-py` `run` and `exec`, `executeUnit` over a `UnitIO`, `East.toSource` and `east-node transpile`, `east-node check` |
307
+ | [reference/cross-language-and-json-schema.md](reference/cross-language-and-json-schema.md) | `East.importFunction` / `East.exportFunctions`, and `jsonSchemaFor` / `typeFromJsonSchema` |
308
+ | [reference/beast2.md](reference/beast2.md) | `encodeBeast2For` / `decodeBeast2For`, paged and streaming writers, `openBeast2PagesFor`, `blob.openBeast` |
671
309
 
672
310
  ## Related skills
673
311
 
@@ -0,0 +1,71 @@
1
+ # Binary serialization (beast2)
2
+
3
+ beast2, the binary form every runtime and e3 store, and collections larger than memory. Part of the `east` skill.
4
+
5
+ ## Binary serialization (beast2)
6
+
7
+ Inside a body the builtins are `East.Blob.encodeBeast(value, 'v2')` and
8
+ `blob.decodeBeast(type, 'v2')`; the functions below are the **host-side**
9
+ TypeScript API for the same format. Blobs are self-describing, and a decoder
10
+ reads any container version its type was written in.
11
+
12
+ | Signature | Description |
13
+ |-----------|-------------|
14
+ | **Whole values** |
15
+ | `encodeBeast2For(T, opts?): (value) => Uint8Array` | `opts.version` (`5` default, `4` for legacy readers); `opts.codec` (`"deflate"` \| `"none"`) and `opts.index` for v5 |
16
+ | `decodeBeast2For(T, opts?): (blob) => ValueTypeOf<T>` | Any container version, in every runtime and browser. The blob's header type must be `T`, or a subtype whose variant cases line up with `T`'s (a `none` reads as any `Option`); any other is refused — `beast2: cannot decode a blob of type … as …`, the words east-c and east-py use. `opts.platform` for decoded functions |
17
+ | `decodeBeast2(blob)` → `{ type, value }` | Without knowing the type |
18
+ | `decodeBeast2ForAsync(T)` | The same, decompressing through the platform's `DecompressionStream` — faster for large blobs in a browser |
19
+ | `encodeEastIR(ir)` · `decodeEastIR(blob)` | A program with its source map |
20
+ | **Collections larger than memory** (v5) |
21
+ | `new Beast2ElementWriter(T, sink, opts?)` | The canonical writer: `.add(element)` one Array/Set element or Dict `[key, value]` — Set elements and Dict keys strictly ascending in East order — and the content-defined cut rule places the segments, so the bytes are a function of the value, identical from east-c and east-py, and a one-row edit re-cuts only the segments around it. Memory is one segment; `.finish()` writes the last, the terminator and the index. `sink` is `(bytes) => void`, or `{ segment(s) }` for one standalone segment blob at a time (`s`: `{ count, fence, logicalBytes, blob }`, under `writer.header`) |
22
+ | `encodeBeast2PagedFor(T)(value)` | One value in memory through the element writer; plain `Map`/`Set` inputs are sorted first |
23
+ | `new Beast2RunSorter(T, openRun, { merge } \| { union })` | Elements in ANY order in, sorted canonical runs out (a key added twice folds with `merge(key, acc, value)`, or `union` for a Set; without one it throws); then `mergeBeast2For(T, { merge } \| { union })(runs, sink)` writes the value. Memory is one run, then one segment per run. A run closes at `RUN_MAX_COUNT` elements or `RUN_MAX_BYTES`, platform constants, so every runtime writes the same runs |
24
+ | `mergeBeast2For(T, opts?)(sources, sink)` | Sorted collections — blobs, range readers or manifests — into their union's blob or manifest, segment by segment; a key several hold folds in input order; `opts.from` / `opts.to` merge a key range |
25
+ | `recutBeast2For(T)(pieces, sink)` | A collection in pieces, some already written, into its canonical segments: a segment the whole shares with its piece is carried unread (`sink.carried`), the rest re-cut (`sink.written`) |
26
+ | `intakeBeast2For(T)(delivery, sink, opts?)` | A collection from outside — a `Beast2SyncRangeReader` over a v4 or v5 blob whose header names exactly `T` — through the Writer into a `Beast2ManifestSink`, a segment at a time: each row is walked by its type, the Writer's own bytes go on as they stand, and any other row is decoded and written again. `opts.segments` takes in the index's segments `[from, to)`, a piece of a large delivery. Refuses another type, a malformed segment or one over `RUN_MAX_BYTES`, a row that does not decode, and keys that do not ascend, with `Beast2IntakeError` in the words every runner uses. What e3's intake units run, on every runner |
27
+ | `new Beast2Writer(T, sink)` · `encodeBeast2SegmentsFor(T)(batches)` | Segments of your own choosing: one per non-empty batch, Set/Dict batches in ascending order |
28
+ | `iterBeast2SegmentsFor(T)(blob)` | One decoded segment at a time |
29
+ | `openBeast2PagesFor(T)(source)` | Random access: `.elementCount` and `.segmentCount` in O(1); `.segment(i)`, `.element(row)`, `.slice(offset, limit)` and `.get(key)` decode only the segments they touch |
30
+ | `openBeast2LazyFor(T, { frozen? })(source)` | An ordinary `SortedMap` / `SortedSet` / array served from the index, a segment at a time — what `blob.openBeast` and `FileSystem.openBeast` return, frozen. `isBeast2LazySafe(T, { frozen })`: only `Ref`- and function-bearing shapes decode whole |
31
+ | `Beast2SyncRangeReader` — `{ size, read(offset, length) }` | Positioned reads, wherever a blob goes, so a file's bytes never sit on the heap whole; `readBeast2Extents` / `readBeast2ExtentsRanged` (HTTP, S3) read the geometry |
32
+ | **Segment manifests** — one object per segment plus a manifest naming them: how e3 stores a collection |
33
+ | `new Beast2ManifestWriter(T, { object(hash, bytes), manifest(bytes) })` | The canonical manifest directory: the header, each segment as it is cut (named by its SHA-256, `<manifest>.segments/<hash>.beast2` on disk), then the manifest; east-c and east-py write the same |
34
+ | `readBeast2Manifest(source)` | The manifest a blob holds (`{ kind, level, type, rule, header, entries: [{ hash, fence, count, bytes }] }`), or `null`; `manifestElementCount` / `manifestByteSize` total it |
35
+ | `{ manifest, segment(i) }` (`Beast2ManifestSource`) | A manifest-held collection wherever a blob goes; `segment(i)` is called only for a segment a read decodes |
36
+ | `spliceBeast2Segments(head, segments)` | A manifest's segments spliced back into one blob, a chunk at a time, decoding nothing and holding only the segment in hand — how e3-api-client downloads a collection. Throws on a segment that is not a segmented, indexed v5 collection, or was written under another header |
37
+ | `sha256Hex(bytes)` | SHA-256 in lowercase hex, pure TypeScript |
38
+
39
+ ```typescript
40
+ import { ArrayType, StructType, StringType, IntegerType,
41
+ Beast2ElementWriter, iterBeast2SegmentsFor, openBeast2PagesFor, decodeBeast2For } from "@elaraai/east";
42
+
43
+ const Rows = ArrayType(StructType({ id: IntegerType, name: StringType }));
44
+
45
+ const out = createWriteStream("rows.beast2");
46
+ const writer = new Beast2ElementWriter(Rows, bytes => out.write(bytes));
47
+ for (const row of rows) writer.add(row); // never whole in memory
48
+ writer.finish();
49
+
50
+ const blob = new Uint8Array(readFileSync("rows.beast2"));
51
+ decodeBeast2For(Rows)(blob); // whole: segments concatenate
52
+ for (const segment of iterBeast2SegmentsFor(Rows)(blob)) consume(segment); // a segment at a time
53
+ openBeast2PagesFor(Rows)(blob).element(4_000_000); // one row: decodes ONE segment
54
+ ```
55
+
56
+ - Set and Dict segments hold the canonical value — strictly ascending,
57
+ disjoint, no duplicate keys — so writers refuse out-of-order elements and
58
+ decoders refuse a non-canonical blob as corrupt.
59
+ - **One encoding per value**: the element writer and `encodeBeast2PagedFor` cut
60
+ by the same rule in every runtime, so equal values are equal bytes and share
61
+ segments in a content-addressed store. `Beast2Writer` writes the batches you
62
+ give it, which every reader also reads.
63
+ - Streaming and paging are v5 only. The writer's segments are self-contained,
64
+ with an index, which paging and parallel decode need: pass
65
+ `{ selfContained: false }` only when aliasing must span segments. `write()`
66
+ skips an empty batch, so a segment count is never zero.
67
+ - Inside a body, `blob.openBeast(T)` and `FileSystem.openBeast(T, path)` hand
68
+ back the frozen lazy value a runner gives a large task input: keyed reads and
69
+ `$.for` cost one segment, mutation is refused (`.copy()` first), and the
70
+ header type is checked against `T` first. `East.Blob.encodeBeast` writes no
71
+ index, so a paged blob comes from the writers above or a runner's output.