@almadar/runtime 6.63.0 → 6.65.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.
@@ -1416,6 +1416,29 @@ declare class OrbitalServerRuntime {
1416
1416
  * instead of re-seeding.
1417
1417
  */
1418
1418
  setDefaultUser(user: UserContext | undefined): void;
1419
+ /** setDefaultUser can land before register() finishes resolving the schema
1420
+ * (a dev host pins a persona right after boot). An ungated restamp there
1421
+ * would hand a read-only persona authorship of drafts, so the restamp is
1422
+ * DEFERRED until the schema exists — register()/registerSync() flush it. */
1423
+ private pendingOwnerRestamp;
1424
+ /**
1425
+ * Re-point the mock seeder's owner stamps at `user`. The adapter's
1426
+ * installed owner gate (see installOwnerGate) decides per row whether the
1427
+ * persona could have CREATED it — a reader keeps seeing the store's
1428
+ * published face instead of inheriting draft ownership from the demo
1429
+ * stamper. Deferred when the schema (and so the gate) isn't resolved yet.
1430
+ */
1431
+ private restampOwnerGated;
1432
+ /**
1433
+ * Install the schema's `@create` directives as the mock seeder's owner
1434
+ * gate — the single authority on who may author a row. Consulted by BOTH
1435
+ * `restampOwner` (persona switches) and `seed()` (a client connect
1436
+ * re-registers, and the deterministic reseed re-stamps with the current
1437
+ * ownerId — gating only the switch path would leak right there). Reads the
1438
+ * CURRENT default user at evaluation time so one installation covers every
1439
+ * later switch. Then flushes any switch that arrived before the schema.
1440
+ */
1441
+ private installOwnerGate;
1419
1442
  /** The viewer a dev host is currently presenting the app as. */
1420
1443
  getDefaultUser(): UserContext | undefined;
1421
1444
  /**
@@ -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-nqwGldMC.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-DOJDoqXu.js';
3
3
  import './types-BaD_ox7e.js';
4
4
  import '@almadar/core';
@@ -1,7 +1,7 @@
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';
1
+ import { createTickScheduler, EventBus, createUnifiedLoader, MockPersistenceAdapter, InMemoryPersistence, preprocessSchema, normalizeCallSiteConfigToValues, StateMachineManager, parseDurationString, validateEventPayload, formatPayloadValidationError, collectDeclaredConfigDefaults, collectDeclaredEntityDefaults, EffectExecutor } from './chunk-LMYD5UB3.js';
2
+ export { InMemoryPersistence, collectDeclaredConfigDefaults } from './chunk-LMYD5UB3.js';
3
3
  import { isValidCronExpression } from './chunk-OU3ITB5S.js';
4
- import { createContextFromBindings, resolveCallSitePayloadCaptures, applyRowAccess, checkMutationAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
4
+ import { createContextFromBindings, checkMutationAccess, resolveCallSitePayloadCaptures, applyRowAccess, accessDeniedMessage } from './chunk-ZJ62H3ES.js';
5
5
  import './chunk-T4VDAB4C.js';
6
6
  import './chunk-SCRAHWOC.js';
7
7
  import './chunk-MLKGABMK.js';
@@ -9,7 +9,7 @@ import { createLogger } from '@almadar/logger';
9
9
  import * as nodeModule from 'module';
10
10
  import { evaluateListenPayloadExpr, evaluateGuard, evaluate } from '@almadar/evaluator';
11
11
  import { DEFAULT_VIEWER, buildResolvedTraitConfigs, isInlineTrait, isEntityCall, applyListenPayloadMapping, personaFromIdentityRow, normalizeUserContext, isRuntimeEntity, isPageReference } from '@almadar/core';
12
- import { ownerFieldsFromSchema, identityEntityName, entityAccessPolicies } from '@almadar/core/mock';
12
+ import { ownerFieldsFromSchema, identityEntityName, entityAccessPoliciesByStoreKey, entityAccessPolicies } from '@almadar/core/mock';
13
13
  import { getPatternFieldsContract } from '@almadar/core/patterns';
14
14
 
15
15
  // src/identity/routing.ts
@@ -355,6 +355,7 @@ var OrbitalServerRuntime = class {
355
355
  this.setupTicks();
356
356
  this.resolvedSchema = schema;
357
357
  this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
358
+ this.installOwnerGate();
358
359
  }
359
360
  /**
360
361
  * Teach the mock seeder which columns hold the viewer's id, derived from the
@@ -391,7 +392,7 @@ var OrbitalServerRuntime = class {
391
392
  this.setupTicks();
392
393
  this.resolvedSchema = schema;
393
394
  this.resolvedTraitConfigs = buildResolvedTraitConfigs(schema);
394
- this.applyIdentityOwnerFields(schema);
395
+ this.installOwnerGate();
395
396
  }
396
397
  /**
397
398
  * Returns the schema that this runtime is currently executing, post-
@@ -1065,7 +1066,55 @@ var OrbitalServerRuntime = class {
1065
1066
  const previousId = this.config.defaultUser?.id;
1066
1067
  this.config.defaultUser = user;
1067
1068
  if (this.persistence instanceof MockPersistenceAdapter && user?.id !== void 0 && user.id !== previousId) {
1068
- this.persistence.restampOwner(user.id);
1069
+ this.restampOwnerGated(user);
1070
+ }
1071
+ }
1072
+ /** setDefaultUser can land before register() finishes resolving the schema
1073
+ * (a dev host pins a persona right after boot). An ungated restamp there
1074
+ * would hand a read-only persona authorship of drafts, so the restamp is
1075
+ * DEFERRED until the schema exists — register()/registerSync() flush it. */
1076
+ pendingOwnerRestamp = false;
1077
+ /**
1078
+ * Re-point the mock seeder's owner stamps at `user`. The adapter's
1079
+ * installed owner gate (see installOwnerGate) decides per row whether the
1080
+ * persona could have CREATED it — a reader keeps seeing the store's
1081
+ * published face instead of inheriting draft ownership from the demo
1082
+ * stamper. Deferred when the schema (and so the gate) isn't resolved yet.
1083
+ */
1084
+ restampOwnerGated(user) {
1085
+ if (!(this.persistence instanceof MockPersistenceAdapter)) return;
1086
+ if (!this.resolvedSchema) {
1087
+ this.pendingOwnerRestamp = true;
1088
+ return;
1089
+ }
1090
+ this.persistence.restampOwner(user.id);
1091
+ }
1092
+ /**
1093
+ * Install the schema's `@create` directives as the mock seeder's owner
1094
+ * gate — the single authority on who may author a row. Consulted by BOTH
1095
+ * `restampOwner` (persona switches) and `seed()` (a client connect
1096
+ * re-registers, and the deterministic reseed re-stamps with the current
1097
+ * ownerId — gating only the switch path would leak right there). Reads the
1098
+ * CURRENT default user at evaluation time so one installation covers every
1099
+ * later switch. Then flushes any switch that arrived before the schema.
1100
+ */
1101
+ installOwnerGate() {
1102
+ if (!(this.persistence instanceof MockPersistenceAdapter) || !this.resolvedSchema) return;
1103
+ const policiesByStore = entityAccessPoliciesByStoreKey(this.resolvedSchema);
1104
+ this.persistence.setOwnerGate((storeKey, candidateRow) => {
1105
+ const user = this.config.defaultUser;
1106
+ if (!user) return true;
1107
+ return checkMutationAccess(candidateRow, policiesByStore.get(storeKey)?.create, { user });
1108
+ });
1109
+ this.persistence.setOwnerCandidateGate((storeKey, candidateRow, candidateIdentityRow) => {
1110
+ const persona = personaFromIdentityRow(candidateIdentityRow);
1111
+ if (!persona) return false;
1112
+ return checkMutationAccess(candidateRow, policiesByStore.get(storeKey)?.create, { user: persona });
1113
+ });
1114
+ if (this.pendingOwnerRestamp) {
1115
+ this.pendingOwnerRestamp = false;
1116
+ const user = this.config.defaultUser;
1117
+ if (user?.id !== void 0) this.restampOwnerGated(user);
1069
1118
  }
1070
1119
  }
1071
1120
  /** The viewer a dev host is currently presenting the app as. */
@@ -1351,14 +1351,21 @@ var EffectExecutor = class _EffectExecutor {
1351
1351
  try {
1352
1352
  if (action === "batch") {
1353
1353
  const operations = args[1];
1354
- await this.handlers.persist("batch", "", { operations });
1354
+ const batchSummary = await this.handlers.persist("batch", "", {
1355
+ operations
1356
+ });
1355
1357
  persistLog.debug("persist:success", {
1356
1358
  action,
1357
1359
  entityType: "batch",
1358
1360
  opCount: operations.length,
1359
1361
  willEmit: emitCfg?.success
1360
1362
  });
1361
- this.emitSuccess(emitCfg, "success", operations, true);
1363
+ this.emitSuccess(
1364
+ emitCfg,
1365
+ "success",
1366
+ batchSummary ?? { totalCount: operations.length },
1367
+ true
1368
+ );
1362
1369
  persistLog.debug("persist:emit-fired", { action, eventName: emitCfg?.success });
1363
1370
  } else {
1364
1371
  const entityType = args[1];
@@ -2128,6 +2135,12 @@ var mockLog = createLogger("almadar:runtime:mock");
2128
2135
  var DEFAULT_MOCK_SEED = 42;
2129
2136
  var SEED_REFERENCE_TIMESTAMP = "2024-01-01T00:00:00.000Z";
2130
2137
  var MS_PER_DAY = 864e5;
2138
+ function isRelationField(field) {
2139
+ return field.type === "relation";
2140
+ }
2141
+ function stampedCellKey(entity, id, column) {
2142
+ return `${entity}\0${id}\0${column}`;
2143
+ }
2131
2144
  var MockPersistenceAdapter = class _MockPersistenceAdapter {
2132
2145
  stores = /* @__PURE__ */ new Map();
2133
2146
  schemas = /* @__PURE__ */ new Map();
@@ -2191,21 +2204,108 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2191
2204
  * No-op when there is no new id, or it matches the current one — an
2192
2205
  * anonymous switch (`newOwnerId === undefined`) leaves existing stamps as
2193
2206
  * they are rather than clearing a non-nullable owner column.
2207
+ *
2208
+ * Both this re-labeling AND `seed()`'s original stamping consult the
2209
+ * `ownerGate` hook (see `setOwnerGate`) — a re-register reseeds identical
2210
+ * rows, so gating only the restamp path leaks the moment a client
2211
+ * connects and the deterministic reseed stamps with the updated ownerId.
2194
2212
  */
2195
2213
  restampOwner(newOwnerId) {
2196
2214
  const oldOwnerId = this.config.ownerId;
2197
2215
  this.config.ownerId = newOwnerId;
2198
2216
  if (!newOwnerId || newOwnerId === oldOwnerId) return;
2217
+ let skipped = 0;
2218
+ let fallbackStamps = 0;
2199
2219
  for (const cell of this.ownerStampedCells) {
2200
2220
  const row = this.stores.get(cell.entity)?.get(cell.id);
2201
- if (row) row[cell.column] = newOwnerId;
2221
+ if (!row) continue;
2222
+ if (this.ownerGate && !this.ownerGate(cell.entity, { ...row, [cell.column]: newOwnerId })) {
2223
+ const fallbackId = this.firstEligibleOwner(cell.entity, cell.column, row);
2224
+ if (fallbackId === void 0) {
2225
+ skipped++;
2226
+ continue;
2227
+ }
2228
+ row[cell.column] = fallbackId;
2229
+ fallbackStamps++;
2230
+ continue;
2231
+ }
2232
+ row[cell.column] = newOwnerId;
2202
2233
  }
2203
2234
  mockLog.debug("mock:owner-restamped", {
2204
2235
  from: oldOwnerId,
2205
2236
  to: newOwnerId,
2206
- cells: this.ownerStampedCells.length
2237
+ cells: this.ownerStampedCells.length,
2238
+ skipped,
2239
+ fallbackStamps
2207
2240
  });
2208
2241
  }
2242
+ /**
2243
+ * The host's answer to "may the current viewer OWN this row?" — evaluated
2244
+ * against the candidate row with the owner column already re-pointed,
2245
+ * `storeKey` being the collection key the stores are keyed by. The runtime
2246
+ * installs the registered schema's `@create` directive here (the single
2247
+ * authority on who authors; no role heuristics), so a read-only persona
2248
+ * never inherits authorship of drafts from the demo stamper — neither on a
2249
+ * persona switch (`restampOwner`) nor on a deterministic reseed (`seed`).
2250
+ * Absent hook = stamp unconditionally (pre-role-model behavior).
2251
+ */
2252
+ ownerGate;
2253
+ setOwnerGate(gate) {
2254
+ this.ownerGate = gate;
2255
+ }
2256
+ /**
2257
+ * The host's answer to "may THIS identity row own this candidate row?" —
2258
+ * evaluated per candidate rather than against the fixed default viewer, so
2259
+ * `linkRelationFields` and the eligible-owner fallback (see
2260
+ * `firstEligibleOwner`) can pick a policy-valid owner from the whole
2261
+ * identity roster instead of only ever checking the current default user.
2262
+ * Absent hook = no candidate filtering (pre-role-model behavior).
2263
+ */
2264
+ ownerCandidateGate;
2265
+ setOwnerCandidateGate(gate) {
2266
+ this.ownerCandidateGate = gate;
2267
+ }
2268
+ /**
2269
+ * The store an owner column's relation field targets, resolved the same
2270
+ * way `linkRelationFields` resolves any relation target (id sibling wins
2271
+ * over the `entity` name string) — but keyed by STORE, mirroring that
2272
+ * method's first-declarer-per-collection rule, so `seed()` and
2273
+ * `restampOwner()` can find it before a `linkRelationFields` pass has run
2274
+ * this registration. Undefined when no registered schema on `storeKey`
2275
+ * declares `column` as a relation field.
2276
+ */
2277
+ resolveOwnerColumnTargetStore(storeKey, column) {
2278
+ for (const [normalizedName, schema] of this.schemas) {
2279
+ if ((this.storeKeyByEntity.get(normalizedName) ?? normalizedName) !== storeKey) continue;
2280
+ const field = schema.fields.find((f) => f.name === column);
2281
+ if (field && isRelationField(field)) {
2282
+ return (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? this.resolveStoreKey(field.relation.entity);
2283
+ }
2284
+ }
2285
+ return void 0;
2286
+ }
2287
+ /**
2288
+ * The first identity-store row (seed order) whose persona is eligible, per
2289
+ * the installed `ownerCandidateGate`, to own `row`'s `column` in `storeKey`
2290
+ * — the deterministic replacement for "skip the stamp" when the CURRENT
2291
+ * viewer fails the `@create` policy (e.g. a reader-default persona against
2292
+ * an author|reviewer-only policy). Undefined when no gate is installed, the
2293
+ * column isn't a resolvable relation, or no identity row is eligible — the
2294
+ * caller keeps today's skip behavior in that case.
2295
+ */
2296
+ firstEligibleOwner(storeKey, column, row) {
2297
+ if (!this.ownerCandidateGate) return void 0;
2298
+ const identityStoreKey = this.resolveOwnerColumnTargetStore(storeKey, column);
2299
+ const identityStore = identityStoreKey ? this.stores.get(identityStoreKey) : void 0;
2300
+ if (!identityStore) return void 0;
2301
+ for (const identityRow of identityStore.values()) {
2302
+ const candidateId = identityRow["id"];
2303
+ if (this.ownerCandidateGate(storeKey, { ...row, [column]: candidateId }, identityRow)) {
2304
+ return candidateId;
2305
+ }
2306
+ }
2307
+ return void 0;
2308
+ }
2209
2309
  /** Re-anchor the PRNG to the configured seed. Called before every
2210
2310
  * re-seed loop so identical reseed sequences produce identical rows
2211
2311
  * (timestamps + generated fields). Without this, the first
@@ -2305,16 +2405,28 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2305
2405
  relationFieldsByStore.set(storeKey, fieldMap);
2306
2406
  }
2307
2407
  for (const f of schema.fields) {
2308
- if (((candidate) => candidate.type === "relation")(f) && !fieldMap.has(f.name)) {
2408
+ if (isRelationField(f) && !fieldMap.has(f.name)) {
2309
2409
  fieldMap.set(f.name, f);
2310
2410
  }
2311
2411
  }
2312
2412
  }
2413
+ const ownerColumnsByStore = /* @__PURE__ */ new Map();
2414
+ for (const { entity, field } of this.parsedOwnerFields()) {
2415
+ const storeKey = this.resolveStoreKey(entity);
2416
+ const set = ownerColumnsByStore.get(storeKey) ?? /* @__PURE__ */ new Set();
2417
+ set.add(field);
2418
+ ownerColumnsByStore.set(storeKey, set);
2419
+ }
2420
+ const stampedCellKeys = new Set(
2421
+ this.ownerStampedCells.map((cell) => stampedCellKey(cell.entity, cell.id, cell.column))
2422
+ );
2313
2423
  for (const [storeKey, fieldMap] of relationFieldsByStore) {
2314
2424
  const store = this.stores.get(storeKey);
2315
2425
  if (!store || fieldMap.size === 0) continue;
2316
2426
  const rows = [...store.values()];
2427
+ const ownerColumns = ownerColumnsByStore.get(storeKey);
2317
2428
  for (const field of fieldMap.values()) {
2429
+ const isOwnerColumn = ownerColumns?.has(field.name) ?? false;
2318
2430
  const targetKey = (field.relation.entityId && this.storeNameById.get(field.relation.entityId)) ?? this.resolveStoreKey(field.relation.entity);
2319
2431
  const targetStore = this.stores.get(targetKey);
2320
2432
  if (!targetStore || targetStore.size === 0) continue;
@@ -2322,7 +2434,7 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2322
2434
  const cardinality = field.relation.cardinality ?? "many";
2323
2435
  if (sameStore && (cardinality === "one" || cardinality === "many-to-one")) {
2324
2436
  rows.forEach((row, i) => {
2325
- if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId) {
2437
+ if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId || stampedCellKeys.has(stampedCellKey(storeKey, row["id"], field.name))) {
2326
2438
  return;
2327
2439
  }
2328
2440
  row[field.name] = i === 0 ? "" : rows[Math.floor((i - 1) / 2)]["id"];
@@ -2330,15 +2442,21 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2330
2442
  continue;
2331
2443
  }
2332
2444
  for (const row of rows) {
2333
- if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId) {
2445
+ const selfId = row["id"];
2446
+ if (this.config.ownerId !== void 0 && row[field.name] === this.config.ownerId || stampedCellKeys.has(stampedCellKey(storeKey, selfId, field.name))) {
2334
2447
  continue;
2335
2448
  }
2336
- const selfId = row["id"];
2337
- const eligible = [];
2449
+ let eligible = [];
2338
2450
  for (const id of targetStore.keys()) {
2339
2451
  if (sameStore && id === selfId) continue;
2340
2452
  eligible.push(id);
2341
2453
  }
2454
+ if (isOwnerColumn && this.ownerCandidateGate) {
2455
+ const gate = this.ownerCandidateGate;
2456
+ eligible = eligible.filter(
2457
+ (id) => gate(storeKey, { ...row, [field.name]: id }, targetStore.get(id))
2458
+ );
2459
+ }
2342
2460
  if (eligible.length === 0) continue;
2343
2461
  if (cardinality === "one" || cardinality === "many-to-one") {
2344
2462
  row[field.name] = randomArrayElement(eligible);
@@ -2416,13 +2534,22 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2416
2534
  if (this.config.debug) {
2417
2535
  mockLog.debug("seeding", { count, entity: entityName });
2418
2536
  }
2419
- const ownerCols = (this.config.ownerFields ?? []).map((pair) => pair.split(".")).filter(([ent]) => ent?.toLowerCase() === normalized).map(([, field]) => field).filter((field) => Boolean(field));
2537
+ const ownerCols = this.parsedOwnerFields().filter(({ entity }) => entity.toLowerCase() === normalized).map(({ field }) => field);
2420
2538
  const ownerId = this.config.ownerId;
2421
2539
  const generated = [];
2540
+ let fallbackStamps = 0;
2422
2541
  for (let i = 0; i < count; i++) {
2423
2542
  const item = this.generateMockItem(entityName, fields, i + 1, persistence);
2424
2543
  if (ownerId && ownerCols.length > 0 && i % 2 === 0) {
2425
2544
  for (const col of ownerCols) {
2545
+ if (this.ownerGate && !this.ownerGate(storeKey, { ...item, [col]: ownerId })) {
2546
+ const fallbackId = this.firstEligibleOwner(storeKey, col, item);
2547
+ if (fallbackId === void 0) continue;
2548
+ item[col] = fallbackId;
2549
+ this.ownerStampedCells.push({ entity: storeKey, id: item.id, column: col });
2550
+ fallbackStamps++;
2551
+ continue;
2552
+ }
2426
2553
  item[col] = ownerId;
2427
2554
  this.ownerStampedCells.push({ entity: storeKey, id: item.id, column: col });
2428
2555
  }
@@ -2433,7 +2560,23 @@ var MockPersistenceAdapter = class _MockPersistenceAdapter {
2433
2560
  updatedAt: typeof item.updatedAt === "string" ? item.updatedAt : ""
2434
2561
  });
2435
2562
  }
2436
- mockLog.debug("mock:seed", () => ({ entityName, count, idsAndTimestamps: JSON.stringify(generated) }));
2563
+ mockLog.debug("mock:seed", () => ({
2564
+ entityName,
2565
+ count,
2566
+ idsAndTimestamps: JSON.stringify(generated),
2567
+ fallbackStamps
2568
+ }));
2569
+ }
2570
+ /** Parsed `config.ownerFields` pairs — every declared "Entity.field" owner
2571
+ * column, split once so `seed()` and `linkRelationFields()` don't each
2572
+ * re-parse the same strings. */
2573
+ parsedOwnerFields() {
2574
+ const out = [];
2575
+ for (const pair of this.config.ownerFields ?? []) {
2576
+ const [entity, field] = pair.split(".");
2577
+ if (entity && field) out.push({ entity, field });
2578
+ }
2579
+ return out;
2437
2580
  }
2438
2581
  /**
2439
2582
  * Generate a single mock item. Field values come from the canonical policy in
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-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';
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-DOJDoqXu.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-DOJDoqXu.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';
@@ -663,8 +663,55 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
663
663
  * No-op when there is no new id, or it matches the current one — an
664
664
  * anonymous switch (`newOwnerId === undefined`) leaves existing stamps as
665
665
  * they are rather than clearing a non-nullable owner column.
666
+ *
667
+ * Both this re-labeling AND `seed()`'s original stamping consult the
668
+ * `ownerGate` hook (see `setOwnerGate`) — a re-register reseeds identical
669
+ * rows, so gating only the restamp path leaks the moment a client
670
+ * connects and the deterministic reseed stamps with the updated ownerId.
666
671
  */
667
672
  restampOwner(newOwnerId: string | undefined): void;
673
+ /**
674
+ * The host's answer to "may the current viewer OWN this row?" — evaluated
675
+ * against the candidate row with the owner column already re-pointed,
676
+ * `storeKey` being the collection key the stores are keyed by. The runtime
677
+ * installs the registered schema's `@create` directive here (the single
678
+ * authority on who authors; no role heuristics), so a read-only persona
679
+ * never inherits authorship of drafts from the demo stamper — neither on a
680
+ * persona switch (`restampOwner`) nor on a deterministic reseed (`seed`).
681
+ * Absent hook = stamp unconditionally (pre-role-model behavior).
682
+ */
683
+ private ownerGate?;
684
+ setOwnerGate(gate: ((storeKey: string, candidateRow: EntityRow) => boolean) | undefined): void;
685
+ /**
686
+ * The host's answer to "may THIS identity row own this candidate row?" —
687
+ * evaluated per candidate rather than against the fixed default viewer, so
688
+ * `linkRelationFields` and the eligible-owner fallback (see
689
+ * `firstEligibleOwner`) can pick a policy-valid owner from the whole
690
+ * identity roster instead of only ever checking the current default user.
691
+ * Absent hook = no candidate filtering (pre-role-model behavior).
692
+ */
693
+ private ownerCandidateGate?;
694
+ setOwnerCandidateGate(gate: ((storeKey: string, candidateRow: EntityRow, candidateIdentityRow: EntityRow) => boolean) | undefined): void;
695
+ /**
696
+ * The store an owner column's relation field targets, resolved the same
697
+ * way `linkRelationFields` resolves any relation target (id sibling wins
698
+ * over the `entity` name string) — but keyed by STORE, mirroring that
699
+ * method's first-declarer-per-collection rule, so `seed()` and
700
+ * `restampOwner()` can find it before a `linkRelationFields` pass has run
701
+ * this registration. Undefined when no registered schema on `storeKey`
702
+ * declares `column` as a relation field.
703
+ */
704
+ private resolveOwnerColumnTargetStore;
705
+ /**
706
+ * The first identity-store row (seed order) whose persona is eligible, per
707
+ * the installed `ownerCandidateGate`, to own `row`'s `column` in `storeKey`
708
+ * — the deterministic replacement for "skip the stamp" when the CURRENT
709
+ * viewer fails the `@create` policy (e.g. a reader-default persona against
710
+ * an author|reviewer-only policy). Undefined when no gate is installed, the
711
+ * column isn't a resolvable relation, or no identity row is eligible — the
712
+ * caller keeps today's skip behavior in that case.
713
+ */
714
+ private firstEligibleOwner;
668
715
  /** Re-anchor the PRNG to the configured seed. Called before every
669
716
  * re-seed loop so identical reseed sequences produce identical rows
670
717
  * (timestamps + generated fields). Without this, the first
@@ -722,6 +769,10 @@ declare class MockPersistenceAdapter implements PersistenceAdapter {
722
769
  * Seed an entity with mock data.
723
770
  */
724
771
  seed(entityName: string, fields: EntityField[], count: number, persistence?: EntityPersistence): void;
772
+ /** Parsed `config.ownerFields` pairs — every declared "Entity.field" owner
773
+ * column, split once so `seed()` and `linkRelationFields()` don't each
774
+ * re-parse the same strings. */
775
+ private parsedOwnerFields;
725
776
  /**
726
777
  * Generate a single mock item. Field values come from the canonical policy in
727
778
  * `@almadar/core/mock`; this method owns only the id and timestamp stamping.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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';
1
+ import { EffectExecutor } from './chunk-LMYD5UB3.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-LMYD5UB3.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';
@@ -340,18 +340,19 @@ function createServerEffectHandlers(opts) {
340
340
  }
341
341
  if (batchFailed) break;
342
342
  }
343
+ const batchSummary = {
344
+ operations: batchResults,
345
+ completedCount: completed.length,
346
+ totalCount: operations.length
347
+ };
343
348
  record({
344
349
  effect: "persist",
345
350
  action: "batch",
346
- data: {
347
- operations: batchResults,
348
- completedCount: completed.length,
349
- totalCount: operations.length
350
- },
351
+ data: batchSummary,
351
352
  success: !batchFailed,
352
353
  ...batchFailed ? { error: batchError } : {}
353
354
  });
354
- return;
355
+ return batchSummary;
355
356
  }
356
357
  const type = targetEntityType || entityType;
357
358
  let resultData;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/runtime",
3
- "version": "6.63.0",
3
+ "version": "6.65.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.73.0",
60
+ "@almadar/core": "^10.75.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.192.0"
64
+ "@almadar/std": "^16.195.0"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "express": "^5.0.0"