@unlaxer/tramli 1.5.2 → 1.6.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.
Files changed (54) hide show
  1. package/dist/cjs/data-flow-graph.d.ts +92 -0
  2. package/dist/cjs/data-flow-graph.js +451 -0
  3. package/dist/cjs/flow-context.d.ts +19 -0
  4. package/dist/cjs/flow-context.js +44 -0
  5. package/dist/cjs/flow-definition.d.ts +85 -0
  6. package/dist/cjs/flow-definition.js +488 -0
  7. package/dist/cjs/flow-engine.d.ts +13 -0
  8. package/dist/cjs/flow-engine.js +244 -0
  9. package/dist/cjs/flow-error.d.ts +14 -0
  10. package/dist/cjs/flow-error.js +34 -0
  11. package/dist/cjs/flow-instance.d.ts +48 -0
  12. package/dist/cjs/flow-instance.js +108 -0
  13. package/dist/cjs/flow-key.d.ts +10 -0
  14. package/dist/cjs/flow-key.js +6 -0
  15. package/dist/cjs/in-memory-flow-store.d.ts +19 -0
  16. package/dist/cjs/in-memory-flow-store.js +27 -0
  17. package/dist/cjs/index.d.ts +16 -0
  18. package/dist/cjs/index.js +29 -0
  19. package/dist/cjs/mermaid-generator.d.ts +9 -0
  20. package/dist/cjs/mermaid-generator.js +80 -0
  21. package/dist/cjs/package.json +1 -0
  22. package/dist/cjs/skeleton-generator.d.ts +10 -0
  23. package/dist/cjs/skeleton-generator.js +48 -0
  24. package/dist/cjs/tramli.d.ts +8 -0
  25. package/dist/cjs/tramli.js +14 -0
  26. package/dist/cjs/types.d.ts +75 -0
  27. package/dist/cjs/types.js +2 -0
  28. package/dist/esm/data-flow-graph.d.ts +92 -0
  29. package/dist/esm/data-flow-graph.js +447 -0
  30. package/dist/esm/flow-context.d.ts +19 -0
  31. package/dist/esm/flow-context.js +40 -0
  32. package/dist/esm/flow-definition.d.ts +85 -0
  33. package/dist/esm/flow-definition.js +480 -0
  34. package/dist/esm/flow-engine.d.ts +13 -0
  35. package/dist/esm/flow-engine.js +240 -0
  36. package/dist/esm/flow-error.d.ts +14 -0
  37. package/dist/esm/flow-error.js +30 -0
  38. package/dist/esm/flow-instance.d.ts +48 -0
  39. package/dist/esm/flow-instance.js +104 -0
  40. package/dist/esm/flow-key.d.ts +10 -0
  41. package/dist/esm/flow-key.js +3 -0
  42. package/dist/esm/in-memory-flow-store.d.ts +19 -0
  43. package/dist/esm/in-memory-flow-store.js +23 -0
  44. package/dist/esm/index.d.ts +16 -0
  45. package/dist/esm/index.js +11 -0
  46. package/dist/esm/mermaid-generator.d.ts +9 -0
  47. package/dist/esm/mermaid-generator.js +76 -0
  48. package/dist/esm/skeleton-generator.d.ts +10 -0
  49. package/dist/esm/skeleton-generator.js +44 -0
  50. package/dist/esm/tramli.d.ts +8 -0
  51. package/dist/esm/tramli.js +10 -0
  52. package/dist/esm/types.d.ts +75 -0
  53. package/dist/esm/types.js +1 -0
  54. package/package.json +8 -4
