@dypai-ai/workflow-core 0.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.
Files changed (38) hide show
  1. package/fixtures/capability-catalog.json +126 -0
  2. package/fixtures/legacy-create-booking.yaml +40 -0
  3. package/package.json +40 -0
  4. package/src/adapters/adapters.test.ts +168 -0
  5. package/src/adapters/engineBinding.ts +35 -0
  6. package/src/adapters/flowDefinitionToIr.ts +141 -0
  7. package/src/adapters/irToWorkflowCodeV2.ts +293 -0
  8. package/src/adapters/legacyYamlToIr.ts +340 -0
  9. package/src/adapters/placeholderToRef.ts +74 -0
  10. package/src/adapters/refToLegacyPlaceholder.ts +33 -0
  11. package/src/adapters/sqlBuilders.ts +81 -0
  12. package/src/adapters/triggers.ts +86 -0
  13. package/src/adapters/types.ts +15 -0
  14. package/src/adapters/workflowCodeTypes.ts +45 -0
  15. package/src/capabilities/agentBrief.ts +42 -0
  16. package/src/capabilities/capabilities.test.ts +126 -0
  17. package/src/capabilities/capabilityRegistry.ts +112 -0
  18. package/src/capabilities/catalogSchema.ts +14 -0
  19. package/src/capabilities/fromCatalog.ts +30 -0
  20. package/src/capabilities/index.ts +35 -0
  21. package/src/capabilities/types.ts +57 -0
  22. package/src/fixtures/createBooking.flow.ts +64 -0
  23. package/src/fixtures/createBooking.ir.ts +103 -0
  24. package/src/fixtures/listBookings.ir.ts +61 -0
  25. package/src/index.ts +172 -0
  26. package/src/ir/refs.ts +103 -0
  27. package/src/ir/schema.ts +149 -0
  28. package/src/ir/sourceMap.ts +59 -0
  29. package/src/ir/types.ts +147 -0
  30. package/src/ir/validate.test.ts +181 -0
  31. package/src/ir/validate.ts +365 -0
  32. package/src/registry/defineNode.ts +19 -0
  33. package/src/registry/nodeRegistry.ts +87 -0
  34. package/src/registry/nodes/dypaiDb.ts +164 -0
  35. package/src/registry/nodes/dypaiEmail.ts +57 -0
  36. package/src/registry/nodes/dypaiFlow.ts +25 -0
  37. package/src/registry/nodes/legacyWorkflow.ts +27 -0
  38. package/tsconfig.json +12 -0
