@driftgate/workflow-compiler 0.1.0-rc.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.
- package/dist/index.d.ts +199 -0
- package/dist/index.js +103 -0
- package/dist/index.js.map +1 -0
- package/package.json +39 -0
- package/src/compiler.test.ts +64 -0
- package/src/compiler.ts +90 -0
- package/src/index.ts +2 -0
- package/src/schema.ts +45 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
declare const WorkflowYamlSchema: z.ZodObject<{
|
|
4
|
+
apiVersion: z.ZodLiteral<"driftgate.ai/v1">;
|
|
5
|
+
kind: z.ZodLiteral<"Workflow">;
|
|
6
|
+
metadata: z.ZodObject<{
|
|
7
|
+
name: z.ZodString;
|
|
8
|
+
workspace: z.ZodString;
|
|
9
|
+
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
name: string;
|
|
11
|
+
workspace: string;
|
|
12
|
+
}, {
|
|
13
|
+
name: string;
|
|
14
|
+
workspace: string;
|
|
15
|
+
}>;
|
|
16
|
+
spec: z.ZodObject<{
|
|
17
|
+
governance: z.ZodDefault<z.ZodObject<{
|
|
18
|
+
policyBindings: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
19
|
+
policyId: z.ZodString;
|
|
20
|
+
mode: z.ZodDefault<z.ZodEnum<["monitor", "enforce"]>>;
|
|
21
|
+
}, "strip", z.ZodTypeAny, {
|
|
22
|
+
policyId: string;
|
|
23
|
+
mode: "monitor" | "enforce";
|
|
24
|
+
}, {
|
|
25
|
+
policyId: string;
|
|
26
|
+
mode?: "monitor" | "enforce" | undefined;
|
|
27
|
+
}>, "many">>;
|
|
28
|
+
slaBindings: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
29
|
+
slaId: z.ZodString;
|
|
30
|
+
}, "strip", z.ZodTypeAny, {
|
|
31
|
+
slaId: string;
|
|
32
|
+
}, {
|
|
33
|
+
slaId: string;
|
|
34
|
+
}>, "many">>;
|
|
35
|
+
}, "strip", z.ZodTypeAny, {
|
|
36
|
+
policyBindings: {
|
|
37
|
+
policyId: string;
|
|
38
|
+
mode: "monitor" | "enforce";
|
|
39
|
+
}[];
|
|
40
|
+
slaBindings: {
|
|
41
|
+
slaId: string;
|
|
42
|
+
}[];
|
|
43
|
+
}, {
|
|
44
|
+
policyBindings?: {
|
|
45
|
+
policyId: string;
|
|
46
|
+
mode?: "monitor" | "enforce" | undefined;
|
|
47
|
+
}[] | undefined;
|
|
48
|
+
slaBindings?: {
|
|
49
|
+
slaId: string;
|
|
50
|
+
}[] | undefined;
|
|
51
|
+
}>>;
|
|
52
|
+
nodes: z.ZodArray<z.ZodObject<{
|
|
53
|
+
id: z.ZodString;
|
|
54
|
+
type: z.ZodString;
|
|
55
|
+
mutation: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
56
|
+
config: z.ZodDefault<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
57
|
+
}, "strip", z.ZodTypeAny, {
|
|
58
|
+
type: string;
|
|
59
|
+
id: string;
|
|
60
|
+
mutation: boolean;
|
|
61
|
+
config: Record<string, unknown>;
|
|
62
|
+
}, {
|
|
63
|
+
type: string;
|
|
64
|
+
id: string;
|
|
65
|
+
mutation?: boolean | undefined;
|
|
66
|
+
config?: Record<string, unknown> | undefined;
|
|
67
|
+
}>, "many">;
|
|
68
|
+
edges: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
69
|
+
from: z.ZodString;
|
|
70
|
+
to: z.ZodString;
|
|
71
|
+
}, "strip", z.ZodTypeAny, {
|
|
72
|
+
from: string;
|
|
73
|
+
to: string;
|
|
74
|
+
}, {
|
|
75
|
+
from: string;
|
|
76
|
+
to: string;
|
|
77
|
+
}>, "many">>>;
|
|
78
|
+
}, "strip", z.ZodTypeAny, {
|
|
79
|
+
governance: {
|
|
80
|
+
policyBindings: {
|
|
81
|
+
policyId: string;
|
|
82
|
+
mode: "monitor" | "enforce";
|
|
83
|
+
}[];
|
|
84
|
+
slaBindings: {
|
|
85
|
+
slaId: string;
|
|
86
|
+
}[];
|
|
87
|
+
};
|
|
88
|
+
nodes: {
|
|
89
|
+
type: string;
|
|
90
|
+
id: string;
|
|
91
|
+
mutation: boolean;
|
|
92
|
+
config: Record<string, unknown>;
|
|
93
|
+
}[];
|
|
94
|
+
edges: {
|
|
95
|
+
from: string;
|
|
96
|
+
to: string;
|
|
97
|
+
}[];
|
|
98
|
+
}, {
|
|
99
|
+
nodes: {
|
|
100
|
+
type: string;
|
|
101
|
+
id: string;
|
|
102
|
+
mutation?: boolean | undefined;
|
|
103
|
+
config?: Record<string, unknown> | undefined;
|
|
104
|
+
}[];
|
|
105
|
+
governance?: {
|
|
106
|
+
policyBindings?: {
|
|
107
|
+
policyId: string;
|
|
108
|
+
mode?: "monitor" | "enforce" | undefined;
|
|
109
|
+
}[] | undefined;
|
|
110
|
+
slaBindings?: {
|
|
111
|
+
slaId: string;
|
|
112
|
+
}[] | undefined;
|
|
113
|
+
} | undefined;
|
|
114
|
+
edges?: {
|
|
115
|
+
from: string;
|
|
116
|
+
to: string;
|
|
117
|
+
}[] | undefined;
|
|
118
|
+
}>;
|
|
119
|
+
}, "strip", z.ZodTypeAny, {
|
|
120
|
+
apiVersion: "driftgate.ai/v1";
|
|
121
|
+
kind: "Workflow";
|
|
122
|
+
metadata: {
|
|
123
|
+
name: string;
|
|
124
|
+
workspace: string;
|
|
125
|
+
};
|
|
126
|
+
spec: {
|
|
127
|
+
governance: {
|
|
128
|
+
policyBindings: {
|
|
129
|
+
policyId: string;
|
|
130
|
+
mode: "monitor" | "enforce";
|
|
131
|
+
}[];
|
|
132
|
+
slaBindings: {
|
|
133
|
+
slaId: string;
|
|
134
|
+
}[];
|
|
135
|
+
};
|
|
136
|
+
nodes: {
|
|
137
|
+
type: string;
|
|
138
|
+
id: string;
|
|
139
|
+
mutation: boolean;
|
|
140
|
+
config: Record<string, unknown>;
|
|
141
|
+
}[];
|
|
142
|
+
edges: {
|
|
143
|
+
from: string;
|
|
144
|
+
to: string;
|
|
145
|
+
}[];
|
|
146
|
+
};
|
|
147
|
+
}, {
|
|
148
|
+
apiVersion: "driftgate.ai/v1";
|
|
149
|
+
kind: "Workflow";
|
|
150
|
+
metadata: {
|
|
151
|
+
name: string;
|
|
152
|
+
workspace: string;
|
|
153
|
+
};
|
|
154
|
+
spec: {
|
|
155
|
+
nodes: {
|
|
156
|
+
type: string;
|
|
157
|
+
id: string;
|
|
158
|
+
mutation?: boolean | undefined;
|
|
159
|
+
config?: Record<string, unknown> | undefined;
|
|
160
|
+
}[];
|
|
161
|
+
governance?: {
|
|
162
|
+
policyBindings?: {
|
|
163
|
+
policyId: string;
|
|
164
|
+
mode?: "monitor" | "enforce" | undefined;
|
|
165
|
+
}[] | undefined;
|
|
166
|
+
slaBindings?: {
|
|
167
|
+
slaId: string;
|
|
168
|
+
}[] | undefined;
|
|
169
|
+
} | undefined;
|
|
170
|
+
edges?: {
|
|
171
|
+
from: string;
|
|
172
|
+
to: string;
|
|
173
|
+
}[] | undefined;
|
|
174
|
+
};
|
|
175
|
+
}>;
|
|
176
|
+
type WorkflowYaml = z.infer<typeof WorkflowYamlSchema>;
|
|
177
|
+
|
|
178
|
+
type CompiledWorkflow = {
|
|
179
|
+
workflow: WorkflowYaml;
|
|
180
|
+
compiledPlan: {
|
|
181
|
+
metadata: WorkflowYaml["metadata"];
|
|
182
|
+
governance: WorkflowYaml["spec"]["governance"];
|
|
183
|
+
nodes: Array<{
|
|
184
|
+
id: string;
|
|
185
|
+
type: string;
|
|
186
|
+
mutation: boolean;
|
|
187
|
+
config: Record<string, unknown>;
|
|
188
|
+
}>;
|
|
189
|
+
edges: Array<{
|
|
190
|
+
from: string;
|
|
191
|
+
to: string;
|
|
192
|
+
}>;
|
|
193
|
+
};
|
|
194
|
+
mutationNodeIds: string[];
|
|
195
|
+
checksum: string;
|
|
196
|
+
};
|
|
197
|
+
declare function compileWorkflowYaml(yamlText: string): CompiledWorkflow;
|
|
198
|
+
|
|
199
|
+
export { type CompiledWorkflow, type WorkflowYaml, WorkflowYamlSchema, compileWorkflowYaml };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// src/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var MetadataSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
workspace: z.string().min(1)
|
|
6
|
+
});
|
|
7
|
+
var PolicyBindingSchema = z.object({
|
|
8
|
+
policyId: z.string().min(1),
|
|
9
|
+
mode: z.enum(["monitor", "enforce"]).default("enforce")
|
|
10
|
+
});
|
|
11
|
+
var SlaBindingSchema = z.object({
|
|
12
|
+
slaId: z.string().min(1)
|
|
13
|
+
});
|
|
14
|
+
var NodeSchema = z.object({
|
|
15
|
+
id: z.string().min(1),
|
|
16
|
+
type: z.string().min(1),
|
|
17
|
+
mutation: z.boolean().optional().default(false),
|
|
18
|
+
config: z.record(z.unknown()).optional().default({})
|
|
19
|
+
});
|
|
20
|
+
var EdgeSchema = z.object({
|
|
21
|
+
from: z.string().min(1),
|
|
22
|
+
to: z.string().min(1)
|
|
23
|
+
});
|
|
24
|
+
var WorkflowYamlSchema = z.object({
|
|
25
|
+
apiVersion: z.literal("driftgate.ai/v1"),
|
|
26
|
+
kind: z.literal("Workflow"),
|
|
27
|
+
metadata: MetadataSchema,
|
|
28
|
+
spec: z.object({
|
|
29
|
+
governance: z.object({
|
|
30
|
+
policyBindings: z.array(PolicyBindingSchema).default([]),
|
|
31
|
+
slaBindings: z.array(SlaBindingSchema).default([])
|
|
32
|
+
}).default({ policyBindings: [], slaBindings: [] }),
|
|
33
|
+
nodes: z.array(NodeSchema).min(1),
|
|
34
|
+
edges: z.array(EdgeSchema).optional().default([])
|
|
35
|
+
})
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// src/compiler.ts
|
|
39
|
+
import { createHash } from "crypto";
|
|
40
|
+
import { parse } from "yaml";
|
|
41
|
+
function stableStringify(value) {
|
|
42
|
+
return JSON.stringify(sortValue(value));
|
|
43
|
+
}
|
|
44
|
+
function sortValue(value) {
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
return value.map((item) => sortValue(item));
|
|
47
|
+
}
|
|
48
|
+
if (!value || typeof value !== "object") {
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
const objectValue = value;
|
|
52
|
+
const sortedKeys = Object.keys(objectValue).sort((left, right) => left.localeCompare(right));
|
|
53
|
+
const next = {};
|
|
54
|
+
for (const key of sortedKeys) {
|
|
55
|
+
next[key] = sortValue(objectValue[key]);
|
|
56
|
+
}
|
|
57
|
+
return next;
|
|
58
|
+
}
|
|
59
|
+
function assertValidReferences(workflow) {
|
|
60
|
+
const nodeIds = new Set(workflow.spec.nodes.map((node) => node.id));
|
|
61
|
+
for (const edge of workflow.spec.edges) {
|
|
62
|
+
if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) {
|
|
63
|
+
throw new Error(`invalid edge reference: ${edge.from} -> ${edge.to}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function compileWorkflowYaml(yamlText) {
|
|
68
|
+
const parsed = parse(yamlText);
|
|
69
|
+
const workflow = WorkflowYamlSchema.parse(parsed);
|
|
70
|
+
assertValidReferences(workflow);
|
|
71
|
+
const nodes = [...workflow.spec.nodes].sort((left, right) => left.id.localeCompare(right.id));
|
|
72
|
+
const edges = [...workflow.spec.edges].sort((left, right) => {
|
|
73
|
+
const fromOrder = left.from.localeCompare(right.from);
|
|
74
|
+
if (fromOrder !== 0) {
|
|
75
|
+
return fromOrder;
|
|
76
|
+
}
|
|
77
|
+
return left.to.localeCompare(right.to);
|
|
78
|
+
});
|
|
79
|
+
const compiledPlan = {
|
|
80
|
+
metadata: workflow.metadata,
|
|
81
|
+
governance: workflow.spec.governance,
|
|
82
|
+
nodes: nodes.map((node) => ({
|
|
83
|
+
id: node.id,
|
|
84
|
+
type: node.type,
|
|
85
|
+
mutation: Boolean(node.mutation),
|
|
86
|
+
config: node.config ?? {}
|
|
87
|
+
})),
|
|
88
|
+
edges
|
|
89
|
+
};
|
|
90
|
+
const mutationNodeIds = compiledPlan.nodes.filter((node) => node.mutation).map((node) => node.id).sort((left, right) => left.localeCompare(right));
|
|
91
|
+
const checksum = createHash("sha256").update(stableStringify(compiledPlan)).digest("hex");
|
|
92
|
+
return {
|
|
93
|
+
workflow,
|
|
94
|
+
compiledPlan,
|
|
95
|
+
mutationNodeIds,
|
|
96
|
+
checksum
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export {
|
|
100
|
+
WorkflowYamlSchema,
|
|
101
|
+
compileWorkflowYaml
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/schema.ts","../src/compiler.ts"],"sourcesContent":["import { z } from \"zod\";\n\nconst MetadataSchema = z.object({\n name: z.string().min(1),\n workspace: z.string().min(1)\n});\n\nconst PolicyBindingSchema = z.object({\n policyId: z.string().min(1),\n mode: z.enum([\"monitor\", \"enforce\"]).default(\"enforce\")\n});\n\nconst SlaBindingSchema = z.object({\n slaId: z.string().min(1)\n});\n\nconst NodeSchema = z.object({\n id: z.string().min(1),\n type: z.string().min(1),\n mutation: z.boolean().optional().default(false),\n config: z.record(z.unknown()).optional().default({})\n});\n\nconst EdgeSchema = z.object({\n from: z.string().min(1),\n to: z.string().min(1)\n});\n\nexport const WorkflowYamlSchema = z.object({\n apiVersion: z.literal(\"driftgate.ai/v1\"),\n kind: z.literal(\"Workflow\"),\n metadata: MetadataSchema,\n spec: z.object({\n governance: z\n .object({\n policyBindings: z.array(PolicyBindingSchema).default([]),\n slaBindings: z.array(SlaBindingSchema).default([])\n })\n .default({ policyBindings: [], slaBindings: [] }),\n nodes: z.array(NodeSchema).min(1),\n edges: z.array(EdgeSchema).optional().default([])\n })\n});\n\nexport type WorkflowYaml = z.infer<typeof WorkflowYamlSchema>;\n","import { createHash } from \"node:crypto\";\nimport { parse } from \"yaml\";\nimport { WorkflowYamlSchema, type WorkflowYaml } from \"./schema.ts\";\n\nexport type CompiledWorkflow = {\n workflow: WorkflowYaml;\n compiledPlan: {\n metadata: WorkflowYaml[\"metadata\"];\n governance: WorkflowYaml[\"spec\"][\"governance\"];\n nodes: Array<{\n id: string;\n type: string;\n mutation: boolean;\n config: Record<string, unknown>;\n }>;\n edges: Array<{ from: string; to: string }>;\n };\n mutationNodeIds: string[];\n checksum: string;\n};\n\nfunction stableStringify(value: unknown): string {\n return JSON.stringify(sortValue(value));\n}\n\nfunction sortValue(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map((item) => sortValue(item));\n }\n if (!value || typeof value !== \"object\") {\n return value;\n }\n const objectValue = value as Record<string, unknown>;\n const sortedKeys = Object.keys(objectValue).sort((left, right) => left.localeCompare(right));\n const next: Record<string, unknown> = {};\n for (const key of sortedKeys) {\n next[key] = sortValue(objectValue[key]);\n }\n return next;\n}\n\nfunction assertValidReferences(workflow: WorkflowYaml): void {\n const nodeIds = new Set(workflow.spec.nodes.map((node) => node.id));\n for (const edge of workflow.spec.edges) {\n if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) {\n throw new Error(`invalid edge reference: ${edge.from} -> ${edge.to}`);\n }\n }\n}\n\nexport function compileWorkflowYaml(yamlText: string): CompiledWorkflow {\n const parsed = parse(yamlText);\n const workflow = WorkflowYamlSchema.parse(parsed);\n assertValidReferences(workflow);\n\n const nodes = [...workflow.spec.nodes].sort((left, right) => left.id.localeCompare(right.id));\n const edges = [...workflow.spec.edges].sort((left, right) => {\n const fromOrder = left.from.localeCompare(right.from);\n if (fromOrder !== 0) {\n return fromOrder;\n }\n return left.to.localeCompare(right.to);\n });\n\n const compiledPlan = {\n metadata: workflow.metadata,\n governance: workflow.spec.governance,\n nodes: nodes.map((node) => ({\n id: node.id,\n type: node.type,\n mutation: Boolean(node.mutation),\n config: node.config ?? {}\n })),\n edges\n };\n\n const mutationNodeIds = compiledPlan.nodes\n .filter((node) => node.mutation)\n .map((node) => node.id)\n .sort((left, right) => left.localeCompare(right));\n\n const checksum = createHash(\"sha256\").update(stableStringify(compiledPlan)).digest(\"hex\");\n\n return {\n workflow,\n compiledPlan,\n mutationNodeIds,\n checksum\n };\n}\n"],"mappings":";AAAA,SAAS,SAAS;AAElB,IAAM,iBAAiB,EAAE,OAAO;AAAA,EAC9B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACtB,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAC7B,CAAC;AAED,IAAM,sBAAsB,EAAE,OAAO;AAAA,EACnC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,MAAM,EAAE,KAAK,CAAC,WAAW,SAAS,CAAC,EAAE,QAAQ,SAAS;AACxD,CAAC;AAED,IAAM,mBAAmB,EAAE,OAAO;AAAA,EAChC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AACzB,CAAC;AAED,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACtB,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,KAAK;AAAA,EAC9C,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC;AAED,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,MAAM,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACtB,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AACtB,CAAC;AAEM,IAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,YAAY,EAAE,QAAQ,iBAAiB;AAAA,EACvC,MAAM,EAAE,QAAQ,UAAU;AAAA,EAC1B,UAAU;AAAA,EACV,MAAM,EAAE,OAAO;AAAA,IACb,YAAY,EACT,OAAO;AAAA,MACN,gBAAgB,EAAE,MAAM,mBAAmB,EAAE,QAAQ,CAAC,CAAC;AAAA,MACvD,aAAa,EAAE,MAAM,gBAAgB,EAAE,QAAQ,CAAC,CAAC;AAAA,IACnD,CAAC,EACA,QAAQ,EAAE,gBAAgB,CAAC,GAAG,aAAa,CAAC,EAAE,CAAC;AAAA,IAClD,OAAO,EAAE,MAAM,UAAU,EAAE,IAAI,CAAC;AAAA,IAChC,OAAO,EAAE,MAAM,UAAU,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC;AAAA,EAClD,CAAC;AACH,CAAC;;;AC1CD,SAAS,kBAAkB;AAC3B,SAAS,aAAa;AAoBtB,SAAS,gBAAgB,OAAwB;AAC/C,SAAO,KAAK,UAAU,UAAU,KAAK,CAAC;AACxC;AAEA,SAAS,UAAU,OAAyB;AAC1C,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,SAAS,UAAU,IAAI,CAAC;AAAA,EAC5C;AACA,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,WAAO;AAAA,EACT;AACA,QAAM,cAAc;AACpB,QAAM,aAAa,OAAO,KAAK,WAAW,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAC3F,QAAM,OAAgC,CAAC;AACvC,aAAW,OAAO,YAAY;AAC5B,SAAK,GAAG,IAAI,UAAU,YAAY,GAAG,CAAC;AAAA,EACxC;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,UAA8B;AAC3D,QAAM,UAAU,IAAI,IAAI,SAAS,KAAK,MAAM,IAAI,CAAC,SAAS,KAAK,EAAE,CAAC;AAClE,aAAW,QAAQ,SAAS,KAAK,OAAO;AACtC,QAAI,CAAC,QAAQ,IAAI,KAAK,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,EAAE,GAAG;AACpD,YAAM,IAAI,MAAM,2BAA2B,KAAK,IAAI,OAAO,KAAK,EAAE,EAAE;AAAA,IACtE;AAAA,EACF;AACF;AAEO,SAAS,oBAAoB,UAAoC;AACtE,QAAM,SAAS,MAAM,QAAQ;AAC7B,QAAM,WAAW,mBAAmB,MAAM,MAAM;AAChD,wBAAsB,QAAQ;AAE9B,QAAM,QAAQ,CAAC,GAAG,SAAS,KAAK,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU,KAAK,GAAG,cAAc,MAAM,EAAE,CAAC;AAC5F,QAAM,QAAQ,CAAC,GAAG,SAAS,KAAK,KAAK,EAAE,KAAK,CAAC,MAAM,UAAU;AAC3D,UAAM,YAAY,KAAK,KAAK,cAAc,MAAM,IAAI;AACpD,QAAI,cAAc,GAAG;AACnB,aAAO;AAAA,IACT;AACA,WAAO,KAAK,GAAG,cAAc,MAAM,EAAE;AAAA,EACvC,CAAC;AAED,QAAM,eAAe;AAAA,IACnB,UAAU,SAAS;AAAA,IACnB,YAAY,SAAS,KAAK;AAAA,IAC1B,OAAO,MAAM,IAAI,CAAC,UAAU;AAAA,MAC1B,IAAI,KAAK;AAAA,MACT,MAAM,KAAK;AAAA,MACX,UAAU,QAAQ,KAAK,QAAQ;AAAA,MAC/B,QAAQ,KAAK,UAAU,CAAC;AAAA,IAC1B,EAAE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,kBAAkB,aAAa,MAClC,OAAO,CAAC,SAAS,KAAK,QAAQ,EAC9B,IAAI,CAAC,SAAS,KAAK,EAAE,EACrB,KAAK,CAAC,MAAM,UAAU,KAAK,cAAc,KAAK,CAAC;AAElD,QAAM,WAAW,WAAW,QAAQ,EAAE,OAAO,gBAAgB,YAAY,CAAC,EAAE,OAAO,KAAK;AAExF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@driftgate/workflow-compiler",
|
|
3
|
+
"version": "0.1.0-rc.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Workflow YAML compiler for DriftGate runtime.",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./src/index.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./src/*": "./src/*",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
|
+
],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public",
|
|
25
|
+
"provenance": true
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/driftgate/driftgate-sdk.git"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup src/index.ts --format esm --target node20 --dts --sourcemap --clean --tsconfig tsconfig.build.json",
|
|
33
|
+
"clean": "rm -rf dist"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"yaml": "^2.8.1",
|
|
37
|
+
"zod": "^3.23.8"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { compileWorkflowYaml } from "./compiler.ts";
|
|
4
|
+
|
|
5
|
+
const workflowYaml = `
|
|
6
|
+
apiVersion: driftgate.ai/v1
|
|
7
|
+
kind: Workflow
|
|
8
|
+
metadata:
|
|
9
|
+
name: refund-agent
|
|
10
|
+
workspace: acme-payments
|
|
11
|
+
spec:
|
|
12
|
+
governance:
|
|
13
|
+
policyBindings:
|
|
14
|
+
- policyId: pol_refunds
|
|
15
|
+
mode: enforce
|
|
16
|
+
slaBindings:
|
|
17
|
+
- slaId: sla_default
|
|
18
|
+
nodes:
|
|
19
|
+
- id: fetch_order
|
|
20
|
+
type: http
|
|
21
|
+
config:
|
|
22
|
+
path: /orders/:id
|
|
23
|
+
- id: decide_refund
|
|
24
|
+
type: llm
|
|
25
|
+
- id: issue_refund
|
|
26
|
+
type: http
|
|
27
|
+
mutation: true
|
|
28
|
+
edges:
|
|
29
|
+
- from: fetch_order
|
|
30
|
+
to: decide_refund
|
|
31
|
+
- from: decide_refund
|
|
32
|
+
to: issue_refund
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
test("compiler is deterministic for stable input", () => {
|
|
36
|
+
const first = compileWorkflowYaml(workflowYaml);
|
|
37
|
+
const second = compileWorkflowYaml(workflowYaml);
|
|
38
|
+
assert.equal(first.checksum, second.checksum);
|
|
39
|
+
assert.deepEqual(first.compiledPlan, second.compiledPlan);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("compiler detects mutation nodes", () => {
|
|
43
|
+
const compiled = compileWorkflowYaml(workflowYaml);
|
|
44
|
+
assert.deepEqual(compiled.mutationNodeIds, ["issue_refund"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("compiler rejects invalid edge references", () => {
|
|
48
|
+
assert.throws(() =>
|
|
49
|
+
compileWorkflowYaml(`
|
|
50
|
+
apiVersion: driftgate.ai/v1
|
|
51
|
+
kind: Workflow
|
|
52
|
+
metadata:
|
|
53
|
+
name: broken
|
|
54
|
+
workspace: acme
|
|
55
|
+
spec:
|
|
56
|
+
nodes:
|
|
57
|
+
- id: a
|
|
58
|
+
type: http
|
|
59
|
+
edges:
|
|
60
|
+
- from: a
|
|
61
|
+
to: missing
|
|
62
|
+
`)
|
|
63
|
+
);
|
|
64
|
+
});
|
package/src/compiler.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { parse } from "yaml";
|
|
3
|
+
import { WorkflowYamlSchema, type WorkflowYaml } from "./schema.ts";
|
|
4
|
+
|
|
5
|
+
export type CompiledWorkflow = {
|
|
6
|
+
workflow: WorkflowYaml;
|
|
7
|
+
compiledPlan: {
|
|
8
|
+
metadata: WorkflowYaml["metadata"];
|
|
9
|
+
governance: WorkflowYaml["spec"]["governance"];
|
|
10
|
+
nodes: Array<{
|
|
11
|
+
id: string;
|
|
12
|
+
type: string;
|
|
13
|
+
mutation: boolean;
|
|
14
|
+
config: Record<string, unknown>;
|
|
15
|
+
}>;
|
|
16
|
+
edges: Array<{ from: string; to: string }>;
|
|
17
|
+
};
|
|
18
|
+
mutationNodeIds: string[];
|
|
19
|
+
checksum: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function stableStringify(value: unknown): string {
|
|
23
|
+
return JSON.stringify(sortValue(value));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sortValue(value: unknown): unknown {
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
return value.map((item) => sortValue(item));
|
|
29
|
+
}
|
|
30
|
+
if (!value || typeof value !== "object") {
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
const objectValue = value as Record<string, unknown>;
|
|
34
|
+
const sortedKeys = Object.keys(objectValue).sort((left, right) => left.localeCompare(right));
|
|
35
|
+
const next: Record<string, unknown> = {};
|
|
36
|
+
for (const key of sortedKeys) {
|
|
37
|
+
next[key] = sortValue(objectValue[key]);
|
|
38
|
+
}
|
|
39
|
+
return next;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertValidReferences(workflow: WorkflowYaml): void {
|
|
43
|
+
const nodeIds = new Set(workflow.spec.nodes.map((node) => node.id));
|
|
44
|
+
for (const edge of workflow.spec.edges) {
|
|
45
|
+
if (!nodeIds.has(edge.from) || !nodeIds.has(edge.to)) {
|
|
46
|
+
throw new Error(`invalid edge reference: ${edge.from} -> ${edge.to}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function compileWorkflowYaml(yamlText: string): CompiledWorkflow {
|
|
52
|
+
const parsed = parse(yamlText);
|
|
53
|
+
const workflow = WorkflowYamlSchema.parse(parsed);
|
|
54
|
+
assertValidReferences(workflow);
|
|
55
|
+
|
|
56
|
+
const nodes = [...workflow.spec.nodes].sort((left, right) => left.id.localeCompare(right.id));
|
|
57
|
+
const edges = [...workflow.spec.edges].sort((left, right) => {
|
|
58
|
+
const fromOrder = left.from.localeCompare(right.from);
|
|
59
|
+
if (fromOrder !== 0) {
|
|
60
|
+
return fromOrder;
|
|
61
|
+
}
|
|
62
|
+
return left.to.localeCompare(right.to);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const compiledPlan = {
|
|
66
|
+
metadata: workflow.metadata,
|
|
67
|
+
governance: workflow.spec.governance,
|
|
68
|
+
nodes: nodes.map((node) => ({
|
|
69
|
+
id: node.id,
|
|
70
|
+
type: node.type,
|
|
71
|
+
mutation: Boolean(node.mutation),
|
|
72
|
+
config: node.config ?? {}
|
|
73
|
+
})),
|
|
74
|
+
edges
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const mutationNodeIds = compiledPlan.nodes
|
|
78
|
+
.filter((node) => node.mutation)
|
|
79
|
+
.map((node) => node.id)
|
|
80
|
+
.sort((left, right) => left.localeCompare(right));
|
|
81
|
+
|
|
82
|
+
const checksum = createHash("sha256").update(stableStringify(compiledPlan)).digest("hex");
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
workflow,
|
|
86
|
+
compiledPlan,
|
|
87
|
+
mutationNodeIds,
|
|
88
|
+
checksum
|
|
89
|
+
};
|
|
90
|
+
}
|
package/src/index.ts
ADDED
package/src/schema.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const MetadataSchema = z.object({
|
|
4
|
+
name: z.string().min(1),
|
|
5
|
+
workspace: z.string().min(1)
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
const PolicyBindingSchema = z.object({
|
|
9
|
+
policyId: z.string().min(1),
|
|
10
|
+
mode: z.enum(["monitor", "enforce"]).default("enforce")
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const SlaBindingSchema = z.object({
|
|
14
|
+
slaId: z.string().min(1)
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const NodeSchema = z.object({
|
|
18
|
+
id: z.string().min(1),
|
|
19
|
+
type: z.string().min(1),
|
|
20
|
+
mutation: z.boolean().optional().default(false),
|
|
21
|
+
config: z.record(z.unknown()).optional().default({})
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const EdgeSchema = z.object({
|
|
25
|
+
from: z.string().min(1),
|
|
26
|
+
to: z.string().min(1)
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const WorkflowYamlSchema = z.object({
|
|
30
|
+
apiVersion: z.literal("driftgate.ai/v1"),
|
|
31
|
+
kind: z.literal("Workflow"),
|
|
32
|
+
metadata: MetadataSchema,
|
|
33
|
+
spec: z.object({
|
|
34
|
+
governance: z
|
|
35
|
+
.object({
|
|
36
|
+
policyBindings: z.array(PolicyBindingSchema).default([]),
|
|
37
|
+
slaBindings: z.array(SlaBindingSchema).default([])
|
|
38
|
+
})
|
|
39
|
+
.default({ policyBindings: [], slaBindings: [] }),
|
|
40
|
+
nodes: z.array(NodeSchema).min(1),
|
|
41
|
+
edges: z.array(EdgeSchema).optional().default([])
|
|
42
|
+
})
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type WorkflowYaml = z.infer<typeof WorkflowYamlSchema>;
|