@elaraai/east-claude-plugin 1.0.98 → 1.0.99

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.
@@ -120,7 +120,8 @@ Task → What do you need?
120
120
  │ │ ├─ Convert → .stringJoin(), .toSet(), .toDict(), .flattenToSet(), .flattenToDict(), .encodeCsv()
121
121
  │ │ ├─ Group → .groupReduce(), .groupSize(), .groupSum(), .groupMean(), .groupMinimum(), .groupMaximum(),
122
122
  │ │ │ .groupToArrays(), .groupToSets(), .groupToDicts(), .groupEvery(), .groupSome()
123
- │ │ └─ Tree → .toTree(Node, key, parent, build) — flat parent-keyed rows to nested nodes (below)
123
+ │ │ └─ Tree → .toTree(Node, key, parent, build) — flat parent-keyed rows to nested nodes;
124
+ │ │ .flattenTree(children) — nested nodes back to { path, node } rows, depth first (below)
124
125
  │ ├─ Set
125
126
  │ │ ├─ Read and mutate → .size(), .has(), .insert(), .tryInsert(), .delete(), .tryDelete(), .clear(), .unionInPlace()
126
127
  │ │ ├─ Set ops → .copy(), .union(), .intersection(), .difference(), .symmetricDifference(), .isSubsetOf(),
@@ -129,6 +130,7 @@ Task → What do you need?
129
130
  │ │ reduce .reduce(), .scan(), .every(), .some(), .sum(), .mean();
130
131
  │ │ convert .toArray(), .toSet(), .toDict(), .flattenToArray(), .flattenToSet(), .flattenToDict(),
131
132
  │ │ .toTree(Node, parent, build) (an element / a key is its own key; Dict callbacks take (value, key));
133
+ │ │ Dict only: .flattenTree(children), the nodes back again (below);
132
134
  │ │ group .groupReduce(), .groupSize(), .groupSum(), .groupMean(), .groupToArrays(), .groupToSets(),
133
135
  │ │ .groupToDicts(), .groupEvery(), .groupSome()
134
136
  │ ├─ Dict → .size(), .has(), .get(), .tryGet(), .keys(), .getKeys(), .insert(), .insertOrUpdate(), .update(),
@@ -164,6 +166,9 @@ Task → What do you need?
164
166
  │ East.is() is identity for mutable collections — FROZEN ones (task inputs) compare by value
165
167
  ├─ Patches (East.*) → East.diff(before, after), East.applyPatch(value, patch),
166
168
  │ East.composePatch(first, second, type), East.invertPatch(patch, type)
169
+ ├─ Build East over types you are handed (a library, a generator) → East.field(v, name, T?), East.fields(v, names?),
170
+ │ East.struct(T, fields), East.withField(v, name, x), East.inCase(T, name, x), East.patchesFor(T): each name
171
+ │ checked as the program builds → "Names and types held as values"
167
172
  │
168
173
  ├─ Ask a question of a value in jq → East.jq(input, program, resultType), checked when the program
169
174
  │ builds → "Queries (typed jq)"
@@ -239,6 +244,42 @@ A variant is built with `variant()`, `some()` or `none` — never a
239
244
  `{ type, value }` literal, which lacks the marker the encoders read. Variants are
240
245
  encoded, compared and validated by case **name**.
241
246
 
247
+ ### Names and types held as values
248
+
249
+ East names a struct's fields and a variant's cases where its code is written:
250
+ `row.site`, `variant("load", x)`. Code that builds programs over types it is
251
+ handed — a library generating a program for whatever record its caller
252
+ declares — holds the names, and the types, as values. These build the same
253
+ program pieces from them, and check each name against the type as the program
254
+ is built: a name the type lacks fails there, naming the name and the type
255
+ (`East.field: .Struct {name, site, status} has no field 'job'`). Nothing in the
256
+ program differs from what the written form builds, so every runtime runs it.
257
+
258
+ ```typescript
259
+ const Machine = StructType({ name: StringType, site: StringType, status: StringType });
260
+ const Reading = VariantType({ load: FloatType, output: FloatType });
261
+
262
+ const site = "site"; // a name a caller hands you
263
+ const siteOf = East.function([Machine], StringType, ($, machine) =>
264
+ East.field(machine, site, StringType)); // typed by the type given, which is checked
265
+
266
+ const stopped = East.function([Machine], Machine, ($, machine) =>
267
+ East.withField(machine, "status", "down")); // the other fields as they are
268
+
269
+ const Place = StructType({ site: StringType, name: StringType });
270
+ const placeOf = East.function([Machine], Place, ($, machine) =>
271
+ East.struct(Place, East.fields(machine, ["site", "name"]))); // by name, or in the type's order
272
+
273
+ const readingOf = (place: string) => East.function([FloatType], Reading, ($, x) =>
274
+ East.inCase(Reading, place, x)); // a variant's case by name
275
+
276
+ const { diff, apply, compose, invert } = East.patchesFor(Machine); // East functions of a type held as a value
277
+ ```
278
+
279
+ A recursive value is read, and rebuilt, through its node. `fields` and
280
+ `withField` read the value once a field: give them a variable, not a
281
+ computation. Code whose names are written keeps writing `row.site`.
282
+
242
283
  ### Error handling
