@morit/cli 1.6.0 → 1.9.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.
@@ -0,0 +1,252 @@
1
+ import { assertSchema, validateValueSchema } from './script-schema.js';
2
+ import { SCRIPT_CONTRACT, validateAst } from './script-contract.js';
3
+ // JSON-only interpreter. No host object or native callable enters the scope.
4
+ export class RuntimeError extends Error {
5
+ constructor(code, message, span) { super(message); this.name = 'RuntimeError'; this.code = code; this.span = span; }
6
+ toJSON() { return { code: this.code, message: this.message, span: this.span }; }
7
+ }
8
+ const forbidden = new Set(['__proto__', 'prototype', 'constructor']);
9
+ const encoder = new TextEncoder();
10
+ const size = value => encoder.encode(value).length;
11
+ const own = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
12
+
13
+ const same = (a, b) => {
14
+ if (a === b) return true;
15
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object' || Array.isArray(a) !== Array.isArray(b)) return false;
16
+ const keys = Object.keys(a); return keys.length === Object.keys(b).length && keys.every(k => own(b, k) && same(a[k], b[k]));
17
+ };
18
+ const truth = value => value !== null && value !== false && value !== 0 && value !== '';
19
+
20
+ export class ScriptRuntime {
21
+ constructor({ modules = {}, profile = 'ui' } = {}) {
22
+ this.modules = modules; this.profile = SCRIPT_CONTRACT.profiles[profile];
23
+ for (const module of Object.values(modules)) {
24
+ validateAst(module.ast || module);
25
+ for (const schema of Object.values(module.exports ?? {})) { validateValueSchema(schema.input_schema); validateValueSchema(schema.output_schema); }
26
+ }
27
+ this.inline = profile === 'inline';
28
+ if (!this.profile) throw new RuntimeError('PROFILE', 'Unknown execution profile');
29
+ }
30
+ start(scope) {
31
+ this.started = performance.now(); this.steps = 0; this.memory = 0; this.stack = [];
32
+ this.checkValue(scope); this.scope = structuredClone(scope);
33
+ }
34
+ error(code, message, node) { throw new RuntimeError(code, message, node?.span); }
35
+ tick(node) {
36
+ if (++this.steps > this.profile.steps) this.error('STEP_LIMIT', 'Execution step limit exceeded', node);
37
+ if (performance.now() - this.started > this.profile.timeout) this.error('TIMEOUT', 'Execution timed out', node);
38
+ }
39
+ checkValue(value, depth = 0) {
40
+ this.memory += 8;
41
+ if (depth > 20 || this.memory > this.profile.memory) this.error('MEMORY_LIMIT', 'Value budget exceeded');
42
+ if (value === null || typeof value === 'boolean') return value;
43
+ if (typeof value === 'number') { if (!Number.isFinite(value) || Math.abs(value) > Number.MAX_SAFE_INTEGER) this.error('TYPE', 'Number outside portable range'); return value; }
44
+ if (typeof value === 'string') {
45
+ const length = size(value); this.memory += length;
46
+ if (length > 32768 || this.memory > this.profile.memory) this.error('MEMORY_LIMIT', 'String budget exceeded'); return value;
47
+ }
48
+ if (Array.isArray(value)) {
49
+ if (value.length > 1000) this.error('MEMORY_LIMIT', 'Array exceeds 1000 entries');
50
+ for (const item of value) this.checkValue(item, depth + 1); return value;
51
+ }
52
+ if (value && typeof value === 'object' && [Object.prototype, null].includes(Object.getPrototypeOf(value))) {
53
+ const keys = Object.keys(value); if (keys.length > 512) this.error('MEMORY_LIMIT', 'Object exceeds 512 keys');
54
+ for (const key of keys) {
55
+ if (forbidden.has(key)) this.error('FORBIDDEN', 'Forbidden object key');
56
+ this.checkValue(key, depth + 1); this.checkValue(value[key], depth + 1);
57
+ }
58
+ return value;
59
+ }
60
+ this.error('TYPE', 'Runtime values must be finite JSON');
61
+ }
62
+ finish(value) {
63
+ this.tick();
64
+ this.checkValue(value);
65
+ if (size(JSON.stringify(value)) > 262144) this.error('OUTPUT_LIMIT', 'Result exceeds 256 KiB');
66
+ return { value, diagnostics: { steps: this.steps, allocated_bytes: this.memory, elapsed_ms: performance.now() - this.started } };
67
+ }
68
+ evaluate(ast, scope = {}) { validateAst(ast, {inline: this.inline}); this.start(scope); return this.finish(this.expr(ast, [new Map(Object.entries(this.scope))])); }
69
+ invoke(moduleId, name, input, scope = {}) {
70
+ this.start(scope); this.checkValue(input);
71
+ return this.finish(this.callModule(moduleId, name, [input], [new Map(Object.entries(this.scope))]));
72
+ }
73
+ find(env, name) { return env.findLast(frame => frame.has(name)); }
74
+ callModule(moduleId, name, args, env) {
75
+ const module = this.modules[moduleId]?.ast || this.modules[moduleId];
76
+ const fn = module?.functions?.find(value => value.name === name && value.exported);
77
+ if (!fn) this.error('FUNCTION', `Unknown export ${moduleId}.${name}`);
78
+ const schema = this.modules[moduleId]?.exports?.[name];
79
+ if (schema) assertSchema(args[0], schema.input_schema);
80
+ const result = this.callFunction(fn, args, env, moduleId);
81
+ if (schema) assertSchema(result, schema.output_schema);
82
+ return result;
83
+ }
84
+ callFunction(fn, args, env, moduleId) {
85
+ if (this.stack.includes(fn) || this.stack.length >= 24) this.error('CALL_LIMIT', 'Recursion or call depth exceeded', fn);
86
+ this.stack.push(fn);
87
+ const frame = new Map(fn.params.map((name, index) => [name, args[index] ?? null]));
88
+ frame.set('$module', moduleId);
89
+ try { return this.block(fn.body, [...env, frame])?.value ?? null; }
90
+ finally { this.stack.pop(); }
91
+ }
92
+ block(body, env) {
93
+ const local = [...env, new Map()];
94
+ for (const node of body) {
95
+ this.tick(node);
96
+ if (node.type === 'return') return { value: this.expr(node.value, local) };
97
+ if (node.type === 'declare') {
98
+ if (local.at(-1).has(node.name)) this.error('NAME', 'Duplicate variable', node);
99
+ local.at(-1).set(node.name, this.expr(node.value, local));
100
+ local.at(-1).set(`$mutable:${node.name}`, node.mutable);
101
+ } else if (node.type === 'assign') {
102
+ const frame = this.find(local, node.name);
103
+ if (!frame?.get(`$mutable:${node.name}`)) this.error('ASSIGN', 'Assignment requires local let variable', node);
104
+ frame.set(node.name, this.expr(node.value, local));
105
+ } else if (node.type === 'if') {
106
+ const result = this.block(truth(this.expr(node.condition, local)) ? node.then : node.else, local); if (result) return result;
107
+ } else if (node.type === 'for') {
108
+ const items = this.expr(node.items, local); if (!Array.isArray(items) || items.length > 1000) this.error('TYPE', 'for requires bounded array', node);
109
+ for (const item of items) { this.tick(node); const result = this.block(node.body, [...local, new Map([[node.name, item]])]); if (result) return result; }
110
+ } else if (node.type === 'expression') this.expr(node.value, local);
111
+ else if (node.type !== 'declare') this.error('AST', 'Unknown statement', node);
112
+ }
113
+ return null;
114
+ }
115
+ expr(node, env) {
116
+ this.tick(node);
117
+ switch (node?.type) {
118
+ case 'literal': return this.checkValue(node.value);
119
+ case 'ref': return node.path.slice(1).split('/').reduce((value, key) => this.member(value, key.replaceAll('~1', '/').replaceAll('~0', '~')), this.scope);
120
+ case 'identifier': {
121
+ const frame = this.find(env, node.name); if (!frame) this.error('NAME', `Unknown variable ${node.name}`, node); return frame.get(node.name);
122
+ }
123
+ case 'array': return this.checkValue(node.items.map(item => this.expr(item, env)));
124
+ case 'object': return this.checkValue(Object.fromEntries(node.entries.map(entry => [entry.key, this.expr(entry.value, env)])));
125
+ case 'member': return this.member(this.expr(node.target, env), this.expr(node.key, env));
126
+ case 'conditional': return this.expr(truth(this.expr(node.condition, env)) ? node.then : node.else, env);
127
+ case 'unary': {
128
+ const value = this.expr(node.value, env); if (node.op === '!') return !truth(value);
129
+ if (typeof value !== 'number') this.error('TYPE', 'Unary arithmetic requires number', node);
130
+ return node.op === '-' ? -value : value;
131
+ }
132
+ case 'binary': return this.binary(node, env);
133
+ case 'call': return this.call(node, env);
134
+ default: this.error('AST', `Unknown expression ${node?.type}`, node);
135
+ }
136
+ }
137
+ member(value, key) {
138
+ if (typeof key !== 'string' && !Number.isInteger(key)) this.error('TYPE', 'Invalid member key');
139
+ if (forbidden.has(String(key))) this.error('FORBIDDEN', 'Forbidden member');
140
+ if (key === 'length' && (typeof value === 'string' || Array.isArray(value))) return typeof value === 'string' ? [...value].length : value.length;
141
+ if (Array.isArray(value)) return Number.isInteger(key) && key >= 0 ? value[key] ?? null : null;
142
+ return value && typeof value === 'object' && own(value, key) ? value[key] : null;
143
+ }
144
+ binary(node, env) {
145
+ const a = this.expr(node.left, env);
146
+ if (node.op === '&&') return truth(a) ? this.expr(node.right, env) : a;
147
+ if (node.op === '||') return truth(a) ? a : this.expr(node.right, env);
148
+ if (node.op === '??') return a === null ? this.expr(node.right, env) : a;
149
+ const b = this.expr(node.right, env);
150
+ if (['==', '==='].includes(node.op)) return same(a, b);
151
+ if (['!=', '!=='].includes(node.op)) return !same(a, b);
152
+ if (typeof a !== typeof b || !['number', 'string'].includes(typeof a)) this.error('TYPE', 'Operands must have matching scalar types', node);
153
+ if (node.op === '<') return a < b; if (node.op === '>') return a > b;
154
+ if (node.op === '<=') return a <= b; if (node.op === '>=') return a >= b;
155
+ if (node.op === '+') return this.checkValue(a + b);
156
+ if (typeof a !== 'number') this.error('TYPE', 'Arithmetic requires numbers', node);
157
+ if ((node.op === '/' || node.op === '%') && b === 0) this.error('ARITHMETIC', 'Division by zero', node);
158
+ return this.checkValue(({ '-': () => a - b, '*': () => a * b, '/': () => a / b, '%': () => a % b })[node.op]?.());
159
+ }
160
+ call(node, env) {
161
+ const target = node.target;
162
+ if (target.type === 'identifier') {
163
+ const moduleId = this.find(env, '$module')?.get('$module');
164
+ const module = this.modules[moduleId]?.ast || this.modules[moduleId];
165
+ const fn = module?.functions?.find(value => value.name === target.name);
166
+ if (!fn) this.error('FUNCTION', 'Unknown function', node);
167
+ return this.callFunction(fn, node.args.map(arg => this.expr(arg, env)), env.slice(0, 1), moduleId);
168
+ }
169
+ if (target.type !== 'member' || target.key.type !== 'literal') this.error('FUNCTION', 'Only named calls allowed', node);
170
+ const name = target.key.value;
171
+ if (forbidden.has(name)) this.error('FORBIDDEN', 'Forbidden call', node);
172
+ const namespace = target.target.type === 'identifier' ? target.target.name : null;
173
+ if (own(this.modules, namespace)) return this.callModule(namespace, name, node.args.map(arg => this.expr(arg, env)), env.slice(0, 1));
174
+ if (['html', 'date', 'string', 'object', 'array', 'number'].includes(namespace)) {
175
+ return this.checkValue(this.builtin(namespace, name, node.args.map(arg => this.expr(arg, env))));
176
+ }
177
+ const receiver = this.expr(target.target, env);
178
+ if (Array.isArray(receiver) && ['map', 'filter', 'reduce'].includes(name)) {
179
+ const lambda = node.args[0]; if (lambda?.type !== 'lambda') this.error('FUNCTION', 'Collection operation requires local lambda', node);
180
+ const moduleId = this.find(env, '$module')?.get('$module');
181
+ if (name === 'reduce') {
182
+ if (node.args.length !== 2) this.error('TYPE', 'reduce requires initial value', node);
183
+ let value = this.expr(node.args[1], env);
184
+ for (let i = 0; i < receiver.length; i++) { this.tick(node); value = this.callFunction(lambda, [value, receiver[i], i], env, moduleId); }
185
+ return this.checkValue(value);
186
+ }
187
+ const result = [];
188
+ for (let i = 0; i < receiver.length; i++) {
189
+ this.tick(node); const value = this.callFunction(lambda, [receiver[i], i], env, moduleId);
190
+ if (name === 'map') result.push(value); else if (truth(value)) result.push(receiver[i]);
191
+ }
192
+ return this.checkValue(result);
193
+ }
194
+ return this.checkValue(this.builtin(Array.isArray(receiver) ? 'array' : typeof receiver, name, [receiver, ...node.args.map(arg => this.expr(arg, env))]));
195
+ }
196
+ builtin(namespace, name, args) {
197
+ const [value, a, b] = args;
198
+ if (namespace === 'html' && typeof value === 'string') {
199
+ const decode = text => text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi, (whole, code) => {
200
+ const named = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'", nbsp: ' ' };
201
+ if (own(named, code.toLowerCase())) return named[code.toLowerCase()];
202
+ const n = code[1].toLowerCase() === 'x' ? parseInt(code.slice(2), 16) : parseInt(code.slice(1), 10);
203
+ return n > 0 && n <= 0x10ffff && !(n >= 0xd800 && n <= 0xdfff) ? String.fromCodePoint(n) : '\ufffd';
204
+ });
205
+ if (name === 'decodeEntities') return decode(value);
206
+ const text = decode(value.replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, '').replace(/<br\s*\/?\s*>|<\/(?:p|div|li)>/gi, '\n').replace(/<[^>]*>/g, '')).trim();
207
+ if (name === 'toText') return text; if (name === 'lines') return text.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
208
+ }
209
+ if (namespace === 'string') {
210
+ if (name === 'from') return value === null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
211
+ if (typeof value !== 'string') this.error('TYPE', 'String operation requires string');
212
+ if (name === 'trim') return value.trim(); if (name === 'toLowerCase') return value.toLowerCase(); if (name === 'toUpperCase') return value.toUpperCase();
213
+ if (name === 'includes' && typeof a === 'string') return value.includes(a);
214
+ if (name === 'split' && typeof a === 'string') { if (a === '') return [...value]; return value.split(a); }
215
+ if (name === 'replaceAll' && typeof a === 'string' && typeof b === 'string' && a !== '') {
216
+ const count = value.split(a).length - 1;
217
+ if (size(value) + count * size(b) > 32768) this.error('MEMORY_LIMIT', 'Replacement exceeds string budget');
218
+ return value.split(a).join(b);
219
+ }
220
+ if (name === 'slice' && Number.isInteger(a) && (b === undefined || Number.isInteger(b))) return [...value].slice(a, b).join('');
221
+ }
222
+ if (namespace === 'array' && Array.isArray(value)) {
223
+ if (name === 'join' && (a === undefined || typeof a === 'string')) {
224
+ if (value.some(v => !['string', 'number', 'boolean'].includes(typeof v) && v !== null)) this.error('TYPE', 'join requires scalar values');
225
+ const separator = a ?? ',';
226
+ if (value.reduce((n, v) => n + size(String(v ?? '')), 0) + Math.max(0, value.length - 1) * size(separator) > 32768) this.error('MEMORY_LIMIT', 'join exceeds string budget');
227
+ return value.join(separator);
228
+ }
229
+ if (name === 'slice' && Number.isInteger(a) && (b === undefined || Number.isInteger(b))) return value.slice(a, b);
230
+ if (name === 'includes') return value.some(v => same(v, a));
231
+ if (name === 'concat' && Array.isArray(a) && value.length + a.length <= 1000) return [...value, ...a];
232
+ }
233
+ if (namespace === 'object' && value && typeof value === 'object' && !Array.isArray(value)) {
234
+ if (name === 'keys') return Object.keys(value).sort(); if (name === 'values') return Object.keys(value).sort().map(k => value[k]);
235
+ if (name === 'has' && typeof a === 'string' && !forbidden.has(a)) return own(value, a);
236
+ }
237
+ if (namespace === 'number') {
238
+ if (name === 'from' && typeof value === 'string' && value.trim() !== '') return Number(value);
239
+ if (typeof value === 'number') {
240
+ if (name === 'round') return Math.floor(value + 0.5); if (name === 'floor') return Math.floor(value); if (name === 'ceil') return Math.ceil(value); if (name === 'abs') return Math.abs(value);
241
+ if (name === 'min' && typeof a === 'number') return Math.min(value, a); if (name === 'max' && typeof a === 'number') return Math.max(value, a);
242
+ }
243
+ }
244
+ if (namespace === 'date' && typeof value === 'string' && /^\d{4}-\d\d-\d\d(?:T.*(?:Z|[+-]\d\d:\d\d))?$/.test(value)) {
245
+ const date = new Date(value); const calendar = new Date(value.slice(0, 10) + 'T00:00:00Z'); if (!Number.isFinite(date.valueOf()) || !Number.isFinite(calendar.valueOf()) || calendar.toISOString().slice(0, 10) !== value.slice(0, 10)) this.error('DATE', 'Invalid ISO date');
246
+ if (name === 'iso') return date.toISOString();
247
+ if (name === 'addDays' && Number.isInteger(a)) { date.setUTCDate(date.getUTCDate() + a); return date.toISOString().slice(0, 10); }
248
+ if (name === 'weekday') return date.getUTCDay();
249
+ }
250
+ this.error('FUNCTION', `Unsupported operation ${namespace}.${name}`);
251
+ }
252
+ }
@@ -0,0 +1,32 @@
1
+ const fail = message => { const error = new Error(`Morit Script: ${message}`); error.code = "SCHEMA"; throw error; };
2
+ export function validateValueSchema(schema, depth = 0) {
3
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema) || depth > 20) fail('invalid value schema');
4
+ const allowed = new Set(['type','properties','required','items','additionalProperties','enum','minimum','maximum','minLength','maxLength','minItems','maxItems','description']);
5
+ if (Object.keys(schema).some(key => !allowed.has(key))) fail('unsupported value schema keyword');
6
+ if (!['null','boolean','number','integer','string','array','object'].includes(schema.type)) fail('value schema requires concrete type');
7
+ if (schema.properties) for (const child of Object.values(schema.properties)) validateValueSchema(child,depth+1);
8
+ if (schema.items) validateValueSchema(schema.items,depth+1);
9
+ if (schema.type === 'array' && !schema.items) fail('array schema requires items');
10
+ if (schema.required && (!Array.isArray(schema.required) || schema.required.some(key => !Object.hasOwn(schema.properties ?? {},key)))) fail('required keys must be declared');
11
+ if (schema.additionalProperties !== undefined && typeof schema.additionalProperties !== 'boolean') fail('additionalProperties must be boolean');
12
+ for (const key of ['minimum','maximum','minLength','maxLength','minItems','maxItems']) if (schema[key] !== undefined && (typeof schema[key] !== 'number' || !Number.isFinite(schema[key]))) fail('invalid schema bound');
13
+ }
14
+
15
+ export function assertSchema(value, schema, path = '$') {
16
+ const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value;
17
+ if (schema.type === 'integer' ? !Number.isInteger(value) : schema.type !== type) fail(`${path} must be ${schema.type}`);
18
+ if (schema.enum && !schema.enum.some(item => JSON.stringify(item) === JSON.stringify(value))) fail(`${path} is not an allowed value`);
19
+ if (typeof value === 'number' && ((schema.minimum !== undefined && value < schema.minimum) || (schema.maximum !== undefined && value > schema.maximum))) fail(`${path} is outside numeric bounds`);
20
+ if (typeof value === 'string' && ((schema.minLength !== undefined && [...value].length < schema.minLength) || (schema.maxLength !== undefined && [...value].length > schema.maxLength))) fail(`${path} is outside string bounds`);
21
+ if (Array.isArray(value)) {
22
+ if ((schema.minItems !== undefined && value.length < schema.minItems) || (schema.maxItems !== undefined && value.length > schema.maxItems)) fail(`${path} is outside array bounds`);
23
+ value.forEach((item,index) => assertSchema(item,schema.items,`${path}[${index}]`));
24
+ } else if (value && typeof value === 'object') {
25
+ for (const key of schema.required ?? []) if (!Object.hasOwn(value,key)) fail(`${path}.${key} is required`);
26
+ for (const [key,item] of Object.entries(value)) {
27
+ if (schema.properties?.[key]) assertSchema(item,schema.properties[key],`${path}.${key}`);
28
+ else if (schema.additionalProperties === false) fail(`${path}.${key} is undeclared`);
29
+ }
30
+ }
31
+ return value;
32
+ }
@@ -0,0 +1,113 @@
1
+ // Generated by sync-script-contract.mjs; edit the backend canonical contract.
2
+ export const SCRIPT_CONTRACT = Object.freeze({
3
+ "version": 1,
4
+ "limits": {
5
+ "modules": 16,
6
+ "module_bytes": 32768,
7
+ "source_bytes": 131072,
8
+ "ast_nodes": 2048,
9
+ "inline_bytes": 256,
10
+ "inline_nodes": 64,
11
+ "call_depth": 24,
12
+ "array_items": 1000,
13
+ "object_keys": 512,
14
+ "value_depth": 20,
15
+ "string_bytes": 32768,
16
+ "output_bytes": 262144
17
+ },
18
+ "profiles": {
19
+ "inline": {
20
+ "steps": 10000,
21
+ "memory": 2097152,
22
+ "timeout": 16
23
+ },
24
+ "ui": {
25
+ "steps": 50000,
26
+ "memory": 8388608,
27
+ "timeout": 100
28
+ },
29
+ "server": {
30
+ "steps": 100000,
31
+ "memory": 16777216,
32
+ "timeout": 250
33
+ }
34
+ },
35
+ "nodes": {
36
+ "module": [
37
+ "version",
38
+ "functions"
39
+ ],
40
+ "function": [
41
+ "name",
42
+ "params",
43
+ "body",
44
+ "exported"
45
+ ],
46
+ "literal": [
47
+ "value"
48
+ ],
49
+ "ref": [
50
+ "path"
51
+ ],
52
+ "identifier": [
53
+ "name"
54
+ ],
55
+ "array": [
56
+ "items"
57
+ ],
58
+ "object": [
59
+ "entries"
60
+ ],
61
+ "member": [
62
+ "target",
63
+ "key"
64
+ ],
65
+ "binary": [
66
+ "op",
67
+ "left",
68
+ "right"
69
+ ],
70
+ "unary": [
71
+ "op",
72
+ "value"
73
+ ],
74
+ "conditional": [
75
+ "condition",
76
+ "then",
77
+ "else"
78
+ ],
79
+ "call": [
80
+ "target",
81
+ "args"
82
+ ],
83
+ "lambda": [
84
+ "params",
85
+ "body"
86
+ ],
87
+ "declare": [
88
+ "name",
89
+ "mutable",
90
+ "value"
91
+ ],
92
+ "assign": [
93
+ "name",
94
+ "value"
95
+ ],
96
+ "return": [
97
+ "value"
98
+ ],
99
+ "expression": [
100
+ "value"
101
+ ],
102
+ "if": [
103
+ "condition",
104
+ "then",
105
+ "else"
106
+ ],
107
+ "for": [
108
+ "name",
109
+ "items",
110
+ "body"
111
+ ]
112
+ }
113
+ });
package/src/ui-v3.js ADDED
@@ -0,0 +1,159 @@
1
+ import {compileExpression} from './morit-script.js';
2
+ import { canonicalJson } from './archive.js';
3
+ import {compileScriptProject} from './script-project.js';
4
+ import {dependencyOrder} from './script-data.js';
5
+ import {validateValueSchema} from './script-schema.js';
6
+
7
+ const fail = message => {throw new Error(`UI Runtime v3: ${message}`);};
8
+ const identifier = value => typeof value === 'string' && /^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(value);
9
+ export function uiV3AllowedProps(type, runtimeContract = {}, baseRuntimeContract = {}) {
10
+ return new Set([
11
+ ...(baseRuntimeContract.component_props?.[type] ?? []),
12
+ ...(runtimeContract.node_props?.[type] ?? []),
13
+ ...(baseRuntimeContract.common_props_excluded_components?.includes(type) ? [] : baseRuntimeContract.common_props ?? []),
14
+ ...(runtimeContract.common_props ?? []),
15
+ ...(runtimeContract.responsive_props ?? []),
16
+ ]);
17
+ }
18
+
19
+ export function requiresMorit19(config, runtimeContract) {
20
+ if (config?.ui_schema !== 3) return false;
21
+ const commonProps = new Set(runtimeContract.common_props);
22
+ if ((config.data_sources ?? []).some(source => source.retry)) return true;
23
+ if (Object.values(config.components ?? {}).some(component => component.default_props || component.slots || component.events)) return true;
24
+ let required = false;
25
+ const visit = value => {
26
+ if (required || value == null) return;
27
+ if (typeof value === 'string') { if (value.includes('runtime.screen')) required = true; return; }
28
+ if (Array.isArray(value)) { value.forEach(visit); return; }
29
+ if (typeof value !== 'object') return;
30
+ if (value.type === 'slot' || value.action?.type === 'emit') { required = true; return; }
31
+ const props = value.props ?? {};
32
+ if (Object.keys(props).some(key => commonProps.has(key)) || props.events || props.curve || props.main_axis_alignment || props.cross_axis_alignment ||
33
+ (['date_field','time_field'].includes(value.type) && Object.keys(props).some(key => !['state_key','label'].includes(key))) ||
34
+ (value.type === 'skeleton' && Object.keys(props).length)) { required = true; return; }
35
+ Object.values(value).forEach(visit);
36
+ };
37
+ visit(config.view); visit(config.bottom_bar); visit(config.app_bar); visit(config.navigation);
38
+ return required;
39
+ }
40
+ export function compileUiV3(config, files, {capabilities = new Set(), routes = new Set(), nodeTypes = [], runtimeContract = {}, baseRuntimeContract = {}} = {}) {
41
+ const allowed = new Set(runtimeContract.config_fields ?? ['ui_schema','icon','description','placement','app_bar','navigation','theme','initial_state','computed_state','logic_modules','components','data_sources','view','bottom_bar','a2ui','compiled']);
42
+ if (Object.keys(config).some(key => !allowed.has(key))) fail('unknown configuration field');
43
+ const source = {...config}; delete source.compiled;
44
+ const compiled = compileScriptProject(JSON.parse(canonicalJson(source).toString()), files);
45
+ const state = config.initial_state ?? {}, computed = config.computed_state ?? {}, components = config.components ?? {};
46
+ if (Object.keys(state).length > 32 || Object.keys(computed).length > 32 || Object.keys(components).length > 32) fail('too many state keys or components');
47
+ for (const key of [...Object.keys(state), ...Object.keys(computed)]) if (!identifier(key)) fail('invalid state name');
48
+ for (const [name,definition] of Object.entries(components)) {
49
+ if (!identifier(name) || !definition || typeof definition !== 'object' || Array.isArray(definition)) fail('invalid component');
50
+ if (Object.keys(definition).some(k=>!(runtimeContract.component_fields??['props_schema','default_props','slots','events','initial_state','computed_state','view']).includes(k))) fail('unknown component field');
51
+ for (const slot of Object.values(definition.slots??{})) if(!slot||typeof slot!=='object'||Array.isArray(slot)||Object.keys(slot).some(k=>!(runtimeContract.component_slot_fields??['required']).includes(k))||('required' in slot&&typeof slot.required!=='boolean'))fail('invalid component slot');
52
+ if(definition.props_schema)validateValueSchema(definition.props_schema);
53
+ for (const event of Object.values(definition.events??{})) {if(!event||typeof event!=='object'||Array.isArray(event)||Object.keys(event).some(k=>!(runtimeContract.component_event_fields??['payload_schema']).includes(k))||!event.payload_schema)fail('invalid component event');validateValueSchema(event.payload_schema);}
54
+ }
55
+ function order(values) {
56
+ function dependencies(value) {
57
+ const result = new Set();
58
+ function ast(n) {
59
+ if (!n || typeof n !== 'object') return;
60
+ if (n.type === 'member' && n.target?.type === 'identifier' && n.target.name === 'computed') {
61
+ if (n.key?.type !== 'literal' || typeof n.key.value !== 'string') fail('computed dependency must use a static key');
62
+ result.add(n.key.value);
63
+ }
64
+ for (const child of Object.values(n)) ast(child);
65
+ }
66
+ function visit(v) {
67
+ if (typeof v === 'string') for (const m of v.matchAll(/\{\{=([\s\S]*?)\}\}/g)) ast(compileExpression(m[1].trim()));
68
+ else if (v && typeof v === 'object') for (const child of Object.values(v)) visit(child);
69
+ }
70
+ visit(value); return [...result];
71
+ }
72
+ const graph = Object.fromEntries(Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).map(([key,value]) => [key,dependencies(value)]));
73
+ return dependencyOrder(graph);
74
+ }
75
+ const sources = config.data_sources ?? [];
76
+ if (!Array.isArray(sources) || sources.length > 8) fail('at most eight data sources allowed');
77
+ const ids = new Set(sources.map(s => s.id));
78
+ if (ids.size !== sources.length) fail('duplicate source id');
79
+ const graph = {};
80
+ for (const s of sources) {
81
+ if (!identifier(s.id) || !capabilities.has(s.capability)) fail('invalid source or capability');
82
+ if (Object.keys(s).some(k => !(runtimeContract.data_source_fields ?? ['id','capability','trigger','query','arguments','refresh_seconds','depends_on','when','result_mode','transform','refresh','cache']).includes(k))) fail('unknown source field');
83
+ if (s.trigger && !(runtimeContract.data_source_triggers ?? ['load','manual']).includes(s.trigger)) fail('invalid trigger');
84
+ if (s.result_mode && !(runtimeContract.result_modes ?? ['raw','envelope']).includes(s.result_mode)) fail('invalid result mode');
85
+ if (s.cache && (!(runtimeContract.cache_policies ?? ['none','memory','stale_while_revalidate']).includes(s.cache.policy) || !Number.isInteger(s.cache.ttl_seconds ?? 60) || (s.cache.ttl_seconds ?? 60) < 0 || (s.cache.ttl_seconds ?? 60) > 86400)) fail('invalid cache policy');
86
+ if (s.retry && (!Number.isInteger(s.retry.max_attempts??0)||(s.retry.max_attempts??0)<0||(s.retry.max_attempts??0)>2||!Number.isInteger(s.retry.delay_ms??200)||(s.retry.delay_ms??200)<200||(s.retry.delay_ms??200)>5000||(s.retry.on??[]).some(v=>!(runtimeContract.retry_events??['timeout','network','server']).includes(v)))) fail('invalid retry policy');
87
+ const interval = s.refresh?.interval_seconds ?? s.refresh_seconds;
88
+ if (interval !== undefined && (!Number.isInteger(interval) || interval < 30 || interval > 86400)) fail('invalid refresh interval');
89
+ if (s.refresh?.on?.some(v => !(runtimeContract.refresh_events ?? ['load','dependency_change','resume','manual']).includes(v))) fail('invalid refresh trigger');
90
+ const deps = s.depends_on ?? [];
91
+ if (!Array.isArray(deps) || deps.some(d => !ids.has(d) && !Object.hasOwn(state,d) && !Object.hasOwn(computed,d))) fail('unknown dependency');
92
+ graph[s.id] = deps.filter(d => ids.has(d));
93
+ const transforms=Array.isArray(s.transform) ? s.transform : s.transform ? [s.transform] : [];
94
+ if(transforms.length>(runtimeContract.max_transform_steps??8))fail('too many transforms');
95
+ for (const transform of transforms) {
96
+ if (!compiled.modules[transform.module]?.exports?.[transform.export]) fail('unknown transform export');
97
+ }
98
+ }
99
+ const validTypes = new Set([...nodeTypes,...(runtimeContract.nodes ?? ['component','repeat','if','data_state','date_field','time_field','skeleton','animated','flex'])]);
100
+ function action(a, stateKeys = new Set(Object.keys(state)), componentEvents = new Set()) {
101
+ if (!a || typeof a !== 'object' || Array.isArray(a)) fail('invalid action');
102
+ if (a.type === 'emit') {
103
+ if(!componentEvents.has(a.event)||a.payload!==undefined&&(!a.payload||typeof a.payload!=='object'||Array.isArray(a.payload)))fail('invalid component event');
104
+ } else if (a.type === 'flow') {
105
+ if (!Array.isArray(a.actions) || !a.actions.length || a.actions.length > 8) fail('invalid action flow');
106
+ for (const child of a.actions) action(child, stateKeys, componentEvents);
107
+ } else if (a.type === 'invoke' && !capabilities.has(a.capability)) fail('unknown action capability');
108
+ else if (a.type === 'navigate' && !routes.has(a.target)) fail('unknown action route');
109
+ else if (a.type === 'refresh' && !ids.has(a.source)) fail('unknown action source');
110
+ else if (a.type === 'set_state' && (!a.values || Object.keys(a.values).some(key => !(a.scope === 'root' ? Object.hasOwn(state,key) : stateKeys.has(key))))) fail('unknown action state');
111
+ else if (!['invoke','navigate','refresh','set_state','back','flow'].includes(a.type)) fail('unknown action type');
112
+ }
113
+ let count = 0;
114
+ function node(n, depth = 0, componentStack = [], scroll = false, stateKeys = new Set(Object.keys(state)), componentEvents = new Set(), componentSlots = new Set()) {
115
+ if (!n || typeof n !== 'object' || Array.isArray(n) || ++count > 320 || depth > 16) fail('invalid or excessive node tree');
116
+ if (!validTypes.has(n.type)) fail(`unknown node ${n.type}`);
117
+ if (Object.keys(n).some(k => !(runtimeContract.node_fields ?? ['id','type','props','children','action','visible_when','slots']).includes(k))) fail('unknown node field');
118
+ const p=n.props??{};
119
+ if (!p || typeof p!=='object' || Array.isArray(p)) fail('invalid props');
120
+ const allowedProps=uiV3AllowedProps(n.type,runtimeContract,baseRuntimeContract);
121
+ if(allowedProps.size&&Object.keys(p).some(k=>!allowedProps.has(k)))fail(`unknown ${n.type} prop`);
122
+ for (const k of ['width','height','min_width','max_width','min_height','max_height','gap','spacing','padding','margin','basis']) {
123
+ const v=p[k]; for (const x of Array.isArray(v)?v:v && typeof v==='object'?Object.values(v):[v]) if (typeof x==='number' && (!Number.isFinite(x)||x<0||x>4096)) fail(`invalid ${k}`);
124
+ }
125
+ for(const axis of ['width','height']) if(typeof p[`min_${axis}`]==='number'&&typeof p[`max_${axis}`]==='number'&&p[`min_${axis}`]>p[`max_${axis}`]) fail(`conflicting ${axis} bounds`);
126
+ if(n.type==='scroll'&&scroll) fail('nested scroll is not allowed');
127
+ if(n.type==='repeat'&&(!identifier(p.as)||!identifier(p.index_as??'index')||p.items===undefined||p.key===undefined)) fail('repeat requires items, alias and stable key');
128
+ if(n.type==='data_state'&&!ids.has(p.source)) fail('unknown data_state source');
129
+ if(['date_field','time_field'].includes(n.type)){
130
+ const spec=runtimeContract.picker_specs?.[n.type];
131
+ if(!spec)fail('missing Material picker contract');
132
+ const pattern=new RegExp(spec.value_pattern);
133
+ const modes=spec.entry_modes;
134
+ if(!identifier(p.state_key)||!stateKeys.has(p.state_key)||(p.label!==undefined&&typeof p.label!=='string')||p.minimum!==undefined&&!pattern.test(p.minimum)||p.maximum!==undefined&&!pattern.test(p.maximum)||p.minimum&&p.maximum&&p.minimum>p.maximum||p.initial_entry_mode!==undefined&&!modes.includes(p.initial_entry_mode))fail('invalid Material picker');
135
+ }
136
+ if(n.type==='component') {
137
+ const def=components[p.component]; if(!def||componentStack.includes(p.component)) fail('unknown or recursive component');
138
+ const declaredSlots=def.slots??{}, supplied=n.slots??{};
139
+ if(Object.keys(supplied).some(name=>!Object.hasOwn(declaredSlots,name))||Object.entries(declaredSlots).some(([name,spec])=>spec.required&&!Object.hasOwn(supplied,name)))fail('invalid component slots');
140
+ const handlers=p.events??{};if(!handlers||typeof handlers!=='object'||Array.isArray(handlers)||Object.keys(handlers).some(name=>!Object.hasOwn(def.events??{},name)))fail('invalid component handlers');
141
+ for(const handler of Object.values(handlers))action(handler,stateKeys);
142
+ node(def.view,depth+1,[...componentStack,p.component],scroll,new Set(Object.keys(def.initial_state??{})),new Set(Object.keys(def.events??{})),new Set(Object.keys(def.slots??{})));
143
+ }
144
+ if(n.type==='slot'&&(!identifier(p.name)||!componentSlots.has(p.name)))fail('invalid component slot');
145
+ if(n.type==='animated' && ((p.duration_ms??200)>1000||!(runtimeContract.animation_transitions??[]).includes(p.transition??'fade')||!(runtimeContract.animation_curves??[]).includes(p.curve??'standard'))) fail('invalid animation');
146
+ if(n.type==='skeleton'&&!(runtimeContract.skeleton_variants??[]).includes(p.variant??'line'))fail('invalid skeleton');
147
+ if(n.action)action(n.action,stateKeys,componentEvents);
148
+ if((n.children??[]).length>32) fail('too many static children');
149
+ for(const c of n.children??[]) node(c,depth+1,componentStack,scroll||n.type==='scroll',stateKeys,componentEvents,componentSlots);
150
+ for(const slot of Object.values(n.slots??{})) for(const child of Array.isArray(slot)?slot:[slot]) node(child,depth+1,componentStack,scroll||n.type==='scroll',stateKeys,componentEvents,componentSlots);
151
+ }
152
+ node(config.view); if(config.bottom_bar)node(config.bottom_bar);
153
+ for(const item of [config.app_bar?.leading,...(config.app_bar?.actions??[]),...(config.navigation?.items??[])])if(item?.action)action(item.action);
154
+ for(const component of Object.values(components))order(component.computed_state??{});
155
+ const bundle={version:1, modules:compiled.modules, expressions:compiled.expressions, computed_order:order(computed), source_order:dependencyOrder(graph)};
156
+ if(config.compiled&&!canonicalJson(config.compiled).equals(canonicalJson(bundle)))fail('compiled bundle differs from source; rebuild the package');
157
+ config.compiled=bundle;
158
+ return compiled;
159
+ }