@almadar/runtime 6.33.0 → 6.35.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,12 +950,17 @@ 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, targetTrait) {
853
960
  const results = [];
854
961
  const scope = scopeOf(entityData);
855
962
  for (const [traitName, trait] of this.traits) {
963
+ if (targetTrait !== void 0 && traitName !== targetTrait) continue;
856
964
  const traitState = this.getOrInitState(traitName, scope);
857
965
  if (!traitState) continue;
858
966
  const key = compositeKey(traitName, scope);
@@ -861,6 +969,7 @@ var StateMachineManager = class {
861
969
  traitState,
862
970
  trait,
863
971
  eventKey,
972
+ eventId,
864
973
  payload,
865
974
  entityData: perTraitEntity,
866
975
  config: this.traitConfigs.get(traitName),
@@ -902,12 +1011,12 @@ var StateMachineManager = class {
902
1011
  * trait to process them sequentially (actor-model guarantee: one event
903
1012
  * at a time per trait, effects fully awaited before the next event).
904
1013
  */
905
- enqueueEvent(eventKey, payload, entityData, entityByTrait) {
1014
+ enqueueEvent(eventKey, payload, entityData, entityByTrait, eventId) {
906
1015
  const scope = scopeOf(entityData);
907
1016
  for (const [traitName] of this.traits) {
908
1017
  const key = compositeKey(traitName, scope);
909
1018
  const queue = this.queues.get(key) ?? [];
910
- queue.push({ eventKey, payload, entityData, entityByTrait });
1019
+ queue.push({ eventKey, eventId, payload, entityData, entityByTrait });
911
1020
  this.queues.set(key, queue);
912
1021
  }
913
1022
  }
@@ -932,6 +1041,7 @@ var StateMachineManager = class {
932
1041
  traitState,
933
1042
  trait,
934
1043
  eventKey: entry.eventKey,
1044
+ eventId: entry.eventId,
935
1045
  payload: entry.payload,
936
1046
  entityData: perTraitEntity,
937
1047
  config: this.traitConfigs.get(traitName),
@@ -2212,6 +2322,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2212
2322
  stores = /* @__PURE__ */ new Map();
2213
2323
  schemas = /* @__PURE__ */ new Map();
2214
2324
  idCounters = /* @__PURE__ */ new Map();
2325
+ /** entityId -> normalized store name, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
2326
+ storeNameById = /* @__PURE__ */ new Map();
2215
2327
  config;
2216
2328
  constructor(config = {}) {
2217
2329
  this.config = {
@@ -2263,6 +2375,9 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2263
2375
  registerEntity(schema, seedCount) {
2264
2376
  const normalized = schema.name.toLowerCase();
2265
2377
  this.schemas.set(normalized, schema);
2378
+ if (schema.id) {
2379
+ this.storeNameById.set(schema.id, normalized);
2380
+ }
2266
2381
  if (schema.seedData && schema.seedData.length > 0) {
2267
2382
  this.seedFromInstances(schema.name, schema.seedData);
2268
2383
  } else {
@@ -2294,7 +2409,8 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2294
2409
  if (relationFields.length === 0) continue;
2295
2410
  for (const row of store.values()) {
2296
2411
  for (const field of relationFields) {
2297
- const targetStore = this.stores.get(field.relation.entity.toLowerCase());
2412
+ const targetNormalized = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? field.relation.entity.toLowerCase();
2413
+ const targetStore = this.stores.get(targetNormalized);
2298
2414
  if (!targetStore || targetStore.size === 0) continue;
2299
2415
  const selfId = row["id"];
2300
2416
  const sameStore = targetStore === store;
@@ -2972,7 +3088,7 @@ async function getExternalLoaderModule() {
2972
3088
  return null;
2973
3089
  }
2974
3090
  try {
2975
- externalLoaderModule = await import('./external-loader-OPXVTNC4.js');
3091
+ externalLoaderModule = await import('./external-loader-FNK5AU6U.js');
2976
3092
  return externalLoaderModule;
2977
3093
  } catch {
2978
3094
  return null;
@@ -3307,10 +3423,10 @@ function substituteConfig(expr, subs) {
3307
3423
  function buildTemplate(trait) {
3308
3424
  const raw = renderOnlyTemplate(trait);
3309
3425
  const emits = trait.emits ?? [];
3310
- if (raw === null) return { template: null, emits };
3426
+ if (raw === null) return { template: null, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3311
3427
  const defaults = collectDeclaredConfigDefaults(trait);
3312
3428
  const template = defaults ? substituteConfig(raw, defaults) : raw;
3313
- return { template, emits };
3429
+ return { template, emits, traitName: trait.name, id: trait.id, embedIds: trait.traitEmbedIds };
3314
3430
  }
3315
3431
  function isLambdaForm(expr) {
3316
3432
  return expr.length === 3 && (expr[0] === "fn" || expr[0] === "lambda");
@@ -3320,7 +3436,8 @@ function spliceExpr(expr, inLambda, st) {
3320
3436
  if (!inLambda || !expr.startsWith(TRAIT_BINDING_PREFIX)) return expr;
3321
3437
  const name = expr.slice(TRAIT_BINDING_PREFIX.length);
3322
3438
  if (name.length === 0 || name.includes(".")) return expr;
3323
- const entry = st.templates.get(name);
3439
+ const embedId = st.embedIds?.[name];
3440
+ const entry = (embedId !== void 0 ? st.templatesById.get(embedId) : void 0) ?? st.templates.get(name);
3324
3441
  if (entry === void 0) return expr;
3325
3442
  if (entry.template === null) {
3326
3443
  throw new LambdaSpliceError(
@@ -3330,10 +3447,13 @@ function spliceExpr(expr, inLambda, st) {
3330
3447
  }
3331
3448
  if (st.visiting.includes(name)) return expr;
3332
3449
  st.visiting.push(name);
3450
+ const prevEmbedIds = st.embedIds;
3451
+ st.embedIds = entry.embedIds;
3333
3452
  const expanded = spliceExpr(entry.template, true, st);
3453
+ st.embedIds = prevEmbedIds;
3334
3454
  st.visiting.pop();
3335
- for (const emit of entry.emits) st.merged.push([name, emit]);
3336
- st.spliced.add(name);
3455
+ for (const emit of entry.emits) st.merged.push([entry.traitName, emit]);
3456
+ st.spliced.add(entry.traitName);
3337
3457
  st.changed = true;
3338
3458
  return expanded;
3339
3459
  }
@@ -3382,28 +3502,38 @@ function mergeWrapperEmits(host, incoming) {
3382
3502
  return emits;
3383
3503
  }
3384
3504
  function removeConsumedWrappers(traits, pages, spliced) {
3505
+ const idToName = /* @__PURE__ */ new Map();
3506
+ for (const { trait } of traits) {
3507
+ if (trait.id) idToName.set(trait.id, trait.name);
3508
+ }
3385
3509
  for (; ; ) {
3386
3510
  const stillReferenced = /* @__PURE__ */ new Set();
3387
3511
  for (const { trait } of traits) {
3512
+ const rawTokens = /* @__PURE__ */ new Set();
3388
3513
  for (const t of trait.stateMachine?.transitions ?? []) {
3389
3514
  if (t.guard !== void 0 && t.guard !== null) {
3390
- collectTraitRefsFromValue(t.guard, stillReferenced);
3515
+ collectTraitRefsFromValue(t.guard, rawTokens);
3391
3516
  }
3392
- collectTraitRefsFromEffects(t.effects, stillReferenced);
3517
+ collectTraitRefsFromEffects(t.effects, rawTokens);
3393
3518
  }
3394
3519
  for (const tick of trait.ticks ?? []) {
3395
3520
  if (tick.guard !== void 0 && tick.guard !== null) {
3396
- collectTraitRefsFromValue(tick.guard, stillReferenced);
3521
+ collectTraitRefsFromValue(tick.guard, rawTokens);
3397
3522
  }
3398
- collectTraitRefsFromEffects(tick.effects, stillReferenced);
3523
+ collectTraitRefsFromEffects(tick.effects, rawTokens);
3399
3524
  }
3400
3525
  if (trait.config) {
3401
3526
  for (const field of Object.values(trait.config)) {
3402
3527
  if (field && typeof field === "object" && "default" in field && field.default !== void 0) {
3403
- collectTraitRefsFromValue(field.default, stillReferenced);
3528
+ collectTraitRefsFromValue(field.default, rawTokens);
3404
3529
  }
3405
3530
  }
3406
3531
  }
3532
+ for (const token of rawTokens) {
3533
+ const embedId = trait.traitEmbedIds?.[token];
3534
+ const resolvedName = (embedId !== void 0 ? idToName.get(embedId) : void 0) ?? token;
3535
+ stillReferenced.add(resolvedName);
3536
+ }
3407
3537
  }
3408
3538
  const removable = /* @__PURE__ */ new Set();
3409
3539
  for (const { trait } of traits) {
@@ -3435,14 +3565,19 @@ function removeConsumedWrappers(traits, pages, spliced) {
3435
3565
  function spliceLambdaTraitRefs(traits, pages) {
3436
3566
  if (traits.length === 0) return;
3437
3567
  const templates = /* @__PURE__ */ new Map();
3568
+ const templatesById = /* @__PURE__ */ new Map();
3438
3569
  for (const { trait } of traits) {
3439
- templates.set(trait.name, buildTemplate(trait));
3570
+ const entry = buildTemplate(trait);
3571
+ templates.set(trait.name, entry);
3572
+ if (trait.id) templatesById.set(trait.id, entry);
3440
3573
  }
3441
3574
  const spliced = /* @__PURE__ */ new Set();
3442
3575
  for (const resolved of traits) {
3443
3576
  const host = resolved.trait;
3444
3577
  const st = {
3445
3578
  templates,
3579
+ templatesById,
3580
+ embedIds: host.traitEmbedIds,
3446
3581
  // Seed with the host itself so a self-reference never self-splices.
3447
3582
  visiting: [host.name],
3448
3583
  merged: [],
@@ -3477,6 +3612,42 @@ function spliceLambdaTraitRefs(traits, pages) {
3477
3612
 
3478
3613
  // src/resolver/reference-resolver.ts
3479
3614
  var refResolverLog = createLogger("almadar:runtime:ref-resolver");
3615
+ function indexOrbitalNodes(orbital, idIndex) {
3616
+ for (const traitRef of orbital.traits ?? []) {
3617
+ if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
3618
+ const trait = traitRef;
3619
+ if (trait.id) {
3620
+ idIndex.set(trait.id, { kind: "trait", node: trait });
3621
+ }
3622
+ for (const ev of trait.stateMachine?.events ?? []) {
3623
+ if (ev.id) {
3624
+ idIndex.set(ev.id, { kind: "event", node: ev });
3625
+ }
3626
+ }
3627
+ }
3628
+ }
3629
+ const entityRef = orbital.entity;
3630
+ if (entityRef && typeof entityRef !== "string" && !("extends" in entityRef) && entityRef.id) {
3631
+ const entity = entityRef;
3632
+ idIndex.set(entity.id, { kind: "entity", node: entity });
3633
+ }
3634
+ for (const pageRef of orbital.pages ?? []) {
3635
+ if (typeof pageRef !== "string" && !("ref" in pageRef)) {
3636
+ const page = pageRef;
3637
+ if (page.id) {
3638
+ idIndex.set(page.id, { kind: "page", node: page });
3639
+ }
3640
+ }
3641
+ }
3642
+ }
3643
+ function buildIdIndex(orbital, orbitals) {
3644
+ const idIndex = /* @__PURE__ */ new Map();
3645
+ indexOrbitalNodes(orbital, idIndex);
3646
+ for (const imported of orbitals.values()) {
3647
+ indexOrbitalNodes(imported.orbital, idIndex);
3648
+ }
3649
+ return idIndex;
3650
+ }
3480
3651
  function renameEventsInRenderUiConfig(node, rename) {
3481
3652
  if (node === null || node === void 0) return node;
3482
3653
  if (Array.isArray(node)) {
@@ -3526,25 +3697,30 @@ function renameEventsInEffects(effects, rename) {
3526
3697
  return effect;
3527
3698
  });
3528
3699
  }
3529
- function renameEntityInRenderUiConfig(node, oldName, newName) {
3700
+ var REBIND_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity"]);
3701
+ var ID_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity", "entityType", "source"]);
3702
+ function renameEntityInRenderUiConfig(node, rename, props) {
3530
3703
  if (node === null || node === void 0) return node;
3531
3704
  if (Array.isArray(node)) {
3532
- return node.map((item) => renameEntityInRenderUiConfig(item, oldName, newName));
3705
+ return node.map((item) => renameEntityInRenderUiConfig(item, rename, props));
3533
3706
  }
3534
3707
  if (typeof node !== "object") return node;
3535
3708
  const obj = node;
3536
3709
  const next = { ...obj };
3537
3710
  for (const [key, value] of Object.entries(obj)) {
3538
- if (key === "entity" && value === oldName) {
3539
- next[key] = newName;
3540
- continue;
3711
+ if (props.has(key) && typeof value === "string") {
3712
+ const replaced = rename(value);
3713
+ if (replaced !== void 0) {
3714
+ next[key] = replaced;
3715
+ continue;
3716
+ }
3541
3717
  }
3542
- next[key] = renameEntityInRenderUiConfig(value, oldName, newName);
3718
+ next[key] = renameEntityInRenderUiConfig(value, rename, props);
3543
3719
  }
3544
3720
  return next;
3545
3721
  }
3546
- function renameEntityInEffects(effects, oldName, newName) {
3547
- return effects.map((effect) => renameEntityInEffect(effect, oldName, newName));
3722
+ function renameEntityInEffects(effects, rename, props) {
3723
+ return effects.map((effect) => renameEntityInEffect(effect, rename, props));
3548
3724
  }
3549
3725
  var ENTITY_AT_POS_1 = /* @__PURE__ */ new Set(["fetch", "ref", "deref", "spawn"]);
3550
3726
  var ALL_ARGS_ARE_EFFECTS = /* @__PURE__ */ new Set([
@@ -3563,20 +3739,22 @@ var ARGS_FROM_POS_2_ARE_EFFECTS = /* @__PURE__ */ new Set([
3563
3739
  "async/throttle",
3564
3740
  "async/interval"
3565
3741
  ]);
3566
- function renameEntityInEffect(effect, oldName, newName) {
3742
+ function renameEntityInEffect(effect, rename, props) {
3567
3743
  if (!Array.isArray(effect) || effect.length === 0) return effect;
3568
3744
  const op = effect[0];
3569
3745
  if (typeof op !== "string") return effect;
3570
3746
  if (op === "render-ui" && effect.length >= 3) {
3571
3747
  const [, slot, config, ...rest] = effect;
3572
- const nextConfig = renameEntityInRenderUiConfig(config, oldName, newName);
3748
+ const nextConfig = renameEntityInRenderUiConfig(config, rename, props);
3573
3749
  return [op, slot, nextConfig, ...rest];
3574
3750
  }
3575
- if (op === "persist" && effect.length >= 3 && effect[2] === oldName) {
3576
- return [op, effect[1], newName, ...effect.slice(3)];
3751
+ if (op === "persist" && effect.length >= 3 && typeof effect[2] === "string") {
3752
+ const replaced = rename(effect[2]);
3753
+ if (replaced !== void 0) return [op, effect[1], replaced, ...effect.slice(3)];
3577
3754
  }
3578
- if (ENTITY_AT_POS_1.has(op) && effect[1] === oldName) {
3579
- return [op, newName, ...effect.slice(2)];
3755
+ if (ENTITY_AT_POS_1.has(op) && typeof effect[1] === "string") {
3756
+ const replaced = rename(effect[1]);
3757
+ if (replaced !== void 0) return [op, replaced, ...effect.slice(2)];
3580
3758
  }
3581
3759
  const skipFirstNonEffectArg = ARGS_FROM_POS_2_ARE_EFFECTS.has(op);
3582
3760
  const recurseAll = ALL_ARGS_ARE_EFFECTS.has(op);
@@ -3585,7 +3763,7 @@ function renameEntityInEffect(effect, oldName, newName) {
3585
3763
  return effect.map((arg, i) => {
3586
3764
  if (i < startIndex) return arg;
3587
3765
  if (Array.isArray(arg)) {
3588
- return renameEntityInEffect(arg, oldName, newName);
3766
+ return renameEntityInEffect(arg, rename, props);
3589
3767
  }
3590
3768
  return arg;
3591
3769
  });
@@ -3597,11 +3775,12 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3597
3775
  if (!linkedEntity || !atomLinked || linkedEntity === atomLinked) return trait;
3598
3776
  const sm = trait.stateMachine;
3599
3777
  if (!sm) return { ...trait, linkedEntity };
3778
+ const rename = (name) => name === atomLinked ? linkedEntity : void 0;
3600
3779
  const nextTransitions = (sm.transitions ?? []).map((t) => {
3601
3780
  const nextEffects = t.effects ? renameEntityInEffects(
3602
3781
  t.effects,
3603
- atomLinked,
3604
- linkedEntity
3782
+ rename,
3783
+ REBIND_ENTITY_PROPS
3605
3784
  ) : t.effects;
3606
3785
  return { ...t, effects: nextEffects };
3607
3786
  });
@@ -3617,6 +3796,88 @@ function applyLinkedEntityRename(trait, linkedEntity) {
3617
3796
  stateMachine: { ...sm, transitions: nextTransitions }
3618
3797
  };
3619
3798
  }
3799
+ function resolveEntityTokensById(trait, idIndex) {
3800
+ const map = trait.entityRefIds;
3801
+ if (!map) return trait;
3802
+ const rewrites = /* @__PURE__ */ new Map();
3803
+ for (const [tokenName, entityId] of Object.entries(map)) {
3804
+ const entry = idIndex.get(entityId);
3805
+ if (entry && entry.kind === "entity") {
3806
+ const currentName = entry.node.name;
3807
+ if (currentName && currentName !== tokenName) {
3808
+ rewrites.set(tokenName, currentName);
3809
+ }
3810
+ }
3811
+ }
3812
+ if (rewrites.size === 0) return trait;
3813
+ const rename = (name) => rewrites.get(name);
3814
+ const sm = trait.stateMachine;
3815
+ const nextTransitions = sm?.transitions ? sm.transitions.map((t) => ({
3816
+ ...t,
3817
+ effects: t.effects ? renameEntityInEffects(
3818
+ t.effects,
3819
+ rename,
3820
+ ID_ENTITY_PROPS
3821
+ ) : t.effects
3822
+ })) : sm?.transitions;
3823
+ const nextTicks = trait.ticks ? trait.ticks.map((tick) => ({
3824
+ ...tick,
3825
+ effects: renameEntityInEffects(
3826
+ tick.effects,
3827
+ rename,
3828
+ ID_ENTITY_PROPS
3829
+ )
3830
+ })) : trait.ticks;
3831
+ const nextInitial = trait.initialEffects ? renameEntityInEffects(
3832
+ trait.initialEffects,
3833
+ rename,
3834
+ ID_ENTITY_PROPS
3835
+ ) : trait.initialEffects;
3836
+ const nextLinked = trait.linkedEntity !== void 0 ? rewrites.get(trait.linkedEntity) ?? trait.linkedEntity : trait.linkedEntity;
3837
+ refResolverLog.info("entity-ref:id-resolve", {
3838
+ trait: trait.name,
3839
+ rewrites: Object.fromEntries(rewrites)
3840
+ });
3841
+ return {
3842
+ ...trait,
3843
+ linkedEntity: nextLinked,
3844
+ ...sm ? { stateMachine: { ...sm, transitions: nextTransitions ?? [] } } : {},
3845
+ ...nextTicks !== void 0 ? { ticks: nextTicks } : {},
3846
+ ...nextInitial !== void 0 ? { initialEffects: nextInitial } : {}
3847
+ };
3848
+ }
3849
+ var REFERENCE_CONFIG_TYPE_TO_ID_KIND = {
3850
+ entity: "entity",
3851
+ trait: "trait",
3852
+ event: "event"
3853
+ };
3854
+ function resolveConfigRefsById(trait, idIndex) {
3855
+ const schema = trait.config;
3856
+ if (!schema) return trait;
3857
+ let nextSchema;
3858
+ const rewrites = [];
3859
+ for (const [key, field] of Object.entries(schema)) {
3860
+ if (!field.refId || !isReferenceConfigType(field.type)) continue;
3861
+ const expectedKind = REFERENCE_CONFIG_TYPE_TO_ID_KIND[field.type];
3862
+ if (!expectedKind) continue;
3863
+ const entry = idIndex.get(field.refId);
3864
+ if (!entry || entry.kind !== expectedKind) continue;
3865
+ const currentName = entry.kind === "entity" ? entry.node.name : entry.kind === "event" ? entry.node.key : entry.node.name;
3866
+ if (!currentName) continue;
3867
+ const isTraitBinding = entry.kind === "trait" && typeof field.default === "string" && field.default.startsWith("@trait.");
3868
+ const nextDefault = isTraitBinding ? `@trait.${currentName}` : currentName;
3869
+ if (nextDefault === field.default) continue;
3870
+ nextSchema ??= { ...schema };
3871
+ nextSchema[key] = { ...field, default: nextDefault };
3872
+ rewrites.push({ key, from: field.default, to: nextDefault });
3873
+ }
3874
+ if (!nextSchema) return trait;
3875
+ refResolverLog.info("config-ref:id-resolve", {
3876
+ trait: trait.name,
3877
+ rewrites
3878
+ });
3879
+ return { ...trait, config: nextSchema };
3880
+ }
3620
3881
  function applyEventRenames(trait, renames) {
3621
3882
  if (!renames || Object.keys(renames).length === 0) return trait;
3622
3883
  const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
@@ -3678,17 +3939,24 @@ var ReferenceResolver = class {
3678
3939
  loader;
3679
3940
  options;
3680
3941
  localTraits;
3942
+ /** id-keyed mirror of `localTraits`, populated wherever the trait carries an `id`. */
3943
+ localTraitsById = /* @__PURE__ */ new Map();
3681
3944
  loaderInitialized = false;
3682
3945
  constructor(options) {
3683
3946
  this.options = options;
3684
3947
  this.loader = options.loader;
3685
3948
  this.localTraits = options.localTraits ?? /* @__PURE__ */ new Map();
3949
+ for (const trait of this.localTraits.values()) {
3950
+ if (trait.id) {
3951
+ this.localTraitsById.set(trait.id, trait);
3952
+ }
3953
+ }
3686
3954
  }
3687
3955
  async ensureLoader() {
3688
3956
  if (this.loader || this.loaderInitialized) return;
3689
3957
  this.loaderInitialized = true;
3690
3958
  try {
3691
- const { ExternalOrbitalLoader } = await import('./external-loader-OPXVTNC4.js');
3959
+ const { ExternalOrbitalLoader } = await import('./external-loader-FNK5AU6U.js');
3692
3960
  this.loader = new ExternalOrbitalLoader(this.options);
3693
3961
  } catch {
3694
3962
  }
@@ -3705,11 +3973,12 @@ var ReferenceResolver = class {
3705
3973
  } };
3706
3974
  const traitsList = orbital.traits ?? [];
3707
3975
  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);
3976
+ const importsResult = alreadyResolved ? { success: true, data: { orbitals: /* @__PURE__ */ new Map(), idIndex: /* @__PURE__ */ new Map() }} : await this.resolveImports(orbital.uses ?? [], sourcePath, importChain);
3709
3977
  if (!importsResult.success) {
3710
3978
  return { success: false, errors: importsResult.errors };
3711
3979
  }
3712
3980
  const imports = importsResult.data;
3981
+ imports.idIndex = buildIdIndex(orbital, imports.orbitals);
3713
3982
  const entityResult = this.resolveEntity(orbital.entity, imports);
3714
3983
  if (!entityResult.success) {
3715
3984
  errors.push(...entityResult.errors);
@@ -3728,6 +3997,10 @@ var ReferenceResolver = class {
3728
3997
  if (!entityResult.success || !traitsResult.success || !pagesResult.success) {
3729
3998
  return { success: false, errors: ["Internal error: unexpected failure state"] };
3730
3999
  }
4000
+ for (const resolvedTrait of traitsResult.data) {
4001
+ resolvedTrait.trait = resolveEntityTokensById(resolvedTrait.trait, imports.idIndex);
4002
+ resolvedTrait.trait = resolveConfigRefsById(resolvedTrait.trait, imports.idIndex);
4003
+ }
3731
4004
  try {
3732
4005
  spliceLambdaTraitRefs(traitsResult.data, pagesResult.data);
3733
4006
  } catch (e) {
@@ -3759,7 +4032,7 @@ var ReferenceResolver = class {
3759
4032
  if (this.options.skipExternalLoading) {
3760
4033
  return {
3761
4034
  success: true,
3762
- data: { orbitals },
4035
+ data: { orbitals, idIndex: /* @__PURE__ */ new Map() },
3763
4036
  warnings: ["External loading skipped"]
3764
4037
  };
3765
4038
  }
@@ -3793,7 +4066,7 @@ var ReferenceResolver = class {
3793
4066
  if (errors.length > 0) {
3794
4067
  return { success: false, errors };
3795
4068
  }
3796
- return { success: true, data: { orbitals }, warnings: [] };
4069
+ return { success: true, data: { orbitals, idIndex: /* @__PURE__ */ new Map() }, warnings: [] };
3797
4070
  }
3798
4071
  /**
3799
4072
  * Resolve entity reference.
@@ -3924,7 +4197,8 @@ var ReferenceResolver = class {
3924
4197
  refObj.linkedEntity,
3925
4198
  refObj.name,
3926
4199
  refObj.events,
3927
- refObj.listens
4200
+ refObj.listens,
4201
+ refObj.refId
3928
4202
  );
3929
4203
  }
3930
4204
  if (typeof traitRef === "string") {
@@ -3938,7 +4212,7 @@ var ReferenceResolver = class {
3938
4212
  /**
3939
4213
  * Resolve a trait reference string.
3940
4214
  */
3941
- resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride) {
4215
+ resolveTraitRefString(ref, imports, config, linkedEntity, overrideName, eventRenames, listensOverride, refId) {
3942
4216
  const parsed = parseImportedTraitRef(ref);
3943
4217
  if (parsed) {
3944
4218
  const imported = imports.orbitals.get(parsed.alias);
@@ -3950,7 +4224,7 @@ var ReferenceResolver = class {
3950
4224
  ]
3951
4225
  };
3952
4226
  }
3953
- const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName);
4227
+ const trait = this.findTraitInOrbital(imported.orbital, parsed.traitName, refId, imports.idIndex);
3954
4228
  if (!trait) {
3955
4229
  return {
3956
4230
  success: false,
@@ -3986,7 +4260,7 @@ var ReferenceResolver = class {
3986
4260
  warnings: []
3987
4261
  };
3988
4262
  }
3989
- const localTrait = this.localTraits.get(ref);
4263
+ const localTrait = (refId && this.localTraitsById.get(refId)) ?? this.localTraits.get(ref);
3990
4264
  if (localTrait) {
3991
4265
  const baseLocal = overrideName ? { ...localTrait, name: overrideName } : localTrait;
3992
4266
  const { trait: configResolvedLocal, errors: localConfigRefErrors } = resolveConfigRefEmitNames(baseLocal, config);
@@ -4023,9 +4297,17 @@ var ReferenceResolver = class {
4023
4297
  };
4024
4298
  }
4025
4299
  /**
4026
- * Find a trait in an orbital by name.
4300
+ * Find a trait in an orbital by name. Id-primary: when the calling ref
4301
+ * carries a `refId` and the id index has a matching trait entry, return it
4302
+ * directly — else fall back to the existing name match unchanged.
4027
4303
  */
4028
- findTraitInOrbital(orbital, traitName) {
4304
+ findTraitInOrbital(orbital, traitName, refId, idIndex) {
4305
+ if (refId && idIndex) {
4306
+ const entry = idIndex.get(refId);
4307
+ if (entry && entry.kind === "trait") {
4308
+ return entry.node;
4309
+ }
4310
+ }
4029
4311
  for (const traitRef of orbital.traits) {
4030
4312
  if (typeof traitRef !== "string" && "stateMachine" in traitRef) {
4031
4313
  if (traitRef.name === traitName) {
@@ -4099,7 +4381,7 @@ var ReferenceResolver = class {
4099
4381
  /**
4100
4382
  * Resolve a page reference string.
4101
4383
  */
4102
- resolvePageRefString(ref, imports) {
4384
+ resolvePageRefString(ref, imports, refId) {
4103
4385
  const parsed = parsePageRef(ref);
4104
4386
  if (!parsed) {
4105
4387
  return {
@@ -4116,7 +4398,7 @@ var ReferenceResolver = class {
4116
4398
  ]
4117
4399
  };
4118
4400
  }
4119
- const page = this.findPageInOrbital(imported.orbital, parsed.pageName);
4401
+ const page = this.findPageInOrbital(imported.orbital, parsed.pageName, refId, imports.idIndex);
4120
4402
  if (!page) {
4121
4403
  return {
4122
4404
  success: false,
@@ -4139,7 +4421,7 @@ var ReferenceResolver = class {
4139
4421
  * Resolve a page reference object with optional path override.
4140
4422
  */
4141
4423
  resolvePageRefObject(refObj, imports) {
4142
- const baseResult = this.resolvePageRefString(refObj.ref, imports);
4424
+ const baseResult = this.resolvePageRefString(refObj.ref, imports, refObj.refId);
4143
4425
  if (!baseResult.success) {
4144
4426
  return baseResult;
4145
4427
  }
@@ -4160,9 +4442,17 @@ var ReferenceResolver = class {
4160
4442
  };
4161
4443
  }
4162
4444
  /**
4163
- * Find a page in an orbital by name.
4445
+ * Find a page in an orbital by name. Id-primary: when the calling ref
4446
+ * carries a `refId` and the id index has a matching page entry, return it
4447
+ * directly — else fall back to the existing name match unchanged.
4164
4448
  */
4165
- findPageInOrbital(orbital, pageName) {
4449
+ findPageInOrbital(orbital, pageName, refId, idIndex) {
4450
+ if (refId && idIndex) {
4451
+ const entry = idIndex.get(refId);
4452
+ if (entry && entry.kind === "page") {
4453
+ return { ...entry.node };
4454
+ }
4455
+ }
4166
4456
  const pages = orbital.pages;
4167
4457
  if (!pages) return null;
4168
4458
  for (const pageRef of pages) {
@@ -4195,6 +4485,9 @@ var ReferenceResolver = class {
4195
4485
  addLocalTraits(traits) {
4196
4486
  for (const trait of traits) {
4197
4487
  this.localTraits.set(trait.name, trait);
4488
+ if (trait.id) {
4489
+ this.localTraitsById.set(trait.id, trait);
4490
+ }
4198
4491
  }
4199
4492
  }
4200
4493
  /**