@gobing-ai/ts-dual-workflow-engine 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,10 +42,11 @@ import {
42
42
  type ActionRunner,
43
43
  } from '@gobing-ai/ts-dual-workflow-engine';
44
44
 
45
- const captureAction: ActionRunner = {
45
+ const captureAction: ActionRunner & { seen: string[] } = {
46
46
  kind: 'capture',
47
+ seen: [],
47
48
  async execute(options) {
48
- console.log(options.message);
49
+ this.seen.push(String(options.message ?? ''));
49
50
  return { ok: true };
50
51
  },
51
52
  };
@@ -74,7 +75,9 @@ const result = await driver.run(
74
75
  { runId: 'approval-1' },
75
76
  );
76
77
 
77
- console.log(result.status, result.finalState);
78
+ result.status; // "done"
79
+ result.finalState; // "done"
80
+ captureAction.seen; // ["approved"]
78
81
  ```
79
82
 
80
83
  The driver persists each state snapshot, phase update, transition, and final run status through the configured persistence adapter.
@@ -105,7 +108,8 @@ const result = await service.run({
105
108
  edges: [{ from: 'start', to: 'done' }],
106
109
  });
107
110
 
108
- console.log(result);
111
+ result.status; // "done"
112
+ result.finalState; // "done"
109
113
  ```
110
114
 
111
115
  The default host includes built-in `note` and `shell` action runners plus an `always` guard. For production systems, register domain-specific runners and keep shell execution explicit.
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAChC,mFAAmF;IACnF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChD;AAED,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAa,GAAG,WAAW,CAGtF;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3G;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAM/D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C,MAAM,WAAW,mBAAmB;IAChC,mFAAmF;IACnF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;CAChD;AAED,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,SAAa,GAAG,WAAW,CAGtF;AAED,yDAAyD;AACzD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,WAAW,CAAC,CAE3G;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAM/D"}
package/dist/config.js CHANGED
@@ -1,10 +1,9 @@
1
- import { loadStructuredConfig } from '@gobing-ai/ts-runtime';
2
- import { parse } from 'yaml';
1
+ import { loadStructuredConfig, parseYamlObject } from '@gobing-ai/ts-runtime';
3
2
  import { WorkflowValidationError } from './errors.js';
4
- import { WorkflowDefSchema } from './schema.js';
3
+ import { StateMachineWorkflowDefSchema, TransitionFlowWorkflowDefSchema } from './schema.js';
5
4
  /** Load a workflow definition from YAML or JSON text. */
6
5
  export function loadWorkflowDefFromText(text, source = '<inline>') {
7
- const parsed = source.endsWith('.json') ? JSON.parse(text) : parse(text);
6
+ const parsed = source.endsWith('.json') ? JSON.parse(text) : parseYamlObject(text);
8
7
  return parseWorkflowDef(parsed, source);
9
8
  }
10
9
  /** Load a workflow definition from a filesystem path. */
@@ -21,34 +20,184 @@ export function validateWorkflowDef(workflow) {
21
20
  }
22
21
  }
23
22
  function validateStateMachine(workflow) {
24
- const states = new Set(workflow.states.map((state) => state.id));
23
+ const errors = [];
24
+ const ids = workflow.states.map((state) => state.id);
25
+ const states = new Set(ids);
26
+ const terminals = new Set(workflow.terminalStates ?? []);
27
+ // Duplicate state ids.
28
+ for (const id of duplicates(ids))
29
+ errors.push(`State "${id}" is declared more than once`);
30
+ // Initial state must be declared and must not be terminal.
25
31
  if (!states.has(workflow.initialState)) {
26
- throw new WorkflowValidationError(`Initial state "${workflow.initialState}" is not declared`);
32
+ errors.push(`Initial state "${workflow.initialState}" is not declared`);
27
33
  }
34
+ else if (terminals.has(workflow.initialState)) {
35
+ errors.push(`Initial state "${workflow.initialState}" must not be a terminal state`);
36
+ }
37
+ // Terminal states must be declared.
38
+ for (const terminal of terminals) {
39
+ if (!states.has(terminal))
40
+ errors.push(`Terminal state "${terminal}" is not declared`);
41
+ }
42
+ const outboundByState = new Map();
28
43
  for (const transition of workflow.transitions) {
29
44
  if (!states.has(transition.from))
30
- throw new WorkflowValidationError(`Transition source "${transition.from}" is not declared`);
45
+ errors.push(`Transition source "${transition.from}" is not declared`);
31
46
  if (!states.has(transition.to))
32
- throw new WorkflowValidationError(`Transition target "${transition.to}" is not declared`);
47
+ errors.push(`Transition target "${transition.to}" is not declared`);
48
+ const list = outboundByState.get(transition.from) ?? [];
49
+ list.push(transition);
50
+ outboundByState.set(transition.from, list);
33
51
  }
52
+ // Explicit terminal states must not declare outgoing transitions. Note: a state
53
+ // with NO transitions is treated as an implicit terminal by the driver (it stops
54
+ // there), so a missing transition is NOT an error; only an *explicit* terminal
55
+ // that also declares transitions is contradictory.
56
+ for (const state of workflow.states) {
57
+ const outbound = outboundByState.get(state.id) ?? [];
58
+ if (terminals.has(state.id)) {
59
+ if (outbound.length > 0)
60
+ errors.push(`Terminal state "${state.id}" must not declare transitions`);
61
+ continue;
62
+ }
63
+ // An unguarded transition is unconditional, so anything after it is dead —
64
+ // it must be declared last among a state's outgoing transitions.
65
+ const ungatedIndex = outbound.findIndex((transition) => transition.guard === undefined);
66
+ if (ungatedIndex !== -1 && ungatedIndex < outbound.length - 1) {
67
+ errors.push(`State "${state.id}" has an unguarded transition that is not last; later transitions are unreachable`);
68
+ }
69
+ }
70
+ // Template variable references must resolve against declared vars / env allowlist.
71
+ errors.push(...checkVariableReferences(collectActionOptions(workflow), workflow.vars, workflow.env));
72
+ throwIfErrors(workflow, errors);
34
73
  }
35
74
  function parseWorkflowDef(parsed, source) {
36
- const result = WorkflowDefSchema.safeParse(parsed);
75
+ // Parse against the specific dialect schema (not the union) so errors carry the
76
+ // exact failing field path instead of the union's generic "Invalid input".
77
+ const schema = selectWorkflowSchema(parsed);
78
+ const result = schema.safeParse(parsed);
37
79
  if (!result.success) {
38
- throw new WorkflowValidationError(`Workflow definition failed schema validation for ${source}: ${result.error.issues.map((issue) => issue.message).join('; ')}`, result.error.issues);
80
+ throw new WorkflowValidationError(`Workflow definition failed schema validation for ${source}: ${formatWorkflowIssues(result.error.issues)}`, result.error.issues);
39
81
  }
40
82
  validateWorkflowDef(result.data);
41
83
  return result.data;
42
84
  }
