@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,126 @@
1
+ {
2
+ "version": "capability-catalog.v1",
3
+ "engine_version": "test-fixture",
4
+ "content_hash": "fixture-capability-catalog",
5
+ "groups": [
6
+ {
7
+ "id": "dypai.db",
8
+ "namespace": "dypai",
9
+ "resource": "db",
10
+ "version": 1,
11
+ "label": "DYPAI Database",
12
+ "category": "database",
13
+ "docs": "Declarative and SQL database operations backed by Postgres.",
14
+ "operations": [
15
+ {
16
+ "id": "dypai.db.query",
17
+ "group_id": "dypai.db",
18
+ "operation": "query",
19
+ "name": "Run SQL query",
20
+ "authoring_signature": "dypai.db.query({ sql, params? })",
21
+ "input_schema": {
22
+ "type": "object",
23
+ "required": ["sql"],
24
+ "properties": {
25
+ "sql": { "type": "string" },
26
+ "params": { "type": "object" }
27
+ }
28
+ },
29
+ "side_effects": ["db.read"],
30
+ "engine_binding": {
31
+ "node_type": "dypai_database",
32
+ "operation": "query",
33
+ "adapter": "dypai.db"
34
+ }
35
+ },
36
+ {
37
+ "id": "dypai.db.insert",
38
+ "group_id": "dypai.db",
39
+ "operation": "insert",
40
+ "name": "Insert row",
41
+ "authoring_signature": "dypai.db.insert({ table, values, returning? })",
42
+ "input_schema": {
43
+ "type": "object",
44
+ "required": ["table", "values"],
45
+ "properties": {
46
+ "table": { "type": "string" },
47
+ "values": { "type": "object" },
48
+ "returning": { "type": "array", "items": { "type": "string" } }
49
+ }
50
+ },
51
+ "side_effects": ["db.write"],
52
+ "engine_binding": {
53
+ "node_type": "dypai_database",
54
+ "operation": "query",
55
+ "adapter": "dypai.db"
56
+ }
57
+ }
58
+ ]
59
+ },
60
+ {
61
+ "id": "stripe.checkout",
62
+ "namespace": "stripe",
63
+ "resource": "checkout",
64
+ "version": 1,
65
+ "label": "Stripe Checkout",
66
+ "category": "payments",
67
+ "provider": "Stripe",
68
+ "operations": [
69
+ {
70
+ "id": "stripe.checkout.createSession",
71
+ "group_id": "stripe.checkout",
72
+ "operation": "createSession",
73
+ "name": "Create checkout session",
74
+ "authoring_signature": "stripe.checkout.createSession({ mode, price_id, success_url, cancel_url, metadata? })",
75
+ "input_schema": {
76
+ "type": "object",
77
+ "required": ["mode", "price_id", "success_url", "cancel_url"],
78
+ "properties": {
79
+ "mode": { "type": "string" },
80
+ "price_id": { "type": "string" },
81
+ "success_url": { "type": "string" },
82
+ "cancel_url": { "type": "string" },
83
+ "metadata": { "type": "object" }
84
+ }
85
+ },
86
+ "required_credentials": ["stripe"],
87
+ "engine_binding": {
88
+ "node_type": "stripe",
89
+ "operation": "stripe_create_checkout_session"
90
+ }
91
+ }
92
+ ]
93
+ },
94
+ {
95
+ "id": "google.sheets",
96
+ "namespace": "google",
97
+ "resource": "sheets",
98
+ "version": 1,
99
+ "label": "Google Sheets",
100
+ "category": "data",
101
+ "operations": [
102
+ {
103
+ "id": "google.sheets.readRows",
104
+ "group_id": "google.sheets",
105
+ "operation": "readRows",
106
+ "name": "Read rows",
107
+ "authoring_signature": "google.sheets.readRows({ spreadsheet_id, sheet_name?, range? })",
108
+ "input_schema": {
109
+ "type": "object",
110
+ "required": ["spreadsheet_id"],
111
+ "properties": {
112
+ "spreadsheet_id": { "type": "string" },
113
+ "sheet_name": { "type": "string" },
114
+ "range": { "type": "string" }
115
+ }
116
+ },
117
+ "required_credentials": ["google_sheets"],
118
+ "engine_binding": {
119
+ "node_type": "google_sheets",
120
+ "operation": "read_rows"
121
+ }
122
+ }
123
+ ]
124
+ }
125
+ ]
126
+ }
@@ -0,0 +1,40 @@
1
+ name: create-booking
2
+ method: POST
3
+ trigger:
4
+ http_api:
5
+ auth_mode: public
6
+ input:
7
+ type: object
8
+ required: [name, email, date]
9
+ additionalProperties: false
10
+ properties:
11
+ name:
12
+ type: string
13
+ email:
14
+ type: string
15
+ format: email
16
+ date:
17
+ type: string
18
+ format: date
19
+ output:
20
+ type: object
21
+ additionalProperties: false
22
+ properties:
23
+ id:
24
+ type: string
25
+ name:
26
+ type: string
27
+ email:
28
+ type: string
29
+ date:
30
+ type: string
31
+ response_cardinality: single
32
+ workflow:
33
+ nodes:
34
+ - id: booking
35
+ type: dypai_database
36
+ operation: query
37
+ query: |
38
+ INSERT INTO public.bookings (name, email, date)
39
+ VALUES (${input.name}, ${input.email}, ${input.date})
40
+ RETURNING id, name, email, date
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@dypai-ai/workflow-core",
3
+ "version": "0.1.0",
4
+ "description": "Workflow IR, adapters, and validation for DYPAI Flow endpoint authoring",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/DYPAI-SOLUTIONS/dypai.git"
13
+ },
14
+ "homepage": "https://dypai.ai",
15
+ "keywords": [
16
+ "dypai",
17
+ "workflow",
18
+ "flow",
19
+ "ir"
20
+ ],
21
+ "exports": {
22
+ ".": "./src/index.ts"
23
+ },
24
+ "files": [
25
+ "src/",
26
+ "fixtures/",
27
+ "tsconfig.json"
28
+ ],
29
+ "scripts": {
30
+ "test": "bun test",
31
+ "typecheck": "bunx tsc --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "yaml": "^2.6.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/bun": "^1.1.14",
38
+ "typescript": "^5.7.2"
39
+ }
40
+ }
@@ -0,0 +1,168 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, test } from "bun:test";
5
+ import { flowDefinitionToIr } from "./flowDefinitionToIr";
6
+ import { irToWorkflowCodeV2, stableStringifyWorkflowCode } from "./irToWorkflowCodeV2";
7
+ import { legacyYamlToIr } from "./legacyYamlToIr";
8
+ import { placeholderToRef } from "./placeholderToRef";
9
+ import { createBookingFlowDefinition } from "../fixtures/createBooking.flow";
10
+ import { createBookingWorkflowIR } from "../fixtures/createBooking.ir";
11
+ import { listBookingsWorkflowIR } from "../fixtures/listBookings.ir";
12
+ import { validateWorkflowIR } from "../ir/validate";
13
+
14
+ const fixturesDir = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "fixtures");
15
+
16
+ describe("legacyYamlToIr", () => {
17
+ test("imports simple CRUD YAML into WorkflowIR with YAML source maps", () => {
18
+ const yamlText = readFileSync(join(fixturesDir, "legacy-create-booking.yaml"), "utf-8");
19
+ const imported = legacyYamlToIr(yamlText, { file: "dypai/endpoints/create-booking.yaml" });
20
+
21
+ expect(imported.ok).toBe(true);
22
+ expect(imported.value?.name).toBe("create-booking");
23
+ expect(imported.value?.source?.kind).toBe("legacy-yaml");
24
+ expect(imported.value?.steps.some((step) => step.node === "dypai.db" && step.operation === "query")).toBe(true);
25
+
26
+ const validated = validateWorkflowIR(imported.value);
27
+ expect(validated.ok).toBe(true);
28
+ });
29
+
30
+ test("flags mutation+query anti-pattern during import", () => {
31
+ const yamlText = `
32
+ name: bad-create
33
+ method: POST
34
+ trigger:
35
+ http_api:
36
+ auth_mode: public
37
+ input:
38
+ type: object
39
+ properties: {}
40
+ output:
41
+ type: object
42
+ properties:
43
+ id:
44
+ type: string
45
+ workflow:
46
+ nodes:
47
+ - id: create
48
+ type: dypai_database
49
+ operation: mutation
50
+ query: INSERT INTO public.items (name) VALUES (\${input.name}) RETURNING id
51
+ `;
52
+ const imported = legacyYamlToIr(yamlText, { file: "dypai/endpoints/bad-create.yaml" });
53
+ expect(imported.ok).toBe(false);
54
+ expect(imported.diagnostics.some((item) => item.rule === "legacy_mutation_with_query")).toBe(true);
55
+ });
56
+ });
57
+
58
+ describe("flowDefinitionToIr", () => {
59
+ test("compiles FlowDefinition fixture to valid WorkflowIR", () => {
60
+ const workflow = flowDefinitionToIr(createBookingFlowDefinition);
61
+ const validated = validateWorkflowIR(workflow);
62
+ expect(validated.ok).toBe(true);
63
+ expect(workflow.steps.some((step) => step.node === "dypai.db" && step.operation === "insert")).toBe(true);
64
+ expect(workflow.responseMap.booking.kind).toBe("step");
65
+ });
66
+ });
67
+
68
+ describe("irToWorkflowCodeV2", () => {
69
+ test("adapts create-booking IR without invalid engine patterns", () => {
70
+ const adapted = irToWorkflowCodeV2(createBookingWorkflowIR);
71
+ expect(adapted.ok).toBe(true);
72
+ expect(adapted.value?.schema_version).toBe("2.0");
73
+
74
+ const databaseNode = adapted.value?.nodes.find((node) => node.id === "booking");
75
+ expect(databaseNode?.node_type).toBe("dypai_database");
76
+ expect(databaseNode?.parameters.operation).toBe("query");
77
+ expect(databaseNode?.parameters.query).toContain("INSERT INTO public.bookings");
78
+ expect(databaseNode?.parameters.operation).not.toBe("mutation");
79
+
80
+ const composeNode = adapted.value?.nodes.find((node) => node.is_return);
81
+ expect(composeNode?.node_type).toBe("set_fields");
82
+ expect(composeNode?.parameters.operation).toBe("compose");
83
+ expect(composeNode?.parameters.fields).toEqual({
84
+ booking: "${vars.booking.row}",
85
+ });
86
+ expect(adapted.value?.edges.some((edge) => edge.target === "respond")).toBe(false);
87
+ expect(adapted.value?.edges).toEqual([
88
+ { source: "booking", target: "__response" },
89
+ ]);
90
+ });
91
+
92
+ test("adapts list-bookings IR with jwt trigger metadata", () => {
93
+ expect(validateWorkflowIR(listBookingsWorkflowIR).ok).toBe(true);
94
+ const adapted = irToWorkflowCodeV2(listBookingsWorkflowIR);
95
+ expect(adapted.ok).toBe(true);
96
+ expect(adapted.value?.metadata?.response_cardinality).toBe("many");
97
+ expect(adapted.value?.execution_config.triggers.http_api).toMatchObject({
98
+ enabled: true,
99
+ auth_mode: "jwt",
100
+ });
101
+
102
+ const listNode = adapted.value?.nodes.find((node) => node.id === "bookings");
103
+ expect(String(listNode?.parameters.query)).toContain("SELECT * FROM public.bookings");
104
+ expect(String(listNode?.parameters.query)).toContain("${current_user_id}");
105
+ expect(String(listNode?.parameters.query)).toContain("LIMIT ${input.limit}");
106
+ expect(String(listNode?.parameters.query)).toContain("OFFSET ${input.offset}");
107
+ });
108
+
109
+ test("generates deterministic workflow_code for golden comparison", () => {
110
+ const first = stableStringifyWorkflowCode(irToWorkflowCodeV2(createBookingWorkflowIR).value!);
111
+ const second = stableStringifyWorkflowCode(irToWorkflowCodeV2(createBookingWorkflowIR).value!);
112
+ expect(first).toBe(second);
113
+ expect(first).toContain('"schema_version": "2.0"');
114
+ expect(first).not.toContain("operation\": \"mutation\"");
115
+ });
116
+
117
+ test("round-trips legacy YAML -> IR -> workflow_code for create-booking", () => {
118
+ const yamlText = readFileSync(join(fixturesDir, "legacy-create-booking.yaml"), "utf-8");
119
+ const imported = legacyYamlToIr(yamlText, { file: "dypai/endpoints/create-booking.yaml" });
120
+ expect(imported.ok).toBe(true);
121
+
122
+ const validated = validateWorkflowIR(imported.value);
123
+ expect(validated.ok).toBe(true);
124
+
125
+ const adapted = irToWorkflowCodeV2(imported.value!);
126
+ expect(adapted.ok).toBe(true);
127
+ expect(adapted.value?.nodes.some((node) => node.node_type === "dypai_database")).toBe(true);
128
+ expect(adapted.value?.nodes.some((node) => node.node_type === "set_fields" && node.is_return)).toBe(true);
129
+ });
130
+ });
131
+
132
+ describe("legacy compatibility helpers", () => {
133
+ test("parses indexed vars placeholders into step refs", () => {
134
+ expect(placeholderToRef("${vars.count[0].total}")).toEqual({
135
+ kind: "step",
136
+ stepId: "count",
137
+ path: ["0", "total"],
138
+ });
139
+ });
140
+
141
+ test("imports unsupported legacy nodes as valid legacy.workflow steps", () => {
142
+ const yamlText = `
143
+ name: proxy
144
+ method: POST
145
+ trigger:
146
+ http_api:
147
+ auth_mode: public
148
+ input:
149
+ type: object
150
+ properties: {}
151
+ output:
152
+ type: object
153
+ properties:
154
+ ok:
155
+ type: boolean
156
+ workflow:
157
+ nodes:
158
+ - id: call_api
159
+ type: http_request
160
+ method: GET
161
+ url: https://example.com
162
+ `;
163
+ const imported = legacyYamlToIr(yamlText, { file: "dypai/endpoints/proxy.yaml" });
164
+ expect(imported.ok).toBe(true);
165
+ expect(imported.value?.steps.some((step) => step.node === "legacy.workflow")).toBe(true);
166
+ expect(validateWorkflowIR(imported.value).ok).toBe(true);
167
+ });
168
+ });
@@ -0,0 +1,35 @@
1
+ import type { EngineBindingJson } from "../capabilities/types";
2
+ import { serializeConfig } from "./refToLegacyPlaceholder";
3
+
4
+ export function applyParameterMap(
5
+ config: Record<string, unknown>,
6
+ binding: EngineBindingJson,
7
+ ): Record<string, unknown> {
8
+ if (!binding.parameter_map) {
9
+ return config;
10
+ }
11
+
12
+ const mapped: Record<string, unknown> = {};
13
+ for (const [key, value] of Object.entries(config)) {
14
+ mapped[binding.parameter_map[key] ?? key] = value;
15
+ }
16
+ return mapped;
17
+ }
18
+
19
+ export function buildEngineParameters(
20
+ config: Record<string, unknown>,
21
+ binding: EngineBindingJson,
22
+ ): Record<string, unknown> {
23
+ const mapped = applyParameterMap(config, binding);
24
+ const parameters = serializeConfig(mapped);
25
+
26
+ if (binding.operation) {
27
+ parameters.operation = binding.operation;
28
+ }
29
+
30
+ if (binding.static_parameters) {
31
+ Object.assign(parameters, binding.static_parameters);
32
+ }
33
+
34
+ return parameters;
35
+ }
@@ -0,0 +1,141 @@
1
+ import type { CapabilityCatalog } from "../capabilities/types";
2
+ import { CapabilityRegistry, resolveCapabilityCall as resolveCatalogCall } from "../capabilities/capabilityRegistry";
3
+ import type {
4
+ AuthIR,
5
+ JsonSchema,
6
+ RefIR,
7
+ ResponseIR,
8
+ SourceLocation,
9
+ TriggerIR,
10
+ WorkflowIR,
11
+ WorkflowStepIR,
12
+ } from "../ir/types";
13
+ import { WORKFLOW_IR_VERSION } from "../ir/types";
14
+
15
+ export type FlowCompileContext = {
16
+ capabilityCatalog?: CapabilityCatalog;
17
+ };
18
+
19
+ export type FlowStepDefinition = {
20
+ id: string;
21
+ call: string;
22
+ config: Record<string, unknown>;
23
+ source?: SourceLocation;
24
+ };
25
+
26
+ export type FlowDefinition = {
27
+ name: string;
28
+ source?: {
29
+ kind: "flow-ts";
30
+ file: string;
31
+ };
32
+ endpoint?: {
33
+ description?: string;
34
+ isTool?: boolean;
35
+ toolDescription?: string;
36
+ };
37
+ trigger: TriggerIR;
38
+ auth: AuthIR;
39
+ input: JsonSchema;
40
+ output: JsonSchema;
41
+ response?: ResponseIR;
42
+ steps: FlowStepDefinition[];
43
+ returns: Record<string, RefIR>;
44
+ edges?: Array<{ from: string; to: string }>;
45
+ };
46
+
47
+ const LEGACY_FLOW_ALIASES: Record<string, { node: string; operation: string; version: number }> = {
48
+ "db.query": { node: "dypai.db", operation: "query", version: 1 },
49
+ "db.insert": { node: "dypai.db", operation: "insert", version: 1 },
50
+ "db.update": { node: "dypai.db", operation: "update", version: 1 },
51
+ "db.list": { node: "dypai.db", operation: "list", version: 1 },
52
+ "email.send": { node: "dypai.email", operation: "send", version: 1 },
53
+ "flow.return": { node: "dypai.flow", operation: "return", version: 1 },
54
+ };
55
+
56
+ const BUILTIN_AUTHORING_GROUPS = new Set(["dypai.db", "dypai.email"]);
57
+
58
+ export function resolveFlowAlias(
59
+ call: string,
60
+ context: FlowCompileContext = {},
61
+ ): { node: string; operation: string; version: number } | null {
62
+ const trimmed = call.trim();
63
+ if (LEGACY_FLOW_ALIASES[trimmed]) return LEGACY_FLOW_ALIASES[trimmed];
64
+
65
+ const registry = context.capabilityCatalog
66
+ ? CapabilityRegistry.fromCatalog(context.capabilityCatalog)
67
+ : new CapabilityRegistry();
68
+
69
+ const resolved = resolveCatalogCall(trimmed, registry);
70
+ if (resolved) return resolved;
71
+
72
+ const parts = trimmed.split(".");
73
+ for (let splitAt = parts.length - 1; splitAt >= 2; splitAt -= 1) {
74
+ const groupId = parts.slice(0, splitAt).join(".");
75
+ if (!BUILTIN_AUTHORING_GROUPS.has(groupId)) continue;
76
+ const operation = parts.slice(splitAt).join(".");
77
+ return { node: groupId, operation, version: 1 };
78
+ }
79
+
80
+ return null;
81
+ }
82
+
83
+ export function flowDefinitionToIr(
84
+ definition: FlowDefinition,
85
+ context: FlowCompileContext = {},
86
+ ): WorkflowIR {
87
+ const steps: WorkflowStepIR[] = definition.steps.map((step) => {
88
+ const resolved = resolveFlowAlias(step.call, context);
89
+ if (!resolved) {
90
+ return {
91
+ id: step.id,
92
+ node: "legacy.workflow",
93
+ operation: "node",
94
+ version: 1,
95
+ config: {
96
+ legacyNode: {
97
+ id: step.id,
98
+ type: step.call,
99
+ ...step.config,
100
+ },
101
+ },
102
+ source: step.source,
103
+ };
104
+ }
105
+
106
+ return {
107
+ id: step.id,
108
+ node: resolved.node,
109
+ operation: resolved.operation,
110
+ version: resolved.version,
111
+ config: step.config,
112
+ source: step.source,
113
+ ...(resolved.node === "dypai.flow" && resolved.operation === "return" ? { return: true } : {}),
114
+ };
115
+ });
116
+
117
+ if (!steps.some((step) => step.node === "dypai.flow" && step.operation === "return")) {
118
+ steps.push({
119
+ id: "return",
120
+ node: "dypai.flow",
121
+ operation: "return",
122
+ version: 1,
123
+ config: {},
124
+ return: true,
125
+ });
126
+ }
127
+
128
+ return {
129
+ version: WORKFLOW_IR_VERSION,
130
+ name: definition.name,
131
+ source: definition.source,
132
+ trigger: definition.trigger,
133
+ auth: definition.auth,
134
+ inputSchema: definition.input,
135
+ outputSchema: definition.output,
136
+ response: definition.response || { cardinality: "single" },
137
+ steps,
138
+ edges: definition.edges,
139
+ responseMap: definition.returns,
140
+ };
141
+ }