@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,7 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_chunk = require("../../chunk-Cek0wNdY.js");
3
- const require_dist = require("../../dist-CkRwnhbo.js");
3
+ const require_dist = require("../../dist-BtNkk3Qz.js");
4
4
  let node_crypto = require("node:crypto");
5
+ let _camstack_types_node = require("@camstack/types/node");
5
6
  let node_fs_promises = require("node:fs/promises");
6
7
  node_fs_promises = require_chunk.__toESM(node_fs_promises);
7
8
  let node_path = require("node:path");
@@ -340,7 +341,7 @@ var BlockSupervisor = class {
340
341
  for (const { block } of compositions) await this.report(block.id, "failed", "no composer is available on this node — a composition cannot run here");
341
342
  return;
342
343
  }
343
- if (runners.find((r) => r.name === "composer")?.state === "failed") {
344
+ if (runners.find((r) => r.name === require_dist.COMPOSER_RUNNER_ID)?.state === "failed") {
344
345
  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");
345
346
  return;
346
347
  }
@@ -1013,15 +1014,103 @@ function armBlocksIntegration(options) {
1013
1014
  })() };
1014
1015
  }
1015
1016
  //#endregion
1017
+ //#region src/builtins/core-blocks/composition-peers.ts
1018
+ /**
1019
+ * The stored compositions a save is judged against, as graph peers (D663,
1020
+ * ruling S2). Each is identified by its BLOCK ID, targets the device it writes
1021
+ * — `composedDeviceRef(block.id)` for a new device, the customized device for an
1022
+ * existing one — and, on an existing device, names the fields it REPLACES, so
1023
+ * its self-read of a replaced field is the native value and never an edge.
1024
+ *
1025
+ * `replaced` is not stored anywhere: it is recomputed at save time from the
1026
+ * peer's target description (`planComposition(..., target)`), read in the same
1027
+ * batch as the candidate's. A peer whose device could not be read has unknown
1028
+ * replaced fields: it is OVER-approximated — nothing replaced, every self-read
1029
+ * an edge (edges only added, never a cycle hidden) — and its device is listed
1030
+ * in `unread`, so the validator refuses a cyclic component only when it holds
1031
+ * a candidate node, and names the device (`findCandidateCycles`). An unreadable
1032
+ * device never blocks an unrelated save. A peer whose device is GONE is treated
1033
+ * the same way (M-1): a stored peer validated earlier may still read it, and
1034
+ * assuming every field replaced would remove edges.
1035
+ *
1036
+ * The candidate's own old version is kept in the list: the graph drops it BY ID
1037
+ * (`compositionEdges`), never by target — that is how a second block on
1038
+ * the same device reaches `duplicate-target`.
1039
+ */
1040
+ function storedCompositions(blocks) {
1041
+ return blocks.flatMap((block) => {
1042
+ const composition = require_dist.coreBlockComposition(block);
1043
+ return composition === null ? [] : [{
1044
+ blockId: block.id,
1045
+ composition
1046
+ }];
1047
+ });
1048
+ }
1049
+ /** The existing devices the stored customizations write — each read once with the candidate's. */
1050
+ function storedCustomizationTargets(blocks) {
1051
+ return storedCompositions(blocks).flatMap(({ composition }) => composition.target.kind === "existing" ? [composition.target.device] : []);
1052
+ }
1053
+ /** The fields a stored customization replaces, or why they are unknown. */
1054
+ function replacedOf(stored, description, lookupCap) {
1055
+ if (description === void 0) return { error: "it was not read" };
1056
+ if (description.kind === "unreadable") return { error: description.error };
1057
+ if (description.kind === "absent") return { error: "the device does not exist" };
1058
+ return require_dist.replacedFieldKeys(require_dist.planComposition(stored.composition, lookupCap, description));
1059
+ }
1060
+ function peerOf(stored, targets, lookupCap, candidateBlockId) {
1061
+ const { blockId, composition } = stored;
1062
+ if (composition.target.kind === "new") return {
1063
+ peer: {
1064
+ blockId,
1065
+ target: require_dist.composedDeviceRef(blockId),
1066
+ composition
1067
+ },
1068
+ unread: null
1069
+ };
1070
+ const device = composition.target.device;
1071
+ const key = require_dist.compositionSourceKey(device);
1072
+ const replaced = replacedOf(stored, targets.get(key), lookupCap);
1073
+ if (!("error" in replaced)) return {
1074
+ peer: {
1075
+ blockId,
1076
+ target: device,
1077
+ composition,
1078
+ replaced
1079
+ },
1080
+ unread: null
1081
+ };
1082
+ return {
1083
+ peer: {
1084
+ blockId,
1085
+ target: device,
1086
+ composition,
1087
+ replaced: /* @__PURE__ */ new Set()
1088
+ },
1089
+ unread: blockId === candidateBlockId ? null : [key, replaced.error]
1090
+ };
1091
+ }
1092
+ function storedPeers(blocks, targets, lookupCap, candidateBlockId) {
1093
+ const read = storedCompositions(blocks).map((stored) => peerOf(stored, targets, lookupCap, candidateBlockId));
1094
+ return {
1095
+ peers: read.map((r) => r.peer),
1096
+ unread: new Map(read.flatMap((r) => r.unread === null ? [] : [r.unread]))
1097
+ };
1098
+ }
1099
+ //#endregion
1016
1100
  //#region src/builtins/core-blocks/composition-api.ts
