@camstack/system 1.1.22 → 1.1.23

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.
@@ -214,7 +214,14 @@ export declare function setWrapperActive(deps: AggregationDeps, input: {
214
214
  * bound caps expose, for the operator's cross-device wiring UI. Binding-driven:
215
215
  * walks `getBindings(deviceId)`, skips wrapper caps (their status schemas are
216
216
  * internal book-keeping, not wireable domain data), and enumerates each cap's
217
- * status schema leaf fields. Behavior unchanged from the inline provider method.
217
+ * status schema leaf fields (per-item fields included for item-array caps).
218
+ *
219
+ * `includeSynthesizable: true` (TARGET pickers only) additionally unions in
220
+ * UNBOUND device-scoped caps that declare `status.empty` (the synthesize-target
221
+ * marker) and whose `deviceTypes` admit this device's persisted type — so the
222
+ * FIRST link to a synthesize-only cap (consumables on an HA vacuum) can be
223
+ * authored before any binding exists. Default (absent/false) behavior is
224
+ * byte-identical to the binding-driven catalog.
218
225
  */
219
226
  export declare function getWireableFields(deps: AggregationDeps, input: Parameters<IDeviceManagerProvider['getWireableFields']>[0]): ReturnType<IDeviceManagerProvider['getWireableFields']>;
220
227
  export {};
@@ -14,6 +14,9 @@ export interface BindingsDeps {
14
14
  readonly ctx: AddonContext;
15
15
  readonly capabilityRegistry: ICapabilityRegistry | undefined;
16
16
  readonly remoteNativeCaps: RemoteNativeCaps;
17
+ /** O(1) gate over devices that have `deviceLinks` (the addon's live set).
18
+ * Optional: when absent, getBindings adds no virtual `linked` entries. */
19
+ readonly devicesWithLinks?: ReadonlySet<number>;
17
20
  }
18
21
  /**
19
22
  * Wire the push-fed cross-process native-cap cache (`remoteNativeCaps`).
@@ -0,0 +1,18 @@
1
+ import { DeviceLink } from '@camstack/types';
2
+ /** The slice of a persisted meta row cycle detection needs. Structural subset
3
+ * of `PersistedDeviceMeta` so tests (and future callers) can pass plain rows. */
4
+ export interface LinkCycleMetaRow {
5
+ readonly id: number;
6
+ readonly stableId: string;
7
+ readonly parentDeviceId: number | null;
8
+ readonly deviceLinks?: readonly DeviceLink[];
9
+ }
10
+ /** Node key of the dependency graph: `${deviceId}:${cap}`. */
11
+ type NodeKey = string;
12
+ /**
13
+ * Detect whether replacing `editedDeviceId`'s links with `editedLinks` closes
14
+ * a dependency cycle. Returns the cycle as an ordered list of node keys
15
+ * (first node repeated at the end) or null when the set is safe.
16
+ */
17
+ export declare function findDeviceLinkCycle(allMeta: Record<string, LinkCycleMetaRow>, editedDeviceId: number, editedLinks: readonly DeviceLink[]): readonly NodeKey[] | null;
18
+ export {};
@@ -1,15 +1,33 @@
1
1
  import { z } from 'zod';
2
- import { DeviceLink } from '@camstack/types';
2
+ import { CapabilityStatusItemArray, DeviceLink } from '@camstack/types';
3
3
  /** One resolved source reading paired with the link that produced it. The
4
4
  * `sourceValue` is already transformed; `undefined` skips the overlay. */
5
5
  export interface ResolvedLinkValue {
6
6
  readonly link: DeviceLink;
7
7
  readonly sourceValue: unknown;
8
8
  }
9
+ /**
10
+ * Resolve one link's transformed value. Field sources read via `readField`
11
+ * (the caller supplies the sibling lookup — sync, e.g. the in-hub state
12
+ * mirror); literal sources use their per-device constant and never consult
13
+ * the reader. Returns `undefined` to skip the overlay (unreadable field).
14
+ * Pure. Async source reads (`resolveLinkedStatus`'s provider `getStatus`
15
+ * path) cannot use this helper for field sources — they branch on
16
+ * `source.kind` directly.
17
+ */
18
+ export declare function resolveLinkValue(link: DeviceLink, readField: (cap: string, fieldPath: string) => unknown): unknown;
9
19
  /**
10
20
  * Overlay transformed source values onto `base` by dot-path, then validate the
11
21
  * result against the target cap's `statusSchema`. On validation failure the
12
22
  * overlay is discarded and `base` is returned unchanged (a misconfigured link
13
23
  * must never corrupt a cap response). Pure — all I/O happens in the caller.
24
+ *
25
+ * Item-array grouping (P2b): when the cap declares `status.itemArray`, links
26
+ * carrying a `target.itemKey` are grouped per key and upserted as items into
27
+ * the array (see `upsertItemArrayLinks`); their `fieldPath` is relative to
28
+ * ONE item ('level', 'status', 'label'). Links WITHOUT an itemKey keep the
29
+ * scalar dot-path behavior unchanged. A link with an itemKey on a cap that
30
+ * declares NO `itemArray` is dropped (its item-relative path must never be
31
+ * scalar-applied to the status root).
14
32
  */
15
- export declare function mergeLinkedStatus(base: Record<string, unknown>, resolved: readonly ResolvedLinkValue[], statusSchema?: z.ZodType): Record<string, unknown>;
33
+ export declare function mergeLinkedStatus(base: Record<string, unknown>, resolved: readonly ResolvedLinkValue[], statusSchema?: z.ZodType, itemArray?: CapabilityStatusItemArray): Record<string, unknown>;
@@ -59,10 +59,15 @@ export declare class DeviceManagerAddon extends BaseAddon {
59
59
  private linkTargets;
60
60
  /** `${sourceDeviceId}:${sourceCap}` → targets to recompute when that source changes. */
61
61
  private linkDependents;
62
- /** Expected source `stableId`s (`${container}-${sourceKey}`) across all links,
62
+ /** Expected source `stableId`s (`${container}-${sourceKey}` for sibling
63
+ * sources, the full `sourceStableId` for global sources) across all links,
63
64
  * resolved or not — gates the `registerDevice` rebuild so only a registering
64
65
  * device that IS a link source triggers a reindex (not every boot restore). */
65
66
  private expectedSourceStableIds;
67
+ /** In-flight `${deviceId}:${cap}` pairs of `resolveLinkedStatus` — the P2e
68
+ * defensive re-entrancy guard bounding link cycles at resolve time. Mutated
69
+ * in place (never reassigned), so the host exposes a direct reference. */
70
+ private readonly linkResolveInFlight;
66
71
  /** Test/diagnostic accessors. */
67
72
  linkTargetKeys(): string[];
68
73
  linkDependentsKeys(): string[];
@@ -104,7 +109,9 @@ export declare class DeviceManagerAddon extends BaseAddon {
104
109
  private discoveryProviderLabel;
105
110
  /** Provider ids that report `supportsDiscovery() === true` (probed in parallel). */
106
111
  private listDiscoveryCapableProviderIds;
107
- /** Build the dependency context the extracted binding resolvers consume. */
112
+ /** Build the dependency context the extracted binding resolvers consume.
113
+ * `devicesWithLinks` is mutated in place (never reassigned) so the direct
114
+ * reference stays live — it gates the virtual `linked` binding step. */
108
115
  private get bindingsDeps();
109
116
  getBindings(input: {
110
117
  deviceId: number;