@@ -0,0 +1,64 @@
1
+ import type { FlowDefinition } from "../adapters/flowDefinitionToIr";
2
+
3
+ export const createBookingFlowDefinition: FlowDefinition = {
4
+ name: "create-booking",
5
+ source: {
6
+ kind: "flow-ts",
7
+ file: "dypai/flows/create-booking.flow.ts",
8
+ },
9
+ trigger: { type: "http", method: "POST" },
10
+ auth: { mode: "public" },
11
+ input: {
12
+ type: "object",
13
+ required: ["name", "email", "date"],
14
+ additionalProperties: false,
15
+ properties: {
16
+ name: { type: "string" },
17
+ email: { type: "string", format: "email" },
18
+ date: { type: "string", format: "date" },
19
+ },
20
+ },
21
+ output: {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ booking: {
26
+ type: "object",
27
+ properties: {
28
+ id: { type: "string" },
29
+ name: { type: "string" },
30
+ email: { type: "string" },
31
+ date: { type: "string" },
32
+ },
33
+ },
34
+ },
35
+ },
36
+ response: { cardinality: "single" },
37
+ steps: [
38
+ {
39
+ id: "booking",
40
+ call: "db.insert",
41
+ config: {
42
+ table: "public.bookings",
43
+ values: {
44
+ name: { kind: "input", path: ["name"] },
45
+ email: { kind: "input", path: ["email"] },
46
+ date: { kind: "input", path: ["date"] },
47
+ },
48
+ returning: ["id", "name", "email", "date"],
49
+ },
50
+ source: {
51
+ file: "dypai/flows/create-booking.flow.ts",
52
+ line: 17,
53
+ stepId: "booking",
54
+ },
55
+ },
56
+ ],
57
+ returns: {
58
+ booking: { kind: "step", stepId: "booking", path: ["row"] },
59
+ },
60
+ edges: [
61
+ { from: "$trigger", to: "booking" },
62
+ { from: "booking", to: "return" },
63
+ ],
64
+ };
@@ -0,0 +1,103 @@
1
+ import type { WorkflowIR } from "../ir/types";
2
+ import { WORKFLOW_IR_VERSION } from "../ir/types";
3
+
4
+ export const createBookingWorkflowIR: WorkflowIR = {
5
+ version: WORKFLOW_IR_VERSION,
6
+ name: "create-booking",
7
+ source: {
8
+ kind: "flow-ts",
9
+ file: "dypai/flows/create-booking.flow.ts",
10
+ compiledAt: "2026-05-21T00:00:00.000Z",
11
+ },
12
+ trigger: { type: "http", method: "POST" },
13
+ auth: { mode: "public" },
14
+ inputSchema: {
15
+ type: "object",
16
+ required: ["name", "email", "date"],
17
+ additionalProperties: false,
18
+ properties: {
19
+ name: { type: "string" },
20
+ email: { type: "string", format: "email" },
21
+ date: { type: "string", format: "date" },
22
+ },
23
+ },
24
+ outputSchema: {
25
+ type: "object",
26
+ additionalProperties: false,
27
+ properties: {
28
+ booking: {
29
+ type: "object",
30
+ additionalProperties: false,
31
+ properties: {
32
+ id: { type: "string" },
33
+ name: { type: "string" },
34
+ email: { type: "string" },
35
+ date: { type: "string" },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ response: { cardinality: "single" },
41
+ steps: [
42
+ {
43
+ id: "booking",
44
+ node: "dypai.db",
45
+ operation: "insert",
46
+ version: 1,
47
+ config: {
48
+ table: "public.bookings",
49
+ values: {
50
+ name: { kind: "input", path: ["name"] },
51
+ email: { kind: "input", path: ["email"] },
52
+ date: { kind: "input", path: ["date"] },
53
+ },
54
+ returning: ["id", "name", "email", "date"],
55
+ },
56
+ source: {
57
+ file: "dypai/flows/create-booking.flow.ts",
58
+ line: 17,
59
+ stepId: "booking",
60
+ },
61
+ },
62
+ {
63
+ id: "respond",
64
+ node: "dypai.flow",
65
+ operation: "return",
66
+ version: 1,
67
+ config: {},
68
+ return: true,
69
+ source: {
70
+ file: "dypai/flows/create-booking.flow.ts",
71
+ line: 28,
72
+ stepId: "respond",
73
+ },
74
+ },
75
+ ],
76
+ edges: [
77
+ { from: "$trigger", to: "booking" },
78
+ { from: "booking", to: "respond" },
79
+ ],
80
+ responseMap: {
81
+ booking: { kind: "step", stepId: "booking", path: ["row"] },
82
+ },
83
+ };
84
+
85
+ export const createBookingWorkflowIRMissingTable: WorkflowIR = {
86
+ ...createBookingWorkflowIR,
87
+ steps: [
88
+ {
89
+ ...createBookingWorkflowIR.steps[0],
90
+ config: {
91
+ values: {
92
+ name: { kind: "input", path: ["name"] },
93
+ },
94
+ },
95
+ source: {
96
+ file: "dypai/flows/create-booking.flow.ts",
97
+ line: 17,
98
+ stepId: "booking",
99
+ },
100
+ },
101
+ createBookingWorkflowIR.steps[1],
102
+ ],
103
+ };
@@ -0,0 +1,61 @@
1
+ import type { WorkflowIR } from "../ir/types";
2
+ import { WORKFLOW_IR_VERSION } from "../ir/types";
3
+
4
+ export const listBookingsWorkflowIR: WorkflowIR = {
5
+ version: WORKFLOW_IR_VERSION,
6
+ name: "list-bookings",
7
+ source: {
8
+ kind: "flow-ts",
9
+ file: "dypai/flows/list-bookings.flow.ts",
10
+ },
11
+ trigger: { type: "http", method: "GET" },
12
+ auth: { mode: "jwt", allowedRoles: ["authenticated"] },
13
+ inputSchema: {
14
+ type: "object",
15
+ additionalProperties: false,
16
+ properties: {
17
+ limit: { type: "integer" },
18
+ offset: { type: "integer" },
19
+ },
20
+ },
21
+ outputSchema: {
22
+ type: "object",
23
+ additionalProperties: false,
24
+ properties: {
25
+ bookings: { type: "array" },
26
+ limit: { type: "integer" },
27
+ offset: { type: "integer" },
28
+ },
29
+ },
30
+ response: { cardinality: "many" },
31
+ steps: [
32
+ {
33
+ id: "bookings",
34
+ node: "dypai.db",
35
+ operation: "list",
36
+ version: 1,
37
+ config: {
38
+ table: "public.bookings",
39
+ where: {
40
+ user_id: { kind: "currentUserId" },
41
+ },
42
+ orderBy: "created_at DESC",
43
+ limit: { kind: "input", path: ["limit"] },
44
+ offset: { kind: "input", path: ["offset"] },
45
+ },
46
+ },
47
+ {
48
+ id: "respond",
49
+ node: "dypai.flow",
50
+ operation: "return",
51
+ version: 1,
52
+ config: {},
53
+ return: true,
54
+ },
55
+ ],
56
+ responseMap: {
57
+ bookings: { kind: "step", stepId: "bookings", path: ["rows"] },
58
+ limit: { kind: "input", path: ["limit"] },
59
+ offset: { kind: "input", path: ["offset"] },
60
+ },
61
+ };
package/src/index.ts ADDED
@@ -0,0 +1,172 @@
1
+ export {
2
+ WORKFLOW_IR_VERSION,
3
+ type AuthIR,
4
+ type IRDiagnostic,
5
+ type IRValidationResult,
6
+ type JsonSchema,
7
+ type NodeDefinition,
8
+ type OperationDefinition,
9
+ type RefIR,
10
+ type RegistryLookup,
11
+ type ResourceIR,
12
+ type ResponseIR,
13
+ type SideEffectKind,
14
+ type SourceLocation,
15
+ type TriggerIR,
16
+ type WorkflowEdgeIR,
17
+ type WorkflowIR,
18
+ type WorkflowIRVersion,
19
+ type WorkflowSource,
20
+ type WorkflowStepIR,
21
+ } from "./ir/types";
22
+
23
+ export {
24
+ collectRefs,
25
+ describeRef,
26
+ isRefIR,
27
+ refToPlaceholder,
28
+ validateRefIR,
29
+ type RefValidationContext,
30
+ } from "./ir/refs";
31
+
32
+ export {
33
+ formatDiagnosticMessage,
34
+ formatSourceLocation,
35
+ stepLocation,
36
+ withSourceLocation,
37
+ } from "./ir/sourceMap";
38
+
39
+ export {
40
+ isRecord,
41
+ isValidEndpointName,
42
+ isValidHttpMethod,
43
+ isWorkflowIR,
44
+ minimalValidWorkflowIR,
45
+ schemaPropertyNames,
46
+ schemaRequired,
47
+ validateJsonValueAgainstSchema,
48
+ type JsonSchemaIssue,
49
+ } from "./ir/schema";
50
+
51
+ export {
52
+ assertValidWorkflowIR,
53
+ validateWorkflowIR,
54
+ type ValidateWorkflowOptions,
55
+ } from "./ir/validate";
56
+
57
+ export {
58
+ defineNode,
59
+ defineOperation,
60
+ mergeDiagnostics,
61
+ operationKey,
62
+ } from "./registry/defineNode";
63
+
64
+ export {
65
+ defaultNodeRegistry,
66
+ dypaiDbNode,
67
+ dypaiEmailNode,
68
+ dypaiFlowNode,
69
+ legacyWorkflowNode,
70
+ NodeRegistry,
71
+ } from "./registry/nodeRegistry";
72
+
73
+ export {
74
+ createBookingWorkflowIR,
75
+ createBookingWorkflowIRMissingTable,
76
+ } from "./fixtures/createBooking.ir";
77
+
78
+ export {
79
+ pendingAdapters,
80
+ type AdapterDeclaration,
81
+ type AdapterRegistry,
82
+ } from "./adapters/types";
83
+
84
+ export type {
85
+ AdaptWorkflowCodeResult,
86
+ AdapterResult,
87
+ EngineWorkflowEdge,
88
+ EngineWorkflowNode,
89
+ ImportYamlResult,
90
+ WorkflowCodeV2,
91
+ } from "./adapters/workflowCodeTypes";
92
+
93
+ export {
94
+ legacyYamlDocumentToIr,
95
+ legacyYamlToIr,
96
+ } from "./adapters/legacyYamlToIr";
97
+
98
+ export {
99
+ flowDefinitionToIr,
100
+ resolveFlowAlias,
101
+ type FlowDefinition,
102
+ type FlowStepDefinition,
103
+ } from "./adapters/flowDefinitionToIr";
104
+
105
+ export {
106
+ irToWorkflowCodeV2,
107
+ stableStringifyWorkflowCode,
108
+ } from "./adapters/irToWorkflowCodeV2";
109
+
110
+ export {
111
+ refToLegacyPlaceholder,
112
+ responseMapToComposeFields,
113
+ serializeConfig,
114
+ serializeConfigValue,
115
+ } from "./adapters/refToLegacyPlaceholder";
116
+
117
+ export {
118
+ buildInsertSql,
119
+ buildListSql,
120
+ buildUpdateSql,
121
+ } from "./adapters/sqlBuilders";
122
+
123
+ export {
124
+ composeFieldsToResponseMap,
125
+ mapObjectPlaceholdersToRefs,
126
+ placeholderToRef,
127
+ } from "./adapters/placeholderToRef";
128
+
129
+ export {
130
+ authFromLegacyTrigger,
131
+ normalizeHttpMethod,
132
+ responseCardinalityFromLegacy,
133
+ triggerFromLegacyDoc,
134
+ triggersToEngineConfig,
135
+ } from "./adapters/triggers";
136
+
137
+ export {
138
+ createBookingFlowDefinition,
139
+ } from "./fixtures/createBooking.flow";
140
+
141
+ export {
142
+ listBookingsWorkflowIR,
143
+ } from "./fixtures/listBookings.ir";
144
+
145
+ export type {
146
+ CapabilityCallResolution,
147
+ CapabilityCatalog,
148
+ CapabilityGroupJson,
149
+ CapabilityLookup,
150
+ CapabilityOperationJson,
151
+ EngineBindingJson,
152
+ } from "./capabilities";
153
+
154
+ export {
155
+ CapabilityRegistry,
156
+ createCapabilityRegistry,
157
+ flattenCapabilityOperations,
158
+ generateCapabilityBrief,
159
+ generateCapabilityBriefFromCatalog,
160
+ isCapabilityCatalog,
161
+ mergeNodeRegistryDefinitions,
162
+ nodeDefinitionsFromCatalog,
163
+ resolveCapabilityCall,
164
+ } from "./capabilities";
165
+
166
+ export type { AdaptWorkflowCodeOptions } from "./adapters/irToWorkflowCodeV2";
167
+ export type { FlowCompileContext } from "./adapters/flowDefinitionToIr";
168
+
169
+ export {
170
+ applyParameterMap,
171
+ buildEngineParameters,
172
+ } from "./adapters/engineBinding";
package/src/ir/refs.ts ADDED
@@ -0,0 +1,103 @@
1
+ import type { RefIR } from "./types";
2
+
3
+ const REF_KINDS = new Set(["input", "currentUserId", "step", "literal", "env"]);
4
+
5
+ export function isRefIR(value: unknown): value is RefIR {
6
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
7
+ const kind = (value as Record<string, unknown>).kind;
8
+ return typeof kind === "string" && REF_KINDS.has(kind);
9
+ }
10
+
11
+ export function refToPlaceholder(ref: RefIR): string {
12
+ switch (ref.kind) {
13
+ case "input":
14
+ return `\${input.${ref.path.join(".")}}`;
15
+ case "currentUserId":
16
+ return "${current_user_id}";
17
+ case "step":
18
+ return ref.path?.length
19
+ ? `\${vars.${ref.stepId}.${ref.path.join(".")}}`
20
+ : `\${vars.${ref.stepId}}`;
21
+ case "literal":
22
+ return typeof ref.value === "string" ? ref.value : JSON.stringify(ref.value);
23
+ case "env":
24
+ return `\${env.${ref.name}}`;
25
+ default:
26
+ return "${unknown}";
27
+ }
28
+ }
29
+
30
+ export function describeRef(ref: RefIR): string {
31
+ switch (ref.kind) {
32
+ case "input":
33
+ return `input.${ref.path.join(".")}`;
34
+ case "currentUserId":
35
+ return "currentUser.id";
36
+ case "step":
37
+ return ref.path?.length
38
+ ? `steps.${ref.stepId}.${ref.path.join(".")}`
39
+ : `steps.${ref.stepId}`;
40
+ case "literal":
41
+ return `literal(${JSON.stringify(ref.value)})`;
42
+ case "env":
43
+ return `env.${ref.name}`;
44
+ default:
45
+ return "unknown ref";
46
+ }
47
+ }
48
+
49
+ export function collectRefs(value: unknown, out: RefIR[] = []): RefIR[] {
50
+ if (isRefIR(value)) {
51
+ out.push(value);
52
+ return out;
53
+ }
54
+ if (Array.isArray(value)) {
55
+ for (const item of value) collectRefs(item, out);
56
+ return out;
57
+ }
58
+ if (value && typeof value === "object") {
59
+ for (const nested of Object.values(value as Record<string, unknown>)) {
60
+ collectRefs(nested, out);
61
+ }
62
+ }
63
+ return out;
64
+ }
65
+
66
+ export type RefValidationContext = {
67
+ inputSchemaProperties: Set<string>;
68
+ stepIds: Set<string>;
69
+ };
70
+
71
+ export function validateRefIR(
72
+ ref: RefIR,
73
+ ctx: RefValidationContext,
74
+ path: string,
75
+ ): string | null {
76
+ switch (ref.kind) {
77
+ case "input": {
78
+ if (!ref.path.length) return `${path}: input ref requires at least one path segment`;
79
+ const root = ref.path[0];
80
+ if (!ctx.inputSchemaProperties.has(root)) {
81
+ return `${path}: input ref '${ref.path.join(".")}' is not declared in inputSchema`;
82
+ }
83
+ return null;
84
+ }
85
+ case "currentUserId":
86
+ return null;
87
+ case "step": {
88
+ if (!ref.stepId.trim()) return `${path}: step ref requires stepId`;
89
+ if (!ctx.stepIds.has(ref.stepId)) {
90
+ return `${path}: step ref '${ref.stepId}' does not match any workflow step`;
91
+ }
92
+ return null;
93
+ }
94
+ case "literal":
95
+ return null;
96
+ case "env": {
97
+ if (!ref.name.trim()) return `${path}: env ref requires name`;
98
+ return null;
99
+ }
100
+ default:
101
+ return `${path}: unknown ref kind`;
102
+ }
103
+ }
@@ -0,0 +1,149 @@
1
+ import { WORKFLOW_IR_VERSION } from "./types";
2
+ import type { JsonSchema, WorkflowIR } from "./types";
3
+
4
+ const ENDPOINT_NAME_PATTERN = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
5
+ const HTTP_METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
6
+
7
+ export function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ export function isWorkflowIR(value: unknown): value is WorkflowIR {
12
+ if (!isRecord(value)) return false;
13
+ return value.version === WORKFLOW_IR_VERSION
14
+ && typeof value.name === "string"
15
+ && isRecord(value.trigger)
16
+ && isRecord(value.auth)
17
+ && isRecord(value.inputSchema)
18
+ && isRecord(value.outputSchema)
19
+ && isRecord(value.response)
20
+ && Array.isArray(value.steps)
21
+ && isRecord(value.responseMap);
22
+ }
23
+
24
+ export function isValidEndpointName(name: string): boolean {
25
+ return ENDPOINT_NAME_PATTERN.test(name);
26
+ }
27
+
28
+ export function isValidHttpMethod(method: string): method is "GET" | "POST" | "PUT" | "PATCH" | "DELETE" {
29
+ return HTTP_METHODS.has(method);
30
+ }
31
+
32
+ export function schemaPropertyNames(schema: JsonSchema): Set<string> {
33
+ const properties = schema.properties;
34
+ if (!isRecord(properties)) return new Set();
35
+ return new Set(Object.keys(properties));
36
+ }
37
+
38
+ export function schemaRequired(schema: JsonSchema): string[] {
39
+ const required = schema.required;
40
+ if (!Array.isArray(required)) return [];
41
+ return required.filter((item): item is string => typeof item === "string");
42
+ }
43
+
44
+ export type JsonSchemaIssue = {
45
+ path: string;
46
+ message: string;
47
+ };
48
+
49
+ export function validateJsonValueAgainstSchema(
50
+ value: unknown,
51
+ schema: JsonSchema,
52
+ path = "$",
53
+ ): JsonSchemaIssue[] {
54
+ const issues: JsonSchemaIssue[] = [];
55
+
56
+ if (schema.const !== undefined && value !== schema.const) {
57
+ issues.push({ path, message: `expected const ${JSON.stringify(schema.const)}` });
58
+ return issues;
59
+ }
60
+
61
+ const declaredType = typeof schema.type === "string" ? schema.type : undefined;
62
+ if (declaredType === "object") {
63
+ if (!isRecord(value)) {
64
+ issues.push({ path, message: "expected object" });
65
+ return issues;
66
+ }
67
+ for (const key of schemaRequired(schema)) {
68
+ if (!(key in value)) {
69
+ issues.push({ path: `${path}.${key}`, message: "required property is missing" });
70
+ }
71
+ }
72
+ const properties = isRecord(schema.properties) ? schema.properties : {};
73
+ for (const [key, propertySchema] of Object.entries(properties)) {
74
+ if (!(key in value)) continue;
75
+ if (!isRecord(propertySchema)) continue;
76
+ issues.push(...validateJsonValueAgainstSchema(value[key], propertySchema, `${path}.${key}`));
77
+ }
78
+ if (schema.additionalProperties === false) {
79
+ for (const key of Object.keys(value)) {
80
+ if (!(key in properties)) {
81
+ issues.push({ path: `${path}.${key}`, message: "additional property is not allowed" });
82
+ }
83
+ }
84
+ }
85
+ return issues;
86
+ }
87
+
88
+ if (declaredType === "array") {
89
+ if (!Array.isArray(value)) {
90
+ issues.push({ path, message: "expected array" });
91
+ return issues;
92
+ }
93
+ const items = isRecord(schema.items) ? schema.items : null;
94
+ if (items) {
95
+ value.forEach((item, index) => {
96
+ issues.push(...validateJsonValueAgainstSchema(item, items, `${path}[${index}]`));
97
+ });
98
+ }
99
+ return issues;
100
+ }
101
+
102
+ if (declaredType === "string") {
103
+ if (typeof value !== "string") issues.push({ path, message: "expected string" });
104
+ return issues;
105
+ }
106
+
107
+ if (declaredType === "number" || declaredType === "integer") {
108
+ if (typeof value !== "number" || Number.isNaN(value)) {
109
+ issues.push({ path, message: `expected ${declaredType}` });
110
+ }
111
+ return issues;
112
+ }
113
+
114
+ if (declaredType === "boolean") {
115
+ if (typeof value !== "boolean") issues.push({ path, message: "expected boolean" });
116
+ return issues;
117
+ }
118
+
119
+ return issues;
120
+ }
121
+
122
+ export function minimalValidWorkflowIR(): WorkflowIR {
123
+ return {
124
+ version: WORKFLOW_IR_VERSION,
125
+ name: "ping",
126
+ trigger: { type: "http", method: "GET" },
127
+ auth: { mode: "public" },
128
+ inputSchema: { type: "object", properties: {} },
129
+ outputSchema: {
130
+ type: "object",
131
+ additionalProperties: false,
132
+ properties: { ok: { type: "boolean" } },
133
+ },
134
+ response: { cardinality: "single" },
135
+ steps: [
136
+ {
137
+ id: "respond",
138
+ node: "dypai.flow",
139
+ operation: "return",
140
+ version: 1,
141
+ config: {},
142
+ return: true,
143
+ },
144
+ ],
145
+ responseMap: {
146
+ ok: { kind: "literal", value: true },
147
+ },
148
+ };
149
+ }
@@ -0,0 +1,59 @@
1
+ import type { IRDiagnostic, SourceLocation } from "./types";
2
+
3
+ export function formatSourceLocation(location: SourceLocation | undefined): string | undefined {
4
+ if (!location) return undefined;
5
+ if (location.file && location.line) {
6
+ const column = location.column ? `:${location.column}` : "";
7
+ return `${location.file}:${location.line}${column}`;
8
+ }
9
+ if (location.file && location.yamlPath) {
10
+ return `${location.file} ${location.yamlPath}`;
11
+ }
12
+ if (location.yamlPath) return location.yamlPath;
13
+ if (location.file) return location.file;
14
+ if (location.stepId) return `step "${location.stepId}"`;
15
+ return undefined;
16
+ }
17
+
18
+ export function diagnosticLocation(
19
+ base: SourceLocation | undefined,
20
+ stepSource?: SourceLocation,
21
+ ): SourceLocation | undefined {
22
+ return {
23
+ ...base,
24
+ ...stepSource,
25
+ file: stepSource?.file ?? base?.file,
26
+ line: stepSource?.line ?? base?.line,
27
+ column: stepSource?.column ?? base?.column,
28
+ yamlPath: stepSource?.yamlPath ?? base?.yamlPath,
29
+ stepId: stepSource?.stepId ?? base?.stepId,
30
+ };
31
+ }
32
+
33
+ export function formatDiagnosticMessage(
34
+ diagnostic: Pick<IRDiagnostic, "message" | "location">,
35
+ ): string {
36
+ const where = formatSourceLocation(diagnostic.location);
37
+ return where ? `${where}\n${diagnostic.message}` : diagnostic.message;
38
+ }
39
+
40
+ export function withSourceLocation(
41
+ diagnostic: IRDiagnostic,
42
+ location: SourceLocation | undefined,
43
+ ): IRDiagnostic {
44
+ if (!location) return diagnostic;
45
+ return {
46
+ ...diagnostic,
47
+ location: diagnosticLocation(diagnostic.location, location),
48
+ };
49
+ }
50
+
51
+ export function stepLocation(
52
+ workflowSource: SourceLocation | undefined,
53
+ step: { id: string; source?: SourceLocation },
54
+ ): SourceLocation {
55
+ return diagnosticLocation(workflowSource, {
56
+ stepId: step.id,
57
+ ...step.source,
58
+ }) ?? { stepId: step.id };
59
+ }