Skip to content

Commit ae18b03

Browse files
authored
Merge pull request #11 from constructive-io/feat/specification
Feat/specification
2 parents 35e7775 + 4c008bf commit ae18b03

118 files changed

Lines changed: 8795 additions & 310 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/evaluator/README.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,14 @@ console.log(result); // 8
5858

5959
- **Lazy evaluation**: Only evaluates nodes that are needed for the output
6060
- **Multi-input ports**: Supports ports that accept multiple incoming edges (values collected in edge array order)
61-
- **Boundary nodes**: Supports `@in/`, `@out/`, and `@prop/` boundary nodes for graph inputs/outputs/props
61+
- **Boundary nodes**: Supports `graphInput`, `graphOutput`, and `graphProp` boundary nodes for graph inputs/outputs/props
62+
63+
## Boundary Node Design
64+
65+
Boundary nodes use a **property-based approach**:
66+
- Node keys are normal identifiers (e.g., `input_a`, `output_result`, `prop_scale`)
67+
- The node's `type` property identifies it as a boundary node: `graphInput`, `graphOutput`, `graphProp`
68+
- The port/prop name is stored as a property: `{ name: 'portName', value: 'a' }` or `{ name: 'propName', value: 'scale' }`
6269

6370
## API
6471

@@ -72,7 +79,7 @@ Evaluates a graph starting from the specified output node/port.
7279
- `definitions: NodeDefinitionWithImpl[]` - Node definitions with implementations
7380
- `outputNode: string` - The node to get output from
7481
- `outputPort: string` - The port to get output from
75-
- `inputs?: Record<string, any>` - External inputs for `@in/` nodes
76-
- `props?: Record<string, any>` - Props for `@prop/` nodes
82+
- `inputs?: Record<string, any>` - External inputs for `graphInput` nodes (keyed by portName)
83+
- `props?: Record<string, any>` - Props for `graphProp` nodes (keyed by propName)
7784

