@camstack/system 1.2.308 → 1.2.310

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/index.js +1 -1
  12. package/dist/builtins/camera-grid/index.mjs +1 -1
  13. package/dist/builtins/composer/composed-device.d.ts +26 -0
  14. package/dist/builtins/composer/composer.addon.d.ts +12 -0
  15. package/dist/builtins/composer/composer.addon.js +1682 -0
  16. package/dist/builtins/composer/composer.addon.mjs +1677 -0
  17. package/dist/builtins/composer/composer.d.ts +103 -0
  18. package/dist/builtins/composer/composition-declarations.d.ts +53 -0
  19. package/dist/builtins/composer/composition-runtime.d.ts +88 -0
  20. package/dist/builtins/composer/deadline-timer.d.ts +18 -0
  21. package/dist/builtins/composer/index.d.ts +1 -0
  22. package/dist/builtins/composer/index.js +8 -0
  23. package/dist/builtins/composer/index.mjs +2 -0
  24. package/dist/builtins/composer/source-tracker.d.ts +78 -0
  25. package/dist/builtins/console-logging/index.js +1 -1
  26. package/dist/builtins/console-logging/index.mjs +1 -1
  27. package/dist/builtins/core-blocks/block-supervisor.d.ts +32 -1
  28. package/dist/builtins/core-blocks/blocks-integration.d.ts +5 -0
  29. package/dist/builtins/core-blocks/composition-api.d.ts +25 -0
  30. package/dist/builtins/core-blocks/composition-sources.d.ts +10 -0
  31. package/dist/builtins/core-blocks/core-block-store.d.ts +34 -2
  32. package/dist/builtins/core-blocks/core-blocks.addon.d.ts +6 -0
  33. package/dist/builtins/core-blocks/core-blocks.addon.js +504 -11
  34. package/dist/builtins/core-blocks/core-blocks.addon.mjs +504 -11
  35. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  36. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  37. package/dist/builtins/doorbell/virtual-doorbell.addon.js +10 -31
  38. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +9 -30
  39. package/dist/builtins/hub-forwarder/index.js +1 -1
  40. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  41. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  42. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  43. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  44. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  45. package/dist/builtins/local-network/local-network.addon.js +1 -1
  46. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  47. package/dist/builtins/loki-logging/index.js +1 -1
  48. package/dist/builtins/loki-logging/index.mjs +1 -1
  49. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  50. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  51. package/dist/builtins/platform-probe/index.js +1 -1
  52. package/dist/builtins/platform-probe/index.mjs +1 -1
  53. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  54. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  55. package/dist/builtins/shared/device-state-changed.d.ts +17 -0
  56. package/dist/builtins/snapshot/index.js +1 -1
  57. package/dist/builtins/snapshot/index.mjs +1 -1
  58. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  59. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  60. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  61. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  62. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  63. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  64. package/dist/builtins/system-config/system-config.addon.js +1 -1
  65. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  66. package/dist/builtins/winston-logging/index.js +1 -1
  67. package/dist/builtins/winston-logging/index.mjs +1 -1
  68. package/dist/device-state-changed-Bk7cvTnz.js +31 -0
  69. package/dist/device-state-changed-DoBbkyEI.mjs +20 -0
  70. package/dist/{dist-DygWCI8k.mjs → dist-B78pkrps.mjs} +1298 -80
  71. package/dist/{dist-DGhk7Tay.js → dist-Cy6DFFjs.js} +1387 -79
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.js +4 -1
  74. package/dist/index.mjs +4 -2
  75. package/dist/{retired-settings-keys-z02aqTy7.mjs → retired-settings-keys-B4_hiWsm.mjs} +1 -1
  76. package/dist/{retired-settings-keys-IZ9MH0GO.js → retired-settings-keys-D9sgD6Yg.js} +1 -1
  77. package/package.json +20 -1