243
284
 
244
285
  ```typescript
@@ -268,11 +309,37 @@ const nest = East.function([ArrayType(Row)], ArrayType(Node), ($, rows) =>
268
309
  ❗ `toTree: cycle through key <key>` — both before any `build`.
269
310
  - A non-recursive `Node` folds bottom-up: `IntegerType` with
270
311
  `children.sum().add(1n)` is each root's subtree size.
312
+
313
+ `flattenTree` is the way back: every node of nested data, each with its path
314
+ of keys (a Dict's) or indices (an Array's) from its root, depth first — each
315
+ node before its descendants, siblings in key or array order. `children` names
316
+ a node's children, of the collection's own type.
317
+
318
+ ```typescript
319
+ const Part = RecursiveType(self => StructType({ name: StringType, cost: FloatType, parts: DictType(StringType, self) }));
320
+
321
+ const flat = East.function([DictType(StringType, Part)], ArrayType(StructType({ path: ArrayType(StringType), name: StringType })), ($, parts) =>
322
+ parts.flattenTree(($, part) => part.unwrap().parts).map(($, e) => ({ path: e.path, name: e.node.unwrap().name })));
323
+ // [{ path: ["pump"], … }, { path: ["pump", "motor"], … }, { path: ["pump", "motor", "rotor"], … }, { path: ["pump", "seal"], … }]
324
+
325
+ // A rollup at every root: toTree over the paths, a node's parent its path less its last key
326
+ const totals = East.function([DictType(StringType, Part)], ArrayType(FloatType), ($, parts) =>
327
+ parts.flattenTree(($, part) => part.unwrap().parts).toTree(FloatType,
328
+ ($, e) => e.path,
329
+ ($, e) => East.greater(e.path.size(), 1n).ifElse(() => some(e.path.slice(0n, e.path.size().subtract(1n))), () => none),
330
+ ($, e, _i, children) => children.sum().add(e.node.unwrap().cost)));
331
+ ```
332
+
333
+ - The result is an Array of `{ path, node }`, not a Dict keyed by path: a Dict
334
+ key is immutable, and a path is an Array.
335
+ - `children` runs once per node, in the result's order. The walk is iterative;
336
+ each path is an array of its own, so a chain n deep holds n(n+1)/2 path
337
+ elements.
271
338
  - **Recursion** — a function calling itself through a captured variable
272
339
  (`$.let(fn, FunctionType(...))`, then `$.assign(fn, East.function(...))`) —
273
340
  is for shallow depths, in the hundreds. Deeper, a call ❗
274
341
  `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;
342
+ every runtime. Walk deep data with `$.while`, `toTree` or `flattenTree`, which are iterative;
276
343
  the C and Python runtimes still bound a recursive VALUE's depth to tens of
277
344
  thousands of levels.
278
345
 
@@ -333,7 +333,6 @@ gh pr create --repo elaraai/east-workspace --title "fix(<lib>): …" --body "…
333
333
  - `hooks/useDatasetValue.ts` — `[...StateImpl, ...BindPlatform, ...OverlayImpl]`.
334
334
  - Import the impl from `@elaraai/east-ui-components/platform`. **Manifest-scoped** functions (data/func/record) go in via their `createScoped*(manifest…)` builder; **browser-local-state** functions (State, overlay, slice) go in **unscoped**, alongside `...StateImpl` / `...OverlayImpl`.
335
335
  - Grep guard before you call a UI platform fn done: `rg -n "MyImpl" libs/east-ui/packages/e3-ui-components/src` must hit `UITaskPreview.tsx` **and** `useDatasetValue.ts`, not just the registering module.
336
- - **Also rebuild + reinstall the `east-ui-preview` VS Code extension** — its webview bundles e3-ui-components, so a deployed package using the new fn renders blank / errors until the extension itself ships the array change. A version bump alone is not enough.
337
336
 
338
337
  ## Do NOT
339
338
 
@@ -132,6 +132,8 @@ Task → What do you need?
132
132
  │ │ East.break_ / continue_ / label → "Control flow — the expression forms", "Sequential logic that stays in east-c"
133
133
  │ ├─ Nest flat parent-keyed rows (any depth) → xs.to_tree(Node, key, parent, build) — not recursion, which is for
134
134
  │ │ shallow depths → "Trees from flat rows, and recursion"
