@nodetool-ai/execution 0.8.1
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.
- package/README.md +129 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/app-debug/app-spec.d.ts +94 -0
- package/dist/app-debug/app-spec.d.ts.map +1 -0
- package/dist/app-debug/app-spec.js +710 -0
- package/dist/app-debug/app-spec.js.map +1 -0
- package/dist/app-debug/application-target.d.ts +53 -0
- package/dist/app-debug/application-target.d.ts.map +1 -0
- package/dist/app-debug/application-target.js +126 -0
- package/dist/app-debug/application-target.js.map +1 -0
- package/dist/app-debug/bundle-target.d.ts +21 -0
- package/dist/app-debug/bundle-target.d.ts.map +1 -0
- package/dist/app-debug/bundle-target.js +51 -0
- package/dist/app-debug/bundle-target.js.map +1 -0
- package/dist/app-debug/graph-shape.d.ts +14 -0
- package/dist/app-debug/graph-shape.d.ts.map +1 -0
- package/dist/app-debug/graph-shape.js +23 -0
- package/dist/app-debug/graph-shape.js.map +1 -0
- package/dist/app-debug/index.d.ts +26 -0
- package/dist/app-debug/index.d.ts.map +1 -0
- package/dist/app-debug/index.js +19 -0
- package/dist/app-debug/index.js.map +1 -0
- package/dist/app-debug/markdown.d.ts +6 -0
- package/dist/app-debug/markdown.d.ts.map +1 -0
- package/dist/app-debug/markdown.js +133 -0
- package/dist/app-debug/markdown.js.map +1 -0
- package/dist/app-debug/runtime.d.ts +267 -0
- package/dist/app-debug/runtime.d.ts.map +1 -0
- package/dist/app-debug/runtime.js +633 -0
- package/dist/app-debug/runtime.js.map +1 -0
- package/dist/app-debug/script-operation.d.ts +58 -0
- package/dist/app-debug/script-operation.d.ts.map +1 -0
- package/dist/app-debug/script-operation.js +44 -0
- package/dist/app-debug/script-operation.js.map +1 -0
- package/dist/app-debug/simulate.d.ts +85 -0
- package/dist/app-debug/simulate.d.ts.map +1 -0
- package/dist/app-debug/simulate.js +991 -0
- package/dist/app-debug/simulate.js.map +1 -0
- package/dist/app-debug/summarize.d.ts +64 -0
- package/dist/app-debug/summarize.d.ts.map +1 -0
- package/dist/app-debug/summarize.js +65 -0
- package/dist/app-debug/summarize.js.map +1 -0
- package/dist/app-debug/types.d.ts +343 -0
- package/dist/app-debug/types.d.ts.map +1 -0
- package/dist/app-debug/types.js +2 -0
- package/dist/app-debug/types.js.map +1 -0
- package/dist/cost-ledger.d.ts +147 -0
- package/dist/cost-ledger.d.ts.map +1 -0
- package/dist/cost-ledger.js +288 -0
- package/dist/cost-ledger.js.map +1 -0
- package/dist/debug/collector.d.ts +28 -0
- package/dist/debug/collector.d.ts.map +1 -0
- package/dist/debug/collector.js +284 -0
- package/dist/debug/collector.js.map +1 -0
- package/dist/debug/index.d.ts +15 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/index.js +16 -0
- package/dist/debug/index.js.map +1 -0
- package/dist/debug/types.d.ts +179 -0
- package/dist/debug/types.d.ts.map +1 -0
- package/dist/debug/types.js +11 -0
- package/dist/debug/types.js.map +1 -0
- package/dist/debug/verdict.d.ts +27 -0
- package/dist/debug/verdict.d.ts.map +1 -0
- package/dist/debug/verdict.js +51 -0
- package/dist/debug/verdict.js.map +1 -0
- package/dist/executor-resolver.d.ts +13 -0
- package/dist/executor-resolver.d.ts.map +1 -0
- package/dist/executor-resolver.js +13 -0
- package/dist/executor-resolver.js.map +1 -0
- package/dist/fal-output-decoder.d.ts +36 -0
- package/dist/fal-output-decoder.d.ts.map +1 -0
- package/dist/fal-output-decoder.js +129 -0
- package/dist/fal-output-decoder.js.map +1 -0
- package/dist/generation-lifecycle.d.ts +54 -0
- package/dist/generation-lifecycle.d.ts.map +1 -0
- package/dist/generation-lifecycle.js +431 -0
- package/dist/generation-lifecycle.js.map +1 -0
- package/dist/generation-recovery-worker.d.ts +100 -0
- package/dist/generation-recovery-worker.d.ts.map +1 -0
- package/dist/generation-recovery-worker.js +995 -0
- package/dist/generation-recovery-worker.js.map +1 -0
- package/dist/generation-tracker.d.ts +83 -0
- package/dist/generation-tracker.d.ts.map +1 -0
- package/dist/generation-tracker.js +436 -0
- package/dist/generation-tracker.js.map +1 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -0
- package/dist/js-script-debug/index.d.ts +13 -0
- package/dist/js-script-debug/index.d.ts.map +1 -0
- package/dist/js-script-debug/index.js +12 -0
- package/dist/js-script-debug/index.js.map +1 -0
- package/dist/js-script-debug/markdown.d.ts +6 -0
- package/dist/js-script-debug/markdown.d.ts.map +1 -0
- package/dist/js-script-debug/markdown.js +62 -0
- package/dist/js-script-debug/markdown.js.map +1 -0
- package/dist/js-script-debug/outputs.d.ts +20 -0
- package/dist/js-script-debug/outputs.d.ts.map +1 -0
- package/dist/js-script-debug/outputs.js +30 -0
- package/dist/js-script-debug/outputs.js.map +1 -0
- package/dist/js-script-debug/report.d.ts +15 -0
- package/dist/js-script-debug/report.d.ts.map +1 -0
- package/dist/js-script-debug/report.js +90 -0
- package/dist/js-script-debug/report.js.map +1 -0
- package/dist/js-script-debug/types.d.ts +63 -0
- package/dist/js-script-debug/types.d.ts.map +1 -0
- package/dist/js-script-debug/types.js +10 -0
- package/dist/js-script-debug/types.js.map +1 -0
- package/dist/js-script-debug/validate.d.ts +38 -0
- package/dist/js-script-debug/validate.d.ts.map +1 -0
- package/dist/js-script-debug/validate.js +137 -0
- package/dist/js-script-debug/validate.js.map +1 -0
- package/dist/message-stream.d.ts +33 -0
- package/dist/message-stream.d.ts.map +1 -0
- package/dist/message-stream.js +58 -0
- package/dist/message-stream.js.map +1 -0
- package/dist/normalize-graph.d.ts +13 -0
- package/dist/normalize-graph.d.ts.map +1 -0
- package/dist/normalize-graph.js +63 -0
- package/dist/normalize-graph.js.map +1 -0
- package/dist/output-names.d.ts +13 -0
- package/dist/output-names.d.ts.map +1 -0
- package/dist/output-names.js +13 -0
- package/dist/output-names.js.map +1 -0
- package/dist/predicates.d.ts +6 -0
- package/dist/predicates.d.ts.map +1 -0
- package/dist/predicates.js +6 -0
- package/dist/predicates.js.map +1 -0
- package/dist/preflight.d.ts +87 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +139 -0
- package/dist/preflight.js.map +1 -0
- package/dist/provider-secret-key.d.ts +3 -0
- package/dist/provider-secret-key.d.ts.map +1 -0
- package/dist/provider-secret-key.js +7 -0
- package/dist/provider-secret-key.js.map +1 -0
- package/dist/service/app-debug-service.d.ts +78 -0
- package/dist/service/app-debug-service.d.ts.map +1 -0
- package/dist/service/app-debug-service.js +201 -0
- package/dist/service/app-debug-service.js.map +1 -0
- package/dist/service/app-run-server.d.ts +28 -0
- package/dist/service/app-run-server.d.ts.map +1 -0
- package/dist/service/app-run-server.js +104 -0
- package/dist/service/app-run-server.js.map +1 -0
- package/dist/service/debug-sessions.d.ts +158 -0
- package/dist/service/debug-sessions.d.ts.map +1 -0
- package/dist/service/debug-sessions.js +358 -0
- package/dist/service/debug-sessions.js.map +1 -0
- package/dist/service/index.d.ts +22 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/index.js +17 -0
- package/dist/service/index.js.map +1 -0
- package/dist/service/job-progress.d.ts +40 -0
- package/dist/service/job-progress.d.ts.map +1 -0
- package/dist/service/job-progress.js +53 -0
- package/dist/service/job-progress.js.map +1 -0
- package/dist/service/workflow-run.d.ts +279 -0
- package/dist/service/workflow-run.d.ts.map +1 -0
- package/dist/service/workflow-run.js +598 -0
- package/dist/service/workflow-run.js.map +1 -0
- package/dist/service/workflow-workspace.d.ts +89 -0
- package/dist/service/workflow-workspace.d.ts.map +1 -0
- package/dist/service/workflow-workspace.js +160 -0
- package/dist/service/workflow-workspace.js.map +1 -0
- package/dist/session.d.ts +62 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +353 -0
- package/dist/session.js.map +1 -0
- package/dist/sketch-debug/index.d.ts +11 -0
- package/dist/sketch-debug/index.d.ts.map +1 -0
- package/dist/sketch-debug/index.js +10 -0
- package/dist/sketch-debug/index.js.map +1 -0
- package/dist/sketch-debug/markdown.d.ts +6 -0
- package/dist/sketch-debug/markdown.d.ts.map +1 -0
- package/dist/sketch-debug/markdown.js +93 -0
- package/dist/sketch-debug/markdown.js.map +1 -0
- package/dist/sketch-debug/report.d.ts +15 -0
- package/dist/sketch-debug/report.d.ts.map +1 -0
- package/dist/sketch-debug/report.js +98 -0
- package/dist/sketch-debug/report.js.map +1 -0
- package/dist/sketch-debug/types.d.ts +62 -0
- package/dist/sketch-debug/types.d.ts.map +1 -0
- package/dist/sketch-debug/types.js +10 -0
- package/dist/sketch-debug/types.js.map +1 -0
- package/dist/sketch-debug/validate.d.ts +14 -0
- package/dist/sketch-debug/validate.d.ts.map +1 -0
- package/dist/sketch-debug/validate.js +478 -0
- package/dist/sketch-debug/validate.js.map +1 -0
- package/dist/supervisor.d.ts +45 -0
- package/dist/supervisor.d.ts.map +1 -0
- package/dist/supervisor.js +97 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/timeline-debug/index.d.ts +11 -0
- package/dist/timeline-debug/index.d.ts.map +1 -0
- package/dist/timeline-debug/index.js +10 -0
- package/dist/timeline-debug/index.js.map +1 -0
- package/dist/timeline-debug/legibility.d.ts +23 -0
- package/dist/timeline-debug/legibility.d.ts.map +1 -0
- package/dist/timeline-debug/legibility.js +240 -0
- package/dist/timeline-debug/legibility.js.map +1 -0
- package/dist/timeline-debug/markdown.d.ts +6 -0
- package/dist/timeline-debug/markdown.d.ts.map +1 -0
- package/dist/timeline-debug/markdown.js +103 -0
- package/dist/timeline-debug/markdown.js.map +1 -0
- package/dist/timeline-debug/motion.d.ts +20 -0
- package/dist/timeline-debug/motion.d.ts.map +1 -0
- package/dist/timeline-debug/motion.js +362 -0
- package/dist/timeline-debug/motion.js.map +1 -0
- package/dist/timeline-debug/report.d.ts +16 -0
- package/dist/timeline-debug/report.d.ts.map +1 -0
- package/dist/timeline-debug/report.js +100 -0
- package/dist/timeline-debug/report.js.map +1 -0
- package/dist/timeline-debug/showcase.d.ts +7 -0
- package/dist/timeline-debug/showcase.d.ts.map +1 -0
- package/dist/timeline-debug/showcase.js +347 -0
- package/dist/timeline-debug/showcase.js.map +1 -0
- package/dist/timeline-debug/types.d.ts +64 -0
- package/dist/timeline-debug/types.d.ts.map +1 -0
- package/dist/timeline-debug/types.js +10 -0
- package/dist/timeline-debug/types.js.map +1 -0
- package/dist/timeline-debug/validate.d.ts +24 -0
- package/dist/timeline-debug/validate.d.ts.map +1 -0
- package/dist/timeline-debug/validate.js +1432 -0
- package/dist/timeline-debug/validate.js.map +1 -0
- package/dist/types.d.ts +211 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +103 -0
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
import { previewValue } from "../debug/collector.js";
|
|
2
|
+
import { DEFAULT_OPERATION_ID, eventToAction, parseBinding, operationTarget, parseInputStateKey, resolveBinding, stateKey } from "@nodetool-ai/app-runtime";
|
|
3
|
+
import { jsScriptRunMessages, scriptAppIO, scriptOperationInvocation } from "./script-operation.js";
|
|
4
|
+
import { bindingScopeFor, documentOperations, extractAppIO, operationSpec, parseAppSpec, validateApp } from "./app-spec.js";
|
|
5
|
+
import { effectiveTimeoutMs, HeadlessAppRuntime, writeRefusal } from "./runtime.js";
|
|
6
|
+
import { isNonEmptyString, isObjectLike, isString } from "../predicates.js";
|
|
7
|
+
/** Resolve a step's widget reference: exact id, unique type, or unique label. */
|
|
8
|
+
function findWidget(spec, ref) {
|
|
9
|
+
const byId = spec.widgets.find((w) => w.id === ref);
|
|
10
|
+
if (byId)
|
|
11
|
+
return byId;
|
|
12
|
+
const byType = spec.widgets.filter((w) => w.type === ref);
|
|
13
|
+
if (byType.length === 1)
|
|
14
|
+
return byType[0];
|
|
15
|
+
if (byType.length > 1) {
|
|
16
|
+
return `widget reference "${ref}" is ambiguous — ${byType.length} widgets of that type; use the component id.`;
|
|
17
|
+
}
|
|
18
|
+
const byLabel = spec.widgets.filter((w) => w.label === ref);
|
|
19
|
+
if (byLabel.length === 1)
|
|
20
|
+
return byLabel[0];
|
|
21
|
+
if (byLabel.length > 1) {
|
|
22
|
+
return `widget reference "${ref}" is ambiguous — ${byLabel.length} widgets share that label; use the component id.`;
|
|
23
|
+
}
|
|
24
|
+
return `no widget matches "${ref}" (tried id, type, and label). Widgets: ${widgetRoster(spec)}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* What the caller could have written instead. Naming only the miss sent one
|
|
28
|
+
* agent round after round guessing the step shape, because "no widget matches
|
|
29
|
+
* undefined" says nothing about what a target may be.
|
|
30
|
+
*/
|
|
31
|
+
function widgetRoster(spec) {
|
|
32
|
+
if (spec.widgets.length === 0)
|
|
33
|
+
return "none — the app has no widgets.";
|
|
34
|
+
const listed = spec.widgets
|
|
35
|
+
.slice(0, 12)
|
|
36
|
+
.map((w) => w.label
|
|
37
|
+
? `${w.id} (${w.type}, label "${w.label}")`
|
|
38
|
+
: `${w.id} (${w.type})`)
|
|
39
|
+
.join(", ");
|
|
40
|
+
return spec.widgets.length > 12 ? `${listed}, …` : listed;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The app's natural run trigger, mirroring what a user would do first: click
|
|
44
|
+
* the first `click`→`run` widget (a Run button), else touch the first
|
|
45
|
+
* `change`→`run` write widget.
|
|
46
|
+
*/
|
|
47
|
+
export function defaultInteractions(spec) {
|
|
48
|
+
const clicker = spec.widgets.find((w) => w.events.some((e) => e.trigger === "click" && e.kind === "run"));
|
|
49
|
+
if (clicker)
|
|
50
|
+
return [{ click: clicker.id }];
|
|
51
|
+
const changer = spec.widgets.find((w) => w.bindingMode === "write" &&
|
|
52
|
+
w.events.some((e) => e.trigger === "change" && e.kind === "run"));
|
|
53
|
+
if (changer)
|
|
54
|
+
return [{ change: changer.id, value: undefined }];
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
function describeStep(step) {
|
|
58
|
+
if ("set" in step)
|
|
59
|
+
return `set ${step.set.key}`;
|
|
60
|
+
if ("seedResource" in step)
|
|
61
|
+
return `seedResource ${step.seedResource.id}`;
|
|
62
|
+
if ("click" in step)
|
|
63
|
+
return `click ${step.click}`;
|
|
64
|
+
if ("run" in step)
|
|
65
|
+
return `run ${step.run}`;
|
|
66
|
+
if ("cancel" in step)
|
|
67
|
+
return `cancel ${step.cancel}`;
|
|
68
|
+
return `change ${step.change}`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Node types that emit on one of several output handles per run. A widget fed
|
|
72
|
+
* from one of their branches is *expected* to stay empty whenever the other
|
|
73
|
+
* branch is taken, so a single run cannot tell "the untaken branch" from "a
|
|
74
|
+
* branch that can never be taken".
|
|
75
|
+
*/
|
|
76
|
+
const BRANCHING_NODE_TYPES = new Set(["nodetool.control.If"]);
|
|
77
|
+
/**
|
|
78
|
+
* Node ids reachable only through a branching node. Walks forward from every
|
|
79
|
+
* branch point over the graph's edges.
|
|
80
|
+
*/
|
|
81
|
+
function conditionalNodeIds(graph) {
|
|
82
|
+
const downstream = new Map();
|
|
83
|
+
for (const edge of graph.edges) {
|
|
84
|
+
const source = isString(edge.source) ? edge.source : null;
|
|
85
|
+
const target = isString(edge.target) ? edge.target : null;
|
|
86
|
+
if (!source || !target)
|
|
87
|
+
continue;
|
|
88
|
+
const list = downstream.get(source);
|
|
89
|
+
if (list)
|
|
90
|
+
list.push(target);
|
|
91
|
+
else
|
|
92
|
+
downstream.set(source, [target]);
|
|
93
|
+
}
|
|
94
|
+
const conditional = new Set();
|
|
95
|
+
const queue = [];
|
|
96
|
+
for (const node of graph.nodes) {
|
|
97
|
+
const id = isString(node.id) ? node.id : null;
|
|
98
|
+
const type = isString(node.type) ? node.type : null;
|
|
99
|
+
if (id && type && BRANCHING_NODE_TYPES.has(type))
|
|
100
|
+
queue.push(id);
|
|
101
|
+
}
|
|
102
|
+
while (queue.length > 0) {
|
|
103
|
+
const current = queue.pop();
|
|
104
|
+
for (const next of downstream.get(current) ?? []) {
|
|
105
|
+
if (conditional.has(next))
|
|
106
|
+
continue;
|
|
107
|
+
conditional.add(next);
|
|
108
|
+
queue.push(next);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return conditional;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Overlay the live values of node-property bindings onto the graph, the way the
|
|
115
|
+
* web runtime does before every run (`collectNodePropertyOverlays` →
|
|
116
|
+
* `withNodeProperties` in `web/src/components/appbuilder/nodeBinding.ts`).
|
|
117
|
+
*
|
|
118
|
+
* A widget bound `op:main/prop:node7#strength` writes an input slot keyed by
|
|
119
|
+
* node and property, which no input node reads — the value only reaches the run
|
|
120
|
+
* as a property of the node it names. The web helpers take ReactFlow nodes, so
|
|
121
|
+
* the mapping is repeated here against the kernel shape (`node.properties`);
|
|
122
|
+
* `parseInputStateKey` — the shared inverse of the state key — is what both
|
|
123
|
+
* sides actually agree on.
|
|
124
|
+
*
|
|
125
|
+
* Only the running operation's slots apply: two operations can bind the same
|
|
126
|
+
* node id in different workflows, so an unscoped merge would let one
|
|
127
|
+
* operation's slider drive the other's run.
|
|
128
|
+
*/
|
|
129
|
+
export function withNodePropertyOverlays(graph, inputs, operationId) {
|
|
130
|
+
const byNode = new Map();
|
|
131
|
+
for (const [key, slot] of Object.entries(inputs)) {
|
|
132
|
+
if (slot.value === undefined)
|
|
133
|
+
continue;
|
|
134
|
+
const parsed = parseInputStateKey(key);
|
|
135
|
+
if (!parsed?.property)
|
|
136
|
+
continue;
|
|
137
|
+
if (parsed.operationId !== operationId)
|
|
138
|
+
continue;
|
|
139
|
+
const existing = byNode.get(parsed.nodeId);
|
|
140
|
+
if (existing)
|
|
141
|
+
existing[parsed.property] = slot.value;
|
|
142
|
+
else
|
|
143
|
+
byNode.set(parsed.nodeId, { [parsed.property]: slot.value });
|
|
144
|
+
}
|
|
145
|
+
if (byNode.size === 0)
|
|
146
|
+
return graph;
|
|
147
|
+
return {
|
|
148
|
+
nodes: graph.nodes.map((node) => {
|
|
149
|
+
const overlay = isString(node.id) ? byNode.get(node.id) : undefined;
|
|
150
|
+
if (!overlay)
|
|
151
|
+
return node;
|
|
152
|
+
const properties = isObjectLike(node.properties)
|
|
153
|
+
? node.properties
|
|
154
|
+
: {};
|
|
155
|
+
return { ...node, properties: { ...properties, ...overlay } };
|
|
156
|
+
}),
|
|
157
|
+
edges: graph.edges
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Whether a value that did arrive leaves the widget showing nothing. `""`,
|
|
162
|
+
* `[]` and `null` all render blank, so counting them as "received a value"
|
|
163
|
+
* reports an app that computed nothing as one that worked.
|
|
164
|
+
*/
|
|
165
|
+
function isEmptyValue(value) {
|
|
166
|
+
if (value === undefined || value === null)
|
|
167
|
+
return true;
|
|
168
|
+
if (isString(value))
|
|
169
|
+
return value.length === 0;
|
|
170
|
+
if (Array.isArray(value))
|
|
171
|
+
return value.length === 0;
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* A workflow can complete while a node hands its own failure downstream as a
|
|
176
|
+
* plain value — a Code node that catches and emits the message, which is how
|
|
177
|
+
* the calculator app that motivated this check reported `'eval' is not
|
|
178
|
+
* defined` while every node reported success. The run carries no error in that
|
|
179
|
+
* case and neither does the invocation, so the value's own shape is the only
|
|
180
|
+
* signal there is. Kept to a stringified `Error` at the very start of the
|
|
181
|
+
* value, where prose that merely discusses an error does not match.
|
|
182
|
+
*/
|
|
183
|
+
const ERROR_VALUE = /^(?:[A-Z][A-Za-z]*)?Error:\s+\S/;
|
|
184
|
+
function errorLikeValue(value) {
|
|
185
|
+
if (!isString(value) || !ERROR_VALUE.test(value))
|
|
186
|
+
return null;
|
|
187
|
+
return value.replace(/\s+/g, " ").slice(0, 200);
|
|
188
|
+
}
|
|
189
|
+
/** Node ids keyed by the `name` their data carries, for name-form bindings. */
|
|
190
|
+
function nodeIdsByName(graph) {
|
|
191
|
+
const byName = new Map();
|
|
192
|
+
for (const node of graph.nodes) {
|
|
193
|
+
const id = isString(node.id) ? node.id : null;
|
|
194
|
+
// Runner shape carries node props under `properties`; editor JSON uses
|
|
195
|
+
// `data`, and this runs against both.
|
|
196
|
+
const props = (node.properties ?? node.data);
|
|
197
|
+
const name = isString(props?.name) ? props.name : null;
|
|
198
|
+
if (id && name && !byName.has(name))
|
|
199
|
+
byName.set(name, id);
|
|
200
|
+
}
|
|
201
|
+
return byName;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* What a headless run leaves to the browser. Conditions and `format` are
|
|
205
|
+
* simulated, so only what needs a DOM (or a provider the simulator has not
|
|
206
|
+
* grown yet) belongs here.
|
|
207
|
+
*/
|
|
208
|
+
const NOT_SIMULATED = [
|
|
209
|
+
"Layout, styling, focus, and scroll — nothing here renders a DOM.",
|
|
210
|
+
"Stored resource collections — a run reads the seeded in-memory provider, never the database, and `openResource` has no editor to open.",
|
|
211
|
+
"Reactive subgraph runs — the browser reruns one input's downstream subgraph, the harness runs the whole workflow."
|
|
212
|
+
];
|
|
213
|
+
/**
|
|
214
|
+
* Variables a widget writes rather than the graph — a chat composer's
|
|
215
|
+
* conversation. Nothing the run emits fills them, so an empty one after a
|
|
216
|
+
* headless run says nothing about the wiring.
|
|
217
|
+
*/
|
|
218
|
+
function uiWrittenVariableIds(spec) {
|
|
219
|
+
const ids = new Set();
|
|
220
|
+
for (const widget of spec?.widgets ?? []) {
|
|
221
|
+
for (const extra of widget.extraBindings) {
|
|
222
|
+
if (extra.ref?.kind === "variable")
|
|
223
|
+
ids.add(extra.ref.variableId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return ids;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* What every read-bound widget shows after a run that ended clean: an error
|
|
230
|
+
* message a node handed downstream as a plain value, and — for the widgets that
|
|
231
|
+
* stayed empty — whether that emptiness is a defect or expected.
|
|
232
|
+
*/
|
|
233
|
+
function widgetValueFindings(report, graph) {
|
|
234
|
+
const issues = [];
|
|
235
|
+
const warnings = [];
|
|
236
|
+
const conditional = conditionalNodeIds(graph);
|
|
237
|
+
const byName = nodeIdsByName(graph);
|
|
238
|
+
const nodeIds = new Set(graph.nodes.map((n) => n.id).filter(isString));
|
|
239
|
+
const uiWrittenVariables = uiWrittenVariableIds(report.spec);
|
|
240
|
+
const ranOperations = new Set(report.invocations.map((i) => i.operationId));
|
|
241
|
+
for (const w of report.widgets) {
|
|
242
|
+
if (w.bindingMode !== "read" || !w.binding)
|
|
243
|
+
continue;
|
|
244
|
+
const message = errorLikeValue(w.display ?? w.value);
|
|
245
|
+
if (message) {
|
|
246
|
+
issues.push(`${w.type} "${w.id}" shows an error message from "${w.binding}": ${message}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
for (const w of report.widgets) {
|
|
250
|
+
if (w.bindingMode !== "read" || !w.binding || w.hasValue)
|
|
251
|
+
continue;
|
|
252
|
+
const ref = parseBinding(w.binding);
|
|
253
|
+
if (ref?.kind === "variable" && uiWrittenVariables.has(ref.variableId)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (ref?.kind === "execution") {
|
|
257
|
+
// Execution state is not a value the graph emits — an empty one only
|
|
258
|
+
// means the operation never ran, or reported no activity.
|
|
259
|
+
if (!ranOperations.has(ref.operationId)) {
|
|
260
|
+
issues.push(`${w.type} "${w.id}" shows operation "${ref.operationId}" ${ref.field}, but that operation never ran.`);
|
|
261
|
+
}
|
|
262
|
+
else if (ref.field === "activity") {
|
|
263
|
+
warnings.push(`${w.type} "${w.id}" shows activity, but the run reported none — only streaming agent nodes emit activity labels.`);
|
|
264
|
+
}
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
// A legacy document binds by node name, and `parseBinding` hands the name
|
|
268
|
+
// back in the `nodeId` slot, so an unrecognised id is retried as a name.
|
|
269
|
+
const parsed = (ref && "nodeId" in ref ? ref.nodeId : null) ?? w.binding;
|
|
270
|
+
const nodeId = nodeIds.has(parsed) ? parsed : (byName.get(parsed) ?? null);
|
|
271
|
+
if (nodeId && conditional.has(nodeId)) {
|
|
272
|
+
// One run takes one branch, so an empty widget here is expected. It is
|
|
273
|
+
// still worth surfacing: a branch that no input can reach looks exactly
|
|
274
|
+
// the same, and only running both branches tells them apart.
|
|
275
|
+
warnings.push(`${w.type} "${w.id}" is bound to "${w.binding}", downstream of a branch that was not taken this run — run the other branch to confirm it can be reached.`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
// The value arrived and is empty ("", [], null). A warning rather than an
|
|
279
|
+
// issue: an output is legitimately empty often enough — no matches, no
|
|
280
|
+
// remainder, nothing to say — that failing the verdict on it would call
|
|
281
|
+
// working apps broken. An app that computed nothing looks the same from
|
|
282
|
+
// here, so the report says what happened and leaves the call to the author.
|
|
283
|
+
// A widget whose `format` template renders nothing keeps the issue below:
|
|
284
|
+
// the value reached it and the template still dropped it, which is wiring,
|
|
285
|
+
// not data.
|
|
286
|
+
if (w.display === null && w.value !== undefined && !w.resourceBindingId) {
|
|
287
|
+
warnings.push(`${w.type} "${w.id}" is bound to "${w.binding}" but received an empty value — confirm the output is meant to be empty.`);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
issues.push(`${w.type} "${w.id}" is bound to "${w.binding}" but never received a value — check the output node emits.`);
|
|
291
|
+
}
|
|
292
|
+
return { issues, warnings };
|
|
293
|
+
}
|
|
294
|
+
function verdictHeadline(firstIssue, runCount) {
|
|
295
|
+
if (firstIssue !== undefined)
|
|
296
|
+
return `App has issues — ${firstIssue}`;
|
|
297
|
+
if (runCount > 0) {
|
|
298
|
+
return `App ran clean — ${runCount} run(s), every bound widget on a taken branch received a value.`;
|
|
299
|
+
}
|
|
300
|
+
return "App wiring is valid (static check only — no run executed).";
|
|
301
|
+
}
|
|
302
|
+
function buildAppVerdict(report, ranWorkflow, graph, conditionIssues) {
|
|
303
|
+
const issues = [...report.validation.errors, ...conditionIssues];
|
|
304
|
+
const warnings = [];
|
|
305
|
+
for (const interaction of report.interactions) {
|
|
306
|
+
if (interaction.error) {
|
|
307
|
+
issues.push(`Interaction "${interaction.step}": ${interaction.error}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const run of report.runs) {
|
|
311
|
+
if (!run.ok) {
|
|
312
|
+
issues.push(`Run ended ${run.status}${run.error ? `: ${run.error}` : ""}`);
|
|
313
|
+
}
|
|
314
|
+
for (const e of run.summary.errors.slice(0, 5)) {
|
|
315
|
+
const where = e.nodeType ?? e.nodeId ?? "workflow";
|
|
316
|
+
issues.push(`Node ${where}: ${e.message.replace(/\s+/g, " ").slice(0, 200)}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
for (const invocation of report.invocations) {
|
|
320
|
+
if (invocation.timedOutMs == null)
|
|
321
|
+
continue;
|
|
322
|
+
issues.push(`Operation "${invocation.operationId}" did not finish within its ${invocation.timedOutMs}ms timeout — the app would show it still running.`);
|
|
323
|
+
}
|
|
324
|
+
// A picker over an unseeded collection renders empty, and no interaction can
|
|
325
|
+
// pick from it — worth saying, but only the script knows what belongs there.
|
|
326
|
+
for (const resource of report.resources) {
|
|
327
|
+
if (resource.seeded)
|
|
328
|
+
continue;
|
|
329
|
+
const shown = report.widgets
|
|
330
|
+
.filter((w) => w.resourceBindingId === resource.id)
|
|
331
|
+
.map((w) => `${w.type} "${w.id}"`);
|
|
332
|
+
if (shown.length === 0)
|
|
333
|
+
continue;
|
|
334
|
+
warnings.push(`${shown.join(", ")} shows resource binding "${resource.id}", which nothing seeded — the collection is empty. Seed it with a seedResource step or a "resource:${resource.id}" param.`);
|
|
335
|
+
}
|
|
336
|
+
const ranClean = ranWorkflow && report.runs.length > 0 && report.runs.every((r) => r.ok);
|
|
337
|
+
if (ranClean) {
|
|
338
|
+
const found = widgetValueFindings(report, graph);
|
|
339
|
+
issues.push(...found.issues);
|
|
340
|
+
warnings.push(...found.warnings);
|
|
341
|
+
}
|
|
342
|
+
const neverRan = ranWorkflow && report.runs.length === 0 && report.invocations.length === 0;
|
|
343
|
+
if (neverRan && (report.spec?.widgets.length ?? 0) > 0) {
|
|
344
|
+
issues.push("No interaction triggered a workflow run — the app was never executed.");
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
ok: issues.length === 0,
|
|
348
|
+
headline: verdictHeadline(issues[0], report.runs.length),
|
|
349
|
+
issues,
|
|
350
|
+
warnings
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
/** A finished script run in the report's server-run shape. */
|
|
354
|
+
function scriptRunReport(result, durationMs, scriptName, messagesFile) {
|
|
355
|
+
const status = result.ok ? "completed" : "failed";
|
|
356
|
+
const report = {
|
|
357
|
+
surface: "server",
|
|
358
|
+
ok: result.ok,
|
|
359
|
+
status,
|
|
360
|
+
error: result.error ?? null,
|
|
361
|
+
durationMs,
|
|
362
|
+
summary: {
|
|
363
|
+
status,
|
|
364
|
+
error: result.error ?? null,
|
|
365
|
+
nodes: [],
|
|
366
|
+
logs: result.logs.map((content) => ({
|
|
367
|
+
nodeId: scriptName,
|
|
368
|
+
severity: "info",
|
|
369
|
+
content
|
|
370
|
+
})),
|
|
371
|
+
edges: [],
|
|
372
|
+
llmCalls: [],
|
|
373
|
+
outputs: [],
|
|
374
|
+
interventions: [],
|
|
375
|
+
counts: {
|
|
376
|
+
nodes: 0,
|
|
377
|
+
completed: result.ok ? 1 : 0,
|
|
378
|
+
errored: result.ok ? 0 : 1,
|
|
379
|
+
logs: result.logs.length,
|
|
380
|
+
outputs: 0,
|
|
381
|
+
llmCalls: 0,
|
|
382
|
+
interventions: 0
|
|
383
|
+
},
|
|
384
|
+
errors: result.error
|
|
385
|
+
? [{ nodeId: scriptName, message: result.error }]
|
|
386
|
+
: []
|
|
387
|
+
},
|
|
388
|
+
trace: null
|
|
389
|
+
};
|
|
390
|
+
if (messagesFile) {
|
|
391
|
+
report.messagesFile = messagesFile;
|
|
392
|
+
}
|
|
393
|
+
return report;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* What a script operation runs: the document the target carries under the key
|
|
397
|
+
* its target names, else the one the host loads by id and version.
|
|
398
|
+
*/
|
|
399
|
+
async function resolveOperationScript(target, carried, loadScript) {
|
|
400
|
+
const bundled = carried?.get(target.scriptId);
|
|
401
|
+
if (bundled)
|
|
402
|
+
return { script: bundled, unavailable: null };
|
|
403
|
+
if (!loadScript) {
|
|
404
|
+
return {
|
|
405
|
+
script: null,
|
|
406
|
+
unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which this host cannot resolve.`
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const script = await loadScript(target.scriptId, target.scriptVersion);
|
|
411
|
+
if (!script) {
|
|
412
|
+
return {
|
|
413
|
+
script: null,
|
|
414
|
+
unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which does not exist.`
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return { script, unavailable: null };
|
|
418
|
+
}
|
|
419
|
+
catch (error) {
|
|
420
|
+
return {
|
|
421
|
+
script: null,
|
|
422
|
+
unavailable: `runs JS script "${target.scriptId}" v${target.scriptVersion}, which could not be loaded: ${String(error)}`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* The graph an operation runs: one the target carries (a bundle's workflows,
|
|
428
|
+
* the host workflow), else the database.
|
|
429
|
+
*/
|
|
430
|
+
async function resolveOperationGraph(operation, host, loadFromDb) {
|
|
431
|
+
const target = operation.workflowId;
|
|
432
|
+
const carried = host.graphs.get(target);
|
|
433
|
+
if (carried)
|
|
434
|
+
return { graph: carried, unavailable: null };
|
|
435
|
+
if (!target || target === "self" || target === host.workflowId) {
|
|
436
|
+
return { graph: host.graph, unavailable: null };
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
const workflow = await loadFromDb(target);
|
|
440
|
+
if (!workflow?.graph) {
|
|
441
|
+
return {
|
|
442
|
+
graph: null,
|
|
443
|
+
unavailable: `runs workflow "${target}", which is not in the local database.`
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
return { graph: workflow.graph, unavailable: null };
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
return {
|
|
450
|
+
graph: null,
|
|
451
|
+
unavailable: `runs workflow "${target}", which could not be loaded: ${String(error)}`
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
export async function simulateApp(resolved, options, deps) {
|
|
456
|
+
const log = deps.onLog ?? (() => { });
|
|
457
|
+
const allowRuns = options.run ?? true;
|
|
458
|
+
const io = extractAppIO(resolved.graph);
|
|
459
|
+
// The target loader already parsed the document; `parseAppSpec` reports any
|
|
460
|
+
// issue it hit.
|
|
461
|
+
const document = resolved.document;
|
|
462
|
+
// Resolve every declared operation against a real graph before parsing the
|
|
463
|
+
// widgets: a binding into a second operation resolves against that
|
|
464
|
+
// operation's surface, not the host's.
|
|
465
|
+
const graphByOperation = new Map();
|
|
466
|
+
const context = {
|
|
467
|
+
defaultOperationId: DEFAULT_OPERATION_ID,
|
|
468
|
+
operations: [],
|
|
469
|
+
variables: document?.variables ?? [],
|
|
470
|
+
resources: document?.resources.map(({ id, name, kind }) => ({ id, name, kind })) ??
|
|
471
|
+
[]
|
|
472
|
+
};
|
|
473
|
+
const bindingByOperation = new Map();
|
|
474
|
+
const scriptByOperation = new Map();
|
|
475
|
+
if (document) {
|
|
476
|
+
for (const binding of documentOperations(document)) {
|
|
477
|
+
const target = operationTarget(binding);
|
|
478
|
+
bindingByOperation.set(binding.id, binding);
|
|
479
|
+
if (target.kind === "script") {
|
|
480
|
+
const { script, unavailable } = await resolveOperationScript(target, resolved.scripts, deps.loadScript);
|
|
481
|
+
if (script)
|
|
482
|
+
scriptByOperation.set(binding.id, script);
|
|
483
|
+
context.operations.push(operationSpec(binding, script ? scriptAppIO(script.document) : null, script && !deps.runScript
|
|
484
|
+
? `runs JS script "${script.name}", which this host cannot execute.`
|
|
485
|
+
: unavailable));
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const { graph, unavailable } = await resolveOperationGraph(binding, {
|
|
489
|
+
workflowId: resolved.info.workflowId,
|
|
490
|
+
graph: resolved.graph,
|
|
491
|
+
graphs: resolved.graphs
|
|
492
|
+
}, deps.loadFromDb);
|
|
493
|
+
const operationIO = graph === resolved.graph ? io : graph ? extractAppIO(graph) : null;
|
|
494
|
+
if (graph)
|
|
495
|
+
graphByOperation.set(binding.id, graph);
|
|
496
|
+
context.operations.push(operationSpec(binding, operationIO, unavailable));
|
|
497
|
+
}
|
|
498
|
+
// The first declared operation, exactly as the web runtime picks it
|
|
499
|
+
// (`operations[0]` in `useAppRuntime`) — so a bare-name binding resolves to
|
|
500
|
+
// the same operation headlessly and in the browser.
|
|
501
|
+
context.defaultOperationId =
|
|
502
|
+
context.operations[0]?.id ?? DEFAULT_OPERATION_ID;
|
|
503
|
+
}
|
|
504
|
+
const { spec, issues: parseIssues, warnings: parseWarnings } = parseAppSpec(resolved.appDoc, io, document ? context : undefined, {
|
|
505
|
+
document,
|
|
506
|
+
issue: resolved.issue
|
|
507
|
+
});
|
|
508
|
+
const validation = spec
|
|
509
|
+
? validateApp(spec, io, context)
|
|
510
|
+
: { errors: [], warnings: [] };
|
|
511
|
+
validation.errors = [...parseIssues, ...validation.errors];
|
|
512
|
+
validation.warnings = [...parseWarnings, ...validation.warnings];
|
|
513
|
+
const report = {
|
|
514
|
+
generatedAt: new Date().toISOString(),
|
|
515
|
+
target: resolved.info,
|
|
516
|
+
app: spec
|
|
517
|
+
? {
|
|
518
|
+
version: spec.version,
|
|
519
|
+
// A document keeps its title in the Puck root; an application row
|
|
520
|
+
// keeps it as the row's name.
|
|
521
|
+
title: spec.title ?? resolved.appName,
|
|
522
|
+
widgetCount: spec.widgets.length
|
|
523
|
+
}
|
|
524
|
+
: null,
|
|
525
|
+
spec,
|
|
526
|
+
io: {
|
|
527
|
+
inputs: io.inputs.map((i) => i.name),
|
|
528
|
+
outputs: io.outputs.map((o) => o.name),
|
|
529
|
+
variables: io.variables
|
|
530
|
+
},
|
|
531
|
+
validation,
|
|
532
|
+
interactions: [],
|
|
533
|
+
runs: [],
|
|
534
|
+
values: {},
|
|
535
|
+
variables: {},
|
|
536
|
+
invocations: [],
|
|
537
|
+
activity: [],
|
|
538
|
+
widgets: [],
|
|
539
|
+
resources: [],
|
|
540
|
+
notSimulated: [...NOT_SIMULATED],
|
|
541
|
+
verdict: { ok: false, headline: "", issues: [] },
|
|
542
|
+
bundleDir: null
|
|
543
|
+
};
|
|
544
|
+
/** Verdict issues the condition simulation found, folded in at the end. */
|
|
545
|
+
const conditionIssues = [];
|
|
546
|
+
if (spec) {
|
|
547
|
+
const runs = report.runs;
|
|
548
|
+
const runWorkflow = async (operationId, graph, workflowId, timeoutMs, params) => {
|
|
549
|
+
log(`Running operation "${operationId}"…`);
|
|
550
|
+
const runInput = {
|
|
551
|
+
graph: withNodePropertyOverlays(graph, runtime.state.inputs, operationId),
|
|
552
|
+
workflowId,
|
|
553
|
+
params
|
|
554
|
+
};
|
|
555
|
+
if (timeoutMs != null) {
|
|
556
|
+
runInput.timeoutMs = timeoutMs;
|
|
557
|
+
}
|
|
558
|
+
const outcome = await deps.runOnServer(runInput);
|
|
559
|
+
// The slot is claimed only once the run settles, so a run the harness
|
|
560
|
+
// timed out on never takes an index from the one that follows it.
|
|
561
|
+
const runIndex = runs.length;
|
|
562
|
+
const messagesFile = await deps.onRunMessages?.(runIndex, outcome.rawMessages);
|
|
563
|
+
if (messagesFile)
|
|
564
|
+
outcome.report.messagesFile = messagesFile;
|
|
565
|
+
runs.push(outcome.report);
|
|
566
|
+
log(`Run ${runIndex + 1}: ${outcome.report.status} · ${outcome.report.summary.counts.errored} node error(s)`);
|
|
567
|
+
// SAFETY: the fold reads a message by field name off the wire shape,
|
|
568
|
+
// and a ProcessingMessage is that shape — it is what the runner
|
|
569
|
+
// serializes and what the web runtime folds.
|
|
570
|
+
return {
|
|
571
|
+
messages: outcome.rawMessages,
|
|
572
|
+
runIndex
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
/**
|
|
576
|
+
* Run one script operation and hand back the stream the fold expects. The
|
|
577
|
+
* run endpoint answers with plain JSON, so `jsScriptRunMessages` is what
|
|
578
|
+
* turns that into per-emit and final output messages.
|
|
579
|
+
*/
|
|
580
|
+
const runScriptOperation = async (operationId, target, script, timeoutMs, inputs) => {
|
|
581
|
+
log(`Running script operation "${operationId}"…`);
|
|
582
|
+
const started = Date.now();
|
|
583
|
+
// A streaming body reads its inputs off the inbox, so the operation's
|
|
584
|
+
// one value per input is staged as a one-item stream instead.
|
|
585
|
+
const invocation = scriptOperationInvocation(script.document, inputs);
|
|
586
|
+
const scriptInput = {
|
|
587
|
+
scriptId: target.scriptId,
|
|
588
|
+
scriptVersion: target.scriptVersion,
|
|
589
|
+
name: script.name,
|
|
590
|
+
document: script.document,
|
|
591
|
+
inputs: invocation.inputs
|
|
592
|
+
};
|
|
593
|
+
if (invocation.inputStreams) {
|
|
594
|
+
scriptInput.inputStreams = invocation.inputStreams;
|
|
595
|
+
}
|
|
596
|
+
if (timeoutMs != null) {
|
|
597
|
+
scriptInput.timeoutMs = timeoutMs;
|
|
598
|
+
}
|
|
599
|
+
const result = await deps.runScript(scriptInput);
|
|
600
|
+
const messages = jsScriptRunMessages(result);
|
|
601
|
+
const runIndex = runs.length;
|
|
602
|
+
// SAFETY: `jsScriptRunMessages` builds the same per-emit and final
|
|
603
|
+
// message shapes the runner streams, and this hook only serializes them
|
|
604
|
+
// to JSONL.
|
|
605
|
+
const messagesFile = await deps.onRunMessages?.(runIndex, messages);
|
|
606
|
+
runs.push(scriptRunReport(result, Date.now() - started, script.name, messagesFile));
|
|
607
|
+
log(`Run ${runIndex + 1}: ${result.ok ? "completed" : "failed"}`);
|
|
608
|
+
return { messages, runIndex };
|
|
609
|
+
};
|
|
610
|
+
const scope = bindingScopeFor(io, context);
|
|
611
|
+
const operations = context.operations.map((op) => {
|
|
612
|
+
const binding = bindingByOperation.get(op.id);
|
|
613
|
+
const script = scriptByOperation.get(op.id) ?? null;
|
|
614
|
+
const graph = graphByOperation.get(op.id) ?? null;
|
|
615
|
+
const operationIO = op.io ?? {
|
|
616
|
+
inputs: [],
|
|
617
|
+
outputs: [],
|
|
618
|
+
variables: [],
|
|
619
|
+
nodeIds: []
|
|
620
|
+
};
|
|
621
|
+
const defaults = {};
|
|
622
|
+
for (const input of operationIO.inputs) {
|
|
623
|
+
if (input.defaultValue === undefined)
|
|
624
|
+
continue;
|
|
625
|
+
defaults[stateKey({ kind: "input", operationId: op.id, nodeId: input.nodeId })] = input.defaultValue;
|
|
626
|
+
}
|
|
627
|
+
const timeoutMs = effectiveTimeoutMs(op.timeoutMs, options.timeoutMs);
|
|
628
|
+
const init = {
|
|
629
|
+
binding,
|
|
630
|
+
outputKeyByNodeId: new Map(operationIO.outputs.map((o) => [
|
|
631
|
+
o.nodeId,
|
|
632
|
+
stateKey({ kind: "output", operationId: op.id, nodeId: o.nodeId })
|
|
633
|
+
])),
|
|
634
|
+
inputNodeIds: operationIO.inputs.map((i) => i.nodeId),
|
|
635
|
+
inputNameByNodeId: new Map(operationIO.inputs.map((i) => [i.nodeId, i.name])),
|
|
636
|
+
inputNodeTypeByNodeId: new Map(operationIO.inputs.map((i) => [i.nodeId, i.nodeType])),
|
|
637
|
+
defaults
|
|
638
|
+
};
|
|
639
|
+
if (script && deps.runScript) {
|
|
640
|
+
init.runWorkflow = (params) => runScriptOperation(op.id, op.target, script, timeoutMs, params);
|
|
641
|
+
}
|
|
642
|
+
// A workflow graph wins over the script binding, exactly as the later
|
|
643
|
+
// spread did before.
|
|
644
|
+
if (graph) {
|
|
645
|
+
init.runWorkflow = (params) => runWorkflow(op.id, graph,
|
|
646
|
+
// A bundle's operations name bundle-local keys, so there is
|
|
647
|
+
// no workflow id to attribute the run to.
|
|
648
|
+
resolved.operationsReferenceKeys
|
|
649
|
+
? null
|
|
650
|
+
: graph === resolved.graph
|
|
651
|
+
? resolved.info.workflowId
|
|
652
|
+
: op.workflowId, timeoutMs, params);
|
|
653
|
+
}
|
|
654
|
+
return init;
|
|
655
|
+
});
|
|
656
|
+
const runtimeInit = {
|
|
657
|
+
operations,
|
|
658
|
+
defaultOperationId: context.defaultOperationId,
|
|
659
|
+
variables: context.variables,
|
|
660
|
+
resources: document?.resources ?? [],
|
|
661
|
+
scope,
|
|
662
|
+
widgets: spec.widgets.map((w) => {
|
|
663
|
+
const widget = { id: w.id };
|
|
664
|
+
if (w.visibleWhen) {
|
|
665
|
+
widget.visibleWhen = w.visibleWhen;
|
|
666
|
+
}
|
|
667
|
+
if (w.disabledWhen) {
|
|
668
|
+
widget.disabledWhen = w.disabledWhen;
|
|
669
|
+
}
|
|
670
|
+
if (w.format) {
|
|
671
|
+
widget.format = w.format;
|
|
672
|
+
}
|
|
673
|
+
return widget;
|
|
674
|
+
})
|
|
675
|
+
};
|
|
676
|
+
if (options.timeoutMs != null) {
|
|
677
|
+
runtimeInit.timeoutMs = options.timeoutMs;
|
|
678
|
+
}
|
|
679
|
+
const runtime = new HeadlessAppRuntime(runtimeInit);
|
|
680
|
+
/**
|
|
681
|
+
* Write a value addressed by name (a param, or an `--interact` set step),
|
|
682
|
+
* returning why it did not land, or null once it did. Reading a name in
|
|
683
|
+
* write mode first and read mode second means an output name resolves — to
|
|
684
|
+
* a slot nothing can write, which is a failed step, not a silent no-op.
|
|
685
|
+
*/
|
|
686
|
+
const writeByName = (name, value, operationId = context.defaultOperationId) => {
|
|
687
|
+
const ref = resolveBinding(name, scope, "write", operationId) ??
|
|
688
|
+
resolveBinding(name, scope, "read", operationId);
|
|
689
|
+
if (!ref)
|
|
690
|
+
return `"${name}" matches no input, output, or variable.`;
|
|
691
|
+
const refusal = writeRefusal(ref);
|
|
692
|
+
if (refusal)
|
|
693
|
+
return `"${name}" ${refusal}.`;
|
|
694
|
+
runtime.write(ref, value);
|
|
695
|
+
return null;
|
|
696
|
+
};
|
|
697
|
+
/** Seed one collection, reporting why a bad seed did nothing. */
|
|
698
|
+
const seedResource = (resourceBindingId, items) => {
|
|
699
|
+
if (!Array.isArray(items)) {
|
|
700
|
+
return `Resource seed for "${resourceBindingId}" is not an array of items.`;
|
|
701
|
+
}
|
|
702
|
+
const seeds = [];
|
|
703
|
+
for (const item of items) {
|
|
704
|
+
const seed = item;
|
|
705
|
+
if (!seed || !isNonEmptyString(seed.id)) {
|
|
706
|
+
return `Resource seed for "${resourceBindingId}" has an item without an "id".`;
|
|
707
|
+
}
|
|
708
|
+
seeds.push(seed);
|
|
709
|
+
}
|
|
710
|
+
try {
|
|
711
|
+
runtime.seedResource(resourceBindingId, seeds);
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
catch (error) {
|
|
715
|
+
return error.message;
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
for (const [key, value] of Object.entries({
|
|
719
|
+
...resolved.fileParams,
|
|
720
|
+
...(options.params ?? {})
|
|
721
|
+
})) {
|
|
722
|
+
// `--params '{"resource:docs": [...]}'` seeds a collection; every other
|
|
723
|
+
// key writes a reactive value.
|
|
724
|
+
if (key.startsWith("resource:")) {
|
|
725
|
+
const failure = seedResource(key.slice("resource:".length), value);
|
|
726
|
+
if (failure)
|
|
727
|
+
report.validation.warnings.push(failure);
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
const failure = writeByName(key, value);
|
|
731
|
+
if (failure)
|
|
732
|
+
report.validation.warnings.push(`Param ${failure}`);
|
|
733
|
+
}
|
|
734
|
+
/** Dispatch one action, tracking which run it produced. */
|
|
735
|
+
const dispatch = async (record, action) => {
|
|
736
|
+
const before = runtime.invocations.length;
|
|
737
|
+
try {
|
|
738
|
+
await runtime.dispatch(action);
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
record.error = record.error ?? error.message;
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
const started = runtime.invocations.slice(before);
|
|
745
|
+
const withRun = started.find((i) => i.runIndex !== null);
|
|
746
|
+
if (withRun)
|
|
747
|
+
record.runIndex = withRun.runIndex;
|
|
748
|
+
if (action.kind === "run") {
|
|
749
|
+
record.error = record.error ?? runtime.errorFor(action.operationId);
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
const steps = options.interact ?? defaultInteractions(spec);
|
|
753
|
+
for (const step of steps) {
|
|
754
|
+
const record = {
|
|
755
|
+
step: describeStep(step),
|
|
756
|
+
actions: [],
|
|
757
|
+
runIndex: null,
|
|
758
|
+
error: null
|
|
759
|
+
};
|
|
760
|
+
report.interactions.push(record);
|
|
761
|
+
if ("set" in step) {
|
|
762
|
+
const failure = writeByName(step.set.key, step.set.value, step.set.operationId);
|
|
763
|
+
if (failure)
|
|
764
|
+
record.error = failure;
|
|
765
|
+
else
|
|
766
|
+
record.actions.push(`set ${step.set.key}`);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
if ("seedResource" in step) {
|
|
770
|
+
const { id, items } = step.seedResource;
|
|
771
|
+
const failure = seedResource(id, items);
|
|
772
|
+
if (failure)
|
|
773
|
+
record.error = failure;
|
|
774
|
+
else
|
|
775
|
+
record.actions.push(`seedResource ${id} (${items.length} item(s))`);
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
if ("run" in step) {
|
|
779
|
+
if (!allowRuns) {
|
|
780
|
+
record.actions.push("run (skipped — --no-run)");
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
783
|
+
record.actions.push(`run ${step.run}`);
|
|
784
|
+
await dispatch(record, { kind: "run", operationId: step.run });
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
if ("cancel" in step) {
|
|
788
|
+
const cancelled = runtime.cancel(step.cancel);
|
|
789
|
+
record.actions.push(cancelled.length > 0
|
|
790
|
+
? `cancel ${step.cancel} (${cancelled.join(", ")})`
|
|
791
|
+
: `cancel ${step.cancel} (nothing running)`);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const trigger = "click" in step ? "click" : "change";
|
|
795
|
+
const found = findWidget(spec, "click" in step ? step.click : step.change);
|
|
796
|
+
if (isString(found)) {
|
|
797
|
+
record.error = found;
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
// A `set` step writes state directly and models the runtime; a click or a
|
|
801
|
+
// change models a user, and a user cannot touch what the app hides or
|
|
802
|
+
// disables.
|
|
803
|
+
const gate = runtime.widgetState(found.id);
|
|
804
|
+
const verb = trigger === "click" ? "clicked" : "changed";
|
|
805
|
+
const named = found.label ?? found.id;
|
|
806
|
+
if (gate && !gate.visible) {
|
|
807
|
+
record.error = `${verb} "${named}" while hidden by \`${gate.visibleWhen ?? "visibleWhen"}\`.`;
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
if (gate?.disabled) {
|
|
811
|
+
record.error = `${verb} "${named}" while disabled by \`${gate.disabledWhen ?? "disabledWhen"}\`.`;
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if ("change" in step && step.value !== undefined) {
|
|
815
|
+
// A change whose value never landed must not go on to fire the widget's
|
|
816
|
+
// events: the run behind them would read the old value.
|
|
817
|
+
if (!found.ref) {
|
|
818
|
+
record.error = found.binding
|
|
819
|
+
? `changed "${named}" but its binding "${found.binding}" resolves to nothing — nothing was set.`
|
|
820
|
+
: `changed "${named}", which has no binding to write to.`;
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const refusal = writeRefusal(found.ref);
|
|
824
|
+
if (refusal) {
|
|
825
|
+
record.error = `changed "${named}", whose binding "${found.binding ?? ""}" ${refusal}.`;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
runtime.write(found.ref, step.value);
|
|
829
|
+
record.actions.push(`set ${found.stateKey ?? found.id}`);
|
|
830
|
+
}
|
|
831
|
+
// A run from a bound write widget carries its binding; the web engine
|
|
832
|
+
// would run just that input's downstream subgraph — headless, both paths
|
|
833
|
+
// are a full authoritative run.
|
|
834
|
+
const from = found.bindingMode === "write"
|
|
835
|
+
? (found.canonicalBinding ?? undefined)
|
|
836
|
+
: undefined;
|
|
837
|
+
const eventCtx = {
|
|
838
|
+
defaultOperationId: context.defaultOperationId,
|
|
839
|
+
resolveVariableId: (key) => {
|
|
840
|
+
const ref = resolveBinding(key, scope, "read");
|
|
841
|
+
return ref?.kind === "variable" ? ref.variableId : null;
|
|
842
|
+
},
|
|
843
|
+
from
|
|
844
|
+
};
|
|
845
|
+
for (const event of found.events) {
|
|
846
|
+
if (event.trigger !== trigger)
|
|
847
|
+
continue;
|
|
848
|
+
const action = eventToAction(event, eventCtx);
|
|
849
|
+
if (!action) {
|
|
850
|
+
record.error =
|
|
851
|
+
record.error ??
|
|
852
|
+
`event "${event.kind}" is incomplete — nothing to dispatch.`;
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
if (action.kind === "run" && !allowRuns) {
|
|
856
|
+
record.actions.push("run (skipped — --no-run)");
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
record.actions.push(action.kind === "setVariable" || action.kind === "toggleVariable"
|
|
860
|
+
? `${action.kind} ${action.variableId}`
|
|
861
|
+
: action.kind === "run" || action.kind === "cancel"
|
|
862
|
+
? `${action.kind} ${action.operationId ?? context.defaultOperationId}`
|
|
863
|
+
: action.kind);
|
|
864
|
+
await dispatch(record, action);
|
|
865
|
+
}
|
|
866
|
+
if (record.actions.length === 0) {
|
|
867
|
+
record.error = `widget has no "${trigger}" events to fire.`;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// The report reads better keyed by the names an author recognizes than by
|
|
871
|
+
// the runtime's namespaced state keys. A second operation prefixes its
|
|
872
|
+
// names so two operations over one workflow stay apart.
|
|
873
|
+
const values = {};
|
|
874
|
+
for (const operation of context.operations) {
|
|
875
|
+
const prefix = operation.id === context.defaultOperationId ? "" : `${operation.id}.`;
|
|
876
|
+
for (const input of operation.io?.inputs ?? []) {
|
|
877
|
+
const value = runtime.read({
|
|
878
|
+
kind: "input",
|
|
879
|
+
operationId: operation.id,
|
|
880
|
+
nodeId: input.nodeId
|
|
881
|
+
});
|
|
882
|
+
if (value !== undefined)
|
|
883
|
+
values[`${prefix}${input.name}`] = value;
|
|
884
|
+
}
|
|
885
|
+
for (const output of operation.io?.outputs ?? []) {
|
|
886
|
+
const value = runtime.read({
|
|
887
|
+
kind: "output",
|
|
888
|
+
operationId: operation.id,
|
|
889
|
+
nodeId: output.nodeId
|
|
890
|
+
});
|
|
891
|
+
if (value !== undefined)
|
|
892
|
+
values[`${prefix}${output.name}`] = value;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const variables = {};
|
|
896
|
+
for (const [variableId, value] of Object.entries(runtime.state.variables)) {
|
|
897
|
+
if (value === undefined)
|
|
898
|
+
continue;
|
|
899
|
+
values[variableId] = value;
|
|
900
|
+
variables[variableId] = value;
|
|
901
|
+
}
|
|
902
|
+
report.values = previewValue(values);
|
|
903
|
+
report.variables = previewValue(variables);
|
|
904
|
+
report.invocations = runtime.invocations.map((invocation) => ({
|
|
905
|
+
id: invocation.id,
|
|
906
|
+
operationId: invocation.operationId,
|
|
907
|
+
status: runtime.state.invocations[invocation.id]?.status ?? "unknown",
|
|
908
|
+
decision: invocation.decision,
|
|
909
|
+
decisionTargets: invocation.decisionTargets,
|
|
910
|
+
runIndex: invocation.runIndex,
|
|
911
|
+
timedOutMs: invocation.timedOutMs,
|
|
912
|
+
error: runtime.state.invocations[invocation.id]?.error ?? null,
|
|
913
|
+
activity: invocation.activity
|
|
914
|
+
}));
|
|
915
|
+
report.activity = [...runtime.activity];
|
|
916
|
+
report.resources = runtime.resourceBindingList.map((binding) => {
|
|
917
|
+
const provider = runtime.resourceProvider(binding.id);
|
|
918
|
+
const items = provider?.list() ?? [];
|
|
919
|
+
return {
|
|
920
|
+
id: binding.id,
|
|
921
|
+
kind: binding.kind,
|
|
922
|
+
seeded: provider !== null,
|
|
923
|
+
items: items.map((item) => {
|
|
924
|
+
const listed = { id: item.ref.id, name: item.name };
|
|
925
|
+
if (item.ref.revision !== undefined) {
|
|
926
|
+
listed.revision = item.ref.revision;
|
|
927
|
+
}
|
|
928
|
+
return listed;
|
|
929
|
+
}),
|
|
930
|
+
selected: provider?.selected()?.id ?? null,
|
|
931
|
+
commands: runtime.resourceCommands
|
|
932
|
+
.filter((c) => c.resourceBindingId === binding.id)
|
|
933
|
+
.map((c) => c.command)
|
|
934
|
+
};
|
|
935
|
+
});
|
|
936
|
+
report.widgets = spec.widgets
|
|
937
|
+
.filter((w) => w.bindingMode !== "layout")
|
|
938
|
+
.map((w) => {
|
|
939
|
+
// A resource widget shows a collection, not a state value — report what
|
|
940
|
+
// its picker or gallery would list.
|
|
941
|
+
if (w.resourceBindingId) {
|
|
942
|
+
const items = runtime.resourceProvider(w.resourceBindingId)?.list() ?? [];
|
|
943
|
+
const state = runtime.widgetState(w.id);
|
|
944
|
+
return {
|
|
945
|
+
id: w.id,
|
|
946
|
+
type: w.type,
|
|
947
|
+
bindingMode: w.bindingMode,
|
|
948
|
+
binding: w.binding,
|
|
949
|
+
stateKey: w.stateKey,
|
|
950
|
+
resourceBindingId: w.resourceBindingId,
|
|
951
|
+
value: previewValue(items.map((item) => ({ id: item.ref.id, name: item.name }))),
|
|
952
|
+
display: null,
|
|
953
|
+
hasValue: items.length > 0,
|
|
954
|
+
visible: state?.visible ?? true,
|
|
955
|
+
disabled: state?.disabled ?? false
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
const value = runtime.read(w.ref);
|
|
959
|
+
// A `format` template is what the widget actually shows, so it — not
|
|
960
|
+
// the raw value — decides whether anything reached the screen.
|
|
961
|
+
const display = runtime.display(w.id);
|
|
962
|
+
const state = runtime.widgetState(w.id);
|
|
963
|
+
return {
|
|
964
|
+
id: w.id,
|
|
965
|
+
type: w.type,
|
|
966
|
+
bindingMode: w.bindingMode,
|
|
967
|
+
binding: w.binding,
|
|
968
|
+
stateKey: w.stateKey,
|
|
969
|
+
value: previewValue(value),
|
|
970
|
+
display,
|
|
971
|
+
hasValue: display !== null ? display.length > 0 : !isEmptyValue(value),
|
|
972
|
+
visible: state?.visible ?? true,
|
|
973
|
+
disabled: state?.disabled ?? false
|
|
974
|
+
};
|
|
975
|
+
});
|
|
976
|
+
for (const w of spec.widgets) {
|
|
977
|
+
if (!w.events.some((e) => e.kind === "run"))
|
|
978
|
+
continue;
|
|
979
|
+
const state = runtime.widgetState(w.id);
|
|
980
|
+
if (!state || state.everReachable)
|
|
981
|
+
continue;
|
|
982
|
+
const blocking = !state.visible
|
|
983
|
+
? `visibleWhen (\`${state.visibleWhen ?? ""}\`) never held`
|
|
984
|
+
: `disabledWhen (\`${state.disabledWhen ?? ""}\`) always held`;
|
|
985
|
+
conditionIssues.push(`${w.type} "${w.label ?? w.id}" runs the app, but its ${blocking} — no interaction could start a run.`);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
report.verdict = buildAppVerdict(report, allowRuns, resolved.graph, conditionIssues);
|
|
989
|
+
return report;
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=simulate.js.map
|