@camstack/system 1.1.23 → 1.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  const require_chunk = require("./chunk-Cek0wNdY.js");
2
- const require_manifest_python_deps = require("./manifest-python-deps-D7iR07uA.js");
2
+ const require_manifest_python_deps = require("./manifest-python-deps-BqXCckob.js");
3
3
  const require_custom_action_registry = require("./custom-action-registry-vLYEFTtv.js");
4
4
  let node_fs = require("node:fs");
5
5
  node_fs = require_chunk.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { I as createUdsLoggerWithControl, L as LocalChildClient, at as setWorkerNativeCapsChangeListener, bt as resolveAddonClass, i as createUdsAddonContext, nt as getWorkerNativeCapProvider, rt as getWorkerNativeCapSnapshot, st as validateProviderRegistrations, t as installManifestPythonDeps, yt as installManifestNativeDeps } from "./manifest-python-deps-hDWMDe_b.mjs";
1
+ import { F as createUdsLoggerWithControl, I as LocalChildClient, i as createUdsAddonContext, it as setWorkerNativeCapsChangeListener, nt as getWorkerNativeCapSnapshot, ot as validateProviderRegistrations, t as installManifestPythonDeps, tt as getWorkerNativeCapProvider, vt as installManifestNativeDeps, yt as resolveAddonClass } from "./manifest-python-deps-Dxl4twM3.mjs";
2
2
  import { t as CustomActionRegistry } from "./custom-action-registry-BEXwC-oo.mjs";
3
3
  import { register } from "node:module";
4
4
  import * as fs from "node:fs";
@@ -2,7 +2,12 @@ import { DeviceLink } from '@camstack/types';
2
2
  /** A target link with its source already resolved to a live numeric id. */
3
3
  export interface ResolvedTargetLink {
4
4
  readonly link: DeviceLink;
5
+ /** Resolved source device id for scalar sources; `-1` sentinel for a literal
6
+ * source AND for an expression source (whose per-binding ids live below). */
5
7
  readonly sourceDeviceId: number;
8
+ /** Expression links only: binding name → resolved source device id. Literal
9
+ * bindings and unresolved bindings are ABSENT from the map. */
10
+ readonly bindingSourceIds?: Readonly<Record<string, number>>;
6
11
  }
7
12
  /** A target that depends on a given source slice (for reactive re-emit). */