135
+ │ ├─ Flatten nested data back to {path, node} rows, depth first → xs.flatten_tree(children) (an Array's or a Dict's)
136
+ │ │ → "Trees from flat rows, and recursion"
135
137
  │ ├─ Query a value with jq → East.jq(input, "jq", result_type) ❗ result_type is required, and types the result ·
136
138
  │ │ a dict of inputs is read as an e3 root → "Queries — East.jq"
137
139
  │ ├─ What a body may capture — and what it refuses, naming the binding → "What a body may reference"
@@ -325,13 +327,20 @@ or call it (a `Call`; in an `East.asyncFunction`, calling an async one is a
325
327
 
326
328
  **Error locations.** A build records the python frames that built each node,
327
329
  so a runtime error names its authoring site: `EastError.location` is the stack —
328
- the lambda's `file:line:column` first, then the `East.function(...)` call and
329
- its callers — and a platform-signature mismatch at `East.compile` names the call
330
- the same way. The map rides the function's beast2 encoding, so the error reads
331
- the same on east-c or east-node. Paths are relative to the working directory;
332
- `set_location_base_path(dir)` (from `east`) pins the base for reproducible
333
- fixtures. An error inside a callback a builtin invokes (`arr.map(...)` and
334
- friends) resolves to the builtin's call site, on every runner.
330
+ the lambda's `file:line:column` first, then any helper between, then the
331
+ `East.function(...)` call — and a platform-signature mismatch at `East.compile`
332
+ names the call the same way. The stack stops at that call: the code that
333
+ imported or called the module is not recorded. A recorded path is relative to
334
+ its file's project root, the nearest directory above it holding a
335
+ `pyproject.toml`, so a function's IR is the same whichever module imported it
336
+ and whatever directory the process ran in. A file in no project is relative to
337
+ the working directory, and `set_location_base_path(dir)` (from `east`) pins one
338
+ base for every path, for reproducible fixtures (`None` restores the default);
339
+ `east-py check` names a refusal's frames relative to the working directory, as
340
+ an editor opens them. The map rides the function's beast2 encoding, so the
341
+ error reads the same on east-c or east-node. An error inside a callback a
342
+ builtin invokes (`arr.map(...)` and friends) resolves to the builtin's call
343
+ site, on every runner.
335
344
  `set_location_capture(False)` (from `east`) builds without recording any
336
345
  locations: there is then no stack walk per node, and an error raised by such a
337
346
  function carries no location. Binding names are read separately and keep
@@ -854,6 +863,13 @@ const score = East.importFunction("pricing", "score", FunctionType([RowType], Fl
854
863
  await e3.export(pkg, "out.zip");
855
864
  ```
856
865
 
866
+ e3 exports each workspace package in one `east-py export-functions --batch`
867
+ process, however many tasks import from it, and keeps the manifests, so a
868
+ redeploy whose python has not changed runs no export (the **e3** skill). A
869
+ function's manifest entry is its own: the same bytes whatever else the process
870
+ built first and whatever directory it ran in, so a task embedding it keeps its
871
+ hash until the function, or a line it was written at, changes.
872
+
857
873
  For a package built elsewhere (published, another repo), or to link
858
874
  in-process, write the manifest where the package lives and pass it:
859
875
 
@@ -1117,7 +1133,7 @@ at the call. `.element_type` is the logical element type.
1117
1133
  | Search | `find_first(target, key=None) -> some/none` · `find_all(value, by=None) -> Array<Integer>` · `find_maximum/find_minimum(by=None) -> some(index)/none` · `find_sorted_first/last(target, key=None) -> int` · `find_sorted_range(target, key=None) -> {start,end}` · `first_map(fn(el)->some/none, out=None)` · `is_sorted(key=None) -> bool` |
1118
1134
  | Flatten | `flat_map(fn(el)->arr, out=None)` (`flatten_to_array` deprecated) · `flatten_to_set(fn(el)->set, out=None)` · `flatten_to_dict(fn(el)->dict, combine=None)` (a duplicate key errors without `combine`) |
1119
1135
  | Columnar | `to_columns(fields=None) -> dict` (numpy per numeric/bool column, `Option<Float>`→NaN, interned strings) · `EastArray.from_columns(element_type, columns)` *(static)* (C-side fill needs numpy columns — float64/int64/bool, `Option<Float>` as float64+NaN; python lists convert per cell) · `map_batches(fn(cols)->cols, out=None, batch_size=100_000)` |
1120
- | Convert | `to_tree(node, key(el[, i]), parent(el[, i])->some/none, build(el, i, children)) -> Array<node>` (flat parent-keyed rows → nested nodes of any depth, iterative — [Trees from flat rows](#trees-from-flat-rows-and-recursion)) · `string_join(sep) -> str` (String arrays) · `to_vector() -> EastVector` (Float/Integer/Boolean elements, in order — east-c VectorFromArray; the expression method emits the same builtin) · `encode_csv(config=None, **options) -> EastBlob` (Array of structs; east-c ArrayEncodeCsv) |
1136
+ | Convert | `to_tree(node, key(el[, i]), parent(el[, i])->some/none, build(el, i, children)) -> Array<node>` (flat parent-keyed rows → nested nodes of any depth, iterative — [Trees from flat rows](#trees-from-flat-rows-and-recursion)) · `flatten_tree(children(el[, i])) -> Array<{path, node}>` (nested nodes back: every node with its path of indices, depth first, iterative) · `string_join(sep) -> str` (String arrays) · `to_vector() -> EastVector` (Float/Integer/Boolean elements, in order — east-c VectorFromArray; the expression method emits the same builtin) · `encode_csv(config=None, **options) -> EastBlob` (Array of structs; east-c ArrayEncodeCsv) |
1121
1137
  | Mutate (in place, the TS names) | `push_last(item)` · `push_first(item)` · `append(array)` (ArrayAppend — a whole array, NOT one element) · `prepend(array)` · `pop_last()` · `pop_first()` · `update(i, item)` ❗bounds · `merge(i, value, update_fn(existing, incoming[, i]))` · `merge_all(array, merge_fn(existing, incoming[, i]))` · `clear()` · `sort_in_place(by=None)` · `reverse_in_place()` — plus the python protocol: `extend(iterable)` (bulk: one crossing; C-to-C for same-type East arrays, raw buffers for numpy) · `insert(i, item)` · `pop(i=-1)` · `remove(item)` · `count(value) -> int` · `index(value) -> int` |
1122
1138
 
1123
1139
  ### EastSet — complete method surface
@@ -1159,7 +1175,7 @@ in `other`'s values; `merge` takes a single differently-typed value.
1159
1175
  | Reduce | `reduce(fn(acc, value[, key]), init)` (TS order) · `scan(fn(acc, value[, key]), init) -> Array` (running fold in key order) · `map_reduce(map_fn(value[, key]), reduce_fn(a, b), out=None)` (raises on empty) · `sum(fn(value[, key])=None)` · `mean(fn(value[, key])=None) -> float` · `every(pred(value[, key])=None) -> bool` · `some(pred(value[, key])=None) -> bool` (native short-circuit) |
1160
1176
  | Group | `group_reduce(key_fn(value[, key]), init_fn(gk), fold_fn(acc, value[, key]), key_out=None, acc_out=None) -> Dict` · `group_size(key_fn)` · `group_sum(key_fn, fn=None)` · `group_mean(key_fn, fn=None)` · `group_every/group_some(key_fn, pred(value[, key]))` · `group_to_arrays/group_to_sets(key_fn, value_fn=None)` · `group_to_dicts(key_fn, key2_fn, value_fn=None, combine=None)` · ⚠️ `group_fold(...)` is the DEPRECATED alias of `group_reduce` |
1161
1177
  | Flatten | `flatten_to_array(fn(value[, key])->arr, out=None)` (the TS Dict name; `flat_map` is Array's and a deprecated spelling here) · `flatten_to_set(fn(value[, key])->set, out=None)` · `flatten_to_dict(fn(value[, key])->dict, combine=None)` (a duplicate key errors without `combine`) |
1162
- | Convert | `keys() -> Set` · `to_array(fn(value[, key]), out=None)` · `to_set(fn(value[, key]), out=None)` · `to_dict(key_fn, value_fn=None, combine=None, key_out=None, value_out=None)` (the value itself when `value_fn` is omitted; a duplicate key errors without `combine`) · `to_tree(node, parent(value[, key])->some/none, build(value, key, children)) -> Array<node>` (each entry's key its key) · `copy()` |
1178
+ | Convert | `keys() -> Set` · `to_array(fn(value[, key]), out=None)` · `to_set(fn(value[, key]), out=None)` · `to_dict(key_fn, value_fn=None, combine=None, key_out=None, value_out=None)` (the value itself when `value_fn` is omitted; a duplicate key errors without `combine`) · `to_tree(node, parent(value[, key])->some/none, build(value, key, children)) -> Array<node>` (each entry's key its key) · `flatten_tree(children(value[, key])) -> Array<{path, node}>` (nested nodes back: every node with its path of keys, depth first, iterative) · `copy()` |
1163
1179
  | Mutate (in place) | `d[k]=v` · `del d[k]` · `insert(k, v)` (errors if present) · `get_or_insert(k, fn(k))` · `insert_or_update(k, v, combine(existing, incoming, k))` · `update(k, v)` (TS; the read-modify-write `update(k, fn(current))` is deprecated) · `swap(k, v) -> prev` · `delete(k)` · `try_delete(k) -> bool` (`update`/`swap`/`delete` error on a missing key) · `pop(k, *default)` · `clear()` |
1164
1180
  | Bulk (in place) | `update_many(keys, values, combine(existing, incoming)=None)` — the whole batch crosses once; a pure/precompiled `combine` resolves collisions C-to-C (dicts as hot-loop accumulators) |
1165
1181
 
@@ -1598,7 +1614,7 @@ east-py version -p east-py-std # the runner's version, and each
1598
1614
  | `exec <unit> [-v] [--profile]` | The runner protocol: one unit file says what to run — a program over its inputs, read as its `decode` says (as `run --decode`), or a merge of an output's parts — and where; the output is written by its kind and a result recorded. Exit 0 when it succeeded, 1 when it failed (the message and locations also go to stderr), 2 when the unit cannot be read or the result written. Relative paths in the unit are relative to its directory |
1599
1615
  | `convert <file> [-o FILE.east] [-v]` | Decodes a beast2 value (its header names the type) and prints it as East text, or writes it to a `.east` file |
1600
1616
  | `transpile <ir> [-o FILE] [--name NAME] [-p PACKAGE]…` | IR as a python module rebuilding it (`--name` binds it, default `main`; each `-p` prints its calls as that package's own functions) — [IR ↔ python](#ir--python-east-py-transpile-and-the-east-c-ir-toolbox) |
1601
- | `export-functions <module> -o FILE [-p PACKAGE]… [--name NAME] [--package-version V] [--only NAME]…` | A module's (a dotted name, or a `.py` path) `east_functions` as a function manifest; every platform call must be provided by a `-p` package; `--name` defaults to the module's top-level name, the version to the installed distribution's (else `0.0.0`); `--only` exports just those — [Cross-language functions](#cross-language-functions-east-py-export-functions-and-eastimport_function) |
1617
+ | `export-functions <module> -o FILE [-p PACKAGE]… [--name NAME] [--package-version V] [--only NAME]…` | A module's (a dotted name, or a `.py` path) `east_functions` as a function manifest; every platform call must be provided by a `-p` package; `--name` defaults to the module's top-level name, the version to the installed distribution's (else `0.0.0`); `--only` exports just those, building only them and what they call (a sibling that does not build fails nothing). `--batch FILE` in place of `-o`/`-p`/`--only` writes several manifests from one import, FILE a beast2 batch (`FunctionExportBatchType`: each entry's `output`, `packages` and `only`) — what `e3.export` runs per workspace package — [Cross-language functions](#cross-language-functions-east-py-export-functions-and-eastimport_function) |
1602
1618
  | `lint [paths…] [--format text\|json] [--disable RULE]… [--exclude DIR]… [--list-rules]` | The rules (default path `.`); exit 1 on any finding, a warning included, 2 on a usage error — [Diagnostics](#diagnostics-at-edit-time--east-py-lint-east-py-check-flake8-east-py-lsp) |
1603
1619
  | `check <targets…> [--format text\|json] [--only-if-enabled]` | The build's own errors; exit 1 on any |
1604
1620
  | `lsp [--probe]` | The Language Server over stdio (needs `pygls`); `--probe` reports whether it can start here |
@@ -1899,10 +1915,30 @@ roots = rows.to_tree(Node,
1899
1915
  - A non-recursive `Node` folds bottom-up: `IntegerType` with
1900
1916
  `children.sum() + 1` is each root's subtree size. A Set's element and a
1901
1917
  Dict's key are their own keys, so those take no `key`.
1918
+
1919
+ `flatten_tree` is the way back: every node of nested data, each with its path
1920
+ of keys (a Dict's) or indices (an Array's) from its root, depth first — each
1921
+ node before its descendants, siblings in key or array order. `children` names a
1922
+ node's children, of the collection's own type:
1923
+
1924
+ ```python
1925
+ from east import DictType, FloatType, StringType, StructType, recursive_type
1926
+
1927
+ Part = recursive_type(lambda self: StructType([("name", StringType), ("cost", FloatType), ("parts", DictType(StringType, self))]))
1928
+
1929
+ nodes = parts.flatten_tree(lambda b, part: part.unwrap().parts)
1930
+ # an Array of {path, node}: ["pump"], ["pump", "motor"], ["pump", "motor", "rotor"], ["pump", "seal"]
1931
+ ```
1932
+
1933
+ - An Array of `{path, node}`, not a Dict keyed by path: a Dict key is
1934
+ immutable, and a path is an Array. `to_tree` over it, keyed by `path` and
1935
+ parented by `path` less its last step, rolls the tree up at every root.
1936
+ - `children` runs once per node, in the result's order. Each path is an array
1937
+ of its own, so a chain n deep holds n(n+1)/2 path elements.
1902
1938
  - **Recursion** — a function calling itself through a captured variable — is
1903
1939
  for shallow depths, in the hundreds. Deeper, a call ❗ `call stack exhausted:
1904
1940
  East calls nested too deeply`, a catchable `EastError` on every runtime. Walk
1905
- deep data with `b.while_` or `to_tree`; a recursive VALUE's depth is still
1941
+ deep data with `b.while_`, `to_tree` or `flatten_tree`; a recursive VALUE's depth is still
1906
1942
  bounded, at tens of thousands of levels, by east-c's recursive collect and
1907
1943
  free.
1908
1944
 
@@ -1997,6 +2033,12 @@ out = EastMatrix(FloatType, model(t).detach().cpu().numpy()) # bridge canonica
1997
2033
  - **Read a captured East collection with `.get(expr)` /
1998
2034
  `.get_or_default(expr, d)` / `.try_get(expr)`** — `[expr]` on a captured
1999
2035
  constant does not build IR (python coerces the index via `__index__`).
2036
+ - **A python set has no order of its own.** Under a Set type
2037
+ (`b.const({"a", "b"}, SetType(StringType))`, `East.new_set(T, {…})`) its
2038
+ elements are built in East's order of `T`, the order an `EastSet` keeps, so
2039
+ the IR is the same in every process; a list keeps the order it is written in.
2040
+ A python set holding a traced value (`{some(x)}`) has nothing to order by and
2041
+ is refused — write a list.
2000
2042
  - **Genuinely-python loops cross the boundary once** — `to_columns()` /
2001
2043
  `EastArray.from_columns` / `map_batches`, never a platform call or a decode per
2002
2044
  element.
@@ -138,10 +138,10 @@ Task → Which tag?
138
138
  │ │ │ ├─ variant (optional) — "tiles" (default) | "wireframe": each tile an outline at its size with its content left out, its spans kept at any width and its declared height exact — a page's thumbnail. It draws no frame of its own: its host frames it (a Library gallery card's media) and it fills the host's height. With no rows it is the blank page — a band over the body, each dashed
139
139
  │ │ │ ├─ width (optional) — the design width ("1440px"): a page's grid is that wide, scrolls inside a narrower host, and its spans answer to it; on the editing canvas it is the most the grid lays out at — a narrower column lays it out at the column's width, a wider one centres it — and the toolbar reads it out
140
140
  │ │ │ ├─ height / maxHeight (optional) — pin or cap the SnapGrid; the grid scrolls within
141
- │ │ │ ├─ edit + editing (optional, given together) — the builder's canvas: every tile keeps its declared span at any width, and each gesture is a DRAFT of the shared editing session — e3-ui's Sheet's and Plan's — each ONE undoable transaction (origin drop / move / resize / remove); the history item (Undo · Redo · Discard · Save) sits in the canvas's toolbar (see The frame), and ⌘Z / Ctrl+Z undo, ⌘⇧Z / Ctrl+Y redo. Editing takes an `Array` source — its order is the tiles' order, so a move is a new order and a new row key; a `Dict` is refused. `edit` names the row fields a gesture writes: { key: "id" (the String identity field), row: "row" (String), span: "span" (Integer), height?: "height" (Option<Integer>; omitted, heights are not edited), align?: "align" (SnapGrid.Types.Align; the field an align request writes — omitted, one is refused), create?: ($, card, at) => R — a dropped library card's new row: `card` the drag grammar's { library, key }, `at` { key (a fresh identity), row (the row it lands in) }; the canvas then writes `at.row` and fits the span }. `editing` is { onApply? | onUpdate?, onPatch?, onDrafted?, mode?, ready? } as on e3-ui's Plan: `onUpdate={handle.write}` with `data={handle}` is the inline adapter; `onPatch` hears SnapGrid.Types.PatchEvent(R); `onDrafted` fn(Array<R>) => Null hears the rows the canvas draws — the source with its drafts, exactly as Save would leave them — as it mounts and whenever they change (a gesture, an undo or redo, a Discard, a source that moved), so a pane beside it reads what it shows; `ready` fn(R) => Editing.Types.Readiness marks the tile it refuses and holds Save. Save leaves exactly the order the canvas shows
141
+ │ │ │ ├─ edit + editing (optional, given together) — the builder's canvas: every tile keeps its declared span at any width, and each gesture is a DRAFT of the shared editing session — e3-ui's Sheet's and Plan's — each ONE undoable transaction (origin drop / move / resize / remove); the history item (Undo · Redo · Discard · Save) sits in the canvas's toolbar (see The frame), and ⌘Z / Ctrl+Z undo, ⌘⇧Z / Ctrl+Y redo; the drafts are kept in the tab through a reload, and come back carried over to the source as it is then. Editing takes an `Array` source — its order is the tiles' order, so a move is a new order and a new row key; a `Dict` is refused. `edit` names the row fields a gesture writes: { key: "id" (the String identity field), row: "row" (String), span: "span" (Integer), height?: "height" (Option<Integer>; omitted, heights are not edited), align?: "align" (SnapGrid.Types.Align; the field an align request writes — omitted, one is refused), create?: ($, card, at) => R — a dropped library card's new row: `card` the drag grammar's { library, key }, `at` { key (a fresh identity), row (the row it lands in) }; the canvas then writes `at.row` and fits the span }. `editing` is { onApply? | onUpdate?, onPatch?, onDrafted?, mode?, ready? } as on e3-ui's Plan: `onUpdate={handle.write}` with `data={handle}` is the inline adapter; `onPatch` hears SnapGrid.Types.PatchEvent(R); `onDrafted` fn(Array<R>) => Null hears the rows the canvas draws — the source with its drafts, exactly as Save would leave them — as it mounts and whenever they change (a gesture, an undo or redo, a Discard, a source that moved), so a pane beside it reads what it shows; `ready` fn(R) => Editing.Types.Readiness marks the tile it refuses and holds Save. Save leaves exactly the order the canvas shows
142
142
  │ │ │ ├─ ui (optional) — the bound selection: `State.bind([SnapGrid.Types.UiState], key, SnapGrid.uiState())` — the canvas draws the tile it holds and writes the one the user picks, so an inspector beside it reads and sets it; omitted, the canvas keeps its own. Its `request` asks the editing canvas for a change, as e3-ui's Plan's `focus` asks for a row: `ui.write(SnapGrid.uiState({ selected: key, request: { key, change: variant("span", 8n) } }))` — `span` (held to the row's room), `row` (counting from 1; a row already holding 6 tiles, or one past the last, is a new row after it), `height` (`none` for its content's), `align` (written to `edit.align`) — taken as ONE gesture of the session by the rules the handles and drags keep, undoable with the rest, and written back `none`
143
143
  │ │ │ ├─ view (optional) — the bound design width and zoom: `State.bind([SnapGrid.Types.ViewState], key, SnapGrid.viewState())` under a key of its own, apart from `ui` — the editing canvas draws at them and writes the width the user presses and the zoom step they take; omitted, the canvas keeps them, starting from `width` and `zoom`
144
- │ │ │ ├─ apply (optional) — the bound Apply request: `State.bind([SnapGrid.Types.ApplyState], key, variant("idle", null))` under a key of its own — a screen that must not leave drafts behind (a publish) writes `variant("asked", id)` with an id of its own, and the editing canvas saves its drafts as its history item does — waiting out a Save already running, retrying one of unknown outcome — and answers under the same id: `applied(id)` once the source confirms them, or at once with none; `refused({ id, reason })` when they cannot land, `reason` in the canvas's words (the conflict's, the rejection's, a check's or the stale source's). The canvas stays the only writer of its drafts; omitted, only the history item saves
144
+ │ │ │ ├─ apply (optional) — the bound Apply request: `State.bind([SnapGrid.Types.ApplyState], key, variant("idle", null))` under a key of its own — a screen that must not leave drafts behind (a publish) writes `variant("asked", id)` with an id of its own, and the editing canvas saves its drafts as its history item does — waiting out a Save already running, retrying one of unknown outcome — and answers under the same id: `applied(id)` once the source confirms them, or at once with none; `refused({ id, reason })` when they cannot land, `reason` in the canvas's words (the conflict's, the rejection's, a check's, the drafts still being carried over to a source that moved, or the conflicts still to choose). The canvas stays the only writer of its drafts; omitted, only the history item saves
145
145
  │ │ │ ├─ widths (optional) — [{ label, icon?, width }] the design widths the editing canvas's toolbar offers, a device each (`{ label: "Desktop", icon: "desktop", width: "1440px" }`, `{ label: "Tablet", icon: "tablet-screen-button", width: "1024px" }`): the one the canvas draws at is pressed, and pressing another draws it at that width
146
146
  │ │ │ ├─ toolbar (optional) — { start?, end? } the host's items in the editing canvas's toolbar, each an array of UIComponents: `start` leads the row (a `<Status>`), `end` closes it (Preview, Publish buttons); they never fold
147
147
  │ │ │ ├─ panes (optional) — { start?, end? } the panes beside the editing canvas, under its toolbar — a palette, an inspector: a `<Dock surface="shell">` pane collapses to its rail and the canvas column takes the room
@@ -629,11 +629,11 @@ Task → Which tag?
629
629
  │ │ │ ├─ slice (required) — the bound handle from Slice.bind
630
630
  │ │ │ ├─ affordances (optional) — ["filter","search","range","breakdown","cohort","presets","brush","legend"]; legends are explicit-only (list "legend" or compose <Slice.Legend>)
631
631
  │ │ │ ├─ persist (optional) — "local" | "session" | "url" opts the state into reload-surviving / shareable-link storage
632
- │ │ │ └─ brush (optional) — the brush strip is rich by default (the range field's format drives the axis labels; a self-excluding count histogram shows the row distribution); brush={{ axis?, count?, buckets? }} opts down to the bare track. The applied window is a full brush selection: drag its body to slide (width preserved), an edge to resize, empty track to draw (also the Plan's horizon-brush gesture)
632
+ │ │ │ └─ brush (optional) — the brush strip is rich by default (the range field's format drives the axis labels; a self-excluding count histogram shows the row distribution); brush={{ axis?, count?, buckets? }} opts down to the bare track. The applied window is a full brush selection: drag its body to slide (width preserved), an edge to resize, empty track to draw (also the Plan's horizon-brush gesture). Its look is the horizon brush's (#1438): bars of one width toned on the heat ramp by count, whole inside the window's band and faded outside it, an outlined tab on each edge, and a hover's tooltip saying a bin's range and count; the start handle, the window and the end handle are three tab stops — ← → move a bin, Shift ten, Home / End the ends, Esc clears
633
633
  │ │ ├─ Nested tags: <Slice.Filter/Search/Range/Breakdown/Legend/Cohort/Presets/Summary slice={slice} /> — per-affordance chrome; <Slice.Cohort mode="toggle"|"manage" allowCreate? group?> (cohorts toggle on chip click; cohorts sharing a `group` render as one captioned run — a FAMILY of alternatives: active members OR within a family, families AND with each other and with standalone cohorts; an empty family member hides on the preset bar unless on; `group="…"` shows one family alone; <Slice.Presets> = toggle-only preset bar); <Slice.Legend> = facet bar (click = in-set multi-select over self-excluding slice.facetGroups(); mode="visibility" = eye rail); Summary/Filter footers read "N of M"
634
634
  │ │ └─ Factories:
635
635
  │ │ ├─ Slice.bind(key, rows, options?) — local Array rows; infers the row type. Options: fields (label/hints/format), range, search, initial, presets, breakdown and match. Initial/preset clauses name typed fields; local operators include gte/contains. The positional form is removed.
636
- │ │ ├─ Slice.bindRecord(key, indexed, options?) — Data.bindPaged(record, { indexes }) from e3-ui; fields/range/search/initial/presets come from declared field indexes. No browser predicate, local operators, match or breakdown. Several exact values then one range/prefix must follow one index; a Set field takes one exact member.
636
+ │ │ ├─ Slice.bindRecord(key, indexed, options?) — Data.bindPaged(record, { indexes }) from e3-ui; fields/range/search/initial/presets come from declared field indexes: a field is any row field an index is led by, whatever its name — `area` and `__tile` included, e3's parts of a generated key being its index's kind's (#1432). No browser predicate, local operators, match or breakdown. Several exact values then one range/prefix must follow one index; a Set field takes one exact member. A source's `pageWhere`/`countWhere` take a read, `{ where, area }`: the narrowing, and a read by area's window or unplaced clause beside it, none for any other read.
637
637
  │ │ ├─ Slice.bindRecords(key, { name: { source: indexed, fields?, range?, search?, initial? }, … }, { presets? }) — several records narrowed from ONE rail: one Slice.bindRecord per named source under `${key}.${name}`; a choice in one source never changes another's rows or counts (no index relates two records). Presets narrow any sources at once (a source a preset doesn't name is cleared); slice.togglePreset(id) / slice.clearAll() act on every part. A component that narrows several kinds of row takes it as slice={{ slice }} with its sources named after them (the e3-ui Allocation's `lanes` and `resources`) and reads each source itself.
638
638
  │ │ ├─ Slice.config(Row, { fields, rangeFieldId?, searchFieldIds?, breakdownFieldIds? }) / Slice.state({…}) — low-level config/state for pure Slice.apply and state operations; bind options infer and seed these automatically.
639
639
  │ │ ├─ Slice.rows(slice) — the narrowed Array for local rows, or for records a live ordered source: one per binding whatever the narrowing, its windows read under the narrowing as it stands, its `query()` naming it (#1354); record entries retain original key beside tagged ik, covering value and optional joined row. Slice.rows(slice, source) reads one source of a Slice.bindRecords handle.
@@ -760,7 +760,7 @@ Task → Which tag?
760
760
  │ └─ plotGutter (optional) — shared gutter (#147): pins the step lane so a Trace stacked under a Chart lines up (left supersedes labelWidth)
761
761
  │
762
762
  ├─ Feedback (status & async signals)
763
- │ ├─ <Banner> — page-spanning notice
763
+ │ ├─ <Banner> — page-spanning notice, the design system's Banner card: one row centred on itself — the status's mark (a triangle for warning/guard, a circle for error, else its 8px dot), the title as its message in the body face at 13, the description under it, the actions at its end (one Default button makes it 48 tall)
764
764
  │ │ └─ Props:
765
765
  │ │ ├─ status (required) — info | warning | success | error | neutral | change | guard | stale
766
766
  │ │ ├─ title (required) — string or UIComponent