@elaraai/east-claude-plugin 1.0.99 → 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 (32) hide show
  1. package/.build/mcp/server.js +3 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/index.json +12207 -216
  4. package/package.json +1 -1
  5. package/skills/e3/SKILL.md +29 -802
  6. package/skills/e3/reference/browser.md +93 -0
  7. package/skills/e3/reference/cli.md +120 -0
  8. package/skills/e3/reference/from-code.md +48 -0
  9. package/skills/e3/reference/rack.md +139 -0
  10. package/skills/e3/reference/record-indexes.md +326 -0
  11. package/skills/e3/reference/record-migrations.md +63 -0
  12. package/skills/e3/reference/repository.md +25 -0
  13. package/skills/east/SKILL.md +30 -459
  14. package/skills/east/reference/beast2.md +71 -0
  15. package/skills/east/reference/cross-language-and-json-schema.md +87 -0
  16. package/skills/east/reference/queries.md +46 -0
  17. package/skills/east/reference/runners-and-ir.md +148 -0
  18. package/skills/east/reference/trees-and-recursion.md +56 -0
  19. package/skills/east/reference/values-in-typescript.md +62 -0
  20. package/skills/east-py/SKILL.md +28 -1647
  21. package/skills/east-py/reference/beast2.md +108 -0
  22. package/skills/east-py/reference/cli.md +51 -0
  23. package/skills/east-py/reference/columns-and-platform-functions.md +87 -0
  24. package/skills/east-py/reference/control-flow-and-queries.md +147 -0
  25. package/skills/east-py/reference/cross-language.md +70 -0
  26. package/skills/east-py/reference/ir-and-diagnostics.md +150 -0
  27. package/skills/east-py/reference/patterns.md +369 -0
  28. package/skills/east-py/reference/performance.md +70 -0
  29. package/skills/east-py/reference/value-methods.md +306 -0
  30. package/skills/east-py/reference/values-construction-and-json.md +88 -0
  31. package/skills/east-py/reference/writing-east-functions.md +275 -0
  32. package/skills/east-py-datascience/SKILL.md +11 -28
