@elaraai/e3-core 0.0.1-beta.0

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 (55) hide show
  1. package/LICENSE.md +50 -0
  2. package/README.md +76 -0
  3. package/dist/src/dataflow.d.ts +96 -0
  4. package/dist/src/dataflow.d.ts.map +1 -0
  5. package/dist/src/dataflow.js +433 -0
  6. package/dist/src/dataflow.js.map +1 -0
  7. package/dist/src/errors.d.ts +87 -0
  8. package/dist/src/errors.d.ts.map +1 -0
  9. package/dist/src/errors.js +178 -0
  10. package/dist/src/errors.js.map +1 -0
  11. package/dist/src/executions.d.ts +163 -0
  12. package/dist/src/executions.d.ts.map +1 -0
  13. package/dist/src/executions.js +535 -0
  14. package/dist/src/executions.js.map +1 -0
  15. package/dist/src/formats.d.ts +38 -0
  16. package/dist/src/formats.d.ts.map +1 -0
  17. package/dist/src/formats.js +115 -0
  18. package/dist/src/formats.js.map +1 -0
  19. package/dist/src/gc.d.ts +54 -0
  20. package/dist/src/gc.d.ts.map +1 -0
  21. package/dist/src/gc.js +232 -0
  22. package/dist/src/gc.js.map +1 -0
  23. package/dist/src/index.d.ts +23 -0
  24. package/dist/src/index.d.ts.map +1 -0
  25. package/dist/src/index.js +68 -0
  26. package/dist/src/index.js.map +1 -0
  27. package/dist/src/objects.d.ts +62 -0
  28. package/dist/src/objects.d.ts.map +1 -0
  29. package/dist/src/objects.js +245 -0
  30. package/dist/src/objects.js.map +1 -0
  31. package/dist/src/packages.d.ts +85 -0
  32. package/dist/src/packages.d.ts.map +1 -0
  33. package/dist/src/packages.js +355 -0
  34. package/dist/src/packages.js.map +1 -0
  35. package/dist/src/repository.d.ts +38 -0
  36. package/dist/src/repository.d.ts.map +1 -0
  37. package/dist/src/repository.js +103 -0
  38. package/dist/src/repository.js.map +1 -0
  39. package/dist/src/tasks.d.ts +63 -0
  40. package/dist/src/tasks.d.ts.map +1 -0
  41. package/dist/src/tasks.js +145 -0
  42. package/dist/src/tasks.js.map +1 -0
  43. package/dist/src/test-helpers.d.ts +44 -0
  44. package/dist/src/test-helpers.d.ts.map +1 -0
  45. package/dist/src/test-helpers.js +141 -0
  46. package/dist/src/test-helpers.js.map +1 -0
  47. package/dist/src/trees.d.ts +156 -0
  48. package/dist/src/trees.d.ts.map +1 -0
  49. package/dist/src/trees.js +607 -0
  50. package/dist/src/trees.js.map +1 -0
  51. package/dist/src/workspaces.d.ts +116 -0
  52. package/dist/src/workspaces.d.ts.map +1 -0
  53. package/dist/src/workspaces.js +356 -0
  54. package/dist/src/workspaces.js.map +1 -0
  55. package/package.json +50 -0