43
- function validateTransitionFlow(workflow) {
44
- const nodes = new Set(workflow.nodes.map((node) => node.id));
45
- if (!nodes.has(workflow.initialNode)) {
46
- throw new WorkflowValidationError(`Initial node "${workflow.initialNode}" is not declared`);
85
+ /** Pick the dialect schema by the shape of the input so diagnostics are field-precise. */
86
+ function selectWorkflowSchema(parsed) {
87
+ if (parsed !== null && typeof parsed === 'object') {
88
+ const value = parsed;
89
+ if (value.kind === 'transition-flow' || 'nodes' in value || 'edges' in value || 'initialNode' in value) {
90
+ return TransitionFlowWorkflowDefSchema;
91
+ }
47
92
  }
93
+ return StateMachineWorkflowDefSchema;
94
+ }
95
+ /** Render Zod issues as `path: message` fragments so the offending field is obvious. */
96
+ function formatWorkflowIssues(issues) {
97
+ return issues
98
+ .map((issue) => {
99
+ const path = issue.path.map(String).join('.');
100
+ return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
101
+ })
102
+ .join('; ');
103
+ }
104
+ function validateTransitionFlow(workflow) {
105
+ const errors = [];
106
+ const ids = workflow.nodes.map((node) => node.id);
107
+ const nodes = new Set(ids);
108
+ // Duplicate node ids.
109
+ for (const id of duplicates(ids))
110
+ errors.push(`Node "${id}" is declared more than once`);
111
+ // Initial node must be declared.
112
+ if (!nodes.has(workflow.initialNode))
113
+ errors.push(`Initial node "${workflow.initialNode}" is not declared`);
114
+ // Edge endpoints must be declared; duplicate from→to edges are ambiguous.
115
+ const seenEdges = new Set();
48
116
  for (const edge of workflow.edges) {
49
117
  if (!nodes.has(edge.from))
50
- throw new WorkflowValidationError(`Edge source "${edge.from}" is not declared`);
118
+ errors.push(`Edge source "${edge.from}" is not declared`);
51
119
  if (!nodes.has(edge.to))
52
- throw new WorkflowValidationError(`Edge target "${edge.to}" is not declared`);
120
+ errors.push(`Edge target "${edge.to}" is not declared`);
121
+ const key = `${edge.from}→${edge.to}`;
122
+ if (seenEdges.has(key))
123
+ errors.push(`Duplicate edge "${key}"`);
124
+ seenEdges.add(key);
125
+ }
126
+ // Template variable references must resolve.
127
+ errors.push(...checkVariableReferences(collectActionOptions(workflow), workflow.vars, workflow.env));
128
+ throwIfErrors(workflow, errors);
129
+ }
130
+ /** Return the ids that appear more than once, in first-seen order. */
131
+ function duplicates(ids) {
132
+ const seen = new Set();
133
+ const dupes = new Set();
134
+ for (const id of ids) {
135
+ if (seen.has(id))
136
+ dupes.add(id);
137
+ seen.add(id);
138
+ }
139
+ return [...dupes];
140
+ }
141
+ /** Gather every action `options` object across a workflow's states/nodes for template scanning. */
142
+ function collectActionOptions(workflow) {
143
+ const optionSets = [];
144
+ const actions = workflow.kind === 'transition-flow'
145
+ ? workflow.nodes.flatMap((node) => (node.action ? [node.action] : []))
146
+ : workflow.states.flatMap((state) => [...(state.onEnter ?? []), ...(state.onExit ?? [])]);
147
+ for (const action of actions) {
148
+ if (action.options)
149
+ optionSets.push(action.options);
150
+ }
151
+ return optionSets;
152
+ }
153
+ /** Reserved template namespaces always available at runtime (not user-declared vars). */
154
+ const RUNTIME_TEMPLATE_NAMESPACES = new Set([
155
+ 'workflow',
156
+ 'runId',
157
+ 'task',
158
+ 'state',
159
+ 'node',
160
+ 'iteration',
161
+ 'run',
162
+ 'runtime',
163
+ ]);
164
+ /**
165
+ * Check `${...}` template references inside action options resolve to something:
166
+ * a declared `vars.X`, an env-allowlisted `env.X`, or a reserved runtime namespace.
167
+ */
168
+ function checkVariableReferences(optionSets, vars, env) {
169
+ const declaredVars = new Set(Object.keys(vars ?? {}));
170
+ const allowedEnv = new Set(env?.allow ?? []);
171
+ const errors = [];
172
+ const reference = /\$\{([^}]+)\}/g;
173
+ for (const options of optionSets) {
174
+ for (const value of Object.values(options)) {
175
+ if (typeof value !== 'string')
176
+ continue;
177
+ for (const match of value.matchAll(reference)) {
178
+ const expr = (match[1] ?? '').trim();
179
+ const [namespace, name] = expr.includes('.') ? expr.split('.', 2) : [expr, undefined];
180
+ if (namespace === 'vars') {
181
+ if (name !== undefined && !declaredVars.has(name)) {
182
+ errors.push(`Unknown variable reference "\${${expr}}" (no var "${name}" declared)`);
183
+ }
184
+ }
185
+ else if (namespace === 'env') {
186
+ if (name !== undefined && !allowedEnv.has(name)) {
187
+ errors.push(`Env reference "\${${expr}}" is not in env.allow`);
188
+ }
189
+ }
190
+ else if (namespace !== undefined && !RUNTIME_TEMPLATE_NAMESPACES.has(namespace)) {
191
+ errors.push(`Unknown template reference "\${${expr}}"`);
192
+ }
193
+ }
194
+ }
195
+ }
196
+ return errors;
197
+ }
198
+ /** Throw a single aggregated validation error when any invariant failed. */
199
+ function throwIfErrors(workflow, errors) {
200
+ if (errors.length > 0) {
201
+ throw new WorkflowValidationError(`Workflow "${workflow.name}" is invalid: ${errors.join('; ')}`, errors);
53
202
  }
54
203
  }
package/dist/schema.d.ts CHANGED
@@ -14,6 +14,8 @@ export declare const StateMachineWorkflowDefSchema: z.ZodObject<{
14
14
  $schema: z.ZodOptional<z.ZodString>;
15
15
  kind: z.ZodOptional<z.ZodLiteral<"state-machine">>;
16
16
  name: z.ZodString;
17
+ version: z.ZodOptional<z.ZodString>;
18
+ description: z.ZodOptional<z.ZodString>;
17
19
  initialState: z.ZodString;
18
20
  terminalStates: z.ZodOptional<z.ZodArray<z.ZodString>>;
19
21
  iterationBound: z.ZodOptional<z.ZodNumber>;
@@ -23,6 +25,7 @@ export declare const StateMachineWorkflowDefSchema: z.ZodObject<{
23
25
  }, z.core.$strip>>;
24
26
  states: z.ZodArray<z.ZodObject<{
25
27
  id: z.ZodString;
28
+ description: z.ZodOptional<z.ZodString>;
26
29
  onEnter: z.ZodOptional<z.ZodArray<z.ZodObject<{
27
30
  kind: z.ZodString;
28
31
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -31,22 +34,25 @@ export declare const StateMachineWorkflowDefSchema: z.ZodObject<{
31
34
  kind: z.ZodString;
32
35
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
33
36
  }, z.core.$strip>>>;
34
- }, z.core.$strip>>;
37
+ }, z.core.$strict>>;
35
38
  transitions: z.ZodArray<z.ZodObject<{
36
39
  from: z.ZodString;
37
40
  to: z.ZodString;
41
+ description: z.ZodOptional<z.ZodString>;
38
42
  trigger: z.ZodOptional<z.ZodString>;
39
43
  guard: z.ZodOptional<z.ZodObject<{
40
44
  kind: z.ZodString;
41
45
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
42
46
  }, z.core.$strip>>;
43
- }, z.core.$strip>>;
44
- }, z.core.$strip>;
47
+ }, z.core.$strict>>;
48
+ }, z.core.$strict>;
45
49
  /** Zod schema for transition-flow workflow definitions. */
