@contractspec/lib.workflow-composer 3.7.10 → 3.7.12
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 +7 -0
- package/dist/browser/index.js +64 -1
- package/dist/index.js +64 -1
- package/dist/node/index.js +64 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -23,6 +23,13 @@ or
|
|
|
23
23
|
|
|
24
24
|
Import the root entrypoint from `@contractspec/lib.workflow-composer`, or choose a documented subpath when you only need one part of the package surface.
|
|
25
25
|
|
|
26
|
+
Workflow composition is deterministic by default:
|
|
27
|
+
|
|
28
|
+
- Extensions are applied in stable priority order.
|
|
29
|
+
- Duplicate injected step ids and invalid anchor combinations are rejected.
|
|
30
|
+
- Hidden-step overlays are validated so they cannot orphan the remaining graph.
|
|
31
|
+
- Extension `metadata` and `annotations` are merged into the composed workflow output.
|
|
32
|
+
|
|
26
33
|
## Architecture
|
|
27
34
|
|
|
28
35
|
- `src/composer.test.ts` is part of the package's public or composition surface.
|
package/dist/browser/index.js
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
function validateExtension(extension, base) {
|
|
11
11
|
const issues = [];
|
|
12
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 }));
|
|
13
16
|
if (!extension.workflow.trim()) {
|
|
14
17
|
issues.push({
|
|
15
18
|
code: "workflow.extension.workflow",
|
|
@@ -22,15 +25,17 @@ function validateExtension(extension, base) {
|
|
|
22
25
|
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
26
28
|
hiddenSteps.forEach((stepId) => {
|
|
27
29
|
if (!baseStepIds.has(stepId)) {
|
|
28
30
|
issues.push({
|
|
29
31
|
code: "workflow.extension.hidden-step",
|
|
30
32
|
message: `hidden step "${stepId}" does not exist`
|
|
31
33
|
});
|
|
34
|
+
return;
|
|
32
35
|
}
|
|
36
|
+
visibleStepIds.delete(stepId);
|
|
33
37
|
});
|
|
38
|
+
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
34
39
|
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
35
40
|
extension.customSteps?.forEach((injection, idx) => {
|
|
36
41
|
const injectionId = injection.inject.id?.trim();
|
|
@@ -90,6 +95,21 @@ function validateExtension(extension, base) {
|
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
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
|
+
}
|
|
93
113
|
});
|
|
94
114
|
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
95
115
|
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
@@ -98,11 +118,54 @@ function validateExtension(extension, base) {
|
|
|
98
118
|
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
99
119
|
});
|
|
100
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
|
+
}
|
|
101
132
|
if (issues.length) {
|
|
102
133
|
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
103
134
|
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
104
135
|
}
|
|
105
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
|
+
}
|
|
106
169
|
|
|
107
170
|
// src/injector.ts
|
|
108
171
|
function applyWorkflowExtension(base, extension) {
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
function validateExtension(extension, base) {
|
|
12
12
|
const issues = [];
|
|
13
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 }));
|
|
14
17
|
if (!extension.workflow.trim()) {
|
|
15
18
|
issues.push({
|
|
16
19
|
code: "workflow.extension.workflow",
|
|
@@ -23,15 +26,17 @@ function validateExtension(extension, base) {
|
|
|
23
26
|
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
27
29
|
hiddenSteps.forEach((stepId) => {
|
|
28
30
|
if (!baseStepIds.has(stepId)) {
|
|
29
31
|
issues.push({
|
|
30
32
|
code: "workflow.extension.hidden-step",
|
|
31
33
|
message: `hidden step "${stepId}" does not exist`
|
|
32
34
|
});
|
|
35
|
+
return;
|
|
33
36
|
}
|
|
37
|
+
visibleStepIds.delete(stepId);
|
|
34
38
|
});
|
|
39
|
+
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
35
40
|
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
36
41
|
extension.customSteps?.forEach((injection, idx) => {
|
|
37
42
|
const injectionId = injection.inject.id?.trim();
|
|
@@ -91,6 +96,21 @@ function validateExtension(extension, base) {
|
|
|
91
96
|
});
|
|
92
97
|
}
|
|
93
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
|
+
}
|
|
94
114
|
});
|
|
95
115
|
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
96
116
|
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
@@ -99,11 +119,54 @@ function validateExtension(extension, base) {
|
|
|
99
119
|
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
100
120
|
});
|
|
101
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
|
+
}
|
|
102
133
|
if (issues.length) {
|
|
103
134
|
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
104
135
|
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
105
136
|
}
|
|
106
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
|
+
}
|
|
107
170
|
|
|
108
171
|
// src/injector.ts
|
|
109
172
|
function applyWorkflowExtension(base, extension) {
|
package/dist/node/index.js
CHANGED
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
function validateExtension(extension, base) {
|
|
11
11
|
const issues = [];
|
|
12
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 }));
|
|
13
16
|
if (!extension.workflow.trim()) {
|
|
14
17
|
issues.push({
|
|
15
18
|
code: "workflow.extension.workflow",
|
|
@@ -22,15 +25,17 @@ function validateExtension(extension, base) {
|
|
|
22
25
|
message: `extension targets "${extension.workflow}" but base workflow is "${base.meta.key}"`
|
|
23
26
|
});
|
|
24
27
|
}
|
|
25
|
-
const hiddenSteps = new Set(extension.hiddenSteps ?? []);
|
|
26
28
|
hiddenSteps.forEach((stepId) => {
|
|
27
29
|
if (!baseStepIds.has(stepId)) {
|
|
28
30
|
issues.push({
|
|
29
31
|
code: "workflow.extension.hidden-step",
|
|
30
32
|
message: `hidden step "${stepId}" does not exist`
|
|
31
33
|
});
|
|
34
|
+
return;
|
|
32
35
|
}
|
|
36
|
+
visibleStepIds.delete(stepId);
|
|
33
37
|
});
|
|
38
|
+
removeHiddenTransitions(visibleTransitions, hiddenSteps);
|
|
34
39
|
const availableAnchorSteps = new Set([...baseStepIds].filter((stepId) => !hiddenSteps.has(stepId)));
|
|
35
40
|
extension.customSteps?.forEach((injection, idx) => {
|
|
36
41
|
const injectionId = injection.inject.id?.trim();
|
|
@@ -90,6 +95,21 @@ function validateExtension(extension, base) {
|
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
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
|
+
}
|
|
93
113
|
});
|
|
94
114
|
const entryStepId = base.definition.entryStepId ?? base.definition.steps[0]?.id;
|
|
95
115
|
if (entryStepId && hiddenSteps.has(entryStepId)) {
|
|
@@ -98,11 +118,54 @@ function validateExtension(extension, base) {
|
|
|
98
118
|
message: `hiddenSteps removes the entry step "${entryStepId}"`
|
|
99
119
|
});
|
|
100
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
|
+
}
|
|
101
132
|
if (issues.length) {
|
|
102
133
|
const reason = issues.map((issue) => `${issue.code}: ${issue.message}`).join("; ");
|
|
103
134
|
throw new Error(`Invalid workflow extension for ${extension.workflow}: ${reason}`);
|
|
104
135
|
}
|
|
105
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
|
+
}
|
|
106
169
|
|
|
107
170
|
// src/injector.ts
|
|
108
171
|
function applyWorkflowExtension(base, extension) {
|
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.12",
|
|
4
4
|
"description": "Tenant-aware workflow composition helpers for ContractSpec.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"contractspec",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@contractspec/lib.contracts-spec": "
|
|
35
|
-
"@contractspec/lib.ai-providers": "3.7.
|
|
34
|
+
"@contractspec/lib.contracts-spec": "5.0.0",
|
|
35
|
+
"@contractspec/lib.ai-providers": "3.7.9",
|
|
36
36
|
"compare-versions": "^6.1.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@contractspec/tool.typescript": "3.7.
|
|
39
|
+
"@contractspec/tool.typescript": "3.7.9",
|
|
40
40
|
"typescript": "^5.9.3",
|
|
41
|
-
"@contractspec/tool.bun": "3.7.
|
|
41
|
+
"@contractspec/tool.bun": "3.7.9"
|
|
42
42
|
},
|
|
43
43
|
"exports": {
|
|
44
44
|
".": {
|