@itmetsam/samurai-framework 0.2.3 → 0.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.
Files changed (101) hide show
  1. package/README.md +5 -0
  2. package/bin/samurai.mjs +7 -5
  3. package/dist/cli/args.d.ts +5 -1
  4. package/dist/cli/args.js +6 -1
  5. package/dist/cli/flow-reporter.d.ts +23 -0
  6. package/dist/cli/flow-reporter.js +98 -0
  7. package/dist/cli/flow.d.ts +4 -0
  8. package/dist/cli/flow.js +240 -0
  9. package/dist/cli/main.d.ts +1 -1
  10. package/dist/cli/main.js +14 -0
  11. package/dist/flows/core/check.d.ts +45 -0
  12. package/dist/flows/core/check.js +205 -0
  13. package/dist/flows/core/expression/check.d.ts +23 -0
  14. package/dist/flows/core/expression/check.js +95 -0
  15. package/dist/flows/core/expression/evaluate.d.ts +22 -0
  16. package/dist/flows/core/expression/evaluate.js +141 -0
  17. package/dist/flows/core/expression/help.d.ts +25 -0
  18. package/dist/flows/core/expression/help.js +75 -0
  19. package/dist/flows/core/expression/index.d.ts +5 -0
  20. package/dist/flows/core/expression/index.js +5 -0
  21. package/dist/flows/core/expression/parse.d.ts +61 -0
  22. package/dist/flows/core/expression/parse.js +317 -0
  23. package/dist/flows/core/expression/scope.d.ts +8 -0
  24. package/dist/flows/core/expression/scope.js +34 -0
  25. package/dist/flows/core/graph.d.ts +11 -0
  26. package/dist/flows/core/graph.js +65 -0
  27. package/dist/flows/core/index.d.ts +17 -0
  28. package/dist/flows/core/index.js +17 -0
  29. package/dist/flows/core/keys.d.ts +8 -0
  30. package/dist/flows/core/keys.js +46 -0
  31. package/dist/flows/core/nodes/condition.d.ts +7 -0
  32. package/dist/flows/core/nodes/condition.js +21 -0
  33. package/dist/flows/core/nodes/define.d.ts +132 -0
  34. package/dist/flows/core/nodes/define.js +21 -0
  35. package/dist/flows/core/nodes/end.d.ts +1 -0
  36. package/dist/flows/core/nodes/end.js +7 -0
  37. package/dist/flows/core/nodes/group.d.ts +5 -0
  38. package/dist/flows/core/nodes/group.js +18 -0
  39. package/dist/flows/core/nodes/index.d.ts +45 -0
  40. package/dist/flows/core/nodes/index.js +55 -0
  41. package/dist/flows/core/nodes/parallel.d.ts +1 -0
  42. package/dist/flows/core/nodes/parallel.js +11 -0
  43. package/dist/flows/core/nodes/path.d.ts +5 -0
  44. package/dist/flows/core/nodes/path.js +21 -0
  45. package/dist/flows/core/nodes/start.d.ts +1 -0
  46. package/dist/flows/core/nodes/start.js +7 -0
  47. package/dist/flows/core/nodes/test.d.ts +1 -0
  48. package/dist/flows/core/nodes/test.js +18 -0
  49. package/dist/flows/core/schema.d.ts +55 -0
  50. package/dist/flows/core/schema.js +33 -0
  51. package/dist/flows/core/steps.d.ts +42 -0
  52. package/dist/flows/core/steps.js +47 -0
  53. package/dist/flows/run/events.d.ts +45 -0
  54. package/dist/flows/run/events.js +1 -0
  55. package/dist/flows/run/files.d.ts +12 -0
  56. package/dist/flows/run/files.js +54 -0
  57. package/dist/flows/run/handlers/condition.d.ts +2 -0
  58. package/dist/flows/run/handlers/condition.js +17 -0
  59. package/dist/flows/run/handlers/end.d.ts +2 -0
  60. package/dist/flows/run/handlers/end.js +1 -0
  61. package/dist/flows/run/handlers/group.d.ts +2 -0
  62. package/dist/flows/run/handlers/group.js +7 -0
  63. package/dist/flows/run/handlers/index.d.ts +4 -0
  64. package/dist/flows/run/handlers/index.js +17 -0
  65. package/dist/flows/run/handlers/parallel.d.ts +3 -0
  66. package/dist/flows/run/handlers/parallel.js +2 -0
  67. package/dist/flows/run/handlers/start.d.ts +2 -0
  68. package/dist/flows/run/handlers/start.js +1 -0
  69. package/dist/flows/run/handlers/support.d.ts +29 -0
  70. package/dist/flows/run/handlers/support.js +22 -0
  71. package/dist/flows/run/handlers/test.d.ts +2 -0
  72. package/dist/flows/run/handlers/test.js +7 -0
  73. package/dist/flows/run/index.d.ts +8 -0
  74. package/dist/flows/run/index.js +7 -0
  75. package/dist/flows/run/project.d.ts +43 -0
  76. package/dist/flows/run/project.js +165 -0
  77. package/dist/flows/run/report.d.ts +39 -0
  78. package/dist/flows/run/report.js +46 -0
  79. package/dist/flows/run/run.d.ts +42 -0
  80. package/dist/flows/run/run.js +250 -0
  81. package/dist/flows/steps/api.d.ts +23 -0
  82. package/dist/flows/steps/api.js +40 -0
  83. package/dist/flows/steps/catalog.d.ts +16 -0
  84. package/dist/flows/steps/catalog.js +29 -0
  85. package/dist/flows/steps/define.d.ts +49 -0
  86. package/dist/flows/steps/define.js +40 -0
  87. package/dist/flows/steps/handler.d.ts +4 -0
  88. package/dist/flows/steps/handler.js +70 -0
  89. package/dist/flows/steps/index.d.ts +5 -0
  90. package/dist/flows/steps/index.js +4 -0
  91. package/dist/flows/steps/set-variable.d.ts +16 -0
  92. package/dist/flows/steps/set-variable.js +20 -0
  93. package/dist/flows/steps/wait.d.ts +8 -0
  94. package/dist/flows/steps/wait.js +14 -0
  95. package/dist/runner/groups.d.ts +39 -0
  96. package/dist/runner/groups.js +58 -0
  97. package/dist/runner/run.d.ts +4 -0
  98. package/dist/runner/run.js +12 -0
  99. package/dist/runner/test-runner.d.ts +12 -2
  100. package/dist/runner/test-runner.js +36 -6
  101. package/package.json +13 -1
