@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.
- package/dist/addon-runner.js +1 -1
- package/dist/addon-runner.mjs +1 -1
- package/dist/auth/auth-manager.d.ts +11 -0
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/index.js +1 -1
- package/dist/builtins/autotrack/index.mjs +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/index.js +1 -1
- package/dist/builtins/camera-grid/index.mjs +1 -1
- package/dist/builtins/composer/composer.addon.js +347 -181
- package/dist/builtins/composer/composer.addon.mjs +347 -181
- package/dist/builtins/composer/composition-evaluation.d.ts +37 -0
- package/dist/builtins/composer/composition-runtime.d.ts +11 -52
- package/dist/builtins/composer/composition-target.d.ts +50 -0
- package/dist/builtins/composer/existing-target.d.ts +19 -1
- package/dist/builtins/composer/own-field-reads.d.ts +19 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +2 -2
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +2 -2
- package/dist/builtins/device-manager/device-manager.addon.js +265 -29
- package/dist/builtins/device-manager/device-manager.addon.mjs +265 -29
- package/dist/builtins/device-manager/device-provider-context.d.ts +6 -0
- package/dist/builtins/device-manager/device-source-activation.d.ts +26 -1
- package/dist/builtins/device-manager/device-state-mirror.d.ts +41 -9
- package/dist/builtins/device-manager/mirror-row-writer.d.ts +59 -1
- package/dist/builtins/doorbell/binding-mirror.d.ts +45 -81
- package/dist/builtins/doorbell/doorbell-extension-slot.d.ts +4 -3
- package/dist/builtins/doorbell/virtual-doorbell.addon.d.ts +53 -11
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +207 -82
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +207 -82
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +47 -3
- package/dist/builtins/local-auth/local-auth.addon.mjs +46 -4
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +4 -4
- package/dist/builtins/system-config/system-config.addon.mjs +4 -4
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{child-cap-dispatch-AAltfBUH.mjs → child-cap-dispatch-C2VINHu1.mjs} +70 -18
- package/dist/{child-cap-dispatch-DzcUd_ct.js → child-cap-dispatch-C8uts1P4.js} +70 -18
- package/dist/{composition-sources-fUyF4z39.mjs → composition-sources-DVClu006.mjs} +1 -1
- package/dist/{composition-sources-BwE1POnF.js → composition-sources-Dep7nBwu.js} +1 -1
- package/dist/{dist-BPvku43i.mjs → dist-Dori4pf3.mjs} +85 -6
- package/dist/{dist-RAl10EfG.js → dist-a6p0V-IL.js} +85 -6
- package/dist/index.js +739 -350
- package/dist/index.mjs +737 -352
- package/dist/kernel/bootstrap-config.d.ts +8 -0
- package/dist/kernel/capability-registry.d.ts +30 -0
- package/dist/kernel/config-manager.d.ts +75 -129
- package/dist/kernel/index.d.ts +5 -2
- package/dist/kernel/moleculer/runtime-state-load.d.ts +19 -0
- package/dist/kernel/settings-door-view.d.ts +24 -1
- package/dist/kernel/settings-read-error.d.ts +37 -0
- package/dist/kernel/settings-view-types.d.ts +105 -0
- package/dist/kernel/store-settings-view.d.ts +17 -0
- package/dist/kernel/system-settings-mirror.d.ts +34 -0
- package/dist/kernel/system-settings-policy.d.ts +47 -0
- package/dist/{retired-settings-keys-D3rGTGq5.js → retired-settings-keys-DC1uHWPZ.js} +1 -1
- package/dist/{retired-settings-keys-DqVWSrVw.mjs → retired-settings-keys-DVcKRgzm.mjs} +1 -1
- 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-
|
|
6
|
+
const require_dist = require("../../dist-a6p0V-IL.js");
|
|
7
7
|
const require_device_state_changed = require("../../device-state-changed-Bk7cvTnz.js");
|
|
8
|
-
const require_composition_sources = require("../../composition-sources-
|
|
8
|
+
const require_composition_sources = require("../../composition-sources-Dep7nBwu.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
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
* event-bus failure nor a refused write can replay
|
|
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.
|
|
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.
|
|
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
|