@camstack/system 1.2.313 → 1.2.314

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.
Files changed (106) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  7. package/dist/builtins/alerts/alerts.addon.js +1 -1
  8. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  9. package/dist/builtins/autotrack/index.js +1 -1
  10. package/dist/builtins/autotrack/index.mjs +1 -1
  11. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  13. package/dist/builtins/camera-grid/index.js +1 -1
  14. package/dist/builtins/camera-grid/index.mjs +1 -1
  15. package/dist/builtins/composer/claim-gate.d.ts +32 -0
  16. package/dist/builtins/composer/composed-device-set.d.ts +38 -0
  17. package/dist/builtins/composer/composed-device.d.ts +4 -3
  18. package/dist/builtins/composer/composer-grafts.d.ts +54 -0
  19. package/dist/builtins/composer/composer-plan.d.ts +89 -0
  20. package/dist/builtins/composer/composer-verdict.d.ts +19 -0
  21. package/dist/builtins/composer/composer.addon.js +2500 -422
  22. package/dist/builtins/composer/composer.addon.mjs +2493 -415
  23. package/dist/builtins/composer/composer.d.ts +66 -51
  24. package/dist/builtins/composer/composition-runtime.d.ts +84 -9
  25. package/dist/builtins/composer/confirmed-seed.d.ts +32 -0
  26. package/dist/builtins/composer/existing-target.d.ts +87 -0
  27. package/dist/builtins/composer/field-record.d.ts +41 -0
  28. package/dist/builtins/composer/graft-host.d.ts +47 -0
  29. package/dist/builtins/composer/held-claims.d.ts +12 -0
  30. package/dist/builtins/composer/owned-fields-target.d.ts +55 -0
  31. package/dist/builtins/composer/slice-assembler.d.ts +10 -0
  32. package/dist/builtins/composer/source-readings.d.ts +122 -0
  33. package/dist/builtins/composer/source-tracker.d.ts +35 -42
  34. package/dist/builtins/console-logging/index.js +1 -1
  35. package/dist/builtins/console-logging/index.mjs +1 -1
  36. package/dist/builtins/core-blocks/composition-api.d.ts +7 -4
  37. package/dist/builtins/core-blocks/composition-peers.d.ts +9 -0
  38. package/dist/builtins/core-blocks/composition-sources.d.ts +14 -3
  39. package/dist/builtins/core-blocks/core-block-store.d.ts +8 -1
  40. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +4 -3
  41. package/dist/builtins/core-blocks/core-blocks.addon.js +326 -50
  42. package/dist/builtins/core-blocks/core-blocks.addon.mjs +326 -50
  43. package/dist/builtins/device-manager/claimed-status-overlay.d.ts +12 -0
  44. package/dist/builtins/device-manager/device-manager.addon.d.ts +16 -0
  45. package/dist/builtins/device-manager/device-manager.addon.js +1825 -317
  46. package/dist/builtins/device-manager/device-manager.addon.mjs +1825 -317
  47. package/dist/builtins/device-manager/device-provider-context.d.ts +24 -2
  48. package/dist/builtins/device-manager/device-row-store.d.ts +2 -0
  49. package/dist/builtins/device-manager/device-state-claim-views.d.ts +77 -0
  50. package/dist/builtins/device-manager/device-state-claims.d.ts +43 -0
  51. package/dist/builtins/device-manager/device-state-mirror.d.ts +145 -61
  52. package/dist/builtins/device-manager/field-claims-index.d.ts +69 -0
  53. package/dist/builtins/device-manager/field-ownership.d.ts +53 -0
  54. package/dist/builtins/device-manager/migrate-device.d.ts +27 -0
  55. package/dist/builtins/device-manager/migrate-guard.d.ts +7 -0
  56. package/dist/builtins/device-manager/migrate-hardware-state.d.ts +55 -0
  57. package/dist/builtins/device-manager/migration-refused.d.ts +23 -0
  58. package/dist/builtins/device-manager/mirror-row-writer.d.ts +138 -0
  59. package/dist/builtins/device-manager/runtime-state-persist-gate.d.ts +4 -0
  60. package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -3
  61. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -3
  62. package/dist/builtins/hub-forwarder/index.js +1 -1
  63. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  64. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  65. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  66. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  67. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  68. package/dist/builtins/local-network/local-network.addon.js +1 -1
  69. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  70. package/dist/builtins/loki-logging/index.js +1 -1
  71. package/dist/builtins/loki-logging/index.mjs +1 -1
  72. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  73. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  74. package/dist/builtins/platform-probe/index.js +1 -1
  75. package/dist/builtins/platform-probe/index.mjs +1 -1
  76. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  77. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  78. package/dist/builtins/snapshot/index.js +1 -1
  79. package/dist/builtins/snapshot/index.mjs +1 -1
  80. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  81. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  82. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  83. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  84. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  85. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  86. package/dist/builtins/system-config/system-config.addon.js +1 -1
  87. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  88. package/dist/builtins/winston-logging/index.js +1 -1
  89. package/dist/builtins/winston-logging/index.mjs +1 -1
  90. package/dist/{child-cap-dispatch-CHTiHbsv.mjs → child-cap-dispatch-AAltfBUH.mjs} +1484 -1417
  91. package/dist/{child-cap-dispatch-Dyn_7ezH.js → child-cap-dispatch-DzcUd_ct.js} +1503 -1424
  92. package/dist/{dist-CkRwnhbo.js → dist-BtNkk3Qz.js} +4577 -3824
  93. package/dist/{dist-Cj7pJmXq.mjs → dist-cH4t1ZEm.mjs} +4541 -3824
  94. package/dist/index.js +137 -8
  95. package/dist/index.mjs +133 -8
  96. package/dist/kernel/capability-registry.d.ts +43 -1
  97. package/dist/kernel/index.d.ts +3 -1
  98. package/dist/kernel/moleculer/device-cap-proxy.d.ts +0 -10
  99. package/dist/kernel/status-overlay.d.ts +18 -0
  100. package/dist/kernel/transport/claimed-get-status.d.ts +15 -0
  101. package/dist/kernel/transport/index.d.ts +2 -0
  102. package/dist/kernel/transport/local-child-registry.d.ts +23 -0
  103. package/dist/kernel/transport/parent-unowned-call.d.ts +25 -0
  104. package/dist/{retired-settings-keys-CGSuXhP8.js → retired-settings-keys-B7imb-qH.js} +1 -1
  105. package/dist/{retired-settings-keys-C1duPikd.mjs → retired-settings-keys-DQYHfZ0S.mjs} +1 -1
  106. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { K as buildCapabilityLookup, X as composedDeviceRef, at as coreBlockSourceKind, d as CORE_BLOCK_ADDON_PREFIX, et as compositionSourceKey, f as CoreBlockSchema, g as DeclaredDevices, it as coreBlockIdFromAddonId, jn as BaseAddon, kn as errMsg, nt as coreBlockAddonId, ot as coreBlocksCapability, rt as coreBlockComposition, t as ALL_CAPABILITY_DEFINITIONS, tt as compositionSourceRefs, u as CORE_BLOCKS_ADDON_ID, wn as validateComposition } from "../../dist-Cj7pJmXq.mjs";
