@toolproof-npm/shared 0.1.111 → 0.1.112

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.
@@ -1,8 +1,2 @@
1
- import type { TerminalConst } from '../types.js';
2
- import type { ExecutionJson, StatefulStrategyJson, StrategyRunJson, StrategyRunStatusJson, StrategyStateJson } from '@toolproof-npm/schema';
3
- export type GetNewIdentity = (identifiable: TerminalConst) => string | Promise<string>;
4
- export declare function normalizeExecutionInputBindingMap(execution: ExecutionJson, strategyState: StrategyStateJson): ExecutionJson;
5
- export declare function statefulStrategyToStrategyRun(statefulStrategy: StatefulStrategyJson, opts?: {
6
- getNewIdentity: GetNewIdentity;
7
- status?: StrategyRunStatusJson;
8
- }): Promise<StrategyRunJson>;
1
+ import type { StepJson, StrategyStateJson } from '@toolproof-npm/schema';
2
+ export declare function getIndependentThreads(steps: StepJson[], strategyState: StrategyStateJson): StepJson[][];
@@ -1,247 +1,139 @@
1
1
  import { CONSTANTS } from '../../constants.js';
2
- export function normalizeExecutionInputBindingMap(execution, strategyState) {
3
- const inputBindingMap = execution.roleBindings?.inputBindingMap ?? {};
4
- const outputBindingMap = execution.roleBindings?.outputBindingMap ?? {};
5
- const bucket = strategyState?.[execution.identity];
6
- if (!bucket) {
7
- return execution;
8
- }
9
- let changed = false;
10
- const nextInputBindingMap = { ...inputBindingMap };
11
- for (const [roleRef, currentResourceId] of Object.entries(inputBindingMap)) {
12
- const entryIdentity = bucket?.[roleRef]?.identity;
13
- if (typeof entryIdentity === 'string' && entryIdentity.length > 0 && entryIdentity !== currentResourceId) {
14
- nextInputBindingMap[roleRef] = entryIdentity;
15
- changed = true;
2
+ export function getIndependentThreads(steps, strategyState) {
3
+ const getOwnerLabel = (ownerIndex) => {
4
+ const step = steps[ownerIndex];
5
+ return `steps[${ownerIndex}] kind=${step?.kind ?? 'unknown'}`;
6
+ };
7
+ // Collect all executions (including macro-nested WorkSteps) and map them back to their
8
+ // owning top-level step index. We keep macro steps atomic at the outer step level.
9
+ const executionIdToOwner = new Map();
10
+ const ownerToExecutionIds = new Map();
11
+ const executionById = new Map();
12
+ const addExecution = (execution, ownerIndex) => {
13
+ if (!execution?.identity)
14
+ return;
15
+ const existingOwner = executionIdToOwner.get(execution.identity);
16
+ if (existingOwner !== undefined) {
17
+ throw new Error(`Duplicate execution.identity '${execution.identity}' found in ${getOwnerLabel(ownerIndex)} and ${getOwnerLabel(existingOwner)}`);
16
18
  }
17
- }
18
- if (!changed) {
19
- return execution;
20
- }
21
- return {
22
- ...execution,
23
- roleBindings: {
24
- inputBindingMap: nextInputBindingMap,
25
- outputBindingMap,
26
- },
19
+ executionIdToOwner.set(execution.identity, ownerIndex);
20
+ executionById.set(execution.identity, execution);
21
+ const bucket = ownerToExecutionIds.get(ownerIndex) ?? [];
22
+ bucket.push(execution.identity);
23
+ ownerToExecutionIds.set(ownerIndex, bucket);
27
24
  };
28
- }
29
- function normalizeStepRoleBindings(step, strategyState) {
30
- if (step.kind === CONSTANTS.STEPS.work) {
31
- const work = step;
32
- if (!work.execution)
33
- return step;
34
- const nextExecution = normalizeExecutionInputBindingMap(work.execution, strategyState);
35
- if (nextExecution === work.execution)
36
- return step;
37
- return {
38
- ...work,
39
- execution: nextExecution,
40
- };
41
- }
42
- if (step.kind === CONSTANTS.STEPS.for) {
43
- const loop = step;
44
- const whatExecution = loop.case?.what?.execution;
45
- const whenExecution = loop.case?.when?.execution;
46
- const nextWhatExecution = whatExecution
47
- ? normalizeExecutionInputBindingMap(whatExecution, strategyState)
48
- : whatExecution;
49
- const nextWhenExecution = whenExecution
50
- ? normalizeExecutionInputBindingMap(whenExecution, strategyState)
51
- : whenExecution;
52
- if (nextWhatExecution === whatExecution && nextWhenExecution === whenExecution)
53
- return step;
54
- return {
55
- ...loop,
56
- case: {
57
- ...loop.case,
58
- what: loop.case?.what
59
- ? {
60
- ...loop.case.what,
61
- execution: nextWhatExecution,
62
- }
63
- : loop.case?.what,
64
- when: loop.case?.when
65
- ? {
66
- ...loop.case.when,
67
- execution: nextWhenExecution,
68
- }
69
- : loop.case?.when,
70
- },
71
- };
72
- }
73
- if (step.kind === CONSTANTS.STEPS.while) {
74
- const loop = step;
75
- const whatExecution = loop.case?.what?.execution;
76
- const whenExecution = loop.case?.when?.execution;
77
- const nextWhatExecution = whatExecution
78
- ? normalizeExecutionInputBindingMap(whatExecution, strategyState)
79
- : whatExecution;
80
- const nextWhenExecution = whenExecution
81
- ? normalizeExecutionInputBindingMap(whenExecution, strategyState)
82
- : whenExecution;
83
- if (nextWhatExecution === whatExecution && nextWhenExecution === whenExecution)
84
- return step;
85
- return {
86
- ...loop,
87
- case: {
88
- ...loop.case,
89
- what: loop.case?.what
90
- ? {
91
- ...loop.case.what,
92
- execution: nextWhatExecution,
93
- }
94
- : loop.case?.what,
95
- when: loop.case?.when
96
- ? {
97
- ...loop.case.when,
98
- execution: nextWhenExecution,
99
- }
100
- : loop.case?.when,
101
- },
102
- };
103
- }
104
- if (step.kind === CONSTANTS.STEPS.branch) {
105
- const branch = step;
106
- const cases = branch.cases ?? [];
107
- let changed = false;
108
- const nextCases = cases.map((caseItem) => {
109
- const whatExecution = caseItem?.what?.execution;
110
- const whenExecution = caseItem?.when?.execution;
111
- const nextWhatExecution = whatExecution
112
- ? normalizeExecutionInputBindingMap(whatExecution, strategyState)
113
- : whatExecution;
114
- const nextWhenExecution = whenExecution
115
- ? normalizeExecutionInputBindingMap(whenExecution, strategyState)
116
- : whenExecution;
117
- if (nextWhatExecution !== whatExecution || nextWhenExecution !== whenExecution) {
118
- changed = true;
25
+ steps.forEach((step, ownerIndex) => {
26
+ if (step.kind === CONSTANTS.STEPS.work) {
27
+ addExecution(step.execution, ownerIndex);
28
+ return;
29
+ }
30
+ if (step.kind === CONSTANTS.STEPS.for) {
31
+ const loop = step;
32
+ addExecution(loop.case?.what?.execution, ownerIndex);
33
+ addExecution(loop.case?.when?.execution, ownerIndex);
34
+ return;
35
+ }
36
+ if (step.kind === CONSTANTS.STEPS.while) {
37
+ const loop = step;
38
+ addExecution(loop.case?.what?.execution, ownerIndex);
39
+ addExecution(loop.case?.when?.execution, ownerIndex);
40
+ return;
41
+ }
42
+ if (step.kind === CONSTANTS.STEPS.branch) {
43
+ const branch = step;
44
+ for (const caseItem of branch.cases ?? []) {
45
+ addExecution(caseItem?.what?.execution, ownerIndex);
46
+ addExecution(caseItem?.when?.execution, ownerIndex);
119
47
  }
120
- return {
121
- ...caseItem,
122
- what: caseItem?.what
123
- ? {
124
- ...caseItem.what,
125
- execution: nextWhatExecution,
126
- }
127
- : caseItem?.what,
128
- when: caseItem?.when
129
- ? {
130
- ...caseItem.when,
131
- execution: nextWhenExecution,
132
- }
133
- : caseItem?.when,
134
- };
135
- });
136
- if (!changed)
137
- return step;
138
- return {
139
- ...branch,
140
- cases: nextCases,
141
- };
142
- }
143
- return step;
144
- }
145
- function normalizeStatefulStrategyInputBindingMaps(statefulStrategy) {
146
- const steps = statefulStrategy.statelessStrategy?.steps;
147
- if (!steps || steps.length === 0)
148
- return statefulStrategy;
149
- const strategyState = (statefulStrategy.strategyState ?? {});
150
- let changed = false;
151
- const nextSteps = steps.map((step) => {
152
- const nextStep = normalizeStepRoleBindings(step, strategyState);
153
- if (nextStep !== step)
154
- changed = true;
155
- return nextStep;
48
+ return;
49
+ }
156
50
  });
157
- if (!changed)
158
- return statefulStrategy;
159
- return {
160
- ...statefulStrategy,
161
- statelessStrategy: {
162
- ...statefulStrategy.statelessStrategy,
163
- steps: nextSteps,
164
- },
51
+ // Undirected adjacency used for connected components (thread groups).
52
+ const ownerAdj = new Map();
53
+ // Directed dependencies used for strict ordering validation: owner -> producers it depends on.
54
+ const ownerDeps = new Map();
55
+ const ensureOwner = (ownerIndex) => {
56
+ if (!ownerAdj.has(ownerIndex))
57
+ ownerAdj.set(ownerIndex, new Set());
58
+ if (!ownerDeps.has(ownerIndex))
59
+ ownerDeps.set(ownerIndex, new Set());
165
60
  };
166
- }
167
- function getIndependentThreads(steps, strategyState) {
168
- const getStepId = (s, i) => s?.execution?.identity || `step-${i}`;
169
- const stepById = new Map(steps.map((s, i) => [getStepId(s, i), s]));
170
- const adj = new Map(); // undirected graph for components
171
- steps.forEach((step, i) => {
172
- const id = getStepId(step, i);
173
- if (!adj.has(id))
174
- adj.set(id, []);
175
- if (step.kind === CONSTANTS.STEPS.work && step.execution) {
176
- const inputBindingMap = step.execution.roleBindings.inputBindingMap;
177
- for (const inputRoleId of Object.keys(inputBindingMap)) {
178
- const entry = strategyState?.[id]?.[inputRoleId];
179
- if (entry?.kind === 'potential-input') {
180
- const creatorId = entry.creationContext?.executionRef;
181
- if (creatorId && stepById.has(creatorId)) {
182
- // Dependency exists
183
- adj.get(id).push(creatorId);
184
- if (!adj.has(creatorId))
185
- adj.set(creatorId, []);
186
- adj.get(creatorId).push(id);
187
- }
188
- }
61
+ for (let i = 0; i < steps.length; i++) {
62
+ ensureOwner(i);
63
+ }
64
+ // Discover dependencies from strategyState. Note: strategyState is keyed by execution.identity.
65
+ for (const [executionId, ownerIndex] of executionIdToOwner) {
66
+ ensureOwner(ownerIndex);
67
+ const bucket = (strategyState?.[executionId] ?? {});
68
+ const execution = executionById.get(executionId);
69
+ const inputBindingMap = execution?.roleBindings?.inputBindingMap ?? {};
70
+ for (const inputRoleId of Object.keys(inputBindingMap)) {
71
+ const entry = bucket?.[inputRoleId];
72
+ if (entry?.kind !== 'potential-input')
73
+ continue;
74
+ const creatorExecutionRef = entry.creationContext?.executionRef;
75
+ if (!creatorExecutionRef) {
76
+ throw new Error(`Unresolvable potential-input in execution '${executionId}' (${getOwnerLabel(ownerIndex)}): missing creationContext.executionRef for role '${inputRoleId}'`);
189
77
  }
78
+ const producerOwner = executionIdToOwner.get(creatorExecutionRef);
79
+ if (producerOwner === undefined) {
80
+ throw new Error(`Unresolvable potential-input in execution '${executionId}' (${getOwnerLabel(ownerIndex)}): creator executionRef '${creatorExecutionRef}' not found in strategy steps`);
81
+ }
82
+ ensureOwner(producerOwner);
83
+ ownerAdj.get(ownerIndex).add(producerOwner);
84
+ ownerAdj.get(producerOwner).add(ownerIndex);
85
+ ownerDeps.get(ownerIndex).add(producerOwner);
190
86
  }
191
- });
87
+ }
88
+ // Connected components over owners.
192
89
  const visited = new Set();
193
90
  const components = [];
194
- for (const [id] of adj) {
195
- if (!visited.has(id)) {
196
- const component = [];
197
- const queue = [id];
198
- visited.add(id);
199
- while (queue.length > 0) {
200
- const node = queue.shift();
201
- component.push(node);
202
- for (const neighbor of adj.get(node) || []) {
203
- if (!visited.has(neighbor)) {
204
- visited.add(neighbor);
205
- queue.push(neighbor);
206
- }
91
+ for (let ownerIndex = 0; ownerIndex < steps.length; ownerIndex++) {
92
+ if (visited.has(ownerIndex))
93
+ continue;
94
+ const component = [];
95
+ const queue = [ownerIndex];
96
+ visited.add(ownerIndex);
97
+ while (queue.length > 0) {
98
+ const node = queue.shift();
99
+ component.push(node);
100
+ for (const neighbor of ownerAdj.get(node) ?? []) {
101
+ if (!visited.has(neighbor)) {
102
+ visited.add(neighbor);
103
+ queue.push(neighbor);
104
+ }
105
+ }
106
+ }
107
+ components.push(component);
108
+ }
109
+ // Strict validation: within a thread, the linear order must not contain forward refs.
110
+ // The engine does not "wait" for dependencies; a consumer-before-producer will fail.
111
+ for (const comp of components) {
112
+ const compSet = new Set(comp);
113
+ const ownersInOrder = steps.map((_, i) => i).filter((i) => compSet.has(i));
114
+ const position = new Map();
115
+ ownersInOrder.forEach((owner, idx) => position.set(owner, idx));
116
+ for (const consumerOwner of ownersInOrder) {
117
+ const deps = ownerDeps.get(consumerOwner);
118
+ if (!deps || deps.size === 0)
119
+ continue;
120
+ const consumerPos = position.get(consumerOwner);
121
+ for (const producerOwner of deps) {
122
+ const producerPos = position.get(producerOwner);
123
+ if (producerPos === undefined) {
124
+ // Should not happen since deps are within steps, but keep strict.
125
+ throw new Error(`Internal error: dependency producer ${getOwnerLabel(producerOwner)} missing from computed thread for ${getOwnerLabel(consumerOwner)}`);
126
+ }
127
+ if (producerPos >= consumerPos) {
128
+ throw new Error(`Invalid step order in thread: ${getOwnerLabel(consumerOwner)} depends on ${getOwnerLabel(producerOwner)}, but producer is not scheduled before consumer`);
207
129
  }
208
130
  }
209
- components.push(component);
210
131
  }
211
132
  }
212
133
  return components
213
- .map((compIds) => {
214
- return steps.filter((s, i) => {
215
- const sId = getStepId(s, i);
216
- return compIds.includes(sId);
217
- });
134
+ .map((compOwners) => {
135
+ const ownerSet = new Set(compOwners);
136
+ return steps.filter((_, i) => ownerSet.has(i));
218
137
  })
219
138
  .filter((group) => group.length > 0);
220
139
  }
221
- export async function statefulStrategyToStrategyRun(statefulStrategy, opts) {
222
- if (!opts?.getNewIdentity) {
223
- throw new Error('statefulStrategyToStrategyRun: opts.getNewIdentity is required');
224
- }
225
- // Normalize roleBindings.inputBindingMap to reflect the identities present in strategyState.
226
- // This is intentionally a pure transformation (no mutation of the provided statefulStrategy object graph).
227
- const normalizedStatefulStrategy = normalizeStatefulStrategyInputBindingMaps(statefulStrategy);
228
- const strategyRunIdentity = (await opts.getNewIdentity(CONSTANTS.TERMINALS.strategy_run));
229
- const steps = normalizedStatefulStrategy.statelessStrategy?.steps ?? [];
230
- const strategyState = (normalizedStatefulStrategy.strategyState ?? {});
231
- // Algorithm to find what steps can run in parallel
232
- const threadStepsGroups = getIndependentThreads(steps, strategyState);
233
- const strategyThreadMap = {};
234
- for (const group of threadStepsGroups) {
235
- const threadIdentity = await opts.getNewIdentity(CONSTANTS.TERMINALS.strategy_thread);
236
- strategyThreadMap[threadIdentity] = group;
237
- }
238
- return {
239
- identity: strategyRunIdentity,
240
- statefulStrategyRef: normalizedStatefulStrategy.identity,
241
- strategyState: strategyState,
242
- strategyRunContext: {
243
- status: opts.status ?? 'running',
244
- },
245
- strategyThreadMap,
246
- };
247
- }
@@ -1,5 +1,5 @@
1
- import type { TerminalConst } from '../types.js';
2
1
  import type { ExecutionJson, StatefulStrategyJson, StrategyRunJson, StrategyRunStatusJson, StrategyStateJson } from '@toolproof-npm/schema';
2
+ import type { TerminalConst } from '../types.js';
3
3
  export type GetNewIdentity = (identifiable: TerminalConst) => string | Promise<string>;
4
4
  export declare function normalizeExecutionInputBindingMap(execution: ExecutionJson, strategyState: StrategyStateJson): ExecutionJson;
5
5
  export declare function createStrategyRun(statefulStrategy: StatefulStrategyJson, opts?: {
@@ -1,4 +1,5 @@
1
1
  import { CONSTANTS } from '../../constants.js';
2
+ import { getIndependentThreads } from './parallelization.js';
2
3
  export function normalizeExecutionInputBindingMap(execution, strategyState) {
3
4
  const inputBindingMap = execution.roleBindings?.inputBindingMap ?? {};
4
5
  const outputBindingMap = execution.roleBindings?.outputBindingMap ?? {};
@@ -164,60 +165,6 @@ function normalizeStatefulStrategyInputBindingMaps(statefulStrategy) {
164
165
  },
165
166
  };
166
167
  }
167
- function getIndependentThreads(steps, strategyState) {
168
- const getStepId = (s, i) => s?.execution?.identity || `step-${i}`;
169
- const stepById = new Map(steps.map((s, i) => [getStepId(s, i), s]));
170
- const adj = new Map(); // undirected graph for components
171
- steps.forEach((step, i) => {
172
- const id = getStepId(step, i);
173
- if (!adj.has(id))
174
- adj.set(id, []);
175
- if (step.kind === CONSTANTS.STEPS.work && step.execution) {
176
- const inputBindingMap = step.execution.roleBindings.inputBindingMap;
177
- for (const inputRoleId of Object.keys(inputBindingMap)) {
178
- const entry = strategyState?.[id]?.[inputRoleId];
179
- if (entry?.kind === 'potential-input') {
180
- const creatorId = entry.creationContext?.executionRef;
181
- if (creatorId && stepById.has(creatorId)) {
182
- // Dependency exists
183
- adj.get(id).push(creatorId);
184
- if (!adj.has(creatorId))
185
- adj.set(creatorId, []);
186
- adj.get(creatorId).push(id);
187
- }
188
- }
189
- }
190
- }
191
- });
192
- const visited = new Set();
193
- const components = [];
194
- for (const [id] of adj) {
195
- if (!visited.has(id)) {
196
- const component = [];
197
- const queue = [id];
198
- visited.add(id);
199
- while (queue.length > 0) {
200
- const node = queue.shift();
201
- component.push(node);
202
- for (const neighbor of adj.get(node) || []) {
203
- if (!visited.has(neighbor)) {
204
- visited.add(neighbor);
205
- queue.push(neighbor);
206
- }
207
- }
208
- }
209
- components.push(component);
210
- }
211
- }
212
- return components
213
- .map((compIds) => {
214
- return steps.filter((s, i) => {
215
- const sId = getStepId(s, i);
216
- return compIds.includes(sId);
217
- });
218
- })
219
- .filter((group) => group.length > 0);
220
- }
221
168
  export async function createStrategyRun(statefulStrategy, opts) {
222
169
  if (!opts?.getNewIdentity) {
223
170
  throw new Error('statefulStrategyToStrategyRun: opts.getNewIdentity is required');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@toolproof-npm/shared",
3
- "version": "0.1.111",
3
+ "version": "0.1.112",
4
4
  "description": "Core library utilities for ToolProof",
5
5
  "keywords": [
6
6
  "toolproof",