1017
1101
  /**
1018
1102
  * The composition half of the `core-blocks` provider (D659). Flat methods,
1019
1103
  * spread into the provider: no nested sub-object.
1020
1104
  *
1021
- * Every save is VALIDATED against the live sources and every other stored
1022
- * composition first; a refusal names each problem by path and reason (D391)
1023
- * and stores nothing. The store's own refusals (not loaded, unknown block, a
1024
- * code block) propagate unchanged — this layer never swallows them.
1105
+ * Every save is VALIDATED against the live sources, its existing target (read
1106
+ * once: slim row + `getBindings`) and every stored composition first; a refusal
1107
+ * names each problem by path and reason (D391) and stores nothing. The store's
1108
+ * own refusals (not loaded, unknown block, a code block) propagate unchanged —
1109
+ * this layer never swallows them.
1110
+ *
1111
+ * A customization of an existing device (D663) has no name of its own: the
1112
+ * server derives it (`customizationBlockName`), refuses one the client sends,
1113
+ * and never lets it move to another device. One per device, by block id (S2).
1025
1114
  */
1026
1115
  var NOT_LOADED_REASON = "core_blocks has not loaded yet";
1027
1116
  function writeQueue() {
@@ -1048,24 +1137,30 @@ function refuseIfChanged(deps, blockId, expectedUpdatedAt) {
1048
1137
  } });
