@almadar/runtime 6.32.0 → 6.34.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, isReferenceConfigType, 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 {
@@ -24,7 +26,7 @@ var EventBus = class {
24
26
  * beyond `maxDepth`, the event is dropped and an error is logged.
25
27
  * This prevents infinite loops from circular emit/listen chains.
26
28
  */
27
- emit(type, payload, source) {
29
+ emit(type, payload, source, routingKey) {
28
30
  if (this.depth >= this.maxDepth) {
29
31
  log.error("circular event loop dropped", { type, depth: this.depth, maxDepth: this.maxDepth });
30
32
  return;
@@ -35,12 +37,13 @@ var EventBus = class {
35
37
  timestamp: Date.now(),
36
38
  source
37
39
  };
38
- const listeners = this.listeners.get(type);
40
+ const deliveryKey = routingKey ?? type;
41
+ const listeners = this.listeners.get(deliveryKey);
39
42
  const listenerCount = listeners?.size ?? 0;
40
43
  if (listenerCount > 0) {
41
44
  log.debug("emit", { type, listenerCount, depth: this.depth });
42
45
  } else {
43
- log.warn("emit no listeners", { type });
46
+ log.debug("emit no listeners", { type });
44
47
  }
45
48
  this.depth++;
46
49
  try {
@@ -289,6 +292,33 @@ var bindLog = createLogger("almadar:runtime:bindings");
289
292
  setNamespaceLevel("almadar:runtime:bindings", "WARN");
290
293
  var renderLog = createLogger("almadar:runtime:render-ui");
291
294
  var CLIENT_ONLY_BINDING_ROOTS = /* @__PURE__ */ new Set(["trait"]);
295
+ var CALLSITE_PAYLOAD_PREFIX = "@callsitePayload.";
296
+ function payloadValueToConfigValue(v) {
297
+ if (v === null || v === void 0) return null;
298
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") return v;
299
+ if (v instanceof Date) return v.toISOString();
300
+ if (Array.isArray(v)) return v.map(payloadValueToConfigValue);
301
+ if (typeof v === "object") {
302
+ const obj = {};
303
+ for (const [k, val] of Object.entries(v)) obj[k] = payloadValueToConfigValue(val);
304
+ return obj;
305
+ }
306
+ return String(v);
307
+ }
308
+ function resolveCallSitePayloadCaptures(config, payload) {
309
+ let ctx;
310
+ const out = {};
311
+ for (const [key, value] of Object.entries(config)) {
312
+ if (typeof value === "string" && value.startsWith(CALLSITE_PAYLOAD_PREFIX)) {
313
+ const field = value.slice(CALLSITE_PAYLOAD_PREFIX.length);
314
+ if (!ctx) ctx = createMinimalContext({}, payload ?? {}, "idle");
315
+ out[key] = payloadValueToConfigValue(resolveBinding(`@payload.${field}`, ctx));
316
+ } else {
317
+ out[key] = value;
318
+ }
319
+ }
320
+ return out;
321
+ }
292
322
  function isClientOnlyBinding(value) {
293
323
  if (!value.startsWith("@")) return false;
294
324
  const afterAt = value.slice(1);
@@ -365,6 +395,7 @@ function interpolateValue(value, ctx) {
365
395
  }
366
396
  return value;
367
397
  }
398
+ var inFlightConfigRecursions = /* @__PURE__ */ new Set();
368
399
  function interpolateString(value, ctx) {
369
400
  if (value.startsWith("@") && isPureBinding(value)) {
370
401
  if (isClientOnlyBinding(value)) {
@@ -373,6 +404,15 @@ function interpolateString(value, ctx) {
373
404
  }
374
405
  const resolved = resolveBinding(value, ctx);
375
406
  bindLog.debug("resolve", { binding: value, resolvedType: typeof resolved });
407
+ if (value.startsWith("@config.") && resolved !== null && typeof resolved === "object" && containsBindings(resolved) && !inFlightConfigRecursions.has(value)) {
408
+ inFlightConfigRecursions.add(value);
409
+ try {
410
+ bindLog.debug("resolve:config-recurse", { binding: value });
411
+ return interpolateValue(resolved, ctx);
412
+ } finally {
413
+ inFlightConfigRecursions.delete(value);
414
+ }
415
+ }
376
416
  return resolved;
377
417
  }
378
418
  if (value.includes("@")) {
@@ -414,12 +454,25 @@ function interpolateArray(value, ctx) {
414
454
  let anyChanged = false;
415
455
  for (let i = 0; i < value.length; i++) {
416
456
  const item = value[i];
457
+ if (Array.isArray(item) && isRenderChildrenMap(item)) {
458
+ const expanded = evaluate(item, ctx);
459
+ if (Array.isArray(expanded)) {
460
+ for (const node of expanded) mapped.push(node);
461
+ }
462
+ anyChanged = true;
463
+ continue;
464
+ }
417
465
  const interpolated = interpolateValue(item, ctx);
418
466
  mapped.push(interpolated);
419
467
  if (interpolated !== item) anyChanged = true;
420
468
  }
421
469
  return anyChanged ? mapped : value;
422
470
  }
471
+ function isRenderChildrenMap(value) {
472
+ if (value.length !== 3 || value[0] !== "array/map") return false;
473
+ const lambda = value[2];
474
+ return Array.isArray(lambda) && lambda.length === 3 && lambda[0] === "fn" && typeof lambda[1] === "string";
475
+ }
423
476
  function isSExpression(value) {
424
477
  if (value.length === 0) return false;
425
478
  const first = value[0];
@@ -497,19 +550,21 @@ function createInitialTraitState(trait) {
497
550
  context: {}
498
551
  };
499
552
  }
500
- function findMatchingTransitions(trait, currentState, eventKey) {
553
+ function findMatchingTransitions(trait, currentState, eventKey, eventId) {
501
554
  if (!trait.transitions || trait.transitions.length === 0) {
502
555
  return [];
503
556
  }
504
557
  return trait.transitions.filter((t) => {
505
- if (Array.isArray(t.from)) {
506
- return t.from.includes(currentState) && t.event === eventKey;
558
+ const fromMatches = Array.isArray(t.from) ? t.from.includes(currentState) : t.from === currentState;
559
+ if (!fromMatches) return false;
560
+ if (eventId && t.eventId) {
561
+ return t.eventId === eventId;
507
562
  }
508
- return t.from === currentState && t.event === eventKey;
563
+ return t.event === eventKey;
509
564
  });
510
565
  }
511
- function findTransition(trait, currentState, eventKey) {
512
- return findMatchingTransitions(trait, currentState, eventKey)[0];
566
+ function findTransition(trait, currentState, eventKey, eventId) {
567
+ return findMatchingTransitions(trait, currentState, eventKey, eventId)[0];
513
568
  }
514
569
  function normalizeEventKey(eventKey) {
515
570
  if (!eventKey) return "";
@@ -520,6 +575,7 @@ function processEvent(options) {
520
575
  traitState,
521
576
  trait,
522
577
  eventKey,
578
+ eventId,
523
579
  payload,
524
580
  entityData,
525
581
  config,
@@ -528,7 +584,7 @@ function processEvent(options) {
528
584
  contextExtensions
529
585
  } = options;
530
586
  const normalizedEvent = normalizeEventKey(eventKey);
531
- const candidates = findMatchingTransitions(trait, traitState.currentState, normalizedEvent);
587
+ const candidates = findMatchingTransitions(trait, traitState.currentState, normalizedEvent, eventId);
532
588
  if (candidates.length === 0) {
533
589
  smLog.debug("noTransition", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState });
534
590
  return {
@@ -647,6 +703,15 @@ function compositeKey(traitName, scope) {
647
703
  }
648
704
  var StateMachineManager = class {
649
705
  traits = /* @__PURE__ */ new Map();
706
+ /**
707
+ * V4 identity index: trait id → trait name. Populated for traits that
708
+ * carry an `id` (ledger-backed schemas). Lets callers resolve a trait's
709
+ * state by its stable id, which survives a mid-session rename — the
710
+ * name-keyed maps are re-pointed on rename via {@link renameTrait}, but
711
+ * an id holder never has to observe the rename at all. Empty for legacy
712
+ * id-free schemas, where every lookup stays name-keyed (unchanged).
713
+ */
714
+ traitIdToName = /* @__PURE__ */ new Map();
650
715
  /**
651
716
  * Per-trait call-site config, surfaced to guard expressions so
652
717
  * `@config.X` resolves at runtime. Populated by the orbital's
@@ -697,6 +762,69 @@ var StateMachineManager = class {
697
762
  */
698
763
  addTrait(trait) {
699
764
  this.traits.set(trait.name, trait);
765
+ if (trait.id !== void 0) {
766
+ this.traitIdToName.set(trait.id, trait.name);
767
+ }
768
+ }
769
+ /**
770
+ * Resolve a trait by its V4 id. Returns undefined when no trait carries
771
+ * that id (legacy schema, or unknown id). Exact-match only — no name
772
+ * similarity.
773
+ */
774
+ getTraitById(traitId) {
775
+ const name = this.traitIdToName.get(traitId);
776
+ return name === void 0 ? void 0 : this.traits.get(name);
777
+ }
778
+ /**
779
+ * Get a trait's current state by its V4 id (id-first lookup). Falls back
780
+ * to `undefined` when the id is unknown. The id survives a rename, so a
781
+ * holder of the id reads the right state without ever seeing the new name.
782
+ */
783
+ getStateById(traitId, entityId) {
784
+ const name = this.traitIdToName.get(traitId);
785
+ return name === void 0 ? void 0 : this.getState(name, entityId);
786
+ }
787
+ /**
788
+ * Apply a mid-session trait rename: re-point the name-keyed maps from
789
+ * `oldName` to `newName`, preserving live state, queues, and the id
790
+ * index. A no-op when the trait isn't registered under `oldName`. This
791
+ * is the interpreter-side of a ledger `curName` edit for the trait's own
792
+ * name-keyed storage; id-keyed references need no update.
793
+ */
794
+ renameTrait(oldName, newName) {
795
+ const trait = this.traits.get(oldName);
796
+ if (!trait || oldName === newName) return;
797
+ const renamed = { ...trait, name: newName };
798
+ this.traits.delete(oldName);
799
+ this.traits.set(newName, renamed);
800
+ if (renamed.id !== void 0) {
801
+ this.traitIdToName.set(renamed.id, newName);
802
+ }
803
+ const remap = (m) => {
804
+ const prefix2 = `${oldName}::`;
805
+ for (const key of [...m.keys()]) {
806
+ if (key.startsWith(prefix2)) {
807
+ const scope = key.slice(prefix2.length);
808
+ const v = m.get(key);
809
+ m.delete(key);
810
+ m.set(`${newName}::${scope}`, v);
811
+ }
812
+ }
813
+ };
814
+ remap(this.states);
815
+ remap(this.queues);
816
+ const prefix = `${oldName}::`;
817
+ for (const key of [...this.processing]) {
818
+ if (key.startsWith(prefix)) {
819
+ this.processing.delete(key);
820
+ this.processing.add(`${newName}::${key.slice(prefix.length)}`);
821
+ }
822
+ }
823
+ const cfg = this.traitConfigs.get(oldName);
824
+ if (cfg !== void 0) {
825
+ this.traitConfigs.set(newName, cfg);
826
+ this.traitConfigs.delete(oldName);
827
+ }
700
828
  }
701
829
  /**
702
830
  * Bind the call-site config for a trait so guard `@config.X`
@@ -715,6 +843,10 @@ var StateMachineManager = class {
715
843
  * Remove a trait from the manager.
716
844
  */
717
845
  removeTrait(traitName) {
846
+ const removed = this.traits.get(traitName);
847
+ if (removed?.id !== void 0) {
848
+ this.traitIdToName.delete(removed.id);
849
+ }
718
850
  this.traits.delete(traitName);
719
851
  const prefix = `${traitName}::`;
720
852
  for (const key of [...this.states.keys()]) {
@@ -802,11 +934,11 @@ var StateMachineManager = class {
802
934
  /**
803
935
  * Check if a trait can handle an event from its current state.
804
936
  */
805
- canHandleEvent(traitName, eventKey, entityId) {
937
+ canHandleEvent(traitName, eventKey, entityId, eventId) {
806
938
  const trait = this.traits.get(traitName);
807
939
  const state = this.getOrInitState(traitName, entityId ?? SINGLETON_SCOPE);
808
940
  if (!trait || !state) return false;
809
- return !!findTransition(trait, state.currentState, normalizeEventKey(eventKey));
941
+ return !!findTransition(trait, state.currentState, normalizeEventKey(eventKey), eventId);
810
942
  }
811
943
  /**
812
944
  * Send an event to all traits.
@@ -818,9 +950,13 @@ var StateMachineManager = class {
818
950
  * `@entity.X` see prior step writes — required for [runtime] entities
819
951
  * that have no persistence row to reload.
820
952
  *
953
+ * `eventId` is the V4 dual-carry id sibling of `eventKey` (see
954
+ * {@link ProcessEventOptions.eventId}) — additive and optional, threaded
955
+ * through to `processEvent` for id-primary transition matching.
956
+ *
821
957
  * @returns Array of transition results (one per trait that had a matching transition)
822
958
  */
823
- sendEvent(eventKey, payload, entityData, entityByTrait) {
959
+ sendEvent(eventKey, payload, entityData, entityByTrait, eventId) {
824
960
  const results = [];
825
961
  const scope = scopeOf(entityData);
826
962
  for (const [traitName, trait] of this.traits) {
@@ -832,6 +968,7 @@ var StateMachineManager = class {
832
968
  traitState,
833
969
  trait,
834
970
  eventKey,
971
+ eventId,
835
972
  payload,
836
973
  entityData: perTraitEntity,
837
974
  config: this.traitConfigs.get(traitName),
@@ -873,12 +1010,12 @@ var StateMachineManager = class {
873
1010
  * trait to process them sequentially (actor-model guarantee: one event
874
1011
  * at a time per trait, effects fully awaited before the next event).
875
1012
  */
876
- enqueueEvent(eventKey, payload, entityData, entityByTrait) {
1013
+ enqueueEvent(eventKey, payload, entityData, entityByTrait, eventId) {
877
1014
  const scope = scopeOf(entityData);
878
1015
  for (const [traitName] of this.traits) {
879
1016
  const key = compositeKey(traitName, scope);
880
1017
  const queue = this.queues.get(key) ?? [];
881
- queue.push({ eventKey, payload, entityData, entityByTrait });
1018
+ queue.push({ eventKey, eventId, payload, entityData, entityByTrait });
882
1019
  this.queues.set(key, queue);
883
1020
  }
884
1021
  }
@@ -903,6 +1040,7 @@ var StateMachineManager = class {
903
1040
  traitState,
904
1041
  trait,
905
1042
  eventKey: entry.eventKey,
1043
+ eventId: entry.eventId,
906
1044
  payload: entry.payload,
907
1045
  entityData: perTraitEntity,
908
1046
  config: this.traitConfigs.get(traitName),
@@ -982,7 +1120,7 @@ var StateMachineManager = class {
982
1120
 
983
1121
  // src/types.ts
984
1122
  var HANDLER_MANIFEST = {
985
- client: ["render-ui", "render", "navigate", "notify", "emit", "set", "log", "ref", "deref", "watch", "send-server"],
1123
+ 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
1124
  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
1125
  test: [
988
1126
  "render-ui",
@@ -1002,7 +1140,11 @@ var HANDLER_MANIFEST = {
1002
1140
  "swap!",
1003
1141
  "watch",
1004
1142
  "atomic",
1005
- "send-server"
1143
+ "send-server",
1144
+ "browser/open-file-picker",
1145
+ "browser/clipboard-read",
1146
+ "browser/clipboard-write",
1147
+ "browser/geolocation-current"
1006
1148
  ],
1007
1149
  ssr: ["render-ui", "render", "fetch", "emit", "set", "log", "ref", "deref"]
1008
1150
  };
@@ -1938,6 +2080,58 @@ var EffectExecutor = class _EffectExecutor {
1938
2080
  }, emitCfg);
1939
2081
  break;
1940
2082
  }
2083
+ // === Browser device operators (client-side, user-gesture) ===
2084
+ // Async host APIs routed to dedicated handler methods. Uniform
2085
+ // `{ result }` / `{ error }` payload via runSubstrate. On the
2086
+ // server (or any host without the API) the handler is absent →
2087
+ // unsupported warning; a thrown error fires `emit.failure`.
2088
+ case "browser/open-file-picker": {
2089
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
2090
+ const options = positional[0];
2091
+ await this.runSubstrate(async () => {
2092
+ if (!this.handlers.browserOpenFilePicker) {
2093
+ this.logUnsupported("browser/open-file-picker");
2094
+ return null;
2095
+ }
2096
+ return this.handlers.browserOpenFilePicker(options);
2097
+ }, emitCfg);
2098
+ break;
2099
+ }
2100
+ case "browser/clipboard-read": {
2101
+ const [, emitCfg] = this.splitSubstrateEmit(args);
2102
+ await this.runSubstrate(async () => {
2103
+ if (!this.handlers.browserClipboardRead) {
2104
+ this.logUnsupported("browser/clipboard-read");
2105
+ return null;
2106
+ }
2107
+ return this.handlers.browserClipboardRead();
2108
+ }, emitCfg);
2109
+ break;
2110
+ }
2111
+ case "browser/clipboard-write": {
2112
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
2113
+ const text = positional[0];
2114
+ await this.runSubstrate(async () => {
2115
+ if (!this.handlers.browserClipboardWrite) {
2116
+ this.logUnsupported("browser/clipboard-write");
2117
+ return null;
2118
+ }
2119
+ return this.handlers.browserClipboardWrite(text);
2120
+ }, emitCfg);
2121
+ break;
2122
+ }
2123
+ case "browser/geolocation-current": {
2124
+ const [positional, emitCfg] = this.splitSubstrateEmit(args);
2125
+ const options = positional[0];
2126
+ await this.runSubstrate(async () => {
2127
+ if (!this.handlers.browserGeolocationCurrent) {
2128
+ this.logUnsupported("browser/geolocation-current");
2129
+ return null;
2130
+ }
2131
+ return this.handlers.browserGeolocationCurrent(options);
2132
+ }, emitCfg);
2133
+ break;
2134
+ }
1941
2135
  default: {
1942
2136
  if (operator.includes("/")) {
1943
2137
  const [positional, emitCfg] = this.splitSubstrateEmit(args);
@@ -2104,21 +2298,6 @@ function collectDeclaredConfigDefaults(trait) {
2104
2298
  }
2105
2299
  return hasAny ? defaults : void 0;
2106
2300
  }
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
2301
  function collectDeclaredEntityDefaults(entity) {
2123
2302
  if (!entity) return void 0;
2124
2303
  const defaults = {};
@@ -2134,7 +2313,7 @@ function collectDeclaredEntityDefaults(entity) {
2134
2313
  var mockLog = createLogger("almadar:runtime:mock");
2135
2314
  var DEFAULT_MOCK_SEED = 42;
2136
2315
  function picsumUrl(entityName, fieldName, width = 400, height = 400) {
2137
- const seed = `${entityName}-${fieldName}-${faker.number.int({ min: 0, max: 1e3 })}`;
2316
+ const seed = `${entityName}-${fieldName}-${randomInt({ min: 0, max: 1e3 })}`;
2138
2317
  return `https://picsum.photos/seed/${encodeURIComponent(seed)}/${width}/${height}`;
2139
2318
  }
2140
2319
  var SEED_REFERENCE_TIMESTAMP = "2024-01-01T00:00:00.000Z";
@@ -2142,6 +2321,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2142
2321
  stores = /* @__PURE__ */ new Map();
2143
2322
  schemas = /* @__PURE__ */ new Map();
2144
2323
  idCounters = /* @__PURE__ */ new Map();
2324
+ /** entityId -> normalized store name, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
2325
+ storeNameById = /* @__PURE__ */ new Map();
2145
2326
  config;
2146
2327
  constructor(config = {}) {
2147
2328
  this.config = {
@@ -2152,17 +2333,17 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2152
2333
  // input doesn't overwrite the default.
2153
2334
  seed: config.seed ?? DEFAULT_MOCK_SEED
2154
2335
  };
2155
- faker.seed(this.config.seed);
2336
+ seedRandom(this.config.seed);
2156
2337
  mockLog.debug("mock:adapter:init", { seed: this.config.seed });
2157
2338
  }
2158
- /** Re-anchor faker's PRNG to the configured seed. Called before every
2339
+ /** Re-anchor the PRNG to the configured seed. Called before every
2159
2340
  * re-seed loop so identical reseed sequences produce identical rows
2160
- * (timestamps + faker-generated fields). Without this, the first
2341
+ * (timestamps + generated fields). Without this, the first
2161
2342
  * reseed produces row set A, the second produces row set B, and
2162
2343
  * diff observers see all rows as "changed" between frames. */
2163
2344
  resetFakerSeed() {
2164
2345
  if (this.config.seed !== void 0) {
2165
- faker.seed(this.config.seed);
2346
+ seedRandom(this.config.seed);
2166
2347
  }
2167
2348
  }
2168
2349
  // ============================================================================
@@ -2188,11 +2369,14 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2188
2369
  /**
2189
2370
  * Register an entity schema and seed mock data.
2190
2371
  * If the schema has seedData, those instances are used directly.
2191
- * Otherwise, random mock data is generated with faker.
2372
+ * Otherwise, random mock data is generated with the seeded PRNG.
2192
2373
  */
2193
2374
  registerEntity(schema, seedCount) {
2194
2375
  const normalized = schema.name.toLowerCase();
2195
2376
  this.schemas.set(normalized, schema);
2377
+ if (schema.id) {
2378
+ this.storeNameById.set(schema.id, normalized);
2379
+ }
2196
2380
  if (schema.seedData && schema.seedData.length > 0) {
2197
2381
  this.seedFromInstances(schema.name, schema.seedData);
2198
2382
  } else {
@@ -2224,7 +2408,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2224
2408
  if (relationFields.length === 0) continue;
2225
2409
  for (const row of store.values()) {
2226
2410
  for (const field of relationFields) {
2227
- const targetStore = this.stores.get(field.relation.entity.toLowerCase());
2411
+ const targetNormalized = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? field.relation.entity.toLowerCase();
2412
+ const targetStore = this.stores.get(targetNormalized);
2228
2413
  if (!targetStore || targetStore.size === 0) continue;
2229
2414
  const selfId = row["id"];
2230
2415
  const sameStore = targetStore === store;
@@ -2236,10 +2421,10 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2236
2421
  if (eligible.length === 0) continue;
2237
2422
  const cardinality = field.relation.cardinality ?? "many";
2238
2423
  if (cardinality === "one" || cardinality === "many-to-one") {
2239
- row[field.name] = faker.helpers.arrayElement(eligible);
2424
+ row[field.name] = randomArrayElement(eligible);
2240
2425
  } 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);
2426
+ const pickCount = Math.min(eligible.length, randomInt({ min: 2, max: 4 }));
2427
+ row[field.name] = shuffleArray(eligible.slice()).slice(0, pickCount);
2243
2428
  }
2244
2429
  }
2245
2430
  }
@@ -2291,7 +2476,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2291
2476
  const id = this.nextId(entityName);
2292
2477
  const item = {
2293
2478
  id,
2294
- createdAt: faker.date.past({ years: 1 }).toISOString(),
2479
+ createdAt: randomPastDate({ years: 1 }).toISOString(),
2295
2480
  updatedAt: SEED_REFERENCE_TIMESTAMP
2296
2481
  };
2297
2482
  for (const field of fields) {
@@ -2333,16 +2518,16 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2333
2518
  case "string":
2334
2519
  return this.generateStringValue(entityName, field, index);
2335
2520
  case "number":
2336
- return faker.number.int({ min: 0, max: 100 });
2521
+ return randomInt({ min: 0, max: 100 });
2337
2522
  case "boolean":
2338
- return faker.datatype.boolean();
2523
+ return randomBoolean();
2339
2524
  case "date":
2340
2525
  case "timestamp":
2341
2526
  case "datetime":
2342
2527
  return this.generateDateValue(field);
2343
2528
  case "enum":
2344
2529
  if (field.values && field.values.length > 0) {
2345
- return faker.helpers.arrayElement(field.values);
2530
+ return randomArrayElement(field.values);
2346
2531
  }
2347
2532
  return null;
2348
2533
  case "relation":
@@ -2367,7 +2552,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2367
2552
  generateArrayValue(entityName, field, index, depth = 0) {
2368
2553
  if (field.type !== "array" || !field.items) return [];
2369
2554
  if (depth >= _MockPersistenceAdapter.MAX_NESTED_DEPTH) return [];
2370
- const count = faker.number.int({ min: 3, max: 5 });
2555
+ const count = randomInt({ min: 3, max: 5 });
2371
2556
  const out = [];
2372
2557
  const elementName = field.name ?? "item";
2373
2558
  for (let i = 0; i < count; i++) {
@@ -2381,7 +2566,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2381
2566
  }
2382
2567
  /**
2383
2568
  * Generate a single object value with each declared property populated
2384
- * by faker. Walks `properties` and recursively delegates to
2569
+ * by the seeded PRNG. Walks `properties` and recursively delegates to
2385
2570
  * `generateFieldValue` per property so nested objects-of-arrays-of-objects
2386
2571
  * compose correctly.
2387
2572
  */
@@ -2398,29 +2583,29 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2398
2583
  /**
2399
2584
  * Generate a string value based on the field's declared schema metadata.
2400
2585
  * Reads `values` (enum) first, then `format` (email/url/phone/uuid/date/
2401
- * datetime), then falls back to faker.lorem.words. No field-name heuristics
2586
+ * datetime), then falls back to randomWords. No field-name heuristics
2402
2587
  * — the schema is the source of truth. If a caller needs a real email, they
2403
2588
  * declare `format: "email"`; if they need an enum, they declare `values: [...]`.
2404
2589
  */
2405
2590
  generateStringValue(entityName, field, _index) {
2406
2591
  const values = "values" in field ? field.values : void 0;
2407
2592
  if (values && values.length > 0) {
2408
- return faker.helpers.arrayElement(values);
2593
+ return randomArrayElement(values);
2409
2594
  }
2410
2595
  const fieldName = field.name ?? "field";
2411
2596
  switch (field.format) {
2412
2597
  case "email":
2413
- return faker.internet.email();
2598
+ return randomEmail();
2414
2599
  case "url":
2415
- return faker.internet.url();
2600
+ return randomUrl();
2416
2601
  case "phone":
2417
- return faker.phone.number();
2602
+ return randomPhone();
2418
2603
  case "uuid":
2419
- return faker.string.uuid();
2604
+ return randomUuid();
2420
2605
  case "date":
2421
- return faker.date.recent().toISOString().split("T")[0];
2606
+ return randomRecentDate().toISOString().split("T")[0];
2422
2607
  case "datetime":
2423
- return faker.date.recent().toISOString();
2608
+ return randomRecentDate().toISOString();
2424
2609
  case "image":
2425
2610
  case "avatar":
2426
2611
  case "thumbnail":
@@ -2430,7 +2615,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2430
2615
  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
2616
  return picsumUrl(entityName, fieldName);
2432
2617
  }
2433
- const value = faker.lorem.words(2);
2618
+ const value = randomWords(2);
2434
2619
  mockLog.debug("field:fallback-lorem", () => ({
2435
2620
  entityName,
2436
2621
  fieldName: field.name,
@@ -2446,7 +2631,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2446
2631
  * field-name heuristics.
2447
2632
  */
2448
2633
  generateDateValue(field) {
2449
- const date = faker.date.recent({ days: 30 });
2634
+ const date = randomRecentDate({ days: 30 });
2450
2635
  if (field.format === "date") return date.toISOString().split("T")[0];
2451
2636
  return date.toISOString();
2452
2637
  }
@@ -2530,7 +2715,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2530
2715
  this.stores.delete(normalized);
2531
2716
  this.idCounters.delete(normalized);
2532
2717
  }
2533
- /** Clear all data + re-anchor faker so the next seed loop reproduces
2718
+ /** Clear all data + re-anchor the PRNG so the next seed loop reproduces
2534
2719
  * identical rows. Hermetic-frame mode calls this between every step
2535
2720
  * via OrbitalServerRuntime.resetMockPersistence. */
2536
2721
  clearAll() {
@@ -2902,7 +3087,7 @@ async function getExternalLoaderModule() {
2902
3087
  return null;
2903
3088
  }
2904
3089
  try {
2905
- externalLoaderModule = await import('./external-loader-OPXVTNC4.js');
3090
+ externalLoaderModule = await import('./external-loader-FNK5AU6U.js');
2906
3091
  return externalLoaderModule;
2907
3092
  } catch {
2908
3093
  return null;
@@ -3158,7 +3343,310 @@ function createUnifiedLoader(options) {
3158
3343
  return new UnifiedLoader(options);
3159
3344
  }
3160
3345
  createLogger("almadar:runtime:studio-config");
3346
+
3347
+ // src/ui/splice-lambda-traits.ts
3348
+ var TRAIT_BINDING_PREFIX = "@trait.";
3349
+ var CONFIG_BINDING_PREFIX = "@config.";
3350
+ var LambdaSpliceError = class extends Error {
3351
+ constructor(code, message) {
3352
+ super(message);
3353
+ this.code = code;
3354
+ this.name = "LambdaSpliceError";
3355
+ }
3356
+ code;
3357
+ };
3358
+ function isRenderUiEffect(effect) {
3359
+ return Array.isArray(effect) && effect.length >= 1 && effect[0] === "render-ui";
3360
+ }
3361
+ function renderOnlyTemplate(trait) {
3362
+ if (trait.listens && trait.listens.length > 0) return null;
3363
+ if (trait.ticks && trait.ticks.length > 0) return null;
3364
+ const sm = trait.stateMachine;
3365
+ const states = sm?.states ?? [];
3366
+ if (states.length > 1) return null;
3367
+ let template = null;
3368
+ for (const transition of sm?.transitions ?? []) {
3369
+ const selfLoop = transition.from === transition.to;
3370
+ const guarded = transition.guard !== void 0 && transition.guard !== null;
3371
+ if (!selfLoop || guarded) return null;
3372
+ for (const effect of transition.effects ?? []) {
3373
+ if (!isRenderUiEffect(effect) || !Array.isArray(effect)) return null;
3374
+ const pattern = effect[2];
3375
+ if (pattern === void 0) return null;
3376
+ if (template !== null) return null;
3377
+ template = pattern;
3378
+ }
3379
+ }
3380
+ return template;
3381
+ }
3382
+ function resolveObjectPath(value, path) {
3383
+ let cur = value;
3384
+ for (const seg of path.split(".")) {
3385
+ if (cur === null || typeof cur !== "object") return void 0;
3386
+ cur = cur[seg];
3387
+ if (cur === void 0) return void 0;
3388
+ }
3389
+ return cur;
3390
+ }
3391
+ function substituteConfig(expr, subs) {
3392
+ if (typeof expr === "string") {
3393
+ if (!expr.startsWith(CONFIG_BINDING_PREFIX)) return expr;
3394
+ const rest = expr.slice(CONFIG_BINDING_PREFIX.length);
3395
+ const dot = rest.indexOf(".");
3396
+ const param = dot === -1 ? rest : rest.slice(0, dot);
3397
+ const trailing = dot === -1 ? void 0 : rest.slice(dot + 1);
3398
+ if (!(param in subs)) return expr;
3399
+ const value = subs[param];
3400
+ if (trailing === void 0) {
3401
+ if (typeof value === "string" && value === expr) return expr;
3402
+ return substituteConfig(value, subs);
3403
+ }
3404
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
3405
+ const leaf = resolveObjectPath(value, trailing);
3406
+ if (leaf !== void 0) return substituteConfig(leaf, subs);
3407
+ }
3408
+ return expr;
3409
+ }
3410
+ if (Array.isArray(expr)) {
3411
+ return expr.map((item) => substituteConfig(item, subs));
3412
+ }
3413
+ if (expr !== null && typeof expr === "object") {
3414
+ const out = {};
3415
+ for (const [k, v] of Object.entries(expr)) {
3416
+ out[k] = substituteConfig(v, subs);
3417
+ }
3418
+ return out;
3419
+ }
3420
+ return expr;
3421
+ }
3422
+ function buildTemplate(trait) {
3423
+ const raw = renderOnlyTemplate(trait);
3424
+ const emits = trait.emits ?? [];
3425
+ if (raw === null) return { template: null, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3426
+ const defaults = collectDeclaredConfigDefaults(trait);
3427
+ const template = defaults ? substituteConfig(raw, defaults) : raw;
3428
+ return { template, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3429
+ }
3430
+ function isLambdaForm(expr) {
3431
+ return expr.length === 3 && (expr[0] === "fn" || expr[0] === "lambda");
3432
+ }
3433
+ function spliceExpr(expr, inLambda, st) {
3434
+ if (typeof expr === "string") {
3435
+ if (!inLambda || !expr.startsWith(TRAIT_BINDING_PREFIX)) return expr;
3436
+ const name = expr.slice(TRAIT_BINDING_PREFIX.length);
3437
+ if (name.length === 0 || name.includes(".")) return expr;
3438
+ const embedId = st.embedIds?.[name];
3439
+ const entry = (embedId !== void 0 ? st.templatesById.get(embedId) : void 0) ?? st.templates.get(name);
3440
+ if (entry === void 0) return expr;
3441
+ if (entry.template === null) {
3442
+ throw new LambdaSpliceError(
3443
+ "LAMBDA_STATEFUL_TRAIT",
3444
+ `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}")`
3445
+ );
3446
+ }
3447
+ if (st.visiting.includes(name)) return expr;
3448
+ st.visiting.push(name);
3449
+ const prevEmbedIds = st.embedIds;
3450
+ st.embedIds = entry.embedIds;
3451
+ const expanded = spliceExpr(entry.template, true, st);
3452
+ st.embedIds = prevEmbedIds;
3453
+ st.visiting.pop();
3454
+ for (const emit of entry.emits) st.merged.push([entry.traitName, emit]);
3455
+ st.spliced.add(entry.traitName);
3456
+ st.changed = true;
3457
+ return expanded;
3458
+ }
3459
+ if (Array.isArray(expr)) {
3460
+ if (isLambdaForm(expr)) {
3461
+ return [expr[0], expr[1], spliceExpr(expr[2], true, st)];
3462
+ }
3463
+ return expr.map((item) => spliceExpr(item, inLambda, st));
3464
+ }
3465
+ if (expr !== null && typeof expr === "object") {
3466
+ const out = {};
3467
+ for (const [k, v] of Object.entries(expr)) {
3468
+ out[k] = spliceExpr(v, inLambda, st);
3469
+ }
3470
+ return out;
3471
+ }
3472
+ return expr;
3473
+ }
3474
+ function canonical(value) {
3475
+ if (Array.isArray(value)) return value.map(canonical);
3476
+ if (value !== null && typeof value === "object") {
3477
+ const out = {};
3478
+ for (const key of Object.keys(value).sort()) {
3479
+ out[key] = canonical(value[key]);
3480
+ }
3481
+ return out;
3482
+ }
3483
+ return value;
3484
+ }
3485
+ function mergeWrapperEmits(host, incoming) {
3486
+ const emits = [...host.emits ?? []];
3487
+ for (const [wrapper, emit] of incoming) {
3488
+ const existing = emits.find((e) => e.event === emit.event);
3489
+ if (existing) {
3490
+ const same = JSON.stringify(canonical(existing)) === JSON.stringify(canonical(emit));
3491
+ if (!same) {
3492
+ throw new LambdaSpliceError(
3493
+ "EMIT_MERGE_CONFLICT",
3494
+ `Emit "${emit.event}" merged from spliced wrapper "${wrapper}" conflicts with an existing "${emit.event}" on host "${host.name}" (different payload shape)`
3495
+ );
3496
+ }
3497
+ } else {
3498
+ emits.push(emit);
3499
+ }
3500
+ }
3501
+ return emits;
3502
+ }
3503
+ function removeConsumedWrappers(traits, pages, spliced) {
3504
+ const idToName = /* @__PURE__ */ new Map();
3505
+ for (const { trait } of traits) {
3506
+ if (trait.id) idToName.set(trait.id, trait.name);
3507
+ }
3508
+ for (; ; ) {
3509
+ const stillReferenced = /* @__PURE__ */ new Set();
3510
+ for (const { trait } of traits) {
3511
+ const rawTokens = /* @__PURE__ */ new Set();
3512
+ for (const t of trait.stateMachine?.transitions ?? []) {
3513
+ if (t.guard !== void 0 && t.guard !== null) {
3514
+ collectTraitRefsFromValue(t.guard, rawTokens);
3515
+ }
3516
+ collectTraitRefsFromEffects(t.effects, rawTokens);
3517
+ }
3518
+ for (const tick of trait.ticks ?? []) {
3519
+ if (tick.guard !== void 0 && tick.guard !== null) {
3520
+ collectTraitRefsFromValue(tick.guard, rawTokens);
3521
+ }
3522
+ collectTraitRefsFromEffects(tick.effects, rawTokens);
3523
+ }
3524
+ if (trait.config) {
3525
+ for (const field of Object.values(trait.config)) {
3526
+ if (field && typeof field === "object" && "default" in field && field.default !== void 0) {
3527
+ collectTraitRefsFromValue(field.default, rawTokens);
3528
+ }
3529
+ }
3530
+ }
3531
+ for (const token of rawTokens) {
3532
+ const embedId = trait.traitEmbedIds?.[token];
3533
+ const resolvedName = (embedId !== void 0 ? idToName.get(embedId) : void 0) ?? token;
3534
+ stillReferenced.add(resolvedName);
3535
+ }
3536
+ }
3537
+ const removable = /* @__PURE__ */ new Set();
3538
+ for (const { trait } of traits) {
3539
+ if (spliced.has(trait.name) && !stillReferenced.has(trait.name)) {
3540
+ removable.add(trait.name);
3541
+ }
3542
+ }
3543
+ if (removable.size === 0) return;
3544
+ for (const { page } of pages) {
3545
+ const pageTraits = page.traits;
3546
+ if (!pageTraits) continue;
3547
+ const survivors2 = pageTraits.filter((pt) => !removable.has(pt.ref)).length;
3548
+ if (survivors2 === 0) {
3549
+ for (const pt of pageTraits) removable.delete(pt.ref);
3550
+ }
3551
+ }
3552
+ if (removable.size === 0) return;
3553
+ for (const resolvedPage of pages) {
3554
+ const pageTraits = resolvedPage.page.traits;
3555
+ if (pageTraits) {
3556
+ resolvedPage.page.traits = pageTraits.filter((pt) => !removable.has(pt.ref));
3557
+ }
3558
+ }
3559
+ const survivors = traits.filter(({ trait }) => !removable.has(trait.name));
3560
+ traits.length = 0;
3561
+ traits.push(...survivors);
3562
+ }
3563
+ }
3564
+ function spliceLambdaTraitRefs(traits, pages) {
3565
+ if (traits.length === 0) return;
3566
+ const templates = /* @__PURE__ */ new Map();
3567
+ const templatesById = /* @__PURE__ */ new Map();
3568
+ for (const { trait } of traits) {
3569
+ const entry = buildTemplate(trait);
3570
+ templates.set(trait.name, entry);
3571
+ if (trait.id) templatesById.set(trait.id, entry);
3572
+ }
3573
+ const spliced = /* @__PURE__ */ new Set();
3574
+ for (const resolved of traits) {
3575
+ const host = resolved.trait;
3576
+ const st = {
3577
+ templates,
3578
+ templatesById,
3579
+ embedIds: host.traitEmbedIds,
3580
+ // Seed with the host itself so a self-reference never self-splices.
3581
+ visiting: [host.name],
3582
+ merged: [],
3583
+ spliced,
3584
+ hostName: host.name,
3585
+ changed: false
3586
+ };
3587
+ const sm = host.stateMachine;
3588
+ const nextTransitions = sm?.transitions ? sm.transitions.map((t) => ({
3589
+ ...t,
3590
+ effects: t.effects ? t.effects.map((e) => spliceExpr(e, false, st)) : t.effects
3591
+ })) : sm?.transitions;
3592
+ const nextTicks = host.ticks ? host.ticks.map((tick) => ({
3593
+ ...tick,
3594
+ effects: tick.effects.map((e) => spliceExpr(e, false, st))
3595
+ })) : host.ticks;
3596
+ if (!st.changed) {
3597
+ continue;
3598
+ }
3599
+ const nextEmits = st.merged.length > 0 ? mergeWrapperEmits(host, st.merged) : host.emits;
3600
+ resolved.trait = {
3601
+ ...host,
3602
+ ...sm ? { stateMachine: { ...sm, transitions: nextTransitions ?? [] } } : {},
3603
+ ...nextTicks !== void 0 ? { ticks: nextTicks } : {},
3604
+ ...nextEmits !== void 0 ? { emits: nextEmits } : {}
3605
+ };
3606
+ }
3607
+ if (spliced.size > 0) {
3608
+ removeConsumedWrappers(traits, pages, spliced);
3609
+ }
3610
+ }
3611
+
3612
+ // src/resolver/reference-resolver.ts
3161
3613
  var refResolverLog = createLogger("almadar:runtime:ref-resolver");
3614
+ function indexOrbitalNodes(orbital, idIndex) {
3615
+ for (const traitRef of orbital.traits ?? []) {
3616
+ if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
3617
+ const trait = traitRef;
3618
+ if (trait.id) {
3619
+ idIndex.set(trait.id, { kind: "trait", node: trait });
3620
+ }
3621
+ for (const ev of trait.stateMachine?.events ?? []) {
3622
+ if (ev.id) {
3623
+ idIndex.set(ev.id, { kind: "event", node: ev });
3624
+ }
3625
+ }
3626
+ }
3627
+ }
3628
+ const entityRef = orbital.entity;
3629
+ if (entityRef && typeof entityRef !== "string" && !("extends" in entityRef) && entityRef.id) {
3630
+ const entity = entityRef;
3631
+ idIndex.set(entity.id, { kind: "entity", node: entity });
3632
+ }
3633
+ for (const pageRef of orbital.pages ?? []) {
3634
+ if (typeof pageRef !== "string" && !("ref" in pageRef)) {
3635
+ const page = pageRef;
3636
+ if (page.id) {
3637
+ idIndex.set(page.id, { kind: "page", node: page });
3638
+ }
3639
+ }
3640
+ }
3641
+ }
3642
+ function buildIdIndex(orbital, orbitals) {
3643
+ const idIndex = /* @__PURE__ */ new Map();
3644
+ indexOrbitalNodes(orbital, idIndex);
3645
+ for (const imported of orbitals.values()) {
3646
+ indexOrbitalNodes(imported.orbital, idIndex);
3647
+ }
3648
+ return idIndex;
3649
+ }
3162
3650
  function renameEventsInRenderUiConfig(node, rename) {
3163
3651
  if (node === null || node === void 0) return node;
3164
3652
  if (Array.isArray(node)) {
@@ -3208,25 +3696,30 @@ function renameEventsInEffects(effects, rename) {
3208
3696
  return effect;
3209
3697
  });
3210
3698
  }
3211
- function renameEntityInRenderUiConfig(node, oldName, newName) {
3699
+ var REBIND_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity"]);
3700
+ var ID_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity", "entityType", "source"]);
3701
+ function renameEntityInRenderUiConfig(node, rename, props) {
3212
3702
  if (node === null || node === void 0) return node;
3213
3703
  if (Array.isArray(node)) {
3214
- return node.map((item) => renameEntityInRenderUiConfig(item, oldName, newName));
3704
+ return node.map((item) => renameEntityInRenderUiConfig(item, rename, props));
3215
3705
  }
3216
3706
  if (typeof node !== "object") return node;
3217
3707
  const obj = node;
3218
3708
  const next = { ...obj };
3219
3709
  for (const [key, value] of Object.entries(obj)) {
3220
- if (key === "entity" && value === oldName) {
3221
- next[key] = newName;
3222
- continue;
3710
+ if (props.has(key) && typeof value === "string") {
3711
+ const replaced = rename(value);
3712
+ if (replaced !== void 0) {
3713
+ next[key] = replaced;
3714
+ continue;
3715
+ }
3223
3716
  }
3224
- next[key] = renameEntityInRenderUiConfig(value, oldName, newName);
3717
+ next[key] = renameEntityInRenderUiConfig(value, rename, props);
3225
3718
  }
3226
3719
  return next;
3227
3720
  }
3228
- function renameEntityInEffects(effects, oldName, newName) {
3229
- return effects.map((effect) => renameEntityInEffect(effect, oldName, newName));
3721
+ function renameEntityInEffects(effects, rename, props) {
3722
+ return effects.map((effect) => renameEntityInEffect(effect, rename, props));
3230
3723
  }
3231
3724
  var ENTITY_AT_POS_1 = /* @__PURE__ */ new Set(["fetch", "ref", "deref", "spawn"]);
3232
3725
  var ALL_ARGS_ARE_EFFECTS = /* @__PURE__ */ new Set([
@@ -3245,20 +3738,22 @@ var ARGS_FROM_POS_2_ARE_EFFECTS = /* @__PURE__ */ new Set([
3245
3738
  "async/throttle",
3246
3739
  "async/interval"
3247
3740
  ]);
3248
- function renameEntityInEffect(effect, oldName, newName) {
3741
+ function renameEntityInEffect(effect, rename, props) {
3249
3742
  if (!Array.isArray(effect) || effect.length === 0) return effect;
3250
3743
  const op = effect[0];
3251
3744
  if (typeof op !== "string") return effect;
3252
3745
  if (op === "render-ui" && effect.length >= 3) {
3253
3746
  const [, slot, config, ...rest] = effect;
3254
- const nextConfig = renameEntityInRenderUiConfig(config, oldName, newName);
3747
+ const nextConfig = renameEntityInRenderUiConfig(config, rename, props);
3255
3748
  return [op, slot, nextConfig, ...rest];
3256
3749
  }
3257
- if (op === "persist" && effect.length >= 3 && effect[2] === oldName) {
3258
- return [op, effect[1], newName, ...effect.slice(3)];
3750
+ if (op === "persist" && effect.length >= 3 && typeof effect[2] === "string") {
3751
+ const replaced = rename(effect[2]);
3752
+ if (replaced !== void 0) return [op, effect[1], replaced, ...effect.slice(3)];
3259
3753
  }
3260
- if (ENTITY_AT_POS_1.has(op) && effect[1] === oldName) {
3261
- return [op, newName, ...effect.slice(2)];
3754
+ if (ENTITY_AT_POS_1.has(op) && typeof effect[1] === "string") {
3755
+ const replaced = rename(effect[1]);
3756
+ if (replaced !== void 0) return [op, replaced, ...effect.slice(2)];
3262
3757
  }
3263
3758
  const skipFirstNonEffectArg = ARGS_FROM_POS_2_ARE_EFFECTS.has(op);
3264
3759
  const recurseAll = ALL_ARGS_ARE_EFFECTS.has(op);
@@ -3267,7 +3762,7 @@ function renameEntityInEffect(effect, oldName, newName) {
3267
3762
  return effect.map((arg, i) => {
3268
3763
  if (i < startIndex) return arg;
3269
3764
  if (Array.isArray(arg)) {
3270
- return renameEntityInEffect(arg, oldName, newName);
3765
+ return renameEntityInEffect(arg, rename, props);
3271
3766
  }
3272
3767
  return arg;
3273
3768
  });
@@ -3279,11 +3774,12 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3279
3774
  if (!linkedEntity || !atomLinked || linkedEntity === atomLinked) return trait;
3280
3775
  const sm = trait.stateMachine;
3281
3776
  if (!sm) return { ...trait, linkedEntity };
3777
+ const rename = (name) => name === atomLinked ? linkedEntity : void 0;
3282
3778
  const nextTransitions = (sm.transitions ?? []).map((t) => {
3283
3779
  const nextEffects = t.effects ? renameEntityInEffects(
3284
3780
  t.effects,
3285
- atomLinked,
3286
- linkedEntity
3781
+ rename,
3782
+ REBIND_ENTITY_PROPS
3287
3783
  ) : t.effects;
3288
3784
  return { ...t, effects: nextEffects };
3289
3785
  });
@@ -3299,6 +3795,85 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3299
3795
  stateMachine: { ...sm, transitions: nextTransitions }
3300
3796
  };
3301
3797
  }
3798
+ function resolveEntityTokensById(trait, idIndex) {
3799
+ const map = trait.entityRefIds;
3800
+ if (!map) return trait;
3801
+ const rewrites = /* @__PURE__ */ new Map();
3802
+ for (const [tokenName, entityId] of Object.entries(map)) {
3803
+ const entry = idIndex.get(entityId);
3804
+ if (entry && entry.kind === "entity") {
3805
+ const currentName = entry.node.name;
3806
+ if (currentName && currentName !== tokenName) {
3807
+ rewrites.set(tokenName, currentName);
3808
+ }
3809
+ }
3810
+ }
3811
+ if (rewrites.size === 0) return trait;
3812
+ const rename = (name) => rewrites.get(name);
3813
+ const sm = trait.stateMachine;
3814
+ const nextTransitions = sm?.transitions ? sm.transitions.map((t) => ({
3815
+ ...t,
3816
+ effects: t.effects ? renameEntityInEffects(
3817
+ t.effects,
3818
+ rename,
3819
+ ID_ENTITY_PROPS
3820
+ ) : t.effects
3821
+ })) : sm?.transitions;
3822
+ const nextTicks = trait.ticks ? trait.ticks.map((tick) => ({
3823
+ ...tick,
3824
+ effects: renameEntityInEffects(
3825
+ tick.effects,
3826
+ rename,
3827
+ ID_ENTITY_PROPS
3828
+ )
3829
+ })) : trait.ticks;
3830
+ const nextInitial = trait.initialEffects ? renameEntityInEffects(
3831
+ trait.initialEffects,
3832
+ rename,
3833
+ ID_ENTITY_PROPS
3834
+ ) : trait.initialEffects;
3835
+ const nextLinked = trait.linkedEntity !== void 0 ? rewrites.get(trait.linkedEntity) ?? trait.linkedEntity : trait.linkedEntity;
3836
+ refResolverLog.info("entity-ref:id-resolve", {
3837
+ trait: trait.name,
3838
+ rewrites: Object.fromEntries(rewrites)
3839
+ });
3840
+ return {
3841
+ ...trait,
3842
+ linkedEntity: nextLinked,
3843
+ ...sm ? { stateMachine: { ...sm, transitions: nextTransitions ?? [] } } : {},
3844
+ ...nextTicks !== void 0 ? { ticks: nextTicks } : {},
3845
+ ...nextInitial !== void 0 ? { initialEffects: nextInitial } : {}
3846
+ };
3847
+ }
3848
+ var REFERENCE_CONFIG_TYPE_TO_ID_KIND = {
3849
+ entity: "entity",
3850
+ trait: "trait",
3851
+ event: "event"
3852
+ };
3853
+ function resolveConfigRefsById(trait, idIndex) {
3854
+ const schema = trait.config;
3855
+ if (!schema) return trait;
3856
+ let nextSchema;
3857
+ const rewrites = [];
3858
+ for (const [key, field] of Object.entries(schema)) {
3859
+ if (!field.refId || !isReferenceConfigType(field.type)) continue;
3860
+ const expectedKind = REFERENCE_CONFIG_TYPE_TO_ID_KIND[field.type];
3861
+ if (!expectedKind) continue;
3862
+ const entry = idIndex.get(field.refId);
3863
+ if (!entry || entry.kind !== expectedKind) continue;
3864
+ const currentName = entry.kind === "entity" ? entry.node.name : entry.kind === "event" ? entry.node.key : entry.node.name;
3865
+ if (!currentName || currentName === field.default) continue;
3866
+ nextSchema ??= { ...schema };
3867
+ nextSchema[key] = { ...field, default: currentName };
3868
+ rewrites.push({ key, from: field.default, to: currentName });
3869
+ }
3870
+ if (!nextSchema) return trait;
3871
+ refResolverLog.info("config-ref:id-resolve", {
3872
+ trait: trait.name,
3873
+ rewrites
3874
+ });
3875
+ return { ...trait, config: nextSchema };
3876
+ }
3302
3877
  function applyEventRenames(trait, renames) {
3303
3878
  if (!renames || Object.keys(renames).length === 0) return trait;
3304
3879
  const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
@@ -3329,21 +3904,55 @@ function applyEventRenames(trait, renames) {
3329
3904
  emits: nextEmits
3330
3905
  };
3331
3906
  }
3907
+ function resolveConfigRefEmitNames(trait, callSiteConfig) {
3908
+ const emits = trait.emits ?? [];
3909
+ const hasRef = emits.some((em) => configRefEventKnob(em.event) !== void 0);
3910
+ if (!hasRef) return { trait, errors: [] };
3911
+ const effectiveConfig = {
3912
+ ...normalizeCallSiteConfigToValues(trait.config) ?? {},
3913
+ ...normalizeCallSiteConfigToValues(callSiteConfig) ?? {}
3914
+ };
3915
+ const errors = [];
3916
+ const nextEmits = emits.map((em) => {
3917
+ if (configRefEventKnob(em.event) === void 0) return em;
3918
+ const result = resolveConfigRefEventName(em.event, trait.config, effectiveConfig);
3919
+ if (!result.ok) {
3920
+ errors.push(
3921
+ `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.`
3922
+ );
3923
+ return em;
3924
+ }
3925
+ refResolverLog.debug("emit-config-ref:resolved", {
3926
+ trait: trait.name,
3927
+ ref: em.event,
3928
+ resolved: result.value
3929
+ });
3930
+ return { ...em, event: result.value };
3931
+ });
3932
+ return { trait: { ...trait, emits: nextEmits }, errors };
3933
+ }
3332
3934
  var ReferenceResolver = class {
3333
3935
  loader;
3334
3936
  options;
3335
3937
  localTraits;
3938
+ /** id-keyed mirror of `localTraits`, populated wherever the trait carries an `id`. */
3939
+ localTraitsById = /* @__PURE__ */ new Map();
3336
3940
  loaderInitialized = false;
3337
3941
  constructor(options) {
3338
3942
  this.options = options;
3339
3943
  this.loader = options.loader;
3340
3944
  this.localTraits = options.localTraits ?? /* @__PURE__ */ new Map();
3945
+ for (const trait of this.localTraits.values()) {
3946
+ if (trait.id) {
3947
+ this.localTraitsById.set(trait.id, trait);
3948
+ }
3949
+ }
3341
3950
  }
3342
3951
  async ensureLoader() {
3343
3952
  if (this.loader || this.loaderInitialized) return;
3344
3953
  this.loaderInitialized = true;
3345
3954
  try {
3346
- const { ExternalOrbitalLoader } = await import('./external-loader-OPXVTNC4.js');
3955
+ const { ExternalOrbitalLoader } = await import('./external-loader-FNK5AU6U.js');
3347
3956
  this.loader = new ExternalOrbitalLoader(this.options);
3348
3957
  } catch {
3349
3958
  }
@@ -3360,11 +3969,12 @@ var ReferenceResolver = class {
3360
3969
  } };
3361
3970
  const traitsList = orbital.traits ?? [];
3362
3971
  const alreadyResolved = traitsList.length > 0 && traitsList.every((t) => isInlineTrait(t));
3363
- const importsResult = alreadyResolved ? { success: true, data: { orbitals: /* @__PURE__ */ new Map() }} : await this.resolveImports(orbital.uses ?? [], sourcePath, importChain);
3972
+ const importsResult = alreadyResolved ? { success: true, data: { orbitals: /* @__PURE__ */ new Map(), idIndex: /* @__PURE__ */ new Map() }} : await this.resolveImports(orbital.uses ?? [], sourcePath, importChain);
3364
3973
  if (!importsResult.success) {
3365
3974
  return { success: false, errors: importsResult.errors };
3366
3975
  }
3367
3976
  const imports = importsResult.data;
3977
+ imports.idIndex = buildIdIndex(orbital, imports.orbitals);
3368
3978
  const entityResult = this.resolveEntity(orbital.entity, imports);
3369
3979
  if (!entityResult.success) {
3370
3980
  errors.push(...entityResult.errors);
@@ -3383,6 +3993,18 @@ var ReferenceResolver = class {
3383
3993
  if (!entityResult.success || !traitsResult.success || !pagesResult.success) {
3384
3994
  return { success: false, errors: ["Internal error: unexpected failure state"] };
3385
3995
  }
3996
+ for (const resolvedTrait of traitsResult.data) {
3997
+ resolvedTrait.trait = resolveEntityTokensById(resolvedTrait.trait, imports.idIndex);
3998
+ resolvedTrait.trait = resolveConfigRefsById(resolvedTrait.trait, imports.idIndex);
3999
+ }
4000
+ try {
4001
+ spliceLambdaTraitRefs(traitsResult.data, pagesResult.data);
4002
+ } catch (e) {
4003
+ if (e instanceof LambdaSpliceError) {
4004
+ return { success: false, errors: [e.message] };
4005
+ }
4006
+ throw e;
4007
+ }
3386
4008
  return {
3387
4009
  success: true,
3388
4010
  data: {
@@ -3406,7 +4028,7 @@ var ReferenceResolver = class {
3406
4028
  if (this.options.skipExternalLoading) {
3407
4029
  return {
3408
4030
  success: true,
3409
- data: { orbitals },
4031
+ data: { orbitals, idIndex: /* @__PURE__ */ new Map() },
3410
4032
  warnings: ["External loading skipped"]
3411
4033
  };
3412
4034
  }
@@ -3440,7 +4062,7 @@ var ReferenceResolver = class {
3440
4062
  if (errors.length > 0) {
3441
4063
  return { success: false, errors };
3442
4064
  }
3443
- return { success: true, data: { orbitals }, warnings: [] };
4065
+ return { success: true, data: { orbitals, idIndex: /* @__PURE__ */ new Map() }, warnings: [] };
3444
4066
  }
3445
4067
  /**
3446
4068
  * Resolve entity reference.
@@ -3549,10 +4171,14 @@ var ReferenceResolver = class {
3549
4171
  */
3550
4172
  resolveTraitRef(traitRef, imports) {
3551
4173
  if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
4174
+ const { trait: resolvedInline, errors } = resolveConfigRefEmitNames(traitRef);
4175
+ if (errors.length > 0) {
4176
+ return { success: false, errors };
4177
+ }
3552
4178
  return {
3553
4179
  success: true,
3554
4180
  data: {
3555
- trait: traitRef,
4181
+ trait: resolvedInline,
3556
4182
  source: { type: "inline" }
3557
4183
  },
3558
4184
  warnings: []
@@ -3567,7 +4193,8 @@ var ReferenceResolver = class {
3567
4193
  refObj.linkedEntity,
3568
4194
  refObj.name,
3569
4195
  refObj.events,
3570
- refObj.listens
4196
+ refObj.listens,
4197
+ refObj.refId
3571
4198
  );
3572
4199
  }
3573
4200
  if (typeof traitRef === "string") {
@@ -3581,7 +4208,7 @@ var ReferenceResolver = class {
3581
4208
  /**
3582
4209
  * Resolve a trait reference string.
3583
4210
  */
3584
- resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride) {
4211
+ resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride, refId) {
3585
4212
  const parsed = parseImportedTraitRef(ref);
3586
4213
  if (parsed) {
3587
4214
  const imported = imports.orbitals.get(parsed.alias);
@@ -3593,7 +4220,7 @@ var ReferenceResolver = class {
3593
4220
  ]
3594
4221
  };
3595
4222
  }
3596
- const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName);
4223
+ const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName, refId, imports.idIndex);
3597
4224
  if (!trait) {
3598
4225
  return {
3599
4226
  success: false,
@@ -3603,7 +4230,11 @@ var ReferenceResolver = class {
3603
4230
  };
3604
4231
  }
3605
4232
  const baseTrait = overrideName ? { ...trait, name: overrideName } : trait;
3606
- const reboundTrait = applyLinkedEntityRename(baseTrait, linkedEntity);
4233
+ const { trait: configResolvedTrait, errors: configRefErrors } = resolveConfigRefEmitNames(baseTrait, config);
4234
+ if (configRefErrors.length > 0) {
4235
+ return { success: false, errors: configRefErrors };
4236
+ }
4237
+ const reboundTrait = applyLinkedEntityRename(configResolvedTrait, linkedEntity);
3607
4238
  const renamedTrait = applyEventRenames(reboundTrait, eventRenames);
3608
4239
  const finalTrait = listensOverride !== void 0 ? { ...renamedTrait, listens: listensOverride } : renamedTrait;
3609
4240
  if (listensOverride !== void 0) {
@@ -3625,10 +4256,14 @@ var ReferenceResolver = class {
3625
4256
  warnings: []
3626
4257
  };
3627
4258
  }
3628
- const localTrait = this.localTraits.get(ref);
4259
+ const localTrait = (refId && this.localTraitsById.get(refId)) ?? this.localTraits.get(ref);
3629
4260
  if (localTrait) {
3630
4261
  const baseLocal = overrideName ? { ...localTrait, name: overrideName } : localTrait;
3631
- const reboundLocal = applyLinkedEntityRename(baseLocal, linkedEntity);
4262
+ const { trait: configResolvedLocal, errors: localConfigRefErrors } = resolveConfigRefEmitNames(baseLocal, config);
4263
+ if (localConfigRefErrors.length > 0) {
4264
+ return { success: false, errors: localConfigRefErrors };
4265
+ }
4266
+ const reboundLocal = applyLinkedEntityRename(configResolvedLocal, linkedEntity);
3632
4267
  const renamedLocalTrait = applyEventRenames(reboundLocal, eventRenames);
3633
4268
  const finalLocalTrait = listensOverride !== void 0 ? { ...renamedLocalTrait, listens: listensOverride } : renamedLocalTrait;
3634
4269
  if (listensOverride !== void 0) {
@@ -3658,9 +4293,17 @@ var ReferenceResolver = class {
3658
4293
  };
3659
4294
  }
3660
4295
  /**
3661
- * Find a trait in an orbital by name.
4296
+ * Find a trait in an orbital by name. Id-primary: when the calling ref
4297
+ * carries a `refId` and the id index has a matching trait entry, return it
4298
+ * directly — else fall back to the existing name match unchanged.
3662
4299
  */
3663
- findTraitInOrbital(orbital, traitName) {
4300
+ findTraitInOrbital(orbital, traitName, refId, idIndex) {
4301
+ if (refId && idIndex) {
4302
+ const entry = idIndex.get(refId);
4303
+ if (entry && entry.kind === "trait") {
4304
+ return entry.node;
4305
+ }
4306
+ }
3664
4307
  for (const traitRef of orbital.traits) {
3665
4308
  if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
3666
4309
  if (traitRef.name === traitName) {
@@ -3734,7 +4377,7 @@ var ReferenceResolver = class {
3734
4377
  /**
3735
4378
  * Resolve a page reference string.
3736
4379
  */
3737
- resolvePageRefString(ref, imports) {
4380
+ resolvePageRefString(ref, imports, refId) {
3738
4381
  const parsed = parsePageRef(ref);
3739
4382
  if (!parsed) {
3740
4383
  return {
@@ -3751,7 +4394,7 @@ var ReferenceResolver = class {
3751
4394
  ]
3752
4395
  };
3753
4396
  }
3754
- const page = this.findPageInOrbital(imported.orbital, parsed.pageName);
4397
+ const page = this.findPageInOrbital(imported.orbital, parsed.pageName, refId, imports.idIndex);
3755
4398
  if (!page) {
3756
4399
  return {
3757
4400
  success: false,
@@ -3774,7 +4417,7 @@ var ReferenceResolver = class {
3774
4417
  * Resolve a page reference object with optional path override.
3775
4418
  */
3776
4419
  resolvePageRefObject(refObj, imports) {
3777
- const baseResult = this.resolvePageRefString(refObj.ref, imports);
4420
+ const baseResult = this.resolvePageRefString(refObj.ref, imports, refObj.refId);
3778
4421
  if (!baseResult.success) {
3779
4422
  return baseResult;
3780
4423
  }
@@ -3795,9 +4438,17 @@ var ReferenceResolver = class {
3795
4438
  };
3796
4439
  }
3797
4440
  /**
3798
- * Find a page in an orbital by name.
4441
+ * Find a page in an orbital by name. Id-primary: when the calling ref
4442
+ * carries a `refId` and the id index has a matching page entry, return it
4443
+ * directly — else fall back to the existing name match unchanged.
3799
4444
  */
3800
- findPageInOrbital(orbital, pageName) {
4445
+ findPageInOrbital(orbital, pageName, refId, idIndex) {
4446
+ if (refId && idIndex) {
4447
+ const entry = idIndex.get(refId);
4448
+ if (entry && entry.kind === "page") {
4449
+ return { ...entry.node };
4450
+ }
4451
+ }
3801
4452
  const pages = orbital.pages;
3802
4453
  if (!pages) return null;
3803
4454
  for (const pageRef of pages) {
@@ -3830,6 +4481,9 @@ var ReferenceResolver = class {
3830
4481
  addLocalTraits(traits) {
3831
4482
  for (const trait of traits) {
3832
4483
  this.localTraits.set(trait.name, trait);
4484
+ if (trait.id) {
4485
+ this.localTraitsById.set(trait.id, trait);
4486
+ }
3833
4487
  }
3834
4488
  }
3835
4489
  /**
@@ -4052,4 +4706,4 @@ var InMemoryPersistence = class {
4052
4706
  }
4053
4707
  };
4054
4708
 
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 };
4709
+ 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 };