1
+ import { An as composedDeviceRef, Bn as BaseAddon, Cn as validateComposition, J as buildCapabilityLookup, Pn as compositionSourceKey, an as replacedFieldKeys, at as coreBlocksCapability, d as CORE_BLOCK_ADDON_PREFIX, en as planComposition, et as compositionSourceRefs, f as CoreBlockSchema, g as DeclaredDevices, it as coreBlockSourceKind, l as COMPOSER_RUNNER_ID, nt as coreBlockComposition, rt as coreBlockIdFromAddonId, t as ALL_CAPABILITY_DEFINITIONS, tt as coreBlockAddonId, u as CORE_BLOCKS_ADDON_ID, zn as errMsg } from "../../dist-cH4t1ZEm.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { customizationBlockName } from "@camstack/types/node";
3
4
  import * as fs from "node:fs/promises";
4
5
  import * as path$1 from "node:path";
5
6
  //#region src/builtins/core-blocks/block-package.ts
@@ -336,7 +337,7 @@ var BlockSupervisor = class {
336
337
  for (const { block } of compositions) await this.report(block.id, "failed", "no composer is available on this node — a composition cannot run here");
337
338
  return;
338
339
  }
339
- if (runners.find((r) => r.name === "composer")?.state === "failed") {
340
+ if (runners.find((r) => r.name === COMPOSER_RUNNER_ID)?.state === "failed") {
340
341
  for (const { block } of compositions) await this.report(block.id, "failed", "the composer runner's crash circuit-breaker tripped — no composition runs until it is restarted");
341
342
  return;
342
343
  }
@@ -1009,15 +1010,103 @@ function armBlocksIntegration(options) {
1009
1010
  })() };
1010
1011
  }
1011
1012
  //#endregion