46
50
  export declare const TransitionFlowWorkflowDefSchema: z.ZodObject<{
47
51
  $schema: z.ZodOptional<z.ZodString>;
48
52
  kind: z.ZodLiteral<"transition-flow">;
49
53
  name: z.ZodString;
54
+ version: z.ZodOptional<z.ZodString>;
55
+ description: z.ZodOptional<z.ZodString>;
50
56
  initialNode: z.ZodString;
51
57
  terminalNodes: z.ZodOptional<z.ZodArray<z.ZodString>>;
52
58
  iterationBound: z.ZodOptional<z.ZodNumber>;
@@ -56,6 +62,7 @@ export declare const TransitionFlowWorkflowDefSchema: z.ZodObject<{
56
62
  }, z.core.$strip>>;
57
63
  nodes: z.ZodArray<z.ZodObject<{
58
64
  id: z.ZodString;
65
+ description: z.ZodOptional<z.ZodString>;
59
66
  type: z.ZodOptional<z.ZodEnum<{
60
67
  action: "action";
61
68
  gate: "gate";
@@ -66,21 +73,24 @@ export declare const TransitionFlowWorkflowDefSchema: z.ZodObject<{
66
73
  kind: z.ZodString;
67
74
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
68
75
  }, z.core.$strip>>;
69
- }, z.core.$strip>>;
76
+ }, z.core.$strict>>;
70
77
  edges: z.ZodArray<z.ZodObject<{
71
78
  from: z.ZodString;
72
79
  to: z.ZodString;
80
+ description: z.ZodOptional<z.ZodString>;
73
81
  condition: z.ZodOptional<z.ZodObject<{
74
82
  kind: z.ZodString;
75
83
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
76
84
  }, z.core.$strip>>;
77
- }, z.core.$strip>>;
78
- }, z.core.$strip>;
85
+ }, z.core.$strict>>;
86
+ }, z.core.$strict>;
79
87
  /** Zod schema for either supported workflow definition shape. */
80
88
  export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
81
89
  $schema: z.ZodOptional<z.ZodString>;
82
90
  kind: z.ZodOptional<z.ZodLiteral<"state-machine">>;
83
91
  name: z.ZodString;
92
+ version: z.ZodOptional<z.ZodString>;
93
+ description: z.ZodOptional<z.ZodString>;
84
94
  initialState: z.ZodString;
85
95
  terminalStates: z.ZodOptional<z.ZodArray<z.ZodString>>;
86
96
  iterationBound: z.ZodOptional<z.ZodNumber>;
