@almadar/runtime 6.61.0 → 6.63.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.
@@ -767,6 +767,11 @@ declare function preprocessSchema(schema: OrbitalSchema, options: PreprocessOpti
767
767
  *
768
768
  * - `persistent` entities share the same collection
769
769
  * - `runtime` entities get isolated collections per orbital
770
+ *
771
+ * NOTE: the runtime's own store keying does NOT go through this helper —
772
+ * `MockPersistenceAdapter` resolves entity → store via its declared
773
+ * `collection` directly (collection-keyed stores, 2026-08-29). This stays
774
+ * exported for external consumers computing display/derived names only.
770
775
  */
771
776
  declare function getIsolatedCollectionName(orbitalName: string, entitySharing: EntitySharingMap): string;
772
777
  /**
@@ -1,4 +1,4 @@
1
1
  import 'express';
2
- export { F as ClientEffectTuple, G as ClientNavigateBackTuple, H as ClientNavigateTuple, J as ClientNotifyTuple, K as ClientRenderUITuple, M as EffectResult, e as InMemoryPersistence, N as LiveBroadcastItem, Q as LoaderConfig, O as OrbitalEventRequest, f as OrbitalEventResponse, T as OrbitalServerRuntime, g as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, V as RuntimeTraitTick, p as collectDeclaredConfigDefaults, W as createOrbitalServerRuntime } from './OrbitalServerRuntime-CZHHxo_A.js';
2
+ export { F as ClientEffectTuple, G as ClientNavigateBackTuple, H as ClientNavigateTuple, J as ClientNotifyTuple, K as ClientRenderUITuple, M as EffectResult, e as InMemoryPersistence, N as LiveBroadcastItem, Q as LoaderConfig, O as OrbitalEventRequest, f as OrbitalEventResponse, T as OrbitalServerRuntime, g as OrbitalServerRuntimeConfig, P as PersistenceAdapter, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, V as RuntimeTraitTick, p as collectDeclaredConfigDefaults, W as createOrbitalServerRuntime } from './OrbitalServerRuntime-nqwGldMC.js';
3
3
  import './types-BaD_ox7e.js';
4
4
  import '@almadar/core';
@@ -1,5 +1,5 @@
1
- import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-QY5EUGR2.js';
2
- export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-QY5EUGR2.js';
1
+ import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-C7SIRVGS.js';
2
+ export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-C7SIRVGS.js';
3
3
  import { isValidCronExpression } from './chunk-OU3ITB5S.js';
4
4
  import { createContextFromBindings, resolveCallSitePayloadCaptures, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
5
5
  import './chunk-T4VDAB4C.js';
@@ -707,6 +707,7 @@ var OrbitalServerRuntime = class {
707
707
  this.persistence.registerEntity({
708
708
  name: entity.name,
709
709
  id: entity.id,
710
+ collection: entity.collection,
710
711
  fields,
711
712
  persistence: entity.persistence
712
713
  });
@@ -727,6 +728,7 @@ var OrbitalServerRuntime = class {
727
728
  this.persistence.registerEntity({
728
729
  name: auxEntity.name,
729
730
  id: auxEntity.id,
731
+ collection: auxEntity.collection,
730
732
  fields: auxFields,
731
733
  persistence: auxEntity.persistence
732
734
  });
@@ -1015,10 +1017,25 @@ var OrbitalServerRuntime = class {
1015
1017
  this.persistence.registerEntity({
1016
1018
  name: entity.name,
1017
1019
  id: entity.id,
1020
+ collection: entity.collection,
1018
1021
  fields,
1019
1022
  persistence: entity.persistence
1020
1023
  });
1021
1024
  }
1025
+ for (const auxRef of registered.schema.auxiliaryEntities ?? []) {
1026
+ if (typeof auxRef === "string" || isEntityCall(auxRef)) continue;
1027
+ if (!auxRef.name || !auxRef.fields) continue;
1028
+ const auxFields = auxRef.fields.filter(
1029
+ (f) => typeof f.name === "string" && f.name.length > 0
1030
+ );
1031
+ this.persistence.registerEntity({
1032
+ name: auxRef.name,
1033
+ id: auxRef.id,
1034
+ collection: auxRef.collection,
1035
+ fields: auxFields,
1036
+ persistence: auxRef.persistence
1037
+ });
1038
+ }
1022
1039
  }
1023
1040
  }
1024
1041
  /**
@@ -1613,8 +1630,9 @@ var OrbitalServerRuntime = class {
1613
1630
  const readPolicy = this.resolvedSchema ? entityAccessPolicies(this.resolvedSchema, fetchEntityType)?.read : void 0;
1614
1631
  const accessBindings = { user: bindingsRef?.user, payload: bindingsRef?.payload, config: bindingsRef?.config };
1615
1632
  if (options?.id) {
1616
- const entity = await this.persistence.getById(fetchEntityType, options.id);
1617
- if (entity && applyRowAccess([entity], readPolicy, void 0, accessBindings).length > 0) {
1633
+ const stored = await this.persistence.getById(fetchEntityType, options.id);
1634
+ if (stored && applyRowAccess([stored], readPolicy, void 0, accessBindings).length > 0) {
1635
+ const entity = { ...stored };
1618
1636
  if (options?.include && options.include.length > 0) {
1619
1637
  await this.populateRelations([entity], fetchEntityType, options.include);
1620
1638
  }
@@ -1638,6 +1656,7 @@ var OrbitalServerRuntime = class {
1638
1656
  entities = entities.slice(0, options.limit);
1639
1657
  }
1640
1658
  if (options?.include && options.include.length > 0) {
1659
+ entities = entities.map((row) => ({ ...row }));
1641
1660
  await this.populateRelations(entities, fetchEntityType, options.include);
1642
1661
  }
1643
1662
  fetchedData[fetchEntityType] = entities;
@@ -355,7 +355,7 @@ function processEvent(options) {
355
355
  let lastFailedGuardTransition;
356
356
  for (const transition of candidates) {
357
357
  smLog.debug("processEvent", { trait: trait.name, event: normalizedEvent, currentState: traitState.currentState, to: transition.to });
358
- if (!transition.guard) {
358
+ if (transition.guard === void 0 || transition.guard === null) {
359
359
  return {
360
360
  executed: true,
361
361
  newState: transition.to,
@@ -2132,8 +2132,14 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2132
2132
  stores = /* @__PURE__ */ new Map();
2133
2133
  schemas = /* @__PURE__ */ new Map();
2134
2134
  idCounters = /* @__PURE__ */ new Map();
2135
- /** entityId -> normalized store name, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
2135
+ /** entityId -> store key, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
2136
2136
  storeNameById = /* @__PURE__ */ new Map();
2137
+ /** normalized entity name -> store key (the declared collection, else the name).
2138
+ * Entities sharing a `persistent:` collection resolve to the same store. */
2139
+ storeKeyByEntity = /* @__PURE__ */ new Map();
2140
+ /** store key -> the first registrant's entity name, used as the minted-id label
2141
+ * so rows in a shared collection carry one consistent id family. */
2142
+ idLabelByStoreKey = /* @__PURE__ */ new Map();
2137
2143
  config;
2138
2144
  /**
2139
2145
  * Every (entity, row id, column) cell `seed()` stamped with `config.ownerId`.
@@ -2213,19 +2219,27 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2213
2219
  // ============================================================================
2214
2220
  // Store Management
2215
2221
  // ============================================================================
2216
- getStore(entityName) {
2222
+ /** Resolve an entity name to its store key: the declared collection when the
2223
+ * entity is registered, the lowercased name otherwise (unregistered ad-hoc
2224
+ * creates keep working). */
2225
+ resolveStoreKey(entityName) {
2217
2226
  const normalized = entityName.toLowerCase();
2218
- if (!this.stores.has(normalized)) {
2219
- this.stores.set(normalized, /* @__PURE__ */ new Map());
2220
- this.idCounters.set(normalized, 0);
2227
+ return this.storeKeyByEntity.get(normalized) ?? normalized;
2228
+ }
2229
+ getStore(entityName) {
2230
+ const key = this.resolveStoreKey(entityName);
2231
+ if (!this.stores.has(key)) {
2232
+ this.stores.set(key, /* @__PURE__ */ new Map());
2233
+ this.idCounters.set(key, 0);
2221
2234
  }
2222
- return this.stores.get(normalized);
2235
+ return this.stores.get(key);
2223
2236
  }
2224
2237
  nextId(entityName) {
2225
- const normalized = entityName.toLowerCase();
2226
- const counter = (this.idCounters.get(normalized) ?? 0) + 1;
2227
- this.idCounters.set(normalized, counter);
2228
- return `${this.capitalizeFirst(entityName)} Id ${counter}`;
2238
+ const key = this.resolveStoreKey(entityName);
2239
+ const counter = (this.idCounters.get(key) ?? 0) + 1;
2240
+ this.idCounters.set(key, counter);
2241
+ const label = this.idLabelByStoreKey.get(key) ?? entityName;
2242
+ return `${this.capitalizeFirst(label)} Id ${counter}`;
2229
2243
  }
2230
2244
  // ============================================================================
2231
2245
  // Schema & Seeding
@@ -2237,12 +2251,20 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2237
2251
  */
2238
2252
  registerEntity(schema, seedCount) {
2239
2253
  const normalized = schema.name.toLowerCase();
2254
+ const storeKey = (schema.collection ?? schema.name).toLowerCase();
2255
+ this.storeKeyByEntity.set(normalized, storeKey);
2256
+ if (!this.idLabelByStoreKey.has(storeKey)) {
2257
+ this.idLabelByStoreKey.set(storeKey, schema.name);
2258
+ }
2240
2259
  this.schemas.set(normalized, schema);
2241
2260
  if (schema.id) {
2242
- this.storeNameById.set(schema.id, normalized);
2261
+ this.storeNameById.set(schema.id, storeKey);
2243
2262
  }
2263
+ const alreadySeeded = (this.stores.get(storeKey)?.size ?? 0) > 0;
2244
2264
  if (schema.seedData && schema.seedData.length > 0) {
2245
2265
  this.seedFromInstances(schema.name, schema.seedData);
2266
+ } else if (alreadySeeded) {
2267
+ this.backfillFields(storeKey, schema);
2246
2268
  } else {
2247
2269
  const requested = seedCount ?? this.config.defaultSeedCount ?? 6;
2248
2270
  const count = sampleRowCount(
@@ -2261,36 +2283,63 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2261
2283
  * of nested-tree atoms (e.g. std-thread-comments-linear with ThreadPost.
2262
2284
  * replies → [ThreadPost]) render empty reply cards.
2263
2285
  *
2264
- * For self-referential relations, each row gets 2–4 sibling IDs (excluding
2265
- * self). For cross-entity relations, IDs are picked from the target store.
2266
- * The runtime caps recursion at depth=2 in `populateRelations`, so
2267
- * grandparent-of-self cycles render two levels deep then stop.
2286
+ * Cross-entity relations pick random IDs from the target store. A
2287
+ * SELF-referential `one`-cardinality relation (a parent column like
2288
+ * `Tag.parentId : Tag`) is linked deterministically instead: row 0 stays a
2289
+ * root (`""`) and row *i* parents to row ⌊(i−1)/2⌋ — a proper forest with
2290
+ * real roots and no cycles, so tree views and `parentId = ""` root fetches
2291
+ * render sensibly. Self-referential `many` relations keep the 2–4 random
2292
+ * sibling IDs (excluding self). The runtime caps recursion at depth=2 in
2293
+ * `populateRelations`, so deep chains render two levels then stop.
2294
+ *
2295
+ * Entities sharing a collection are linked once per STORE, over the union
2296
+ * of their declared relation fields (first declarer of a field name wins).
2268
2297
  */
2269
2298
  linkRelationFields() {
2299
+ const relationFieldsByStore = /* @__PURE__ */ new Map();
2270
2300
  for (const [normalizedName, schema] of this.schemas) {
2271
- const store = this.stores.get(normalizedName);
2272
- if (!store) continue;
2273
- const relationFields = schema.fields.filter(
2274
- (f) => f.type === "relation"
2275
- );
2276
- if (relationFields.length === 0) continue;
2277
- for (const row of store.values()) {
2278
- for (const field of relationFields) {
2301
+ const storeKey = this.storeKeyByEntity.get(normalizedName) ?? normalizedName;
2302
+ let fieldMap = relationFieldsByStore.get(storeKey);
2303
+ if (!fieldMap) {
2304
+ fieldMap = /* @__PURE__ */ new Map();
2305
+ relationFieldsByStore.set(storeKey, fieldMap);
2306
+ }
2307
+ for (const f of schema.fields) {
2308
+ if (((candidate) => candidate.type === "relation")(f) && !fieldMap.has(f.name)) {
2309
+ fieldMap.set(f.name, f);
2310
+ }
2311
+ }
2312
+ }
2313
+ for (const [storeKey, fieldMap] of relationFieldsByStore) {
2314
+ const store = this.stores.get(storeKey);
2315
+ if (!store || fieldMap.size === 0) continue;
2316
+ const rows = [...store.values()];
2317
+ for (const field of fieldMap.values()) {
2318
+ const targetKey = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? this.resolveStoreKey(field.relation.entity);
2319
+ const targetStore = this.stores.get(targetKey);
2320
+ if (!targetStore || targetStore.size === 0) continue;
2321
+ const sameStore = targetStore === store;
2322
+ const cardinality = field.relation.cardinality ?? "many";
2323
+ if (sameStore && (cardinality === "one" || cardinality === "many-to-one")) {
2324
+ rows.forEach((row, i) => {
2325
+ if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId) {
2326
+ return;
2327
+ }
2328
+ row[field.name] = i === 0 ? "" : rows[Math.floor((i - 1) / 2)]["id"];
2329
+ });
2330
+ continue;
2331
+ }
2332
+ for (const row of rows) {
2279
2333
  if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId) {
2280
2334
  continue;
2281
2335
  }
2282
- const targetNormalized = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? field.relation.entity.toLowerCase();
2283
- const targetStore = this.stores.get(targetNormalized);
2284
- if (!targetStore || targetStore.size === 0) continue;
2285
2336
  const selfId = row["id"];
2286
- const sameStore = targetStore === store;
2287
2337
  const eligible = [];
2288
2338
  for (const id of targetStore.keys()) {
2289
2339
  if (sameStore && id === selfId) continue;
2290
2340
  eligible.push(id);
2291
2341
  }
2292
2342
  if (eligible.length === 0) continue;
2293
- const cardinality = field.relation.cardinality ?? "many";
2294
2343
  if (cardinality === "one" || cardinality === "many-to-one") {
2295
2344
  row[field.name] = randomArrayElement(eligible);
2296
2345
  } else {
@@ -2301,6 +2350,43 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2301
2350
  }
2302
2351
  }
2303
2352
  }
2353
+ /**
2354
+ * Fill fields a late-registering sibling schema declares that the shared
2355
+ * store's existing rows lack. The first registrant on a collection seeds
2356
+ * with ITS field list; a sibling declaring extra columns (e.g. `Tag.parentId`
2357
+ * arriving after `WikiTagRef {id,name}`) would otherwise read `undefined`
2358
+ * where its filters expect a value. Values come from the same canonical
2359
+ * sample policy as seeding; relation fields are then linked by the global
2360
+ * `linkRelationFields` pass that follows registration.
2361
+ */
2362
+ backfillFields(storeKey, schema) {
2363
+ const store = this.stores.get(storeKey);
2364
+ if (!store) return;
2365
+ const candidates = schema.fields.filter(
2366
+ (f) => f.name !== "id" && f.name !== "createdAt" && f.name !== "updatedAt"
2367
+ );
2368
+ if (candidates.length === 0) return;
2369
+ let index = 0;
2370
+ for (const row of store.values()) {
2371
+ index += 1;
2372
+ const absent = candidates.filter((f) => row[f.name] === void 0);
2373
+ if (absent.length === 0) continue;
2374
+ const sample = sampleRow(
2375
+ { name: schema.name, persistence: schema.persistence, fields: absent },
2376
+ { index, strategy: "seeded", persistence: schema.persistence }
2377
+ );
2378
+ for (const f of absent) {
2379
+ if (sample[f.name] !== void 0) {
2380
+ row[f.name] = sample[f.name];
2381
+ }
2382
+ }
2383
+ }
2384
+ mockLog.debug("mock:backfill", {
2385
+ storeKey,
2386
+ entity: schema.name,
2387
+ fields: candidates.map((f) => f.name)
2388
+ });
2389
+ }
2304
2390
  /**
2305
2391
  * Seed an entity with pre-authored instance data.
2306
2392
  */
@@ -2326,6 +2412,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2326
2412
  seed(entityName, fields, count, persistence) {
2327
2413
  const store = this.getStore(entityName);
2328
2414
  const normalized = entityName.toLowerCase();
2415
+ const storeKey = this.resolveStoreKey(entityName);
2329
2416
  if (this.config.debug) {
2330
2417
  mockLog.debug("seeding", { count, entity: entityName });
2331
2418
  }
@@ -2337,7 +2424,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2337
2424
  if (ownerId && ownerCols.length > 0 && i % 2 === 0) {
2338
2425
  for (const col of ownerCols) {
2339
2426
  item[col] = ownerId;
2340
- this.ownerStampedCells.push({ entity: normalized, id: item.id, column: col });
2427
+ this.ownerStampedCells.push({ entity: storeKey, id: item.id, column: col });
2341
2428
  }
2342
2429
  }
2343
2430
  store.set(item.id, item);
@@ -2467,9 +2554,9 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2467
2554
  * Clear all data for an entity.
2468
2555
  */
2469
2556
  clear(entityName) {
2470
- const normalized = entityName.toLowerCase();
2471
- this.stores.delete(normalized);
2472
- this.idCounters.delete(normalized);
2557
+ const key = this.resolveStoreKey(entityName);
2558
+ this.stores.delete(key);
2559
+ this.idCounters.delete(key);
2473
2560
  }
2474
2561
  /** Clear all data + re-anchor the PRNG so the next seed loop reproduces
2475
2562
  * identical rows. Hermetic-frame mode calls this between every step
@@ -2478,6 +2565,9 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2478
2565
  this.stores.clear();
2479
2566
  this.idCounters.clear();
2480
2567
  this.ownerStampedCells = [];
2568
+ this.storeKeyByEntity.clear();
2569
+ this.idLabelByStoreKey.clear();
2570
+ this.storeNameById.clear();
2481
2571
  this.resetFakerSeed();
2482
2572
  mockLog.debug("mock:adapter:clearAll", { reanchored: this.config.seed });
2483
2573
  }
@@ -3448,16 +3538,50 @@ function renameEventsInRenderUiConfig(node, rename) {
3448
3538
  return next;
3449
3539
  }
3450
3540
  function renameEventsInEffects(effects, rename) {
3451
- return effects.map((effect) => {
3452
- if (!Array.isArray(effect)) return effect;
3453
- if (effect[0] === "render-ui" && effect.length >= 3) {
3454
- const slot = effect[1];
3455
- const config = effect[2];
3456
- const nextConfig = renameEventsInRenderUiConfig(config, rename);
3457
- return [effect[0], slot, nextConfig, ...effect.slice(3)];
3458
- }
3459
- return effect;
3460
- });
3541
+ return effects.map((effect) => renameEventRefsInNode(effect, rename));
3542
+ }
3543
+ var EMIT_OPTION_SLOTS = /* @__PURE__ */ new Set(["success", "failure", "on_message", "on_change"]);
3544
+ function renameEventRefsInNode(node, rename) {
3545
+ if (node === null || node === void 0) return node;
3546
+ if (Array.isArray(node)) {
3547
+ if (node[0] === "render-ui" && node.length >= 3) {
3548
+ const nextConfig = renameEventsInRenderUiConfig(node[2], rename);
3549
+ return [
3550
+ node[0],
3551
+ node[1],
3552
+ nextConfig,
3553
+ ...node.slice(3).map((x) => renameEventRefsInNode(x, rename))
3554
+ ];
3555
+ }
3556
+ const out = node.map((x) => renameEventRefsInNode(x, rename));
3557
+ if (node[0] === "emit" && typeof node[1] === "string") {
3558
+ out[1] = rename(node[1]) ?? node[1];
3559
+ }
3560
+ return out;
3561
+ }
3562
+ if (typeof node === "object") {
3563
+ const obj = node;
3564
+ const next = {};
3565
+ for (const [k, v] of Object.entries(obj)) {
3566
+ if (k === "emit") {
3567
+ if (typeof v === "string") {
3568
+ next[k] = rename(v) ?? v;
3569
+ } else if (v !== null && typeof v === "object" && !Array.isArray(v)) {
3570
+ const slots = {};
3571
+ for (const [slot, slotV] of Object.entries(v)) {
3572
+ slots[slot] = EMIT_OPTION_SLOTS.has(slot) && typeof slotV === "string" ? rename(slotV) ?? slotV : renameEventRefsInNode(slotV, rename);
3573
+ }
3574
+ next[k] = slots;
3575
+ } else {
3576
+ next[k] = renameEventRefsInNode(v, rename);
3577
+ }
3578
+ continue;
3579
+ }
3580
+ next[k] = renameEventRefsInNode(v, rename);
3581
+ }
3582
+ return next;
3583
+ }
3584
+ return node;
3461
3585
  }
3462
3586
  var REBIND_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity"]);
3463
3587
  var ID_ENTITY_PROPS = /* @__PURE__ */ new Set(["entity", "entityType", "source"]);
@@ -3751,13 +3875,12 @@ function applyEventRenames(trait, renames) {
3751
3875
  if (!renames || Object.keys(renames).length === 0) return trait;
3752
3876
  const rename = (k) => k !== void 0 && k in renames ? renames[k] : k;
3753
3877
  const sm = trait.stateMachine;
3754
- if (!sm) return trait;
3755
- const nextTransitions = (sm.transitions ?? []).map((t) => {
3878
+ const nextTransitions = (sm?.transitions ?? []).map((t) => {
3756
3879
  const nextEvent = rename(t.event) ?? t.event;
3757
3880
  const nextEffects = t.effects ? renameEventsInEffects(t.effects, rename) : t.effects;
3758
3881
  return { ...t, event: nextEvent, effects: nextEffects };
3759
3882
  });
3760
- const nextEvents = (sm.events ?? []).map((e) => {
3883
+ const nextEvents = (sm?.events ?? []).map((e) => {
3761
3884
  const newKey = rename(e.key);
3762
3885
  if (newKey === e.key) return e;
3763
3886
  return { ...e, key: newKey ?? e.key };
@@ -3767,13 +3890,30 @@ function applyEventRenames(trait, renames) {
3767
3890
  const newEvent = rename(em.event);
3768
3891
  return newEvent === em.event ? em : { ...em, event: newEvent ?? em.event };
3769
3892
  });
3893
+ const nextTicks = (trait.ticks ?? []).map(
3894
+ (tk) => tk.effects ? {
3895
+ ...tk,
3896
+ effects: renameEventsInEffects(
3897
+ tk.effects,
3898
+ rename
3899
+ )
3900
+ } : tk
3901
+ );
3902
+ const nextListens = (trait.listens ?? []).map((l) => {
3903
+ const sourced = l.source !== void 0 && l.source.kind !== "any";
3904
+ const nextEvent = sourced ? l.event : rename(l.event) ?? l.event;
3905
+ const nextTriggers = rename(l.triggers) ?? l.triggers;
3906
+ return nextEvent === l.event && nextTriggers === l.triggers ? l : { ...l, event: nextEvent, triggers: nextTriggers };
3907
+ });
3770
3908
  return {
3771
3909
  ...trait,
3772
- stateMachine: {
3910
+ stateMachine: sm ? {
3773
3911
  ...sm,
3774
3912
  transitions: nextTransitions,
3775
3913
  events: nextEvents
3776
- },
3914
+ } : sm,
3915
+ ...trait.ticks ? { ticks: nextTicks } : {},
3916
+ ...trait.listens ? { listens: nextListens } : {},
3777
3917
  emits: nextEmits
3778
3918
  };
3779
3919
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { g as RuntimePatternValue, B as BindingContext, f as EvaluationContextExtensions, P as PatternProps, E as EffectHandlers, h as EffectContext, i as ExecutionEnvironment, j as EffectResult, S as ServiceCallContext, T as TraitDefinition } from './types-BaD_ox7e.js';
2
2
  export { k as BrowserFileMeta, l as BrowserFilePickerOptions, m as BrowserGeolocationOptions, n as BrowserGeolocationPosition, C as ConfigContext, o as Effect, a as EventListener, H as HANDLER_MANIFEST, I as IEventBus, b as RuntimeConfig, R as RuntimeEvent, d as TraitState, c as TransitionObserver, e as TransitionResult, U as Unsubscribe } from './types-BaD_ox7e.js';
3
- import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital, P as PersistenceAdapter } from './OrbitalServerRuntime-CZHHxo_A.js';
4
- export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as collectDeclaredConfigDefaults, q as collectDeclaredEntityDefaults, r as createInitialTraitState, s as findInitialState, t as findTransition, u as getIsolatedCollectionName, v as getNamespacedEvent, w as isBrowser, x as isElectron, y as isNamespacedEvent, z as isNode, A as normalizeEventKey, B as parseNamespacedEvent, C as preprocessSchema, D as processEvent } from './OrbitalServerRuntime-CZHHxo_A.js';
3
+ import { U as UnifiedLoaderOptions, S as SchemaLoader, I as ImportChainLike, L as LoadResult, a as LoadedSchema, b as LoadedOrbital, P as PersistenceAdapter } from './OrbitalServerRuntime-nqwGldMC.js';
4
+ export { E as EntitySharingMap, c as EventBus, d as EventNamespaceMap, e as InMemoryPersistence, O as OrbitalEventRequest, f as OrbitalEventResponse, g as OrbitalServerRuntimeConfig, h as PreprocessOptions, i as PreprocessResult, j as PreprocessedSchema, k as ProcessEventOptions, R as RegisteredOrbital, l as RuntimeOrbital, m as RuntimeOrbitalSchema, n as RuntimeTrait, o as StateMachineManager, p as collectDeclaredConfigDefaults, q as collectDeclaredEntityDefaults, r as createInitialTraitState, s as findInitialState, t as findTransition, u as getIsolatedCollectionName, v as getNamespacedEvent, w as isBrowser, x as isElectron, y as isNamespacedEvent, z as isNode, A as normalizeEventKey, B as parseNamespacedEvent, C as preprocessSchema, D as processEvent } from './OrbitalServerRuntime-nqwGldMC.js';
5
5
  import { EvaluationContext, SExpressionEvaluator } from '@almadar/evaluator';
6
6
  export { EvaluationContext, createMinimalContext } from '@almadar/evaluator';
7
7
  import { RenderBindingMarker, SExpr, RuntimeValue, TraitConfigObject, EventPayload, PatternConfig, EntityId, EntityField, EntityRow, EntityPersistence, ServiceParams, EntityAccessPolicies, PayloadField, OrbitalDefinition, OrbitalSchema } from '@almadar/core';
@@ -571,6 +571,15 @@ interface EntitySchema {
571
571
  name: string;
572
572
  /** V4 dual-carry id sibling of `name` — optional until the Phase-7 flip. */
573
573
  id?: EntityId;
574
+ /**
575
+ * Declared `persistent: <collection>` name. Entities declaring the SAME
576
+ * collection share ONE store — a shadow entity (`WikiTagRef [persistent:
577
+ * tags]`) reads the very rows its sibling (`Tag [persistent: tags]`) seeds,
578
+ * matching the compiled path's dedup-by-collection (orbital-shell-typescript
579
+ * seed.rs) and docs/Almadar_Entity.md's collection rule. Absent → the store
580
+ * is keyed by entity name, as before.
581
+ */
582
+ collection?: string;
574
583
  fields: NamedEntityField[];
575
584
  /** Pre-authored instance data from the schema (used instead of generated mocks) */
576
585
  seedData?: EntityRow[];
@@ -610,8 +619,14 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
610
619
  private stores;
611
620
  private schemas;
612
621
  private idCounters;
613
- /** entityId -> normalized store name, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
622
+ /** entityId -> store key, so relation lookups can prefer the id sibling over `relation.entity` name-matching. */
614
623
  private storeNameById;
624
+ /** normalized entity name -> store key (the declared collection, else the name).
625
+ * Entities sharing a `persistent:` collection resolve to the same store. */
626
+ private storeKeyByEntity;
627
+ /** store key -> the first registrant's entity name, used as the minted-id label
628
+ * so rows in a shared collection carry one consistent id family. */
629
+ private idLabelByStoreKey;
615
630
  private config;
616
631
  /**
617
632
  * Every (entity, row id, column) cell `seed()` stamped with `config.ownerId`.
@@ -656,6 +671,10 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
656
671
  * reseed produces row set A, the second produces row set B, and
657
672
  * diff observers see all rows as "changed" between frames. */
658
673
  resetFakerSeed(): void;
674
+ /** Resolve an entity name to its store key: the declared collection when the
675
+ * entity is registered, the lowercased name otherwise (unregistered ad-hoc
676
+ * creates keep working). */
677
+ private resolveStoreKey;
659
678
  private getStore;
660
679
  private nextId;
661
680
  /**
@@ -672,12 +691,29 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
672
691
  * of nested-tree atoms (e.g. std-thread-comments-linear with ThreadPost.
673
692
  * replies → [ThreadPost]) render empty reply cards.
674
693
  *
675
- * For self-referential relations, each row gets 2–4 sibling IDs (excluding
676
- * self). For cross-entity relations, IDs are picked from the target store.
677
- * The runtime caps recursion at depth=2 in `populateRelations`, so
678
- * grandparent-of-self cycles render two levels deep then stop.
694
+ * Cross-entity relations pick random IDs from the target store. A
695
+ * SELF-referential `one`-cardinality relation (a parent column like
696
+ * `Tag.parentId : Tag`) is linked deterministically instead: row 0 stays a
697
+ * root (`""`) and row *i* parents to row ⌊(i−1)/2⌋ — a proper forest with
698
+ * real roots and no cycles, so tree views and `parentId = ""` root fetches
699
+ * render sensibly. Self-referential `many` relations keep the 2–4 random
700
+ * sibling IDs (excluding self). The runtime caps recursion at depth=2 in
701
+ * `populateRelations`, so deep chains render two levels then stop.
702
+ *
703
+ * Entities sharing a collection are linked once per STORE, over the union
704
+ * of their declared relation fields (first declarer of a field name wins).
679
705
  */
680
706
  linkRelationFields(): void;
707
+ /**
708
+ * Fill fields a late-registering sibling schema declares that the shared
709
+ * store's existing rows lack. The first registrant on a collection seeds
710
+ * with ITS field list; a sibling declaring extra columns (e.g. `Tag.parentId`
711
+ * arriving after `WikiTagRef {id,name}`) would otherwise read `undefined`
712
+ * where its filters expect a value. Values come from the same canonical
713
+ * sample policy as seeding; relation fields are then linked by the global
714
+ * `linkRelationFields` pass that follows registration.
715
+ */
716
+ private backfillFields;
681
717
  /**
682
718
  * Seed an entity with pre-authored instance data.
683
719
  */
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { EffectExecutor } from './chunk-QY5EUGR2.js';
2
- export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes } from './chunk-QY5EUGR2.js';
1
+ import { EffectExecutor } from './chunk-C7SIRVGS.js';
2
+ export { EffectExecutor, EventBus, HANDLER_MANIFEST, InMemoryPersistence, MockPersistenceAdapter, StateMachineManager, TickScheduler, buildEmitsFromTraits, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, createInitialTraitState, createMockPersistence, createTestExecutor, createTickScheduler, createUnifiedLoader, findInitialState, findTransition, formatPayloadValidationError, getIsolatedCollectionName, getNamespacedEvent, isBrowser, isElectron, isNamespacedEvent, isNode, isValidDurationString, normalizeCallSiteConfigToValues, normalizeEventKey, parseDurationString, parseNamespacedEvent, preprocessSchema, processEvent, validateEventPayload, validatePayloadShapes } from './chunk-C7SIRVGS.js';
3
3
  export { cronMatches, cronMinuteKey, isValidCronExpression, parseCron, parseCronField } from './chunk-OU3ITB5S.js';
4
4
  import { createContextFromBindings, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
5
5
  export { CALLSITE_PAYLOAD_PREFIX, applyRowAccess, checkMutationAccess, containsBindings, createContextFromBindings, createMinimalContext, deferEntityBindings, extractBindings, interpolateProps, interpolateValue, resolveCallSitePayloadCaptures } from './chunk-ZJ62H3ES.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/runtime",
3
- "version": "6.61.0",
3
+ "version": "6.63.0",
4
4
  "description": "Interpreted runtime for Almadar orbital applications (OrbitalServerRuntime)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,11 +57,11 @@
57
57
  "access": "public"
58
58
  },
59
59
  "dependencies": {
60
- "@almadar/core": "^10.70.0",
60
+ "@almadar/core": "^10.73.0",
61
61
  "@almadar/evaluator": "^2.42.0",
62
62
  "@almadar/logger": "^1.11.0",
63
63
  "@almadar/server": "^2.38.0",
64
- "@almadar/std": "^16.186.0"
64
+ "@almadar/std": "^16.192.0"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "express": "^5.0.0"
@@ -72,7 +72,7 @@
72
72
  }
73
73
  },
74
74
  "devDependencies": {
75
- "@almadar/eslint-plugin": "^2.15.0",
75
+ "@almadar/eslint-plugin": "^2.16.0",
76
76
  "@types/express": "^5.0.0",
77
77
  "@types/node": "^20.0.0",
78
78
  "@typescript-eslint/parser": "8.65.0",