@camstack/system 1.2.307 → 1.2.309
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/composed-device.d.ts +26 -0
- package/dist/builtins/composer/composer.addon.d.ts +12 -0
- package/dist/builtins/composer/composer.addon.js +1682 -0
- package/dist/builtins/composer/composer.addon.mjs +1677 -0
- package/dist/builtins/composer/composer.d.ts +103 -0
- package/dist/builtins/composer/composition-declarations.d.ts +53 -0
- package/dist/builtins/composer/composition-runtime.d.ts +88 -0
- package/dist/builtins/composer/deadline-timer.d.ts +18 -0
- package/dist/builtins/composer/index.d.ts +1 -0
- package/dist/builtins/composer/index.js +8 -0
- package/dist/builtins/composer/index.mjs +2 -0
- package/dist/builtins/composer/source-tracker.d.ts +78 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/block-supervisor.d.ts +32 -1
- package/dist/builtins/core-blocks/blocks-integration.d.ts +5 -0
- package/dist/builtins/core-blocks/composition-api.d.ts +25 -0
- package/dist/builtins/core-blocks/composition-sources.d.ts +10 -0
- package/dist/builtins/core-blocks/core-block-store.d.ts +34 -2
- package/dist/builtins/core-blocks/core-blocks.addon.d.ts +6 -0
- package/dist/builtins/core-blocks/core-blocks.addon.js +504 -11
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +504 -11
- 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/virtual-doorbell.addon.js +10 -31
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +9 -30
- 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/shared/device-state-changed.d.ts +17 -0
- 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/device-state-changed-Bk7cvTnz.js +31 -0
- package/dist/device-state-changed-DoBbkyEI.mjs +20 -0
- package/dist/{dist-DdfJa8Zn.mjs → dist-BEuQPDk_.mjs} +1333 -83
- package/dist/{dist-D3ch99WD.js → dist-BhO9eYKO.js} +1422 -82
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.mjs +4 -2
- package/dist/{retired-settings-keys-BJNhhXap.mjs → retired-settings-keys-CQvG6B-Q.mjs} +1 -1
- package/dist/{retired-settings-keys-DG_LEPqC.js → retired-settings-keys-DHgnJqS3.js} +1 -1
- package/package.json +20 -1
|
@@ -6549,6 +6549,49 @@ var ModelProviderIdSchema = zod.z.enum([
|
|
|
6549
6549
|
"scrypted",
|
|
6550
6550
|
"custom"
|
|
6551
6551
|
]);
|
|
6552
|
+
var ModelLicenseRecordSchema = zod.z.object({
|
|
6553
|
+
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
6554
|
+
weights: zod.z.string().min(1),
|
|
6555
|
+
/** SPDX id of the upstream code that defines / trained the network. */
|
|
6556
|
+
code: zod.z.string().min(1).optional(),
|
|
6557
|
+
/** Training data whose terms add obligations (attribution, non-commercial). */
|
|
6558
|
+
data: zod.z.object({
|
|
6559
|
+
name: zod.z.string().min(1),
|
|
6560
|
+
terms: zod.z.string().min(1),
|
|
6561
|
+
url: zod.z.string().url().optional()
|
|
6562
|
+
}).optional(),
|
|
6563
|
+
/** The project the weights come from. Required on built-in entries (guard). */
|
|
6564
|
+
upstream: zod.z.object({
|
|
6565
|
+
name: zod.z.string().min(1),
|
|
6566
|
+
url: zod.z.string().url()
|
|
6567
|
+
}).optional(),
|
|
6568
|
+
/** Where the licence text governing the weights is published. */
|
|
6569
|
+
url: zod.z.string().url().optional(),
|
|
6570
|
+
/** Attribution the licence REQUIRES, verbatim. */
|
|
6571
|
+
attribution: zod.z.string().min(1).optional(),
|
|
6572
|
+
/** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
|
|
6573
|
+
modifications: zod.z.string().min(1).optional(),
|
|
6574
|
+
/** Who serves the file a node downloads. */
|
|
6575
|
+
hosting: zod.z.enum([
|
|
6576
|
+
"camstack-hf",
|
|
6577
|
+
"third-party",
|
|
6578
|
+
"built-in"
|
|
6579
|
+
]).optional(),
|
|
6580
|
+
/** True when the weights or their data forbid commercial use. */
|
|
6581
|
+
nonCommercial: zod.z.boolean().optional()
|
|
6582
|
+
});
|
|
6583
|
+
/**
|
|
6584
|
+
* The licence of a model, as a record rather than a bare SPDX string: a
|
|
6585
|
+
* permissive CODE licence routinely sits on top of restrictive WEIGHTS or
|
|
6586
|
+
* DATA, and the obligations (attribution, modification notices, source offers)
|
|
6587
|
+
* are per upstream. A persisted custom-model row written when this field was a
|
|
6588
|
+
* string still parses — the string is lifted into `{ weights }`.
|
|
6589
|
+
*
|
|
6590
|
+
* Every licence surface (the admin "Models and licenses" page,
|
|
6591
|
+
* `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
|
|
6592
|
+
* these records by `scripts/gen-model-licenses.ts` (D661).
|
|
6593
|
+
*/
|
|
6594
|
+
var ModelLicenseSchema = zod.z.preprocess((value) => typeof value === "string" ? { weights: value } : value, ModelLicenseRecordSchema);
|
|
6552
6595
|
var ModelCatalogEntrySchema = zod.z.object({
|
|
6553
6596
|
id: zod.z.string(),
|
|
6554
6597
|
name: zod.z.string(),
|
|
@@ -6637,11 +6680,11 @@ var ModelCatalogEntrySchema = zod.z.object({
|
|
|
6637
6680
|
p95LatencyMs: zod.z.record(zod.z.string(), zod.z.number()).optional()
|
|
6638
6681
|
}).optional(),
|
|
6639
6682
|
/**
|
|
6640
|
-
*
|
|
6641
|
-
*
|
|
6642
|
-
*
|
|
6683
|
+
* The model's licence record — see {@link ModelLicenseSchema}. Optional in
|
|
6684
|
+
* the schema (a custom model's author may state none); REQUIRED and complete
|
|
6685
|
+
* on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
|
|
6643
6686
|
*/
|
|
6644
|
-
license:
|
|
6687
|
+
license: ModelLicenseSchema.optional(),
|
|
6645
6688
|
/**
|
|
6646
6689
|
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
6647
6690
|
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
@@ -6755,23 +6798,6 @@ var ExpressionEvalError = class extends Error {
|
|
|
6755
6798
|
this.name = "ExpressionEvalError";
|
|
6756
6799
|
}
|
|
6757
6800
|
};
|
|
6758
|
-
/**
|
|
6759
|
-
* Frozen, null-prototype builtin function table for the expression engine
|
|
6760
|
-
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
6761
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
6762
|
-
* own-property check against it.
|
|
6763
|
-
*
|
|
6764
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
6765
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
6766
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
6767
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
6768
|
-
* callable — they are simply "unknown function" at parse time.
|
|
6769
|
-
*
|
|
6770
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
6771
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
6772
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
6773
|
-
* closed rather than emitting a garbage value.
|
|
6774
|
-
*/
|
|
6775
6801
|
function asFiniteNumber(value, name, index) {
|
|
6776
6802
|
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
6777
6803
|
return value;
|
|
@@ -6787,6 +6813,10 @@ function finiteResult(value, name) {
|
|
|
6787
6813
|
function allFiniteNumbers(args, name) {
|
|
6788
6814
|
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
6789
6815
|
}
|
|
6816
|
+
function asBoolean$1(value, name, index) {
|
|
6817
|
+
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
6818
|
+
return value;
|
|
6819
|
+
}
|
|
6790
6820
|
var INF = Number.POSITIVE_INFINITY;
|
|
6791
6821
|
var table = {
|
|
6792
6822
|
min: {
|
|
@@ -6887,9 +6917,44 @@ var table = {
|
|
|
6887
6917
|
if (from === to) return x;
|
|
6888
6918
|
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
6889
6919
|
}
|
|
6920
|
+
},
|
|
6921
|
+
any: {
|
|
6922
|
+
minArgs: 1,
|
|
6923
|
+
maxArgs: INF,
|
|
6924
|
+
apply: (args) => args.map((a, i) => asBoolean$1(a, "any", i)).some((b) => b)
|
|
6925
|
+
},
|
|
6926
|
+
all: {
|
|
6927
|
+
minArgs: 1,
|
|
6928
|
+
maxArgs: INF,
|
|
6929
|
+
apply: (args) => args.map((a, i) => asBoolean$1(a, "all", i)).every((b) => b)
|
|
6930
|
+
},
|
|
6931
|
+
within: {
|
|
6932
|
+
minArgs: 2,
|
|
6933
|
+
maxArgs: 2,
|
|
6934
|
+
apply: (args, hooks) => {
|
|
6935
|
+
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
6936
|
+
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
6937
|
+
const at = args[0];
|
|
6938
|
+
if (at === null) return false;
|
|
6939
|
+
const ts = asFiniteNumber(at, "within", 0);
|
|
6940
|
+
const now = hooks.now;
|
|
6941
|
+
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
6942
|
+
const inside = now - ts <= windowMs;
|
|
6943
|
+
if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
|
|
6944
|
+
return inside;
|
|
6945
|
+
}
|
|
6946
|
+
},
|
|
6947
|
+
latest: {
|
|
6948
|
+
minArgs: 1,
|
|
6949
|
+
maxArgs: INF,
|
|
6950
|
+
apply: (args) => {
|
|
6951
|
+
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
|
|
6952
|
+
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
6953
|
+
}
|
|
6890
6954
|
}
|
|
6891
6955
|
};
|
|
6892
|
-
|
|
6956
|
+
/** Frozen, null-prototype builtin table. */
|
|
6957
|
+
var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
|
|
6893
6958
|
/** The set of valid builtin names — used by the parser to reject unknown
|
|
6894
6959
|
* callees at parse time (immediate author feedback). */
|
|
6895
6960
|
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
@@ -7302,7 +7367,118 @@ function getCached(source) {
|
|
|
7302
7367
|
function compileExpressionSafe(source) {
|
|
7303
7368
|
return getCached(source);
|
|
7304
7369
|
}
|
|
7305
|
-
|
|
7370
|
+
/**
|
|
7371
|
+
* Tree-walking evaluator for the safe expression mini-language.
|
|
7372
|
+
*
|
|
7373
|
+
* SECURITY (spec §4 rule 2/5):
|
|
7374
|
+
* - The scope is an `Object.create(null)` copy of ONLY the caller's own
|
|
7375
|
+
* enumerable binding entries, so `name in scope` is a pure own-key check and
|
|
7376
|
+
* `constructor` / `__proto__` / `toString` are plain unknown identifiers.
|
|
7377
|
+
* - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
|
|
7378
|
+
* directly — the only external calls are into the frozen builtin table.
|
|
7379
|
+
* - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
|
|
7380
|
+
* construction; the step counter is defense-in-depth for a crafted max-size
|
|
7381
|
+
* AST. Nothing blocks: there are no timers, awaits or unbounded loops.
|
|
7382
|
+
*/
|
|
7383
|
+
var EMPTY_HOOKS = Object.freeze({});
|
|
7384
|
+
/** Build a null-prototype scope from own-enumerable binding entries. Inherited
|
|
7385
|
+
* keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
|
|
7386
|
+
* so nothing smuggles in via the prototype chain. */
|
|
7387
|
+
function createExpressionScope(bindings) {
|
|
7388
|
+
const scope = Object.create(null);
|
|
7389
|
+
for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
|
|
7390
|
+
return scope;
|
|
7391
|
+
}
|
|
7392
|
+
function isFiniteNumber(value) {
|
|
7393
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
7394
|
+
}
|
|
7395
|
+
/** JS truthiness of a primitive value. */
|
|
7396
|
+
function truthy(value) {
|
|
7397
|
+
return Boolean(value);
|
|
7398
|
+
}
|
|
7399
|
+
function requireFinite(value, context) {
|
|
7400
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
|
|
7401
|
+
return value;
|
|
7402
|
+
}
|
|
7403
|
+
function step(ctx) {
|
|
7404
|
+
ctx.steps += 1;
|
|
7405
|
+
if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
|
|
7406
|
+
}
|
|
7407
|
+
function evalNode(node, ctx) {
|
|
7408
|
+
step(ctx);
|
|
7409
|
+
switch (node.kind) {
|
|
7410
|
+
case "literal": return node.value;
|
|
7411
|
+
case "identifier":
|
|
7412
|
+
if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
|
|
7413
|
+
return ctx.scope[node.name];
|
|
7414
|
+
case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
|
|
7415
|
+
case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
|
|
7416
|
+
case "logical": {
|
|
7417
|
+
const left = evalNode(node.left, ctx);
|
|
7418
|
+
if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
|
|
7419
|
+
return truthy(left) ? left : evalNode(node.right, ctx);
|
|
7420
|
+
}
|
|
7421
|
+
case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
7422
|
+
case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
function evalUnary(op, operand) {
|
|
7426
|
+
if (op === "!") return !truthy(operand);
|
|
7427
|
+
if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
|
|
7428
|
+
return requireFinite(-operand, "unary \"-\"");
|
|
7429
|
+
}
|
|
7430
|
+
function evalBinary(op, left, right) {
|
|
7431
|
+
switch (op) {
|
|
7432
|
+
case "==": return left === right;
|
|
7433
|
+
case "!=": return left !== right;
|
|
7434
|
+
case "+":
|
|
7435
|
+
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
7436
|
+
if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
|
|
7437
|
+
throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
|
|
7438
|
+
case "-":
|
|
7439
|
+
case "*":
|
|
7440
|
+
case "/":
|
|
7441
|
+
case "%":
|
|
7442
|
+
if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
|
|
7443
|
+
return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
|
|
7444
|
+
case "<":
|
|
7445
|
+
case "<=":
|
|
7446
|
+
case ">":
|
|
7447
|
+
case ">=":
|
|
7448
|
+
if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
7449
|
+
if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
7450
|
+
throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
|
|
7451
|
+
}
|
|
7452
|
+
}
|
|
7453
|
+
function evalCall(callee, args, hooks) {
|
|
7454
|
+
if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
|
|
7455
|
+
const builtin = EXPRESSION_BUILTINS[callee];
|
|
7456
|
+
if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
|
|
7457
|
+
return builtin.apply(args, hooks);
|
|
7458
|
+
}
|
|
7459
|
+
/** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
|
|
7460
|
+
* runtime failure (unknown identifier, type mismatch, non-finite result,
|
|
7461
|
+
* step-budget exhaustion). */
|
|
7462
|
+
function evaluateAst(node, scope, opts) {
|
|
7463
|
+
return evalNode(node, {
|
|
7464
|
+
scope,
|
|
7465
|
+
hooks: opts?.hooks ?? EMPTY_HOOKS,
|
|
7466
|
+
maxSteps: opts?.maxSteps ?? 4096,
|
|
7467
|
+
steps: 0
|
|
7468
|
+
});
|
|
7469
|
+
}
|
|
7470
|
+
/**
|
|
7471
|
+
* Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
|
|
7472
|
+
* Non-primitive values (objects, arrays, `undefined`, functions, bigint,
|
|
7473
|
+
* symbol) and non-finite numbers become `undefined` so the caller can apply
|
|
7474
|
+
* its binding-miss policy (→ `null`). `null` itself is a valid value.
|
|
7475
|
+
*/
|
|
7476
|
+
function toExpressionValue(raw) {
|
|
7477
|
+
if (raw === null) return null;
|
|
7478
|
+
if (typeof raw === "string") return raw;
|
|
7479
|
+
if (typeof raw === "boolean") return raw;
|
|
7480
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
7481
|
+
}
|
|
7306
7482
|
/**
|
|
7307
7483
|
* Author-time validation. Returns `null` when the source is valid, else a
|
|
7308
7484
|
* human-readable error message. Checks: the expression compiles; binding count
|
|
@@ -7328,6 +7504,42 @@ function validateExpressionSource(src) {
|
|
|
7328
7504
|
return null;
|
|
7329
7505
|
}
|
|
7330
7506
|
/**
|
|
7507
|
+
* Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
|
|
7508
|
+
* plus the injected `now` (supplied by the caller for determinism and
|
|
7509
|
+
* testability), compiles via the LRU, and evaluates. Any failure (parse or
|
|
7510
|
+
* eval) returns `{ ok: false }` — the caller treats that as "skip this
|
|
7511
|
+
* derivation", never as a throw that takes the pass down.
|
|
7512
|
+
*/
|
|
7513
|
+
function evaluateExpressionSource(expr, bindingValues, now, opts) {
|
|
7514
|
+
const compiled = compileExpressionSafe(expr);
|
|
7515
|
+
if (!compiled.ok) return {
|
|
7516
|
+
ok: false,
|
|
7517
|
+
error: compiled.error
|
|
7518
|
+
};
|
|
7519
|
+
const scope = createExpressionScope({
|
|
7520
|
+
...bindingValues,
|
|
7521
|
+
["now"]: now
|
|
7522
|
+
});
|
|
7523
|
+
const hooks = {
|
|
7524
|
+
...opts?.hooks,
|
|
7525
|
+
now
|
|
7526
|
+
};
|
|
7527
|
+
try {
|
|
7528
|
+
return {
|
|
7529
|
+
ok: true,
|
|
7530
|
+
value: evaluateAst(compiled.parsed.ast, scope, {
|
|
7531
|
+
...opts,
|
|
7532
|
+
hooks
|
|
7533
|
+
})
|
|
7534
|
+
};
|
|
7535
|
+
} catch (err) {
|
|
7536
|
+
return {
|
|
7537
|
+
ok: false,
|
|
7538
|
+
error: err instanceof ExpressionEvalError ? err.message : String(err)
|
|
7539
|
+
};
|
|
7540
|
+
}
|
|
7541
|
+
}
|
|
7542
|
+
/**
|
|
7331
7543
|
* What an expression's named bindings READ from.
|
|
7332
7544
|
*
|
|
7333
7545
|
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
@@ -7527,7 +7739,7 @@ var AutomationActionSchema = zod.z.discriminatedUnion("kind", [
|
|
|
7527
7739
|
code: zod.z.string().min(1).max(2e4)
|
|
7528
7740
|
})
|
|
7529
7741
|
]);
|
|
7530
|
-
zod.z.object({
|
|
7742
|
+
var AutomationRecipeSchema = zod.z.object({
|
|
7531
7743
|
triggers: zod.z.array(AutomationTriggerSchema),
|
|
7532
7744
|
conditions: AutomationConditionSchema.optional(),
|
|
7533
7745
|
actions: zod.z.array(AutomationActionSchema)
|
|
@@ -10551,6 +10763,270 @@ function kebabToCamel(s) {
|
|
|
10551
10763
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
10552
10764
|
}
|
|
10553
10765
|
/**
|
|
10766
|
+
* A COMPOSITION: a device assembled from other devices' state (D659).
|
|
10767
|
+
*
|
|
10768
|
+
* The data half of the composed-devices spec §1. A composition is stored inside
|
|
10769
|
+
* a core block (`CoreBlock.source.kind === 'composition'`) and run by the shared
|
|
10770
|
+
* `composer` runner. It names a TARGET and, for each capability of the target, a
|
|
10771
|
+
* FIELD MAP: every state field has exactly one source.
|
|
10772
|
+
*
|
|
10773
|
+
* Sources are keyed by `(addonId, stableId)`, NEVER by numeric device id. A
|
|
10774
|
+
* resync reissues numeric ids, and a composition that stored them would be
|
|
10775
|
+
* orphaned by the first re-adoption (the linked-devices lesson). A `stableId` is
|
|
10776
|
+
* unique only within its owning addon, so the owner travels with it.
|
|
10777
|
+
*
|
|
10778
|
+
* Later slices EXTEND these unions and do not reshape them. The variants a later
|
|
10779
|
+
* slice implements are already in the wire shape, and `validateComposition`
|
|
10780
|
+
* refuses them BY NAME until then:
|
|
10781
|
+
* target `existing` → slice 2 · feature `passthrough` → slice 3 ·
|
|
10782
|
+
* `commands` (`forward`) → slice 3 · source `code` → slice 4.
|
|
10783
|
+
*/
|
|
10784
|
+
/** The owner of every composed device: the `composer` builtin. */
|
|
10785
|
+
var COMPOSER_ADDON_ID = "composer";
|
|
10786
|
+
/** A composed device's stableId is this prefix plus the block id. */
|
|
10787
|
+
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
10788
|
+
/** Longest snippet a `code` source may carry (slice 4). */
|
|
10789
|
+
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
10790
|
+
var CompositionSourceRefSchema = zod.z.object({
|
|
10791
|
+
/** The owning addon; `stableId` is unique only within it. */
|
|
10792
|
+
addonId: zod.z.string().min(1),
|
|
10793
|
+
stableId: zod.z.string().min(1)
|
|
10794
|
+
});
|
|
10795
|
+
/** One field of one capability of one source device. */
|
|
10796
|
+
var CompositionFieldReadSchema = zod.z.object({
|
|
10797
|
+
source: CompositionSourceRefSchema,
|
|
10798
|
+
cap: zod.z.string().min(1),
|
|
10799
|
+
/** Dotted path into the source cap's runtime-state slice. */
|
|
10800
|
+
fieldPath: zod.z.string().min(1)
|
|
10801
|
+
});
|
|
10802
|
+
/** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
|
|
10803
|
+
var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: zod.z.literal("from") });
|
|
10804
|
+
/** A constant binding. No device is read. */
|
|
10805
|
+
var CompositionLiteralBindingSchema = zod.z.object({
|
|
10806
|
+
kind: zod.z.literal("literal"),
|
|
10807
|
+
value: zod.z.union([
|
|
10808
|
+
zod.z.string(),
|
|
10809
|
+
zod.z.number(),
|
|
10810
|
+
zod.z.boolean(),
|
|
10811
|
+
zod.z.null()
|
|
10812
|
+
])
|
|
10813
|
+
});
|
|
10814
|
+
var CompositionBindingSchema = zod.z.discriminatedUnion("kind", [CompositionFromSourceSchema, CompositionLiteralBindingSchema]);
|
|
10815
|
+
/**
|
|
10816
|
+
* `expression`: a formula over named bindings, in the salvaged expression
|
|
10817
|
+
* engine. Validated at parse by the SAME function every other consumer runs,
|
|
10818
|
+
* so the editor and the store cannot disagree.
|
|
10819
|
+
*/
|
|
10820
|
+
var CompositionExpressionSourceSchema = zod.z.object({
|
|
10821
|
+
kind: zod.z.literal("expression"),
|
|
10822
|
+
expr: zod.z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
10823
|
+
bindings: zod.z.record(zod.z.string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
|
|
10824
|
+
}).superRefine((src, ctx) => {
|
|
10825
|
+
const err = validateExpressionSource(src);
|
|
10826
|
+
if (err !== null) ctx.addIssue({
|
|
10827
|
+
code: "custom",
|
|
10828
|
+
message: err,
|
|
10829
|
+
path: ["expr"]
|
|
10830
|
+
});
|
|
10831
|
+
});
|
|
10832
|
+
/** `code`: RESERVED for slice 4 (own runner, one-way eject). */
|
|
10833
|
+
var CompositionCodeSourceSchema = zod.z.object({
|
|
10834
|
+
kind: zod.z.literal("code"),
|
|
10835
|
+
code: zod.z.string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
|
|
10836
|
+
});
|
|
10837
|
+
var CompositionFieldSourceSchema = zod.z.discriminatedUnion("kind", [
|
|
10838
|
+
CompositionFromSourceSchema,
|
|
10839
|
+
CompositionExpressionSourceSchema,
|
|
10840
|
+
CompositionCodeSourceSchema
|
|
10841
|
+
]);
|
|
10842
|
+
/** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
|
|
10843
|
+
var CompositionForwardCommandSchema = zod.z.object({
|
|
10844
|
+
kind: zod.z.literal("forward"),
|
|
10845
|
+
source: CompositionSourceRefSchema,
|
|
10846
|
+
cap: zod.z.string().min(1),
|
|
10847
|
+
method: zod.z.string().min(1)
|
|
10848
|
+
});
|
|
10849
|
+
var CompositionCommandTargetSchema = zod.z.discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
|
|
10850
|
+
var CompositionFieldsFeatureSchema = zod.z.object({
|
|
10851
|
+
kind: zod.z.literal("fields"),
|
|
10852
|
+
cap: zod.z.string().min(1),
|
|
10853
|
+
fields: zod.z.record(zod.z.string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
|
|
10854
|
+
/** RESERVED for slice 3; refused by the validator when non-empty. */
|
|
10855
|
+
commands: zod.z.record(zod.z.string().min(1), CompositionCommandTargetSchema).optional()
|
|
10856
|
+
});
|
|
10857
|
+
/** RESERVED for slice 3: a media / data-plane cap, whole, from one source. */
|
|
10858
|
+
var CompositionPassthroughFeatureSchema = zod.z.object({
|
|
10859
|
+
kind: zod.z.literal("passthrough"),
|
|
10860
|
+
cap: zod.z.string().min(1),
|
|
10861
|
+
source: CompositionSourceRefSchema
|
|
10862
|
+
});
|
|
10863
|
+
var CompositionFeatureSchema = zod.z.discriminatedUnion("kind", [CompositionFieldsFeatureSchema, CompositionPassthroughFeatureSchema]);
|
|
10864
|
+
/**
|
|
10865
|
+
* A NEW device. It has no `name` of its own: the block's name IS the device's
|
|
10866
|
+
* name (2026-08-08). A second copy here would be a second writer of one string (D62).
|
|
10867
|
+
*/
|
|
10868
|
+
var CompositionNewTargetSchema = zod.z.object({
|
|
10869
|
+
kind: zod.z.literal("new"),
|
|
10870
|
+
type: zod.z.enum(DeviceType),
|
|
10871
|
+
role: zod.z.enum(DeviceRole).optional()
|
|
10872
|
+
});
|
|
10873
|
+
/** RESERVED for slice 2: customize an existing device. */
|
|
10874
|
+
var CompositionExistingTargetSchema = zod.z.object({
|
|
10875
|
+
kind: zod.z.literal("existing"),
|
|
10876
|
+
device: CompositionSourceRefSchema
|
|
10877
|
+
});
|
|
10878
|
+
var CompositionTargetSchema = zod.z.discriminatedUnion("kind", [CompositionNewTargetSchema, CompositionExistingTargetSchema]);
|
|
10879
|
+
var CompositionSchema = zod.z.object({
|
|
10880
|
+
target: CompositionTargetSchema,
|
|
10881
|
+
features: zod.z.array(CompositionFeatureSchema).min(1).max(16)
|
|
10882
|
+
}).superRefine((composition, ctx) => {
|
|
10883
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10884
|
+
composition.features.forEach((feature, index) => {
|
|
10885
|
+
if (seen.has(feature.cap)) ctx.addIssue({
|
|
10886
|
+
code: "custom",
|
|
10887
|
+
message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
|
|
10888
|
+
path: [
|
|
10889
|
+
"features",
|
|
10890
|
+
index,
|
|
10891
|
+
"cap"
|
|
10892
|
+
]
|
|
10893
|
+
});
|
|
10894
|
+
seen.add(feature.cap);
|
|
10895
|
+
});
|
|
10896
|
+
});
|
|
10897
|
+
function compositionSourceKey(ref) {
|
|
10898
|
+
return `${ref.addonId}/${ref.stableId}`;
|
|
10899
|
+
}
|
|
10900
|
+
function compositionSliceKey(ref, cap) {
|
|
10901
|
+
return `${compositionSourceKey(ref)}#${cap}`;
|
|
10902
|
+
}
|
|
10903
|
+
function compositionReadKey(read) {
|
|
10904
|
+
return `${compositionSliceKey(read.source, read.cap)}.${read.fieldPath}`;
|
|
10905
|
+
}
|
|
10906
|
+
function composedDeviceStableId(blockId) {
|
|
10907
|
+
return `${COMPOSED_DEVICE_STABLE_ID_PREFIX}${blockId}`;
|
|
10908
|
+
}
|
|
10909
|
+
function composedDeviceRef(blockId) {
|
|
10910
|
+
return {
|
|
10911
|
+
addonId: COMPOSER_ADDON_ID,
|
|
10912
|
+
stableId: composedDeviceStableId(blockId)
|
|
10913
|
+
};
|
|
10914
|
+
}
|
|
10915
|
+
/** Every device field a source reads: `from` is one read, an expression is its `from` bindings. */
|
|
10916
|
+
function fieldReads(source) {
|
|
10917
|
+
switch (source.kind) {
|
|
10918
|
+
case "from": return [{
|
|
10919
|
+
source: source.source,
|
|
10920
|
+
cap: source.cap,
|
|
10921
|
+
fieldPath: source.fieldPath
|
|
10922
|
+
}];
|
|
10923
|
+
case "expression": return Object.values(source.bindings).flatMap((binding) => binding.kind === "from" ? [{
|
|
10924
|
+
source: binding.source,
|
|
10925
|
+
cap: binding.cap,
|
|
10926
|
+
fieldPath: binding.fieldPath
|
|
10927
|
+
}] : []);
|
|
10928
|
+
case "code": return [];
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
/**
|
|
10932
|
+
* What the validator says about a composition and what the composer says about
|
|
10933
|
+
* a running one: the wire shapes the API returns (D659).
|
|
10934
|
+
*/
|
|
10935
|
+
var CompositionProblemCodeSchema = zod.z.enum([
|
|
10936
|
+
"reserved-variant",
|
|
10937
|
+
"unknown-capability",
|
|
10938
|
+
"not-composable",
|
|
10939
|
+
"wrong-device-type",
|
|
10940
|
+
"has-commands",
|
|
10941
|
+
"unknown-field",
|
|
10942
|
+
"nested-field",
|
|
10943
|
+
"missing-field",
|
|
10944
|
+
"kind-mismatch",
|
|
10945
|
+
"may-be-null",
|
|
10946
|
+
"source-missing",
|
|
10947
|
+
"source-unreadable",
|
|
10948
|
+
"source-capability-missing",
|
|
10949
|
+
"source-field-unknown",
|
|
10950
|
+
"expression-invalid",
|
|
10951
|
+
"cycle",
|
|
10952
|
+
"duplicate-target"
|
|
10953
|
+
]);
|
|
10954
|
+
/** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
|
|
10955
|
+
var CompositionProblemSchema = zod.z.object({
|
|
10956
|
+
code: CompositionProblemCodeSchema,
|
|
10957
|
+
path: zod.z.string(),
|
|
10958
|
+
message: zod.z.string()
|
|
10959
|
+
});
|
|
10960
|
+
var CompositionFieldKindSchema = zod.z.enum([
|
|
10961
|
+
"string",
|
|
10962
|
+
"number",
|
|
10963
|
+
"boolean",
|
|
10964
|
+
"enum"
|
|
10965
|
+
]);
|
|
10966
|
+
/** How a field says "unavailable": `null` when its schema allows it, else the device goes offline (D393). */
|
|
10967
|
+
var CompositionUnavailableModeSchema = zod.z.enum(["null", "offline"]);
|
|
10968
|
+
/**
|
|
10969
|
+
* `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
|
|
10970
|
+
* field of its cap changes value · `constant` reads nothing, computed once.
|
|
10971
|
+
*/
|
|
10972
|
+
var CompositionFieldRoleSchema = zod.z.enum([
|
|
10973
|
+
"derived",
|
|
10974
|
+
"clock",
|
|
10975
|
+
"constant"
|
|
10976
|
+
]);
|
|
10977
|
+
var CompositionFieldPlanSchema = zod.z.object({
|
|
10978
|
+
cap: zod.z.string(),
|
|
10979
|
+
path: zod.z.string(),
|
|
10980
|
+
/** Index into `composition.features` this field's capability came from —
|
|
10981
|
+
* lets a source/cycle problem name its path as `features[N].fields.<path>`
|
|
10982
|
+
* (the same convention `planComposition`'s own problems already use). */
|
|
10983
|
+
featureIndex: zod.z.number().int().nonnegative(),
|
|
10984
|
+
kind: CompositionFieldKindSchema,
|
|
10985
|
+
nullable: zod.z.boolean(),
|
|
10986
|
+
required: zod.z.boolean(),
|
|
10987
|
+
unavailable: CompositionUnavailableModeSchema,
|
|
10988
|
+
role: CompositionFieldRoleSchema,
|
|
10989
|
+
source: CompositionFieldSourceSchema,
|
|
10990
|
+
reads: zod.z.array(CompositionFieldReadSchema)
|
|
10991
|
+
});
|
|
10992
|
+
var CompositionValidationSchema = zod.z.object({
|
|
10993
|
+
ok: zod.z.boolean(),
|
|
10994
|
+
problems: zod.z.array(CompositionProblemSchema),
|
|
10995
|
+
fields: zod.z.array(CompositionFieldPlanSchema)
|
|
10996
|
+
});
|
|
10997
|
+
/** Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts` (Task 11). */
|
|
10998
|
+
var CompositionRunStatusSchema = zod.z.enum([
|
|
10999
|
+
"stopped",
|
|
11000
|
+
"starting",
|
|
11001
|
+
"running",
|
|
11002
|
+
"failed"
|
|
11003
|
+
]);
|
|
11004
|
+
var CompositionFieldStateKindSchema = zod.z.enum([
|
|
11005
|
+
"fresh",
|
|
11006
|
+
"stale",
|
|
11007
|
+
"unavailable"
|
|
11008
|
+
]);
|
|
11009
|
+
var CompositionFieldStateSchema = zod.z.object({
|
|
11010
|
+
cap: zod.z.string(),
|
|
11011
|
+
path: zod.z.string(),
|
|
11012
|
+
state: CompositionFieldStateKindSchema,
|
|
11013
|
+
/** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
|
|
11014
|
+
value: zod.z.union([
|
|
11015
|
+
zod.z.string(),
|
|
11016
|
+
zod.z.number(),
|
|
11017
|
+
zod.z.boolean(),
|
|
11018
|
+
zod.z.null()
|
|
11019
|
+
]),
|
|
11020
|
+
reason: zod.z.string().optional()
|
|
11021
|
+
});
|
|
11022
|
+
var CompositionBlockStateSchema = zod.z.object({
|
|
11023
|
+
blockId: zod.z.string(),
|
|
11024
|
+
deviceId: zod.z.number().nullable(),
|
|
11025
|
+
status: CompositionRunStatusSchema,
|
|
11026
|
+
reason: zod.z.string().optional(),
|
|
11027
|
+
fields: zod.z.array(CompositionFieldStateSchema)
|
|
11028
|
+
});
|
|
11029
|
+
/**
|
|
10554
11030
|
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
10555
11031
|
* its own process.
|
|
10556
11032
|
*
|
|
@@ -10634,6 +11110,30 @@ var CoreBlockInputSchema = zod.z.object({
|
|
|
10634
11110
|
*/
|
|
10635
11111
|
integrationId: zod.z.string().optional()
|
|
10636
11112
|
});
|
|
11113
|
+
/**
|
|
11114
|
+
* What a block IS (D659). Absent means `code` — every row stored before this
|
|
11115
|
+
* union keeps working unchanged.
|
|
11116
|
+
* - `code`: the TypeScript in `code`, compiled and run in its own runner;
|
|
11117
|
+
* - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
|
|
11118
|
+
* - `composition`: declarative, run by the shared `composer`; `code` is `''`.
|
|
11119
|
+
*/
|
|
11120
|
+
var CoreBlockSourceSchema = zod.z.discriminatedUnion("kind", [
|
|
11121
|
+
zod.z.object({ kind: zod.z.literal("code") }),
|
|
11122
|
+
zod.z.object({
|
|
11123
|
+
kind: zod.z.literal("recipe"),
|
|
11124
|
+
recipe: AutomationRecipeSchema
|
|
11125
|
+
}),
|
|
11126
|
+
zod.z.object({
|
|
11127
|
+
kind: zod.z.literal("composition"),
|
|
11128
|
+
composition: CompositionSchema
|
|
11129
|
+
})
|
|
11130
|
+
]);
|
|
11131
|
+
/** The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7). */
|
|
11132
|
+
var CompositionBlockInputSchema = zod.z.object({
|
|
11133
|
+
name: zod.z.string().min(1).max(120),
|
|
11134
|
+
enabled: zod.z.boolean(),
|
|
11135
|
+
composition: CompositionSchema
|
|
11136
|
+
});
|
|
10637
11137
|
/** A stored block. */
|
|
10638
11138
|
var CoreBlockSchema = CoreBlockInputSchema.extend({
|
|
10639
11139
|
id: zod.z.string(),
|
|
@@ -10649,8 +11149,46 @@ var CoreBlockSchema = CoreBlockInputSchema.extend({
|
|
|
10649
11149
|
*/
|
|
10650
11150
|
lastError: zod.z.string().optional(),
|
|
10651
11151
|
/** Ms epoch of the last state change. */
|
|
10652
|
-
lastChangedAt: zod.z.number()
|
|
11152
|
+
lastChangedAt: zod.z.number(),
|
|
11153
|
+
/** What the block is. Absent means `code` (D659). */
|
|
11154
|
+
source: CoreBlockSourceSchema.optional()
|
|
11155
|
+
});
|
|
11156
|
+
function coreBlockSourceKind(block) {
|
|
11157
|
+
return block.source?.kind ?? "code";
|
|
11158
|
+
}
|
|
11159
|
+
function coreBlockComposition(block) {
|
|
11160
|
+
return block.source?.kind === "composition" ? block.source.composition : null;
|
|
11161
|
+
}
|
|
11162
|
+
/** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
|
|
11163
|
+
var CompositionStateReadSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
|
|
11164
|
+
kind: zod.z.literal("answered"),
|
|
11165
|
+
state: CompositionBlockStateSchema.nullable()
|
|
11166
|
+
}), zod.z.object({
|
|
11167
|
+
kind: zod.z.literal("unreachable"),
|
|
11168
|
+
error: zod.z.string()
|
|
11169
|
+
})]);
|
|
11170
|
+
/** A stored composition block and what the composer says about it. */
|
|
11171
|
+
var CompositionViewSchema = zod.z.object({
|
|
11172
|
+
block: CoreBlockSchema,
|
|
11173
|
+
state: CompositionStateReadSchema
|
|
10653
11174
|
});
|
|
11175
|
+
/**
|
|
11176
|
+
* A read of the stored compositions. `not-loaded` while the `core_blocks`
|
|
11177
|
+
* table has not been READ: an empty answer then would make "not yet known"
|
|
11178
|
+
* look like "none", and invite the operator to recreate what exists (D315).
|
|
11179
|
+
*/
|
|
11180
|
+
var CompositionsNotLoadedSchema = zod.z.object({
|
|
11181
|
+
state: zod.z.literal("not-loaded"),
|
|
11182
|
+
reason: zod.z.string()
|
|
11183
|
+
});
|
|
11184
|
+
var CompositionListReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
|
|
11185
|
+
state: zod.z.literal("loaded"),
|
|
11186
|
+
views: zod.z.array(CompositionViewSchema)
|
|
11187
|
+
}), CompositionsNotLoadedSchema]);
|
|
11188
|
+
var CompositionGetReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
|
|
11189
|
+
state: zod.z.literal("loaded"),
|
|
11190
|
+
view: CompositionViewSchema.nullable()
|
|
11191
|
+
}), CompositionsNotLoadedSchema]);
|
|
10654
11192
|
/** What a compile attempt produced. */
|
|
10655
11193
|
var CoreBlockCompileResultSchema = zod.z.object({
|
|
10656
11194
|
ok: zod.z.boolean(),
|
|
@@ -10732,7 +11270,116 @@ var coreBlocksCapability = {
|
|
|
10732
11270
|
getTypeDefs: method(zod.z.object({}), zod.z.object({ libs: zod.z.array(zod.z.object({
|
|
10733
11271
|
filePath: zod.z.string(),
|
|
10734
11272
|
content: zod.z.string()
|
|
10735
|
-
})) }), { auth: "admin" })
|
|
11273
|
+
})) }), { auth: "admin" }),
|
|
11274
|
+
/**
|
|
11275
|
+
* Create a composition block (D659). VALIDATED first against the live
|
|
11276
|
+
* sources and every stored composition; a refusal names every problem by
|
|
11277
|
+
* path and reason (D391) and stores nothing.
|
|
11278
|
+
*/
|
|
11279
|
+
createComposition: method(zod.z.object({ block: CompositionBlockInputSchema }), zod.z.object({ block: CoreBlockSchema }), {
|
|
11280
|
+
kind: "mutation",
|
|
11281
|
+
auth: "admin",
|
|
11282
|
+
caller: "required"
|
|
11283
|
+
}),
|
|
11284
|
+
/**
|
|
11285
|
+
* Patch a composition block. A changed `composition` is validated exactly
|
|
11286
|
+
* as on create. `expectedUpdatedAt` is the `updatedAt` the caller READ: a
|
|
11287
|
+
* block changed since is refused by name, never silently overwritten.
|
|
11288
|
+
*/
|
|
11289
|
+
updateComposition: method(zod.z.object({
|
|
11290
|
+
blockId: zod.z.string(),
|
|
11291
|
+
expectedUpdatedAt: zod.z.number(),
|
|
11292
|
+
block: CompositionBlockInputSchema.partial()
|
|
11293
|
+
}), zod.z.object({ block: CoreBlockSchema }), {
|
|
11294
|
+
kind: "mutation",
|
|
11295
|
+
auth: "admin",
|
|
11296
|
+
caller: "required"
|
|
11297
|
+
}),
|
|
11298
|
+
/** One composition and the composer's state for it, or `not-loaded` (D315). */
|
|
11299
|
+
getComposition: method(zod.z.object({ blockId: zod.z.string() }), CompositionGetReadSchema, { auth: "admin" }),
|
|
11300
|
+
listCompositions: method(zod.z.object({}), CompositionListReadSchema, { auth: "admin" }),
|
|
11301
|
+
/**
|
|
11302
|
+
* Validate without saving — the same verdict the save reaches. A mutation
|
|
11303
|
+
* for its body size; it writes nothing, so its access is `view`.
|
|
11304
|
+
*/
|
|
11305
|
+
validateComposition: method(zod.z.object({
|
|
11306
|
+
composition: CompositionSchema,
|
|
11307
|
+
blockId: zod.z.string().optional()
|
|
11308
|
+
}), CompositionValidationSchema, {
|
|
11309
|
+
kind: "mutation",
|
|
11310
|
+
auth: "admin",
|
|
11311
|
+
access: "view"
|
|
11312
|
+
})
|
|
11313
|
+
}
|
|
11314
|
+
};
|
|
11315
|
+
/**
|
|
11316
|
+
* composer — runs every DECLARATIVE composition (D659).
|
|
11317
|
+
*
|
|
11318
|
+
* Internal and admin-only: its one caller is `core-blocks`' supervisor, which
|
|
11319
|
+
* sends the WHOLE set of composition blocks on every converge (idempotent),
|
|
11320
|
+
* and reads their state for the API. Nothing else talks to it.
|
|
11321
|
+
*/
|
|
11322
|
+
/** The composer's runner id, `resolveRunnerId(decl, 'composer')`; pinned by `composer-manifest.spec.ts`. */
|
|
11323
|
+
var COMPOSER_RUNNER_ID = COMPOSER_ADDON_ID;
|
|
11324
|
+
var ComposerBlockSchema = zod.z.object({
|
|
11325
|
+
id: zod.z.string().min(1),
|
|
11326
|
+
name: zod.z.string().min(1).max(120),
|
|
11327
|
+
enabled: zod.z.boolean(),
|
|
11328
|
+
updatedAt: zod.z.number(),
|
|
11329
|
+
composition: CompositionSchema
|
|
11330
|
+
});
|
|
11331
|
+
/**
|
|
11332
|
+
* A stored composition row that no longer PARSES — a schema a release
|
|
11333
|
+
* tightened, a row written by a newer build. Sent by id so the composer keeps
|
|
11334
|
+
* its device as a placeholder and reports the block `failed` naming the parse
|
|
11335
|
+
* error. Leaving it out would read, twice, exactly like a delete (D49).
|
|
11336
|
+
*/
|
|
11337
|
+
var ComposerUnreadableBlockSchema = zod.z.object({
|
|
11338
|
+
id: zod.z.string().min(1),
|
|
11339
|
+
/** The row's stored name column; null when even that is unreadable (the device keeps the name it has). */
|
|
11340
|
+
name: zod.z.string().nullable(),
|
|
11341
|
+
/** The parse error, in the operator's words. */
|
|
11342
|
+
error: zod.z.string()
|
|
11343
|
+
});
|
|
11344
|
+
var ComposerApplyInputSchema = zod.z.object({
|
|
11345
|
+
/** The shared Blocks integration; null until `core-blocks` has reconciled it. */
|
|
11346
|
+
integrationId: zod.z.string().nullable(),
|
|
11347
|
+
/** False when the `core_blocks` read failed: then nothing may be withdrawn (D49). */
|
|
11348
|
+
storeLoaded: zod.z.boolean(),
|
|
11349
|
+
blocks: zod.z.array(ComposerBlockSchema),
|
|
11350
|
+
/** Operator restarts: rebuild these runtimes and re-resolve their sources. Stores nothing (D62). */
|
|
11351
|
+
restartBlockIds: zod.z.array(zod.z.string()),
|
|
11352
|
+
/** Composition rows that exist but do not parse: held, never withdrawn, reported failed. */
|
|
11353
|
+
unreadableBlocks: zod.z.array(ComposerUnreadableBlockSchema)
|
|
11354
|
+
});
|
|
11355
|
+
var ComposerBlockVerdictSchema = zod.z.object({
|
|
11356
|
+
blockId: zod.z.string(),
|
|
11357
|
+
status: CompositionRunStatusSchema,
|
|
11358
|
+
reason: zod.z.string().optional()
|
|
11359
|
+
});
|
|
11360
|
+
var composerCapability = {
|
|
11361
|
+
name: "composer",
|
|
11362
|
+
scope: "system",
|
|
11363
|
+
mode: "singleton",
|
|
11364
|
+
internal: true,
|
|
11365
|
+
methods: {
|
|
11366
|
+
/**
|
|
11367
|
+
* Not `apply`: tRPC reserves `apply`, `call` and `then` as router keys, and
|
|
11368
|
+
* `router({})` throws on one — every cap router is built at boot.
|
|
11369
|
+
*/
|
|
11370
|
+
applyBlocks: method(ComposerApplyInputSchema, zod.z.object({
|
|
11371
|
+
blocks: zod.z.array(ComposerBlockVerdictSchema),
|
|
11372
|
+
/**
|
|
11373
|
+
* False when the composer declared and withdrew NOTHING (store not loaded,
|
|
11374
|
+
* integration unknown, its own rows unread or read empty once). An empty
|
|
11375
|
+
* apply the caller owes is paid off only by one the composer acted on (D49).
|
|
11376
|
+
*/
|
|
11377
|
+
acted: zod.z.boolean()
|
|
11378
|
+
}), {
|
|
11379
|
+
kind: "mutation",
|
|
11380
|
+
auth: "admin"
|
|
11381
|
+
}),
|
|
11382
|
+
getStates: method(zod.z.object({ blockIds: zod.z.array(zod.z.string()) }), zod.z.object({ states: zod.z.array(CompositionBlockStateSchema) }), { auth: "admin" })
|
|
10736
11383
|
}
|
|
10737
11384
|
};
|
|
10738
11385
|
/**
|
|
@@ -18285,8 +18932,8 @@ var NcTestResultSchema = zod.z.object({
|
|
|
18285
18932
|
className: zod.z.string().optional(),
|
|
18286
18933
|
label: zod.z.string().optional()
|
|
18287
18934
|
});
|
|
18288
|
-
/** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is
|
|
18289
|
-
*
|
|
18935
|
+
/** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is
|
|
18936
|
+
* `z.infer` of this schema — one shape, no twin. `mixed` is DERIVED
|
|
18290
18937
|
* (a stored system rule whose kinds span families, D637) and never authored. */
|
|
18291
18938
|
var NcRuleKindSchema = zod.z.enum([
|
|
18292
18939
|
"detection",
|
|
@@ -41624,6 +42271,620 @@ function createDeviceExtensionProvider(options) {
|
|
|
41624
42271
|
};
|
|
41625
42272
|
}
|
|
41626
42273
|
/**
|
|
42274
|
+
* The field graph across every composition, and the cycles in it (D659).
|
|
42275
|
+
*
|
|
42276
|
+
* A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
|
|
42277
|
+
* edge points from a composed field to each field it reads. A cycle, direct or
|
|
42278
|
+
* through another composition, is refused at save by name (spec §2).
|
|
42279
|
+
*
|
|
42280
|
+
* The graph is built for ONE save: the CANDIDATE composition (the one being
|
|
42281
|
+
* validated) plus every STORED composition. The candidate ALWAYS replaces any
|
|
42282
|
+
* stored peer that targets the same device, regardless of where that stored
|
|
42283
|
+
* peer sits in `stored` — whether a save introduces or removes a cycle must
|
|
42284
|
+
* never depend on array order. Two DIFFERENT stored peers that (wrongly)
|
|
42285
|
+
* target the same device are corrupted state, not a cycle to resolve by
|
|
42286
|
+
* picking one silently; that is refused by name (D391).
|
|
42287
|
+
*
|
|
42288
|
+
* `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
|
|
42289
|
+
* marks a node `done` after its first expansion) — enough to refuse a save,
|
|
42290
|
+
* not to enumerate every elementary cycle through a shared node. A dotted
|
|
42291
|
+
* `fieldPath` is matched only as an exact string: a write to `state.value` and
|
|
42292
|
+
* a read of `state` would not connect (no nested field writes exist yet).
|
|
42293
|
+
*/
|
|
42294
|
+
/** Two stored compositions both target the same device — refused by name, never silently resolved. */
|
|
42295
|
+
var CompositionGraphError = class extends Error {};
|
|
42296
|
+
function peerEdges(peer) {
|
|
42297
|
+
return peer.composition.features.flatMap((feature) => feature.kind !== "fields" ? [] : Object.entries(feature.fields).map(([path, source]) => [compositionReadKey({
|
|
42298
|
+
source: peer.target,
|
|
42299
|
+
cap: feature.cap,
|
|
42300
|
+
fieldPath: path
|
|
42301
|
+
}), fieldReads(source).map(compositionReadKey)]));
|
|
42302
|
+
}
|
|
42303
|
+
/**
|
|
42304
|
+
* Every peer that actually takes part in the graph: each stored peer minus
|
|
42305
|
+
* whichever one targets the same device as the candidate (the candidate
|
|
42306
|
+
* replaces it — this drop happens before any edge is built, so the
|
|
42307
|
+
* candidate's old edges never enter the map to begin with), plus the
|
|
42308
|
+
* candidate itself, exactly once. Throws {@link CompositionGraphError} if two
|
|
42309
|
+
* DIFFERENT stored peers (neither of them the candidate's own old version)
|
|
42310
|
+
* target the same device.
|
|
42311
|
+
*/
|
|
42312
|
+
function resolvedPeers(candidate, stored) {
|
|
42313
|
+
const candidateKey = compositionSourceKey(candidate.target);
|
|
42314
|
+
const survivors = /* @__PURE__ */ new Map();
|
|
42315
|
+
for (const peer of stored) {
|
|
42316
|
+
const key = compositionSourceKey(peer.target);
|
|
42317
|
+
if (key === candidateKey) continue;
|
|
42318
|
+
if (survivors.has(key)) throw new CompositionGraphError(`two stored compositions both target device \`${key}\` — refusing to build the field graph`);
|
|
42319
|
+
survivors.set(key, peer);
|
|
42320
|
+
}
|
|
42321
|
+
return [...survivors.values(), candidate];
|
|
42322
|
+
}
|
|
42323
|
+
function compositionEdges(candidate, stored) {
|
|
42324
|
+
const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
|
|
42325
|
+
return new Map(edges);
|
|
42326
|
+
}
|
|
42327
|
+
function findCompositionCycles(candidate, stored) {
|
|
42328
|
+
const edges = compositionEdges(candidate, stored);
|
|
42329
|
+
const marks = /* @__PURE__ */ new Map();
|
|
42330
|
+
const found = /* @__PURE__ */ new Map();
|
|
42331
|
+
const visit = (node, stack) => {
|
|
42332
|
+
const mark = marks.get(node);
|
|
42333
|
+
if (mark === "done") return;
|
|
42334
|
+
if (mark === "visiting") {
|
|
42335
|
+
const path = [...stack.slice(stack.indexOf(node)), node];
|
|
42336
|
+
const id = [...new Set(path)].toSorted().join("|");
|
|
42337
|
+
if (!found.has(id)) found.set(id, { path });
|
|
42338
|
+
return;
|
|
42339
|
+
}
|
|
42340
|
+
marks.set(node, "visiting");
|
|
42341
|
+
for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
|
|
42342
|
+
marks.set(node, "done");
|
|
42343
|
+
};
|
|
42344
|
+
for (const node of edges.keys()) visit(node, []);
|
|
42345
|
+
return [...found.values()];
|
|
42346
|
+
}
|
|
42347
|
+
function describeRead(read) {
|
|
42348
|
+
return `${compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
|
|
42349
|
+
}
|
|
42350
|
+
function fromReading(reading, read) {
|
|
42351
|
+
if (reading.state === "unavailable") return {
|
|
42352
|
+
state: "unavailable",
|
|
42353
|
+
value: null,
|
|
42354
|
+
reason: `${describeRead(read)}: ${reading.reason}`,
|
|
42355
|
+
fatal: reading.fatal
|
|
42356
|
+
};
|
|
42357
|
+
return {
|
|
42358
|
+
state: reading.state,
|
|
42359
|
+
value: reading.value,
|
|
42360
|
+
reason: reading.state === "stale" ? `${describeRead(read)}: ${reading.reason}` : null,
|
|
42361
|
+
deadline: null
|
|
42362
|
+
};
|
|
42363
|
+
}
|
|
42364
|
+
function expressionOutcome(source, read, now) {
|
|
42365
|
+
const values = {};
|
|
42366
|
+
const staleReasons = [];
|
|
42367
|
+
let fatalUnavailable = null;
|
|
42368
|
+
let nonFatalUnavailable = null;
|
|
42369
|
+
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
42370
|
+
if (binding.kind === "literal") {
|
|
42371
|
+
values[name] = binding.value;
|
|
42372
|
+
continue;
|
|
42373
|
+
}
|
|
42374
|
+
const reading = read(binding);
|
|
42375
|
+
if (reading.state === "unavailable") {
|
|
42376
|
+
const outcome = {
|
|
42377
|
+
state: "unavailable",
|
|
42378
|
+
value: null,
|
|
42379
|
+
reason: `binding \`${name}\` (${describeRead(binding)}): ${reading.reason}`,
|
|
42380
|
+
fatal: reading.fatal
|
|
42381
|
+
};
|
|
42382
|
+
if (reading.fatal) {
|
|
42383
|
+
fatalUnavailable = outcome;
|
|
42384
|
+
break;
|
|
42385
|
+
}
|
|
42386
|
+
if (nonFatalUnavailable === null) nonFatalUnavailable = outcome;
|
|
42387
|
+
continue;
|
|
42388
|
+
}
|
|
42389
|
+
values[name] = reading.value;
|
|
42390
|
+
if (reading.state === "stale") staleReasons.push(`\`${name}\`: ${reading.reason}`);
|
|
42391
|
+
}
|
|
42392
|
+
if (fatalUnavailable !== null) return fatalUnavailable;
|
|
42393
|
+
if (nonFatalUnavailable !== null) return nonFatalUnavailable;
|
|
42394
|
+
const deadlines = [];
|
|
42395
|
+
const result = evaluateExpressionSource(source.expr, values, now, { hooks: { noteDeadline: (at) => deadlines.push(at) } });
|
|
42396
|
+
if (!result.ok) return {
|
|
42397
|
+
state: "unavailable",
|
|
42398
|
+
value: null,
|
|
42399
|
+
reason: `the expression failed: ${result.error}`,
|
|
42400
|
+
fatal: false
|
|
42401
|
+
};
|
|
42402
|
+
return {
|
|
42403
|
+
state: staleReasons.length > 0 ? "stale" : "fresh",
|
|
42404
|
+
value: result.value,
|
|
42405
|
+
reason: staleReasons.length > 0 ? staleReasons.join("; ") : null,
|
|
42406
|
+
deadline: deadlines.length > 0 ? Math.min(...deadlines) : null
|
|
42407
|
+
};
|
|
42408
|
+
}
|
|
42409
|
+
function evaluateCompositionField(input) {
|
|
42410
|
+
const { source } = input;
|
|
42411
|
+
switch (source.kind) {
|
|
42412
|
+
case "from": return fromReading(input.read(source), source);
|
|
42413
|
+
case "expression": return expressionOutcome(source, input.read, input.now);
|
|
42414
|
+
case "code": return {
|
|
42415
|
+
state: "unavailable",
|
|
42416
|
+
value: null,
|
|
42417
|
+
reason: "code fields are not runnable in the composer — they arrive in slice 4",
|
|
42418
|
+
fatal: true
|
|
42419
|
+
};
|
|
42420
|
+
}
|
|
42421
|
+
}
|
|
42422
|
+
var NUMBER = {
|
|
42423
|
+
kind: "fixed",
|
|
42424
|
+
result: ["number"]
|
|
42425
|
+
};
|
|
42426
|
+
var BOOLEAN = {
|
|
42427
|
+
kind: "fixed",
|
|
42428
|
+
result: ["boolean"]
|
|
42429
|
+
};
|
|
42430
|
+
/** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
|
|
42431
|
+
var BUILTIN_RESULT_KINDS = Object.freeze({
|
|
42432
|
+
min: NUMBER,
|
|
42433
|
+
max: NUMBER,
|
|
42434
|
+
abs: NUMBER,
|
|
42435
|
+
floor: NUMBER,
|
|
42436
|
+
ceil: NUMBER,
|
|
42437
|
+
sqrt: NUMBER,
|
|
42438
|
+
round: NUMBER,
|
|
42439
|
+
pow: NUMBER,
|
|
42440
|
+
clamp: NUMBER,
|
|
42441
|
+
avg: NUMBER,
|
|
42442
|
+
sum: NUMBER,
|
|
42443
|
+
age: NUMBER,
|
|
42444
|
+
convert: NUMBER,
|
|
42445
|
+
coalesce: { kind: "coalesce" },
|
|
42446
|
+
any: BOOLEAN,
|
|
42447
|
+
all: BOOLEAN,
|
|
42448
|
+
within: BOOLEAN,
|
|
42449
|
+
latest: {
|
|
42450
|
+
kind: "fixed",
|
|
42451
|
+
result: ["number", "null"]
|
|
42452
|
+
}
|
|
42453
|
+
});
|
|
42454
|
+
var KindInferenceError = class extends Error {};
|
|
42455
|
+
function kindOfValue(value) {
|
|
42456
|
+
if (value === null) return "null";
|
|
42457
|
+
if (typeof value === "number") return "number";
|
|
42458
|
+
if (typeof value === "string") return "string";
|
|
42459
|
+
return "boolean";
|
|
42460
|
+
}
|
|
42461
|
+
function only(kind) {
|
|
42462
|
+
return new Set([kind]);
|
|
42463
|
+
}
|
|
42464
|
+
function union(...sets) {
|
|
42465
|
+
return new Set(sets.flatMap((s) => [...s]));
|
|
42466
|
+
}
|
|
42467
|
+
function withoutNull(set) {
|
|
42468
|
+
return new Set([...set].filter((k) => k !== "null"));
|
|
42469
|
+
}
|
|
42470
|
+
function binaryKinds(op, left, right) {
|
|
42471
|
+
if (op === "+") {
|
|
42472
|
+
const all = [...left, ...right];
|
|
42473
|
+
if (all.every((k) => k === "string")) return only("string");
|
|
42474
|
+
if (all.every((k) => k === "number")) return only("number");
|
|
42475
|
+
return new Set(["number", "string"]);
|
|
42476
|
+
}
|
|
42477
|
+
if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
|
|
42478
|
+
return only("boolean");
|
|
42479
|
+
}
|
|
42480
|
+
/** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
|
|
42481
|
+
function arityRange(builtin) {
|
|
42482
|
+
if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
|
|
42483
|
+
if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
|
|
42484
|
+
return `${builtin.minArgs}–${builtin.maxArgs}`;
|
|
42485
|
+
}
|
|
42486
|
+
function callKinds(callee, args) {
|
|
42487
|
+
const builtin = EXPRESSION_BUILTINS[callee];
|
|
42488
|
+
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)}`);
|
|
42489
|
+
const rule = BUILTIN_RESULT_KINDS[callee];
|
|
42490
|
+
if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
|
|
42491
|
+
if (rule.kind === "fixed") return new Set(rule.result);
|
|
42492
|
+
const nonNull = union(...args.map(withoutNull));
|
|
42493
|
+
return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
|
|
42494
|
+
}
|
|
42495
|
+
function kindsOf(node, bindings) {
|
|
42496
|
+
switch (node.kind) {
|
|
42497
|
+
case "literal": return only(kindOfValue(node.value));
|
|
42498
|
+
case "identifier": {
|
|
42499
|
+
if (node.name === "now") return only("number");
|
|
42500
|
+
const bound = bindings.get(node.name);
|
|
42501
|
+
if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
|
|
42502
|
+
return bound;
|
|
42503
|
+
}
|
|
42504
|
+
case "unary": return only(node.op === "!" ? "boolean" : "number");
|
|
42505
|
+
case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
42506
|
+
case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
|
|
42507
|
+
case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
|
|
42508
|
+
case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
|
|
42509
|
+
}
|
|
42510
|
+
}
|
|
42511
|
+
function inferExpressionKinds(input) {
|
|
42512
|
+
const compiled = compileExpressionSafe(input.expr);
|
|
42513
|
+
if (!compiled.ok) return {
|
|
42514
|
+
ok: false,
|
|
42515
|
+
error: compiled.error
|
|
42516
|
+
};
|
|
42517
|
+
try {
|
|
42518
|
+
return {
|
|
42519
|
+
ok: true,
|
|
42520
|
+
kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
|
|
42521
|
+
};
|
|
42522
|
+
} catch (err) {
|
|
42523
|
+
if (err instanceof KindInferenceError) return {
|
|
42524
|
+
ok: false,
|
|
42525
|
+
error: err.message
|
|
42526
|
+
};
|
|
42527
|
+
throw err;
|
|
42528
|
+
}
|
|
42529
|
+
}
|
|
42530
|
+
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
42531
|
+
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
42532
|
+
function unwrapSchema(schema) {
|
|
42533
|
+
let s = schema;
|
|
42534
|
+
while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
|
|
42535
|
+
return s;
|
|
42536
|
+
}
|
|
42537
|
+
function leafKind(schema) {
|
|
42538
|
+
const s = unwrapSchema(schema);
|
|
42539
|
+
if (s instanceof zod.z.ZodEnum) return {
|
|
42540
|
+
kind: "enum",
|
|
42541
|
+
enumValues: s.options
|
|
42542
|
+
};
|
|
42543
|
+
if (s instanceof zod.z.ZodNumber) return { kind: "number" };
|
|
42544
|
+
if (s instanceof zod.z.ZodString) return { kind: "string" };
|
|
42545
|
+
if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
|
|
42546
|
+
return null;
|
|
42547
|
+
}
|
|
42548
|
+
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
42549
|
+
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
42550
|
+
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
42551
|
+
* are skipped — never throws. */
|
|
42552
|
+
function enumerateSchemaFields(schema, prefix = "") {
|
|
42553
|
+
const root = unwrapSchema(schema);
|
|
42554
|
+
if (!(root instanceof zod.z.ZodObject)) return [];
|
|
42555
|
+
const out = [];
|
|
42556
|
+
const shape = root.shape;
|
|
42557
|
+
for (const [key, field] of Object.entries(shape)) {
|
|
42558
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
42559
|
+
const inner = unwrapSchema(field);
|
|
42560
|
+
if (inner instanceof zod.z.ZodObject) {
|
|
42561
|
+
out.push(...enumerateSchemaFields(inner, path));
|
|
42562
|
+
continue;
|
|
42563
|
+
}
|
|
42564
|
+
const leaf = leafKind(field);
|
|
42565
|
+
if (leaf) out.push({
|
|
42566
|
+
path,
|
|
42567
|
+
kind: leaf.kind,
|
|
42568
|
+
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
42569
|
+
});
|
|
42570
|
+
}
|
|
42571
|
+
return out;
|
|
42572
|
+
}
|
|
42573
|
+
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
42574
|
+
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
42575
|
+
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
42576
|
+
* source), each tagged `item: true` so the authoring UI collects an
|
|
42577
|
+
* `itemKey` alongside the field. Never throws. */
|
|
42578
|
+
function enumerateItemArrayFields(itemArray) {
|
|
42579
|
+
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
42580
|
+
...f,
|
|
42581
|
+
item: true
|
|
42582
|
+
}));
|
|
42583
|
+
}
|
|
42584
|
+
/**
|
|
42585
|
+
* What a capability's state field IS: kind, nullability, whether required. The
|
|
42586
|
+
* validator needs nullability, which `enumerateSchemaFields` does not report,
|
|
42587
|
+
* and that decides a field's unavailable mode (D393).
|
|
42588
|
+
*/
|
|
42589
|
+
function fieldAt(schema, path) {
|
|
42590
|
+
let current = schema;
|
|
42591
|
+
for (const segment of path.split(".")) {
|
|
42592
|
+
const container = unwrapSchema(current);
|
|
42593
|
+
if (!(container instanceof zod.z.ZodObject)) return null;
|
|
42594
|
+
const next = container.shape[segment];
|
|
42595
|
+
if (!(next instanceof zod.z.ZodType)) return null;
|
|
42596
|
+
current = next;
|
|
42597
|
+
}
|
|
42598
|
+
return current;
|
|
42599
|
+
}
|
|
42600
|
+
function describeStateField(schema, path) {
|
|
42601
|
+
const leaf = enumerateSchemaFields(schema).find((f) => f.path === path);
|
|
42602
|
+
if (leaf === void 0) return null;
|
|
42603
|
+
const field = fieldAt(schema, path);
|
|
42604
|
+
if (field === null) return null;
|
|
42605
|
+
return {
|
|
42606
|
+
path,
|
|
42607
|
+
kind: leaf.kind,
|
|
42608
|
+
nullable: field.safeParse(null).success,
|
|
42609
|
+
required: !field.safeParse(void 0).success,
|
|
42610
|
+
...leaf.enumValues === void 0 ? {} : { enumValues: leaf.enumValues }
|
|
42611
|
+
};
|
|
42612
|
+
}
|
|
42613
|
+
function describeTopLevelStateFields(schema) {
|
|
42614
|
+
return new Map(enumerateSchemaFields(schema).filter((f) => !f.path.includes(".")).flatMap((f) => {
|
|
42615
|
+
const described = describeStateField(schema, f.path);
|
|
42616
|
+
return described === null ? [] : [[f.path, described]];
|
|
42617
|
+
}));
|
|
42618
|
+
}
|
|
42619
|
+
/** Top-level keys a value MUST carry: those whose schema refuses `undefined`. */
|
|
42620
|
+
function requiredTopLevelKeys(schema) {
|
|
42621
|
+
const root = unwrapSchema(schema);
|
|
42622
|
+
if (!(root instanceof zod.z.ZodObject)) return [];
|
|
42623
|
+
return Object.entries(root.shape).flatMap(([key, field]) => field instanceof zod.z.ZodType && !field.safeParse(void 0).success ? [key] : []);
|
|
42624
|
+
}
|
|
42625
|
+
function buildCapabilityLookup(defs) {
|
|
42626
|
+
const byName = new Map(defs.map((d) => [d.name, d]));
|
|
42627
|
+
return (capName) => byName.get(capName) ?? null;
|
|
42628
|
+
}
|
|
42629
|
+
/** Caps every `BaseDevice` registers on itself; a second registration throws. */
|
|
42630
|
+
var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
|
|
42631
|
+
var PENDING_SELF = {
|
|
42632
|
+
addonId: COMPOSER_ADDON_ID,
|
|
42633
|
+
stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
|
|
42634
|
+
};
|
|
42635
|
+
function problem(code, path, message) {
|
|
42636
|
+
return {
|
|
42637
|
+
code,
|
|
42638
|
+
path,
|
|
42639
|
+
message
|
|
42640
|
+
};
|
|
42641
|
+
}
|
|
42642
|
+
function refused(...problems) {
|
|
42643
|
+
return {
|
|
42644
|
+
problems,
|
|
42645
|
+
fields: []
|
|
42646
|
+
};
|
|
42647
|
+
}
|
|
42648
|
+
function readKinds(read, lookupCap) {
|
|
42649
|
+
const def = lookupCap(read.cap);
|
|
42650
|
+
const schema = def?.runtimeState ?? def?.status?.schema;
|
|
42651
|
+
if (def === null || schema === void 0) return {
|
|
42652
|
+
ok: false,
|
|
42653
|
+
code: "source-field-unknown",
|
|
42654
|
+
message: `no capability \`${read.cap}\` with state to read`
|
|
42655
|
+
};
|
|
42656
|
+
const field = describeStateField(schema, read.fieldPath);
|
|
42657
|
+
if (field === null) return {
|
|
42658
|
+
ok: false,
|
|
42659
|
+
code: "source-field-unknown",
|
|
42660
|
+
message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
|
|
42661
|
+
};
|
|
42662
|
+
const base = field.kind === "enum" ? "string" : field.kind;
|
|
42663
|
+
return {
|
|
42664
|
+
ok: true,
|
|
42665
|
+
kinds: new Set(field.nullable ? [base, "null"] : [base])
|
|
42666
|
+
};
|
|
42667
|
+
}
|
|
42668
|
+
function sourceKinds(source, lookupCap) {
|
|
42669
|
+
if (source.kind === "from") return readKinds(source, lookupCap);
|
|
42670
|
+
if (source.kind === "code") return {
|
|
42671
|
+
ok: false,
|
|
42672
|
+
code: "reserved-variant",
|
|
42673
|
+
message: "code fields arrive in slice 4"
|
|
42674
|
+
};
|
|
42675
|
+
const bindingKinds = /* @__PURE__ */ new Map();
|
|
42676
|
+
for (const [name, binding] of Object.entries(source.bindings)) {
|
|
42677
|
+
if (binding.kind === "literal") {
|
|
42678
|
+
bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
|
|
42679
|
+
continue;
|
|
42680
|
+
}
|
|
42681
|
+
const kinds = readKinds(binding, lookupCap);
|
|
42682
|
+
if (!kinds.ok) return {
|
|
42683
|
+
...kinds,
|
|
42684
|
+
message: `binding \`${name}\`: ${kinds.message}`
|
|
42685
|
+
};
|
|
42686
|
+
bindingKinds.set(name, kinds.kinds);
|
|
42687
|
+
}
|
|
42688
|
+
const inferred = inferExpressionKinds({
|
|
42689
|
+
expr: source.expr,
|
|
42690
|
+
bindingKinds
|
|
42691
|
+
});
|
|
42692
|
+
return inferred.ok ? {
|
|
42693
|
+
ok: true,
|
|
42694
|
+
kinds: inferred.kinds
|
|
42695
|
+
} : {
|
|
42696
|
+
ok: false,
|
|
42697
|
+
code: "expression-invalid",
|
|
42698
|
+
message: inferred.error
|
|
42699
|
+
};
|
|
42700
|
+
}
|
|
42701
|
+
function compatibility(cap, target, kinds, at) {
|
|
42702
|
+
const allowed = target.kind === "enum" ? "string" : target.kind;
|
|
42703
|
+
const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
|
|
42704
|
+
const problems = [];
|
|
42705
|
+
if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
|
|
42706
|
+
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`));
|
|
42707
|
+
return problems;
|
|
42708
|
+
}
|
|
42709
|
+
/**
|
|
42710
|
+
* A string a source produces WITHOUT reading any device — a field-level
|
|
42711
|
+
* `expression` source whose compiled AST is a bare string literal (`"red"`,
|
|
42712
|
+
* never `a` or `a + "x"`). Anything else that can only produce `'string'` at
|
|
42713
|
+
* the kind level (a `from` read of an enum-typed source field, a computed
|
|
42714
|
+
* expression) is NOT checked against the target's `enumValues` here: the
|
|
42715
|
+
* kind inference already collapsed it to the coarse `'string'` kind, losing
|
|
42716
|
+
* whichever enum it actually came from, so membership can only be judged at
|
|
42717
|
+
* write time against the runtime-state schema. Known limitation — see the
|
|
42718
|
+
* fix report for Task 6 round 1.
|
|
42719
|
+
*/
|
|
42720
|
+
function literalStringValue(source) {
|
|
42721
|
+
if (source.kind !== "expression") return null;
|
|
42722
|
+
const compiled = compileExpressionSafe(source.expr);
|
|
42723
|
+
if (!compiled.ok) return null;
|
|
42724
|
+
const ast = compiled.parsed.ast;
|
|
42725
|
+
return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
|
|
42726
|
+
}
|
|
42727
|
+
/**
|
|
42728
|
+
* An enum target whose source is a save-time-KNOWN string literal outside its
|
|
42729
|
+
* `enumValues` is refused now rather than left to fail the write later. A
|
|
42730
|
+
* dynamic source (a `from` read, a non-literal expression) is left to
|
|
42731
|
+
* runtime — see {@link literalStringValue}.
|
|
42732
|
+
*/
|
|
42733
|
+
function enumLiteralProblem(cap, target, source, at) {
|
|
42734
|
+
if (target.kind !== "enum" || target.enumValues === void 0) return null;
|
|
42735
|
+
const value = literalStringValue(source);
|
|
42736
|
+
if (value === null || target.enumValues.includes(value)) return null;
|
|
42737
|
+
const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
|
|
42738
|
+
return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
|
|
42739
|
+
}
|
|
42740
|
+
function roleOf(source) {
|
|
42741
|
+
if (source.kind !== "expression") return "derived";
|
|
42742
|
+
if (fieldReads(source).length > 0) return "derived";
|
|
42743
|
+
const compiled = compileExpressionSafe(source.expr);
|
|
42744
|
+
return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
|
|
42745
|
+
}
|
|
42746
|
+
function planFeature(feature, featureIndex, target, lookupCap) {
|
|
42747
|
+
const at = `features[${featureIndex}]`;
|
|
42748
|
+
const def = lookupCap(feature.cap);
|
|
42749
|
+
if (def === null) return refused(problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``));
|
|
42750
|
+
if (BASE_DEVICE_CAPS.has(def.name)) return refused(problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`));
|
|
42751
|
+
const state = def.runtimeState;
|
|
42752
|
+
if (def.scope !== "device" || state === void 0) return refused(problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`));
|
|
42753
|
+
const capProblems = [];
|
|
42754
|
+
if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(target.type)) capProblems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${target.type}\` device (it allows ${def.deviceTypes.join(", ")})`));
|
|
42755
|
+
const served = Object.entries(def.methods).filter(([, m]) => m.systemOnly !== true).map(([name]) => `\`${name}\``);
|
|
42756
|
+
if (served.length > 0) capProblems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
|
|
42757
|
+
if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
|
|
42758
|
+
const targetFields = describeTopLevelStateFields(state);
|
|
42759
|
+
for (const key of requiredTopLevelKeys(state)) if (!targetFields.has(key)) capProblems.push(problem("not-composable", `${at}.fields`, `\`${def.name}.${key}\` is not a scalar — nested fields arrive in a later slice`));
|
|
42760
|
+
else if (!(key in feature.fields)) capProblems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
|
|
42761
|
+
const perField = Object.entries(feature.fields).map(([path, source]) => {
|
|
42762
|
+
const fieldAt = `${at}.fields.${path}`;
|
|
42763
|
+
if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
|
|
42764
|
+
const tf = targetFields.get(path);
|
|
42765
|
+
if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
|
|
42766
|
+
const kinds = sourceKinds(source, lookupCap);
|
|
42767
|
+
if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
|
|
42768
|
+
const plan = {
|
|
42769
|
+
cap: def.name,
|
|
42770
|
+
path,
|
|
42771
|
+
featureIndex,
|
|
42772
|
+
kind: tf.kind,
|
|
42773
|
+
nullable: tf.nullable,
|
|
42774
|
+
required: tf.required,
|
|
42775
|
+
unavailable: tf.nullable ? "null" : "offline",
|
|
42776
|
+
role: roleOf(source),
|
|
42777
|
+
source,
|
|
42778
|
+
reads: [...fieldReads(source)]
|
|
42779
|
+
};
|
|
42780
|
+
const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
|
|
42781
|
+
return {
|
|
42782
|
+
problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
|
|
42783
|
+
fields: [plan]
|
|
42784
|
+
};
|
|
42785
|
+
});
|
|
42786
|
+
return {
|
|
42787
|
+
problems: [...capProblems, ...perField.flatMap((r) => r.problems)],
|
|
42788
|
+
fields: perField.flatMap((r) => r.fields)
|
|
42789
|
+
};
|
|
42790
|
+
}
|
|
42791
|
+
function planComposition(composition, lookupCap) {
|
|
42792
|
+
const target = composition.target;
|
|
42793
|
+
if (target.kind === "existing") return refused(problem("reserved-variant", "target", "an EXISTING device as the target arrives in slice 2 — this release composes new devices only"));
|
|
42794
|
+
const results = composition.features.map((feature, index) => {
|
|
42795
|
+
const at = `features[${index}]`;
|
|
42796
|
+
if (feature.kind === "passthrough") return refused(problem("reserved-variant", at, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`));
|
|
42797
|
+
return planFeature(feature, index, target, lookupCap);
|
|
42798
|
+
});
|
|
42799
|
+
return {
|
|
42800
|
+
problems: results.flatMap((r) => r.problems),
|
|
42801
|
+
fields: results.flatMap((r) => r.fields)
|
|
42802
|
+
};
|
|
42803
|
+
}
|
|
42804
|
+
function compositionSourceRefs(composition) {
|
|
42805
|
+
const refs = composition.features.flatMap((f) => f.kind === "fields" ? Object.values(f.fields).flatMap(fieldReads).map((r) => r.source) : []);
|
|
42806
|
+
return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
|
|
42807
|
+
}
|
|
42808
|
+
function sourceProblems(fields, ctx) {
|
|
42809
|
+
const selfKey = ctx.self === null ? null : compositionSourceKey(ctx.self);
|
|
42810
|
+
return fields.flatMap((field) => field.reads.flatMap((read) => {
|
|
42811
|
+
const at = `features[${field.featureIndex}].fields.${field.path}`;
|
|
42812
|
+
const key = compositionSourceKey(read.source);
|
|
42813
|
+
if (key === selfKey) return fields.some((f) => f.cap === read.cap && f.path === read.fieldPath) ? [] : [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
|
|
42814
|
+
const desc = ctx.sources.get(key);
|
|
42815
|
+
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`)];
|
|
42816
|
+
if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
|
|
42817
|
+
if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
|
|
42818
|
+
return [];
|
|
42819
|
+
}));
|
|
42820
|
+
}
|
|
42821
|
+
/**
|
|
42822
|
+
* A cycle's node keys are `compositionReadKey` strings:
|
|
42823
|
+
* `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
|
|
42824
|
+
* CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
|
|
42825
|
+
* back out so the refusal can point at that exact field, matching the
|
|
42826
|
+
* documented `features[N].fields.<path>` convention — slice 1 has no nested
|
|
42827
|
+
* field writes, so splitting `rest` on the FIRST `.` is exact.
|
|
42828
|
+
*/
|
|
42829
|
+
function candidateFieldOfNode(nodeKey, candidateKey) {
|
|
42830
|
+
const prefix = `${candidateKey}#`;
|
|
42831
|
+
if (!nodeKey.startsWith(prefix)) return null;
|
|
42832
|
+
const rest = nodeKey.slice(prefix.length);
|
|
42833
|
+
const dot = rest.indexOf(".");
|
|
42834
|
+
if (dot === -1) return null;
|
|
42835
|
+
return {
|
|
42836
|
+
cap: rest.slice(0, dot),
|
|
42837
|
+
fieldPath: rest.slice(dot + 1)
|
|
42838
|
+
};
|
|
42839
|
+
}
|
|
42840
|
+
/** The first node of a cycle's path that is one of THIS composition's own
|
|
42841
|
+
* planned fields, as a documented-convention path; `'features'` when the
|
|
42842
|
+
* cycle runs entirely through other stored compositions. */
|
|
42843
|
+
function cyclePath(nodePath, candidateKey, fields) {
|
|
42844
|
+
for (const nodeKey of nodePath) {
|
|
42845
|
+
const hit = candidateFieldOfNode(nodeKey, candidateKey);
|
|
42846
|
+
if (hit === null) continue;
|
|
42847
|
+
const field = fields.find((f) => f.cap === hit.cap && f.path === hit.fieldPath);
|
|
42848
|
+
if (field !== void 0) return `features[${field.featureIndex}].fields.${field.path}`;
|
|
42849
|
+
}
|
|
42850
|
+
return "features";
|
|
42851
|
+
}
|
|
42852
|
+
/**
|
|
42853
|
+
* Cycle check runs against the CANDIDATE (this composition, targeting `self`
|
|
42854
|
+
* when it already exists, else a placeholder that cannot collide with a
|
|
42855
|
+
* stored target) plus every OTHER stored composition. Two stored peers that
|
|
42856
|
+
* (wrongly) share a target are corrupted state — `findCompositionCycles`
|
|
42857
|
+
* throws `CompositionGraphError` rather than silently picking one, and this
|
|
42858
|
+
* is refused BY NAME (D391) instead of escaping as an uncaught throw.
|
|
42859
|
+
*/
|
|
42860
|
+
function cycleProblems(composition, ctx, fields) {
|
|
42861
|
+
const candidate = {
|
|
42862
|
+
target: ctx.self ?? PENDING_SELF,
|
|
42863
|
+
composition
|
|
42864
|
+
};
|
|
42865
|
+
const candidateKey = compositionSourceKey(candidate.target);
|
|
42866
|
+
try {
|
|
42867
|
+
return findCompositionCycles(candidate, ctx.peers).map((c) => problem("cycle", cyclePath(c.path, candidateKey, fields), `a field reads itself: ${c.path.join(" → ")}`));
|
|
42868
|
+
} catch (err) {
|
|
42869
|
+
if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
|
|
42870
|
+
throw err;
|
|
42871
|
+
}
|
|
42872
|
+
}
|
|
42873
|
+
function validateComposition(composition, ctx) {
|
|
42874
|
+
const plan = planComposition(composition, ctx.lookupCap);
|
|
42875
|
+
const cycles = cycleProblems(composition, ctx, plan.fields);
|
|
42876
|
+
const problems = [
|
|
42877
|
+
...plan.problems,
|
|
42878
|
+
...sourceProblems(plan.fields, ctx),
|
|
42879
|
+
...cycles
|
|
42880
|
+
];
|
|
42881
|
+
return {
|
|
42882
|
+
ok: problems.length === 0,
|
|
42883
|
+
problems,
|
|
42884
|
+
fields: [...plan.fields]
|
|
42885
|
+
};
|
|
42886
|
+
}
|
|
42887
|
+
/**
|
|
41627
42888
|
* Runtime defaults -- used by ConfigManager.get() for backward compatibility
|
|
41628
42889
|
* until Plan B wires all runtime settings to the system_settings SQL table.
|
|
41629
42890
|
*
|
|
@@ -44325,60 +45586,6 @@ var DISPLAY_ICON_VOCABULARY = [
|
|
|
44325
45586
|
DISPLAY_ICON_VOCABULARY.map((e) => e.key);
|
|
44326
45587
|
DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
|
|
44327
45588
|
new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
|
|
44328
|
-
/** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
|
|
44329
|
-
* Zod v4 exposes `.unwrap()` on all three wrapper classes. */
|
|
44330
|
-
function unwrap(schema) {
|
|
44331
|
-
let s = schema;
|
|
44332
|
-
while (s instanceof zod.z.ZodNullable || s instanceof zod.z.ZodOptional || s instanceof zod.z.ZodDefault) s = s.unwrap();
|
|
44333
|
-
return s;
|
|
44334
|
-
}
|
|
44335
|
-
function leafKind(schema) {
|
|
44336
|
-
const s = unwrap(schema);
|
|
44337
|
-
if (s instanceof zod.z.ZodEnum) return {
|
|
44338
|
-
kind: "enum",
|
|
44339
|
-
enumValues: s.options
|
|
44340
|
-
};
|
|
44341
|
-
if (s instanceof zod.z.ZodNumber) return { kind: "number" };
|
|
44342
|
-
if (s instanceof zod.z.ZodString) return { kind: "string" };
|
|
44343
|
-
if (s instanceof zod.z.ZodBoolean) return { kind: "boolean" };
|
|
44344
|
-
return null;
|
|
44345
|
-
}
|
|
44346
|
-
/** Walk a cap status schema into flat wireable leaf fields (dotted paths).
|
|
44347
|
-
* Recurses into nested ZodObject (unwrapping nullable/optional/default first),
|
|
44348
|
-
* so fields with null live values are still offered. Arrays / unknown shapes
|
|
44349
|
-
* are skipped — never throws. */
|
|
44350
|
-
function enumerateSchemaFields(schema, prefix = "") {
|
|
44351
|
-
const root = unwrap(schema);
|
|
44352
|
-
if (!(root instanceof zod.z.ZodObject)) return [];
|
|
44353
|
-
const out = [];
|
|
44354
|
-
const shape = root.shape;
|
|
44355
|
-
for (const [key, field] of Object.entries(shape)) {
|
|
44356
|
-
const path = prefix ? `${prefix}.${key}` : key;
|
|
44357
|
-
const inner = unwrap(field);
|
|
44358
|
-
if (inner instanceof zod.z.ZodObject) {
|
|
44359
|
-
out.push(...enumerateSchemaFields(inner, path));
|
|
44360
|
-
continue;
|
|
44361
|
-
}
|
|
44362
|
-
const leaf = leafKind(field);
|
|
44363
|
-
if (leaf) out.push({
|
|
44364
|
-
path,
|
|
44365
|
-
kind: leaf.kind,
|
|
44366
|
-
...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
|
|
44367
|
-
});
|
|
44368
|
-
}
|
|
44369
|
-
return out;
|
|
44370
|
-
}
|
|
44371
|
-
/** Enumerate the per-item wireable fields of an item-array cap (see
|
|
44372
|
-
* `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
|
|
44373
|
-
* `keyField` (the key comes from the link's `itemKey`, never from a wired
|
|
44374
|
-
* source), each tagged `item: true` so the authoring UI collects an
|
|
44375
|
-
* `itemKey` alongside the field. Never throws. */
|
|
44376
|
-
function enumerateItemArrayFields(itemArray) {
|
|
44377
|
-
return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
|
|
44378
|
-
...f,
|
|
44379
|
-
item: true
|
|
44380
|
-
}));
|
|
44381
|
-
}
|
|
44382
45589
|
/**
|
|
44383
45590
|
* Runtime list of cap names with status. Used by the settings
|
|
44384
45591
|
* aggregator to enumerate caps whose `status` should be polled +
|
|
@@ -44500,6 +45707,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
|
|
|
44500
45707
|
carbonMonoxideCapability,
|
|
44501
45708
|
climateControlCapability,
|
|
44502
45709
|
colorCapability,
|
|
45710
|
+
composerCapability,
|
|
44503
45711
|
connectionTestCapability,
|
|
44504
45712
|
connectivityCapability,
|
|
44505
45713
|
consumablesCapability,
|
|
@@ -45573,6 +46781,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
45573
46781
|
addonId: null,
|
|
45574
46782
|
access: "create"
|
|
45575
46783
|
},
|
|
46784
|
+
"composer.applyBlocks": {
|
|
46785
|
+
capName: "composer",
|
|
46786
|
+
capScope: "system",
|
|
46787
|
+
addonId: null,
|
|
46788
|
+
access: "create"
|
|
46789
|
+
},
|
|
46790
|
+
"composer.getStates": {
|
|
46791
|
+
capName: "composer",
|
|
46792
|
+
capScope: "system",
|
|
46793
|
+
addonId: null,
|
|
46794
|
+
access: "view"
|
|
46795
|
+
},
|
|
45576
46796
|
"connectionTest.describeTest": {
|
|
45577
46797
|
capName: "connection-test",
|
|
45578
46798
|
capScope: "system",
|
|
@@ -45633,6 +46853,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
45633
46853
|
addonId: null,
|
|
45634
46854
|
access: "create"
|
|
45635
46855
|
},
|
|
46856
|
+
"coreBlocks.createComposition": {
|
|
46857
|
+
capName: "core-blocks",
|
|
46858
|
+
capScope: "system",
|
|
46859
|
+
addonId: null,
|
|
46860
|
+
access: "create"
|
|
46861
|
+
},
|
|
45636
46862
|
"coreBlocks.delete": {
|
|
45637
46863
|
capName: "core-blocks",
|
|
45638
46864
|
capScope: "system",
|
|
@@ -45645,6 +46871,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
45645
46871
|
addonId: null,
|
|
45646
46872
|
access: "view"
|
|
45647
46873
|
},
|
|
46874
|
+
"coreBlocks.getComposition": {
|
|
46875
|
+
capName: "core-blocks",
|
|
46876
|
+
capScope: "system",
|
|
46877
|
+
addonId: null,
|
|
46878
|
+
access: "view"
|
|
46879
|
+
},
|
|
45648
46880
|
"coreBlocks.getTypeDefs": {
|
|
45649
46881
|
capName: "core-blocks",
|
|
45650
46882
|
capScope: "system",
|
|
@@ -45657,6 +46889,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
45657
46889
|
addonId: null,
|
|
45658
46890
|
access: "view"
|
|
45659
46891
|
},
|
|
46892
|
+
"coreBlocks.listCompositions": {
|
|
46893
|
+
capName: "core-blocks",
|
|
46894
|
+
capScope: "system",
|
|
46895
|
+
addonId: null,
|
|
46896
|
+
access: "view"
|
|
46897
|
+
},
|
|
45660
46898
|
"coreBlocks.restart": {
|
|
45661
46899
|
capName: "core-blocks",
|
|
45662
46900
|
capScope: "system",
|
|
@@ -45675,6 +46913,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
45675
46913
|
addonId: null,
|
|
45676
46914
|
access: "create"
|
|
45677
46915
|
},
|
|
46916
|
+
"coreBlocks.updateComposition": {
|
|
46917
|
+
capName: "core-blocks",
|
|
46918
|
+
capScope: "system",
|
|
46919
|
+
addonId: null,
|
|
46920
|
+
access: "create"
|
|
46921
|
+
},
|
|
46922
|
+
"coreBlocks.validateComposition": {
|
|
46923
|
+
capName: "core-blocks",
|
|
46924
|
+
capScope: "system",
|
|
46925
|
+
addonId: null,
|
|
46926
|
+
access: "view"
|
|
46927
|
+
},
|
|
45678
46928
|
"cover.close": {
|
|
45679
46929
|
capName: "cover",
|
|
45680
46930
|
capScope: "device",
|
|
@@ -55151,6 +56401,18 @@ Object.defineProperty(exports, "CAP_NAMES_WITH_STATUS", {
|
|
|
55151
56401
|
return CAP_NAMES_WITH_STATUS;
|
|
55152
56402
|
}
|
|
55153
56403
|
});
|
|
56404
|
+
Object.defineProperty(exports, "COMPOSER_ADDON_ID", {
|
|
56405
|
+
enumerable: true,
|
|
56406
|
+
get: function() {
|
|
56407
|
+
return COMPOSER_ADDON_ID;
|
|
56408
|
+
}
|
|
56409
|
+
});
|
|
56410
|
+
Object.defineProperty(exports, "COMPOSER_RUNNER_ID", {
|
|
56411
|
+
enumerable: true,
|
|
56412
|
+
get: function() {
|
|
56413
|
+
return COMPOSER_RUNNER_ID;
|
|
56414
|
+
}
|
|
56415
|
+
});
|
|
55154
56416
|
Object.defineProperty(exports, "CORE_BLOCKS_ADDON_ID", {
|
|
55155
56417
|
enumerable: true,
|
|
55156
56418
|
get: function() {
|
|
@@ -55445,6 +56707,12 @@ Object.defineProperty(exports, "batteryCapability", {
|
|
|
55445
56707
|
return batteryCapability;
|
|
55446
56708
|
}
|
|
55447
56709
|
});
|
|
56710
|
+
Object.defineProperty(exports, "buildCapabilityLookup", {
|
|
56711
|
+
enumerable: true,
|
|
56712
|
+
get: function() {
|
|
56713
|
+
return buildCapabilityLookup;
|
|
56714
|
+
}
|
|
56715
|
+
});
|
|
55448
56716
|
Object.defineProperty(exports, "buildFfmpegArgs", {
|
|
55449
56717
|
enumerable: true,
|
|
55450
56718
|
get: function() {
|
|
@@ -55469,18 +56737,66 @@ Object.defineProperty(exports, "collectSecretConfigKeys", {
|
|
|
55469
56737
|
return collectSecretConfigKeys;
|
|
55470
56738
|
}
|
|
55471
56739
|
});
|
|
56740
|
+
Object.defineProperty(exports, "composedDeviceRef", {
|
|
56741
|
+
enumerable: true,
|
|
56742
|
+
get: function() {
|
|
56743
|
+
return composedDeviceRef;
|
|
56744
|
+
}
|
|
56745
|
+
});
|
|
56746
|
+
Object.defineProperty(exports, "composedDeviceStableId", {
|
|
56747
|
+
enumerable: true,
|
|
56748
|
+
get: function() {
|
|
56749
|
+
return composedDeviceStableId;
|
|
56750
|
+
}
|
|
56751
|
+
});
|
|
56752
|
+
Object.defineProperty(exports, "composerCapability", {
|
|
56753
|
+
enumerable: true,
|
|
56754
|
+
get: function() {
|
|
56755
|
+
return composerCapability;
|
|
56756
|
+
}
|
|
56757
|
+
});
|
|
56758
|
+
Object.defineProperty(exports, "compositionSliceKey", {
|
|
56759
|
+
enumerable: true,
|
|
56760
|
+
get: function() {
|
|
56761
|
+
return compositionSliceKey;
|
|
56762
|
+
}
|
|
56763
|
+
});
|
|
56764
|
+
Object.defineProperty(exports, "compositionSourceKey", {
|
|
56765
|
+
enumerable: true,
|
|
56766
|
+
get: function() {
|
|
56767
|
+
return compositionSourceKey;
|
|
56768
|
+
}
|
|
56769
|
+
});
|
|
56770
|
+
Object.defineProperty(exports, "compositionSourceRefs", {
|
|
56771
|
+
enumerable: true,
|
|
56772
|
+
get: function() {
|
|
56773
|
+
return compositionSourceRefs;
|
|
56774
|
+
}
|
|
56775
|
+
});
|
|
55472
56776
|
Object.defineProperty(exports, "coreBlockAddonId", {
|
|
55473
56777
|
enumerable: true,
|
|
55474
56778
|
get: function() {
|
|
55475
56779
|
return coreBlockAddonId;
|
|
55476
56780
|
}
|
|
55477
56781
|
});
|
|
56782
|
+
Object.defineProperty(exports, "coreBlockComposition", {
|
|
56783
|
+
enumerable: true,
|
|
56784
|
+
get: function() {
|
|
56785
|
+
return coreBlockComposition;
|
|
56786
|
+
}
|
|
56787
|
+
});
|
|
55478
56788
|
Object.defineProperty(exports, "coreBlockIdFromAddonId", {
|
|
55479
56789
|
enumerable: true,
|
|
55480
56790
|
get: function() {
|
|
55481
56791
|
return coreBlockIdFromAddonId;
|
|
55482
56792
|
}
|
|
55483
56793
|
});
|
|
56794
|
+
Object.defineProperty(exports, "coreBlockSourceKind", {
|
|
56795
|
+
enumerable: true,
|
|
56796
|
+
get: function() {
|
|
56797
|
+
return coreBlockSourceKind;
|
|
56798
|
+
}
|
|
56799
|
+
});
|
|
55484
56800
|
Object.defineProperty(exports, "coreBlocksCapability", {
|
|
55485
56801
|
enumerable: true,
|
|
55486
56802
|
get: function() {
|
|
@@ -55595,6 +56911,12 @@ Object.defineProperty(exports, "errMsg", {
|
|
|
55595
56911
|
return errMsg;
|
|
55596
56912
|
}
|
|
55597
56913
|
});
|
|
56914
|
+
Object.defineProperty(exports, "evaluateCompositionField", {
|
|
56915
|
+
enumerable: true,
|
|
56916
|
+
get: function() {
|
|
56917
|
+
return evaluateCompositionField;
|
|
56918
|
+
}
|
|
56919
|
+
});
|
|
55598
56920
|
Object.defineProperty(exports, "evaluateSensorEdge", {
|
|
55599
56921
|
enumerable: true,
|
|
55600
56922
|
get: function() {
|
|
@@ -55835,6 +57157,12 @@ Object.defineProperty(exports, "parseStreamParamsFormPatch", {
|
|
|
55835
57157
|
return parseStreamParamsFormPatch;
|
|
55836
57158
|
}
|
|
55837
57159
|
});
|
|
57160
|
+
Object.defineProperty(exports, "planComposition", {
|
|
57161
|
+
enumerable: true,
|
|
57162
|
+
get: function() {
|
|
57163
|
+
return planComposition;
|
|
57164
|
+
}
|
|
57165
|
+
});
|
|
55838
57166
|
Object.defineProperty(exports, "platformProbeCapability", {
|
|
55839
57167
|
enumerable: true,
|
|
55840
57168
|
get: function() {
|
|
@@ -55979,12 +57307,24 @@ Object.defineProperty(exports, "streamQualityLabel", {
|
|
|
55979
57307
|
return streamQualityLabel;
|
|
55980
57308
|
}
|
|
55981
57309
|
});
|
|
57310
|
+
Object.defineProperty(exports, "toExpressionValue", {
|
|
57311
|
+
enumerable: true,
|
|
57312
|
+
get: function() {
|
|
57313
|
+
return toExpressionValue;
|
|
57314
|
+
}
|
|
57315
|
+
});
|
|
55982
57316
|
Object.defineProperty(exports, "userManagementCapability", {
|
|
55983
57317
|
enumerable: true,
|
|
55984
57318
|
get: function() {
|
|
55985
57319
|
return userManagementCapability;
|
|
55986
57320
|
}
|
|
55987
57321
|
});
|
|
57322
|
+
Object.defineProperty(exports, "validateComposition", {
|
|
57323
|
+
enumerable: true,
|
|
57324
|
+
get: function() {
|
|
57325
|
+
return validateComposition;
|
|
57326
|
+
}
|
|
57327
|
+
});
|
|
55988
57328
|
Object.defineProperty(exports, "vectorDimFromBase64", {
|
|
55989
57329
|
enumerable: true,
|
|
55990
57330
|
get: function() {
|