1049
1138
  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`);
1050
1139
  }
1140
+ /** The device a composition writes, when it is an EXISTING one; null for a new device. */
1141
+ function existingTarget(composition) {
1142
+ return composition.target.kind === "existing" ? composition.target.device : null;
1143
+ }
1051
1144
  async function validateAgainstStore(deps, composition, blockId) {
1052
1145
  if (!deps.store.isLoaded()) throw new Error(`${NOT_LOADED_REASON} — a composition cannot be judged against the stored ones until it has; retry shortly`);
1053
1146
  const self = blockId === null ? null : require_dist.composedDeviceRef(blockId);
1054
- const peers = deps.store.list().flatMap((block) => {
1055
- const peer = require_dist.coreBlockComposition(block);
1056
- return peer === null || block.id === blockId ? [] : [{
1057
- target: require_dist.composedDeviceRef(block.id),
1058
- composition: peer
1059
- }];
1060
- });
1061
- const selfKey = self === null ? null : require_dist.compositionSourceKey(self);
1062
- const refs = require_dist.compositionSourceRefs(composition).filter((r) => require_dist.compositionSourceKey(r) !== selfKey);
1063
- const sources = await deps.describeSources(refs);
1147
+ const target = existingTarget(composition);
1148
+ const ownKeys = new Set([self, target].flatMap((ref) => ref === null ? [] : [require_dist.compositionSourceKey(ref)]));
1149
+ const refs = require_dist.compositionSourceRefs(composition).filter((r) => !ownKeys.has(require_dist.compositionSourceKey(r)));
1150
+ const blocks = deps.store.list();
1151
+ const read = await deps.describe(refs, [...target === null ? [] : [target], ...storedCustomizationTargets(blocks)]);
1152
+ const stored = storedPeers(blocks, read.targets, deps.lookupCap, blockId);
1064
1153
  return require_dist.validateComposition(composition, {
1065
1154
  lookupCap: deps.lookupCap,
1066
- sources,
1155
+ sources: read.sources,
1067
1156
  self,
1068
- peers
1157
+ blockId,
1158
+ target: target === null ? null : read.targets.get(require_dist.compositionSourceKey(target)) ?? {
1159
+ kind: "unreadable",
1160
+ error: "it was not read"
1161
+ },
1162
+ peers: stored.peers,
1163
+ unreadTargets: stored.unread
1069
1164
  });
1070
1165
  }
1071
1166
  function describeProblems(validation) {
@@ -1100,6 +1195,16 @@ async function readStates(deps, blockIds) {
1100
1195
  }]));
1101
1196
  }
1102
1197
  }
1198
+ /**
1199
+ * One block's composer state. A block the answer leaves out is UNREACHABLE,
1200
+ * never folded into "no composition" (D315, M-4).
1201
+ */
1202
+ async function readState(deps, blockId) {
1203
+ return (await readStates(deps, [blockId])).get(blockId) ?? {
1204
+ kind: "unreachable",
1205
+ error: "the composer state read returned no entry for the block"
1206
+ };
1207
+ }
1103
1208
  function toViews(blocks, states) {
1104
1209
  return blocks.flatMap((block) => {
1105
1210
  const state = states.get(block.id);
@@ -1109,6 +1214,98 @@ function toViews(blocks, states) {
1109
1214
  }];
1110
1215
  });
1111
1216
  }
1217
+ var NO_NAME_OF_ITS_OWN = "a customization has no name of its own — the device's name is shown; omit `name`";
1218
+ var NEW_DEVICE_NEEDS_NAME = "a new device needs a name";
1219
+ var DEVICE_IS_FIXED = "a customization's device cannot be changed — delete it and customize the other device";
1220
+ var NEVER_BECOMES_CUSTOMIZATION = "a composed device cannot become the customization of another device — create the customization instead";
1221
+ /**
1222
+ * The numeric id of a save's existing target, for the refusal line's tag
1223
+ * (D391: a line about a device carries `tags.deviceId`). Read only on a
1224
+ * refusal — one listing — and a failed read names itself on the line instead.
1225
+ */
1226
+ async function resolveTarget(deps, target) {
1227
+ try {
1228
+ const found = (await deps.describe([], [target])).targets.get(require_dist.compositionSourceKey(target));
1229
+ if (found?.kind === "found") return {
1230
+ kind: "device",
1231
+ deviceId: found.deviceId
1232
+ };
1233
+ return {
1234
+ kind: "unresolved",
1235
+ why: found?.kind ?? "not described"
1236
+ };
1237
+ } catch (err) {
1238
+ return {
1239
+ kind: "unresolved",
1240
+ why: require_dist.errMsg(err)
1241
+ };
1242
+ }
1243
+ }
1244
+ /** A save refused before validation, by name (D391): logged, then thrown. */
1245
+ async function refuseSave(deps, subject, target, reason) {
1246
+ const resolved = target === null ? null : await resolveTarget(deps, target);
1247
+ deps.logger.warn("composition save refused", {
1248
+ ...resolved?.kind === "device" ? { tags: { deviceId: resolved.deviceId } } : {},
1249
+ meta: {
1250
+ name: subject.name,
1251
+ blockId: subject.blockId,
1252
+ target: target === null ? null : require_dist.compositionSourceKey(target),
1253
+ ...resolved?.kind === "unresolved" ? { targetUnresolved: resolved.why } : {},
1254
+ reason
1255
+ }
1256
+ });
1257
+ throw new Error(`core-blocks: the composition "${subject.name}" was refused — ${reason}`);
1258
+ }
1259
+ /**
1260
+ * The name a new block is stored under (rule 1): a new device's own, required;
1261
+ * a customization's DERIVED from its device (ruling S2), never the client's.
1262
+ */
1263
+ async function nameOnCreate(deps, block) {
1264
+ const target = existingTarget(block.composition);
1265
+ if (target === null) {
1266
+ if (block.name === void 0) return refuseSave(deps, {
1267
+ name: "(unnamed)",
1268
+ blockId: null
1269
+ }, null, NEW_DEVICE_NEEDS_NAME);
1270
+ return block.name;
1271
+ }
1272
+ if (block.name !== void 0) return refuseSave(deps, {
1273
+ name: block.name,
1274
+ blockId: null
1275
+ }, target, NO_NAME_OF_ITS_OWN);
1276
+ return (0, _camstack_types_node.customizationBlockName)(target);
1277
+ }
1278
+ /**
1279
+ * A customization is pinned to its device and carries no client name (rule 2);
1280
+ * a composed device never turns into one — its name would be the client's. An
1281
+ * unknown or code block falls through to the store, whose refusal names it.
1282
+ */
1283
+ async function refuseRetarget(deps, blockId, patch) {
1284
+ const stored = deps.store.get(blockId);
1285
+ const current = stored === null ? null : require_dist.coreBlockComposition(stored);
1286
+ if (stored === null || current === null) return;
1287
+ const subject = {
1288
+ name: stored.name,
1289
+ blockId
1290
+ };
1291
+ const pinned = existingTarget(current);
1292
+ const next = patch.composition === void 0 ? pinned : existingTarget(patch.composition);
1293
+ if (pinned === null) {
1294
+ if (next !== null) return refuseSave(deps, subject, next, NEVER_BECOMES_CUSTOMIZATION);
1295
+ return;
1296
+ }
1297
+ if (patch.name !== void 0) return refuseSave(deps, subject, pinned, NO_NAME_OF_ITS_OWN);
1298
+ if (next === null || require_dist.compositionSourceKey(next) !== require_dist.compositionSourceKey(pinned)) return refuseSave(deps, subject, pinned, DEVICE_IS_FIXED);
1299
+ }
1300
+ /** The block customizing `device`, found by TARGET — never by its derived name (rule 4). */
1301
+ function customizationOf(blocks, device) {
1302
+ const key = require_dist.compositionSourceKey(device);
1303
+ return blocks.find((block) => {
1304
+ const composition = require_dist.coreBlockComposition(block);
1305
+ const target = composition === null ? null : existingTarget(composition);
1306
+ return target !== null && require_dist.compositionSourceKey(target) === key;
1307
+ }) ?? null;
1308
+ }
1112
1309
  /**
1113
1310
  * Every write runs validate-then-persist on ONE chain. Validation awaits a
1114
1311
  * source read before the save lands; two unserialized saves (A reads B, B
@@ -1119,12 +1316,16 @@ function buildCompositionApi(deps) {
1119
1316
  const serialized = writeQueue();
1120
1317
  return {
1121
1318
  createComposition: ({ block, caller }) => serialized(async () => {
1319
+ const name = await nameOnCreate(deps, block);
1122
1320
  const validation = await validateAgainstStore(deps, block.composition, null);
1123
1321
  if (!validation.ok) refuse(deps, {
1124
- name: block.name,
1322
+ name,
1125
1323
  blockId: null
1126
1324
  }, validation);
1127
- const created = await deps.store.createComposition(block, caller.userId);
1325
+ const created = await deps.store.createComposition({
1326
+ ...block,
1327
+ name
1328
+ }, caller.userId);
1128
1329
  deps.logger.info("composition created", { meta: {
1129
1330
  blockId: created.id,
1130
1331
  name: created.name,
@@ -1135,6 +1336,7 @@ function buildCompositionApi(deps) {
1135
1336
  }),
1136
1337
  updateComposition: ({ blockId, expectedUpdatedAt, block, caller }) => serialized(async () => {
1137
1338
  refuseIfChanged(deps, blockId, expectedUpdatedAt);
1339
+ await refuseRetarget(deps, blockId, block);
1138
1340
  if (block.composition !== void 0) {
1139
1341
  const validation = await validateAgainstStore(deps, block.composition, blockId);
1140
1342
  if (!validation.ok) refuse(deps, {
@@ -1160,10 +1362,12 @@ function buildCompositionApi(deps) {
1160
1362
  state: "loaded",
1161
1363
  view: null
1162
1364
  };
1163
- const [view] = toViews([block], await readStates(deps, [blockId]));
1164
1365
  return {
1165
1366
  state: "loaded",
1166
- view: view ?? null
1367
+ view: {
1368
+ block,
1369
+ state: await readState(deps, blockId)
1370
+ }
1167
1371
  };
1168
1372
  },
1169
1373
  listCompositions: async () => {
@@ -1181,18 +1385,45 @@ function buildCompositionApi(deps) {
1181
1385
  views: toViews(blocks, await readStates(deps, blocks.map((b) => b.id)))
1182
1386
  };
1183
1387
  },
1388
+ getCustomization: async ({ addonId, stableId }) => {
1389
+ if (!deps.store.isLoaded()) return {
1390
+ state: "not-loaded",
1391
+ reason: NOT_LOADED_REASON
1392
+ };
1393
+ const block = customizationOf(deps.store.list(), {
1394
+ addonId,
1395
+ stableId
1396
+ });
1397
+ if (block === null) return {
1398
+ state: "loaded",
1399
+ view: null
1400
+ };
1401
+ return {
1402
+ state: "loaded",
1403
+ view: {
1404
+ block,
1405
+ state: await readState(deps, block.id)
1406
+ }
1407
+ };
1408
+ },
1184
1409
  validateComposition: async ({ composition, blockId }) => validateAgainstStore(deps, composition, blockId ?? null)
1185
1410
  };
1186
1411
  }
1187
1412
  //#endregion
1188
1413
  //#region src/builtins/core-blocks/composition-sources.ts
1189
1414
  /**
1190
- * Describe a composition's sources for SAVE-time validation (D659): one
1191
- * `listAll` per owning addon, one `getBindings` per found device. A read that
1192
- * fails makes the source `unreadable`, and the save is REFUSED rather than
1415
+ * Describe what a composition reads and writes for SAVE-time validation
1416
+ * (D659, D663): one `listAll` per owning addon, one `getBindings` per found
1417
+ * device — shared by a device that is both a source and a target. A read that
1418
+ * fails makes the device `unreadable`, and the save is REFUSED rather than
1193
1419
  * stored unchecked. Refusing a save destroys nothing (D49).
1194
1420
  *
1195
- * Sources are keyed by `{addonId, stableId}` — never a numeric device id,
1421
+ * A SOURCE is described by the caps bound to it; an EXISTING TARGET by its row
1422
+ * type and each bound cap with the addon serving it — what `planComposition`
1423
+ * needs to tell a replace from an add (the composer reads it the same way, from
1424
+ * the same two reads: the slim row and `getBindings`).
1425
+ *
1426
+ * Devices are keyed by `{addonId, stableId}` — never a numeric device id,
1196
1427
  * which is this node's allocation and not the composition's identity.
1197
1428
  */
1198
1429
  async function listOwners(ports, refs) {
@@ -1206,7 +1437,7 @@ async function listOwners(ports, refs) {
1206
1437
  }));
1207
1438
  return new Map(listed);
1208
1439
  }
1209
- async function describeOne(ports, ref, listing) {
1440
+ function rowOf(ref, listing) {
1210
1441
  if (listing === void 0) return {
1211
1442
  kind: "unreadable",
1212
1443
  error: `\`${ref.addonId}\` was not listed`
