@almadar/runtime 6.32.0 → 6.33.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.
@@ -1,10 +1,12 @@
1
1
  import { parseCron, cronMinuteKey, cronMatches } from './chunk-OU3ITB5S.js';
2
+ import { seedRandom, randomArrayElement, randomInt, shuffleArray, randomPastDate, randomBoolean, randomRecentDate, randomUuid, randomPhone, randomUrl, randomEmail, randomWords } from './chunk-OQJIK6PZ.js';
3
+ import { collectTraitRefsFromValue, collectTraitRefsFromEffects } from './chunk-SCRAHWOC.js';
2
4
  import { createLogger, setNamespaceLevel } from '@almadar/logger';
3
- import { resolveBinding, evaluate, createMinimalContext, evaluateGuard, SExpressionEvaluator } from '@almadar/evaluator';
5
+ import { createMinimalContext, resolveBinding, evaluate, evaluateGuard, SExpressionEvaluator } from '@almadar/evaluator';
4
6
  export { createMinimalContext } from '@almadar/evaluator';
5
7
  import { isKnownStdOperator } from '@almadar/std/registry';
6
- import { isCallSiteConfigDeclaration, OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef } from '@almadar/core';
7
- import { faker } from '@faker-js/faker';
8
+ import { OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef, configRefEventKnob, normalizeCallSiteConfigToValues, resolveConfigRefEventName } from '@almadar/core';
9
+ export { normalizeCallSiteConfigToValues } from '@almadar/core';
8
10
 
9
11
  var log = createLogger("almadar:runtime:eventbus");