@@ -6647,6 +6647,16 @@ var ModelCatalogEntrySchema = z.object({
6647
6647
  */
6648
6648
  postprocessor: z.custom().optional(),
6649
6649
  /**
6650
+ * Per-MODEL default confidence floor. Absent ⇒ the step's
6651
+ * `StepDefinition.defaultConfidence` applies. A score is a property of one
6652
+ * model's output scale, not of the step slot it sits in: YuNet's
6653
+ * `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
6654
+ * face gallery with wheels and hands (D662, amends D645). An operator's
6655
+ * explicit value still wins over it. Resolved in ONE place —
6656
+ * `addon-pipeline/.../registry/effective-confidence.ts`.
6657
+ */
6658
+ defaultConfidence: z.number().min(0).max(1).optional(),
6659
+ /**
6650
6660
  * When true, the executor produces a landmark-aligned crop (similarity warp
6651
6661
  * onto the canonical template) before this step runs, instead of a plain
6652
6662
  * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
@@ -6798,23 +6808,6 @@ var ExpressionEvalError = class extends Error {
6798
6808
  this.name = "ExpressionEvalError";
6799
6809
  }
6800
6810
  };
6801
- /**
6802
- * Frozen, null-prototype builtin function table for the expression engine
6803
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
6804
- * parser rejects any callee not in it, and the evaluator gates each call on an
6805
- * own-property check against it.
6806
- *
6807
- * Because the object has a NULL prototype AND is `Object.freeze`d:
6808
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
6809
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
6810
- * (there is no `Object.prototype` in the chain), so those names are not
6811
- * callable — they are simply "unknown function" at parse time.
6812
- *
6813
- * Every numeric argument is validated as a finite number and every numeric
6814
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
6815
- * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
6816
- * closed rather than emitting a garbage value.
6817
- */
6818
6811
  function asFiniteNumber(value, name, index) {
6819
6812
  if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
6820
6813
  return value;
@@ -6830,6 +6823,10 @@ function finiteResult(value, name) {
6830
6823
  function allFiniteNumbers(args, name) {
6831
6824
  return args.map((a, idx) => asFiniteNumber(a, name, idx));
6832
6825
  }
6826
+ function asBoolean$1(value, name, index) {
6827
+ if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
6828
+ return value;
6829
+ }
6833
6830
  var INF = Number.POSITIVE_INFINITY;
6834
6831
  var table = {
6835
6832
  min: {
@@ -6930,9 +6927,44 @@ var table = {
6930
6927
  if (from === to) return x;
6931
6928
  throw new ExpressionEvalError("convert: unit conversion table not installed");
6932
6929
  }
6930
+ },
6931
+ any: {
6932
+ minArgs: 1,
6933
+ maxArgs: INF,
6934
+ apply: (args) => args.map((a, i) => asBoolean$1(a, "any", i)).some((b) => b)
6935
+ },
6936
+ all: {
6937
+ minArgs: 1,
6938
+ maxArgs: INF,
6939
+ apply: (args) => args.map((a, i) => asBoolean$1(a, "all", i)).every((b) => b)
6940
+ },
6941
+ within: {
6942
+ minArgs: 2,
6943
+ maxArgs: 2,
6944
+ apply: (args, hooks) => {
6945
+ const windowMs = asFiniteNumber(args[1], "within", 1);
6946
+ if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
6947
+ const at = args[0];
6948
+ if (at === null) return false;
6949
+ const ts = asFiniteNumber(at, "within", 0);
6950
+ const now = hooks.now;
6951
+ if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
6952
+ const inside = now - ts <= windowMs;
6953
+ if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
6954
+ return inside;
6955
+ }
6956
+ },
6957
+ latest: {
6958
+ minArgs: 1,
6959
+ maxArgs: INF,
6960
+ apply: (args) => {
6961
+ const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
6962
+ return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
6963
+ }
6933
6964
  }
6934
6965
  };
6935
- Object.freeze(Object.assign(Object.create(null), table));
6966
+ /** Frozen, null-prototype builtin table. */
6967
+ var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
6936
6968
  /** The set of valid builtin names — used by the parser to reject unknown
6937
6969
  * callees at parse time (immediate author feedback). */
6938
6970
  var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
@@ -7345,7 +7377,118 @@ function getCached(source) {
7345
7377
  function compileExpressionSafe(source) {
7346
7378
  return getCached(source);
7347
7379
  }
7348
- Object.freeze({});
7380
+ /**
7381
+ * Tree-walking evaluator for the safe expression mini-language.
7382
+ *
7383
+ * SECURITY (spec §4 rule 2/5):
7384
+ * - The scope is an `Object.create(null)` copy of ONLY the caller's own
7385
+ * enumerable binding entries, so `name in scope` is a pure own-key check and
7386
+ * `constructor` / `__proto__` / `toString` are plain unknown identifiers.
7387
+ * - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
7388
+ * directly — the only external calls are into the frozen builtin table.
7389
+ * - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
7390
+ * construction; the step counter is defense-in-depth for a crafted max-size
7391
+ * AST. Nothing blocks: there are no timers, awaits or unbounded loops.
7392
+ */
7393
+ var EMPTY_HOOKS = Object.freeze({});
7394
+ /** Build a null-prototype scope from own-enumerable binding entries. Inherited
7395
+ * keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
7396
+ * so nothing smuggles in via the prototype chain. */
7397
+ function createExpressionScope(bindings) {
7398
+ const scope = Object.create(null);
7399
+ for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
7400
+ return scope;
7401
+ }
7402
+ function isFiniteNumber(value) {
7403
+ return typeof value === "number" && Number.isFinite(value);
7404
+ }
7405
+ /** JS truthiness of a primitive value. */
7406
+ function truthy(value) {
7407
+ return Boolean(value);
7408
+ }
7409
+ function requireFinite(value, context) {
7410
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
7411
+ return value;
7412
+ }
7413
+ function step(ctx) {
7414
+ ctx.steps += 1;
7415
+ if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
7416
+ }
7417
+ function evalNode(node, ctx) {
7418
+ step(ctx);
7419
+ switch (node.kind) {
7420
+ case "literal": return node.value;
7421
+ case "identifier":
7422
+ if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
7423
+ return ctx.scope[node.name];
7424
+ case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
7425
+ case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
7426
+ case "logical": {
7427
+ const left = evalNode(node.left, ctx);
7428
+ if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
7429
+ return truthy(left) ? left : evalNode(node.right, ctx);
7430
+ }
7431
+ case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
7432
+ case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
7433
+ }
7434
+ }
7435
+ function evalUnary(op, operand) {
7436
+ if (op === "!") return !truthy(operand);
7437
+ if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
7438
+ return requireFinite(-operand, "unary \"-\"");
7439
+ }
7440
+ function evalBinary(op, left, right) {
7441
+ switch (op) {
7442
+ case "==": return left === right;
7443
+ case "!=": return left !== right;
7444
+ case "+":
7445
+ if (typeof left === "string" && typeof right === "string") return left + right;
7446
+ if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
7447
+ throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
7448
+ case "-":
7449
+ case "*":
7450
+ case "/":
7451
+ case "%":
7452
+ if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
7453
+ return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
7454
+ case "<":
7455
+ case "<=":
7456
+ case ">":
7457
+ case ">=":
7458
+ if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
7459
+ if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
7460
+ throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
7461
+ }
7462
+ }
7463
+ function evalCall(callee, args, hooks) {
7464
+ if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
7465
+ const builtin = EXPRESSION_BUILTINS[callee];
7466
+ if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
7467
+ return builtin.apply(args, hooks);
7468
+ }
7469
+ /** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
7470
+ * runtime failure (unknown identifier, type mismatch, non-finite result,
7471
+ * step-budget exhaustion). */
7472
+ function evaluateAst(node, scope, opts) {
7473
+ return evalNode(node, {
7474
+ scope,
7475
+ hooks: opts?.hooks ?? EMPTY_HOOKS,
7476
+ maxSteps: opts?.maxSteps ?? 4096,
7477
+ steps: 0
7478
+ });
7479
+ }
7480
+ /**
7481
+ * Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
7482
+ * Non-primitive values (objects, arrays, `undefined`, functions, bigint,
7483
+ * symbol) and non-finite numbers become `undefined` so the caller can apply
7484
+ * its binding-miss policy (→ `null`). `null` itself is a valid value.
7485
+ */
7486
+ function toExpressionValue(raw) {
7487
+ if (raw === null) return null;
7488
+ if (typeof raw === "string") return raw;
7489
+ if (typeof raw === "boolean") return raw;
7490
+ if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
7491
+ }
7349
7492
  /**
7350
7493
  * Author-time validation. Returns `null` when the source is valid, else a
7351
7494
  * human-readable error message. Checks: the expression compiles; binding count
@@ -7371,6 +7514,42 @@ function validateExpressionSource(src) {
7371
7514
  return null;
7372
7515
  }
7373
7516
  /**
7517
+ * Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
7518
+ * plus the injected `now` (supplied by the caller for determinism and
7519
+ * testability), compiles via the LRU, and evaluates. Any failure (parse or
7520
+ * eval) returns `{ ok: false }` — the caller treats that as "skip this
7521
+ * derivation", never as a throw that takes the pass down.
7522
+ */
7523
+ function evaluateExpressionSource(expr, bindingValues, now, opts) {
7524
+ const compiled = compileExpressionSafe(expr);
7525
+ if (!compiled.ok) return {
7526
+ ok: false,
7527
+ error: compiled.error
7528
+ };
7529
+ const scope = createExpressionScope({
7530
+ ...bindingValues,
7531
+ ["now"]: now
7532
+ });
7533
+ const hooks = {
7534
+ ...opts?.hooks,
7535
+ now
7536
+ };
7537
+ try {
7538
+ return {
7539
+ ok: true,
7540
+ value: evaluateAst(compiled.parsed.ast, scope, {
7541
+ ...opts,
7542
+ hooks
7543
+ })
7544
+ };
7545
+ } catch (err) {
7546
+ return {
7547
+ ok: false,
7548
+ error: err instanceof ExpressionEvalError ? err.message : String(err)
7549
+ };
7550
+ }
7551
+ }
7552
+ /**
7374
7553
  * What an expression's named bindings READ from.
7375
7554
  *
7376
7555
  * Salvaged verbatim from the deleted device-link mechanism. Wiring's source
@@ -7570,7 +7749,7 @@ var AutomationActionSchema = z.discriminatedUnion("kind", [
7570
7749
  code: z.string().min(1).max(2e4)
7571
7750
  })
7572
7751
  ]);
7573
- z.object({
7752
+ var AutomationRecipeSchema = z.object({
7574
7753
  triggers: z.array(AutomationTriggerSchema),
7575
7754
  conditions: AutomationConditionSchema.optional(),
7576
7755
  actions: z.array(AutomationActionSchema)
@@ -10594,6 +10773,270 @@ function kebabToCamel(s) {
10594
10773
  return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
10595
10774
  }
10596
10775
  /**
10776
+ * A COMPOSITION: a device assembled from other devices' state (D659).
10777
+ *
10778
+ * The data half of the composed-devices spec §1. A composition is stored inside
10779
+ * a core block (`CoreBlock.source.kind === 'composition'`) and run by the shared
10780
+ * `composer` runner. It names a TARGET and, for each capability of the target, a
10781
+ * FIELD MAP: every state field has exactly one source.
10782
+ *
10783
+ * Sources are keyed by `(addonId, stableId)`, NEVER by numeric device id. A
10784
+ * resync reissues numeric ids, and a composition that stored them would be
10785
+ * orphaned by the first re-adoption (the linked-devices lesson). A `stableId` is
10786
+ * unique only within its owning addon, so the owner travels with it.
10787
+ *
10788
+ * Later slices EXTEND these unions and do not reshape them. The variants a later
10789
+ * slice implements are already in the wire shape, and `validateComposition`
10790
+ * refuses them BY NAME until then:
10791
+ * target `existing` → slice 2 · feature `passthrough` → slice 3 ·
10792
+ * `commands` (`forward`) → slice 3 · source `code` → slice 4.
10793
+ */
10794
+ /** The owner of every composed device: the `composer` builtin. */
10795
+ var COMPOSER_ADDON_ID = "composer";
10796
+ /** A composed device's stableId is this prefix plus the block id. */
10797
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
10798
+ /** Longest snippet a `code` source may carry (slice 4). */
10799
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
10800
+ var CompositionSourceRefSchema = z.object({
10801
+ /** The owning addon; `stableId` is unique only within it. */
10802
+ addonId: z.string().min(1),
10803
+ stableId: z.string().min(1)
10804
+ });
10805
+ /** One field of one capability of one source device. */
10806
+ var CompositionFieldReadSchema = z.object({
10807
+ source: CompositionSourceRefSchema,
10808
+ cap: z.string().min(1),
10809
+ /** Dotted path into the source cap's runtime-state slice. */
10810
+ fieldPath: z.string().min(1)
10811
+ });
10812
+ /** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
10813
+ var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: z.literal("from") });
10814
+ /** A constant binding. No device is read. */
10815
+ var CompositionLiteralBindingSchema = z.object({
10816
+ kind: z.literal("literal"),
10817
+ value: z.union([
10818
+ z.string(),
10819
+ z.number(),
10820
+ z.boolean(),
10821
+ z.null()
10822
+ ])
10823
+ });
10824
+ var CompositionBindingSchema = z.discriminatedUnion("kind", [CompositionFromSourceSchema, CompositionLiteralBindingSchema]);
10825
+ /**
10826
+ * `expression`: a formula over named bindings, in the salvaged expression
10827
+ * engine. Validated at parse by the SAME function every other consumer runs,
10828
+ * so the editor and the store cannot disagree.
10829
+ */
10830
+ var CompositionExpressionSourceSchema = z.object({
10831
+ kind: z.literal("expression"),
10832
+ expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
10833
+ bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
10834
+ }).superRefine((src, ctx) => {
10835
+ const err = validateExpressionSource(src);
10836
+ if (err !== null) ctx.addIssue({
10837
+ code: "custom",
10838
+ message: err,
10839
+ path: ["expr"]
10840
+ });
10841
+ });
10842
+ /** `code`: RESERVED for slice 4 (own runner, one-way eject). */
10843
+ var CompositionCodeSourceSchema = z.object({
10844
+ kind: z.literal("code"),
10845
+ code: z.string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
10846
+ });
10847
+ var CompositionFieldSourceSchema = z.discriminatedUnion("kind", [
10848
+ CompositionFromSourceSchema,
10849
+ CompositionExpressionSourceSchema,
10850
+ CompositionCodeSourceSchema
10851
+ ]);
10852
+ /** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
10853
+ var CompositionForwardCommandSchema = z.object({
10854
+ kind: z.literal("forward"),
10855
+ source: CompositionSourceRefSchema,
10856
+ cap: z.string().min(1),
10857
+ method: z.string().min(1)
10858
+ });
10859
+ var CompositionCommandTargetSchema = z.discriminatedUnion("kind", [CompositionForwardCommandSchema, CompositionCodeSourceSchema]);
10860
+ var CompositionFieldsFeatureSchema = z.object({
10861
+ kind: z.literal("fields"),
10862
+ cap: z.string().min(1),
10863
+ fields: z.record(z.string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
10864
+ /** RESERVED for slice 3; refused by the validator when non-empty. */
10865
+ commands: z.record(z.string().min(1), CompositionCommandTargetSchema).optional()
10866
+ });
10867
+ /** RESERVED for slice 3: a media / data-plane cap, whole, from one source. */
10868
+ var CompositionPassthroughFeatureSchema = z.object({
10869
+ kind: z.literal("passthrough"),
10870
+ cap: z.string().min(1),
10871
+ source: CompositionSourceRefSchema
10872
+ });
10873
+ var CompositionFeatureSchema = z.discriminatedUnion("kind", [CompositionFieldsFeatureSchema, CompositionPassthroughFeatureSchema]);
10874
+ /**
10875
+ * A NEW device. It has no `name` of its own: the block's name IS the device's
10876
+ * name (2026-08-08). A second copy here would be a second writer of one string (D62).
10877
+ */
10878
+ var CompositionNewTargetSchema = z.object({
10879
+ kind: z.literal("new"),
10880
+ type: z.enum(DeviceType),
10881
+ role: z.enum(DeviceRole).optional()
10882
+ });
10883
+ /** RESERVED for slice 2: customize an existing device. */
10884
+ var CompositionExistingTargetSchema = z.object({
10885
+ kind: z.literal("existing"),
10886
+ device: CompositionSourceRefSchema
10887
+ });
10888
+ var CompositionTargetSchema = z.discriminatedUnion("kind", [CompositionNewTargetSchema, CompositionExistingTargetSchema]);
10889
+ var CompositionSchema = z.object({
10890
+ target: CompositionTargetSchema,
10891
+ features: z.array(CompositionFeatureSchema).min(1).max(16)
10892
+ }).superRefine((composition, ctx) => {
10893
+ const seen = /* @__PURE__ */ new Set();
10894
+ composition.features.forEach((feature, index) => {
10895
+ if (seen.has(feature.cap)) ctx.addIssue({
10896
+ code: "custom",
10897
+ message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
10898
+ path: [
10899
+ "features",
10900
+ index,
10901
+ "cap"
10902
+ ]
10903
+ });
10904
+ seen.add(feature.cap);
10905
+ });
10906
+ });
10907
+ function compositionSourceKey(ref) {
10908
+ return `${ref.addonId}/${ref.stableId}`;
10909
+ }
10910
+ function compositionSliceKey(ref, cap) {
10911
+ return `${compositionSourceKey(ref)}#${cap}`;
10912
+ }
10913
+ function compositionReadKey(read) {
10914
+ return `${compositionSliceKey(read.source, read.cap)}.${read.fieldPath}`;
10915
+ }
10916
+ function composedDeviceStableId(blockId) {
10917
+ return `${COMPOSED_DEVICE_STABLE_ID_PREFIX}${blockId}`;
10918
+ }
10919
+ function composedDeviceRef(blockId) {
10920
+ return {
10921
+ addonId: COMPOSER_ADDON_ID,
10922
+ stableId: composedDeviceStableId(blockId)
10923
+ };
10924
+ }
10925
+ /** Every device field a source reads: `from` is one read, an expression is its `from` bindings. */
10926
+ function fieldReads(source) {
10927
+ switch (source.kind) {
10928
+ case "from": return [{
10929
+ source: source.source,
10930
+ cap: source.cap,
10931
+ fieldPath: source.fieldPath
10932
+ }];
10933
+ case "expression": return Object.values(source.bindings).flatMap((binding) => binding.kind === "from" ? [{
10934
+ source: binding.source,
10935
+ cap: binding.cap,
10936
+ fieldPath: binding.fieldPath
10937
+ }] : []);
10938
+ case "code": return [];
10939
+ }
10940
+ }
10941
+ /**
10942
+ * What the validator says about a composition and what the composer says about
10943
+ * a running one: the wire shapes the API returns (D659).
10944
+ */
10945
+ var CompositionProblemCodeSchema = z.enum([
10946
+ "reserved-variant",
10947
+ "unknown-capability",
10948
+ "not-composable",
10949
+ "wrong-device-type",
10950
+ "has-commands",
10951
+ "unknown-field",
10952
+ "nested-field",
10953
+ "missing-field",
10954
+ "kind-mismatch",
10955
+ "may-be-null",
10956
+ "source-missing",
10957
+ "source-unreadable",
10958
+ "source-capability-missing",
10959
+ "source-field-unknown",
10960
+ "expression-invalid",
10961
+ "cycle",
10962
+ "duplicate-target"
10963
+ ]);
10964
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
10965
+ var CompositionProblemSchema = z.object({
10966
+ code: CompositionProblemCodeSchema,
10967
+ path: z.string(),
10968
+ message: z.string()
10969
+ });
10970
+ var CompositionFieldKindSchema = z.enum([
10971
+ "string",
10972
+ "number",
10973
+ "boolean",
10974
+ "enum"
10975
+ ]);
10976
+ /** How a field says "unavailable": `null` when its schema allows it, else the device goes offline (D393). */
10977
+ var CompositionUnavailableModeSchema = z.enum(["null", "offline"]);
10978
+ /**
10979
+ * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
10980
+ * field of its cap changes value · `constant` reads nothing, computed once.
10981
+ */
10982
+ var CompositionFieldRoleSchema = z.enum([
10983
+ "derived",
10984
+ "clock",
10985
+ "constant"
10986
+ ]);
10987
+ var CompositionFieldPlanSchema = z.object({
10988
+ cap: z.string(),
10989
+ path: z.string(),
10990
+ /** Index into `composition.features` this field's capability came from —
10991
+ * lets a source/cycle problem name its path as `features[N].fields.<path>`
10992
+ * (the same convention `planComposition`'s own problems already use). */
10993
+ featureIndex: z.number().int().nonnegative(),
10994
+ kind: CompositionFieldKindSchema,
10995
+ nullable: z.boolean(),
10996
+ required: z.boolean(),
10997
+ unavailable: CompositionUnavailableModeSchema,
10998
+ role: CompositionFieldRoleSchema,
10999
+ source: CompositionFieldSourceSchema,
11000
+ reads: z.array(CompositionFieldReadSchema)
11001
+ });
11002
+ var CompositionValidationSchema = z.object({
11003
+ ok: z.boolean(),
11004
+ problems: z.array(CompositionProblemSchema),
11005
+ fields: z.array(CompositionFieldPlanSchema)
11006
+ });
11007
+ /** Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts` (Task 11). */
11008
+ var CompositionRunStatusSchema = z.enum([
11009
+ "stopped",
11010
+ "starting",
11011
+ "running",
11012
+ "failed"
11013
+ ]);
11014
+ var CompositionFieldStateKindSchema = z.enum([
11015
+ "fresh",
11016
+ "stale",
11017
+ "unavailable"
11018
+ ]);
11019
+ var CompositionFieldStateSchema = z.object({
11020
+ cap: z.string(),
11021
+ path: z.string(),
11022
+ state: CompositionFieldStateKindSchema,
11023
+ /** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
11024
+ value: z.union([
11025
+ z.string(),
11026
+ z.number(),
11027
+ z.boolean(),
11028
+ z.null()
11029
+ ]),
11030
+ reason: z.string().optional()
11031
+ });
11032
+ var CompositionBlockStateSchema = z.object({
11033
+ blockId: z.string(),
11034
+ deviceId: z.number().nullable(),
11035
+ status: CompositionRunStatusSchema,
11036
+ reason: z.string().optional(),
11037
+ fields: z.array(CompositionFieldStateSchema)
11038
+ });
11039
+ /**
10597
11040
  * core-blocks — user-authored TypeScript, stored in the kernel and executed in
10598
11041
  * its own process.
10599
11042
  *
@@ -10677,6 +11120,30 @@ var CoreBlockInputSchema = z.object({
10677
11120
  */
10678
11121
  integrationId: z.string().optional()
10679
11122
  });
11123
+ /**
11124
+ * What a block IS (D659). Absent means `code` — every row stored before this
11125
+ * union keeps working unchanged.
11126
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
11127
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
11128
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
11129
+ */
11130
+ var CoreBlockSourceSchema = z.discriminatedUnion("kind", [
11131
+ z.object({ kind: z.literal("code") }),
11132
+ z.object({
11133
+ kind: z.literal("recipe"),
11134
+ recipe: AutomationRecipeSchema
11135
+ }),
11136
+ z.object({
11137
+ kind: z.literal("composition"),
11138
+ composition: CompositionSchema
11139
+ })
11140
+ ]);
11141
+ /** The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7). */
11142
+ var CompositionBlockInputSchema = z.object({
11143
+ name: z.string().min(1).max(120),
11144
+ enabled: z.boolean(),
11145
+ composition: CompositionSchema
11146
+ });
10680
11147
  /** A stored block. */
10681
11148
  var CoreBlockSchema = CoreBlockInputSchema.extend({
10682
11149
  id: z.string(),
@@ -10692,8 +11159,46 @@ var CoreBlockSchema = CoreBlockInputSchema.extend({
10692
11159
  */
10693
11160
  lastError: z.string().optional(),
10694
11161
  /** Ms epoch of the last state change. */
10695
- lastChangedAt: z.number()
11162
+ lastChangedAt: z.number(),
11163
+ /** What the block is. Absent means `code` (D659). */
11164
+ source: CoreBlockSourceSchema.optional()
11165
+ });
11166
+ function coreBlockSourceKind(block) {
11167
+ return block.source?.kind ?? "code";
11168
+ }
11169
+ function coreBlockComposition(block) {
11170
+ return block.source?.kind === "composition" ? block.source.composition : null;
11171
+ }
11172
+ /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
11173
+ var CompositionStateReadSchema = z.discriminatedUnion("kind", [z.object({
11174
+ kind: z.literal("answered"),
11175
+ state: CompositionBlockStateSchema.nullable()
11176
+ }), z.object({
11177
+ kind: z.literal("unreachable"),
11178
+ error: z.string()
11179
+ })]);
11180
+ /** A stored composition block and what the composer says about it. */
11181
+ var CompositionViewSchema = z.object({
11182
+ block: CoreBlockSchema,
11183
+ state: CompositionStateReadSchema
11184
+ });
11185
+ /**
11186
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
11187
+ * table has not been READ: an empty answer then would make "not yet known"
11188
+ * look like "none", and invite the operator to recreate what exists (D315).
11189
+ */
11190
+ var CompositionsNotLoadedSchema = z.object({
11191
+ state: z.literal("not-loaded"),
11192
+ reason: z.string()
10696
11193
  });
11194
+ var CompositionListReadSchema = z.discriminatedUnion("state", [z.object({
11195
+ state: z.literal("loaded"),
11196
+ views: z.array(CompositionViewSchema)
11197
+ }), CompositionsNotLoadedSchema]);
11198
+ var CompositionGetReadSchema = z.discriminatedUnion("state", [z.object({
11199
+ state: z.literal("loaded"),
11200
+ view: CompositionViewSchema.nullable()
11201
+ }), CompositionsNotLoadedSchema]);
10697
11202
  /** What a compile attempt produced. */
10698
11203
  var CoreBlockCompileResultSchema = z.object({
10699
11204
  ok: z.boolean(),
@@ -10775,7 +11280,116 @@ var coreBlocksCapability = {
10775
11280
  getTypeDefs: method(z.object({}), z.object({ libs: z.array(z.object({
10776
11281
  filePath: z.string(),
10777
11282
  content: z.string()
10778
- })) }), { auth: "admin" })
11283
+ })) }), { auth: "admin" }),
11284
+ /**
11285
+ * Create a composition block (D659). VALIDATED first against the live
11286
+ * sources and every stored composition; a refusal names every problem by
11287
+ * path and reason (D391) and stores nothing.
11288
+ */
11289
+ createComposition: method(z.object({ block: CompositionBlockInputSchema }), z.object({ block: CoreBlockSchema }), {
11290
+ kind: "mutation",
11291
+ auth: "admin",
11292
+ caller: "required"
11293
+ }),
11294
+ /**
11295
+ * Patch a composition block. A changed `composition` is validated exactly
11296
+ * as on create. `expectedUpdatedAt` is the `updatedAt` the caller READ: a
11297
+ * block changed since is refused by name, never silently overwritten.
11298
+ */
11299
+ updateComposition: method(z.object({
11300
+ blockId: z.string(),
11301
+ expectedUpdatedAt: z.number(),
11302
+ block: CompositionBlockInputSchema.partial()
11303
+ }), z.object({ block: CoreBlockSchema }), {
11304
+ kind: "mutation",
11305
+ auth: "admin",
11306
+ caller: "required"
11307
+ }),
11308
+ /** One composition and the composer's state for it, or `not-loaded` (D315). */
11309
+ getComposition: method(z.object({ blockId: z.string() }), CompositionGetReadSchema, { auth: "admin" }),
11310
+ listCompositions: method(z.object({}), CompositionListReadSchema, { auth: "admin" }),
11311
+ /**
11312
+ * Validate without saving — the same verdict the save reaches. A mutation
11313
+ * for its body size; it writes nothing, so its access is `view`.
11314
+ */
11315
+ validateComposition: method(z.object({
11316
+ composition: CompositionSchema,
11317
+ blockId: z.string().optional()
11318
+ }), CompositionValidationSchema, {
11319
+ kind: "mutation",
11320
+ auth: "admin",
11321
+ access: "view"
11322
+ })
11323
+ }
11324
+ };
11325
+ /**
11326
+ * composer — runs every DECLARATIVE composition (D659).
11327
+ *
11328
+ * Internal and admin-only: its one caller is `core-blocks`' supervisor, which
11329
+ * sends the WHOLE set of composition blocks on every converge (idempotent),
11330
+ * and reads their state for the API. Nothing else talks to it.
11331
+ */
11332
+ /** The composer's runner id, `resolveRunnerId(decl, 'composer')`; pinned by `composer-manifest.spec.ts`. */
11333
+ var COMPOSER_RUNNER_ID = COMPOSER_ADDON_ID;
11334
+ var ComposerBlockSchema = z.object({
11335
+ id: z.string().min(1),
11336
+ name: z.string().min(1).max(120),
11337
+ enabled: z.boolean(),
11338
+ updatedAt: z.number(),
11339
+ composition: CompositionSchema
11340
+ });
11341
+ /**
11342
+ * A stored composition row that no longer PARSES — a schema a release
11343
+ * tightened, a row written by a newer build. Sent by id so the composer keeps
11344
+ * its device as a placeholder and reports the block `failed` naming the parse
11345
+ * error. Leaving it out would read, twice, exactly like a delete (D49).
11346
+ */
11347
+ var ComposerUnreadableBlockSchema = z.object({
11348
+ id: z.string().min(1),
11349
+ /** The row's stored name column; null when even that is unreadable (the device keeps the name it has). */
11350
+ name: z.string().nullable(),
11351
+ /** The parse error, in the operator's words. */
11352
+ error: z.string()
11353
+ });
11354
+ var ComposerApplyInputSchema = z.object({
11355
+ /** The shared Blocks integration; null until `core-blocks` has reconciled it. */
11356
+ integrationId: z.string().nullable(),
11357
+ /** False when the `core_blocks` read failed: then nothing may be withdrawn (D49). */
11358
+ storeLoaded: z.boolean(),
11359
+ blocks: z.array(ComposerBlockSchema),
11360
+ /** Operator restarts: rebuild these runtimes and re-resolve their sources. Stores nothing (D62). */
11361
+ restartBlockIds: z.array(z.string()),
11362
+ /** Composition rows that exist but do not parse: held, never withdrawn, reported failed. */
11363
+ unreadableBlocks: z.array(ComposerUnreadableBlockSchema)
11364
+ });
11365
+ var ComposerBlockVerdictSchema = z.object({
11366
+ blockId: z.string(),
11367
+ status: CompositionRunStatusSchema,
11368
+ reason: z.string().optional()
11369
+ });
11370
+ var composerCapability = {
11371
+ name: "composer",
11372
+ scope: "system",
11373
+ mode: "singleton",
11374
+ internal: true,
11375
+ methods: {
11376
+ /**
11377
+ * Not `apply`: tRPC reserves `apply`, `call` and `then` as router keys, and
11378
+ * `router({})` throws on one — every cap router is built at boot.
11379
+ */
11380
+ applyBlocks: method(ComposerApplyInputSchema, z.object({
11381
+ blocks: z.array(ComposerBlockVerdictSchema),
11382
+ /**
11383
+ * False when the composer declared and withdrew NOTHING (store not loaded,
11384
+ * integration unknown, its own rows unread or read empty once). An empty
11385
+ * apply the caller owes is paid off only by one the composer acted on (D49).
11386
+ */
11387
+ acted: z.boolean()
11388
+ }), {
11389
+ kind: "mutation",
11390
+ auth: "admin"
11391
+ }),
11392
+ getStates: method(z.object({ blockIds: z.array(z.string()) }), z.object({ states: z.array(CompositionBlockStateSchema) }), { auth: "admin" })
10779
11393
  }
10780
11394
  };
