@elaraai/e3-types 0.0.1-alpha.2

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.
@@ -0,0 +1,419 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+
6
+ /**
7
+ * Data structure and path types for e3.
8
+ *
9
+ * Terminology:
10
+ * - **Dataset**: A location holding a value (leaf node in the data tree)
11
+ * - **Tree**: A location containing datasets or nested trees (branch node)
12
+ * - **Structure**: The shape of the data tree (what trees/datasets exist and their types)
13
+ * - **Path**: An address pointing to a dataset or tree
14
+ *
15
+ * Paths use East's keypath syntax:
16
+ * - `.field` for struct field access (backtick-quoted for special chars)
17
+ * - `[N]` for array index access (future)
18
+ * - `[key]` for dict key lookup (future)
19
+ *
20
+ * @see East serialization docs for keypath syntax details
21
+ */
22
+
23
+ import { VariantType, StringType, ArrayType, DictType, RecursiveType, ValueTypeOf, printIdentifier, variant, EastTypeType } from '@elaraai/east';
24
+
25
+ /**
26
+ * Structure definition for a data tree node.
27
+ *
28
+ * Defines the shape of the data tree - which paths are datasets (hold values)
29
+ * and which are trees (hold other nodes).
30
+ *
31
+ * @remarks
32
+ * - `value`: A dataset - holds a typed value. The type is an `EastTypeValue`.
33
+ * - `struct`: A tree - has named children, each with its own structure.
34
+ *
35
+ * MVP only supports struct trees. Future: array, dict, variant trees.
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * // A dataset holding an Integer value
40
+ * const dataset: Structure = variant('value', variant('Integer', null));
41
+ *
42
+ * // A tree with named children
43
+ * const tree: Structure = variant('struct', new Map([
44
+ * ['count', variant('value', variant('Integer', null))],
45
+ * ['items', variant('value', variant('Array', variant('String', null)))],
46
+ * ]));
47
+ * ```
48
+ */
49
+ export const StructureType = RecursiveType(self => VariantType({
50
+ /** Dataset: East type of the value (homoiconic EastTypeValue) */
51
+ value: EastTypeType,
52
+ /** Struct tree: named children mapping to child structures */
53
+ struct: DictType(StringType, self),
54
+ }));
55
+ export type StructureType = typeof StructureType;
56
+
57
+ export type Structure = ValueTypeOf<typeof StructureType>;
58
+
59
+ // Backwards compatibility alias
60
+ /** @deprecated Use StructureType instead */
61
+ export const DatasetSchemaType = StructureType;
62
+ /** @deprecated Use Structure instead */
63
+ export type DatasetSchemaType = StructureType;
64
+ /** @deprecated Use Structure instead */
65
+ export type DatasetSchema = Structure;
66
+
67
+ /**
68
+ * Path segment for navigating data trees.
69
+ *
70
+ * Uses East keypath syntax for consistency:
71
+ * - `field`: Struct field access (rendered as `.field` or `` .`field` `` if quoted)
72
+ * - `index`: Array element access (rendered as `[N]`) - future
73
+ * - `key`: Dict key lookup (rendered as `[key]`) - future
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Struct field access
78
+ * const segment: PathSegment = variant('field', 'sales');
79
+ *
80
+ * // Array index (future)
81
+ * const segment: PathSegment = variant('index', 0n);
82
+ * ```
83
+ */
84
+ export const PathSegmentType = VariantType({
85
+ /** Struct field access by name */
86
+ field: StringType,
87
+ // Future: case: StringType for variant case identifiers
88
+ // Future: index: IntegerType for array access
89
+ // Future: key: StringType (or polymorphic) for dict access
90
+ });
91
+ export type PathSegmentType = typeof PathSegmentType;
92
+
93
+ export type PathSegment = ValueTypeOf<typeof PathSegmentType>;
94
+
95
+ /**
96
+ * Path: sequence of segments identifying a location in a data tree.
97
+ *
98
+ * Paths point to either a dataset (leaf) or a tree (branch).
99
+ * Used by tasks to specify where inputs come from and where outputs go.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * // Path to .inputs.sales.data
104
+ * const path: TreePath = [
105
+ * variant('field', 'inputs'),
106
+ * variant('field', 'sales'),
107
+ * variant('field', 'data'),
108
+ * ];
109
+ * ```
110
+ */
111
+ export const TreePathType = ArrayType(PathSegmentType);
112
+ export type TreePathType = typeof TreePathType;
113
+
114
+ export type TreePath = ValueTypeOf<typeof TreePathType>;
115
+
116
+ /**
117
+ * Converts a path to East keypath string representation.
118
+ *
119
+ * Uses East's keypath syntax: `.field` for simple identifiers,
120
+ * `` .`field` `` for identifiers needing quoting.
121
+ *
122
+ * @param path - The path to convert
123
+ * @returns A keypath string (e.g., ".inputs.sales" or ".inputs.`my/field`")
124
+ *
125
+ * @example
126
+ * ```ts
127
+ * const path = treePath('inputs', 'sales');
128
+ * pathToString(path); // ".inputs.sales"
129
+ *
130
+ * const path2 = treePath('inputs', 'my/field');
131
+ * pathToString(path2); // ".inputs.`my/field`"
132
+ * ```
133
+ */
134
+ export function pathToString(path: TreePath): string {
135
+ return path.map(segment => {
136
+ if (segment.type === 'field') {
137
+ return '.' + printIdentifier(segment.value);
138
+ } else {
139
+ throw new Error(`pathToString: unsupported path segment type: ${segment.type}`);
140
+ }
141
+ }).join('');
142
+ }
143
+
144
+ /**
145
+ * Result of parsing a path with structure validation.
146
+ */
147
+ export interface ParsePathResult {
148
+ /** The parsed path segments */
149
+ path: TreePath;
150
+ /** The structure at the path location */
151
+ structure: Structure;
152
+ }
153
+
154
+ /**
155
+ * Parses an East keypath string into a path, validating against the structure.
156
+ *
157
+ * Supports `.field` syntax for struct field access.
158
+ * Backtick-quoted identifiers (`` .`field` ``) are supported for special chars.
159
+ *
160
+ * @param pathStr - A keypath string (e.g., ".inputs.sales")
161
+ * @param structure - The root structure to validate against
162
+ * @returns The parsed path and the structure at that location
163
+ *
164
+ * @throws {Error} If a field doesn't exist in the structure or path descends into a dataset
165
+ *
166
+ * @remarks
167
+ * - Empty string returns empty path (root) with the root structure
168
+ * - Path must start with `.` (no leading slash)
169
+ * - Backtick escaping: `` \` `` for literal backtick, `\\` for backslash
170
+ * - Future: will disambiguate variant cases from struct fields using structure
171
+ * - Future: will use structure to parse dict keys with correct type
172
+ *
173
+ * @example
174
+ * ```ts
175
+ * const structure = variant('struct', new Map([
176
+ * ['inputs', variant('struct', new Map([
177
+ * ['sales', variant('value', variant('Integer', null))],
178
+ * ]))],
179
+ * ]));
180
+ *
181
+ * const { path, structure: leafStructure } = parsePath('.inputs.sales', structure);
182
+ * // path = [field('inputs'), field('sales')]
183
+ * // leafStructure = variant('value', variant('Integer', null))
184
+ * ```
185
+ */
186
+ export function parsePath(pathStr: string, structure: Structure): ParsePathResult {
187
+ if (pathStr === '') return { path: [], structure };
188
+
189
+ const segments: TreePath = [];
190
+ let currentStructure = structure;
191
+ let pos = 0;
192
+
193
+ while (pos < pathStr.length) {
194
+ if (pathStr[pos] === '.') {
195
+ pos++;
196
+
197
+ // Parse identifier (TODO: export parseIdentifier from east package)
198
+ let fieldName: string;
199
+
200
+ // Check for backtick-quoted identifier
201
+ if (pos < pathStr.length && pathStr[pos] === '`') {
202
+ pos++;
203
+ fieldName = '';
204
+ while (pos < pathStr.length && pathStr[pos] !== '`') {
205
+ if (pathStr[pos] === '\\' && pos + 1 < pathStr.length) {
206
+ // Escape sequence
207
+ pos++;
208
+ fieldName += pathStr[pos];
209
+ } else {
210
+ fieldName += pathStr[pos];
211
+ }
212
+ pos++;
213
+ }
214
+ if (pos < pathStr.length && pathStr[pos] === '`') {
215
+ pos++; // consume closing backtick
216
+ }
217
+ } else {
218
+ // Simple identifier: [a-zA-Z_][a-zA-Z0-9_]*
219
+ fieldName = '';
220
+ while (pos < pathStr.length && /[a-zA-Z0-9_]/.test(pathStr[pos]!)) {
221
+ fieldName += pathStr[pos];
222
+ pos++;
223
+ }
224
+ }
225
+
226
+ if (fieldName.length === 0) {
227
+ throw new Error(`parsePath: expected identifier after '.' at position ${pos}`);
228
+ }
229
+
230
+ // Validate against structure
231
+ if (currentStructure.type === 'value') {
232
+ throw new Error(`parsePath: cannot descend into dataset at '${pathToString(segments)}'`);
233
+ }
234
+
235
+ // currentStructure.type === 'struct' (only other option after 'value' check)
236
+ const fields = currentStructure.value;
237
+ const childStructure = fields.get(fieldName);
238
+ if (childStructure === undefined) {
239
+ const available = [...fields.keys()].map(k => printIdentifier(k)).join(', ');
240
+ throw new Error(`parsePath: field '${fieldName}' not found at '${pathToString(segments)}'. Available: ${available}`);
241
+ }
242
+ segments.push(variant('field', fieldName));
243
+ currentStructure = childStructure;
244
+ } else {
245
+ throw new Error(`parsePath: unexpected character at position ${pos}: '${pathStr[pos]}'`);
246
+ }
247
+ }
248
+
249
+ return { path: segments, structure: currentStructure };
250
+ }
251
+
252
+ /**
253
+ * Creates a path from field names, validating against the structure.
254
+ *
255
+ * Convenience function for the common case of navigating through struct fields.
256
+ *
257
+ * @param structure - The root structure to validate against
258
+ * @param fields - Field names to include in the path
259
+ * @returns The parsed path and the structure at that location
260
+ *
261
+ * @throws {Error} If a field doesn't exist in the structure
262
+ *
263
+ * @example
264
+ * ```ts
265
+ * const { path, structure: leafStructure } = treePath(rootStructure, 'inputs', 'sales');
266
+ * ```
267
+ */
268
+ export function treePath(structure: Structure, ...fields: string[]): ParsePathResult {
269
+ return parsePath('.' + fields.map(printIdentifier).join('.'), structure);
270
+ }
271
+
272
+ /**
273
+ * Result of parsing a dataset path specification.
274
+ */
275
+ export interface ParseDatasetPathResult {
276
+ /** Workspace name */
277
+ ws: string;
278
+ /** Path within the workspace */
279
+ path: TreePath;
280
+ }
281
+
282
+ /**
283
+ * Parse workspace.path.to.dataset syntax into workspace name and TreePath.
284
+ *
285
+ * This is a lenient parser that does not validate against a structure.
286
+ * Use this for parsing user input where the structure is not yet known.
287
+ *
288
+ * @param pathSpec - Path specification in dot notation (e.g., "production.inputs.sales")
289
+ * @returns Workspace name and path segments
290
+ *
291
+ * @throws {Error} If path is empty or has unclosed backticks
292
+ *
293
+ * @example
294
+ * ```ts
295
+ * parseDatasetPath("production")
296
+ * // { ws: "production", path: [] }
297
+ *
298
+ * parseDatasetPath("production.inputs.sales")
299
+ * // { ws: "production", path: [field("inputs"), field("sales")] }
300
+ *
301
+ * // For field names with special characters, use backticks:
302
+ * parseDatasetPath("production.`my field`")
303
+ * // { ws: "production", path: [field("my field")] }
304
+ * ```
305
+ */
306
+ export function parseDatasetPath(pathSpec: string): ParseDatasetPathResult {
307
+ const segments = parsePathSegments(pathSpec);
308
+
309
+ if (segments.length === 0) {
310
+ throw new Error('Path cannot be empty');
311
+ }
312
+
313
+ const ws = segments[0]!;
314
+ const path: TreePath = segments.slice(1).map((s) => variant('field', s));
315
+
316
+ return { ws, path };
317
+ }
318
+
319
+ /**
320
+ * Parse dot-separated path into segments, handling backtick-quoted identifiers.
321
+ * @internal
322
+ */
323
+ function parsePathSegments(pathSpec: string): string[] {
324
+ const segments: string[] = [];
325
+ let current = '';
326
+ let inBackticks = false;
327
+
328
+ for (let i = 0; i < pathSpec.length; i++) {
329
+ const char = pathSpec[i];
330
+
331
+ if (char === '`') {
332
+ inBackticks = !inBackticks;
333
+ } else if (char === '.' && !inBackticks) {
334
+ if (current.length > 0) {
335
+ segments.push(current);
336
+ current = '';
337
+ }
338
+ } else {
339
+ current += char;
340
+ }
341
+ }
342
+
343
+ if (current.length > 0) {
344
+ segments.push(current);
345
+ }
346
+
347
+ if (inBackticks) {
348
+ throw new Error('Unclosed backtick in path');
349
+ }
350
+
351
+ return segments;
352
+ }
353
+
354
+ /**
355
+ * Result of parsing a package reference.
356
+ */
357
+ export interface ParsePackageRefResult {
358
+ /** Package name */
359
+ name: string;
360
+ /** Version string, or undefined if not specified */
361
+ version?: string;
362
+ }
363
+
364
+ /**
365
+ * Parse a package reference like "name" or "name@version".
366
+ *
367
+ * @param ref - Package reference string
368
+ * @returns Package name and optional version
369
+ *
370
+ * @example
371
+ * ```ts
372
+ * parsePackageRef("my-package")
373
+ * // { name: "my-package", version: undefined }
374
+ *
375
+ * parsePackageRef("my-package@1.0.0")
376
+ * // { name: "my-package", version: "1.0.0" }
377
+ *
378
+ * // Scoped packages work too:
379
+ * parsePackageRef("@scope/package@2.0.0")
380
+ * // { name: "@scope/package", version: "2.0.0" }
381
+ * ```
382
+ */
383
+ export function parsePackageRef(ref: string): ParsePackageRefResult {
384
+ const atIdx = ref.lastIndexOf('@');
385
+ // Handle scoped packages like @scope/name - only split on @ after position 0
386
+ if (atIdx > 0) {
387
+ return {
388
+ name: ref.slice(0, atIdx),
389
+ version: ref.slice(atIdx + 1),
390
+ };
391
+ }
392
+ return { name: ref };
393
+ }
394
+
395
+ /**
396
+ * Convert URL path segments to a TreePath.
397
+ *
398
+ * Takes slash-separated, URL-encoded path segments and converts them to
399
+ * a TreePath of field variants.
400
+ *
401
+ * @param urlPath - URL path string (e.g., "inputs/sales/data" or "/inputs/sales/data")
402
+ * @returns TreePath of field segments
403
+ *
404
+ * @example
405
+ * ```ts
406
+ * urlPathToTreePath("inputs/sales/data")
407
+ * // [field("inputs"), field("sales"), field("data")]
408
+ *
409
+ * urlPathToTreePath("inputs/my%20field")
410
+ * // [field("inputs"), field("my field")]
411
+ *
412
+ * urlPathToTreePath("")
413
+ * // []
414
+ * ```
415
+ */
416
+ export function urlPathToTreePath(urlPath: string): TreePath {
417
+ const segments = urlPath.split('/').filter(p => p);
418
+ return segments.map(segment => variant('field', decodeURIComponent(segment)));
419
+ }
package/src/task.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+
6
+ /**
7
+ * Task object types for e3.
8
+ *
9
+ * A task object defines a complete executable unit: the command IR that
10
+ * generates the exec args, where to read inputs from, and where to write output.
11
+ *
12
+ * Task objects are stored in the object store and referenced by packages.
13
+ * They are content-addressed, enabling deduplication and memoization.
14
+ *
15
+ * Input and output types are inferred from the package's structure at the
16
+ * specified paths - the task just references locations, not types.
17
+ */
18
+
19
+ import { StructType, StringType, ArrayType, ValueTypeOf } from '@elaraai/east';
20
+ import { TreePathType } from './structure.js';
21
+
22
+ /**
23
+ * Task object stored in the object store.
24
+ *
25
+ * A task is a complete executable unit that reads from input dataset paths
26
+ * and writes to an output dataset path. The commandIr is evaluated at runtime
27
+ * to produce the exec args.
28
+ *
29
+ * @remarks
30
+ * - `commandIr`: Hash of East IR object that produces exec args
31
+ * - IR signature: (inputs: Array<String>, output: String) -> Array<String>
32
+ * - `inputs` are paths to staged input .beast2 files
33
+ * - `output` is the path where output should be written
34
+ * - Returns array of strings to exec (e.g., ["sh", "-c", "python ..."])
35
+ * - `inputs`: Paths to input datasets in the data tree
36
+ * - `output`: Path to the output dataset in the data tree
37
+ *
38
+ * Types are not stored in the task - they are inferred from the package's
39
+ * structure at the specified paths. This keeps tasks simple and avoids
40
+ * redundant type information.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * import { variant } from '@elaraai/east';
45
+ *
46
+ * // Task with command IR that generates: ["sh", "-c", "python script.py <input> <output>"]
47
+ * const task: TaskObject = {
48
+ * commandIr: '5e7a3b...', // hash of compiled IR
49
+ * inputs: [
50
+ * [variant('field', 'inputs'), variant('field', 'sales')],
51
+ * ],
52
+ * output: [variant('field', 'tasks'), variant('field', 'train'), variant('field', 'output')],
53
+ * };
54
+ * ```
55
+ */
56
+ export const TaskObjectType = StructType({
57
+ /** Hash of East IR that generates exec args: (inputs, output) -> Array<String> */
58
+ commandIr: StringType,
59
+ /** Input paths: where to read each input dataset from the data tree */
60
+ inputs: ArrayType(TreePathType),
61
+ /** Output path: where to write the output dataset in the data tree */
62
+ output: TreePathType,
63
+ });
64
+ export type TaskObjectType = typeof TaskObjectType;
65
+
66
+ export type TaskObject = ValueTypeOf<typeof TaskObjectType>;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Copyright (c) 2025 Elara AI Pty Ltd
3
+ * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
+ */
5
+
6
+ /**
7
+ * Workspace state type definitions.
8
+ *
9
+ * A workspace is a mutable working copy of a package. The state tracks:
10
+ * - Which package was deployed (immutable reference via hash)
11
+ * - When the deployment occurred
12
+ * - Current root data tree hash
13
+ * - When the root was last updated
14
+ *
15
+ * State file location: workspaces/<name>/state.beast2
16
+ * No state file = workspace exists but not yet deployed.
17
+ */
18
+
19
+ import { StructType, StringType, DateTimeType, ValueTypeOf } from '@elaraai/east';
20
+
21
+ /**
22
+ * Workspace state stored in workspaces/<name>/state.beast2
23
+ *
24
+ * Contains both deployment info and current data root in a single
25
+ * atomic unit to ensure consistency.
26
+ *
27
+ * Future audit trail support:
28
+ * When we implement full audit trail, this state will move to the object
29
+ * store (content-addressed) with a ref file pointing to current state hash.
30
+ * Additional fields for the Merkle chain:
31
+ *
32
+ * previousStateHash: NullableType(StringType), // null for initial deploy
33
+ * message: StringType, // "deployed package X", "user Y wrote to dataset Z"
34
+ *
35
+ * This gives a complete history of workspace changes, similar to git commits.
36
+ */
37
+ export const WorkspaceStateType = StructType({
38
+ /** Name of the deployed package */
39
+ packageName: StringType,
40
+ /** Version of the deployed package */
41
+ packageVersion: StringType,
42
+ /** Hash of the package object at deploy time (immutable reference) */
43
+ packageHash: StringType,
44
+ /** UTC datetime when the package was deployed */
45
+ deployedAt: DateTimeType,
46
+ /** Current root data tree hash */
47
+ rootHash: StringType,
48
+ /** UTC datetime when root was last updated */
49
+ rootUpdatedAt: DateTimeType,
50
+ });
51
+
52
+ export type WorkspaceState = ValueTypeOf<typeof WorkspaceStateType>;
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "NodeNext",
7
+ "outDir": "./dist",
8
+ "rootDir": "./",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true,
17
+ "composite": true
18
+ },
19
+ "include": ["src/**/*"],
20
+ "exclude": ["node_modules", "dist"]
21
+ }