1013
+ //#region src/builtins/core-blocks/composition-peers.ts
1014
+ /**
1015
+ * The stored compositions a save is judged against, as graph peers (D663,
1016
+ * ruling S2). Each is identified by its BLOCK ID, targets the device it writes
1017
+ * — `composedDeviceRef(block.id)` for a new device, the customized device for an
1018
+ * existing one — and, on an existing device, names the fields it REPLACES, so
1019
+ * its self-read of a replaced field is the native value and never an edge.
1020
+ *
1021
+ * `replaced` is not stored anywhere: it is recomputed at save time from the
1022
+ * peer's target description (`planComposition(..., target)`), read in the same
1023
+ * batch as the candidate's. A peer whose device could not be read has unknown
1024
+ * replaced fields: it is OVER-approximated — nothing replaced, every self-read
1025
+ * an edge (edges only added, never a cycle hidden) — and its device is listed
1026
+ * in `unread`, so the validator refuses a cyclic component only when it holds
1027
+ * a candidate node, and names the device (`findCandidateCycles`). An unreadable
1028
+ * device never blocks an unrelated save. A peer whose device is GONE is treated
1029
+ * the same way (M-1): a stored peer validated earlier may still read it, and
1030
+ * assuming every field replaced would remove edges.
1031
+ *
1032
+ * The candidate's own old version is kept in the list: the graph drops it BY ID
1033
+ * (`compositionEdges`), never by target — that is how a second block on
1034
+ * the same device reaches `duplicate-target`.
1035
+ */
1036
+ function storedCompositions(blocks) {
1037
+ return blocks.flatMap((block) => {
1038
+ const composition = coreBlockComposition(block);
1039
+ return composition === null ? [] : [{
1040
+ blockId: block.id,
1041
+ composition
1042
+ }];
1043
+ });
1044
+ }
1045
+ /** The existing devices the stored customizations write — each read once with the candidate's. */
1046
+ function storedCustomizationTargets(blocks) {
1047
+ return storedCompositions(blocks).flatMap(({ composition }) => composition.target.kind === "existing" ? [composition.target.device] : []);
1048
+ }
1049
+ /** The fields a stored customization replaces, or why they are unknown. */
1050
+ function replacedOf(stored, description, lookupCap) {
1051
+ if (description === void 0) return { error: "it was not read" };
1052
+ if (description.kind === "unreadable") return { error: description.error };
1053
+ if (description.kind === "absent") return { error: "the device does not exist" };
1054
+ return replacedFieldKeys(planComposition(stored.composition, lookupCap, description));
1055
+ }
1056
+ function peerOf(stored, targets, lookupCap, candidateBlockId) {
1057
+ const { blockId, composition } = stored;
1058
+ if (composition.target.kind === "new") return {
1059
+ peer: {
1060
+ blockId,
1061
+ target: composedDeviceRef(blockId),
1062
+ composition
1063
+ },
1064
+ unread: null
1065
+ };
1066
+ const device = composition.target.device;
1067
+ const key = compositionSourceKey(device);
1068
+ const replaced = replacedOf(stored, targets.get(key), lookupCap);
1069
+ if (!("error" in replaced)) return {
1070
+ peer: {
1071
+ blockId,
1072
+ target: device,
1073
+ composition,
1074
+ replaced
1075
+ },
1076
+ unread: null
1077
+ };
1078
+ return {
1079
+ peer: {
1080
+ blockId,
1081
+ target: device,
1082
+ composition,
1083
+ replaced: /* @__PURE__ */ new Set()
1084
+ },
1085
+ unread: blockId === candidateBlockId ? null : [key, replaced.error]
1086
+ };
1087
+ }
1088
+ function storedPeers(blocks, targets, lookupCap, candidateBlockId) {
1089
+ const read = storedCompositions(blocks).map((stored) => peerOf(stored, targets, lookupCap, candidateBlockId));
1090
+ return {
1091
+ peers: read.map((r) => r.peer),
1092
+ unread: new Map(read.flatMap((r) => r.unread === null ? [] : [r.unread]))
1093
+ };
1094
+ }
1095
+ //#endregion
1012
1096
  //#region src/builtins/core-blocks/composition-api.ts