@@ -1216,24 +1447,68 @@ async function describeOne(ports, ref, listing) {
1216
1447
  error: listing.error
1217
1448
  };
1218
1449
  const row = listing.find((r) => r.stableId === ref.stableId);
1219
- if (row === void 0) return { kind: "absent" };
1220
- try {
1221
- return {
1222
- kind: "found",
1223
- deviceId: row.id,
1224
- caps: new Set(await ports.boundCaps(row.id))
1225
- };
1226
- } catch (err) {
1227
- return {
1228
- kind: "unreadable",
1229
- error: require_dist.errMsg(err)
1230
- };
1231
- }
1450
+ return row === void 0 ? { kind: "absent" } : {
1451
+ kind: "row",
1452
+ row
1453
+ };
1454
+ }
1455
+ /** ONE `getBindings` per found device, whether it is read as a source, a target, or both. */
1456
+ async function readBindings(ports, deviceIds) {
1457
+ const read = await Promise.all([...new Set(deviceIds)].map(async (deviceId) => {
1458
+ try {
1459
+ return [deviceId, await ports.bindings(deviceId)];
1460
+ } catch (err) {
1461
+ return [deviceId, { error: require_dist.errMsg(err) }];
1462
+ }
1463
+ }));
1464
+ return new Map(read);
1465
+ }
1466
+ function describeSource(lookup, bindings) {
1467
+ if (lookup.kind !== "row") return lookup;
1468
+ const bound = bindings.get(lookup.row.id);
1469
+ if (bound === void 0) return {
1470
+ kind: "unreadable",
1471
+ error: "its bindings were not read"
1472
+ };
1473
+ if ("error" in bound) return {
1474
+ kind: "unreadable",
1475
+ error: bound.error
1476
+ };
1477
+ return {
1478
+ kind: "found",
1479
+ deviceId: lookup.row.id,
1480
+ caps: new Set(bound.map((b) => b.capName))
1481
+ };
1232
1482
  }
