@camstack/system 1.2.316 → 1.2.318

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-BwE1POnF.js → composition-sources-BjRPhWtU.js} +1 -1
  69. package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-Cg2MP-qf.mjs} +1 -1
  70. package/dist/{dist-BPvku43i.mjs → dist-CQ9vXSUh.mjs} +220 -8
  71. package/dist/{dist-RAl10EfG.js → dist-Dcy0gVHu.js} +220 -8
  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-DqVWSrVw.mjs → retired-settings-keys-DNIbyfTR.mjs} +1 -1
  86. package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-pzlrLUbA.js} +1 -1
  87. package/package.json +1 -1
@@ -3,9 +3,9 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  require("../../chunk-Cek0wNdY.js");
6
- const require_dist = require("../../dist-RAl10EfG.js");
6
+ const require_dist = require("../../dist-Dcy0gVHu.js");
7
7
  const require_device_state_changed = require("../../device-state-changed-Bk7cvTnz.js");
8
- const require_composition_sources = require("../../composition-sources-BwE1POnF.js");
8
+ const require_composition_sources = require("../../composition-sources-BjRPhWtU.js");
9
9
  //#region src/builtins/composer/composed-device.ts
10
10
  /**
11
11
  * A composed device: an ordinary `BaseDevice` whose caps are served by the
@@ -630,88 +630,6 @@ function logPlanTransition(logger, previous, p, liveTags) {
630
630
  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);
631
631
  }
632
632
  //#endregion
633
- //#region src/builtins/composer/slice-assembler.ts
634
- /**
635
- * From the runtime's flat field values to the slice a target takes (D663).
636
- *
637
- * A top-level field is written under its path. An item leaf
638
- * (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
639
- * every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
640
- * path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
641
- * unsourced nullable leaf is `null` because the cap says so, not because this
642
- * module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
643
- * are the composer's; items are sorted by key so two writes of the same
644
- * values are one slice.
645
- *
646
- * A cap the lookup does not know as an item array cannot be assembled: that
647
- * is thrown by name, and the runtime's write path turns a throw into a named,
648
- * retried failure — never a half-built array.
649
- */
650
- function isRecord$1(value) {
651
- return typeof value === "object" && value !== null && !Array.isArray(value);
652
- }
653
- /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
654
- function setPath(record, segments, value) {
655
- const [head, ...rest] = segments;
656
- if (head === void 0) return record;
657
- if (rest.length === 0) return {
658
- ...record,
659
- [head]: value
660
- };
661
- const current = record[head];
662
- return {
663
- ...record,
664
- [head]: setPath(isRecord$1(current) ? current : {}, rest, value)
665
- };
666
- }
667
- function itemArrayOf(group, lookupCap) {
668
- const itemArray = lookupCap(group.cap)?.status?.itemArray;
669
- if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${require_dist.compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
670
- return itemArray;
671
- }
672
- function assembleItem(group, lookupCap) {
673
- const itemArray = itemArrayOf(group, lookupCap);
674
- const base = {
675
- ...itemArray.emptyItem,
676
- [itemArray.keyField]: group.key,
677
- ...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
678
- ...require_dist.COMPOSED_ITEM_FIXED_FIELDS
679
- };
680
- return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
681
- }
682
- function groupItems(entries) {
683
- const groups = /* @__PURE__ */ new Map();
684
- for (const entry of entries) {
685
- const item = entry.plan.item;
686
- if (item === void 0) continue;
687
- const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
688
- const group = groups.get(id) ?? {
689
- cap: entry.plan.cap,
690
- arrayPath: item.arrayPath,
691
- key: item.key,
692
- label: item.label,
693
- leaves: []
694
- };
695
- groups.set(id, {
696
- ...group,
697
- leaves: [...group.leaves, entry]
698
- });
699
- }
700
- return [...groups.values()];
701
- }
702
- function compareKeys(a, b) {
703
- return a < b ? -1 : a > b ? 1 : 0;
704
- }
705
- function assembleSlice(entries, lookupCap) {
706
- const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
707
- const groups = groupItems(entries);
708
- 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))]);
709
- return {
710
- ...top,
711
- ...Object.fromEntries(arrays)
712
- };
713
- }
714
- //#endregion
715
633
  //#region src/builtins/composer/field-record.ts