8
13
  export interface DependentLink {
@@ -15,9 +20,14 @@ export interface LinkIndexes {
15
20
  /** `${sourceDeviceId}:${sourceCap}` → targets to recompute on a source change. */
16
21
  readonly dependents: Map<string, DependentLink[]>;
17
22
  }
23
+ /** One resolved link entry fed to `buildLinkIndexes`. */
24
+ export interface LinkIndexEntry {
25
+ readonly targetDeviceId: number;
26
+ readonly link: DeviceLink;
27
+ /** Resolved scalar source id; `-1` for literal/expression sources. */
28
+ readonly sourceDeviceId: number;
29
+ /** Expression links: binding name → resolved source device id. */
30
+ readonly bindingSourceIds?: Readonly<Record<string, number>>;
31
+ }
18
32
  /** Build both lookup maps from resolved link entries. Pure — order-preserving. */
19
- export declare function buildLinkIndexes(entries: ReadonlyArray<{
20
- targetDeviceId: number;
21
- link: DeviceLink;
22
- sourceDeviceId: number;
23
- }>): LinkIndexes;
33
+ export declare function buildLinkIndexes(entries: ReadonlyArray<LinkIndexEntry>): LinkIndexes;
@@ -1,5 +1,6 @@
1
1
  import { z } from 'zod';
2
2
  import { CapabilityStatusItemArray, DeviceLink } from '@camstack/types';
3
+ import { ResolvedTargetLink } from './device-link-overlay.js';
3
4
  /** One resolved source reading paired with the link that produced it. The
4
5
  * `sourceValue` is already transformed; `undefined` skips the overlay. */
5
6
  export interface ResolvedLinkValue {
@@ -16,6 +17,16 @@ export interface ResolvedLinkValue {
16
17
  * `source.kind` directly.
17
18
  */
18
19
  export declare function resolveLinkValue(link: DeviceLink, readField: (cap: string, fieldPath: string) => unknown): unknown;
20
+ /**
21
+ * Sync-channel expression resolution: bindings read from the mirror via
22
+ * `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
23
+ * on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
24
+ * binding id (dangling source) or a non-primitive read resolves to `null`.
25
+ * Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
26
+ * the mirror path is hot and churn-free; the async channel logs. Compile
27
+ * failures cost one parse ever thanks to the negative LRU compile cache. Pure.
28
+ */
29
+ export declare function resolveExpressionLinkValue(rl: ResolvedTargetLink, readField: (deviceId: number, cap: string, fieldPath: string) => unknown, now: number): unknown;
19
30
  /**
20
31
  * Overlay transformed source values onto `base` by dot-path, then validate the
21
32
  * result against the target cap's `statusSchema`. On validation failure the
@@ -300,7 +300,8 @@ function toDeviceInfo(addonId, device, metadata = null, metaRow = null) {
300
300
  ...metaRow?.linkDeviceId !== void 0 ? { linkDeviceId: metaRow.linkDeviceId } : {},
301
301
  ...metaRow?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: metaRow.primaryChildEntityId } : {},
302
302
  ...metaRow?.childLayout !== void 0 ? { childLayout: metaRow.childLayout } : {},
303
- ...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {}
303
+ ...metaRow?.deviceLinks !== void 0 ? { deviceLinks: metaRow.deviceLinks } : {},
304
+ ...metaRow?.display !== void 0 ? { display: metaRow.display } : {}
304
305
  };
305
306
  }
306
307
  function resolveDeviceById(registry, deviceId) {
@@ -1371,20 +1372,41 @@ var DeviceEventPropagator = class {
1371
1372
  function nodeKey(deviceId, cap) {
1372
1373
  return `${deviceId}:${cap}`;
1373
1374
  }
1375
+ /** The target's container stableId — its parent's (falling back to its own for
1376
+ * a top-level target). Sibling FIELD sources resolve relative to this, the same
1377
+ * rule `rebuildLinkDependents` / `resolveLinkedStatus` apply. */
1378
+ function containerStableId(targetRow, rowById) {
1379
+ return targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId;
1380
+ }
1381
+ /** Resolve ONE field/global/literal binding to its `(deviceId, cap)` node, or
1382
+ * null when it is a literal or resolves to no known device. */
1383
+ function resolveBindingNode(binding, targetRow, rowById, idByStableId) {
1384
+ if (binding.kind === "literal") return null;
1385
+ const wantedStableId = binding.kind === "global" ? binding.sourceStableId : `${containerStableId(targetRow, rowById)}-${binding.sourceKey}`;
1386
+ const srcId = idByStableId.get(wantedStableId);
1387
+ if (srcId === void 0) return null;
1388
+ return nodeKey(srcId, binding.cap);
1389
+ }
1374
1390
  /**
1375
- * Resolve a link source to its `(deviceId, cap)` node, or null when the
1376
- * source is a literal (no device) or does not resolve to a known device.
1377
- * Sibling FIELD sources resolve against the TARGET's container stableId
1378
- * (its parent's, falling back to its own for a top-level target) the same
1379
- * rule `rebuildLinkDependents` / `resolveLinkedStatus` apply.
1391
+ * Resolve a link source to the set of `(deviceId, cap)` nodes it depends on.
1392
+ * A literal source depends on nothing; a field/global source depends on exactly
1393
+ * one node; an EXPRESSION source depends on one node per resolvable non-literal
1394
+ * binding. Unresolvable (dangling) bindings contribute no edgethey cannot
1395
+ * loop while dangling; a cycle that only becomes resolvable later is bounded by
1396
+ * the resolve-time re-entrancy guard.
1380
1397
  */
1381
- function resolveSourceNode(link, targetRow, rowById, idByStableId) {
1398
+ function resolveSourceNodes(link, targetRow, rowById, idByStableId) {
1382
1399
  const src = link.source;
1383
- if (src.kind === "literal") return null;
1384
- const wantedStableId = src.kind === "global" ? src.sourceStableId : `${targetRow.parentDeviceId !== null ? rowById.get(targetRow.parentDeviceId)?.stableId ?? targetRow.stableId : targetRow.stableId}-${src.sourceKey}`;
1385
- const srcId = idByStableId.get(wantedStableId);
1386
- if (srcId === void 0) return null;
1387
- return nodeKey(srcId, src.cap);
1400
+ if (src.kind === "expression") {
1401
+ const nodes = [];
1402
+ for (const binding of Object.values(src.bindings)) {
1403
+ const node = resolveBindingNode(binding, targetRow, rowById, idByStableId);
1404
+ if (node !== null) nodes.push(node);
1405
+ }
1406
+ return nodes;
1407
+ }
1408
+ const node = resolveBindingNode(src, targetRow, rowById, idByStableId);
1409
+ return node !== null ? [node] : [];
1388
1410
  }
1389
1411
  /**
1390
1412
  * Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
@@ -1402,11 +1424,11 @@ function findDeviceLinkCycle(allMeta, editedDeviceId, editedLinks) {
1402
1424
  const dependsOn = /* @__PURE__ */ new Map();
1403
1425
  const addEdges = (row, links) => {
1404
1426
  for (const link of links) {
1405
- const srcNode = resolveSourceNode(link, row, rowById, idByStableId);
1406
- if (srcNode === null) continue;
1427
+ const srcNodes = resolveSourceNodes(link, row, rowById, idByStableId);
1428
+ if (srcNodes.length === 0) continue;
1407
1429
  const tNode = nodeKey(row.id, link.target.cap);
1408
1430
  const list = dependsOn.get(tNode) ?? [];
1409
- list.push(srcNode);
1431
+ for (const srcNode of srcNodes) list.push(srcNode);
1410
1432
  dependsOn.set(tNode, list);
1411
1433
  }
1412
1434
  };
@@ -1473,7 +1495,7 @@ function dfsFindCycle(root, dependsOn) {
1473
1495
  * removeDevice), config persistence (persistConfig, loadConfig), the meta
1474
1496
  * surface load (loadMeta, loadRuntimeState), every meta setter (setName,
1475
1497
  * setLocation, setType, setIntegrationId, setLinkDeviceId,
1476
- * setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole,
1498
+ * setPrimaryChildEntityId, setChildLayout, setDeviceLinks, setRole, setDisplay,
1477
1499
  * applyInitialMeta, setMetadata, setDisabled), and the location registry
1478
1500
  * (listLocations, addLocation, removeLocation).
1479
1501
  *
@@ -1582,6 +1604,7 @@ async function registerDevice(pctx, input) {
1582
1604
  ...existingMeta?.childLayout !== void 0 ? { childLayout: existingMeta.childLayout } : {},
1583
1605
  ...existingMeta?.deviceLinks !== void 0 ? { deviceLinks: existingMeta.deviceLinks } : {},
1584
1606
  ...existingMeta?.role !== void 0 ? { role: existingMeta.role } : {},
1607
+ ...existingMeta?.display !== void 0 ? { display: existingMeta.display } : {},
1585
1608
  parentDeviceId,
1586
1609
  id,
1587
1610
  features: featuresArr,
@@ -2023,6 +2046,11 @@ async function setDeviceLinks(pctx, input) {
2023
2046
  const { deviceId, deviceLinks } = input;
2024
2047
  const cycle = findDeviceLinkCycle(await pctx.metaStore.readMeta(), deviceId, deviceLinks);
2025
2048
  if (cycle) throw new Error(`[device-manager] setDeviceLinks: cross-device link cycle: ${cycle.join(" → ")}`);
2049
+ for (const link of deviceLinks) {
2050
+ if (link.source.kind !== "expression") continue;
2051
+ const err = (0, _camstack_types.validateExpressionSource)(link.source);
2052
+ if (err) throw new Error(`[device-manager] setDeviceLinks: invalid expression on link '${link.id}': ${err}`);
2053
+ }
2026
2054
  await pctx.metaStore.withMetaWriteLock(async () => {
2027
2055
  const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
2028
2056
  if (!persisted) throw new Error(`[device-manager] setDeviceLinks: unknown device id=${deviceId}`);
@@ -2093,6 +2121,84 @@ async function setRole(pctx, input) {
2093
2121
  });
2094
2122
  }
2095
2123
  /**
2124
+ * Normalize an operator-authored display override at write time so the render
2125
+ * path's `UNIT_TABLE` lookups always hit canonical spellings — maps `unit` and
2126
+ * every `perCap[*].unit` through `normalizeUnit`. Pure: rebuilds new objects,
2127
+ * never mutates the input. A unit `normalizeUnit` cannot canonicalize is left
2128
+ * as-is (the render path refuses conversion for unknown spellings).
2129
+ */
2130
+ function normalizeDisplayOverride(display) {
2131
+ return {
2132
+ ...display,
2133
+ ...display.unit !== void 0 ? { unit: (0, _camstack_types.normalizeUnit)(display.unit) ?? display.unit } : {},
2134
+ ...display.perCap !== void 0 ? { perCap: Object.fromEntries(Object.entries(display.perCap).map(([cap, refine]) => [cap, refine.unit !== void 0 ? {
2135
+ ...refine,
2136
+ unit: (0, _camstack_types.normalizeUnit)(refine.unit) ?? refine.unit
2137
+ } : refine])) } : {}
2138
+ };
2139
+ }
2140
+ /**
2141
+ * Set (or clear) the per-device display override on a device's meta row.
2142
+ * Mirrors `setChildLayout` persistence; `null` REMOVES the `display` key
2143
+ * entirely (immutable rest-destructure — matches the "absent ⇒ no override"
2144
+ * projection contract, never persists `display: undefined`). Override units are
2145
+ * normalized at write so the render path always looks up canonical spellings.
2146
+ */
2147
+ async function setDisplay(pctx, input) {
2148
+ const { deviceId, display } = input;
2149
+ const normalized = display === null ? null : normalizeDisplayOverride(display);
2150
+ await pctx.metaStore.withMetaWriteLock(async () => {
2151
+ const persisted = await pctx.metaStore.resolvePersistedById(deviceId);
2152
+ if (!persisted) throw new Error(`[device-manager] setDisplay: unknown device id=${deviceId}`);
2153
+ const { meta: m } = persisted;
2154
+ const key = String(deviceId);
2155
+ const allMeta = await pctx.metaStore.readMeta();
2156
+ const nextRow = normalized === null ? (({ display: _drop, ...rest }) => rest)(m) : {
2157
+ ...m,
2158
+ display: normalized
2159
+ };
2160
+ await pctx.settings.writeAddonStore({ deviceMeta: {
2161
+ ...allMeta,
2162
+ [key]: nextRow
2163
+ } });
2164
+ });
2165
+ pctx.host.ctx.eventBus.emit({
2166
+ id: (0, node_crypto.randomUUID)(),
2167
+ timestamp: /* @__PURE__ */ new Date(),
2168
+ source: {
2169
+ type: "device",
2170
+ id: deviceId
2171
+ },
2172
+ category: _camstack_types.EventCategory.DeviceMetaChanged,
2173
+ data: {
2174
+ deviceId,
2175
+ field: "display",
2176
+ value: normalized
2177
+ }
2178
+ });
2179
+ }
2180
+ /**
2181
+ * Read the operator-authored per-role display defaults. Empty record when none
2182
+ * set. Not per-device — a plain top-level-key read.
2183
+ */
2184
+ async function getRoleDisplayDefaults(pctx, _input) {
2185
+ return { defaults: (await pctx.metaStore.readStore()).roleDisplayDefaults ?? {} };
2186
+ }
2187
+ /**
2188
+ * Replace the per-role display defaults whole-record (full replace). Override
2189
+ * units are normalized (`normalizeUnit`) at write so the render path always
2190
+ * looks up canonical spellings. Single-writer top-level key — no lock, no RMW,
2191
+ * no interaction with the `deviceMeta` write lock. Not per-device, so no event
2192
+ * is emitted; the UI invalidates its own query on mutate.
2193
+ */
2194
+ async function setRoleDisplayDefaults(pctx, input) {
2195
+ const normalized = Object.fromEntries(Object.entries(input.defaults).map(([role, def]) => [role, def.unit !== void 0 ? {
2196
+ ...def,
2197
+ unit: (0, _camstack_types.normalizeUnit)(def.unit) ?? def.unit
2198
+ } : def]));
2199
+ await pctx.settings.writeAddonStore({ roleDisplayDefaults: normalized });
2200
+ }
2201
+ /**
2096
2202
  * Batched meta pre-seed. Applies every provided field to the
2097
2203
  * device's meta row in ONE read-modify-write under a single
2098
2204
  * `withMetaWriteLock` acquisition (one `deviceMeta` blob write),
@@ -2337,23 +2443,33 @@ async function removeLocation(pctx, input) {
2337
2443
  function buildLinkIndexes(entries) {
2338
2444
  const targets = /* @__PURE__ */ new Map();
2339
2445
  const dependents = /* @__PURE__ */ new Map();
2340
- for (const { targetDeviceId, link, sourceDeviceId } of entries) {
2446
+ for (const { targetDeviceId, link, sourceDeviceId, bindingSourceIds } of entries) {
2341
2447
  const tKey = `${targetDeviceId}:${link.target.cap}`;
2342
2448
  const tList = targets.get(tKey) ?? [];
2343
2449
  tList.push({
2344
2450
  link,
2345
- sourceDeviceId
2451
+ sourceDeviceId,
2452
+ ...bindingSourceIds !== void 0 ? { bindingSourceIds } : {}
2346
2453
  });
2347
2454
  targets.set(tKey, tList);
2348
- if (link.source.kind !== "literal") {
2349
- const sKey = `${sourceDeviceId}:${link.source.cap}`;
2455
+ const addDependent = (sourceId, sourceCap) => {
2456
+ const sKey = `${sourceId}:${sourceCap}`;
2350
2457
  const sList = dependents.get(sKey) ?? [];
2351
2458
  sList.push({
2352
2459
  targetDeviceId,
2353
2460
  targetCap: link.target.cap
2354
2461
  });
2355
2462
  dependents.set(sKey, sList);
2463
+ };
2464
+ if (link.source.kind === "expression") {
2465
+ if (bindingSourceIds !== void 0) for (const [name, bindingDeviceId] of Object.entries(bindingSourceIds)) {
2466
+ const binding = link.source.bindings[name];
2467
+ if (binding === void 0 || binding.kind === "literal") continue;
2468
+ addDependent(bindingDeviceId, binding.cap);
2469
+ }
2470
+ continue;
2356
2471
  }
2472
+ if (link.source.kind !== "literal") addDependent(sourceDeviceId, link.source.cap);
2357
2473
  }
2358
2474
  return {
2359
2475
  targets,
@@ -2508,6 +2624,23 @@ var DeviceMetaStore = class {
2508
2624
  if (container === void 0) continue;
2509
2625
  for (const link of targetMeta.deviceLinks ?? []) {
2510
2626
  const src = link.source;
2627
+ if (src.kind === "expression") {
2628
+ const bindingSourceIds = {};
2629
+ for (const [name, b] of Object.entries(src.bindings)) {
2630
+ if (b.kind === "literal") continue;
2631
+ const wanted = b.kind === "global" ? b.sourceStableId : `${container}-${b.sourceKey}`;
2632
+ expectedSources.add(wanted);
2633
+ const bid = idByStableId.get(wanted);
2634
+ if (bid !== void 0) bindingSourceIds[name] = bid;
2635
+ }
2636
+ entries.push({
2637
+ targetDeviceId: targetId,
2638
+ link,
2639
+ sourceDeviceId: -1,
2640
+ bindingSourceIds
2641
+ });
2642
+ continue;
2643
+ }
2511
2644
  if (src.kind === "literal") {
2512
2645
  entries.push({
2513
2646
  targetDeviceId: targetId,
@@ -2625,6 +2758,7 @@ async function listAll(pctx, input) {
2625
2758
  ...m?.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
2626
2759
  ...m?.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
2627
2760
  ...m?.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
2761
+ ...m?.display !== void 0 ? { display: m.display } : {},
2628
2762
  ...(() => {
2629
2763
  const si = buildSourceInfoFromConfig(persistedConfig ?? {}, stableId, aid);
2630
2764
  return si !== void 0 ? { sourceInfo: si } : {};
@@ -2673,6 +2807,7 @@ async function getDevice(pctx, input) {
2673
2807
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
2674
2808
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
2675
2809
  ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
2810
+ ...m.display !== void 0 ? { display: m.display } : {},
2676
2811
  ...sourceInfoGetDevice !== void 0 ? { sourceInfo: sourceInfoGetDevice } : {}
2677
2812
  };
2678
2813
  }
@@ -2735,6 +2870,7 @@ async function getChildren(pctx, input) {
2735
2870
  ...m.primaryChildEntityId !== void 0 ? { primaryChildEntityId: m.primaryChildEntityId } : {},
2736
2871
  ...m.childLayout !== void 0 ? { childLayout: m.childLayout } : {},
2737
2872
  ...m.deviceLinks !== void 0 ? { deviceLinks: m.deviceLinks } : {},
2873
+ ...m.display !== void 0 ? { display: m.display } : {},
2738
2874
  ...sourceInfoChild !== void 0 ? { sourceInfo: sourceInfoChild } : {}
2739
2875
  });
2740
2876
  }
@@ -3074,7 +3210,37 @@ function isRecord(x) {
3074
3210
  * `source.kind` directly.
3075
3211
  */
3076
3212
  function resolveLinkValue(link, readField) {
3077
- return (0, _camstack_types.applyTransform)(link.source.kind === "literal" ? link.source.value : readField(link.source.cap, link.source.fieldPath), link.transform);
3213
+ const src = link.source;
3214
+ if (src.kind === "expression") return void 0;
3215
+ return (0, _camstack_types.applyTransform)(src.kind === "literal" ? src.value : readField(src.cap, src.fieldPath), link.transform);
3216
+ }
3217
+ /**
3218
+ * Sync-channel expression resolution: bindings read from the mirror via
3219
+ * `readField(deviceId, cap, fieldPath)` using the pre-resolved per-binding ids
3220
+ * on the `ResolvedTargetLink`. A literal binding uses its constant; an absent
3221
+ * binding id (dangling source) or a non-primitive read resolves to `null`.
3222
+ * Returns `undefined` on parse/eval failure (skip overlay) — silent by design:
3223
+ * the mirror path is hot and churn-free; the async channel logs. Compile
3224
+ * failures cost one parse ever thanks to the negative LRU compile cache. Pure.
3225
+ */
3226
+ function resolveExpressionLinkValue(rl, readField, now) {
3227
+ const src = rl.link.source;
3228
+ if (src.kind !== "expression") return void 0;
3229
+ const bindingValues = {};
3230
+ for (const [name, b] of Object.entries(src.bindings)) {
3231
+ if (b.kind === "literal") {
3232
+ bindingValues[name] = b.value;
3233
+ continue;
3234
+ }
3235
+ const bId = rl.bindingSourceIds?.[name];
3236
+ if (bId === void 0) {
3237
+ bindingValues[name] = null;
3238
+ continue;
3239
+ }
3240
+ bindingValues[name] = (0, _camstack_types.toExpressionValue)(readField(bId, b.cap, b.fieldPath)) ?? null;
3241
+ }
3242
+ const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
3243
+ return result.ok ? (0, _camstack_types.applyTransform)(result.value, rl.link.transform) : void 0;
3078
3244
  }
3079
3245
  /** Narrow Zod v4's structural `$ZodType` (returned by `.unwrap()`) back to the
3080
3246
  * concrete classic `z.ZodType`. Every runtime schema is a `z.ZodType`, so this
@@ -3395,9 +3561,10 @@ var DeviceStateMirror = class DeviceStateMirror {
3395
3561
  const raw = this.stateMirror.get(deviceId)?.get(cap) ?? null;
3396
3562
  const links = this.linkHost.linkTargets.get(`${deviceId}:${cap}`);
3397
3563
  if (!links || links.length === 0) return raw ? { ...raw } : null;
3564
+ const now = Date.now();
3398
3565
  const resolved = links.map((rl) => ({
3399
3566
  link: rl.link,
3400
- sourceValue: resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
3567
+ sourceValue: rl.link.source.kind === "expression" ? resolveExpressionLinkValue(rl, (srcDeviceId, srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(srcDeviceId)?.get(srcCap), fieldPath), now) : resolveLinkValue(rl.link, (srcCap, fieldPath) => (0, _camstack_types.getByPath)(this.stateMirror.get(rl.sourceDeviceId)?.get(srcCap), fieldPath))
3401
3568
  }));
3402
3569
  const capStatus = this.linkHost.capabilityRegistry?.getDefinition(cap)?.status;
3403
3570
  const schema = capStatus?.schema;
@@ -3588,6 +3755,39 @@ async function resolveLinkedStatusInner(pctx, input) {
3588
3755
  const parentMeta = allMeta[String(parentId)];
3589
3756
  if (parentMeta) containerStableId = parentMeta.stableId;
3590
3757
  }
3758
+ const now = Date.now();
3759
+ const statusMemo = /* @__PURE__ */ new Map();
3760
+ const readSourceStatus = async (srcId, srcCap) => {
3761
+ const key = `${srcId}:${srcCap}`;
3762
+ const cached = statusMemo.get(key);
3763
+ if (cached !== void 0) return cached;
3764
+ let result;
3765
+ try {
3766
+ const srcProvider = capRegistry?.getProviderForDevice(srcCap, srcId);
3767
+ result = {
3768
+ status: typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0,
3769
+ ok: true
3770
+ };
3771
+ } catch (err) {
3772
+ pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
3773
+ tags: {
3774
+ deviceId,
3775
+ capName: cap
3776
+ },
3777
+ meta: {
3778
+ sourceCap: srcCap,
3779
+ sourceId: srcId,
3780
+ error: err instanceof Error ? err.message : String(err)
3781
+ }
3782
+ });
3783
+ result = {
3784
+ status: void 0,
3785
+ ok: false
3786
+ };
3787
+ }
3788
+ statusMemo.set(key, result);
3789
+ return result;
3790
+ };
3591
3791
  const resolved = [];
3592
3792
  for (const link of links) {
3593
3793
  const src = link.source;
@@ -3598,27 +3798,46 @@ async function resolveLinkedStatusInner(pctx, input) {
3598
3798
  });
3599
3799
  continue;
3600
3800
  }
3601
- const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
3602
- if (srcId === null) continue;
3603
- try {
3604
- const srcProvider = capRegistry?.getProviderForDevice(src.cap, srcId);
3605
- const raw = (0, _camstack_types.getByPath)(typeof srcProvider?.getStatus === "function" ? await srcProvider.getStatus({ deviceId: srcId }) : void 0, src.fieldPath);
3606
- resolved.push({
3801
+ if (src.kind === "expression") {
3802
+ const bindingValues = {};
3803
+ for (const [name, b] of Object.entries(src.bindings)) {
3804
+ if (b.kind === "literal") {
3805
+ bindingValues[name] = b.value;
3806
+ continue;
3807
+ }
3808
+ const bId = b.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(b.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, b.sourceKey, allMeta);
3809
+ if (bId === null) {
3810
+ bindingValues[name] = null;
3811
+ continue;
3812
+ }
3813
+ bindingValues[name] = (0, _camstack_types.toExpressionValue)((0, _camstack_types.getByPath)((await readSourceStatus(bId, b.cap)).status, b.fieldPath)) ?? null;
3814
+ }
3815
+ const result = (0, _camstack_types.evaluateLinkExpression)(src.expr, bindingValues, now);
3816
+ if (result.ok) resolved.push({
3607
3817
  link,
3608
- sourceValue: (0, _camstack_types.applyTransform)(raw, link.transform)
3818
+ sourceValue: (0, _camstack_types.applyTransform)(result.value, link.transform)
3609
3819
  });
3610
- } catch (err) {
3611
- pctx.host.ctx.logger.warn("resolveLinkedStatus: source read failed", {
3820
+ else pctx.host.ctx.logger.warn("resolveLinkedStatus: expression skipped", {
3612
3821
  tags: {
3613
3822
  deviceId,
3614
3823
  capName: cap
3615
3824
  },
3616
3825
  meta: {
3617
- sourceKey: src.kind === "global" ? src.sourceStableId : src.sourceKey,
3618
- error: err instanceof Error ? err.message : String(err)
3826
+ linkId: link.id,
3827
+ error: result.error
3619
3828
  }
3620
3829
  });
3830
+ continue;
3621
3831
  }
3832
+ const srcId = src.kind === "global" ? pctx.metaStore.resolveGlobalSourceDeviceId(src.sourceStableId, allMeta) : pctx.metaStore.resolveSourceDeviceId(containerStableId, src.sourceKey, allMeta);
3833
+ if (srcId === null) continue;
3834
+ const read = await readSourceStatus(srcId, src.cap);
3835
+ if (!read.ok) continue;
3836
+ const raw = (0, _camstack_types.getByPath)(read.status, src.fieldPath);
3837
+ resolved.push({
3838
+ link,
3839
+ sourceValue: (0, _camstack_types.applyTransform)(raw, link.transform)
3840
+ });
3622
3841
  }
3623
3842
  if (resolved.length === 0) return null;
3624
3843
  const synthesizing = !isRecord$1(baseStatus);
@@ -3931,6 +4150,9 @@ var DeviceManagerAddon = class extends _camstack_types.BaseAddon {
3931
4150
  setChildLayout: (input) => setChildLayout(pctx, input),
3932
4151
  setDeviceLinks: (input) => setDeviceLinks(pctx, input),
3933
4152
  setRole: (input) => setRole(pctx, input),
4153
+ setDisplay: (input) => setDisplay(pctx, input),
4154
+ getRoleDisplayDefaults: (input) => getRoleDisplayDefaults(pctx, input),
4155
+ setRoleDisplayDefaults: (input) => setRoleDisplayDefaults(pctx, input),
3934
4156
  applyInitialMeta: (input) => applyInitialMeta(pctx, input),
3935
4157
  setMetadata: (input) => setMetadata(pctx, input),
3936
4158
  setDisabled: (input) => setDisabled(pctx, input),