@unlaxer/tramli 0.2.0 → 1.1.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'];
@@ -274,6 +274,18 @@ export class Builder {
274
274
  newAvailable.add(p);
275
275
  }
276
276
  this.checkRequiresProducesFrom(def, t.to, newAvailable, stateAvailable, errors);
277
+ // Error path analysis: if processor fails, its produces are NOT available
278
+ if (t.processor) {
279
+ const errorTarget = def.errorTransitions.get(t.from);
280
+ if (errorTarget) {
281
+ const errorAvailable = new Set(stateAvailable.get(state));
282
+ if (t.guard) {
283
+ for (const p of t.guard.produces)
284
+ errorAvailable.add(p);
285
+ }
286
+ this.checkRequiresProducesFrom(def, errorTarget, errorAvailable, stateAvailable, errors);
287
+ }
288
+ }
277
289
  }
278
290
  }
279
291
  checkAutoExternalConflict(def, errors) {
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli",
3
- "version": "0.2.0",
3
+ "version": "1.1.0",
4
4
  "description": "Constrained flow engine — state machines that prevent invalid transitions at build time",
5
5
  "type": "module",
6
6
  "exports": {