@@ -0,0 +1,132 @@
1
+ import type { FlowNodeModel } from "../schema.js";
2
+ /** A problem `checkFlow` or a node's `validate` reports */
3
+ export interface FlowProblem {
4
+ level: "error" | "warning";
5
+ message: string;
6
+ nodeId?: string;
7
+ }
8
+ /** What a node's `validate` can read besides the node itself */
9
+ export interface NodeCtx {
10
+ /** The definition's `label`: the one place a node's name is written */
11
+ label: string;
12
+ /** The test ids that exist; undefined when checkFlow was not given them (no reference check) */
13
+ tests?: ReadonlySet<string>;
14
+ /** The group names that exist; undefined when checkFlow was not given them */
15
+ groups?: ReadonlySet<string>;
16
+ }
17
+ export interface Option {
18
+ label: string;
19
+ value: string;
20
+ }
21
+ interface FieldBase {
22
+ /** Where the value lives in the node: `title`, `subtitle`, `continueSession`, `config.<key>`, `ref.testId` or `ref.group` */
23
+ path: string;
24
+ label?: string;
25
+ }
26
+ /**
27
+ * One setting of a node, as data. The editor decides how to render it. The app's
28
+ * `custom` field type (a React component) is not part of core: the editor attaches
29
+ * those to a kind itself.
30
+ */
31
+ export type Field = (FieldBase & {
32
+ type: "text";
33
+ placeholder?: string;
34
+ default?: string;
35
+ }) | (FieldBase & {
36
+ type: "code";
37
+ placeholder?: string;
38
+ default?: string;
39
+ }) | (FieldBase & {
40
+ type: "expression";
41
+ placeholder?: string;
42
+ default?: string;
43
+ }) | (FieldBase & {
44
+ type: "number";
45
+ unit?: string;
46
+ default?: number;
47
+ }) | (FieldBase & {
48
+ type: "select";
49
+ options: Option[];
50
+ default?: string;
51
+ }) | (FieldBase & {
52
+ type: "segmented";
53
+ options: Option[];
54
+ default?: string;
55
+ }) | (FieldBase & {
56
+ type: "switch";
57
+ default?: boolean;
58
+ }) | (FieldBase & {
59
+ type: "test";
60
+ }) | (FieldBase & {
61
+ type: "group";
62
+ });
63
+ /** How a run treats a node in the graph */
64
+ export type Shape = "start" | "end" | "step" | "branches";
65
+ /** The tones of the design system's flow chips */
66
+ export type BranchTone = "neutral" | "success" | "warning" | "error" | "info";
67
+ /** One named way out of a `branches` node */
68
+ export interface Branch {
69
+ /** Stored as the edge's `sourceHandle` */
70
+ name: string;
71
+ /** The chip on the canvas; leave it out for no chip */
72
+ label?: string;
73
+ /** The chip's colour: a theme tone, or any CSS colour such as "#7c3aed" */
74
+ color?: BranchTone | (string & {});
75
+ }
76
+ /** Branches that come and go: filled ones plus one empty to drop onto, at least `min` in all */
77
+ export interface DynamicBranches {
78
+ dynamic: true;
79
+ min: number;
80
+ }
81
+ /** The type a field's value has in `config` */
82
+ type ValueOf<F> = F extends {
83
+ type: "number";
84
+ } ? number : F extends {
85
+ type: "switch";
86
+ } ? boolean : string;
87
+ /** `config` as the definition's `config.*` fields describe it */
88
+ export type ConfigOf<F extends readonly Field[]> = {
89
+ [K in F[number] as K["path"] extends `config.${infer P}` ? P : never]: ValueOf<K>;
90
+ };
91
+ /** A node whose `config` is typed (and filled from the field defaults) */
92
+ export type TypedNode<C> = Omit<FlowNodeModel, "config"> & {
93
+ config: C;
94
+ };
95
+ /**
96
+ * The core half of a node kind: data and pure functions only. What a kind does in a
97
+ * run is a handler in `flows/run`; how it looks is the editor's half.
98
+ */
99
+ export interface NodeCoreDefinition<K extends string = string, C = Record<string, unknown>> {
100
+ /** Stored in flow files as the node's `kind` */
101
+ kind: K;
102
+ /** The kind's name; messages use it */
103
+ label: string;
104
+ shape: Shape;
105
+ /**
106
+ * `branches`: a fixed list (the first is the one kept on delete), or
107
+ * `{ dynamic: true, min }` for a list that grows as branches are filled, named `branch-<n>`
108
+ */
109
+ branches?: readonly Branch[] | DynamicBranches;
110
+ fields: readonly Field[];
111
+ /** The node can write outputs (shared-context variables) */
112
+ outputs?: boolean;
113
+ /** A step's output names: readable later as `nodes.<key>.<name>` */
114
+ results?: readonly string[];
115
+ /** The path of the field holding the name of a variable the node writes (`vars.<name>`) */
116
+ writesVar?: string;
117
+ /** The node has an "on failure" choice */
118
+ onFailure?: boolean;
119
+ /** Pure: problems with this node alone. `node.config` has the field defaults filled in. */
120
+ validate?: (node: TypedNode<C>, ctx: NodeCtx) => FlowProblem[];
121
+ }
122
+ /** Any definition, for the registry */
123
+ export type AnyNodeDefinition = NodeCoreDefinition<string, any>;
124
+ /** Declares a node kind; the type of `config` comes from the `config.*` fields */
125
+ export declare function defineNode<const K extends string, const F extends readonly Field[]>(def: Omit<NodeCoreDefinition<K, ConfigOf<F>>, "fields"> & {
126
+ fields: F;
127
+ }): NodeCoreDefinition<K, ConfigOf<F>>;
128
+ /** The values the definition's `config.*` fields start with */
129
+ export declare function configDefaults(def: Pick<AnyNodeDefinition, "fields">): Record<string, unknown>;
130
+ /** The node with every field default filled in, so `validate` sees typed config even for old files */
131
+ export declare function withDefaults<C>(def: NodeCoreDefinition<string, C>, node: FlowNodeModel): TypedNode<C>;
132
+ export {};
@@ -0,0 +1,21 @@
1
+ /** Declares a node kind; the type of `config` comes from the `config.*` fields */
2
+ export function defineNode(def) {
3
+ return def;
4
+ }
5
+ /** The values the definition's `config.*` fields start with */
6
+ export function configDefaults(def) {
7
+ const config = {};
8
+ for (const f of def.fields)
9
+ if (f.path.startsWith("config.") &&
10
+ "default" in f &&
11
+ f.default !== undefined)
12
+ config[f.path.slice("config.".length)] = f.default;
13
+ return config;
14
+ }
15
+ /** The node with every field default filled in, so `validate` sees typed config even for old files */
16
+ export function withDefaults(def, node) {
17
+ return {
18
+ ...node,
19
+ config: { ...configDefaults(def), ...node.config },
20
+ };
21
+ }
@@ -0,0 +1 @@
1
+ export declare const end: import("./define.js").NodeCoreDefinition<"end", import("./define.js").ConfigOf<readonly []>>;
@@ -0,0 +1,7 @@
1
+ import { defineNode } from "./define.js";
2
+ export const end = defineNode({
3
+ kind: "end",
4
+ label: "End",
5
+ shape: "end",
6
+ fields: [],
7
+ });
@@ -0,0 +1,5 @@
1
+ export declare const group: import("./define.js").NodeCoreDefinition<"group", import("./define.js").ConfigOf<readonly [{
2
+ readonly path: "ref.group";
3
+ readonly type: "group";
4
+ readonly label: "Group";
5
+ }]>>;
@@ -0,0 +1,18 @@
1
+ import { defineNode } from "./define.js";
2
+ // The app's editor adds the `continueSession` field (a custom component) to this kind.
3
+ export const group = defineNode({
4
+ kind: "group",
5
+ label: "Group",
6
+ shape: "step",
7
+ fields: [{ path: "ref.group", type: "group", label: "Group" }],
8
+ outputs: true,
9
+ validate: (node, ctx) => ctx.groups && !(node.ref?.group && ctx.groups.has(node.ref.group))
10
+ ? [
11
+ {
12
+ level: "error",
13
+ nodeId: node.id,
14
+ message: `The ${ctx.label.toLowerCase()} ${node.ref?.group ?? ""} no longer exists.`,
15
+ },
16
+ ]
17
+ : [],
18
+ });
@@ -0,0 +1,45 @@
1
+ import type { AnyNodeDefinition, Branch, DynamicBranches } from "./define.js";
2
+ declare const registered: readonly [import("./define.js").NodeCoreDefinition<"start", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"end", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"test", import("./define.js").ConfigOf<readonly []>>, import("./define.js").NodeCoreDefinition<"group", import("./define.js").ConfigOf<readonly [{
3
+ readonly path: "ref.group";
4
+ readonly type: "group";
5
+ readonly label: "Group";
6
+ }]>>, import("./define.js").NodeCoreDefinition<"condition", import("./define.js").ConfigOf<readonly [{
7
+ readonly path: "config.expr";
8
+ readonly type: "expression";
9
+ readonly label: "Expression";
10
+ readonly placeholder: "nodes.login.status == \"passed\"";
11
+ readonly default: "";
12
+ }]>>, import("./define.js").NodeCoreDefinition<"parallel", import("./define.js").ConfigOf<readonly []>>];
13
+ export type NodeKind = (typeof registered)[number]["kind"];
14
+ export declare const NODES: readonly AnyNodeDefinition[];
15
+ /** The definition of a kind; throws for an unknown one */
16
+ export declare function nodeDef(kind: string): AnyNodeDefinition;
17
+ /** The kinds core defines itself; every other kind is a step */
18
+ export declare const CLOSED_KINDS: readonly string[];
19
+ /** How a run treats a node in the graph: a kind core doesn't define is a step */
20
+ export declare const shapeOf: (node: {
21
+ kind: string;
22
+ }) => import("./define.js").Shape;
23
+ /** The branch names a dynamic list gives its branches: branch-0, branch-1, ... */
24
+ export declare const dynamicName: (index: number) => string;
25
+ /** The dynamic branch settings of a `branches` node; undefined for a fixed list and any other shape */
26
+ export declare const dynamicOf: (node: {
27
+ kind: string;
28
+ }) => DynamicBranches | undefined;
29
+ /**
30
+ * The branches a new `branches` node starts with, in order: its fixed list, or
31
+ * `min` empty ones for a dynamic list. Empty for any other shape.
32
+ */
33
+ export declare const branchesOf: (node: {
34
+ kind: string;
35
+ }) => readonly Branch[];
36
+ /** The branch an edge out of a `branches` node leaves by (a dynamic list has no labels or colours) */
37
+ export declare const branchOf: (node: {
38
+ kind: string;
39
+ }, handle: string | undefined) => Branch | undefined;
40
+ /** The names of the branches `branchesOf` lists */
41
+ export declare const branchNames: (node: {
42
+ kind: string;
43
+ }) => string[];
44
+ export * from "./define.js";
45
+ export * from "./path.js";
@@ -0,0 +1,55 @@
1
+ import { condition } from "./condition.js";
2
+ import { end } from "./end.js";
3
+ import { group } from "./group.js";
4
+ import { parallel } from "./parallel.js";
5
+ import { start } from "./start.js";
6
+ import { test } from "./test.js";
7
+ // Library order. Every other kind is a step (`flows/steps`).
8
+ const registered = [start, end, test, group, condition, parallel];
9
+ export const NODES = registered;
10
+ /** The definition of a kind; throws for an unknown one */
11
+ export function nodeDef(kind) {
12
+ const def = NODES.find((d) => d.kind === kind);
13
+ if (!def)
14
+ throw new Error(`Unknown node kind: ${kind}`);
15
+ return def;
16
+ }
17
+ /** The definition of a kind core registers; undefined for any other (a step) */
18
+ const closedDef = (kind) => NODES.find((d) => d.kind === kind);
19
+ /** The kinds core defines itself; every other kind is a step */
20
+ export const CLOSED_KINDS = NODES.map((d) => d.kind);
21
+ /** How a run treats a node in the graph: a kind core doesn't define is a step */
22
+ export const shapeOf = (node) => closedDef(node.kind)?.shape ?? "step";
23
+ /** The branch names a dynamic list gives its branches: branch-0, branch-1, ... */
24
+ export const dynamicName = (index) => `branch-${index}`;
25
+ /** The dynamic branch settings of a `branches` node; undefined for a fixed list and any other shape */
26
+ export const dynamicOf = (node) => {
27
+ const def = closedDef(node.kind);
28
+ return def?.shape === "branches" &&
29
+ def.branches &&
30
+ !Array.isArray(def.branches)
31
+ ? def.branches
32
+ : undefined;
33
+ };
34
+ /**
35
+ * The branches a new `branches` node starts with, in order: its fixed list, or
36
+ * `min` empty ones for a dynamic list. Empty for any other shape.
37
+ */
38
+ export const branchesOf = (node) => {
39
+ const def = closedDef(node.kind);
40
+ if (!def || def.shape !== "branches" || !def.branches)
41
+ return [];
42
+ if (Array.isArray(def.branches))
43
+ return def.branches;
44
+ return Array.from({ length: def.branches.min }, (_, i) => ({ name: dynamicName(i) }));
45
+ };
46
+ /** The branch an edge out of a `branches` node leaves by (a dynamic list has no labels or colours) */
47
+ export const branchOf = (node, handle) => dynamicOf(node)
48
+ ? handle
49
+ ? { name: handle }
50
+ : undefined
51
+ : branchesOf(node).find((b) => b.name === handle);
52
+ /** The names of the branches `branchesOf` lists */
53
+ export const branchNames = (node) => branchesOf(node).map((b) => b.name);
54
+ export * from "./define.js";
55
+ export * from "./path.js";
@@ -0,0 +1 @@
1
+ export declare const parallel: import("./define.js").NodeCoreDefinition<"parallel", import("./define.js").ConfigOf<readonly []>>;
@@ -0,0 +1,11 @@
1
+ import { defineNode } from "./define.js";
2
+ // Every branch leads on to the node after the Parallel. Branches are dropped
3
+ // onto one at a time: the list is always its filled branches plus one empty.
4
+ export const parallel = defineNode({
5
+ kind: "parallel",
6
+ label: "Parallel",
7
+ shape: "branches",
8
+ branches: { dynamic: true, min: 2 },
9
+ fields: [],
10
+ onFailure: true,
11
+ });
@@ -0,0 +1,5 @@
1
+ import type { FlowNodeModel } from "../schema.js";
2
+ /** Reads a field's value from the node: `title`, `subtitle`, `continueSession`, `config.*` or `ref.*` */
3
+ export declare function getPath(node: FlowNodeModel, path: string): unknown;
4
+ /** The patch that writes a field's value, keeping the other keys of `config` and `ref` */
5
+ export declare function setPath(node: FlowNodeModel, path: string, value: unknown): Partial<FlowNodeModel>;
@@ -0,0 +1,21 @@
1
+ /** Splits `config.expr` into `["config", "expr"]`; `title` is a top-level key */
2
+ function split(path) {
3
+ const i = path.indexOf(".");
4
+ return i < 0 ? [path, undefined] : [path.slice(0, i), path.slice(i + 1)];
5
+ }
6
+ /** Reads a field's value from the node: `title`, `subtitle`, `continueSession`, `config.*` or `ref.*` */
7
+ export function getPath(node, path) {
8
+ const [head, rest] = split(path);
9
+ const top = node[head];
10
+ if (rest === undefined)
11
+ return top;
12
+ return top?.[rest];
13
+ }
14
+ /** The patch that writes a field's value, keeping the other keys of `config` and `ref` */
15
+ export function setPath(node, path, value) {
16
+ const [head, rest] = split(path);
17
+ if (rest === undefined)
18
+ return { [head]: value };
19
+ const top = node[head];
20
+ return { [head]: { ...top, [rest]: value } };
21
+ }
@@ -0,0 +1 @@
1
+ export declare const start: import("./define.js").NodeCoreDefinition<"start", import("./define.js").ConfigOf<readonly []>>;
@@ -0,0 +1,7 @@
1
+ import { defineNode } from "./define.js";
2
+ export const start = defineNode({
3
+ kind: "start",
4
+ label: "Start",
5
+ shape: "start",
6
+ fields: [],
7
+ });
@@ -0,0 +1 @@
1
+ export declare const test: import("./define.js").NodeCoreDefinition<"test", import("./define.js").ConfigOf<readonly []>>;
@@ -0,0 +1,18 @@
1
+ import { defineNode } from "./define.js";
2
+ // The app's editor adds the `continueSession` field (a custom component) to this kind.
3
+ export const test = defineNode({
4
+ kind: "test",
5
+ label: "Test",
6
+ shape: "step",
7
+ fields: [],
8
+ outputs: true,
9
+ validate: (node, ctx) => ctx.tests && !(node.ref?.testId && ctx.tests.has(node.ref.testId))
10
+ ? [
11
+ {
12
+ level: "error",
13
+ nodeId: node.id,
14
+ message: `A ${ctx.label.toLowerCase()} in this flow was deleted or moved.`,
15
+ },
16
+ ]
17
+ : [],
18
+ });
@@ -0,0 +1,55 @@
1
+ export interface FlowNodeOutput {
2
+ /** Name of the shared-context variable the node writes */
3
+ name: string;
4
+ }
5
+ export interface FlowNodeModel {
6
+ id: string;
7
+ /** Short name expressions use: nodes.<key>. Lowercase letters, digits and _, unique in the flow */
8
+ key?: string;
9
+ /** A registered node kind (see the renderer's node definitions) */
10
+ kind: string;
11
+ title?: string;
12
+ subtitle?: string;
13
+ /** test: the project test id (file::name). group: the group name */
14
+ ref?: {
15
+ testId?: string;
16
+ group?: string;
17
+ };
18
+ outputs?: FlowNodeOutput[];
19
+ /** What to do when the node fails; "stop" ends the run there */
20
+ onFailure: "stop" | "continue";
21
+ /** Continue in the previous node's browser session (kept in the file; the framework opens a fresh browser per test today) */
22
+ continueSession?: boolean;
23
+ /** Per-kind settings, named by each definition's `config.*` fields*/
24
+ config?: Record<string, unknown>;
25
+ position: {
26
+ x: number;
27
+ y: number;
28
+ };
29
+ }
30
+ export interface FlowEdgeModel {
31
+ id: string;
32
+ source: string;
33
+ target: string;
34
+ /** The named branch of a node with branches (a condition's "true" or "false", a parallel's "branch-0") */
35
+ sourceHandle?: string;
36
+ }
37
+ /** One flow, as stored in <project>/flows/<id>.flow.json */
38
+ export interface FlowFile {
39
+ id: string;
40
+ /** The file format version; a missing one means 1. See readFlowFile */
41
+ version?: number;
42
+ name: string;
43
+ description?: string;
44
+ /** The environment the flow runs in; the header's selection otherwise */
45
+ envDefault?: string;
46
+ nodes: FlowNodeModel[];
47
+ edges: FlowEdgeModel[];
48
+ }
49
+ /** The flow file format version this framework writes and reads */
50
+ export declare const FLOW_VERSION = 1;
51
+ /**
52
+ * Read a parsed flow file (JSON.parse output) into a FlowFile.
53
+ * A missing `version` is 1; older files are migrated; a file from a newer SAMURAI throws.
54
+ */
55
+ export declare function readFlowFile(raw: unknown): FlowFile;
@@ -0,0 +1,33 @@
1
+ /** The flow file format version this framework writes and reads */
2
+ export const FLOW_VERSION = 1;
3
+ /** Steps, in order, that lift an older file to FLOW_VERSION. Empty while version 1 is the only one. */
4
+ const MIGRATIONS = [];
5
+ /**
6
+ * Read a parsed flow file (JSON.parse output) into a FlowFile.
7
+ * A missing `version` is 1; older files are migrated; a file from a newer SAMURAI throws.
8
+ */
9
+ export function readFlowFile(raw) {
10
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
11
+ throw new Error("A flow file must be a JSON object.");
12
+ }
13
+ let flow = raw;
14
+ const version = flow.version === undefined ? 1 : flow.version;
15
+ if (typeof version !== "number" ||
16
+ !Number.isInteger(version) ||
17
+ version < 1) {
18
+ throw new Error("The flow file's version must be a positive whole number.");
19
+ }
20
+ if (version > FLOW_VERSION) {
21
+ throw new Error("This flow was saved by a newer SAMURAI. Update @itmetsam/samurai-framework.");
22
+ }
23
+ if (!Array.isArray(flow.nodes))
24
+ throw new Error("A flow file needs a nodes array.");
25
+ if (!Array.isArray(flow.edges))
26
+ throw new Error("A flow file needs an edges array.");
27
+ for (let at = version; at < FLOW_VERSION; at++) {
28
+ const step = MIGRATIONS.find((m) => m.from === at);
29
+ if (step)
30
+ flow = step.migrate(flow);
31
+ }
32
+ return { ...flow, version: FLOW_VERSION };
33
+ }
@@ -0,0 +1,42 @@
1
+ import type { Field, FlowProblem, NodeCoreDefinition } from "./nodes/define.js";
2
+ import type { FlowNodeModel } from "./schema.js";
3
+ /** Rules a step field carries: checked live in the builder and again by checkFlow */
4
+ export interface FieldRules {
5
+ required?: boolean;
6
+ /** `number` fields */
7
+ min?: number;
8
+ max?: number;
9
+ /** `text` fields: a regular expression the value must match */
10
+ pattern?: string;
11
+ /** Said instead of the default when `pattern` doesn't match */
12
+ patternMessage?: string;
13
+ }
14
+ export type StepField = Field & FieldRules;
15
+ export type StepSection = "control" | "integrations";
16
+ export interface StepOutput {
17
+ name: string;
18
+ label?: string;
19
+ }
20
+ /** A step as data: what the app's catalog holds. No functions. */
21
+ export interface StepInfo {
22
+ /** Stored in flow files as the node's `kind` */
23
+ kind: string;
24
+ label: string;
25
+ /** A design-system icon name */
26
+ icon: string;
27
+ section: StepSection;
28
+ /** `config.*` fields; Title and Description are added by `stepNodeOf` */
29
+ fields: readonly StepField[];
30
+ /** The line under the title: `{config.x}` is replaced with the field's value */
31
+ summary?: string;
32
+ outputs?: readonly StepOutput[];
33
+ /** The path of the field holding a variable name the step writes */
34
+ writesVar?: string;
35
+ }
36
+ export declare const STEP_KIND: RegExp;
37
+ /** A node's own result fields: an output can't take these names */
38
+ export declare const RESERVED_OUTPUTS: readonly string[];
39
+ /** Problems with `node`'s values against the fields' rules, as "Label: message" */
40
+ export declare function fieldProblems(fields: readonly StepField[], node: FlowNodeModel, label: string): FlowProblem[];
41
+ /** The definition core's walker and checkFlow use for a step; `validate` is the step's own (engine only) */
42
+ export declare function stepNodeOf(info: StepInfo, validate?: NodeCoreDefinition["validate"]): NodeCoreDefinition;
@@ -0,0 +1,47 @@
1
+ import { getPath } from "./nodes/path.js";
2
+ export const STEP_KIND = /^[a-z][a-z0-9-]*$/;
3
+ /** A node's own result fields: an output can't take these names */
4
+ export const RESERVED_OUTPUTS = ["status", "durationMs", "error"];
5
+ const TITLE_FIELDS = [
6
+ { path: "title", type: "text", label: "Title" },
7
+ { path: "subtitle", type: "text", label: "Description" },
8
+ ];
9
+ const empty = (value) => value === undefined || (typeof value === "string" && value.trim() === "");
10
+ /** Problems with `node`'s values against the fields' rules, as "Label: message" */
11
+ export function fieldProblems(fields, node, label) {
12
+ const problems = [];
13
+ const add = (message) => problems.push({ level: "error", nodeId: node.id, message: `${label}: ${message}` });
14
+ for (const f of fields) {
15
+ const name = f.label ?? f.path;
16
+ const value = getPath(node, f.path);
17
+ if (f.required && empty(value)) {
18
+ add(`${name} is required.`);
19
+ continue;
20
+ }
21
+ if (typeof value === "number") {
22
+ if (f.min !== undefined && value < f.min)
23
+ add(`${name} must be at least ${f.min}.`);
24
+ if (f.max !== undefined && value > f.max)
25
+ add(`${name} must be at most ${f.max}.`);
26
+ }
27
+ if (f.pattern && typeof value === "string" && value !== "" && !new RegExp(f.pattern).test(value))
28
+ add(f.patternMessage ?? `${name} doesn't match ${f.pattern}.`);
29
+ }
30
+ return problems;
31
+ }
32
+ /** The definition core's walker and checkFlow use for a step; `validate` is the step's own (engine only) */
33
+ export function stepNodeOf(info, validate) {
34
+ return {
35
+ kind: info.kind,
36
+ label: info.label,
37
+ shape: "step",
38
+ fields: [...TITLE_FIELDS, ...info.fields],
39
+ onFailure: true,
40
+ results: (info.outputs ?? []).map((o) => o.name),
41
+ ...(info.writesVar !== undefined && { writesVar: info.writesVar }),
42
+ validate: (node, ctx) => [
43
+ ...fieldProblems(info.fields, node, ctx.label),
44
+ ...(validate?.(node, ctx) ?? []),
45
+ ],
46
+ };
47
+ }
@@ -0,0 +1,45 @@
1
+ import type { RunEvent } from "../../runner/reporter.js";
2
+ export type NodeStatus = "passed" | "failed" | "skipped" | "not-run";
3
+ export interface NodeResult {
4
+ status: NodeStatus;
5
+ durationMs?: number;
6
+ error?: string;
7
+ /** Why it was skipped, e.g. "Not supported yet" */
8
+ note?: string;
9
+ /** Condition: the branch taken */
10
+ branch?: string;
11
+ /** What the node wrote to vars */
12
+ wrote?: Record<string, unknown>;
13
+ /** A step's outputs, readable later as `nodes.<key>.<name>` */
14
+ outputs?: Record<string, unknown>;
15
+ }
16
+ /** One test of a node: absolute file and full name */
17
+ export interface NodeTest {
18
+ file: string;
19
+ name: string;
20
+ }
21
+ export type FlowEvent = {
22
+ type: "flow-start";
23
+ environment: string;
24
+ } | {
25
+ type: "node-start";
26
+ nodeId: string;
27
+ } | {
28
+ type: "node-end";
29
+ nodeId: string;
30
+ result: NodeResult;
31
+ } | {
32
+ type: "test";
33
+ nodeId: string;
34
+ event: RunEvent;
35
+ } | {
36
+ type: "flow-end";
37
+ summary: FlowSummary;
38
+ };
39
+ export interface FlowSummary {
40
+ status: "passed" | "failed" | "cancelled";
41
+ nodes: Record<string, NodeResult>;
42
+ vars: Record<string, unknown>;
43
+ /** The flow couldn't be walked at all */
44
+ error?: string;
45
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { type FlowFile } from "../core/index.js";
2
+ export declare const FLOW_SUFFIX = ".flow.json";
3
+ /** The folder a project keeps its flows in */
4
+ export declare const flowsDir: (projectDir: string) => string;
5
+ /** The ids of the project's flows (file name without `.flow.json`), sorted. Other files are ignored; no folder means no flows. */
6
+ export declare function listFlows(projectDir: string): Promise<string[]>;
7
+ /** Reads `<project>/flows/<id>.flow.json`; the flow's `id` is the file name, whatever the file says */
8
+ export declare function readFlow(projectDir: string, id: string): Promise<FlowFile>;
9
+ /** Reads a `.flow.json` file by its path; the flow's `id` is `id`, else the file name without the suffix */
10
+ export declare function readFlowPath(file: string, id?: string): Promise<FlowFile>;
11
+ /** Reads every flow of the project, in id order; one that can't be read throws */
12
+ export declare function readFlows(projectDir: string): Promise<FlowFile[]>;