@calliopelabs/ui 0.7.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +253 -3
- package/dist/appearance/AppearancePicker.js +4 -2
- package/dist/appearance/AppearanceProvider.d.ts +10 -2
- package/dist/appearance/AppearanceProvider.js +6 -2
- package/dist/appearance/AppearanceScript.d.ts +15 -4
- package/dist/appearance/AppearanceScript.js +11 -5
- package/dist/appearance/appearance.js +1 -1
- package/dist/appearance/boot.d.ts +13 -2
- package/dist/appearance/boot.js +35 -5
- package/dist/appearance/product.d.ts +13 -0
- package/dist/appearance/product.js +2 -2
- package/dist/appearance/runtime-brand.d.ts +106 -0
- package/dist/appearance/runtime-brand.js +420 -0
- package/dist/css/appearance.js +147 -0
- package/dist/css/calliope.css +1221 -0
- package/dist/css/components.css +729 -0
- package/dist/css/tokens.css +491 -0
- package/dist/data/DefinitionList.d.ts +16 -0
- package/dist/data/DefinitionList.js +9 -0
- package/dist/data/Stat.d.ts +25 -0
- package/dist/data/Stat.js +19 -0
- package/dist/data/Tag.d.ts +21 -0
- package/dist/data/Tag.js +18 -0
- package/dist/data/types.d.ts +3 -2
- package/dist/data/use-local-table.d.ts +36 -0
- package/dist/data/use-local-table.js +127 -0
- package/dist/index.d.ts +42 -3
- package/dist/index.js +39 -1
- package/dist/nav/Nav.d.ts +2 -1
- package/dist/nav/Nav.js +34 -15
- package/dist/nav/model.d.ts +21 -0
- package/dist/page/PageHeader.d.ts +5 -3
- package/dist/page/PageHeader.js +5 -4
- package/dist/page/Panel.d.ts +19 -0
- package/dist/page/Panel.js +12 -0
- package/dist/primitives/Avatar.d.ts +37 -0
- package/dist/primitives/Avatar.js +43 -0
- package/dist/primitives/Field.d.ts +33 -0
- package/dist/primitives/Field.js +30 -0
- package/dist/primitives/Segmented.d.ts +30 -0
- package/dist/primitives/Segmented.js +20 -0
- package/dist/primitives/Select.d.ts +40 -0
- package/dist/primitives/Select.js +47 -0
- package/dist/primitives/Switch.d.ts +7 -0
- package/dist/primitives/Switch.js +8 -0
- package/dist/primitives/Textarea.d.ts +3 -0
- package/dist/primitives/Textarea.js +6 -0
- package/dist/provider/messages.d.ts +412 -0
- package/dist/provider/messages.js +633 -0
- package/dist/shell/BrandMark.d.ts +25 -3
- package/dist/shell/BrandMark.js +34 -4
- package/dist/shell/RailPanel.js +2 -2
- package/dist/shell/RailTree.js +2 -2
- package/dist/shell/Sidebar.d.ts +9 -2
- package/dist/shell/Sidebar.js +10 -4
- package/dist/shell/UserMenu.d.ts +3 -1
- package/dist/shell/UserMenu.js +3 -11
- package/dist/shell/rail-boot.d.ts +28 -0
- package/dist/shell/rail-boot.js +45 -0
- package/dist/shell/rail-state.d.ts +6 -1
- package/dist/shell/rail-state.js +18 -2
- package/dist/styles/preset.css +4 -0
- package/dist/styles/theme.css +36 -13
- package/dist/theme/css-layer.d.ts +19 -0
- package/dist/theme/css-layer.js +171 -0
- package/dist/theme/css.d.ts +1 -0
- package/dist/theme/css.js +45 -9
- package/dist/workflow/ArtifactCard.d.ts +47 -0
- package/dist/workflow/ArtifactCard.js +45 -0
- package/dist/workflow/DocumentReview.d.ts +74 -0
- package/dist/workflow/DocumentReview.js +54 -0
- package/dist/workflow/FormReview.d.ts +53 -0
- package/dist/workflow/FormReview.js +83 -0
- package/dist/workflow/GateBadge.d.ts +11 -0
- package/dist/workflow/GateBadge.js +14 -0
- package/dist/workflow/MediaReview.d.ts +63 -0
- package/dist/workflow/MediaReview.js +166 -0
- package/dist/workflow/ReviewPanel.d.ts +41 -0
- package/dist/workflow/ReviewPanel.js +59 -0
- package/dist/workflow/RunTimeline.d.ts +25 -0
- package/dist/workflow/RunTimeline.js +56 -0
- package/dist/workflow/WorkflowCard.d.ts +39 -0
- package/dist/workflow/WorkflowCard.js +56 -0
- package/dist/workflow/WorkflowGallery.d.ts +43 -0
- package/dist/workflow/WorkflowGallery.js +69 -0
- package/dist/workflow/canvas/WorkflowCanvas.d.ts +39 -0
- package/dist/workflow/canvas/WorkflowCanvas.js +648 -0
- package/dist/workflow/definition.d.ts +408 -0
- package/dist/workflow/definition.js +411 -0
- package/dist/workflow/editor/WorkflowDiff.d.ts +17 -0
- package/dist/workflow/editor/WorkflowDiff.js +25 -0
- package/dist/workflow/editor/WorkflowImportPreview.d.ts +19 -0
- package/dist/workflow/editor/WorkflowImportPreview.js +50 -0
- package/dist/workflow/editor/WorkflowInspector.d.ts +40 -0
- package/dist/workflow/editor/WorkflowInspector.js +349 -0
- package/dist/workflow/editor/WorkflowJsonView.d.ts +16 -0
- package/dist/workflow/editor/WorkflowJsonView.js +40 -0
- package/dist/workflow/editor/WorkflowPalette.d.ts +16 -0
- package/dist/workflow/editor/WorkflowPalette.js +36 -0
- package/dist/workflow/editor/WorkflowProblems.d.ts +20 -0
- package/dist/workflow/editor/WorkflowProblems.js +51 -0
- package/dist/workflow/editor/WorkflowWorkspace.d.ts +49 -0
- package/dist/workflow/editor/WorkflowWorkspace.js +154 -0
- package/dist/workflow/gallery.d.ts +21 -0
- package/dist/workflow/gallery.js +29 -0
- package/dist/workflow/layout.d.ts +49 -0
- package/dist/workflow/layout.js +162 -0
- package/dist/workflow/model.d.ts +50 -0
- package/dist/workflow/model.js +7 -0
- package/dist/workflow/palette.d.ts +34 -0
- package/dist/workflow/palette.js +70 -0
- package/dist/workflow/portable.d.ts +162 -0
- package/dist/workflow/portable.js +547 -0
- package/dist/workflow/problems.d.ts +38 -0
- package/dist/workflow/problems.js +409 -0
- package/dist/workflow/run.d.ts +94 -0
- package/dist/workflow/run.js +31 -0
- package/dist/workflow/workspace.d.ts +33 -0
- package/dist/workflow/workspace.js +17 -0
- package/package.json +6 -1
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading, comparing and moving workflow definitions: a strict reader that
|
|
3
|
+
* keeps only the fields the format knows (so nothing else, a credential or a
|
|
4
|
+
* grant, rides along), a diff that says what a change does to what runs, and
|
|
5
|
+
* versioned packages with a preview of what importing one does at a
|
|
6
|
+
* destination. Pure: the host reads and writes the files and owns authority.
|
|
7
|
+
*/
|
|
8
|
+
import { WORKFLOW_DEFINITION_VERSION, WORKFLOW_NODE_KINDS, WORKFLOW_SELECTORS, edgeId, findGateKind, } from "./definition.js";
|
|
9
|
+
import { checkDefinition, definitionAvailability, nodeAvailability, } from "./problems.js";
|
|
10
|
+
const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
11
|
+
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
12
|
+
const num = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
|
|
13
|
+
const bool = (v) => (typeof v === "boolean" ? v : undefined);
|
|
14
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
15
|
+
const strs = (v) => arr(v).filter((x) => typeof x === "string");
|
|
16
|
+
/** Drops the keys whose value is undefined, so a read definition compares equal to a written one. */
|
|
17
|
+
function clean(o) {
|
|
18
|
+
for (const k of Object.keys(o))
|
|
19
|
+
if (o[k] === undefined)
|
|
20
|
+
delete o[k];
|
|
21
|
+
return o;
|
|
22
|
+
}
|
|
23
|
+
class ReadError extends Error {
|
|
24
|
+
path;
|
|
25
|
+
constructor(path) {
|
|
26
|
+
super(path);
|
|
27
|
+
this.path = path;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function need(v, path) {
|
|
31
|
+
if (v === undefined)
|
|
32
|
+
throw new ReadError(path);
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
function readPort(raw, path) {
|
|
36
|
+
const o = isObj(raw) ? raw : {};
|
|
37
|
+
return clean({
|
|
38
|
+
id: need(str(o.id), `${path}.id`),
|
|
39
|
+
label: need(str(o.label), `${path}.label`),
|
|
40
|
+
type: need(str(o.type), `${path}.type`),
|
|
41
|
+
optional: bool(o.optional),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const readPorts = (raw, path) => raw === undefined ? undefined : arr(raw).map((p, i) => readPort(p, `${path}[${i}]`));
|
|
45
|
+
function readCondition(raw, path) {
|
|
46
|
+
const o = isObj(raw) ? raw : {};
|
|
47
|
+
const value = o.value;
|
|
48
|
+
return clean({
|
|
49
|
+
fact: need(str(o.fact), `${path}.fact`),
|
|
50
|
+
node: str(o.node),
|
|
51
|
+
op: need(str(o.op), `${path}.op`),
|
|
52
|
+
value: typeof value === "string" || typeof value === "boolean" || num(value) !== undefined
|
|
53
|
+
? value
|
|
54
|
+
: undefined,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function readRef(raw, path) {
|
|
58
|
+
if (raw === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
const o = isObj(raw) ? raw : {};
|
|
61
|
+
return { module: need(str(o.module), `${path}.module`), id: need(str(o.id), `${path}.id`) };
|
|
62
|
+
}
|
|
63
|
+
function readNode(raw, path) {
|
|
64
|
+
const o = isObj(raw) ? raw : {};
|
|
65
|
+
const kind = str(o.kind);
|
|
66
|
+
if (!kind || !WORKFLOW_NODE_KINDS.includes(kind))
|
|
67
|
+
throw new ReadError(`${path}.kind`);
|
|
68
|
+
const at = isObj(o.at) && num(o.at.x) !== undefined && num(o.at.y) !== undefined
|
|
69
|
+
? { x: o.at.x, y: o.at.y }
|
|
70
|
+
: undefined;
|
|
71
|
+
const base = {
|
|
72
|
+
id: need(str(o.id), `${path}.id`),
|
|
73
|
+
label: need(str(o.label), `${path}.label`),
|
|
74
|
+
at,
|
|
75
|
+
note: str(o.note),
|
|
76
|
+
};
|
|
77
|
+
switch (kind) {
|
|
78
|
+
case "step":
|
|
79
|
+
return clean({
|
|
80
|
+
...base,
|
|
81
|
+
kind,
|
|
82
|
+
preset: readRef(o.preset, `${path}.preset`),
|
|
83
|
+
inputs: readPorts(o.inputs, `${path}.inputs`),
|
|
84
|
+
outputs: readPorts(o.outputs, `${path}.outputs`),
|
|
85
|
+
model: str(o.model),
|
|
86
|
+
harness: str(o.harness),
|
|
87
|
+
effort: str(o.effort),
|
|
88
|
+
});
|
|
89
|
+
case "subflow":
|
|
90
|
+
return clean({ ...base, kind, flow: str(o.flow) ?? "", each: str(o.each) });
|
|
91
|
+
case "gate": {
|
|
92
|
+
const p = isObj(o.policy) ? o.policy : undefined;
|
|
93
|
+
const onTimeout = str(p?.onTimeout);
|
|
94
|
+
return clean({
|
|
95
|
+
...base,
|
|
96
|
+
kind,
|
|
97
|
+
gate: need(str(o.gate), `${path}.gate`),
|
|
98
|
+
policy: p
|
|
99
|
+
? clean({
|
|
100
|
+
roles: p.roles === undefined ? undefined : strs(p.roles),
|
|
101
|
+
members: p.members === undefined ? undefined : strs(p.members),
|
|
102
|
+
quorum: num(p.quorum),
|
|
103
|
+
timeoutHours: num(p.timeoutHours),
|
|
104
|
+
onTimeout: onTimeout === "fail" || onTimeout === "escalate" ? onTimeout : undefined,
|
|
105
|
+
})
|
|
106
|
+
: undefined,
|
|
107
|
+
threshold: num(o.threshold),
|
|
108
|
+
rule: str(o.rule),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
case "condition":
|
|
112
|
+
return clean({
|
|
113
|
+
...base,
|
|
114
|
+
kind,
|
|
115
|
+
cases: arr(o.cases).map((c, i) => {
|
|
116
|
+
const co = isObj(c) ? c : {};
|
|
117
|
+
return {
|
|
118
|
+
id: need(str(co.id), `${path}.cases[${i}].id`),
|
|
119
|
+
label: need(str(co.label), `${path}.cases[${i}].label`),
|
|
120
|
+
when: readCondition(co.when, `${path}.cases[${i}].when`),
|
|
121
|
+
};
|
|
122
|
+
}),
|
|
123
|
+
});
|
|
124
|
+
case "goto":
|
|
125
|
+
return clean({ ...base, kind, target: str(o.target) ?? "", max: num(o.max) });
|
|
126
|
+
case "fanout":
|
|
127
|
+
return clean({ ...base, kind });
|
|
128
|
+
case "join": {
|
|
129
|
+
const wait = str(o.wait);
|
|
130
|
+
return clean({
|
|
131
|
+
...base,
|
|
132
|
+
kind,
|
|
133
|
+
wait: wait === "any" || wait === "n" ? wait : "all",
|
|
134
|
+
n: num(o.n),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
case "variants": {
|
|
138
|
+
const select = str(o.select);
|
|
139
|
+
return clean({
|
|
140
|
+
...base,
|
|
141
|
+
kind,
|
|
142
|
+
preset: readRef(o.preset, `${path}.preset`),
|
|
143
|
+
variants: arr(o.variants).map((v, i) => {
|
|
144
|
+
const vo = isObj(v) ? v : {};
|
|
145
|
+
return clean({
|
|
146
|
+
id: need(str(vo.id), `${path}.variants[${i}].id`),
|
|
147
|
+
label: need(str(vo.label), `${path}.variants[${i}].label`),
|
|
148
|
+
model: str(vo.model),
|
|
149
|
+
harness: str(vo.harness),
|
|
150
|
+
effort: str(vo.effort),
|
|
151
|
+
});
|
|
152
|
+
}),
|
|
153
|
+
select: WORKFLOW_SELECTORS.includes(select ?? "")
|
|
154
|
+
? select
|
|
155
|
+
: undefined,
|
|
156
|
+
evaluator: str(o.evaluator),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
case "evaluator":
|
|
160
|
+
return clean({
|
|
161
|
+
...base,
|
|
162
|
+
kind,
|
|
163
|
+
question: str(o.question),
|
|
164
|
+
levels: strs(o.levels),
|
|
165
|
+
threshold: num(o.threshold),
|
|
166
|
+
routes: bool(o.routes) ?? false,
|
|
167
|
+
minConfidence: num(o.minConfidence),
|
|
168
|
+
});
|
|
169
|
+
case "classifier":
|
|
170
|
+
return clean({
|
|
171
|
+
...base,
|
|
172
|
+
kind,
|
|
173
|
+
question: str(o.question),
|
|
174
|
+
options: arr(o.options).map((v, i) => {
|
|
175
|
+
const vo = isObj(v) ? v : {};
|
|
176
|
+
return clean({
|
|
177
|
+
id: need(str(vo.id), `${path}.options[${i}].id`),
|
|
178
|
+
label: need(str(vo.label), `${path}.options[${i}].label`),
|
|
179
|
+
description: str(vo.description),
|
|
180
|
+
});
|
|
181
|
+
}),
|
|
182
|
+
other: bool(o.other) ?? false,
|
|
183
|
+
minConfidence: num(o.minConfidence),
|
|
184
|
+
});
|
|
185
|
+
case "check":
|
|
186
|
+
return clean({
|
|
187
|
+
...base,
|
|
188
|
+
kind,
|
|
189
|
+
question: str(o.question) ?? "",
|
|
190
|
+
threshold: num(o.threshold) ?? 0.5,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
function readEdge(raw, path) {
|
|
195
|
+
const o = isObj(raw) ? raw : {};
|
|
196
|
+
const kind = str(o.kind);
|
|
197
|
+
if (kind !== "next" && kind !== "loop" && kind !== "data")
|
|
198
|
+
throw new ReadError(`${path}.kind`);
|
|
199
|
+
return clean({
|
|
200
|
+
kind,
|
|
201
|
+
from: need(str(o.from), `${path}.from`),
|
|
202
|
+
to: need(str(o.to), `${path}.to`),
|
|
203
|
+
port: str(o.port),
|
|
204
|
+
input: str(o.input),
|
|
205
|
+
label: str(o.label),
|
|
206
|
+
max: num(o.max),
|
|
207
|
+
until: o.until === undefined ? undefined : readCondition(o.until, `${path}.until`),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function readFlow(raw, path) {
|
|
211
|
+
if (!isObj(raw))
|
|
212
|
+
throw new ReadError(path);
|
|
213
|
+
return clean({
|
|
214
|
+
label: str(raw.label),
|
|
215
|
+
inputs: readPorts(raw.inputs, `${path}.inputs`),
|
|
216
|
+
outputs: readPorts(raw.outputs, `${path}.outputs`),
|
|
217
|
+
nodes: arr(raw.nodes).map((n, i) => readNode(n, `${path}.nodes[${i}]`)),
|
|
218
|
+
edges: arr(raw.edges).map((e, i) => readEdge(e, `${path}.edges[${i}]`)),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
function readDefinitionObject(raw) {
|
|
222
|
+
if (!isObj(raw))
|
|
223
|
+
throw new ReadError("$");
|
|
224
|
+
if (raw.version !== WORKFLOW_DEFINITION_VERSION)
|
|
225
|
+
throw new VersionError(raw.version);
|
|
226
|
+
if (!isObj(raw.flows))
|
|
227
|
+
throw new ReadError("$.flows");
|
|
228
|
+
const flows = {};
|
|
229
|
+
for (const [name, f] of Object.entries(raw.flows))
|
|
230
|
+
flows[name] = readFlow(f, `$.flows.${name}`);
|
|
231
|
+
return clean({
|
|
232
|
+
version: WORKFLOW_DEFINITION_VERSION,
|
|
233
|
+
name: need(str(raw.name), "$.name"),
|
|
234
|
+
description: str(raw.description),
|
|
235
|
+
module: str(raw.module),
|
|
236
|
+
main: need(str(raw.main), "$.main"),
|
|
237
|
+
flows,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
class VersionError extends Error {
|
|
241
|
+
version;
|
|
242
|
+
constructor(version) {
|
|
243
|
+
super("version");
|
|
244
|
+
this.version = version;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function attempt(read) {
|
|
248
|
+
try {
|
|
249
|
+
return { ok: true, value: read() };
|
|
250
|
+
}
|
|
251
|
+
catch (e) {
|
|
252
|
+
if (e instanceof VersionError)
|
|
253
|
+
return { ok: false, error: { code: "unsupported-version", version: e.version } };
|
|
254
|
+
if (e instanceof ReadError)
|
|
255
|
+
return { ok: false, error: { code: "not-a-definition", path: e.path } };
|
|
256
|
+
throw e;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Reads a definition from parsed JSON or a JSON string. Keeps only the fields
|
|
261
|
+
* the format knows; anything else is dropped, never carried.
|
|
262
|
+
*/
|
|
263
|
+
export function readWorkflowDefinition(raw) {
|
|
264
|
+
if (typeof raw === "string") {
|
|
265
|
+
try {
|
|
266
|
+
raw = JSON.parse(raw);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
return { ok: false, error: { code: "not-json" } };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return attempt(() => readDefinitionObject(raw));
|
|
273
|
+
}
|
|
274
|
+
/** JSON with sorted keys and no undefined values: two documents that say the same compare equal. */
|
|
275
|
+
const json = (v) => JSON.stringify(v, (_key, value) => isObj(value)
|
|
276
|
+
? Object.fromEntries(Object.keys(value)
|
|
277
|
+
.sort()
|
|
278
|
+
.filter((k) => value[k] !== undefined)
|
|
279
|
+
.map((k) => [k, value[k]]))
|
|
280
|
+
: value);
|
|
281
|
+
const withoutAt = (n) => json({ ...n, at: undefined });
|
|
282
|
+
const flowShape = (f) => json({ label: f.label, inputs: f.inputs, outputs: f.outputs });
|
|
283
|
+
/** What changing `before` into `after` does to what runs. Positions are reported apart, as `moved`. */
|
|
284
|
+
export function diffDefinitions(before, after) {
|
|
285
|
+
const details = [];
|
|
286
|
+
for (const k of ["name", "description", "module", "main"])
|
|
287
|
+
if (before[k] !== after[k])
|
|
288
|
+
details.push(k);
|
|
289
|
+
const flowsAdded = Object.keys(after.flows).filter((n) => !before.flows[n]);
|
|
290
|
+
const flowsRemoved = Object.keys(before.flows).filter((n) => !after.flows[n]);
|
|
291
|
+
const flows = [];
|
|
292
|
+
let shapeChanged = false;
|
|
293
|
+
for (const [name, b] of Object.entries(after.flows)) {
|
|
294
|
+
const a = before.flows[name];
|
|
295
|
+
if (!a)
|
|
296
|
+
continue;
|
|
297
|
+
if (flowShape(a) !== flowShape(b))
|
|
298
|
+
shapeChanged = true;
|
|
299
|
+
const an = new Map(a.nodes.map((n) => [n.id, n]));
|
|
300
|
+
const bn = new Map(b.nodes.map((n) => [n.id, n]));
|
|
301
|
+
const ae = new Map(a.edges.map((e) => [edgeId(e), e]));
|
|
302
|
+
const be = new Map(b.edges.map((e) => [edgeId(e), e]));
|
|
303
|
+
const item = (n) => ({ id: n.id, label: n.label });
|
|
304
|
+
const edge = (f, e) => {
|
|
305
|
+
const l = (id) => f.nodes.find((n) => n.id === id)?.label ?? id;
|
|
306
|
+
return { id: edgeId(e), kind: e.kind, from: l(e.from), to: l(e.to) };
|
|
307
|
+
};
|
|
308
|
+
const d = {
|
|
309
|
+
flow: name,
|
|
310
|
+
nodesAdded: b.nodes.filter((n) => !an.has(n.id)).map(item),
|
|
311
|
+
nodesRemoved: a.nodes.filter((n) => !bn.has(n.id)).map(item),
|
|
312
|
+
nodesChanged: b.nodes
|
|
313
|
+
.filter((n) => an.has(n.id) && withoutAt(an.get(n.id)) !== withoutAt(n))
|
|
314
|
+
.map(item),
|
|
315
|
+
moved: b.nodes
|
|
316
|
+
.filter((n) => {
|
|
317
|
+
const was = an.get(n.id);
|
|
318
|
+
return was && withoutAt(was) === withoutAt(n) && json(was.at) !== json(n.at);
|
|
319
|
+
})
|
|
320
|
+
.map(item),
|
|
321
|
+
edgesAdded: b.edges.filter((e) => !ae.has(edgeId(e))).map((e) => edge(b, e)),
|
|
322
|
+
edgesRemoved: a.edges.filter((e) => !be.has(edgeId(e))).map((e) => edge(a, e)),
|
|
323
|
+
edgesChanged: b.edges
|
|
324
|
+
.filter((e) => ae.has(edgeId(e)) && json(ae.get(edgeId(e))) !== json(e))
|
|
325
|
+
.map((e) => edge(b, e)),
|
|
326
|
+
};
|
|
327
|
+
const { flow: _flow, ...lists } = d;
|
|
328
|
+
if (Object.values(lists).some((l) => l.length))
|
|
329
|
+
flows.push(d);
|
|
330
|
+
}
|
|
331
|
+
const same = !details.length &&
|
|
332
|
+
!flowsAdded.length &&
|
|
333
|
+
!flowsRemoved.length &&
|
|
334
|
+
!shapeChanged &&
|
|
335
|
+
flows.every((f) => !f.nodesAdded.length &&
|
|
336
|
+
!f.nodesRemoved.length &&
|
|
337
|
+
!f.nodesChanged.length &&
|
|
338
|
+
!f.edgesAdded.length &&
|
|
339
|
+
!f.edgesRemoved.length &&
|
|
340
|
+
!f.edgesChanged.length);
|
|
341
|
+
return { details, flowsAdded, flowsRemoved, flows, same };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Whether two definitions are the same document. With `positions: false`,
|
|
345
|
+
* whether they run the same (a moved node does not count).
|
|
346
|
+
*/
|
|
347
|
+
export function sameDefinition(a, b, options = {}) {
|
|
348
|
+
if (!a || !b)
|
|
349
|
+
return a === b;
|
|
350
|
+
const d = diffDefinitions(a, b);
|
|
351
|
+
return d.same && (options.positions === false || d.flows.every((f) => !f.moved.length));
|
|
352
|
+
}
|
|
353
|
+
// ---------------------------------------------------------------- packages
|
|
354
|
+
export const WORKFLOW_PACKAGE_FORMAT = "calliope.workflow-package";
|
|
355
|
+
export const WORKFLOW_PACKAGE_VERSION = 1;
|
|
356
|
+
/** What a definition needs, against the registry it was written for. */
|
|
357
|
+
export function requirementsOf(definition, registry) {
|
|
358
|
+
const nodes = Object.values(definition.flows).flatMap((f) => f.nodes);
|
|
359
|
+
const edges = Object.values(definition.flows).flatMap((f) => f.edges);
|
|
360
|
+
const presets = new Map();
|
|
361
|
+
const modules = new Map();
|
|
362
|
+
const capabilities = new Set();
|
|
363
|
+
const gateKinds = new Set();
|
|
364
|
+
const facts = new Set();
|
|
365
|
+
const operators = new Set();
|
|
366
|
+
if (definition.module) {
|
|
367
|
+
const m = registry.modules.find((x) => x.id === definition.module);
|
|
368
|
+
modules.set(definition.module, clean({ id: definition.module, label: m?.label ?? definition.module, version: m?.version }));
|
|
369
|
+
}
|
|
370
|
+
const conditions = edges.flatMap((e) => (e.until ? [e.until] : []));
|
|
371
|
+
for (const n of nodes) {
|
|
372
|
+
if ((n.kind === "step" || n.kind === "variants") && n.preset) {
|
|
373
|
+
const m = registry.modules.find((x) => x.id === n.preset?.module);
|
|
374
|
+
const p = m?.presets.find((x) => x.id === n.preset?.id);
|
|
375
|
+
modules.set(n.preset.module, clean({ id: n.preset.module, label: m?.label ?? n.preset.module, version: m?.version }));
|
|
376
|
+
presets.set(`${n.preset.module}/${n.preset.id}`, {
|
|
377
|
+
...n.preset,
|
|
378
|
+
label: p?.label ?? n.preset.id,
|
|
379
|
+
});
|
|
380
|
+
for (const c of p?.capabilities ?? [])
|
|
381
|
+
capabilities.add(c);
|
|
382
|
+
}
|
|
383
|
+
if (n.kind === "gate") {
|
|
384
|
+
gateKinds.add(n.gate);
|
|
385
|
+
for (const c of findGateKind(registry, n.gate)?.capabilities ?? [])
|
|
386
|
+
capabilities.add(c);
|
|
387
|
+
}
|
|
388
|
+
if (n.kind === "condition")
|
|
389
|
+
conditions.push(...n.cases.map((c) => c.when));
|
|
390
|
+
}
|
|
391
|
+
for (const c of conditions) {
|
|
392
|
+
facts.add(c.fact);
|
|
393
|
+
operators.add(c.op);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
modules: [...modules.values()],
|
|
397
|
+
presets: [...presets.values()],
|
|
398
|
+
capabilities: [...capabilities],
|
|
399
|
+
gateKinds: [...gateKinds],
|
|
400
|
+
facts: [...facts],
|
|
401
|
+
operators: [...operators],
|
|
402
|
+
nodeKinds: WORKFLOW_NODE_KINDS.filter((k) => nodes.some((n) => n.kind === k)),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** Gates keep their roles and quorum; named members are this organization's, not the package's. */
|
|
406
|
+
function withoutMembers(definition) {
|
|
407
|
+
const flows = {};
|
|
408
|
+
for (const [name, f] of Object.entries(definition.flows))
|
|
409
|
+
flows[name] = {
|
|
410
|
+
...f,
|
|
411
|
+
nodes: f.nodes.map((n) => {
|
|
412
|
+
if (n.kind !== "gate" || !n.policy?.members)
|
|
413
|
+
return n;
|
|
414
|
+
const { members: _members, ...policy } = n.policy;
|
|
415
|
+
return { ...n, policy };
|
|
416
|
+
}),
|
|
417
|
+
};
|
|
418
|
+
return { ...definition, flows };
|
|
419
|
+
}
|
|
420
|
+
/** A package of a definition, for the host to write out. */
|
|
421
|
+
export function exportWorkflowPackage(definition, registry, source) {
|
|
422
|
+
const read = readWorkflowDefinition(JSON.stringify(definition));
|
|
423
|
+
if (!read.ok)
|
|
424
|
+
throw new Error("exportWorkflowPackage: the definition does not read back");
|
|
425
|
+
const portable = withoutMembers(read.value);
|
|
426
|
+
return {
|
|
427
|
+
format: WORKFLOW_PACKAGE_FORMAT,
|
|
428
|
+
version: WORKFLOW_PACKAGE_VERSION,
|
|
429
|
+
definition: portable,
|
|
430
|
+
requires: requirementsOf(portable, registry),
|
|
431
|
+
...(source ? { source } : {}),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
function readRequirements(raw) {
|
|
435
|
+
const o = isObj(raw) ? raw : {};
|
|
436
|
+
const list = (v) => arr(v).filter(isObj);
|
|
437
|
+
return {
|
|
438
|
+
modules: list(o.modules).flatMap((m) => str(m.id)
|
|
439
|
+
? [
|
|
440
|
+
clean({
|
|
441
|
+
id: str(m.id),
|
|
442
|
+
label: str(m.label) ?? m.id,
|
|
443
|
+
version: str(m.version),
|
|
444
|
+
}),
|
|
445
|
+
]
|
|
446
|
+
: []),
|
|
447
|
+
presets: list(o.presets).flatMap((p) => str(p.module) && str(p.id)
|
|
448
|
+
? [
|
|
449
|
+
{
|
|
450
|
+
module: p.module,
|
|
451
|
+
id: p.id,
|
|
452
|
+
label: str(p.label) ?? p.id,
|
|
453
|
+
},
|
|
454
|
+
]
|
|
455
|
+
: []),
|
|
456
|
+
capabilities: strs(o.capabilities),
|
|
457
|
+
gateKinds: strs(o.gateKinds),
|
|
458
|
+
facts: strs(o.facts),
|
|
459
|
+
operators: strs(o.operators),
|
|
460
|
+
nodeKinds: strs(o.nodeKinds).filter((k) => WORKFLOW_NODE_KINDS.includes(k)),
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/** Reads a package from parsed JSON or a JSON string, checking its format and version. */
|
|
464
|
+
export function readWorkflowPackage(raw) {
|
|
465
|
+
if (typeof raw === "string") {
|
|
466
|
+
try {
|
|
467
|
+
raw = JSON.parse(raw);
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return { ok: false, error: { code: "not-json" } };
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (!isObj(raw) || raw.format !== WORKFLOW_PACKAGE_FORMAT)
|
|
474
|
+
return { ok: false, error: { code: "not-a-package" } };
|
|
475
|
+
if (raw.version !== WORKFLOW_PACKAGE_VERSION)
|
|
476
|
+
return { ok: false, error: { code: "unsupported-version", version: raw.version } };
|
|
477
|
+
const definition = readWorkflowDefinition(raw.definition);
|
|
478
|
+
if (!definition.ok)
|
|
479
|
+
return definition;
|
|
480
|
+
const s = isObj(raw.source) ? raw.source : undefined;
|
|
481
|
+
return {
|
|
482
|
+
ok: true,
|
|
483
|
+
value: {
|
|
484
|
+
format: WORKFLOW_PACKAGE_FORMAT,
|
|
485
|
+
version: WORKFLOW_PACKAGE_VERSION,
|
|
486
|
+
definition: withoutMembers(definition.value),
|
|
487
|
+
requires: readRequirements(raw.requires),
|
|
488
|
+
...(s
|
|
489
|
+
? {
|
|
490
|
+
source: clean({
|
|
491
|
+
factory: str(s.factory),
|
|
492
|
+
revision: str(s.revision),
|
|
493
|
+
exportedAt: str(s.exportedAt),
|
|
494
|
+
}),
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* What importing a package would do at a destination: the module and
|
|
502
|
+
* capability gaps, the vocabulary it does not know, the nodes that would be
|
|
503
|
+
* disabled, the problems there, and, when it replaces a definition, the diff.
|
|
504
|
+
*/
|
|
505
|
+
export function previewWorkflowImport(pkg, destination, replacing) {
|
|
506
|
+
const { definition, requires } = pkg;
|
|
507
|
+
const modules = requires.modules.map((m) => {
|
|
508
|
+
const here = destination.modules.find((x) => x.id === m.id);
|
|
509
|
+
const base = clean({ id: m.id, label: m.label, version: m.version });
|
|
510
|
+
if (!here)
|
|
511
|
+
return { ...base, status: "missing" };
|
|
512
|
+
const installed = here.version ? { installed: here.version } : {};
|
|
513
|
+
if (here.availability && here.availability.status !== "available")
|
|
514
|
+
return { ...base, ...installed, status: "unavailable", availability: here.availability };
|
|
515
|
+
if (m.version && here.version && m.version !== here.version)
|
|
516
|
+
return { ...base, ...installed, status: "version" };
|
|
517
|
+
return { ...base, ...installed, status: "available" };
|
|
518
|
+
});
|
|
519
|
+
const missingCapabilities = destination.capabilities
|
|
520
|
+
? requires.capabilities.filter((c) => !destination.capabilities?.includes(c))
|
|
521
|
+
: [];
|
|
522
|
+
const disabled = [];
|
|
523
|
+
const gatesNeedingApprovers = [];
|
|
524
|
+
for (const [flow, f] of Object.entries(definition.flows))
|
|
525
|
+
for (const n of f.nodes) {
|
|
526
|
+
const availability = nodeAvailability(n, destination);
|
|
527
|
+
if (availability.status !== "available")
|
|
528
|
+
disabled.push({ flow, id: n.id, label: n.label, availability });
|
|
529
|
+
if (n.kind === "gate" &&
|
|
530
|
+
findGateKind(destination, n.gate)?.fields.includes("approvers") &&
|
|
531
|
+
!n.policy?.roles?.length)
|
|
532
|
+
gatesNeedingApprovers.push({ id: n.id, label: n.label });
|
|
533
|
+
}
|
|
534
|
+
return {
|
|
535
|
+
definition,
|
|
536
|
+
availability: definitionAvailability(definition, destination),
|
|
537
|
+
modules,
|
|
538
|
+
missingCapabilities,
|
|
539
|
+
unknownGateKinds: requires.gateKinds.filter((g) => !findGateKind(destination, g)),
|
|
540
|
+
unknownFacts: requires.facts.filter((f) => !destination.facts.some((x) => x.id === f)),
|
|
541
|
+
unknownOperators: requires.operators.filter((o) => !destination.operators.some((x) => x.id === o)),
|
|
542
|
+
disabled,
|
|
543
|
+
gatesNeedingApprovers,
|
|
544
|
+
problems: checkDefinition(definition, destination),
|
|
545
|
+
...(replacing ? { diff: diffDefinitions(replacing, definition) } : {}),
|
|
546
|
+
};
|
|
547
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What is wrong with a workflow definition, at three levels, and whether this
|
|
3
|
+
* destination can run each node. `blocking` problems stop a publish,
|
|
4
|
+
* `warning`s do not, and `not-run` marks a node this destination does not run
|
|
5
|
+
* (yet): its module, a capability or its kind is missing here. Problems carry
|
|
6
|
+
* a code and values, never English; the editor's copy names them. Pure.
|
|
7
|
+
*/
|
|
8
|
+
import { type WorkflowDefinition, type WorkflowNode, type WorkflowRegistry } from "./definition.js";
|
|
9
|
+
import type { WorkflowAvailability } from "./model.js";
|
|
10
|
+
export type WorkflowProblemLevel = "blocking" | "warning" | "not-run";
|
|
11
|
+
export declare const WORKFLOW_PROBLEM_CODES: readonly ["no-main", "flow-empty", "duplicate-id", "unreachable", "dangling-edge", "missing-exit", "cycle", "loop-no-until", "loop-no-max", "loop-not-back", "exit-unwired", "input-unbound", "data-port-missing", "data-type", "data-order", "unknown-preset", "unknown-model", "unknown-harness", "unknown-effort", "subflow-none", "subflow-missing", "subflow-recursive", "unknown-gate-kind", "gate-no-approver", "gate-quorum", "gate-no-threshold", "gate-no-rule", "gate-no-timeout-action", "no-cases", "unknown-fact", "unknown-operator", "operator-mismatch", "value-missing", "value-type", "fact-node-missing", "goto-no-target", "goto-missing-target", "goto-no-max", "fanout-few", "join-few", "join-no-n", "variants-few", "variants-no-select", "variants-no-evaluator", "rubric-short", "no-threshold", "no-question", "options-few", "option-duplicate", "not-runnable"];
|
|
12
|
+
export type WorkflowProblemCode = (typeof WORKFLOW_PROBLEM_CODES)[number];
|
|
13
|
+
export interface WorkflowProblem {
|
|
14
|
+
level: WorkflowProblemLevel;
|
|
15
|
+
code: WorkflowProblemCode;
|
|
16
|
+
/** The flow it is in. */
|
|
17
|
+
flow?: string;
|
|
18
|
+
/** What it points at: a node id, an edge id (`edgeId`). Neither: the flow or the definition. */
|
|
19
|
+
node?: string;
|
|
20
|
+
edge?: string;
|
|
21
|
+
/** What the copy names: labels, exits, types, counts. */
|
|
22
|
+
values: Record<string, string | number>;
|
|
23
|
+
/** For `not-runnable`: what this destination says about the node. */
|
|
24
|
+
availability?: WorkflowAvailability;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether this destination can run a node, reusing the gallery's
|
|
28
|
+
* availability: `unsupported` when its kind, module, preset or gate kind is
|
|
29
|
+
* not here, `missing-capabilities` with the names when the module is but a
|
|
30
|
+
* capability it needs is not.
|
|
31
|
+
*/
|
|
32
|
+
export declare function nodeAvailability(node: WorkflowNode, registry: WorkflowRegistry): WorkflowAvailability;
|
|
33
|
+
/** Whether this destination can run the whole definition: every node of every flow. */
|
|
34
|
+
export declare function definitionAvailability(definition: WorkflowDefinition, registry: WorkflowRegistry): WorkflowAvailability;
|
|
35
|
+
/** Every problem in a definition, against what this destination's registry says. */
|
|
36
|
+
export declare function checkDefinition(definition: WorkflowDefinition, registry: WorkflowRegistry): WorkflowProblem[];
|
|
37
|
+
/** The problems that stop a publish. */
|
|
38
|
+
export declare const blockingProblems: (problems: readonly WorkflowProblem[]) => WorkflowProblem[];
|