1013
1097
  /**
1014
1098
  * The composition half of the `core-blocks` provider (D659). Flat methods,
1015
1099
  * spread into the provider: no nested sub-object.
1016
1100
  *
1017
- * Every save is VALIDATED against the live sources and every other stored
1018
- * composition first; a refusal names each problem by path and reason (D391)
1019
- * and stores nothing. The store's own refusals (not loaded, unknown block, a
1020
- * code block) propagate unchanged — this layer never swallows them.
1101
+ * Every save is VALIDATED against the live sources, its existing target (read
1102
+ * once: slim row + `getBindings`) and every stored composition first; a refusal
1103
+ * names each problem by path and reason (D391) and stores nothing. The store's
1104
+ * own refusals (not loaded, unknown block, a code block) propagate unchanged —
1105
+ * this layer never swallows them.
1106
+ *
1107
+ * A customization of an existing device (D663) has no name of its own: the
1108
+ * server derives it (`customizationBlockName`), refuses one the client sends,
1109
+ * and never lets it move to another device. One per device, by block id (S2).
1021
1110
  */
1022
1111
  var NOT_LOADED_REASON = "core_blocks has not loaded yet";
1023
1112
  function writeQueue() {
@@ -1044,24 +1133,30 @@ function refuseIfChanged(deps, blockId, expectedUpdatedAt) {
1044
1133
  } });
1045
1134
  throw new Error(`core-blocks: the composition "${stored.name}" changed since you read it: stored updatedAt ${stored.updatedAt}, yours ${expectedUpdatedAt} — reload and re-apply your edit`);
1046
1135
  }
1136
+ /** The device a composition writes, when it is an EXISTING one; null for a new device. */
1137
+ function existingTarget(composition) {
1138
+ return composition.target.kind === "existing" ? composition.target.device : null;
1139
+ }
1047
1140
  async function validateAgainstStore(deps, composition, blockId) {
1048
1141
  if (!deps.store.isLoaded()) throw new Error(`${NOT_LOADED_REASON} — a composition cannot be judged against the stored ones until it has; retry shortly`);
1049
1142
  const self = blockId === null ? null : composedDeviceRef(blockId);
1050
- const peers = deps.store.list().flatMap((block) => {
1051
- const peer = coreBlockComposition(block);
1052
- return peer === null || block.id === blockId ? [] : [{
1053
- target: composedDeviceRef(block.id),
1054
- composition: peer
1055
- }];
1056
- });
1057
- const selfKey = self === null ? null : compositionSourceKey(self);
1058
- const refs = compositionSourceRefs(composition).filter((r) => compositionSourceKey(r) !== selfKey);
1059
- const sources = await deps.describeSources(refs);
1143
+ const target = existingTarget(composition);
1144
+ const ownKeys = new Set([self, target].flatMap((ref) => ref === null ? [] : [compositionSourceKey(ref)]));
1145
+ const refs = compositionSourceRefs(composition).filter((r) => !ownKeys.has(compositionSourceKey(r)));
1146
+ const blocks = deps.store.list();
1147
+ const read = await deps.describe(refs, [...target === null ? [] : [target], ...storedCustomizationTargets(blocks)]);
1148
+ const stored = storedPeers(blocks, read.targets, deps.lookupCap, blockId);
1060
1149
  return validateComposition(composition, {
1061
1150
  lookupCap: deps.lookupCap,
1062
- sources,
1151
+ sources: read.sources,
1063
1152
  self,
1064
- peers
1153
+ blockId,
1154
+ target: target === null ? null : read.targets.get(compositionSourceKey(target)) ?? {
1155
+ kind: "unreadable",
1156
+ error: "it was not read"
1157
+ },
1158
+ peers: stored.peers,
1159
+ unreadTargets: stored.unread
1065
1160
  });
1066
1161
  }
1067
1162
  function describeProblems(validation) {
@@ -1096,6 +1191,16 @@ async function readStates(deps, blockIds) {
1096
1191
  }]));
1097
1192
  }
1098
1193
  }
1194
+ /**
1195
+ * One block's composer state. A block the answer leaves out is UNREACHABLE,
1196
+ * never folded into "no composition" (D315, M-4).
1197
+ */
1198
+ async function readState(deps, blockId) {
1199
+ return (await readStates(deps, [blockId])).get(blockId) ?? {
1200
+ kind: "unreachable",
1201
+ error: "the composer state read returned no entry for the block"
1202
+ };
1203
+ }
1099
1204
  function toViews(blocks, states) {
1100
1205
  return blocks.flatMap((block) => {
1101
1206
  const state = states.get(block.id);
@@ -1105,6 +1210,98 @@ function toViews(blocks, states) {
1105
1210
  }];
1106
1211
  });
1107
1212
  }