716
634
  /**
717
635
  * One composed field's record and the pure judgements over it (D659, D663):
@@ -724,11 +642,11 @@ var NO_FIELDS = /* @__PURE__ */ new Set();
724
642
  function fieldKey(plan) {
725
643
  return require_dist.compositionFieldKey(plan);
726
644
  }
727
- function isRecord(value) {
645
+ function isRecord$1(value) {
728
646
  return typeof value === "object" && value !== null && !Array.isArray(value);
729
647
  }
730
648
  function valueAtPath$1(value, path) {
731
- return path.split(".").reduce((node, segment) => isRecord(node) ? node[segment] : void 0, value);
649
+ return path.split(".").reduce((node, segment) => isRecord$1(node) ? node[segment] : void 0, value);
732
650
  }
733
651
  function initialValue(plan, byCap, lookupCap) {
734
652
  const slice = byCap?.get(plan.cap);
@@ -739,7 +657,7 @@ function initialValue(plan, byCap, lookupCap) {
739
657
  if (itemArray === void 0 || itemArray.path !== item.arrayPath) return void 0;
740
658
  const entries = valueAtPath$1(slice, item.arrayPath);
741
659
  if (!Array.isArray(entries)) return void 0;
742
- return valueAtPath$1(entries.find((entry) => isRecord(entry) && entry[itemArray.keyField] === item.key), plan.path);
660
+ return valueAtPath$1(entries.find((entry) => isRecord$1(entry) && entry[itemArray.keyField] === item.key), plan.path);
743
661
  }
744
662
  function createFieldRecord(plan, byCap, lookupCap) {
745
663
  const initial = initialValue(plan, byCap, lookupCap);
@@ -820,6 +738,283 @@ function pendingReason(cap, pending) {
820
738
  return pending.kind === "refused" ? `the \`${cap}\` capability refused the value: ${pending.error}` : `writing the \`${cap}\` capability threw: ${pending.error}`;
821
739
  }
822
740
  //#endregion
741
+ //#region src/builtins/composer/own-field-reads.ts
742
+ /** Derived fields before accumulators: a count reads the value its own pass just computed. */
743
+ var EVALUATION_ORDER = {
744
+ derived: 0,
745
+ accumulator: 1
746
+ };
747
+ /** `keys` in evaluation order: derived, then accumulators, then the rest; stable within a rank. */
748
+ function orderForEvaluation(keys, records) {
749
+ const rank = (k) => EVALUATION_ORDER[records.get(k)?.plan.role ?? ""] ?? 2;
750
+ return [...keys].toSorted((a, b) => rank(a) - rank(b));
751
+ }
752
+ /**
753
+ * The own field `key` as the runtime holds it right now (`records`: the pass in
754
+ * progress) — exactly what the runtime PUBLISHES for it, which is what the hub
755
+ * answered before (review H1). An unavailable field under the `null` or
756
+ * `unknown` policy is published as `null` / `'unknown'`, so a reader sees that
757
+ * value; only a field nothing is written for (`release`, `offline`) reads as
758
+ * unavailable. Answering those as unavailable left `count` unprimed while a
759
+ * source was late at boot, and it then swallowed the first press as its seed.
760
+ *
761
+ * Not evaluated yet is unavailable: nothing is published, and unknown is never
762
+ * `null` (D393). The evaluation order makes this unreachable for a derived
763
+ * reader of a derived field. Never fatal here: a gone source fails the block
764
+ * through the field that reads it directly.
765
+ */
766
+ function ownFieldReading(records, key) {
767
+ const record = records.get(key);
768
+ const outcome = record?.outcome ?? null;
769
+ if (record === void 0 || outcome === null) return {
770
+ state: "unavailable",
771
+ reason: `own field \`${key}\` not evaluated yet`,
772
+ fatal: false
773
+ };
774
+ if (outcome.state === "unavailable") {
775
+ const published = writable(record);
776
+ if (published !== void 0) return {
777
+ state: "fresh",
778
+ value: published
779
+ };
780
+ return {
781
+ state: "unavailable",
782
+ reason: `own field \`${key}\`: ${outcome.reason}`,
783
+ fatal: false,
784
+ ...outcome.confirmed === true ? { confirmed: true } : {}
785
+ };
786
+ }
787
+ if (outcome.state === "stale") return {
788
+ state: "stale",
789
+ value: outcome.value,
790
+ reason: `own field \`${key}\`: ${outcome.reason ?? "stale"}`
791
+ };
792
+ return {
793
+ state: "fresh",
794
+ value: outcome.value
795
+ };
796
+ }
797
+ //#endregion
798
+ //#region src/builtins/composer/composition-evaluation.ts
799
+ /**
800
+ * Evaluating a composition's fields (D659, D670): one pass over the keys a
801
+ * change affects, the own-field readers it must re-run (D670 amendment), and
802
+ * the state-derived events it emits. Split from `composition-runtime.ts`,
803
+ * which owns the records and hands them in through {@link EvaluationHost}.
804
+ *
805
+ * - Stateful expression memory commits only when the whole expression
806
+ * succeeds.
807
+ * - A declared state-derived event advances its high-water and is emitted
808
+ * BEFORE the corresponding target write; the committed records are visible
809
+ * to a synchronous subscriber first.
810
+ * - Derived fields are evaluated before accumulators, and every field reading
811
+ * an own field that changed is re-evaluated in the same pass.
812
+ */
813
+ function strictFiniteAdvance(previous, next) {
814
+ return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
815
+ }
816
+ /** The record key an own read is answered from; null when `read` is a source's. */
817
+ function ownKeyOf(host, read) {
818
+ if (host.isOwnRead === void 0 || !host.isOwnRead(read)) return null;
819
+ const key = `${read.cap}.${read.fieldPath}`;
820
+ return host.records().has(key) ? key : null;
821
+ }
822
+ /**
823
+ * `keys`, derived before accumulators, then — in the same pass — every
824
+ * derived/accumulator field that reads an own field that changed. Bounded by
825
+ * the record count: the validator refuses a cycle, so a chain is at most that
826
+ * long (D6).
827
+ */
828
+ function evaluateWithOwnReaders(host, keys, now) {
829
+ const changed = /* @__PURE__ */ new Set();
830
+ const valueCaps = /* @__PURE__ */ new Set();
831
+ let round = orderForEvaluation(keys, host.records());
832
+ for (let hop = 0; round.length > 0 && hop <= host.records().size; hop++) {
833
+ const delta = evaluateFields(host, round, now);
834
+ for (const k of delta.keys) changed.add(k);
835
+ for (const c of delta.valueCaps) valueCaps.add(c);
836
+ round = orderForEvaluation([...host.records().values()].filter((r) => (r.plan.role === "derived" || r.plan.role === "accumulator") && r.plan.reads.some((read) => {
837
+ const own = ownKeyOf(host, read);
838
+ return own !== null && delta.keys.has(own);
839
+ })).map((r) => fieldKey(r.plan)), host.records());
840
+ }
841
+ return {
842
+ keys: changed,
843
+ valueCaps
844
+ };
845
+ }
846
+ /** Evaluate `keys` in order against the records in progress; commit whatever was evaluated, even on a throw. */
847
+ function evaluateFields(host, keys, now) {
848
+ const changed = /* @__PURE__ */ new Set();
849
+ const valueCaps = /* @__PURE__ */ new Set();
850
+ const next = new Map(host.records());
851
+ const read = (r) => {
852
+ const own = ownKeyOf(host, r);
853
+ return own === null ? host.read(r) : ownFieldReading(next, own);
854
+ };
855
+ try {
856
+ for (const key of keys) {
857
+ const record = next.get(key);
858
+ if (record === void 0) continue;
859
+ const candidateMemory = require_dist.cloneStatefulExpressionMemory(record.expressionMemory);
860
+ const outcome = require_dist.evaluateCompositionField({
861
+ source: record.plan.source,
862
+ read,
863
+ now,
864
+ stateful: {
865
+ currentOutput: currentOutput(record),
866
+ expressionMemory: candidateMemory,
867
+ startedAtMs: host.startedAtMs,
868
+ callPathPrefix: fieldKey(record.plan)
869
+ }
870
+ });
871
+ const expressionSucceeded = outcome.state !== "unavailable";
872
+ let evaluated = {
873
+ ...record,
874
+ outcome,
875
+ ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
876
+ };
877
+ const eventPlans = host.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
878
+ if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
879
+ const timestamp = outcome.value;
880
+ evaluated = {
881
+ ...evaluated,
882
+ eventHighWater: timestamp
883
+ };
884
+ next.set(key, evaluated);
885
+ host.commit(next);
886
+ for (const eventPlan of eventPlans) host.emit(eventPlan, timestamp);
887
+ } else next.set(key, evaluated);
888
+ if (sameOutcome(record.outcome, outcome)) continue;
889
+ if (record.outcome?.state !== outcome.state) host.logTransition(record, outcome);
890
+ if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
891
+ changed.add(key);
892
+ }
893
+ } finally {
894
+ host.commit(next);
895
+ }
896
+ return {
897
+ keys: changed,
898
+ valueCaps
899
+ };
900
+ }
901
+ /** Emit one state-derived event; a delivery failure is logged, never thrown (D670). */
902
+ function emitStateDerivedEvent(ctx, plan, timestamp) {
903
+ const extras = {
904
+ tags: { deviceId: ctx.deviceId },
905
+ meta: {
906
+ blockId: ctx.blockId,
907
+ cap: plan.cap,
908
+ eventName: plan.eventName,
909
+ category: plan.category,
910
+ timestamp
911
+ }
912
+ };
913
+ try {
914
+ if (plan.category !== require_dist.EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
915
+ const event = require_dist.createEvent(require_dist.EventCategory.DoorbellOnPressed, {
916
+ type: "device",
917
+ id: ctx.deviceId,
918
+ deviceId: ctx.deviceId
919
+ }, {
920
+ deviceId: ctx.deviceId,
921
+ timestamp
922
+ });
923
+ ctx.events.emit(event);
924
+ ctx.logger.debug("composition: state-derived event emitted", extras);
925
+ } catch (error) {
926
+ ctx.logger.warn("composition: state-derived event delivery failed", {
927
+ ...extras,
928
+ meta: {
929
+ ...extras.meta,
930
+ error: errorText(error)
931
+ }
932
+ });
933
+ }
934
+ }
935
+ //#endregion
936
+ //#region src/builtins/composer/slice-assembler.ts
937
+ /**
938
+ * From the runtime's flat field values to the slice a target takes (D663).
939
+ *
940
+ * A top-level field is written under its path. An item leaf
941
+ * (`consumables.items[desiccant].remaining.value`) is one entry of an ARRAY:
942
+ * every leaf of one item is grouped by `(cap, arrayPath, key)` and set, by
943
+ * path segment, onto the cap's OWN `status.itemArray.emptyItem` — so an
944
+ * unsourced nullable leaf is `null` because the cap says so, not because this
945
+ * module guessed. The key, the label and {@link COMPOSED_ITEM_FIXED_FIELDS}
946
+ * are the composer's; items are sorted by key so two writes of the same
947
+ * values are one slice.
948
+ *
949
+ * A cap the lookup does not know as an item array cannot be assembled: that
950
+ * is thrown by name, and the runtime's write path turns a throw into a named,
951
+ * retried failure — never a half-built array.
952
+ */
953
+ function isRecord(value) {
954
+ return typeof value === "object" && value !== null && !Array.isArray(value);
955
+ }
956
+ /** `setPath(item, ['remaining', 'value'], 0)` → `{ ...item, remaining: { ...item.remaining, value: 0 } }`. */
957
+ function setPath(record, segments, value) {
958
+ const [head, ...rest] = segments;
959
+ if (head === void 0) return record;
960
+ if (rest.length === 0) return {
961
+ ...record,
962
+ [head]: value
963
+ };
964
+ const current = record[head];
965
+ return {
966
+ ...record,
967
+ [head]: setPath(isRecord(current) ? current : {}, rest, value)
968
+ };
969
+ }
970
+ function itemArrayOf(group, lookupCap) {
971
+ const itemArray = lookupCap(group.cap)?.status?.itemArray;
972
+ if (itemArray === void 0 || itemArray.path !== group.arrayPath) throw new Error(`\`${group.cap}\` has no item array at \`${group.arrayPath}\` — \`${require_dist.compositionItemFieldPath(group.arrayPath, group.key, "*")}\` cannot be assembled`);
973
+ return itemArray;
974
+ }
975
+ function assembleItem(group, lookupCap) {
976
+ const itemArray = itemArrayOf(group, lookupCap);
977
+ const base = {
978
+ ...itemArray.emptyItem,
979
+ [itemArray.keyField]: group.key,
980
+ ...itemArray.labelField === void 0 ? {} : { [itemArray.labelField]: group.label },
981
+ ...require_dist.COMPOSED_ITEM_FIXED_FIELDS
982
+ };
983
+ return group.leaves.reduce((item, leaf) => setPath(item, leaf.plan.path.split("."), leaf.value), base);
984
+ }
985
+ function groupItems(entries) {
986
+ const groups = /* @__PURE__ */ new Map();
987
+ for (const entry of entries) {
988
+ const item = entry.plan.item;
989
+ if (item === void 0) continue;
990
+ const id = `${entry.plan.cap}\u0000${item.arrayPath}\u0000${item.key}`;
991
+ const group = groups.get(id) ?? {
992
+ cap: entry.plan.cap,
993
+ arrayPath: item.arrayPath,
994
+ key: item.key,
995
+ label: item.label,
996
+ leaves: []
997
+ };
998
+ groups.set(id, {
999
+ ...group,
1000
+ leaves: [...group.leaves, entry]
1001
+ });
1002
+ }
1003
+ return [...groups.values()];
1004
+ }
1005
+ function compareKeys(a, b) {
1006
+ return a < b ? -1 : a > b ? 1 : 0;
1007
+ }
1008
+ function assembleSlice(entries, lookupCap) {
1009
+ const top = Object.fromEntries(entries.filter((e) => e.plan.item === void 0).map((e) => [e.plan.path, e.value]));
1010
+ const groups = groupItems(entries);
1011
+ 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))]);
1012
+ return {
1013
+ ...top,
1014
+ ...Object.fromEntries(arrays)
1015
+ };
1016
+ }
1017
+ //#endregion
823
1018
  //#region src/builtins/composer/composition-runtime.ts