10
12
  var EventBus = class {
@@ -289,6 +291,33 @@ var bindLog = createLogger("almadar:runtime:bindings");
289
291
  setNamespaceLevel("almadar:runtime:bindings", "WARN");
290
292
  var renderLog = createLogger("almadar:runtime:render-ui");
291
293
  var CLIENT_ONLY_BINDING_ROOTS = /* @__PURE__ */ new Set(["trait"]);
294
+ var CALLSITE_PAYLOAD_PREFIX = "@callsitePayload.";
295
+ function payloadValueToConfigValue(v) {
296
+ if (v === null || v === void 0) return null;
297
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
298
+ if (v instanceof Date) return v.toISOString();
299
+ if (Array.isArray(v)) return v.map(payloadValueToConfigValue);
300
+ if (typeof v === "object") {
301
+ const obj = {};
302
+ for (const [k, val] of Object.entries(v)) obj[k] = payloadValueToConfigValue(val);
303
+ return obj;
304
+ }
305
+ return String(v);
306
+ }
307
+ function resolveCallSitePayloadCaptures(config, payload) {
308
+ let ctx;
309
+ const out = {};
310
+ for (const [key, value] of Object.entries(config)) {
311
+ if (typeof value === "string" && value.startsWith(CALLSITE_PAYLOAD_PREFIX)) {
312
+ const field = value.slice(CALLSITE_PAYLOAD_PREFIX.length);
313
+ if (!ctx) ctx = createMinimalContext({}, payload ?? {}, "idle");
314
+ out[key] = payloadValueToConfigValue(resolveBinding(`@payload.${field}`, ctx));
315
+ } else {
316
+ out[key] = value;
317
+ }
318
+ }
319
+ return out;
320
+ }
292
321
  function isClientOnlyBinding(value) {
293
322
  if (!value.startsWith("@")) return false;
294
323
  const afterAt = value.slice(1);
@@ -982,7 +1011,7 @@ var StateMachineManager = class {
982
1011
 
983
1012
  // src/types.ts
984
1013
  var HANDLER_MANIFEST = {
985
- client: ["render-ui", "render", "navigate", "notify", "emit", "set", "log", "ref", "deref", "watch", "send-server"],
1014
+ client: ["render-ui", "render", "navigate", "notify", "emit", "set", "log", "ref", "deref", "watch", "send-server", "browser/open-file-picker", "browser/clipboard-read", "browser/clipboard-write", "browser/geolocation-current"],
986
1015
  server: ["persist", "fetch", "fetch-stream", "call-service", "emit", "set", "spawn", "despawn", "log", "ref", "deref", "swap!", "atomic", "os/watch-files", "os/watch-process", "os/watch-port", "os/watch-http", "os/watch-cron", "os/watch-signal", "os/watch-env", "os/debounce"],
987
1016
  test: [
988
1017
  "render-ui",
@@ -1002,7 +1031,11 @@ var HANDLER_MANIFEST = {
1002
1031
  "swap!",
1003
1032
  "watch",
1004
1033
  "atomic",
1005
- "send-server"
1034
+ "send-server",
1035
+ "browser/open-file-picker",
1036
+ "browser/clipboard-read",
1037
+ "browser/clipboard-write",
1038
+ "browser/geolocation-current"
1006
1039
  ],
1007
1040
  ssr: ["render-ui", "render", "fetch", "emit", "set", "log", "ref", "deref"]
1008
1041
  };
@@ -1938,6 +1971,58 @@ var EffectExecutor = class _EffectExecutor {
1938
1971
  }, emitCfg);
1939
1972
  break;
1940
1973
  }
1974
+ // === Browser device operators (client-side, user-gesture) ===
1975
+ // Async host APIs routed to dedicated handler methods. Uniform
1976
+ // `{ result }` / `{ error }` payload via runSubstrate. On the
1977
+ // server (or any host without the API) the handler is absent →
1978
+ // unsupported warning; a thrown error fires `emit.failure`.
1979
+ case "browser/open-file-picker": {
1980
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
1981
+ const options = positional[0];
1982
+ await this.runSubstrate(async () => {
1983
+ if (!this.handlers.browserOpenFilePicker) {
1984
+ this.logUnsupported("browser/open-file-picker");
1985
+ return null;
1986
+ }
1987
+ return this.handlers.browserOpenFilePicker(options);
1988
+ }, emitCfg);
1989
+ break;
1990
+ }
1991
+ case "browser/clipboard-read": {
1992
+ const [, emitCfg] = this.splitSubstrateEmit(args);
1993
+ await this.runSubstrate(async () => {
1994
+ if (!this.handlers.browserClipboardRead) {
1995
+ this.logUnsupported("browser/clipboard-read");
1996
+ return null;
1997
+ }
1998
+ return this.handlers.browserClipboardRead();
1999
+ }, emitCfg);
2000
+ break;
2001
+ }
2002
+ case "browser/clipboard-write": {
2003
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
2004
+ const text = positional[0];
2005
+ await this.runSubstrate(async () => {
2006
+ if (!this.handlers.browserClipboardWrite) {
2007
+ this.logUnsupported("browser/clipboard-write");
2008
+ return null;
2009
+ }
2010
+ return this.handlers.browserClipboardWrite(text);
2011
+ }, emitCfg);
2012
+ break;
2013
+ }
2014
+ case "browser/geolocation-current": {
2015
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
2016
+ const options = positional[0];
2017
+ await this.runSubstrate(async () => {
2018
+ if (!this.handlers.browserGeolocationCurrent) {
2019
+ this.logUnsupported("browser/geolocation-current");
2020
+ return null;
2021
+ }
2022
+ return this.handlers.browserGeolocationCurrent(options);
2023
+ }, emitCfg);
2024
+ break;
2025
+ }
1941
2026
  default: {
1942
2027
  if (operator.includes("/")) {
1943
2028
  const [positional, emitCfg] = this.splitSubstrateEmit(args);
@@ -2104,21 +2189,6 @@ function collectDeclaredConfigDefaults(trait) {
2104
2189
  }
2105
2190
  return hasAny ? defaults : void 0;
2106
2191
  }
2107
- function normalizeCallSiteConfigToValues(config) {
2108
- if (config === void 0) {
2109
- return void 0;
2110
- }
2111
- const out = {};
2112
- let hasAny = false;
2113
- for (const [key, entry] of Object.entries(config)) {
2114
- const value = isCallSiteConfigDeclaration(entry) ? entry.default : entry;
2115
- if (value !== void 0) {
2116
- out[key] = value;
2117
- hasAny = true;
2118
- }
2119
- }
2120
- return hasAny ? out : void 0;
2121
- }
2122
2192
  function collectDeclaredEntityDefaults(entity) {
2123
2193
  if (!entity) return void 0;
2124
2194
  const defaults = {};
@@ -2134,7 +2204,7 @@ function collectDeclaredEntityDefaults(entity) {
2134
2204
  var mockLog = createLogger("almadar:runtime:mock");
2135
2205
  var DEFAULT_MOCK_SEED = 42;
2136
2206
  function picsumUrl(entityName, fieldName, width = 400, height = 400) {
2137
- const seed = `${entityName}-${fieldName}-${faker.number.int({ min: 0, max: 1e3 })}`;
2207
+ const seed = `${entityName}-${fieldName}-${randomInt({ min: 0, max: 1e3 })}`;
2138
2208
  return `https://picsum.photos/seed/${encodeURIComponent(seed)}/${width}/${height}`;
2139
2209
  }
2140
2210
  var SEED_REFERENCE_TIMESTAMP = "2024-01-01T00:00:00.000Z";
@@ -2152,17 +2222,17 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2152
2222
  // input doesn't overwrite the default.
2153
2223
  seed: config.seed ?? DEFAULT_MOCK_SEED
2154
2224
  };
2155
- faker.seed(this.config.seed);
2225
+ seedRandom(this.config.seed);
2156
2226
  mockLog.debug("mock:adapter:init", { seed: this.config.seed });
2157
2227
  }
2158
- /** Re-anchor faker's PRNG to the configured seed. Called before every
2228
+ /** Re-anchor the PRNG to the configured seed. Called before every
2159
2229
  * re-seed loop so identical reseed sequences produce identical rows
2160
- * (timestamps + faker-generated fields). Without this, the first
2230
+ * (timestamps + generated fields). Without this, the first
2161
2231
  * reseed produces row set A, the second produces row set B, and
2162
2232
  * diff observers see all rows as "changed" between frames. */
2163
2233
  resetFakerSeed() {
2164
2234
  if (this.config.seed !== void 0) {
2165
- faker.seed(this.config.seed);
2235
+ seedRandom(this.config.seed);
2166
2236
  }
2167
2237
  }
2168
2238
  // ============================================================================
@@ -2188,7 +2258,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2188
2258
  /**
2189
2259
  * Register an entity schema and seed mock data.
2190
2260
  * If the schema has seedData, those instances are used directly.
2191
- * Otherwise, random mock data is generated with faker.
2261
+ * Otherwise, random mock data is generated with the seeded PRNG.
2192
2262
  */
2193
2263
  registerEntity(schema, seedCount) {
2194
2264
  const normalized = schema.name.toLowerCase();
@@ -2236,10 +2306,10 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2236
2306
  if (eligible.length === 0) continue;
2237
2307
  const cardinality = field.relation.cardinality ?? "many";
2238
2308
  if (cardinality === "one" || cardinality === "many-to-one") {
2239
- row[field.name] = faker.helpers.arrayElement(eligible);
2309
+ row[field.name] = randomArrayElement(eligible);
2240
2310
  } else {
2241
- const pickCount = Math.min(eligible.length, faker.number.int({ min: 2, max: 4 }));
2242
- row[field.name] = faker.helpers.shuffle(eligible.slice()).slice(0, pickCount);
2311
+ const pickCount = Math.min(eligible.length, randomInt({ min: 2, max: 4 }));
2312
+ row[field.name] = shuffleArray(eligible.slice()).slice(0, pickCount);
2243
2313
  }
2244
2314
  }
2245
2315
  }
@@ -2291,7 +2361,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2291
2361
  const id = this.nextId(entityName);
2292
2362
  const item = {
2293
2363
  id,
2294
- createdAt: faker.date.past({ years: 1 }).toISOString(),
2364
+ createdAt: randomPastDate({ years: 1 }).toISOString(),
2295
2365
  updatedAt: SEED_REFERENCE_TIMESTAMP
2296
2366
  };
2297
2367
  for (const field of fields) {
@@ -2333,16 +2403,16 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2333
2403
  case "string":
2334
2404
  return this.generateStringValue(entityName, field, index);
2335
2405
  case "number":
2336
- return faker.number.int({ min: 0, max: 100 });
2406
+ return randomInt({ min: 0, max: 100 });
2337
2407
  case "boolean":
2338
- return faker.datatype.boolean();
2408
+ return randomBoolean();
2339
2409
  case "date":
2340
2410
  case "timestamp":
2341
2411
  case "datetime":
2342
2412
  return this.generateDateValue(field);
2343
2413
  case "enum":
2344
2414
  if (field.values && field.values.length > 0) {
2345
- return faker.helpers.arrayElement(field.values);
2415
+ return randomArrayElement(field.values);
2346
2416
  }
2347
2417
  return null;
2348
2418
  case "relation":
@@ -2367,7 +2437,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2367
2437
  generateArrayValue(entityName, field, index, depth = 0) {
2368
2438
  if (field.type !== "array" || !field.items) return [];
2369
2439
  if (depth >= _MockPersistenceAdapter.MAX_NESTED_DEPTH) return [];
2370
- const count = faker.number.int({ min: 3, max: 5 });
2440
+ const count = randomInt({ min: 3, max: 5 });
2371
2441
  const out = [];
2372
2442
  const elementName = field.name ?? "item";
2373
2443
  for (let i = 0; i < count; i++) {
@@ -2381,7 +2451,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2381
2451
  }
2382
2452
  /**
2383
2453
  * Generate a single object value with each declared property populated
2384
- * by faker. Walks `properties` and recursively delegates to
2454
+ * by the seeded PRNG. Walks `properties` and recursively delegates to
2385
2455
  * `generateFieldValue` per property so nested objects-of-arrays-of-objects
2386
2456
  * compose correctly.
2387
2457
  */
@@ -2398,29 +2468,29 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2398
2468
  /**
2399
2469
  * Generate a string value based on the field's declared schema metadata.
2400
2470
  * Reads `values` (enum) first, then `format` (email/url/phone/uuid/date/
2401
- * datetime), then falls back to faker.lorem.words. No field-name heuristics
2471
+ * datetime), then falls back to randomWords. No field-name heuristics
2402
2472
  * — the schema is the source of truth. If a caller needs a real email, they
2403
2473
  * declare `format: "email"`; if they need an enum, they declare `values: [...]`.
2404
2474
  */
2405
2475
  generateStringValue(entityName, field, _index) {
2406
2476
  const values = "values" in field ? field.values : void 0;
2407
2477
  if (values && values.length > 0) {
2408
- return faker.helpers.arrayElement(values);
2478
+ return randomArrayElement(values);
2409
2479
  }
2410
2480
  const fieldName = field.name ?? "field";
2411
2481
  switch (field.format) {
2412
2482
  case "email":
2413
- return faker.internet.email();
2483
+ return randomEmail();
2414
2484
  case "url":
2415
- return faker.internet.url();
2485
+ return randomUrl();
2416
2486
  case "phone":
2417
- return faker.phone.number();
2487
+ return randomPhone();
2418
2488
  case "uuid":
2419
- return faker.string.uuid();
2489
+ return randomUuid();
2420
2490
  case "date":
2421
- return faker.date.recent().toISOString().split("T")[0];
2491
+ return randomRecentDate().toISOString().split("T")[0];
2422
2492
  case "datetime":
2423
- return faker.date.recent().toISOString();
2493
+ return randomRecentDate().toISOString();
2424
2494
  case "image":
2425
2495
  case "avatar":
2426
2496
  case "thumbnail":
@@ -2430,7 +2500,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2430
2500
  if (lname === "image" || lname === "imageurl" || lname === "image_url" || lname === "photo" || lname === "photourl" || lname === "photo_url" || lname === "avatar" || lname === "avatarurl" || lname === "avatar_url" || lname === "thumbnail" || lname === "thumbnailurl" || lname === "thumbnail_url" || lname === "picture" || lname === "pictureurl" || lname === "cover" || lname === "coverurl" || lname === "banner" || lname === "bannerurl") {
2431
2501
  return picsumUrl(entityName, fieldName);
2432
2502
  }
2433
- const value = faker.lorem.words(2);
2503
+ const value = randomWords(2);
2434
2504
  mockLog.debug("field:fallback-lorem", () => ({
2435
2505
  entityName,
2436
2506
  fieldName: field.name,
@@ -2446,7 +2516,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2446
2516
  * field-name heuristics.
2447
2517
  */
2448
2518
  generateDateValue(field) {
2449
- const date = faker.date.recent({ days: 30 });
2519
+ const date = randomRecentDate({ days: 30 });
2450
2520
  if (field.format === "date") return date.toISOString().split("T")[0];
2451
2521
  return date.toISOString();
2452
2522
  }
@@ -2530,7 +2600,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2530
2600
  this.stores.delete(normalized);
2531
2601
  this.idCounters.delete(normalized);
2532
2602
  }
2533
- /** Clear all data + re-anchor faker so the next seed loop reproduces
2603
+ /** Clear all data + re-anchor the PRNG so the next seed loop reproduces
2534
2604
  * identical rows. Hermetic-frame mode calls this between every step
2535
2605
  * via OrbitalServerRuntime.resetMockPersistence. */
2536
2606
  clearAll() {
@@ -3158,6 +3228,254 @@ function createUnifiedLoader(options) {
3158
3228
  return new UnifiedLoader(options);
3159
3229
  }
3160
3230
  createLogger("almadar:runtime:studio-config");
3231
+
3232
+ // src/ui/splice-lambda-traits.ts
3233
+ var TRAIT_BINDING_PREFIX = "@trait.";
3234
+ var CONFIG_BINDING_PREFIX = "@config.";
3235
+ var LambdaSpliceError = class extends Error {
3236
+ constructor(code, message) {
3237
+ super(message);
3238
+ this.code = code;
3239
+ this.name = "LambdaSpliceError";
3240
+ }
3241
+ code;
3242
+ };
3243
+ function isRenderUiEffect(effect) {
3244
+ return Array.isArray(effect) && effect.length >= 1 && effect[0] === "render-ui";
3245
+ }
3246
+ function renderOnlyTemplate(trait) {
3247
+ if (trait.listens && trait.listens.length > 0) return null;
3248
+ if (trait.ticks && trait.ticks.length > 0) return null;
3249
+ const sm = trait.stateMachine;
3250
+ const states = sm?.states ?? [];
3251
+ if (states.length > 1) return null;
3252
+ let template = null;
3253
+ for (const transition of sm?.transitions ?? []) {
3254
+ const selfLoop = transition.from === transition.to;
3255
+ const guarded = transition.guard !== void 0 && transition.guard !== null;
3256
+ if (!selfLoop || guarded) return null;
3257
+ for (const effect of transition.effects ?? []) {
3258
+ if (!isRenderUiEffect(effect) || !Array.isArray(effect)) return null;
3259
+ const pattern = effect[2];
3260
+ if (pattern === void 0) return null;
3261
+ if (template !== null) return null;
3262
+ template = pattern;
3263
+ }
3264
+ }
3265
+ return template;
3266
+ }
3267
+ function resolveObjectPath(value, path) {
3268
+ let cur = value;
3269
+ for (const seg of path.split(".")) {
3270
+ if (cur === null || typeof cur !== "object") return void 0;
3271
+ cur = cur[seg];
3272
+ if (cur === void 0) return void 0;
3273
+ }
3274
+ return cur;
3275
+ }
3276
+ function substituteConfig(expr, subs) {
3277
+ if (typeof expr === "string") {
3278
+ if (!expr.startsWith(CONFIG_BINDING_PREFIX)) return expr;
3279
+ const rest = expr.slice(CONFIG_BINDING_PREFIX.length);
3280
+ const dot = rest.indexOf(".");
3281
+ const param = dot === -1 ? rest : rest.slice(0, dot);
3282
+ const trailing = dot === -1 ? void 0 : rest.slice(dot + 1);
3283
+ if (!(param in subs)) return expr;
3284
+ const value = subs[param];
3285
+ if (trailing === void 0) {
3286
+ if (typeof value === "string" && value === expr) return expr;
3287
+ return substituteConfig(value, subs);
3288
+ }
3289
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
3290
+ const leaf = resolveObjectPath(value, trailing);
3291
+ if (leaf !== void 0) return substituteConfig(leaf, subs);
3292
+ }
3293
+ return expr;
3294
+ }
3295
+ if (Array.isArray(expr)) {
3296
+ return expr.map((item) => substituteConfig(item, subs));
3297
+ }
3298
+ if (expr !== null && typeof expr === "object") {
3299
+ const out = {};
3300
+ for (const [k, v] of Object.entries(expr)) {
3301
+ out[k] = substituteConfig(v, subs);
3302
+ }
3303
+ return out;
3304
+ }
3305
+ return expr;
3306
+ }
3307
+ function buildTemplate(trait) {
3308
+ const raw = renderOnlyTemplate(trait);
3309
+ const emits = trait.emits ?? [];
3310
+ if (raw === null) return { template: null, emits };
3311
+ const defaults = collectDeclaredConfigDefaults(trait);
3312
+ const template = defaults ? substituteConfig(raw, defaults) : raw;
3313
+ return { template, emits };
3314
+ }
3315
+ function isLambdaForm(expr) {
3316
+ return expr.length === 3 && (expr[0] === "fn" || expr[0] === "lambda");
3317
+ }
3318
+ function spliceExpr(expr, inLambda, st) {
3319
+ if (typeof expr === "string") {
3320
+ if (!inLambda || !expr.startsWith(TRAIT_BINDING_PREFIX)) return expr;
3321
+ const name = expr.slice(TRAIT_BINDING_PREFIX.length);
3322
+ if (name.length === 0 || name.includes(".")) return expr;
3323
+ const entry = st.templates.get(name);
3324
+ if (entry === void 0) return expr;
3325
+ if (entry.template === null) {
3326
+ throw new LambdaSpliceError(
3327
+ "LAMBDA_STATEFUL_TRAIT",
3328
+ `Trait "${name}" keeps its own state machine and cannot be expanded per list item \u2014 use the list pattern's item events (referenced inside a lambda in trait "${st.hostName}")`
3329
+ );
3330
+ }
3331
+ if (st.visiting.includes(name)) return expr;
3332
+ st.visiting.push(name);
3333
+ const expanded = spliceExpr(entry.template, true, st);
3334
+ st.visiting.pop();
3335
+ for (const emit of entry.emits) st.merged.push([name, emit]);
3336
+ st.spliced.add(name);
3337
+ st.changed = true;
3338
+ return expanded;
3339
+ }
3340
+ if (Array.isArray(expr)) {
3341
+ if (isLambdaForm(expr)) {
3342
+ return [expr[0], expr[1], spliceExpr(expr[2], true, st)];
3343
+ }
3344
+ return expr.map((item) => spliceExpr(item, inLambda, st));
3345
+ }
3346
+ if (expr !== null && typeof expr === "object") {
3347
+ const out = {};
3348
+ for (const [k, v] of Object.entries(expr)) {
3349
+ out[k] = spliceExpr(v, inLambda, st);
3350
+ }
3351
+ return out;
3352
+ }
3353
+ return expr;
3354
+ }
3355
+ function canonical(value) {
3356
+ if (Array.isArray(value)) return value.map(canonical);
3357
+ if (value !== null && typeof value === "object") {
3358
+ const out = {};
3359
+ for (const key of Object.keys(value).sort()) {
3360
+ out[key] = canonical(value[key]);
3361
+ }
3362
+ return out;
3363
+ }
3364
+ return value;
3365
+ }
3366
+ function mergeWrapperEmits(host, incoming) {
3367
+ const emits = [...host.emits ?? []];
3368
+ for (const [wrapper, emit] of incoming) {
3369
+ const existing = emits.find((e) => e.event === emit.event);
3370
+ if (existing) {
3371
+ const same = JSON.stringify(canonical(existing)) === JSON.stringify(canonical(emit));
3372
+ if (!same) {
3373
+ throw new LambdaSpliceError(
3374
+ "EMIT_MERGE_CONFLICT",
3375
+ `Emit "${emit.event}" merged from spliced wrapper "${wrapper}" conflicts with an existing "${emit.event}" on host "${host.name}" (different payload shape)`
3376
+ );
3377
+ }
3378
+ } else {
3379
+ emits.push(emit);
3380
+ }
3381
+ }
3382
+ return emits;
3383
+ }
3384
+ function removeConsumedWrappers(traits, pages, spliced) {
3385
+ for (; ; ) {
3386
+ const stillReferenced = /* @__PURE__ */ new Set();
3387
+ for (const { trait } of traits) {
3388
+ for (const t of trait.stateMachine?.transitions ?? []) {
3389
+ if (t.guard !== void 0 && t.guard !== null) {
3390
+ collectTraitRefsFromValue(t.guard, stillReferenced);
3391
+ }
3392
+ collectTraitRefsFromEffects(t.effects, stillReferenced);
3393
+ }
3394
+ for (const tick of trait.ticks ?? []) {
3395
+ if (tick.guard !== void 0 && tick.guard !== null) {
3396
+ collectTraitRefsFromValue(tick.guard, stillReferenced);
3397
+ }
3398
+ collectTraitRefsFromEffects(tick.effects, stillReferenced);
3399
+ }
3400
+ if (trait.config) {
3401
+ for (const field of Object.values(trait.config)) {
3402
+ if (field && typeof field === "object" && "default" in field && field.default !== void 0) {
3403
+ collectTraitRefsFromValue(field.default, stillReferenced);
3404
+ }
3405
+ }
3406
+ }
3407
+ }
3408
+ const removable = /* @__PURE__ */ new Set();
3409
+ for (const { trait } of traits) {
3410
+ if (spliced.has(trait.name) && !stillReferenced.has(trait.name)) {
3411
+ removable.add(trait.name);
3412
+ }
3413
+ }
3414
+ if (removable.size === 0) return;
3415
+ for (const { page } of pages) {
3416
+ const pageTraits = page.traits;
3417
+ if (!pageTraits) continue;
3418
+ const survivors2 = pageTraits.filter((pt) => !removable.has(pt.ref)).length;
3419
+ if (survivors2 === 0) {
3420
+ for (const pt of pageTraits) removable.delete(pt.ref);
3421
+ }
3422
+ }
3423
+ if (removable.size === 0) return;
3424
+ for (const resolvedPage of pages) {
3425
+ const pageTraits = resolvedPage.page.traits;
3426
+ if (pageTraits) {
3427
+ resolvedPage.page.traits = pageTraits.filter((pt) => !removable.has(pt.ref));
3428
+ }
3429
+ }
3430
+ const survivors = traits.filter(({ trait }) => !removable.has(trait.name));
3431
+ traits.length = 0;
3432
+ traits.push(...survivors);
3433
+ }
3434
+ }
3435
+ function spliceLambdaTraitRefs(traits, pages) {
3436
+ if (traits.length === 0) return;
3437
+ const templates = /* @__PURE__ */ new Map();
3438
+ for (const { trait } of traits) {
3439
+ templates.set(trait.name, buildTemplate(trait));
3440
+ }
3441
+ const spliced = /* @__PURE__ */ new Set();
3442
+ for (const resolved of traits) {
3443
+ const host = resolved.trait;
3444
+ const st = {
3445
+ templates,
3446
+ // Seed with the host itself so a self-reference never self-splices.
3447
+ visiting: [host.name],
3448
+ merged: [],
3449
+ spliced,
3450
+ hostName: host.name,
3451
+ changed: false
3452
+ };
3453
+ const sm = host.stateMachine;
3454
+ const nextTransitions = sm?.transitions ? sm.transitions.map((t) => ({
3455
+ ...t,
3456
+ effects: t.effects ? t.effects.map((e) => spliceExpr(e, false, st)) : t.effects
3457
+ })) : sm?.transitions;
3458
+ const nextTicks = host.ticks ? host.ticks.map((tick) => ({
3459
+ ...tick,
3460
+ effects: tick.effects.map((e) => spliceExpr(e, false, st))
3461
+ })) : host.ticks;
3462
+ if (!st.changed) {
3463
+ continue;
3464
+ }
3465
+ const nextEmits = st.merged.length > 0 ? mergeWrapperEmits(host, st.merged) : host.emits;
3466
+ resolved.trait = {
3467
+ ...host,
3468
+ ...sm ? { stateMachine: { ...sm, transitions: nextTransitions ?? [] } } : {},
3469
+ ...nextTicks !== void 0 ? { ticks: nextTicks } : {},
3470
+ ...nextEmits !== void 0 ? { emits: nextEmits } : {}
3471
+ };
3472
+ }
3473
+ if (spliced.size > 0) {
3474
+ removeConsumedWrappers(traits, pages, spliced);
3475
+ }
3476
+ }
3477
+
3478
+ // src/resolver/reference-resolver.ts
3161
3479
  var refResolverLog = createLogger("almadar:runtime:ref-resolver");
3162
3480
  function renameEventsInRenderUiConfig(node, rename) {
3163
3481
  if (node === null || node === void 0) return node;
@@ -3329,6 +3647,33 @@ function applyEventRenames(trait, renames) {
3329
3647
  emits: nextEmits
3330
3648
  };
3331
3649
  }
3650
+ function resolveConfigRefEmitNames(trait, callSiteConfig) {
3651
+ const emits = trait.emits ?? [];
3652
+ const hasRef = emits.some((em) => configRefEventKnob(em.event) !== void 0);
3653
+ if (!hasRef) return { trait, errors: [] };
3654
+ const effectiveConfig = {
3655
+ ...normalizeCallSiteConfigToValues(trait.config) ?? {},
3656
+ ...normalizeCallSiteConfigToValues(callSiteConfig) ?? {}
3657
+ };
3658
+ const errors = [];
3659
+ const nextEmits = emits.map((em) => {
3660
+ if (configRefEventKnob(em.event) === void 0) return em;
3661
+ const result = resolveConfigRefEventName(em.event, trait.config, effectiveConfig);
3662
+ if (!result.ok) {
3663
+ errors.push(
3664
+ `Trait "${trait.name}" emits \`${em.event}\` but the reference is invalid (${result.error}): the knob must be a declared string-typed config field with a default.`
3665
+ );
3666
+ return em;
3667
+ }
3668
+ refResolverLog.debug("emit-config-ref:resolved", {
3669
+ trait: trait.name,
3670
+ ref: em.event,
3671
+ resolved: result.value
3672
+ });
3673
+ return { ...em, event: result.value };
3674
+ });
3675
+ return { trait: { ...trait, emits: nextEmits }, errors };
3676
+ }
3332
3677
  var ReferenceResolver = class {
3333
3678
  loader;
3334
3679
  options;
@@ -3383,6 +3728,14 @@ var ReferenceResolver = class {
3383
3728
  if (!entityResult.success || !traitsResult.success || !pagesResult.success) {
3384
3729
  return { success: false, errors: ["Internal error: unexpected failure state"] };
3385
3730
  }
3731
+ try {
3732
+ spliceLambdaTraitRefs(traitsResult.data, pagesResult.data);
3733
+ } catch (e) {
3734
+ if (e instanceof LambdaSpliceError) {
3735
+ return { success: false, errors: [e.message] };
3736
+ }
3737
+ throw e;
3738
+ }
3386
3739
  return {
3387
3740
  success: true,
3388
3741
  data: {
@@ -3549,10 +3902,14 @@ var ReferenceResolver = class {
3549
3902
  */
3550
3903
  resolveTraitRef(traitRef, imports) {
3551
3904
  if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
3905
+ const { trait: resolvedInline, errors } = resolveConfigRefEmitNames(traitRef);
3906
+ if (errors.length > 0) {
3907
+ return { success: false, errors };
3908
+ }
3552
3909
  return {
3553
3910
  success: true,
3554
3911
  data: {
3555
- trait: traitRef,
3912
+ trait: resolvedInline,
3556
3913
  source: { type: "inline" }
3557
3914
  },
3558
3915
  warnings: []
@@ -3603,7 +3960,11 @@ var ReferenceResolver = class {
3603
3960
  };
3604
3961
  }
3605
3962
  const baseTrait = overrideName ? { ...trait, name: overrideName } : trait;
3606
- const reboundTrait = applyLinkedEntityRename(baseTrait, linkedEntity);
3963
+ const { trait: configResolvedTrait, errors: configRefErrors } = resolveConfigRefEmitNames(baseTrait, config);
3964
+ if (configRefErrors.length > 0) {
3965
+ return { success: false, errors: configRefErrors };
3966
+ }
3967
+ const reboundTrait = applyLinkedEntityRename(configResolvedTrait, linkedEntity);
3607
3968
  const renamedTrait = applyEventRenames(reboundTrait, eventRenames);
3608
3969
  const finalTrait = listensOverride !== void 0 ? { ...renamedTrait, listens: listensOverride } : renamedTrait;
3609
3970
  if (listensOverride !== void 0) {
@@ -3628,7 +3989,11 @@ var ReferenceResolver = class {
3628
3989
  const localTrait = this.localTraits.get(ref);
3629
3990
  if (localTrait) {
3630
3991
  const baseLocal = overrideName ? { ...localTrait, name: overrideName } : localTrait;
3631
- const reboundLocal = applyLinkedEntityRename(baseLocal, linkedEntity);
3992
+ const { trait: configResolvedLocal, errors: localConfigRefErrors } = resolveConfigRefEmitNames(baseLocal, config);
3993
+ if (localConfigRefErrors.length > 0) {
3994
+ return { success: false, errors: localConfigRefErrors };
3995
+ }
3996
+ const reboundLocal = applyLinkedEntityRename(configResolvedLocal, linkedEntity);
3632
3997
  const renamedLocalTrait = applyEventRenames(reboundLocal, eventRenames);
3633
3998
  const finalLocalTrait = listensOverride !== void 0 ? { ...renamedLocalTrait, listens: listensOverride } : renamedLocalTrait;
3634
3999
  if (listensOverride !== void 0) {
@@ -4052,4 +4417,4 @@ var InMemoryPersistence = class {
4052
4417
  }
4053
4418
  };
4054
4419
 
4055
- export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, containsBindings, createContextFromBindings, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes };
4420
+ export { CALLSITE_PAYLOAD_PREFIX, EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, containsBindings, createContextFromBindings, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, extractBindings, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, interpolateProps, interpolateValue, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, resolveCallSitePayloadCaptures, validateEventPayload, validatePayloadShapes };