@camstack/system 1.2.322 → 1.2.323
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/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 +2 -2
- package/dist/builtins/composer/composer.addon.mjs +2 -2
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/composition-api.d.ts +4 -2
- package/dist/builtins/core-blocks/core-blocks.addon.js +6 -4
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +6 -4
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +1 -13
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +6 -64
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +5 -63
- 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 +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- 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 +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{composition-sources-RzhpNlEu.mjs → composition-sources-CeAPNF5Q.mjs} +1 -1
- package/dist/{composition-sources-CB_xQieM.js → composition-sources-DkVxrXLm.js} +1 -1
- package/dist/{dist-DENeGKrn.mjs → dist-Bs6UyVGd.mjs} +1702 -1232
- package/dist/{dist-D25eq33W.js → dist-Bup0sTZb.js} +1719 -1243
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-BH9LRNsg.js → retired-settings-keys-Db0P4Yvn.js} +1 -1
- package/dist/{retired-settings-keys-iFdgpbfM.mjs → retired-settings-keys-LROp1DXx.mjs} +1 -1
- package/package.json +1 -1
|
@@ -11717,6 +11717,899 @@ var CompositionBlockStateSchema = zod.z.object({
|
|
|
11717
11717
|
reason: zod.z.string().optional(),
|
|
11718
11718
|
fields: zod.z.array(CompositionFieldStateSchema)
|
|
11719
11719
|
});
|
|
11720
|
+
function problem(code, path, message) {
|
|
11721
|
+
return {
|
|
11722
|
+
code,
|
|
11723
|
+
path,
|
|
11724
|
+
message
|
|
11725
|
+
};
|
|
11726
|
+
}
|
|
11727
|
+
function refused(...problems) {
|
|
11728
|
+
return {
|
|
11729
|
+
problems,
|
|
11730
|
+
fields: [],
|
|
11731
|
+
features: [],
|
|
11732
|
+
events: []
|
|
11733
|
+
};
|
|
11734
|
+
}
|
|
11735
|
+
/** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
|
|
11736
|
+
function compositionFieldAt(field) {
|
|
11737
|
+
const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
|
|
11738
|
+
return `features[${field.featureIndex}].${inner}.${field.path}`;
|
|
11739
|
+
}
|
|
11740
|
+
var NUMBER = {
|
|
11741
|
+
kind: "fixed",
|
|
11742
|
+
result: ["number"]
|
|
11743
|
+
};
|
|
11744
|
+
var BOOLEAN = {
|
|
11745
|
+
kind: "fixed",
|
|
11746
|
+
result: ["boolean"]
|
|
11747
|
+
};
|
|
11748
|
+
/** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
|
|
11749
|
+
var BUILTIN_RESULT_KINDS = Object.freeze({
|
|
11750
|
+
min: NUMBER,
|
|
11751
|
+
max: NUMBER,
|
|
11752
|
+
abs: NUMBER,
|
|
11753
|
+
floor: NUMBER,
|
|
11754
|
+
ceil: NUMBER,
|
|
11755
|
+
sqrt: NUMBER,
|
|
11756
|
+
round: NUMBER,
|
|
11757
|
+
pow: NUMBER,
|
|
11758
|
+
clamp: NUMBER,
|
|
11759
|
+
avg: NUMBER,
|
|
11760
|
+
sum: NUMBER,
|
|
11761
|
+
age: NUMBER,
|
|
11762
|
+
convert: NUMBER,
|
|
11763
|
+
coalesce: { kind: "coalesce" },
|
|
11764
|
+
any: BOOLEAN,
|
|
11765
|
+
all: BOOLEAN,
|
|
11766
|
+
within: BOOLEAN,
|
|
11767
|
+
latest: {
|
|
11768
|
+
kind: "fixed",
|
|
11769
|
+
result: ["number", "null"]
|
|
11770
|
+
},
|
|
11771
|
+
number: {
|
|
11772
|
+
kind: "fixed",
|
|
11773
|
+
result: ["number", "null"]
|
|
11774
|
+
},
|
|
11775
|
+
rose: {
|
|
11776
|
+
kind: "fixed",
|
|
11777
|
+
result: ["number", "null"]
|
|
11778
|
+
},
|
|
11779
|
+
count: NUMBER
|
|
11780
|
+
});
|
|
11781
|
+
var KindInferenceError = class extends Error {};
|
|
11782
|
+
function kindOfValue(value) {
|
|
11783
|
+
if (value === null) return "null";
|
|
11784
|
+
if (typeof value === "number") return "number";
|
|
11785
|
+
if (typeof value === "string") return "string";
|
|
11786
|
+
return "boolean";
|
|
11787
|
+
}
|
|
11788
|
+
function only(kind) {
|
|
11789
|
+
return new Set([kind]);
|
|
11790
|
+
}
|
|
11791
|
+
function union(...sets) {
|
|
11792
|
+
return new Set(sets.flatMap((s) => [...s]));
|
|
11793
|
+
}
|
|
11794
|
+
function withoutNull(set) {
|
|
11795
|
+
return new Set([...set].filter((k) => k !== "null"));
|
|
11796
|
+
}
|
|
11797
|
+
function binaryKinds(op, left, right) {
|
|
11798
|
+
if (op === "+") {
|
|
11799
|
+
const all = [...left, ...right];
|
|
11800
|
+
if (all.every((k) => k === "string")) return only("string");
|
|
11801
|
+
if (all.every((k) => k === "number")) return only("number");
|
|
11802
|
+
return new Set(["number", "string"]);
|
|
11803
|
+
}
|
|
11804
|
+
if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
|
|
11805
|
+
return only("boolean");
|
|
11806
|
+
}
|
|
11807
|
+
/** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
|
|
11808
|
+
function arityRange(builtin) {
|
|
11809
|
+
if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
|
|
11810
|
+
if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
|
|
11811
|
+
return `${builtin.minArgs}–${builtin.maxArgs}`;
|
|
11812
|
+
}
|
|
11813
|
+
function callKinds(callee, args) {
|
|
11814
|
+
const builtin = EXPRESSION_BUILTINS[callee];
|
|
11815
|
+
if (builtin !== void 0 && (args.length < builtin.minArgs || args.length > builtin.maxArgs)) throw new KindInferenceError(`${callee}: wrong number of arguments (${args.length}); expected ${arityRange(builtin)}`);
|
|
11816
|
+
const rule = BUILTIN_RESULT_KINDS[callee];
|
|
11817
|
+
if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
|
|
11818
|
+
if (rule.kind === "fixed") return new Set(rule.result);
|
|
11819
|
+
const nonNull = union(...args.map(withoutNull));
|
|
11820
|
+
return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
|
|
11821
|
+
}
|
|
11822
|
+
function kindsOf(node, bindings) {
|
|
11823
|
+
switch (node.kind) {
|
|
11824
|
+
case "literal": return only(kindOfValue(node.value));
|
|
11825
|
+
case "identifier": {
|
|
11826
|
+
if (node.name === "now") return only("number");
|
|
11827
|
+
const bound = bindings.get(node.name);
|
|
11828
|
+
if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
|
|
11829
|
+
return bound;
|
|
11830
|
+
}
|
|
11831
|
+
case "unary": return only(node.op === "!" ? "boolean" : "number");
|
|
11832
|
+
case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
11833
|
+
case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
11834
|
+
case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
|
|
11835
|
+
case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
|
|
11836
|
+
}
|
|
11837
|
+
}
|
|
11838
|
+
function nodeContainsCall(node, callee) {
|
|
11839
|
+
switch (node.kind) {
|
|
11840
|
+
case "literal":
|
|
11841
|
+
case "identifier": return false;
|
|
11842
|
+
case "unary": return nodeContainsCall(node.operand, callee);
|
|
11843
|
+
case "binary":
|
|
11844
|
+
case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
|
|
11845
|
+
case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
|
|
11846
|
+
case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
|
|
11847
|
+
}
|
|
11848
|
+
}
|
|
11849
|
+
function expressionContainsBuiltinCall(expr, callee) {
|
|
11850
|
+
const compiled = compileExpressionSafe(expr);
|
|
11851
|
+
return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
|
|
11852
|
+
}
|
|
11853
|
+
function inferExpressionKinds(input) {
|
|
11854
|
+
const compiled = compileExpressionSafe(input.expr);
|
|
11855
|
+
if (!compiled.ok) return {
|
|
11856
|
+
ok: false,
|
|
11857
|
+
error: compiled.error
|
|
11858
|
+
};
|
|
11859
|
+
try {
|
|
11860
|
+
return {
|
|
11861
|
+
ok: true,
|
|
11862
|
+
kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
|
|
11863
|
+
};
|
|
11864
|
+
} catch (err) {
|
|
11865
|
+
if (err instanceof KindInferenceError) return {
|
|
11866
|
+
ok: false,
|
|
11867
|
+
error: err.message
|
|
11868
|
+
};
|
|
11869
|
+
throw err;
|
|
11870
|
+
}
|
|
11871
|
+
}
|
|
11872
|
+
/**
|
|
11873
|
+
* The kind half of planning a composition field (D659): which kinds a source
|
|
11874
|
+
* can produce, whether they fit a target field, and the field's role.
|
|
11875
|
+
* Shared by the top-level planner (`validate-composition.ts`) and the item
|
|
11876
|
+
* planner (`composition-items.ts`). Internal to the composition module — NOT
|
|
11877
|
+
* in the barrel: these are planner internals, not a published contract.
|
|
11878
|
+
*/
|
|
11879
|
+
function readKinds(read, lookupCap) {
|
|
11880
|
+
const def = lookupCap(read.cap);
|
|
11881
|
+
const schema = def?.runtimeState ?? def?.status?.schema;
|
|
11882
|
+
if (def === null || schema === void 0) return {
|
|
11883
|
+
ok: false,
|
|
11884
|
+
code: "source-field-unknown",
|
|
11885
|
+
message: `no capability \`${read.cap}\` with state to read`
|
|
11886
|
+
};
|
|
11887
|
+
const field = describeStateField(schema, read.fieldPath);
|
|
11888
|
+
if (field === null) return {
|
|
11889
|
+
ok: false,
|
|
11890
|
+
code: "source-field-unknown",
|
|
11891
|
+
message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
|
|
11892
|
+
};
|
|
11893
|
+
const base = field.kind === "enum" ? "string" : field.kind;
|
|
11894
|
+
return {
|
|
11895
|
+
ok: true,
|
|
11896
|
+
kinds: new Set(field.nullable ? [base, "null"] : [base])
|
|
11897
|
+
};
|
|
11898
|
+
}
|
|
11899
|
+
function sourceKinds(source, lookupCap) {
|
|
11900
|
+
if (source.kind === "from") return readKinds(source, lookupCap);
|
|
11901
|
+
if (source.kind === "code") return {
|
|
11902
|
+
ok: false,
|
|
11903
|
+
code: "reserved-variant",
|
|
11904
|
+
message: "code fields arrive in slice 4"
|
|
11905
|
+
};
|
|
11906
|
+
const bindingKinds = /* @__PURE__ */ new Map();
|
|
11907
|
+
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
11908
|
+
if (binding.kind === "literal") {
|
|
11909
|
+
bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
|
|
11910
|
+
continue;
|
|
11911
|
+
}
|
|
11912
|
+
const kinds = readKinds(binding, lookupCap);
|
|
11913
|
+
if (!kinds.ok) return {
|
|
11914
|
+
...kinds,
|
|
11915
|
+
message: `binding \`${name}\`: ${kinds.message}`
|
|
11916
|
+
};
|
|
11917
|
+
bindingKinds.set(name, kinds.kinds);
|
|
11918
|
+
}
|
|
11919
|
+
const inferred = inferExpressionKinds({
|
|
11920
|
+
expr: source.expr,
|
|
11921
|
+
bindingKinds
|
|
11922
|
+
});
|
|
11923
|
+
return inferred.ok ? {
|
|
11924
|
+
ok: true,
|
|
11925
|
+
kinds: inferred.kinds
|
|
11926
|
+
} : {
|
|
11927
|
+
ok: false,
|
|
11928
|
+
code: "expression-invalid",
|
|
11929
|
+
message: inferred.error
|
|
11930
|
+
};
|
|
11931
|
+
}
|
|
11932
|
+
function compatibility(cap, target, kinds, at) {
|
|
11933
|
+
const allowed = target.kind === "enum" ? "string" : target.kind;
|
|
11934
|
+
const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
|
|
11935
|
+
const problems = [];
|
|
11936
|
+
if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
|
|
11937
|
+
if (kinds.has("null") && !target.nullable) problems.push(problem("may-be-null", at, `\`${cap}.${target.path}\` cannot be null and this source can produce null — wrap it in coalesce(…) or pick a non-null source`));
|
|
11938
|
+
return problems;
|
|
11939
|
+
}
|
|
11940
|
+
/**
|
|
11941
|
+
* A string a source produces WITHOUT reading any device — a field-level
|
|
11942
|
+
* `expression` source whose compiled AST is a bare string literal (`"red"`,
|
|
11943
|
+
* never `a` or `a + "x"`). Anything else that can only produce `'string'` at
|
|
11944
|
+
* the kind level (a `from` read of an enum-typed source field, a computed
|
|
11945
|
+
* expression) is NOT checked against the target's `enumValues` here: the
|
|
11946
|
+
* kind inference already collapsed it to the coarse `'string'` kind, losing
|
|
11947
|
+
* whichever enum it actually came from, so membership can only be judged at
|
|
11948
|
+
* write time against the runtime-state schema. Known limitation — see the
|
|
11949
|
+
* fix report for Task 6 round 1.
|
|
11950
|
+
*/
|
|
11951
|
+
function literalStringValue(source) {
|
|
11952
|
+
if (source.kind !== "expression") return null;
|
|
11953
|
+
const compiled = compileExpressionSafe(source.expr);
|
|
11954
|
+
if (!compiled.ok) return null;
|
|
11955
|
+
const ast = compiled.parsed.ast;
|
|
11956
|
+
return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
|
|
11957
|
+
}
|
|
11958
|
+
/**
|
|
11959
|
+
* An enum target whose source is a save-time-KNOWN string literal outside its
|
|
11960
|
+
* `enumValues` is refused now rather than left to fail the write later. A
|
|
11961
|
+
* dynamic source (a `from` read, a non-literal expression) is left to
|
|
11962
|
+
* runtime — see {@link literalStringValue}.
|
|
11963
|
+
*/
|
|
11964
|
+
function enumLiteralProblem(cap, target, source, at) {
|
|
11965
|
+
if (target.kind !== "enum" || target.enumValues === void 0) return null;
|
|
11966
|
+
const value = literalStringValue(source);
|
|
11967
|
+
if (value === null || target.enumValues.includes(value)) return null;
|
|
11968
|
+
const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
|
|
11969
|
+
return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
|
|
11970
|
+
}
|
|
11971
|
+
function roleOf(source) {
|
|
11972
|
+
if (source.kind !== "expression") return "derived";
|
|
11973
|
+
if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
|
|
11974
|
+
if (fieldReads(source).length > 0) return "derived";
|
|
11975
|
+
const compiled = compileExpressionSafe(source.expr);
|
|
11976
|
+
return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
|
|
11977
|
+
}
|
|
11978
|
+
/**
|
|
11979
|
+
* How a planned field says "unavailable", per feature mode and AFTER `roleOf`
|
|
11980
|
+
* (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
|
|
11981
|
+
* own offline, and D393 forbids a stale number, so on an existing target a
|
|
11982
|
+
* field that READS a source never gets `offline`.
|
|
11983
|
+
*/
|
|
11984
|
+
function unavailableFor(tf, role, mode, cap, at) {
|
|
11985
|
+
if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
|
|
11986
|
+
ok: true,
|
|
11987
|
+
unavailable: "release"
|
|
11988
|
+
};
|
|
11989
|
+
if (mode === "new" || role !== "derived") return {
|
|
11990
|
+
ok: true,
|
|
11991
|
+
unavailable: tf.nullable ? "null" : "offline"
|
|
11992
|
+
};
|
|
11993
|
+
if (tf.nullable) return {
|
|
11994
|
+
ok: true,
|
|
11995
|
+
unavailable: "null"
|
|
11996
|
+
};
|
|
11997
|
+
if (tf.enumValues?.includes("unknown") === true) return {
|
|
11998
|
+
ok: true,
|
|
11999
|
+
unavailable: "unknown"
|
|
12000
|
+
};
|
|
12001
|
+
return {
|
|
12002
|
+
ok: false,
|
|
12003
|
+
problem: problem("not-replaceable", at, `\`${cap}.${tf.path}\` reads a source but can say neither null nor 'unknown', and an ADDED capability has no native reading to fall back to (D393, D663)`)
|
|
12004
|
+
};
|
|
12005
|
+
}
|
|
12006
|
+
/** Item fields the composer writes itself, with the value it writes. */
|
|
12007
|
+
var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
|
|
12008
|
+
function reservedItemFields(itemArray) {
|
|
12009
|
+
return new Set([
|
|
12010
|
+
itemArray.keyField,
|
|
12011
|
+
...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
|
|
12012
|
+
...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
|
|
12013
|
+
]);
|
|
12014
|
+
}
|
|
12015
|
+
/** A required leaf under an optional parent is required only once something writes that parent. */
|
|
12016
|
+
function leafRequired(itemArray, path, written) {
|
|
12017
|
+
const described = describeStateField(itemArray.itemSchema, path);
|
|
12018
|
+
if (described === null || !described.required) return false;
|
|
12019
|
+
return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
|
|
12020
|
+
}
|
|
12021
|
+
function missingLeaves({ capName, itemArray, reserved }, item, at) {
|
|
12022
|
+
const written = Object.keys(item.fields);
|
|
12023
|
+
return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
|
|
12024
|
+
if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
|
|
12025
|
+
if (!leafRequired(itemArray, leaf.path, written)) return [];
|
|
12026
|
+
const described = describeStateField(itemArray.itemSchema, leaf.path);
|
|
12027
|
+
if (described === null || described.nullable) return [];
|
|
12028
|
+
return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
|
|
12029
|
+
});
|
|
12030
|
+
}
|
|
12031
|
+
function planItemField(ctx, leaf) {
|
|
12032
|
+
const { capName, itemArray, featureIndex, lookupCap } = ctx;
|
|
12033
|
+
const { key, label, path, source } = leaf;
|
|
12034
|
+
const at = `features[${featureIndex}].items.${key}.fields.${path}`;
|
|
12035
|
+
if (ctx.reserved.has(path)) return refused(problem("item-field-reserved", at, `\`${capName}.${itemArray.path}[].${path}\` is set by the composer itself — it takes no source`));
|
|
12036
|
+
const tf = describeStateField(itemArray.itemSchema, path);
|
|
12037
|
+
if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
|
|
12038
|
+
const kinds = sourceKinds(source, lookupCap);
|
|
12039
|
+
if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
|
|
12040
|
+
const role = roleOf(source);
|
|
12041
|
+
const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
|
|
12042
|
+
if (!unavailable.ok) return refused(unavailable.problem);
|
|
12043
|
+
const plan = {
|
|
12044
|
+
cap: capName,
|
|
12045
|
+
path,
|
|
12046
|
+
item: {
|
|
12047
|
+
arrayPath: itemArray.path,
|
|
12048
|
+
key,
|
|
12049
|
+
label
|
|
12050
|
+
},
|
|
12051
|
+
featureIndex,
|
|
12052
|
+
kind: tf.kind,
|
|
12053
|
+
nullable: tf.nullable,
|
|
12054
|
+
required: tf.required,
|
|
12055
|
+
unavailable: unavailable.unavailable,
|
|
12056
|
+
role,
|
|
12057
|
+
source,
|
|
12058
|
+
reads: [...fieldReads(source)]
|
|
12059
|
+
};
|
|
12060
|
+
const enumProblem = enumLiteralProblem(capName, tf, source, at);
|
|
12061
|
+
return {
|
|
12062
|
+
problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
|
|
12063
|
+
fields: [plan],
|
|
12064
|
+
features: [],
|
|
12065
|
+
events: []
|
|
12066
|
+
};
|
|
12067
|
+
}
|
|
12068
|
+
function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
|
|
12069
|
+
const at = `features[${featureIndex}].items`;
|
|
12070
|
+
const itemArray = def.status?.itemArray;
|
|
12071
|
+
if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
|
|
12072
|
+
if (itemArray.path !== "items") return refused(problem("not-item-array", at, `\`${def.name}\`'s item array is at \`${itemArray.path}\`, not \`${COMPOSITION_ITEM_ARRAY_PATH}\` — not composable yet`));
|
|
12073
|
+
const ctx = {
|
|
12074
|
+
capName: def.name,
|
|
12075
|
+
itemArray,
|
|
12076
|
+
reserved: reservedItemFields(itemArray),
|
|
12077
|
+
featureIndex,
|
|
12078
|
+
lookupCap,
|
|
12079
|
+
mode
|
|
12080
|
+
};
|
|
12081
|
+
const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
|
|
12082
|
+
problems: missingLeaves(ctx, item, `${at}.${key}`),
|
|
12083
|
+
fields: [],
|
|
12084
|
+
features: [],
|
|
12085
|
+
events: []
|
|
12086
|
+
}, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
|
|
12087
|
+
key,
|
|
12088
|
+
label: item.label,
|
|
12089
|
+
path,
|
|
12090
|
+
source
|
|
12091
|
+
}))]);
|
|
12092
|
+
return {
|
|
12093
|
+
problems: results.flatMap((r) => r.problems),
|
|
12094
|
+
fields: results.flatMap((r) => r.fields),
|
|
12095
|
+
features: [],
|
|
12096
|
+
events: []
|
|
12097
|
+
};
|
|
12098
|
+
}
|
|
12099
|
+
/**
|
|
12100
|
+
* The field graph across every composition, and the cycles in it (D659).
|
|
12101
|
+
*
|
|
12102
|
+
* A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
|
|
12103
|
+
* edge points from a composed field to each field it reads. A cycle, direct or
|
|
12104
|
+
* through another composition, is refused at save by name (spec §2).
|
|
12105
|
+
*
|
|
12106
|
+
* The graph is built for ONE save: the CANDIDATE composition (the one being
|
|
12107
|
+
* validated) plus every STORED composition. The candidate replaces its own old
|
|
12108
|
+
* version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
|
|
12109
|
+
* introduces or removes a cycle must never depend on array order. A DIFFERENT
|
|
12110
|
+
* block targeting the candidate's device is refused by name: one composition
|
|
12111
|
+
* per device (D663). Two different stored peers that (wrongly) target the same
|
|
12112
|
+
* device are corrupted state, not a cycle to resolve by picking one silently;
|
|
12113
|
+
* that is refused by name too (D391).
|
|
12114
|
+
*
|
|
12115
|
+
* A customization of an EXISTING device reads that device's NATIVE value for a
|
|
12116
|
+
* field it replaces or does not write ("Invert state" reads the `on` it
|
|
12117
|
+
* replaces): such a self-read is no edge, so it is never a cycle (D663).
|
|
12118
|
+
* Another composition reading that field reads the customizer's output.
|
|
12119
|
+
*
|
|
12120
|
+
* `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
|
|
12121
|
+
* marks a node `done` after its first expansion) — not every elementary cycle
|
|
12122
|
+
* through a shared node, so the path it names may AVOID the candidate while
|
|
12123
|
+
* one through it goes unreported. What a save refuses is therefore decided per
|
|
12124
|
+
* strongly connected component (`findCandidateCycles`, C-1). A dotted
|
|
12125
|
+
* `fieldPath` is matched only as an exact string: a write to `state.value` and
|
|
12126
|
+
* a read of `state` would not connect (no nested field writes exist yet).
|
|
12127
|
+
*
|
|
12128
|
+
* An item leaf is its own node, `…#consumables.items[desiccant].status`
|
|
12129
|
+
* (`featureFieldEntries`), so two items sharing an inner path never merge. No
|
|
12130
|
+
* READ can name an array element (`describeStateField` never describes one),
|
|
12131
|
+
* so an item node is write-only: its edges to sources are what the check needs.
|
|
12132
|
+
*/
|
|
12133
|
+
/** Two blocks' compositions target the same device — refused by name, never silently resolved. */
|
|
12134
|
+
var CompositionGraphError = class extends Error {};
|
|
12135
|
+
/** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
|
|
12136
|
+
function writtenFields(peer) {
|
|
12137
|
+
return new Set(peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
|
|
12138
|
+
}
|
|
12139
|
+
/** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
|
|
12140
|
+
function readsComposedValue(peer, written, read) {
|
|
12141
|
+
if (compositionSourceKey(read.source) !== compositionSourceKey(peer.target)) return true;
|
|
12142
|
+
const field = `${read.cap}.${read.fieldPath}`;
|
|
12143
|
+
return written.has(field) && peer.replaced?.has(field) !== true;
|
|
12144
|
+
}
|
|
12145
|
+
function peerEdges(peer) {
|
|
12146
|
+
const written = writtenFields(peer);
|
|
12147
|
+
return peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
|
|
12148
|
+
source: peer.target,
|
|
12149
|
+
cap: feature.cap,
|
|
12150
|
+
fieldPath
|
|
12151
|
+
}), fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(compositionReadKey)]));
|
|
12152
|
+
}
|
|
12153
|
+
/**
|
|
12154
|
+
* Every peer that actually takes part in the graph: each stored peer minus the
|
|
12155
|
+
* candidate's own old version (same block id — the drop happens before any
|
|
12156
|
+
* edge is built, so its old edges never enter the map), plus the candidate
|
|
12157
|
+
* itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
|
|
12158
|
+
* block targets the candidate's device, or two stored peers share a device.
|
|
12159
|
+
*/
|
|
12160
|
+
function resolvedPeers(candidate, stored) {
|
|
12161
|
+
const candidateKey = compositionSourceKey(candidate.target);
|
|
12162
|
+
const survivors = /* @__PURE__ */ new Map();
|
|
12163
|
+
for (const peer of stored) {
|
|
12164
|
+
if (peer.blockId === candidate.blockId) continue;
|
|
12165
|
+
const key = compositionSourceKey(peer.target);
|
|
12166
|
+
if (key === candidateKey) throw new CompositionGraphError(`block \`${peer.blockId}\` and block \`${candidate.blockId}\` both target device \`${key}\` — one composition per device; delete or edit the existing one`);
|
|
12167
|
+
const clash = survivors.get(key);
|
|
12168
|
+
if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
|
|
12169
|
+
survivors.set(key, peer);
|
|
12170
|
+
}
|
|
12171
|
+
return [...survivors.values(), candidate];
|
|
12172
|
+
}
|
|
12173
|
+
function compositionEdges(candidate, stored) {
|
|
12174
|
+
const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
|
|
12175
|
+
return new Map(edges);
|
|
12176
|
+
}
|
|
12177
|
+
/** Tarjan's strongly connected components over every node of the graph (sources included). */
|
|
12178
|
+
function stronglyConnected(edges) {
|
|
12179
|
+
const index = /* @__PURE__ */ new Map();
|
|
12180
|
+
const low = /* @__PURE__ */ new Map();
|
|
12181
|
+
const onStack = /* @__PURE__ */ new Set();
|
|
12182
|
+
const stack = [];
|
|
12183
|
+
const components = [];
|
|
12184
|
+
const connect = (node) => {
|
|
12185
|
+
index.set(node, index.size);
|
|
12186
|
+
low.set(node, index.get(node) ?? 0);
|
|
12187
|
+
stack.push(node);
|
|
12188
|
+
onStack.add(node);
|
|
12189
|
+
for (const next of edges.get(node) ?? []) if (!index.has(next)) {
|
|
12190
|
+
connect(next);
|
|
12191
|
+
low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
|
|
12192
|
+
} else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
|
|
12193
|
+
if (low.get(node) !== index.get(node)) return;
|
|
12194
|
+
const component = [];
|
|
12195
|
+
for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
|
|
12196
|
+
onStack.delete(member);
|
|
12197
|
+
component.push(member);
|
|
12198
|
+
if (member === node) break;
|
|
12199
|
+
}
|
|
12200
|
+
components.push(component);
|
|
12201
|
+
};
|
|
12202
|
+
for (const node of edges.keys()) if (!index.has(node)) connect(node);
|
|
12203
|
+
return components;
|
|
12204
|
+
}
|
|
12205
|
+
/** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
|
|
12206
|
+
function cycleThrough(start, component, edges) {
|
|
12207
|
+
const parent = /* @__PURE__ */ new Map();
|
|
12208
|
+
const queue = [start];
|
|
12209
|
+
for (let head = 0; head < queue.length; head += 1) {
|
|
12210
|
+
const node = queue[head];
|
|
12211
|
+
if (node === void 0) break;
|
|
12212
|
+
for (const next of edges.get(node) ?? []) {
|
|
12213
|
+
if (next === start) {
|
|
12214
|
+
const chain = [];
|
|
12215
|
+
for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
|
|
12216
|
+
return [
|
|
12217
|
+
start,
|
|
12218
|
+
...chain.reverse(),
|
|
12219
|
+
start
|
|
12220
|
+
];
|
|
12221
|
+
}
|
|
12222
|
+
if (!component.has(next) || parent.has(next)) continue;
|
|
12223
|
+
parent.set(next, node);
|
|
12224
|
+
queue.push(next);
|
|
12225
|
+
}
|
|
12226
|
+
}
|
|
12227
|
+
return [start, start];
|
|
12228
|
+
}
|
|
12229
|
+
/**
|
|
12230
|
+
* The cycles THIS save closes, decided per strongly connected component — never
|
|
12231
|
+
* per reported DFS path, which names one back-edge per revisit and can report a
|
|
12232
|
+
* component's cycle by a path that avoids the candidate while the one through it
|
|
12233
|
+
* goes unreported. A component refuses when it holds a candidate node and a
|
|
12234
|
+
* cycle (more than one node, or a self-loop). Components without a candidate
|
|
12235
|
+
* node are not this save's cycles.
|
|
12236
|
+
*/
|
|
12237
|
+
function findCandidateCycles(candidate, stored) {
|
|
12238
|
+
const edges = compositionEdges(candidate, stored);
|
|
12239
|
+
const prefix = `${compositionSourceKey(candidate.target)}#`;
|
|
12240
|
+
return stronglyConnected(edges).flatMap((members) => {
|
|
12241
|
+
const start = members.find((n) => n.startsWith(prefix));
|
|
12242
|
+
if (start === void 0) return [];
|
|
12243
|
+
const component = new Set(members);
|
|
12244
|
+
const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
|
|
12245
|
+
if (members.length === 1 && !selfLoop) return [];
|
|
12246
|
+
return [{
|
|
12247
|
+
path: cycleThrough(start, component, edges),
|
|
12248
|
+
component
|
|
12249
|
+
}];
|
|
12250
|
+
});
|
|
12251
|
+
}
|
|
12252
|
+
function buildCapabilityLookup(defs) {
|
|
12253
|
+
const byName = new Map(defs.map((d) => [d.name, d]));
|
|
12254
|
+
return (capName) => byName.get(capName) ?? null;
|
|
12255
|
+
}
|
|
12256
|
+
/** Caps every `BaseDevice` registers on itself; a second registration throws. */
|
|
12257
|
+
var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
|
|
12258
|
+
var PENDING_SELF = {
|
|
12259
|
+
addonId: COMPOSER_ADDON_ID,
|
|
12260
|
+
stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
|
|
12261
|
+
};
|
|
12262
|
+
function composableCap(feature, at, lookupCap) {
|
|
12263
|
+
const def = lookupCap(feature.cap);
|
|
12264
|
+
if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
|
|
12265
|
+
if (BASE_DEVICE_CAPS.has(def.name)) return problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`);
|
|
12266
|
+
const state = def.runtimeState;
|
|
12267
|
+
if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
|
|
12268
|
+
return {
|
|
12269
|
+
def,
|
|
12270
|
+
state,
|
|
12271
|
+
fields: describeTopLevelStateFields(state)
|
|
12272
|
+
};
|
|
12273
|
+
}
|
|
12274
|
+
/**
|
|
12275
|
+
* A capability BUILT by the composition (a new device, or one ADDED to an
|
|
12276
|
+
* existing device) must fit the device type, serve no command, and have every
|
|
12277
|
+
* required field. A REPLACE needs none of it: the cap is bound (a fact), the
|
|
12278
|
+
* native keeps serving its methods, and only the named fields are owned (D663).
|
|
12279
|
+
*/
|
|
12280
|
+
function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
|
|
12281
|
+
const problems = [];
|
|
12282
|
+
if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
|
|
12283
|
+
const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
|
|
12284
|
+
if (served.length > 0) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
|
|
12285
|
+
const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
|
|
12286
|
+
for (const key of requiredTopLevelKeys(state)) {
|
|
12287
|
+
if (key === itemArrayPath) continue;
|
|
12288
|
+
if (!targetFields.has(key)) problems.push(problem("not-composable", `${at}.fields`, `\`${def.name}.${key}\` is not a scalar — nested fields arrive in a later slice`));
|
|
12289
|
+
else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
|
|
12290
|
+
}
|
|
12291
|
+
return problems;
|
|
12292
|
+
}
|
|
12293
|
+
function planTopLevelField({ def, fields }, field, mode, lookupCap) {
|
|
12294
|
+
const { featureIndex, path, source } = field;
|
|
12295
|
+
const fieldAt = `features[${featureIndex}].fields.${path}`;
|
|
12296
|
+
if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
|
|
12297
|
+
const tf = fields.get(path);
|
|
12298
|
+
if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
|
|
12299
|
+
const kinds = sourceKinds(source, lookupCap);
|
|
12300
|
+
if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
|
|
12301
|
+
const role = roleOf(source);
|
|
12302
|
+
const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
|
|
12303
|
+
if (!unavailable.ok) return refused(unavailable.problem);
|
|
12304
|
+
const plan = {
|
|
12305
|
+
cap: def.name,
|
|
12306
|
+
path,
|
|
12307
|
+
featureIndex,
|
|
12308
|
+
kind: tf.kind,
|
|
12309
|
+
nullable: tf.nullable,
|
|
12310
|
+
required: tf.required,
|
|
12311
|
+
unavailable: unavailable.unavailable,
|
|
12312
|
+
role,
|
|
12313
|
+
source,
|
|
12314
|
+
reads: [...fieldReads(source)]
|
|
12315
|
+
};
|
|
12316
|
+
const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
|
|
12317
|
+
return {
|
|
12318
|
+
problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
|
|
12319
|
+
fields: [plan],
|
|
12320
|
+
features: [],
|
|
12321
|
+
events: []
|
|
12322
|
+
};
|
|
12323
|
+
}
|
|
12324
|
+
function planFeature(feature, featureIndex, target, lookupCap) {
|
|
12325
|
+
const at = `features[${featureIndex}]`;
|
|
12326
|
+
const cap = composableCap(feature, at, lookupCap);
|
|
12327
|
+
if ("code" in cap) return refused(cap);
|
|
12328
|
+
const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
|
|
12329
|
+
if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
|
|
12330
|
+
const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
|
|
12331
|
+
featureIndex,
|
|
12332
|
+
path,
|
|
12333
|
+
source
|
|
12334
|
+
}, target.mode, lookupCap));
|
|
12335
|
+
const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
|
|
12336
|
+
const all = [...perField, ...items];
|
|
12337
|
+
const fields = all.flatMap((result) => result.fields);
|
|
12338
|
+
const featurePlan = {
|
|
12339
|
+
featureIndex,
|
|
12340
|
+
cap: cap.def.name,
|
|
12341
|
+
mode: target.mode
|
|
12342
|
+
};
|
|
12343
|
+
const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
|
|
12344
|
+
return {
|
|
12345
|
+
problems: [
|
|
12346
|
+
...capProblems,
|
|
12347
|
+
...all.flatMap((r) => r.problems),
|
|
12348
|
+
...eventPlan.problems
|
|
12349
|
+
],
|
|
12350
|
+
fields,
|
|
12351
|
+
features: [featurePlan],
|
|
12352
|
+
events: eventPlan.events
|
|
12353
|
+
};
|
|
12354
|
+
}
|
|
12355
|
+
/** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
|
|
12356
|
+
function isNativeCap(target, cap) {
|
|
12357
|
+
const owner = target.nativeCaps.get(cap);
|
|
12358
|
+
return owner !== void 0 && owner !== "composer";
|
|
12359
|
+
}
|
|
12360
|
+
/** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
|
|
12361
|
+
function planTargetOf(composition, target) {
|
|
12362
|
+
if (composition.target.kind === "new") return {
|
|
12363
|
+
kind: "new",
|
|
12364
|
+
type: composition.target.type
|
|
12365
|
+
};
|
|
12366
|
+
const key = compositionSourceKey(composition.target.device);
|
|
12367
|
+
if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
|
|
12368
|
+
if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
|
|
12369
|
+
if (target.kind === "unreadable") return problem("target-unreadable", "target", `could not read target device \`${key}\` (${target.error}) — the save is refused rather than stored unchecked`);
|
|
12370
|
+
return {
|
|
12371
|
+
kind: "existing",
|
|
12372
|
+
found: target
|
|
12373
|
+
};
|
|
12374
|
+
}
|
|
12375
|
+
function featureTargetOf(target, cap) {
|
|
12376
|
+
if (target.kind === "new") return {
|
|
12377
|
+
mode: "new",
|
|
12378
|
+
deviceType: target.type
|
|
12379
|
+
};
|
|
12380
|
+
return {
|
|
12381
|
+
mode: isNativeCap(target.found, cap) ? "replace" : "add",
|
|
12382
|
+
deviceType: target.found.type
|
|
12383
|
+
};
|
|
12384
|
+
}
|
|
12385
|
+
/**
|
|
12386
|
+
* The static plan. `target` describes an EXISTING target device (one read);
|
|
12387
|
+
* it is ignored for a new one. On an existing target each feature REPLACES a
|
|
12388
|
+
* cap the device has natively, else ADDS it (D663).
|
|
12389
|
+
*/
|
|
12390
|
+
function planComposition(composition, lookupCap, target) {
|
|
12391
|
+
const planTarget = planTargetOf(composition, target);
|
|
12392
|
+
if ("code" in planTarget) return refused(planTarget);
|
|
12393
|
+
const results = composition.features.map((feature, index) => {
|
|
12394
|
+
const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
|
|
12395
|
+
return compositionFeatureEnabled(feature) ? result : {
|
|
12396
|
+
problems: result.problems,
|
|
12397
|
+
fields: [],
|
|
12398
|
+
features: [],
|
|
12399
|
+
events: []
|
|
12400
|
+
};
|
|
12401
|
+
});
|
|
12402
|
+
return {
|
|
12403
|
+
problems: results.flatMap((r) => r.problems),
|
|
12404
|
+
fields: results.flatMap((r) => r.fields),
|
|
12405
|
+
features: results.flatMap((r) => r.features),
|
|
12406
|
+
events: results.flatMap((r) => r.events)
|
|
12407
|
+
};
|
|
12408
|
+
}
|
|
12409
|
+
function compositionSourceRefs(composition) {
|
|
12410
|
+
const refs = composition.features.flatMap((f) => compositionFeatureEnabled(f) && f.kind === "fields" ? featureSources(f).flatMap(fieldReads).map((r) => r.source) : []);
|
|
12411
|
+
return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
|
|
12412
|
+
}
|
|
12413
|
+
/**
|
|
12414
|
+
* The candidate's device: the existing target itself, else `self`, else a
|
|
12415
|
+
* placeholder that cannot collide with a stored target. An existing target is
|
|
12416
|
+
* known even on create, so a second customization of it is refused on create too.
|
|
12417
|
+
*/
|
|
12418
|
+
function candidateSelf(composition, ctx, plan) {
|
|
12419
|
+
return {
|
|
12420
|
+
ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
|
|
12421
|
+
native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
|
|
12422
|
+
replaced: replacedFieldKeys(plan)
|
|
12423
|
+
};
|
|
12424
|
+
}
|
|
12425
|
+
/**
|
|
12426
|
+
* `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
|
|
12427
|
+
* candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
|
|
12428
|
+
* derivation, so the graph reads both the same way.
|
|
12429
|
+
*/
|
|
12430
|
+
function replacedFieldKeys(plan) {
|
|
12431
|
+
const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
|
|
12432
|
+
return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
|
|
12433
|
+
}
|
|
12434
|
+
/**
|
|
12435
|
+
* A read of the candidate's own device. Its own composed output (a new device's
|
|
12436
|
+
* field, an ADDED field) is the slice-1 own-field rule; on an existing target
|
|
12437
|
+
* every other field — replaced, or not composed at all — is the NATIVE value,
|
|
12438
|
+
* valid when the device serves that cap natively (D663, rule 5).
|
|
12439
|
+
*/
|
|
12440
|
+
function selfReadProblems(read, at, fields, self) {
|
|
12441
|
+
const readKey = `${read.cap}.${read.fieldPath}`;
|
|
12442
|
+
if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
|
|
12443
|
+
if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
|
|
12444
|
+
if (isNativeCap(self.native, read.cap)) return [];
|
|
12445
|
+
return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
|
|
12446
|
+
}
|
|
12447
|
+
function sourceProblems(fields, ctx, self) {
|
|
12448
|
+
const selfKey = compositionSourceKey(self.ref);
|
|
12449
|
+
return fields.flatMap((field) => field.reads.flatMap((read) => {
|
|
12450
|
+
const at = compositionFieldAt(field);
|
|
12451
|
+
const key = compositionSourceKey(read.source);
|
|
12452
|
+
if (key === selfKey) return selfReadProblems(read, at, fields, self);
|
|
12453
|
+
const desc = ctx.sources.get(key);
|
|
12454
|
+
if (desc === void 0 || desc.kind === "unreadable") return [problem("source-unreadable", at, `could not read source \`${key}\` (${desc === void 0 ? "it was not read" : desc.error}) — the save is refused rather than stored unchecked`)];
|
|
12455
|
+
if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
|
|
12456
|
+
if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
|
|
12457
|
+
return [];
|
|
12458
|
+
}));
|
|
12459
|
+
}
|
|
12460
|
+
/**
|
|
12461
|
+
* A cycle's node keys are `compositionReadKey` strings:
|
|
12462
|
+
* `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
|
|
12463
|
+
* CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
|
|
12464
|
+
* back out so the refusal can point at that exact field, matching the
|
|
12465
|
+
* documented `features[N].fields.<path>` convention. A cap name has no `.`,
|
|
12466
|
+
* so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
|
|
12467
|
+
* (`items[desiccant].status`) is matched through `compositionFieldKey`.
|
|
12468
|
+
*/
|
|
12469
|
+
function candidateFieldOfNode(nodeKey, candidateKey) {
|
|
12470
|
+
const prefix = `${candidateKey}#`;
|
|
12471
|
+
if (!nodeKey.startsWith(prefix)) return null;
|
|
12472
|
+
const rest = nodeKey.slice(prefix.length);
|
|
12473
|
+
const dot = rest.indexOf(".");
|
|
12474
|
+
if (dot === -1) return null;
|
|
12475
|
+
return {
|
|
12476
|
+
cap: rest.slice(0, dot),
|
|
12477
|
+
fieldPath: rest.slice(dot + 1)
|
|
12478
|
+
};
|
|
12479
|
+
}
|
|
12480
|
+
/** The first node of a cycle's path that is one of THIS composition's own
|
|
12481
|
+
* planned fields, as a documented-convention path; `'features'` when the
|
|
12482
|
+
* cycle runs entirely through other stored compositions. */
|
|
12483
|
+
function cyclePath(nodePath, candidateKey, fields) {
|
|
12484
|
+
for (const nodeKey of nodePath) {
|
|
12485
|
+
const hit = candidateFieldOfNode(nodeKey, candidateKey);
|
|
12486
|
+
if (hit === null) continue;
|
|
12487
|
+
const hitKey = `${hit.cap}.${hit.fieldPath}`;
|
|
12488
|
+
const field = fields.find((f) => compositionFieldKey(f) === hitKey);
|
|
12489
|
+
if (field !== void 0) return compositionFieldAt(field);
|
|
12490
|
+
}
|
|
12491
|
+
return "features";
|
|
12492
|
+
}
|
|
12493
|
+
/** The first unread device any of `nodes` belongs to; null when none does. */
|
|
12494
|
+
function unreadDeviceOf(nodes, unread) {
|
|
12495
|
+
for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
|
|
12496
|
+
key,
|
|
12497
|
+
error
|
|
12498
|
+
};
|
|
12499
|
+
return null;
|
|
12500
|
+
}
|
|
12501
|
+
/**
|
|
12502
|
+
* Cycle check runs against the CANDIDATE (this composition, as block
|
|
12503
|
+
* `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
|
|
12504
|
+
* decided per strongly connected component (`findCandidateCycles`): a cyclic
|
|
12505
|
+
* component holding a candidate node refuses, with one representative cycle
|
|
12506
|
+
* through it; one without is not this save's cycle. A
|
|
12507
|
+
* stored composition of the same block is its old version; another block on
|
|
12508
|
+
* the same device, or two stored peers sharing a device, make
|
|
12509
|
+
* `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
|
|
12510
|
+
* `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
|
|
12511
|
+
*/
|
|
12512
|
+
function cycleProblems(composition, ctx, fields, self) {
|
|
12513
|
+
const candidate = {
|
|
12514
|
+
blockId: ctx.blockId ?? "(unsaved)",
|
|
12515
|
+
target: self.ref,
|
|
12516
|
+
composition,
|
|
12517
|
+
replaced: self.replaced
|
|
12518
|
+
};
|
|
12519
|
+
const candidateKey = compositionSourceKey(candidate.target);
|
|
12520
|
+
const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
|
|
12521
|
+
try {
|
|
12522
|
+
return findCandidateCycles(candidate, ctx.peers).map((c) => {
|
|
12523
|
+
const at = cyclePath(c.path, candidateKey, fields);
|
|
12524
|
+
const through = unreadDeviceOf([...c.component], unread);
|
|
12525
|
+
return through === null ? problem("cycle", at, `a field reads itself: ${c.path.join(" → ")}`) : problem("cycle", at, `possible cycle through \`${through.key}\` — its state could not be read (${through.error}): ${c.path.join(" → ")}`);
|
|
12526
|
+
});
|
|
12527
|
+
} catch (err) {
|
|
12528
|
+
if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
|
|
12529
|
+
throw err;
|
|
12530
|
+
}
|
|
12531
|
+
}
|
|
12532
|
+
function validateComposition(composition, ctx) {
|
|
12533
|
+
const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
|
|
12534
|
+
const self = candidateSelf(composition, ctx, plan);
|
|
12535
|
+
const cycles = cycleProblems(composition, ctx, plan.fields, self);
|
|
12536
|
+
const problems = [
|
|
12537
|
+
...plan.problems,
|
|
12538
|
+
...sourceProblems(plan.fields, ctx, self),
|
|
12539
|
+
...cycles
|
|
12540
|
+
];
|
|
12541
|
+
return {
|
|
12542
|
+
ok: problems.length === 0,
|
|
12543
|
+
problems,
|
|
12544
|
+
fields: [...plan.fields],
|
|
12545
|
+
features: [...plan.features],
|
|
12546
|
+
events: [...plan.events]
|
|
12547
|
+
};
|
|
12548
|
+
}
|
|
12549
|
+
/**
|
|
12550
|
+
* What a composition may BUILD on a device type (G2, D676): the caps a new
|
|
12551
|
+
* device of that type, or an existing one that lacks them, can be given. The
|
|
12552
|
+
* blockers are the validator's own refusal codes, and a spec compares this
|
|
12553
|
+
* listing with `planComposition` for every capability — the editor can never
|
|
12554
|
+
* offer a feature the save would refuse for the same reason.
|
|
12555
|
+
*/
|
|
12556
|
+
var ComposableFieldSchema = zod.z.object({
|
|
12557
|
+
path: zod.z.string(),
|
|
12558
|
+
kind: CompositionFieldKindSchema,
|
|
12559
|
+
nullable: zod.z.boolean(),
|
|
12560
|
+
required: zod.z.boolean(),
|
|
12561
|
+
enumValues: zod.z.array(zod.z.string()).optional()
|
|
12562
|
+
});
|
|
12563
|
+
var ComposableCapabilitySchema = zod.z.object({
|
|
12564
|
+
cap: zod.z.string(),
|
|
12565
|
+
fields: zod.z.array(ComposableFieldSchema),
|
|
12566
|
+
/** Present for an item-array cap (`consumables.items`). */
|
|
12567
|
+
itemArray: zod.z.object({
|
|
12568
|
+
path: zod.z.string(),
|
|
12569
|
+
keyField: zod.z.string()
|
|
12570
|
+
}).optional()
|
|
12571
|
+
});
|
|
12572
|
+
var ComposableCapabilitiesSchema = zod.z.object({ caps: zod.z.array(ComposableCapabilitySchema) });
|
|
12573
|
+
/** Why a cap cannot be BUILT (new device, or added to an existing one) on `deviceType`; [] = it can. */
|
|
12574
|
+
function builtCapabilityBlockers(def, deviceType) {
|
|
12575
|
+
if (BASE_DEVICE_CAPS.has(def.name)) return ["not-composable"];
|
|
12576
|
+
const state = def.runtimeState;
|
|
12577
|
+
if (def.scope !== "device" || state === void 0) return ["not-composable"];
|
|
12578
|
+
const blockers = [];
|
|
12579
|
+
if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) blockers.push("wrong-device-type");
|
|
12580
|
+
if (Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).length > 0) blockers.push("has-commands");
|
|
12581
|
+
const fields = describeTopLevelStateFields(state);
|
|
12582
|
+
const itemArrayPath = def.status?.itemArray?.path ?? null;
|
|
12583
|
+
if (requiredTopLevelKeys(state).some((key) => key !== itemArrayPath && !fields.has(key))) blockers.push("not-composable");
|
|
12584
|
+
if (planCompositionEvents(def, {
|
|
12585
|
+
featureIndex: 0,
|
|
12586
|
+
cap: def.name,
|
|
12587
|
+
mode: "new"
|
|
12588
|
+
}, []).problems.some((p) => p.code === "not-composable") && !blockers.includes("not-composable")) blockers.push("not-composable");
|
|
12589
|
+
return blockers;
|
|
12590
|
+
}
|
|
12591
|
+
/** Every cap with no blocker, sorted by name. */
|
|
12592
|
+
function describeComposableCaps(defs, deviceType) {
|
|
12593
|
+
return defs.filter((def) => builtCapabilityBlockers(def, deviceType).length === 0).map((def) => {
|
|
12594
|
+
const state = def.runtimeState;
|
|
12595
|
+
const fields = state === void 0 ? [] : [...describeTopLevelStateFields(state).values()];
|
|
12596
|
+
const itemArray = def.status?.itemArray;
|
|
12597
|
+
return {
|
|
12598
|
+
cap: def.name,
|
|
12599
|
+
fields: fields.map((f) => ({
|
|
12600
|
+
path: f.path,
|
|
12601
|
+
kind: f.kind,
|
|
12602
|
+
nullable: f.nullable,
|
|
12603
|
+
required: f.required,
|
|
12604
|
+
...f.enumValues === void 0 ? {} : { enumValues: [...f.enumValues] }
|
|
12605
|
+
})),
|
|
12606
|
+
...itemArray === void 0 ? {} : { itemArray: {
|
|
12607
|
+
path: itemArray.path,
|
|
12608
|
+
keyField: itemArray.keyField
|
|
12609
|
+
} }
|
|
12610
|
+
};
|
|
12611
|
+
}).toSorted((a, b) => a.cap.localeCompare(b.cap));
|
|
12612
|
+
}
|
|
11720
12613
|
/**
|
|
11721
12614
|
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
11722
12615
|
* its own process.
|
|
@@ -12013,6 +12906,14 @@ var coreBlocksCapability = {
|
|
|
12013
12906
|
stableId: zod.z.string().min(1)
|
|
12014
12907
|
}), CompositionCustomizationReadSchema, { auth: "admin" }),
|
|
12015
12908
|
/**
|
|
12909
|
+
* The capabilities a composition may BUILD on a device of `deviceType`
|
|
12910
|
+
* (a new device, or added to an existing one), with their fields — the
|
|
12911
|
+
* editor's feature list and the presets' availability (G2, D676). Built
|
|
12912
|
+
* from the validator's own blockers; a replace of a BOUND cap is not
|
|
12913
|
+
* listed here (the device's bindings say what it has).
|
|
12914
|
+
*/
|
|
12915
|
+
listComposableCaps: method(zod.z.object({ deviceType: zod.z.enum(DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }),
|
|
12916
|
+
/**
|
|
12016
12917
|
* Validate without saving — the same verdict the save reaches. A mutation
|
|
12017
12918
|
* for its body size; it writes nothing, so its access is `view`.
|
|
12018
12919
|
*/
|
|
@@ -20992,7 +21893,7 @@ function cameraSensorRefKey(ref) {
|
|
|
20992
21893
|
}
|
|
20993
21894
|
/** CS-1 accepts exactly one mode; CS-2 (D673) widens it to confirm / replace. */
|
|
20994
21895
|
var CameraSensorsModeSchema = zod.z.enum(["in-addition"]);
|
|
20995
|
-
zod.z.object({
|
|
21896
|
+
var CameraSensorsConfigSchema = zod.z.object({
|
|
20996
21897
|
mode: CameraSensorsModeSchema,
|
|
20997
21898
|
sensors: zod.z.array(CameraSensorRefSchema).min(1).max(8)
|
|
20998
21899
|
}).superRefine((value, ctx) => {
|
|
@@ -26179,6 +27080,16 @@ var NodeInferenceDevicesSchema = zod.z.object({
|
|
|
26179
27080
|
* Strictly the control plane: never sees a frame, never calls inference,
|
|
26180
27081
|
* never drains queues. The runner does that work locally on each agent.
|
|
26181
27082
|
*/
|
|
27083
|
+
/**
|
|
27084
|
+
* The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
|
|
27085
|
+
* camera's attached sensors (D672). `config: null` removes the attachment —
|
|
27086
|
+
* the camera reads `not-configured` again, never `unreadable`.
|
|
27087
|
+
*/
|
|
27088
|
+
var SetCameraSensorsInputSchema = zod.z.object({
|
|
27089
|
+
deviceId: zod.z.number().int().nonnegative(),
|
|
27090
|
+
/** `null` removes the attachment: the camera goes back to `not-configured`. */
|
|
27091
|
+
config: CameraSensorsConfigSchema.nullable()
|
|
27092
|
+
});
|
|
26182
27093
|
var pipelineOrchestratorCapability = {
|
|
26183
27094
|
name: "pipeline-orchestrator",
|
|
26184
27095
|
scope: "system",
|
|
@@ -26644,6 +27555,18 @@ var pipelineOrchestratorCapability = {
|
|
|
26644
27555
|
*/
|
|
26645
27556
|
getCameraStatuses: method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), zod.z.array(CameraStatusSchema).readonly()),
|
|
26646
27557
|
/**
|
|
27558
|
+
* Attach sensors to a camera, or detach them with `config: null` (D672
|
|
27559
|
+
* configuration, G5/D676). Written to the orchestrator's per-device store
|
|
27560
|
+
* under the same key `applyDeviceSettingsPatch` writes, and announced as a
|
|
27561
|
+
* settings change so the camera-sensors bridge refreshes. Refused by name
|
|
27562
|
+
* — nothing written — for a device that cannot be read, does not exist,
|
|
27563
|
+
* is not a camera, or names itself as its own sensor.
|
|
27564
|
+
*/
|
|
27565
|
+
setCameraSensors: method(SetCameraSensorsInputSchema, zod.z.object({ success: zod.z.literal(true) }), {
|
|
27566
|
+
kind: "mutation",
|
|
27567
|
+
auth: "admin"
|
|
27568
|
+
}),
|
|
27569
|
+
/**
|
|
26647
27570
|
* Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
|
|
26648
27571
|
* the addon process and returns immediately with the job snapshot — the
|
|
26649
27572
|
* work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
|
|
@@ -42641,978 +43564,736 @@ var WeatherStatusSchema = zod.z.object({
|
|
|
42641
43564
|
/** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
|
|
42642
43565
|
pressureUnit: zod.z.string().nullable(),
|
|
42643
43566
|
/** Wind speed in the reported unit. Null when not provided. */
|
|
42644
|
-
windSpeed: zod.z.number().nullable(),
|
|
42645
|
-
/** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
|
|
42646
|
-
windSpeedUnit: zod.z.string().nullable(),
|
|
42647
|
-
/** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
|
|
42648
|
-
windBearing: zod.z.number().nullable(),
|
|
42649
|
-
/** Ms epoch when the slice was last updated. */
|
|
42650
|
-
lastFetchedAt: zod.z.number()
|
|
42651
|
-
});
|
|
42652
|
-
var weatherCapability = {
|
|
42653
|
-
name: "weather",
|
|
42654
|
-
scope: "device",
|
|
42655
|
-
deviceNative: true,
|
|
42656
|
-
mode: "singleton",
|
|
42657
|
-
deviceTypes: [DeviceType.Weather],
|
|
42658
|
-
methods: {},
|
|
42659
|
-
status: {
|
|
42660
|
-
schema: WeatherStatusSchema,
|
|
42661
|
-
kind: "push"
|
|
42662
|
-
},
|
|
42663
|
-
/**
|
|
42664
|
-
* Runtime-state slice — mirrored by the kernel. The UI reads the
|
|
42665
|
-
* current conditions directly from the slice on each weather push.
|
|
42666
|
-
*/
|
|
42667
|
-
runtimeState: WeatherStatusSchema,
|
|
42668
|
-
/**
|
|
42669
|
-
* Runtime-state durability: **session** — a forecast is stale the moment the hub is down; the provider re-fetches on connect.
|
|
42670
|
-
*
|
|
42671
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
42672
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
42673
|
-
*/
|
|
42674
|
-
durability: "session"
|
|
42675
|
-
};
|
|
42676
|
-
/**
|
|
42677
|
-
* Per-zone occupancy aggregation produced by the analytics frame
|
|
42678
|
-
* processor on every inference result. Covers the full combinatorial
|
|
42679
|
-
* matrix the operator UI needs: total objects everywhere, total
|
|
42680
|
-
* objects per zone, single class everywhere, single class per zone,
|
|
42681
|
-
* objects outside any zone.
|
|
42682
|
-
*
|
|
42683
|
-
* Counts are derived from the analytics tracker (tracked detections
|
|
42684
|
-
* with stable trackIds), not raw detector hits — this filters out
|
|
42685
|
-
* one-off detector flickers and gives counts that match what the user
|
|
42686
|
-
* sees on the live overlay.
|
|
42687
|
-
*
|
|
42688
|
-
* Overlap policy: a detection that intersects two zones counts in
|
|
42689
|
-
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
42690
|
-
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
42691
|
-
* `unzoned` counts only detections that landed in zero zones.
|
|
42692
|
-
*/
|
|
42693
|
-
var PerScopeBreakdownSchema = zod.z.object({
|
|
42694
|
-
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
42695
|
-
totalObjects: zod.z.number().int().nonnegative(),
|
|
42696
|
-
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
42697
|
-
byClass: zod.z.record(zod.z.string(), zod.z.number().int().nonnegative()),
|
|
42698
|
-
/**
|
|
42699
|
-
* Of `totalObjects`, how many are STANDING — objects the census holds,
|
|
42700
|
-
* present but not going anywhere (a parked car, a bin, a statue).
|
|
42701
|
-
*
|
|
42702
|
-
* The operator's question about a scene has two halves and they answer
|
|
42703
|
-
* different things: "what is parked here" is stable for hours and must not
|
|
42704
|
-
* flap, while "who is walking through" is the thing an alert is about. One
|
|
42705
|
-
* number could only serve one of them, and `totalObjects` served the first
|
|
42706
|
-
* badly — a passer-by moved it and every occupancy rule saw a change.
|
|
42707
|
-
*
|
|
42708
|
-
* `standing + transient === totalObjects`, always. The total keeps its old
|
|
42709
|
-
* meaning exactly, so no rule written against it changes behaviour.
|
|
42710
|
-
*
|
|
42711
|
-
* ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
|
|
42712
|
-
* this field cannot say, and a reader that folds absence in as 0 reports "no
|
|
42713
|
-
* parked objects here" about a camera whose lot is full.
|
|
42714
|
-
*/
|
|
42715
|
-
standingObjects: zod.z.number().int().nonnegative().optional(),
|
|
42716
|
-
/** Of `totalObjects`, how many are passing through. See `standingObjects` —
|
|
42717
|
-
* same absence rule. */
|
|
42718
|
-
transientObjects: zod.z.number().int().nonnegative().optional()
|
|
42719
|
-
});
|
|
42720
|
-
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
42721
|
-
zoneId: zod.z.string(),
|
|
42722
|
-
zoneName: zod.z.string(),
|
|
42723
|
-
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
42724
|
-
* with the per-track detail panel and live overlay. */
|
|
42725
|
-
trackIds: zod.z.array(zod.z.string()).readonly()
|
|
42726
|
-
});
|
|
42727
|
-
/**
|
|
42728
|
-
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
42729
|
-
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
42730
|
-
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
42731
|
-
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
42732
|
-
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
42733
|
-
* of flooding the live event feed.
|
|
42734
|
-
*/
|
|
42735
|
-
var StationaryObjectSchema = zod.z.object({
|
|
42736
|
-
id: zod.z.string(),
|
|
42737
|
-
className: zod.z.string(),
|
|
42738
|
-
bbox: zod.z.object({
|
|
42739
|
-
x: zod.z.number(),
|
|
42740
|
-
y: zod.z.number(),
|
|
42741
|
-
w: zod.z.number(),
|
|
42742
|
-
h: zod.z.number()
|
|
42743
|
-
}),
|
|
42744
|
-
frameWidth: zod.z.number().int().nonnegative(),
|
|
42745
|
-
frameHeight: zod.z.number().int().nonnegative(),
|
|
42746
|
-
/** When the source track was first seen. */
|
|
42747
|
-
firstSeenAt: zod.z.number().int(),
|
|
42748
|
-
/** When the object was recognised as parked (promotion time). */
|
|
42749
|
-
becameStationaryAt: zod.z.number().int(),
|
|
42750
|
-
/** Last frame a detection confirmed the object is still there. */
|
|
42751
|
-
lastConfirmedAt: zod.z.number().int(),
|
|
42752
|
-
/** Enrichment label carried from the source track (identity / plate). */
|
|
42753
|
-
label: zod.z.string().optional(),
|
|
42754
|
-
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
42755
|
-
keyFrameMediaId: zod.z.number().int().optional()
|
|
42756
|
-
});
|
|
42757
|
-
var CameraOccupancySnapshotSchema = zod.z.object({
|
|
42758
|
-
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
42759
|
-
ts: zod.z.number().int(),
|
|
42760
|
-
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
42761
|
-
frameWidth: zod.z.number().int().nonnegative(),
|
|
42762
|
-
frameHeight: zod.z.number().int().nonnegative(),
|
|
42763
|
-
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
42764
|
-
zones: zod.z.array(ZoneScopeBreakdownSchema).readonly(),
|
|
42765
|
-
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
42766
|
-
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
42767
|
-
frame: PerScopeBreakdownSchema,
|
|
42768
|
-
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
42769
|
-
unzoned: PerScopeBreakdownSchema,
|
|
42770
|
-
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
42771
|
-
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
42772
|
-
* than as repeated tracks/events. */
|
|
42773
|
-
stationaryObjects: zod.z.array(StationaryObjectSchema).readonly().optional()
|
|
42774
|
-
});
|
|
42775
|
-
/**
|
|
42776
|
-
* One camera's row in a `getCurrentSnapshotBatch` answer.
|
|
42777
|
-
*
|
|
42778
|
-
* THREE outcomes, and the single-camera method could only express two of them
|
|
42779
|
-
* because `snapshot: null` was already spoken for:
|
|
42780
|
-
*
|
|
42781
|
-
* - `read: 'read'`, `snapshot` present — the live occupancy reading.
|
|
42782
|
-
* - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
|
|
42783
|
-
* yet: no frame since boot, and no parked-object registry to hydrate from.
|
|
42784
|
-
* - `read: 'unreadable'` — the owner could not answer for this
|
|
42785
|
-
* camera. `snapshot` is null, and it does NOT mean "nothing parked here".
|
|
42786
|
-
*
|
|
42787
|
-
* Collapsing the last two is the failure this field exists to prevent: a
|
|
42788
|
-
* hydration that threw would otherwise render as an empty Stationary section,
|
|
42789
|
-
* which is a definite claim about a camera nobody could read.
|
|
42790
|
-
*/
|
|
42791
|
-
var CameraOccupancySnapshotForDeviceSchema = zod.z.object({
|
|
42792
|
-
deviceId: zod.z.number(),
|
|
42793
|
-
read: zod.z.enum(["read", "unreadable"]),
|
|
42794
|
-
snapshot: CameraOccupancySnapshotSchema.nullable()
|
|
42795
|
-
});
|
|
42796
|
-
/**
|
|
42797
|
-
* Time-series resolution. The history methods return one bucket per
|
|
42798
|
-
* step over the requested range. Smaller resolutions cost more
|
|
42799
|
-
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
42800
|
-
* for arbitrary fractional buckets.
|
|
42801
|
-
*/
|
|
42802
|
-
var HistoryResolutionEnum = zod.z.enum([
|
|
42803
|
-
"minute",
|
|
42804
|
-
"5min",
|
|
42805
|
-
"hour"
|
|
42806
|
-
]);
|
|
42807
|
-
var HistoryRangeSchema = zod.z.object({
|
|
42808
|
-
/** Range start (epoch ms, inclusive). */
|
|
42809
|
-
from: zod.z.number().int(),
|
|
42810
|
-
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
42811
|
-
to: zod.z.number().int(),
|
|
42812
|
-
resolution: HistoryResolutionEnum
|
|
42813
|
-
});
|
|
42814
|
-
var HistoryPointSchema = zod.z.object({
|
|
42815
|
-
/** Bucket midpoint (epoch ms). */
|
|
42816
|
-
ts: zod.z.number().int(),
|
|
42817
|
-
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
42818
|
-
count: zod.z.number().int().nonnegative()
|
|
42819
|
-
});
|
|
42820
|
-
/**
|
|
42821
|
-
* Zone Analytics capability — spatial occupancy aggregations over
|
|
42822
|
-
* tracked detections. Hosted by `addon-pipeline-analytics`.
|
|
42823
|
-
*
|
|
42824
|
-
* Live snapshot is mirrored into the `zone-occupancy` device-state
|
|
42825
|
-
* slice on every frame; the cap method `getCurrentSnapshot` is a
|
|
42826
|
-
* direct read for callers that don't want to subscribe.
|
|
42827
|
-
*
|
|
42828
|
-
* History methods are sourced from a 60-minute rolling in-memory
|
|
42829
|
-
* ring of frame-level samples (sub-second resolution). Queries
|
|
42830
|
-
* outside the ring window return empty arrays — durable storage
|
|
42831
|
-
* is a follow-up (Step 4b: persisted samples → SQL store).
|
|
42832
|
-
*
|
|
42833
|
-
* Every history method takes an optional `className` filter:
|
|
42834
|
-
* - omitted → bucket count includes ALL classes in scope
|
|
42835
|
-
* - present → bucket count restricted to that class
|
|
42836
|
-
*
|
|
42837
|
-
* The `getZoneHistory` / `getCameraHistory` / `getUnzonedHistory`
|
|
42838
|
-
* triple covers the three spatial scopes (per-zone, frame-wide,
|
|
42839
|
-
* outside-any-zone). Crossing them with `className?` gives the full
|
|
42840
|
-
* combinatorial coverage the operator UI requested.
|
|
42841
|
-
*
|
|
42842
|
-
* ## ⚠️ The three history methods are DEPRECATED
|
|
42843
|
-
*
|
|
42844
|
-
* They are served from a 60-MINUTE in-memory ring. It dies with the process,
|
|
42845
|
-
* so after a restart they report nothing about a night that did happen, and any
|
|
42846
|
-
* window past an hour is silently clamped — a caller cannot tell a clamp from
|
|
42847
|
-
* an empty scene. The presets in the occupancy chart existed to hide that
|
|
42848
|
-
* horizon rather than to answer a question.
|
|
42849
|
-
*
|
|
42850
|
-
* Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
|
|
42851
|
-
* instead. Those rows are already written for every camera, ~60 bytes per
|
|
42852
|
-
* (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
|
|
42853
|
-
* Spell the scope with `occupancyScope(zoneId?, className?)` — the same
|
|
42854
|
-
* function the writer uses, so the two cannot drift.
|
|
42855
|
-
*
|
|
42856
|
-
* The ring stays until these three go, and it is the only thing left reading
|
|
42857
|
-
* it. Do not add a fourth caller.
|
|
42858
|
-
*/
|
|
42859
|
-
var zoneAnalyticsCapability = {
|
|
42860
|
-
name: "zone-analytics",
|
|
42861
|
-
scope: "device",
|
|
42862
|
-
mode: "singleton",
|
|
42863
|
-
deviceTypes: [DeviceType.Camera],
|
|
42864
|
-
methods: {
|
|
42865
|
-
/** Latest computed occupancy snapshot for this camera. Null when
|
|
42866
|
-
* the analytics pipeline hasn't seen a frame for this device yet
|
|
42867
|
-
* (no inference result emitted since boot or since binding was
|
|
42868
|
-
* activated). */
|
|
42869
|
-
getCurrentSnapshot: method(zod.z.object({ deviceId: zod.z.number() }), CameraOccupancySnapshotSchema.nullable()),
|
|
42870
|
-
/**
|
|
42871
|
-
* The same snapshot, for a SET of cameras, in one round trip.
|
|
42872
|
-
*
|
|
42873
|
-
* The Events page's Stationary section polls this every 15s for every
|
|
42874
|
-
* selected camera. Fanned out client-side that is one query per camera to
|
|
42875
|
-
* read a `Map.get` at the owner — the answer costs nothing, the round trip
|
|
42876
|
-
* costs everything. Batched, N transports become one and the per-device
|
|
42877
|
-
* work is unchanged.
|
|
42878
|
-
*
|
|
42879
|
-
* Every requested deviceId gets a row, tagged `read` — see
|
|
42880
|
-
* {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
|
|
42881
|
-
* not answer for is `'unreadable'`, never an empty reading.
|
|
42882
|
-
*/
|
|
42883
|
-
getCurrentSnapshotBatch: method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).min(1).max(200) }), zod.z.array(CameraOccupancySnapshotForDeviceSchema).readonly()),
|
|
42884
|
-
/** Time-series object count inside one zone. `className` optional —
|
|
42885
|
-
* omit to count every class in the zone. */
|
|
42886
|
-
getZoneHistory: method(zod.z.object({
|
|
42887
|
-
deviceId: zod.z.number(),
|
|
42888
|
-
zoneId: zod.z.string(),
|
|
42889
|
-
className: zod.z.string().optional()
|
|
42890
|
-
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly()),
|
|
42891
|
-
/** Time-series frame-wide object count (everywhere). */
|
|
42892
|
-
getCameraHistory: method(zod.z.object({
|
|
42893
|
-
deviceId: zod.z.number(),
|
|
42894
|
-
className: zod.z.string().optional()
|
|
42895
|
-
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly()),
|
|
42896
|
-
/** Time-series count of objects outside every zone. */
|
|
42897
|
-
getUnzonedHistory: method(zod.z.object({
|
|
42898
|
-
deviceId: zod.z.number(),
|
|
42899
|
-
className: zod.z.string().optional()
|
|
42900
|
-
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly())
|
|
42901
|
-
},
|
|
42902
|
-
/**
|
|
42903
|
-
* Runtime-state slice — the latest occupancy snapshot mirrored by
|
|
42904
|
-
* the analytics frame processor on every inference result. Consumers
|
|
42905
|
-
* read via `device.state.zoneAnalytics.value` and stay in sync
|
|
42906
|
-
* automatically; the explicit `getCurrentSnapshot` cap method is
|
|
42907
|
-
* still useful for one-off polls without a subscription.
|
|
42908
|
-
*/
|
|
42909
|
-
runtimeState: CameraOccupancySnapshotSchema,
|
|
42910
|
-
/**
|
|
42911
|
-
* Runtime-state durability: **session** — per-frame analytics; with `audio-metrics` it is ~90 % of the offered write rate. Re-derived on the next frame.
|
|
42912
|
-
*
|
|
42913
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
42914
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
42915
|
-
*/
|
|
42916
|
-
durability: "session"
|
|
42917
|
-
};
|
|
42918
|
-
/**
|
|
42919
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
42920
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
42921
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
42922
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
42923
|
-
*
|
|
42924
|
-
* Extend the enum here when a new gating consumer comes online (audio
|
|
42925
|
-
* gating, alert filtering, …) — no other surface needs to change.
|
|
42926
|
-
*/
|
|
42927
|
-
var ZoneRuleStageEnum = zod.z.enum([
|
|
42928
|
-
"motion",
|
|
42929
|
-
"detection",
|
|
42930
|
-
"package"
|
|
42931
|
-
]);
|
|
42932
|
-
/**
|
|
42933
|
-
* Zone rules capability — per-camera CRUD over the {@link ZoneRule}
|
|
42934
|
-
* arrays that decide how each pipeline stage uses the polygon zones.
|
|
42935
|
-
*
|
|
42936
|
-
* Hosted by `addon-pipeline-orchestrator` alongside the zones provider
|
|
42937
|
-
* so the operator has a single hub-side source of truth for both
|
|
42938
|
-
* geometry and behaviour. Per-stage rules are stored under the
|
|
42939
|
-
* `zoneRules.<stage>` key in the orchestrator's per-device store and
|
|
42940
|
-
* mirrored to the device-state slice `<stage>-zone-rules` on every
|
|
42941
|
-
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
42942
|
-
* subscribe to that slice and refresh their gating without
|
|
42943
|
-
* round-tripping the cap.
|
|
42944
|
-
*
|
|
42945
|
-
* Sets are bulk-replace — the operator UI sends the new rule list
|
|
42946
|
-
* wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
|
|
42947
|
-
* naturally in the rule editor without per-rule mutation chatter.
|
|
42948
|
-
*/
|
|
42949
|
-
var zoneRulesCapability = {
|
|
42950
|
-
name: "zone-rules",
|
|
42951
|
-
scope: "device",
|
|
42952
|
-
mode: "singleton",
|
|
42953
|
-
deviceTypes: [DeviceType.Camera],
|
|
42954
|
-
methods: {
|
|
42955
|
-
/** Read the full rule list for a given stage (empty when no rules
|
|
42956
|
-
* are defined yet). */
|
|
42957
|
-
listRules: method(zod.z.object({
|
|
42958
|
-
deviceId: zod.z.number(),
|
|
42959
|
-
stage: ZoneRuleStageEnum
|
|
42960
|
-
}), zod.z.array(ZoneRuleSchema).readonly()),
|
|
42961
|
-
/** Bulk-replace the rule list for one stage. The provider validates
|
|
42962
|
-
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
42963
|
-
* thresholds in range) and rejects the whole patch if any entry
|
|
42964
|
-
* is invalid — partial writes are a configuration footgun. */
|
|
42965
|
-
setRules: method(zod.z.object({
|
|
42966
|
-
deviceId: zod.z.number(),
|
|
42967
|
-
stage: ZoneRuleStageEnum,
|
|
42968
|
-
rules: zod.z.array(ZoneRuleSchema).readonly()
|
|
42969
|
-
}), zod.z.void(), {
|
|
42970
|
-
kind: "mutation",
|
|
42971
|
-
auth: "admin"
|
|
42972
|
-
})
|
|
43567
|
+
windSpeed: zod.z.number().nullable(),
|
|
43568
|
+
/** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
|
|
43569
|
+
windSpeedUnit: zod.z.string().nullable(),
|
|
43570
|
+
/** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
|
|
43571
|
+
windBearing: zod.z.number().nullable(),
|
|
43572
|
+
/** Ms epoch when the slice was last updated. */
|
|
43573
|
+
lastFetchedAt: zod.z.number()
|
|
43574
|
+
});
|
|
43575
|
+
var weatherCapability = {
|
|
43576
|
+
name: "weather",
|
|
43577
|
+
scope: "device",
|
|
43578
|
+
deviceNative: true,
|
|
43579
|
+
mode: "singleton",
|
|
43580
|
+
deviceTypes: [DeviceType.Weather],
|
|
43581
|
+
methods: {},
|
|
43582
|
+
status: {
|
|
43583
|
+
schema: WeatherStatusSchema,
|
|
43584
|
+
kind: "push"
|
|
42973
43585
|
},
|
|
42974
43586
|
/**
|
|
42975
|
-
* Runtime-state slice —
|
|
42976
|
-
*
|
|
42977
|
-
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
42978
|
-
* `{motion, detection, package}` shape, so subscribers always get the
|
|
42979
|
-
* complete current set. Consumers that only care about one stage
|
|
42980
|
-
* just read the matching property.
|
|
42981
|
-
*
|
|
42982
|
-
* `package` backs the package-drop detector — a package zone is a
|
|
42983
|
-
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
42984
|
-
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
42985
|
-
* The orchestrator provider writes this stage as a first-class slice
|
|
42986
|
-
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
42987
|
-
* package}` shape, so consumers read the current package rules directly
|
|
42988
|
-
* off `device.state.zoneRules.value.package`.
|
|
43587
|
+
* Runtime-state slice — mirrored by the kernel. The UI reads the
|
|
43588
|
+
* current conditions directly from the slice on each weather push.
|
|
42989
43589
|
*/
|
|
42990
|
-
runtimeState:
|
|
42991
|
-
motion: zod.z.array(ZoneRuleSchema).readonly(),
|
|
42992
|
-
detection: zod.z.array(ZoneRuleSchema).readonly(),
|
|
42993
|
-
package: zod.z.array(ZoneRuleSchema).readonly()
|
|
42994
|
-
}),
|
|
43590
|
+
runtimeState: WeatherStatusSchema,
|
|
42995
43591
|
/**
|
|
42996
|
-
* Runtime-state durability: **
|
|
43592
|
+
* Runtime-state durability: **session** — a forecast is stale the moment the hub is down; the provider re-fetches on connect.
|
|
42997
43593
|
*
|
|
42998
43594
|
* See `RuntimeStateDurability`. Enforced by
|
|
42999
43595
|
* `scripts/check-runtime-state-durability.ts`.
|
|
43000
43596
|
*/
|
|
43001
|
-
durability: "
|
|
43597
|
+
durability: "session"
|
|
43002
43598
|
};
|
|
43003
|
-
function isValueOf(slot, value) {
|
|
43004
|
-
return value.slot === slot;
|
|
43005
|
-
}
|
|
43006
|
-
/** Bind slot `slot` to its owner's functions. See the module docblock. */
|
|
43007
|
-
function defineExtensionSlotProvider(slot, impl) {
|
|
43008
|
-
return {
|
|
43009
|
-
slot,
|
|
43010
|
-
list: async (deviceId) => {
|
|
43011
|
-
const listing = await impl.list(deviceId);
|
|
43012
|
-
if (listing === null) return null;
|
|
43013
|
-
const value = listing.value;
|
|
43014
|
-
return {
|
|
43015
|
-
...listing,
|
|
43016
|
-
value
|
|
43017
|
-
};
|
|
43018
|
-
},
|
|
43019
|
-
set: async (deviceId, value) => {
|
|
43020
|
-
if (!isValueOf(slot, value)) throw new Error(`device-extension: a "${value.slot}" value was sent to slot "${slot}" — refused`);
|
|
43021
|
-
return await impl.set(deviceId, value);
|
|
43022
|
-
},
|
|
43023
|
-
candidates: (deviceId) => impl.candidates(deviceId)
|
|
43024
|
-
};
|
|
43025
|
-
}
|
|
43026
|
-
/** `<source>:<slotId>`. */
|
|
43027
|
-
function extensionSlotRef(source, slot) {
|
|
43028
|
-
return `${source}:${slot}`;
|
|
43029
|
-
}
|
|
43030
|
-
/** Build the `device-extension` provider one owner registers. */
|
|
43031
|
-
function createDeviceExtensionProvider(options) {
|
|
43032
|
-
const { source, label, slots, hostTypeOf, logger } = options;
|
|
43033
|
-
const offered = (slot, hostType) => hostType !== null && EXTENSION_SLOTS[slot].hostDeviceTypes.some((t) => t === hostType);
|
|
43034
|
-
const resolve = async (deviceId, slotRef) => {
|
|
43035
|
-
const prefix = `${source}:`;
|
|
43036
|
-
const slot = slotRef.startsWith(prefix) ? slots.find((s) => s.slot === slotRef.slice(prefix.length)) : void 0;
|
|
43037
|
-
if (slot === void 0) throw new Error(`device-extension: "${slotRef}" is not a slot of "${source}" — it serves ${slots.map((s) => extensionSlotRef(source, s.slot)).join(", ") || "(none)"}`);
|
|
43038
|
-
const hostType = await hostTypeOf(deviceId);
|
|
43039
|
-
if (!offered(slot.slot, hostType)) throw new Error(`device-extension: slot "${slot.slot}" is not offered on device ${deviceId} (type ${hostType ?? "unknown"}; offered on ${EXTENSION_SLOTS[slot.slot].hostDeviceTypes.join(", ")})`);
|
|
43040
|
-
return slot;
|
|
43041
|
-
};
|
|
43042
|
-
return {
|
|
43043
|
-
listSources: async () => [{
|
|
43044
|
-
source,
|
|
43045
|
-
label,
|
|
43046
|
-
availability: { state: "ok" }
|
|
43047
|
-
}],
|
|
43048
|
-
listSlots: async (input) => {
|
|
43049
|
-
const hostType = await hostTypeOf(input.deviceId);
|
|
43050
|
-
const rows = [];
|
|
43051
|
-
for (const slot of slots) {
|
|
43052
|
-
if (!offered(slot.slot, hostType)) continue;
|
|
43053
|
-
const listing = await slot.list(input.deviceId);
|
|
43054
|
-
if (listing === null) continue;
|
|
43055
|
-
rows.push({
|
|
43056
|
-
state: "ok",
|
|
43057
|
-
slotRef: extensionSlotRef(source, slot.slot),
|
|
43058
|
-
slot: slot.slot,
|
|
43059
|
-
authority: listing.authority,
|
|
43060
|
-
activation: listing.activation,
|
|
43061
|
-
value: listing.value,
|
|
43062
|
-
sources: [...listing.sources],
|
|
43063
|
-
...listing.impliedSources !== void 0 ? { impliedSources: [...listing.impliedSources] } : {}
|
|
43064
|
-
});
|
|
43065
|
-
}
|
|
43066
|
-
return rows;
|
|
43067
|
-
},
|
|
43068
|
-
listCandidates: async (input) => {
|
|
43069
|
-
return { candidates: [...await (await resolve(input.deviceId, input.slotRef)).candidates(input.deviceId)] };
|
|
43070
|
-
},
|
|
43071
|
-
setSlot: async (input) => {
|
|
43072
|
-
const result = await (await resolve(input.deviceId, input.slotRef)).set(input.deviceId, input.value);
|
|
43073
|
-
logger.info("device-extension: slot written", {
|
|
43074
|
-
tags: { deviceId: input.deviceId },
|
|
43075
|
-
meta: {
|
|
43076
|
-
slotRef: input.slotRef,
|
|
43077
|
-
reload: result.reload
|
|
43078
|
-
}
|
|
43079
|
-
});
|
|
43080
|
-
return result;
|
|
43081
|
-
}
|
|
43082
|
-
};
|
|
43083
|
-
}
|
|
43084
43599
|
/**
|
|
43085
|
-
*
|
|
43086
|
-
*
|
|
43087
|
-
*
|
|
43088
|
-
*
|
|
43089
|
-
*
|
|
43090
|
-
*
|
|
43091
|
-
* The graph is built for ONE save: the CANDIDATE composition (the one being
|
|
43092
|
-
* validated) plus every STORED composition. The candidate replaces its own old
|
|
43093
|
-
* version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
|
|
43094
|
-
* introduces or removes a cycle must never depend on array order. A DIFFERENT
|
|
43095
|
-
* block targeting the candidate's device is refused by name: one composition
|
|
43096
|
-
* per device (D663). Two different stored peers that (wrongly) target the same
|
|
43097
|
-
* device are corrupted state, not a cycle to resolve by picking one silently;
|
|
43098
|
-
* that is refused by name too (D391).
|
|
43099
|
-
*
|
|
43100
|
-
* A customization of an EXISTING device reads that device's NATIVE value for a
|
|
43101
|
-
* field it replaces or does not write ("Invert state" reads the `on` it
|
|
43102
|
-
* replaces): such a self-read is no edge, so it is never a cycle (D663).
|
|
43103
|
-
* Another composition reading that field reads the customizer's output.
|
|
43600
|
+
* Per-zone occupancy aggregation produced by the analytics frame
|
|
43601
|
+
* processor on every inference result. Covers the full combinatorial
|
|
43602
|
+
* matrix the operator UI needs: total objects everywhere, total
|
|
43603
|
+
* objects per zone, single class everywhere, single class per zone,
|
|
43604
|
+
* objects outside any zone.
|
|
43104
43605
|
*
|
|
43105
|
-
*
|
|
43106
|
-
*
|
|
43107
|
-
*
|
|
43108
|
-
*
|
|
43109
|
-
* strongly connected component (`findCandidateCycles`, C-1). A dotted
|
|
43110
|
-
* `fieldPath` is matched only as an exact string: a write to `state.value` and
|
|
43111
|
-
* a read of `state` would not connect (no nested field writes exist yet).
|
|
43606
|
+
* Counts are derived from the analytics tracker (tracked detections
|
|
43607
|
+
* with stable trackIds), not raw detector hits — this filters out
|
|
43608
|
+
* one-off detector flickers and gives counts that match what the user
|
|
43609
|
+
* sees on the live overlay.
|
|
43112
43610
|
*
|
|
43113
|
-
*
|
|
43114
|
-
*
|
|
43115
|
-
*
|
|
43116
|
-
*
|
|
43117
|
-
*/
|
|
43118
|
-
/** Two blocks' compositions target the same device — refused by name, never silently resolved. */
|
|
43119
|
-
var CompositionGraphError = class extends Error {};
|
|
43120
|
-
/** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
|
|
43121
|
-
function writtenFields(peer) {
|
|
43122
|
-
return new Set(peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
|
|
43123
|
-
}
|
|
43124
|
-
/** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
|
|
43125
|
-
function readsComposedValue(peer, written, read) {
|
|
43126
|
-
if (compositionSourceKey(read.source) !== compositionSourceKey(peer.target)) return true;
|
|
43127
|
-
const field = `${read.cap}.${read.fieldPath}`;
|
|
43128
|
-
return written.has(field) && peer.replaced?.has(field) !== true;
|
|
43129
|
-
}
|
|
43130
|
-
function peerEdges(peer) {
|
|
43131
|
-
const written = writtenFields(peer);
|
|
43132
|
-
return peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
|
|
43133
|
-
source: peer.target,
|
|
43134
|
-
cap: feature.cap,
|
|
43135
|
-
fieldPath
|
|
43136
|
-
}), fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(compositionReadKey)]));
|
|
43137
|
-
}
|
|
43138
|
-
/**
|
|
43139
|
-
* Every peer that actually takes part in the graph: each stored peer minus the
|
|
43140
|
-
* candidate's own old version (same block id — the drop happens before any
|
|
43141
|
-
* edge is built, so its old edges never enter the map), plus the candidate
|
|
43142
|
-
* itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
|
|
43143
|
-
* block targets the candidate's device, or two stored peers share a device.
|
|
43611
|
+
* Overlap policy: a detection that intersects two zones counts in
|
|
43612
|
+
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
43613
|
+
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
43614
|
+
* `unzoned` counts only detections that landed in zero zones.
|
|
43144
43615
|
*/
|
|
43145
|
-
|
|
43146
|
-
|
|
43147
|
-
|
|
43148
|
-
|
|
43149
|
-
|
|
43150
|
-
|
|
43151
|
-
|
|
43152
|
-
|
|
43153
|
-
|
|
43154
|
-
|
|
43155
|
-
|
|
43156
|
-
|
|
43157
|
-
|
|
43158
|
-
|
|
43159
|
-
|
|
43160
|
-
|
|
43161
|
-
|
|
43162
|
-
|
|
43163
|
-
|
|
43164
|
-
|
|
43165
|
-
|
|
43166
|
-
|
|
43167
|
-
|
|
43168
|
-
|
|
43169
|
-
|
|
43170
|
-
|
|
43171
|
-
|
|
43172
|
-
|
|
43173
|
-
|
|
43174
|
-
|
|
43175
|
-
|
|
43176
|
-
|
|
43177
|
-
|
|
43178
|
-
|
|
43179
|
-
const component = [];
|
|
43180
|
-
for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
|
|
43181
|
-
onStack.delete(member);
|
|
43182
|
-
component.push(member);
|
|
43183
|
-
if (member === node) break;
|
|
43184
|
-
}
|
|
43185
|
-
components.push(component);
|
|
43186
|
-
};
|
|
43187
|
-
for (const node of edges.keys()) if (!index.has(node)) connect(node);
|
|
43188
|
-
return components;
|
|
43189
|
-
}
|
|
43190
|
-
/** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
|
|
43191
|
-
function cycleThrough(start, component, edges) {
|
|
43192
|
-
const parent = /* @__PURE__ */ new Map();
|
|
43193
|
-
const queue = [start];
|
|
43194
|
-
for (let head = 0; head < queue.length; head += 1) {
|
|
43195
|
-
const node = queue[head];
|
|
43196
|
-
if (node === void 0) break;
|
|
43197
|
-
for (const next of edges.get(node) ?? []) {
|
|
43198
|
-
if (next === start) {
|
|
43199
|
-
const chain = [];
|
|
43200
|
-
for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
|
|
43201
|
-
return [
|
|
43202
|
-
start,
|
|
43203
|
-
...chain.reverse(),
|
|
43204
|
-
start
|
|
43205
|
-
];
|
|
43206
|
-
}
|
|
43207
|
-
if (!component.has(next) || parent.has(next)) continue;
|
|
43208
|
-
parent.set(next, node);
|
|
43209
|
-
queue.push(next);
|
|
43210
|
-
}
|
|
43211
|
-
}
|
|
43212
|
-
return [start, start];
|
|
43213
|
-
}
|
|
43616
|
+
var PerScopeBreakdownSchema = zod.z.object({
|
|
43617
|
+
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
43618
|
+
totalObjects: zod.z.number().int().nonnegative(),
|
|
43619
|
+
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
43620
|
+
byClass: zod.z.record(zod.z.string(), zod.z.number().int().nonnegative()),
|
|
43621
|
+
/**
|
|
43622
|
+
* Of `totalObjects`, how many are STANDING — objects the census holds,
|
|
43623
|
+
* present but not going anywhere (a parked car, a bin, a statue).
|
|
43624
|
+
*
|
|
43625
|
+
* The operator's question about a scene has two halves and they answer
|
|
43626
|
+
* different things: "what is parked here" is stable for hours and must not
|
|
43627
|
+
* flap, while "who is walking through" is the thing an alert is about. One
|
|
43628
|
+
* number could only serve one of them, and `totalObjects` served the first
|
|
43629
|
+
* badly — a passer-by moved it and every occupancy rule saw a change.
|
|
43630
|
+
*
|
|
43631
|
+
* `standing + transient === totalObjects`, always. The total keeps its old
|
|
43632
|
+
* meaning exactly, so no rule written against it changes behaviour.
|
|
43633
|
+
*
|
|
43634
|
+
* ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
|
|
43635
|
+
* this field cannot say, and a reader that folds absence in as 0 reports "no
|
|
43636
|
+
* parked objects here" about a camera whose lot is full.
|
|
43637
|
+
*/
|
|
43638
|
+
standingObjects: zod.z.number().int().nonnegative().optional(),
|
|
43639
|
+
/** Of `totalObjects`, how many are passing through. See `standingObjects` —
|
|
43640
|
+
* same absence rule. */
|
|
43641
|
+
transientObjects: zod.z.number().int().nonnegative().optional()
|
|
43642
|
+
});
|
|
43643
|
+
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
43644
|
+
zoneId: zod.z.string(),
|
|
43645
|
+
zoneName: zod.z.string(),
|
|
43646
|
+
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
43647
|
+
* with the per-track detail panel and live overlay. */
|
|
43648
|
+
trackIds: zod.z.array(zod.z.string()).readonly()
|
|
43649
|
+
});
|
|
43214
43650
|
/**
|
|
43215
|
-
*
|
|
43216
|
-
*
|
|
43217
|
-
*
|
|
43218
|
-
*
|
|
43219
|
-
*
|
|
43220
|
-
*
|
|
43651
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
43652
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
43653
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
43654
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
43655
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
43656
|
+
* of flooding the live event feed.
|
|
43221
43657
|
*/
|
|
43222
|
-
|
|
43223
|
-
|
|
43224
|
-
|
|
43225
|
-
|
|
43226
|
-
|
|
43227
|
-
|
|
43228
|
-
|
|
43229
|
-
|
|
43230
|
-
|
|
43231
|
-
|
|
43232
|
-
|
|
43233
|
-
|
|
43234
|
-
|
|
43235
|
-
|
|
43236
|
-
|
|
43237
|
-
|
|
43238
|
-
|
|
43239
|
-
|
|
43240
|
-
|
|
43241
|
-
|
|
43242
|
-
|
|
43243
|
-
}
|
|
43244
|
-
|
|
43245
|
-
|
|
43246
|
-
|
|
43247
|
-
|
|
43248
|
-
|
|
43249
|
-
|
|
43250
|
-
|
|
43251
|
-
|
|
43252
|
-
/**
|
|
43253
|
-
|
|
43254
|
-
|
|
43255
|
-
|
|
43256
|
-
|
|
43257
|
-
|
|
43258
|
-
|
|
43259
|
-
|
|
43260
|
-
|
|
43261
|
-
var BOOLEAN = {
|
|
43262
|
-
kind: "fixed",
|
|
43263
|
-
result: ["boolean"]
|
|
43264
|
-
};
|
|
43265
|
-
/** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
|
|
43266
|
-
var BUILTIN_RESULT_KINDS = Object.freeze({
|
|
43267
|
-
min: NUMBER,
|
|
43268
|
-
max: NUMBER,
|
|
43269
|
-
abs: NUMBER,
|
|
43270
|
-
floor: NUMBER,
|
|
43271
|
-
ceil: NUMBER,
|
|
43272
|
-
sqrt: NUMBER,
|
|
43273
|
-
round: NUMBER,
|
|
43274
|
-
pow: NUMBER,
|
|
43275
|
-
clamp: NUMBER,
|
|
43276
|
-
avg: NUMBER,
|
|
43277
|
-
sum: NUMBER,
|
|
43278
|
-
age: NUMBER,
|
|
43279
|
-
convert: NUMBER,
|
|
43280
|
-
coalesce: { kind: "coalesce" },
|
|
43281
|
-
any: BOOLEAN,
|
|
43282
|
-
all: BOOLEAN,
|
|
43283
|
-
within: BOOLEAN,
|
|
43284
|
-
latest: {
|
|
43285
|
-
kind: "fixed",
|
|
43286
|
-
result: ["number", "null"]
|
|
43287
|
-
},
|
|
43288
|
-
number: {
|
|
43289
|
-
kind: "fixed",
|
|
43290
|
-
result: ["number", "null"]
|
|
43291
|
-
},
|
|
43292
|
-
rose: {
|
|
43293
|
-
kind: "fixed",
|
|
43294
|
-
result: ["number", "null"]
|
|
43295
|
-
},
|
|
43296
|
-
count: NUMBER
|
|
43658
|
+
var StationaryObjectSchema = zod.z.object({
|
|
43659
|
+
id: zod.z.string(),
|
|
43660
|
+
className: zod.z.string(),
|
|
43661
|
+
bbox: zod.z.object({
|
|
43662
|
+
x: zod.z.number(),
|
|
43663
|
+
y: zod.z.number(),
|
|
43664
|
+
w: zod.z.number(),
|
|
43665
|
+
h: zod.z.number()
|
|
43666
|
+
}),
|
|
43667
|
+
frameWidth: zod.z.number().int().nonnegative(),
|
|
43668
|
+
frameHeight: zod.z.number().int().nonnegative(),
|
|
43669
|
+
/** When the source track was first seen. */
|
|
43670
|
+
firstSeenAt: zod.z.number().int(),
|
|
43671
|
+
/** When the object was recognised as parked (promotion time). */
|
|
43672
|
+
becameStationaryAt: zod.z.number().int(),
|
|
43673
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
43674
|
+
lastConfirmedAt: zod.z.number().int(),
|
|
43675
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
43676
|
+
label: zod.z.string().optional(),
|
|
43677
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
43678
|
+
keyFrameMediaId: zod.z.number().int().optional()
|
|
43679
|
+
});
|
|
43680
|
+
var CameraOccupancySnapshotSchema = zod.z.object({
|
|
43681
|
+
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
43682
|
+
ts: zod.z.number().int(),
|
|
43683
|
+
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
43684
|
+
frameWidth: zod.z.number().int().nonnegative(),
|
|
43685
|
+
frameHeight: zod.z.number().int().nonnegative(),
|
|
43686
|
+
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
43687
|
+
zones: zod.z.array(ZoneScopeBreakdownSchema).readonly(),
|
|
43688
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
43689
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
43690
|
+
frame: PerScopeBreakdownSchema,
|
|
43691
|
+
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
43692
|
+
unzoned: PerScopeBreakdownSchema,
|
|
43693
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
43694
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
43695
|
+
* than as repeated tracks/events. */
|
|
43696
|
+
stationaryObjects: zod.z.array(StationaryObjectSchema).readonly().optional()
|
|
43297
43697
|
});
|
|
43298
|
-
var KindInferenceError = class extends Error {};
|
|
43299
|
-
function kindOfValue(value) {
|
|
43300
|
-
if (value === null) return "null";
|
|
43301
|
-
if (typeof value === "number") return "number";
|
|
43302
|
-
if (typeof value === "string") return "string";
|
|
43303
|
-
return "boolean";
|
|
43304
|
-
}
|
|
43305
|
-
function only(kind) {
|
|
43306
|
-
return new Set([kind]);
|
|
43307
|
-
}
|
|
43308
|
-
function union(...sets) {
|
|
43309
|
-
return new Set(sets.flatMap((s) => [...s]));
|
|
43310
|
-
}
|
|
43311
|
-
function withoutNull(set) {
|
|
43312
|
-
return new Set([...set].filter((k) => k !== "null"));
|
|
43313
|
-
}
|
|
43314
|
-
function binaryKinds(op, left, right) {
|
|
43315
|
-
if (op === "+") {
|
|
43316
|
-
const all = [...left, ...right];
|
|
43317
|
-
if (all.every((k) => k === "string")) return only("string");
|
|
43318
|
-
if (all.every((k) => k === "number")) return only("number");
|
|
43319
|
-
return new Set(["number", "string"]);
|
|
43320
|
-
}
|
|
43321
|
-
if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
|
|
43322
|
-
return only("boolean");
|
|
43323
|
-
}
|
|
43324
|
-
/** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
|
|
43325
|
-
function arityRange(builtin) {
|
|
43326
|
-
if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
|
|
43327
|
-
if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
|
|
43328
|
-
return `${builtin.minArgs}–${builtin.maxArgs}`;
|
|
43329
|
-
}
|
|
43330
|
-
function callKinds(callee, args) {
|
|
43331
|
-
const builtin = EXPRESSION_BUILTINS[callee];
|
|
43332
|
-
if (builtin !== void 0 && (args.length < builtin.minArgs || args.length > builtin.maxArgs)) throw new KindInferenceError(`${callee}: wrong number of arguments (${args.length}); expected ${arityRange(builtin)}`);
|
|
43333
|
-
const rule = BUILTIN_RESULT_KINDS[callee];
|
|
43334
|
-
if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
|
|
43335
|
-
if (rule.kind === "fixed") return new Set(rule.result);
|
|
43336
|
-
const nonNull = union(...args.map(withoutNull));
|
|
43337
|
-
return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
|
|
43338
|
-
}
|
|
43339
|
-
function kindsOf(node, bindings) {
|
|
43340
|
-
switch (node.kind) {
|
|
43341
|
-
case "literal": return only(kindOfValue(node.value));
|
|
43342
|
-
case "identifier": {
|
|
43343
|
-
if (node.name === "now") return only("number");
|
|
43344
|
-
const bound = bindings.get(node.name);
|
|
43345
|
-
if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
|
|
43346
|
-
return bound;
|
|
43347
|
-
}
|
|
43348
|
-
case "unary": return only(node.op === "!" ? "boolean" : "number");
|
|
43349
|
-
case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
43350
|
-
case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
43351
|
-
case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
|
|
43352
|
-
case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
|
|
43353
|
-
}
|
|
43354
|
-
}
|
|
43355
|
-
function nodeContainsCall(node, callee) {
|
|
43356
|
-
switch (node.kind) {
|
|
43357
|
-
case "literal":
|
|
43358
|
-
case "identifier": return false;
|
|
43359
|
-
case "unary": return nodeContainsCall(node.operand, callee);
|
|
43360
|
-
case "binary":
|
|
43361
|
-
case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
|
|
43362
|
-
case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
|
|
43363
|
-
case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
|
|
43364
|
-
}
|
|
43365
|
-
}
|
|
43366
|
-
function expressionContainsBuiltinCall(expr, callee) {
|
|
43367
|
-
const compiled = compileExpressionSafe(expr);
|
|
43368
|
-
return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
|
|
43369
|
-
}
|
|
43370
|
-
function inferExpressionKinds(input) {
|
|
43371
|
-
const compiled = compileExpressionSafe(input.expr);
|
|
43372
|
-
if (!compiled.ok) return {
|
|
43373
|
-
ok: false,
|
|
43374
|
-
error: compiled.error
|
|
43375
|
-
};
|
|
43376
|
-
try {
|
|
43377
|
-
return {
|
|
43378
|
-
ok: true,
|
|
43379
|
-
kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
|
|
43380
|
-
};
|
|
43381
|
-
} catch (err) {
|
|
43382
|
-
if (err instanceof KindInferenceError) return {
|
|
43383
|
-
ok: false,
|
|
43384
|
-
error: err.message
|
|
43385
|
-
};
|
|
43386
|
-
throw err;
|
|
43387
|
-
}
|
|
43388
|
-
}
|
|
43389
43698
|
/**
|
|
43390
|
-
*
|
|
43391
|
-
*
|
|
43392
|
-
*
|
|
43393
|
-
*
|
|
43394
|
-
*
|
|
43699
|
+
* One camera's row in a `getCurrentSnapshotBatch` answer.
|
|
43700
|
+
*
|
|
43701
|
+
* THREE outcomes, and the single-camera method could only express two of them
|
|
43702
|
+
* because `snapshot: null` was already spoken for:
|
|
43703
|
+
*
|
|
43704
|
+
* - `read: 'read'`, `snapshot` present — the live occupancy reading.
|
|
43705
|
+
* - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
|
|
43706
|
+
* yet: no frame since boot, and no parked-object registry to hydrate from.
|
|
43707
|
+
* - `read: 'unreadable'` — the owner could not answer for this
|
|
43708
|
+
* camera. `snapshot` is null, and it does NOT mean "nothing parked here".
|
|
43709
|
+
*
|
|
43710
|
+
* Collapsing the last two is the failure this field exists to prevent: a
|
|
43711
|
+
* hydration that threw would otherwise render as an empty Stationary section,
|
|
43712
|
+
* which is a definite claim about a camera nobody could read.
|
|
43395
43713
|
*/
|
|
43396
|
-
|
|
43397
|
-
|
|
43398
|
-
|
|
43399
|
-
|
|
43400
|
-
|
|
43401
|
-
code: "source-field-unknown",
|
|
43402
|
-
message: `no capability \`${read.cap}\` with state to read`
|
|
43403
|
-
};
|
|
43404
|
-
const field = describeStateField(schema, read.fieldPath);
|
|
43405
|
-
if (field === null) return {
|
|
43406
|
-
ok: false,
|
|
43407
|
-
code: "source-field-unknown",
|
|
43408
|
-
message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
|
|
43409
|
-
};
|
|
43410
|
-
const base = field.kind === "enum" ? "string" : field.kind;
|
|
43411
|
-
return {
|
|
43412
|
-
ok: true,
|
|
43413
|
-
kinds: new Set(field.nullable ? [base, "null"] : [base])
|
|
43414
|
-
};
|
|
43415
|
-
}
|
|
43416
|
-
function sourceKinds(source, lookupCap) {
|
|
43417
|
-
if (source.kind === "from") return readKinds(source, lookupCap);
|
|
43418
|
-
if (source.kind === "code") return {
|
|
43419
|
-
ok: false,
|
|
43420
|
-
code: "reserved-variant",
|
|
43421
|
-
message: "code fields arrive in slice 4"
|
|
43422
|
-
};
|
|
43423
|
-
const bindingKinds = /* @__PURE__ */ new Map();
|
|
43424
|
-
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
43425
|
-
if (binding.kind === "literal") {
|
|
43426
|
-
bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
|
|
43427
|
-
continue;
|
|
43428
|
-
}
|
|
43429
|
-
const kinds = readKinds(binding, lookupCap);
|
|
43430
|
-
if (!kinds.ok) return {
|
|
43431
|
-
...kinds,
|
|
43432
|
-
message: `binding \`${name}\`: ${kinds.message}`
|
|
43433
|
-
};
|
|
43434
|
-
bindingKinds.set(name, kinds.kinds);
|
|
43435
|
-
}
|
|
43436
|
-
const inferred = inferExpressionKinds({
|
|
43437
|
-
expr: source.expr,
|
|
43438
|
-
bindingKinds
|
|
43439
|
-
});
|
|
43440
|
-
return inferred.ok ? {
|
|
43441
|
-
ok: true,
|
|
43442
|
-
kinds: inferred.kinds
|
|
43443
|
-
} : {
|
|
43444
|
-
ok: false,
|
|
43445
|
-
code: "expression-invalid",
|
|
43446
|
-
message: inferred.error
|
|
43447
|
-
};
|
|
43448
|
-
}
|
|
43449
|
-
function compatibility(cap, target, kinds, at) {
|
|
43450
|
-
const allowed = target.kind === "enum" ? "string" : target.kind;
|
|
43451
|
-
const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
|
|
43452
|
-
const problems = [];
|
|
43453
|
-
if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
|
|
43454
|
-
if (kinds.has("null") && !target.nullable) problems.push(problem("may-be-null", at, `\`${cap}.${target.path}\` cannot be null and this source can produce null — wrap it in coalesce(…) or pick a non-null source`));
|
|
43455
|
-
return problems;
|
|
43456
|
-
}
|
|
43714
|
+
var CameraOccupancySnapshotForDeviceSchema = zod.z.object({
|
|
43715
|
+
deviceId: zod.z.number(),
|
|
43716
|
+
read: zod.z.enum(["read", "unreadable"]),
|
|
43717
|
+
snapshot: CameraOccupancySnapshotSchema.nullable()
|
|
43718
|
+
});
|
|
43457
43719
|
/**
|
|
43458
|
-
*
|
|
43459
|
-
*
|
|
43460
|
-
*
|
|
43461
|
-
*
|
|
43462
|
-
* expression) is NOT checked against the target's `enumValues` here: the
|
|
43463
|
-
* kind inference already collapsed it to the coarse `'string'` kind, losing
|
|
43464
|
-
* whichever enum it actually came from, so membership can only be judged at
|
|
43465
|
-
* write time against the runtime-state schema. Known limitation — see the
|
|
43466
|
-
* fix report for Task 6 round 1.
|
|
43720
|
+
* Time-series resolution. The history methods return one bucket per
|
|
43721
|
+
* step over the requested range. Smaller resolutions cost more
|
|
43722
|
+
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
43723
|
+
* for arbitrary fractional buckets.
|
|
43467
43724
|
*/
|
|
43468
|
-
|
|
43469
|
-
|
|
43470
|
-
|
|
43471
|
-
|
|
43472
|
-
|
|
43473
|
-
|
|
43474
|
-
|
|
43725
|
+
var HistoryResolutionEnum = zod.z.enum([
|
|
43726
|
+
"minute",
|
|
43727
|
+
"5min",
|
|
43728
|
+
"hour"
|
|
43729
|
+
]);
|
|
43730
|
+
var HistoryRangeSchema = zod.z.object({
|
|
43731
|
+
/** Range start (epoch ms, inclusive). */
|
|
43732
|
+
from: zod.z.number().int(),
|
|
43733
|
+
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
43734
|
+
to: zod.z.number().int(),
|
|
43735
|
+
resolution: HistoryResolutionEnum
|
|
43736
|
+
});
|
|
43737
|
+
var HistoryPointSchema = zod.z.object({
|
|
43738
|
+
/** Bucket midpoint (epoch ms). */
|
|
43739
|
+
ts: zod.z.number().int(),
|
|
43740
|
+
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
43741
|
+
count: zod.z.number().int().nonnegative()
|
|
43742
|
+
});
|
|
43475
43743
|
/**
|
|
43476
|
-
*
|
|
43477
|
-
*
|
|
43478
|
-
*
|
|
43479
|
-
*
|
|
43744
|
+
* Zone Analytics capability — spatial occupancy aggregations over
|
|
43745
|
+
* tracked detections. Hosted by `addon-pipeline-analytics`.
|
|
43746
|
+
*
|
|
43747
|
+
* Live snapshot is mirrored into the `zone-occupancy` device-state
|
|
43748
|
+
* slice on every frame; the cap method `getCurrentSnapshot` is a
|
|
43749
|
+
* direct read for callers that don't want to subscribe.
|
|
43750
|
+
*
|
|
43751
|
+
* History methods are sourced from a 60-minute rolling in-memory
|
|
43752
|
+
* ring of frame-level samples (sub-second resolution). Queries
|
|
43753
|
+
* outside the ring window return empty arrays — durable storage
|
|
43754
|
+
* is a follow-up (Step 4b: persisted samples → SQL store).
|
|
43755
|
+
*
|
|
43756
|
+
* Every history method takes an optional `className` filter:
|
|
43757
|
+
* - omitted → bucket count includes ALL classes in scope
|
|
43758
|
+
* - present → bucket count restricted to that class
|
|
43759
|
+
*
|
|
43760
|
+
* The `getZoneHistory` / `getCameraHistory` / `getUnzonedHistory`
|
|
43761
|
+
* triple covers the three spatial scopes (per-zone, frame-wide,
|
|
43762
|
+
* outside-any-zone). Crossing them with `className?` gives the full
|
|
43763
|
+
* combinatorial coverage the operator UI requested.
|
|
43764
|
+
*
|
|
43765
|
+
* ## ⚠️ The three history methods are DEPRECATED
|
|
43766
|
+
*
|
|
43767
|
+
* They are served from a 60-MINUTE in-memory ring. It dies with the process,
|
|
43768
|
+
* so after a restart they report nothing about a night that did happen, and any
|
|
43769
|
+
* window past an hour is silently clamped — a caller cannot tell a clamp from
|
|
43770
|
+
* an empty scene. The presets in the occupancy chart existed to hide that
|
|
43771
|
+
* horizon rather than to answer a question.
|
|
43772
|
+
*
|
|
43773
|
+
* Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
|
|
43774
|
+
* instead. Those rows are already written for every camera, ~60 bytes per
|
|
43775
|
+
* (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
|
|
43776
|
+
* Spell the scope with `occupancyScope(zoneId?, className?)` — the same
|
|
43777
|
+
* function the writer uses, so the two cannot drift.
|
|
43778
|
+
*
|
|
43779
|
+
* The ring stays until these three go, and it is the only thing left reading
|
|
43780
|
+
* it. Do not add a fourth caller.
|
|
43480
43781
|
*/
|
|
43481
|
-
|
|
43482
|
-
|
|
43483
|
-
|
|
43484
|
-
|
|
43485
|
-
|
|
43486
|
-
|
|
43487
|
-
|
|
43488
|
-
|
|
43489
|
-
|
|
43490
|
-
|
|
43491
|
-
|
|
43492
|
-
|
|
43493
|
-
|
|
43494
|
-
|
|
43782
|
+
var zoneAnalyticsCapability = {
|
|
43783
|
+
name: "zone-analytics",
|
|
43784
|
+
scope: "device",
|
|
43785
|
+
mode: "singleton",
|
|
43786
|
+
deviceTypes: [DeviceType.Camera],
|
|
43787
|
+
methods: {
|
|
43788
|
+
/** Latest computed occupancy snapshot for this camera. Null when
|
|
43789
|
+
* the analytics pipeline hasn't seen a frame for this device yet
|
|
43790
|
+
* (no inference result emitted since boot or since binding was
|
|
43791
|
+
* activated). */
|
|
43792
|
+
getCurrentSnapshot: method(zod.z.object({ deviceId: zod.z.number() }), CameraOccupancySnapshotSchema.nullable()),
|
|
43793
|
+
/**
|
|
43794
|
+
* The same snapshot, for a SET of cameras, in one round trip.
|
|
43795
|
+
*
|
|
43796
|
+
* The Events page's Stationary section polls this every 15s for every
|
|
43797
|
+
* selected camera. Fanned out client-side that is one query per camera to
|
|
43798
|
+
* read a `Map.get` at the owner — the answer costs nothing, the round trip
|
|
43799
|
+
* costs everything. Batched, N transports become one and the per-device
|
|
43800
|
+
* work is unchanged.
|
|
43801
|
+
*
|
|
43802
|
+
* Every requested deviceId gets a row, tagged `read` — see
|
|
43803
|
+
* {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
|
|
43804
|
+
* not answer for is `'unreadable'`, never an empty reading.
|
|
43805
|
+
*/
|
|
43806
|
+
getCurrentSnapshotBatch: method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).min(1).max(200) }), zod.z.array(CameraOccupancySnapshotForDeviceSchema).readonly()),
|
|
43807
|
+
/** Time-series object count inside one zone. `className` optional —
|
|
43808
|
+
* omit to count every class in the zone. */
|
|
43809
|
+
getZoneHistory: method(zod.z.object({
|
|
43810
|
+
deviceId: zod.z.number(),
|
|
43811
|
+
zoneId: zod.z.string(),
|
|
43812
|
+
className: zod.z.string().optional()
|
|
43813
|
+
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly()),
|
|
43814
|
+
/** Time-series frame-wide object count (everywhere). */
|
|
43815
|
+
getCameraHistory: method(zod.z.object({
|
|
43816
|
+
deviceId: zod.z.number(),
|
|
43817
|
+
className: zod.z.string().optional()
|
|
43818
|
+
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly()),
|
|
43819
|
+
/** Time-series count of objects outside every zone. */
|
|
43820
|
+
getUnzonedHistory: method(zod.z.object({
|
|
43821
|
+
deviceId: zod.z.number(),
|
|
43822
|
+
className: zod.z.string().optional()
|
|
43823
|
+
}).extend(HistoryRangeSchema.shape), zod.z.array(HistoryPointSchema).readonly())
|
|
43824
|
+
},
|
|
43825
|
+
/**
|
|
43826
|
+
* Runtime-state slice — the latest occupancy snapshot mirrored by
|
|
43827
|
+
* the analytics frame processor on every inference result. Consumers
|
|
43828
|
+
* read via `device.state.zoneAnalytics.value` and stay in sync
|
|
43829
|
+
* automatically; the explicit `getCurrentSnapshot` cap method is
|
|
43830
|
+
* still useful for one-off polls without a subscription.
|
|
43831
|
+
*/
|
|
43832
|
+
runtimeState: CameraOccupancySnapshotSchema,
|
|
43833
|
+
/**
|
|
43834
|
+
* Runtime-state durability: **session** — per-frame analytics; with `audio-metrics` it is ~90 % of the offered write rate. Re-derived on the next frame.
|
|
43835
|
+
*
|
|
43836
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
43837
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
43838
|
+
*/
|
|
43839
|
+
durability: "session"
|
|
43840
|
+
};
|
|
43495
43841
|
/**
|
|
43496
|
-
*
|
|
43497
|
-
*
|
|
43498
|
-
* own
|
|
43499
|
-
*
|
|
43842
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
43843
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
43844
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
43845
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
43846
|
+
*
|
|
43847
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
43848
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
43500
43849
|
*/
|
|
43501
|
-
|
|
43502
|
-
|
|
43503
|
-
|
|
43504
|
-
|
|
43505
|
-
|
|
43506
|
-
|
|
43507
|
-
|
|
43508
|
-
|
|
43509
|
-
|
|
43510
|
-
|
|
43511
|
-
|
|
43512
|
-
|
|
43513
|
-
|
|
43514
|
-
|
|
43515
|
-
|
|
43516
|
-
|
|
43517
|
-
|
|
43518
|
-
|
|
43519
|
-
|
|
43520
|
-
|
|
43521
|
-
|
|
43522
|
-
|
|
43523
|
-
|
|
43524
|
-
|
|
43525
|
-
|
|
43526
|
-
|
|
43527
|
-
|
|
43528
|
-
|
|
43529
|
-
|
|
43530
|
-
|
|
43531
|
-
|
|
43532
|
-
|
|
43533
|
-
|
|
43534
|
-
|
|
43535
|
-
|
|
43536
|
-
|
|
43537
|
-
|
|
43538
|
-
|
|
43539
|
-
|
|
43540
|
-
|
|
43541
|
-
|
|
43542
|
-
|
|
43543
|
-
|
|
43544
|
-
|
|
43545
|
-
|
|
43546
|
-
|
|
43850
|
+
var ZoneRuleStageEnum = zod.z.enum([
|
|
43851
|
+
"motion",
|
|
43852
|
+
"detection",
|
|
43853
|
+
"package"
|
|
43854
|
+
]);
|
|
43855
|
+
/**
|
|
43856
|
+
* Zone rules capability — per-camera CRUD over the {@link ZoneRule}
|
|
43857
|
+
* arrays that decide how each pipeline stage uses the polygon zones.
|
|
43858
|
+
*
|
|
43859
|
+
* Hosted by `addon-pipeline-orchestrator` alongside the zones provider
|
|
43860
|
+
* so the operator has a single hub-side source of truth for both
|
|
43861
|
+
* geometry and behaviour. Per-stage rules are stored under the
|
|
43862
|
+
* `zoneRules.<stage>` key in the orchestrator's per-device store and
|
|
43863
|
+
* mirrored to the device-state slice `<stage>-zone-rules` on every
|
|
43864
|
+
* mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
|
|
43865
|
+
* subscribe to that slice and refresh their gating without
|
|
43866
|
+
* round-tripping the cap.
|
|
43867
|
+
*
|
|
43868
|
+
* Sets are bulk-replace — the operator UI sends the new rule list
|
|
43869
|
+
* wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
|
|
43870
|
+
* naturally in the rule editor without per-rule mutation chatter.
|
|
43871
|
+
*/
|
|
43872
|
+
var zoneRulesCapability = {
|
|
43873
|
+
name: "zone-rules",
|
|
43874
|
+
scope: "device",
|
|
43875
|
+
mode: "singleton",
|
|
43876
|
+
deviceTypes: [DeviceType.Camera],
|
|
43877
|
+
methods: {
|
|
43878
|
+
/** Read the full rule list for a given stage (empty when no rules
|
|
43879
|
+
* are defined yet). */
|
|
43880
|
+
listRules: method(zod.z.object({
|
|
43881
|
+
deviceId: zod.z.number(),
|
|
43882
|
+
stage: ZoneRuleStageEnum
|
|
43883
|
+
}), zod.z.array(ZoneRuleSchema).readonly()),
|
|
43884
|
+
/** Bulk-replace the rule list for one stage. The provider validates
|
|
43885
|
+
* each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
|
|
43886
|
+
* thresholds in range) and rejects the whole patch if any entry
|
|
43887
|
+
* is invalid — partial writes are a configuration footgun. */
|
|
43888
|
+
setRules: method(zod.z.object({
|
|
43889
|
+
deviceId: zod.z.number(),
|
|
43890
|
+
stage: ZoneRuleStageEnum,
|
|
43891
|
+
rules: zod.z.array(ZoneRuleSchema).readonly()
|
|
43892
|
+
}), zod.z.void(), {
|
|
43893
|
+
kind: "mutation",
|
|
43894
|
+
auth: "admin"
|
|
43895
|
+
})
|
|
43896
|
+
},
|
|
43897
|
+
/**
|
|
43898
|
+
* Runtime-state slice — every stage mirrored together so consumers
|
|
43899
|
+
* see one reactive handle (`device.state.zoneRules.value`) instead
|
|
43900
|
+
* of one per stage. Bulk-replace mutations on any stage write the full
|
|
43901
|
+
* `{motion, detection, package}` shape, so subscribers always get the
|
|
43902
|
+
* complete current set. Consumers that only care about one stage
|
|
43903
|
+
* just read the matching property.
|
|
43904
|
+
*
|
|
43905
|
+
* `package` backs the package-drop detector — a package zone is a
|
|
43906
|
+
* `ZoneRule` on the `'package'` stage referencing drawn polygons
|
|
43907
|
+
* (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
|
|
43908
|
+
* The orchestrator provider writes this stage as a first-class slice
|
|
43909
|
+
* (Phase 4): every mutation mirrors the full `{motion, detection,
|
|
43910
|
+
* package}` shape, so consumers read the current package rules directly
|
|
43911
|
+
* off `device.state.zoneRules.value.package`.
|
|
43912
|
+
*/
|
|
43913
|
+
runtimeState: zod.z.object({
|
|
43914
|
+
motion: zod.z.array(ZoneRuleSchema).readonly(),
|
|
43915
|
+
detection: zod.z.array(ZoneRuleSchema).readonly(),
|
|
43916
|
+
package: zod.z.array(ZoneRuleSchema).readonly()
|
|
43917
|
+
}),
|
|
43918
|
+
/**
|
|
43919
|
+
* Runtime-state durability: **restored** — operator intent, mutation-only, same argument as `zones`.
|
|
43920
|
+
*
|
|
43921
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
43922
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
43923
|
+
*/
|
|
43924
|
+
durability: "restored"
|
|
43925
|
+
};
|
|
43926
|
+
function isValueOf(slot, value) {
|
|
43927
|
+
return value.slot === slot;
|
|
43547
43928
|
}
|
|
43548
|
-
|
|
43549
|
-
|
|
43550
|
-
const { key, label, path, source } = leaf;
|
|
43551
|
-
const at = `features[${featureIndex}].items.${key}.fields.${path}`;
|
|
43552
|
-
if (ctx.reserved.has(path)) return refused(problem("item-field-reserved", at, `\`${capName}.${itemArray.path}[].${path}\` is set by the composer itself — it takes no source`));
|
|
43553
|
-
const tf = describeStateField(itemArray.itemSchema, path);
|
|
43554
|
-
if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
|
|
43555
|
-
const kinds = sourceKinds(source, lookupCap);
|
|
43556
|
-
if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
|
|
43557
|
-
const role = roleOf(source);
|
|
43558
|
-
const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
|
|
43559
|
-
if (!unavailable.ok) return refused(unavailable.problem);
|
|
43560
|
-
const plan = {
|
|
43561
|
-
cap: capName,
|
|
43562
|
-
path,
|
|
43563
|
-
item: {
|
|
43564
|
-
arrayPath: itemArray.path,
|
|
43565
|
-
key,
|
|
43566
|
-
label
|
|
43567
|
-
},
|
|
43568
|
-
featureIndex,
|
|
43569
|
-
kind: tf.kind,
|
|
43570
|
-
nullable: tf.nullable,
|
|
43571
|
-
required: tf.required,
|
|
43572
|
-
unavailable: unavailable.unavailable,
|
|
43573
|
-
role,
|
|
43574
|
-
source,
|
|
43575
|
-
reads: [...fieldReads(source)]
|
|
43576
|
-
};
|
|
43577
|
-
const enumProblem = enumLiteralProblem(capName, tf, source, at);
|
|
43929
|
+
/** Bind slot `slot` to its owner's functions. See the module docblock. */
|
|
43930
|
+
function defineExtensionSlotProvider(slot, impl) {
|
|
43578
43931
|
return {
|
|
43579
|
-
|
|
43580
|
-
|
|
43581
|
-
|
|
43582
|
-
|
|
43932
|
+
slot,
|
|
43933
|
+
list: async (deviceId) => {
|
|
43934
|
+
const listing = await impl.list(deviceId);
|
|
43935
|
+
if (listing === null) return null;
|
|
43936
|
+
const value = listing.value;
|
|
43937
|
+
return {
|
|
43938
|
+
...listing,
|
|
43939
|
+
value
|
|
43940
|
+
};
|
|
43941
|
+
},
|
|
43942
|
+
set: async (deviceId, value) => {
|
|
43943
|
+
if (!isValueOf(slot, value)) throw new Error(`device-extension: a "${value.slot}" value was sent to slot "${slot}" — refused`);
|
|
43944
|
+
return await impl.set(deviceId, value);
|
|
43945
|
+
},
|
|
43946
|
+
candidates: (deviceId) => impl.candidates(deviceId)
|
|
43583
43947
|
};
|
|
43584
43948
|
}
|
|
43585
|
-
|
|
43586
|
-
|
|
43587
|
-
|
|
43588
|
-
|
|
43589
|
-
|
|
43590
|
-
|
|
43591
|
-
|
|
43592
|
-
|
|
43593
|
-
|
|
43594
|
-
|
|
43595
|
-
|
|
43596
|
-
|
|
43949
|
+
/** `<source>:<slotId>`. */
|
|
43950
|
+
function extensionSlotRef(source, slot) {
|
|
43951
|
+
return `${source}:${slot}`;
|
|
43952
|
+
}
|
|
43953
|
+
/** Build the `device-extension` provider one owner registers. */
|
|
43954
|
+
function createDeviceExtensionProvider(options) {
|
|
43955
|
+
const { source, label, slots, hostTypeOf, logger } = options;
|
|
43956
|
+
const offered = (slot, hostType) => hostType !== null && EXTENSION_SLOTS[slot].hostDeviceTypes.some((t) => t === hostType);
|
|
43957
|
+
const resolve = async (deviceId, slotRef) => {
|
|
43958
|
+
const prefix = `${source}:`;
|
|
43959
|
+
const slot = slotRef.startsWith(prefix) ? slots.find((s) => s.slot === slotRef.slice(prefix.length)) : void 0;
|
|
43960
|
+
if (slot === void 0) throw new Error(`device-extension: "${slotRef}" is not a slot of "${source}" — it serves ${slots.map((s) => extensionSlotRef(source, s.slot)).join(", ") || "(none)"}`);
|
|
43961
|
+
const hostType = await hostTypeOf(deviceId);
|
|
43962
|
+
if (!offered(slot.slot, hostType)) throw new Error(`device-extension: slot "${slot.slot}" is not offered on device ${deviceId} (type ${hostType ?? "unknown"}; offered on ${EXTENSION_SLOTS[slot.slot].hostDeviceTypes.join(", ")})`);
|
|
43963
|
+
return slot;
|
|
43597
43964
|
};
|
|
43598
|
-
const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
|
|
43599
|
-
problems: missingLeaves(ctx, item, `${at}.${key}`),
|
|
43600
|
-
fields: [],
|
|
43601
|
-
features: [],
|
|
43602
|
-
events: []
|
|
43603
|
-
}, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
|
|
43604
|
-
key,
|
|
43605
|
-
label: item.label,
|
|
43606
|
-
path,
|
|
43607
|
-
source
|
|
43608
|
-
}))]);
|
|
43609
43965
|
return {
|
|
43610
|
-
|
|
43611
|
-
|
|
43612
|
-
|
|
43613
|
-
|
|
43966
|
+
listSources: async () => [{
|
|
43967
|
+
source,
|
|
43968
|
+
label,
|
|
43969
|
+
availability: { state: "ok" }
|
|
43970
|
+
}],
|
|
43971
|
+
listSlots: async (input) => {
|
|
43972
|
+
const hostType = await hostTypeOf(input.deviceId);
|
|
43973
|
+
const rows = [];
|
|
43974
|
+
for (const slot of slots) {
|
|
43975
|
+
if (!offered(slot.slot, hostType)) continue;
|
|
43976
|
+
const listing = await slot.list(input.deviceId);
|
|
43977
|
+
if (listing === null) continue;
|
|
43978
|
+
rows.push({
|
|
43979
|
+
state: "ok",
|
|
43980
|
+
slotRef: extensionSlotRef(source, slot.slot),
|
|
43981
|
+
slot: slot.slot,
|
|
43982
|
+
authority: listing.authority,
|
|
43983
|
+
activation: listing.activation,
|
|
43984
|
+
value: listing.value,
|
|
43985
|
+
sources: [...listing.sources],
|
|
43986
|
+
...listing.impliedSources !== void 0 ? { impliedSources: [...listing.impliedSources] } : {}
|
|
43987
|
+
});
|
|
43988
|
+
}
|
|
43989
|
+
return rows;
|
|
43990
|
+
},
|
|
43991
|
+
listCandidates: async (input) => {
|
|
43992
|
+
return { candidates: [...await (await resolve(input.deviceId, input.slotRef)).candidates(input.deviceId)] };
|
|
43993
|
+
},
|
|
43994
|
+
setSlot: async (input) => {
|
|
43995
|
+
const result = await (await resolve(input.deviceId, input.slotRef)).set(input.deviceId, input.value);
|
|
43996
|
+
logger.info("device-extension: slot written", {
|
|
43997
|
+
tags: { deviceId: input.deviceId },
|
|
43998
|
+
meta: {
|
|
43999
|
+
slotRef: input.slotRef,
|
|
44000
|
+
reload: result.reload
|
|
44001
|
+
}
|
|
44002
|
+
});
|
|
44003
|
+
return result;
|
|
44004
|
+
}
|
|
43614
44005
|
};
|
|
43615
44006
|
}
|
|
44007
|
+
/**
|
|
44008
|
+
* Frozen unit → spec table. `•` in the doc marks each dimension's canonical
|
|
44009
|
+
* unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
|
|
44010
|
+
* `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
|
|
44011
|
+
* decimals). K derives from `°C = K − 273.15`.
|
|
44012
|
+
*/
|
|
44013
|
+
var UNIT_TABLE = {
|
|
44014
|
+
"°C": {
|
|
44015
|
+
dimension: "temperature",
|
|
44016
|
+
factor: 1,
|
|
44017
|
+
offset: 0
|
|
44018
|
+
},
|
|
44019
|
+
"°F": {
|
|
44020
|
+
dimension: "temperature",
|
|
44021
|
+
factor: 5 / 9,
|
|
44022
|
+
offset: -160 / 9
|
|
44023
|
+
},
|
|
44024
|
+
K: {
|
|
44025
|
+
dimension: "temperature",
|
|
44026
|
+
factor: 1,
|
|
44027
|
+
offset: -273.15
|
|
44028
|
+
},
|
|
44029
|
+
hPa: {
|
|
44030
|
+
dimension: "pressure",
|
|
44031
|
+
factor: 1,
|
|
44032
|
+
offset: 0
|
|
44033
|
+
},
|
|
44034
|
+
kPa: {
|
|
44035
|
+
dimension: "pressure",
|
|
44036
|
+
factor: 10,
|
|
44037
|
+
offset: 0
|
|
44038
|
+
},
|
|
44039
|
+
Pa: {
|
|
44040
|
+
dimension: "pressure",
|
|
44041
|
+
factor: .01,
|
|
44042
|
+
offset: 0
|
|
44043
|
+
},
|
|
44044
|
+
mbar: {
|
|
44045
|
+
dimension: "pressure",
|
|
44046
|
+
factor: 1,
|
|
44047
|
+
offset: 0
|
|
44048
|
+
},
|
|
44049
|
+
bar: {
|
|
44050
|
+
dimension: "pressure",
|
|
44051
|
+
factor: 1e3,
|
|
44052
|
+
offset: 0
|
|
44053
|
+
},
|
|
44054
|
+
inHg: {
|
|
44055
|
+
dimension: "pressure",
|
|
44056
|
+
factor: 33.8639,
|
|
44057
|
+
offset: 0
|
|
44058
|
+
},
|
|
44059
|
+
mmHg: {
|
|
44060
|
+
dimension: "pressure",
|
|
44061
|
+
factor: 1.33322,
|
|
44062
|
+
offset: 0
|
|
44063
|
+
},
|
|
44064
|
+
psi: {
|
|
44065
|
+
dimension: "pressure",
|
|
44066
|
+
factor: 68.9476,
|
|
44067
|
+
offset: 0
|
|
44068
|
+
},
|
|
44069
|
+
"m/s": {
|
|
44070
|
+
dimension: "speed",
|
|
44071
|
+
factor: 1,
|
|
44072
|
+
offset: 0
|
|
44073
|
+
},
|
|
44074
|
+
"km/h": {
|
|
44075
|
+
dimension: "speed",
|
|
44076
|
+
factor: 1 / 3.6,
|
|
44077
|
+
offset: 0
|
|
44078
|
+
},
|
|
44079
|
+
mph: {
|
|
44080
|
+
dimension: "speed",
|
|
44081
|
+
factor: .44704,
|
|
44082
|
+
offset: 0
|
|
44083
|
+
},
|
|
44084
|
+
kn: {
|
|
44085
|
+
dimension: "speed",
|
|
44086
|
+
factor: .514444,
|
|
44087
|
+
offset: 0
|
|
44088
|
+
},
|
|
44089
|
+
"mm/h": {
|
|
44090
|
+
dimension: "precipitation-rate",
|
|
44091
|
+
factor: 1,
|
|
44092
|
+
offset: 0
|
|
44093
|
+
},
|
|
44094
|
+
"in/h": {
|
|
44095
|
+
dimension: "precipitation-rate",
|
|
44096
|
+
factor: 25.4,
|
|
44097
|
+
offset: 0
|
|
44098
|
+
},
|
|
44099
|
+
m: {
|
|
44100
|
+
dimension: "length",
|
|
44101
|
+
factor: 1,
|
|
44102
|
+
offset: 0
|
|
44103
|
+
},
|
|
44104
|
+
mm: {
|
|
44105
|
+
dimension: "length",
|
|
44106
|
+
factor: .001,
|
|
44107
|
+
offset: 0
|
|
44108
|
+
},
|
|
44109
|
+
cm: {
|
|
44110
|
+
dimension: "length",
|
|
44111
|
+
factor: .01,
|
|
44112
|
+
offset: 0
|
|
44113
|
+
},
|
|
44114
|
+
km: {
|
|
44115
|
+
dimension: "length",
|
|
44116
|
+
factor: 1e3,
|
|
44117
|
+
offset: 0
|
|
44118
|
+
},
|
|
44119
|
+
in: {
|
|
44120
|
+
dimension: "length",
|
|
44121
|
+
factor: .0254,
|
|
44122
|
+
offset: 0
|
|
44123
|
+
},
|
|
44124
|
+
ft: {
|
|
44125
|
+
dimension: "length",
|
|
44126
|
+
factor: .3048,
|
|
44127
|
+
offset: 0
|
|
44128
|
+
},
|
|
44129
|
+
mi: {
|
|
44130
|
+
dimension: "length",
|
|
44131
|
+
factor: 1609.344,
|
|
44132
|
+
offset: 0
|
|
44133
|
+
},
|
|
44134
|
+
lx: {
|
|
44135
|
+
dimension: "illuminance",
|
|
44136
|
+
factor: 1,
|
|
44137
|
+
offset: 0
|
|
44138
|
+
},
|
|
44139
|
+
"W/m²": {
|
|
44140
|
+
dimension: "irradiance",
|
|
44141
|
+
factor: 1,
|
|
44142
|
+
offset: 0
|
|
44143
|
+
},
|
|
44144
|
+
W: {
|
|
44145
|
+
dimension: "power",
|
|
44146
|
+
factor: 1,
|
|
44147
|
+
offset: 0
|
|
44148
|
+
},
|
|
44149
|
+
kW: {
|
|
44150
|
+
dimension: "power",
|
|
44151
|
+
factor: 1e3,
|
|
44152
|
+
offset: 0
|
|
44153
|
+
},
|
|
44154
|
+
VA: {
|
|
44155
|
+
dimension: "apparent-power",
|
|
44156
|
+
factor: 1,
|
|
44157
|
+
offset: 0
|
|
44158
|
+
},
|
|
44159
|
+
Wh: {
|
|
44160
|
+
dimension: "energy",
|
|
44161
|
+
factor: 1,
|
|
44162
|
+
offset: 0
|
|
44163
|
+
},
|
|
44164
|
+
kWh: {
|
|
44165
|
+
dimension: "energy",
|
|
44166
|
+
factor: 1e3,
|
|
44167
|
+
offset: 0
|
|
44168
|
+
},
|
|
44169
|
+
MWh: {
|
|
44170
|
+
dimension: "energy",
|
|
44171
|
+
factor: 1e6,
|
|
44172
|
+
offset: 0
|
|
44173
|
+
},
|
|
44174
|
+
V: {
|
|
44175
|
+
dimension: "voltage",
|
|
44176
|
+
factor: 1,
|
|
44177
|
+
offset: 0
|
|
44178
|
+
},
|
|
44179
|
+
mV: {
|
|
44180
|
+
dimension: "voltage",
|
|
44181
|
+
factor: .001,
|
|
44182
|
+
offset: 0
|
|
44183
|
+
},
|
|
44184
|
+
A: {
|
|
44185
|
+
dimension: "current",
|
|
44186
|
+
factor: 1,
|
|
44187
|
+
offset: 0
|
|
44188
|
+
},
|
|
44189
|
+
mA: {
|
|
44190
|
+
dimension: "current",
|
|
44191
|
+
factor: .001,
|
|
44192
|
+
offset: 0
|
|
44193
|
+
},
|
|
44194
|
+
ppm: {
|
|
44195
|
+
dimension: "concentration-volume",
|
|
44196
|
+
factor: 1,
|
|
44197
|
+
offset: 0
|
|
44198
|
+
},
|
|
44199
|
+
ppb: {
|
|
44200
|
+
dimension: "concentration-volume",
|
|
44201
|
+
factor: .001,
|
|
44202
|
+
offset: 0
|
|
44203
|
+
},
|
|
44204
|
+
"µg/m³": {
|
|
44205
|
+
dimension: "concentration-mass",
|
|
44206
|
+
factor: 1,
|
|
44207
|
+
offset: 0
|
|
44208
|
+
},
|
|
44209
|
+
"mg/m³": {
|
|
44210
|
+
dimension: "concentration-mass",
|
|
44211
|
+
factor: 1e3,
|
|
44212
|
+
offset: 0
|
|
44213
|
+
},
|
|
44214
|
+
kg: {
|
|
44215
|
+
dimension: "mass",
|
|
44216
|
+
factor: 1,
|
|
44217
|
+
offset: 0
|
|
44218
|
+
},
|
|
44219
|
+
g: {
|
|
44220
|
+
dimension: "mass",
|
|
44221
|
+
factor: .001,
|
|
44222
|
+
offset: 0
|
|
44223
|
+
},
|
|
44224
|
+
lb: {
|
|
44225
|
+
dimension: "mass",
|
|
44226
|
+
factor: .453592,
|
|
44227
|
+
offset: 0
|
|
44228
|
+
},
|
|
44229
|
+
oz: {
|
|
44230
|
+
dimension: "mass",
|
|
44231
|
+
factor: .0283495,
|
|
44232
|
+
offset: 0
|
|
44233
|
+
},
|
|
44234
|
+
"%": {
|
|
44235
|
+
dimension: "percentage",
|
|
44236
|
+
factor: 1,
|
|
44237
|
+
offset: 0
|
|
44238
|
+
}
|
|
44239
|
+
};
|
|
44240
|
+
/** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
|
|
44241
|
+
function specFor(unit) {
|
|
44242
|
+
return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
|
|
44243
|
+
}
|
|
44244
|
+
/** Value in `spec`'s unit → its dimension's canonical unit. */
|
|
44245
|
+
function toCanonical(value, spec) {
|
|
44246
|
+
return value * spec.factor + spec.offset;
|
|
44247
|
+
}
|
|
44248
|
+
/** Canonical-unit value → the unit described by `spec`. */
|
|
44249
|
+
function fromCanonical(canonical, spec) {
|
|
44250
|
+
return (canonical - spec.offset) / spec.factor;
|
|
44251
|
+
}
|
|
44252
|
+
/**
|
|
44253
|
+
* Graceful variant for the render path: returns the converted value, or
|
|
44254
|
+
* `undefined` when the conversion is refused (unknown unit / cross-dimension).
|
|
44255
|
+
* Never throws — a render caller falls back to the raw value + source unit.
|
|
44256
|
+
*/
|
|
44257
|
+
function tryConvertUnit(value, from, to) {
|
|
44258
|
+
if (from === to) return value;
|
|
44259
|
+
const fromSpec = specFor(from);
|
|
44260
|
+
if (!fromSpec) return void 0;
|
|
44261
|
+
const toSpec = specFor(to);
|
|
44262
|
+
if (!toSpec) return void 0;
|
|
44263
|
+
if (fromSpec.dimension !== toSpec.dimension) return void 0;
|
|
44264
|
+
return fromCanonical(toCanonical(value, fromSpec), toSpec);
|
|
44265
|
+
}
|
|
44266
|
+
/**
|
|
44267
|
+
* One composed field, evaluated from what its sources say NOW (D659).
|
|
44268
|
+
*
|
|
44269
|
+
* Pure. The composer calls it on every source change, and the slice-5 editor
|
|
44270
|
+
* will call it for its live preview, so both see the same answer.
|
|
44271
|
+
*
|
|
44272
|
+
* The rules (spec §2):
|
|
44273
|
+
* - a source that is offline or unreadable keeps its last value and is marked
|
|
44274
|
+
* STALE; the field is stale and keeps computing (D49);
|
|
44275
|
+
* - a source that is GONE makes the field UNAVAILABLE, `fatal`, and never a
|
|
44276
|
+
* plausible number (D393);
|
|
44277
|
+
* - a source that is unavailable but NOT gone (e.g. a cap unregistered at
|
|
44278
|
+
* runtime, R7b) makes the field UNAVAILABLE, non-fatal, `value: null`: it
|
|
44279
|
+
* may come back on its own, no block failure is raised for it;
|
|
44280
|
+
* - an expression that fails on a value makes the field unavailable for that
|
|
44281
|
+
* value, and NOT fatal: the next value may compute.
|
|
44282
|
+
*
|
|
44283
|
+
* `confirmed` (D663, D49): whether a NON-fatal unavailability is an ANSWER — a
|
|
44284
|
+
* cap the provider unregistered and a re-read agreed, a field the source
|
|
44285
|
+
* reported without, an expression that failed on a value — or only the
|
|
44286
|
+
* absence of one (a failed read, a first or empty read, a source not tracked
|
|
44287
|
+
* yet). Only a confirmed unavailability may hand a `release`-mode field back
|
|
44288
|
+
* to the native provider; an unconfirmed one holds it on its last value.
|
|
44289
|
+
* Absent means NOT confirmed. Fatal is always confirmed.
|
|
44290
|
+
*
|
|
44291
|
+
* When an expression has more than one unavailable binding, a FATAL one always
|
|
44292
|
+
* wins the outcome, regardless of position: every binding is scanned before
|
|
44293
|
+
* the field is judged, so an earlier non-fatal binding (e.g. a cap that is
|
|
44294
|
+
* momentarily unregistered) can never hide a later fatal one (a deleted
|
|
44295
|
+
* source) behind an early return.
|
|
44296
|
+
*/
|
|
43616
44297
|
function describeRead(read) {
|
|
43617
44298
|
return `${compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
|
|
43618
44299
|
}
|
|
@@ -43679,7 +44360,10 @@ function expressionOutcome(source, read, now, stateful) {
|
|
|
43679
44360
|
}
|
|
43680
44361
|
const deadlines = [];
|
|
43681
44362
|
const result = evaluateExpressionSource(source.expr, values, now, {
|
|
43682
|
-
hooks: {
|
|
44363
|
+
hooks: {
|
|
44364
|
+
noteDeadline: (at) => deadlines.push(at),
|
|
44365
|
+
convert: (value, from, to) => tryConvertUnit(value, normalizeUnit(from) ?? from, normalizeUnit(to) ?? to) ?? null
|
|
44366
|
+
},
|
|
43683
44367
|
...stateful === void 0 ? {} : { invokeStateful: (invocation) => evaluateStatefulExpressionCall(stateful.callPathPrefix === void 0 ? invocation : {
|
|
43684
44368
|
...invocation,
|
|
43685
44369
|
path: `${stateful.callPathPrefix}@${invocation.path}`
|
|
@@ -43717,301 +44401,70 @@ function evaluateCompositionField(input) {
|
|
|
43717
44401
|
};
|
|
43718
44402
|
}
|
|
43719
44403
|
}
|
|
43720
|
-
function buildCapabilityLookup(defs) {
|
|
43721
|
-
const byName = new Map(defs.map((d) => [d.name, d]));
|
|
43722
|
-
return (capName) => byName.get(capName) ?? null;
|
|
43723
|
-
}
|
|
43724
|
-
/** Caps every `BaseDevice` registers on itself; a second registration throws. */
|
|
43725
|
-
var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
|
|
43726
|
-
var PENDING_SELF = {
|
|
43727
|
-
addonId: COMPOSER_ADDON_ID,
|
|
43728
|
-
stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
|
|
43729
|
-
};
|
|
43730
|
-
function composableCap(feature, at, lookupCap) {
|
|
43731
|
-
const def = lookupCap(feature.cap);
|
|
43732
|
-
if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
|
|
43733
|
-
if (BASE_DEVICE_CAPS.has(def.name)) return problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`);
|
|
43734
|
-
const state = def.runtimeState;
|
|
43735
|
-
if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
|
|
43736
|
-
return {
|
|
43737
|
-
def,
|
|
43738
|
-
state,
|
|
43739
|
-
fields: describeTopLevelStateFields(state)
|
|
43740
|
-
};
|
|
43741
|
-
}
|
|
43742
44404
|
/**
|
|
43743
|
-
*
|
|
43744
|
-
*
|
|
43745
|
-
*
|
|
43746
|
-
*
|
|
44405
|
+
* The doorbell of a camera as a customization (D671). ONE builder: the
|
|
44406
|
+
* virtual-doorbell migration and the admin UI's "Use as doorbell" preset both
|
|
44407
|
+
* call it, so a preset can never write a shape the migration would refuse as
|
|
44408
|
+
* "an unrelated existing block".
|
|
43747
44409
|
*/
|
|
43748
|
-
|
|
43749
|
-
|
|
43750
|
-
|
|
43751
|
-
|
|
43752
|
-
|
|
43753
|
-
|
|
43754
|
-
|
|
43755
|
-
|
|
43756
|
-
|
|
43757
|
-
|
|
43758
|
-
|
|
43759
|
-
|
|
43760
|
-
}
|
|
43761
|
-
|
|
43762
|
-
const { featureIndex, path, source } = field;
|
|
43763
|
-
const fieldAt = `features[${featureIndex}].fields.${path}`;
|
|
43764
|
-
if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
|
|
43765
|
-
const tf = fields.get(path);
|
|
43766
|
-
if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
|
|
43767
|
-
const kinds = sourceKinds(source, lookupCap);
|
|
43768
|
-
if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
|
|
43769
|
-
const role = roleOf(source);
|
|
43770
|
-
const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
|
|
43771
|
-
if (!unavailable.ok) return refused(unavailable.problem);
|
|
43772
|
-
const plan = {
|
|
43773
|
-
cap: def.name,
|
|
43774
|
-
path,
|
|
43775
|
-
featureIndex,
|
|
43776
|
-
kind: tf.kind,
|
|
43777
|
-
nullable: tf.nullable,
|
|
43778
|
-
required: tf.required,
|
|
43779
|
-
unavailable: unavailable.unavailable,
|
|
43780
|
-
role,
|
|
43781
|
-
source,
|
|
43782
|
-
reads: [...fieldReads(source)]
|
|
43783
|
-
};
|
|
43784
|
-
const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
|
|
43785
|
-
return {
|
|
43786
|
-
problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
|
|
43787
|
-
fields: [plan],
|
|
43788
|
-
features: [],
|
|
43789
|
-
events: []
|
|
43790
|
-
};
|
|
43791
|
-
}
|
|
43792
|
-
function planFeature(feature, featureIndex, target, lookupCap) {
|
|
43793
|
-
const at = `features[${featureIndex}]`;
|
|
43794
|
-
const cap = composableCap(feature, at, lookupCap);
|
|
43795
|
-
if ("code" in cap) return refused(cap);
|
|
43796
|
-
const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
|
|
43797
|
-
if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
|
|
43798
|
-
const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
|
|
43799
|
-
featureIndex,
|
|
43800
|
-
path,
|
|
43801
|
-
source
|
|
43802
|
-
}, target.mode, lookupCap));
|
|
43803
|
-
const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
|
|
43804
|
-
const all = [...perField, ...items];
|
|
43805
|
-
const fields = all.flatMap((result) => result.fields);
|
|
43806
|
-
const featurePlan = {
|
|
43807
|
-
featureIndex,
|
|
43808
|
-
cap: cap.def.name,
|
|
43809
|
-
mode: target.mode
|
|
43810
|
-
};
|
|
43811
|
-
const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
|
|
43812
|
-
return {
|
|
43813
|
-
problems: [
|
|
43814
|
-
...capProblems,
|
|
43815
|
-
...all.flatMap((r) => r.problems),
|
|
43816
|
-
...eventPlan.problems
|
|
43817
|
-
],
|
|
43818
|
-
fields,
|
|
43819
|
-
features: [featurePlan],
|
|
43820
|
-
events: eventPlan.events
|
|
43821
|
-
};
|
|
43822
|
-
}
|
|
43823
|
-
/** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
|
|
43824
|
-
function isNativeCap(target, cap) {
|
|
43825
|
-
const owner = target.nativeCaps.get(cap);
|
|
43826
|
-
return owner !== void 0 && owner !== "composer";
|
|
43827
|
-
}
|
|
43828
|
-
/** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
|
|
43829
|
-
function planTargetOf(composition, target) {
|
|
43830
|
-
if (composition.target.kind === "new") return {
|
|
43831
|
-
kind: "new",
|
|
43832
|
-
type: composition.target.type
|
|
43833
|
-
};
|
|
43834
|
-
const key = compositionSourceKey(composition.target.device);
|
|
43835
|
-
if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
|
|
43836
|
-
if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
|
|
43837
|
-
if (target.kind === "unreadable") return problem("target-unreadable", "target", `could not read target device \`${key}\` (${target.error}) — the save is refused rather than stored unchecked`);
|
|
43838
|
-
return {
|
|
43839
|
-
kind: "existing",
|
|
43840
|
-
found: target
|
|
43841
|
-
};
|
|
43842
|
-
}
|
|
43843
|
-
function featureTargetOf(target, cap) {
|
|
43844
|
-
if (target.kind === "new") return {
|
|
43845
|
-
mode: "new",
|
|
43846
|
-
deviceType: target.type
|
|
43847
|
-
};
|
|
43848
|
-
return {
|
|
43849
|
-
mode: isNativeCap(target.found, cap) ? "replace" : "add",
|
|
43850
|
-
deviceType: target.found.type
|
|
43851
|
-
};
|
|
44410
|
+
/** A rise closer than this to the previous one is the same press (D671). */
|
|
44411
|
+
var DOORBELL_RISE_DEBOUNCE_MS = 2e3;
|
|
44412
|
+
/** Every source cap of `source` that `SOURCE_CAP_ACTIVE_FIELD` knows AND the device has bound. */
|
|
44413
|
+
function doorbellFeatureSources(source, boundCaps) {
|
|
44414
|
+
return Object.keys(SOURCE_CAP_ACTIVE_FIELD).filter((cap) => Object.hasOwn(SOURCE_CAP_CHANGED_AT_FIELD, cap) && boundCaps.includes(cap)).toSorted().flatMap((cap) => {
|
|
44415
|
+
const activeField = SOURCE_CAP_ACTIVE_FIELD[cap];
|
|
44416
|
+
const changedAtField = SOURCE_CAP_CHANGED_AT_FIELD[cap];
|
|
44417
|
+
return activeField === void 0 || changedAtField === void 0 ? [] : [{
|
|
44418
|
+
source,
|
|
44419
|
+
cap,
|
|
44420
|
+
activeField,
|
|
44421
|
+
changedAtField
|
|
44422
|
+
}];
|
|
44423
|
+
});
|
|
43852
44424
|
}
|
|
43853
|
-
/**
|
|
43854
|
-
|
|
43855
|
-
|
|
43856
|
-
|
|
43857
|
-
|
|
43858
|
-
|
|
43859
|
-
|
|
43860
|
-
|
|
43861
|
-
|
|
43862
|
-
|
|
43863
|
-
|
|
43864
|
-
|
|
43865
|
-
|
|
43866
|
-
|
|
43867
|
-
|
|
44425
|
+
/** The D671 feature, byte for byte what the migration writes. Throws on zero sources. */
|
|
44426
|
+
function buildDoorbellCompositionFeature(input) {
|
|
44427
|
+
const sources = [...input.sources].toSorted((a, b) => `${a.source.addonId}/${a.source.stableId}#${a.cap}`.localeCompare(`${b.source.addonId}/${b.source.stableId}#${b.cap}`));
|
|
44428
|
+
if (sources.length === 0) throw new Error("doorbell composition needs at least one source");
|
|
44429
|
+
const edgeBindings = {};
|
|
44430
|
+
sources.forEach((entry, index) => {
|
|
44431
|
+
edgeBindings[`active${index}`] = {
|
|
44432
|
+
kind: "from",
|
|
44433
|
+
source: entry.source,
|
|
44434
|
+
cap: entry.cap,
|
|
44435
|
+
fieldPath: entry.activeField
|
|
44436
|
+
};
|
|
44437
|
+
edgeBindings[`changed${index}`] = {
|
|
44438
|
+
kind: "from",
|
|
44439
|
+
source: entry.source,
|
|
44440
|
+
cap: entry.cap,
|
|
44441
|
+
fieldPath: entry.changedAtField
|
|
43868
44442
|
};
|
|
43869
44443
|
});
|
|
43870
44444
|
return {
|
|
43871
|
-
|
|
43872
|
-
|
|
43873
|
-
|
|
43874
|
-
|
|
43875
|
-
|
|
43876
|
-
|
|
43877
|
-
|
|
43878
|
-
|
|
43879
|
-
|
|
43880
|
-
|
|
43881
|
-
|
|
43882
|
-
|
|
43883
|
-
|
|
43884
|
-
|
|
43885
|
-
|
|
43886
|
-
|
|
43887
|
-
|
|
43888
|
-
|
|
43889
|
-
|
|
43890
|
-
|
|
43891
|
-
|
|
43892
|
-
}
|
|
43893
|
-
|
|
43894
|
-
* `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
|
|
43895
|
-
* candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
|
|
43896
|
-
* derivation, so the graph reads both the same way.
|
|
43897
|
-
*/
|
|
43898
|
-
function replacedFieldKeys(plan) {
|
|
43899
|
-
const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
|
|
43900
|
-
return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
|
|
43901
|
-
}
|
|
43902
|
-
/**
|
|
43903
|
-
* A read of the candidate's own device. Its own composed output (a new device's
|
|
43904
|
-
* field, an ADDED field) is the slice-1 own-field rule; on an existing target
|
|
43905
|
-
* every other field — replaced, or not composed at all — is the NATIVE value,
|
|
43906
|
-
* valid when the device serves that cap natively (D663, rule 5).
|
|
43907
|
-
*/
|
|
43908
|
-
function selfReadProblems(read, at, fields, self) {
|
|
43909
|
-
const readKey = `${read.cap}.${read.fieldPath}`;
|
|
43910
|
-
if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
|
|
43911
|
-
if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
|
|
43912
|
-
if (isNativeCap(self.native, read.cap)) return [];
|
|
43913
|
-
return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
|
|
43914
|
-
}
|
|
43915
|
-
function sourceProblems(fields, ctx, self) {
|
|
43916
|
-
const selfKey = compositionSourceKey(self.ref);
|
|
43917
|
-
return fields.flatMap((field) => field.reads.flatMap((read) => {
|
|
43918
|
-
const at = compositionFieldAt(field);
|
|
43919
|
-
const key = compositionSourceKey(read.source);
|
|
43920
|
-
if (key === selfKey) return selfReadProblems(read, at, fields, self);
|
|
43921
|
-
const desc = ctx.sources.get(key);
|
|
43922
|
-
if (desc === void 0 || desc.kind === "unreadable") return [problem("source-unreadable", at, `could not read source \`${key}\` (${desc === void 0 ? "it was not read" : desc.error}) — the save is refused rather than stored unchecked`)];
|
|
43923
|
-
if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
|
|
43924
|
-
if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
|
|
43925
|
-
return [];
|
|
43926
|
-
}));
|
|
43927
|
-
}
|
|
43928
|
-
/**
|
|
43929
|
-
* A cycle's node keys are `compositionReadKey` strings:
|
|
43930
|
-
* `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
|
|
43931
|
-
* CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
|
|
43932
|
-
* back out so the refusal can point at that exact field, matching the
|
|
43933
|
-
* documented `features[N].fields.<path>` convention. A cap name has no `.`,
|
|
43934
|
-
* so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
|
|
43935
|
-
* (`items[desiccant].status`) is matched through `compositionFieldKey`.
|
|
43936
|
-
*/
|
|
43937
|
-
function candidateFieldOfNode(nodeKey, candidateKey) {
|
|
43938
|
-
const prefix = `${candidateKey}#`;
|
|
43939
|
-
if (!nodeKey.startsWith(prefix)) return null;
|
|
43940
|
-
const rest = nodeKey.slice(prefix.length);
|
|
43941
|
-
const dot = rest.indexOf(".");
|
|
43942
|
-
if (dot === -1) return null;
|
|
43943
|
-
return {
|
|
43944
|
-
cap: rest.slice(0, dot),
|
|
43945
|
-
fieldPath: rest.slice(dot + 1)
|
|
43946
|
-
};
|
|
43947
|
-
}
|
|
43948
|
-
/** The first node of a cycle's path that is one of THIS composition's own
|
|
43949
|
-
* planned fields, as a documented-convention path; `'features'` when the
|
|
43950
|
-
* cycle runs entirely through other stored compositions. */
|
|
43951
|
-
function cyclePath(nodePath, candidateKey, fields) {
|
|
43952
|
-
for (const nodeKey of nodePath) {
|
|
43953
|
-
const hit = candidateFieldOfNode(nodeKey, candidateKey);
|
|
43954
|
-
if (hit === null) continue;
|
|
43955
|
-
const hitKey = `${hit.cap}.${hit.fieldPath}`;
|
|
43956
|
-
const field = fields.find((f) => compositionFieldKey(f) === hitKey);
|
|
43957
|
-
if (field !== void 0) return compositionFieldAt(field);
|
|
43958
|
-
}
|
|
43959
|
-
return "features";
|
|
43960
|
-
}
|
|
43961
|
-
/** The first unread device any of `nodes` belongs to; null when none does. */
|
|
43962
|
-
function unreadDeviceOf(nodes, unread) {
|
|
43963
|
-
for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
|
|
43964
|
-
key,
|
|
43965
|
-
error
|
|
43966
|
-
};
|
|
43967
|
-
return null;
|
|
43968
|
-
}
|
|
43969
|
-
/**
|
|
43970
|
-
* Cycle check runs against the CANDIDATE (this composition, as block
|
|
43971
|
-
* `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
|
|
43972
|
-
* decided per strongly connected component (`findCandidateCycles`): a cyclic
|
|
43973
|
-
* component holding a candidate node refuses, with one representative cycle
|
|
43974
|
-
* through it; one without is not this save's cycle. A
|
|
43975
|
-
* stored composition of the same block is its old version; another block on
|
|
43976
|
-
* the same device, or two stored peers sharing a device, make
|
|
43977
|
-
* `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
|
|
43978
|
-
* `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
|
|
43979
|
-
*/
|
|
43980
|
-
function cycleProblems(composition, ctx, fields, self) {
|
|
43981
|
-
const candidate = {
|
|
43982
|
-
blockId: ctx.blockId ?? "(unsaved)",
|
|
43983
|
-
target: self.ref,
|
|
43984
|
-
composition,
|
|
43985
|
-
replaced: self.replaced
|
|
43986
|
-
};
|
|
43987
|
-
const candidateKey = compositionSourceKey(candidate.target);
|
|
43988
|
-
const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
|
|
43989
|
-
try {
|
|
43990
|
-
return findCandidateCycles(candidate, ctx.peers).map((c) => {
|
|
43991
|
-
const at = cyclePath(c.path, candidateKey, fields);
|
|
43992
|
-
const through = unreadDeviceOf([...c.component], unread);
|
|
43993
|
-
return through === null ? problem("cycle", at, `a field reads itself: ${c.path.join(" → ")}`) : problem("cycle", at, `possible cycle through \`${through.key}\` — its state could not be read (${through.error}): ${c.path.join(" → ")}`);
|
|
43994
|
-
});
|
|
43995
|
-
} catch (err) {
|
|
43996
|
-
if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
|
|
43997
|
-
throw err;
|
|
43998
|
-
}
|
|
43999
|
-
}
|
|
44000
|
-
function validateComposition(composition, ctx) {
|
|
44001
|
-
const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
|
|
44002
|
-
const self = candidateSelf(composition, ctx, plan);
|
|
44003
|
-
const cycles = cycleProblems(composition, ctx, plan.fields, self);
|
|
44004
|
-
const problems = [
|
|
44005
|
-
...plan.problems,
|
|
44006
|
-
...sourceProblems(plan.fields, ctx, self),
|
|
44007
|
-
...cycles
|
|
44008
|
-
];
|
|
44009
|
-
return {
|
|
44010
|
-
ok: problems.length === 0,
|
|
44011
|
-
problems,
|
|
44012
|
-
fields: [...plan.fields],
|
|
44013
|
-
features: [...plan.features],
|
|
44014
|
-
events: [...plan.events]
|
|
44445
|
+
kind: "fields",
|
|
44446
|
+
cap: "doorbell",
|
|
44447
|
+
enabled: input.enabled,
|
|
44448
|
+
fields: {
|
|
44449
|
+
lastPressedAt: {
|
|
44450
|
+
kind: "expression",
|
|
44451
|
+
expr: `latest(${sources.map((_, index) => `rose(active${index}, changed${index}, ${DOORBELL_RISE_DEBOUNCE_MS})`).join(", ")})`,
|
|
44452
|
+
bindings: edgeBindings
|
|
44453
|
+
},
|
|
44454
|
+
pressCountSinceStart: {
|
|
44455
|
+
kind: "expression",
|
|
44456
|
+
expr: "count(lastPressedAt)",
|
|
44457
|
+
bindings: { lastPressedAt: {
|
|
44458
|
+
kind: "from",
|
|
44459
|
+
source: {
|
|
44460
|
+
addonId: input.camera.addonId,
|
|
44461
|
+
stableId: input.camera.stableId
|
|
44462
|
+
},
|
|
44463
|
+
cap: "doorbell",
|
|
44464
|
+
fieldPath: "lastPressedAt"
|
|
44465
|
+
} }
|
|
44466
|
+
}
|
|
44467
|
+
}
|
|
44015
44468
|
};
|
|
44016
44469
|
}
|
|
44017
44470
|
/**
|
|
@@ -48031,6 +48484,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
48031
48484
|
addonId: null,
|
|
48032
48485
|
access: "view"
|
|
48033
48486
|
},
|
|
48487
|
+
"coreBlocks.listComposableCaps": {
|
|
48488
|
+
capName: "core-blocks",
|
|
48489
|
+
capScope: "system",
|
|
48490
|
+
addonId: null,
|
|
48491
|
+
access: "view"
|
|
48492
|
+
},
|
|
48034
48493
|
"coreBlocks.listCompositions": {
|
|
48035
48494
|
capName: "core-blocks",
|
|
48036
48495
|
capScope: "system",
|
|
@@ -51715,6 +52174,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
51715
52174
|
addonId: null,
|
|
51716
52175
|
access: "create"
|
|
51717
52176
|
},
|
|
52177
|
+
"pipelineOrchestrator.setCameraSensors": {
|
|
52178
|
+
capName: "pipeline-orchestrator",
|
|
52179
|
+
capScope: "system",
|
|
52180
|
+
addonId: null,
|
|
52181
|
+
access: "create"
|
|
52182
|
+
},
|
|
51718
52183
|
"pipelineOrchestrator.setCameraStepOverride": {
|
|
51719
52184
|
capName: "pipeline-orchestrator",
|
|
51720
52185
|
capScope: "system",
|
|
@@ -55425,6 +55890,11 @@ Object.freeze({
|
|
|
55425
55890
|
form: "single",
|
|
55426
55891
|
optional: false
|
|
55427
55892
|
}],
|
|
55893
|
+
"pipelineOrchestrator.setCameraSensors": [{
|
|
55894
|
+
name: "deviceId",
|
|
55895
|
+
form: "single",
|
|
55896
|
+
optional: false
|
|
55897
|
+
}],
|
|
55428
55898
|
"pipelineOrchestrator.setCameraStepOverride": [{
|
|
55429
55899
|
name: "deviceId",
|
|
55430
55900
|
form: "single",
|
|
@@ -57780,18 +58250,6 @@ Object.defineProperty(exports, "SOURCE_CAPS", {
|
|
|
57780
58250
|
return SOURCE_CAPS;
|
|
57781
58251
|
}
|
|
57782
58252
|
});
|
|
57783
|
-
Object.defineProperty(exports, "SOURCE_CAP_ACTIVE_FIELD", {
|
|
57784
|
-
enumerable: true,
|
|
57785
|
-
get: function() {
|
|
57786
|
-
return SOURCE_CAP_ACTIVE_FIELD;
|
|
57787
|
-
}
|
|
57788
|
-
});
|
|
57789
|
-
Object.defineProperty(exports, "SOURCE_CAP_CHANGED_AT_FIELD", {
|
|
57790
|
-
enumerable: true,
|
|
57791
|
-
get: function() {
|
|
57792
|
-
return SOURCE_CAP_CHANGED_AT_FIELD;
|
|
57793
|
-
}
|
|
57794
|
-
});
|
|
57795
58253
|
Object.defineProperty(exports, "STORAGE_ACCESS_FALLBACK", {
|
|
57796
58254
|
enumerable: true,
|
|
57797
58255
|
get: function() {
|
|
@@ -57918,6 +58376,12 @@ Object.defineProperty(exports, "buildCapabilityLookup", {
|
|
|
57918
58376
|
return buildCapabilityLookup;
|
|
57919
58377
|
}
|
|
57920
58378
|
});
|
|
58379
|
+
Object.defineProperty(exports, "buildDoorbellCompositionFeature", {
|
|
58380
|
+
enumerable: true,
|
|
58381
|
+
get: function() {
|
|
58382
|
+
return buildDoorbellCompositionFeature;
|
|
58383
|
+
}
|
|
58384
|
+
});
|
|
57921
58385
|
Object.defineProperty(exports, "buildFfmpegArgs", {
|
|
57922
58386
|
enumerable: true,
|
|
57923
58387
|
get: function() {
|
|
@@ -58080,6 +58544,12 @@ Object.defineProperty(exports, "deriveBatteryPresence", {
|
|
|
58080
58544
|
return deriveBatteryPresence;
|
|
58081
58545
|
}
|
|
58082
58546
|
});
|
|
58547
|
+
Object.defineProperty(exports, "describeComposableCaps", {
|
|
58548
|
+
enumerable: true,
|
|
58549
|
+
get: function() {
|
|
58550
|
+
return describeComposableCaps;
|
|
58551
|
+
}
|
|
58552
|
+
});
|
|
58083
58553
|
Object.defineProperty(exports, "deviceExtensionCapability", {
|
|
58084
58554
|
enumerable: true,
|
|
58085
58555
|
get: function() {
|
|
@@ -58110,6 +58580,12 @@ Object.defineProperty(exports, "doorbellCapability", {
|
|
|
58110
58580
|
return doorbellCapability;
|
|
58111
58581
|
}
|
|
58112
58582
|
});
|
|
58583
|
+
Object.defineProperty(exports, "doorbellFeatureSources", {
|
|
58584
|
+
enumerable: true,
|
|
58585
|
+
get: function() {
|
|
58586
|
+
return doorbellFeatureSources;
|
|
58587
|
+
}
|
|
58588
|
+
});
|
|
58113
58589
|
Object.defineProperty(exports, "emitDownForOwnedCaps", {
|
|
58114
58590
|
enumerable: true,
|
|
58115
58591
|
get: function() {
|