824
1019
  /**
825
1020
  * One running composition: per-field state, writes to the composed device, and
@@ -868,10 +1063,11 @@ function pendingReason(cap, pending) {
868
1063
  * not land through `onAsyncFailure`: the cap is marked write-pending by name
869
1064
  * and re-written whole on the next pass — no retry timer (D3).
870
1065
  *
871
- * Slice 2b (D670): stateful expression memory commits only when the whole
872
- * expression succeeds. A declared state-derived event advances its seeded
873
- * high-water and is emitted before the corresponding target write; neither an
874
- * event-bus failure nor a refused write can replay the event.
1066
+ * Slice 2b (D670): evaluation — stateful memory, state-derived events before
1067
+ * their write, and reads of the composition's OWN fields answered from the
1068
+ * records rather than the hub (D670 amendment) — is `composition-evaluation.ts`.
1069
+ * Neither an event-bus failure nor a refused write can replay an event, and the
1070
+ * hub's echo of the runtime's own write re-runs nothing.
875
1071
  */
876
1072
  /**
877
1073
  * A refusal nothing on this side can retry away: another owner holds the
@@ -884,9 +1080,6 @@ var TERMINAL_REFUSALS = new Set([
884
1080
  "invalid-slice",
885
1081
  "unknown-device"
886
1082
  ]);
887
- function strictFiniteAdvance(previous, next) {
888
- return typeof next === "number" && Number.isFinite(next) && (previous === null || next > previous);
889
- }
890
1083
  var CompositionRuntime = class {
891
1084
  deps;
892
1085
  records;
@@ -910,11 +1103,31 @@ var CompositionRuntime = class {
910
1103
  releaseFlaps = /* @__PURE__ */ new Map();