1233
- async function describeCompositionSources(ports, refs) {
1234
- const owners = await listOwners(ports, refs);
1235
- const described = await Promise.all(refs.map(async (ref) => [require_dist.compositionSourceKey(ref), await describeOne(ports, ref, owners.get(ref.addonId))]));
1236
- return new Map(described);
1483
+ function describeTarget(lookup, bindings) {
1484
+ if (lookup.kind !== "row") return lookup;
1485
+ const { id, type } = lookup.row;
1486
+ const bound = bindings.get(id);
1487
+ if (bound === void 0) return {
1488
+ kind: "unreadable",
1489
+ error: "its bindings were not read"
1490
+ };
1491
+ if ("error" in bound) return {
1492
+ kind: "unreadable",
1493
+ error: bound.error
1494
+ };
1495
+ return {
1496
+ kind: "found",
1497
+ deviceId: id,
1498
+ type,
1499
+ nativeCaps: new Map(bound.map((b) => [b.capName, b.nativeAddonId]))
1500
+ };
1501
+ }
1502
+ async function describeCompositionSources(ports, sources, targets = []) {
1503
+ const owners = await listOwners(ports, [...sources, ...targets]);
1504
+ const lookup = (ref) => [require_dist.compositionSourceKey(ref), rowOf(ref, owners.get(ref.addonId))];
1505
+ const sourceRows = sources.map(lookup);
1506
+ const targetRows = targets.map(lookup);
1507
+ const bindings = await readBindings(ports, [...sourceRows, ...targetRows].flatMap(([, l]) => l.kind === "row" ? [l.row.id] : []));
1508
+ return {
1509
+ sources: new Map(sourceRows.map(([key, l]) => [key, describeSource(l, bindings)])),
1510
+ targets: new Map(targetRows.map(([key, l]) => [key, describeTarget(l, bindings)]))
1511
+ };
1237
1512
  }
1238
1513
  //#endregion
1239
1514
  //#region src/builtins/core-blocks/core-blocks.addon.ts
@@ -1507,9 +1782,10 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
1507
1782
  };