@@ -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.
@@ -0,0 +1,87 @@
1
+ # Cross-language functions and JSON contracts
2
+
3
+ `East.importFunction` / `East.exportFunctions`, and `jsonSchemaFor` / `typeFromJsonSchema`. Part of the `east` skill.
4
+
5
+ ## Cross-language functions: `East.importFunction` / `East.exportFunctions`
6
+
7
+ A function authored in python (with its `East.function`) is called from a
8
+ TypeScript body by name, and the other way round; the deployed program is pure
9
+ IR. Functions travel as a **manifest** — their IR, declared types and platform
10
+ dependencies. In an e3 project `e3.export` builds and links manifests itself
11
+ (see **e3**); by hand:
12
+
13
+ ```bash
14
+ east-py export-functions pricing -o pricing.functions.beast2 -p east-py-std # python side
15
+ east-node export-functions dist/maths.js -o maths.functions.beast2 # reads the module's `eastFunctions`
16
+ ```
17
+
18
+ ```typescript
19
+ const score = East.importFunction("pricing", "score", FunctionType([RowType], FloatType)); // typed, callable
20
+ const total = East.function([ArrayType(RowType)], FloatType, ($, rows) => rows.map(($, r) => score(r)).sum());
21
+
22
+ // in-process: link, then compile — linkImports returns a sync or async function's IR
23
+ const { ir } = East.linkImports(total, [East.decodeFunctionManifest(readFileSync("pricing.functions.beast2"))]);
24
+ new EastIR(ir as FunctionIR).compile([])(rows);
25
+
26
+ // export TypeScript functions for python to import
27
+ writeFileSync("maths.functions.beast2", East.encodeFunctionManifest(East.exportFunctions("maths", "1.0.0", { double, halve })));
28
+ ```
29
+
30
+ - The declared type must equal the exported type **exactly** — a mismatch is a
31
+ build error naming both. Unlinked, the reference is a `Platform` node named
32
+ `east.importFunction`, so compiling it without linking fails loudly.
33
+ - Only closed values export: no captures, and no unresolved imports of their own.
34
+ - A manifest carries each function's source map: `linkImports` adds its
35
+ locations to the importer's map, which it returns as `sourceMap`, so an
36
+ error or a profile names the exporter's source — the python line included.
37
+ - The imported function's platform calls must be provided by the consuming
38
+ runner (stock families count across runtimes: `east-py-std` ≡
39
+ `@elaraai/east-node-std` ≡ `east-c-std`). See `docs/conventions/EAST_CODEGEN.md`.
40
+
41
+ ## A JSON contract for a type: `jsonSchemaFor` / `typeFromJsonSchema`
42
+
43
+ `East.String.printJson` writes East JSON, lossless and self-describing.
44
+ `jsonSchemaFor(T)` emits the JSON Schema of exactly that encoding, so a third
45
+ party can validate a payload before sending it; `typeFromJsonSchema` goes the
46
+ other way. Both are host-side, like `compareFor`.
47
+
48
+ ```typescript
49
+ writeFileSync("contract.schema.json", JSON.stringify(jsonSchemaFor(ArrayType(ReadingType), { draft: "draft-07" }), null, 2));
50
+ const T = typeFromJsonSchema(JSON.parse(readFileSync("partner.schema.json", "utf-8")));
51
+ ```
52
+
53
+ | Signature | Description |
54
+ |-----------|-------------|
55
+ | `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` |
56
+ | `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, …) |
57
+ | `EAST_JSON_PATTERNS` | The lexical forms of East JSON's scalars (`integer`, `blob`, `floatSpecials`), for a reader enforcing exactly what the schema says |
58
+
59
+ - **An `Option<T>` is `null` or `T`'s own encoding** wherever `T` can never
60
+ encode as `null` (`none` → `null`, `some(7n)` → `"7"`; a struct field is
61
+ `"note": null` or `"note": "x"`, and an absent key is still an error). Only
62
+ `Option<Null>` and `Option<Option<T>>` keep the tagged
63
+ `{"type": …, "value": …}` object. A `Recursive` payload is judged by what it
64
+ wraps. The rule is a total function of the type, the same on every runtime,
65
+ and the tagged object under a flat Option is refused by the payload's decoder
66
+ (`expected string, got {"type":"none","value":null}`).
67
+ - **A `DateTime` is `format: "date-time"`**: every decoder reads any RFC 3339
68
+ date-time — `Z` or any offset (read as UTC), `t`/`z` in either case, any
69
+ fractional digits (past the millisecond dropped, never rounded), a leap second
70
+ as the Unix time its fields add up to — in years 0001–9999. `printJson`
71
+ writes one form: UTC, three fractional digits, `+00:00`.
72
+ - **Other scalars are described as the encoder emits them**: an `Integer` a
73
+ quoted i64 decimal, a `Blob` lowercase hex (`parseJson` also takes uppercase,
74
+ which the contract does not), so a producer that validates never sends what
75
+ the strict reader rejects. The patterns spell digits `[0-9]`, so python and
76
+ JavaScript validators accept the same strings.
77
+ - **The document is deterministic and identical across languages** (east-py's
78
+ `json_schema_for` emits the same bytes), and its `x-east-type` annotations
79
+ make the inverse exact: `Set` vs `Array`, `Dict` vs an array of pairs, a flat
80
+ Option vs any `oneOf`. A foreign schema without them converts by a
81
+ documented structural mapping; `nullable: true`, `["string", "null"]` and a
82
+ `oneOf` of null and one schema read as an Option, and a
83
+ `format: "date-time"` string as a `DateTime`.
84
+ - **Recursion binds one `RecursiveType` per cycle group**; definitions that
85
+ need two binders are refused, naming them.
86
+ - To READ a JSON document larger than memory under a contract, use `Json.open`
87
+ / `Json.next` from **east-node-std** (and its east-py-std and east-c-std twins).
@@ -0,0 +1,46 @@
1
+ # Queries (typed jq)
2
+
3
+ `East.jq` inside an East body, and the host-side checker. Part of the `east` skill.
4
+
5
+ ## Queries (typed jq)
6
+
7
+ `East.jq` asks a question of a value in jq, inside an East body. The program is
8
+ parsed, type-checked against the input's East type and translated to ordinary
9
+ IR when the function is built, so it runs on every runner; a mistake is a
10
+ `QueryError` at build time, with the checker's sentence, span and fixes. The
11
+ language is jq 1.8 over East values: `libs/east/devdocs/QUERY.md` in the East
12
+ repository says what each builtin does, and every place East differs from jq.
13
+
14
+ ```typescript
15
+ const Order = StructType({ id: IntegerType, total: FloatType });
16
+
17
+ const bigOrders = East.function([ArrayType(Order)], ArrayType(IntegerType), ($, orders) =>
18
+ East.jq(orders, "[.[] | select(.total > 1000) | .id]", ArrayType(IntegerType)));
19
+
20
+ // An object of inputs is read as an e3 root: `.orders` is that input alone, so a lazy one stays lazy.
21
+ const revenue = East.function([ArrayType(Order)], FloatType, ($, orders) =>
22
+ East.jq({ orders }, ".orders | map(.total) | add", FloatType));
23
+ ```
24
+
25
+ | Signature | Description | Example |
26
+ |-----------|-------------|---------|
27
+ | **In an East body** |
28
+ | `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()` |
29
+ | **Host-side** |
30
+ | `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` |
31
+ | `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)` |
32
+ | `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` |
33
+ | `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(), [])` |
34
+ | `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) })` |
35
+ | `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` |
36
+
37
+ - **The values are East's.** A Dict is an object keyed by its key type, so
38
+ `.byId[1035]` looks up an Integer key; a variant reads as `{type, value}`,
39
+ and `select(.status.type == "shipped")` narrows it; an Option is `null` or
40
+ its value. `.name` on a struct that lacks the field is an error (`.name?`
41
+ gives `null`), and so is comparing values of two types.
42
+ - **A lookup can miss**, so `.[i]` and `.[k]` give an `Option`, and
43
+ `first(f)` gives at most one output, so its result type is `Option<T>`.
44
+ - **In code a query is a call of the `Query` builtin**, which carries the
45
+ program as written beside its translation: every runtime runs it as any
46
+ builtin, and `East.toSource` prints it back as `East.jq(…)`.
@@ -0,0 +1,148 @@
1
+ # Runners, units, IR and checking a module
2
+
3
+ Running a program from the command line, a runner-protocol unit in a host of your own, IR to source, and `east-node check`. Part of the `east` skill.
4
+
5
+ ## Contents
6
+ - Runners: running a program from the command line
7
+ - Units in a host of your own: `executeUnit` and `UnitIO`
8
+ - IR → source: `East.toSource` and `east-node transpile`
9
+ - Checking that a module builds: `east-node check`
10
+
11
+ ## Runners: running a program from the command line
12
+
13
+ A program's IR runs on any of three runners, which take the same arguments.
14
+ Write it with `writeFileSync("prog.beast2", encodeEastIR(fn.toIR()))` — which
15
+ keeps its source map, so errors name your lines — or `fn.toIR().toJSON()`.
16
+
17
+ ```bash
18
+ east-node run prog.beast2 -p @elaraai/east-node-std -i a.beast2 -i b.beast2 -o out.beast2 # -v: timing and peak memory
19
+ east-c run prog.beast2 -p east-c-std -i a.beast2 -o out.beast2 [--profile] # native; --profile: time per function
20
+ east-py run prog.beast2 -p east-py-std -i a.beast2 -o out.beast2 [--profile] # python (the east-py skill)
21
+ ```
22
+
23
+ - `--profile` (east-c and east-py, `run` and `exec`) prints every East
24
+ function and platform function the work called, by self time, with its
25
+ calls and where it is defined and called. `EAST_PROFILE=1` profiles without
26
+ the flag — e3 hands its environment to the runners, so a task's report
27
+ lands in its log — and `EAST_PROFILE_INTERVAL=N` prints the profile so far
28
+ every N seconds while the work runs.
29
+ - Each `-i` is one argument, in parameter order, and each `-p` loads one
30
+ platform package (east-node needs at least one); `-o` writes the result,
31
+ else it is printed. IR and values may be `.beast2`, `.beast`, `.east` or
32
+ `.json`.
33
+ - `exec <unit>` is the runner protocol e3 speaks: it runs one unit file — a
34
+ program over its inputs, a merge of an output's parts, or the intake of a
35
+ delivered collection — writes the output by its kind and records a result;
36
+ exit 0 when it succeeded, 1 when it failed.
37
+ - `version [-p <package>…]` prints the runner's version and each package's.
38
+ - east-c also has `convert <file> [-o <file>] [--type <east-type>]`, a value
39
+ from one format to another, and the IR toolbox: `ir normalize` (the canonical
40
+ form — the round-trip equality contract), `ir diff a b` (the first structural
41
+ difference) and `ir convert` (JSON ↔ beast2, the source map kept).
42
+ - east-node also has `transpile`, `check`, `lsp` and `export-functions`, below;
43
+ east-py has the python twins, and `lint`.
44
+
45
+ ### Units in a host of your own: `executeUnit` and `UnitIO`
46
+
47
+ east-node's `exec` is a file wrapper around `executeUnit`, the one TypeScript
48
+ unit runner, which does a unit's work over a `UnitIO` rather than a file
49
+ system — so a host with none runs units too: e3-web runs them in a browser's
50
+ Web Workers. east-c has the same shape (`east/unit.h`).
51
+
52
+ | Signature | Description |
53
+ |-----------|-------------|
54
+ | `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 |
55
+ | `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 |
56
+ | `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 |
57
+ | `options.report?: UnitRunReport` | Hears how a run unit's inputs open and what reading them came to: the account `exec -v` prints |
58
+ | `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 |
59
+ | `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 |
60
+ | `UnitType` · `UnitResultType` | The unit and its result as East types, for a host that keeps them as beast2 |
61
+
62
+ - The unit's thread grant (`threads`) caps the frame pool for the rest of the
63
+ process: a grant of one frames every output inline.
64
+ - A unit whose host places segments as they are read (`fetch: true`) has
65
+ `io.segment` asked for each segment of a staged manifest before it is read.
66
+ `InMemoryUnitIO` holds every segment already, so its unit says `fetch: false`,
67
+ and a segment it does not hold fails at once, as any absent file does.
68
+
69
+ ```typescript
70
+ import { East, IntegerType, InMemoryUnitIO, decodeBeast2For, encodeBeast2For, encodeEastIR, executeUnit, variant, type Unit } from "@elaraai/east";
71
+
72
+ const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
73
+
74
+ // A unit names its files by path: here, the program, its one input and its output.
75
+ const unit: Unit = {
76
+ work: variant("run", {
77
+ program: "program.beast2",
78
+ inputs: ["x.beast2"],
79
+ output: variant("value", "out.beast2"),
80
+ decode: variant("lazy", null),
81
+ }),
82
+ platforms: [], // the platform packages it needs, each resolved by `options.platforms`
83
+ threads: 1n,
84
+ fetch: false, // every segment it reads is in the IO already
85
+ result: "result.beast2",
86
+ };
87
+ const io = new InMemoryUnitIO([
88
+ ["program.beast2", encodeEastIR(double.toIR())],
89
+ ["x.beast2", encodeBeast2For(IntegerType)(21n)],
90
+ ]);
91
+ const result = await executeUnit(unit, io, { platforms: () => [] });
92
+ // result.outcome: ok, or failed with its message and source locations; a failure is never thrown
93
+ decodeBeast2For(IntegerType)(io.read("out.beast2")); // 42n
94
+ ```
95
+
96
+ ## IR → source: `East.toSource` and `east-node transpile`
97
+
98
+ Any IR — yours, or one exported from python — prints back as a module whose
99
+ `East.function(...)` rebuilds the same IR (equal under `east-c ir normalize`).
100
+
101
+ ```typescript
102
+ const double = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
103
+ console.log(East.toSource(double));
104
+ // import { East, … IntegerType } from "@elaraai/east";
105
+ // export const main = East.function([IntegerType], IntegerType, ($, x) => x.multiply(2n));
106
+ ```
107
+
108
+ ```bash
109
+ east-node transpile double.beast2 -o double.ts # any runtime's IR
110
+ east-node transpile ./ir/ -o ./ts/ --rebuild ./rebuilt/ # a directory, and the IR each module builds back
111
+ east-py transpile double.beast2 -o double.py # the python twin
112
+ ```
113
+
114
+ - Statements print as the `$`-forms, a bound value as `$.let(value, T)`, an
115
+ Option as `some(v)` / `none`, other values as `East.value(v, T)`, builtins as
116
+ their methods; one with no method spelling prints as
117
+ `East.builtin(name, [T...], [args], out)`, which rebuilds. `East.as(v, T)` and
118
+ `East.wrapRecursive(v, T)` spell the `As` and `WrapRecursive` nodes.
119
+ - A platform call prints as its library spells it when you pass the library:
120
+ `East.toSource(fn, { libraries: { "@elaraai/east-node-io": await import("@elaraai/east-node-io") } })`.
121
+ Without it the call hoists to a declaration named after the function
122
+ (`const tar_create = East.asyncPlatform("tar_create", …)`). The printer finds
123
+ the handles in the module's exports — each handle `East.platform` returns
124
+ carries its `name`, `inputs`, `output` and `async`, and
125
+ `isPlatformDeclaration(x)` tells one from a function.
126
+ - Variables keep the names you wrote, read by the TypeScript compiler (an
127
+ optional peer); without it, or in a browser, they print as `_N`, as does a
128
+ slot the body did not name (`($, x) =>` for a `(value, index)` callback).
129
+ - The contract and the round-trip suites: `docs/conventions/EAST_CODEGEN.md`.
130
+
131
+ ## Checking that a module builds: `east-node check`
132
+
133
+ `tsc` type-checks the TypeScript around a program, not the program: East's own
134
+ refusals — a body whose type differs from the declared output, a capture the
135
+ builder refuses, an `IRAnalysisError` — happen when `East.function` runs, at
136
+ import.
137
+
138
+ ```bash
139
+ east-node check dist/index.js # every broken function, at its line
140
+ east-node check dist/index.js --format json # the records `east-py check --format json` emits
141
+ east-node lsp # the diagnostics over stdio (needs @elaraai/east-diagnostics)
142
+ ```
143
+
144
+ It reports **every** broken function, not the first. Importing a module runs
145
+ it, so `EAST_CHECK=1` is set meanwhile, and a module should skip import-time
146
+ work when it sees it. Edit-time idiom checks are separate:
147
+ `@elaraai/tsserver-plugin-east` in the editor, `@elaraai/eslint-plugin-east`'s
148
+ `east/east-rules` in `make lint`.
@@ -0,0 +1,56 @@
1
+ # Trees from flat rows, and recursion
2
+
3
+ `toTree`, `flattenTree`, and when recursion is safe. Part of the `east` skill.
4
+
5
+ ## Trees from flat rows, and recursion
6
+
7
+ `toTree` builds nested data — any depth, your own node type — from rows that
8
+ name their key and their parent's key. `build` runs once per row, children
9
+ first; the result is the roots in source order.
10
+
11
+ ```typescript
12
+ const Row = StructType({ id: StringType, parent: OptionType(StringType), name: StringType });
13
+ const Node = RecursiveType(self => StructType({ name: StringType, children: ArrayType(self) }));
14
+
15
+ const nest = East.function([ArrayType(Row)], ArrayType(Node), ($, rows) =>
16
+ rows.toTree(Node, ($, r) => r.id, ($, r) => r.parent, ($, r, _i, children) => ({ name: r.name, children })));
17
+ ```
18
+
19
+ - A row whose parent is `none` is a root, and so is an orphan (its parent key
20
+ is not in the rows). A repeated key ❗ `toTree: duplicate key <key>`; a cycle
21
+ ❗ `toTree: cycle through key <key>` — both before any `build`.
22
+ - A non-recursive `Node` folds bottom-up: `IntegerType` with
23
+ `children.sum().add(1n)` is each root's subtree size.
24
+
25
+ `flattenTree` is the way back: every node of nested data, each with its path
26
+ of keys (a Dict's) or indices (an Array's) from its root, depth first — each
27
+ node before its descendants, siblings in key or array order. `children` names
28
+ a node's children, of the collection's own type.
29
+
30
+ ```typescript
31
+ const Part = RecursiveType(self => StructType({ name: StringType, cost: FloatType, parts: DictType(StringType, self) }));
32
+
33
+ const flat = East.function([DictType(StringType, Part)], ArrayType(StructType({ path: ArrayType(StringType), name: StringType })), ($, parts) =>
34
+ parts.flattenTree(($, part) => part.unwrap().parts).map(($, e) => ({ path: e.path, name: e.node.unwrap().name })));
35
+ // [{ path: ["pump"], … }, { path: ["pump", "motor"], … }, { path: ["pump", "motor", "rotor"], … }, { path: ["pump", "seal"], … }]
36
+
37
+ // A rollup at every root: toTree over the paths, a node's parent its path less its last key
38
+ const totals = East.function([DictType(StringType, Part)], ArrayType(FloatType), ($, parts) =>
39
+ parts.flattenTree(($, part) => part.unwrap().parts).toTree(FloatType,
40
+ ($, e) => e.path,
41
+ ($, e) => East.greater(e.path.size(), 1n).ifElse(() => some(e.path.slice(0n, e.path.size().subtract(1n))), () => none),
42
+ ($, e, _i, children) => children.sum().add(e.node.unwrap().cost)));
43
+ ```
44
+
45
+ - The result is an Array of `{ path, node }`, not a Dict keyed by path: a Dict
46
+ key is immutable, and a path is an Array.
47
+ - `children` runs once per node, in the result's order. The walk is iterative;
48
+ each path is an array of its own, so a chain n deep holds n(n+1)/2 path
49
+ elements.
50
+ - **Recursion** — a function calling itself through a captured variable
51
+ (`$.let(fn, FunctionType(...))`, then `$.assign(fn, East.function(...))`) —
52
+ is for shallow depths, in the hundreds. Deeper, a call ❗
53
+ `call stack exhausted: East calls nested too deeply`, a catchable error on
54
+ every runtime. Walk deep data with `$.while`, `toTree` or `flattenTree`, which are iterative;
55
+ the C and Python runtimes still bound a recursive VALUE's depth to tens of
56
+ thousands of levels.
@@ -0,0 +1,62 @@
1
+ # East values in TypeScript
2
+
3
+ Host-side functions over East values (no expression, no IR), and building programs over names and types held as values. Part of the `east` skill.
4
+
5
+ ## Values in TypeScript
6
+
7
+ Host-side functions for East values — build time, no expression, no IR. Each
8
+ takes the type and returns a function (`compareFor(T)(a, b)`).
9
+
10
+ | Signature | Description |
11
+ |-----------|-------------|
12
+ | **Comparing** |
13
+ | `compareFor(T)(a, b): -1 \| 0 \| 1` | East's total order — the order sets, dicts and `.sort()` use (NaN last) |
14
+ | `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 |
15
+ | `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 |
16
+ | `new SortedMap(entries?, compareFor(K))` · `new SortedSet(values?, compareFor(K))` | `Map` / `Set` in East order — what decoders return |
17
+ | **Types** |
18
+ | `isValueOf(value, T)` | Whether a TypeScript value is a value of `T` |
19
+ | `printType(T)` · `isTypeEqual(a, b)` · `isSubtype(a, b)` | Print a type in East text; compare types |
20
+ | `diffTypes(actual, expected)` → `renderTypeDiff(diffs)` | The differences between two types, each with its path |
21
+ | `defaultValue(T)` · `minimalValue(T)` | A value of `T`: its default, and its least |
22
+ | **Codecs** — each `…For(T)` builds a codec for one type |
23
+ | `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]` |
24
+ | `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) |
25
+ | `encodeCsvFor(StructT, config?)(rows)` · `decodeCsvFor(StructT, config?)(blob)` | An array of structs ↔ CSV bytes |
26
+ | `encodeBeast2For(T)(value)` · `decodeBeast2For(T)(blob)` | beast2, the binary form every runtime and e3 store — see below |
27
+
28
+ ## Names and types held as values
29
+
30
+ East names a struct's fields and a variant's cases where its code is written:
31
+ `row.site`, `variant("load", x)`. Code that builds programs over types it is
32
+ handed — a library generating a program for whatever record its caller
33
+ declares — holds the names, and the types, as values. These build the same
34
+ program pieces from them, and check each name against the type as the program
35
+ is built: a name the type lacks fails there, naming the name and the type
36
+ (`East.field: .Struct {name, site, status} has no field 'job'`). Nothing in the
37
+ program differs from what the written form builds, so every runtime runs it.
38
+
39
+ ```typescript
40
+ const Machine = StructType({ name: StringType, site: StringType, status: StringType });
41
+ const Reading = VariantType({ load: FloatType, output: FloatType });
42
+
43
+ const site = "site"; // a name a caller hands you
44
+ const siteOf = East.function([Machine], StringType, ($, machine) =>
45
+ East.field(machine, site, StringType)); // typed by the type given, which is checked
46
+
47
+ const stopped = East.function([Machine], Machine, ($, machine) =>
48
+ East.withField(machine, "status", "down")); // the other fields as they are
49
+
50
+ const Place = StructType({ site: StringType, name: StringType });
51
+ const placeOf = East.function([Machine], Place, ($, machine) =>
52
+ East.struct(Place, East.fields(machine, ["site", "name"]))); // by name, or in the type's order
53
+
54
+ const readingOf = (place: string) => East.function([FloatType], Reading, ($, x) =>
55
+ East.inCase(Reading, place, x)); // a variant's case by name
56
+
57
+ const { diff, apply, compose, invert } = East.patchesFor(Machine); // East functions of a type held as a value
58
+ ```
59
+
60
+ A recursive value is read, and rebuilt, through its node. `fields` and
61
+ `withField` read the value once a field: give them a variable, not a
62
+ computation. Code whose names are written keeps writing `row.site`.