10781
11395
  /**
@@ -18328,8 +18942,8 @@ var NcTestResultSchema = z.object({
18328
18942
  className: z.string().optional(),
18329
18943
  label: z.string().optional()
18330
18944
  });
18331
- /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is the
18332
- * same union; the template-vars spec pins the two together. `mixed` is DERIVED
18945
+ /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is
18946
+ * `z.infer` of this schema — one shape, no twin. `mixed` is DERIVED
18333
18947
  * (a stored system rule whose kinds span families, D637) and never authored. */
18334
18948
  var NcRuleKindSchema = z.enum([
18335
18949
  "detection",
@@ -22385,7 +22999,8 @@ var PipelineModelOptionSchema = z.object({
22385
22999
  })),
22386
23000
  group: ModelVariantGroupSchema.optional(),
22387
23001
  legacy: z.boolean().optional(),
22388
- provider: ModelProviderIdSchema.optional()
23002
+ provider: ModelProviderIdSchema.optional(),
23003
+ defaultConfidence: z.number().optional()
22389
23004
  });
22390
23005
  var ConfigFieldBridge = z.custom();
22391
23006
  var PipelineAddonSchemaSchema = z.object({
@@ -41667,6 +42282,620 @@ function createDeviceExtensionProvider(options) {
41667
42282
  };
41668
42283
  }
41669
42284
  /**
42285
+ * The field graph across every composition, and the cycles in it (D659).
42286
+ *
42287
+ * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
42288
+ * edge points from a composed field to each field it reads. A cycle, direct or
42289
+ * through another composition, is refused at save by name (spec §2).
42290
+ *
42291
+ * The graph is built for ONE save: the CANDIDATE composition (the one being
42292
+ * validated) plus every STORED composition. The candidate ALWAYS replaces any
42293
+ * stored peer that targets the same device, regardless of where that stored
42294
+ * peer sits in `stored` — whether a save introduces or removes a cycle must
42295
+ * never depend on array order. Two DIFFERENT stored peers that (wrongly)
42296
+ * target the same device are corrupted state, not a cycle to resolve by
42297
+ * picking one silently; that is refused by name (D391).
42298
+ *
42299
+ * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
42300
+ * marks a node `done` after its first expansion) — enough to refuse a save,
42301
+ * not to enumerate every elementary cycle through a shared node. A dotted
42302
+ * `fieldPath` is matched only as an exact string: a write to `state.value` and
42303
+ * a read of `state` would not connect (no nested field writes exist yet).
42304
+ */
42305
+ /** Two stored compositions both target the same device — refused by name, never silently resolved. */
42306
+ var CompositionGraphError = class extends Error {};
42307
+ function peerEdges(peer) {
42308
+ return peer.composition.features.flatMap((feature) => feature.kind !== "fields" ? [] : Object.entries(feature.fields).map(([path, source]) => [compositionReadKey({
42309
+ source: peer.target,
42310
+ cap: feature.cap,
42311
+ fieldPath: path
42312
+ }), fieldReads(source).map(compositionReadKey)]));
42313
+ }
42314
+ /**
42315
+ * Every peer that actually takes part in the graph: each stored peer minus
42316
+ * whichever one targets the same device as the candidate (the candidate
42317
+ * replaces it — this drop happens before any edge is built, so the
42318
+ * candidate's old edges never enter the map to begin with), plus the
42319
+ * candidate itself, exactly once. Throws {@link CompositionGraphError} if two
42320
+ * DIFFERENT stored peers (neither of them the candidate's own old version)
42321
+ * target the same device.
42322
+ */
42323
+ function resolvedPeers(candidate, stored) {
42324
+ const candidateKey = compositionSourceKey(candidate.target);
42325
+ const survivors = /* @__PURE__ */ new Map();
42326
+ for (const peer of stored) {
42327
+ const key = compositionSourceKey(peer.target);
42328
+ if (key === candidateKey) continue;
42329
+ if (survivors.has(key)) throw new CompositionGraphError(`two stored compositions both target device \`${key}\` — refusing to build the field graph`);
42330
+ survivors.set(key, peer);
42331
+ }
42332
+ return [...survivors.values(), candidate];
42333
+ }
42334
+ function compositionEdges(candidate, stored) {
42335
+ const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
42336
+ return new Map(edges);
42337
+ }
42338
+ function findCompositionCycles(candidate, stored) {
42339
+ const edges = compositionEdges(candidate, stored);
42340
+ const marks = /* @__PURE__ */ new Map();
42341
+ const found = /* @__PURE__ */ new Map();
42342
+ const visit = (node, stack) => {
42343
+ const mark = marks.get(node);
42344
+ if (mark === "done") return;
42345
+ if (mark === "visiting") {
42346
+ const path = [...stack.slice(stack.indexOf(node)), node];
42347
+ const id = [...new Set(path)].toSorted().join("|");
42348
+ if (!found.has(id)) found.set(id, { path });
42349
+ return;
42350
+ }
42351
+ marks.set(node, "visiting");
42352
+ for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
42353
+ marks.set(node, "done");
42354
+ };
42355
+ for (const node of edges.keys()) visit(node, []);
42356
+ return [...found.values()];
42357
+ }
42358
+ function describeRead(read) {
42359
+ return `${compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
42360
+ }
42361
+ function fromReading(reading, read) {
42362
+ if (reading.state === "unavailable") return {
42363
+ state: "unavailable",
42364
+ value: null,
42365
+ reason: `${describeRead(read)}: ${reading.reason}`,
42366
+ fatal: reading.fatal
42367
+ };
42368
+ return {
42369
+ state: reading.state,
42370
+ value: reading.value,
42371
+ reason: reading.state === "stale" ? `${describeRead(read)}: ${reading.reason}` : null,
42372
+ deadline: null
42373
+ };
42374
+ }
42375
+ function expressionOutcome(source, read, now) {
42376
+ const values = {};
42377
+ const staleReasons = [];
42378
+ let fatalUnavailable = null;
42379
+ let nonFatalUnavailable = null;
42380
+ for (const [name, binding] of Object.entries(source.bindings)) {
42381
+ if (binding.kind === "literal") {
42382
+ values[name] = binding.value;
42383
+ continue;
42384
+ }
42385
+ const reading = read(binding);
42386
+ if (reading.state === "unavailable") {
42387
+ const outcome = {
42388
+ state: "unavailable",
42389
+ value: null,
42390
+ reason: `binding \`${name}\` (${describeRead(binding)}): ${reading.reason}`,
42391
+ fatal: reading.fatal
42392
+ };
42393
+ if (reading.fatal) {
42394
+ fatalUnavailable = outcome;
42395
+ break;
42396
+ }
42397
+ if (nonFatalUnavailable === null) nonFatalUnavailable = outcome;
42398
+ continue;
42399
+ }
42400
+ values[name] = reading.value;
42401
+ if (reading.state === "stale") staleReasons.push(`\`${name}\`: ${reading.reason}`);
42402
+ }
42403
+ if (fatalUnavailable !== null) return fatalUnavailable;
42404
+ if (nonFatalUnavailable !== null) return nonFatalUnavailable;
42405
+ const deadlines = [];
42406
+ const result = evaluateExpressionSource(source.expr, values, now, { hooks: { noteDeadline: (at) => deadlines.push(at) } });
42407
+ if (!result.ok) return {
42408
+ state: "unavailable",
42409
+ value: null,
42410
+ reason: `the expression failed: ${result.error}`,
42411
+ fatal: false
42412
+ };
42413
+ return {
42414
+ state: staleReasons.length > 0 ? "stale" : "fresh",
42415
+ value: result.value,
42416
+ reason: staleReasons.length > 0 ? staleReasons.join("; ") : null,
42417
+ deadline: deadlines.length > 0 ? Math.min(...deadlines) : null
42418
+ };
42419
+ }
42420
+ function evaluateCompositionField(input) {
42421
+ const { source } = input;
42422
+ switch (source.kind) {
42423
+ case "from": return fromReading(input.read(source), source);
42424
+ case "expression": return expressionOutcome(source, input.read, input.now);
42425
+ case "code": return {
42426
+ state: "unavailable",
42427
+ value: null,
42428
+ reason: "code fields are not runnable in the composer — they arrive in slice 4",
42429
+ fatal: true
42430
+ };
42431
+ }
42432
+ }
42433
+ var NUMBER = {
42434
+ kind: "fixed",
42435
+ result: ["number"]
42436
+ };
42437
+ var BOOLEAN = {
42438
+ kind: "fixed",
42439
+ result: ["boolean"]
42440
+ };
42441
+ /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
42442
+ var BUILTIN_RESULT_KINDS = Object.freeze({
42443
+ min: NUMBER,
42444
+ max: NUMBER,
42445
+ abs: NUMBER,
42446
+ floor: NUMBER,
42447
+ ceil: NUMBER,
42448
+ sqrt: NUMBER,
42449
+ round: NUMBER,
42450
+ pow: NUMBER,
42451
+ clamp: NUMBER,
42452
+ avg: NUMBER,
42453
+ sum: NUMBER,
42454
+ age: NUMBER,
42455
+ convert: NUMBER,
42456
+ coalesce: { kind: "coalesce" },
42457
+ any: BOOLEAN,
42458
+ all: BOOLEAN,
42459
+ within: BOOLEAN,
42460
+ latest: {
42461
+ kind: "fixed",
42462
+ result: ["number", "null"]
42463
+ }
42464
+ });
42465
+ var KindInferenceError = class extends Error {};
42466
+ function kindOfValue(value) {
42467
+ if (value === null) return "null";
42468
+ if (typeof value === "number") return "number";
42469
+ if (typeof value === "string") return "string";
42470
+ return "boolean";
42471
+ }
42472
+ function only(kind) {
42473
+ return new Set([kind]);
42474
+ }
42475
+ function union(...sets) {
42476
+ return new Set(sets.flatMap((s) => [...s]));
42477
+ }
42478
+ function withoutNull(set) {
42479
+ return new Set([...set].filter((k) => k !== "null"));
42480
+ }
42481
+ function binaryKinds(op, left, right) {
42482
+ if (op === "+") {
42483
+ const all = [...left, ...right];
42484
+ if (all.every((k) => k === "string")) return only("string");
42485
+ if (all.every((k) => k === "number")) return only("number");
42486
+ return new Set(["number", "string"]);
42487
+ }
42488
+ if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
42489
+ return only("boolean");
42490
+ }
42491
+ /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
42492
+ function arityRange(builtin) {
42493
+ if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
42494
+ if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
42495
+ return `${builtin.minArgs}–${builtin.maxArgs}`;
42496
+ }
42497
+ function callKinds(callee, args) {
42498
+ const builtin = EXPRESSION_BUILTINS[callee];
42499
+ 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)}`);
42500
+ const rule = BUILTIN_RESULT_KINDS[callee];
42501
+ if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
42502
+ if (rule.kind === "fixed") return new Set(rule.result);
42503
+ const nonNull = union(...args.map(withoutNull));
42504
+ return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
42505
+ }
42506
+ function kindsOf(node, bindings) {
42507
+ switch (node.kind) {
42508
+ case "literal": return only(kindOfValue(node.value));
42509
+ case "identifier": {
42510
+ if (node.name === "now") return only("number");
42511
+ const bound = bindings.get(node.name);
42512
+ if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
42513
+ return bound;
42514
+ }
42515
+ case "unary": return only(node.op === "!" ? "boolean" : "number");
42516
+ case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
42517
+ case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
42518
+ case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
42519
+ case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
42520
+ }
42521
+ }
42522
+ function inferExpressionKinds(input) {
42523
+ const compiled = compileExpressionSafe(input.expr);
42524
+ if (!compiled.ok) return {
42525
+ ok: false,
42526
+ error: compiled.error
42527
+ };
42528
+ try {
42529
+ return {
42530
+ ok: true,
42531
+ kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
42532
+ };
42533
+ } catch (err) {
42534
+ if (err instanceof KindInferenceError) return {
42535
+ ok: false,
42536
+ error: err.message
42537
+ };
42538
+ throw err;
42539
+ }
42540
+ }
42541
+ /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
42542
+ * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
42543
+ function unwrapSchema(schema) {
42544
+ let s = schema;
42545
+ while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
42546
+ return s;
42547
+ }
42548
+ function leafKind(schema) {
42549
+ const s = unwrapSchema(schema);
42550
+ if (s instanceof z.ZodEnum) return {
42551
+ kind: "enum",
42552
+ enumValues: s.options
42553
+ };
42554
+ if (s instanceof z.ZodNumber) return { kind: "number" };
42555
+ if (s instanceof z.ZodString) return { kind: "string" };
42556
+ if (s instanceof z.ZodBoolean) return { kind: "boolean" };
42557
+ return null;
42558
+ }
42559
+ /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
42560
+ * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
42561
+ * so fields with null live values are still offered. Arrays / unknown shapes
42562
+ * are skipped — never throws. */
42563
+ function enumerateSchemaFields(schema, prefix = "") {
42564
+ const root = unwrapSchema(schema);
42565
+ if (!(root instanceof z.ZodObject)) return [];
42566
+ const out = [];
42567
+ const shape = root.shape;
42568
+ for (const [key, field] of Object.entries(shape)) {
42569
+ const path = prefix ? `${prefix}.${key}` : key;
42570
+ const inner = unwrapSchema(field);
42571
+ if (inner instanceof z.ZodObject) {
42572
+ out.push(...enumerateSchemaFields(inner, path));
42573
+ continue;
42574
+ }
42575
+ const leaf = leafKind(field);
42576
+ if (leaf) out.push({
42577
+ path,
42578
+ kind: leaf.kind,
42579
+ ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
42580
+ });
42581
+ }
42582
+ return out;
42583
+ }
42584
+ /** Enumerate the per-item wireable fields of an item-array cap (see
42585
+ * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
42586
+ * `keyField` (the key comes from the link's `itemKey`, never from a wired
42587
+ * source), each tagged `item: true` so the authoring UI collects an
42588
+ * `itemKey` alongside the field. Never throws. */
42589
+ function enumerateItemArrayFields(itemArray) {
42590
+ return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
42591
+ ...f,
42592
+ item: true
42593
+ }));
42594
+ }
42595
+ /**
42596
+ * What a capability's state field IS: kind, nullability, whether required. The
42597
+ * validator needs nullability, which `enumerateSchemaFields` does not report,
42598
+ * and that decides a field's unavailable mode (D393).
42599
+ */
42600
+ function fieldAt(schema, path) {
42601
+ let current = schema;
42602
+ for (const segment of path.split(".")) {
42603
+ const container = unwrapSchema(current);
42604
+ if (!(container instanceof z.ZodObject)) return null;
42605
+ const next = container.shape[segment];
42606
+ if (!(next instanceof z.ZodType)) return null;
42607
+ current = next;
42608
+ }
42609
+ return current;
42610
+ }
42611
+ function describeStateField(schema, path) {
42612
+ const leaf = enumerateSchemaFields(schema).find((f) => f.path === path);
42613
+ if (leaf === void 0) return null;
42614
+ const field = fieldAt(schema, path);
42615
+ if (field === null) return null;
42616
+ return {
42617
+ path,
42618
+ kind: leaf.kind,
42619
+ nullable: field.safeParse(null).success,
42620
+ required: !field.safeParse(void 0).success,
42621
+ ...leaf.enumValues === void 0 ? {} : { enumValues: leaf.enumValues }
42622
+ };
42623
+ }
42624
+ function describeTopLevelStateFields(schema) {
42625
+ return new Map(enumerateSchemaFields(schema).filter((f) => !f.path.includes(".")).flatMap((f) => {
42626
+ const described = describeStateField(schema, f.path);
42627
+ return described === null ? [] : [[f.path, described]];
42628
+ }));
42629
+ }
42630
+ /** Top-level keys a value MUST carry: those whose schema refuses `undefined`. */
42631
+ function requiredTopLevelKeys(schema) {
42632
+ const root = unwrapSchema(schema);
42633
+ if (!(root instanceof z.ZodObject)) return [];
42634
+ return Object.entries(root.shape).flatMap(([key, field]) => field instanceof z.ZodType && !field.safeParse(void 0).success ? [key] : []);
42635
+ }
42636
+ function buildCapabilityLookup(defs) {
42637
+ const byName = new Map(defs.map((d) => [d.name, d]));
42638
+ return (capName) => byName.get(capName) ?? null;
42639
+ }
42640
+ /** Caps every `BaseDevice` registers on itself; a second registration throws. */
42641
+ var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
42642
+ var PENDING_SELF = {
42643
+ addonId: COMPOSER_ADDON_ID,
42644
+ stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
42645
+ };
42646
+ function problem(code, path, message) {
42647
+ return {
42648
+ code,
42649
+ path,
42650
+ message
42651
+ };
42652
+ }
42653
+ function refused(...problems) {
42654
+ return {
42655
+ problems,
42656
+ fields: []
42657
+ };
42658
+ }
42659
+ function readKinds(read, lookupCap) {
42660
+ const def = lookupCap(read.cap);
42661
+ const schema = def?.runtimeState ?? def?.status?.schema;
42662
+ if (def === null || schema === void 0) return {
42663
+ ok: false,
42664
+ code: "source-field-unknown",
42665
+ message: `no capability \`${read.cap}\` with state to read`
42666
+ };
42667
+ const field = describeStateField(schema, read.fieldPath);
42668
+ if (field === null) return {
42669
+ ok: false,
42670
+ code: "source-field-unknown",
42671
+ message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
42672
+ };
42673
+ const base = field.kind === "enum" ? "string" : field.kind;
42674
+ return {
42675
+ ok: true,
42676
+ kinds: new Set(field.nullable ? [base, "null"] : [base])
42677
+ };
42678
+ }
42679
+ function sourceKinds(source, lookupCap) {
42680
+ if (source.kind === "from") return readKinds(source, lookupCap);
42681
+ if (source.kind === "code") return {
42682
+ ok: false,
42683
+ code: "reserved-variant",
42684
+ message: "code fields arrive in slice 4"
42685
+ };
42686
+ const bindingKinds = /* @__PURE__ */ new Map();
42687
+ for (const [name, binding] of Object.entries(source.bindings)) {
42688
+ if (binding.kind === "literal") {
42689
+ bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
42690
+ continue;
42691
+ }
42692
+ const kinds = readKinds(binding, lookupCap);
42693
+ if (!kinds.ok) return {
42694
+ ...kinds,
42695
+ message: `binding \`${name}\`: ${kinds.message}`
42696
+ };
42697
+ bindingKinds.set(name, kinds.kinds);
42698
+ }
42699
+ const inferred = inferExpressionKinds({
42700
+ expr: source.expr,
42701
+ bindingKinds
42702
+ });
42703
+ return inferred.ok ? {
42704
+ ok: true,
42705
+ kinds: inferred.kinds
42706
+ } : {
42707
+ ok: false,
42708
+ code: "expression-invalid",
42709
+ message: inferred.error
42710
+ };
42711
+ }
42712
+ function compatibility(cap, target, kinds, at) {
42713
+ const allowed = target.kind === "enum" ? "string" : target.kind;
42714
+ const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
42715
+ const problems = [];
42716
+ if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
42717
+ 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`));
42718
+ return problems;
42719
+ }
42720
+ /**
42721
+ * A string a source produces WITHOUT reading any device — a field-level
42722
+ * `expression` source whose compiled AST is a bare string literal (`"red"`,
42723
+ * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
42724
+ * the kind level (a `from` read of an enum-typed source field, a computed
42725
+ * expression) is NOT checked against the target's `enumValues` here: the
42726
+ * kind inference already collapsed it to the coarse `'string'` kind, losing
42727
+ * whichever enum it actually came from, so membership can only be judged at
42728
+ * write time against the runtime-state schema. Known limitation — see the
42729
+ * fix report for Task 6 round 1.
42730
+ */
42731
+ function literalStringValue(source) {
42732
+ if (source.kind !== "expression") return null;
42733
+ const compiled = compileExpressionSafe(source.expr);
42734
+ if (!compiled.ok) return null;
42735
+ const ast = compiled.parsed.ast;
42736
+ return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
42737
+ }
42738
+ /**
42739
+ * An enum target whose source is a save-time-KNOWN string literal outside its
42740
+ * `enumValues` is refused now rather than left to fail the write later. A
42741
+ * dynamic source (a `from` read, a non-literal expression) is left to
42742
+ * runtime — see {@link literalStringValue}.
42743
+ */
42744
+ function enumLiteralProblem(cap, target, source, at) {
42745
+ if (target.kind !== "enum" || target.enumValues === void 0) return null;
42746
+ const value = literalStringValue(source);
42747
+ if (value === null || target.enumValues.includes(value)) return null;
42748
+ const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
42749
+ return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
42750
+ }
42751
+ function roleOf(source) {
42752
+ if (source.kind !== "expression") return "derived";
42753
+ if (fieldReads(source).length > 0) return "derived";
42754
+ const compiled = compileExpressionSafe(source.expr);
42755
+ return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
42756
+ }
42757
+ function planFeature(feature, featureIndex, target, lookupCap) {
42758
+ const at = `features[${featureIndex}]`;
42759
+ const def = lookupCap(feature.cap);
42760
+ if (def === null) return refused(problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``));
42761
+ 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`));
42762
+ const state = def.runtimeState;
42763
+ if (def.scope !== "device" || state === void 0) return refused(problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`));
42764
+ const capProblems = [];
42765
+ 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(", ")})`));
42766
+ const served = Object.entries(def.methods).filter(([, m]) => m.systemOnly !== true).map(([name]) => `\`${name}\``);
42767
+ 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)`));
42768
+ if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
42769
+ const targetFields = describeTopLevelStateFields(state);
42770
+ 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`));
42771
+ else if (!(key in feature.fields)) capProblems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
42772
+ const perField = Object.entries(feature.fields).map(([path, source]) => {
42773
+ const fieldAt = `${at}.fields.${path}`;
42774
+ if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
42775
+ const tf = targetFields.get(path);
42776
+ if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
42777
+ const kinds = sourceKinds(source, lookupCap);
42778
+ if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
42779
+ const plan = {
42780
+ cap: def.name,
42781
+ path,
42782
+ featureIndex,
42783
+ kind: tf.kind,
42784
+ nullable: tf.nullable,
42785
+ required: tf.required,
42786
+ unavailable: tf.nullable ? "null" : "offline",
42787
+ role: roleOf(source),
42788
+ source,
42789
+ reads: [...fieldReads(source)]
42790
+ };
42791
+ const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
42792
+ return {
42793
+ problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
42794
+ fields: [plan]
42795
+ };
42796
+ });
42797
+ return {
42798
+ problems: [...capProblems, ...perField.flatMap((r) => r.problems)],
42799
+ fields: perField.flatMap((r) => r.fields)
42800
+ };
42801
+ }
42802
+ function planComposition(composition, lookupCap) {
42803
+ const target = composition.target;
42804
+ 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"));
42805
+ const results = composition.features.map((feature, index) => {
42806
+ const at = `features[${index}]`;
42807
+ if (feature.kind === "passthrough") return refused(problem("reserved-variant", at, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`));
42808
+ return planFeature(feature, index, target, lookupCap);
42809
+ });
42810
+ return {
42811
+ problems: results.flatMap((r) => r.problems),
42812
+ fields: results.flatMap((r) => r.fields)
42813
+ };
42814
+ }
42815
+ function compositionSourceRefs(composition) {
42816
+ const refs = composition.features.flatMap((f) => f.kind === "fields" ? Object.values(f.fields).flatMap(fieldReads).map((r) => r.source) : []);
42817
+ return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
42818
+ }
42819
+ function sourceProblems(fields, ctx) {
42820
+ const selfKey = ctx.self === null ? null : compositionSourceKey(ctx.self);
42821
+ return fields.flatMap((field) => field.reads.flatMap((read) => {
42822
+ const at = `features[${field.featureIndex}].fields.${field.path}`;
42823
+ const key = compositionSourceKey(read.source);
42824
+ 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}\``)];
42825
+ const desc = ctx.sources.get(key);
42826
+ 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`)];
42827
+ if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
42828
+ if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
42829
+ return [];
42830
+ }));
42831
+ }
42832
+ /**
42833
+ * A cycle's node keys are `compositionReadKey` strings:
42834
+ * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
42835
+ * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
42836
+ * back out so the refusal can point at that exact field, matching the
42837
+ * documented `features[N].fields.<path>` convention — slice 1 has no nested
42838
+ * field writes, so splitting `rest` on the FIRST `.` is exact.
42839
+ */
42840
+ function candidateFieldOfNode(nodeKey, candidateKey) {
42841
+ const prefix = `${candidateKey}#`;
42842
+ if (!nodeKey.startsWith(prefix)) return null;
42843
+ const rest = nodeKey.slice(prefix.length);
42844
+ const dot = rest.indexOf(".");
42845
+ if (dot === -1) return null;
42846
+ return {
42847
+ cap: rest.slice(0, dot),
42848
+ fieldPath: rest.slice(dot + 1)
42849
+ };
42850
+ }
42851
+ /** The first node of a cycle's path that is one of THIS composition's own
42852
+ * planned fields, as a documented-convention path; `'features'` when the
42853
+ * cycle runs entirely through other stored compositions. */
42854
+ function cyclePath(nodePath, candidateKey, fields) {
42855
+ for (const nodeKey of nodePath) {
42856
+ const hit = candidateFieldOfNode(nodeKey, candidateKey);
42857
+ if (hit === null) continue;
42858
+ const field = fields.find((f) => f.cap === hit.cap && f.path === hit.fieldPath);
42859
+ if (field !== void 0) return `features[${field.featureIndex}].fields.${field.path}`;
42860
+ }
42861
+ return "features";
42862
+ }
42863
+ /**
42864
+ * Cycle check runs against the CANDIDATE (this composition, targeting `self`
42865
+ * when it already exists, else a placeholder that cannot collide with a
42866
+ * stored target) plus every OTHER stored composition. Two stored peers that
42867
+ * (wrongly) share a target are corrupted state — `findCompositionCycles`
42868
+ * throws `CompositionGraphError` rather than silently picking one, and this
42869
+ * is refused BY NAME (D391) instead of escaping as an uncaught throw.
42870
+ */
42871
+ function cycleProblems(composition, ctx, fields) {
42872
+ const candidate = {
42873
+ target: ctx.self ?? PENDING_SELF,
42874
+ composition
42875
+ };
42876
+ const candidateKey = compositionSourceKey(candidate.target);
42877
+ try {
42878
+ return findCompositionCycles(candidate, ctx.peers).map((c) => problem("cycle", cyclePath(c.path, candidateKey, fields), `a field reads itself: ${c.path.join(" → ")}`));
42879
+ } catch (err) {
42880
+ if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
42881
+ throw err;
42882
+ }
42883
+ }
42884
+ function validateComposition(composition, ctx) {
42885
+ const plan = planComposition(composition, ctx.lookupCap);
42886
+ const cycles = cycleProblems(composition, ctx, plan.fields);
42887
+ const problems = [
42888
+ ...plan.problems,
42889
+ ...sourceProblems(plan.fields, ctx),
42890
+ ...cycles
42891
+ ];
42892
+ return {
42893
+ ok: problems.length === 0,
42894
+ problems,
42895
+ fields: [...plan.fields]
42896
+ };
42897
+ }
42898
+ /**
41670
42899
  * Runtime defaults -- used by ConfigManager.get() for backward compatibility
41671
42900
  * until Plan B wires all runtime settings to the system_settings SQL table.
41672
42901
  *
@@ -44368,60 +45597,6 @@ var DISPLAY_ICON_VOCABULARY = [
44368
45597
  DISPLAY_ICON_VOCABULARY.map((e) => e.key);
44369
45598
  DISPLAY_ICON_VOCABULARY.filter((e) => e.aliasOf === void 0).map((e) => e.key);
44370
45599
  new Map(DISPLAY_ICON_VOCABULARY.map((e) => [e.key, e]));
44371
- /** Unwrap ZodNullable / ZodOptional / ZodDefault wrappers to reach the inner type.
44372
- * Zod v4 exposes `.unwrap()` on all three wrapper classes. */
44373
- function unwrap(schema) {
44374
- let s = schema;
44375
- while (s instanceof z.ZodNullable || s instanceof z.ZodOptional || s instanceof z.ZodDefault) s = s.unwrap();
44376
- return s;
44377
- }
44378
- function leafKind(schema) {
44379
- const s = unwrap(schema);
44380
- if (s instanceof z.ZodEnum) return {
44381
- kind: "enum",
44382
- enumValues: s.options
44383
- };
44384
- if (s instanceof z.ZodNumber) return { kind: "number" };
44385
- if (s instanceof z.ZodString) return { kind: "string" };
44386
- if (s instanceof z.ZodBoolean) return { kind: "boolean" };
44387
- return null;
44388
- }
44389
- /** Walk a cap status schema into flat wireable leaf fields (dotted paths).
44390
- * Recurses into nested ZodObject (unwrapping nullable/optional/default first),
44391
- * so fields with null live values are still offered. Arrays / unknown shapes
44392
- * are skipped — never throws. */
44393
- function enumerateSchemaFields(schema, prefix = "") {
44394
- const root = unwrap(schema);
44395
- if (!(root instanceof z.ZodObject)) return [];
44396
- const out = [];
44397
- const shape = root.shape;
44398
- for (const [key, field] of Object.entries(shape)) {
44399
- const path = prefix ? `${prefix}.${key}` : key;
44400
- const inner = unwrap(field);
44401
- if (inner instanceof z.ZodObject) {
44402
- out.push(...enumerateSchemaFields(inner, path));
44403
- continue;
44404
- }
44405
- const leaf = leafKind(field);
44406
- if (leaf) out.push({
44407
- path,
44408
- kind: leaf.kind,
44409
- ...leaf.enumValues ? { enumValues: leaf.enumValues } : {}
44410
- });
44411
- }
44412
- return out;
44413
- }
44414
- /** Enumerate the per-item wireable fields of an item-array cap (see
44415
- * `CapabilityStatusItemArray`): the item schema's leaf fields, minus the
44416
- * `keyField` (the key comes from the link's `itemKey`, never from a wired
44417
- * source), each tagged `item: true` so the authoring UI collects an
44418
- * `itemKey` alongside the field. Never throws. */
44419
- function enumerateItemArrayFields(itemArray) {
44420
- return enumerateSchemaFields(itemArray.itemSchema).filter((f) => f.path !== itemArray.keyField).map((f) => ({
44421
- ...f,
44422
- item: true
44423
- }));
44424
- }
44425
45600
  /**
44426
45601
  * Runtime list of cap names with status. Used by the settings
44427
45602
  * aggregator to enumerate caps whose `status` should be polled +
@@ -44543,6 +45718,7 @@ var ALL_CAPABILITY_DEFINITIONS = [
44543
45718
  carbonMonoxideCapability,
44544
45719
  climateControlCapability,
44545
45720
  colorCapability,
45721
+ composerCapability,
44546
45722
  connectionTestCapability,
44547
45723
  connectivityCapability,
44548
45724
  consumablesCapability,
@@ -45616,6 +46792,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
45616
46792
  addonId: null,
45617
46793
  access: "create"
45618
46794
  },
46795
+ "composer.applyBlocks": {
46796
+ capName: "composer",
46797
+ capScope: "system",
46798
+ addonId: null,
46799
+ access: "create"
46800
+ },
46801
+ "composer.getStates": {
46802
+ capName: "composer",
46803
+ capScope: "system",
46804
+ addonId: null,
46805
+ access: "view"
46806
+ },
45619
46807
  "connectionTest.describeTest": {
45620
46808
  capName: "connection-test",
45621
46809
  capScope: "system",
@@ -45676,6 +46864,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
45676
46864
  addonId: null,
45677
46865
  access: "create"
45678
46866
  },
46867
+ "coreBlocks.createComposition": {
46868
+ capName: "core-blocks",
46869
+ capScope: "system",
46870
+ addonId: null,
46871
+ access: "create"
46872
+ },
45679
46873
  "coreBlocks.delete": {
45680
46874
  capName: "core-blocks",
45681
46875
  capScope: "system",
@@ -45688,6 +46882,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
45688
46882
  addonId: null,
45689
46883
  access: "view"
45690
46884
  },
46885
+ "coreBlocks.getComposition": {
46886
+ capName: "core-blocks",
46887
+ capScope: "system",
46888
+ addonId: null,
46889
+ access: "view"
46890
+ },
45691
46891
  "coreBlocks.getTypeDefs": {
45692
46892
  capName: "core-blocks",
45693
46893
  capScope: "system",
@@ -45700,6 +46900,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
45700
46900
  addonId: null,
45701
46901
  access: "view"
45702
46902
  },
46903
+ "coreBlocks.listCompositions": {
46904
+ capName: "core-blocks",
46905
+ capScope: "system",
46906
+ addonId: null,
46907
+ access: "view"
46908
+ },
45703
46909
  "coreBlocks.restart": {
45704
46910
  capName: "core-blocks",
45705
46911
  capScope: "system",
@@ -45718,6 +46924,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
45718
46924
  addonId: null,
45719
46925
  access: "create"
45720
46926
  },
46927
+ "coreBlocks.updateComposition": {
46928
+ capName: "core-blocks",
46929
+ capScope: "system",
46930
+ addonId: null,
46931
+ access: "create"
46932
+ },
46933
+ "coreBlocks.validateComposition": {
46934
+ capName: "core-blocks",
46935
+ capScope: "system",
46936
+ addonId: null,
46937
+ access: "view"
46938
+ },
45721
46939
  "cover.close": {
45722
46940
  capName: "cover",
45723
46941
  capScope: "device",
@@ -55146,4 +56364,4 @@ function enumerateInferenceDevices(hw) {
55146
56364
  return out;
55147
56365
  }
55148
56366
  //#endregion
55149
- export { decodeVectorBase64 as $, scoreRuntimes as $t, ScopedTokenSchema as A, emitDownForOwnedCaps as An, locationIconKey as At, authProviderCapability as B, resolveCapMount as Bn, objectInputDeclaresAddonId as Bt, RECORDING_SOURCE_CAMSTACK_ADDON as C, ReadinessRegistry as Cn, isSourceCap as Ct, SOURCE_CAPS as D, asNumber as Dn, legacyModeOf as Dt, RecordingStatusSchema as E, asJsonObject as En, legacyAutoLinkIds as Et, UserRecordSchema as F, isEvent as Fn, mayWriteToLocation as Ft, cameraGridLayoutCapability as G, ptzAutotrackCapability as Gt, bareAddonId as H, sleep as Hn, parseStreamParamsFormPatch as Ht, addonPagesCapability as I, nodePin as In, metricsProviderCapability as It, coreBlockIdFromAddonId as J, resolveLocationMode as Jt, collectSecretConfigKeys as K, reducePoints as Kt, addonSettingsCapability as L, parseJsonObject as Ln, modeMayWrite as Lt, StorageLocationModeSchema as M, expandCapMethods as Mn, logLevelAtMost as Mt, StorageLocationTypeSchema as N, hydrateSchema as Nn, looseSchema as Nt, STORAGE_ACCESS_FALLBACK as O, asString as On, lifecycleJobSchema as Ot, StorageMigrationJobSchema as P, isDeviceConfigCap as Pn, mayReadLocation as Pt, declarationOwnerNodeId as Q, scopesAllowDeviceCap as Qt, addonWidgetsCapability as R, parseJsonUnknown as Rn, motionCapability as Rt, MOTION_CLOSE_AFTER_MS as S, DeviceType as Sn, isSameAddonId as St, RUNTIME_DEFAULTS as T, WELL_KNOWN_TAB_MAP as Tn, kebabToCamel as Tt, batteryCapability as U, EventCategory as Un, platformProbeCapability as Ut, backupCapability as V, scopeKey as Vn, objectInputDeclaresRequiredDeviceId as Vt, buildStreamParamsConfigSchema as W, procedureAuthKey as Wt, createDeviceExtensionProvider as X, runtimeStatePolicyFor as Xt, coreBlocksCapability as Y, resolveMethodAuth as Yt, dataStoreProviderCapability as Z, scopesAllowAddon as Zt, LOG_LEVEL_RANK as _, DATAPLANE_SECRET_HEADER as _n, gbToBytes as _t, BatteryStatusSchema as a, storageOccupancyCapability as an, deviceStatusCapability as at, METHOD_ACCESS_MAP as b, DeviceFeature as bn, isLocationEnabled as bt, CORE_BLOCKS_ADDON_ID as c, streamQualityLabel as cn, enumerateItemArrayFields as ct, DAY_NAMES as d, vectorIndexDimConflictError as dn, evictionPolicyOfLocation as dt, settingsStoreCapability as en, defineExtensionSlotProvider as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, vectorStoreCapability as fn, expandContainer as ft, FIRST_LEVEL_MACRO_CLASSES as g, BaseAddon as gn, formatMinutes as gt, DeviceStatusSchema as h, buildFfmpegArgs as hn, foldSnapshotByFunction as ht, BaseDevice as i, storageMigrationCapability as in, deviceStateCapability as it, StorageCleanupJobSchema as j, emitReadiness as jn, logDestinationCapability as jt, STREAM_PROFILE_META as k, createEvent as kn, localNetworkCapability as kt, CORE_BLOCK_ADDON_PREFIX as l, userManagementCapability as ln, enumerateSchemaFields as lt, DeclaredDevices as m, errMsg as mn, filesystemBrowseCapability as mt, AlertSchema as n, snapshotCapability as nn, deviceExtensionCapability as nt, CAMERA_SWITCH_ORDER as o, storageProviderCapability as on, doorbellCapability as ot, DERIVED_CAMERA_SILENCED_CAP_NAMES as p, withLocationMode as pn, extractNestedAddonId as pt, coreBlockAddonId as q, resolveBucketMs as qt, ApiKeyRecordSchema as r, storageCapability as rn, deviceManagerCapability as rt, CAP_NAMES_WITH_STATUS as s, streamCatalogCapability as sn, enumerateInferenceDevices as st, ALL_CAPABILITY_DEFINITIONS as t, siteZoneKey as tn, deriveBatteryPresence as tt, CoreBlockSchema as u, vectorDimFromBase64 as un, evaluateSensorEdge as ut, LegacyStorageLocationDefaultSchema as v, DEVICE_SETTINGS_CONTRIBUTION_METHODS as vn, isArrayOutputSchema as vt, REDACTED_SECRET as w, ReadinessTimeoutError as wn, isVoidInput as wt, MINUTES_PER_DAY as x, DeviceRole as xn, isObjectInput as xt, LocationIconIdSchema as y, DEVICE_STATUS_METHOD as yn, isCollectionArrayMethod as yt, alertsCapability as z, readinessKey as zn, normalizeUnit as zt };
56367
+ export { compositionSliceKey as $, parseJsonUnknown as $n, objectInputDeclaresRequiredDeviceId as $t, STORAGE_ACCESS_FALLBACK as A, buildFfmpegArgs as An, isArrayOutputSchema as At, addonWidgetsCapability as B, WELL_KNOWN_TAB_MAP as Bn, lifecycleJobSchema as Bt, MINUTES_PER_DAY as C, userManagementCapability as Cn, evictionPolicyOfLocation as Ct, RUNTIME_DEFAULTS as D, vectorStoreCapability as Dn, foldSnapshotByFunction as Dt, REDACTED_SECRET as E, vectorIndexDimConflictError as En, filesystemBrowseCapability as Et, StorageLocationTypeSchema as F, DeviceFeature as Fn, isSourceCap as Ft, batteryCapability as G, emitDownForOwnedCaps as Gn, looseSchema as Gt, authProviderCapability as H, asNumber as Hn, locationIconKey as Ht, StorageMigrationJobSchema as I, DeviceRole as In, isVoidInput as It, cameraGridLayoutCapability as J, hydrateSchema as Jn, metricsProviderCapability as Jt, buildCapabilityLookup as K, emitReadiness as Kn, mayReadLocation as Kt, UserRecordSchema as L, DeviceType as Ln, kebabToCamel as Lt, ScopedTokenSchema as M, DATAPLANE_SECRET_HEADER as Mn, isLocationEnabled as Mt, StorageCleanupJobSchema as N, DEVICE_SETTINGS_CONTRIBUTION_METHODS as Nn, isObjectInput as Nt, RecordingStatusSchema as O, withLocationMode as On, formatMinutes as Ot, StorageLocationModeSchema as P, DEVICE_STATUS_METHOD as Pn, isSameAddonId as Pt, composerCapability as Q, parseJsonObject as Qn, objectInputDeclaresAddonId as Qt, addonPagesCapability as R, ReadinessRegistry as Rn, legacyAutoLinkIds as Rt, METHOD_ACCESS_MAP as S, toExpressionValue as Sn, evaluateSensorEdge as St, RECORDING_SOURCE_CAMSTACK_ADDON as T, vectorDimFromBase64 as Tn, extractNestedAddonId as Tt, backupCapability as U, asString as Un, logDestinationCapability as Ut, alertsCapability as V, asJsonObject as Vn, localNetworkCapability as Vt, bareAddonId as W, createEvent as Wn, logLevelAtMost as Wt, composedDeviceRef as X, isEvent as Xn, motionCapability as Xt, collectSecretConfigKeys as Y, isDeviceConfigCap as Yn, modeMayWrite as Yt, composedDeviceStableId as Z, nodePin as Zn, normalizeUnit as Zt, DeviceStatusSchema as _, storageMigrationCapability as _n, doorbellCapability as _t, BatteryStatusSchema as a, reducePoints as an, coreBlockSourceKind as at, LegacyStorageLocationDefaultSchema as b, streamCatalogCapability as bn, enumerateSchemaFields as bt, COMPOSER_ADDON_ID as c, resolveMethodAuth as cn, dataStoreProviderCapability as ct, CORE_BLOCK_ADDON_PREFIX as d, scopesAllowDeviceCap as dn, defineExtensionSlotProvider as dt, parseStreamParamsFormPatch as en, readinessKey as er, compositionSourceKey as et, CoreBlockSchema as f, scoreRuntimes as fn, deriveBatteryPresence as ft, DeclaredDevices as g, storageCapability as gn, deviceStatusCapability as gt, DERIVED_CAMERA_SILENCED_CAP_NAMES as h, snapshotCapability as hn, deviceStateCapability as ht, BaseDevice as i, ptzAutotrackCapability as in, EventCategory as ir, coreBlockIdFromAddonId as it, STREAM_PROFILE_META as j, BaseAddon as jn, isCollectionArrayMethod as jt, SOURCE_CAPS as k, errMsg as kn, gbToBytes as kt, COMPOSER_RUNNER_ID as l, runtimeStatePolicyFor as ln, declarationOwnerNodeId as lt, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as m, siteZoneKey as mn, deviceManagerCapability as mt, AlertSchema as n, platformProbeCapability as nn, scopeKey as nr, coreBlockAddonId as nt, CAMERA_SWITCH_ORDER as o, resolveBucketMs as on, coreBlocksCapability as ot, DAY_NAMES as p, settingsStoreCapability as pn, deviceExtensionCapability as pt, buildStreamParamsConfigSchema as q, expandCapMethods as qn, mayWriteToLocation as qt, ApiKeyRecordSchema as r, procedureAuthKey as rn, sleep as rr, coreBlockComposition as rt, CAP_NAMES_WITH_STATUS as s, resolveLocationMode as sn, createDeviceExtensionProvider as st, ALL_CAPABILITY_DEFINITIONS as t, planComposition as tn, resolveCapMount as tr, compositionSourceRefs as tt, CORE_BLOCKS_ADDON_ID as u, scopesAllowAddon as un, decodeVectorBase64 as ut, FIRST_LEVEL_MACRO_CLASSES as v, storageOccupancyCapability as vn, enumerateInferenceDevices as vt, MOTION_CLOSE_AFTER_MS as w, validateComposition as wn, expandContainer as wt, LocationIconIdSchema as x, streamQualityLabel as xn, evaluateCompositionField as xt, LOG_LEVEL_RANK as y, storageProviderCapability as yn, enumerateItemArrayFields as yt, addonSettingsCapability as z, ReadinessTimeoutError as zn, legacyModeOf as zt };