911
1104
  /** Caps the target refused for good (`TERMINAL_REFUSALS`): never written again by this runtime. */
912
1105
  terminal = /* @__PURE__ */ new Map();
1106
+ /** What `composition-evaluation.ts` reads and commits through. */
1107
+ evaluation;
913
1108
  constructor(deps) {
914
1109
  this.deps = deps;
915
1110
  this.startedAtMs = deps.now();
916
1111
  this.records = new Map(deps.fields.map((plan) => [fieldKey(plan), createFieldRecord(plan, deps.initialByCap, deps.lookupCap)]));
917
1112
  deps.target.onAsyncFailure?.((cap, failure) => this.onTargetWriteFailed(cap, failure));
1113
+ const eventContext = {
1114
+ deviceId: deps.target.id,
1115
+ blockId: deps.blockId,
1116
+ events: deps.events,
1117
+ logger: deps.logger
1118
+ };
1119
+ this.evaluation = {
1120
+ records: () => this.records,
1121
+ commit: (records) => {
1122
+ this.records = records;
1123
+ },
1124
+ read: deps.read,
1125
+ ...deps.isOwnRead === void 0 ? {} : { isOwnRead: deps.isOwnRead },
1126
+ eventPlans: deps.eventPlans,
1127
+ startedAtMs: this.startedAtMs,
1128
+ emit: (plan, timestamp) => emitStateDerivedEvent(eventContext, plan, timestamp),
1129
+ logTransition: (record, outcome) => this.logTransition(record, outcome)
1130
+ };
918
1131
  }
