@contractspec/lib.workflow-composer 3.7.16 → 3.7.18
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/dist/browser/index.js +1 -339
- package/dist/index.js +1 -339
- package/dist/node/index.js +1 -339
- package/package.json +7 -7
package/dist/browser/index.js
CHANGED
|
@@ -1,339 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { satisfies } from "compare-versions";
|
|
3
|
-
|
|
4
|
-
// src/injector.ts
|
|
5
|
-
import {
|
|
6
|
-
validateWorkflowSpec
|
|
7
|
-
} from "@contractspec/lib.contracts-spec/workflow";
|
|
8
|
-
|
|
9
|
-
// src/validator.ts
|
|
10
|
-
function validateExtension(extension, base) {
|
|
11
|
-
const issues = [];
|
|
12
|
-
const baseStepIds = new Set(base.definition.steps.map((step) => step.id));
|
|
13
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
14
|
-
const visibleStepIds = new Set(baseStepIds);
|
|
15
|
-
const visibleTransitions = base.definition.transitions.filter((transition) => visibleStepIds.has(transition.from) && visibleStepIds.has(transition.to)).map((transition) => ({ ...transition }));
|
|
16
|
-
if (!extension.workflow.trim()) {
|
|
17
|
-
issues.push({
|
|
18
|
-
code: "workflow.extension.workflow",
|
|
19
|
-
message: "workflow is required"
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
if (extension.workflow !== base.meta.key) {
|
|
23
|
-
issues.push({
|
|
24
|
-
code: "workflow.extension.workflow.mismatch",
|
|
25
|
-
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
hiddenSteps.forEach((stepId) => {
|
|
29
|
-
if (!baseStepIds.has(stepId)) {
|
|
30
|
-
issues.push({
|
|
31
|
-
code: "workflow.extension.hidden-step",
|
|
32
|
-
message: `hidden step "${stepId}" does not exist`
|
|
33
|
-
});
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
visibleStepIds.delete(stepId);
|
|
37
|
-
});
|
|
38
|
-
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
39
|
-
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
40
|
-
extension.customSteps?.forEach((injection, idx) => {
|
|
41
|
-
const injectionId = injection.inject.id?.trim();
|
|
42
|
-
if (!injectionId) {
|
|
43
|
-
issues.push({
|
|
44
|
-
code: "workflow.extension.step.id",
|
|
45
|
-
message: `customSteps[${idx}] is missing an id`
|
|
46
|
-
});
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (injection.id && injection.id !== injectionId) {
|
|
50
|
-
issues.push({
|
|
51
|
-
code: "workflow.extension.step.id-mismatch",
|
|
52
|
-
message: `customSteps[${idx}] has mismatched id (id="${injection.id}", inject.id="${injectionId}")`
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (availableAnchorSteps.has(injectionId)) {
|
|
56
|
-
issues.push({
|
|
57
|
-
code: "workflow.extension.step.id.duplicate",
|
|
58
|
-
message: `customSteps[${idx}] injects duplicate step id "${injectionId}"`
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
if (injection.after && injection.before) {
|
|
62
|
-
issues.push({
|
|
63
|
-
code: "workflow.extension.step.anchor.multiple",
|
|
64
|
-
message: `customSteps[${idx}] cannot set both after and before`
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (!injection.after && !injection.before) {
|
|
68
|
-
issues.push({
|
|
69
|
-
code: "workflow.extension.step.anchor",
|
|
70
|
-
message: `customSteps[${idx}] must set after or before`
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
if (injection.after && !availableAnchorSteps.has(injection.after)) {
|
|
74
|
-
issues.push({
|
|
75
|
-
code: "workflow.extension.step.after",
|
|
76
|
-
message: `customSteps[${idx}] references unknown step "${injection.after}"`
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (injection.before && !availableAnchorSteps.has(injection.before)) {
|
|
80
|
-
issues.push({
|
|
81
|
-
code: "workflow.extension.step.before",
|
|
82
|
-
message: `customSteps[${idx}] references unknown step "${injection.before}"`
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (injection.transitionFrom && !availableAnchorSteps.has(injection.transitionFrom)) {
|
|
86
|
-
issues.push({
|
|
87
|
-
code: "workflow.extension.step.transition-from",
|
|
88
|
-
message: `customSteps[${idx}] references unknown transitionFrom step "${injection.transitionFrom}"`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (injection.transitionTo && !availableAnchorSteps.has(injection.transitionTo)) {
|
|
92
|
-
issues.push({
|
|
93
|
-
code: "workflow.extension.step.transition-to",
|
|
94
|
-
message: `customSteps[${idx}] references unknown transitionTo step "${injection.transitionTo}"`
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
availableAnchorSteps.add(injectionId);
|
|
98
|
-
visibleStepIds.add(injectionId);
|
|
99
|
-
if (injection.transitionFrom) {
|
|
100
|
-
visibleTransitions.push({
|
|
101
|
-
from: injection.transitionFrom,
|
|
102
|
-
to: injectionId,
|
|
103
|
-
condition: injection.when
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
if (injection.transitionTo) {
|
|
107
|
-
visibleTransitions.push({
|
|
108
|
-
from: injectionId,
|
|
109
|
-
to: injection.transitionTo,
|
|
110
|
-
condition: injection.when
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
115
|
-
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
116
|
-
issues.push({
|
|
117
|
-
code: "workflow.extension.hidden-step.entry",
|
|
118
|
-
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if (entryStepId && !hiddenSteps.has(entryStepId)) {
|
|
122
|
-
const reachable = collectReachableSteps(entryStepId, visibleTransitions);
|
|
123
|
-
for (const stepId of visibleStepIds) {
|
|
124
|
-
if (!reachable.has(stepId)) {
|
|
125
|
-
issues.push({
|
|
126
|
-
code: "workflow.extension.hidden-step.orphan",
|
|
127
|
-
message: `extension leaves step "${stepId}" unreachable from entry step "${entryStepId}"`
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (issues.length) {
|
|
133
|
-
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
134
|
-
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function removeHiddenTransitions(transitions, hiddenSteps) {
|
|
138
|
-
for (let index = transitions.length - 1;index >= 0; index -= 1) {
|
|
139
|
-
const transition = transitions[index];
|
|
140
|
-
if (!transition) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (hiddenSteps.has(transition.from) || hiddenSteps.has(transition.to)) {
|
|
144
|
-
transitions.splice(index, 1);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function collectReachableSteps(entryStepId, transitions) {
|
|
149
|
-
const adjacency = new Map;
|
|
150
|
-
for (const transition of transitions) {
|
|
151
|
-
const next = adjacency.get(transition.from) ?? [];
|
|
152
|
-
next.push(transition.to);
|
|
153
|
-
adjacency.set(transition.from, next);
|
|
154
|
-
}
|
|
155
|
-
const reachable = new Set;
|
|
156
|
-
const queue = [entryStepId];
|
|
157
|
-
while (queue.length > 0) {
|
|
158
|
-
const current = queue.shift();
|
|
159
|
-
if (!current || reachable.has(current)) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
reachable.add(current);
|
|
163
|
-
for (const next of adjacency.get(current) ?? []) {
|
|
164
|
-
queue.push(next);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return reachable;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/injector.ts
|
|
171
|
-
function applyWorkflowExtension(base, extension) {
|
|
172
|
-
validateExtension(extension, base);
|
|
173
|
-
const spec = cloneWorkflowSpec(base);
|
|
174
|
-
const steps = [...spec.definition.steps];
|
|
175
|
-
let transitions = [...spec.definition.transitions];
|
|
176
|
-
const hiddenSet = new Set(extension.hiddenSteps ?? []);
|
|
177
|
-
hiddenSet.forEach((stepId) => {
|
|
178
|
-
const idx = steps.findIndex((step) => step.id === stepId);
|
|
179
|
-
if (idx !== -1) {
|
|
180
|
-
steps.splice(idx, 1);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
if (hiddenSet.size) {
|
|
184
|
-
transitions = transitions.filter((transition) => !hiddenSet.has(transition.from) && !hiddenSet.has(transition.to));
|
|
185
|
-
}
|
|
186
|
-
extension.customSteps?.forEach((injection) => {
|
|
187
|
-
insertStep(steps, injection);
|
|
188
|
-
wireTransitions(transitions, injection);
|
|
189
|
-
});
|
|
190
|
-
spec.definition.steps = steps;
|
|
191
|
-
spec.definition.transitions = dedupeTransitions(transitions);
|
|
192
|
-
spec.metadata = mergeRecords(spec.metadata, extension.metadata);
|
|
193
|
-
spec.annotations = mergeRecords(spec.annotations, extension.annotations);
|
|
194
|
-
const issues = validateWorkflowSpec(spec);
|
|
195
|
-
const blockingIssues = issues.filter((issue) => issue.level === "error");
|
|
196
|
-
if (blockingIssues.length > 0) {
|
|
197
|
-
throw new Error(`Invalid composed workflow ${spec.meta.key}.v${spec.meta.version}: ${blockingIssues.map((issue) => issue.message).join("; ")}`);
|
|
198
|
-
}
|
|
199
|
-
return spec;
|
|
200
|
-
}
|
|
201
|
-
function insertStep(steps, injection) {
|
|
202
|
-
const anchorIndex = resolveAnchorIndex(steps, injection);
|
|
203
|
-
if (anchorIndex === -1) {
|
|
204
|
-
throw new Error(`Unable to place injected step "${injection.inject.id}"`);
|
|
205
|
-
}
|
|
206
|
-
steps.splice(anchorIndex, 0, { ...injection.inject });
|
|
207
|
-
}
|
|
208
|
-
function resolveAnchorIndex(steps, injection) {
|
|
209
|
-
if (injection.after) {
|
|
210
|
-
const idx = steps.findIndex((step) => step.id === injection.after);
|
|
211
|
-
return idx === -1 ? -1 : idx + 1;
|
|
212
|
-
}
|
|
213
|
-
if (injection.before) {
|
|
214
|
-
const idx = steps.findIndex((step) => step.id === injection.before);
|
|
215
|
-
return idx === -1 ? -1 : idx;
|
|
216
|
-
}
|
|
217
|
-
return steps.length;
|
|
218
|
-
}
|
|
219
|
-
function wireTransitions(transitions, injection) {
|
|
220
|
-
if (!injection.inject.id)
|
|
221
|
-
return;
|
|
222
|
-
if (injection.transitionFrom) {
|
|
223
|
-
transitions.push({
|
|
224
|
-
from: injection.transitionFrom,
|
|
225
|
-
to: injection.inject.id,
|
|
226
|
-
condition: injection.when
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
if (injection.transitionTo) {
|
|
230
|
-
transitions.push({
|
|
231
|
-
from: injection.inject.id,
|
|
232
|
-
to: injection.transitionTo,
|
|
233
|
-
condition: injection.when
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function dedupeTransitions(transitions) {
|
|
238
|
-
const seen = new Set;
|
|
239
|
-
const result = [];
|
|
240
|
-
transitions.forEach((transition) => {
|
|
241
|
-
const key = `${transition.from}->${transition.to}:${transition.condition ?? ""}`;
|
|
242
|
-
if (seen.has(key))
|
|
243
|
-
return;
|
|
244
|
-
seen.add(key);
|
|
245
|
-
result.push(transition);
|
|
246
|
-
});
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
function cloneWorkflowSpec(spec) {
|
|
250
|
-
return JSON.parse(JSON.stringify(spec));
|
|
251
|
-
}
|
|
252
|
-
function mergeRecords(base, patch) {
|
|
253
|
-
if (!base && !patch) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
...base ?? {},
|
|
258
|
-
...patch ?? {}
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/merger.ts
|
|
263
|
-
function mergeExtensions(extensions) {
|
|
264
|
-
return [...extensions].sort((a, b) => {
|
|
265
|
-
const priorityOrder = (a.priority ?? 0) - (b.priority ?? 0);
|
|
266
|
-
if (priorityOrder !== 0)
|
|
267
|
-
return priorityOrder;
|
|
268
|
-
const workflowOrder = a.workflow.localeCompare(b.workflow);
|
|
269
|
-
if (workflowOrder !== 0)
|
|
270
|
-
return workflowOrder;
|
|
271
|
-
const tenantOrder = (a.tenantId ?? "").localeCompare(b.tenantId ?? "");
|
|
272
|
-
if (tenantOrder !== 0)
|
|
273
|
-
return tenantOrder;
|
|
274
|
-
const roleOrder = (a.role ?? "").localeCompare(b.role ?? "");
|
|
275
|
-
if (roleOrder !== 0)
|
|
276
|
-
return roleOrder;
|
|
277
|
-
const deviceOrder = (a.device ?? "").localeCompare(b.device ?? "");
|
|
278
|
-
if (deviceOrder !== 0)
|
|
279
|
-
return deviceOrder;
|
|
280
|
-
return (a.baseVersion ?? "").localeCompare(b.baseVersion ?? "");
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/composer.ts
|
|
285
|
-
class WorkflowComposer {
|
|
286
|
-
extensions = [];
|
|
287
|
-
register(extension) {
|
|
288
|
-
this.extensions.push(extension);
|
|
289
|
-
return this;
|
|
290
|
-
}
|
|
291
|
-
registerMany(extensions) {
|
|
292
|
-
extensions.forEach((extension) => {
|
|
293
|
-
this.register(extension);
|
|
294
|
-
});
|
|
295
|
-
return this;
|
|
296
|
-
}
|
|
297
|
-
compose(params) {
|
|
298
|
-
const applicable = mergeExtensions(this.extensions.filter((extension) => matches(params, extension)));
|
|
299
|
-
return applicable.reduce((acc, extension) => applyWorkflowExtension(acc, extension), params.base);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function matches(params, extension) {
|
|
303
|
-
if (extension.workflow !== params.base.meta.key)
|
|
304
|
-
return false;
|
|
305
|
-
if (extension.baseVersion && !satisfies(params.base.meta.version, extension.baseVersion)) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
if (extension.tenantId && extension.tenantId !== params.tenantId) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
if (extension.role && extension.role !== params.role) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
if (extension.device && extension.device !== params.device) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
// src/templates.ts
|
|
320
|
-
function approvalStepTemplate(options) {
|
|
321
|
-
return {
|
|
322
|
-
id: options.id,
|
|
323
|
-
label: options.label,
|
|
324
|
-
type: options.type ?? "human",
|
|
325
|
-
description: options.description ?? "Tenant-specific approval",
|
|
326
|
-
action: options.action,
|
|
327
|
-
guard: options.guardExpression ? {
|
|
328
|
-
type: "expression",
|
|
329
|
-
value: options.guardExpression
|
|
330
|
-
} : undefined
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
export {
|
|
334
|
-
validateExtension,
|
|
335
|
-
mergeExtensions,
|
|
336
|
-
approvalStepTemplate,
|
|
337
|
-
applyWorkflowExtension,
|
|
338
|
-
WorkflowComposer
|
|
339
|
-
};
|
|
1
|
+
import{satisfies as W}from"compare-versions";import{validateWorkflowSpec as i}from"@contractspec/lib.contracts-spec/workflow";function d(o,e){let r=[],s=new Set(e.definition.steps.map((t)=>t.id)),a=new Set(o.hiddenSteps??[]),f=new Set(s),m=e.definition.transitions.filter((t)=>f.has(t.from)&&f.has(t.to)).map((t)=>({...t}));if(!o.workflow.trim())r.push({code:"workflow.extension.workflow",message:"workflow is required"});if(o.workflow!==e.meta.key)r.push({code:"workflow.extension.workflow.mismatch",message:`extension targets "${o.workflow}" but base workflow is "${e.meta.key}"`});a.forEach((t)=>{if(!s.has(t)){r.push({code:"workflow.extension.hidden-step",message:`hidden step "${t}" does not exist`});return}f.delete(t)}),c(m,a);let w=new Set([...s].filter((t)=>!a.has(t)));o.customSteps?.forEach((t,p)=>{let u=t.inject.id?.trim();if(!u){r.push({code:"workflow.extension.step.id",message:`customSteps[${p}] is missing an id`});return}if(t.id&&t.id!==u)r.push({code:"workflow.extension.step.id-mismatch",message:`customSteps[${p}] has mismatched id (id="${t.id}", inject.id="${u}")`});if(w.has(u))r.push({code:"workflow.extension.step.id.duplicate",message:`customSteps[${p}] injects duplicate step id "${u}"`});if(t.after&&t.before)r.push({code:"workflow.extension.step.anchor.multiple",message:`customSteps[${p}] cannot set both after and before`});if(!t.after&&!t.before)r.push({code:"workflow.extension.step.anchor",message:`customSteps[${p}] must set after or before`});if(t.after&&!w.has(t.after))r.push({code:"workflow.extension.step.after",message:`customSteps[${p}] references unknown step "${t.after}"`});if(t.before&&!w.has(t.before))r.push({code:"workflow.extension.step.before",message:`customSteps[${p}] references unknown step "${t.before}"`});if(t.transitionFrom&&!w.has(t.transitionFrom))r.push({code:"workflow.extension.step.transition-from",message:`customSteps[${p}] references unknown transitionFrom step "${t.transitionFrom}"`});if(t.transitionTo&&!w.has(t.transitionTo))r.push({code:"workflow.extension.step.transition-to",message:`customSteps[${p}] references unknown transitionTo step "${t.transitionTo}"`});if(w.add(u),f.add(u),t.transitionFrom)m.push({from:t.transitionFrom,to:u,condition:t.when});if(t.transitionTo)m.push({from:u,to:t.transitionTo,condition:t.when})});let l=e.definition.entryStepId??e.definition.steps[0]?.id;if(l&&a.has(l))r.push({code:"workflow.extension.hidden-step.entry",message:`hiddenSteps removes the entry step "${l}"`});if(l&&!a.has(l)){let t=g(l,m);for(let p of f)if(!t.has(p))r.push({code:"workflow.extension.hidden-step.orphan",message:`extension leaves step "${p}" unreachable from entry step "${l}"`})}if(r.length){let t=r.map((p)=>`${p.code}: ${p.message}`).join("; ");throw Error(`Invalid workflow extension for ${o.workflow}: ${t}`)}}function c(o,e){for(let r=o.length-1;r>=0;r-=1){let s=o[r];if(!s)continue;if(e.has(s.from)||e.has(s.to))o.splice(r,1)}}function g(o,e){let r=new Map;for(let f of e){let m=r.get(f.from)??[];m.push(f.to),r.set(f.from,m)}let s=new Set,a=[o];while(a.length>0){let f=a.shift();if(!f||s.has(f))continue;s.add(f);for(let m of r.get(f)??[])a.push(m)}return s}function h(o,e){d(e,o);let r=v(o),s=[...r.definition.steps],a=[...r.definition.transitions],f=new Set(e.hiddenSteps??[]);if(f.forEach((l)=>{let t=s.findIndex((p)=>p.id===l);if(t!==-1)s.splice(t,1)}),f.size)a=a.filter((l)=>!f.has(l.from)&&!f.has(l.to));e.customSteps?.forEach((l)=>{S(s,l),$(a,l)}),r.definition.steps=s,r.definition.transitions=E(a),r.metadata=n(r.metadata,e.metadata),r.annotations=n(r.annotations,e.annotations);let w=i(r).filter((l)=>l.level==="error");if(w.length>0)throw Error(`Invalid composed workflow ${r.meta.key}.v${r.meta.version}: ${w.map((l)=>l.message).join("; ")}`);return r}function S(o,e){let r=y(o,e);if(r===-1)throw Error(`Unable to place injected step "${e.inject.id}"`);o.splice(r,0,{...e.inject})}function y(o,e){if(e.after){let r=o.findIndex((s)=>s.id===e.after);return r===-1?-1:r+1}if(e.before){let r=o.findIndex((s)=>s.id===e.before);return r===-1?-1:r}return o.length}function $(o,e){if(!e.inject.id)return;if(e.transitionFrom)o.push({from:e.transitionFrom,to:e.inject.id,condition:e.when});if(e.transitionTo)o.push({from:e.inject.id,to:e.transitionTo,condition:e.when})}function E(o){let e=new Set,r=[];return o.forEach((s)=>{let a=`${s.from}->${s.to}:${s.condition??""}`;if(e.has(a))return;e.add(a),r.push(s)}),r}function v(o){return JSON.parse(JSON.stringify(o))}function n(o,e){if(!o&&!e)return;return{...o??{},...e??{}}}function k(o){return[...o].sort((e,r)=>{let s=(e.priority??0)-(r.priority??0);if(s!==0)return s;let a=e.workflow.localeCompare(r.workflow);if(a!==0)return a;let f=(e.tenantId??"").localeCompare(r.tenantId??"");if(f!==0)return f;let m=(e.role??"").localeCompare(r.role??"");if(m!==0)return m;let w=(e.device??"").localeCompare(r.device??"");if(w!==0)return w;return(e.baseVersion??"").localeCompare(r.baseVersion??"")})}class x{extensions=[];register(o){return this.extensions.push(o),this}registerMany(o){return o.forEach((e)=>{this.register(e)}),this}compose(o){return k(this.extensions.filter((r)=>T(o,r))).reduce((r,s)=>h(r,s),o.base)}}function T(o,e){if(e.workflow!==o.base.meta.key)return!1;if(e.baseVersion&&!W(o.base.meta.version,e.baseVersion))return!1;if(e.tenantId&&e.tenantId!==o.tenantId)return!1;if(e.role&&e.role!==o.role)return!1;if(e.device&&e.device!==o.device)return!1;return!0}function H(o){return{id:o.id,label:o.label,type:o.type??"human",description:o.description??"Tenant-specific approval",action:o.action,guard:o.guardExpression?{type:"expression",value:o.guardExpression}:void 0}}export{d as validateExtension,k as mergeExtensions,H as approvalStepTemplate,h as applyWorkflowExtension,x as WorkflowComposer};
|
package/dist/index.js
CHANGED
|
@@ -1,340 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
|
|
3
|
-
import { satisfies } from "compare-versions";
|
|
4
|
-
|
|
5
|
-
// src/injector.ts
|
|
6
|
-
import {
|
|
7
|
-
validateWorkflowSpec
|
|
8
|
-
} from "@contractspec/lib.contracts-spec/workflow";
|
|
9
|
-
|
|
10
|
-
// src/validator.ts
|
|
11
|
-
function validateExtension(extension, base) {
|
|
12
|
-
const issues = [];
|
|
13
|
-
const baseStepIds = new Set(base.definition.steps.map((step) => step.id));
|
|
14
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
15
|
-
const visibleStepIds = new Set(baseStepIds);
|
|
16
|
-
const visibleTransitions = base.definition.transitions.filter((transition) => visibleStepIds.has(transition.from) && visibleStepIds.has(transition.to)).map((transition) => ({ ...transition }));
|
|
17
|
-
if (!extension.workflow.trim()) {
|
|
18
|
-
issues.push({
|
|
19
|
-
code: "workflow.extension.workflow",
|
|
20
|
-
message: "workflow is required"
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
if (extension.workflow !== base.meta.key) {
|
|
24
|
-
issues.push({
|
|
25
|
-
code: "workflow.extension.workflow.mismatch",
|
|
26
|
-
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
hiddenSteps.forEach((stepId) => {
|
|
30
|
-
if (!baseStepIds.has(stepId)) {
|
|
31
|
-
issues.push({
|
|
32
|
-
code: "workflow.extension.hidden-step",
|
|
33
|
-
message: `hidden step "${stepId}" does not exist`
|
|
34
|
-
});
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
visibleStepIds.delete(stepId);
|
|
38
|
-
});
|
|
39
|
-
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
40
|
-
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
41
|
-
extension.customSteps?.forEach((injection, idx) => {
|
|
42
|
-
const injectionId = injection.inject.id?.trim();
|
|
43
|
-
if (!injectionId) {
|
|
44
|
-
issues.push({
|
|
45
|
-
code: "workflow.extension.step.id",
|
|
46
|
-
message: `customSteps[${idx}] is missing an id`
|
|
47
|
-
});
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (injection.id && injection.id !== injectionId) {
|
|
51
|
-
issues.push({
|
|
52
|
-
code: "workflow.extension.step.id-mismatch",
|
|
53
|
-
message: `customSteps[${idx}] has mismatched id (id="${injection.id}", inject.id="${injectionId}")`
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
if (availableAnchorSteps.has(injectionId)) {
|
|
57
|
-
issues.push({
|
|
58
|
-
code: "workflow.extension.step.id.duplicate",
|
|
59
|
-
message: `customSteps[${idx}] injects duplicate step id "${injectionId}"`
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
if (injection.after && injection.before) {
|
|
63
|
-
issues.push({
|
|
64
|
-
code: "workflow.extension.step.anchor.multiple",
|
|
65
|
-
message: `customSteps[${idx}] cannot set both after and before`
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
if (!injection.after && !injection.before) {
|
|
69
|
-
issues.push({
|
|
70
|
-
code: "workflow.extension.step.anchor",
|
|
71
|
-
message: `customSteps[${idx}] must set after or before`
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
if (injection.after && !availableAnchorSteps.has(injection.after)) {
|
|
75
|
-
issues.push({
|
|
76
|
-
code: "workflow.extension.step.after",
|
|
77
|
-
message: `customSteps[${idx}] references unknown step "${injection.after}"`
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
if (injection.before && !availableAnchorSteps.has(injection.before)) {
|
|
81
|
-
issues.push({
|
|
82
|
-
code: "workflow.extension.step.before",
|
|
83
|
-
message: `customSteps[${idx}] references unknown step "${injection.before}"`
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
if (injection.transitionFrom && !availableAnchorSteps.has(injection.transitionFrom)) {
|
|
87
|
-
issues.push({
|
|
88
|
-
code: "workflow.extension.step.transition-from",
|
|
89
|
-
message: `customSteps[${idx}] references unknown transitionFrom step "${injection.transitionFrom}"`
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
if (injection.transitionTo && !availableAnchorSteps.has(injection.transitionTo)) {
|
|
93
|
-
issues.push({
|
|
94
|
-
code: "workflow.extension.step.transition-to",
|
|
95
|
-
message: `customSteps[${idx}] references unknown transitionTo step "${injection.transitionTo}"`
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
availableAnchorSteps.add(injectionId);
|
|
99
|
-
visibleStepIds.add(injectionId);
|
|
100
|
-
if (injection.transitionFrom) {
|
|
101
|
-
visibleTransitions.push({
|
|
102
|
-
from: injection.transitionFrom,
|
|
103
|
-
to: injectionId,
|
|
104
|
-
condition: injection.when
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
if (injection.transitionTo) {
|
|
108
|
-
visibleTransitions.push({
|
|
109
|
-
from: injectionId,
|
|
110
|
-
to: injection.transitionTo,
|
|
111
|
-
condition: injection.when
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
116
|
-
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
117
|
-
issues.push({
|
|
118
|
-
code: "workflow.extension.hidden-step.entry",
|
|
119
|
-
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
if (entryStepId && !hiddenSteps.has(entryStepId)) {
|
|
123
|
-
const reachable = collectReachableSteps(entryStepId, visibleTransitions);
|
|
124
|
-
for (const stepId of visibleStepIds) {
|
|
125
|
-
if (!reachable.has(stepId)) {
|
|
126
|
-
issues.push({
|
|
127
|
-
code: "workflow.extension.hidden-step.orphan",
|
|
128
|
-
message: `extension leaves step "${stepId}" unreachable from entry step "${entryStepId}"`
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
if (issues.length) {
|
|
134
|
-
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
135
|
-
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
function removeHiddenTransitions(transitions, hiddenSteps) {
|
|
139
|
-
for (let index = transitions.length - 1;index >= 0; index -= 1) {
|
|
140
|
-
const transition = transitions[index];
|
|
141
|
-
if (!transition) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
if (hiddenSteps.has(transition.from) || hiddenSteps.has(transition.to)) {
|
|
145
|
-
transitions.splice(index, 1);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function collectReachableSteps(entryStepId, transitions) {
|
|
150
|
-
const adjacency = new Map;
|
|
151
|
-
for (const transition of transitions) {
|
|
152
|
-
const next = adjacency.get(transition.from) ?? [];
|
|
153
|
-
next.push(transition.to);
|
|
154
|
-
adjacency.set(transition.from, next);
|
|
155
|
-
}
|
|
156
|
-
const reachable = new Set;
|
|
157
|
-
const queue = [entryStepId];
|
|
158
|
-
while (queue.length > 0) {
|
|
159
|
-
const current = queue.shift();
|
|
160
|
-
if (!current || reachable.has(current)) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
reachable.add(current);
|
|
164
|
-
for (const next of adjacency.get(current) ?? []) {
|
|
165
|
-
queue.push(next);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return reachable;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// src/injector.ts
|
|
172
|
-
function applyWorkflowExtension(base, extension) {
|
|
173
|
-
validateExtension(extension, base);
|
|
174
|
-
const spec = cloneWorkflowSpec(base);
|
|
175
|
-
const steps = [...spec.definition.steps];
|
|
176
|
-
let transitions = [...spec.definition.transitions];
|
|
177
|
-
const hiddenSet = new Set(extension.hiddenSteps ?? []);
|
|
178
|
-
hiddenSet.forEach((stepId) => {
|
|
179
|
-
const idx = steps.findIndex((step) => step.id === stepId);
|
|
180
|
-
if (idx !== -1) {
|
|
181
|
-
steps.splice(idx, 1);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
if (hiddenSet.size) {
|
|
185
|
-
transitions = transitions.filter((transition) => !hiddenSet.has(transition.from) && !hiddenSet.has(transition.to));
|
|
186
|
-
}
|
|
187
|
-
extension.customSteps?.forEach((injection) => {
|
|
188
|
-
insertStep(steps, injection);
|
|
189
|
-
wireTransitions(transitions, injection);
|
|
190
|
-
});
|
|
191
|
-
spec.definition.steps = steps;
|
|
192
|
-
spec.definition.transitions = dedupeTransitions(transitions);
|
|
193
|
-
spec.metadata = mergeRecords(spec.metadata, extension.metadata);
|
|
194
|
-
spec.annotations = mergeRecords(spec.annotations, extension.annotations);
|
|
195
|
-
const issues = validateWorkflowSpec(spec);
|
|
196
|
-
const blockingIssues = issues.filter((issue) => issue.level === "error");
|
|
197
|
-
if (blockingIssues.length > 0) {
|
|
198
|
-
throw new Error(`Invalid composed workflow ${spec.meta.key}.v${spec.meta.version}: ${blockingIssues.map((issue) => issue.message).join("; ")}`);
|
|
199
|
-
}
|
|
200
|
-
return spec;
|
|
201
|
-
}
|
|
202
|
-
function insertStep(steps, injection) {
|
|
203
|
-
const anchorIndex = resolveAnchorIndex(steps, injection);
|
|
204
|
-
if (anchorIndex === -1) {
|
|
205
|
-
throw new Error(`Unable to place injected step "${injection.inject.id}"`);
|
|
206
|
-
}
|
|
207
|
-
steps.splice(anchorIndex, 0, { ...injection.inject });
|
|
208
|
-
}
|
|
209
|
-
function resolveAnchorIndex(steps, injection) {
|
|
210
|
-
if (injection.after) {
|
|
211
|
-
const idx = steps.findIndex((step) => step.id === injection.after);
|
|
212
|
-
return idx === -1 ? -1 : idx + 1;
|
|
213
|
-
}
|
|
214
|
-
if (injection.before) {
|
|
215
|
-
const idx = steps.findIndex((step) => step.id === injection.before);
|
|
216
|
-
return idx === -1 ? -1 : idx;
|
|
217
|
-
}
|
|
218
|
-
return steps.length;
|
|
219
|
-
}
|
|
220
|
-
function wireTransitions(transitions, injection) {
|
|
221
|
-
if (!injection.inject.id)
|
|
222
|
-
return;
|
|
223
|
-
if (injection.transitionFrom) {
|
|
224
|
-
transitions.push({
|
|
225
|
-
from: injection.transitionFrom,
|
|
226
|
-
to: injection.inject.id,
|
|
227
|
-
condition: injection.when
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
if (injection.transitionTo) {
|
|
231
|
-
transitions.push({
|
|
232
|
-
from: injection.inject.id,
|
|
233
|
-
to: injection.transitionTo,
|
|
234
|
-
condition: injection.when
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function dedupeTransitions(transitions) {
|
|
239
|
-
const seen = new Set;
|
|
240
|
-
const result = [];
|
|
241
|
-
transitions.forEach((transition) => {
|
|
242
|
-
const key = `${transition.from}->${transition.to}:${transition.condition ?? ""}`;
|
|
243
|
-
if (seen.has(key))
|
|
244
|
-
return;
|
|
245
|
-
seen.add(key);
|
|
246
|
-
result.push(transition);
|
|
247
|
-
});
|
|
248
|
-
return result;
|
|
249
|
-
}
|
|
250
|
-
function cloneWorkflowSpec(spec) {
|
|
251
|
-
return JSON.parse(JSON.stringify(spec));
|
|
252
|
-
}
|
|
253
|
-
function mergeRecords(base, patch) {
|
|
254
|
-
if (!base && !patch) {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
return {
|
|
258
|
-
...base ?? {},
|
|
259
|
-
...patch ?? {}
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// src/merger.ts
|
|
264
|
-
function mergeExtensions(extensions) {
|
|
265
|
-
return [...extensions].sort((a, b) => {
|
|
266
|
-
const priorityOrder = (a.priority ?? 0) - (b.priority ?? 0);
|
|
267
|
-
if (priorityOrder !== 0)
|
|
268
|
-
return priorityOrder;
|
|
269
|
-
const workflowOrder = a.workflow.localeCompare(b.workflow);
|
|
270
|
-
if (workflowOrder !== 0)
|
|
271
|
-
return workflowOrder;
|
|
272
|
-
const tenantOrder = (a.tenantId ?? "").localeCompare(b.tenantId ?? "");
|
|
273
|
-
if (tenantOrder !== 0)
|
|
274
|
-
return tenantOrder;
|
|
275
|
-
const roleOrder = (a.role ?? "").localeCompare(b.role ?? "");
|
|
276
|
-
if (roleOrder !== 0)
|
|
277
|
-
return roleOrder;
|
|
278
|
-
const deviceOrder = (a.device ?? "").localeCompare(b.device ?? "");
|
|
279
|
-
if (deviceOrder !== 0)
|
|
280
|
-
return deviceOrder;
|
|
281
|
-
return (a.baseVersion ?? "").localeCompare(b.baseVersion ?? "");
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// src/composer.ts
|
|
286
|
-
class WorkflowComposer {
|
|
287
|
-
extensions = [];
|
|
288
|
-
register(extension) {
|
|
289
|
-
this.extensions.push(extension);
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
registerMany(extensions) {
|
|
293
|
-
extensions.forEach((extension) => {
|
|
294
|
-
this.register(extension);
|
|
295
|
-
});
|
|
296
|
-
return this;
|
|
297
|
-
}
|
|
298
|
-
compose(params) {
|
|
299
|
-
const applicable = mergeExtensions(this.extensions.filter((extension) => matches(params, extension)));
|
|
300
|
-
return applicable.reduce((acc, extension) => applyWorkflowExtension(acc, extension), params.base);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
function matches(params, extension) {
|
|
304
|
-
if (extension.workflow !== params.base.meta.key)
|
|
305
|
-
return false;
|
|
306
|
-
if (extension.baseVersion && !satisfies(params.base.meta.version, extension.baseVersion)) {
|
|
307
|
-
return false;
|
|
308
|
-
}
|
|
309
|
-
if (extension.tenantId && extension.tenantId !== params.tenantId) {
|
|
310
|
-
return false;
|
|
311
|
-
}
|
|
312
|
-
if (extension.role && extension.role !== params.role) {
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
if (extension.device && extension.device !== params.device) {
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
// src/templates.ts
|
|
321
|
-
function approvalStepTemplate(options) {
|
|
322
|
-
return {
|
|
323
|
-
id: options.id,
|
|
324
|
-
label: options.label,
|
|
325
|
-
type: options.type ?? "human",
|
|
326
|
-
description: options.description ?? "Tenant-specific approval",
|
|
327
|
-
action: options.action,
|
|
328
|
-
guard: options.guardExpression ? {
|
|
329
|
-
type: "expression",
|
|
330
|
-
value: options.guardExpression
|
|
331
|
-
} : undefined
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
export {
|
|
335
|
-
validateExtension,
|
|
336
|
-
mergeExtensions,
|
|
337
|
-
approvalStepTemplate,
|
|
338
|
-
applyWorkflowExtension,
|
|
339
|
-
WorkflowComposer
|
|
340
|
-
};
|
|
2
|
+
import{satisfies as W}from"compare-versions";import{validateWorkflowSpec as i}from"@contractspec/lib.contracts-spec/workflow";function d(o,e){let r=[],s=new Set(e.definition.steps.map((t)=>t.id)),a=new Set(o.hiddenSteps??[]),f=new Set(s),m=e.definition.transitions.filter((t)=>f.has(t.from)&&f.has(t.to)).map((t)=>({...t}));if(!o.workflow.trim())r.push({code:"workflow.extension.workflow",message:"workflow is required"});if(o.workflow!==e.meta.key)r.push({code:"workflow.extension.workflow.mismatch",message:`extension targets "${o.workflow}" but base workflow is "${e.meta.key}"`});a.forEach((t)=>{if(!s.has(t)){r.push({code:"workflow.extension.hidden-step",message:`hidden step "${t}" does not exist`});return}f.delete(t)}),c(m,a);let w=new Set([...s].filter((t)=>!a.has(t)));o.customSteps?.forEach((t,p)=>{let u=t.inject.id?.trim();if(!u){r.push({code:"workflow.extension.step.id",message:`customSteps[${p}] is missing an id`});return}if(t.id&&t.id!==u)r.push({code:"workflow.extension.step.id-mismatch",message:`customSteps[${p}] has mismatched id (id="${t.id}", inject.id="${u}")`});if(w.has(u))r.push({code:"workflow.extension.step.id.duplicate",message:`customSteps[${p}] injects duplicate step id "${u}"`});if(t.after&&t.before)r.push({code:"workflow.extension.step.anchor.multiple",message:`customSteps[${p}] cannot set both after and before`});if(!t.after&&!t.before)r.push({code:"workflow.extension.step.anchor",message:`customSteps[${p}] must set after or before`});if(t.after&&!w.has(t.after))r.push({code:"workflow.extension.step.after",message:`customSteps[${p}] references unknown step "${t.after}"`});if(t.before&&!w.has(t.before))r.push({code:"workflow.extension.step.before",message:`customSteps[${p}] references unknown step "${t.before}"`});if(t.transitionFrom&&!w.has(t.transitionFrom))r.push({code:"workflow.extension.step.transition-from",message:`customSteps[${p}] references unknown transitionFrom step "${t.transitionFrom}"`});if(t.transitionTo&&!w.has(t.transitionTo))r.push({code:"workflow.extension.step.transition-to",message:`customSteps[${p}] references unknown transitionTo step "${t.transitionTo}"`});if(w.add(u),f.add(u),t.transitionFrom)m.push({from:t.transitionFrom,to:u,condition:t.when});if(t.transitionTo)m.push({from:u,to:t.transitionTo,condition:t.when})});let l=e.definition.entryStepId??e.definition.steps[0]?.id;if(l&&a.has(l))r.push({code:"workflow.extension.hidden-step.entry",message:`hiddenSteps removes the entry step "${l}"`});if(l&&!a.has(l)){let t=g(l,m);for(let p of f)if(!t.has(p))r.push({code:"workflow.extension.hidden-step.orphan",message:`extension leaves step "${p}" unreachable from entry step "${l}"`})}if(r.length){let t=r.map((p)=>`${p.code}: ${p.message}`).join("; ");throw Error(`Invalid workflow extension for ${o.workflow}: ${t}`)}}function c(o,e){for(let r=o.length-1;r>=0;r-=1){let s=o[r];if(!s)continue;if(e.has(s.from)||e.has(s.to))o.splice(r,1)}}function g(o,e){let r=new Map;for(let f of e){let m=r.get(f.from)??[];m.push(f.to),r.set(f.from,m)}let s=new Set,a=[o];while(a.length>0){let f=a.shift();if(!f||s.has(f))continue;s.add(f);for(let m of r.get(f)??[])a.push(m)}return s}function h(o,e){d(e,o);let r=v(o),s=[...r.definition.steps],a=[...r.definition.transitions],f=new Set(e.hiddenSteps??[]);if(f.forEach((l)=>{let t=s.findIndex((p)=>p.id===l);if(t!==-1)s.splice(t,1)}),f.size)a=a.filter((l)=>!f.has(l.from)&&!f.has(l.to));e.customSteps?.forEach((l)=>{S(s,l),$(a,l)}),r.definition.steps=s,r.definition.transitions=E(a),r.metadata=n(r.metadata,e.metadata),r.annotations=n(r.annotations,e.annotations);let w=i(r).filter((l)=>l.level==="error");if(w.length>0)throw Error(`Invalid composed workflow ${r.meta.key}.v${r.meta.version}: ${w.map((l)=>l.message).join("; ")}`);return r}function S(o,e){let r=y(o,e);if(r===-1)throw Error(`Unable to place injected step "${e.inject.id}"`);o.splice(r,0,{...e.inject})}function y(o,e){if(e.after){let r=o.findIndex((s)=>s.id===e.after);return r===-1?-1:r+1}if(e.before){let r=o.findIndex((s)=>s.id===e.before);return r===-1?-1:r}return o.length}function $(o,e){if(!e.inject.id)return;if(e.transitionFrom)o.push({from:e.transitionFrom,to:e.inject.id,condition:e.when});if(e.transitionTo)o.push({from:e.inject.id,to:e.transitionTo,condition:e.when})}function E(o){let e=new Set,r=[];return o.forEach((s)=>{let a=`${s.from}->${s.to}:${s.condition??""}`;if(e.has(a))return;e.add(a),r.push(s)}),r}function v(o){return JSON.parse(JSON.stringify(o))}function n(o,e){if(!o&&!e)return;return{...o??{},...e??{}}}function k(o){return[...o].sort((e,r)=>{let s=(e.priority??0)-(r.priority??0);if(s!==0)return s;let a=e.workflow.localeCompare(r.workflow);if(a!==0)return a;let f=(e.tenantId??"").localeCompare(r.tenantId??"");if(f!==0)return f;let m=(e.role??"").localeCompare(r.role??"");if(m!==0)return m;let w=(e.device??"").localeCompare(r.device??"");if(w!==0)return w;return(e.baseVersion??"").localeCompare(r.baseVersion??"")})}class x{extensions=[];register(o){return this.extensions.push(o),this}registerMany(o){return o.forEach((e)=>{this.register(e)}),this}compose(o){return k(this.extensions.filter((r)=>T(o,r))).reduce((r,s)=>h(r,s),o.base)}}function T(o,e){if(e.workflow!==o.base.meta.key)return!1;if(e.baseVersion&&!W(o.base.meta.version,e.baseVersion))return!1;if(e.tenantId&&e.tenantId!==o.tenantId)return!1;if(e.role&&e.role!==o.role)return!1;if(e.device&&e.device!==o.device)return!1;return!0}function H(o){return{id:o.id,label:o.label,type:o.type??"human",description:o.description??"Tenant-specific approval",action:o.action,guard:o.guardExpression?{type:"expression",value:o.guardExpression}:void 0}}export{d as validateExtension,k as mergeExtensions,H as approvalStepTemplate,h as applyWorkflowExtension,x as WorkflowComposer};
|
package/dist/node/index.js
CHANGED
|
@@ -1,339 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { satisfies } from "compare-versions";
|
|
3
|
-
|
|
4
|
-
// src/injector.ts
|
|
5
|
-
import {
|
|
6
|
-
validateWorkflowSpec
|
|
7
|
-
} from "@contractspec/lib.contracts-spec/workflow";
|
|
8
|
-
|
|
9
|
-
// src/validator.ts
|
|
10
|
-
function validateExtension(extension, base) {
|
|
11
|
-
const issues = [];
|
|
12
|
-
const baseStepIds = new Set(base.definition.steps.map((step) => step.id));
|
|
13
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
14
|
-
const visibleStepIds = new Set(baseStepIds);
|
|
15
|
-
const visibleTransitions = base.definition.transitions.filter((transition) => visibleStepIds.has(transition.from) && visibleStepIds.has(transition.to)).map((transition) => ({ ...transition }));
|
|
16
|
-
if (!extension.workflow.trim()) {
|
|
17
|
-
issues.push({
|
|
18
|
-
code: "workflow.extension.workflow",
|
|
19
|
-
message: "workflow is required"
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
if (extension.workflow !== base.meta.key) {
|
|
23
|
-
issues.push({
|
|
24
|
-
code: "workflow.extension.workflow.mismatch",
|
|
25
|
-
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
hiddenSteps.forEach((stepId) => {
|
|
29
|
-
if (!baseStepIds.has(stepId)) {
|
|
30
|
-
issues.push({
|
|
31
|
-
code: "workflow.extension.hidden-step",
|
|
32
|
-
message: `hidden step "${stepId}" does not exist`
|
|
33
|
-
});
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
visibleStepIds.delete(stepId);
|
|
37
|
-
});
|
|
38
|
-
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
39
|
-
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
40
|
-
extension.customSteps?.forEach((injection, idx) => {
|
|
41
|
-
const injectionId = injection.inject.id?.trim();
|
|
42
|
-
if (!injectionId) {
|
|
43
|
-
issues.push({
|
|
44
|
-
code: "workflow.extension.step.id",
|
|
45
|
-
message: `customSteps[${idx}] is missing an id`
|
|
46
|
-
});
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (injection.id && injection.id !== injectionId) {
|
|
50
|
-
issues.push({
|
|
51
|
-
code: "workflow.extension.step.id-mismatch",
|
|
52
|
-
message: `customSteps[${idx}] has mismatched id (id="${injection.id}", inject.id="${injectionId}")`
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
if (availableAnchorSteps.has(injectionId)) {
|
|
56
|
-
issues.push({
|
|
57
|
-
code: "workflow.extension.step.id.duplicate",
|
|
58
|
-
message: `customSteps[${idx}] injects duplicate step id "${injectionId}"`
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
if (injection.after && injection.before) {
|
|
62
|
-
issues.push({
|
|
63
|
-
code: "workflow.extension.step.anchor.multiple",
|
|
64
|
-
message: `customSteps[${idx}] cannot set both after and before`
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
if (!injection.after && !injection.before) {
|
|
68
|
-
issues.push({
|
|
69
|
-
code: "workflow.extension.step.anchor",
|
|
70
|
-
message: `customSteps[${idx}] must set after or before`
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
if (injection.after && !availableAnchorSteps.has(injection.after)) {
|
|
74
|
-
issues.push({
|
|
75
|
-
code: "workflow.extension.step.after",
|
|
76
|
-
message: `customSteps[${idx}] references unknown step "${injection.after}"`
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
if (injection.before && !availableAnchorSteps.has(injection.before)) {
|
|
80
|
-
issues.push({
|
|
81
|
-
code: "workflow.extension.step.before",
|
|
82
|
-
message: `customSteps[${idx}] references unknown step "${injection.before}"`
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
if (injection.transitionFrom && !availableAnchorSteps.has(injection.transitionFrom)) {
|
|
86
|
-
issues.push({
|
|
87
|
-
code: "workflow.extension.step.transition-from",
|
|
88
|
-
message: `customSteps[${idx}] references unknown transitionFrom step "${injection.transitionFrom}"`
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
if (injection.transitionTo && !availableAnchorSteps.has(injection.transitionTo)) {
|
|
92
|
-
issues.push({
|
|
93
|
-
code: "workflow.extension.step.transition-to",
|
|
94
|
-
message: `customSteps[${idx}] references unknown transitionTo step "${injection.transitionTo}"`
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
availableAnchorSteps.add(injectionId);
|
|
98
|
-
visibleStepIds.add(injectionId);
|
|
99
|
-
if (injection.transitionFrom) {
|
|
100
|
-
visibleTransitions.push({
|
|
101
|
-
from: injection.transitionFrom,
|
|
102
|
-
to: injectionId,
|
|
103
|
-
condition: injection.when
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
if (injection.transitionTo) {
|
|
107
|
-
visibleTransitions.push({
|
|
108
|
-
from: injectionId,
|
|
109
|
-
to: injection.transitionTo,
|
|
110
|
-
condition: injection.when
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
115
|
-
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
116
|
-
issues.push({
|
|
117
|
-
code: "workflow.extension.hidden-step.entry",
|
|
118
|
-
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
if (entryStepId && !hiddenSteps.has(entryStepId)) {
|
|
122
|
-
const reachable = collectReachableSteps(entryStepId, visibleTransitions);
|
|
123
|
-
for (const stepId of visibleStepIds) {
|
|
124
|
-
if (!reachable.has(stepId)) {
|
|
125
|
-
issues.push({
|
|
126
|
-
code: "workflow.extension.hidden-step.orphan",
|
|
127
|
-
message: `extension leaves step "${stepId}" unreachable from entry step "${entryStepId}"`
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
if (issues.length) {
|
|
133
|
-
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
134
|
-
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
function removeHiddenTransitions(transitions, hiddenSteps) {
|
|
138
|
-
for (let index = transitions.length - 1;index >= 0; index -= 1) {
|
|
139
|
-
const transition = transitions[index];
|
|
140
|
-
if (!transition) {
|
|
141
|
-
continue;
|
|
142
|
-
}
|
|
143
|
-
if (hiddenSteps.has(transition.from) || hiddenSteps.has(transition.to)) {
|
|
144
|
-
transitions.splice(index, 1);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
function collectReachableSteps(entryStepId, transitions) {
|
|
149
|
-
const adjacency = new Map;
|
|
150
|
-
for (const transition of transitions) {
|
|
151
|
-
const next = adjacency.get(transition.from) ?? [];
|
|
152
|
-
next.push(transition.to);
|
|
153
|
-
adjacency.set(transition.from, next);
|
|
154
|
-
}
|
|
155
|
-
const reachable = new Set;
|
|
156
|
-
const queue = [entryStepId];
|
|
157
|
-
while (queue.length > 0) {
|
|
158
|
-
const current = queue.shift();
|
|
159
|
-
if (!current || reachable.has(current)) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
reachable.add(current);
|
|
163
|
-
for (const next of adjacency.get(current) ?? []) {
|
|
164
|
-
queue.push(next);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return reachable;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// src/injector.ts
|
|
171
|
-
function applyWorkflowExtension(base, extension) {
|
|
172
|
-
validateExtension(extension, base);
|
|
173
|
-
const spec = cloneWorkflowSpec(base);
|
|
174
|
-
const steps = [...spec.definition.steps];
|
|
175
|
-
let transitions = [...spec.definition.transitions];
|
|
176
|
-
const hiddenSet = new Set(extension.hiddenSteps ?? []);
|
|
177
|
-
hiddenSet.forEach((stepId) => {
|
|
178
|
-
const idx = steps.findIndex((step) => step.id === stepId);
|
|
179
|
-
if (idx !== -1) {
|
|
180
|
-
steps.splice(idx, 1);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
if (hiddenSet.size) {
|
|
184
|
-
transitions = transitions.filter((transition) => !hiddenSet.has(transition.from) && !hiddenSet.has(transition.to));
|
|
185
|
-
}
|
|
186
|
-
extension.customSteps?.forEach((injection) => {
|
|
187
|
-
insertStep(steps, injection);
|
|
188
|
-
wireTransitions(transitions, injection);
|
|
189
|
-
});
|
|
190
|
-
spec.definition.steps = steps;
|
|
191
|
-
spec.definition.transitions = dedupeTransitions(transitions);
|
|
192
|
-
spec.metadata = mergeRecords(spec.metadata, extension.metadata);
|
|
193
|
-
spec.annotations = mergeRecords(spec.annotations, extension.annotations);
|
|
194
|
-
const issues = validateWorkflowSpec(spec);
|
|
195
|
-
const blockingIssues = issues.filter((issue) => issue.level === "error");
|
|
196
|
-
if (blockingIssues.length > 0) {
|
|
197
|
-
throw new Error(`Invalid composed workflow ${spec.meta.key}.v${spec.meta.version}: ${blockingIssues.map((issue) => issue.message).join("; ")}`);
|
|
198
|
-
}
|
|
199
|
-
return spec;
|
|
200
|
-
}
|
|
201
|
-
function insertStep(steps, injection) {
|
|
202
|
-
const anchorIndex = resolveAnchorIndex(steps, injection);
|
|
203
|
-
if (anchorIndex === -1) {
|
|
204
|
-
throw new Error(`Unable to place injected step "${injection.inject.id}"`);
|
|
205
|
-
}
|
|
206
|
-
steps.splice(anchorIndex, 0, { ...injection.inject });
|
|
207
|
-
}
|
|
208
|
-
function resolveAnchorIndex(steps, injection) {
|
|
209
|
-
if (injection.after) {
|
|
210
|
-
const idx = steps.findIndex((step) => step.id === injection.after);
|
|
211
|
-
return idx === -1 ? -1 : idx + 1;
|
|
212
|
-
}
|
|
213
|
-
if (injection.before) {
|
|
214
|
-
const idx = steps.findIndex((step) => step.id === injection.before);
|
|
215
|
-
return idx === -1 ? -1 : idx;
|
|
216
|
-
}
|
|
217
|
-
return steps.length;
|
|
218
|
-
}
|
|
219
|
-
function wireTransitions(transitions, injection) {
|
|
220
|
-
if (!injection.inject.id)
|
|
221
|
-
return;
|
|
222
|
-
if (injection.transitionFrom) {
|
|
223
|
-
transitions.push({
|
|
224
|
-
from: injection.transitionFrom,
|
|
225
|
-
to: injection.inject.id,
|
|
226
|
-
condition: injection.when
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
if (injection.transitionTo) {
|
|
230
|
-
transitions.push({
|
|
231
|
-
from: injection.inject.id,
|
|
232
|
-
to: injection.transitionTo,
|
|
233
|
-
condition: injection.when
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
function dedupeTransitions(transitions) {
|
|
238
|
-
const seen = new Set;
|
|
239
|
-
const result = [];
|
|
240
|
-
transitions.forEach((transition) => {
|
|
241
|
-
const key = `${transition.from}->${transition.to}:${transition.condition ?? ""}`;
|
|
242
|
-
if (seen.has(key))
|
|
243
|
-
return;
|
|
244
|
-
seen.add(key);
|
|
245
|
-
result.push(transition);
|
|
246
|
-
});
|
|
247
|
-
return result;
|
|
248
|
-
}
|
|
249
|
-
function cloneWorkflowSpec(spec) {
|
|
250
|
-
return JSON.parse(JSON.stringify(spec));
|
|
251
|
-
}
|
|
252
|
-
function mergeRecords(base, patch) {
|
|
253
|
-
if (!base && !patch) {
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
return {
|
|
257
|
-
...base ?? {},
|
|
258
|
-
...patch ?? {}
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// src/merger.ts
|
|
263
|
-
function mergeExtensions(extensions) {
|
|
264
|
-
return [...extensions].sort((a, b) => {
|
|
265
|
-
const priorityOrder = (a.priority ?? 0) - (b.priority ?? 0);
|
|
266
|
-
if (priorityOrder !== 0)
|
|
267
|
-
return priorityOrder;
|
|
268
|
-
const workflowOrder = a.workflow.localeCompare(b.workflow);
|
|
269
|
-
if (workflowOrder !== 0)
|
|
270
|
-
return workflowOrder;
|
|
271
|
-
const tenantOrder = (a.tenantId ?? "").localeCompare(b.tenantId ?? "");
|
|
272
|
-
if (tenantOrder !== 0)
|
|
273
|
-
return tenantOrder;
|
|
274
|
-
const roleOrder = (a.role ?? "").localeCompare(b.role ?? "");
|
|
275
|
-
if (roleOrder !== 0)
|
|
276
|
-
return roleOrder;
|
|
277
|
-
const deviceOrder = (a.device ?? "").localeCompare(b.device ?? "");
|
|
278
|
-
if (deviceOrder !== 0)
|
|
279
|
-
return deviceOrder;
|
|
280
|
-
return (a.baseVersion ?? "").localeCompare(b.baseVersion ?? "");
|
|
281
|
-
});
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// src/composer.ts
|
|
285
|
-
class WorkflowComposer {
|
|
286
|
-
extensions = [];
|
|
287
|
-
register(extension) {
|
|
288
|
-
this.extensions.push(extension);
|
|
289
|
-
return this;
|
|
290
|
-
}
|
|
291
|
-
registerMany(extensions) {
|
|
292
|
-
extensions.forEach((extension) => {
|
|
293
|
-
this.register(extension);
|
|
294
|
-
});
|
|
295
|
-
return this;
|
|
296
|
-
}
|
|
297
|
-
compose(params) {
|
|
298
|
-
const applicable = mergeExtensions(this.extensions.filter((extension) => matches(params, extension)));
|
|
299
|
-
return applicable.reduce((acc, extension) => applyWorkflowExtension(acc, extension), params.base);
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
function matches(params, extension) {
|
|
303
|
-
if (extension.workflow !== params.base.meta.key)
|
|
304
|
-
return false;
|
|
305
|
-
if (extension.baseVersion && !satisfies(params.base.meta.version, extension.baseVersion)) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
if (extension.tenantId && extension.tenantId !== params.tenantId) {
|
|
309
|
-
return false;
|
|
310
|
-
}
|
|
311
|
-
if (extension.role && extension.role !== params.role) {
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
if (extension.device && extension.device !== params.device) {
|
|
315
|
-
return false;
|
|
316
|
-
}
|
|
317
|
-
return true;
|
|
318
|
-
}
|
|
319
|
-
// src/templates.ts
|
|
320
|
-
function approvalStepTemplate(options) {
|
|
321
|
-
return {
|
|
322
|
-
id: options.id,
|
|
323
|
-
label: options.label,
|
|
324
|
-
type: options.type ?? "human",
|
|
325
|
-
description: options.description ?? "Tenant-specific approval",
|
|
326
|
-
action: options.action,
|
|
327
|
-
guard: options.guardExpression ? {
|
|
328
|
-
type: "expression",
|
|
329
|
-
value: options.guardExpression
|
|
330
|
-
} : undefined
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
export {
|
|
334
|
-
validateExtension,
|
|
335
|
-
mergeExtensions,
|
|
336
|
-
approvalStepTemplate,
|
|
337
|
-
applyWorkflowExtension,
|
|
338
|
-
WorkflowComposer
|
|
339
|
-
};
|
|
1
|
+
import{satisfies as W}from"compare-versions";import{validateWorkflowSpec as i}from"@contractspec/lib.contracts-spec/workflow";function d(o,e){let r=[],s=new Set(e.definition.steps.map((t)=>t.id)),a=new Set(o.hiddenSteps??[]),f=new Set(s),m=e.definition.transitions.filter((t)=>f.has(t.from)&&f.has(t.to)).map((t)=>({...t}));if(!o.workflow.trim())r.push({code:"workflow.extension.workflow",message:"workflow is required"});if(o.workflow!==e.meta.key)r.push({code:"workflow.extension.workflow.mismatch",message:`extension targets "${o.workflow}" but base workflow is "${e.meta.key}"`});a.forEach((t)=>{if(!s.has(t)){r.push({code:"workflow.extension.hidden-step",message:`hidden step "${t}" does not exist`});return}f.delete(t)}),c(m,a);let w=new Set([...s].filter((t)=>!a.has(t)));o.customSteps?.forEach((t,p)=>{let u=t.inject.id?.trim();if(!u){r.push({code:"workflow.extension.step.id",message:`customSteps[${p}] is missing an id`});return}if(t.id&&t.id!==u)r.push({code:"workflow.extension.step.id-mismatch",message:`customSteps[${p}] has mismatched id (id="${t.id}", inject.id="${u}")`});if(w.has(u))r.push({code:"workflow.extension.step.id.duplicate",message:`customSteps[${p}] injects duplicate step id "${u}"`});if(t.after&&t.before)r.push({code:"workflow.extension.step.anchor.multiple",message:`customSteps[${p}] cannot set both after and before`});if(!t.after&&!t.before)r.push({code:"workflow.extension.step.anchor",message:`customSteps[${p}] must set after or before`});if(t.after&&!w.has(t.after))r.push({code:"workflow.extension.step.after",message:`customSteps[${p}] references unknown step "${t.after}"`});if(t.before&&!w.has(t.before))r.push({code:"workflow.extension.step.before",message:`customSteps[${p}] references unknown step "${t.before}"`});if(t.transitionFrom&&!w.has(t.transitionFrom))r.push({code:"workflow.extension.step.transition-from",message:`customSteps[${p}] references unknown transitionFrom step "${t.transitionFrom}"`});if(t.transitionTo&&!w.has(t.transitionTo))r.push({code:"workflow.extension.step.transition-to",message:`customSteps[${p}] references unknown transitionTo step "${t.transitionTo}"`});if(w.add(u),f.add(u),t.transitionFrom)m.push({from:t.transitionFrom,to:u,condition:t.when});if(t.transitionTo)m.push({from:u,to:t.transitionTo,condition:t.when})});let l=e.definition.entryStepId??e.definition.steps[0]?.id;if(l&&a.has(l))r.push({code:"workflow.extension.hidden-step.entry",message:`hiddenSteps removes the entry step "${l}"`});if(l&&!a.has(l)){let t=g(l,m);for(let p of f)if(!t.has(p))r.push({code:"workflow.extension.hidden-step.orphan",message:`extension leaves step "${p}" unreachable from entry step "${l}"`})}if(r.length){let t=r.map((p)=>`${p.code}: ${p.message}`).join("; ");throw Error(`Invalid workflow extension for ${o.workflow}: ${t}`)}}function c(o,e){for(let r=o.length-1;r>=0;r-=1){let s=o[r];if(!s)continue;if(e.has(s.from)||e.has(s.to))o.splice(r,1)}}function g(o,e){let r=new Map;for(let f of e){let m=r.get(f.from)??[];m.push(f.to),r.set(f.from,m)}let s=new Set,a=[o];while(a.length>0){let f=a.shift();if(!f||s.has(f))continue;s.add(f);for(let m of r.get(f)??[])a.push(m)}return s}function h(o,e){d(e,o);let r=v(o),s=[...r.definition.steps],a=[...r.definition.transitions],f=new Set(e.hiddenSteps??[]);if(f.forEach((l)=>{let t=s.findIndex((p)=>p.id===l);if(t!==-1)s.splice(t,1)}),f.size)a=a.filter((l)=>!f.has(l.from)&&!f.has(l.to));e.customSteps?.forEach((l)=>{S(s,l),$(a,l)}),r.definition.steps=s,r.definition.transitions=E(a),r.metadata=n(r.metadata,e.metadata),r.annotations=n(r.annotations,e.annotations);let w=i(r).filter((l)=>l.level==="error");if(w.length>0)throw Error(`Invalid composed workflow ${r.meta.key}.v${r.meta.version}: ${w.map((l)=>l.message).join("; ")}`);return r}function S(o,e){let r=y(o,e);if(r===-1)throw Error(`Unable to place injected step "${e.inject.id}"`);o.splice(r,0,{...e.inject})}function y(o,e){if(e.after){let r=o.findIndex((s)=>s.id===e.after);return r===-1?-1:r+1}if(e.before){let r=o.findIndex((s)=>s.id===e.before);return r===-1?-1:r}return o.length}function $(o,e){if(!e.inject.id)return;if(e.transitionFrom)o.push({from:e.transitionFrom,to:e.inject.id,condition:e.when});if(e.transitionTo)o.push({from:e.inject.id,to:e.transitionTo,condition:e.when})}function E(o){let e=new Set,r=[];return o.forEach((s)=>{let a=`${s.from}->${s.to}:${s.condition??""}`;if(e.has(a))return;e.add(a),r.push(s)}),r}function v(o){return JSON.parse(JSON.stringify(o))}function n(o,e){if(!o&&!e)return;return{...o??{},...e??{}}}function k(o){return[...o].sort((e,r)=>{let s=(e.priority??0)-(r.priority??0);if(s!==0)return s;let a=e.workflow.localeCompare(r.workflow);if(a!==0)return a;let f=(e.tenantId??"").localeCompare(r.tenantId??"");if(f!==0)return f;let m=(e.role??"").localeCompare(r.role??"");if(m!==0)return m;let w=(e.device??"").localeCompare(r.device??"");if(w!==0)return w;return(e.baseVersion??"").localeCompare(r.baseVersion??"")})}class x{extensions=[];register(o){return this.extensions.push(o),this}registerMany(o){return o.forEach((e)=>{this.register(e)}),this}compose(o){return k(this.extensions.filter((r)=>T(o,r))).reduce((r,s)=>h(r,s),o.base)}}function T(o,e){if(e.workflow!==o.base.meta.key)return!1;if(e.baseVersion&&!W(o.base.meta.version,e.baseVersion))return!1;if(e.tenantId&&e.tenantId!==o.tenantId)return!1;if(e.role&&e.role!==o.role)return!1;if(e.device&&e.device!==o.device)return!1;return!0}function H(o){return{id:o.id,label:o.label,type:o.type??"human",description:o.description??"Tenant-specific approval",action:o.action,guard:o.guardExpression?{type:"expression",value:o.guardExpression}:void 0}}export{d as validateExtension,k as mergeExtensions,H as approvalStepTemplate,h as applyWorkflowExtension,x as WorkflowComposer};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contractspec/lib.workflow-composer",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.18",
|
|
4
4
|
"description": "Tenant-aware workflow composition helpers for ContractSpec.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -24,21 +24,21 @@
|
|
|
24
24
|
"dev": "contractspec-bun-build dev",
|
|
25
25
|
"clean": "rimraf dist .turbo",
|
|
26
26
|
"lint": "bun lint:fix",
|
|
27
|
-
"lint:fix": "biome check --write --unsafe --only=nursery/useSortedClasses . && biome check --write .",
|
|
28
|
-
"lint:check": "biome check .",
|
|
27
|
+
"lint:fix": "node ../../../scripts/biome.cjs check --write --unsafe --only=nursery/useSortedClasses . && node ../../../scripts/biome.cjs check --write .",
|
|
28
|
+
"lint:check": "node ../../../scripts/biome.cjs check .",
|
|
29
29
|
"test": "bun test --pass-with-no-tests",
|
|
30
30
|
"prebuild": "contractspec-bun-build prebuild",
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@contractspec/lib.contracts-spec": "5.0
|
|
35
|
-
"@contractspec/lib.ai-providers": "3.7.
|
|
34
|
+
"@contractspec/lib.contracts-spec": "5.2.0",
|
|
35
|
+
"@contractspec/lib.ai-providers": "3.7.13",
|
|
36
36
|
"compare-versions": "^6.1.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@contractspec/tool.typescript": "3.7.
|
|
39
|
+
"@contractspec/tool.typescript": "3.7.13",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"@contractspec/tool.bun": "3.7.
|
|
41
|
+
"@contractspec/tool.bun": "3.7.14"
|
|
42
42
|
},
|
|
43
43
|
"exports": {
|
|
44
44
|
".": {
|