@@ -0,0 +1,240 @@
1
+ import { FlowContext } from './flow-context.js';
2
+ import { FlowInstance } from './flow-instance.js';
3
+ import { FlowError } from './flow-error.js';
4
+ const MAX_CHAIN_DEPTH = 10;
5
+ export class FlowEngine {
6
+ store;
7
+ constructor(store) {
8
+ this.store = store;
9
+ }
10
+ async startFlow(definition, sessionId, initialData) {
11
+ const flowId = crypto.randomUUID();
12
+ const ctx = new FlowContext(flowId);
13
+ for (const [key, value] of initialData)
14
+ ctx.put(key, value);
15
+ const initial = definition.initialState;
16
+ if (!initial)
17
+ throw new FlowError('INVALID_FLOW_DEFINITION', 'No initial state');
18
+ const expiresAt = new Date(Date.now() + definition.ttl);
19
+ const flow = new FlowInstance(flowId, sessionId, definition, ctx, initial, expiresAt);
20
+ this.store.create(flow);
21
+ await this.executeAutoChain(flow);
22
+ this.store.save(flow);
23
+ return flow;
24
+ }
25
+ async resumeAndExecute(flowId, definition, externalData) {
26
+ const flow = this.store.loadForUpdate(flowId, definition);
27
+ if (!flow)
28
+ throw new FlowError('FLOW_NOT_FOUND', `Flow ${flowId} not found or already completed`);
29
+ if (externalData) {
30
+ for (const [key, value] of externalData)
31
+ flow.context.put(key, value);
32
+ }
33
+ if (new Date() > flow.expiresAt) {
34
+ flow.complete('EXPIRED');
35
+ this.store.save(flow);
36
+ return flow;
37
+ }
38
+ // If actively in a sub-flow, delegate resume
39
+ if (flow.activeSubFlow) {
40
+ return this.resumeSubFlow(flow, definition);
41
+ }
42
+ const currentState = flow.currentState;
43
+ const transition = definition.externalFrom(currentState);
44
+ if (!transition)
45
+ throw FlowError.invalidTransition(currentState, currentState);
46
+ const guard = transition.guard;
47
+ if (guard) {
48
+ const output = await guard.validate(flow.context);
49
+ switch (output.type) {
50
+ case 'accepted': {
51
+ const backup = flow.context.snapshot();
52
+ if (output.data) {
53
+ for (const [key, value] of output.data)
54
+ flow.context.put(key, value);
55
+ }
56
+ try {
57
+ if (transition.processor)
58
+ await transition.processor.process(flow.context);
59
+ const from = flow.currentState;
60
+ flow.transitionTo(transition.to);
61
+ this.store.recordTransition(flow.id, from, transition.to, guard.name, flow.context);
62
+ }
63
+ catch (e) {
64
+ flow.context.restoreFrom(backup);
65
+ this.handleError(flow, currentState, e instanceof Error ? e : new Error(String(e)));
66
+ this.store.save(flow);
67
+ return flow;
68
+ }
69
+ break;
70
+ }
71
+ case 'rejected': {
72
+ flow.incrementGuardFailure();
73
+ if (flow.guardFailureCount >= definition.maxGuardRetries) {
74
+ this.handleError(flow, currentState);
75
+ }
76
+ this.store.save(flow);
77
+ return flow;
78
+ }
79
+ case 'expired': {
80
+ flow.complete('EXPIRED');
81
+ this.store.save(flow);
82
+ return flow;
83
+ }
84
+ }
85
+ }
86
+ else {
87
+ const from = flow.currentState;
88
+ flow.transitionTo(transition.to);
89
+ this.store.recordTransition(flow.id, from, transition.to, 'external', flow.context);
90
+ }
91
+ await this.executeAutoChain(flow);
92
+ this.store.save(flow);
93
+ return flow;
94
+ }
95
+ async executeAutoChain(flow) {
96
+ let depth = 0;
97
+ while (depth < MAX_CHAIN_DEPTH) {
98
+ const current = flow.currentState;
99
+ if (flow.definition.stateConfig[current].terminal) {
100
+ flow.complete(current);
101
+ break;
102
+ }
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
+ }
113
+ const autoOrBranch = transitions.find(t => t.type === 'auto' || t.type === 'branch');
114
+ if (!autoOrBranch)
115
+ break;
116
+ const backup = flow.context.snapshot();
117
+ try {
118
+ if (autoOrBranch.type === 'auto') {
119
+ if (autoOrBranch.processor)
120
+ await autoOrBranch.processor.process(flow.context);
121
+ const from = flow.currentState;
122
+ flow.transitionTo(autoOrBranch.to);
123
+ this.store.recordTransition(flow.id, from, autoOrBranch.to, autoOrBranch.processor?.name ?? 'auto', flow.context);
124
+ }
125
+ else {
126
+ const branch = autoOrBranch.branch;
127
+ const label = await branch.decide(flow.context);
128
+ const target = autoOrBranch.branchTargets.get(label);
129
+ if (!target) {
130
+ throw new FlowError('UNKNOWN_BRANCH', `Branch '${branch.name}' returned unknown label: ${label}`);
131
+ }
132
+ const specific = transitions.find(t => t.type === 'branch' && t.to === target) ?? autoOrBranch;
133
+ if (specific.processor)
134
+ await specific.processor.process(flow.context);
135
+ const from = flow.currentState;
136
+ flow.transitionTo(target);
137
+ this.store.recordTransition(flow.id, from, target, `${branch.name}:${label}`, flow.context);
138
+ }
139
+ }
140
+ catch (e) {
141
+ flow.context.restoreFrom(backup);
142
+ this.handleError(flow, flow.currentState, e instanceof Error ? e : new Error(String(e)));
143
+ return;
144
+ }
145
+ depth++;
146
+ }
147
+ if (depth >= MAX_CHAIN_DEPTH)
148
+ throw FlowError.maxChainDepth();
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
+ // Error bubbling: no exit mapping → fall back to parent's error transitions
167
+ this.handleError(parentFlow, parentFlow.currentState);
168
+ return 1;
169
+ }
170
+ return 0; // sub-flow stopped at external
171
+ }
172
+ async resumeSubFlow(parentFlow, parentDef) {
173
+ const subFlow = parentFlow.activeSubFlow;
174
+ const subDef = subFlow.definition;
175
+ const transition = subDef.externalFrom(subFlow.currentState);
176
+ if (!transition) {
177
+ throw new FlowError('INVALID_TRANSITION', `No external transition from sub-flow state ${subFlow.currentState}`);
178
+ }
179
+ const guard = transition.guard;
180
+ if (guard) {
181
+ const output = await guard.validate(parentFlow.context);
182
+ if (output.type === 'accepted') {
183
+ if (output.data) {
184
+ for (const [key, value] of output.data)
185
+ parentFlow.context.put(key, value);
186
+ }
187
+ subFlow.transitionTo(transition.to);
188
+ this.store.recordTransition(parentFlow.id, subFlow.currentState, transition.to, guard.name, parentFlow.context);
189
+ }
190
+ else if (output.type === 'rejected') {
191
+ subFlow.incrementGuardFailure();
192
+ if (subFlow.guardFailureCount >= subDef.maxGuardRetries) {
193
+ subFlow.complete('ERROR');
194
+ }
195
+ this.store.save(parentFlow);
196
+ return parentFlow;
197
+ }
198
+ else {
199
+ parentFlow.complete('EXPIRED');
200
+ this.store.save(parentFlow);
201
+ return parentFlow;
202
+ }
203
+ }
204
+ else {
205
+ subFlow.transitionTo(transition.to);
206
+ }
207
+ await this.executeAutoChain(subFlow);
208
+ if (subFlow.isCompleted) {
209
+ parentFlow.setActiveSubFlow(null);
210
+ const subFlowT = parentDef.transitionsFrom(parentFlow.currentState)
211
+ .find(t => t.type === 'sub_flow');
212
+ if (subFlowT?.exitMappings) {
213
+ const target = subFlowT.exitMappings.get(subFlow.exitState);
214
+ if (target) {
215
+ const from = parentFlow.currentState;
216
+ parentFlow.transitionTo(target);
217
+ this.store.recordTransition(parentFlow.id, from, target, `subFlow:${subDef.name}/${subFlow.exitState}`, parentFlow.context);
218
+ await this.executeAutoChain(parentFlow);
219
+ }
220
+ }
221
+ }
222
+ this.store.save(parentFlow);
223
+ return parentFlow;
224
+ }
225
+ handleError(flow, fromState, cause) {
226
+ if (cause)
227
+ flow.setLastError(`${cause.constructor.name}: ${cause.message}`);
228
+ const errorTarget = flow.definition.errorTransitions.get(fromState);
229
+ if (errorTarget) {
230
+ const from = flow.currentState;
231
+ flow.transitionTo(errorTarget);
232
+ this.store.recordTransition(flow.id, from, errorTarget, 'error', flow.context);
233
+ if (flow.definition.stateConfig[errorTarget].terminal)
234
+ flow.complete(errorTarget);
235
+ }
236
+ else {
237
+ flow.complete('TERMINAL_ERROR');
238
+ }
239
+ }
240
+ }
@@ -0,0 +1,14 @@
1
+ export declare class FlowError extends Error {
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>;
7
+ constructor(code: string, message: string);
8
+ /** Attach context snapshot to this error. */
9
+ withContextSnapshot(available: Set<string>, missing: Set<string>): this;
10
+ static invalidTransition(from: string, to: string): FlowError;
11
+ static missingContext(key: string): FlowError;
12
+ static dagCycle(detail: string): FlowError;
13
+ static maxChainDepth(): FlowError;
14
+ }
@@ -0,0 +1,30 @@
1
+ export class FlowError extends Error {
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;
7
+ constructor(code, message) {
8
+ super(message);
9
+ this.code = code;
10
+ this.name = 'FlowError';
11
+ }
12
+ /** Attach context snapshot to this error. */
13
+ withContextSnapshot(available, missing) {
14
+ this.availableTypes = available;
15
+ this.missingTypes = missing;
16
+ return this;
17
+ }
18
+ static invalidTransition(from, to) {
19
+ return new FlowError('INVALID_TRANSITION', `Invalid transition from ${from} to ${to}`);
20
+ }
21
+ static missingContext(key) {
22
+ return new FlowError('MISSING_CONTEXT', `Missing context key: ${key}`);
23
+ }
24
+ static dagCycle(detail) {
25
+ return new FlowError('DAG_CYCLE', `Auto/Branch transitions contain a cycle: ${detail}`);
26
+ }
27
+ static maxChainDepth() {
28
+ return new FlowError('MAX_CHAIN_DEPTH', 'Auto-chain exceeded maximum depth of 10');
29
+ }
30
+ }
@@ -0,0 +1,48 @@
1
+ import type { FlowDefinition } from './flow-definition.js';
2
+ import type { FlowContext } from './flow-context.js';
3
+ export declare class FlowInstance<S extends string> {
4
+ readonly id: string;
5
+ readonly sessionId: string;
6
+ readonly definition: FlowDefinition<S>;
7
+ readonly context: FlowContext;
8
+ private _currentState;
9
+ private _guardFailureCount;
10
+ private _version;
11
+ readonly createdAt: Date;
12
+ readonly expiresAt: Date;
13
+ private _exitState;
14
+ private _activeSubFlow;
15
+ private _lastError;
16
+ constructor(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, expiresAt: Date);
17
+ /**
18
+ * Restore a FlowInstance from persisted state.
19
+ * Used by FlowStore implementations to reconstruct instances loaded from storage.
20
+ */
21
+ static restore<S extends string>(id: string, sessionId: string, definition: FlowDefinition<S>, context: FlowContext, currentState: S, createdAt: Date, expiresAt: Date, guardFailureCount: number, version: number, exitState: string | null): FlowInstance<S>;
22
+ get currentState(): S;
23
+ get guardFailureCount(): number;
24
+ get version(): number;
25
+ get exitState(): string | null;
26
+ get isCompleted(): boolean;
27
+ get activeSubFlow(): FlowInstance<any> | null;
28
+ /** Last error message (set when a processor throws and error transition fires). */
29
+ get lastError(): string | null;
30
+ /** State path from root to deepest active sub-flow. */
31
+ statePath(): string[];
32
+ /** State path as slash-separated string. */
33
+ statePathString(): string;
34
+ /** Data types available in context at current state (from data-flow graph). */
35
+ availableData(): Set<string>;
36
+ /** Data types that the next transition requires but are not yet in context. */
37
+ missingFor(): string[];
38
+ /** Types required by the next external transition (including in active sub-flows). */
39
+ waitingFor(): string[];
40
+ /** Return a copy with the given version. For FlowStore optimistic locking. */
41
+ withVersion(newVersion: number): FlowInstance<S>;
42
+ /** @internal */ transitionTo(state: S): void;
43
+ /** @internal */ incrementGuardFailure(): void;
44
+ /** @internal */ complete(exitState: string): void;
45
+ /** @internal */ setVersion(version: number): void;
46
+ /** @internal */ setActiveSubFlow(sub: FlowInstance<any> | null): void;
47
+ /** @internal */ setLastError(error: string | null): void;
48
+ }
@@ -0,0 +1,104 @@
1
+ export class FlowInstance {
2
+ id;
3
+ sessionId;
4
+ definition;
5
+ context;
6
+ _currentState;
7
+ _guardFailureCount;
8
+ _version;
9
+ createdAt;
10
+ expiresAt;
11
+ _exitState;
12
+ _activeSubFlow = null;
13
+ _lastError = null;
14
+ constructor(id, sessionId, definition, context, currentState, expiresAt) {
15
+ this.id = id;
16
+ this.sessionId = sessionId;
17
+ this.definition = definition;
18
+ this.context = context;
19
+ this._currentState = currentState;
20
+ this._guardFailureCount = 0;
21
+ this._version = 0;
22
+ this.createdAt = new Date();
23
+ this.expiresAt = expiresAt;
24
+ this._exitState = null;
25
+ }
26
+ /**
27
+ * Restore a FlowInstance from persisted state.
28
+ * Used by FlowStore implementations to reconstruct instances loaded from storage.
29
+ */
30
+ static restore(id, sessionId, definition, context, currentState, createdAt, expiresAt, guardFailureCount, version, exitState) {
31
+ const instance = Object.create(FlowInstance.prototype);
32
+ // Use defineProperties to set readonly fields
33
+ Object.defineProperty(instance, 'id', { value: id, writable: false });
34
+ Object.defineProperty(instance, 'sessionId', { value: sessionId, writable: false });
35
+ Object.defineProperty(instance, 'definition', { value: definition, writable: false });
36
+ Object.defineProperty(instance, 'context', { value: context, writable: false });
37
+ Object.defineProperty(instance, 'createdAt', { value: createdAt, writable: false });
38
+ Object.defineProperty(instance, 'expiresAt', { value: expiresAt, writable: false });
39
+ instance._currentState = currentState;
40
+ instance._guardFailureCount = guardFailureCount;
41
+ instance._version = version;
42
+ instance._exitState = exitState;
43
+ return instance;
44
+ }
45
+ get currentState() { return this._currentState; }
46
+ get guardFailureCount() { return this._guardFailureCount; }
47
+ get version() { return this._version; }
48
+ get exitState() { return this._exitState; }
49
+ get isCompleted() { return this._exitState !== null; }
50
+ get activeSubFlow() { return this._activeSubFlow; }
51
+ /** Last error message (set when a processor throws and error transition fires). */
52
+ get lastError() { return this._lastError; }
53
+ /** State path from root to deepest active sub-flow. */
54
+ statePath() {
55
+ const path = [this._currentState];
56
+ if (this._activeSubFlow)
57
+ path.push(...this._activeSubFlow.statePath());
58
+ return path;
59
+ }
60
+ /** State path as slash-separated string. */
61
+ statePathString() { return this.statePath().join('/'); }
62
+ /** Data types available in context at current state (from data-flow graph). */
63
+ availableData() {
64
+ return this.definition.dataFlowGraph?.availableAt(this._currentState) ?? new Set();
65
+ }
66
+ /** Data types that the next transition requires but are not yet in context. */
67
+ missingFor() {
68
+ const missing = [];
69
+ for (const t of this.definition.transitionsFrom(this._currentState)) {
70
+ if (t.guard)
71
+ for (const r of t.guard.requires) {
72
+ if (!this.context.has(r))
73
+ missing.push(r);
74
+ }
75
+ if (t.processor)
76
+ for (const r of t.processor.requires) {
77
+ if (!this.context.has(r))
78
+ missing.push(r);
79
+ }
80
+ }
81
+ return [...new Set(missing)];
82
+ }
83
+ /** Types required by the next external transition (including in active sub-flows). */
84
+ waitingFor() {
85
+ if (this._activeSubFlow)
86
+ return this._activeSubFlow.waitingFor();
87
+ const ext = this.definition.externalFrom(this._currentState);
88
+ if (!ext?.guard)
89
+ return [];
90
+ return [...ext.guard.requires];
91
+ }
92
+ /** Return a copy with the given version. For FlowStore optimistic locking. */
93
+ withVersion(newVersion) {
94
+ const copy = FlowInstance.restore(this.id, this.sessionId, this.definition, this.context, this._currentState, this.createdAt, this.expiresAt, this._guardFailureCount, newVersion, this._exitState);
95
+ copy.setActiveSubFlow(this._activeSubFlow);
96
+ return copy;
97
+ }
98
+ /** @internal */ transitionTo(state) { this._currentState = state; }
99
+ /** @internal */ incrementGuardFailure() { this._guardFailureCount++; }
100
+ /** @internal */ complete(exitState) { this._exitState = exitState; }
101
+ /** @internal */ setVersion(version) { this._version = version; }
102
+ /** @internal */ setActiveSubFlow(sub) { this._activeSubFlow = sub; }
103
+ /** @internal */ setLastError(error) { this._lastError = error; }
104
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Branded string type for type-safe FlowContext keys.
3
+ *
4
+ * Use dedicated FlowKey instances as keys, not raw strings.
5
+ * Each key maps to exactly one data type in the context.
6
+ */
7
+ export type FlowKey<T> = string & {
8
+ readonly __type: T;
9
+ };
10
+ export declare function flowKey<T>(name: string): FlowKey<T>;
@@ -0,0 +1,3 @@
1
+ export function flowKey(name) {
2
+ return name;
3
+ }
@@ -0,0 +1,19 @@
1
+ import type { FlowContext } from './flow-context.js';
2
+ import type { FlowInstance } from './flow-instance.js';
3
+ export interface TransitionRecord {
4
+ flowId: string;
5
+ from: string | null;
6
+ to: string;
7
+ trigger: string;
8
+ subFlow: string | null;
9
+ timestamp: Date;
10
+ }
11
+ export declare class InMemoryFlowStore {
12
+ private flows;
13
+ private _transitionLog;
14
+ create(flow: FlowInstance<any>): void;
15
+ loadForUpdate<S extends string>(flowId: string, _definition?: any): FlowInstance<S> | undefined;
16
+ save(flow: FlowInstance<any>): void;
17
+ recordTransition(flowId: string, from: string | null, to: string, trigger: string, _ctx: FlowContext): void;
18
+ get transitionLog(): readonly TransitionRecord[];
19
+ }
@@ -0,0 +1,23 @@
1
+ export class InMemoryFlowStore {
2
+ flows = new Map();
3
+ _transitionLog = [];
4
+ create(flow) {
5
+ this.flows.set(flow.id, flow);
6
+ }
7
+ loadForUpdate(flowId, _definition) {
8
+ const flow = this.flows.get(flowId);
9
+ if (!flow || flow.isCompleted)
10
+ return undefined;
11
+ return flow;
12
+ }
13
+ save(flow) {
14
+ this.flows.set(flow.id, flow);
15
+ }
16
+ recordTransition(flowId, from, to, trigger, _ctx) {
17
+ const subFlow = trigger.startsWith('subFlow:') ? trigger.substring(8, trigger.indexOf('/')) : null;
18
+ this._transitionLog.push({ flowId, from, to, trigger, subFlow, timestamp: new Date() });
19
+ }
20
+ get transitionLog() {
21
+ return this._transitionLog;
22
+ }
23
+ }
@@ -0,0 +1,16 @@
1
+ export { Tramli } from './tramli.js';
2
+ export { FlowEngine } from './flow-engine.js';
3
+ export { FlowContext } from './flow-context.js';
4
+ export { FlowInstance } from './flow-instance.js';
5
+ export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
6
+ export { FlowError } from './flow-error.js';
7
+ export { InMemoryFlowStore } from './in-memory-flow-store.js';
8
+ export type { TransitionRecord } from './in-memory-flow-store.js';
9
+ export { MermaidGenerator } from './mermaid-generator.js';
10
+ export { SkeletonGenerator } from './skeleton-generator.js';
11
+ export type { TargetLanguage } from './skeleton-generator.js';
12
+ export { DataFlowGraph } from './data-flow-graph.js';
13
+ export type { NodeInfo } from './data-flow-graph.js';
14
+ export { flowKey } from './flow-key.js';
15
+ export type { FlowKey } from './flow-key.js';
16
+ export type { StateConfig, GuardOutput, TransitionType, Transition, StateProcessor, TransitionGuard, BranchProcessor, } from './types.js';
@@ -0,0 +1,11 @@
1
+ export { Tramli } from './tramli.js';
2
+ export { FlowEngine } from './flow-engine.js';
3
+ export { FlowContext } from './flow-context.js';
4
+ export { FlowInstance } from './flow-instance.js';
5
+ export { FlowDefinition, Builder, FromBuilder, BranchBuilder, SubFlowBuilder } from './flow-definition.js';
6
+ export { FlowError } from './flow-error.js';
7
+ export { InMemoryFlowStore } from './in-memory-flow-store.js';
8
+ export { MermaidGenerator } from './mermaid-generator.js';
9
+ export { SkeletonGenerator } from './skeleton-generator.js';
10
+ export { DataFlowGraph } from './data-flow-graph.js';
11
+ export { flowKey } from './flow-key.js';
@@ -0,0 +1,9 @@
1
+ import type { FlowDefinition } from './flow-definition.js';
2
+ export declare class MermaidGenerator {
3
+ static generate<S extends string>(def: FlowDefinition<S>): string;
4
+ /** Generate Mermaid diagram highlighting external transitions and their data contracts. */
5
+ static generateExternalContract<S extends string>(def: FlowDefinition<S>): string;
6
+ /** Generate Mermaid data-flow diagram from requires/produces declarations. */
7
+ static generateDataFlow<S extends string>(def: FlowDefinition<S>): string;
8
+ private static transitionLabel;
9
+ }
@@ -0,0 +1,76 @@
1
+ export class MermaidGenerator {
2
+ static generate(def) {
3
+ const lines = ['stateDiagram-v2'];
4
+ if (def.initialState)
5
+ lines.push(` [*] --> ${def.initialState}`);
6
+ const seen = new Set();
7
+ for (const t of def.transitions) {
8
+ if (t.type === 'sub_flow' && t.subFlowDefinition) {
9
+ const subDef = t.subFlowDefinition;
10
+ lines.push(` state ${t.from} {`);
11
+ if (subDef.initialState)
12
+ lines.push(` [*] --> ${subDef.initialState}`);
13
+ for (const st of subDef.transitions) {
14
+ const sLabel = this.transitionLabel(st);
15
+ lines.push(sLabel ? ` ${st.from} --> ${st.to}: ${sLabel}` : ` ${st.from} --> ${st.to}`);
16
+ }
17
+ for (const term of subDef.terminalStates)
18
+ lines.push(` ${term} --> [*]`);
19
+ lines.push(' }');
20
+ if (t.exitMappings) {
21
+ for (const [exitName, target] of t.exitMappings) {
22
+ lines.push(` ${t.from} --> ${target}: ${exitName}`);
23
+ }
24
+ }
25
+ continue;
26
+ }
27
+ const key = `${t.from}->${t.to}`;
28
+ if (seen.has(key))
29
+ continue;
30
+ seen.add(key);
31
+ const label = this.transitionLabel(t);
32
+ lines.push(label ? ` ${t.from} --> ${t.to}: ${label}` : ` ${t.from} --> ${t.to}`);
33
+ }
34
+ for (const [from, to] of def.errorTransitions) {
35
+ const key = `${from}->${to}`;
36
+ if (!seen.has(key)) {
37
+ seen.add(key);
38
+ lines.push(` ${from} --> ${to}: error`);
39
+ }
40
+ }
41
+ for (const s of def.terminalStates) {
42
+ lines.push(` ${s} --> [*]`);
43
+ }
44
+ return lines.join('\n') + '\n';
45
+ }
46
+ /** Generate Mermaid diagram highlighting external transitions and their data contracts. */
47
+ static generateExternalContract(def) {
48
+ const lines = ['flowchart LR'];
49
+ for (const t of def.transitions) {
50
+ if (t.type !== 'external' || !t.guard)
51
+ continue;
52
+ lines.push(` subgraph ${t.from}_to_${t.to}`);
53
+ lines.push(' direction TB');
54
+ lines.push(` ${t.guard.name}{"[${t.guard.name}]"}`);
55
+ for (const req of t.guard.requires)
56
+ lines.push(` ${req} -->|client sends| ${t.guard.name}`);
57
+ for (const prod of t.guard.produces)
58
+ lines.push(` ${t.guard.name} -->|returns| ${prod}`);
59
+ lines.push(' end');
60
+ }
61
+ return lines.join('\n') + '\n';
62
+ }
63
+ /** Generate Mermaid data-flow diagram from requires/produces declarations. */
64
+ static generateDataFlow(def) {
65
+ return def.dataFlowGraph?.toMermaid() ?? '';
66
+ }
67
+ static transitionLabel(t) {
68
+ if (t.type === 'auto')
69
+ return t.processor?.name ?? '';
70
+ if (t.type === 'external')
71
+ return t.guard ? `[${t.guard.name}]` : '';
72
+ if (t.type === 'branch')
73
+ return t.branch?.name ?? '';
74
+ return '';
75
+ }
76
+ }
@@ -0,0 +1,10 @@
1
+ import type { FlowDefinition } from './flow-definition.js';
2
+ export type TargetLanguage = 'java' | 'typescript' | 'rust';
3
+ /**
4
+ * Generates Processor skeleton code from a FlowDefinition's requires/produces contracts.
5
+ */
6
+ export declare class SkeletonGenerator {
7
+ static generate<S extends string>(def: FlowDefinition<S>, lang: TargetLanguage): string;
8
+ private static genProcessor;
9
+ private static genGuard;
10
+ }