package/LICENSE.md ADDED
@@ -0,0 +1,50 @@
1
+ # Business Source License 1.1
2
+
3
+ Copyright (c) 2025 Elara AI Pty Ltd
4
+
5
+ ## License
6
+
7
+ **Licensor:** Elara AI Pty Ltd
8
+
9
+ **Licensed Work:** @elaraai/e3-core
10
+
11
+ **Change Date:** Four years from the date of each release
12
+
13
+ **Change License:** AGPL-3.0
14
+
15
+ ## Terms
16
+
17
+ The Licensed Work is provided under the terms of the Business Source License 1.1 as detailed below.
18
+
19
+ ### Grant of Rights
20
+
21
+ The Licensor grants you the right to copy, modify, create derivative works, redistribute, and make non-production use of the Licensed Work.
22
+
23
+ ### Production Use Limitation
24
+
25
+ **"Production Use"** means any use by or on behalf of a for-profit entity, other than for evaluation, testing, or development purposes.
26
+
27
+ Production Use requires a separate commercial license from the Licensor.
28
+
29
+ ### Change Date
30
+
31
+ On the Change Date (four years after each release), the Licensed Work will be made available under the Change License (AGPL-3.0).
32
+
33
+ ### No Warranty
34
+
35
+ THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND. THE LICENSOR DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
36
+
37
+ ## Commercial Licensing
38
+
39
+ To obtain a commercial license for Production Use, contact:
40
+
41
+ **Email:** support@elara.ai
42
+ **Website:** https://elaraai.com
43
+
44
+ ## Governing Law
45
+
46
+ This license is governed by the laws of New South Wales, Australia.
47
+
48
+ ---
49
+
50
+ *Elara AI Pty Ltd*
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @elaraai/e3-core
2
+
3
+ Core library for e3 repository operations, similar to libgit2 for git.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @elaraai/e3-core
9
+ ```
10
+
11
+ ## Overview
12
+
13
+ Pure business logic with no UI dependencies. Use this to build custom tools, integrations, or alternative interfaces on top of e3.
14
+
15
+ ## API
16
+
17
+ ### Repository
18
+
19
+ ```typescript
20
+ import { initRepository, findRepository, getRepository } from '@elaraai/e3-core';
21
+
22
+ initRepository('/path/to/project');
23
+ const repoPath = findRepository(); // Searches cwd and parents
24
+ ```
25
+
26
+ ### Objects
27
+
28
+ ```typescript
29
+ import { storeObject, loadObject, computeTaskId } from '@elaraai/e3-core';
30
+
31
+ const hash = await storeObject(repoPath, data, '.beast2');
32
+ const data = await loadObject(repoPath, hash, '.beast2');
33
+ const taskId = computeTaskId(irHash, argsHashes);
34
+ ```
35
+
36
+ ### Commits
37
+
38
+ ```typescript
39
+ import { createNewTaskCommit, createTaskDoneCommit, loadCommit } from '@elaraai/e3-core';
40
+
41
+ const commitHash = await createNewTaskCommit(repoPath, taskId, irHash, argsHashes, 'node', null);
42
+ const commit = await loadCommit(repoPath, commitHash);
43
+ ```
44
+
45
+ ### Tasks
46
+
47
+ ```typescript
48
+ import { updateTaskState, getTaskState, listTasks } from '@elaraai/e3-core';
49
+
50
+ await updateTaskState(repoPath, taskId, commitHash);
51
+ const commit = await getTaskState(repoPath, taskId);
52
+ const tasks = await listTasks(repoPath);
53
+ ```
54
+
55
+ ### Refs
56
+
57
+ ```typescript
58
+ import { setTaskRef, deleteTaskRef, listTaskRefs, resolveToTaskId } from '@elaraai/e3-core';
59
+
60
+ await setTaskRef(repoPath, 'my-task', taskId);
61
+ const taskId = await resolveToTaskId(repoPath, 'my-task');
62
+ ```
63
+
64
+ ## Related Repos
65
+
66
+ - **[east](https://github.com/elaraai/east)** - East language core
67
+ - **[east-node](https://github.com/elaraai/east-node)** - Node.js runtime and platform functions
68
+ - **[east-py](https://github.com/elaraai/east-py)** - Python runtime and data science
69
+
70
+ ## About Elara
71
+
72
+ e3 is developed by [Elara AI](https://elaraai.com/), an AI-powered platform that creates economic digital twins of businesses. e3 powers the execution layer of Elara solutions, enabling durable and efficient execution of East programs across multiple runtimes.
73
+
74
+ ## License
75
+
76
+ BSL 1.1. See [LICENSE.md](./LICENSE.md).
@@ -0,0 +1,96 @@
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
+ * Result of executing a single task in the dataflow.
7
+ */
8
+ export interface TaskExecutionResult {
9
+ /** Task name */
10
+ name: string;
11
+ /** Whether the task was cached */
12
+ cached: boolean;
13
+ /** Final state */
14
+ state: 'success' | 'failed' | 'error' | 'skipped';
15
+ /** Error message if state is 'error' */
16
+ error?: string;
17
+ /** Exit code if state is 'failed' */
18
+ exitCode?: number;
19
+ /** Duration in milliseconds */
20
+ duration: number;
21
+ }
22
+ /**
23
+ * Result of a dataflow execution.
24
+ */
25
+ export interface DataflowResult {
26
+ /** Overall success - true if all tasks completed successfully */
27
+ success: boolean;
28
+ /** Number of tasks executed (not from cache) */
29
+ executed: number;
30
+ /** Number of tasks served from cache */
31
+ cached: number;
32
+ /** Number of tasks that failed */
33
+ failed: number;
34
+ /** Number of tasks skipped due to upstream failure */
35
+ skipped: number;
36
+ /** Per-task results */
37
+ tasks: TaskExecutionResult[];
38
+ /** Total duration in milliseconds */
39
+ duration: number;
40
+ }
41
+ /**
42
+ * Options for dataflow execution.
43
+ */
44
+ export interface DataflowOptions {
45
+ /** Maximum concurrent task executions (default: 4) */
46
+ concurrency?: number;
47
+ /** Force re-execution even if cached (default: false) */
48
+ force?: boolean;
49
+ /** Filter to run only specific task(s) by exact name */
50
+ filter?: string;
51
+ /** Callback when a task starts */
52
+ onTaskStart?: (name: string) => void;
53
+ /** Callback when a task completes */
54
+ onTaskComplete?: (result: TaskExecutionResult) => void;
55
+ /** Callback for task stdout */
56
+ onStdout?: (taskName: string, data: string) => void;
57
+ /** Callback for task stderr */
58
+ onStderr?: (taskName: string, data: string) => void;
59
+ }
60
+ /**
61
+ * Execute all tasks in a workspace according to the dependency graph.
62
+ *
63
+ * Tasks are executed in parallel where dependencies allow, respecting
64
+ * the concurrency limit. On failure, no new tasks are launched but
65
+ * running tasks are allowed to complete.
66
+ *
67
+ * @param repoPath - Path to .e3 repository
68
+ * @param ws - Workspace name
69
+ * @param options - Execution options
70
+ * @returns Result of the dataflow execution
71
+ * @throws {WorkspaceNotFoundError} If workspace doesn't exist
72
+ * @throws {WorkspaceNotDeployedError} If workspace has no package deployed
73
+ * @throws {TaskNotFoundError} If filter specifies a task that doesn't exist
74
+ * @throws {DataflowError} If execution fails for other reasons
75
+ */
76
+ export declare function dataflowExecute(repoPath: string, ws: string, options?: DataflowOptions): Promise<DataflowResult>;
77
+ /**
78
+ * Get the dependency graph for a workspace (for visualization/debugging).
79
+ *
80
+ * @param repoPath - Path to .e3 repository
81
+ * @param ws - Workspace name
82
+ * @returns Graph information
83
+ * @throws {WorkspaceNotFoundError} If workspace doesn't exist
84
+ * @throws {WorkspaceNotDeployedError} If workspace has no package deployed
85
+ * @throws {DataflowError} If graph building fails for other reasons
86
+ */
87
+ export declare function dataflowGetGraph(repoPath: string, ws: string): Promise<{
88
+ tasks: Array<{
89
+ name: string;
90
+ hash: string;
91
+ inputs: string[];
92
+ output: string;
93
+ dependsOn: string[];
94
+ }>;
95
+ }>;
96
+ //# sourceMappingURL=dataflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dataflow.d.ts","sourceRoot":"","sources":["../../src/dataflow.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAsEH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,gBAAgB;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,MAAM,EAAE,OAAO,CAAC;IAChB,kBAAkB;IAClB,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,SAAS,CAAC;IAClD,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,OAAO,EAAE,OAAO,CAAC;IACjB,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,uBAAuB;IACvB,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAC7B,qCAAqC;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,sDAAsD;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,qCAAqC;IACrC,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACvD,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpD,+BAA+B;IAC/B,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAqHD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CA2RzB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CACpC,QAAQ,EAAE,MAAM,EAChB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC;IACT,KAAK,EAAE,KAAK,CAAC;QACX,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC,CAAC;CACJ,CAAC,CA0CD"}
@@ -0,0 +1,433 @@
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
+ * Dataflow execution for e3 workspaces.
7
+ *
8
+ * Executes tasks in a workspace based on their dependency graph. Tasks are
9
+ * executed in parallel where possible, respecting a concurrency limit.
10
+ *
11
+ * The execution model is event-driven with a work queue:
12
+ * 1. Build dependency graph from tasks (input paths -> task -> output path)
13
+ * 2. Compute reverse dependencies (which tasks depend on each output)
14
+ * 3. Initialize ready queue with tasks whose inputs are all assigned
15
+ * 4. Execute tasks from ready queue, respecting concurrency limit
16
+ * 5. On task completion, update workspace and check dependents for readiness
17
+ * 6. On failure, stop launching new tasks but wait for running ones
18
+ */
19
+ import { decodeBeast2For } from '@elaraai/east';
20
+ import { PackageObjectType, TaskObjectType, WorkspaceStateType, pathToString, } from '@elaraai/e3-types';
21
+ import { objectRead } from './objects.js';
22
+ import { taskExecute, executionGetOutput, inputsHash, } from './executions.js';
23
+ import { workspaceGetDatasetHash, workspaceSetDatasetByHash, } from './trees.js';
24
+ import { E3Error, WorkspaceNotFoundError, WorkspaceNotDeployedError, TaskNotFoundError, DataflowError, isNotFoundError, } from './errors.js';
25
+ import * as fs from 'fs/promises';
26
+ import * as path from 'path';
27
+ // =============================================================================
28
+ // Workspace State Reader
29
+ // =============================================================================
30
+ /**
31
+ * Read workspace state from file.
32
+ * @throws {WorkspaceNotFoundError} If workspace doesn't exist
33
+ * @throws {WorkspaceNotDeployedError} If workspace has no package deployed
34
+ */
35
+ async function readWorkspaceState(repoPath, ws) {
36
+ const stateFile = path.join(repoPath, 'workspaces', `${ws}.beast2`);
37
+ let data;
38
+ try {
39
+ data = await fs.readFile(stateFile);
40
+ }
41
+ catch (err) {
42
+ if (isNotFoundError(err)) {
43
+ throw new WorkspaceNotFoundError(ws);
44
+ }
45
+ throw err;
46
+ }
47
+ if (data.length === 0) {
48
+ throw new WorkspaceNotDeployedError(ws);
49
+ }
50
+ const decoder = decodeBeast2For(WorkspaceStateType);
51
+ return decoder(data);
52
+ }
53
+ // =============================================================================
54
+ // Dependency Graph Building
55
+ // =============================================================================
56
+ /**
57
+ * Build the dependency graph for a workspace.
58
+ *
59
+ * Returns:
60
+ * - taskNodes: Map of task name -> TaskNode
61
+ * - outputToTask: Map of output path string -> task name
62
+ * - taskDependents: Map of task name -> set of dependent task names
63
+ */
64
+ async function buildDependencyGraph(repoPath, ws) {
65
+ // Read workspace state to get package hash
66
+ const state = await readWorkspaceState(repoPath, ws);
67
+ // Read package object to get tasks map
68
+ const pkgData = await objectRead(repoPath, state.packageHash);
69
+ const pkgDecoder = decodeBeast2For(PackageObjectType);
70
+ const pkgObject = pkgDecoder(Buffer.from(pkgData));
71
+ const taskNodes = new Map();
72
+ const outputToTask = new Map(); // output path -> task name
73
+ // First pass: load all tasks and build output->task map
74
+ const taskDecoder = decodeBeast2For(TaskObjectType);
75
+ for (const [taskName, taskHash] of pkgObject.tasks) {
76
+ const taskData = await objectRead(repoPath, taskHash);
77
+ const task = taskDecoder(Buffer.from(taskData));
78
+ const outputPathStr = pathToString(task.output);
79
+ outputToTask.set(outputPathStr, taskName);
80
+ taskNodes.set(taskName, {
81
+ name: taskName,
82
+ hash: taskHash,
83
+ task,
84
+ inputPaths: task.inputs,
85
+ outputPath: task.output,
86
+ unresolvedCount: 0, // Will be computed below
87
+ });
88
+ }
89
+ // Build reverse dependency map: task -> tasks that depend on it
90
+ const taskDependents = new Map();
91
+ for (const taskName of taskNodes.keys()) {
92
+ taskDependents.set(taskName, new Set());
93
+ }
94
+ // Second pass: compute dependencies and unresolved counts
95
+ for (const [taskName, node] of taskNodes) {
96
+ for (const inputPath of node.inputPaths) {
97
+ const inputPathStr = pathToString(inputPath);
98
+ const producerTask = outputToTask.get(inputPathStr);
99
+ if (producerTask) {
100
+ // This input comes from another task's output.
101
+ // The task cannot run until the producer task completes,
102
+ // regardless of whether the output is currently assigned
103
+ // (it might be stale from a previous run).
104
+ taskDependents.get(producerTask).add(taskName);
105
+ node.unresolvedCount++;
106
+ }
107
+ // If not produced by a task, it's an external input - check if assigned
108
+ else {
109
+ const { refType } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
110
+ if (refType === 'unassigned') {
111
+ // External input that is unassigned - this task can never run
112
+ node.unresolvedCount++;
113
+ }
114
+ }
115
+ }
116
+ }
117
+ return { taskNodes, outputToTask, taskDependents };
118
+ }
119
+ // =============================================================================
120
+ // Dataflow Execution
121
+ // =============================================================================
122
+ /**
123
+ * Execute all tasks in a workspace according to the dependency graph.
124
+ *
125
+ * Tasks are executed in parallel where dependencies allow, respecting
126
+ * the concurrency limit. On failure, no new tasks are launched but
127
+ * running tasks are allowed to complete.
128
+ *
129
+ * @param repoPath - Path to .e3 repository
130
+ * @param ws - Workspace name
131
+ * @param options - Execution options
132
+ * @returns Result of the dataflow execution
133
+ * @throws {WorkspaceNotFoundError} If workspace doesn't exist
134
+ * @throws {WorkspaceNotDeployedError} If workspace has no package deployed
135
+ * @throws {TaskNotFoundError} If filter specifies a task that doesn't exist
136
+ * @throws {DataflowError} If execution fails for other reasons
137
+ */
138
+ export async function dataflowExecute(repoPath, ws, options = {}) {
139
+ const startTime = Date.now();
140
+ const concurrency = options.concurrency ?? 4;
141
+ let taskNodes;
142
+ let taskDependents;
143
+ try {
144
+ // Build dependency graph
145
+ const graph = await buildDependencyGraph(repoPath, ws);
146
+ taskNodes = graph.taskNodes;
147
+ taskDependents = graph.taskDependents;
148
+ }
149
+ catch (err) {
150
+ // Re-throw E3Errors as-is
151
+ if (err instanceof E3Error)
152
+ throw err;
153
+ // Wrap unexpected errors
154
+ throw new DataflowError(`Failed to build dependency graph: ${err instanceof Error ? err.message : err}`);
155
+ }
156
+ // Apply filter if specified
157
+ const filteredTaskNames = options.filter
158
+ ? new Set([options.filter])
159
+ : null;
160
+ // Validate filter
161
+ if (filteredTaskNames && options.filter && !taskNodes.has(options.filter)) {
162
+ throw new TaskNotFoundError(options.filter);
163
+ }
164
+ // Track execution state
165
+ const results = [];
166
+ let executed = 0;
167
+ let cached = 0;
168
+ let failed = 0;
169
+ let skipped = 0;
170
+ let hasFailure = false;
171
+ // Ready queue: tasks with all dependencies resolved
172
+ const readyQueue = [];
173
+ const completed = new Set();
174
+ const inProgress = new Set();
175
+ // Initialize ready queue with tasks that have no unresolved dependencies
176
+ // and pass the filter (if any)
177
+ for (const [taskName, node] of taskNodes) {
178
+ if (node.unresolvedCount === 0) {
179
+ if (!filteredTaskNames || filteredTaskNames.has(taskName)) {
180
+ readyQueue.push(taskName);
181
+ }
182
+ }
183
+ }
184
+ // Check if the task has a valid cached execution for current inputs
185
+ // Returns the output hash if cached, null if re-execution is needed
186
+ async function getCachedOutput(taskName) {
187
+ const node = taskNodes.get(taskName);
188
+ // Gather current input hashes
189
+ const currentInputHashes = [];
190
+ for (const inputPath of node.inputPaths) {
191
+ const { refType, hash } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
192
+ if (refType !== 'value' || hash === null) {
193
+ // Input not assigned, can't be cached
194
+ return null;
195
+ }
196
+ currentInputHashes.push(hash);
197
+ }
198
+ // Check if there's a cached execution for these inputs
199
+ const inHash = inputsHash(currentInputHashes);
200
+ const cachedOutputHash = await executionGetOutput(repoPath, node.hash, inHash);
201
+ if (cachedOutputHash === null) {
202
+ // No cached execution for current inputs
203
+ return null;
204
+ }
205
+ // Also verify the workspace output matches the cached output
206
+ // (in case the workspace was modified outside of execution)
207
+ const { refType, hash: wsOutputHash } = await workspaceGetDatasetHash(repoPath, ws, node.outputPath);
208
+ if (refType !== 'value' || wsOutputHash !== cachedOutputHash) {
209
+ // Workspace output doesn't match cached output, need to re-execute
210
+ // (or update workspace with cached value)
211
+ return null;
212
+ }
213
+ return cachedOutputHash;
214
+ }
215
+ // Execute a single task
216
+ async function executeTask(taskName) {
217
+ const node = taskNodes.get(taskName);
218
+ const taskStartTime = Date.now();
219
+ options.onTaskStart?.(taskName);
220
+ // Gather input hashes
221
+ const inputHashes = [];
222
+ for (const inputPath of node.inputPaths) {
223
+ const { refType, hash } = await workspaceGetDatasetHash(repoPath, ws, inputPath);
224
+ if (refType !== 'value' || hash === null) {
225
+ // Input not available - should not happen if dependency tracking is correct
226
+ return {
227
+ name: taskName,
228
+ cached: false,
229
+ state: 'error',
230
+ error: `Input at ${pathToString(inputPath)} is not assigned (refType: ${refType})`,
231
+ duration: Date.now() - taskStartTime,
232
+ };
233
+ }
234
+ inputHashes.push(hash);
235
+ }
236
+ // Execute the task
237
+ const execOptions = {
238
+ force: options.force,
239
+ onStdout: options.onStdout ? (data) => options.onStdout(taskName, data) : undefined,
240
+ onStderr: options.onStderr ? (data) => options.onStderr(taskName, data) : undefined,
241
+ };
242
+ const result = await taskExecute(repoPath, node.hash, inputHashes, execOptions);
243
+ // Build task result
244
+ const taskResult = {
245
+ name: taskName,
246
+ cached: result.cached,
247
+ state: result.state,
248
+ duration: Date.now() - taskStartTime,
249
+ };
250
+ if (result.state === 'error') {
251
+ taskResult.error = result.error ?? undefined;
252
+ }
253
+ else if (result.state === 'failed') {
254
+ taskResult.exitCode = result.exitCode ?? undefined;
255
+ taskResult.error = result.error ?? undefined;
256
+ }
257
+ // On success, update the workspace with the output
258
+ if (result.state === 'success' && result.outputHash) {
259
+ await workspaceSetDatasetByHash(repoPath, ws, node.outputPath, result.outputHash);
260
+ }
261
+ return taskResult;
262
+ }
263
+ // Process dependents when a task completes
264
+ function notifyDependents(taskName) {
265
+ const dependents = taskDependents.get(taskName) ?? new Set();
266
+ for (const depName of dependents) {
267
+ if (completed.has(depName) || inProgress.has(depName))
268
+ continue;
269
+ // Skip dependents not in the filter
270
+ if (filteredTaskNames && !filteredTaskNames.has(depName))
271
+ continue;
272
+ const depNode = taskNodes.get(depName);
273
+ depNode.unresolvedCount--;
274
+ if (depNode.unresolvedCount === 0 && !readyQueue.includes(depName)) {
275
+ readyQueue.push(depName);
276
+ }
277
+ }
278
+ }
279
+ // Mark dependents as skipped when a task fails
280
+ function skipDependents(taskName) {
281
+ const dependents = taskDependents.get(taskName) ?? new Set();
282
+ for (const depName of dependents) {
283
+ if (completed.has(depName) || inProgress.has(depName))
284
+ continue;
285
+ // Skip dependents not in the filter
286
+ if (filteredTaskNames && !filteredTaskNames.has(depName))
287
+ continue;
288
+ // Recursively skip
289
+ completed.add(depName);
290
+ skipped++;
291
+ results.push({
292
+ name: depName,
293
+ cached: false,
294
+ state: 'skipped',
295
+ duration: 0,
296
+ });
297
+ options.onTaskComplete?.({
298
+ name: depName,
299
+ cached: false,
300
+ state: 'skipped',
301
+ duration: 0,
302
+ });
303
+ skipDependents(depName);
304
+ }
305
+ }
306
+ // Main execution loop using a work-stealing approach
307
+ const runningPromises = new Map();
308
+ async function processQueue() {
309
+ while (true) {
310
+ // Check if we're done
311
+ if (readyQueue.length === 0 && runningPromises.size === 0) {
312
+ break;
313
+ }
314
+ // Launch tasks up to concurrency limit if no failure
315
+ while (!hasFailure && readyQueue.length > 0 && runningPromises.size < concurrency) {
316
+ const taskName = readyQueue.shift();
317
+ if (completed.has(taskName) || inProgress.has(taskName))
318
+ continue;
319
+ // Check if there's a valid cached execution for current inputs
320
+ const cachedOutputHash = await getCachedOutput(taskName);
321
+ if (cachedOutputHash !== null && !options.force) {
322
+ // Valid cached execution exists for current inputs
323
+ completed.add(taskName);
324
+ cached++;
325
+ const result = {
326
+ name: taskName,
327
+ cached: true,
328
+ state: 'success',
329
+ duration: 0,
330
+ };
331
+ results.push(result);
332
+ options.onTaskComplete?.(result);
333
+ notifyDependents(taskName);
334
+ continue;
335
+ }
336
+ inProgress.add(taskName);
337
+ const promise = (async () => {
338
+ try {
339
+ const result = await executeTask(taskName);
340
+ inProgress.delete(taskName);
341
+ completed.add(taskName);
342
+ results.push(result);
343
+ options.onTaskComplete?.(result);
344
+ if (result.state === 'success') {
345
+ if (result.cached) {
346
+ cached++;
347
+ }
348
+ else {
349
+ executed++;
350
+ }
351
+ notifyDependents(taskName);
352
+ }
353
+ else {
354
+ failed++;
355
+ hasFailure = true;
356
+ skipDependents(taskName);
357
+ }
358
+ }
359
+ finally {
360
+ runningPromises.delete(taskName);
361
+ }
362
+ })();
363
+ runningPromises.set(taskName, promise);
364
+ }
365
+ // Wait for at least one task to complete if we can't launch more
366
+ if (runningPromises.size > 0) {
367
+ await Promise.race(runningPromises.values());
368
+ }
369
+ else if (readyQueue.length === 0) {
370
+ // No running tasks and no ready tasks - we might have unresolvable dependencies
371
+ break;
372
+ }
373
+ }
374
+ }
375
+ await processQueue();
376
+ // Wait for any remaining tasks
377
+ if (runningPromises.size > 0) {
378
+ await Promise.all(runningPromises.values());
379
+ }
380
+ return {
381
+ success: !hasFailure,
382
+ executed,
383
+ cached,
384
+ failed,
385
+ skipped,
386
+ tasks: results,
387
+ duration: Date.now() - startTime,
388
+ };
389
+ }
390
+ /**
391
+ * Get the dependency graph for a workspace (for visualization/debugging).
392
+ *
393
+ * @param repoPath - Path to .e3 repository
394
+ * @param ws - Workspace name
395
+ * @returns Graph information
396
+ * @throws {WorkspaceNotFoundError} If workspace doesn't exist
397
+ * @throws {WorkspaceNotDeployedError} If workspace has no package deployed
398
+ * @throws {DataflowError} If graph building fails for other reasons
399
+ */
400
+ export async function dataflowGetGraph(repoPath, ws) {
401
+ let taskNodes;
402
+ let outputToTask;
403
+ try {
404
+ const graph = await buildDependencyGraph(repoPath, ws);
405
+ taskNodes = graph.taskNodes;
406
+ outputToTask = graph.outputToTask;
407
+ }
408
+ catch (err) {
409
+ if (err instanceof E3Error)
410
+ throw err;
411
+ throw new DataflowError(`Failed to build dependency graph: ${err instanceof Error ? err.message : err}`);
412
+ }
413
+ const tasks = [];
414
+ for (const [taskName, node] of taskNodes) {
415
+ const dependsOn = [];
416
+ for (const inputPath of node.inputPaths) {
417
+ const inputPathStr = pathToString(inputPath);
418
+ const producerTask = outputToTask.get(inputPathStr);
419
+ if (producerTask) {
420
+ dependsOn.push(producerTask);
421
+ }
422
+ }
423
+ tasks.push({
424
+ name: taskName,
425
+ hash: node.hash,
426
+ inputs: node.inputPaths.map(pathToString),
427
+ output: pathToString(node.outputPath),
428
+ dependsOn,
429
+ });
430
+ }
431
+ return { tasks };
432
+ }
433
+ //# sourceMappingURL=dataflow.js.map