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.
Files changed (3) hide show
  1. package/index.js +5 -1
  2. package/package.json +2 -1
  3. 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.0.0',
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.1",
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 };