1213
+ var NO_NAME_OF_ITS_OWN = "a customization has no name of its own — the device's name is shown; omit `name`";
1214
+ var NEW_DEVICE_NEEDS_NAME = "a new device needs a name";
1215
+ var DEVICE_IS_FIXED = "a customization's device cannot be changed — delete it and customize the other device";
1216
+ var NEVER_BECOMES_CUSTOMIZATION = "a composed device cannot become the customization of another device — create the customization instead";
1217
+ /**
1218
+ * The numeric id of a save's existing target, for the refusal line's tag
1219
+ * (D391: a line about a device carries `tags.deviceId`). Read only on a
1220
+ * refusal — one listing — and a failed read names itself on the line instead.
1221
+ */
1222
+ async function resolveTarget(deps, target) {
1223
+ try {
1224
+ const found = (await deps.describe([], [target])).targets.get(compositionSourceKey(target));
1225
+ if (found?.kind === "found") return {
1226
+ kind: "device",
1227
+ deviceId: found.deviceId
1228
+ };
1229
+ return {
1230
+ kind: "unresolved",
1231
+ why: found?.kind ?? "not described"
1232
+ };
1233
+ } catch (err) {
1234
+ return {
1235
+ kind: "unresolved",
1236
+ why: errMsg(err)
1237
+ };
1238
+ }
1239
+ }
1240
+ /** A save refused before validation, by name (D391): logged, then thrown. */
1241
+ async function refuseSave(deps, subject, target, reason) {
1242
+ const resolved = target === null ? null : await resolveTarget(deps, target);
1243
+ deps.logger.warn("composition save refused", {
1244
+ ...resolved?.kind === "device" ? { tags: { deviceId: resolved.deviceId } } : {},
1245
+ meta: {
1246
+ name: subject.name,
1247
+ blockId: subject.blockId,
1248
+ target: target === null ? null : compositionSourceKey(target),
1249
+ ...resolved?.kind === "unresolved" ? { targetUnresolved: resolved.why } : {},
1250
+ reason
1251
+ }
1252
+ });
1253
+ throw new Error(`core-blocks: the composition "${subject.name}" was refused — ${reason}`);
1254
+ }
1255
+ /**
1256
+ * The name a new block is stored under (rule 1): a new device's own, required;
1257
+ * a customization's DERIVED from its device (ruling S2), never the client's.
1258
+ */
1259
+ async function nameOnCreate(deps, block) {
1260
+ const target = existingTarget(block.composition);
1261
+ if (target === null) {
1262
+ if (block.name === void 0) return refuseSave(deps, {
1263
+ name: "(unnamed)",
1264
+ blockId: null
1265
+ }, null, NEW_DEVICE_NEEDS_NAME);
1266
+ return block.name;
1267
+ }
1268
+ if (block.name !== void 0) return refuseSave(deps, {
1269
+ name: block.name,
1270
+ blockId: null
1271
+ }, target, NO_NAME_OF_ITS_OWN);
1272
+ return customizationBlockName(target);
1273
+ }
1274
+ /**
1275
+ * A customization is pinned to its device and carries no client name (rule 2);
1276
+ * a composed device never turns into one — its name would be the client's. An
1277
+ * unknown or code block falls through to the store, whose refusal names it.
1278
+ */
1279
+ async function refuseRetarget(deps, blockId, patch) {
1280
+ const stored = deps.store.get(blockId);
1281
+ const current = stored === null ? null : coreBlockComposition(stored);
1282
+ if (stored === null || current === null) return;
1283
+ const subject = {
1284
+ name: stored.name,
1285
+ blockId
1286
+ };
1287
+ const pinned = existingTarget(current);
1288
+ const next = patch.composition === void 0 ? pinned : existingTarget(patch.composition);
1289
+ if (pinned === null) {
1290
+ if (next !== null) return refuseSave(deps, subject, next, NEVER_BECOMES_CUSTOMIZATION);
1291
+ return;
1292
+ }
1293
+ if (patch.name !== void 0) return refuseSave(deps, subject, pinned, NO_NAME_OF_ITS_OWN);
1294
+ if (next === null || compositionSourceKey(next) !== compositionSourceKey(pinned)) return refuseSave(deps, subject, pinned, DEVICE_IS_FIXED);
1295
+ }
1296
+ /** The block customizing `device`, found by TARGET — never by its derived name (rule 4). */
1297
+ function customizationOf(blocks, device) {
1298
+ const key = compositionSourceKey(device);
1299
+ return blocks.find((block) => {
1300
+ const composition = coreBlockComposition(block);
1301
+ const target = composition === null ? null : existingTarget(composition);
1302
+ return target !== null && compositionSourceKey(target) === key;
1303
+ }) ?? null;
1304
+ }
1108
1305
  /**
1109
1306
  * Every write runs validate-then-persist on ONE chain. Validation awaits a
1110
1307
  * source read before the save lands; two unserialized saves (A reads B, B
@@ -1115,12 +1312,16 @@ function buildCompositionApi(deps) {
1115
1312
  const serialized = writeQueue();
1116
1313
  return {
1117
1314
  createComposition: ({ block, caller }) => serialized(async () => {
1315
+ const name = await nameOnCreate(deps, block);
1118
1316
  const validation = await validateAgainstStore(deps, block.composition, null);
1119
1317
  if (!validation.ok) refuse(deps, {
1120
- name: block.name,
1318
+ name,
1121
1319
  blockId: null
1122
1320
  }, validation);
1123
- const created = await deps.store.createComposition(block, caller.userId);
1321
+ const created = await deps.store.createComposition({
1322
+ ...block,
1323
+ name
1324
+ }, caller.userId);
1124
1325
  deps.logger.info("composition created", { meta: {
1125
1326
  blockId: created.id,
1126
1327
  name: created.name,
@@ -1131,6 +1332,7 @@ function buildCompositionApi(deps) {
1131
1332
  }),
1132
1333
  updateComposition: ({ blockId, expectedUpdatedAt, block, caller }) => serialized(async () => {
1133
1334
  refuseIfChanged(deps, blockId, expectedUpdatedAt);
1335
+ await refuseRetarget(deps, blockId, block);
1134
1336
  if (block.composition !== void 0) {
1135
1337
  const validation = await validateAgainstStore(deps, block.composition, blockId);
1136
1338
  if (!validation.ok) refuse(deps, {
@@ -1156,10 +1358,12 @@ function buildCompositionApi(deps) {
1156
1358
  state: "loaded",
1157
1359
  view: null
1158
1360
  };
1159
- const [view] = toViews([block], await readStates(deps, [blockId]));
1160
1361
  return {
1161
1362
  state: "loaded",
1162
- view: view ?? null
1363
+ view: {
1364
+ block,
1365
+ state: await readState(deps, blockId)
1366
+ }
1163
1367
  };
1164
1368
  },
1165
1369
  listCompositions: async () => {
@@ -1177,18 +1381,45 @@ function buildCompositionApi(deps) {
1177
1381
  views: toViews(blocks, await readStates(deps, blocks.map((b) => b.id)))
1178
1382
  };
1179
1383
  },
1384
+ getCustomization: async ({ addonId, stableId }) => {
1385
+ if (!deps.store.isLoaded()) return {
1386
+ state: "not-loaded",
1387
+ reason: NOT_LOADED_REASON
1388
+ };
1389
+ const block = customizationOf(deps.store.list(), {
1390
+ addonId,
1391
+ stableId
1392
+ });
1393
+ if (block === null) return {
1394
+ state: "loaded",
1395
+ view: null
1396
+ };
1397
+ return {
1398
+ state: "loaded",
1399
+ view: {
1400
+ block,
1401
+ state: await readState(deps, block.id)
1402
+ }
1403
+ };
1404
+ },
1180
1405
  validateComposition: async ({ composition, blockId }) => validateAgainstStore(deps, composition, blockId ?? null)
1181
1406
  };
1182
1407
  }
1183
1408
  //#endregion
1184
1409
  //#region src/builtins/core-blocks/composition-sources.ts
1185
1410
  /**
1186
- * Describe a composition's sources for SAVE-time validation (D659): one
1187
- * `listAll` per owning addon, one `getBindings` per found device. A read that
1188
- * fails makes the source `unreadable`, and the save is REFUSED rather than
1411
+ * Describe what a composition reads and writes for SAVE-time validation
1412
+ * (D659, D663): one `listAll` per owning addon, one `getBindings` per found
1413
+ * device — shared by a device that is both a source and a target. A read that
1414
+ * fails makes the device `unreadable`, and the save is REFUSED rather than
1189
1415
  * stored unchecked. Refusing a save destroys nothing (D49).
1190
1416
  *
1191
- * Sources are keyed by `{addonId, stableId}` — never a numeric device id,
1417
+ * A SOURCE is described by the caps bound to it; an EXISTING TARGET by its row
1418
+ * type and each bound cap with the addon serving it — what `planComposition`
1419
+ * needs to tell a replace from an add (the composer reads it the same way, from
1420
+ * the same two reads: the slim row and `getBindings`).
1421
+ *
1422
+ * Devices are keyed by `{addonId, stableId}` — never a numeric device id,
1192
1423
  * which is this node's allocation and not the composition's identity.
1193
1424
  */
