@camstack/system 1.2.316 → 1.2.317

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 (87) hide show
  1. package/dist/addon-runner.js +1 -1
  2. package/dist/addon-runner.mjs +1 -1
  3. package/dist/auth/auth-manager.d.ts +11 -0
  4. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  5. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  6. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  7. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  8. package/dist/builtins/alerts/alerts.addon.js +1 -1
  9. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  10. package/dist/builtins/autotrack/index.js +1 -1
  11. package/dist/builtins/autotrack/index.mjs +1 -1
  12. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  13. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  14. package/dist/builtins/camera-grid/index.js +1 -1
  15. package/dist/builtins/camera-grid/index.mjs +1 -1
  16. package/dist/builtins/composer/composer.addon.js +347 -181
  17. package/dist/builtins/composer/composer.addon.mjs +347 -181
  18. package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
  19. package/dist/builtins/composer/composition-runtime.d.ts +11 -52
  20. package/dist/builtins/composer/composition-target.d.ts +50 -0
  21. package/dist/builtins/composer/existing-target.d.ts +19 -1
  22. package/dist/builtins/composer/own-field-reads.d.ts +19 -0
  23. package/dist/builtins/console-logging/index.js +1 -1
  24. package/dist/builtins/console-logging/index.mjs +1 -1
  25. package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
  26. package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
  27. package/dist/builtins/device-manager/device-manager.addon.js +265 -29
  28. package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
  29. package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
  30. package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
  31. package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
  32. package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
  33. package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
  34. package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
  35. package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
  36. package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
  37. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
  38. package/dist/builtins/hub-forwarder/index.js +1 -1
  39. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  40. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  41. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  42. package/dist/builtins/local-auth/local-auth.addon.js +47 -3
  43. package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
  44. package/dist/builtins/local-network/local-network.addon.js +1 -1
  45. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  46. package/dist/builtins/loki-logging/index.js +1 -1
  47. package/dist/builtins/loki-logging/index.mjs +1 -1
  48. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  50. package/dist/builtins/platform-probe/index.js +1 -1
  51. package/dist/builtins/platform-probe/index.mjs +1 -1
  52. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  54. package/dist/builtins/snapshot/index.js +1 -1
  55. package/dist/builtins/snapshot/index.mjs +1 -1
  56. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  57. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  59. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  60. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  61. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  62. package/dist/builtins/system-config/system-config.addon.js +4 -4
  63. package/dist/builtins/system-config/system-config.addon.mjs +4 -4
  64. package/dist/builtins/winston-logging/index.js +1 -1
  65. package/dist/builtins/winston-logging/index.mjs +1 -1
  66. package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
  67. package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
  68. package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
  69. package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
  71. package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
  72. package/dist/index.js +739 -350
  73. package/dist/index.mjs +737 -352
  74. package/dist/kernel/bootstrap-config.d.ts +8 -0
  75. package/dist/kernel/capability-registry.d.ts +30 -0
  76. package/dist/kernel/config-manager.d.ts +75 -129
  77. package/dist/kernel/index.d.ts +5 -2
  78. package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
  79. package/dist/kernel/settings-door-view.d.ts +24 -1
  80. package/dist/kernel/settings-read-error.d.ts +37 -0
  81. package/dist/kernel/settings-view-types.d.ts +105 -0
  82. package/dist/kernel/store-settings-view.d.ts +17 -0
  83. package/dist/kernel/system-settings-mirror.d.ts +34 -0
  84. package/dist/kernel/system-settings-policy.d.ts +47 -0
  85. package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
  86. package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
  87. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
- import { An as cloneStatefulExpressionMemory, Bn as toExpressionValue, Fn as compositionSliceKey, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Ln as createStatefulExpressionMemory, Mn as composedDeviceStableId, Nn as compositionFeatureEnabled, Pn as compositionItemFieldPath, Un as DeviceType, Wn as errMsg, X as composerCapability, Xt as planComposition, Z as compositionFieldKey, c as COMPOSED_ITEM_FIXED_FIELDS, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, t as ALL_CAPABILITY_DEFINITIONS, tr as createEvent, vt as evaluateCompositionField, wn as COMPOSER_ADDON_ID } from "../../dist-BPvku43i.mjs";
1
+ import { An as cloneStatefulExpressionMemory, Bn as toExpressionValue, Fn as compositionSliceKey, Gn as BaseAddon, In as compositionSourceKey, K as buildCapabilityLookup, Ln as createStatefulExpressionMemory, Mn as composedDeviceStableId, Nn as compositionFeatureEnabled, Pn as compositionItemFieldPath, Un as DeviceType, Wn as errMsg, X as composerCapability, Xt as planComposition, Z as compositionFieldKey, c as COMPOSED_ITEM_FIXED_FIELDS, h as DeclaredDevices, hr as EventCategory, i as BaseDevice, jn as composedDeviceRef, t as ALL_CAPABILITY_DEFINITIONS, tr as createEvent, vt as evaluateCompositionField, wn as COMPOSER_ADDON_ID } from "../../dist-Dori4pf3.mjs";
2
2
  import { n as parseDeviceStateChanged, t as isRecord$2 } from "../../device-state-changed-DoBbkyEI.mjs";
