0n-spec 1.0.1 → 1.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.
- package/index.js +5 -1
- package/package.json +2 -1
- package/resolve.js +281 -0
package/index.js
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const path = require('path');
|
|
12
|
+
const { resolve, deepGet, evaluateExpression } = require('./resolve');
|
|
12
13
|
|
|
13
14
|
const schemasDir = path.join(__dirname, 'schemas');
|
|
14
15
|
|
|
@@ -246,6 +247,9 @@ module.exports = {
|
|
|
246
247
|
init,
|
|
247
248
|
list,
|
|
248
249
|
loadSchema,
|
|
250
|
+
resolve,
|
|
251
|
+
deepGet,
|
|
252
|
+
evaluateExpression,
|
|
249
253
|
TYPES: ['connection', 'workflow', 'snapshot', 'config', 'execution'],
|
|
250
|
-
VERSION: '1.
|
|
254
|
+
VERSION: '1.1.0',
|
|
251
255
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0n-spec",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The .0n Standard - Universal configuration format for AI orchestration. Powers 0nMCP (535 tools, 26 services).",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
"files": [
|
|
45
45
|
"index.js",
|
|
46
|
+
"resolve.js",
|
|
46
47
|
"cli.js",
|
|
47
48
|
"schemas",
|
|
48
49
|
"examples",
|
package/resolve.js
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* .0n Standard — Template Resolution Engine
|
|
3
|
+
*
|
|
4
|
+
* Resolves {{expression}} templates against a context object.
|
|
5
|
+
*
|
|
6
|
+
* Supported expressions:
|
|
7
|
+
* {{inputs.x}} — input values
|
|
8
|
+
* {{step_id.nested.field}} — step output references
|
|
9
|
+
* {{env.VAR}} — environment variables
|
|
10
|
+
* {{now}} — ISO timestamp
|
|
11
|
+
* {{uuid}} — UUID v4
|
|
12
|
+
* {{amount * 100}} — safe math
|
|
13
|
+
* {{grade == 'A'}} — condition evaluation
|
|
14
|
+
*
|
|
15
|
+
* Context shape: { inputs: {}, steps: { step_id: { ...output } }, env: process.env }
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { randomUUID } = require('crypto');
|
|
19
|
+
|
|
20
|
+
// ── Deep path access: "a.b[0].c" → obj.a.b[0].c ─────────
|
|
21
|
+
|
|
22
|
+
function deepGet(obj, path) {
|
|
23
|
+
if (!obj || !path) return undefined;
|
|
24
|
+
|
|
25
|
+
const segments = path.replace(/\[(\d+)\]/g, '.$1').split('.');
|
|
26
|
+
let current = obj;
|
|
27
|
+
|
|
28
|
+
for (const seg of segments) {
|
|
29
|
+
if (current == null) return undefined;
|
|
30
|
+
current = current[seg];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return current;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── Safe math/condition tokenizer (no eval) ──────────────
|
|
37
|
+
|
|
38
|
+
const OPERATORS = {
|
|
39
|
+
'+': (a, b) => a + b,
|
|
40
|
+
'-': (a, b) => a - b,
|
|
41
|
+
'*': (a, b) => a * b,
|
|
42
|
+
'/': (a, b) => b !== 0 ? a / b : 0,
|
|
43
|
+
'%': (a, b) => b !== 0 ? a % b : 0,
|
|
44
|
+
'==': (a, b) => a === b,
|
|
45
|
+
'!=': (a, b) => a !== b,
|
|
46
|
+
'>': (a, b) => a > b,
|
|
47
|
+
'<': (a, b) => a < b,
|
|
48
|
+
'>=': (a, b) => a >= b,
|
|
49
|
+
'<=': (a, b) => a <= b,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Operator precedence groups (higher = binds tighter)
|
|
53
|
+
const PRECEDENCE = {
|
|
54
|
+
'==': 1, '!=': 1, '>': 1, '<': 1, '>=': 1, '<=': 1,
|
|
55
|
+
'+': 2, '-': 2,
|
|
56
|
+
'*': 3, '/': 3, '%': 3,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function tokenize(expr) {
|
|
60
|
+
const tokens = [];
|
|
61
|
+
let i = 0;
|
|
62
|
+
|
|
63
|
+
while (i < expr.length) {
|
|
64
|
+
// Skip whitespace
|
|
65
|
+
if (/\s/.test(expr[i])) { i++; continue; }
|
|
66
|
+
|
|
67
|
+
// String literal
|
|
68
|
+
if (expr[i] === "'" || expr[i] === '"') {
|
|
69
|
+
const quote = expr[i];
|
|
70
|
+
let str = '';
|
|
71
|
+
i++;
|
|
72
|
+
while (i < expr.length && expr[i] !== quote) {
|
|
73
|
+
str += expr[i];
|
|
74
|
+
i++;
|
|
75
|
+
}
|
|
76
|
+
i++; // closing quote
|
|
77
|
+
tokens.push({ type: 'value', value: str });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Two-char operators
|
|
82
|
+
if (i + 1 < expr.length) {
|
|
83
|
+
const two = expr[i] + expr[i + 1];
|
|
84
|
+
if (OPERATORS[two]) {
|
|
85
|
+
tokens.push({ type: 'op', value: two });
|
|
86
|
+
i += 2;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Single-char operators
|
|
92
|
+
if (OPERATORS[expr[i]]) {
|
|
93
|
+
tokens.push({ type: 'op', value: expr[i] });
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Number
|
|
99
|
+
if (/[\d.]/.test(expr[i])) {
|
|
100
|
+
let num = '';
|
|
101
|
+
while (i < expr.length && /[\d.]/.test(expr[i])) {
|
|
102
|
+
num += expr[i];
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
tokens.push({ type: 'value', value: parseFloat(num) });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Identifier (variable reference)
|
|
110
|
+
if (/[a-zA-Z_$]/.test(expr[i])) {
|
|
111
|
+
let ident = '';
|
|
112
|
+
while (i < expr.length && /[a-zA-Z0-9_.$\[\]]/.test(expr[i])) {
|
|
113
|
+
ident += expr[i];
|
|
114
|
+
i++;
|
|
115
|
+
}
|
|
116
|
+
// Boolean literals
|
|
117
|
+
if (ident === 'true') {
|
|
118
|
+
tokens.push({ type: 'value', value: true });
|
|
119
|
+
} else if (ident === 'false') {
|
|
120
|
+
tokens.push({ type: 'value', value: false });
|
|
121
|
+
} else if (ident === 'null') {
|
|
122
|
+
tokens.push({ type: 'value', value: null });
|
|
123
|
+
} else {
|
|
124
|
+
tokens.push({ type: 'ref', value: ident });
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Skip unknown chars
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return tokens;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function evaluateTokens(tokens, context) {
|
|
137
|
+
// Resolve all references to values
|
|
138
|
+
const resolved = tokens.map(t => {
|
|
139
|
+
if (t.type === 'ref') {
|
|
140
|
+
return { type: 'value', value: resolveRef(t.value, context) };
|
|
141
|
+
}
|
|
142
|
+
return t;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// If single value, return it
|
|
146
|
+
if (resolved.length === 1 && resolved[0].type === 'value') {
|
|
147
|
+
return resolved[0].value;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Shunting-yard evaluation with precedence
|
|
151
|
+
const values = [];
|
|
152
|
+
const ops = [];
|
|
153
|
+
|
|
154
|
+
function applyOp() {
|
|
155
|
+
const op = ops.pop();
|
|
156
|
+
const b = values.pop();
|
|
157
|
+
const a = values.pop();
|
|
158
|
+
const fn = OPERATORS[op];
|
|
159
|
+
values.push(fn != null ? fn(a, b) : undefined);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const token of resolved) {
|
|
163
|
+
if (token.type === 'value') {
|
|
164
|
+
values.push(token.value);
|
|
165
|
+
} else if (token.type === 'op') {
|
|
166
|
+
while (
|
|
167
|
+
ops.length > 0 &&
|
|
168
|
+
(PRECEDENCE[ops[ops.length - 1]] || 0) >= (PRECEDENCE[token.value] || 0)
|
|
169
|
+
) {
|
|
170
|
+
applyOp();
|
|
171
|
+
}
|
|
172
|
+
ops.push(token.value);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
while (ops.length > 0) {
|
|
177
|
+
applyOp();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return values[0];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Reference resolution ─────────────────────────────────
|
|
184
|
+
|
|
185
|
+
function resolveRef(ref, context) {
|
|
186
|
+
// Built-ins
|
|
187
|
+
if (ref === 'now') return new Date().toISOString();
|
|
188
|
+
if (ref === 'uuid') return randomUUID();
|
|
189
|
+
|
|
190
|
+
// env.VAR
|
|
191
|
+
if (ref.startsWith('env.')) {
|
|
192
|
+
return deepGet(context.env || process.env, ref.slice(4));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// inputs.x
|
|
196
|
+
if (ref.startsWith('inputs.')) {
|
|
197
|
+
return deepGet(context.inputs, ref.slice(7));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// step references — try context.steps first, then top-level context
|
|
201
|
+
const val = deepGet(context.steps, ref);
|
|
202
|
+
if (val !== undefined) return val;
|
|
203
|
+
|
|
204
|
+
// Fallback: try direct context lookup
|
|
205
|
+
return deepGet(context, ref);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Expression evaluation ────────────────────────────────
|
|
209
|
+
|
|
210
|
+
function evaluateExpression(expr, context) {
|
|
211
|
+
const trimmed = expr.trim();
|
|
212
|
+
|
|
213
|
+
// Fast-path: simple built-ins
|
|
214
|
+
if (trimmed === 'now') return new Date().toISOString();
|
|
215
|
+
if (trimmed === 'uuid') return randomUUID();
|
|
216
|
+
|
|
217
|
+
// Fast-path: simple reference (no operators)
|
|
218
|
+
if (/^[a-zA-Z_$][a-zA-Z0-9_.$\[\]]*$/.test(trimmed)) {
|
|
219
|
+
return resolveRef(trimmed, context);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Tokenize and evaluate (handles math + conditions)
|
|
223
|
+
const tokens = tokenize(trimmed);
|
|
224
|
+
if (tokens.length === 0) return trimmed;
|
|
225
|
+
|
|
226
|
+
return evaluateTokens(tokens, context);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Main resolve function ────────────────────────────────
|
|
230
|
+
|
|
231
|
+
const TEMPLATE_RE = /\{\{(.+?)\}\}/g;
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Resolve a template value against a context.
|
|
235
|
+
*
|
|
236
|
+
* @param {*} template - String, object, or array containing {{}} expressions
|
|
237
|
+
* @param {object} context - { inputs: {}, steps: {}, env: {} }
|
|
238
|
+
* @returns {*} Resolved value with native types preserved
|
|
239
|
+
*/
|
|
240
|
+
function resolve(template, context) {
|
|
241
|
+
if (template == null) return template;
|
|
242
|
+
|
|
243
|
+
// Recurse into arrays
|
|
244
|
+
if (Array.isArray(template)) {
|
|
245
|
+
return template.map(item => resolve(item, context));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Recurse into objects
|
|
249
|
+
if (typeof template === 'object') {
|
|
250
|
+
const result = {};
|
|
251
|
+
for (const [key, value] of Object.entries(template)) {
|
|
252
|
+
result[resolve(key, context)] = resolve(value, context);
|
|
253
|
+
}
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Only process strings
|
|
258
|
+
if (typeof template !== 'string') return template;
|
|
259
|
+
|
|
260
|
+
// Check if the entire string is a single expression → return native type
|
|
261
|
+
const singleMatch = template.match(/^\{\{(.+?)\}\}$/);
|
|
262
|
+
if (singleMatch) {
|
|
263
|
+
return evaluateExpression(singleMatch[1], context);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Mixed template: "Hello {{inputs.name}}, total: {{inputs.amount * 100}}"
|
|
267
|
+
// → always returns string
|
|
268
|
+
if (!TEMPLATE_RE.test(template)) return template;
|
|
269
|
+
|
|
270
|
+
// Reset lastIndex since we tested above
|
|
271
|
+
TEMPLATE_RE.lastIndex = 0;
|
|
272
|
+
|
|
273
|
+
return template.replace(TEMPLATE_RE, (_, expr) => {
|
|
274
|
+
const val = evaluateExpression(expr, context);
|
|
275
|
+
if (val === undefined || val === null) return '';
|
|
276
|
+
if (typeof val === 'object') return JSON.stringify(val);
|
|
277
|
+
return String(val);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { resolve, deepGet, evaluateExpression };
|