@clypra/runtime 1.0.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.
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Unit Tests - Graph Validator
3
+ */
4
+
5
+ import { describe, it, expect } from "vitest";
6
+ import { GraphValidator } from "../validator";
7
+ import type { MediaProcessingGraph, GraphNode } from "../types";
8
+
9
+ describe("GraphValidator", () => {
10
+ const createNode = (id: string, type: string, inputCount: number = 1): GraphNode => ({
11
+ id,
12
+ type,
13
+ version: 1,
14
+ params: {},
15
+ inputs:
16
+ inputCount > 0
17
+ ? {
18
+ in: { id: "in", name: "input", type: "Texture" },
19
+ }
20
+ : {},
21
+ outputs: {
22
+ out: { id: "out", name: "output", type: "Texture" },
23
+ },
24
+ capabilities: {
25
+ temporal: false,
26
+ stateful: false,
27
+ spatial: false,
28
+ geometry: false,
29
+ inputsCount: inputCount,
30
+ },
31
+ requirements: {
32
+ temporalRadius: 0,
33
+ preferredPrecision: "fp16",
34
+ multipass: false,
35
+ supportsHalfResolution: true,
36
+ },
37
+ lifecycle: "Created",
38
+ });
39
+
40
+ describe("validate", () => {
41
+ it("should validate a correct graph", () => {
42
+ const graph: MediaProcessingGraph = {
43
+ id: "test-graph",
44
+ nodes: [createNode("node1", "source", 0), createNode("node2", "effect", 1)],
45
+ edges: [
46
+ {
47
+ fromNodeId: "node1",
48
+ fromPinId: "out",
49
+ toNodeId: "node2",
50
+ toPinId: "in",
51
+ },
52
+ ],
53
+ };
54
+
55
+ const validator = new GraphValidator();
56
+ const result = validator.validate(graph);
57
+
58
+ expect(result.valid).toBe(true);
59
+ expect(result.errors).toHaveLength(0);
60
+ });
61
+
62
+ it("should detect cycles", () => {
63
+ const graph: MediaProcessingGraph = {
64
+ id: "test-graph",
65
+ nodes: [createNode("node1", "effect"), createNode("node2", "effect")],
66
+ edges: [
67
+ {
68
+ fromNodeId: "node1",
69
+ fromPinId: "out",
70
+ toNodeId: "node2",
71
+ toPinId: "in",
72
+ },
73
+ {
74
+ fromNodeId: "node2",
75
+ fromPinId: "out",
76
+ toNodeId: "node1",
77
+ toPinId: "in",
78
+ },
79
+ ],
80
+ };
81
+
82
+ const validator = new GraphValidator();
83
+ const result = validator.validate(graph);
84
+
85
+ expect(result.valid).toBe(false);
86
+ expect(result.errors.length).toBeGreaterThan(0);
87
+ expect(result.errors.some((e) => e.type === "cycle")).toBe(true);
88
+ });
89
+
90
+ it("should detect missing source nodes", () => {
91
+ const graph: MediaProcessingGraph = {
92
+ id: "test-graph",
93
+ nodes: [createNode("node2", "effect")],
94
+ edges: [
95
+ {
96
+ fromNodeId: "node1", // This node doesn't exist
97
+ fromPinId: "out",
98
+ toNodeId: "node2",
99
+ toPinId: "in",
100
+ },
101
+ ],
102
+ };
103
+
104
+ const validator = new GraphValidator();
105
+ const result = validator.validate(graph);
106
+
107
+ expect(result.valid).toBe(false);
108
+ expect(result.errors.some((e) => e.type === "invalid-node")).toBe(true);
109
+ });
110
+
111
+ it("should detect missing target nodes", () => {
112
+ const graph: MediaProcessingGraph = {
113
+ id: "test-graph",
114
+ nodes: [createNode("node1", "source", 0)],
115
+ edges: [
116
+ {
117
+ fromNodeId: "node1",
118
+ fromPinId: "out",
119
+ toNodeId: "node2", // This node doesn't exist
120
+ toPinId: "in",
121
+ },
122
+ ],
123
+ };
124
+
125
+ const validator = new GraphValidator();
126
+ const result = validator.validate(graph);
127
+
128
+ expect(result.valid).toBe(false);
129
+ expect(result.errors.some((e) => e.type === "invalid-node")).toBe(true);
130
+ });
131
+
132
+ it("should detect missing output pins", () => {
133
+ const graph: MediaProcessingGraph = {
134
+ id: "test-graph",
135
+ nodes: [createNode("node1", "source", 0), createNode("node2", "effect")],
136
+ edges: [
137
+ {
138
+ fromNodeId: "node1",
139
+ fromPinId: "nonexistent", // This pin doesn't exist
140
+ toNodeId: "node2",
141
+ toPinId: "in",
142
+ },
143
+ ],
144
+ };
145
+
146
+ const validator = new GraphValidator();
147
+ const result = validator.validate(graph);
148
+
149
+ expect(result.valid).toBe(false);
150
+ expect(result.errors.some((e) => e.type === "missing-connection")).toBe(true);
151
+ });
152
+
153
+ it("should detect missing input pins", () => {
154
+ const graph: MediaProcessingGraph = {
155
+ id: "test-graph",
156
+ nodes: [createNode("node1", "source", 0), createNode("node2", "effect")],
157
+ edges: [
158
+ {
159
+ fromNodeId: "node1",
160
+ fromPinId: "out",
161
+ toNodeId: "node2",
162
+ toPinId: "nonexistent", // This pin doesn't exist
163
+ },
164
+ ],
165
+ };
166
+
167
+ const validator = new GraphValidator();
168
+ const result = validator.validate(graph);
169
+
170
+ expect(result.valid).toBe(false);
171
+ expect(result.errors.some((e) => e.type === "missing-connection")).toBe(true);
172
+ });
173
+
174
+ it("should detect type mismatches", () => {
175
+ const node1 = createNode("node1", "source", 0);
176
+ const node2 = createNode("node2", "effect");
177
+
178
+ // Change output type to Depth
179
+ node1.outputs.out.type = "Depth";
180
+ // Input expects Texture
181
+
182
+ const graph: MediaProcessingGraph = {
183
+ id: "test-graph",
184
+ nodes: [node1, node2],
185
+ edges: [
186
+ {
187
+ fromNodeId: "node1",
188
+ fromPinId: "out",
189
+ toNodeId: "node2",
190
+ toPinId: "in",
191
+ },
192
+ ],
193
+ };
194
+
195
+ const validator = new GraphValidator();
196
+ const result = validator.validate(graph);
197
+
198
+ expect(result.valid).toBe(false);
199
+ expect(result.errors.some((e) => e.type === "type-mismatch")).toBe(true);
200
+ });
201
+
202
+ it("should accept valid empty graph", () => {
203
+ const graph: MediaProcessingGraph = {
204
+ id: "empty-graph",
205
+ nodes: [],
206
+ edges: [],
207
+ };
208
+
209
+ const validator = new GraphValidator();
210
+ const result = validator.validate(graph);
211
+
212
+ expect(result.valid).toBe(true);
213
+ expect(result.errors).toHaveLength(0);
214
+ });
215
+
216
+ it("should handle graph with isolated nodes", () => {
217
+ const graph: MediaProcessingGraph = {
218
+ id: "test-graph",
219
+ nodes: [
220
+ createNode("node1", "source", 0),
221
+ createNode("node2", "effect"),
222
+ createNode("node3", "source", 0), // Isolated
223
+ ],
224
+ edges: [
225
+ {
226
+ fromNodeId: "node1",
227
+ fromPinId: "out",
228
+ toNodeId: "node2",
229
+ toPinId: "in",
230
+ },
231
+ ],
232
+ };
233
+
234
+ const validator = new GraphValidator();
235
+ const result = validator.validate(graph);
236
+
237
+ // Graph structure is valid, but node3 has missing connection
238
+ // This may generate errors for unconnected inputs
239
+ expect(result).toBeDefined();
240
+ });
241
+
242
+ it("should generate warnings for multipass effects", () => {
243
+ const node = createNode("node1", "blur", 0);
244
+ node.requirements.multipass = true;
245
+
246
+ const graph: MediaProcessingGraph = {
247
+ id: "test-graph",
248
+ nodes: [node],
249
+ edges: [],
250
+ };
251
+
252
+ const validator = new GraphValidator();
253
+ const result = validator.validate(graph);
254
+
255
+ expect(result.warnings).toBeDefined();
256
+ expect(result.warnings.some((w) => w.type === "performance")).toBe(true);
257
+ });
258
+
259
+ it("should generate warnings for high temporal radius", () => {
260
+ const node = createNode("node1", "temporal-effect", 0);
261
+ node.requirements.temporalRadius = 10;
262
+
263
+ const graph: MediaProcessingGraph = {
264
+ id: "test-graph",
265
+ nodes: [node],
266
+ edges: [],
267
+ };
268
+
269
+ const validator = new GraphValidator();
270
+ const result = validator.validate(graph);
271
+
272
+ expect(result.warnings).toBeDefined();
273
+ expect(result.warnings.some((w) => w.type === "performance" && w.severity === "high")).toBe(true);
274
+ });
275
+
276
+ it("should generate warnings for stateful effects", () => {
277
+ const node = createNode("node1", "stateful-effect", 0);
278
+ node.capabilities.stateful = true;
279
+
280
+ const graph: MediaProcessingGraph = {
281
+ id: "test-graph",
282
+ nodes: [node],
283
+ edges: [],
284
+ };
285
+
286
+ const validator = new GraphValidator();
287
+ const result = validator.validate(graph);
288
+
289
+ expect(result.warnings).toBeDefined();
290
+ expect(result.warnings.some((w) => w.type === "compatibility")).toBe(true);
291
+ });
292
+
293
+ it("should handle complex valid graph", () => {
294
+ const input1 = createNode("input1", "MediaInput", 0);
295
+ const input2 = createNode("input2", "MediaInput", 0);
296
+ const effect1 = createNode("effect1", "blur");
297
+ const effect2 = createNode("effect2", "brightness");
298
+
299
+ // Create blend node with 2 inputs
300
+ const blend: GraphNode = {
301
+ id: "blend",
302
+ type: "blend",
303
+ version: 1,
304
+ params: {},
305
+ inputs: {
306
+ in1: { id: "in1", name: "input1", type: "Texture" },
307
+ in2: { id: "in2", name: "input2", type: "Texture" },
308
+ },
309
+ outputs: {
310
+ out: { id: "out", name: "output", type: "Texture" },
311
+ },
312
+ capabilities: {
313
+ temporal: false,
314
+ stateful: false,
315
+ spatial: true,
316
+ geometry: false,
317
+ inputsCount: 2,
318
+ },
319
+ requirements: {
320
+ temporalRadius: 0,
321
+ preferredPrecision: "fp16",
322
+ multipass: false,
323
+ supportsHalfResolution: true,
324
+ },
325
+ lifecycle: "Created",
326
+ };
327
+
328
+ const output = createNode("output", "Output");
329
+
330
+ const graph: MediaProcessingGraph = {
331
+ id: "complex-graph",
332
+ nodes: [input1, input2, effect1, effect2, blend, output],
333
+ edges: [
334
+ { fromNodeId: "input1", fromPinId: "out", toNodeId: "effect1", toPinId: "in" },
335
+ { fromNodeId: "input2", fromPinId: "out", toNodeId: "effect2", toPinId: "in" },
336
+ { fromNodeId: "effect1", fromPinId: "out", toNodeId: "blend", toPinId: "in1" },
337
+ { fromNodeId: "effect2", fromPinId: "out", toNodeId: "blend", toPinId: "in2" },
338
+ { fromNodeId: "blend", fromPinId: "out", toNodeId: "output", toPinId: "in" },
339
+ ],
340
+ };
341
+
342
+ const validator = new GraphValidator();
343
+ const result = validator.validate(graph);
344
+
345
+ expect(result.valid).toBe(true);
346
+ expect(result.errors).toHaveLength(0);
347
+ });
348
+ });
349
+
350
+ describe("error reporting", () => {
351
+ it("should provide detailed error information", () => {
352
+ const graph: MediaProcessingGraph = {
353
+ id: "test-graph",
354
+ nodes: [createNode("node1", "effect"), createNode("node2", "effect")],
355
+ edges: [
356
+ {
357
+ fromNodeId: "node1",
358
+ fromPinId: "out",
359
+ toNodeId: "node2",
360
+ toPinId: "in",
361
+ },
362
+ {
363
+ fromNodeId: "node2",
364
+ fromPinId: "out",
365
+ toNodeId: "node1",
366
+ toPinId: "in",
367
+ },
368
+ ],
369
+ };
370
+
371
+ const validator = new GraphValidator();
372
+ const result = validator.validate(graph);
373
+
374
+ expect(result.errors.length).toBeGreaterThan(0);
375
+ const error = result.errors[0];
376
+ expect(error.type).toBeDefined();
377
+ expect(error.message).toBeDefined();
378
+ expect(typeof error.message).toBe("string");
379
+ });
380
+ });
381
+ });
@@ -0,0 +1,263 @@
1
+ /**
2
+ * @clypra/runtime — Graph Builder
3
+ *
4
+ * Constructs media processing graphs from effect definitions.
5
+ * Used by all Labs to build execution graphs.
6
+ */
7
+
8
+ import type { GraphNode, GraphEdge, MediaProcessingGraph, EffectCapabilities, EffectRequirements, GraphDataType } from "./types";
9
+ import { GraphHelper } from "./types";
10
+
11
+ export interface EffectDefinition {
12
+ id: string;
13
+ type: string;
14
+ parameters?: Record<string, any>;
15
+ inputs?: Array<{
16
+ id: string;
17
+ name: string;
18
+ type: GraphDataType;
19
+ }>;
20
+ outputs?: Array<{
21
+ id: string;
22
+ name: string;
23
+ type: GraphDataType;
24
+ }>;
25
+ capabilities?: Partial<EffectCapabilities>;
26
+ requirements?: Partial<EffectRequirements>;
27
+ }
28
+
29
+ export interface MediaInput {
30
+ id: string;
31
+ type: "video" | "image" | "feature-map";
32
+ source: string;
33
+ }
34
+
35
+ /**
36
+ * GraphBuilder - Constructs media processing graphs
37
+ */
38
+ export class GraphBuilder {
39
+ private graphId: string;
40
+ private graph: MediaProcessingGraph;
41
+
42
+ constructor(graphId: string = "graph-" + Date.now()) {
43
+ this.graphId = graphId;
44
+ this.graph = GraphHelper.create(graphId);
45
+ }
46
+
47
+ /**
48
+ * Build graph from effect definition and inputs
49
+ */
50
+ build(effect: EffectDefinition, inputs: MediaInput[]): MediaProcessingGraph {
51
+ // Reset graph
52
+ this.graph = GraphHelper.create(this.graphId);
53
+
54
+ // Add input nodes
55
+ const inputNodes = this.createInputNodes(inputs);
56
+ for (const node of inputNodes) {
57
+ this.graph = GraphHelper.withNode(this.graph, node);
58
+ }
59
+
60
+ // Add effect node
61
+ const effectNode = this.createEffectNode(effect);
62
+ this.graph = GraphHelper.withNode(this.graph, effectNode);
63
+
64
+ // Connect inputs to effect
65
+ for (let i = 0; i < inputNodes.length; i++) {
66
+ const inputNode = inputNodes[i];
67
+ const inputPin = effect.inputs?.[i] || { id: "input", name: "Input", type: "Texture" };
68
+
69
+ this.graph = GraphHelper.withEdge(this.graph, inputNode.id, "output", effectNode.id, inputPin.id);
70
+ }
71
+
72
+ // Add output node
73
+ const outputNode = this.createOutputNode();
74
+ this.graph = GraphHelper.withNode(this.graph, outputNode);
75
+
76
+ // Connect effect to output
77
+ const outputPin = effect.outputs?.[0] || { id: "output", name: "Output", type: "Texture" };
78
+ this.graph = GraphHelper.withEdge(this.graph, effectNode.id, outputPin.id, outputNode.id, "input");
79
+
80
+ return this.graph;
81
+ }
82
+
83
+ /**
84
+ * Build graph from multiple effects (composition)
85
+ */
86
+ buildComposite(effects: EffectDefinition[], inputs: MediaInput[]): MediaProcessingGraph {
87
+ this.graph = GraphHelper.create(this.graphId);
88
+
89
+ // Add input nodes
90
+ const inputNodes = this.createInputNodes(inputs);
91
+ for (const node of inputNodes) {
92
+ this.graph = GraphHelper.withNode(this.graph, node);
93
+ }
94
+
95
+ // Chain effects
96
+ let previousNodes = inputNodes;
97
+ for (const effect of effects) {
98
+ const effectNode = this.createEffectNode(effect);
99
+ this.graph = GraphHelper.withNode(this.graph, effectNode);
100
+
101
+ // Connect previous nodes to this effect
102
+ for (let i = 0; i < previousNodes.length && i < (effect.inputs?.length || 1); i++) {
103
+ const inputPin = effect.inputs?.[i] || { id: "input", name: "Input", type: "Texture" };
104
+ this.graph = GraphHelper.withEdge(this.graph, previousNodes[i].id, "output", effectNode.id, inputPin.id);
105
+ }
106
+
107
+ previousNodes = [effectNode];
108
+ }
109
+
110
+ // Add output node
111
+ const outputNode = this.createOutputNode();
112
+ this.graph = GraphHelper.withNode(this.graph, outputNode);
113
+
114
+ // Connect last effect to output
115
+ if (previousNodes.length > 0) {
116
+ this.graph = GraphHelper.withEdge(this.graph, previousNodes[0].id, "output", outputNode.id, "input");
117
+ }
118
+
119
+ return this.graph;
120
+ }
121
+
122
+ /**
123
+ * Get the current graph
124
+ */
125
+ getGraph(): MediaProcessingGraph {
126
+ return this.graph;
127
+ }
128
+
129
+ /**
130
+ * Create input nodes from media inputs
131
+ */
132
+ private createInputNodes(inputs: MediaInput[]): GraphNode[] {
133
+ return inputs.map((input, index) => ({
134
+ id: `input-${index}`,
135
+ type: "MediaInput",
136
+ version: 0,
137
+ params: {
138
+ source: input.source,
139
+ mediaType: input.type,
140
+ },
141
+ inputs: {},
142
+ outputs: {
143
+ output: {
144
+ id: "output",
145
+ name: "Output",
146
+ type: "Texture",
147
+ },
148
+ },
149
+ capabilities: {
150
+ temporal: false,
151
+ stateful: false,
152
+ spatial: false,
153
+ geometry: false,
154
+ inputsCount: 0,
155
+ },
156
+ requirements: {
157
+ temporalRadius: 0,
158
+ preferredPrecision: "fp16",
159
+ multipass: false,
160
+ supportsHalfResolution: true,
161
+ },
162
+ lifecycle: "Created",
163
+ }));
164
+ }
165
+
166
+ /**
167
+ * Create effect node from definition
168
+ */
169
+ private createEffectNode(effect: EffectDefinition): GraphNode {
170
+ const inputs: Record<string, any> = {};
171
+ if (effect.inputs) {
172
+ for (const input of effect.inputs) {
173
+ inputs[input.id] = {
174
+ id: input.id,
175
+ name: input.name,
176
+ type: input.type,
177
+ };
178
+ }
179
+ } else {
180
+ // Default single input
181
+ inputs.input = {
182
+ id: "input",
183
+ name: "Input",
184
+ type: "Texture",
185
+ };
186
+ }
187
+
188
+ const outputs: Record<string, any> = {};
189
+ if (effect.outputs) {
190
+ for (const output of effect.outputs) {
191
+ outputs[output.id] = {
192
+ id: output.id,
193
+ name: output.name,
194
+ type: output.type,
195
+ };
196
+ }
197
+ } else {
198
+ // Default single output
199
+ outputs.output = {
200
+ id: "output",
201
+ name: "Output",
202
+ type: "Texture",
203
+ };
204
+ }
205
+
206
+ return {
207
+ id: effect.id,
208
+ type: effect.type,
209
+ version: 0,
210
+ params: effect.parameters || {},
211
+ inputs,
212
+ outputs,
213
+ capabilities: {
214
+ temporal: effect.capabilities?.temporal || false,
215
+ stateful: effect.capabilities?.stateful || false,
216
+ spatial: effect.capabilities?.spatial || true,
217
+ geometry: effect.capabilities?.geometry || false,
218
+ inputsCount: effect.inputs?.length || 1,
219
+ },
220
+ requirements: {
221
+ temporalRadius: effect.requirements?.temporalRadius || 0,
222
+ preferredPrecision: effect.requirements?.preferredPrecision || "fp16",
223
+ multipass: effect.requirements?.multipass || false,
224
+ supportsHalfResolution: effect.requirements?.supportsHalfResolution || true,
225
+ },
226
+ lifecycle: "Created",
227
+ };
228
+ }
229
+
230
+ /**
231
+ * Create output node
232
+ */
233
+ private createOutputNode(): GraphNode {
234
+ return {
235
+ id: "output",
236
+ type: "Output",
237
+ version: 0,
238
+ params: {},
239
+ inputs: {
240
+ input: {
241
+ id: "input",
242
+ name: "Input",
243
+ type: "Texture",
244
+ },
245
+ },
246
+ outputs: {},
247
+ capabilities: {
248
+ temporal: false,
249
+ stateful: false,
250
+ spatial: false,
251
+ geometry: false,
252
+ inputsCount: 1,
253
+ },
254
+ requirements: {
255
+ temporalRadius: 0,
256
+ preferredPrecision: "fp16",
257
+ multipass: false,
258
+ supportsHalfResolution: true,
259
+ },
260
+ lifecycle: "Created",
261
+ };
262
+ }
263
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Graph Builder - Media Processing Graph Construction
3
+ *
4
+ * This module handles the construction of media processing graphs from effect definitions.
5
+ * It is responsible for:
6
+ * - Building node trees from effect compositions
7
+ * - Validating graph structure
8
+ * - Resolving dependencies
9
+ * - Capability tracking
10
+ */
11
+
12
+ export * from "./types";
13
+ export * from "./builder";
14
+ export * from "./validator";