1508
1783
  }
1509
1784
  /**
1510
- * The composition methods (D659), flat. Sources are read over `ctx.api` at
1511
- * SAVE time — one listing per owning addon, one binding read per found
1512
- * device — and a read that fails refuses the save rather than skipping it.
1785
+ * The composition methods (D659), flat. Sources and existing targets are read
1786
+ * over `ctx.api` at SAVE time — one slim listing per owning addon, one binding
1787
+ * read per found device — and a read that fails refuses the save rather than
1788
+ * skipping it.
1513
1789
  */
1514
1790
  buildCompositionApi() {
1515
1791
  const requireApi = () => {
@@ -1519,13 +1795,13 @@ var CoreBlocksAddon = class extends require_dist.BaseAddon {
1519
1795
  };
1520
1796
  return buildCompositionApi({
1521
1797
  store: this.requireStore(),
1522
- describeSources: (refs) => describeCompositionSources({
1798
+ describe: (sources, targets) => describeCompositionSources({
1523
1799
  listByAddon: async (addonId) => requireApi().deviceManager.listAll.query({
1524
1800
  addonId,
1525
1801
  projection: "slim"
1526
1802
  }),
1527
- boundCaps: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries.map((e) => e.capName)
1528
- }, refs),
1803
+ bindings: async (deviceId) => (await requireApi().deviceManager.getBindings.query({ deviceId })).entries
1804
+ }, sources, targets),
1529
1805
  lookupCap: require_dist.buildCapabilityLookup(require_dist.ALL_CAPABILITY_DEFINITIONS),
1530
1806
  composerStates: async (blockIds) => (await requireApi().composer.getStates.query({ blockIds: [...blockIds] })).states,
1531
1807
  convergeSoon: () => this.convergeSoon(),