@unlaxer/tramli 1.0.0 → 1.2.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.
@@ -1,5 +1,7 @@
1
1
  import type { FlowDefinition } from './flow-definition.js';
2
2
  import type { FlowKey } from './flow-key.js';
3
+ import type { StateProcessor } from './types.js';
4
+ import type { FlowContext } from './flow-context.js';
3
5
  export interface NodeInfo<S extends string> {
4
6
  name: string;
5
7
  fromState: S;
@@ -25,8 +27,36 @@ export declare class DataFlowGraph<S extends string> {
25
27
  consumersOf(key: FlowKey<unknown>): NodeInfo<S>[];
26
28
  /** Types produced but never required by any downstream processor/guard. */
27
29
  deadData(): Set<string>;
30
+ /** Data lifetime: which states a type is first produced and last consumed. */
31
+ lifetime(key: FlowKey<unknown>): {
32
+ firstProduced: S;
33
+ lastConsumed: S;
34
+ } | null;
35
+ /** Context pruning hints: for each state, types available but not required at that state. */
36
+ pruningHints(): Map<S, Set<string>>;
37
+ /**
38
+ * Check if processor B can replace processor A without breaking data-flow.
39
+ * B is compatible with A if: B requires no more than A, and B produces at least what A produces.
40
+ */
41
+ static isCompatible<S extends string>(a: {
42
+ requires: FlowKey<unknown>[];
43
+ produces: FlowKey<unknown>[];
44
+ }, b: {
45
+ requires: FlowKey<unknown>[];
46
+ produces: FlowKey<unknown>[];
47
+ }): boolean;
48
+ /**
49
+ * Verify a processor's declared requires/produces against actual context usage.
50
+ * Returns list of violations (empty = OK).
51
+ */
52
+ static verifyProcessor<S extends string>(processor: StateProcessor<S>, ctx: FlowContext): Promise<string[]>;
28
53
  /** All type nodes in the graph. */
29
54
  allTypes(): Set<string>;
55
+ /**
56
+ * Assert that a flow instance's context satisfies the data-flow invariant.
57
+ * Returns list of missing type keys (empty = OK).
58
+ */
59
+ assertDataFlow(ctx: FlowContext, currentState: S): string[];
30
60
  /** Generate Mermaid data-flow diagram. */
31
61
  toMermaid(): string;
32
62
  static build<S extends string>(def: FlowDefinition<S>, initiallyAvailable: string[]): DataFlowGraph<S>;
@@ -34,6 +34,95 @@ export class DataFlowGraph {
34
34
  dead.delete(c);
35
35
  return dead;
36
36
  }
37
+ /** Data lifetime: which states a type is first produced and last consumed. */
38
+ lifetime(key) {
39
+ const prods = this._producers.get(key);
40
+ const cons = this._consumers.get(key);
41
+ if (!prods || prods.length === 0)
42
+ return null;
43
+ const firstProduced = prods[0].toState;
44
+ const lastConsumed = cons && cons.length > 0 ? cons[cons.length - 1].fromState : firstProduced;
45
+ return { firstProduced, lastConsumed };
46
+ }
47
+ /** Context pruning hints: for each state, types available but not required at that state. */
48
+ pruningHints() {
49
+ const consumedAt = new Map();
50
+ for (const [typeName, nodes] of this._consumers) {
51
+ for (const node of nodes) {
52
+ if (!consumedAt.has(node.fromState))
53
+ consumedAt.set(node.fromState, new Set());
54
+ consumedAt.get(node.fromState).add(typeName);
55
+ }
56
+ }
57
+ const hints = new Map();
58
+ for (const [state, available] of this._availableAtState) {
59
+ const needed = consumedAt.get(state) ?? new Set();
60
+ const prunable = new Set();
61
+ for (const type of available) {
62
+ if (!needed.has(type))
63
+ prunable.add(type);
64
+ }
65
+ if (prunable.size > 0)
66
+ hints.set(state, prunable);
67
+ }
68
+ return hints;
69
+ }
70
+ /**
71
+ * Check if processor B can replace processor A without breaking data-flow.
72
+ * B is compatible with A if: B requires no more than A, and B produces at least what A produces.
73
+ */
74
+ static isCompatible(a, b) {
75
+ const aReqs = new Set(a.requires);
76
+ const bReqs = new Set(b.requires);
77
+ const aProds = new Set(a.produces);
78
+ const bProds = new Set(b.produces);
79
+ for (const r of bReqs) {
80
+ if (!aReqs.has(r))
81
+ return false;
82
+ }
83
+ for (const p of aProds) {
84
+ if (!bProds.has(p))
85
+ return false;
86
+ }
87
+ return true;
88
+ }
89
+ /**
90
+ * Verify a processor's declared requires/produces against actual context usage.
91
+ * Returns list of violations (empty = OK).
92
+ */
93
+ static async verifyProcessor(processor, ctx) {
94
+ const violations = [];
95
+ for (const req of processor.requires) {
96
+ if (!ctx.has(req))
97
+ violations.push(`requires ${req} but not in context`);
98
+ }
99
+ const beforeKeys = new Set();
100
+ for (const req of processor.requires) {
101
+ if (ctx.has(req))
102
+ beforeKeys.add(req);
103
+ }
104
+ // Capture all existing keys
105
+ const snapshot = ctx.snapshot();
106
+ const existingKeys = new Set(snapshot.keys());
107
+ try {
108
+ await processor.process(ctx);
109
+ }
110
+ catch (e) {
111
+ violations.push(`threw ${e.constructor.name}: ${e.message}`);
112
+ return violations;
113
+ }
114
+ const afterSnapshot = ctx.snapshot();
115
+ for (const prod of processor.produces) {
116
+ if (!afterSnapshot.has(prod))
117
+ violations.push(`declares produces ${prod} but did not put it`);
118
+ }
119
+ for (const [key] of afterSnapshot) {
120
+ if (!existingKeys.has(key) && !processor.produces.includes(key)) {
121
+ violations.push(`put ${key} but did not declare it in produces`);
122
+ }
123
+ }
124
+ return violations;
125
+ }
37
126
  /** All type nodes in the graph. */
38
127
  allTypes() {
39
128
  const types = new Set(this._allProduced);
@@ -41,6 +130,18 @@ export class DataFlowGraph {
41
130
  types.add(c);
42
131
  return types;
43
132
  }
133
+ /**
134
+ * Assert that a flow instance's context satisfies the data-flow invariant.
135
+ * Returns list of missing type keys (empty = OK).
136
+ */
137
+ assertDataFlow(ctx, currentState) {
138
+ const missing = [];
139
+ for (const type of this.availableAt(currentState)) {
140
+ if (!ctx.has(type))
141
+ missing.push(type);
142
+ }
143
+ return missing;
144
+ }
44
145
  /** Generate Mermaid data-flow diagram. */
45
146
  toMermaid() {
46
147
  const lines = ['flowchart LR'];
@@ -47,6 +47,7 @@ export declare class Builder<S extends string> {
47
47
  private checkRequiresProducesFrom;
48
48
  private checkAutoExternalConflict;
49
49
  private checkTerminalNoOutgoing;
50
+ private checkSubFlowExitCompleteness;
50
51
  }
51
52
  export declare class FromBuilder<S extends string> {
52
53
  private readonly builder;
@@ -55,6 +56,16 @@ export declare class FromBuilder<S extends string> {
55
56
  auto(to: S, processor: StateProcessor<S>): Builder<S>;
56
57
  external(to: S, guard: TransitionGuard<S>, processor?: StateProcessor<S>): Builder<S>;
57
58
  branch(branch: BranchProcessor<S>): BranchBuilder<S>;
59
+ subFlow(subFlowDef: FlowDefinition<any>): SubFlowBuilder<S>;
60
+ }
61
+ export declare class SubFlowBuilder<S extends string> {
62
+ private readonly builder;
63
+ private readonly fromState;
64
+ private readonly subFlowDef;
65
+ private readonly exitMap;
66
+ constructor(builder: Builder<S>, fromState: S, subFlowDef: FlowDefinition<any>);
67
+ onExit(terminalName: string, parentState: S): this;
68
+ endSubFlow(): Builder<S>;
58
69
  }
59
70
  export declare class BranchBuilder<S extends string> {
60
71
  private readonly builder;
@@ -118,6 +118,7 @@ export class Builder {
118
118
  this.checkRequiresProduces(def, errors);
119
119
  this.checkAutoExternalConflict(def, errors);
120
120
  this.checkTerminalNoOutgoing(def, errors);
121
+ this.checkSubFlowExitCompleteness(def, errors);
121
122
  if (errors.length > 0) {
122
123
  throw new FlowError('INVALID_FLOW_DEFINITION', `Flow '${this.name}' has ${errors.length} validation error(s):\n - ${errors.join('\n - ')}`);
123
124
  }
@@ -131,6 +132,15 @@ export class Builder {
131
132
  while (queue.length > 0) {
132
133
  const current = queue.shift();
133
134
  for (const t of def.transitionsFrom(current)) {
135
+ if (t.type === 'sub_flow' && t.exitMappings) {
136
+ for (const target of t.exitMappings.values()) {
137
+ if (!visited.has(target)) {
138
+ visited.add(target);
139
+ queue.push(target);
140
+ }
141
+ }
142
+ continue;
143
+ }
134
144
  if (!visited.has(t.to)) {
135
145
  visited.add(t.to);
136
146
  queue.push(t.to);
@@ -163,6 +173,13 @@ export class Builder {
163
173
  return false;
164
174
  visited.add(state);
165
175
  for (const t of def.transitionsFrom(state)) {
176
+ if (t.type === 'sub_flow' && t.exitMappings) {
177
+ for (const target of t.exitMappings.values()) {
178
+ if (this.canReachTerminal(def, target, visited))
179
+ return true;
180
+ }
181
+ continue;
182
+ }
166
183
  if (this.canReachTerminal(def, t.to, visited))
167
184
  return true;
168
185
  }
@@ -300,11 +317,23 @@ export class Builder {
300
317
  }
301
318
  checkTerminalNoOutgoing(def, errors) {
302
319
  for (const t of def.transitions) {
303
- if (def.stateConfig[t.from].terminal) {
320
+ if (def.stateConfig[t.from].terminal && t.type !== 'sub_flow') {
304
321
  errors.push(`Terminal state ${t.from} has an outgoing transition to ${t.to}`);
305
322
  }
306
323
  }
307
324
  }
325
+ checkSubFlowExitCompleteness(def, errors) {
326
+ for (const t of def.transitions) {
327
+ if (t.type !== 'sub_flow' || !t.subFlowDefinition)
328
+ continue;
329
+ const subDef = t.subFlowDefinition;
330
+ for (const terminal of subDef.terminalStates) {
331
+ if (!t.exitMappings?.has(terminal)) {
332
+ errors.push(`SubFlow '${subDef.name}' at ${t.from} has terminal state ${terminal} with no onExit mapping`);
333
+ }
334
+ }
335
+ }
336
+ }
308
337
  }
309
338
  export class FromBuilder {
310
339
  builder;
@@ -330,6 +359,34 @@ export class FromBuilder {
330
359
  branch(branch) {
331
360
  return new BranchBuilder(this.builder, this.fromState, branch);
332
361
  }
362
+ subFlow(subFlowDef) {
363
+ return new SubFlowBuilder(this.builder, this.fromState, subFlowDef);
364
+ }
365
+ }
366
+ export class SubFlowBuilder {
367
+ builder;
368
+ fromState;
369
+ subFlowDef;
370
+ exitMap = new Map();
371
+ constructor(builder, fromState, subFlowDef) {
372
+ this.builder = builder;
373
+ this.fromState = fromState;
374
+ this.subFlowDef = subFlowDef;
375
+ }
376
+ onExit(terminalName, parentState) {
377
+ this.exitMap.set(terminalName, parentState);
378
+ return this;
379
+ }
380
+ endSubFlow() {
381
+ this.builder.addTransition({
382
+ from: this.fromState, to: this.fromState, type: 'sub_flow',
383
+ processor: undefined, guard: undefined, branch: undefined,
384
+ branchTargets: new Map(),
385
+ subFlowDefinition: this.subFlowDef,
386
+ exitMappings: new Map(this.exitMap),
387
+ });
388
+ return this.builder;
389
+ }
333
390
  }
334
391
  export class BranchBuilder {
335
392
  builder;
@@ -1,22 +1,13 @@
1
1
  import type { FlowDefinition } from './flow-definition.js';
2
2
  import { FlowInstance } from './flow-instance.js';
3
3
  import type { InMemoryFlowStore } from './in-memory-flow-store.js';
4
- /**
5
- * Generic engine that drives all flow state machines.
6
- *
7
- * Exceptions:
8
- * - FLOW_NOT_FOUND: resumeAndExecute with unknown or completed flowId
9
- * - INVALID_TRANSITION: resumeAndExecute when no external transition exists
10
- * - MAX_CHAIN_DEPTH: auto-chain exceeded 10 steps
11
- * - EXPIRED: flow TTL exceeded at resumeAndExecute entry
12
- *
13
- * Processor and branch exceptions are caught and routed to error transitions.
14
- */
15
4
  export declare class FlowEngine {
16
5
  private readonly store;
17
6
  constructor(store: InMemoryFlowStore);
18
7
  startFlow<S extends string>(definition: FlowDefinition<S>, sessionId: string, initialData: Map<string, unknown>): Promise<FlowInstance<S>>;
19
8
  resumeAndExecute<S extends string>(flowId: string, definition: FlowDefinition<S>, externalData?: Map<string, unknown>): Promise<FlowInstance<S>>;
20
9
  private executeAutoChain;
10
+ private executeSubFlow;
11
+ private resumeSubFlow;
21
12
  private handleError;
22
13
  }
@@ -2,17 +2,6 @@ import { FlowContext } from './flow-context.js';
2
2
  import { FlowInstance } from './flow-instance.js';
3
3
  import { FlowError } from './flow-error.js';
4
4
  const MAX_CHAIN_DEPTH = 10;
5
- /**
6
- * Generic engine that drives all flow state machines.
7
- *
8
- * Exceptions:
9
- * - FLOW_NOT_FOUND: resumeAndExecute with unknown or completed flowId
10
- * - INVALID_TRANSITION: resumeAndExecute when no external transition exists
11
- * - MAX_CHAIN_DEPTH: auto-chain exceeded 10 steps
12
- * - EXPIRED: flow TTL exceeded at resumeAndExecute entry
13
- *
14
- * Processor and branch exceptions are caught and routed to error transitions.
15
- */
16
5
  export class FlowEngine {
17
6
  store;
18
7
  constructor(store) {
@@ -46,6 +35,10 @@ export class FlowEngine {
46
35
  this.store.save(flow);
47
36
  return flow;
48
37
  }
38
+ // If actively in a sub-flow, delegate resume
39
+ if (flow.activeSubFlow) {
40
+ return this.resumeSubFlow(flow, definition);
41
+ }
49
42
  const currentState = flow.currentState;
50
43
  const transition = definition.externalFrom(currentState);
51
44
  if (!transition)
@@ -108,6 +101,15 @@ export class FlowEngine {
108
101
  break;
109
102
  }
110
103
  const transitions = flow.definition.transitionsFrom(current);
104
+ // Check for sub-flow transition
105
+ const subFlowT = transitions.find(t => t.type === 'sub_flow');
106
+ if (subFlowT) {
107
+ const advanced = await this.executeSubFlow(flow, subFlowT);
108
+ depth += advanced;
109
+ if (advanced === 0)
110
+ break; // sub-flow stopped at external
111
+ continue;
112
+ }
111
113
  const autoOrBranch = transitions.find(t => t.type === 'auto' || t.type === 'branch');
112
114
  if (!autoOrBranch)
113
115
  break;
@@ -145,6 +147,78 @@ export class FlowEngine {
145
147
  if (depth >= MAX_CHAIN_DEPTH)
146
148
  throw FlowError.maxChainDepth();
147
149
  }
150
+ async executeSubFlow(parentFlow, subFlowTransition) {
151
+ const subDef = subFlowTransition.subFlowDefinition;
152
+ const exitMappings = subFlowTransition.exitMappings;
153
+ const subInitial = subDef.initialState;
154
+ const subFlow = new FlowInstance(parentFlow.id, parentFlow.sessionId, subDef, parentFlow.context, subInitial, parentFlow.expiresAt);
155
+ parentFlow.setActiveSubFlow(subFlow);
156
+ await this.executeAutoChain(subFlow);
157
+ if (subFlow.isCompleted) {
158
+ parentFlow.setActiveSubFlow(null);
159
+ const target = exitMappings.get(subFlow.exitState);
160
+ if (target) {
161
+ const from = parentFlow.currentState;
162
+ parentFlow.transitionTo(target);
163
+ this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
164
+ return 1;
165
+ }
166
+ }
167
+ return 0; // sub-flow stopped at external
168
+ }
169
+ async resumeSubFlow(parentFlow, parentDef) {
170
+ const subFlow = parentFlow.activeSubFlow;
171
+ const subDef = subFlow.definition;
172
+ const transition = subDef.externalFrom(subFlow.currentState);
173
+ if (!transition) {
174
+ throw new FlowError('INVALID_TRANSITION', `No external transition from sub-flow state ${subFlow.currentState}`);
175
+ }
176
+ const guard = transition.guard;
177
+ if (guard) {
178
+ const output = await guard.validate(parentFlow.context);
179
+ if (output.type === 'accepted') {
180
+ if (output.data) {
181
+ for (const [key, value] of output.data)
182
+ parentFlow.context.put(key, value);
183
+ }
184
+ subFlow.transitionTo(transition.to);
185
+ this.store.recordTransition(parentFlow.id, subFlow.currentState, transition.to, guard.name, parentFlow.context);
186
+ }
187
+ else if (output.type === 'rejected') {
188
+ subFlow.incrementGuardFailure();
189
+ if (subFlow.guardFailureCount >= subDef.maxGuardRetries) {
190
+ subFlow.complete('ERROR');
191
+ }
192
+ this.store.save(parentFlow);
193
+ return parentFlow;
194
+ }
195
+ else {
196
+ parentFlow.complete('EXPIRED');
197
+ this.store.save(parentFlow);
198
+ return parentFlow;
199
+ }
200
+ }
201
+ else {
202
+ subFlow.transitionTo(transition.to);
203
+ }
204
+ await this.executeAutoChain(subFlow);
205
+ if (subFlow.isCompleted) {
206
+ parentFlow.setActiveSubFlow(null);
207
+ const subFlowT = parentDef.transitionsFrom(parentFlow.currentState)
208
+ .find(t => t.type === 'sub_flow');
209
+ if (subFlowT?.exitMappings) {
210
+ const target = subFlowT.exitMappings.get(subFlow.exitState);
211
+ if (target) {
212
+ const from = parentFlow.currentState;
213
+ parentFlow.transitionTo(target);
214
+ this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
215
+ await this.executeAutoChain(parentFlow);
216
+ }
217
+ }
218
+ }
219
+ this.store.save(parentFlow);
220
+ return parentFlow;
221
+ }
148
222
  handleError(flow, fromState) {
149
223
  const errorTarget = flow.definition.errorTransitions.get(fromState);
150
224
  if (errorTarget) {
@@ -1,6 +1,12 @@
1
1
  export declare class FlowError extends Error {
2
2
  readonly code: string;
3
+ /** Types that were available in context when the error occurred. */
4
+ availableTypes?: Set<string>;
5
+ /** Types that were expected but missing (if applicable). */
6
+ missingTypes?: Set<string>;
3
7
  constructor(code: string, message: string);
8
+ /** Attach context snapshot to this error. */
9
+ withContextSnapshot(available: Set<string>, missing: Set<string>): this;
4
10
  static invalidTransition(from: string, to: string): FlowError;
5
11
  static missingContext(key: string): FlowError;
6
12
  static dagCycle(detail: string): FlowError;
@@ -1,10 +1,20 @@
1
1
  export class FlowError extends Error {
2
2
  code;
3
+ /** Types that were available in context when the error occurred. */
4
+ availableTypes;
5
+ /** Types that were expected but missing (if applicable). */
6
+ missingTypes;
3
7
  constructor(code, message) {
4
8
  super(message);
5
9
  this.code = code;
6
10
  this.name = 'FlowError';
7
11
  }
12
+ /** Attach context snapshot to this error. */
13
+ withContextSnapshot(available, missing) {
14
+ this.availableTypes = available;
15
+ this.missingTypes = missing;
16
+ return this;
17
+ }
8
18
  static invalidTransition(from, to) {
9
19
  return new FlowError('INVALID_TRANSITION', `Invalid transition from ${from} to ${to}`);
10
20
  }
@@ -11,6 +11,7 @@ export declare class FlowInstance<S extends string> {
11
11
  readonly createdAt: Date;
12
12
  readonly expiresAt: Date;
13
13
  private _exitState;
14
+ private _activeSubFlow;
14
15
  constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
15
16
  /**
16
17
  * Restore a FlowInstance from persisted state.
@@ -22,8 +23,10 @@ export declare class FlowInstance<S extends string> {
22
23
  get version(): number;
23
24
  get exitState(): string | null;
24
25
  get isCompleted(): boolean;
26
+ get activeSubFlow(): FlowInstance<any> | null;
25
27
  /** @internal */ transitionTo(state: S): void;
26
28
  /** @internal */ incrementGuardFailure(): void;
27
29
  /** @internal */ complete(exitState: string): void;
28
30
  /** @internal */ setVersion(version: number): void;
31
+ /** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
29
32
  }
@@ -9,6 +9,7 @@ export class FlowInstance {
9
9
  createdAt;
10
10
  expiresAt;
11
11
  _exitState;
12
+ _activeSubFlow = null;
12
13
  constructor(id, sessionId, definition, context, currentState, expiresAt) {
13
14
  this.id = id;
14
15
  this.sessionId = sessionId;
@@ -45,8 +46,10 @@ export class FlowInstance {
45
46
  get version() { return this._version; }
46
47
  get exitState() { return this._exitState; }
47
48
  get isCompleted() { return this._exitState !== null; }
49
+ get activeSubFlow() { return this._activeSubFlow; }
48
50
  /** @internal */ transitionTo(state) { this._currentState = state; }
49
51
  /** @internal */ incrementGuardFailure() { this._guardFailureCount++; }
50
52
  /** @internal */ complete(exitState) { this._exitState = exitState; }
51
53
  /** @internal */ setVersion(version) { this._version = version; }
54
+ /** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
52
55
  }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export { Tramli } from './tramli.js';
2
2
  export { FlowEngine } from './flow-engine.js';
3
3
  export { FlowContext } from './flow-context.js';
4
4
  export { FlowInstance } from './flow-instance.js';
5
- export { FlowDefinition, Builder, FromBuilder, BranchBuilder } from './flow-definition.js';
5
+ export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
6
6
  export { FlowError } from './flow-error.js';
7
7
  export { InMemoryFlowStore } from './in-memory-flow-store.js';
8
8
  export type { TransitionRecord } from './in-memory-flow-store.js';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { Tramli } from './tramli.js';
2
2
  export { FlowEngine } from './flow-engine.js';
3
3
  export { FlowContext } from './flow-context.js';
4
4
  export { FlowInstance } from './flow-instance.js';
5
- export { FlowDefinition, Builder, FromBuilder, BranchBuilder } from './flow-definition.js';
5
+ export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
6
6
  export { FlowError } from './flow-error.js';
7
7
  export { InMemoryFlowStore } from './in-memory-flow-store.js';
8
8
  export { MermaidGenerator } from './mermaid-generator.js';
package/dist/types.d.ts CHANGED
@@ -16,7 +16,7 @@ export type GuardOutput = {
16
16
  type: 'expired';
17
17
  };
18
18
  /** Transition types. */
19
- export type TransitionType = 'auto' | 'external' | 'branch';
19
+ export type TransitionType = 'auto' | 'external' | 'branch' | 'sub_flow';
20
20
  /** A single transition in the flow definition. */
21
21
  export interface Transition<S extends string> {
22
22
  from: S;
@@ -26,6 +26,8 @@ export interface Transition<S extends string> {
26
26
  guard?: TransitionGuard<S>;
27
27
  branch?: BranchProcessor<S>;
28
28
  branchTargets: Map<string, S>;
29
+ subFlowDefinition?: import('./flow-definition.js').FlowDefinition<any>;
30
+ exitMappings?: Map<string, S>;
29
31
  }
30
32
  /**
31
33
  * Processes a state transition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {