@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,710 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure app-spec layer: parse a workflow's app document into a flat widget
|
|
3
|
+
* list, extract the graph's bindable surface, and statically validate the
|
|
4
|
+
* wiring between the two.
|
|
5
|
+
*
|
|
6
|
+
* The semantics — widget catalog, document parsing, binding resolution — come
|
|
7
|
+
* from `@nodetool-ai/app-runtime`, so this harness checks the same rules the
|
|
8
|
+
* web runtime enforces instead of a copy that drifts away from it.
|
|
9
|
+
*/
|
|
10
|
+
import { DEFAULT_OPERATION_ID, encodeBinding, operationTarget, parseApplicationDocument, parseBinding, parseCondition, resolveBinding, stateKey, widgetBindingProps, widgetMode, WIDGET_CATALOG } from "@nodetool-ai/app-runtime";
|
|
11
|
+
import { isNonEmptyString, isString } from "../predicates.js";
|
|
12
|
+
const SET_VARIABLE_NODE_TYPE = "nodetool.variable.SetVariable";
|
|
13
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
14
|
+
const str = (v) => (isString(v) ? v : null);
|
|
15
|
+
const isPuckNode = (v) => isRecord(v) && typeof v.type === "string" && isRecord(v.props);
|
|
16
|
+
/**
|
|
17
|
+
* A widget's stored `visibleWhen`/`disabledWhen` props. Undefined when the
|
|
18
|
+
* widget carries none, so an unconditional widget's spec is unchanged.
|
|
19
|
+
*/
|
|
20
|
+
const parseConditionProps = (raw) => {
|
|
21
|
+
if (!isRecord(raw))
|
|
22
|
+
return undefined;
|
|
23
|
+
const binding = str(raw.binding);
|
|
24
|
+
if (!binding)
|
|
25
|
+
return undefined;
|
|
26
|
+
const op = str(raw.op);
|
|
27
|
+
const value = str(raw.value);
|
|
28
|
+
const props = { binding };
|
|
29
|
+
if (op) {
|
|
30
|
+
props.op = op;
|
|
31
|
+
}
|
|
32
|
+
if (value !== null) {
|
|
33
|
+
props.value = value;
|
|
34
|
+
}
|
|
35
|
+
return props;
|
|
36
|
+
};
|
|
37
|
+
const parseEvents = (raw) => {
|
|
38
|
+
if (!Array.isArray(raw))
|
|
39
|
+
return [];
|
|
40
|
+
const events = [];
|
|
41
|
+
for (const item of raw) {
|
|
42
|
+
if (!isRecord(item))
|
|
43
|
+
continue;
|
|
44
|
+
if (item.trigger !== "click" && item.trigger !== "change")
|
|
45
|
+
continue;
|
|
46
|
+
events.push({
|
|
47
|
+
trigger: item.trigger,
|
|
48
|
+
kind: str(item.kind) ?? "run",
|
|
49
|
+
// The builder stores "" for unset key/value — normalize to undefined.
|
|
50
|
+
key: str(item.key) || undefined,
|
|
51
|
+
value: str(item.value) || undefined,
|
|
52
|
+
operationId: str(item.operationId) || undefined,
|
|
53
|
+
invocationId: str(item.invocationId) || undefined,
|
|
54
|
+
resourceBindingId: str(item.resourceBindingId) || undefined,
|
|
55
|
+
command: str(item.command) || undefined
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return events;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* The binding scope an app resolves against: every declared operation with the
|
|
62
|
+
* surface of the workflow it runs, plus the declared variables. Without a
|
|
63
|
+
* context this is the single-operation shape a legacy `app_doc` has.
|
|
64
|
+
*/
|
|
65
|
+
export function bindingScopeFor(io, context) {
|
|
66
|
+
const operationIO = (id, own) => ({
|
|
67
|
+
operationId: id,
|
|
68
|
+
inputs: (own ?? io).inputs.map(({ nodeId, name }) => ({ nodeId, name })),
|
|
69
|
+
outputs: (own ?? io).outputs.map(({ nodeId, name }) => ({ nodeId, name })),
|
|
70
|
+
nodeIds: (own ?? io).nodeIds,
|
|
71
|
+
variableNames: (own ?? io).variables
|
|
72
|
+
});
|
|
73
|
+
if (!context) {
|
|
74
|
+
return {
|
|
75
|
+
defaultOperationId: DEFAULT_OPERATION_ID,
|
|
76
|
+
operations: [operationIO(DEFAULT_OPERATION_ID, io)],
|
|
77
|
+
variables: []
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
defaultOperationId: context.defaultOperationId,
|
|
82
|
+
operations: context.operations.map((op) => operationIO(op.id, op.io)),
|
|
83
|
+
variables: context.variables
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* True for the widgets the catalog gives a `resourceBindingId` field — the ones
|
|
88
|
+
* that read a document collection through the resource provider.
|
|
89
|
+
*/
|
|
90
|
+
const usesResourceBinding = (type) => WIDGET_CATALOG[type]?.fields.resourceBindingId !== undefined;
|
|
91
|
+
/**
|
|
92
|
+
* A widget that renders one operation's whole input surface rather than one
|
|
93
|
+
* slot. It carries `operationId` where the state widgets carry `binding`, so
|
|
94
|
+
* the unbound-write warning does not apply to it.
|
|
95
|
+
*/
|
|
96
|
+
const rendersOperationInputs = (type) => WIDGET_CATALOG[type]?.fields.operationId !== undefined;
|
|
97
|
+
const modeToBindingMode = (mode) => mode === "write" ? "write" : mode === "read" ? "read" : "none";
|
|
98
|
+
/**
|
|
99
|
+
* Widgets whose value is a file the user has to supply on the spot. None of
|
|
100
|
+
* them starts with a value — a Sketch Pad publishes nothing until the first
|
|
101
|
+
* stroke, a recorder nothing until the first take — so a run they feed can
|
|
102
|
+
* start with the input unset. A Workflow Form is deliberately not one of
|
|
103
|
+
* these: it renders every input of its operation rather than binding one, so
|
|
104
|
+
* there is no single binding a run trigger could be guarded on.
|
|
105
|
+
*/
|
|
106
|
+
const MEDIA_INPUT_WIDGETS = new Set([
|
|
107
|
+
"ImageInput",
|
|
108
|
+
"SketchPad",
|
|
109
|
+
"CameraCapture",
|
|
110
|
+
"AudioInput",
|
|
111
|
+
"AudioRecorder",
|
|
112
|
+
"VideoInput",
|
|
113
|
+
"DocumentInput"
|
|
114
|
+
]);
|
|
115
|
+
/** True when an input node ships a media value of its own to fall back on. */
|
|
116
|
+
const hasMediaDefault = (value) => {
|
|
117
|
+
if (isNonEmptyString(value))
|
|
118
|
+
return true;
|
|
119
|
+
if (!isRecord(value))
|
|
120
|
+
return false;
|
|
121
|
+
return isNonEmptyString(value.uri) || isNonEmptyString(value.asset_id);
|
|
122
|
+
};
|
|
123
|
+
/** Record that some widget displays `field` of `operationId`. */
|
|
124
|
+
const addExecField = (fields, operationId, field) => {
|
|
125
|
+
const set = fields.get(operationId) ?? new Set();
|
|
126
|
+
set.add(field);
|
|
127
|
+
fields.set(operationId, set);
|
|
128
|
+
};
|
|
129
|
+
/** What a second click does under each policy, for the re-entry warning. */
|
|
130
|
+
const REENTRY_CONSEQUENCE = {
|
|
131
|
+
replace: "cancels the running job and starts it again",
|
|
132
|
+
queue: "queues another run behind the one in flight",
|
|
133
|
+
parallel: "starts a second run alongside the one in flight"
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* The operations a parsed document declares, as report specs. A document with
|
|
137
|
+
* none gets the implicit single operation a legacy `app_doc` runs.
|
|
138
|
+
*/
|
|
139
|
+
export function documentOperations(document) {
|
|
140
|
+
if (document.operations.length > 0)
|
|
141
|
+
return document.operations;
|
|
142
|
+
return [
|
|
143
|
+
{
|
|
144
|
+
id: DEFAULT_OPERATION_ID,
|
|
145
|
+
name: "Run",
|
|
146
|
+
workflowId: "",
|
|
147
|
+
inputs: {},
|
|
148
|
+
outputs: {},
|
|
149
|
+
policy: "replace"
|
|
150
|
+
}
|
|
151
|
+
];
|
|
152
|
+
}
|
|
153
|
+
/** Parse an app document (object or JSON string), or explain why it will not. */
|
|
154
|
+
export function parseAppDocument(appDoc) {
|
|
155
|
+
if (appDoc == null) {
|
|
156
|
+
return {
|
|
157
|
+
document: null,
|
|
158
|
+
issue: "Workflow has no app_doc — build the app in the App Builder first."
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
let raw = appDoc;
|
|
162
|
+
if (isString(raw)) {
|
|
163
|
+
try {
|
|
164
|
+
raw = JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return {
|
|
168
|
+
document: null,
|
|
169
|
+
issue: "app_doc is a string but not valid JSON."
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const document = parseApplicationDocument(raw, { hostWorkflowId: "self" });
|
|
174
|
+
return document
|
|
175
|
+
? { document, issue: null }
|
|
176
|
+
: {
|
|
177
|
+
document: null,
|
|
178
|
+
issue: "app_doc is not a valid application document (no `ui`/`data`)."
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Variables the document declared as persisted but that the parser downgraded:
|
|
183
|
+
* only user-scoped variables may persist, and the parser applies that silently.
|
|
184
|
+
*/
|
|
185
|
+
function persistDowngrades(appDoc) {
|
|
186
|
+
const raw = isString(appDoc) ? safeParse(appDoc) : appDoc;
|
|
187
|
+
if (!isRecord(raw) || !Array.isArray(raw.variables))
|
|
188
|
+
return [];
|
|
189
|
+
const names = [];
|
|
190
|
+
for (const entry of raw.variables) {
|
|
191
|
+
if (!isRecord(entry) || entry.persist !== true)
|
|
192
|
+
continue;
|
|
193
|
+
if (entry.scope === "user")
|
|
194
|
+
continue;
|
|
195
|
+
names.push(str(entry.name) ?? str(entry.id) ?? "(unnamed)");
|
|
196
|
+
}
|
|
197
|
+
return names;
|
|
198
|
+
}
|
|
199
|
+
const safeParse = (value) => {
|
|
200
|
+
try {
|
|
201
|
+
return JSON.parse(value);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Parse an app document (object or JSON string) into an {@link AppSpec},
|
|
209
|
+
* resolving every widget binding to a node-ID reference against the live
|
|
210
|
+
* graph. Slot children are discovered from the widget catalog, falling back to
|
|
211
|
+
* a structural scan so a new layout widget keeps working before the catalog
|
|
212
|
+
* knows about it.
|
|
213
|
+
*/
|
|
214
|
+
export function parseAppSpec(appDoc, io, context,
|
|
215
|
+
/**
|
|
216
|
+
* The document the target loader already parsed. Passed in so an application
|
|
217
|
+
* or bundle target is not re-parsed as if it were a `workflow.app_doc` —
|
|
218
|
+
* `appDoc` stays the raw stored value, which is what the persist-downgrade
|
|
219
|
+
* check needs to see.
|
|
220
|
+
*/
|
|
221
|
+
preparsed) {
|
|
222
|
+
const { document, issue } = preparsed ?? parseAppDocument(appDoc);
|
|
223
|
+
if (!document)
|
|
224
|
+
return { spec: null, issues: [issue ?? ""], warnings: [] };
|
|
225
|
+
const issues = [];
|
|
226
|
+
const warnings = [];
|
|
227
|
+
for (const name of persistDowngrades(appDoc)) {
|
|
228
|
+
warnings.push(`Variable "${name}" declares persist: true with scope: "instance" — only user-scoped variables persist, so it was downgraded to in-memory.`);
|
|
229
|
+
}
|
|
230
|
+
const scope = bindingScopeFor(io, context);
|
|
231
|
+
const rootProps = isRecord(document.ui.root.props)
|
|
232
|
+
? document.ui.root.props
|
|
233
|
+
: {};
|
|
234
|
+
const title = str(rootProps.title);
|
|
235
|
+
const widgets = [];
|
|
236
|
+
const walk = (items, parentId, slot) => {
|
|
237
|
+
for (const item of items) {
|
|
238
|
+
if (!isPuckNode(item))
|
|
239
|
+
continue;
|
|
240
|
+
const id = str(item.props.id) ?? `${item.type}-${widgets.length}`;
|
|
241
|
+
const bindingMode = widgetMode(item.type);
|
|
242
|
+
const binding = str(item.props.binding) || null;
|
|
243
|
+
// A resource widget addresses a collection, not app state, so it carries
|
|
244
|
+
// `resourceBindingId` where the state widgets carry `binding`.
|
|
245
|
+
const resourceBindingId = usesResourceBinding(item.type)
|
|
246
|
+
? str(item.props.resourceBindingId) || null
|
|
247
|
+
: null;
|
|
248
|
+
const visibleWhen = parseConditionProps(item.props.visibleWhen);
|
|
249
|
+
const disabledWhen = parseConditionProps(item.props.disabledWhen);
|
|
250
|
+
const format = str(item.props.format) || null;
|
|
251
|
+
const ref = binding
|
|
252
|
+
? resolveBinding(binding, scope, modeToBindingMode(bindingMode))
|
|
253
|
+
: bindingMode === "write"
|
|
254
|
+
? // An unbound write widget still holds its own value — as widget-local
|
|
255
|
+
// view state, which cannot collide with a workflow input.
|
|
256
|
+
{ kind: "view", componentId: id, prop: "value" }
|
|
257
|
+
: null;
|
|
258
|
+
const widget = {
|
|
259
|
+
id,
|
|
260
|
+
type: item.type,
|
|
261
|
+
bindingMode,
|
|
262
|
+
binding,
|
|
263
|
+
ref,
|
|
264
|
+
stateKey: ref ? stateKey(ref) : null,
|
|
265
|
+
canonicalBinding: ref ? encodeBinding(ref) : null,
|
|
266
|
+
resourceBindingId,
|
|
267
|
+
extraBindings: widgetBindingProps(item.type).flatMap(({ prop, mode }) => {
|
|
268
|
+
const value = str(item.props[prop]);
|
|
269
|
+
if (!value)
|
|
270
|
+
return [];
|
|
271
|
+
return [
|
|
272
|
+
{ prop, binding: value, ref: resolveBinding(value, scope, mode) }
|
|
273
|
+
];
|
|
274
|
+
}),
|
|
275
|
+
label: str(item.props.label) ?? str(item.props.text) ?? null,
|
|
276
|
+
operationId: rendersOperationInputs(item.type)
|
|
277
|
+
? str(item.props.operationId) || null
|
|
278
|
+
: null,
|
|
279
|
+
events: parseEvents(item.props.events),
|
|
280
|
+
parentId,
|
|
281
|
+
slot
|
|
282
|
+
};
|
|
283
|
+
if (visibleWhen) {
|
|
284
|
+
widget.visibleWhen = visibleWhen;
|
|
285
|
+
}
|
|
286
|
+
if (disabledWhen) {
|
|
287
|
+
widget.disabledWhen = disabledWhen;
|
|
288
|
+
}
|
|
289
|
+
if (format) {
|
|
290
|
+
widget.format = format;
|
|
291
|
+
}
|
|
292
|
+
widgets.push(widget);
|
|
293
|
+
const slots = WIDGET_CATALOG[item.type]?.slots;
|
|
294
|
+
for (const [prop, value] of Object.entries(item.props)) {
|
|
295
|
+
if (slots && !slots.includes(prop))
|
|
296
|
+
continue;
|
|
297
|
+
if (Array.isArray(value) && value.some(isPuckNode)) {
|
|
298
|
+
walk(value, id, prop);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
walk(document.ui.content, null, null);
|
|
304
|
+
if (widgets.length === 0) {
|
|
305
|
+
issues.push("The app document has no widgets — nothing to render or run.");
|
|
306
|
+
}
|
|
307
|
+
const operations = context?.operations ??
|
|
308
|
+
documentOperations(document).map((binding) => operationSpec(binding, null, null));
|
|
309
|
+
return {
|
|
310
|
+
spec: {
|
|
311
|
+
version: document.schemaVersion,
|
|
312
|
+
title,
|
|
313
|
+
widgets,
|
|
314
|
+
operations,
|
|
315
|
+
variables: context?.variables ?? document.variables,
|
|
316
|
+
resources: document.resources.map(({ id, name, kind }) => ({
|
|
317
|
+
id,
|
|
318
|
+
name,
|
|
319
|
+
kind
|
|
320
|
+
}))
|
|
321
|
+
},
|
|
322
|
+
issues,
|
|
323
|
+
warnings
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
/** An operation binding plus the surface it resolved against, for the report. */
|
|
327
|
+
export function operationSpec(binding, io, unavailable) {
|
|
328
|
+
return {
|
|
329
|
+
id: binding.id,
|
|
330
|
+
name: binding.name,
|
|
331
|
+
workflowId: binding.workflowId,
|
|
332
|
+
target: operationTarget(binding),
|
|
333
|
+
policy: binding.policy,
|
|
334
|
+
timeoutMs: binding.timeoutMs ?? null,
|
|
335
|
+
inputs: binding.inputs,
|
|
336
|
+
outputs: binding.outputs,
|
|
337
|
+
io,
|
|
338
|
+
unavailable
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
/** Read a node property from the kernel graph shape (`properties`, not `data`). */
|
|
342
|
+
const nodeProps = (node) => isRecord(node.properties) ? node.properties : {};
|
|
343
|
+
const nodeName = (node) => {
|
|
344
|
+
const name = nodeProps(node).name;
|
|
345
|
+
if (isNonEmptyString(name))
|
|
346
|
+
return name;
|
|
347
|
+
return str(node.id) ?? "";
|
|
348
|
+
};
|
|
349
|
+
const isOutputNodeType = (type) => type.includes(".output.") || type === "nodetool.workflows.base_node.Preview";
|
|
350
|
+
/**
|
|
351
|
+
* Extract the app-bindable surface from a kernel-shape graph: input nodes,
|
|
352
|
+
* output/Preview nodes, and SetVariable channel names — the same surface the
|
|
353
|
+
* web builder offers in its binding pickers.
|
|
354
|
+
*/
|
|
355
|
+
export function extractAppIO(graph) {
|
|
356
|
+
const io = { inputs: [], outputs: [], variables: [], nodeIds: [] };
|
|
357
|
+
const variables = new Set();
|
|
358
|
+
for (const node of graph.nodes) {
|
|
359
|
+
const type = str(node.type);
|
|
360
|
+
const id = str(node.id);
|
|
361
|
+
if (!type || !id)
|
|
362
|
+
continue;
|
|
363
|
+
io.nodeIds.push(id);
|
|
364
|
+
if (type.startsWith("nodetool.input.")) {
|
|
365
|
+
io.inputs.push({
|
|
366
|
+
nodeId: id,
|
|
367
|
+
nodeType: type,
|
|
368
|
+
name: nodeName(node),
|
|
369
|
+
defaultValue: nodeProps(node).value
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
else if (isOutputNodeType(type)) {
|
|
373
|
+
io.outputs.push({ nodeId: id, nodeType: type, name: nodeName(node) });
|
|
374
|
+
}
|
|
375
|
+
else if (type === SET_VARIABLE_NODE_TYPE) {
|
|
376
|
+
const name = str(nodeProps(node).name)?.trim();
|
|
377
|
+
if (name)
|
|
378
|
+
variables.add(name);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
io.variables = [...variables].sort((a, b) => a.localeCompare(b));
|
|
382
|
+
return io;
|
|
383
|
+
}
|
|
384
|
+
const VARIABLE_EVENT_KINDS = new Set([
|
|
385
|
+
"setVariable",
|
|
386
|
+
"toggleVariable",
|
|
387
|
+
"setState",
|
|
388
|
+
"toggleState"
|
|
389
|
+
]);
|
|
390
|
+
/** Every id a variable binding may name: declared ids/names + graph channels. */
|
|
391
|
+
function knownVariableIds(io, context) {
|
|
392
|
+
const known = new Set(io.variables);
|
|
393
|
+
for (const variable of context?.variables ?? []) {
|
|
394
|
+
known.add(variable.id);
|
|
395
|
+
known.add(variable.name);
|
|
396
|
+
}
|
|
397
|
+
for (const operation of context?.operations ?? []) {
|
|
398
|
+
for (const name of operation.io?.variables ?? [])
|
|
399
|
+
known.add(name);
|
|
400
|
+
}
|
|
401
|
+
return known;
|
|
402
|
+
}
|
|
403
|
+
/** Statically check the app's wiring against the workflow's bindable surface. */
|
|
404
|
+
export function validateApp(spec, io, context) {
|
|
405
|
+
const errors = [];
|
|
406
|
+
const warnings = [];
|
|
407
|
+
const scope = bindingScopeFor(io, context);
|
|
408
|
+
const defaultOperationId = context?.defaultOperationId ?? DEFAULT_OPERATION_ID;
|
|
409
|
+
// Without a context there is one operation over the host workflow, so the
|
|
410
|
+
// host surface is its surface.
|
|
411
|
+
const operations = spec.operations.map((op) => op.io || op.id !== defaultOperationId ? op : { ...op, io });
|
|
412
|
+
const operationIds = new Set(operations.map((op) => op.id));
|
|
413
|
+
// Node ids per operation, for the operations whose workflow resolved. An
|
|
414
|
+
// operation with no `io` has no graph to check against, so its bindings are
|
|
415
|
+
// left alone rather than reported as missing nodes.
|
|
416
|
+
const nodeIdsByOperation = new Map(operations
|
|
417
|
+
.filter((op) => op.io !== null)
|
|
418
|
+
.map((op) => [op.id, new Set(op.io?.nodeIds ?? [])]));
|
|
419
|
+
/**
|
|
420
|
+
* The node an ID-form binding names, when the operation's graph does not have
|
|
421
|
+
* it. `parseBinding` accepts any well-formed token, so a widget bound to a
|
|
422
|
+
* node that was deleted (or never existed) resolves to a ref addressing a
|
|
423
|
+
* state slot no run ever fills — a silent blank widget without this check.
|
|
424
|
+
*/
|
|
425
|
+
const missingNode = (binding, ref) => {
|
|
426
|
+
if (!parseBinding(binding) || !ref || !("nodeId" in ref))
|
|
427
|
+
return null;
|
|
428
|
+
const known = nodeIdsByOperation.get(ref.operationId);
|
|
429
|
+
if (!known || known.has(ref.nodeId))
|
|
430
|
+
return null;
|
|
431
|
+
return { operationId: ref.operationId, nodeId: ref.nodeId };
|
|
432
|
+
};
|
|
433
|
+
const variableIds = knownVariableIds(io, context);
|
|
434
|
+
const resourceIds = new Set(spec.resources.map((r) => r.id));
|
|
435
|
+
let hasRunTrigger = false;
|
|
436
|
+
/** `opId:nodeId` of every output a widget displays. */
|
|
437
|
+
const displayedOutputs = new Set();
|
|
438
|
+
/** Operations some widget event can run. */
|
|
439
|
+
const runnableOperations = new Set();
|
|
440
|
+
/** Operation ids whose execution state a widget shows. */
|
|
441
|
+
const execBindings = new Map();
|
|
442
|
+
/** Execution fields a widget actually displays, per operation. */
|
|
443
|
+
const execFields = new Map();
|
|
444
|
+
/** Widgets whose click runs an operation — the re-entry surface. */
|
|
445
|
+
const runTriggers = [];
|
|
446
|
+
/** Media input widgets — the ones a run can start without a value for. */
|
|
447
|
+
const mediaWidgets = [];
|
|
448
|
+
for (const w of spec.widgets) {
|
|
449
|
+
const where = `${w.type} "${w.id}"`;
|
|
450
|
+
if (w.bindingMode === "unknown") {
|
|
451
|
+
errors.push(`${where}: unknown widget type — not in the app-builder catalog.`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (w.binding && !w.ref) {
|
|
455
|
+
errors.push(w.bindingMode === "write"
|
|
456
|
+
? `${where}: bound to "${w.binding}" but the workflow has no input node or node property that resolves it.`
|
|
457
|
+
: `${where}: bound to "${w.binding}" but the workflow has no output or variable with that name.`);
|
|
458
|
+
}
|
|
459
|
+
else if (!w.binding &&
|
|
460
|
+
w.bindingMode === "write" &&
|
|
461
|
+
!usesResourceBinding(w.type) &&
|
|
462
|
+
!rendersOperationInputs(w.type)) {
|
|
463
|
+
warnings.push(`${where}: not bound to an input — its value stays local UI state.`);
|
|
464
|
+
}
|
|
465
|
+
// A resource widget with no collection behind it renders an empty picker
|
|
466
|
+
// the user can never choose from, which a run-only check never surfaces.
|
|
467
|
+
if (usesResourceBinding(w.type)) {
|
|
468
|
+
if (!w.resourceBindingId) {
|
|
469
|
+
errors.push(`${where}: no resource binding selected — the widget has no collection to show. Add one with a resource binding.`);
|
|
470
|
+
}
|
|
471
|
+
else if (!resourceIds.has(w.resourceBindingId)) {
|
|
472
|
+
errors.push(`${where}: bound to resource "${w.resourceBindingId}" but the app declares no such resource binding.`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// A form renders the inputs of the operation it names; a stale name renders
|
|
476
|
+
// an empty form, which a run-only check reads as an app that asks for
|
|
477
|
+
// nothing rather than one that lost its inputs.
|
|
478
|
+
if (w.operationId && !operationIds.has(w.operationId)) {
|
|
479
|
+
errors.push(`${where}: renders the inputs of operation "${w.operationId}" but the app declares no such operation.`);
|
|
480
|
+
}
|
|
481
|
+
// An explicit `op:<id>/…` token resolves on its own syntax, so a token
|
|
482
|
+
// naming an operation the document never declared has to be caught here.
|
|
483
|
+
const explicit = parseBinding(w.binding);
|
|
484
|
+
if (explicit && "operationId" in explicit && explicit.operationId) {
|
|
485
|
+
if (!operationIds.has(explicit.operationId)) {
|
|
486
|
+
errors.push(`${where}: bound to "${w.binding}" but the app declares no operation "${explicit.operationId}".`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const missing = missingNode(w.binding, w.ref);
|
|
490
|
+
if (missing) {
|
|
491
|
+
errors.push(`${where}: bound to "${w.binding}" but operation "${missing.operationId}" runs a workflow with no node "${missing.nodeId}".`);
|
|
492
|
+
}
|
|
493
|
+
// An unresolvable condition binding is not enforced at runtime — the widget
|
|
494
|
+
// shows and stays enabled — so the author sees a condition that quietly
|
|
495
|
+
// does nothing.
|
|
496
|
+
for (const [prop, props] of [
|
|
497
|
+
["visibleWhen", w.visibleWhen],
|
|
498
|
+
["disabledWhen", w.disabledWhen]
|
|
499
|
+
]) {
|
|
500
|
+
if (!props?.binding || parseCondition(props, scope))
|
|
501
|
+
continue;
|
|
502
|
+
warnings.push(`${where}: ${prop} reads "${props.binding}", which resolves to nothing — the condition never applies, so the widget stays visible and enabled.`);
|
|
503
|
+
}
|
|
504
|
+
for (const extra of w.extraBindings) {
|
|
505
|
+
const extraMissing = missingNode(extra.binding, extra.ref);
|
|
506
|
+
if (!extra.ref) {
|
|
507
|
+
errors.push(`${where}: ${extra.prop} is bound to "${extra.binding}" but the workflow has no output or variable with that name.`);
|
|
508
|
+
}
|
|
509
|
+
else if (extraMissing) {
|
|
510
|
+
errors.push(`${where}: ${extra.prop} is bound to "${extra.binding}" but operation "${extraMissing.operationId}" runs a workflow with no node "${extraMissing.nodeId}".`);
|
|
511
|
+
}
|
|
512
|
+
else if (extra.ref.kind === "output") {
|
|
513
|
+
displayedOutputs.add(`${extra.ref.operationId}:${extra.ref.nodeId}`);
|
|
514
|
+
}
|
|
515
|
+
else if (extra.ref.kind === "execution") {
|
|
516
|
+
addExecField(execFields, extra.ref.operationId, extra.ref.field);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (w.ref?.kind === "output") {
|
|
520
|
+
displayedOutputs.add(`${w.ref.operationId}:${w.ref.nodeId}`);
|
|
521
|
+
}
|
|
522
|
+
if (w.ref?.kind === "execution") {
|
|
523
|
+
const list = execBindings.get(w.ref.operationId) ?? [];
|
|
524
|
+
list.push(`${where} (${w.ref.field})`);
|
|
525
|
+
execBindings.set(w.ref.operationId, list);
|
|
526
|
+
addExecField(execFields, w.ref.operationId, w.ref.field);
|
|
527
|
+
}
|
|
528
|
+
if (MEDIA_INPUT_WIDGETS.has(w.type) && w.ref) {
|
|
529
|
+
mediaWidgets.push(w);
|
|
530
|
+
}
|
|
531
|
+
for (const event of w.events) {
|
|
532
|
+
const target = event.operationId ?? defaultOperationId;
|
|
533
|
+
if (event.kind === "run") {
|
|
534
|
+
hasRunTrigger = true;
|
|
535
|
+
runnableOperations.add(target);
|
|
536
|
+
if (event.trigger === "click") {
|
|
537
|
+
runTriggers.push({ widget: w, operationId: target });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
if ((event.kind === "run" || event.kind === "cancel") &&
|
|
541
|
+
!operationIds.has(target)) {
|
|
542
|
+
errors.push(`${where}: ${event.kind} targets operation "${target}" but the app declares no such operation.`);
|
|
543
|
+
}
|
|
544
|
+
if (VARIABLE_EVENT_KINDS.has(event.kind) &&
|
|
545
|
+
!resolveBinding(event.key, scope, "read")) {
|
|
546
|
+
errors.push(`${where}: ${event.kind} targets variable "${event.key ?? ""}" but the workflow has no SetVariable node with that name.`);
|
|
547
|
+
}
|
|
548
|
+
if (event.kind === "resourceCommand" || event.kind === "openResource") {
|
|
549
|
+
if (!event.resourceBindingId ||
|
|
550
|
+
!resourceIds.has(event.resourceBindingId)) {
|
|
551
|
+
errors.push(`${where}: ${event.kind} targets resource "${event.resourceBindingId ?? ""}" but the app declares no such resource binding.`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (spec.widgets.length > 0 && !hasRunTrigger) {
|
|
557
|
+
errors.push('No widget has a "run" event — the app can never execute the workflow. Add a Run button or an on-change run.');
|
|
558
|
+
}
|
|
559
|
+
// The Button primitive disables itself while its click action runs an
|
|
560
|
+
// operation. Other run-capable widgets still need an explicit guard because
|
|
561
|
+
// they do not inherit that browser behavior.
|
|
562
|
+
for (const trigger of runTriggers) {
|
|
563
|
+
const guard = trigger.widget.disabledWhen?.binding
|
|
564
|
+
? resolveBinding(trigger.widget.disabledWhen.binding, scope)
|
|
565
|
+
: null;
|
|
566
|
+
if (guard?.kind === "execution" &&
|
|
567
|
+
guard.operationId === trigger.operationId &&
|
|
568
|
+
guard.field === "running") {
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
if (trigger.widget.type === "Button")
|
|
572
|
+
continue;
|
|
573
|
+
const policy = operations.find((op) => op.id === trigger.operationId)?.policy ??
|
|
574
|
+
"parallel";
|
|
575
|
+
warnings.push(`${trigger.widget.type} "${trigger.widget.id}" runs operation "${trigger.operationId}" but has no disabledWhen on its running state — a second click ${REENTRY_CONSEQUENCE[policy]}. Guard it with disabledWhen on "op:${trigger.operationId}/exec#running".`);
|
|
576
|
+
}
|
|
577
|
+
// A media input starts empty by construction — no file is picked and a Sketch
|
|
578
|
+
// Pad publishes nothing until the first stroke — so an unguarded run sends
|
|
579
|
+
// nothing for it. The model answers from the prompt alone or the provider
|
|
580
|
+
// rejects the call, and either way the run is paid for.
|
|
581
|
+
const declaredDefaults = new Map((context?.variables ?? spec.variables).map((v) => [v.id, v.default]));
|
|
582
|
+
for (const widget of mediaWidgets) {
|
|
583
|
+
for (const { operationId, input } of operationInputsFilledBy(widget, operations)) {
|
|
584
|
+
if (!runnableOperations.has(operationId))
|
|
585
|
+
continue;
|
|
586
|
+
if (hasMediaDefault(input.defaultValue))
|
|
587
|
+
continue;
|
|
588
|
+
if (widget.ref?.kind === "variable" &&
|
|
589
|
+
hasMediaDefault(declaredDefaults.get(widget.ref.variableId))) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
const guarded = runTriggers.some((trigger) => trigger.operationId === operationId &&
|
|
593
|
+
[trigger.widget.disabledWhen, trigger.widget.visibleWhen].some((props) => {
|
|
594
|
+
if (props?.binding == null || widget.stateKey == null) {
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
const ref = resolveBinding(props.binding, scope);
|
|
598
|
+
return ref != null && stateKey(ref) === widget.stateKey;
|
|
599
|
+
}));
|
|
600
|
+
if (guarded)
|
|
601
|
+
continue;
|
|
602
|
+
warnings.push(`${widget.type} "${widget.id}" fills input "${input.name}" of operation "${operationId}", which has no default — nothing stops a run before the user supplies it. Guard the run trigger with disabledWhen on "${widget.binding ?? widget.canonicalBinding ?? ""}".`);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
for (const operationId of runnableOperations) {
|
|
606
|
+
const shown = execFields.get(operationId) ?? new Set();
|
|
607
|
+
if (!shown.has("error")) {
|
|
608
|
+
warnings.push(`No widget shows the error state of operation "${operationId}" — a failed run leaves the app looking idle with nothing explaining why. Add an Alert bound to "op:${operationId}/exec#error".`);
|
|
609
|
+
}
|
|
610
|
+
if (!shown.has("running") &&
|
|
611
|
+
!shown.has("progress") &&
|
|
612
|
+
!shown.has("activity")) {
|
|
613
|
+
warnings.push(`No widget shows the progress of operation "${operationId}" — the user gets no sign a run is underway. Add a Progress bound to "op:${operationId}/exec#progress".`);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
for (const operation of operations) {
|
|
617
|
+
const label = `Operation "${operation.id}"`;
|
|
618
|
+
if (operation.unavailable) {
|
|
619
|
+
errors.push(`${label}: ${operation.unavailable}`);
|
|
620
|
+
}
|
|
621
|
+
validateMappings(operation, variableIds, resourceIds, errors, warnings);
|
|
622
|
+
const mappedToVariable = new Set(Object.entries(operation.outputs)
|
|
623
|
+
.filter(([, mapping]) => mapping.to === "variable")
|
|
624
|
+
.map(([nodeId]) => nodeId));
|
|
625
|
+
for (const output of operation.io?.outputs ?? []) {
|
|
626
|
+
if (displayedOutputs.has(`${operation.id}:${output.nodeId}`))
|
|
627
|
+
continue;
|
|
628
|
+
if (mappedToVariable.has(output.nodeId))
|
|
629
|
+
continue;
|
|
630
|
+
warnings.push(`Workflow output "${output.name}" is not displayed by any widget.`);
|
|
631
|
+
}
|
|
632
|
+
if (spec.widgets.length > 0 && !runnableOperations.has(operation.id)) {
|
|
633
|
+
const shown = execBindings.get(operation.id);
|
|
634
|
+
// A display of an operation nothing can start shows nothing, ever.
|
|
635
|
+
if (shown) {
|
|
636
|
+
errors.push(`${label} is never run by any widget, but ${shown.join(", ")} shows its execution state.`);
|
|
637
|
+
}
|
|
638
|
+
else {
|
|
639
|
+
warnings.push(`${label} is declared but no widget event runs it.`);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { errors, warnings };
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* The operation inputs a widget's value reaches: the one it binds directly, and
|
|
647
|
+
* any input mapped `{from: "variable"}` from the variable it writes — the shape
|
|
648
|
+
* the shipped apps use, where one picker feeds several operations.
|
|
649
|
+
*/
|
|
650
|
+
function operationInputsFilledBy(widget, operations) {
|
|
651
|
+
const filled = [];
|
|
652
|
+
for (const operation of operations) {
|
|
653
|
+
const inputs = operation.io?.inputs ?? [];
|
|
654
|
+
for (const input of inputs) {
|
|
655
|
+
const direct = widget.ref?.kind === "input" &&
|
|
656
|
+
widget.ref.operationId === operation.id &&
|
|
657
|
+
widget.ref.nodeId === input.nodeId;
|
|
658
|
+
const mapping = operation.inputs[input.nodeId];
|
|
659
|
+
const viaVariable = widget.ref?.kind === "variable" &&
|
|
660
|
+
mapping?.from === "variable" &&
|
|
661
|
+
mapping.variableId === widget.ref.variableId;
|
|
662
|
+
if (direct || viaVariable) {
|
|
663
|
+
filled.push({ operationId: operation.id, input });
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return filled;
|
|
668
|
+
}
|
|
669
|
+
/** Check one operation's input/output mappings against what the app declares. */
|
|
670
|
+
function validateMappings(operation, variableIds, resourceIds, errors, warnings) {
|
|
671
|
+
const label = `Operation "${operation.id}"`;
|
|
672
|
+
const inputNodeIds = new Set((operation.io?.inputs ?? []).map((i) => i.nodeId));
|
|
673
|
+
const outputNodeIds = new Set((operation.io?.outputs ?? []).map((o) => o.nodeId));
|
|
674
|
+
for (const [nodeId, mapping] of Object.entries(operation.inputs)) {
|
|
675
|
+
if (operation.io && !inputNodeIds.has(nodeId)) {
|
|
676
|
+
errors.push(`${label}: input mapping for node "${nodeId}", which the workflow has no input node for.`);
|
|
677
|
+
}
|
|
678
|
+
switch (mapping.from) {
|
|
679
|
+
case "variable":
|
|
680
|
+
if (!variableIds.has(mapping.variableId)) {
|
|
681
|
+
errors.push(`${label}: input "${nodeId}" reads variable "${mapping.variableId}", which the app does not declare.`);
|
|
682
|
+
}
|
|
683
|
+
break;
|
|
684
|
+
case "constant":
|
|
685
|
+
if (mapping.value === undefined) {
|
|
686
|
+
warnings.push(`${label}: input "${nodeId}" is mapped to a constant with no value — the run sends nothing for it.`);
|
|
687
|
+
}
|
|
688
|
+
break;
|
|
689
|
+
case "resource":
|
|
690
|
+
if (!resourceIds.has(mapping.resourceBindingId)) {
|
|
691
|
+
errors.push(`${label}: input "${nodeId}" reads resource binding "${mapping.resourceBindingId}", which the app does not declare.`);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
warnings.push(`${label}: input "${nodeId}" comes from resource binding "${mapping.resourceBindingId}" — a headless run only sees a seeded collection, so seed it with a seedResource step or a "resource:${mapping.resourceBindingId}" param before running.`);
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
default:
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
for (const [nodeId, mapping] of Object.entries(operation.outputs)) {
|
|
702
|
+
if (operation.io && !outputNodeIds.has(nodeId)) {
|
|
703
|
+
errors.push(`${label}: output mapping for node "${nodeId}", which the workflow has no output node for.`);
|
|
704
|
+
}
|
|
705
|
+
if (mapping.to === "variable" && !variableIds.has(mapping.variableId)) {
|
|
706
|
+
errors.push(`${label}: output "${nodeId}" writes variable "${mapping.variableId}", which the app does not declare.`);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
//# sourceMappingURL=app-spec.js.map
|