3
- import { n as normalizedNativeOwner } from "../../composition-sources-fUyF4z39.mjs";
3
+ import { n as normalizedNativeOwner } from "../../composition-sources-DVClu006.mjs";
4
4
  import { z } from "zod";
5
5
  //#region src/builtins/composer/composed-device.ts
6
6
  /**
@@ -626,88 +626,6 @@ function logPlanTransition(logger, previous, p, liveTags) {
626
626
  else logger.warn(p.declaration === null ? "composition does not plan — no device is declared for it" : "composition no longer plans — its device is kept offline with no capabilities", extras);
627
627
  }
628
628
  //#endregion
629
- //#region src/builtins/composer/slice-assembler.ts
630
- /**
631
- * From the runtime's flat field values to the slice a target takes (D663).
632
- *
633
- * A top-level field is written under its path. An item leaf
634
- * (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
635
- * every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
636
- * path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
637
- * unsourced nullable leaf is `null` because the cap says so, not because this
638
- * module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
639
- * are the composer's; items are sorted by key so two writes of the same
640
- * values are one slice.
641
- *
642
- * A cap the lookup does not know as an item array cannot be assembled: that
643
- * is thrown by name, and the runtime's write path turns a throw into a named,
644
- * retried failure — never a half-built array.
645
- */
646
- function isRecord$1(value) {
647
- return typeof value === "object" && value !== null && !Array.isArray(value);
648
- }
649
- /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
650
- function setPath(record, segments, value) {
651
- const [head, ...rest] = segments;
652
- if (head === void 0) return record;
653
- if (rest.length === 0) return {
654
- ...record,
655
- [head]: value
656
- };
657
- const current = record[head];
658
- return {
659
- ...record,
660
- [head]: setPath(isRecord$1(current) ? current : {}, rest, value)
661
- };
662
- }
663
- function itemArrayOf(group, lookupCap) {
664
- const itemArray = lookupCap(group.cap)?.status?.itemArray;
665
- if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
666
- return itemArray;
667
- }
668
- function assembleItem(group, lookupCap) {
669
- const itemArray = itemArrayOf(group, lookupCap);
670
- const base = {
671
- ...itemArray.emptyItem,
672
- [itemArray.keyField]: group.key,
673
- ...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
674
- ...COMPOSED_ITEM_FIXED_FIELDS
675
- };
676
- return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
677
- }
678
- function groupItems(entries) {
679
- const groups = /* @__PURE__ */ new Map();
680
- for (const entry of entries) {
681
- const item = entry.plan.item;
682
- if (item === void 0) continue;
683
- const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
684
- const group = groups.get(id) ?? {
685
- cap: entry.plan.cap,
686
- arrayPath: item.arrayPath,
687
- key: item.key,
688
- label: item.label,
689
- leaves: []
690
- };
691
- groups.set(id, {
692
- ...group,
693
- leaves: [...group.leaves, entry]
694
- });
695
- }
696
- return [...groups.values()];
697
- }
698
- function compareKeys(a, b) {
699
- return a < b ? -1 : a > b ? 1 : 0;
700
- }
701
- function assembleSlice(entries, lookupCap) {
702
- const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
703
- const groups = groupItems(entries);
704
- const arrays = [...new Set(groups.map((g) => g.arrayPath))].map((arrayPath) => [arrayPath, groups.filter((g) => g.arrayPath === arrayPath).toSorted((a, b) => compareKeys(a.key, b.key)).map((g) => assembleItem(g, lookupCap))]);
705
- return {
706
- ...top,
707
- ...Object.fromEntries(arrays)
708
- };
709
- }
710
- //#endregion
711
629
  //#region src/builtins/composer/field-record.ts
712
630
  /**
713
631
  * One composed field's record and the pure judgements over it (D659, D663):
@@ -720,11 +638,11 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
720
638
  function fieldKey(plan) {
721
639
  return compositionFieldKey(plan);
722
640
  }
723
- function isRecord(value) {
641
+ function isRecord$1(value) {
724
642
  return typeof value === "object" && value !== null && !Array.isArray(value);
725
643
  }
726
644
  function valueAtPath$1(value, path) {
727
- return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
645
+ return path.split(".").reduce((node, segment) => isRecord$1(node) ? node[segment] : void 0, value);
728
646
  }
729
647
  function initialValue(plan, byCap, lookupCap) {
730
648
  const slice = byCap?.get(plan.cap);
@@ -735,7 +653,7 @@ function initialValue(plan, byCap, lookupCap) {
735
653
  if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
736
654
  const entries = valueAtPath$1(slice, item.arrayPath);
737
655
  if (!Array.isArray(entries)) return void 0;
738
- return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
656
+ return valueAtPath$1(entries.find((entry) => isRecord$1(entry) && entry[itemArray.keyField] === item.key), plan.path);
739
657
  }
740
658
  function createFieldRecord(plan, byCap, lookupCap) {
741
659
  const initial = initialValue(plan, byCap, lookupCap);
@@ -816,6 +734,283 @@ function pendingReason(cap, pending) {
816
734
  return pending.kind === "refused" ? `the \`${cap}\` capability refused the value: ${pending.error}` : `writing the \`${cap}\` capability threw: ${pending.error}`;
817
735
  }
818
736
  //#endregion
737
+ //#region src/builtins/composer/own-field-reads.ts
738
+ /** Derived fields before accumulators: a count reads the value its own pass just computed. */
739
+ var EVALUATION_ORDER = {
740
+ derived: 0,
741
+ accumulator: 1
742
+ };
743
+ /** `keys` in evaluation order: derived, then accumulators, then the rest; stable within a rank. */
744
+ function orderForEvaluation(keys, records) {
745
+ const rank = (k) => EVALUATION_ORDER[records.get(k)?.plan.role ?? ""] ?? 2;
746
+ return [...keys].toSorted((a, b) => rank(a) - rank(b));
747
+ }
748
+ /**
749
+ * The own field `key` as the runtime holds it right now (`records`: the pass in
750
+ * progress) — exactly what the runtime PUBLISHES for it, which is what the hub
751
+ * answered before (review H1). An unavailable field under the `null` or
752
+ * `unknown` policy is published as `null` / `'unknown'`, so a reader sees that
753
+ * value; only a field nothing is written for (`release`, `offline`) reads as
754
+ * unavailable. Answering those as unavailable left `count` unprimed while a
755
+ * source was late at boot, and it then swallowed the first press as its seed.
756
+ *
757
+ * Not evaluated yet is unavailable: nothing is published, and unknown is never
758
+ * `null` (D393). The evaluation order makes this unreachable for a derived
759
+ * reader of a derived field. Never fatal here: a gone source fails the block
760
+ * through the field that reads it directly.
761
+ */
762
+ function ownFieldReading(records, key) {
763
+ const record = records.get(key);
764
+ const outcome = record?.outcome ?? null;
765
+ if (record === void 0 || outcome === null) return {
766
+ state: "unavailable",
767
+ reason: `own field \`${key}\` not evaluated yet`,
768
+ fatal: false
769
+ };
770
+ if (outcome.state === "unavailable") {
771
+ const published = writable(record);
772
+ if (published !== void 0) return {
773
+ state: "fresh",
774
+ value: published
775
+ };
776
+ return {
777
+ state: "unavailable",
778
+ reason: `own field \`${key}\`: ${outcome.reason}`,
779
+ fatal: false,
780
+ ...outcome.confirmed === true ? { confirmed: true } : {}
781
+ };
782
+ }
783
+ if (outcome.state === "stale") return {
784
+ state: "stale",
785
+ value: outcome.value,
786
+ reason: `own field \`${key}\`: ${outcome.reason ?? "stale"}`
787
+ };
788
+ return {
789
+ state: "fresh",
790
+ value: outcome.value
791
+ };
792
+ }
793
+ //#endregion
794
+ //#region src/builtins/composer/composition-evaluation.ts
795
+ /**
796
+ * Evaluating a composition's fields (D659, D670): one pass over the keys a
797
+ * change affects, the own-field readers it must re-run (D670 amendment), and
798
+ * the state-derived events it emits. Split from `composition-runtime.ts`,
799
+ * which owns the records and hands them in through {@link EvaluationHost}.
800
+ *
801
+ * - Stateful expression memory commits only when the whole expression
802
+ * succeeds.
803
+ * - A declared state-derived event advances its high-water and is emitted
804
+ * BEFORE the corresponding target write; the committed records are visible
805
+ * to a synchronous subscriber first.
806
+ * - Derived fields are evaluated before accumulators, and every field reading
807
+ * an own field that changed is re-evaluated in the same pass.
808
+ */
809
+ function strictFiniteAdvance(previous, next) {
810
+ return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
811
+ }
812
+ /** The record key an own read is answered from; null when `read` is a source's. */
813
+ function ownKeyOf(host, read) {
814
+ if (host.isOwnRead === void 0 || !host.isOwnRead(read)) return null;
815
+ const key = `${read.cap}.${read.fieldPath}`;
816
+ return host.records().has(key) ? key : null;
817
+ }
818
+ /**
819
+ * `keys`, derived before accumulators, then — in the same pass — every
820
+ * derived/accumulator field that reads an own field that changed. Bounded by
821
+ * the record count: the validator refuses a cycle, so a chain is at most that
822
+ * long (D6).
823
+ */
824
+ function evaluateWithOwnReaders(host, keys, now) {
825
+ const changed = /* @__PURE__ */ new Set();
826
+ const valueCaps = /* @__PURE__ */ new Set();
827
+ let round = orderForEvaluation(keys, host.records());
828
+ for (let hop = 0; round.length > 0 && hop <= host.records().size; hop++) {
829
+ const delta = evaluateFields(host, round, now);
830
+ for (const k of delta.keys) changed.add(k);
831
+ for (const c of delta.valueCaps) valueCaps.add(c);
832
+ round = orderForEvaluation([...host.records().values()].filter((r) => (r.plan.role === "derived" || r.plan.role === "accumulator") && r.plan.reads.some((read) => {
833
+ const own = ownKeyOf(host, read);
834
+ return own !== null && delta.keys.has(own);
835
+ })).map((r) => fieldKey(r.plan)), host.records());
836
+ }
837
+ return {
838
+ keys: changed,
839
+ valueCaps
840
+ };
841
+ }
842
+ /** Evaluate `keys` in order against the records in progress; commit whatever was evaluated, even on a throw. */
843
+ function evaluateFields(host, keys, now) {
844
+ const changed = /* @__PURE__ */ new Set();
845
+ const valueCaps = /* @__PURE__ */ new Set();
846
+ const next = new Map(host.records());
847
+ const read = (r) => {
848
+ const own = ownKeyOf(host, r);
849
+ return own === null ? host.read(r) : ownFieldReading(next, own);
850
+ };
851
+ try {
852
+ for (const key of keys) {
853
+ const record = next.get(key);
854
+ if (record === void 0) continue;
855
+ const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
856
+ const outcome = evaluateCompositionField({
857
+ source: record.plan.source,
858
+ read,
859
+ now,
860
+ stateful: {
861
+ currentOutput: currentOutput(record),
862
+ expressionMemory: candidateMemory,
863
+ startedAtMs: host.startedAtMs,
864
+ callPathPrefix: fieldKey(record.plan)
865
+ }
866
+ });
867
+ const expressionSucceeded = outcome.state !== "unavailable";
868
+ let evaluated = {
869
+ ...record,
870
+ outcome,
871
+ ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
872
+ };
873
+ const eventPlans = host.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
874
+ if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
875
+ const timestamp = outcome.value;
876
+ evaluated = {
877
+ ...evaluated,
878
+ eventHighWater: timestamp
879
+ };
880
+ next.set(key, evaluated);
881
+ host.commit(next);
882
+ for (const eventPlan of eventPlans) host.emit(eventPlan, timestamp);
883
+ } else next.set(key, evaluated);
884
+ if (sameOutcome(record.outcome, outcome)) continue;
885
+ if (record.outcome?.state !== outcome.state) host.logTransition(record, outcome);
886
+ if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
887
+ changed.add(key);
888
+ }
889
+ } finally {
890
+ host.commit(next);
891
+ }
892
+ return {
893
+ keys: changed,
894
+ valueCaps
895
+ };
896
+ }
897
+ /** Emit one state-derived event; a delivery failure is logged, never thrown (D670). */
898
+ function emitStateDerivedEvent(ctx, plan, timestamp) {
899
+ const extras = {
900
+ tags: { deviceId: ctx.deviceId },
901
+ meta: {
902
+ blockId: ctx.blockId,
903
+ cap: plan.cap,
904
+ eventName: plan.eventName,
905
+ category: plan.category,
906
+ timestamp
907
+ }
908
+ };
909
+ try {
910
+ if (plan.category !== EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
911
+ const event = createEvent(EventCategory.DoorbellOnPressed, {
912
+ type: "device",
913
+ id: ctx.deviceId,
914
+ deviceId: ctx.deviceId
915
+ }, {
916
+ deviceId: ctx.deviceId,
917
+ timestamp
918
+ });
919
+ ctx.events.emit(event);
920
+ ctx.logger.debug("composition: state-derived event emitted", extras);
921
+ } catch (error) {
922
+ ctx.logger.warn("composition: state-derived event delivery failed", {
923
+ ...extras,
924
+ meta: {
925
+ ...extras.meta,
926
+ error: errorText(error)
927
+ }
928
+ });
929
+ }
930
+ }
931
+ //#endregion
932
+ //#region src/builtins/composer/slice-assembler.ts
933
+ /**
934
+ * From the runtime's flat field values to the slice a target takes (D663).
935
+ *
936
+ * A top-level field is written under its path. An item leaf
937
+ * (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
938
+ * every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
939
+ * path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
940
+ * unsourced nullable leaf is `null` because the cap says so, not because this
941
+ * module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
942
+ * are the composer's; items are sorted by key so two writes of the same
943
+ * values are one slice.
944
+ *
945
+ * A cap the lookup does not know as an item array cannot be assembled: that
946
+ * is thrown by name, and the runtime's write path turns a throw into a named,
947
+ * retried failure — never a half-built array.
948
+ */
949
+ function isRecord(value) {
950
+ return typeof value === "object" && value !== null && !Array.isArray(value);
951
+ }
952
+ /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
953
+ function setPath(record, segments, value) {
954
+ const [head, ...rest] = segments;
955
+ if (head === void 0) return record;
956
+ if (rest.length === 0) return {
957
+ ...record,
958
+ [head]: value
959
+ };
960
+ const current = record[head];
961
+ return {
962
+ ...record,
963
+ [head]: setPath(isRecord(current) ? current : {}, rest, value)
964
+ };
965
+ }
966
+ function itemArrayOf(group, lookupCap) {
967
+ const itemArray = lookupCap(group.cap)?.status?.itemArray;
968
+ if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
969
+ return itemArray;
970
+ }
971
+ function assembleItem(group, lookupCap) {
972
+ const itemArray = itemArrayOf(group, lookupCap);
973
+ const base = {
974
+ ...itemArray.emptyItem,
975
+ [itemArray.keyField]: group.key,
976
+ ...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
977
+ ...COMPOSED_ITEM_FIXED_FIELDS
978
+ };
979
+ return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
980
+ }
981
+ function groupItems(entries) {
982
+ const groups = /* @__PURE__ */ new Map();
983
+ for (const entry of entries) {
984
+ const item = entry.plan.item;
985
+ if (item === void 0) continue;
986
+ const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
987
+ const group = groups.get(id) ?? {
988
+ cap: entry.plan.cap,
989
+ arrayPath: item.arrayPath,
990
+ key: item.key,
991
+ label: item.label,
992
+ leaves: []
993
+ };
994
+ groups.set(id, {
995
+ ...group,
996
+ leaves: [...group.leaves, entry]
997
+ });
998
+ }
999
+ return [...groups.values()];
1000
+ }
1001
+ function compareKeys(a, b) {
1002
+ return a < b ? -1 : a > b ? 1 : 0;
1003
+ }
1004
+ function assembleSlice(entries, lookupCap) {
1005
+ const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
1006
+ const groups = groupItems(entries);
1007
+ const arrays = [...new Set(groups.map((g) => g.arrayPath))].map((arrayPath) => [arrayPath, groups.filter((g) => g.arrayPath === arrayPath).toSorted((a, b) => compareKeys(a.key, b.key)).map((g) => assembleItem(g, lookupCap))]);
1008
+ return {
1009
+ ...top,
1010
+ ...Object.fromEntries(arrays)
1011
+ };
1012
+ }
1013
+ //#endregion
819
1014
  //#region src/builtins/composer/composition-runtime.ts
820
1015
  /**
821
1016
  * One running composition: per-field state, writes to the composed device, and
@@ -864,10 +1059,11 @@ function pendingReason(cap, pending) {
864
1059
  * not land through `onAsyncFailure`: the cap is marked write-pending by name
865
1060
  * and re-written whole on the next pass — no retry timer (D3).
866
1061
  *
867
- * Slice 2b (D670): stateful expression memory commits only when the whole
868
- * expression succeeds. A declared state-derived event advances its seeded
869
- * high-water and is emitted before the corresponding target write; neither an
870
- * event-bus failure nor a refused write can replay the event.
1062
+ * Slice 2b (D670): evaluation — stateful memory, state-derived events before
1063
+ * their write, and reads of the composition's OWN fields answered from the
1064
+ * records rather than the hub (D670 amendment) — is `composition-evaluation.ts`.
1065
+ * Neither an event-bus failure nor a refused write can replay an event, and the
1066
+ * hub's echo of the runtime's own write re-runs nothing.
871
1067
  */
872
1068
  /**
873
1069
  * A refusal nothing on this side can retry away: another owner holds the
@@ -880,9 +1076,6 @@ var TERMINAL_REFUSALS = new Set([
880
1076
  "invalid-slice",
881
1077
  "unknown-device"
882
1078
  ]);
883
- function strictFiniteAdvance(previous, next) {
884
- return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
885
- }
886
1079
  var CompositionRuntime = class {
887
1080
  deps;
888
1081
  records;
@@ -906,11 +1099,31 @@ var CompositionRuntime = class {
906
1099
  releaseFlaps = /* @__PURE__ */ new Map();
907
1100
  /** Caps the target refused for good (`TERMINAL_REFUSALS`): never written again by this runtime. */
908
1101
  terminal = /* @__PURE__ */ new Map();
1102
+ /** What `composition-evaluation.ts` reads and commits through. */
1103
+ evaluation;
909
1104
  constructor(deps) {
910
1105
  this.deps = deps;
911
1106
  this.startedAtMs = deps.now();
912
1107
  this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
913
1108
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1109
+ const eventContext = {
1110
+ deviceId: deps.target.id,
1111
+ blockId: deps.blockId,
1112
+ events: deps.events,
1113
+ logger: deps.logger
1114
+ };
1115
+ this.evaluation = {
1116
+ records: () => this.records,
1117
+ commit: (records) => {
1118
+ this.records = records;
1119
+ },
1120
+ read: deps.read,
1121
+ ...deps.isOwnRead === void 0 ? {} : { isOwnRead: deps.isOwnRead },
1122
+ eventPlans: deps.eventPlans,
1123
+ startedAtMs: this.startedAtMs,
1124
+ emit: (plan, timestamp) => emitStateDerivedEvent(eventContext, plan, timestamp),
1125
+ logTransition: (record, outcome) => this.logTransition(record, outcome)
1126
+ };
914
1127
  }
915
1128
  start() {
916
1129
  if (this.disabledReason !== null) return;
@@ -919,7 +1132,7 @@ var CompositionRuntime = class {
919
1132
  onSourcesChanged(sliceKeys) {
920
1133
  if (this.disabledReason !== null) return;
921
1134
  this.guarded("source", true, () => {
922
- const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => sliceKeys.has(compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
1135
+ const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => ownKeyOf(this.evaluation, read) === null && sliceKeys.has(compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
923
1136
  if (affected.length > 0) this.pass(affected, "source");
924
1137
  });
925
1138
  }
@@ -1068,12 +1281,12 @@ var CompositionRuntime = class {
1068
1281
  pass(keys, trigger) {
1069
1282
  const initial = trigger === "start";
1070
1283
  const now = this.deps.now();
1071
- const calculated = this.evaluate(keys.filter((k) => {
1284
+ const calculated = evaluateWithOwnReaders(this.evaluation, keys.filter((k) => {
1072
1285
  const role = this.records.get(k)?.plan.role;
1073
1286
  return role === "derived" || role === "accumulator";
1074
1287
  }), now);
1075
1288
  const followers = [...this.records.values()].filter((r) => r.plan.role === "clock" ? initial || calculated.valueCaps.has(r.plan.cap) : r.plan.role === "constant" && initial).map((r) => fieldKey(r.plan));
1076
- const extra = this.evaluate(followers, now);
1289
+ const extra = evaluateFields(this.evaluation, followers, now);
1077
1290
  const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
1078
1291
  const cap = this.records.get(k)?.plan.cap;
1079
1292
  return cap === void 0 ? [] : [cap];
@@ -1083,89 +1296,6 @@ var CompositionRuntime = class {
1083
1296
  this.updateFailure();
1084
1297
  this.updateAvailability();
1085
1298
  }
1086
- evaluate(keys, now) {
1087
- const changed = /* @__PURE__ */ new Set();
1088
- const valueCaps = /* @__PURE__ */ new Set();
1089
- const next = new Map(this.records);
1090
- try {
1091
- for (const key of keys) {
1092
- const record = next.get(key);
1093
- if (record === void 0) continue;
1094
- const candidateMemory = cloneStatefulExpressionMemory(record.expressionMemory);
1095
- const outcome = evaluateCompositionField({
1096
- source: record.plan.source,
1097
- read: this.deps.read,
1098
- now,
1099
- stateful: {
1100
- currentOutput: currentOutput(record),
1101
- expressionMemory: candidateMemory,
1102
- startedAtMs: this.startedAtMs,
1103
- callPathPrefix: fieldKey(record.plan)
1104
- }
1105
- });
1106
- const expressionSucceeded = outcome.state !== "unavailable";
1107
- let evaluated = {
1108
- ...record,
1109
- outcome,
1110
- ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
1111
- };
1112
- const eventPlans = this.deps.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
1113
- if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
1114
- const timestamp = outcome.value;
1115
- evaluated = {
1116
- ...evaluated,
1117
- eventHighWater: timestamp
1118
- };
1119
- next.set(key, evaluated);
1120
- this.records = next;
1121
- for (const eventPlan of eventPlans) this.emitStateEvent(eventPlan, timestamp);
1122
- } else next.set(key, evaluated);
1123
- if (sameOutcome(record.outcome, outcome)) continue;
1124
- if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
1125
- if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
1126
- changed.add(key);
1127
- }
1128
- } finally {
1129
- this.records = next;
1130
- }
1131
- return {
1132
- keys: changed,
1133
- valueCaps
1134
- };
1135
- }
1136
- emitStateEvent(plan, timestamp) {
1137
- const extras = {
1138
- tags: { deviceId: this.deps.target.id },
1139
- meta: {
1140
- blockId: this.deps.blockId,
1141
- cap: plan.cap,
1142
- eventName: plan.eventName,
1143
- category: plan.category,
1144
- timestamp
1145
- }
1146
- };
1147
- try {
1148
- if (plan.category !== EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
1149
- const event = createEvent(EventCategory.DoorbellOnPressed, {
1150
- type: "device",
1151
- id: this.deps.target.id,
1152
- deviceId: this.deps.target.id
1153
- }, {
1154
- deviceId: this.deps.target.id,
1155
- timestamp
1156
- });
1157
- this.deps.events.emit(event);
1158
- this.deps.logger.debug("composition: state-derived event emitted", extras);
1159
- } catch (error) {
1160
- this.deps.logger.warn("composition: state-derived event delivery failed", {
1161
- ...extras,
1162
- meta: {
1163
- ...extras.meta,
1164
- error: errorText(error)
1165
- }
1166
- });
1167
- }
1168
- }
1169
1299
  recordsOf(cap) {
1170
1300
  return [...this.records.values()].filter((r) => r.plan.cap === cap);
1171
1301
  }
@@ -1529,6 +1659,29 @@ var ConfirmedSeed = class {
1529
1659
  * (`HeldClaims`, C-1).
1530
1660
  */
1531
1661
  /**
1662
+ * Review M3: an ADDED `restored` cap seeded from NOTHING says so, once per
1663
+ * runtime build, on its device. After a restart its state comes from the
1664
+ * target's runtime-state row; a row that lost the slice (D677: 615's doorbell,
1665
+ * dropped at a hub boot) used to surface as "not seeded yet" and now seeds a
1666
+ * clean `{ null, 0 }` — a lifetime count silently reset unless this line
1667
+ * names it. A first composition of a device that never had the slice logs it
1668
+ * too: the line cannot tell the two apart, and says both.
1669
+ */
1670
+ function reportSeedlessRestoredCaps(input) {
1671
+ for (const cap of addedCapsOf(input.features)) {
1672
+ if (input.lookupCap(cap)?.durability !== "restored") continue;
1673
+ if ((input.seed.byCap.get(cap) ?? null) !== null) continue;
1674
+ input.logger.info("composed restored capability seeded from nothing — the target had no slice for it (a first composition, or a lost runtime-state row)", {
1675
+ tags: { deviceId: input.deviceId },
1676
+ meta: {
1677
+ blockId: input.blockId,
1678
+ cap,
1679
+ seededFrom: "none"
1680
+ }
1681
+ });
1682
+ }
1683
+ }
1684
+ /**
1532
1685
  * Read every affected effective capability exactly once, before the composer
1533
1686
  * grafts or claims any of them. A null slice is a successful empty seed; a
1534
1687
  * rejection is deliberately left to the caller so ownership cannot begin.
@@ -2492,6 +2645,14 @@ var Composer = class {
2492
2645
  let targetSeed;
2493
2646
  if (p.target.kind === "existing" && p.target.deviceId !== null && p.target.waiting === null && p.target.gone === null && p.block.enabled && p.fields.length > 0) try {
2494
2647
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2648
+ reportSeedlessRestoredCaps({
2649
+ logger: this.deps.logger,
2650
+ blockId: p.block.id,
2651
+ deviceId: p.target.deviceId,
2652
+ features: p.target.features,
2653
+ seed: targetSeed,
2654
+ lookupCap: this.deps.lookupCap
2655
+ });
2495
2656
  } catch (err) {
2496
2657
  const reason = `the target device state could not be read before ownership (${errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
2497
2658
  this.deps.logger.warn(previous?.runtime === null || previous === void 0 ? "customization target state could not be read — ownership is held" : "customization edit target state could not be read — the previous runtime stays installed", {
@@ -2604,6 +2765,7 @@ var Composer = class {
2604
2765
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2605
2766
  const targetKey = p.target.kind === "existing" ? compositionSourceKey(p.target.ref) : null;
2606
2767
  const nativeCaps = new Set(self?.native ?? []);
2768
+ const ownKey = targetKey ?? compositionSourceKey(composedDeviceRef(p.block.id));
2607
2769
  return new CompositionRuntime({
2608
2770
  blockId: p.block.id,
2609
2771
  fields: p.fields,
@@ -2613,6 +2775,7 @@ var Composer = class {
2613
2775
  lookupCap: this.deps.lookupCap,
2614
2776
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2615
2777
  read: (read) => this.deps.tracker.read(read, compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2778
+ isOwnRead: (read) => compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2616
2779
  now: this.deps.now,
2617
2780
  timer: this.deps.newTimer(),
2618
2781
  logger: this.deps.logger.child("runtime")
@@ -2838,7 +3001,10 @@ var NodeDeadlineTimer = class {
2838
3001
  * this runner must never answer `{}`.
2839
3002
  * - The context is `createContext(stableId, id)` on a FOREIGN device id: the
2840
3003
  * second use of the forked-runner native path D659 lists as unverified live
2841
- * (D659 live item 1). Task 16 step 4.1 is its live proof.
3004
+ * (D659 live item 1). Task 16 step 4.1 is its live proof. Its EMPTY runtime
3005
+ * state is correct and deliberate (D677): no device is built from it and no
3006
+ * slice is written through it — the graft only registers method-serving
3007
+ * natives, and `getStatus` reads the hub's merged slice.
2842
3008
  * - `ungraft` removes only what THIS host registered, through the context's
2843
3009
  * optional `unregisterNativeCap`. An older kernel without it leaves the graft
2844
3010
  * routable until the composer respawns — warned once per device, never
@@ -0,0 +1,37 @@
1
+ import { CompositionEventPlan, CompositionFieldRead, FieldOutcome, IScopedLogger, SourceReader } from '@camstack/types';
2
+ import { CompositionEventEmitter } from './composition-target.js';
3
+ import { FieldRecord } from './field-record.js';
4
+ export interface EvaluationDelta {
5
+ readonly keys: ReadonlySet<string>;
6
+ readonly valueCaps: ReadonlySet<string>;
7
+ }
8
+ /** What an evaluation reads from, and writes back to, the runtime that owns the records. */
9
+ export interface EvaluationHost {
10
+ records(): ReadonlyMap<string, FieldRecord>;
11
+ commit(records: ReadonlyMap<string, FieldRecord>): void;
12
+ readonly read: SourceReader;
13
+ readonly isOwnRead?: (read: CompositionFieldRead) => boolean;
14
+ readonly eventPlans: readonly CompositionEventPlan[];
15
+ readonly startedAtMs: number;
16
+ emit(plan: CompositionEventPlan, timestamp: number): void;
17
+ logTransition(record: FieldRecord, outcome: FieldOutcome): void;
18
+ }
19
+ /** The record key an own read is answered from; null when `read` is a source's. */
20
+ export declare function ownKeyOf(host: EvaluationHost, read: CompositionFieldRead): string | null;
21
+ /**
22
+ * `keys`, derived before accumulators, then — in the same pass — every
23
+ * derived/accumulator field that reads an own field that changed. Bounded by
24
+ * the record count: the validator refuses a cycle, so a chain is at most that
25
+ * long (D6).
26
+ */
27
+ export declare function evaluateWithOwnReaders(host: EvaluationHost, keys: readonly string[], now: number): EvaluationDelta;
28
+ /** Evaluate `keys` in order against the records in progress; commit whatever was evaluated, even on a throw. */
29
+ export declare function evaluateFields(host: EvaluationHost, keys: readonly string[], now: number): EvaluationDelta;
30
+ export interface StateEventContext {
31
+ readonly deviceId: number;
32
+ readonly blockId: string;
33
+ readonly events: CompositionEventEmitter;
34
+ readonly logger: IScopedLogger;
35
+ }
36
+ /** Emit one state-derived event; a delivery failure is logged, never thrown (D670). */
37
+ export declare function emitStateDerivedEvent(ctx: StateEventContext, plan: CompositionEventPlan, timestamp: number): void;