@@ -90,6 +100,7 @@ export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
90
100
  }, z.core.$strip>>;
91
101
  states: z.ZodArray<z.ZodObject<{
92
102
  id: z.ZodString;
103
+ description: z.ZodOptional<z.ZodString>;
93
104
  onEnter: z.ZodOptional<z.ZodArray<z.ZodObject<{
94
105
  kind: z.ZodString;
95
106
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -98,20 +109,23 @@ export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
98
109
  kind: z.ZodString;
99
110
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
100
111
  }, z.core.$strip>>>;
101
- }, z.core.$strip>>;
112
+ }, z.core.$strict>>;
102
113
  transitions: z.ZodArray<z.ZodObject<{
103
114
  from: z.ZodString;
104
115
  to: z.ZodString;
116
+ description: z.ZodOptional<z.ZodString>;
105
117
  trigger: z.ZodOptional<z.ZodString>;
106
118
  guard: z.ZodOptional<z.ZodObject<{
107
119
  kind: z.ZodString;
108
120
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
109
121
  }, z.core.$strip>>;
110
- }, z.core.$strip>>;
111
- }, z.core.$strip>, z.ZodObject<{
122
+ }, z.core.$strict>>;
123
+ }, z.core.$strict>, z.ZodObject<{
112
124
  $schema: z.ZodOptional<z.ZodString>;
113
125
  kind: z.ZodLiteral<"transition-flow">;
114
126
  name: z.ZodString;
127
+ version: z.ZodOptional<z.ZodString>;
128
+ description: z.ZodOptional<z.ZodString>;
115
129
  initialNode: z.ZodString;
116
130
  terminalNodes: z.ZodOptional<z.ZodArray<z.ZodString>>;
117
131
  iterationBound: z.ZodOptional<z.ZodNumber>;
@@ -121,6 +135,7 @@ export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
121
135
  }, z.core.$strip>>;
122
136
  nodes: z.ZodArray<z.ZodObject<{
123
137
  id: z.ZodString;
138
+ description: z.ZodOptional<z.ZodString>;
124
139
  type: z.ZodOptional<z.ZodEnum<{
125
140
  action: "action";
126
141
  gate: "gate";
@@ -131,14 +146,15 @@ export declare const WorkflowDefSchema: z.ZodUnion<readonly [z.ZodObject<{
131
146
  kind: z.ZodString;
132
147
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
133
148
  }, z.core.$strip>>;
134
- }, z.core.$strip>>;
149
+ }, z.core.$strict>>;
135
150
  edges: z.ZodArray<z.ZodObject<{
136
151
  from: z.ZodString;
137
152
  to: z.ZodString;
153
+ description: z.ZodOptional<z.ZodString>;
138
154
  condition: z.ZodOptional<z.ZodObject<{
139
155
  kind: z.ZodString;
140
156
  options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
141
157
  }, z.core.$strip>>;
142
- }, z.core.$strip>>;
143
- }, z.core.$strip>]>;
158
+ }, z.core.$strict>>;
159
+ }, z.core.$strict>]>;
144
160
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;iBAG1B,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AAEH,yDAAyD;AACzD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwBxC,CAAC;AAEH,2DAA2D;AAC3D,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuB1C,CAAC;AAEH,iEAAiE;AACjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;mBAA4E,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAyBxB,kDAAkD;AAClD,eAAO,MAAM,eAAe;;;iBAG1B,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,cAAc;;;iBAGzB,CAAC;AAEH,yDAAyD;AACzD,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAmC7B,CAAC;AAEd,2DAA2D;AAC3D,eAAO,MAAM,+BAA+B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAkC/B,CAAC;AAEd,iEAAiE;AACjE,eAAO,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBAA4E,CAAC"}
package/dist/schema.js CHANGED
@@ -1,4 +1,23 @@
1
1
  import { z } from 'zod';