919
1132
  start() {
920
1133
  if (this.disabledReason !== null) return;
@@ -923,7 +1136,7 @@ var CompositionRuntime = class {
923
1136
  onSourcesChanged(sliceKeys) {
924
1137
  if (this.disabledReason !== null) return;
925
1138
  this.guarded("source", true, () => {
926
- const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => sliceKeys.has(require_dist.compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
1139
+ const affected = [...this.records.values()].filter((r) => r.plan.reads.some((read) => ownKeyOf(this.evaluation, read) === null && sliceKeys.has(require_dist.compositionSliceKey(read.source, read.cap)))).map((r) => fieldKey(r.plan));
927
1140
  if (affected.length > 0) this.pass(affected, "source");
928
1141
  });
929
1142
  }
@@ -1072,12 +1285,12 @@ var CompositionRuntime = class {
1072
1285
  pass(keys, trigger) {
1073
1286
  const initial = trigger === "start";
1074
1287
  const now = this.deps.now();
1075
- const calculated = this.evaluate(keys.filter((k) => {
1288
+ const calculated = evaluateWithOwnReaders(this.evaluation, keys.filter((k) => {
1076
1289
  const role = this.records.get(k)?.plan.role;
1077
1290
  return role === "derived" || role === "accumulator";
1078
1291
  }), now);
1079
1292
  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));
1080
- const extra = this.evaluate(followers, now);
1293
+ const extra = evaluateFields(this.evaluation, followers, now);
1081
1294
  const evaluatedCaps = new Set([...keys, ...followers].flatMap((k) => {
1082
1295
  const cap = this.records.get(k)?.plan.cap;
1083
1296
  return cap === void 0 ? [] : [cap];
@@ -1087,89 +1300,6 @@ var CompositionRuntime = class {
1087
1300
  this.updateFailure();
1088
1301
  this.updateAvailability();
1089
1302
  }
1090
- evaluate(keys, now) {
1091
- const changed = /* @__PURE__ */ new Set();
1092
- const valueCaps = /* @__PURE__ */ new Set();
1093
- const next = new Map(this.records);
1094
- try {
1095
- for (const key of keys) {
1096
- const record = next.get(key);
1097
- if (record === void 0) continue;
1098
- const candidateMemory = require_dist.cloneStatefulExpressionMemory(record.expressionMemory);
1099
- const outcome = require_dist.evaluateCompositionField({
1100
- source: record.plan.source,
1101
- read: this.deps.read,
1102
- now,
1103
- stateful: {
1104
- currentOutput: currentOutput(record),
1105
- expressionMemory: candidateMemory,
1106
- startedAtMs: this.startedAtMs,
1107
- callPathPrefix: fieldKey(record.plan)
1108
- }
1109
- });
1110
- const expressionSucceeded = outcome.state !== "unavailable";
1111
- let evaluated = {
1112
- ...record,
1113
- outcome,
1114
- ...expressionSucceeded ? { expressionMemory: candidateMemory } : {}
1115
- };
1116
- const eventPlans = this.deps.eventPlans.filter((eventPlan) => eventPlan.fieldKey === fieldKey(record.plan));
1117
- if (expressionSucceeded && eventPlans.length > 0 && strictFiniteAdvance(record.eventHighWater, outcome.value)) {
1118
- const timestamp = outcome.value;
1119
- evaluated = {
1120
- ...evaluated,
1121
- eventHighWater: timestamp
1122
- };
1123
- next.set(key, evaluated);
1124
- this.records = next;
1125
- for (const eventPlan of eventPlans) this.emitStateEvent(eventPlan, timestamp);
1126
- } else next.set(key, evaluated);
1127
- if (sameOutcome(record.outcome, outcome)) continue;
1128
- if (record.outcome?.state !== outcome.state) this.logTransition(record, outcome);
1129
- if (valueOf(record.outcome) !== valueOf(outcome)) valueCaps.add(record.plan.cap);
1130
- changed.add(key);
1131
- }
1132
- } finally {
1133
- this.records = next;
1134
- }
1135
- return {
1136
- keys: changed,
1137
- valueCaps
1138
- };
1139
- }
1140
- emitStateEvent(plan, timestamp) {
1141
- const extras = {
1142
- tags: { deviceId: this.deps.target.id },
1143
- meta: {
1144
- blockId: this.deps.blockId,
1145
- cap: plan.cap,
1146
- eventName: plan.eventName,
1147
- category: plan.category,
1148
- timestamp
1149
- }
1150
- };
1151
- try {
1152
- if (plan.category !== require_dist.EventCategory.DoorbellOnPressed) throw new Error(`unsupported state-derived event category \`${plan.category}\``);
1153
- const event = require_dist.createEvent(require_dist.EventCategory.DoorbellOnPressed, {
1154
- type: "device",
1155
- id: this.deps.target.id,
1156
- deviceId: this.deps.target.id
1157
- }, {
1158
- deviceId: this.deps.target.id,
1159
- timestamp
1160
- });
1161
- this.deps.events.emit(event);
1162
- this.deps.logger.debug("composition: state-derived event emitted", extras);
1163
- } catch (error) {
1164
- this.deps.logger.warn("composition: state-derived event delivery failed", {
1165
- ...extras,
1166
- meta: {
1167
- ...extras.meta,
1168
- error: errorText(error)
1169
- }
1170
- });
1171
- }
1172
- }
1173
1303
  recordsOf(cap) {
1174
1304
  return [...this.records.values()].filter((r) => r.plan.cap === cap);
1175
1305
  }
@@ -1533,6 +1663,29 @@ var ConfirmedSeed = class {
1533
1663
  * (`HeldClaims`, C-1).
1534
1664
  */
1535
1665
  /**
1666
+ * Review M3: an ADDED `restored` cap seeded from NOTHING says so, once per
1667
+ * runtime build, on its device. After a restart its state comes from the
1668
+ * target's runtime-state row; a row that lost the slice (D677: 615's doorbell,
1669
+ * dropped at a hub boot) used to surface as "not seeded yet" and now seeds a
1670
+ * clean `{ null, 0 }` — a lifetime count silently reset unless this line
1671
+ * names it. A first composition of a device that never had the slice logs it
1672
+ * too: the line cannot tell the two apart, and says both.
1673
+ */
1674
+ function reportSeedlessRestoredCaps(input) {
1675
+ for (const cap of addedCapsOf(input.features)) {
1676
+ if (input.lookupCap(cap)?.durability !== "restored") continue;
1677
+ if ((input.seed.byCap.get(cap) ?? null) !== null) continue;
1678
+ 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)", {
1679
+ tags: { deviceId: input.deviceId },
1680
+ meta: {
1681
+ blockId: input.blockId,
1682
+ cap,
1683
+ seededFrom: "none"
1684
+ }
1685
+ });
1686
+ }
1687
+ }
1688
+ /**
1536
1689
  * Read every affected effective capability exactly once, before the composer
1537
1690
  * grafts or claims any of them. A null slice is a successful empty seed; a
1538
1691
  * rejection is deliberately left to the caller so ownership cannot begin.
@@ -2496,6 +2649,14 @@ var Composer = class {
2496
2649
  let targetSeed;
2497
2650
  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 {
2498
2651
  targetSeed = await readExistingTargetSeeds(p.target.deviceId, p.fields.map((field) => field.cap), this.deps.readExistingTargetCap);
2652
+ reportSeedlessRestoredCaps({
2653
+ logger: this.deps.logger,
2654
+ blockId: p.block.id,
2655
+ deviceId: p.target.deviceId,
2656
+ features: p.target.features,
2657
+ seed: targetSeed,
2658
+ lookupCap: this.deps.lookupCap
2659
+ });
2499
2660
  } catch (err) {
2500
2661
  const reason = `the target device state could not be read before ownership (${require_dist.errMsg(err)}) — nothing is grafted or claimed until it is; retried on the next apply`;
2501
2662
  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", {
@@ -2608,6 +2769,7 @@ var Composer = class {
2608
2769
  const self = p.target.kind === "existing" ? selfReadCaps(p.fields, p.target.ref, p.target.features) : null;
2609
2770
  const targetKey = p.target.kind === "existing" ? require_dist.compositionSourceKey(p.target.ref) : null;
2610
2771
  const nativeCaps = new Set(self?.native ?? []);
2772
+ const ownKey = targetKey ?? require_dist.compositionSourceKey(require_dist.composedDeviceRef(p.block.id));
2611
2773
  return new CompositionRuntime({
2612
2774
  blockId: p.block.id,
2613
2775
  fields: p.fields,
@@ -2617,6 +2779,7 @@ var Composer = class {
2617
2779
  lookupCap: this.deps.lookupCap,
2618
2780
  ...seed === void 0 ? {} : { initialByCap: seed.byCap },
2619
2781
  read: (read) => this.deps.tracker.read(read, require_dist.compositionSourceKey(read.source) === targetKey && nativeCaps.has(read.cap) ? "native" : "effective"),
2782
+ isOwnRead: (read) => require_dist.compositionSourceKey(read.source) === ownKey && !nativeCaps.has(read.cap),
2620
2783
  now: this.deps.now,
2621
2784
  timer: this.deps.newTimer(),
2622
2785
  logger: this.deps.logger.child("runtime")
@@ -2842,7 +3005,10 @@ var NodeDeadlineTimer = class {
2842
3005
  * this runner must never answer `{}`.
2843
3006
  * - The context is `createContext(stableId, id)` on a FOREIGN device id: the
2844
3007
  * second use of the forked-runner native path D659 lists as unverified live
2845
- * (D659 live item 1). Task 16 step 4.1 is its live proof.
3008
+ * (D659 live item 1). Task 16 step 4.1 is its live proof. Its EMPTY runtime
3009
+ * state is correct and deliberate (D677): no device is built from it and no
3010
+ * slice is written through it — the graft only registers method-serving
3011
+ * natives, and `getStatus` reads the hub's merged slice.
2846
3012
  * - `ungraft` removes only what THIS host registered, through the context's
2847
3013
  * optional `unregisterNativeCap`. An older kernel without it leaves the graft
2848
3014
  * routable until the composer respawns — warned once per device, never