@unlaxer/tramli-plugins 3.1.0 → 3.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.
@@ -36,16 +36,35 @@ export interface GenerationPlugin<I, O> extends FlowPlugin {
36
36
  /** Documentation plugin — generates string documentation. */
37
37
  export interface DocumentationPlugin<I> extends GenerationPlugin<I, string> {
38
38
  }
39
+ /** Describes where in a flow definition a finding is located. */
40
+ export type FindingLocation = {
41
+ type: 'transition';
42
+ fromState: string;
43
+ toState: string;
44
+ } | {
45
+ type: 'state';
46
+ state: string;
47
+ } | {
48
+ type: 'data';
49
+ dataKey: string;
50
+ } | {
51
+ type: 'flow';
52
+ };
53
+ /** A single analysis finding. */
54
+ export interface FindingEntry {
55
+ pluginId: string;
56
+ severity: string;
57
+ message: string;
58
+ location?: FindingLocation;
59
+ }
39
60
  /** Plugin report — collects analysis findings. */
40
61
  export declare class PluginReport {
41
62
  private entries;
42
63
  add(pluginId: string, severity: string, message: string): void;
43
64
  warn(pluginId: string, message: string): void;
44
65
  error(pluginId: string, message: string): void;
66
+ warnAt(pluginId: string, message: string, location: FindingLocation): void;
67
+ errorAt(pluginId: string, message: string, location: FindingLocation): void;
45
68
  asText(): string;
46
- findings(): {
47
- pluginId: string;
48
- severity: string;
49
- message: string;
50
- }[];
69
+ findings(): FindingEntry[];
51
70
  }
@@ -13,11 +13,30 @@ class PluginReport {
13
13
  error(pluginId, message) {
14
14
  this.add(pluginId, 'ERROR', message);
15
15
  }
16
+ warnAt(pluginId, message, location) {
17
+ this.entries.push({ pluginId, severity: 'WARN', message, location });
18
+ }
19
+ errorAt(pluginId, message, location) {
20
+ this.entries.push({ pluginId, severity: 'ERROR', message, location });
21
+ }
16
22
  asText() {
17
23
  if (this.entries.length === 0)
18
24
  return 'No findings.';
19
- return this.entries.map(e => `[${e.severity}] ${e.pluginId}: ${e.message}`).join('\n');
25
+ return this.entries.map(e => {
26
+ let text = `[${e.severity}] ${e.pluginId}: ${e.message}`;
27
+ if (e.location)
28
+ text += ` @ ${formatLocation(e.location)}`;
29
+ return text;
30
+ }).join('\n');
20
31
  }
21
32
  findings() { return [...this.entries]; }
22
33
  }
23
34
  exports.PluginReport = PluginReport;
35
+ function formatLocation(loc) {
36
+ switch (loc.type) {
37
+ case 'transition': return `transition(${loc.fromState} -> ${loc.toState})`;
38
+ case 'state': return `state(${loc.state})`;
39
+ case 'data': return `data(${loc.dataKey})`;
40
+ case 'flow': return 'flow';
41
+ }
42
+ }
@@ -1,5 +1,5 @@
1
1
  export { PluginReport } from './api/types.js';
2
- export type { PluginKind, PluginDescriptor, FlowPlugin, AnalysisPlugin, StorePlugin, EnginePlugin, RuntimeAdapterPlugin, GenerationPlugin, DocumentationPlugin as DocumentationPluginSPI, } from './api/types.js';
2
+ export type { FindingLocation, FindingEntry, PluginKind, PluginDescriptor, FlowPlugin, AnalysisPlugin, StorePlugin, EnginePlugin, RuntimeAdapterPlugin, GenerationPlugin, DocumentationPlugin as DocumentationPluginSPI, } from './api/types.js';
3
3
  export { PluginRegistry } from './api/plugin-registry.js';
4
4
  export { AuditStorePlugin } from './audit/audit-store-plugin.js';
5
5
  export { AuditingFlowStore } from './audit/auditing-flow-store.js';
@@ -32,5 +32,5 @@ export { allDefaultPolicies } from './lint/default-flow-policies.js';
32
32
  export type { FlowPolicy } from './lint/types.js';
33
33
  export { ScenarioTestPlugin } from './testing/scenario-test-plugin.js';
34
34
  export { ScenarioGenerationPlugin } from './testing/scenario-generation-plugin.js';
35
- export type { FlowScenario, FlowTestPlan } from './testing/types.js';
35
+ export type { FlowScenario, FlowTestPlan, ScenarioKind } from './testing/types.js';
36
36
  export { GuaranteedSubflowValidator } from './subflow/guaranteed-subflow-validator.js';
@@ -4,7 +4,7 @@ exports.allDefaultPolicies = allDefaultPolicies;
4
4
  function warnTerminalWithOutgoing(def, report) {
5
5
  for (const state of def.terminalStates) {
6
6
  if (def.transitionsFrom(state).length > 0) {
7
- report.warn('policy/terminal-outgoing', `terminal state ${state} has outgoing transitions`);
7
+ report.warnAt('policy/terminal-outgoing', `terminal state ${state} has outgoing transitions`, { type: 'state', state });
8
8
  }
9
9
  }
10
10
  }
@@ -12,7 +12,7 @@ function warnTooManyExternals(def, report) {
12
12
  for (const state of def.allStates()) {
13
13
  const externals = def.transitionsFrom(state).filter(t => t.type === 'external');
14
14
  if (externals.length > 3) {
15
- report.warn('policy/external-count', `state ${state} has ${externals.length} external transitions`);
15
+ report.warnAt('policy/external-count', `state ${state} has ${externals.length} external transitions`, { type: 'state', state });
16
16
  }
17
17
  }
18
18
  }
@@ -21,13 +21,13 @@ function warnDeadProducedData(def, report) {
21
21
  return;
22
22
  const dead = def.dataFlowGraph.deadData();
23
23
  for (const key of dead) {
24
- report.warn('policy/dead-data', `produced but never consumed: ${key}`);
24
+ report.warnAt('policy/dead-data', `produced but never consumed: ${key}`, { type: 'data', dataKey: key });
25
25
  }
26
26
  }
27
27
  function warnOverwideProcessors(def, report) {
28
28
  for (const t of def.transitions) {
29
29
  if (t.processor && t.processor.produces.length > 3) {
30
- report.warn('policy/overwide-processor', `${t.processor.name} produces ${t.processor.produces.length} types; consider splitting it`);
30
+ report.warnAt('policy/overwide-processor', `${t.processor.name} produces ${t.processor.produces.length} types; consider splitting it`, { type: 'transition', fromState: t.from, toState: t.to });
31
31
  }
32
32
  }
33
33
  }
@@ -2,6 +2,7 @@ import type { FlowDefinition } from '@unlaxer/tramli';
2
2
  import type { FlowTestPlan } from './types.js';
3
3
  /**
4
4
  * Generates BDD-style test scenarios from a flow definition.
5
+ * Covers happy paths, error transitions, guard rejections, and timeout expiry.
5
6
  */
6
7
  export declare class ScenarioTestPlugin {
7
8
  generate<S extends string>(definition: FlowDefinition<S>): FlowTestPlan;
@@ -3,10 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ScenarioTestPlugin = void 0;
4
4
  /**
5
5
  * Generates BDD-style test scenarios from a flow definition.
6
+ * Covers happy paths, error transitions, guard rejections, and timeout expiry.
6
7
  */
7
8
  class ScenarioTestPlugin {
8
9
  generate(definition) {
9
10
  const scenarios = [];
11
+ // Happy path scenarios from transitions
10
12
  for (const t of definition.transitions) {
11
13
  const steps = [];
12
14
  steps.push(`given flow in ${t.from}`);
@@ -20,7 +22,67 @@ class ScenarioTestPlugin {
20
22
  steps.push(`when branch ${t.branch.name} selects a route`);
21
23
  }
22
24
  steps.push(`then flow reaches ${t.to}`);
23
- scenarios.push({ name: `${t.from}_to_${t.to}`, steps });
25
+ scenarios.push({ name: `${t.from}_to_${t.to}`, kind: 'happy', steps });
26
+ }
27
+ // Error path scenarios from errorTransitions
28
+ for (const [from, to] of definition.errorTransitions) {
29
+ scenarios.push({
30
+ name: `error_${from}_to_${to}`,
31
+ kind: 'error',
32
+ steps: [
33
+ `given flow in ${from}`,
34
+ `when processor throws an error`,
35
+ `then flow transitions to ${to} via on_error`,
36
+ ],
37
+ });
38
+ }
39
+ // Exception route scenarios
40
+ if (definition.exceptionRoutes) {
41
+ for (const [from, routes] of definition.exceptionRoutes) {
42
+ for (const route of routes) {
43
+ const label = route.errorClass?.name ?? 'error';
44
+ scenarios.push({
45
+ name: `step_error_${from}_${label}_to_${route.target}`,
46
+ kind: 'error',
47
+ steps: [
48
+ `given flow in ${from}`,
49
+ `when error matching ${label} is thrown`,
50
+ `then flow transitions to ${route.target} via on_step_error`,
51
+ ],
52
+ });
53
+ }
54
+ }
55
+ }
56
+ // Guard rejection scenarios
57
+ for (const t of definition.transitions) {
58
+ if (t.type === 'external' && t.guard) {
59
+ const errorTarget = definition.errorTransitions.get(t.from);
60
+ scenarios.push({
61
+ name: `guard_reject_${t.from}_${t.guard.name}`,
62
+ kind: 'guard_rejection',
63
+ steps: [
64
+ `given flow in ${t.from}`,
65
+ `when guard ${t.guard.name} rejects ${definition.maxGuardRetries} times`,
66
+ errorTarget
67
+ ? `then flow transitions to ${errorTarget} via error`
68
+ : `then flow enters TERMINAL_ERROR`,
69
+ ],
70
+ });
71
+ }
72
+ }
73
+ // Timeout scenarios
74
+ for (const t of definition.transitions) {
75
+ if (t.timeout != null) {
76
+ scenarios.push({
77
+ name: `timeout_${t.from}`,
78
+ kind: 'timeout',
79
+ steps: [
80
+ `given flow in ${t.from}`,
81
+ `when per-state timeout of ${t.timeout}ms expires`,
82
+ `then flow completes as EXPIRED`,
83
+ ],
84
+ });
85
+ }
24
86
  }
25
87
  return { scenarios };
26
88
  }
@@ -1,5 +1,7 @@
1
+ export type ScenarioKind = 'happy' | 'error' | 'guard_rejection' | 'timeout';
1
2
  export interface FlowScenario {
2
3
  name: string;
4
+ kind: ScenarioKind;
3
5
  steps: string[];
4
6
  }
5
7
  export interface FlowTestPlan {
@@ -36,16 +36,35 @@ export interface GenerationPlugin<I, O> extends FlowPlugin {
36
36
  /** Documentation plugin — generates string documentation. */
37
37
  export interface DocumentationPlugin<I> extends GenerationPlugin<I, string> {
38
38
  }
39
+ /** Describes where in a flow definition a finding is located. */
40
+ export type FindingLocation = {
41
+ type: 'transition';
42
+ fromState: string;
43
+ toState: string;
44
+ } | {
45
+ type: 'state';
46
+ state: string;
47
+ } | {
48
+ type: 'data';
49
+ dataKey: string;
50
+ } | {
51
+ type: 'flow';
52
+ };
53
+ /** A single analysis finding. */
54
+ export interface FindingEntry {
55
+ pluginId: string;
56
+ severity: string;
57
+ message: string;
58
+ location?: FindingLocation;
59
+ }
39
60
  /** Plugin report — collects analysis findings. */
40
61
  export declare class PluginReport {
41
62
  private entries;
42
63
  add(pluginId: string, severity: string, message: string): void;
43
64
  warn(pluginId: string, message: string): void;
44
65
  error(pluginId: string, message: string): void;
66
+ warnAt(pluginId: string, message: string, location: FindingLocation): void;
67
+ errorAt(pluginId: string, message: string, location: FindingLocation): void;
45
68
  asText(): string;
46
- findings(): {
47
- pluginId: string;
48
- severity: string;
49
- message: string;
50
- }[];
69
+ findings(): FindingEntry[];
51
70
  }
@@ -10,10 +10,29 @@ export class PluginReport {
10
10
  error(pluginId, message) {
11
11
  this.add(pluginId, 'ERROR', message);
12
12
  }
13
+ warnAt(pluginId, message, location) {
14
+ this.entries.push({ pluginId, severity: 'WARN', message, location });
15
+ }
16
+ errorAt(pluginId, message, location) {
17
+ this.entries.push({ pluginId, severity: 'ERROR', message, location });
18
+ }
13
19
  asText() {
14
20
  if (this.entries.length === 0)
15
21
  return 'No findings.';
16
- return this.entries.map(e => `[${e.severity}] ${e.pluginId}: ${e.message}`).join('\n');
22
+ return this.entries.map(e => {
23
+ let text = `[${e.severity}] ${e.pluginId}: ${e.message}`;
24
+ if (e.location)
25
+ text += ` @ ${formatLocation(e.location)}`;
26
+ return text;
27
+ }).join('\n');
17
28
  }
18
29
  findings() { return [...this.entries]; }
19
30
  }
31
+ function formatLocation(loc) {
32
+ switch (loc.type) {
33
+ case 'transition': return `transition(${loc.fromState} -> ${loc.toState})`;
34
+ case 'state': return `state(${loc.state})`;
35
+ case 'data': return `data(${loc.dataKey})`;
36
+ case 'flow': return 'flow';
37
+ }
38
+ }
@@ -1,5 +1,5 @@
1
1
  export { PluginReport } from './api/types.js';
2
- export type { PluginKind, PluginDescriptor, FlowPlugin, AnalysisPlugin, StorePlugin, EnginePlugin, RuntimeAdapterPlugin, GenerationPlugin, DocumentationPlugin as DocumentationPluginSPI, } from './api/types.js';
2
+ export type { FindingLocation, FindingEntry, PluginKind, PluginDescriptor, FlowPlugin, AnalysisPlugin, StorePlugin, EnginePlugin, RuntimeAdapterPlugin, GenerationPlugin, DocumentationPlugin as DocumentationPluginSPI, } from './api/types.js';
3
3
  export { PluginRegistry } from './api/plugin-registry.js';
4
4
  export { AuditStorePlugin } from './audit/audit-store-plugin.js';
5
5
  export { AuditingFlowStore } from './audit/auditing-flow-store.js';
@@ -32,5 +32,5 @@ export { allDefaultPolicies } from './lint/default-flow-policies.js';
32
32
  export type { FlowPolicy } from './lint/types.js';
33
33
  export { ScenarioTestPlugin } from './testing/scenario-test-plugin.js';
34
34
  export { ScenarioGenerationPlugin } from './testing/scenario-generation-plugin.js';
35
- export type { FlowScenario, FlowTestPlan } from './testing/types.js';
35
+ export type { FlowScenario, FlowTestPlan, ScenarioKind } from './testing/types.js';
36
36
  export { GuaranteedSubflowValidator } from './subflow/guaranteed-subflow-validator.js';
@@ -1,7 +1,7 @@
1
1
  function warnTerminalWithOutgoing(def, report) {
2
2
  for (const state of def.terminalStates) {
3
3
  if (def.transitionsFrom(state).length > 0) {
4
- report.warn('policy/terminal-outgoing', `terminal state ${state} has outgoing transitions`);
4
+ report.warnAt('policy/terminal-outgoing', `terminal state ${state} has outgoing transitions`, { type: 'state', state });
5
5
  }
6
6
  }
7
7
  }
@@ -9,7 +9,7 @@ function warnTooManyExternals(def, report) {
9
9
  for (const state of def.allStates()) {
10
10
  const externals = def.transitionsFrom(state).filter(t => t.type === 'external');
11
11
  if (externals.length > 3) {
12
- report.warn('policy/external-count', `state ${state} has ${externals.length} external transitions`);
12
+ report.warnAt('policy/external-count', `state ${state} has ${externals.length} external transitions`, { type: 'state', state });
13
13
  }
14
14
  }
15
15
  }
@@ -18,13 +18,13 @@ function warnDeadProducedData(def, report) {
18
18
  return;
19
19
  const dead = def.dataFlowGraph.deadData();
20
20
  for (const key of dead) {
21
- report.warn('policy/dead-data', `produced but never consumed: ${key}`);
21
+ report.warnAt('policy/dead-data', `produced but never consumed: ${key}`, { type: 'data', dataKey: key });
22
22
  }
23
23
  }
24
24
  function warnOverwideProcessors(def, report) {
25
25
  for (const t of def.transitions) {
26
26
  if (t.processor && t.processor.produces.length > 3) {
27
- report.warn('policy/overwide-processor', `${t.processor.name} produces ${t.processor.produces.length} types; consider splitting it`);
27
+ report.warnAt('policy/overwide-processor', `${t.processor.name} produces ${t.processor.produces.length} types; consider splitting it`, { type: 'transition', fromState: t.from, toState: t.to });
28
28
  }
29
29
  }
30
30
  }
@@ -2,6 +2,7 @@ import type { FlowDefinition } from '@unlaxer/tramli';
2
2
  import type { FlowTestPlan } from './types.js';
3
3
  /**
4
4
  * Generates BDD-style test scenarios from a flow definition.
5
+ * Covers happy paths, error transitions, guard rejections, and timeout expiry.
5
6
  */
6
7
  export declare class ScenarioTestPlugin {
7
8
  generate<S extends string>(definition: FlowDefinition<S>): FlowTestPlan;
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Generates BDD-style test scenarios from a flow definition.
3
+ * Covers happy paths, error transitions, guard rejections, and timeout expiry.
3
4
  */
4
5
  export class ScenarioTestPlugin {
5
6
  generate(definition) {
6
7
  const scenarios = [];
8
+ // Happy path scenarios from transitions
7
9
  for (const t of definition.transitions) {
8
10
  const steps = [];
9
11
  steps.push(`given flow in ${t.from}`);
@@ -17,7 +19,67 @@ export class ScenarioTestPlugin {
17
19
  steps.push(`when branch ${t.branch.name} selects a route`);
18
20
  }
19
21
  steps.push(`then flow reaches ${t.to}`);
20
- scenarios.push({ name: `${t.from}_to_${t.to}`, steps });
22
+ scenarios.push({ name: `${t.from}_to_${t.to}`, kind: 'happy', steps });
23
+ }
24
+ // Error path scenarios from errorTransitions
25
+ for (const [from, to] of definition.errorTransitions) {
26
+ scenarios.push({
27
+ name: `error_${from}_to_${to}`,
28
+ kind: 'error',
29
+ steps: [
30
+ `given flow in ${from}`,
31
+ `when processor throws an error`,
32
+ `then flow transitions to ${to} via on_error`,
33
+ ],
34
+ });
35
+ }
36
+ // Exception route scenarios
37
+ if (definition.exceptionRoutes) {
38
+ for (const [from, routes] of definition.exceptionRoutes) {
39
+ for (const route of routes) {
40
+ const label = route.errorClass?.name ?? 'error';
41
+ scenarios.push({
42
+ name: `step_error_${from}_${label}_to_${route.target}`,
43
+ kind: 'error',
44
+ steps: [
45
+ `given flow in ${from}`,
46
+ `when error matching ${label} is thrown`,
47
+ `then flow transitions to ${route.target} via on_step_error`,
48
+ ],
49
+ });
50
+ }
51
+ }
52
+ }
53
+ // Guard rejection scenarios
54
+ for (const t of definition.transitions) {
55
+ if (t.type === 'external' && t.guard) {
56
+ const errorTarget = definition.errorTransitions.get(t.from);
57
+ scenarios.push({
58
+ name: `guard_reject_${t.from}_${t.guard.name}`,
59
+ kind: 'guard_rejection',
60
+ steps: [
61
+ `given flow in ${t.from}`,
62
+ `when guard ${t.guard.name} rejects ${definition.maxGuardRetries} times`,
63
+ errorTarget
64
+ ? `then flow transitions to ${errorTarget} via error`
65
+ : `then flow enters TERMINAL_ERROR`,
66
+ ],
67
+ });
68
+ }
69
+ }
70
+ // Timeout scenarios
71
+ for (const t of definition.transitions) {
72
+ if (t.timeout != null) {
73
+ scenarios.push({
74
+ name: `timeout_${t.from}`,
75
+ kind: 'timeout',
76
+ steps: [
77
+ `given flow in ${t.from}`,
78
+ `when per-state timeout of ${t.timeout}ms expires`,
79
+ `then flow completes as EXPIRED`,
80
+ ],
81
+ });
82
+ }
21
83
  }
22
84
  return { scenarios };
23
85
  }
@@ -1,5 +1,7 @@
1
+ export type ScenarioKind = 'happy' | 'error' | 'guard_rejection' | 'timeout';
1
2
  export interface FlowScenario {
2
3
  name: string;
4
+ kind: ScenarioKind;
3
5
  steps: string[];
4
6
  }
5
7
  export interface FlowTestPlan {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unlaxer/tramli-plugins",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "Plugin pack for tramli — audit, eventstore, observability, resume, idempotency, hierarchy, diagram, docs, lint, testing, subflow",
5
5
  "type": "module",
6
6
  "exports": {