2
+ /** Identifier names reserved for runtime template namespaces; not allowed as user vars. */
3
+ const RESERVED_VAR_NAMES = new Set(['task', 'state', 'node', 'iteration', 'run', 'runtime']);
4
+ /** Valid identifier pattern for variable and env names. */
5
+ const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
6
+ /** User variables: identifier-keyed string map; reserved runtime names are rejected. */
7
+ const VarsSchema = z.record(z.string(), z.string()).superRefine((vars, ctx) => {
8
+ for (const key of Object.keys(vars)) {
9
+ if (!IDENTIFIER.test(key)) {
10
+ ctx.addIssue({ code: 'custom', message: `Invalid variable name "${key}" (must be a valid identifier)` });
11
+ }
12
+ if (RESERVED_VAR_NAMES.has(key)) {
13
+ ctx.addIssue({ code: 'custom', message: `Variable name "${key}" is reserved for runtime use` });
14
+ }
15
+ }
16
+ });
17
+ /** Environment allowlist: identifier-named env vars exposed to templates. */
18
+ const EnvSchema = z.object({
19
+ allow: z.array(z.string().regex(IDENTIFIER, 'env.allow entries must be valid identifiers')).optional(),
20
+ });
2
21
  /** Zod schema for workflow action definitions. */
3
22
  export const ActionDefSchema = z.object({
4
23
  kind: z.string().min(1),
@@ -10,47 +29,69 @@ export const GuardDefSchema = z.object({
10
29
  options: z.record(z.string(), z.unknown()).optional(),
11
30
  });
12
31
  /** Zod schema for state-machine workflow definitions. */
13
- export const StateMachineWorkflowDefSchema = z.object({
32
+ export const StateMachineWorkflowDefSchema = z
33
+ .object({
14
34
  $schema: z.string().optional(),
15
35
  kind: z.literal('state-machine').optional(),
16
36
  name: z.string().min(1),
37
+ // Optional, behavior-free document version tag (accepted for forward/backward compat).
38
+ version: z.string().optional(),
39
+ description: z.string().optional(),
17
40
  initialState: z.string().min(1),
18
41
  terminalStates: z.array(z.string().min(1)).optional(),
19
42
  iterationBound: z.number().int().positive().optional(),
20
- vars: z.record(z.string(), z.string()).optional(),
21
- env: z.object({ allow: z.array(z.string()).optional() }).optional(),
22
- states: z.array(z.object({
43
+ vars: VarsSchema.optional(),
44
+ env: EnvSchema.optional(),
45
+ states: z.array(z
46
+ .object({
23
47
  id: z.string().min(1),
48
+ description: z.string().optional(),
24
49
  onEnter: z.array(ActionDefSchema).optional(),
25
50
  onExit: z.array(ActionDefSchema).optional(),
26
- })),
27
- transitions: z.array(z.object({
51
+ })
52
+ .strict()),
53
+ transitions: z.array(z
54
+ .object({
28
55
  from: z.string().min(1),
29
56
  to: z.string().min(1),
57
+ description: z.string().optional(),
30
58
  trigger: z.string().optional(),
31
59
  guard: GuardDefSchema.optional(),
32
- })),
33
- });
60
+ })
61
+ .strict()),
62
+ })
63
+ .strict();
34
64
  /** Zod schema for transition-flow workflow definitions. */
35
- export const TransitionFlowWorkflowDefSchema = z.object({
65
+ export const TransitionFlowWorkflowDefSchema = z
66
+ .object({
36
67
  $schema: z.string().optional(),
37
68
  kind: z.literal('transition-flow'),
38
69
  name: z.string().min(1),
70
+ // Optional, behavior-free document version tag (accepted for forward/backward compat).
71
+ version: z.string().optional(),
72
+ description: z.string().optional(),
39
73
  initialNode: z.string().min(1),
40
74
  terminalNodes: z.array(z.string().min(1)).optional(),
41
75
  iterationBound: z.number().int().positive().optional(),
42
- vars: z.record(z.string(), z.string()).optional(),
43
- env: z.object({ allow: z.array(z.string()).optional() }).optional(),
44
- nodes: z.array(z.object({
76
+ vars: VarsSchema.optional(),
77
+ env: EnvSchema.optional(),
78
+ nodes: z.array(z
79
+ .object({
45
80
  id: z.string().min(1),
81
+ description: z.string().optional(),
46
82
  type: z.enum(['action', 'gate', 'parallel', 'decision']).optional(),
47
83
  action: ActionDefSchema.optional(),
48
- })),
49
- edges: z.array(z.object({
84
+ })
85
+ .strict()),
86
+ edges: z.array(z
87
+ .object({
50
88
  from: z.string().min(1),
51
89
  to: z.string().min(1),
90
+ description: z.string().optional(),
52
91
  condition: GuardDefSchema.optional(),
53
- })),
54
- });
92
+ })
93
+ .strict()),
94
+ })
95
+ .strict();
55
96
  /** Zod schema for either supported workflow definition shape. */
56
97
  export const WorkflowDefSchema = z.union([StateMachineWorkflowDefSchema, TransitionFlowWorkflowDefSchema]);
@@ -1 +1 @@
1
- {"version":3,"file":"state-machine.d.ts","sourceRoot":"","sources":["../src/state-machine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAGR,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAElB,iBAAiB,EACpB,MAAM,SAAS,CAAC;AAGjB,yDAAyD;AACzD,MAAM,WAAW,yBAAyB;IACtC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACpD;AAED,wEAAwE;AACxE,qBAAa,kBAAkB;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,yBAAyB;IAE/D,6DAA6D;IACvD,GAAG,CAAC,QAAQ,EAAE,uBAAuB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA4G5F,UAAU;YA6BV,IAAI;YAYJ,IAAI;CAYrB"}
1
+ {"version":3,"file":"state-machine.d.ts","sourceRoot":"","sources":["../src/state-machine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAGR,uBAAuB,EACvB,0BAA0B,EAC1B,kBAAkB,EAElB,iBAAiB,EACpB,MAAM,SAAS,CAAC;AAGjB,yDAAyD;AACzD,MAAM,WAAW,yBAAyB;IACtC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACpD;AAED,wEAAwE;AACxE,qBAAa,kBAAkB;IACf,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,yBAAyB;IAE/D,6DAA6D;IACvD,GAAG,CAAC,QAAQ,EAAE,uBAAuB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA8G5F,UAAU;YA8BV,IAAI;YAYJ,IAAI;CAYrB"}
@@ -28,7 +28,7 @@ export class StateMachineDriver {
28
28
  await this.options.persistence.saveWorkflowState(runId, current.id, { transitionsTaken });
29
29
  await this.options.persistence.savePhase(runId, current.id, 'running');
30
30
  // 2. Execute this state's on-enter actions in declaration order.
31
- lastActionResult = await this.runActions(current.onEnter ?? [], workflow.name, current.id, runId, vars, env, options);
31
+ lastActionResult = await this.runActions(current.onEnter ?? [], workflow.name, current.id, runId, vars, env, options, transitionsTaken);
32
32
  if (lastActionResult?.ok === false)
33
33
  return await this.fail(runId, workflow.name, mode, current.id, transitionsTaken, lastActionResult.error);
34
34
  // 3. Stop immediately when an action explicitly declares terminal success.
@@ -51,7 +51,7 @@ export class StateMachineDriver {
51
51
  return await this.fail(runId, workflow.name, mode, current.id, transitionsTaken, 'no-passing-transition');
52
52
  }
53
53
  // 6. Execute this state's on-exit actions before changing state.
54
- const exitResult = await this.runActions(current.onExit ?? [], workflow.name, current.id, runId, vars, env, options);
54
+ const exitResult = await this.runActions(current.onExit ?? [], workflow.name, current.id, runId, vars, env, options, transitionsTaken);
55
55
  if (exitResult?.ok === false)
56
56
  return await this.fail(runId, workflow.name, mode, current.id, transitionsTaken, exitResult.error);
57
57
  // 7. Persist transition and move to the target state.
@@ -66,13 +66,13 @@ export class StateMachineDriver {
66
66
  current = nextState;
67
67
  }
68
68
  }
69
- async runActions(actions, workflowName, stateId, runId, vars, env, options) {
69
+ async runActions(actions, workflowName, stateId, runId, vars, env, options, transitionsTaken) {
70
70
  let last;
71
71
  for (const action of actions) {
72
72
  const resolved = resolveTemplates(action.options ?? {}, {
73
73
  vars,
74
74
  env,
75
- builtins: { workflow: workflowName, state: stateId, runId },
75
+ builtins: runtimeBuiltins(workflowName, stateId, runId, transitionsTaken),
76
76
  });
77
77
  last = await this.options.host.runAction(action.kind, resolved, {
78
78
  runId,
@@ -98,6 +98,19 @@ export class StateMachineDriver {
98
98
  return { runId, workflowName, mode, status: 'failed', finalState, transitionsTaken, reason };
99
99
  }
100
100
  }
101
+ /** Built-in bare template values available to state-machine action options. */
102
+ function runtimeBuiltins(workflowName, stateId, runId, transitionsTaken) {
103
+ return {
104
+ workflow: workflowName,
105
+ runId,
106
+ task: workflowName,
107
+ state: stateId,
108
+ node: stateId,
109
+ iteration: transitionsTaken,
110
+ run: runId,
111
+ runtime: 'state-machine',
112
+ };
113
+ }
101
114
  async function firstPassingTransition(transitions, host, context) {
102
115
  for (const transition of transitions) {
103
116
  if (transition.guard === undefined)
@@ -1 +1 @@
1
- {"version":3,"file":"transition-flow.d.ts","sourceRoot":"","sources":["../src/transition-flow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAER,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,EAElB,iBAAiB,EACpB,MAAM,SAAS,CAAC;AAGjB,2DAA2D;AAC3D,MAAM,WAAW,2BAA2B;IACxC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACpD;AAED,0EAA0E;AAC1E,qBAAa,oBAAoB;IACjB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,2BAA2B;IAEjE,+DAA+D;IACzD,GAAG,CAAC,QAAQ,EAAE,yBAAyB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA8F9F,IAAI;YAYJ,IAAI;CAYrB"}
1
+ {"version":3,"file":"transition-flow.d.ts","sourceRoot":"","sources":["../src/transition-flow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACjD,OAAO,KAAK,EAER,yBAAyB,EACzB,0BAA0B,EAC1B,kBAAkB,EAElB,iBAAiB,EACpB,MAAM,SAAS,CAAC;AAGjB,2DAA2D;AAC3D,MAAM,WAAW,2BAA2B;IACxC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,WAAW,EAAE,0BAA0B,CAAC;CACpD;AAED,0EAA0E;AAC1E,qBAAa,oBAAoB;IACjB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,2BAA2B;IAEjE,+DAA+D;IACzD,GAAG,CAAC,QAAQ,EAAE,yBAAyB,EAAE,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA8F9F,IAAI;YAYJ,IAAI;CAYrB"}
@@ -1,4 +1,5 @@
1
1
  import { getProcessEnv } from '@gobing-ai/ts-runtime';
2
+ import { FSMError } from './errors.js';
2
3
  import { mergeVars, resolveTemplates } from './variables.js';
3
4
  /** Transition-flow workflow driver with an R7 single control function. */
4
5
  export class TransitionFlowDriver {
@@ -21,7 +22,7 @@ export class TransitionFlowDriver {
21
22
  let lastActionResult;
22
23
  const iterationBound = workflow.iterationBound ?? 50;
23
24
  if (current === undefined) {
24
- throw new Error(`Initial node "${workflow.initialNode}" is not declared`);
25
+ throw new FSMError(`Initial node "${workflow.initialNode}" is not declared`);
25
26
  }
26
27
  while (true) {
27
28
  // 1. Persist current node snapshot before action execution.
@@ -32,7 +33,7 @@ export class TransitionFlowDriver {
32
33
  const resolved = resolveTemplates(current.action.options ?? {}, {
33
34
  vars,
34
35
  env,
35
- builtins: { workflow: workflow.name, node: current.id, runId },
36
+ builtins: runtimeBuiltins(workflow.name, current.id, runId, transitionsTaken),
36
37
  });
37
38
  lastActionResult = await this.options.host.runAction(current.action.kind, resolved, {
38
39
  runId,
@@ -74,7 +75,7 @@ export class TransitionFlowDriver {
74
75
  // 7. Move to the target node and repeat.
75
76
  const nextNode = nodes.get(edge.to);
76
77
  if (nextNode === undefined)
77
- throw new Error(`Edge target "${edge.to}" is not declared`);
78
+ throw new FSMError(`Edge target "${edge.to}" is not declared`);
78
79
  current = nextNode;
79
80
  }
80
81
  }
@@ -89,6 +90,19 @@ export class TransitionFlowDriver {
89
90
  return { runId, workflowName, mode, status: 'failed', finalState, transitionsTaken, reason };
90
91
  }
91
92
  }
93
+ /** Built-in bare template values available to transition-flow action options. */
94
+ function runtimeBuiltins(workflowName, nodeId, runId, transitionsTaken) {
95
+ return {
96
+ workflow: workflowName,
97
+ runId,
98
+ task: workflowName,
99
+ state: nodeId,
100
+ node: nodeId,
101
+ iteration: transitionsTaken,
102
+ run: runId,
103
+ runtime: 'transition-flow',
104
+ };
105
+ }
92
106
  async function firstPassingEdge(edges, host, context) {
93
107
  for (const edge of edges) {
94
108
  if (edge.condition === undefined)