7885
**Returns:** The value at the specified output port
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import type { NodeDefinitionWithImpl } from '../src/types';
2+
/**
3+
* Core node definitions with implementations.
4+
* These nodes handle JSON manipulation, flow control, and string operations.
5+
*/
6+
/**
7+
* core/json/select - Extract a value from JSON by path
8+
*
9+
* Props:
10+
* - path: string (dot-path like "a.b.c" or "data.user.email")
11+
*
12+
* Inputs:
13+
* - obj: json (the object to extract from)
14+
*
15+
* Outputs:
16+
* - value: any (the extracted value)
17+
*/
18+
export declare const jsonSelectDef: NodeDefinitionWithImpl;
19+
/**
20+
* core/json/object - Build a JSON object from named inputs
21+
*
22+
* This node has dynamic inputs - any input wired to it becomes a key in the output object.
23+
* The implementation receives all inputs as a Record<string, any>.
24+
*
25+
* Inputs:
26+
* - (dynamic) arbitrary named inputs
27+
*
28+
* Outputs:
29+
* - value: json (the constructed object)
30+
*/
31+
export declare const jsonObjectDef: NodeDefinitionWithImpl;
32+
/**
33+
* core/flow/guard - Stop the flow if a condition fails
34+
*
35+
* Inputs:
36+
* - ok: boolean (the condition to check)
37+
* - error?: json (optional error info)
38+
*
39+
* Outputs:
40+
* - pass: signal (emitted if ok is true)
41+
* - fail: signal (emitted if ok is false)
42+
* - error: json (the error if failed)
43+
*/
44+
export declare const flowGuardDef: NodeDefinitionWithImpl;
45+
/**
46+
* core/string/template - Build a string from a template with placeholders
47+
*
48+
* Props:
49+
* - template: string (template with {{placeholder}} syntax)
50+
*
51+
* Inputs:
52+
* - (dynamic) values to substitute into the template
53+
*
54+
* Outputs:
55+
* - value: string (the resulting string)
56+
*/
57+
export declare const stringTemplateDef: NodeDefinitionWithImpl;
58+
/**
59+
* core/string/concat - Concatenate strings with an optional separator
60+
*
61+
* Props:
62+
* - prefix: string (optional prefix)
63+
* - suffix: string (optional suffix)
64+
*
65+
* Inputs:
66+
* - value: string (the main value)
67+
*
68+
* Outputs:
69+
* - value: string (the resulting string)
70+
*/
71+
export declare const stringConcatDef: NodeDefinitionWithImpl;
72+
export declare const coreDefinitions: NodeDefinitionWithImpl[];
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.coreDefinitions = exports.stringConcatDef = exports.stringTemplateDef = exports.flowGuardDef = exports.jsonObjectDef = exports.jsonSelectDef = void 0;
4+
/**
5+
* Core node definitions with implementations.
6+
* These nodes handle JSON manipulation, flow control, and string operations.
7+
*/
8+
/**
9+
* core/json/select - Extract a value from JSON by path
10+
*
11+
* Props:
12+
* - path: string (dot-path like "a.b.c" or "data.user.email")
13+
*
14+
* Inputs:
15+
* - obj: json (the object to extract from)
16+
*
17+
* Outputs:
18+
* - value: any (the extracted value)
19+
*/
20+
exports.jsonSelectDef = {
21+
context: 'core',
22+
category: 'json',
23+
type: 'core/json/select',
24+
icon: 'circle',
25+
inputs: [
26+
{ name: 'obj', type: 'json' }
27+
],
28+
outputs: [
29+
{ name: 'value', type: 'any' }
30+
],
31+
props: [
32+
{ name: 'path', type: 'string', default: '' }
33+
],
34+
description: 'Extract a value from JSON by dot-path',
35+
impl: (inputs, props) => {
36+
const { obj } = inputs;
37+
const { path } = props;
38+
if (!path || obj === undefined || obj === null) {
39+
return { value: undefined };
40+
}
41+
const parts = path.split('.');
42+
let current = obj;
43+
for (const part of parts) {
44+
if (current === undefined || current === null) {
45+
return { value: undefined };
46+
}
47+
// Handle array index access like "items.0.name"
48+
const index = parseInt(part, 10);
49+
if (!isNaN(index) && Array.isArray(current)) {
50+
current = current[index];
51+
}
52+
else if (typeof current === 'object') {
53+
current = current[part];
54+
}
55+
else {
56+
return { value: undefined };
57+
}
58+
}
59+
return { value: current };
60+
}
61+
};
62+
/**
63+
* core/json/object - Build a JSON object from named inputs
64+
*
65+
* This node has dynamic inputs - any input wired to it becomes a key in the output object.
66+
* The implementation receives all inputs as a Record<string, any>.
67+
*
68+
* Inputs:
69+
* - (dynamic) arbitrary named inputs
70+
*
71+
* Outputs:
72+
* - value: json (the constructed object)
73+
*/
74+
exports.jsonObjectDef = {
75+
context: 'core',
76+
category: 'json',
77+
type: 'core/json/object',
78+
icon: 'braces',
79+
inputs: [], // Dynamic inputs - any input name is valid
80+
outputs: [
81+
{ name: 'value', type: 'json' }
82+
],
83+
props: [],
84+
description: 'Build a JSON object from named inputs',
85+
impl: (inputs) => {
86+
// All inputs become keys in the output object
87+
return { value: { ...inputs } };
88+
}
89+
};
90+
/**
91+
* core/flow/guard - Stop the flow if a condition fails
92+
*
93+
* Inputs:
94+
* - ok: boolean (the condition to check)
95+
* - error?: json (optional error info)
96+
*
97+
* Outputs:
98+
* - pass: signal (emitted if ok is true)
99+
* - fail: signal (emitted if ok is false)
100+
* - error: json (the error if failed)
101+
*/
102+
exports.flowGuardDef = {
103+
context: 'core',
104+
category: 'flow',
105+
type: 'core/flow/guard',
106+
icon: 'zap',
107+
inputs: [
108+
{ name: 'ok', type: 'boolean' },
109+
{ name: 'error', type: 'json' }
110+
],
111+
outputs: [
112+
{ name: 'pass', type: 'signal' },
113+
{ name: 'fail', type: 'signal' },
114+
{ name: 'error', type: 'json' }
115+
],
116+
props: [],
117+
description: 'Stop the flow if a condition fails',
118+
impl: (inputs) => {
119+
const { ok, error } = inputs;
120+
if (ok) {
121+
return {
122+
pass: true,
123+
fail: false,
124+
error: null
125+
};
126+
}
127+
else {
128+
return {
129+
pass: false,
130+
fail: true,
131+
error: error || { message: 'Guard condition failed' }
132+
};
133+
}
134+
}
135+
};
136+
/**
137+
* core/string/template - Build a string from a template with placeholders
138+
*
139+
* Props:
140+
* - template: string (template with {{placeholder}} syntax)
141+
*
142+
* Inputs:
143+
* - (dynamic) values to substitute into the template
144+
*
145+
* Outputs:
146+
* - value: string (the resulting string)
147+
*/
148+
exports.stringTemplateDef = {
149+
context: 'core',
150+
category: 'string',
151+
type: 'core/string/template',
152+
icon: 'quote',
153+
inputs: [], // Dynamic inputs based on template placeholders
154+
outputs: [
155+
{ name: 'value', type: 'string' }
156+
],
157+
props: [
158+
{ name: 'template', type: 'string', default: '' }
159+
],
160+
description: 'Build a string from a template with {{placeholder}} syntax',
161+
impl: (inputs, props) => {
162+
const { template } = props;
163+
if (!template) {
164+
return { value: '' };
165+
}
166+
// Replace {{placeholder}} with input values
167+
const result = template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
168+
const value = inputs[key];
169+
if (value === undefined || value === null) {
170+
return match; // Keep placeholder if no value
171+
}
172+
return String(value);
173+
});
174+
return { value: result };
175+
}
176+
};
177+
/**
178+
* core/string/concat - Concatenate strings with an optional separator
179+
*
180+
* Props:
181+
* - prefix: string (optional prefix)
182+
* - suffix: string (optional suffix)
183+
*
184+
* Inputs:
185+
* - value: string (the main value)
186+
*
187+
* Outputs:
188+
* - value: string (the resulting string)
189+
*/
190+
exports.stringConcatDef = {
191+
context: 'core',
192+
category: 'string',
193+
type: 'core/string/concat',
194+
icon: 'link',
195+
inputs: [
196+
{ name: 'value', type: 'string' }
197+
],
198+
outputs: [
199+
{ name: 'value', type: 'string' }
200+
],
201+
props: [
202+
{ name: 'prefix', type: 'string', default: '' },
203+
{ name: 'suffix', type: 'string', default: '' }
204+
],
205+
description: 'Concatenate strings with optional prefix/suffix',
206+
impl: (inputs, props) => {
207+
const { value = '' } = inputs;
208+
const { prefix = '', suffix = '' } = props;
209+
return { value: `${prefix}${value}${suffix}` };
210+
}
211+
};
212+
exports.coreDefinitions = [
213+
exports.jsonSelectDef,
214+
exports.jsonObjectDef,
215+
exports.flowGuardDef,
216+
exports.stringTemplateDef,
217+
exports.stringConcatDef
218+
];
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import type { NodeDefinitionWithImpl } from '../src/types';
2+
/**
3+
* Math node definitions with implementations for testing.
4+
*/
5+
export declare const constNumberDef: NodeDefinitionWithImpl;
6+
export declare const addDef: NodeDefinitionWithImpl;
7+
export declare const multiplyDef: NodeDefinitionWithImpl;
8+
export declare const mathDefinitions: NodeDefinitionWithImpl[];
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.mathDefinitions = exports.multiplyDef = exports.addDef = exports.constNumberDef = void 0;
4+
/**
5+
* Math node definitions with implementations for testing.
6+
*/
7+
exports.constNumberDef = {
8+
context: 'js',
9+
category: 'const',
10+
type: 'js/const/number',
11+
icon: 'hash',
12+
outputs: [{ name: 'value', type: 'number' }],
13+
props: [{ name: 'value', type: 'number', default: 0 }],
14+
description: 'Outputs a constant number value',
15+
impl: (_inputs, props) => ({
16+
value: props.value ?? 0
17+
})
18+
};
19+
exports.addDef = {
20+
context: 'js',
21+
category: 'math',
22+
type: 'js/math/add',
23+
icon: 'plus',
24+
inputs: [
25+
{ name: 'a', type: 'number' },
26+
{ name: 'b', type: 'number' }
27+
],
28+
outputs: [{ name: 'sum', type: 'number' }],
29+
description: 'Adds two numbers',
30+
impl: (inputs) => ({
31+
sum: (inputs.a ?? 0) + (inputs.b ?? 0)
32+
})
33+
};
34+
exports.multiplyDef = {
35+
context: 'js',
36+
category: 'math',
37+
type: 'js/math/multiply',
38+
icon: 'x',
39+
inputs: [
40+
{ name: 'a', type: 'number' },
41+
{ name: 'b', type: 'number' }
42+
],
43+
outputs: [{ name: 'product', type: 'number' }],
44+
description: 'Multiplies two numbers',
45+
impl: (inputs) => ({
46+
product: (inputs.a ?? 0) * (inputs.b ?? 0)
47+
})
48+
};
49+
exports.mathDefinitions = [
50+
exports.constNumberDef,
51+
exports.addDef,
52+
exports.multiplyDef
53+
];

0 commit comments

Comments
 (0)