@unlaxer/tramli 1.3.0 → 1.4.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/dist/data-flow-graph.d.ts +25 -0
- package/dist/data-flow-graph.js +127 -0
- package/dist/flow-definition.d.ts +4 -0
- package/dist/flow-definition.js +36 -0
- package/dist/flow-instance.d.ts +4 -0
- package/dist/flow-instance.js +21 -0
- package/dist/mermaid-generator.d.ts +2 -0
- package/dist/mermaid-generator.js +17 -0
- package/package.json +1 -1
|
@@ -57,7 +57,32 @@ export declare class DataFlowGraph<S extends string> {
|
|
|
57
57
|
* Returns list of missing type keys (empty = OK).
|
|
58
58
|
*/
|
|
59
59
|
assertDataFlow(ctx: FlowContext, currentState: S): string[];
|
|
60
|
+
/** Impact analysis: all producers and consumers of a given type. */
|
|
61
|
+
impactOf(key: FlowKey<unknown>): {
|
|
62
|
+
producers: NodeInfo<S>[];
|
|
63
|
+
consumers: NodeInfo<S>[];
|
|
64
|
+
};
|
|
65
|
+
/** Parallelism hints: pairs of processors with no data dependency. */
|
|
66
|
+
parallelismHints(): [string, string][];
|
|
67
|
+
/** Structured JSON representation. */
|
|
68
|
+
toJson(): string;
|
|
60
69
|
/** Generate Mermaid data-flow diagram. */
|
|
61
70
|
toMermaid(): string;
|
|
71
|
+
/** Test scaffold: for each processor, list required type names. */
|
|
72
|
+
testScaffold(): Map<string, string[]>;
|
|
73
|
+
/** Generate data-flow invariant assertions as strings. */
|
|
74
|
+
generateInvariantAssertions(): string[];
|
|
75
|
+
/** Cross-flow map: types that one flow produces and another requires. */
|
|
76
|
+
static crossFlowMap(...graphs: DataFlowGraph<any>[]): string[];
|
|
77
|
+
/** Diff two data-flow graphs. */
|
|
78
|
+
static diff(before: DataFlowGraph<any>, after: DataFlowGraph<any>): {
|
|
79
|
+
addedTypes: Set<string>;
|
|
80
|
+
removedTypes: Set<string>;
|
|
81
|
+
addedEdges: Set<string>;
|
|
82
|
+
removedEdges: Set<string>;
|
|
83
|
+
};
|
|
84
|
+
private static collectEdges;
|
|
85
|
+
/** Version compatibility: check if v1 instances can resume on v2 definition. */
|
|
86
|
+
static versionCompatibility<S extends string>(before: DataFlowGraph<S>, after: DataFlowGraph<S>): string[];
|
|
62
87
|
static build<S extends string>(def: FlowDefinition<S>, initiallyAvailable: string[]): DataFlowGraph<S>;
|
|
63
88
|
}
|
package/dist/data-flow-graph.js
CHANGED
|
@@ -142,6 +142,63 @@ export class DataFlowGraph {
|
|
|
142
142
|
}
|
|
143
143
|
return missing;
|
|
144
144
|
}
|
|
145
|
+
/** Impact analysis: all producers and consumers of a given type. */
|
|
146
|
+
impactOf(key) {
|
|
147
|
+
return { producers: this.producersOf(key), consumers: this.consumersOf(key) };
|
|
148
|
+
}
|
|
149
|
+
/** Parallelism hints: pairs of processors with no data dependency. */
|
|
150
|
+
parallelismHints() {
|
|
151
|
+
const allNodes = new Set();
|
|
152
|
+
for (const nodes of this._producers.values())
|
|
153
|
+
for (const n of nodes)
|
|
154
|
+
allNodes.add(n.name);
|
|
155
|
+
for (const nodes of this._consumers.values())
|
|
156
|
+
for (const n of nodes)
|
|
157
|
+
allNodes.add(n.name);
|
|
158
|
+
const list = [...allNodes];
|
|
159
|
+
const hints = [];
|
|
160
|
+
for (let i = 0; i < list.length; i++) {
|
|
161
|
+
for (let j = i + 1; j < list.length; j++) {
|
|
162
|
+
const aProds = new Set(), bReqs = new Set();
|
|
163
|
+
const bProds = new Set(), aReqs = new Set();
|
|
164
|
+
for (const [t, ns] of this._producers) {
|
|
165
|
+
for (const n of ns) {
|
|
166
|
+
if (n.name === list[i])
|
|
167
|
+
aProds.add(t);
|
|
168
|
+
if (n.name === list[j])
|
|
169
|
+
bProds.add(t);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const [t, ns] of this._consumers) {
|
|
173
|
+
for (const n of ns) {
|
|
174
|
+
if (n.name === list[i])
|
|
175
|
+
aReqs.add(t);
|
|
176
|
+
if (n.name === list[j])
|
|
177
|
+
bReqs.add(t);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const aDepB = [...aReqs].some(r => bProds.has(r));
|
|
181
|
+
const bDepA = [...bReqs].some(r => aProds.has(r));
|
|
182
|
+
if (!aDepB && !bDepA)
|
|
183
|
+
hints.push([list[i], list[j]]);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return hints;
|
|
187
|
+
}
|
|
188
|
+
/** Structured JSON representation. */
|
|
189
|
+
toJson() {
|
|
190
|
+
const types = [...this.allTypes()].map(t => {
|
|
191
|
+
const entry = { name: t };
|
|
192
|
+
const prods = this.producersOf(t);
|
|
193
|
+
if (prods.length)
|
|
194
|
+
entry.producers = prods.map(p => p.name);
|
|
195
|
+
const cons = this.consumersOf(t);
|
|
196
|
+
if (cons.length)
|
|
197
|
+
entry.consumers = cons.map(c => c.name);
|
|
198
|
+
return entry;
|
|
199
|
+
});
|
|
200
|
+
return JSON.stringify({ types, deadData: [...this.deadData()] }, null, 2);
|
|
201
|
+
}
|
|
145
202
|
/** Generate Mermaid data-flow diagram. */
|
|
146
203
|
toMermaid() {
|
|
147
204
|
const lines = ['flowchart LR'];
|
|
@@ -166,6 +223,76 @@ export class DataFlowGraph {
|
|
|
166
223
|
}
|
|
167
224
|
return lines.join('\n') + '\n';
|
|
168
225
|
}
|
|
226
|
+
/** Test scaffold: for each processor, list required type names. */
|
|
227
|
+
testScaffold() {
|
|
228
|
+
const scaffold = new Map();
|
|
229
|
+
for (const [typeName, nodes] of this._consumers) {
|
|
230
|
+
for (const node of nodes) {
|
|
231
|
+
if (!scaffold.has(node.name))
|
|
232
|
+
scaffold.set(node.name, []);
|
|
233
|
+
scaffold.get(node.name).push(typeName);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return scaffold;
|
|
237
|
+
}
|
|
238
|
+
/** Generate data-flow invariant assertions as strings. */
|
|
239
|
+
generateInvariantAssertions() {
|
|
240
|
+
const assertions = [];
|
|
241
|
+
for (const [state, types] of this._availableAtState) {
|
|
242
|
+
assertions.push(`At state ${state}: context must contain [${[...types].sort().join(', ')}]`);
|
|
243
|
+
}
|
|
244
|
+
return assertions;
|
|
245
|
+
}
|
|
246
|
+
// ─── Cross-flow / Versioning utilities ─────────────────────
|
|
247
|
+
/** Cross-flow map: types that one flow produces and another requires. */
|
|
248
|
+
static crossFlowMap(...graphs) {
|
|
249
|
+
const results = [];
|
|
250
|
+
for (let i = 0; i < graphs.length; i++) {
|
|
251
|
+
for (let j = 0; j < graphs.length; j++) {
|
|
252
|
+
if (i === j)
|
|
253
|
+
continue;
|
|
254
|
+
for (const produced of graphs[i]._allProduced) {
|
|
255
|
+
if (graphs[j]._allConsumed.has(produced)) {
|
|
256
|
+
results.push(`${produced}: flow ${i} produces → flow ${j} consumes`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
/** Diff two data-flow graphs. */
|
|
264
|
+
static diff(before, after) {
|
|
265
|
+
const beforeTypes = before.allTypes(), afterTypes = after.allTypes();
|
|
266
|
+
const addedTypes = new Set([...afterTypes].filter(t => !beforeTypes.has(t)));
|
|
267
|
+
const removedTypes = new Set([...beforeTypes].filter(t => !afterTypes.has(t)));
|
|
268
|
+
const beforeEdges = DataFlowGraph.collectEdges(before), afterEdges = DataFlowGraph.collectEdges(after);
|
|
269
|
+
const addedEdges = new Set([...afterEdges].filter(e => !beforeEdges.has(e)));
|
|
270
|
+
const removedEdges = new Set([...beforeEdges].filter(e => !afterEdges.has(e)));
|
|
271
|
+
return { addedTypes, removedTypes, addedEdges, removedEdges };
|
|
272
|
+
}
|
|
273
|
+
static collectEdges(graph) {
|
|
274
|
+
const edges = new Set();
|
|
275
|
+
for (const [t, ns] of graph._producers)
|
|
276
|
+
for (const n of ns)
|
|
277
|
+
edges.add(`${n.name} --produces--> ${t}`);
|
|
278
|
+
for (const [t, ns] of graph._consumers)
|
|
279
|
+
for (const n of ns)
|
|
280
|
+
edges.add(`${t} --requires--> ${n.name}`);
|
|
281
|
+
return edges;
|
|
282
|
+
}
|
|
283
|
+
/** Version compatibility: check if v1 instances can resume on v2 definition. */
|
|
284
|
+
static versionCompatibility(before, after) {
|
|
285
|
+
const issues = [];
|
|
286
|
+
for (const [state, beforeAvail] of before._availableAtState) {
|
|
287
|
+
const afterAvail = after._availableAtState.get(state) ?? new Set();
|
|
288
|
+
for (const type of afterAvail) {
|
|
289
|
+
if (!beforeAvail.has(type)) {
|
|
290
|
+
issues.push(`State ${state}: v2 expects ${type} but v1 instances may not have it`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return issues;
|
|
295
|
+
}
|
|
169
296
|
// ─── Builder ─────────────────────────────────────────────
|
|
170
297
|
static build(def, initiallyAvailable) {
|
|
171
298
|
const stateAvail = new Map();
|
|
@@ -15,6 +15,10 @@ export declare class FlowDefinition<S extends string> {
|
|
|
15
15
|
transitionsFrom(state: S): Transition<S>[];
|
|
16
16
|
externalFrom(state: S): Transition<S> | undefined;
|
|
17
17
|
allStates(): S[];
|
|
18
|
+
/**
|
|
19
|
+
* Create a new FlowDefinition with a sub-flow inserted before a specific transition.
|
|
20
|
+
*/
|
|
21
|
+
withPlugin(from: S, to: S, pluginFlow: FlowDefinition<any>): FlowDefinition<S>;
|
|
18
22
|
static builder<S extends string>(name: string, stateConfig: Record<S, StateConfig>): Builder<S>;
|
|
19
23
|
}
|
|
20
24
|
export declare class Builder<S extends string> {
|
package/dist/flow-definition.js
CHANGED
|
@@ -37,6 +37,42 @@ export class FlowDefinition {
|
|
|
37
37
|
allStates() {
|
|
38
38
|
return Object.keys(this.stateConfig);
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a new FlowDefinition with a sub-flow inserted before a specific transition.
|
|
42
|
+
*/
|
|
43
|
+
withPlugin(from, to, pluginFlow) {
|
|
44
|
+
const newTransitions = [];
|
|
45
|
+
let replaced = false;
|
|
46
|
+
for (const t of this.transitions) {
|
|
47
|
+
if (t.from === from && t.to === to && !replaced) {
|
|
48
|
+
const exitMap = new Map();
|
|
49
|
+
for (const terminal of pluginFlow.terminalStates)
|
|
50
|
+
exitMap.set(terminal, to);
|
|
51
|
+
newTransitions.push({
|
|
52
|
+
from, to: from, type: 'sub_flow',
|
|
53
|
+
processor: t.processor, guard: undefined, branch: undefined,
|
|
54
|
+
branchTargets: new Map(),
|
|
55
|
+
subFlowDefinition: pluginFlow, exitMappings: exitMap,
|
|
56
|
+
});
|
|
57
|
+
replaced = true;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
newTransitions.push(t);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const result = Object.create(FlowDefinition.prototype);
|
|
64
|
+
Object.assign(result, {
|
|
65
|
+
name: this.name + '+plugin:' + pluginFlow.name,
|
|
66
|
+
stateConfig: this.stateConfig, ttl: this.ttl,
|
|
67
|
+
maxGuardRetries: this.maxGuardRetries,
|
|
68
|
+
transitions: newTransitions,
|
|
69
|
+
errorTransitions: new Map(this.errorTransitions),
|
|
70
|
+
initialState: this.initialState,
|
|
71
|
+
terminalStates: this.terminalStates,
|
|
72
|
+
dataFlowGraph: null, // will be rebuilt if needed
|
|
73
|
+
});
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
40
76
|
// ─── Builder ─────────────────────────────────────────────
|
|
41
77
|
static builder(name, stateConfig) {
|
|
42
78
|
return new Builder(name, stateConfig);
|
package/dist/flow-instance.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export declare class FlowInstance<S extends string> {
|
|
|
28
28
|
statePath(): string[];
|
|
29
29
|
/** State path as slash-separated string. */
|
|
30
30
|
statePathString(): string;
|
|
31
|
+
/** Data types available in context at current state (from data-flow graph). */
|
|
32
|
+
availableData(): Set<string>;
|
|
33
|
+
/** Data types that the next transition requires but are not yet in context. */
|
|
34
|
+
missingFor(): string[];
|
|
31
35
|
/** Types required by the next external transition (including in active sub-flows). */
|
|
32
36
|
waitingFor(): string[];
|
|
33
37
|
/** Return a copy with the given version. For FlowStore optimistic locking. */
|
package/dist/flow-instance.js
CHANGED
|
@@ -56,6 +56,27 @@ export class FlowInstance {
|
|
|
56
56
|
}
|
|
57
57
|
/** State path as slash-separated string. */
|
|
58
58
|
statePathString() { return this.statePath().join('/'); }
|
|
59
|
+
/** Data types available in context at current state (from data-flow graph). */
|
|
60
|
+
availableData() {
|
|
61
|
+
return this.definition.dataFlowGraph?.availableAt(this._currentState) ?? new Set();
|
|
62
|
+
}
|
|
63
|
+
/** Data types that the next transition requires but are not yet in context. */
|
|
64
|
+
missingFor() {
|
|
65
|
+
const missing = [];
|
|
66
|
+
for (const t of this.definition.transitionsFrom(this._currentState)) {
|
|
67
|
+
if (t.guard)
|
|
68
|
+
for (const r of t.guard.requires) {
|
|
69
|
+
if (!this.context.has(r))
|
|
70
|
+
missing.push(r);
|
|
71
|
+
}
|
|
72
|
+
if (t.processor)
|
|
73
|
+
for (const r of t.processor.requires) {
|
|
74
|
+
if (!this.context.has(r))
|
|
75
|
+
missing.push(r);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return [...new Set(missing)];
|
|
79
|
+
}
|
|
59
80
|
/** Types required by the next external transition (including in active sub-flows). */
|
|
60
81
|
waitingFor() {
|
|
61
82
|
if (this._activeSubFlow)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { FlowDefinition } from './flow-definition.js';
|
|
2
2
|
export declare class MermaidGenerator {
|
|
3
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;
|
|
4
6
|
/** Generate Mermaid data-flow diagram from requires/produces declarations. */
|
|
5
7
|
static generateDataFlow<S extends string>(def: FlowDefinition<S>): string;
|
|
6
8
|
private static transitionLabel;
|
|
@@ -43,6 +43,23 @@ export class MermaidGenerator {
|
|
|
43
43
|
}
|
|
44
44
|
return lines.join('\n') + '\n';
|
|
45
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
|
+
}
|
|
46
63
|
/** Generate Mermaid data-flow diagram from requires/produces declarations. */
|
|
47
64
|
static generateDataFlow(def) {
|
|
48
65
|
return def.dataFlowGraph?.toMermaid() ?? '';
|