1194
1425
  async function listOwners(ports, refs) {
@@ -1202,7 +1433,7 @@ async function listOwners(ports, refs) {
1202
1433
  }));
1203
1434
  return new Map(listed);
1204
1435
  }
1205
- async function describeOne(ports, ref, listing) {
1436
+ function rowOf(ref, listing) {
1206
1437
  if (listing === void 0) return {
1207
1438
  kind: "unreadable",
1208
1439
  error: `\`${ref.addonId}\` was not listed`
@@ -1212,24 +1443,68 @@ async function describeOne(ports, ref, listing) {
1212
1443
  error: listing.error
1213
1444
  };
1214
1445
  const row = listing.find((r) => r.stableId === ref.stableId);
1215
- if (row === void 0) return { kind: "absent" };
1216
- try {
1217
- return {
1218
- kind: "found",
1219
- deviceId: row.id,
1220
- caps: new Set(await ports.boundCaps(row.id))
1221
- };
1222
- } catch (err) {
1223
- return {
1224
- kind: "unreadable",
1225
- error: errMsg(err)
1226
- };
1227
- }
1446
+ return row === void 0 ? { kind: "absent" } : {
1447
+ kind: "row",
1448
+ row
1449
+ };
1450
+ }
1451
+ /** ONE `getBindings` per found device, whether it is read as a source, a target, or both. */
1452
+ async function readBindings(ports, deviceIds) {
1453
+ const read = await Promise.all([...new Set(deviceIds)].map(async (deviceId) => {
1454
+ try {
1455
+ return [deviceId, await ports.bindings(deviceId)];
1456
+ } catch (err) {
1457
+ return [deviceId, { error: errMsg(err) }];
1458
+ }
1459
+ }));
1460
+ return new Map(read);
1461
+ }
1462
+ function describeSource(lookup, bindings) {
1463
+ if (lookup.kind !== "row") return lookup;
1464
+ const bound = bindings.get(lookup.row.id);
1465
+ if (bound === void 0) return {
1466
+ kind: "unreadable",
1467
+ error: "its bindings were not read"
1468
+ };
1469
+ if ("error" in bound) return {
1470
+ kind: "unreadable",
1471
+ error: bound.error
1472
+ };
1473
+ return {
1474
+ kind: "found",
1475
+ deviceId: lookup.row.id,
1476
+ caps: new Set(bound.map((b) => b.capName))
1477
+ };
1228
1478
  }
1229
- async function describeCompositionSources(ports, refs) {
1230
- const owners = await listOwners(ports, refs);
1231
- const described = await Promise.all(refs.map(async (ref) => [compositionSourceKey(ref), await describeOne(ports, ref, owners.get(ref.addonId))]));
1232
- return new Map(described);
1479
+ function describeTarget(lookup, bindings) {
1480
+ if (lookup.kind !== "row") return lookup;
1481
+ const { id, type } = lookup.row;
1482
+ const bound = bindings.get(id);
1483
+ if (bound === void 0) return {
1484
+ kind: "unreadable",
1485
+ error: "its bindings were not read"
1486
+ };
1487
+ if ("error" in bound) return {
1488
+ kind: "unreadable",
1489
+ error: bound.error
1490
+ };
1491
+ return {
1492
+ kind: "found",
1493
+ deviceId: id,
1494
+ type,
1495
+ nativeCaps: new Map(bound.map((b) => [b.capName, b.nativeAddonId]))
1496
+ };
1497
+ }
1498
+ async function describeCompositionSources(ports, sources, targets = []) {
1499
+ const owners = await listOwners(ports, [...sources, ...targets]);
1500
+ const lookup = (ref) => [compositionSourceKey(ref), rowOf(ref, owners.get(ref.addonId))];
1501
+ const sourceRows = sources.map(lookup);
1502
+ const targetRows = targets.map(lookup);
1503
+ const bindings = await readBindings(ports, [...sourceRows, ...targetRows].flatMap(([, l]) => l.kind === "row" ? [l.row.id] : []));
1504
+ return {
1505
+ sources: new Map(sourceRows.map(([key, l]) => [key, describeSource(l, bindings)])),
1506
+ targets: new Map(targetRows.map(([key, l]) => [key, describeTarget(l, bindings)]))
1507
+ };
1233
1508
  }
1234
1509
  //#endregion
1235
1510
  //#region src/builtins/core-blocks/core-blocks.addon.ts
@@ -1503,9 +1778,10 @@ var CoreBlocksAddon = class extends BaseAddon {
1503
1778
  };
1504
1779
  }
1505
1780
  /**
1506
- * The composition methods (D659), flat. Sources are read over `ctx.api` at
1507
- * SAVE time — one listing per owning addon, one binding read per found
1508
- * device — and a read that fails refuses the save rather than skipping it.
1781
+ * The composition methods (D659), flat. Sources and existing targets are read
1782
+ * over `ctx.api` at SAVE time — one slim listing per owning addon, one binding
1783
+ * read per found device — and a read that fails refuses the save rather than
1784
+ * skipping it.
1509
1785
  */
1510
1786
  buildCompositionApi() {
1511
1787
  const requireApi = () => {
@@ -1515,13 +1791,13 @@ var CoreBlocksAddon = class extends BaseAddon {
1515
1791
  };
1516
1792
  return buildCompositionApi({
1517
1793
  store: this.requireStore(),
1518
- describeSources: (refs) => describeCompositionSources({
1794
+ describe: (sources, targets) => describeCompositionSources({
1519
1795
  listByAddon: async (addonId) => requireApi().deviceManager.listAll.query({
1520
1796
  addonId,
1521
1797
  projection: "slim"
1522
1798
  }),
1523
- boundCaps: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries.map((e) => e.capName)
1524
- }, refs),
1799
+ bindings: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries
1800
+ }, sources, targets),
1525
1801
  lookupCap: buildCapabilityLookup(ALL_CAPABILITY_DEFINITIONS),
1526
1802
  composerStates: async (blockIds) => (await requireApi().composer.getStates.query({ blockIds: [...blockIds] })).states,
1527
1803
  convergeSoon: () => this.convergeSoon(),
@@ -0,0 +1,12 @@
1
+ import { ICapabilityRegistry, IScopedLogger } from '@camstack/types';
2
+ import { DeviceManagerSettings } from './device-meta-store.js';
3
+ import { ClaimsIndexView, DeviceStateMirror } from './device-state-mirror.js';
4
+ export interface ClaimedStatusOverlayDeps {
5
+ readonly mirror: DeviceStateMirror;
6
+ /** The claims index as it is NOW — loaded or not; read at every question, never captured. */
7
+ readonly claimsIndex: () => ClaimsIndexView;
8
+ /** The settings door the mirror reads a device's row through. */
9
+ readonly settings: () => DeviceManagerSettings;
10
+ readonly logger: IScopedLogger;
11
+ }
12
+ export declare function installClaimedStatusOverlay(registry: ICapabilityRegistry, deps: ClaimedStatusOverlayDeps): () => void;
@@ -30,6 +30,13 @@ export declare class DeviceManagerAddon extends BaseAddon {
30
30
  * `ctx`); `null` before initialise / when `ctx.settings` is absent.
31
31
  */
32
32
  private stateMirrorImpl;
33
+ /**
34
+ * Removes the claimed-status overlay this addon installed on the capability
35
+ * registry (D663, S3): `getStatus` of a claimed cap answers the mirror for
36
+ * every caller while it is installed. Run in `onShutdown`; `null` before
37
+ * init or when the kernel handed the addon no registry.
38
+ */
39
+ private uninstallStatusOverlay;
33
40
  /**
34
41
  * Cross-process native-provider cache: deviceId (numeric) → capName → { addonId, nodeId }.
35
42
  * Kept in sync with `DeviceBindingsChanged` push events emitted by forked
@@ -64,6 +71,15 @@ export declare class DeviceManagerAddon extends BaseAddon {
64
71
  private readonly removalFence;
65
72
  /** The runtime-state mirror, asserted present (constructed in onInitialize). */
66
73
  private get stateMirror();
74
+ /**
75
+ * The per-field claims index (D663): which devices carry a claim, for the
76
+ * seed and for `listClaims`. Constructed at init and loaded before the first
77
+ * provider method can run; a load that fails leaves it `not-loaded`, and
78
+ * every seed reads its row until a later call loads it.
79
+ */
80
+ private fieldClaimsImpl;
81
+ /** The claims index, asserted present (constructed in onInitialize). */
82
+ private get fieldClaims();
67
83
  /** Build the `ProviderHost` the extracted provider-method modules
68
84
  * (`device-meta-actions`, `device-queries`) reach — exposes the addon's
69
85
  * private state/methods they need without leaking the class internals.