adfinem 0.0.0 → 0.1.1

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 (112) hide show
  1. package/.env.example +13 -0
  2. package/CHANGELOG.md +17 -0
  3. package/CODE_OF_CONDUCT.md +21 -0
  4. package/CONTRIBUTING.md +29 -0
  5. package/LICENSE +21 -0
  6. package/README.md +97 -3
  7. package/SECURITY.md +13 -0
  8. package/catalogs/.gitkeep +0 -0
  9. package/catalogs/api-operations.yaml +21 -0
  10. package/catalogs/batches.yaml +74 -0
  11. package/catalogs/queries.yaml +75 -0
  12. package/config/environments.yaml +13 -0
  13. package/dist/actions/assert-db.js +3 -0
  14. package/dist/actions/run-eod.js +3 -0
  15. package/dist/adapters/api/api-collections.js +296 -0
  16. package/dist/adapters/api/body-utils.js +9 -0
  17. package/dist/adapters/api/rest-client.js +557 -0
  18. package/dist/adapters/api/soap-client.js +5 -0
  19. package/dist/adapters/db/assertions.js +87 -0
  20. package/dist/adapters/db/oracle-client.js +115 -0
  21. package/dist/adapters/db/query-catalog.js +75 -0
  22. package/dist/adapters/unix/batch-catalog.js +71 -0
  23. package/dist/adapters/unix/batch-input-files.js +36 -0
  24. package/dist/adapters/unix/batch-runner.js +382 -0
  25. package/dist/adapters/unix/ssh-client.js +228 -0
  26. package/dist/app/server.js +827 -0
  27. package/dist/cli.js +516 -0
  28. package/dist/config/environments.js +138 -0
  29. package/dist/config/registry.js +18 -0
  30. package/dist/config/secrets.js +123 -0
  31. package/dist/dsl/parser.js +20 -0
  32. package/dist/dsl/schema.js +182 -0
  33. package/dist/dsl/types.js +1 -0
  34. package/dist/dsl/validator.js +264 -0
  35. package/dist/engine/captures.js +68 -0
  36. package/dist/engine/context.js +69 -0
  37. package/dist/engine/evidence.js +33 -0
  38. package/dist/engine/known-errors.js +129 -0
  39. package/dist/engine/retry.js +13 -0
  40. package/dist/engine/runner.js +710 -0
  41. package/dist/engine/step-result.js +58 -0
  42. package/dist/flows/catalog-normalizer.js +72 -0
  43. package/dist/flows/compiler.js +237 -0
  44. package/dist/flows/concat.js +130 -0
  45. package/dist/flows/parser.js +21 -0
  46. package/dist/flows/schema.js +142 -0
  47. package/dist/flows/types.js +1 -0
  48. package/dist/flows/validator.js +470 -0
  49. package/dist/reports/html-report.js +112 -0
  50. package/dist/reports/junit-report.js +48 -0
  51. package/docs/.gitkeep +0 -0
  52. package/docs/DB_UNIX_OPERATIONS.md +118 -0
  53. package/docs/FLOW_BUILDER.md +87 -0
  54. package/flows/account_processing_cycle.flow.yaml +88 -0
  55. package/flows/new_flow.flow.yaml +22 -0
  56. package/package.json +98 -11
  57. package/scenarios/smoke/account-processing-smoke.yaml +44 -0
  58. package/scenarios/smoke/api-db-batch-check.yaml +40 -0
  59. package/src/actions/assert-db.ts +6 -0
  60. package/src/actions/run-eod.ts +6 -0
  61. package/src/adapters/api/api-collections.ts +375 -0
  62. package/src/adapters/api/body-utils.ts +10 -0
  63. package/src/adapters/api/rest-client.ts +587 -0
  64. package/src/adapters/api/soap-client.ts +7 -0
  65. package/src/adapters/db/assertions.ts +83 -0
  66. package/src/adapters/db/oracle-client.ts +133 -0
  67. package/src/adapters/db/query-catalog.ts +80 -0
  68. package/src/adapters/unix/batch-catalog.ts +81 -0
  69. package/src/adapters/unix/batch-input-files.ts +39 -0
  70. package/src/adapters/unix/batch-runner.ts +456 -0
  71. package/src/adapters/unix/ssh-client.ts +248 -0
  72. package/src/app/server.ts +914 -0
  73. package/src/cli.ts +517 -0
  74. package/src/config/environments.ts +193 -0
  75. package/src/config/registry.ts +23 -0
  76. package/src/config/secrets.ts +128 -0
  77. package/src/dsl/parser.ts +24 -0
  78. package/src/dsl/schema.ts +189 -0
  79. package/src/dsl/types.ts +371 -0
  80. package/src/dsl/validator.ts +282 -0
  81. package/src/engine/captures.ts +66 -0
  82. package/src/engine/context.ts +76 -0
  83. package/src/engine/evidence.ts +35 -0
  84. package/src/engine/known-errors.ts +145 -0
  85. package/src/engine/retry.ts +11 -0
  86. package/src/engine/runner.ts +746 -0
  87. package/src/engine/step-result.ts +64 -0
  88. package/src/flows/catalog-normalizer.ts +86 -0
  89. package/src/flows/compiler.ts +247 -0
  90. package/src/flows/concat.ts +149 -0
  91. package/src/flows/parser.ts +27 -0
  92. package/src/flows/schema.ts +154 -0
  93. package/src/flows/types.ts +130 -0
  94. package/src/flows/validator.ts +468 -0
  95. package/src/llm/system-prompt.md +9 -0
  96. package/src/reports/html-report.ts +113 -0
  97. package/src/reports/junit-report.ts +55 -0
  98. package/src/types/oracledb.d.ts +1 -0
  99. package/templates/.gitkeep +0 -0
  100. package/templates/api/create-test-case.json +5 -0
  101. package/templates/api/record-test-activity.json +6 -0
  102. package/tsconfig.json +15 -0
  103. package/vite.config.ts +17 -0
  104. package/web/index.html +12 -0
  105. package/web/src/App.tsx +6588 -0
  106. package/web/src/main.tsx +10 -0
  107. package/web/src/styles.css +3147 -0
  108. package/web-dist/assets/elk.bundled-ChwRCIWJ.js +24 -0
  109. package/web-dist/assets/index-CArbX4zm.css +1 -0
  110. package/web-dist/assets/index-vDCbj8xB.js +28 -0
  111. package/web-dist/index.html +13 -0
  112. package/index.js +0 -1
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import { explainKnownError } from "./known-errors.js";
3
+ export function hashInput(value) {
4
+ return createHash("sha256").update(JSON.stringify(value ?? {})).digest("hex");
5
+ }
6
+ export function durationBetween(startedAt, endedAt) {
7
+ const startedMs = Date.parse(startedAt);
8
+ const endedMs = Date.parse(endedAt);
9
+ if (!Number.isFinite(startedMs) || !Number.isFinite(endedMs))
10
+ return undefined;
11
+ return Math.max(0, endedMs - startedMs);
12
+ }
13
+ export function failedStep(stepId, layer, startedAt, input, error, evidence = []) {
14
+ const err = error instanceof Error ? error : new Error(String(error));
15
+ const explained = explainKnownError(err);
16
+ const endedAt = new Date().toISOString();
17
+ return {
18
+ stepId,
19
+ layer,
20
+ status: "failed",
21
+ startedAt,
22
+ endedAt,
23
+ durationMs: durationBetween(startedAt, endedAt),
24
+ inputHash: hashInput(input),
25
+ captures: {},
26
+ evidence,
27
+ error: {
28
+ message: explained.message,
29
+ stack: err.stack,
30
+ rawOutput: explained.hints.length > 0 ? `Hints:\n${explained.hints.map((hint) => `- ${hint}`).join("\n")}` : undefined
31
+ }
32
+ };
33
+ }
34
+ export function cancelledStep(stepId, layer, startedAt, input, evidence = []) {
35
+ const endedAt = new Date().toISOString();
36
+ return {
37
+ stepId,
38
+ layer,
39
+ status: "cancelled",
40
+ startedAt,
41
+ endedAt,
42
+ durationMs: durationBetween(startedAt, endedAt),
43
+ inputHash: hashInput(input),
44
+ captures: {},
45
+ evidence,
46
+ error: {
47
+ message: "Run was stopped by user request."
48
+ }
49
+ };
50
+ }
51
+ export function isCancellationError(error) {
52
+ return error instanceof Error && error.name === "AbortError";
53
+ }
54
+ export function cancellationError() {
55
+ const error = new Error("Run was stopped by user request.");
56
+ error.name = "AbortError";
57
+ return error;
58
+ }
@@ -0,0 +1,72 @@
1
+ import { normalizeBindParamRecord } from "../adapters/db/query-catalog.js";
2
+ import { batchInputFileParamNames } from "../adapters/unix/batch-input-files.js";
3
+ export function normalizeFlowCatalogParams(flow, catalogs) {
4
+ return {
5
+ ...flow,
6
+ nodes: flow.nodes.map((node) => normalizeNode(node, flow, catalogs))
7
+ };
8
+ }
9
+ function normalizeNode(node, flow, catalogs) {
10
+ if (node.type === "api_operation") {
11
+ return {
12
+ ...node,
13
+ postActions: node.postActions?.map((action) => normalizeAction(action, flow, catalogs))
14
+ };
15
+ }
16
+ if (node.type === "parallel") {
17
+ return {
18
+ ...node,
19
+ branches: node.branches.map((branch) => ({
20
+ ...branch,
21
+ nodes: branch.nodes.map((child) => normalizeNode(child, flow, catalogs))
22
+ }))
23
+ };
24
+ }
25
+ if (node.type === "loop") {
26
+ return {
27
+ ...node,
28
+ nodes: node.nodes.map((child) => normalizeNode(child, flow, catalogs))
29
+ };
30
+ }
31
+ return normalizeAction(node, flow, catalogs);
32
+ }
33
+ function normalizeAction(node, flow, catalogs) {
34
+ if (node.type === "db_query" || node.type === "db_assert" || node.type === "db_execute") {
35
+ const specs = catalogs.queries[node.query]?.params;
36
+ if (!specs || Object.keys(specs).length === 0)
37
+ return node;
38
+ return {
39
+ ...node,
40
+ params: normalizeNamedParams(normalizeBindParamRecord(node.params ?? node.input ?? {}), normalizeBindParamRecord(specs))
41
+ };
42
+ }
43
+ if (node.type === "unix_batch") {
44
+ const batch = catalogs.batches[node.batch];
45
+ const args = batch?.args ?? [];
46
+ const fileParamNames = batchInputFileParamNames(batch);
47
+ if (args.length === 0 && fileParamNames.length === 0)
48
+ return node;
49
+ return {
50
+ ...node,
51
+ params: normalizeNamedArgs(node.params ?? node.input ?? {}, args, fileParamNames)
52
+ };
53
+ }
54
+ return node;
55
+ }
56
+ function normalizeNamedParams(current, specs) {
57
+ const normalized = {};
58
+ for (const [name, spec] of Object.entries(specs)) {
59
+ if (current[name] !== undefined) {
60
+ normalized[name] = current[name];
61
+ continue;
62
+ }
63
+ if (spec.required) {
64
+ normalized[name] = "";
65
+ }
66
+ }
67
+ return normalized;
68
+ }
69
+ function normalizeNamedArgs(current, args, extraNames = []) {
70
+ const allowed = new Set([...args.map((arg) => arg.name), ...extraNames]);
71
+ return Object.fromEntries(Object.entries(current).filter(([name]) => allowed.has(name)));
72
+ }
@@ -0,0 +1,237 @@
1
+ export function compileFlow(flow, options = {}) {
2
+ const environment = options.environment ?? flow.environment;
3
+ const effectiveFlow = applyFlowEnvironmentInputs(flow, environment);
4
+ const steps = [];
5
+ const stepMap = [];
6
+ for (const node of orderedNodes(effectiveFlow)) {
7
+ if (node.disabled)
8
+ continue;
9
+ const step = compileNode(node);
10
+ steps.push(step);
11
+ stepMap.push({ flowNodeId: node.id, scenarioStepId: step.id, type: node.type });
12
+ if (node.type === "api_operation") {
13
+ for (const postAction of node.postActions ?? []) {
14
+ if (postAction.disabled)
15
+ continue;
16
+ const postStep = compileNode(postAction, node);
17
+ steps.push(postStep);
18
+ stepMap.push({
19
+ flowNodeId: postAction.id,
20
+ scenarioStepId: postStep.id,
21
+ type: postAction.type,
22
+ postActionOf: node.id
23
+ });
24
+ }
25
+ }
26
+ appendControlStepMap(node, stepMap);
27
+ }
28
+ const scenario = {
29
+ id: effectiveFlow.id,
30
+ environment,
31
+ variables: effectiveFlow.variables,
32
+ steps
33
+ };
34
+ return { flow: effectiveFlow, scenario, stepMap };
35
+ }
36
+ export function applyFlowEnvironmentInputs(flow, environment = flow.environment) {
37
+ const inputSet = flow.environmentInputs?.[environment];
38
+ if (!inputSet)
39
+ return flow;
40
+ const nodeInputs = inputSet.nodes ?? {};
41
+ return {
42
+ ...flow,
43
+ variables: mergeRecords(flow.variables, inputSet.variables),
44
+ nodes: flow.nodes.map((node) => applyNodeEnvironmentInputs(node, nodeInputs))
45
+ };
46
+ }
47
+ export function orderedNodes(flow) {
48
+ if (!flow.edges?.length)
49
+ return flow.nodes;
50
+ if (flow.ui?.manualEdges)
51
+ return flow.nodes;
52
+ return linearOrderFromEdges(flow) ?? flow.nodes;
53
+ }
54
+ export function isCompleteLinearEdgeChain(flow) {
55
+ if (!flow.edges?.length)
56
+ return true;
57
+ if (flow.ui?.manualEdges)
58
+ return false;
59
+ return Boolean(linearOrderFromEdges(flow));
60
+ }
61
+ function linearOrderFromEdges(flow) {
62
+ if (!flow.edges?.length)
63
+ return flow.nodes;
64
+ if (flow.nodes.length <= 1)
65
+ return flow.nodes;
66
+ if (flow.edges.length !== flow.nodes.length - 1)
67
+ return undefined;
68
+ const byId = new Map(flow.nodes.map((node) => [node.id, node]));
69
+ const outgoing = new Map();
70
+ const incoming = new Set();
71
+ for (const edge of flow.edges) {
72
+ if (!byId.has(edge.from) || !byId.has(edge.to) || edge.from === edge.to)
73
+ return undefined;
74
+ if (outgoing.has(edge.from) || incoming.has(edge.to))
75
+ return undefined;
76
+ outgoing.set(edge.from, edge.to);
77
+ incoming.add(edge.to);
78
+ }
79
+ const starts = flow.nodes.filter((node) => !incoming.has(node.id));
80
+ if (starts.length !== 1)
81
+ return undefined;
82
+ const ordered = [];
83
+ const seen = new Set();
84
+ let current = starts[0];
85
+ while (current && !seen.has(current.id)) {
86
+ ordered.push(current);
87
+ seen.add(current.id);
88
+ const nextId = outgoing.get(current.id);
89
+ current = nextId ? byId.get(nextId) : undefined;
90
+ }
91
+ return seen.size === flow.nodes.length ? ordered : undefined;
92
+ }
93
+ function compileNode(node, postActionOf) {
94
+ if (node.type === "api_operation") {
95
+ return cleanStep({
96
+ id: node.id,
97
+ action: node.operation,
98
+ via: "api",
99
+ input: node.input ?? node.params,
100
+ request: node.request,
101
+ assertions: node.assertions,
102
+ capture: node.capture,
103
+ continueOnFailure: node.continueOnFailure,
104
+ expectedOutcome: node.expectedOutcome,
105
+ captureOnFailure: node.captureOnFailure
106
+ });
107
+ }
108
+ if (node.type === "db_query" || node.type === "db_assert" || node.type === "db_execute") {
109
+ return cleanStep({
110
+ id: node.id,
111
+ action: node.type,
112
+ via: "db",
113
+ query: node.query,
114
+ params: node.params ?? node.input,
115
+ assertions: node.assertions,
116
+ capture: node.capture,
117
+ continueOnFailure: node.continueOnFailure,
118
+ expectedOutcome: node.expectedOutcome,
119
+ captureOnFailure: node.captureOnFailure
120
+ });
121
+ }
122
+ if (node.type === "unix_batch") {
123
+ return cleanStep({
124
+ id: node.id,
125
+ action: "unix_batch",
126
+ via: "unix",
127
+ batch: node.batch,
128
+ params: node.params ?? node.input,
129
+ retry: node.retry,
130
+ assertions: node.assertions,
131
+ capture: node.capture,
132
+ continueOnFailure: node.continueOnFailure,
133
+ expectedOutcome: node.expectedOutcome,
134
+ captureOnFailure: node.captureOnFailure
135
+ });
136
+ }
137
+ if (node.type === "parallel") {
138
+ return cleanStep({
139
+ id: node.id,
140
+ action: "__parallel",
141
+ via: "control",
142
+ control: "parallel",
143
+ join: node.join ?? "all",
144
+ continueOnFailure: node.continueOnFailure,
145
+ branches: node.branches.map((branch) => ({
146
+ id: branch.id,
147
+ label: branch.label,
148
+ steps: branch.nodes.filter((child) => !child.disabled).map((child) => compileNode(child))
149
+ }))
150
+ });
151
+ }
152
+ if (node.type === "loop") {
153
+ return cleanStep({
154
+ id: node.id,
155
+ action: "__loop",
156
+ via: "control",
157
+ control: "loop",
158
+ loop: node.loop,
159
+ continueOnFailure: node.continueOnFailure,
160
+ steps: node.nodes.filter((child) => !child.disabled).map((child) => compileNode(child))
161
+ });
162
+ }
163
+ const unsupported = node;
164
+ throw new Error(`Unsupported flow node type '${unsupported.type ?? "unknown"}'${postActionOf ? ` after '${postActionOf.id}'` : ""}.`);
165
+ }
166
+ function applyNodeEnvironmentInputs(node, nodeInputs) {
167
+ if (node.type === "api_operation") {
168
+ const overrides = nodeInputs[node.id];
169
+ return {
170
+ ...node,
171
+ input: mergeRecords(node.input ?? node.params, overrides),
172
+ postActions: node.postActions?.map((action) => applyActionEnvironmentInputs(action, nodeInputs))
173
+ };
174
+ }
175
+ if (node.type === "parallel") {
176
+ return {
177
+ ...node,
178
+ branches: node.branches.map((branch) => ({
179
+ ...branch,
180
+ nodes: branch.nodes.map((child) => applyNodeEnvironmentInputs(child, nodeInputs))
181
+ }))
182
+ };
183
+ }
184
+ if (node.type === "loop") {
185
+ return {
186
+ ...node,
187
+ nodes: node.nodes.map((child) => applyNodeEnvironmentInputs(child, nodeInputs))
188
+ };
189
+ }
190
+ return applyActionEnvironmentInputs(node, nodeInputs);
191
+ }
192
+ function applyActionEnvironmentInputs(node, nodeInputs) {
193
+ const overrides = nodeInputs[node.id];
194
+ if (!overrides)
195
+ return node;
196
+ return {
197
+ ...node,
198
+ params: mergeRecords(node.params ?? node.input, overrides)
199
+ };
200
+ }
201
+ function mergeRecords(base, overrides) {
202
+ if (!base && !overrides)
203
+ return undefined;
204
+ return {
205
+ ...(base ?? {}),
206
+ ...(overrides ?? {})
207
+ };
208
+ }
209
+ function cleanStep(step) {
210
+ return Object.fromEntries(Object.entries(step).filter(([, value]) => value !== undefined));
211
+ }
212
+ export function flowNodeOutputPrefix(id) {
213
+ return `${id}.`;
214
+ }
215
+ export function nodeTypeLabel(type) {
216
+ return type.replace(/_/g, " ");
217
+ }
218
+ function appendControlStepMap(node, stepMap) {
219
+ if (node.type === "parallel") {
220
+ for (const branch of node.branches) {
221
+ for (const child of branch.nodes) {
222
+ if (child.disabled)
223
+ continue;
224
+ stepMap.push({ flowNodeId: child.id, scenarioStepId: child.id, type: child.type });
225
+ appendControlStepMap(child, stepMap);
226
+ }
227
+ }
228
+ }
229
+ if (node.type === "loop") {
230
+ for (const child of node.nodes) {
231
+ if (child.disabled)
232
+ continue;
233
+ stepMap.push({ flowNodeId: child.id, scenarioStepId: child.id, type: child.type });
234
+ appendControlStepMap(child, stepMap);
235
+ }
236
+ }
237
+ }
@@ -0,0 +1,130 @@
1
+ import { orderedNodes } from "./compiler.js";
2
+ export function concatFlows(flows, options) {
3
+ if (flows.length < 2)
4
+ throw new Error("At least two flows are required to concatenate.");
5
+ const environment = options.environment ?? commonEnvironment(flows);
6
+ const variables = mergeVariables(flows, Boolean(options.allowVariableOverrides));
7
+ const usedIds = new Set();
8
+ const nodes = [];
9
+ for (const flow of flows) {
10
+ const sourceNodes = orderedNodes(flow);
11
+ const sourceIds = collectNodeIds(sourceNodes);
12
+ const needsPrefix = prefixRequired(options.nodePrefixMode ?? "auto", sourceIds, usedIds);
13
+ const prefix = `${sanitizeId(flow.id)}_`;
14
+ const idMap = new Map();
15
+ for (const id of sourceIds) {
16
+ const mapped = needsPrefix ? uniqueId(`${prefix}${id}`, usedIds) : uniqueId(id, usedIds);
17
+ idMap.set(id, mapped);
18
+ usedIds.add(mapped);
19
+ }
20
+ for (const node of sourceNodes) {
21
+ nodes.push(rewriteNode(node, idMap));
22
+ }
23
+ }
24
+ return {
25
+ version: 1,
26
+ id: options.id,
27
+ name: options.name,
28
+ environment,
29
+ variables,
30
+ nodes,
31
+ edges: buildTimelineEdges(nodes)
32
+ };
33
+ }
34
+ function commonEnvironment(flows) {
35
+ const environments = [...new Set(flows.map((flow) => flow.environment))];
36
+ if (environments.length === 1)
37
+ return environments[0];
38
+ throw new Error(`Input flows use different environments (${environments.join(", ")}). Pass --env to choose the generated flow environment.`);
39
+ }
40
+ function mergeVariables(flows, allowOverrides) {
41
+ const merged = {};
42
+ const sources = new Map();
43
+ for (const flow of flows) {
44
+ for (const [name, value] of Object.entries(flow.variables ?? {})) {
45
+ if (Object.prototype.hasOwnProperty.call(merged, name) && stableJson(merged[name]) !== stableJson(value) && !allowOverrides) {
46
+ throw new Error(`Variable '${name}' has different values in '${sources.get(name)}' and '${flow.id}'. Pass --allow-variable-overrides to keep the later value.`);
47
+ }
48
+ merged[name] = value;
49
+ sources.set(name, flow.id);
50
+ }
51
+ }
52
+ return Object.keys(merged).length ? merged : undefined;
53
+ }
54
+ function prefixRequired(mode, sourceIds, usedIds) {
55
+ if (mode === "always")
56
+ return true;
57
+ if (mode === "never")
58
+ return false;
59
+ for (const id of sourceIds) {
60
+ if (usedIds.has(id))
61
+ return true;
62
+ }
63
+ return false;
64
+ }
65
+ function collectNodeIds(nodes) {
66
+ const ids = new Set();
67
+ for (const node of nodes) {
68
+ ids.add(node.id);
69
+ if (node.type === "api_operation") {
70
+ for (const postAction of node.postActions ?? [])
71
+ ids.add(postAction.id);
72
+ }
73
+ }
74
+ return ids;
75
+ }
76
+ function rewriteNode(node, idMap) {
77
+ const rewritten = rewriteValue(node, idMap);
78
+ rewritten.id = idMap.get(node.id) ?? node.id;
79
+ if (rewritten.type === "api_operation") {
80
+ rewritten.postActions = rewritten.postActions?.map((action) => ({
81
+ ...action,
82
+ id: idMap.get(action.id) ?? action.id
83
+ }));
84
+ }
85
+ return rewritten;
86
+ }
87
+ function rewriteValue(value, idMap) {
88
+ if (typeof value === "string")
89
+ return rewriteReferences(value, idMap);
90
+ if (Array.isArray(value))
91
+ return value.map((entry) => rewriteValue(entry, idMap));
92
+ if (value && typeof value === "object") {
93
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, rewriteValue(entry, idMap)]));
94
+ }
95
+ return value;
96
+ }
97
+ function rewriteReferences(value, idMap) {
98
+ return value.replace(/\$\{([A-Za-z0-9_-]+)\.([A-Za-z0-9_.-]+)\}/g, (match, nodeId, outputName) => {
99
+ const mapped = idMap.get(nodeId);
100
+ return mapped ? `\${${mapped}.${outputName}}` : match;
101
+ });
102
+ }
103
+ function buildTimelineEdges(nodes) {
104
+ if (nodes.length < 2)
105
+ return undefined;
106
+ return nodes.slice(0, -1).map((node, index) => ({ from: node.id, to: nodes[index + 1].id }));
107
+ }
108
+ function uniqueId(seed, usedIds) {
109
+ let candidate = sanitizeId(seed);
110
+ let suffix = 2;
111
+ while (usedIds.has(candidate)) {
112
+ candidate = `${sanitizeId(seed)}_${suffix}`;
113
+ suffix += 1;
114
+ }
115
+ return candidate;
116
+ }
117
+ function sanitizeId(value) {
118
+ const normalized = value.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
119
+ return normalized || "flow";
120
+ }
121
+ function stableJson(value) {
122
+ if (!value || typeof value !== "object")
123
+ return JSON.stringify(value);
124
+ if (Array.isArray(value))
125
+ return `[${value.map(stableJson).join(",")}]`;
126
+ return `{${Object.entries(value)
127
+ .sort(([a], [b]) => a.localeCompare(b))
128
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
129
+ .join(",")}}`;
130
+ }
@@ -0,0 +1,21 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import YAML from "yaml";
4
+ import { flowFileSchema } from "./schema.js";
5
+ const yamlOptions = { defaultStringType: "PLAIN", defaultKeyType: "PLAIN", lineWidth: 0 };
6
+ export async function loadFlow(path) {
7
+ const parsed = YAML.parse(await readFile(path, "utf8"));
8
+ return flowFileSchema.parse(parsed);
9
+ }
10
+ export async function readFlowSource(path) {
11
+ return readFile(path, "utf8");
12
+ }
13
+ export async function writeFlow(path, flow) {
14
+ const outputPath = resolve(path);
15
+ await mkdir(dirname(outputPath), { recursive: true });
16
+ const validated = flowFileSchema.parse(flow);
17
+ await writeFile(outputPath, YAML.stringify(validated, yamlOptions), "utf8");
18
+ }
19
+ export function flowToYaml(flow) {
20
+ return YAML.stringify(flowFileSchema.parse(flow), yamlOptions);
21
+ }
@@ -0,0 +1,142 @@
1
+ import { z } from "zod";
2
+ import { apiAssertionSchema, apiRequestSpecSchema } from "../dsl/schema.js";
3
+ const scalar = z.union([z.string(), z.number(), z.boolean(), z.null()]);
4
+ const jsonValue = z.lazy(() => z.union([
5
+ scalar,
6
+ z.array(jsonValue),
7
+ z.record(jsonValue)
8
+ ]));
9
+ const id = z.string().min(1).regex(/^[A-Za-z0-9_-]+$/);
10
+ const mappingObject = z.record(jsonValue);
11
+ const captureObject = z.record(z.string().min(1));
12
+ const environmentInputSetSchema = z.object({
13
+ variables: mappingObject.optional(),
14
+ nodes: z.record(mappingObject).optional()
15
+ }).strict();
16
+ const retrySchema = z.object({
17
+ attempts: z.number().int().positive().optional(),
18
+ delaySeconds: z.number().nonnegative().optional()
19
+ }).strict();
20
+ const expectedOutcomeSchema = z.enum(["positive", "negative", "setup", "teardown"]);
21
+ const loopDateFormatSchema = z.enum(["YYYY-MM-DD", "DD/MM/YYYY", "MM/DD/YYYY"]);
22
+ const loopDateCursorSchema = z.object({
23
+ outputName: z.string().min(1).optional(),
24
+ start: z.string().min(1).optional(),
25
+ inputFormat: loopDateFormatSchema.optional(),
26
+ outputFormat: loopDateFormatSchema.optional(),
27
+ advance: z.object({
28
+ mode: z.enum(["days", "months", "nth_day_of_month", "first_of_month", "end_of_month"]),
29
+ amount: z.number().int().positive().optional(),
30
+ day: z.number().int().min(1).max(31).optional()
31
+ }).strict().optional()
32
+ }).strict();
33
+ const loopSpecSchema = z.object({
34
+ mode: z.enum(["count", "foreach"]),
35
+ count: z.union([z.number().int().nonnegative(), z.string()]).optional(),
36
+ items: jsonValue.optional(),
37
+ itemName: z.string().min(1).optional(),
38
+ maxIterations: z.number().int().positive().optional(),
39
+ dateCursor: loopDateCursorSchema.optional()
40
+ }).strict();
41
+ const parallelJoinModeSchema = z.enum(["all", "any", "fail_fast"]);
42
+ const actionBase = {
43
+ id,
44
+ label: z.string().min(1).optional(),
45
+ input: mappingObject.optional(),
46
+ params: mappingObject.optional(),
47
+ request: apiRequestSpecSchema.optional(),
48
+ assertions: z.array(apiAssertionSchema).optional(),
49
+ capture: captureObject.optional(),
50
+ continueOnFailure: z.boolean().optional(),
51
+ expectedOutcome: expectedOutcomeSchema.optional(),
52
+ captureOnFailure: z.boolean().optional(),
53
+ disabled: z.boolean().optional(),
54
+ section: z.string().min(1).optional()
55
+ };
56
+ export const flowDbQueryNodeSchema = z.object({
57
+ ...actionBase,
58
+ type: z.literal("db_query"),
59
+ query: z.string().min(1)
60
+ }).strict();
61
+ export const flowDbAssertNodeSchema = z.object({
62
+ ...actionBase,
63
+ type: z.literal("db_assert"),
64
+ query: z.string().min(1)
65
+ }).strict();
66
+ export const flowDbExecuteNodeSchema = z.object({
67
+ ...actionBase,
68
+ type: z.literal("db_execute"),
69
+ query: z.string().min(1)
70
+ }).strict();
71
+ export const flowUnixBatchNodeSchema = z.object({
72
+ ...actionBase,
73
+ type: z.literal("unix_batch"),
74
+ batch: z.string().min(1),
75
+ retry: retrySchema.optional()
76
+ }).strict();
77
+ export const flowActionNodeSchema = z.discriminatedUnion("type", [
78
+ flowDbQueryNodeSchema,
79
+ flowDbAssertNodeSchema,
80
+ flowDbExecuteNodeSchema,
81
+ flowUnixBatchNodeSchema
82
+ ]);
83
+ export const flowApiOperationNodeSchema = z.object({
84
+ id,
85
+ label: z.string().min(1).optional(),
86
+ type: z.literal("api_operation"),
87
+ operation: z.string().min(1),
88
+ input: mappingObject.optional(),
89
+ params: mappingObject.optional(),
90
+ request: apiRequestSpecSchema.optional(),
91
+ assertions: z.array(apiAssertionSchema).optional(),
92
+ capture: captureObject.optional(),
93
+ continueOnFailure: z.boolean().optional(),
94
+ expectedOutcome: expectedOutcomeSchema.optional(),
95
+ captureOnFailure: z.boolean().optional(),
96
+ disabled: z.boolean().optional(),
97
+ section: z.string().min(1).optional(),
98
+ postActions: z.array(flowActionNodeSchema).optional()
99
+ }).strict();
100
+ export const flowNodeSchema = z.lazy(() => z.discriminatedUnion("type", [
101
+ flowApiOperationNodeSchema,
102
+ flowDbQueryNodeSchema,
103
+ flowDbAssertNodeSchema,
104
+ flowDbExecuteNodeSchema,
105
+ flowUnixBatchNodeSchema,
106
+ z.object({
107
+ ...actionBase,
108
+ type: z.literal("parallel"),
109
+ branches: z.array(z.object({
110
+ id,
111
+ label: z.string().min(1).optional(),
112
+ nodes: z.array(flowNodeSchema)
113
+ }).strict()).min(1),
114
+ join: parallelJoinModeSchema.optional()
115
+ }).strict(),
116
+ z.object({
117
+ ...actionBase,
118
+ type: z.literal("loop"),
119
+ loop: loopSpecSchema,
120
+ nodes: z.array(flowNodeSchema).min(1)
121
+ }).strict()
122
+ ]));
123
+ export const flowFileSchema = z.object({
124
+ version: z.literal(1),
125
+ id,
126
+ name: z.string().min(1).optional(),
127
+ environment: z.string().min(1),
128
+ variables: mappingObject.optional(),
129
+ environmentInputs: z.record(environmentInputSetSchema).optional(),
130
+ nodes: z.array(flowNodeSchema),
131
+ edges: z.array(z.object({
132
+ from: id,
133
+ to: id
134
+ }).strict()).optional(),
135
+ ui: z.object({
136
+ positions: z.record(z.object({
137
+ x: z.number(),
138
+ y: z.number()
139
+ }).strict()).optional(),
140
+ manualEdges: z.boolean().optional()
141
+ }).strict().optional()
142
+ }).strict();
@@ -0,0 +1 @@
1
+ export {};