@almadar/runtime 6.33.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.
@@ -5,7 +5,7 @@ import { createLogger, setNamespaceLevel } from '@almadar/logger';
5
5
  import { createMinimalContext, resolveBinding, evaluate, evaluateGuard, SExpressionEvaluator } from '@almadar/evaluator';
6
6
  export { createMinimalContext } from '@almadar/evaluator';
7
7
  import { isKnownStdOperator } from '@almadar/std/registry';
8
- import { OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef, configRefEventKnob, normalizeCallSiteConfigToValues, resolveConfigRefEventName } from '@almadar/core';
8
+ import { OrbitalSchemaSchema, isInlineTrait, isEntityCall, isEntityReference, parseEntityRef, parseImportedTraitRef, isPageReference, isPageReferenceString, isPageReferenceObject, parsePageRef, isReferenceConfigType, configRefEventKnob, normalizeCallSiteConfigToValues, resolveConfigRefEventName } from '@almadar/core';
9
9
  export { normalizeCallSiteConfigToValues } from '@almadar/core';
10
10
 
11
11
  var log = createLogger("almadar:runtime:eventbus");
@@ -26,7 +26,7 @@ var EventBus = class {
26
26
  * beyond `maxDepth`, the event is dropped and an error is logged.
27
27
  * This prevents infinite loops from circular emit/listen chains.
28
28
  */
29
- emit(type, payload, source) {
29
+ emit(type, payload, source, routingKey) {
30
30
  if (this.depth >= this.maxDepth) {
31
31
  log.error("circular event loop dropped", { type, depth: this.depth, maxDepth: this.maxDepth });
32
32
  return;
@@ -37,12 +37,13 @@ var EventBus = class {
37
37
  timestamp: Date.now(),
38
38
  source
39
39
  };
40
- const listeners = this.listeners.get(type);
40
+ const deliveryKey = routingKey ?? type;
41
+ const listeners = this.listeners.get(deliveryKey);
41
42
  const listenerCount = listeners?.size ?? 0;
42
43
  if (listenerCount > 0) {
43
44
  log.debug("emit", { type, listenerCount, depth: this.depth });
44
45
  } else {
45
- log.warn("emit no listeners", { type });
46
+ log.debug("emit no listeners", { type });
46
47
  }
47
48
  this.depth++;
48
49
  try {
@@ -394,6 +395,7 @@ function interpolateValue(value, ctx) {
394
395
  }
395
396
  return value;
396
397
  }
398
+ var inFlightConfigRecursions = /* @__PURE__ */ new Set();
397
399
  function interpolateString(value, ctx) {
398
400
  if (value.startsWith("@") && isPureBinding(value)) {
399
401
  if (isClientOnlyBinding(value)) {
@@ -402,6 +404,15 @@ function interpolateString(value, ctx) {
402
404
  }
403
405
  const resolved = resolveBinding(value, ctx);
404
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
+ }
405
416
  return resolved;
406
417
  }
407
418
  if (value.includes("@")) {
@@ -443,12 +454,25 @@ function interpolateArray(value, ctx) {
443
454
  let anyChanged = false;
444
455
  for (let i = 0; i < value.length; i++) {
445
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
+ }
446
465
  const interpolated = interpolateValue(item, ctx);
447
466
  mapped.push(interpolated);
448
467
  if (interpolated !== item) anyChanged = true;
449
468
  }
450
469
  return anyChanged ? mapped : value;
451
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
+ }
452
476
  function isSExpression(value) {
453
477
  if (value.length === 0) return false;
454
478
  const first = value[0];
@@ -526,19 +550,21 @@ function createInitialTraitState(trait) {
526
550
  context: {}
527
551
  };
528
552
  }
529
- function findMatchingTransitions(trait, currentState, eventKey) {
553
+ function findMatchingTransitions(trait, currentState, eventKey, eventId) {
530
554
  if (!trait.transitions || trait.transitions.length === 0) {
531
555
  return [];
532
556
  }
533
557
  return trait.transitions.filter((t) => {
534
- if (Array.isArray(t.from)) {
535
- 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;
536
562
  }
537
- return t.from === currentState && t.event === eventKey;
563
+ return t.event === eventKey;
538
564
  });
539
565
  }
540
- function findTransition(trait, currentState, eventKey) {
541
- return findMatchingTransitions(trait, currentState, eventKey)[0];
566
+ function findTransition(trait, currentState, eventKey, eventId) {
567
+ return findMatchingTransitions(trait, currentState, eventKey, eventId)[0];
542
568
  }
543
569
  function normalizeEventKey(eventKey) {
544
570
  if (!eventKey) return "";
@@ -549,6 +575,7 @@ function processEvent(options) {
549
575
  traitState,
550
576
  trait,
551
577
  eventKey,
578
+ eventId,
552
579
  payload,
553
580
  entityData,
554
581
  config,
@@ -557,7 +584,7 @@ function processEvent(options) {
557
584
  contextExtensions
558
585
  } = options;
559
586
  const normalizedEvent = normalizeEventKey(eventKey);
560
- const candidates = findMatchingTransitions(trait, traitState.currentState, normalizedEvent);
587
+ const candidates = findMatchingTransitions(trait, traitState.currentState, normalizedEvent, eventId);
561
588
  if (candidates.length === 0) {
562
589
  smLog.debug("noTransition", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState });
563
590
  return {
@@ -676,6 +703,15 @@ function compositeKey(traitName, scope) {
676
703
  }
677
704
  var StateMachineManager = class {
678
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();
679
715
  /**
680
716
  * Per-trait call-site config, surfaced to guard expressions so
681
717
  * `@config.X` resolves at runtime. Populated by the orbital's
@@ -726,6 +762,69 @@ var StateMachineManager = class {
726
762
  */
727
763
  addTrait(trait) {
728
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
+ }
729
828
  }
730
829
  /**
731
830
  * Bind the call-site config for a trait so guard `@config.X`
@@ -744,6 +843,10 @@ var StateMachineManager = class {
744
843
  * Remove a trait from the manager.
745
844
  */
746
845
  removeTrait(traitName) {
846
+ const removed = this.traits.get(traitName);
847
+ if (removed?.id !== void 0) {
848
+ this.traitIdToName.delete(removed.id);
849
+ }
747
850
  this.traits.delete(traitName);
748
851
  const prefix = `${traitName}::`;
749
852
  for (const key of [...this.states.keys()]) {
@@ -831,11 +934,11 @@ var StateMachineManager = class {
831
934
  /**
832
935
  * Check if a trait can handle an event from its current state.
833
936
  */
834
- canHandleEvent(traitName, eventKey, entityId) {
937
+ canHandleEvent(traitName, eventKey, entityId, eventId) {
835
938
  const trait = this.traits.get(traitName);
836
939
  const state = this.getOrInitState(traitName, entityId ?? SINGLETON_SCOPE);
837
940
  if (!trait || !state) return false;
838
- return !!findTransition(trait, state.currentState, normalizeEventKey(eventKey));
941
+ return !!findTransition(trait, state.currentState, normalizeEventKey(eventKey), eventId);
839
942
  }
840
943
  /**
841
944
  * Send an event to all traits.
@@ -847,9 +950,13 @@ var StateMachineManager = class {
847
950
  * `@entity.X` see prior step writes — required for [runtime] entities
848
951
  * that have no persistence row to reload.
849
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
+ *
850
957
  * @returns Array of transition results (one per trait that had a matching transition)
851
958
  */
852
- sendEvent(eventKey, payload, entityData, entityByTrait) {
959
+ sendEvent(eventKey, payload, entityData, entityByTrait, eventId) {
853
960
  const results = [];
854
961
  const scope = scopeOf(entityData);
855
962
  for (const [traitName, trait] of this.traits) {
@@ -861,6 +968,7 @@ var StateMachineManager = class {
861
968
  traitState,
862
969
  trait,
863
970
  eventKey,
971
+ eventId,
864
972
  payload,
865
973
  entityData: perTraitEntity,
866
974
  config: this.traitConfigs.get(traitName),
@@ -902,12 +1010,12 @@ var StateMachineManager = class {
902
1010
  * trait to process them sequentially (actor-model guarantee: one event
903
1011
  * at a time per trait, effects fully awaited before the next event).
904
1012
  */
905
- enqueueEvent(eventKey, payload, entityData, entityByTrait) {
1013
+ enqueueEvent(eventKey, payload, entityData, entityByTrait, eventId) {
906
1014
  const scope = scopeOf(entityData);
907
1015
  for (const [traitName] of this.traits) {
908
1016
  const key = compositeKey(traitName, scope);
909
1017
  const queue = this.queues.get(key) ?? [];
910
- queue.push({ eventKey, payload, entityData, entityByTrait });
1018
+ queue.push({ eventKey, eventId, payload, entityData, entityByTrait });
911
1019
  this.queues.set(key, queue);
912
1020
  }
913
1021
  }
@@ -932,6 +1040,7 @@ var StateMachineManager = class {
932
1040
  traitState,
933
1041
  trait,
934
1042
  eventKey: entry.eventKey,
1043
+ eventId: entry.eventId,
935
1044
  payload: entry.payload,
936
1045
  entityData: perTraitEntity,
937
1046
  config: this.traitConfigs.get(traitName),
@@ -2212,6 +2321,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2212
2321
  stores = /* @__PURE__ */ new Map();
2213
2322
  schemas = /* @__PURE__ */ new Map();
2214
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();
2215
2326
  config;
2216
2327
  constructor(config = {}) {
2217
2328
  this.config = {
@@ -2263,6 +2374,9 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2263
2374
  registerEntity(schema, seedCount) {
2264
2375
  const normalized = schema.name.toLowerCase();
2265
2376
  this.schemas.set(normalized, schema);
2377
+ if (schema.id) {
2378
+ this.storeNameById.set(schema.id, normalized);
2379
+ }
2266
2380
  if (schema.seedData && schema.seedData.length > 0) {
2267
2381
  this.seedFromInstances(schema.name, schema.seedData);
2268
2382
  } else {
@@ -2294,7 +2408,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2294
2408
  if (relationFields.length === 0) continue;
2295
2409
  for (const row of store.values()) {
2296
2410
  for (const field of relationFields) {
2297
- 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);
2298
2413
  if (!targetStore || targetStore.size === 0) continue;
2299
2414
  const selfId = row["id"];
2300
2415
  const sameStore = targetStore === store;
@@ -2972,7 +3087,7 @@ async function getExternalLoaderModule() {
2972
3087
  return null;
2973
3088
  }
2974
3089
  try {
2975
- externalLoaderModule = await import('./external-loader-OPXVTNC4.js');
3090
+ externalLoaderModule = await import('./external-loader-FNK5AU6U.js');
2976
3091
  return externalLoaderModule;
2977
3092
  } catch {
2978
3093
  return null;
@@ -3307,10 +3422,10 @@ function substituteConfig(expr, subs) {
3307
3422
  function buildTemplate(trait) {
3308
3423
  const raw = renderOnlyTemplate(trait);
3309
3424
  const emits = trait.emits ?? [];
3310
- if (raw === null) return { template: null, emits };
3425
+ if (raw === null) return { template: null, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3311
3426
  const defaults = collectDeclaredConfigDefaults(trait);
3312
3427
  const template = defaults ? substituteConfig(raw, defaults) : raw;
3313
- return { template, emits };
3428
+ return { template, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3314
3429
  }
3315
3430
  function isLambdaForm(expr) {
3316
3431
  return expr.length === 3 && (expr[0] === "fn" || expr[0] === "lambda");
@@ -3320,7 +3435,8 @@ function spliceExpr(expr, inLambda, st) {
3320
3435
  if (!inLambda || !expr.startsWith(TRAIT_BINDING_PREFIX)) return expr;
3321
3436
  const name = expr.slice(TRAIT_BINDING_PREFIX.length);
3322
3437
  if (name.length === 0 || name.includes(".")) return expr;
3323
- const entry = st.templates.get(name);
3438
+ const embedId = st.embedIds?.[name];
3439
+ const entry = (embedId !== void 0 ? st.templatesById.get(embedId) : void 0) ?? st.templates.get(name);
3324
3440
  if (entry === void 0) return expr;
3325
3441
  if (entry.template === null) {
3326
3442
  throw new LambdaSpliceError(
@@ -3330,10 +3446,13 @@ function spliceExpr(expr, inLambda, st) {
3330
3446
  }
3331
3447
  if (st.visiting.includes(name)) return expr;
3332
3448
  st.visiting.push(name);
3449
+ const prevEmbedIds = st.embedIds;
3450
+ st.embedIds = entry.embedIds;
3333
3451
  const expanded = spliceExpr(entry.template, true, st);
3452
+ st.embedIds = prevEmbedIds;
3334
3453
  st.visiting.pop();
3335
- for (const emit of entry.emits) st.merged.push([name, emit]);
3336
- st.spliced.add(name);
3454
+ for (const emit of entry.emits) st.merged.push([entry.traitName, emit]);
3455
+ st.spliced.add(entry.traitName);
3337
3456
  st.changed = true;
3338
3457
  return expanded;
3339
3458
  }
@@ -3382,28 +3501,38 @@ function mergeWrapperEmits(host, incoming) {
3382
3501
  return emits;
3383
3502
  }
3384
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
+ }
3385
3508
  for (; ; ) {
3386
3509
  const stillReferenced = /* @__PURE__ */ new Set();
3387
3510
  for (const { trait } of traits) {
3511
+ const rawTokens = /* @__PURE__ */ new Set();
3388
3512
  for (const t of trait.stateMachine?.transitions ?? []) {
3389
3513
  if (t.guard !== void 0 && t.guard !== null) {
3390
- collectTraitRefsFromValue(t.guard, stillReferenced);
3514
+ collectTraitRefsFromValue(t.guard, rawTokens);
3391
3515
  }
3392
- collectTraitRefsFromEffects(t.effects, stillReferenced);
3516
+ collectTraitRefsFromEffects(t.effects, rawTokens);
3393
3517
  }
3394
3518
  for (const tick of trait.ticks ?? []) {
3395
3519
  if (tick.guard !== void 0 && tick.guard !== null) {
3396
- collectTraitRefsFromValue(tick.guard, stillReferenced);
3520
+ collectTraitRefsFromValue(tick.guard, rawTokens);
3397
3521
  }
3398
- collectTraitRefsFromEffects(tick.effects, stillReferenced);
3522
+ collectTraitRefsFromEffects(tick.effects, rawTokens);
3399
3523
  }
3400
3524
  if (trait.config) {
3401
3525
  for (const field of Object.values(trait.config)) {
3402
3526
  if (field && typeof field === "object" && "default" in field && field.default !== void 0) {
3403
- collectTraitRefsFromValue(field.default, stillReferenced);
3527
+ collectTraitRefsFromValue(field.default, rawTokens);
3404
3528
  }
3405
3529
  }
3406
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
+ }
3407
3536
  }
3408
3537
  const removable = /* @__PURE__ */ new Set();
3409
3538
  for (const { trait } of traits) {
@@ -3435,14 +3564,19 @@ function removeConsumedWrappers(traits, pages, spliced) {
3435
3564
  function spliceLambdaTraitRefs(traits, pages) {
3436
3565
  if (traits.length === 0) return;
3437
3566
  const templates = /* @__PURE__ */ new Map();
3567
+ const templatesById = /* @__PURE__ */ new Map();
3438
3568
  for (const { trait } of traits) {
3439
- templates.set(trait.name, buildTemplate(trait));
3569
+ const entry = buildTemplate(trait);
3570
+ templates.set(trait.name, entry);
3571
+ if (trait.id) templatesById.set(trait.id, entry);
3440
3572
  }
3441
3573
  const spliced = /* @__PURE__ */ new Set();
3442
3574
  for (const resolved of traits) {
3443
3575
  const host = resolved.trait;
3444
3576
  const st = {
3445
3577
  templates,
3578
+ templatesById,
3579
+ embedIds: host.traitEmbedIds,
3446
3580
  // Seed with the host itself so a self-reference never self-splices.
3447
3581
  visiting: [host.name],
3448
3582
  merged: [],
@@ -3477,6 +3611,42 @@ function spliceLambdaTraitRefs(traits, pages) {
3477
3611
 
3478
3612
  // src/resolver/reference-resolver.ts
3479
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
+ }
3480
3650
  function renameEventsInRenderUiConfig(node, rename) {
3481
3651
  if (node === null || node === void 0) return node;
3482
3652
  if (Array.isArray(node)) {
@@ -3526,25 +3696,30 @@ function renameEventsInEffects(effects, rename) {
3526
3696
  return effect;
3527
3697
  });
3528
3698
  }
3529
- 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) {
3530
3702
  if (node === null || node === void 0) return node;
3531
3703
  if (Array.isArray(node)) {
3532
- return node.map((item) => renameEntityInRenderUiConfig(item, oldName, newName));
3704
+ return node.map((item) => renameEntityInRenderUiConfig(item, rename, props));
3533
3705
  }
3534
3706
  if (typeof node !== "object") return node;
3535
3707
  const obj = node;
3536
3708
  const next = { ...obj };
3537
3709
  for (const [key, value] of Object.entries(obj)) {
3538
- if (key === "entity" && value === oldName) {
3539
- next[key] = newName;
3540
- 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
+ }
3541
3716
  }
3542
- next[key] = renameEntityInRenderUiConfig(value, oldName, newName);
3717
+ next[key] = renameEntityInRenderUiConfig(value, rename, props);
3543
3718
  }
3544
3719
  return next;
3545
3720
  }
3546
- function renameEntityInEffects(effects, oldName, newName) {
3547
- return effects.map((effect) => renameEntityInEffect(effect, oldName, newName));
3721
+ function renameEntityInEffects(effects, rename, props) {
3722
+ return effects.map((effect) => renameEntityInEffect(effect, rename, props));
3548
3723
  }
3549
3724
  var ENTITY_AT_POS_1 = /* @__PURE__ */ new Set(["fetch", "ref", "deref", "spawn"]);
3550
3725
  var ALL_ARGS_ARE_EFFECTS = /* @__PURE__ */ new Set([
@@ -3563,20 +3738,22 @@ var ARGS_FROM_POS_2_ARE_EFFECTS = /* @__PURE__ */ new Set([
3563
3738
  "async/throttle",
3564
3739
  "async/interval"
3565
3740
  ]);
3566
- function renameEntityInEffect(effect, oldName, newName) {
3741
+ function renameEntityInEffect(effect, rename, props) {
3567
3742
  if (!Array.isArray(effect) || effect.length === 0) return effect;
3568
3743
  const op = effect[0];
3569
3744
  if (typeof op !== "string") return effect;
3570
3745
  if (op === "render-ui" && effect.length >= 3) {
3571
3746
  const [, slot, config, ...rest] = effect;
3572
- const nextConfig = renameEntityInRenderUiConfig(config, oldName, newName);
3747
+ const nextConfig = renameEntityInRenderUiConfig(config, rename, props);
3573
3748
  return [op, slot, nextConfig, ...rest];
3574
3749
  }
3575
- if (op === "persist" && effect.length >= 3 && effect[2] === oldName) {
3576
- 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)];
3577
3753
  }
3578
- if (ENTITY_AT_POS_1.has(op) && effect[1] === oldName) {
3579
- 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)];
3580
3757
  }
3581
3758
  const skipFirstNonEffectArg = ARGS_FROM_POS_2_ARE_EFFECTS.has(op);
3582
3759
  const recurseAll = ALL_ARGS_ARE_EFFECTS.has(op);
@@ -3585,7 +3762,7 @@ function renameEntityInEffect(effect, oldName, newName) {
3585
3762
  return effect.map((arg, i) => {
3586
3763
  if (i < startIndex) return arg;
3587
3764
  if (Array.isArray(arg)) {
3588
- return renameEntityInEffect(arg, oldName, newName);
3765
+ return renameEntityInEffect(arg, rename, props);
3589
3766
  }
3590
3767
  return arg;
3591
3768
  });
@@ -3597,11 +3774,12 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3597
3774
  if (!linkedEntity || !atomLinked || linkedEntity === atomLinked) return trait;
3598
3775
  const sm = trait.stateMachine;
3599
3776
  if (!sm) return { ...trait, linkedEntity };
3777
+ const rename = (name) => name === atomLinked ? linkedEntity : void 0;
3600
3778
  const nextTransitions = (sm.transitions ?? []).map((t) => {
3601
3779
  const nextEffects = t.effects ? renameEntityInEffects(
3602
3780
  t.effects,
3603
- atomLinked,
3604
- linkedEntity
3781
+ rename,
3782
+ REBIND_ENTITY_PROPS
3605
3783
  ) : t.effects;
3606
3784
  return { ...t, effects: nextEffects };
3607
3785
  });
@@ -3617,6 +3795,85 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3617
3795
  stateMachine: { ...sm, transitions: nextTransitions }
3618
3796
  };
3619
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
+ }
3620
3877
  function applyEventRenames(trait, renames) {
3621
3878
  if (!renames || Object.keys(renames).length === 0) return trait;
3622
3879
  const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
@@ -3678,17 +3935,24 @@ var ReferenceResolver = class {
3678
3935
  loader;
3679
3936
  options;
3680
3937
  localTraits;
3938
+ /** id-keyed mirror of `localTraits`, populated wherever the trait carries an `id`. */
3939
+ localTraitsById = /* @__PURE__ */ new Map();
3681
3940
  loaderInitialized = false;
3682
3941
  constructor(options) {
3683
3942
  this.options = options;
3684
3943
  this.loader = options.loader;
3685
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
+ }
3686
3950
  }
3687
3951
  async ensureLoader() {
3688
3952
  if (this.loader || this.loaderInitialized) return;
3689
3953
  this.loaderInitialized = true;
3690
3954
  try {
3691
- const { ExternalOrbitalLoader } = await import('./external-loader-OPXVTNC4.js');
3955
+ const { ExternalOrbitalLoader } = await import('./external-loader-FNK5AU6U.js');
3692
3956
  this.loader = new ExternalOrbitalLoader(this.options);
3693
3957
  } catch {
3694
3958
  }
@@ -3705,11 +3969,12 @@ var ReferenceResolver = class {
3705
3969
  } };
3706
3970
  const traitsList = orbital.traits ?? [];
3707
3971
  const alreadyResolved = traitsList.length > 0 && traitsList.every((t) => isInlineTrait(t));
3708
- 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);
3709
3973
  if (!importsResult.success) {
3710
3974
  return { success: false, errors: importsResult.errors };
3711
3975
  }
3712
3976
  const imports = importsResult.data;
3977
+ imports.idIndex = buildIdIndex(orbital, imports.orbitals);
3713
3978
  const entityResult = this.resolveEntity(orbital.entity, imports);
3714
3979
  if (!entityResult.success) {
3715
3980
  errors.push(...entityResult.errors);
@@ -3728,6 +3993,10 @@ var ReferenceResolver = class {
3728
3993
  if (!entityResult.success || !traitsResult.success || !pagesResult.success) {
3729
3994
  return { success: false, errors: ["Internal error: unexpected failure state"] };
3730
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
+ }
3731
4000
  try {
3732
4001
  spliceLambdaTraitRefs(traitsResult.data, pagesResult.data);
3733
4002
  } catch (e) {
@@ -3759,7 +4028,7 @@ var ReferenceResolver = class {
3759
4028
  if (this.options.skipExternalLoading) {
3760
4029
  return {
3761
4030
  success: true,
3762
- data: { orbitals },
4031
+ data: { orbitals, idIndex: /* @__PURE__ */ new Map() },
3763
4032
  warnings: ["External loading skipped"]
3764
4033
  };
3765
4034
  }
@@ -3793,7 +4062,7 @@ var ReferenceResolver = class {
3793
4062
  if (errors.length > 0) {
3794
4063
  return { success: false, errors };
3795
4064
  }
3796
- return { success: true, data: { orbitals }, warnings: [] };
4065
+ return { success: true, data: { orbitals, idIndex: /* @__PURE__ */ new Map() }, warnings: [] };
3797
4066
  }
3798
4067
  /**
3799
4068
  * Resolve entity reference.
@@ -3924,7 +4193,8 @@ var ReferenceResolver = class {
3924
4193
  refObj.linkedEntity,
3925
4194
  refObj.name,
3926
4195
  refObj.events,
3927
- refObj.listens
4196
+ refObj.listens,
4197
+ refObj.refId
3928
4198
  );
3929
4199
  }
3930
4200
  if (typeof traitRef === "string") {
@@ -3938,7 +4208,7 @@ var ReferenceResolver = class {
3938
4208
  /**
3939
4209
  * Resolve a trait reference string.
3940
4210
  */
3941
- resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride) {
4211
+ resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride, refId) {
3942
4212
  const parsed = parseImportedTraitRef(ref);
3943
4213
  if (parsed) {
3944
4214
  const imported = imports.orbitals.get(parsed.alias);
@@ -3950,7 +4220,7 @@ var ReferenceResolver = class {
3950
4220
  ]
3951
4221
  };
3952
4222
  }
3953
- const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName);
4223
+ const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName, refId, imports.idIndex);
3954
4224
  if (!trait) {
3955
4225
  return {
3956
4226
  success: false,
@@ -3986,7 +4256,7 @@ var ReferenceResolver = class {
3986
4256
  warnings: []
3987
4257
  };
3988
4258
  }
3989
- const localTrait = this.localTraits.get(ref);
4259
+ const localTrait = (refId && this.localTraitsById.get(refId)) ?? this.localTraits.get(ref);
3990
4260
  if (localTrait) {
3991
4261
  const baseLocal = overrideName ? { ...localTrait, name: overrideName } : localTrait;
3992
4262
  const { trait: configResolvedLocal, errors: localConfigRefErrors } = resolveConfigRefEmitNames(baseLocal, config);
@@ -4023,9 +4293,17 @@ var ReferenceResolver = class {
4023
4293
  };
4024
4294
  }
4025
4295
  /**
4026
- * 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.
4027
4299
  */
4028
- 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
+ }
4029
4307
  for (const traitRef of orbital.traits) {
4030
4308
  if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
4031
4309
  if (traitRef.name === traitName) {
@@ -4099,7 +4377,7 @@ var ReferenceResolver = class {
4099
4377
  /**
4100
4378
  * Resolve a page reference string.
4101
4379
  */
4102
- resolvePageRefString(ref, imports) {
4380
+ resolvePageRefString(ref, imports, refId) {
4103
4381
  const parsed = parsePageRef(ref);
4104
4382
  if (!parsed) {
4105
4383
  return {
@@ -4116,7 +4394,7 @@ var ReferenceResolver = class {
4116
4394
  ]
4117
4395
  };
4118
4396
  }
4119
- const page = this.findPageInOrbital(imported.orbital, parsed.pageName);
4397
+ const page = this.findPageInOrbital(imported.orbital, parsed.pageName, refId, imports.idIndex);
4120
4398
  if (!page) {
4121
4399
  return {
4122
4400
  success: false,
@@ -4139,7 +4417,7 @@ var ReferenceResolver = class {
4139
4417
  * Resolve a page reference object with optional path override.
4140
4418
  */
4141
4419
  resolvePageRefObject(refObj, imports) {
4142
- const baseResult = this.resolvePageRefString(refObj.ref, imports);
4420
+ const baseResult = this.resolvePageRefString(refObj.ref, imports, refObj.refId);
4143
4421
  if (!baseResult.success) {
4144
4422
  return baseResult;
4145
4423
  }
@@ -4160,9 +4438,17 @@ var ReferenceResolver = class {
4160
4438
  };
4161
4439
  }
4162
4440
  /**
4163
- * 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.
4164
4444
  */
4165
- 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
+ }
4166
4452
  const pages = orbital.pages;
4167
4453
  if (!pages) return null;
4168
4454
  for (const pageRef of pages) {
@@ -4195,6 +4481,9 @@ var ReferenceResolver = class {
4195
4481
  addLocalTraits(traits) {
4196
4482
  for (const trait of traits) {
4197
4483
  this.localTraits.set(trait.name, trait);
4484
+ if (trait.id) {
4485
+ this.localTraitsById.set(trait.id, trait);
4486
+ }
4198
4487
  }
4199
4488
  }
4200
4489
  /**