@camstack/types 1.2.274 → 1.2.276

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/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_sleep = require("./sleep-WOeeZ4FU.js");
3
3
  const require_event_category = require("./event-category-MbnB-6AN.js");
4
- const require_composition = require("./composition-BRxAnxtU.js");
4
+ const require_composition = require("./composition-DG4jVTfB.js");
5
5
  const require_err_msg = require("./err-msg-Dx2Kor0g.js");
6
6
  const require_enums = require("./enums.js");
7
7
  let zod = require("zod");
@@ -833,6 +833,28 @@ function egressTranscodeSharingKey(request, delivery = egressTransportFromReques
833
833
  return `egress:${require_composition.canonicalHash(canonicalEgressPlan(request, delivery))}`;
834
834
  }
835
835
  //#endregion
836
+ //#region src/ffmpeg/pinned-binary.ts
837
+ /**
838
+ * Memoize `ensure` on SUCCESS only.
839
+ *
840
+ * A failure is not remembered: the next caller asks again, so a node whose
841
+ * first download failed (no network at boot) recovers on the next spawn that
842
+ * needs it — with no timer and no retry loop, because the ask is driven by
843
+ * work that actually wants the binary.
844
+ */
845
+ function memoizePinnedFfmpeg(ensure) {
846
+ let pending = null;
847
+ return () => {
848
+ if (pending !== null) return pending;
849
+ const attempt = ensure();
850
+ pending = attempt;
851
+ attempt.catch(() => {
852
+ if (pending === attempt) pending = null;
853
+ });
854
+ return attempt;
855
+ };
856
+ }
857
+ //#endregion
836
858
  //#region src/health/wiring-health.ts
837
859
  /**
838
860
  * Deep wiring healthcheck — snapshot of active reachability probes across
@@ -9768,269 +9790,1198 @@ var CompositionBlockStateSchema = zod.z.object({
9768
9790
  fields: zod.z.array(CompositionFieldStateSchema)
9769
9791
  });
9770
9792
  //#endregion
9771
- //#region src/capabilities/core-blocks.cap.ts
9772
- /**
9773
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
9774
- * its own process.
9775
- *
9776
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
9777
- *
9778
- * The first use is **owning devices without being a device provider**: a block
9779
- * declares devices under a system or custom integration and drives their state,
9780
- * with the same `ctx` an addon gets. Automations come later; nothing here
9781
- * models a trigger.
9782
- *
9783
- * **Stated plainly, because it does not change by being true:** a block has an
9784
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
9785
- * with no review step. What makes that survivable is not a sandbox, it is
9786
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
9787
- * so a block that throws or never returns is marked `failed` and visible
9788
- * instead of taking the hub with it (D6). Every method here is admin-only, and
9789
- * must stay so.
9790
- */
9791
- /**
9792
- * Prefix for a block's generated addon id AND its runner id — one addon, one
9793
- * process, so the two are the same string (D2).
9794
- *
9795
- * It lives on the CONTRACT rather than beside the code that materialises the
9796
- * package, because it is not an implementation detail: the operator surface
9797
- * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
9798
- * with no cap and no store of its own. Two copies of this string is one copy
9799
- * too many — a drift would show as a logs pane that is simply always empty.
9800
- */
9801
- var CORE_BLOCK_ADDON_PREFIX = "core-block-";
9793
+ //#region src/composition/plan-result.ts
9794
+ function problem(code, path, message) {
9795
+ return {
9796
+ code,
9797
+ path,
9798
+ message
9799
+ };
9800
+ }
9801
+ function refused(...problems) {
9802
+ return {
9803
+ problems,
9804
+ fields: [],
9805
+ features: [],
9806
+ events: []
9807
+ };
9808
+ }
9809
+ /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
9810
+ function compositionFieldAt(field) {
9811
+ const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
9812
+ return `features[${field.featureIndex}].${inner}.${field.path}`;
9813
+ }
9814
+ //#endregion
9815
+ //#region src/composition/expression-kinds.ts
9816
+ var NUMBER = {
9817
+ kind: "fixed",
9818
+ result: ["number"]
9819
+ };
9820
+ var BOOLEAN = {
9821
+ kind: "fixed",
9822
+ result: ["boolean"]
9823
+ };
9824
+ /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
9825
+ var BUILTIN_RESULT_KINDS = Object.freeze({
9826
+ min: NUMBER,
9827
+ max: NUMBER,
9828
+ abs: NUMBER,
9829
+ floor: NUMBER,
9830
+ ceil: NUMBER,
9831
+ sqrt: NUMBER,
9832
+ round: NUMBER,
9833
+ pow: NUMBER,
9834
+ clamp: NUMBER,
9835
+ avg: NUMBER,
9836
+ sum: NUMBER,
9837
+ age: NUMBER,
9838
+ convert: NUMBER,
9839
+ coalesce: { kind: "coalesce" },
9840
+ any: BOOLEAN,
9841
+ all: BOOLEAN,
9842
+ within: BOOLEAN,
9843
+ latest: {
9844
+ kind: "fixed",
9845
+ result: ["number", "null"]
9846
+ },
9847
+ number: {
9848
+ kind: "fixed",
9849
+ result: ["number", "null"]
9850
+ },
9851
+ rose: {
9852
+ kind: "fixed",
9853
+ result: ["number", "null"]
9854
+ },
9855
+ count: NUMBER
9856
+ });
9857
+ var KindInferenceError = class extends Error {};
9858
+ function kindOfValue(value) {
9859
+ if (value === null) return "null";
9860
+ if (typeof value === "number") return "number";
9861
+ if (typeof value === "string") return "string";
9862
+ return "boolean";
9863
+ }
9864
+ function only(kind) {
9865
+ return new Set([kind]);
9866
+ }
9867
+ function union(...sets) {
9868
+ return new Set(sets.flatMap((s) => [...s]));
9869
+ }
9870
+ function withoutNull(set) {
9871
+ return new Set([...set].filter((k) => k !== "null"));
9872
+ }
9873
+ function binaryKinds(op, left, right) {
9874
+ if (op === "+") {
9875
+ const all = [...left, ...right];
9876
+ if (all.every((k) => k === "string")) return only("string");
9877
+ if (all.every((k) => k === "number")) return only("number");
9878
+ return new Set(["number", "string"]);
9879
+ }
9880
+ if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
9881
+ return only("boolean");
9882
+ }
9883
+ /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
9884
+ function arityRange(builtin) {
9885
+ if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
9886
+ if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
9887
+ return `${builtin.minArgs}–${builtin.maxArgs}`;
9888
+ }
9889
+ function callKinds(callee, args) {
9890
+ const builtin = require_composition.EXPRESSION_BUILTINS[callee];
9891
+ 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)}`);
9892
+ const rule = BUILTIN_RESULT_KINDS[callee];
9893
+ if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
9894
+ if (rule.kind === "fixed") return new Set(rule.result);
9895
+ const nonNull = union(...args.map(withoutNull));
9896
+ return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
9897
+ }
9898
+ function kindsOf(node, bindings) {
9899
+ switch (node.kind) {
9900
+ case "literal": return only(kindOfValue(node.value));
9901
+ case "identifier": {
9902
+ if (node.name === "now") return only("number");
9903
+ const bound = bindings.get(node.name);
9904
+ if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
9905
+ return bound;
9906
+ }
9907
+ case "unary": return only(node.op === "!" ? "boolean" : "number");
9908
+ case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
9909
+ case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
9910
+ case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
9911
+ case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
9912
+ }
9913
+ }
9914
+ function nodeContainsCall(node, callee) {
9915
+ switch (node.kind) {
9916
+ case "literal":
9917
+ case "identifier": return false;
9918
+ case "unary": return nodeContainsCall(node.operand, callee);
9919
+ case "binary":
9920
+ case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
9921
+ case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
9922
+ case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
9923
+ }
9924
+ }
9925
+ function expressionContainsBuiltinCall(expr, callee) {
9926
+ const compiled = require_composition.compileExpressionSafe(expr);
9927
+ return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
9928
+ }
9929
+ function inferExpressionKinds(input) {
9930
+ const compiled = require_composition.compileExpressionSafe(input.expr);
9931
+ if (!compiled.ok) return {
9932
+ ok: false,
9933
+ error: compiled.error
9934
+ };
9935
+ try {
9936
+ return {
9937
+ ok: true,
9938
+ kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
9939
+ };
9940
+ } catch (err) {
9941
+ if (err instanceof KindInferenceError) return {
9942
+ ok: false,
9943
+ error: err.message
9944
+ };
9945
+ throw err;
9946
+ }
9947
+ }
9948
+ //#endregion
9949
+ //#region src/composition/field-kinds.ts
9802
9950
  /**
9803
- * The builtin's own addon id — and, because of that, the OWNER of the
9804
- * integration every block's devices hang from.
9805
- *
9806
- * A block cannot own one. Every integration read is gated on the installed-addon
9807
- * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
9808
- * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
9809
- * null forever while the write succeeds — one invisible, undeletable row per
9810
- * start. `core-blocks` IS installed, so it reconciles one shared integration and
9811
- * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
9812
- *
9813
- * Exported from the contract, next to the runner prefix and for the same reason:
9814
- * a block reads it as
9815
- * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
9816
- * so a second copy of the string is a block that silently declares into nowhere.
9951
+ * The kind half of planning a composition field (D659): which kinds a source
9952
+ * can produce, whether they fit a target field, and the field's role.
9953
+ * Shared by the top-level planner (`validate-composition.ts`) and the item
9954
+ * planner (`composition-items.ts`). Internal to the composition module — NOT
9955
+ * in the barrel: these are planner internals, not a published contract.
9817
9956
  */
9818
- var CORE_BLOCKS_ADDON_ID = "core-blocks";
9819
- /** The addon/runner id a block's process runs under. */
9820
- function coreBlockAddonId(blockId) {
9821
- return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
9957
+ function readKinds(read, lookupCap) {
9958
+ const def = lookupCap(read.cap);
9959
+ const schema = def?.runtimeState ?? def?.status?.schema;
9960
+ if (def === null || schema === void 0) return {
9961
+ ok: false,
9962
+ code: "source-field-unknown",
9963
+ message: `no capability \`${read.cap}\` with state to read`
9964
+ };
9965
+ const field = describeStateField(schema, read.fieldPath);
9966
+ if (field === null) return {
9967
+ ok: false,
9968
+ code: "source-field-unknown",
9969
+ message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
9970
+ };
9971
+ const base = field.kind === "enum" ? "string" : field.kind;
9972
+ return {
9973
+ ok: true,
9974
+ kinds: new Set(field.nullable ? [base, "null"] : [base])
9975
+ };
9822
9976
  }
9823
- /** The block id behind a generated addon/runner id, or null when the id belongs
9824
- * to something else. The inverse of {@link coreBlockAddonId}. */
9825
- function coreBlockIdFromAddonId(addonId) {
9826
- if (!addonId.startsWith("core-block-")) return null;
9827
- const rest = addonId.slice(11);
9828
- return rest.length > 0 ? rest : null;
9977
+ function sourceKinds(source, lookupCap) {
9978
+ if (source.kind === "from") return readKinds(source, lookupCap);
9979
+ if (source.kind === "code") return {
9980
+ ok: false,
9981
+ code: "reserved-variant",
9982
+ message: "code fields arrive in slice 4"
9983
+ };
9984
+ const bindingKinds = /* @__PURE__ */ new Map();
9985
+ for (const [name, binding] of Object.entries(source.bindings)) {
9986
+ if (binding.kind === "literal") {
9987
+ bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
9988
+ continue;
9989
+ }
9990
+ const kinds = readKinds(binding, lookupCap);
9991
+ if (!kinds.ok) return {
9992
+ ...kinds,
9993
+ message: `binding \`${name}\`: ${kinds.message}`
9994
+ };
9995
+ bindingKinds.set(name, kinds.kinds);
9996
+ }
9997
+ const inferred = inferExpressionKinds({
9998
+ expr: source.expr,
9999
+ bindingKinds
10000
+ });
10001
+ return inferred.ok ? {
10002
+ ok: true,
10003
+ kinds: inferred.kinds
10004
+ } : {
10005
+ ok: false,
10006
+ code: "expression-invalid",
10007
+ message: inferred.error
10008
+ };
10009
+ }
10010
+ function compatibility(cap, target, kinds, at) {
10011
+ const allowed = target.kind === "enum" ? "string" : target.kind;
10012
+ const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
10013
+ const problems = [];
10014
+ if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
10015
+ 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`));
10016
+ return problems;
9829
10017
  }
9830
- /** Where a block runs. The operator chooses — a block driving a device on an
9831
- * agent is the reason placement is not fixed to the hub. */
9832
- var CoreBlockPlacementSchema = zod.z.union([zod.z.literal("hub"), zod.z.string().min(1)]);
9833
- /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
9834
- * a failing block reads the same way a failing addon does. `degraded` is a
9835
- * composition on an existing device with a field handed back to the native
9836
- * provider while its source is unavailable (S1b, D663); the error names it. */
9837
- var CoreBlockStatusSchema = zod.z.enum([
9838
- "stopped",
9839
- "starting",
9840
- "running",
9841
- "degraded",
9842
- "failed"
9843
- ]);
9844
- /** Client-authored fields. */
9845
- var CoreBlockInputSchema = zod.z.object({
9846
- name: zod.z.string().min(1).max(120),
9847
- /** TypeScript source. Compiled server-side before it is ever stored — a
9848
- * block that does not compile is a fork failure the operator would meet
9849
- * minutes later, in a log, instead of in the editor. */
9850
- code: zod.z.string().max(2e5),
9851
- enabled: zod.z.boolean(),
9852
- placement: CoreBlockPlacementSchema,
9853
- /**
9854
- * Integration the block's devices hang from. Absent = the system integration
9855
- * blocks share. A block may declare its own instead.
9856
- */
9857
- integrationId: zod.z.string().optional()
9858
- });
9859
10018
  /**
9860
- * What a block IS (D659). Absent means `code` — every row stored before this
9861
- * union keeps working unchanged.
9862
- * - `code`: the TypeScript in `code`, compiled and run in its own runner;
9863
- * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
9864
- * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
10019
+ * A string a source produces WITHOUT reading any device — a field-level
10020
+ * `expression` source whose compiled AST is a bare string literal (`"red"`,
10021
+ * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
10022
+ * the kind level (a `from` read of an enum-typed source field, a computed
10023
+ * expression) is NOT checked against the target's `enumValues` here: the
10024
+ * kind inference already collapsed it to the coarse `'string'` kind, losing
10025
+ * whichever enum it actually came from, so membership can only be judged at
10026
+ * write time against the runtime-state schema. Known limitation — see the
10027
+ * fix report for Task 6 round 1.
9865
10028
  */
9866
- var CoreBlockSourceSchema = zod.z.discriminatedUnion("kind", [
9867
- zod.z.object({ kind: zod.z.literal("code") }),
9868
- zod.z.object({
9869
- kind: zod.z.literal("recipe"),
9870
- recipe: AutomationRecipeSchema
9871
- }),
9872
- zod.z.object({
9873
- kind: zod.z.literal("composition"),
9874
- composition: require_composition.CompositionSchema
9875
- })
9876
- ]);
10029
+ function literalStringValue(source) {
10030
+ if (source.kind !== "expression") return null;
10031
+ const compiled = require_composition.compileExpressionSafe(source.expr);
10032
+ if (!compiled.ok) return null;
10033
+ const ast = compiled.parsed.ast;
10034
+ return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
10035
+ }
9877
10036
  /**
9878
- * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
9879
- *
9880
- * `name`: REQUIRED for a `new` target — the block's name IS the device's name
9881
- * (D659). ABSENT for an `existing` target — a customization has no name of its
9882
- * own; the server derives one (`customizationBlockName`, D663) and refuses a
9883
- * client-sent name by name.
10037
+ * An enum target whose source is a save-time-KNOWN string literal outside its
10038
+ * `enumValues` is refused now rather than left to fail the write later. A
10039
+ * dynamic source (a `from` read, a non-literal expression) is left to
10040
+ * runtime — see {@link literalStringValue}.
9884
10041
  */
9885
- var CompositionBlockInputSchema = zod.z.object({
9886
- name: zod.z.string().min(1).max(120).optional(),
9887
- enabled: zod.z.boolean(),
9888
- composition: require_composition.CompositionSchema
9889
- });
9890
- /** A stored block. */
9891
- var CoreBlockSchema = CoreBlockInputSchema.extend({
9892
- id: zod.z.string(),
9893
- createdAt: zod.z.number(),
9894
- updatedAt: zod.z.number(),
9895
- /** Server-stamped author. */
9896
- createdBy: zod.z.string(),
9897
- status: CoreBlockStatusSchema,
9898
- /**
9899
- * Why the block is not running, when it is not. The operator's ONLY window
9900
- * into a block that failed at load — a block that is silently absent is the
9901
- * failure mode this whole feature has to avoid.
9902
- */
9903
- lastError: zod.z.string().optional(),
9904
- /** Ms epoch of the last state change. */
9905
- lastChangedAt: zod.z.number(),
9906
- /** What the block is. Absent means `code` (D659). */
9907
- source: CoreBlockSourceSchema.optional()
9908
- });
9909
- function coreBlockSourceKind(block) {
9910
- return block.source?.kind ?? "code";
10042
+ function enumLiteralProblem(cap, target, source, at) {
10043
+ if (target.kind !== "enum" || target.enumValues === void 0) return null;
10044
+ const value = literalStringValue(source);
10045
+ if (value === null || target.enumValues.includes(value)) return null;
10046
+ const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
10047
+ return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
9911
10048
  }
9912
- function coreBlockComposition(block) {
9913
- return block.source?.kind === "composition" ? block.source.composition : null;
10049
+ function roleOf(source) {
10050
+ if (source.kind !== "expression") return "derived";
10051
+ if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
10052
+ if (require_composition.fieldReads(source).length > 0) return "derived";
10053
+ const compiled = require_composition.compileExpressionSafe(source.expr);
10054
+ return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
9914
10055
  }
9915
- /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
9916
- var CompositionStateReadSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
9917
- kind: zod.z.literal("answered"),
9918
- state: CompositionBlockStateSchema.nullable()
9919
- }), zod.z.object({
9920
- kind: zod.z.literal("unreachable"),
9921
- error: zod.z.string()
9922
- })]);
9923
- /** A stored composition block and what the composer says about it. */
9924
- var CompositionViewSchema = zod.z.object({
9925
- block: CoreBlockSchema,
9926
- state: CompositionStateReadSchema
9927
- });
9928
10056
  /**
9929
- * A read of the stored compositions. `not-loaded` while the `core_blocks`
9930
- * table has not been READ: an empty answer then would make "not yet known"
9931
- * look like "none", and invite the operator to recreate what exists (D315).
10057
+ * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
10058
+ * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
10059
+ * own offline, and D393 forbids a stale number, so on an existing target a
10060
+ * field that READS a source never gets `offline`.
9932
10061
  */
9933
- var CompositionsNotLoadedSchema = zod.z.object({
9934
- state: zod.z.literal("not-loaded"),
9935
- reason: zod.z.string()
9936
- });
9937
- var CompositionListReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9938
- state: zod.z.literal("loaded"),
9939
- views: zod.z.array(CompositionViewSchema)
9940
- }), CompositionsNotLoadedSchema]);
9941
- var CompositionGetReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9942
- state: zod.z.literal("loaded"),
9943
- view: CompositionViewSchema.nullable()
9944
- }), CompositionsNotLoadedSchema]);
9945
- /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
9946
- var CompositionCustomizationReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9947
- state: zod.z.literal("loaded"),
9948
- view: CompositionViewSchema.nullable()
9949
- }), CompositionsNotLoadedSchema]);
9950
- /** What a compile attempt produced. */
9951
- var CoreBlockCompileResultSchema = zod.z.object({
9952
- ok: zod.z.boolean(),
9953
- /** Present when `ok` is false — the first error, in the author's words. */
9954
- error: zod.z.string().optional(),
9955
- line: zod.z.number().optional(),
9956
- column: zod.z.number().optional()
9957
- });
9958
- var coreBlocksCapability = {
9959
- name: "core-blocks",
9960
- scope: "system",
9961
- mode: "singleton",
9962
- methods: {
9963
- list: require_sleep.method(zod.z.object({}), zod.z.object({ blocks: zod.z.array(CoreBlockSchema) }), { auth: "admin" }),
9964
- get: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
9965
- /**
9966
- * Create a block. The code is COMPILED first: storing source that does not
9967
- * compile turns an editor error into a fork failure the operator meets
9968
- * minutes later in a log.
9969
- */
9970
- create: require_sleep.method(zod.z.object({ block: CoreBlockInputSchema }), zod.z.object({ block: CoreBlockSchema }), {
9971
- kind: "mutation",
9972
- auth: "admin",
9973
- caller: "required"
9974
- }),
9975
- update: require_sleep.method(zod.z.object({
9976
- blockId: zod.z.string(),
9977
- block: CoreBlockInputSchema.partial()
9978
- }), zod.z.object({ block: CoreBlockSchema }), {
9979
- kind: "mutation",
9980
- auth: "admin",
9981
- caller: "required"
9982
- }),
9983
- delete: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ success: zod.z.literal(true) }), {
9984
- kind: "mutation",
9985
- auth: "admin"
9986
- }),
9987
- setEnabled: require_sleep.method(zod.z.object({
9988
- blockId: zod.z.string(),
9989
- enabled: zod.z.boolean()
9990
- }), zod.z.object({ block: CoreBlockSchema }), {
9991
- kind: "mutation",
9992
- auth: "admin"
9993
- }),
9994
- /**
9995
- * Stop this block's runner and let the supervisor bring it back.
9996
- *
9997
- * **It stores nothing.** There is no "restart requested" flag and no second
9998
- * lifecycle field: the supervisor already stops and respawns a runner when
9999
- * the code hash changes, and this exposes that same path deliberately
10000
- * (D62 — a switch writes the authority that already owned the function).
10001
- * The next reconcile pass remains the authority, so a restart that lost a
10002
- * race is corrected within one pass rather than leaving a stale flag behind.
10003
- *
10004
- * Refused for a DISABLED block: nothing would come back, and a button that
10005
- * silently does nothing is worse than one that says why.
10006
- */
10007
- restart: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema }), {
10008
- kind: "mutation",
10009
- auth: "admin"
10010
- }),
10011
- /**
10012
- * Type-check without saving — what the editor calls as the author types, so
10013
- * the compiler's verdict is the same one the server will reach.
10014
- */
10015
- compile: require_sleep.method(zod.z.object({ code: zod.z.string() }), CoreBlockCompileResultSchema, {
10016
- kind: "mutation",
10017
- auth: "admin"
10018
- }),
10019
- /**
10020
- * The declaration files the editor type-checks against.
10021
- *
10022
- * Served rather than bundled: the graph is 3.4 MB across 344 files, and
10023
- * bundling it would roughly double the admin remote. Served rather than
10024
- * hand-stubbed: a block runs with the same `ctx` an addon gets, so a stub
10025
- * would drift and the editor would confidently autocomplete methods that
10026
- * do not exist.
10027
- */
10028
- getTypeDefs: require_sleep.method(zod.z.object({}), zod.z.object({ libs: zod.z.array(zod.z.object({
10029
- filePath: zod.z.string(),
10030
- content: zod.z.string()
10031
- })) }), { auth: "admin" }),
10032
- /**
10033
- * Create a composition block (D659). VALIDATED first against the live
10062
+ function unavailableFor(tf, role, mode, cap, at) {
10063
+ if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
10064
+ ok: true,
10065
+ unavailable: "release"
10066
+ };
10067
+ if (mode === "new" || role !== "derived") return {
10068
+ ok: true,
10069
+ unavailable: tf.nullable ? "null" : "offline"
10070
+ };
10071
+ if (tf.nullable) return {
10072
+ ok: true,
10073
+ unavailable: "null"
10074
+ };
10075
+ if (tf.enumValues?.includes("unknown") === true) return {
10076
+ ok: true,
10077
+ unavailable: "unknown"
10078
+ };
10079
+ return {
10080
+ ok: false,
10081
+ problem: problem("not-replaceable", at, `\`${cap}.${tf.path}\` reads a source but can say neither null nor 'unknown', and an ADDED capability has no native reading to fall back to (D393, D663)`)
10082
+ };
10083
+ }
10084
+ //#endregion
10085
+ //#region src/composition/composition-items.ts
10086
+ /** Item fields the composer writes itself, with the value it writes. */
10087
+ var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
10088
+ function reservedItemFields(itemArray) {
10089
+ return new Set([
10090
+ itemArray.keyField,
10091
+ ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
10092
+ ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
10093
+ ]);
10094
+ }
10095
+ /** A required leaf under an optional parent is required only once something writes that parent. */
10096
+ function leafRequired(itemArray, path, written) {
10097
+ const described = describeStateField(itemArray.itemSchema, path);
10098
+ if (described === null || !described.required) return false;
10099
+ return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
10100
+ }
10101
+ function missingLeaves({ capName, itemArray, reserved }, item, at) {
10102
+ const written = Object.keys(item.fields);
10103
+ return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
10104
+ if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
10105
+ if (!leafRequired(itemArray, leaf.path, written)) return [];
10106
+ const described = describeStateField(itemArray.itemSchema, leaf.path);
10107
+ if (described === null || described.nullable) return [];
10108
+ return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
10109
+ });
10110
+ }
10111
+ function planItemField(ctx, leaf) {
10112
+ const { capName, itemArray, featureIndex, lookupCap } = ctx;
10113
+ const { key, label, path, source } = leaf;
10114
+ const at = `features[${featureIndex}].items.${key}.fields.${path}`;
10115
+ if (ctx.reserved.has(path)) return refused(problem("item-field-reserved", at, `\`${capName}.${itemArray.path}[].${path}\` is set by the composer itself — it takes no source`));
10116
+ const tf = describeStateField(itemArray.itemSchema, path);
10117
+ if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
10118
+ const kinds = sourceKinds(source, lookupCap);
10119
+ if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
10120
+ const role = roleOf(source);
10121
+ const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
10122
+ if (!unavailable.ok) return refused(unavailable.problem);
10123
+ const plan = {
10124
+ cap: capName,
10125
+ path,
10126
+ item: {
10127
+ arrayPath: itemArray.path,
10128
+ key,
10129
+ label
10130
+ },
10131
+ featureIndex,
10132
+ kind: tf.kind,
10133
+ nullable: tf.nullable,
10134
+ required: tf.required,
10135
+ unavailable: unavailable.unavailable,
10136
+ role,
10137
+ source,
10138
+ reads: [...require_composition.fieldReads(source)]
10139
+ };
10140
+ const enumProblem = enumLiteralProblem(capName, tf, source, at);
10141
+ return {
10142
+ problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
10143
+ fields: [plan],
10144
+ features: [],
10145
+ events: []
10146
+ };
10147
+ }
10148
+ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
10149
+ const at = `features[${featureIndex}].items`;
10150
+ const itemArray = def.status?.itemArray;
10151
+ if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
10152
+ if (itemArray.path !== "items") return refused(problem("not-item-array", at, `\`${def.name}\`'s item array is at \`${itemArray.path}\`, not \`${require_composition.COMPOSITION_ITEM_ARRAY_PATH}\` — not composable yet`));
10153
+ const ctx = {
10154
+ capName: def.name,
10155
+ itemArray,
10156
+ reserved: reservedItemFields(itemArray),
10157
+ featureIndex,
10158
+ lookupCap,
10159
+ mode
10160
+ };
10161
+ const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
10162
+ problems: missingLeaves(ctx, item, `${at}.${key}`),
10163
+ fields: [],
10164
+ features: [],
10165
+ events: []
10166
+ }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
10167
+ key,
10168
+ label: item.label,
10169
+ path,
10170
+ source
10171
+ }))]);
10172
+ return {
10173
+ problems: results.flatMap((r) => r.problems),
10174
+ fields: results.flatMap((r) => r.fields),
10175
+ features: [],
10176
+ events: []
10177
+ };
10178
+ }
10179
+ //#endregion
10180
+ //#region src/composition/composition-graph.ts
10181
+ /**
10182
+ * The field graph across every composition, and the cycles in it (D659).
10183
+ *
10184
+ * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
10185
+ * edge points from a composed field to each field it reads. A cycle, direct or
10186
+ * through another composition, is refused at save by name (spec §2).
10187
+ *
10188
+ * The graph is built for ONE save: the CANDIDATE composition (the one being
10189
+ * validated) plus every STORED composition. The candidate replaces its own old
10190
+ * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
10191
+ * introduces or removes a cycle must never depend on array order. A DIFFERENT
10192
+ * block targeting the candidate's device is refused by name: one composition
10193
+ * per device (D663). Two different stored peers that (wrongly) target the same
10194
+ * device are corrupted state, not a cycle to resolve by picking one silently;
10195
+ * that is refused by name too (D391).
10196
+ *
10197
+ * A customization of an EXISTING device reads that device's NATIVE value for a
10198
+ * field it replaces or does not write ("Invert state" reads the `on` it
10199
+ * replaces): such a self-read is no edge, so it is never a cycle (D663).
10200
+ * Another composition reading that field reads the customizer's output.
10201
+ *
10202
+ * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
10203
+ * marks a node `done` after its first expansion) — not every elementary cycle
10204
+ * through a shared node, so the path it names may AVOID the candidate while
10205
+ * one through it goes unreported. What a save refuses is therefore decided per
10206
+ * strongly connected component (`findCandidateCycles`, C-1). A dotted
10207
+ * `fieldPath` is matched only as an exact string: a write to `state.value` and
10208
+ * a read of `state` would not connect (no nested field writes exist yet).
10209
+ *
10210
+ * An item leaf is its own node, `…#consumables.items[desiccant].status`
10211
+ * (`featureFieldEntries`), so two items sharing an inner path never merge. No
10212
+ * READ can name an array element (`describeStateField` never describes one),
10213
+ * so an item node is write-only: its edges to sources are what the check needs.
10214
+ */
10215
+ /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
10216
+ var CompositionGraphError = class extends Error {};
10217
+ /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
10218
+ function writtenFields(peer) {
10219
+ return new Set(peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
10220
+ }
10221
+ /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
10222
+ function readsComposedValue(peer, written, read) {
10223
+ if (require_composition.compositionSourceKey(read.source) !== require_composition.compositionSourceKey(peer.target)) return true;
10224
+ const field = `${read.cap}.${read.fieldPath}`;
10225
+ return written.has(field) && peer.replaced?.has(field) !== true;
10226
+ }
10227
+ function peerEdges(peer) {
10228
+ const written = writtenFields(peer);
10229
+ return peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath, source }) => [require_composition.compositionReadKey({
10230
+ source: peer.target,
10231
+ cap: feature.cap,
10232
+ fieldPath
10233
+ }), require_composition.fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(require_composition.compositionReadKey)]));
10234
+ }
10235
+ /**
10236
+ * Every peer that actually takes part in the graph: each stored peer minus the
10237
+ * candidate's own old version (same block id — the drop happens before any
10238
+ * edge is built, so its old edges never enter the map), plus the candidate
10239
+ * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
10240
+ * block targets the candidate's device, or two stored peers share a device.
10241
+ */
10242
+ function resolvedPeers(candidate, stored) {
10243
+ const candidateKey = require_composition.compositionSourceKey(candidate.target);
10244
+ const survivors = /* @__PURE__ */ new Map();
10245
+ for (const peer of stored) {
10246
+ if (peer.blockId === candidate.blockId) continue;
10247
+ const key = require_composition.compositionSourceKey(peer.target);
10248
+ if (key === candidateKey) throw new CompositionGraphError(`block \`${peer.blockId}\` and block \`${candidate.blockId}\` both target device \`${key}\` — one composition per device; delete or edit the existing one`);
10249
+ const clash = survivors.get(key);
10250
+ if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
10251
+ survivors.set(key, peer);
10252
+ }
10253
+ return [...survivors.values(), candidate];
10254
+ }
10255
+ function compositionEdges(candidate, stored) {
10256
+ const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
10257
+ return new Map(edges);
10258
+ }
10259
+ function findCompositionCycles(candidate, stored) {
10260
+ const edges = compositionEdges(candidate, stored);
10261
+ const marks = /* @__PURE__ */ new Map();
10262
+ const found = /* @__PURE__ */ new Map();
10263
+ const visit = (node, stack) => {
10264
+ const mark = marks.get(node);
10265
+ if (mark === "done") return;
10266
+ if (mark === "visiting") {
10267
+ const path = [...stack.slice(stack.indexOf(node)), node];
10268
+ const id = [...new Set(path)].toSorted().join("|");
10269
+ if (!found.has(id)) found.set(id, { path });
10270
+ return;
10271
+ }
10272
+ marks.set(node, "visiting");
10273
+ for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
10274
+ marks.set(node, "done");
10275
+ };
10276
+ for (const node of edges.keys()) visit(node, []);
10277
+ return [...found.values()];
10278
+ }
10279
+ /** Tarjan's strongly connected components over every node of the graph (sources included). */
10280
+ function stronglyConnected(edges) {
10281
+ const index = /* @__PURE__ */ new Map();
10282
+ const low = /* @__PURE__ */ new Map();
10283
+ const onStack = /* @__PURE__ */ new Set();
10284
+ const stack = [];
10285
+ const components = [];
10286
+ const connect = (node) => {
10287
+ index.set(node, index.size);
10288
+ low.set(node, index.get(node) ?? 0);
10289
+ stack.push(node);
10290
+ onStack.add(node);
10291
+ for (const next of edges.get(node) ?? []) if (!index.has(next)) {
10292
+ connect(next);
10293
+ low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
10294
+ } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
10295
+ if (low.get(node) !== index.get(node)) return;
10296
+ const component = [];
10297
+ for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
10298
+ onStack.delete(member);
10299
+ component.push(member);
10300
+ if (member === node) break;
10301
+ }
10302
+ components.push(component);
10303
+ };
10304
+ for (const node of edges.keys()) if (!index.has(node)) connect(node);
10305
+ return components;
10306
+ }
10307
+ /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
10308
+ function cycleThrough(start, component, edges) {
10309
+ const parent = /* @__PURE__ */ new Map();
10310
+ const queue = [start];
10311
+ for (let head = 0; head < queue.length; head += 1) {
10312
+ const node = queue[head];
10313
+ if (node === void 0) break;
10314
+ for (const next of edges.get(node) ?? []) {
10315
+ if (next === start) {
10316
+ const chain = [];
10317
+ for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
10318
+ return [
10319
+ start,
10320
+ ...chain.reverse(),
10321
+ start
10322
+ ];
10323
+ }
10324
+ if (!component.has(next) || parent.has(next)) continue;
10325
+ parent.set(next, node);
10326
+ queue.push(next);
10327
+ }
10328
+ }
10329
+ return [start, start];
10330
+ }
10331
+ /**
10332
+ * The cycles THIS save closes, decided per strongly connected component — never
10333
+ * per reported DFS path, which names one back-edge per revisit and can report a
10334
+ * component's cycle by a path that avoids the candidate while the one through it
10335
+ * goes unreported. A component refuses when it holds a candidate node and a
10336
+ * cycle (more than one node, or a self-loop). Components without a candidate
10337
+ * node are not this save's cycles.
10338
+ */
10339
+ function findCandidateCycles(candidate, stored) {
10340
+ const edges = compositionEdges(candidate, stored);
10341
+ const prefix = `${require_composition.compositionSourceKey(candidate.target)}#`;
10342
+ return stronglyConnected(edges).flatMap((members) => {
10343
+ const start = members.find((n) => n.startsWith(prefix));
10344
+ if (start === void 0) return [];
10345
+ const component = new Set(members);
10346
+ const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
10347
+ if (members.length === 1 && !selfLoop) return [];
10348
+ return [{
10349
+ path: cycleThrough(start, component, edges),
10350
+ component
10351
+ }];
10352
+ });
10353
+ }
10354
+ //#endregion
10355
+ //#region src/composition/validate-composition.ts
10356
+ function buildCapabilityLookup(defs) {
10357
+ const byName = new Map(defs.map((d) => [d.name, d]));
10358
+ return (capName) => byName.get(capName) ?? null;
10359
+ }
10360
+ /** Caps every `BaseDevice` registers on itself; a second registration throws. */
10361
+ var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
10362
+ var PENDING_SELF = {
10363
+ addonId: require_composition.COMPOSER_ADDON_ID,
10364
+ stableId: `${require_composition.COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
10365
+ };
10366
+ /** The candidate's block id before its first save. Stored block ids are `randomUUID()`, so none can carry it. */
10367
+ var PENDING_BLOCK_ID = "(unsaved)";
10368
+ function composableCap(feature, at, lookupCap) {
10369
+ const def = lookupCap(feature.cap);
10370
+ if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
10371
+ if (BASE_DEVICE_CAPS.has(def.name)) return problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`);
10372
+ const state = def.runtimeState;
10373
+ if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
10374
+ return {
10375
+ def,
10376
+ state,
10377
+ fields: describeTopLevelStateFields(state)
10378
+ };
10379
+ }
10380
+ /**
10381
+ * A capability BUILT by the composition (a new device, or one ADDED to an
10382
+ * existing device) must fit the device type, serve no command, and have every
10383
+ * required field. A REPLACE needs none of it: the cap is bound (a fact), the
10384
+ * native keeps serving its methods, and only the named fields are owned (D663).
10385
+ */
10386
+ function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
10387
+ const problems = [];
10388
+ if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
10389
+ const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
10390
+ if (served.length > 0) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
10391
+ const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
10392
+ for (const key of requiredTopLevelKeys(state)) {
10393
+ if (key === itemArrayPath) continue;
10394
+ if (!targetFields.has(key)) problems.push(problem("not-composable", `${at}.fields`, `\`${def.name}.${key}\` is not a scalar — nested fields arrive in a later slice`));
10395
+ else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
10396
+ }
10397
+ return problems;
10398
+ }
10399
+ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
10400
+ const { featureIndex, path, source } = field;
10401
+ const fieldAt = `features[${featureIndex}].fields.${path}`;
10402
+ if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
10403
+ const tf = fields.get(path);
10404
+ if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
10405
+ const kinds = sourceKinds(source, lookupCap);
10406
+ if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
10407
+ const role = roleOf(source);
10408
+ const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
10409
+ if (!unavailable.ok) return refused(unavailable.problem);
10410
+ const plan = {
10411
+ cap: def.name,
10412
+ path,
10413
+ featureIndex,
10414
+ kind: tf.kind,
10415
+ nullable: tf.nullable,
10416
+ required: tf.required,
10417
+ unavailable: unavailable.unavailable,
10418
+ role,
10419
+ source,
10420
+ reads: [...require_composition.fieldReads(source)]
10421
+ };
10422
+ const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
10423
+ return {
10424
+ problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
10425
+ fields: [plan],
10426
+ features: [],
10427
+ events: []
10428
+ };
10429
+ }
10430
+ function planFeature(feature, featureIndex, target, lookupCap) {
10431
+ const at = `features[${featureIndex}]`;
10432
+ const cap = composableCap(feature, at, lookupCap);
10433
+ if ("code" in cap) return refused(cap);
10434
+ const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
10435
+ if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
10436
+ const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
10437
+ featureIndex,
10438
+ path,
10439
+ source
10440
+ }, target.mode, lookupCap));
10441
+ const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
10442
+ const all = [...perField, ...items];
10443
+ const fields = all.flatMap((result) => result.fields);
10444
+ const featurePlan = {
10445
+ featureIndex,
10446
+ cap: cap.def.name,
10447
+ mode: target.mode
10448
+ };
10449
+ const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
10450
+ return {
10451
+ problems: [
10452
+ ...capProblems,
10453
+ ...all.flatMap((r) => r.problems),
10454
+ ...eventPlan.problems
10455
+ ],
10456
+ fields,
10457
+ features: [featurePlan],
10458
+ events: eventPlan.events
10459
+ };
10460
+ }
10461
+ /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
10462
+ function isNativeCap(target, cap) {
10463
+ const owner = target.nativeCaps.get(cap);
10464
+ return owner !== void 0 && owner !== "composer";
10465
+ }
10466
+ /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
10467
+ function planTargetOf(composition, target) {
10468
+ if (composition.target.kind === "new") return {
10469
+ kind: "new",
10470
+ type: composition.target.type
10471
+ };
10472
+ const key = require_composition.compositionSourceKey(composition.target.device);
10473
+ if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
10474
+ if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
10475
+ if (target.kind === "unreadable") return problem("target-unreadable", "target", `could not read target device \`${key}\` (${target.error}) — the save is refused rather than stored unchecked`);
10476
+ return {
10477
+ kind: "existing",
10478
+ found: target
10479
+ };
10480
+ }
10481
+ function featureTargetOf(target, cap) {
10482
+ if (target.kind === "new") return {
10483
+ mode: "new",
10484
+ deviceType: target.type
10485
+ };
10486
+ return {
10487
+ mode: isNativeCap(target.found, cap) ? "replace" : "add",
10488
+ deviceType: target.found.type
10489
+ };
10490
+ }
10491
+ /**
10492
+ * The static plan. `target` describes an EXISTING target device (one read);
10493
+ * it is ignored for a new one. On an existing target each feature REPLACES a
10494
+ * cap the device has natively, else ADDS it (D663).
10495
+ */
10496
+ function planComposition(composition, lookupCap, target) {
10497
+ const planTarget = planTargetOf(composition, target);
10498
+ if ("code" in planTarget) return refused(planTarget);
10499
+ const results = composition.features.map((feature, index) => {
10500
+ const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
10501
+ return require_composition.compositionFeatureEnabled(feature) ? result : {
10502
+ problems: result.problems,
10503
+ fields: [],
10504
+ features: [],
10505
+ events: []
10506
+ };
10507
+ });
10508
+ return {
10509
+ problems: results.flatMap((r) => r.problems),
10510
+ fields: results.flatMap((r) => r.fields),
10511
+ features: results.flatMap((r) => r.features),
10512
+ events: results.flatMap((r) => r.events)
10513
+ };
10514
+ }
10515
+ function compositionSourceRefs(composition) {
10516
+ const refs = composition.features.flatMap((f) => require_composition.compositionFeatureEnabled(f) && f.kind === "fields" ? require_composition.featureSources(f).flatMap(require_composition.fieldReads).map((r) => r.source) : []);
10517
+ return [...new Map(refs.map((r) => [require_composition.compositionSourceKey(r), r])).values()];
10518
+ }
10519
+ /**
10520
+ * The candidate's device: the existing target itself, else `self`, else a
10521
+ * placeholder that cannot collide with a stored target. An existing target is
10522
+ * known even on create, so a second customization of it is refused on create too.
10523
+ */
10524
+ function candidateSelf(composition, ctx, plan) {
10525
+ return {
10526
+ ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
10527
+ native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
10528
+ replaced: replacedFieldKeys(plan)
10529
+ };
10530
+ }
10531
+ /**
10532
+ * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
10533
+ * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
10534
+ * derivation, so the graph reads both the same way.
10535
+ */
10536
+ function replacedFieldKeys(plan) {
10537
+ const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
10538
+ return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
10539
+ }
10540
+ /**
10541
+ * A read of the candidate's own device. Its own composed output (a new device's
10542
+ * field, an ADDED field) is the slice-1 own-field rule; on an existing target
10543
+ * every other field — replaced, or not composed at all — is the NATIVE value,
10544
+ * valid when the device serves that cap natively (D663, rule 5).
10545
+ */
10546
+ function selfReadProblems(read, at, fields, self) {
10547
+ const readKey = `${read.cap}.${read.fieldPath}`;
10548
+ if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
10549
+ if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
10550
+ if (isNativeCap(self.native, read.cap)) return [];
10551
+ return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
10552
+ }
10553
+ function sourceProblems(fields, ctx, self) {
10554
+ const selfKey = require_composition.compositionSourceKey(self.ref);
10555
+ return fields.flatMap((field) => field.reads.flatMap((read) => {
10556
+ const at = compositionFieldAt(field);
10557
+ const key = require_composition.compositionSourceKey(read.source);
10558
+ if (key === selfKey) return selfReadProblems(read, at, fields, self);
10559
+ const desc = ctx.sources.get(key);
10560
+ 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`)];
10561
+ if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
10562
+ if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
10563
+ return [];
10564
+ }));
10565
+ }
10566
+ /**
10567
+ * A cycle's node keys are `compositionReadKey` strings:
10568
+ * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
10569
+ * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
10570
+ * back out so the refusal can point at that exact field, matching the
10571
+ * documented `features[N].fields.<path>` convention. A cap name has no `.`,
10572
+ * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
10573
+ * (`items[desiccant].status`) is matched through `compositionFieldKey`.
10574
+ */
10575
+ function candidateFieldOfNode(nodeKey, candidateKey) {
10576
+ const prefix = `${candidateKey}#`;
10577
+ if (!nodeKey.startsWith(prefix)) return null;
10578
+ const rest = nodeKey.slice(prefix.length);
10579
+ const dot = rest.indexOf(".");
10580
+ if (dot === -1) return null;
10581
+ return {
10582
+ cap: rest.slice(0, dot),
10583
+ fieldPath: rest.slice(dot + 1)
10584
+ };
10585
+ }
10586
+ /** The first node of a cycle's path that is one of THIS composition's own
10587
+ * planned fields, as a documented-convention path; `'features'` when the
10588
+ * cycle runs entirely through other stored compositions. */
10589
+ function cyclePath(nodePath, candidateKey, fields) {
10590
+ for (const nodeKey of nodePath) {
10591
+ const hit = candidateFieldOfNode(nodeKey, candidateKey);
10592
+ if (hit === null) continue;
10593
+ const hitKey = `${hit.cap}.${hit.fieldPath}`;
10594
+ const field = fields.find((f) => compositionFieldKey(f) === hitKey);
10595
+ if (field !== void 0) return compositionFieldAt(field);
10596
+ }
10597
+ return "features";
10598
+ }
10599
+ /** The first unread device any of `nodes` belongs to; null when none does. */
10600
+ function unreadDeviceOf(nodes, unread) {
10601
+ for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
10602
+ key,
10603
+ error
10604
+ };
10605
+ return null;
10606
+ }
10607
+ /**
10608
+ * Cycle check runs against the CANDIDATE (this composition, as block
10609
+ * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
10610
+ * decided per strongly connected component (`findCandidateCycles`): a cyclic
10611
+ * component holding a candidate node refuses, with one representative cycle
10612
+ * through it; one without is not this save's cycle. A
10613
+ * stored composition of the same block is its old version; another block on
10614
+ * the same device, or two stored peers sharing a device, make
10615
+ * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
10616
+ * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
10617
+ */
10618
+ function cycleProblems(composition, ctx, fields, self) {
10619
+ const candidate = {
10620
+ blockId: ctx.blockId ?? "(unsaved)",
10621
+ target: self.ref,
10622
+ composition,
10623
+ replaced: self.replaced
10624
+ };
10625
+ const candidateKey = require_composition.compositionSourceKey(candidate.target);
10626
+ const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
10627
+ try {
10628
+ return findCandidateCycles(candidate, ctx.peers).map((c) => {
10629
+ const at = cyclePath(c.path, candidateKey, fields);
10630
+ const through = unreadDeviceOf([...c.component], unread);
10631
+ return through === null ? problem("cycle", at, `a field reads itself: ${c.path.join(" → ")}`) : problem("cycle", at, `possible cycle through \`${through.key}\` — its state could not be read (${through.error}): ${c.path.join(" → ")}`);
10632
+ });
10633
+ } catch (err) {
10634
+ if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
10635
+ throw err;
10636
+ }
10637
+ }
10638
+ function validateComposition(composition, ctx) {
10639
+ const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
10640
+ const self = candidateSelf(composition, ctx, plan);
10641
+ const cycles = cycleProblems(composition, ctx, plan.fields, self);
10642
+ const problems = [
10643
+ ...plan.problems,
10644
+ ...sourceProblems(plan.fields, ctx, self),
10645
+ ...cycles
10646
+ ];
10647
+ return {
10648
+ ok: problems.length === 0,
10649
+ problems,
10650
+ fields: [...plan.fields],
10651
+ features: [...plan.features],
10652
+ events: [...plan.events]
10653
+ };
10654
+ }
10655
+ //#endregion
10656
+ //#region src/composition/composable-caps.ts
10657
+ /**
10658
+ * What a composition may BUILD on a device type (G2, D676): the caps a new
10659
+ * device of that type, or an existing one that lacks them, can be given. The
10660
+ * blockers are the validator's own refusal codes, and a spec compares this
10661
+ * listing with `planComposition` for every capability — the editor can never
10662
+ * offer a feature the save would refuse for the same reason.
10663
+ */
10664
+ var ComposableFieldSchema = zod.z.object({
10665
+ path: zod.z.string(),
10666
+ kind: CompositionFieldKindSchema,
10667
+ nullable: zod.z.boolean(),
10668
+ required: zod.z.boolean(),
10669
+ enumValues: zod.z.array(zod.z.string()).optional()
10670
+ });
10671
+ var ComposableCapabilitySchema = zod.z.object({
10672
+ cap: zod.z.string(),
10673
+ fields: zod.z.array(ComposableFieldSchema),
10674
+ /** Present for an item-array cap (`consumables.items`). */
10675
+ itemArray: zod.z.object({
10676
+ path: zod.z.string(),
10677
+ keyField: zod.z.string()
10678
+ }).optional()
10679
+ });
10680
+ var ComposableCapabilitiesSchema = zod.z.object({ caps: zod.z.array(ComposableCapabilitySchema) });
10681
+ /** Why a cap cannot be BUILT (new device, or added to an existing one) on `deviceType`; [] = it can. */
10682
+ function builtCapabilityBlockers(def, deviceType) {
10683
+ if (BASE_DEVICE_CAPS.has(def.name)) return ["not-composable"];
10684
+ const state = def.runtimeState;
10685
+ if (def.scope !== "device" || state === void 0) return ["not-composable"];
10686
+ const blockers = [];
10687
+ if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) blockers.push("wrong-device-type");
10688
+ if (Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).length > 0) blockers.push("has-commands");
10689
+ const fields = describeTopLevelStateFields(state);
10690
+ const itemArrayPath = def.status?.itemArray?.path ?? null;
10691
+ if (requiredTopLevelKeys(state).some((key) => key !== itemArrayPath && !fields.has(key))) blockers.push("not-composable");
10692
+ if (planCompositionEvents(def, {
10693
+ featureIndex: 0,
10694
+ cap: def.name,
10695
+ mode: "new"
10696
+ }, []).problems.some((p) => p.code === "not-composable") && !blockers.includes("not-composable")) blockers.push("not-composable");
10697
+ return blockers;
10698
+ }
10699
+ /** Every cap with no blocker, sorted by name. */
10700
+ function describeComposableCaps(defs, deviceType) {
10701
+ return defs.filter((def) => builtCapabilityBlockers(def, deviceType).length === 0).map((def) => {
10702
+ const state = def.runtimeState;
10703
+ const fields = state === void 0 ? [] : [...describeTopLevelStateFields(state).values()];
10704
+ const itemArray = def.status?.itemArray;
10705
+ return {
10706
+ cap: def.name,
10707
+ fields: fields.map((f) => ({
10708
+ path: f.path,
10709
+ kind: f.kind,
10710
+ nullable: f.nullable,
10711
+ required: f.required,
10712
+ ...f.enumValues === void 0 ? {} : { enumValues: [...f.enumValues] }
10713
+ })),
10714
+ ...itemArray === void 0 ? {} : { itemArray: {
10715
+ path: itemArray.path,
10716
+ keyField: itemArray.keyField
10717
+ } }
10718
+ };
10719
+ }).toSorted((a, b) => a.cap.localeCompare(b.cap));
10720
+ }
10721
+ //#endregion
10722
+ //#region src/capabilities/core-blocks.cap.ts
10723
+ /**
10724
+ * core-blocks — user-authored TypeScript, stored in the kernel and executed in
10725
+ * its own process.
10726
+ *
10727
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
10728
+ *
10729
+ * The first use is **owning devices without being a device provider**: a block
10730
+ * declares devices under a system or custom integration and drives their state,
10731
+ * with the same `ctx` an addon gets. Automations come later; nothing here
10732
+ * models a trigger.
10733
+ *
10734
+ * **Stated plainly, because it does not change by being true:** a block has an
10735
+ * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
10736
+ * with no review step. What makes that survivable is not a sandbox, it is
10737
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
10738
+ * so a block that throws or never returns is marked `failed` and visible
10739
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
10740
+ * must stay so.
10741
+ */
10742
+ /**
10743
+ * Prefix for a block's generated addon id AND its runner id — one addon, one
10744
+ * process, so the two are the same string (D2).
10745
+ *
10746
+ * It lives on the CONTRACT rather than beside the code that materialises the
10747
+ * package, because it is not an implementation detail: the operator surface
10748
+ * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
10749
+ * with no cap and no store of its own. Two copies of this string is one copy
10750
+ * too many — a drift would show as a logs pane that is simply always empty.
10751
+ */
10752
+ var CORE_BLOCK_ADDON_PREFIX = "core-block-";
10753
+ /**
10754
+ * The builtin's own addon id — and, because of that, the OWNER of the
10755
+ * integration every block's devices hang from.
10756
+ *
10757
+ * A block cannot own one. Every integration read is gated on the installed-addon
10758
+ * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
10759
+ * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
10760
+ * null forever while the write succeeds — one invisible, undeletable row per
10761
+ * start. `core-blocks` IS installed, so it reconciles one shared integration and
10762
+ * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
10763
+ *
10764
+ * Exported from the contract, next to the runner prefix and for the same reason:
10765
+ * a block reads it as
10766
+ * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
10767
+ * so a second copy of the string is a block that silently declares into nowhere.
10768
+ */
10769
+ var CORE_BLOCKS_ADDON_ID = "core-blocks";
10770
+ /** The addon/runner id a block's process runs under. */
10771
+ function coreBlockAddonId(blockId) {
10772
+ return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
10773
+ }
10774
+ /** The block id behind a generated addon/runner id, or null when the id belongs
10775
+ * to something else. The inverse of {@link coreBlockAddonId}. */
10776
+ function coreBlockIdFromAddonId(addonId) {
10777
+ if (!addonId.startsWith("core-block-")) return null;
10778
+ const rest = addonId.slice(11);
10779
+ return rest.length > 0 ? rest : null;
10780
+ }
10781
+ /** Where a block runs. The operator chooses — a block driving a device on an
10782
+ * agent is the reason placement is not fixed to the hub. */
10783
+ var CoreBlockPlacementSchema = zod.z.union([zod.z.literal("hub"), zod.z.string().min(1)]);
10784
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
10785
+ * a failing block reads the same way a failing addon does. `degraded` is a
10786
+ * composition on an existing device with a field handed back to the native
10787
+ * provider while its source is unavailable (S1b, D663); the error names it. */
10788
+ var CoreBlockStatusSchema = zod.z.enum([
10789
+ "stopped",
10790
+ "starting",
10791
+ "running",
10792
+ "degraded",
10793
+ "failed"
10794
+ ]);
10795
+ /** Client-authored fields. */
10796
+ var CoreBlockInputSchema = zod.z.object({
10797
+ name: zod.z.string().min(1).max(120),
10798
+ /** TypeScript source. Compiled server-side before it is ever stored — a
10799
+ * block that does not compile is a fork failure the operator would meet
10800
+ * minutes later, in a log, instead of in the editor. */
10801
+ code: zod.z.string().max(2e5),
10802
+ enabled: zod.z.boolean(),
10803
+ placement: CoreBlockPlacementSchema,
10804
+ /**
10805
+ * Integration the block's devices hang from. Absent = the system integration
10806
+ * blocks share. A block may declare its own instead.
10807
+ */
10808
+ integrationId: zod.z.string().optional()
10809
+ });
10810
+ /**
10811
+ * What a block IS (D659). Absent means `code` — every row stored before this
10812
+ * union keeps working unchanged.
10813
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
10814
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
10815
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
10816
+ */
10817
+ var CoreBlockSourceSchema = zod.z.discriminatedUnion("kind", [
10818
+ zod.z.object({ kind: zod.z.literal("code") }),
10819
+ zod.z.object({
10820
+ kind: zod.z.literal("recipe"),
10821
+ recipe: AutomationRecipeSchema
10822
+ }),
10823
+ zod.z.object({
10824
+ kind: zod.z.literal("composition"),
10825
+ composition: require_composition.CompositionSchema
10826
+ })
10827
+ ]);
10828
+ /**
10829
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
10830
+ *
10831
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
10832
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
10833
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
10834
+ * client-sent name by name.
10835
+ */
10836
+ var CompositionBlockInputSchema = zod.z.object({
10837
+ name: zod.z.string().min(1).max(120).optional(),
10838
+ enabled: zod.z.boolean(),
10839
+ composition: require_composition.CompositionSchema
10840
+ });
10841
+ /** A stored block. */
10842
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
10843
+ id: zod.z.string(),
10844
+ createdAt: zod.z.number(),
10845
+ updatedAt: zod.z.number(),
10846
+ /** Server-stamped author. */
10847
+ createdBy: zod.z.string(),
10848
+ status: CoreBlockStatusSchema,
10849
+ /**
10850
+ * Why the block is not running, when it is not. The operator's ONLY window
10851
+ * into a block that failed at load — a block that is silently absent is the
10852
+ * failure mode this whole feature has to avoid.
10853
+ */
10854
+ lastError: zod.z.string().optional(),
10855
+ /** Ms epoch of the last state change. */
10856
+ lastChangedAt: zod.z.number(),
10857
+ /** What the block is. Absent means `code` (D659). */
10858
+ source: CoreBlockSourceSchema.optional()
10859
+ });
10860
+ function coreBlockSourceKind(block) {
10861
+ return block.source?.kind ?? "code";
10862
+ }
10863
+ function coreBlockComposition(block) {
10864
+ return block.source?.kind === "composition" ? block.source.composition : null;
10865
+ }
10866
+ /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
10867
+ var CompositionStateReadSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
10868
+ kind: zod.z.literal("answered"),
10869
+ state: CompositionBlockStateSchema.nullable()
10870
+ }), zod.z.object({
10871
+ kind: zod.z.literal("unreachable"),
10872
+ error: zod.z.string()
10873
+ })]);
10874
+ /** A stored composition block and what the composer says about it. */
10875
+ var CompositionViewSchema = zod.z.object({
10876
+ block: CoreBlockSchema,
10877
+ state: CompositionStateReadSchema
10878
+ });
10879
+ /**
10880
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
10881
+ * table has not been READ: an empty answer then would make "not yet known"
10882
+ * look like "none", and invite the operator to recreate what exists (D315).
10883
+ */
10884
+ var CompositionsNotLoadedSchema = zod.z.object({
10885
+ state: zod.z.literal("not-loaded"),
10886
+ reason: zod.z.string()
10887
+ });
10888
+ var CompositionListReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10889
+ state: zod.z.literal("loaded"),
10890
+ views: zod.z.array(CompositionViewSchema)
10891
+ }), CompositionsNotLoadedSchema]);
10892
+ var CompositionGetReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10893
+ state: zod.z.literal("loaded"),
10894
+ view: CompositionViewSchema.nullable()
10895
+ }), CompositionsNotLoadedSchema]);
10896
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
10897
+ var CompositionCustomizationReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10898
+ state: zod.z.literal("loaded"),
10899
+ view: CompositionViewSchema.nullable()
10900
+ }), CompositionsNotLoadedSchema]);
10901
+ /** What a compile attempt produced. */
10902
+ var CoreBlockCompileResultSchema = zod.z.object({
10903
+ ok: zod.z.boolean(),
10904
+ /** Present when `ok` is false — the first error, in the author's words. */
10905
+ error: zod.z.string().optional(),
10906
+ line: zod.z.number().optional(),
10907
+ column: zod.z.number().optional()
10908
+ });
10909
+ var coreBlocksCapability = {
10910
+ name: "core-blocks",
10911
+ scope: "system",
10912
+ mode: "singleton",
10913
+ methods: {
10914
+ list: require_sleep.method(zod.z.object({}), zod.z.object({ blocks: zod.z.array(CoreBlockSchema) }), { auth: "admin" }),
10915
+ get: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
10916
+ /**
10917
+ * Create a block. The code is COMPILED first: storing source that does not
10918
+ * compile turns an editor error into a fork failure the operator meets
10919
+ * minutes later in a log.
10920
+ */
10921
+ create: require_sleep.method(zod.z.object({ block: CoreBlockInputSchema }), zod.z.object({ block: CoreBlockSchema }), {
10922
+ kind: "mutation",
10923
+ auth: "admin",
10924
+ caller: "required"
10925
+ }),
10926
+ update: require_sleep.method(zod.z.object({
10927
+ blockId: zod.z.string(),
10928
+ block: CoreBlockInputSchema.partial()
10929
+ }), zod.z.object({ block: CoreBlockSchema }), {
10930
+ kind: "mutation",
10931
+ auth: "admin",
10932
+ caller: "required"
10933
+ }),
10934
+ delete: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ success: zod.z.literal(true) }), {
10935
+ kind: "mutation",
10936
+ auth: "admin"
10937
+ }),
10938
+ setEnabled: require_sleep.method(zod.z.object({
10939
+ blockId: zod.z.string(),
10940
+ enabled: zod.z.boolean()
10941
+ }), zod.z.object({ block: CoreBlockSchema }), {
10942
+ kind: "mutation",
10943
+ auth: "admin"
10944
+ }),
10945
+ /**
10946
+ * Stop this block's runner and let the supervisor bring it back.
10947
+ *
10948
+ * **It stores nothing.** There is no "restart requested" flag and no second
10949
+ * lifecycle field: the supervisor already stops and respawns a runner when
10950
+ * the code hash changes, and this exposes that same path deliberately
10951
+ * (D62 — a switch writes the authority that already owned the function).
10952
+ * The next reconcile pass remains the authority, so a restart that lost a
10953
+ * race is corrected within one pass rather than leaving a stale flag behind.
10954
+ *
10955
+ * Refused for a DISABLED block: nothing would come back, and a button that
10956
+ * silently does nothing is worse than one that says why.
10957
+ */
10958
+ restart: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema }), {
10959
+ kind: "mutation",
10960
+ auth: "admin"
10961
+ }),
10962
+ /**
10963
+ * Type-check without saving — what the editor calls as the author types, so
10964
+ * the compiler's verdict is the same one the server will reach.
10965
+ */
10966
+ compile: require_sleep.method(zod.z.object({ code: zod.z.string() }), CoreBlockCompileResultSchema, {
10967
+ kind: "mutation",
10968
+ auth: "admin"
10969
+ }),
10970
+ /**
10971
+ * The declaration files the editor type-checks against.
10972
+ *
10973
+ * Served rather than bundled: the graph is 3.4 MB across 344 files, and
10974
+ * bundling it would roughly double the admin remote. Served rather than
10975
+ * hand-stubbed: a block runs with the same `ctx` an addon gets, so a stub
10976
+ * would drift and the editor would confidently autocomplete methods that
10977
+ * do not exist.
10978
+ */
10979
+ getTypeDefs: require_sleep.method(zod.z.object({}), zod.z.object({ libs: zod.z.array(zod.z.object({
10980
+ filePath: zod.z.string(),
10981
+ content: zod.z.string()
10982
+ })) }), { auth: "admin" }),
10983
+ /**
10984
+ * Create a composition block (D659). VALIDATED first against the live
10034
10985
  * sources and every stored composition; a refusal names every problem by
10035
10986
  * path and reason (D391) and stores nothing.
10036
10987
  */
@@ -10065,6 +11016,14 @@ var coreBlocksCapability = {
10065
11016
  stableId: zod.z.string().min(1)
10066
11017
  }), CompositionCustomizationReadSchema, { auth: "admin" }),
10067
11018
  /**
11019
+ * The capabilities a composition may BUILD on a device of `deviceType`
11020
+ * (a new device, or added to an existing one), with their fields — the
11021
+ * editor's feature list and the presets' availability (G2, D676). Built
11022
+ * from the validator's own blockers; a replace of a BOUND cap is not
11023
+ * listed here (the device's bindings say what it has).
11024
+ */
11025
+ listComposableCaps: require_sleep.method(zod.z.object({ deviceType: zod.z.enum(require_err_msg.DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }),
11026
+ /**
10068
11027
  * Validate without saving — the same verdict the save reaches. A mutation
10069
11028
  * for its body size; it writes nothing, so its access is `view`.
10070
11029
  */
@@ -26288,6 +27247,16 @@ var NodeInferenceDevicesSchema = zod.z.object({
26288
27247
  * Strictly the control plane: never sees a frame, never calls inference,
26289
27248
  * never drains queues. The runner does that work locally on each agent.
26290
27249
  */
27250
+ /**
27251
+ * The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
27252
+ * camera's attached sensors (D672). `config: null` removes the attachment —
27253
+ * the camera reads `not-configured` again, never `unreadable`.
27254
+ */
27255
+ var SetCameraSensorsInputSchema = zod.z.object({
27256
+ deviceId: zod.z.number().int().nonnegative(),
27257
+ /** `null` removes the attachment: the camera goes back to `not-configured`. */
27258
+ config: CameraSensorsConfigSchema.nullable()
27259
+ });
26291
27260
  var pipelineOrchestratorCapability = {
26292
27261
  name: "pipeline-orchestrator",
26293
27262
  scope: "system",
@@ -26753,6 +27722,18 @@ var pipelineOrchestratorCapability = {
26753
27722
  */
26754
27723
  getCameraStatuses: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), zod.z.array(CameraStatusSchema).readonly()),
26755
27724
  /**
27725
+ * Attach sensors to a camera, or detach them with `config: null` (D672
27726
+ * configuration, G5/D676). Written to the orchestrator's per-device store
27727
+ * under the same key `applyDeviceSettingsPatch` writes, and announced as a
27728
+ * settings change so the camera-sensors bridge refreshes. Refused by name
27729
+ * — nothing written — for a device that cannot be read, does not exist,
27730
+ * is not a camera, or names itself as its own sensor.
27731
+ */
27732
+ setCameraSensors: require_sleep.method(SetCameraSensorsInputSchema, zod.z.object({ success: zod.z.literal(true) }), {
27733
+ kind: "mutation",
27734
+ auth: "admin"
27735
+ }),
27736
+ /**
26756
27737
  * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
26757
27738
  * the addon process and returns immediately with the job snapshot — the
26758
27739
  * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
@@ -44658,569 +45639,366 @@ function createDeviceExtensionProvider(options) {
44658
45639
  };
44659
45640
  }
44660
45641
  //#endregion
44661
- //#region src/composition/composition-graph.ts
44662
- /**
44663
- * The field graph across every composition, and the cycles in it (D659).
44664
- *
44665
- * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
44666
- * edge points from a composed field to each field it reads. A cycle, direct or
44667
- * through another composition, is refused at save by name (spec §2).
44668
- *
44669
- * The graph is built for ONE save: the CANDIDATE composition (the one being
44670
- * validated) plus every STORED composition. The candidate replaces its own old
44671
- * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
44672
- * introduces or removes a cycle must never depend on array order. A DIFFERENT
44673
- * block targeting the candidate's device is refused by name: one composition
44674
- * per device (D663). Two different stored peers that (wrongly) target the same
44675
- * device are corrupted state, not a cycle to resolve by picking one silently;
44676
- * that is refused by name too (D391).
44677
- *
44678
- * A customization of an EXISTING device reads that device's NATIVE value for a
44679
- * field it replaces or does not write ("Invert state" reads the `on` it
44680
- * replaces): such a self-read is no edge, so it is never a cycle (D663).
44681
- * Another composition reading that field reads the customizer's output.
44682
- *
44683
- * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
44684
- * marks a node `done` after its first expansion) — not every elementary cycle
44685
- * through a shared node, so the path it names may AVOID the candidate while
44686
- * one through it goes unreported. What a save refuses is therefore decided per
44687
- * strongly connected component (`findCandidateCycles`, C-1). A dotted
44688
- * `fieldPath` is matched only as an exact string: a write to `state.value` and
44689
- * a read of `state` would not connect (no nested field writes exist yet).
44690
- *
44691
- * An item leaf is its own node, `…#consumables.items[desiccant].status`
44692
- * (`featureFieldEntries`), so two items sharing an inner path never merge. No
44693
- * READ can name an array element (`describeStateField` never describes one),
44694
- * so an item node is write-only: its edges to sources are what the check needs.
44695
- */
44696
- /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
44697
- var CompositionGraphError = class extends Error {};
44698
- /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
44699
- function writtenFields(peer) {
44700
- return new Set(peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
44701
- }
44702
- /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
44703
- function readsComposedValue(peer, written, read) {
44704
- if (require_composition.compositionSourceKey(read.source) !== require_composition.compositionSourceKey(peer.target)) return true;
44705
- const field = `${read.cap}.${read.fieldPath}`;
44706
- return written.has(field) && peer.replaced?.has(field) !== true;
44707
- }
44708
- function peerEdges(peer) {
44709
- const written = writtenFields(peer);
44710
- return peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath, source }) => [require_composition.compositionReadKey({
44711
- source: peer.target,
44712
- cap: feature.cap,
44713
- fieldPath
44714
- }), require_composition.fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(require_composition.compositionReadKey)]));
44715
- }
44716
- /**
44717
- * Every peer that actually takes part in the graph: each stored peer minus the
44718
- * candidate's own old version (same block id — the drop happens before any
44719
- * edge is built, so its old edges never enter the map), plus the candidate
44720
- * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
44721
- * block targets the candidate's device, or two stored peers share a device.
44722
- */
44723
- function resolvedPeers(candidate, stored) {
44724
- const candidateKey = require_composition.compositionSourceKey(candidate.target);
44725
- const survivors = /* @__PURE__ */ new Map();
44726
- for (const peer of stored) {
44727
- if (peer.blockId === candidate.blockId) continue;
44728
- const key = require_composition.compositionSourceKey(peer.target);
44729
- if (key === candidateKey) throw new CompositionGraphError(`block \`${peer.blockId}\` and block \`${candidate.blockId}\` both target device \`${key}\` — one composition per device; delete or edit the existing one`);
44730
- const clash = survivors.get(key);
44731
- if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
44732
- survivors.set(key, peer);
44733
- }
44734
- return [...survivors.values(), candidate];
44735
- }
44736
- function compositionEdges(candidate, stored) {
44737
- const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
44738
- return new Map(edges);
44739
- }
44740
- function findCompositionCycles(candidate, stored) {
44741
- const edges = compositionEdges(candidate, stored);
44742
- const marks = /* @__PURE__ */ new Map();
44743
- const found = /* @__PURE__ */ new Map();
44744
- const visit = (node, stack) => {
44745
- const mark = marks.get(node);
44746
- if (mark === "done") return;
44747
- if (mark === "visiting") {
44748
- const path = [...stack.slice(stack.indexOf(node)), node];
44749
- const id = [...new Set(path)].toSorted().join("|");
44750
- if (!found.has(id)) found.set(id, { path });
44751
- return;
44752
- }
44753
- marks.set(node, "visiting");
44754
- for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
44755
- marks.set(node, "done");
44756
- };
44757
- for (const node of edges.keys()) visit(node, []);
44758
- return [...found.values()];
44759
- }
44760
- /** Tarjan's strongly connected components over every node of the graph (sources included). */
44761
- function stronglyConnected(edges) {
44762
- const index = /* @__PURE__ */ new Map();
44763
- const low = /* @__PURE__ */ new Map();
44764
- const onStack = /* @__PURE__ */ new Set();
44765
- const stack = [];
44766
- const components = [];
44767
- const connect = (node) => {
44768
- index.set(node, index.size);
44769
- low.set(node, index.get(node) ?? 0);
44770
- stack.push(node);
44771
- onStack.add(node);
44772
- for (const next of edges.get(node) ?? []) if (!index.has(next)) {
44773
- connect(next);
44774
- low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
44775
- } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
44776
- if (low.get(node) !== index.get(node)) return;
44777
- const component = [];
44778
- for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
44779
- onStack.delete(member);
44780
- component.push(member);
44781
- if (member === node) break;
44782
- }
44783
- components.push(component);
44784
- };
44785
- for (const node of edges.keys()) if (!index.has(node)) connect(node);
44786
- return components;
44787
- }
44788
- /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
44789
- function cycleThrough(start, component, edges) {
44790
- const parent = /* @__PURE__ */ new Map();
44791
- const queue = [start];
44792
- for (let head = 0; head < queue.length; head += 1) {
44793
- const node = queue[head];
44794
- if (node === void 0) break;
44795
- for (const next of edges.get(node) ?? []) {
44796
- if (next === start) {
44797
- const chain = [];
44798
- for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
44799
- return [
44800
- start,
44801
- ...chain.reverse(),
44802
- start
44803
- ];
44804
- }
44805
- if (!component.has(next) || parent.has(next)) continue;
44806
- parent.set(next, node);
44807
- queue.push(next);
44808
- }
44809
- }
44810
- return [start, start];
44811
- }
45642
+ //#region src/units/unit-table.ts
44812
45643
  /**
44813
- * The cycles THIS save closes, decided per strongly connected component — never
44814
- * per reported DFS path, which names one back-edge per revisit and can report a
44815
- * component's cycle by a path that avoids the candidate while the one through it
44816
- * goes unreported. A component refuses when it holds a candidate node and a
44817
- * cycle (more than one node, or a self-loop). Components without a candidate
44818
- * node are not this save's cycles.
45644
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
45645
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
45646
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
45647
+ * decimals). K derives from `°C = K − 273.15`.
44819
45648
  */
44820
- function findCandidateCycles(candidate, stored) {
44821
- const edges = compositionEdges(candidate, stored);
44822
- const prefix = `${require_composition.compositionSourceKey(candidate.target)}#`;
44823
- return stronglyConnected(edges).flatMap((members) => {
44824
- const start = members.find((n) => n.startsWith(prefix));
44825
- if (start === void 0) return [];
44826
- const component = new Set(members);
44827
- const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
44828
- if (members.length === 1 && !selfLoop) return [];
44829
- return [{
44830
- path: cycleThrough(start, component, edges),
44831
- component
44832
- }];
44833
- });
44834
- }
44835
- //#endregion
44836
- //#region src/composition/plan-result.ts
44837
- function problem(code, path, message) {
44838
- return {
44839
- code,
44840
- path,
44841
- message
44842
- };
44843
- }
44844
- function refused(...problems) {
44845
- return {
44846
- problems,
44847
- fields: [],
44848
- features: [],
44849
- events: []
44850
- };
44851
- }
44852
- /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
44853
- function compositionFieldAt(field) {
44854
- const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
44855
- return `features[${field.featureIndex}].${inner}.${field.path}`;
44856
- }
44857
- //#endregion
44858
- //#region src/composition/expression-kinds.ts
44859
- var NUMBER = {
44860
- kind: "fixed",
44861
- result: ["number"]
44862
- };
44863
- var BOOLEAN = {
44864
- kind: "fixed",
44865
- result: ["boolean"]
44866
- };
44867
- /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
44868
- var BUILTIN_RESULT_KINDS = Object.freeze({
44869
- min: NUMBER,
44870
- max: NUMBER,
44871
- abs: NUMBER,
44872
- floor: NUMBER,
44873
- ceil: NUMBER,
44874
- sqrt: NUMBER,
44875
- round: NUMBER,
44876
- pow: NUMBER,
44877
- clamp: NUMBER,
44878
- avg: NUMBER,
44879
- sum: NUMBER,
44880
- age: NUMBER,
44881
- convert: NUMBER,
44882
- coalesce: { kind: "coalesce" },
44883
- any: BOOLEAN,
44884
- all: BOOLEAN,
44885
- within: BOOLEAN,
44886
- latest: {
44887
- kind: "fixed",
44888
- result: ["number", "null"]
45649
+ var UNIT_TABLE = {
45650
+ "°C": {
45651
+ dimension: "temperature",
45652
+ factor: 1,
45653
+ offset: 0
45654
+ },
45655
+ "°F": {
45656
+ dimension: "temperature",
45657
+ factor: 5 / 9,
45658
+ offset: -160 / 9
45659
+ },
45660
+ K: {
45661
+ dimension: "temperature",
45662
+ factor: 1,
45663
+ offset: -273.15
45664
+ },
45665
+ hPa: {
45666
+ dimension: "pressure",
45667
+ factor: 1,
45668
+ offset: 0
45669
+ },
45670
+ kPa: {
45671
+ dimension: "pressure",
45672
+ factor: 10,
45673
+ offset: 0
45674
+ },
45675
+ Pa: {
45676
+ dimension: "pressure",
45677
+ factor: .01,
45678
+ offset: 0
45679
+ },
45680
+ mbar: {
45681
+ dimension: "pressure",
45682
+ factor: 1,
45683
+ offset: 0
45684
+ },
45685
+ bar: {
45686
+ dimension: "pressure",
45687
+ factor: 1e3,
45688
+ offset: 0
45689
+ },
45690
+ inHg: {
45691
+ dimension: "pressure",
45692
+ factor: 33.8639,
45693
+ offset: 0
45694
+ },
45695
+ mmHg: {
45696
+ dimension: "pressure",
45697
+ factor: 1.33322,
45698
+ offset: 0
45699
+ },
45700
+ psi: {
45701
+ dimension: "pressure",
45702
+ factor: 68.9476,
45703
+ offset: 0
45704
+ },
45705
+ "m/s": {
45706
+ dimension: "speed",
45707
+ factor: 1,
45708
+ offset: 0
45709
+ },
45710
+ "km/h": {
45711
+ dimension: "speed",
45712
+ factor: 1 / 3.6,
45713
+ offset: 0
45714
+ },
45715
+ mph: {
45716
+ dimension: "speed",
45717
+ factor: .44704,
45718
+ offset: 0
45719
+ },
45720
+ kn: {
45721
+ dimension: "speed",
45722
+ factor: .514444,
45723
+ offset: 0
45724
+ },
45725
+ "mm/h": {
45726
+ dimension: "precipitation-rate",
45727
+ factor: 1,
45728
+ offset: 0
45729
+ },
45730
+ "in/h": {
45731
+ dimension: "precipitation-rate",
45732
+ factor: 25.4,
45733
+ offset: 0
45734
+ },
45735
+ m: {
45736
+ dimension: "length",
45737
+ factor: 1,
45738
+ offset: 0
45739
+ },
45740
+ mm: {
45741
+ dimension: "length",
45742
+ factor: .001,
45743
+ offset: 0
45744
+ },
45745
+ cm: {
45746
+ dimension: "length",
45747
+ factor: .01,
45748
+ offset: 0
45749
+ },
45750
+ km: {
45751
+ dimension: "length",
45752
+ factor: 1e3,
45753
+ offset: 0
44889
45754
  },
44890
- number: {
44891
- kind: "fixed",
44892
- result: ["number", "null"]
45755
+ in: {
45756
+ dimension: "length",
45757
+ factor: .0254,
45758
+ offset: 0
44893
45759
  },
44894
- rose: {
44895
- kind: "fixed",
44896
- result: ["number", "null"]
45760
+ ft: {
45761
+ dimension: "length",
45762
+ factor: .3048,
45763
+ offset: 0
44897
45764
  },
44898
- count: NUMBER
44899
- });
44900
- var KindInferenceError = class extends Error {};
44901
- function kindOfValue(value) {
44902
- if (value === null) return "null";
44903
- if (typeof value === "number") return "number";
44904
- if (typeof value === "string") return "string";
44905
- return "boolean";
44906
- }
44907
- function only(kind) {
44908
- return new Set([kind]);
44909
- }
44910
- function union(...sets) {
44911
- return new Set(sets.flatMap((s) => [...s]));
44912
- }
44913
- function withoutNull(set) {
44914
- return new Set([...set].filter((k) => k !== "null"));
44915
- }
44916
- function binaryKinds(op, left, right) {
44917
- if (op === "+") {
44918
- const all = [...left, ...right];
44919
- if (all.every((k) => k === "string")) return only("string");
44920
- if (all.every((k) => k === "number")) return only("number");
44921
- return new Set(["number", "string"]);
45765
+ mi: {
45766
+ dimension: "length",
45767
+ factor: 1609.344,
45768
+ offset: 0
45769
+ },
45770
+ lx: {
45771
+ dimension: "illuminance",
45772
+ factor: 1,
45773
+ offset: 0
45774
+ },
45775
+ "W/m²": {
45776
+ dimension: "irradiance",
45777
+ factor: 1,
45778
+ offset: 0
45779
+ },
45780
+ W: {
45781
+ dimension: "power",
45782
+ factor: 1,
45783
+ offset: 0
45784
+ },
45785
+ kW: {
45786
+ dimension: "power",
45787
+ factor: 1e3,
45788
+ offset: 0
45789
+ },
45790
+ VA: {
45791
+ dimension: "apparent-power",
45792
+ factor: 1,
45793
+ offset: 0
45794
+ },
45795
+ Wh: {
45796
+ dimension: "energy",
45797
+ factor: 1,
45798
+ offset: 0
45799
+ },
45800
+ kWh: {
45801
+ dimension: "energy",
45802
+ factor: 1e3,
45803
+ offset: 0
45804
+ },
45805
+ MWh: {
45806
+ dimension: "energy",
45807
+ factor: 1e6,
45808
+ offset: 0
45809
+ },
45810
+ V: {
45811
+ dimension: "voltage",
45812
+ factor: 1,
45813
+ offset: 0
45814
+ },
45815
+ mV: {
45816
+ dimension: "voltage",
45817
+ factor: .001,
45818
+ offset: 0
45819
+ },
45820
+ A: {
45821
+ dimension: "current",
45822
+ factor: 1,
45823
+ offset: 0
45824
+ },
45825
+ mA: {
45826
+ dimension: "current",
45827
+ factor: .001,
45828
+ offset: 0
45829
+ },
45830
+ ppm: {
45831
+ dimension: "concentration-volume",
45832
+ factor: 1,
45833
+ offset: 0
45834
+ },
45835
+ ppb: {
45836
+ dimension: "concentration-volume",
45837
+ factor: .001,
45838
+ offset: 0
45839
+ },
45840
+ "µg/m³": {
45841
+ dimension: "concentration-mass",
45842
+ factor: 1,
45843
+ offset: 0
45844
+ },
45845
+ "mg/m³": {
45846
+ dimension: "concentration-mass",
45847
+ factor: 1e3,
45848
+ offset: 0
45849
+ },
45850
+ kg: {
45851
+ dimension: "mass",
45852
+ factor: 1,
45853
+ offset: 0
45854
+ },
45855
+ g: {
45856
+ dimension: "mass",
45857
+ factor: .001,
45858
+ offset: 0
45859
+ },
45860
+ lb: {
45861
+ dimension: "mass",
45862
+ factor: .453592,
45863
+ offset: 0
45864
+ },
45865
+ oz: {
45866
+ dimension: "mass",
45867
+ factor: .0283495,
45868
+ offset: 0
45869
+ },
45870
+ "%": {
45871
+ dimension: "percentage",
45872
+ factor: 1,
45873
+ offset: 0
44922
45874
  }
44923
- if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
44924
- return only("boolean");
44925
- }
44926
- /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
44927
- function arityRange(builtin) {
44928
- if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
44929
- if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
44930
- return `${builtin.minArgs}–${builtin.maxArgs}`;
44931
- }
44932
- function callKinds(callee, args) {
44933
- const builtin = require_composition.EXPRESSION_BUILTINS[callee];
44934
- 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)}`);
44935
- const rule = BUILTIN_RESULT_KINDS[callee];
44936
- if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
44937
- if (rule.kind === "fixed") return new Set(rule.result);
44938
- const nonNull = union(...args.map(withoutNull));
44939
- return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
44940
- }
44941
- function kindsOf(node, bindings) {
44942
- switch (node.kind) {
44943
- case "literal": return only(kindOfValue(node.value));
44944
- case "identifier": {
44945
- if (node.name === "now") return only("number");
44946
- const bound = bindings.get(node.name);
44947
- if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
44948
- return bound;
44949
- }
44950
- case "unary": return only(node.op === "!" ? "boolean" : "number");
44951
- case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
44952
- case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
44953
- case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
44954
- case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
45875
+ };
45876
+ //#endregion
45877
+ //#region src/units/convert.ts
45878
+ /**
45879
+ * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
45880
+ *
45881
+ * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
45882
+ * from the `try*` variant) whenever a unit is unknown or the two units belong
45883
+ * to different dimensions — cross-dimension conversion is deliberately
45884
+ * impossible so a render override can never make the value and its unit label
45885
+ * disagree.
45886
+ *
45887
+ * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
45888
+ * calling here — the table keys only canonical spellings.
45889
+ */
45890
+ /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
45891
+ * units + a machine-readable `reason` for callers that branch on the cause. */
45892
+ var UnitConversionError = class extends Error {
45893
+ from;
45894
+ to;
45895
+ reason;
45896
+ constructor(from, to, reason) {
45897
+ super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
45898
+ this.name = "UnitConversionError";
45899
+ this.from = from;
45900
+ this.to = to;
45901
+ this.reason = reason;
44955
45902
  }
45903
+ };
45904
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
45905
+ function specFor(unit) {
45906
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
44956
45907
  }
44957
- function nodeContainsCall(node, callee) {
44958
- switch (node.kind) {
44959
- case "literal":
44960
- case "identifier": return false;
44961
- case "unary": return nodeContainsCall(node.operand, callee);
44962
- case "binary":
44963
- case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
44964
- case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
44965
- case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
44966
- }
45908
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
45909
+ function unitDimension(unit) {
45910
+ return specFor(unit)?.dimension;
44967
45911
  }
44968
- function expressionContainsBuiltinCall(expr, callee) {
44969
- const compiled = require_composition.compileExpressionSafe(expr);
44970
- return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
45912
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
45913
+ function toCanonical(value, spec) {
45914
+ return value * spec.factor + spec.offset;
44971
45915
  }
44972
- function inferExpressionKinds(input) {
44973
- const compiled = require_composition.compileExpressionSafe(input.expr);
44974
- if (!compiled.ok) return {
44975
- ok: false,
44976
- error: compiled.error
44977
- };
44978
- try {
44979
- return {
44980
- ok: true,
44981
- kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
44982
- };
44983
- } catch (err) {
44984
- if (err instanceof KindInferenceError) return {
44985
- ok: false,
44986
- error: err.message
44987
- };
44988
- throw err;
44989
- }
45916
+ /** Canonical-unit value → the unit described by `spec`. */
45917
+ function fromCanonical(canonical, spec) {
45918
+ return (canonical - spec.offset) / spec.factor;
44990
45919
  }
44991
- //#endregion
44992
- //#region src/composition/field-kinds.ts
44993
45920
  /**
44994
- * The kind half of planning a composition field (D659): which kinds a source
44995
- * can produce, whether they fit a target field, and the field's role.
44996
- * Shared by the top-level planner (`validate-composition.ts`) and the item
44997
- * planner (`composition-items.ts`). Internal to the composition module — NOT
44998
- * in the barrel: these are planner internals, not a published contract.
45921
+ * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
45922
+ * Throws `UnitConversionError` when either unit is unknown or the units belong
45923
+ * to different dimensions.
44999
45924
  */
45000
- function readKinds(read, lookupCap) {
45001
- const def = lookupCap(read.cap);
45002
- const schema = def?.runtimeState ?? def?.status?.schema;
45003
- if (def === null || schema === void 0) return {
45004
- ok: false,
45005
- code: "source-field-unknown",
45006
- message: `no capability \`${read.cap}\` with state to read`
45007
- };
45008
- const field = describeStateField(schema, read.fieldPath);
45009
- if (field === null) return {
45010
- ok: false,
45011
- code: "source-field-unknown",
45012
- message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
45013
- };
45014
- const base = field.kind === "enum" ? "string" : field.kind;
45015
- return {
45016
- ok: true,
45017
- kinds: new Set(field.nullable ? [base, "null"] : [base])
45018
- };
45019
- }
45020
- function sourceKinds(source, lookupCap) {
45021
- if (source.kind === "from") return readKinds(source, lookupCap);
45022
- if (source.kind === "code") return {
45023
- ok: false,
45024
- code: "reserved-variant",
45025
- message: "code fields arrive in slice 4"
45026
- };
45027
- const bindingKinds = /* @__PURE__ */ new Map();
45028
- for (const [name, binding] of Object.entries(source.bindings)) {
45029
- if (binding.kind === "literal") {
45030
- bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
45031
- continue;
45032
- }
45033
- const kinds = readKinds(binding, lookupCap);
45034
- if (!kinds.ok) return {
45035
- ...kinds,
45036
- message: `binding \`${name}\`: ${kinds.message}`
45037
- };
45038
- bindingKinds.set(name, kinds.kinds);
45039
- }
45040
- const inferred = inferExpressionKinds({
45041
- expr: source.expr,
45042
- bindingKinds
45043
- });
45044
- return inferred.ok ? {
45045
- ok: true,
45046
- kinds: inferred.kinds
45047
- } : {
45048
- ok: false,
45049
- code: "expression-invalid",
45050
- message: inferred.error
45051
- };
45052
- }
45053
- function compatibility(cap, target, kinds, at) {
45054
- const allowed = target.kind === "enum" ? "string" : target.kind;
45055
- const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
45056
- const problems = [];
45057
- if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
45058
- 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`));
45059
- return problems;
45925
+ function convertUnit(value, from, to) {
45926
+ if (from === to) return value;
45927
+ const fromSpec = specFor(from);
45928
+ if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
45929
+ const toSpec = specFor(to);
45930
+ if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
45931
+ if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
45932
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45060
45933
  }
45061
45934
  /**
45062
- * A string a source produces WITHOUT reading any device — a field-level
45063
- * `expression` source whose compiled AST is a bare string literal (`"red"`,
45064
- * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
45065
- * the kind level (a `from` read of an enum-typed source field, a computed
45066
- * expression) is NOT checked against the target's `enumValues` here: the
45067
- * kind inference already collapsed it to the coarse `'string'` kind, losing
45068
- * whichever enum it actually came from, so membership can only be judged at
45069
- * write time against the runtime-state schema. Known limitation — see the
45070
- * fix report for Task 6 round 1.
45935
+ * Graceful variant for the render path: returns the converted value, or
45936
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
45937
+ * Never throws — a render caller falls back to the raw value + source unit.
45071
45938
  */
45072
- function literalStringValue(source) {
45073
- if (source.kind !== "expression") return null;
45074
- const compiled = require_composition.compileExpressionSafe(source.expr);
45075
- if (!compiled.ok) return null;
45076
- const ast = compiled.parsed.ast;
45077
- return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
45939
+ function tryConvertUnit(value, from, to) {
45940
+ if (from === to) return value;
45941
+ const fromSpec = specFor(from);
45942
+ if (!fromSpec) return void 0;
45943
+ const toSpec = specFor(to);
45944
+ if (!toSpec) return void 0;
45945
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
45946
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45078
45947
  }
45079
45948
  /**
45080
- * An enum target whose source is a save-time-KNOWN string literal outside its
45081
- * `enumValues` is refused now rather than left to fail the write later. A
45082
- * dynamic source (a `from` read, a non-literal expression) is left to
45083
- * runtime — see {@link literalStringValue}.
45949
+ * Whether `value` can be converted between `from` and `to` (same dimension,
45950
+ * both known). Cheap boolean probe for the UI's same-dimension unit picker and
45951
+ * the resolver's convertibility branch — compute ONCE so a unit label and its
45952
+ * value math can never disagree.
45084
45953
  */
45085
- function enumLiteralProblem(cap, target, source, at) {
45086
- if (target.kind !== "enum" || target.enumValues === void 0) return null;
45087
- const value = literalStringValue(source);
45088
- if (value === null || target.enumValues.includes(value)) return null;
45089
- const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
45090
- return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
45091
- }
45092
- function roleOf(source) {
45093
- if (source.kind !== "expression") return "derived";
45094
- if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
45095
- if (require_composition.fieldReads(source).length > 0) return "derived";
45096
- const compiled = require_composition.compileExpressionSafe(source.expr);
45097
- return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
45954
+ function canConvertUnit(from, to) {
45955
+ if (from === to) return true;
45956
+ const fromDim = unitDimension(from);
45957
+ if (fromDim === void 0) return false;
45958
+ return fromDim === unitDimension(to);
45098
45959
  }
45099
45960
  /**
45100
- * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
45101
- * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
45102
- * own offline, and D393 forbids a stale number, so on an existing target a
45103
- * field that READS a source never gets `offline`.
45961
+ * Every known unit spelling in a dimension (table insertion order — canonical
45962
+ * unit first). Powers the UI's same-dimension unit picker.
45104
45963
  */
45105
- function unavailableFor(tf, role, mode, cap, at) {
45106
- if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
45107
- ok: true,
45108
- unavailable: "release"
45109
- };
45110
- if (mode === "new" || role !== "derived") return {
45111
- ok: true,
45112
- unavailable: tf.nullable ? "null" : "offline"
45113
- };
45114
- if (tf.nullable) return {
45115
- ok: true,
45116
- unavailable: "null"
45117
- };
45118
- if (tf.enumValues?.includes("unknown") === true) return {
45119
- ok: true,
45120
- unavailable: "unknown"
45121
- };
45122
- return {
45123
- ok: false,
45124
- problem: problem("not-replaceable", at, `\`${cap}.${tf.path}\` reads a source but can say neither null nor 'unknown', and an ADDED capability has no native reading to fall back to (D393, D663)`)
45125
- };
45126
- }
45127
- //#endregion
45128
- //#region src/composition/composition-items.ts
45129
- /** Item fields the composer writes itself, with the value it writes. */
45130
- var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
45131
- function reservedItemFields(itemArray) {
45132
- return new Set([
45133
- itemArray.keyField,
45134
- ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
45135
- ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
45136
- ]);
45137
- }
45138
- /** A required leaf under an optional parent is required only once something writes that parent. */
45139
- function leafRequired(itemArray, path, written) {
45140
- const described = describeStateField(itemArray.itemSchema, path);
45141
- if (described === null || !described.required) return false;
45142
- return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
45143
- }
45144
- function missingLeaves({ capName, itemArray, reserved }, item, at) {
45145
- const written = Object.keys(item.fields);
45146
- return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
45147
- if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
45148
- if (!leafRequired(itemArray, leaf.path, written)) return [];
45149
- const described = describeStateField(itemArray.itemSchema, leaf.path);
45150
- if (described === null || described.nullable) return [];
45151
- return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
45152
- });
45153
- }
45154
- function planItemField(ctx, leaf) {
45155
- const { capName, itemArray, featureIndex, lookupCap } = ctx;
45156
- const { key, label, path, source } = leaf;
45157
- const at = `features[${featureIndex}].items.${key}.fields.${path}`;
45158
- if (ctx.reserved.has(path)) return refused(problem("item-field-reserved", at, `\`${capName}.${itemArray.path}[].${path}\` is set by the composer itself — it takes no source`));
45159
- const tf = describeStateField(itemArray.itemSchema, path);
45160
- if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
45161
- const kinds = sourceKinds(source, lookupCap);
45162
- if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
45163
- const role = roleOf(source);
45164
- const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
45165
- if (!unavailable.ok) return refused(unavailable.problem);
45166
- const plan = {
45167
- cap: capName,
45168
- path,
45169
- item: {
45170
- arrayPath: itemArray.path,
45171
- key,
45172
- label
45173
- },
45174
- featureIndex,
45175
- kind: tf.kind,
45176
- nullable: tf.nullable,
45177
- required: tf.required,
45178
- unavailable: unavailable.unavailable,
45179
- role,
45180
- source,
45181
- reads: [...require_composition.fieldReads(source)]
45182
- };
45183
- const enumProblem = enumLiteralProblem(capName, tf, source, at);
45184
- return {
45185
- problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
45186
- fields: [plan],
45187
- features: [],
45188
- events: []
45189
- };
45190
- }
45191
- function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
45192
- const at = `features[${featureIndex}].items`;
45193
- const itemArray = def.status?.itemArray;
45194
- if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
45195
- if (itemArray.path !== "items") return refused(problem("not-item-array", at, `\`${def.name}\`'s item array is at \`${itemArray.path}\`, not \`${require_composition.COMPOSITION_ITEM_ARRAY_PATH}\` — not composable yet`));
45196
- const ctx = {
45197
- capName: def.name,
45198
- itemArray,
45199
- reserved: reservedItemFields(itemArray),
45200
- featureIndex,
45201
- lookupCap,
45202
- mode
45203
- };
45204
- const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
45205
- problems: missingLeaves(ctx, item, `${at}.${key}`),
45206
- fields: [],
45207
- features: [],
45208
- events: []
45209
- }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
45210
- key,
45211
- label: item.label,
45212
- path,
45213
- source
45214
- }))]);
45215
- return {
45216
- problems: results.flatMap((r) => r.problems),
45217
- fields: results.flatMap((r) => r.fields),
45218
- features: [],
45219
- events: []
45220
- };
45964
+ function unitsForDimension(dimension) {
45965
+ const out = [];
45966
+ for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
45967
+ return out;
45221
45968
  }
45222
45969
  //#endregion
45223
45970
  //#region src/composition/evaluate-field.ts
45971
+ /**
45972
+ * One composed field, evaluated from what its sources say NOW (D659).
45973
+ *
45974
+ * Pure. The composer calls it on every source change, and the slice-5 editor
45975
+ * will call it for its live preview, so both see the same answer.
45976
+ *
45977
+ * The rules (spec §2):
45978
+ * - a source that is offline or unreadable keeps its last value and is marked
45979
+ * STALE; the field is stale and keeps computing (D49);
45980
+ * - a source that is GONE makes the field UNAVAILABLE, `fatal`, and never a
45981
+ * plausible number (D393);
45982
+ * - a source that is unavailable but NOT gone (e.g. a cap unregistered at
45983
+ * runtime, R7b) makes the field UNAVAILABLE, non-fatal, `value: null`: it
45984
+ * may come back on its own, no block failure is raised for it;
45985
+ * - an expression that fails on a value makes the field unavailable for that
45986
+ * value, and NOT fatal: the next value may compute.
45987
+ *
45988
+ * `confirmed` (D663, D49): whether a NON-fatal unavailability is an ANSWER — a
45989
+ * cap the provider unregistered and a re-read agreed, a field the source
45990
+ * reported without, an expression that failed on a value — or only the
45991
+ * absence of one (a failed read, a first or empty read, a source not tracked
45992
+ * yet). Only a confirmed unavailability may hand a `release`-mode field back
45993
+ * to the native provider; an unconfirmed one holds it on its last value.
45994
+ * Absent means NOT confirmed. Fatal is always confirmed.
45995
+ *
45996
+ * When an expression has more than one unavailable binding, a FATAL one always
45997
+ * wins the outcome, regardless of position: every binding is scanned before
45998
+ * the field is judged, so an earlier non-fatal binding (e.g. a cap that is
45999
+ * momentarily unregistered) can never hide a later fatal one (a deleted
46000
+ * source) behind an early return.
46001
+ */
45224
46002
  function describeRead(read) {
45225
46003
  return `${require_composition.compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
45226
46004
  }
@@ -45287,7 +46065,10 @@ function expressionOutcome(source, read, now, stateful) {
45287
46065
  }
45288
46066
  const deadlines = [];
45289
46067
  const result = require_composition.evaluateExpressionSource(source.expr, values, now, {
45290
- hooks: { noteDeadline: (at) => deadlines.push(at) },
46068
+ hooks: {
46069
+ noteDeadline: (at) => deadlines.push(at),
46070
+ convert: (value, from, to) => tryConvertUnit(value, normalizeUnit(from) ?? from, normalizeUnit(to) ?? to) ?? null
46071
+ },
45291
46072
  ...stateful === void 0 ? {} : { invokeStateful: (invocation) => require_composition.evaluateStatefulExpressionCall(stateful.callPathPrefix === void 0 ? invocation : {
45292
46073
  ...invocation,
45293
46074
  path: `${stateful.callPathPrefix}@${invocation.path}`
@@ -45526,304 +46307,71 @@ function feederCustomizationExample(sources) {
45526
46307
  });
45527
46308
  }
45528
46309
  //#endregion
45529
- //#region src/composition/validate-composition.ts
45530
- function buildCapabilityLookup(defs) {
45531
- const byName = new Map(defs.map((d) => [d.name, d]));
45532
- return (capName) => byName.get(capName) ?? null;
45533
- }
45534
- /** Caps every `BaseDevice` registers on itself; a second registration throws. */
45535
- var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
45536
- var PENDING_SELF = {
45537
- addonId: require_composition.COMPOSER_ADDON_ID,
45538
- stableId: `${require_composition.COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
45539
- };
45540
- /** The candidate's block id before its first save. Stored block ids are `randomUUID()`, so none can carry it. */
45541
- var PENDING_BLOCK_ID = "(unsaved)";
45542
- function composableCap(feature, at, lookupCap) {
45543
- const def = lookupCap(feature.cap);
45544
- if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
45545
- if (BASE_DEVICE_CAPS.has(def.name)) return problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`);
45546
- const state = def.runtimeState;
45547
- if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
45548
- return {
45549
- def,
45550
- state,
45551
- fields: describeTopLevelStateFields(state)
45552
- };
45553
- }
45554
- /**
45555
- * A capability BUILT by the composition (a new device, or one ADDED to an
45556
- * existing device) must fit the device type, serve no command, and have every
45557
- * required field. A REPLACE needs none of it: the cap is bound (a fact), the
45558
- * native keeps serving its methods, and only the named fields are owned (D663).
45559
- */
45560
- function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
45561
- const problems = [];
45562
- if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
45563
- const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
45564
- if (served.length > 0) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
45565
- const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
45566
- for (const key of requiredTopLevelKeys(state)) {
45567
- if (key === itemArrayPath) continue;
45568
- if (!targetFields.has(key)) problems.push(problem("not-composable", `${at}.fields`, `\`${def.name}.${key}\` is not a scalar — nested fields arrive in a later slice`));
45569
- else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
45570
- }
45571
- return problems;
45572
- }
45573
- function planTopLevelField({ def, fields }, field, mode, lookupCap) {
45574
- const { featureIndex, path, source } = field;
45575
- const fieldAt = `features[${featureIndex}].fields.${path}`;
45576
- if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
45577
- const tf = fields.get(path);
45578
- if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
45579
- const kinds = sourceKinds(source, lookupCap);
45580
- if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
45581
- const role = roleOf(source);
45582
- const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
45583
- if (!unavailable.ok) return refused(unavailable.problem);
45584
- const plan = {
45585
- cap: def.name,
45586
- path,
45587
- featureIndex,
45588
- kind: tf.kind,
45589
- nullable: tf.nullable,
45590
- required: tf.required,
45591
- unavailable: unavailable.unavailable,
45592
- role,
45593
- source,
45594
- reads: [...require_composition.fieldReads(source)]
45595
- };
45596
- const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
45597
- return {
45598
- problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
45599
- fields: [plan],
45600
- features: [],
45601
- events: []
45602
- };
45603
- }
45604
- function planFeature(feature, featureIndex, target, lookupCap) {
45605
- const at = `features[${featureIndex}]`;
45606
- const cap = composableCap(feature, at, lookupCap);
45607
- if ("code" in cap) return refused(cap);
45608
- const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
45609
- if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
45610
- const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
45611
- featureIndex,
45612
- path,
45613
- source
45614
- }, target.mode, lookupCap));
45615
- const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
45616
- const all = [...perField, ...items];
45617
- const fields = all.flatMap((result) => result.fields);
45618
- const featurePlan = {
45619
- featureIndex,
45620
- cap: cap.def.name,
45621
- mode: target.mode
45622
- };
45623
- const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
45624
- return {
45625
- problems: [
45626
- ...capProblems,
45627
- ...all.flatMap((r) => r.problems),
45628
- ...eventPlan.problems
45629
- ],
45630
- fields,
45631
- features: [featurePlan],
45632
- events: eventPlan.events
45633
- };
45634
- }
45635
- /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
45636
- function isNativeCap(target, cap) {
45637
- const owner = target.nativeCaps.get(cap);
45638
- return owner !== void 0 && owner !== "composer";
45639
- }
45640
- /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
45641
- function planTargetOf(composition, target) {
45642
- if (composition.target.kind === "new") return {
45643
- kind: "new",
45644
- type: composition.target.type
45645
- };
45646
- const key = require_composition.compositionSourceKey(composition.target.device);
45647
- if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
45648
- if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
45649
- if (target.kind === "unreadable") return problem("target-unreadable", "target", `could not read target device \`${key}\` (${target.error}) — the save is refused rather than stored unchecked`);
45650
- return {
45651
- kind: "existing",
45652
- found: target
45653
- };
45654
- }
45655
- function featureTargetOf(target, cap) {
45656
- if (target.kind === "new") return {
45657
- mode: "new",
45658
- deviceType: target.type
45659
- };
45660
- return {
45661
- mode: isNativeCap(target.found, cap) ? "replace" : "add",
45662
- deviceType: target.found.type
45663
- };
46310
+ //#region src/composition/presets/doorbell-feature.ts
46311
+ /**
46312
+ * The doorbell of a camera as a customization (D671). ONE builder: the
46313
+ * virtual-doorbell migration and the admin UI's "Use as doorbell" preset both
46314
+ * call it, so a preset can never write a shape the migration would refuse as
46315
+ * "an unrelated existing block".
46316
+ */
46317
+ /** A rise closer than this to the previous one is the same press (D671). */
46318
+ var DOORBELL_RISE_DEBOUNCE_MS = 2e3;
46319
+ /** Every source cap of `source` that `SOURCE_CAP_ACTIVE_FIELD` knows AND the device has bound. */
46320
+ function doorbellFeatureSources(source, boundCaps) {
46321
+ return Object.keys(require_composition.SOURCE_CAP_ACTIVE_FIELD).filter((cap) => Object.hasOwn(require_composition.SOURCE_CAP_CHANGED_AT_FIELD, cap) && boundCaps.includes(cap)).toSorted().flatMap((cap) => {
46322
+ const activeField = require_composition.SOURCE_CAP_ACTIVE_FIELD[cap];
46323
+ const changedAtField = require_composition.SOURCE_CAP_CHANGED_AT_FIELD[cap];
46324
+ return activeField === void 0 || changedAtField === void 0 ? [] : [{
46325
+ source,
46326
+ cap,
46327
+ activeField,
46328
+ changedAtField
46329
+ }];
46330
+ });
45664
46331
  }
45665
- /**
45666
- * The static plan. `target` describes an EXISTING target device (one read);
45667
- * it is ignored for a new one. On an existing target each feature REPLACES a
45668
- * cap the device has natively, else ADDS it (D663).
45669
- */
45670
- function planComposition(composition, lookupCap, target) {
45671
- const planTarget = planTargetOf(composition, target);
45672
- if ("code" in planTarget) return refused(planTarget);
45673
- const results = composition.features.map((feature, index) => {
45674
- const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
45675
- return require_composition.compositionFeatureEnabled(feature) ? result : {
45676
- problems: result.problems,
45677
- fields: [],
45678
- features: [],
45679
- events: []
46332
+ /** The D671 feature, byte for byte what the migration writes. Throws on zero sources. */
46333
+ function buildDoorbellCompositionFeature(input) {
46334
+ const sources = [...input.sources].toSorted((a, b) => `${a.source.addonId}/${a.source.stableId}#${a.cap}`.localeCompare(`${b.source.addonId}/${b.source.stableId}#${b.cap}`));
46335
+ if (sources.length === 0) throw new Error("doorbell composition needs at least one source");
46336
+ const edgeBindings = {};
46337
+ sources.forEach((entry, index) => {
46338
+ edgeBindings[`active${index}`] = {
46339
+ kind: "from",
46340
+ source: entry.source,
46341
+ cap: entry.cap,
46342
+ fieldPath: entry.activeField
46343
+ };
46344
+ edgeBindings[`changed${index}`] = {
46345
+ kind: "from",
46346
+ source: entry.source,
46347
+ cap: entry.cap,
46348
+ fieldPath: entry.changedAtField
45680
46349
  };
45681
46350
  });
45682
46351
  return {
45683
- problems: results.flatMap((r) => r.problems),
45684
- fields: results.flatMap((r) => r.fields),
45685
- features: results.flatMap((r) => r.features),
45686
- events: results.flatMap((r) => r.events)
45687
- };
45688
- }
45689
- function compositionSourceRefs(composition) {
45690
- const refs = composition.features.flatMap((f) => require_composition.compositionFeatureEnabled(f) && f.kind === "fields" ? require_composition.featureSources(f).flatMap(require_composition.fieldReads).map((r) => r.source) : []);
45691
- return [...new Map(refs.map((r) => [require_composition.compositionSourceKey(r), r])).values()];
45692
- }
45693
- /**
45694
- * The candidate's device: the existing target itself, else `self`, else a
45695
- * placeholder that cannot collide with a stored target. An existing target is
45696
- * known even on create, so a second customization of it is refused on create too.
45697
- */
45698
- function candidateSelf(composition, ctx, plan) {
45699
- return {
45700
- ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
45701
- native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
45702
- replaced: replacedFieldKeys(plan)
45703
- };
45704
- }
45705
- /**
45706
- * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
45707
- * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
45708
- * derivation, so the graph reads both the same way.
45709
- */
45710
- function replacedFieldKeys(plan) {
45711
- const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
45712
- return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
45713
- }
45714
- /**
45715
- * A read of the candidate's own device. Its own composed output (a new device's
45716
- * field, an ADDED field) is the slice-1 own-field rule; on an existing target
45717
- * every other field — replaced, or not composed at all — is the NATIVE value,
45718
- * valid when the device serves that cap natively (D663, rule 5).
45719
- */
45720
- function selfReadProblems(read, at, fields, self) {
45721
- const readKey = `${read.cap}.${read.fieldPath}`;
45722
- if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
45723
- if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
45724
- if (isNativeCap(self.native, read.cap)) return [];
45725
- return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
45726
- }
45727
- function sourceProblems(fields, ctx, self) {
45728
- const selfKey = require_composition.compositionSourceKey(self.ref);
45729
- return fields.flatMap((field) => field.reads.flatMap((read) => {
45730
- const at = compositionFieldAt(field);
45731
- const key = require_composition.compositionSourceKey(read.source);
45732
- if (key === selfKey) return selfReadProblems(read, at, fields, self);
45733
- const desc = ctx.sources.get(key);
45734
- 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`)];
45735
- if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
45736
- if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
45737
- return [];
45738
- }));
45739
- }
45740
- /**
45741
- * A cycle's node keys are `compositionReadKey` strings:
45742
- * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
45743
- * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
45744
- * back out so the refusal can point at that exact field, matching the
45745
- * documented `features[N].fields.<path>` convention. A cap name has no `.`,
45746
- * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
45747
- * (`items[desiccant].status`) is matched through `compositionFieldKey`.
45748
- */
45749
- function candidateFieldOfNode(nodeKey, candidateKey) {
45750
- const prefix = `${candidateKey}#`;
45751
- if (!nodeKey.startsWith(prefix)) return null;
45752
- const rest = nodeKey.slice(prefix.length);
45753
- const dot = rest.indexOf(".");
45754
- if (dot === -1) return null;
45755
- return {
45756
- cap: rest.slice(0, dot),
45757
- fieldPath: rest.slice(dot + 1)
45758
- };
45759
- }
45760
- /** The first node of a cycle's path that is one of THIS composition's own
45761
- * planned fields, as a documented-convention path; `'features'` when the
45762
- * cycle runs entirely through other stored compositions. */
45763
- function cyclePath(nodePath, candidateKey, fields) {
45764
- for (const nodeKey of nodePath) {
45765
- const hit = candidateFieldOfNode(nodeKey, candidateKey);
45766
- if (hit === null) continue;
45767
- const hitKey = `${hit.cap}.${hit.fieldPath}`;
45768
- const field = fields.find((f) => compositionFieldKey(f) === hitKey);
45769
- if (field !== void 0) return compositionFieldAt(field);
45770
- }
45771
- return "features";
45772
- }
45773
- /** The first unread device any of `nodes` belongs to; null when none does. */
45774
- function unreadDeviceOf(nodes, unread) {
45775
- for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
45776
- key,
45777
- error
45778
- };
45779
- return null;
45780
- }
45781
- /**
45782
- * Cycle check runs against the CANDIDATE (this composition, as block
45783
- * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
45784
- * decided per strongly connected component (`findCandidateCycles`): a cyclic
45785
- * component holding a candidate node refuses, with one representative cycle
45786
- * through it; one without is not this save's cycle. A
45787
- * stored composition of the same block is its old version; another block on
45788
- * the same device, or two stored peers sharing a device, make
45789
- * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
45790
- * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
45791
- */
45792
- function cycleProblems(composition, ctx, fields, self) {
45793
- const candidate = {
45794
- blockId: ctx.blockId ?? "(unsaved)",
45795
- target: self.ref,
45796
- composition,
45797
- replaced: self.replaced
45798
- };
45799
- const candidateKey = require_composition.compositionSourceKey(candidate.target);
45800
- const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
45801
- try {
45802
- return findCandidateCycles(candidate, ctx.peers).map((c) => {
45803
- const at = cyclePath(c.path, candidateKey, fields);
45804
- const through = unreadDeviceOf([...c.component], unread);
45805
- return through === null ? problem("cycle", at, `a field reads itself: ${c.path.join(" → ")}`) : problem("cycle", at, `possible cycle through \`${through.key}\` — its state could not be read (${through.error}): ${c.path.join(" → ")}`);
45806
- });
45807
- } catch (err) {
45808
- if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
45809
- throw err;
45810
- }
45811
- }
45812
- function validateComposition(composition, ctx) {
45813
- const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
45814
- const self = candidateSelf(composition, ctx, plan);
45815
- const cycles = cycleProblems(composition, ctx, plan.fields, self);
45816
- const problems = [
45817
- ...plan.problems,
45818
- ...sourceProblems(plan.fields, ctx, self),
45819
- ...cycles
45820
- ];
45821
- return {
45822
- ok: problems.length === 0,
45823
- problems,
45824
- fields: [...plan.fields],
45825
- features: [...plan.features],
45826
- events: [...plan.events]
46352
+ kind: "fields",
46353
+ cap: "doorbell",
46354
+ enabled: input.enabled,
46355
+ fields: {
46356
+ lastPressedAt: {
46357
+ kind: "expression",
46358
+ expr: `latest(${sources.map((_, index) => `rose(active${index}, changed${index}, ${DOORBELL_RISE_DEBOUNCE_MS})`).join(", ")})`,
46359
+ bindings: edgeBindings
46360
+ },
46361
+ pressCountSinceStart: {
46362
+ kind: "expression",
46363
+ expr: "count(lastPressedAt)",
46364
+ bindings: { lastPressedAt: {
46365
+ kind: "from",
46366
+ source: {
46367
+ addonId: input.camera.addonId,
46368
+ stableId: input.camera.stableId
46369
+ },
46370
+ cap: "doorbell",
46371
+ fieldPath: "lastPressedAt"
46372
+ } }
46373
+ }
46374
+ }
45827
46375
  };
45828
46376
  }
45829
46377
  //#endregion
@@ -45952,7 +46500,6 @@ var RUNTIME_DEFAULTS = {
45952
46500
  models: "camstack-data/models"
45953
46501
  },
45954
46502
  providers: [],
45955
- "ffmpeg.binaryPath": "ffmpeg",
45956
46503
  "ffmpeg.hwAccel": "auto",
45957
46504
  "ffmpeg.threadCount": 0,
45958
46505
  "auth.tokenExpiry": "30d"
@@ -52950,6 +53497,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
52950
53497
  addonId: null,
52951
53498
  access: "view"
52952
53499
  },
53500
+ "coreBlocks.listComposableCaps": {
53501
+ capName: "core-blocks",
53502
+ capScope: "system",
53503
+ addonId: null,
53504
+ access: "view"
53505
+ },
52953
53506
  "coreBlocks.listCompositions": {
52954
53507
  capName: "core-blocks",
52955
53508
  capScope: "system",
@@ -56634,6 +57187,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
56634
57187
  addonId: null,
56635
57188
  access: "create"
56636
57189
  },
57190
+ "pipelineOrchestrator.setCameraSensors": {
57191
+ capName: "pipeline-orchestrator",
57192
+ capScope: "system",
57193
+ addonId: null,
57194
+ access: "create"
57195
+ },
56637
57196
  "pipelineOrchestrator.setCameraStepOverride": {
56638
57197
  capName: "pipeline-orchestrator",
56639
57198
  capScope: "system",
@@ -60679,6 +61238,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
60679
61238
  form: "single",
60680
61239
  optional: false
60681
61240
  }],
61241
+ "pipelineOrchestrator.setCameraSensors": [{
61242
+ name: "deviceId",
61243
+ form: "single",
61244
+ optional: false
61245
+ }],
60682
61246
  "pipelineOrchestrator.setCameraStepOverride": [{
60683
61247
  name: "deviceId",
60684
61248
  form: "single",
@@ -61501,6 +62065,7 @@ var SYSTEM_SCOPE_DEVICE_METHODS = [
61501
62065
  "pipelineOrchestrator.getPipelineDevicePin",
61502
62066
  "pipelineOrchestrator.resolvePipeline",
61503
62067
  "pipelineOrchestrator.setCameraPipelineForAgent",
62068
+ "pipelineOrchestrator.setCameraSensors",
61504
62069
  "pipelineOrchestrator.setCameraStepOverride",
61505
62070
  "pipelineOrchestrator.setCameraStepToggle",
61506
62071
  "pipelineOrchestrator.setCameraSwitch",
@@ -62153,6 +62718,7 @@ function createSystemProxy(api) {
62153
62718
  getComposition: (input) => dispatch("coreBlocks", "getComposition", "query", input),
62154
62719
  listCompositions: (input) => dispatch("coreBlocks", "listCompositions", "query", input),
62155
62720
  getCustomization: (input) => dispatch("coreBlocks", "getCustomization", "query", input),
62721
+ listComposableCaps: (input) => dispatch("coreBlocks", "listComposableCaps", "query", input),
62156
62722
  validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input)
62157
62723
  },
62158
62724
  decoder: {
@@ -62484,6 +63050,7 @@ function createSystemProxy(api) {
62484
63050
  getNodeInferenceDevices: (input) => dispatch("pipelineOrchestrator", "getNodeInferenceDevices", "query", input),
62485
63051
  resetNodePipelineDefaults: (input) => dispatch("pipelineOrchestrator", "resetNodePipelineDefaults", "mutation", input),
62486
63052
  getCameraStatuses: (input) => dispatch("pipelineOrchestrator", "getCameraStatuses", "query", input),
63053
+ setCameraSensors: (input) => dispatch("pipelineOrchestrator", "setCameraSensors", "mutation", input),
62487
63054
  reconcileFromDisk: (input) => dispatch("pipelineOrchestrator", "reconcileFromDisk", "mutation", input),
62488
63055
  getReconcileFromDiskStatus: (input) => dispatch("pipelineOrchestrator", "getReconcileFromDiskStatus", "query", input),
62489
63056
  listTemplates: (input) => dispatch("pipelineOrchestrator", "listTemplates", "query", input),
@@ -66678,334 +67245,6 @@ var DEVICE_TYPE_INFO = { ["camera"]: {
66678
67245
  icon: "camera"
66679
67246
  } };
66680
67247
  //#endregion
66681
- //#region src/units/unit-table.ts
66682
- /**
66683
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
66684
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
66685
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
66686
- * decimals). K derives from `°C = K − 273.15`.
66687
- */
66688
- var UNIT_TABLE = {
66689
- "°C": {
66690
- dimension: "temperature",
66691
- factor: 1,
66692
- offset: 0
66693
- },
66694
- "°F": {
66695
- dimension: "temperature",
66696
- factor: 5 / 9,
66697
- offset: -160 / 9
66698
- },
66699
- K: {
66700
- dimension: "temperature",
66701
- factor: 1,
66702
- offset: -273.15
66703
- },
66704
- hPa: {
66705
- dimension: "pressure",
66706
- factor: 1,
66707
- offset: 0
66708
- },
66709
- kPa: {
66710
- dimension: "pressure",
66711
- factor: 10,
66712
- offset: 0
66713
- },
66714
- Pa: {
66715
- dimension: "pressure",
66716
- factor: .01,
66717
- offset: 0
66718
- },
66719
- mbar: {
66720
- dimension: "pressure",
66721
- factor: 1,
66722
- offset: 0
66723
- },
66724
- bar: {
66725
- dimension: "pressure",
66726
- factor: 1e3,
66727
- offset: 0
66728
- },
66729
- inHg: {
66730
- dimension: "pressure",
66731
- factor: 33.8639,
66732
- offset: 0
66733
- },
66734
- mmHg: {
66735
- dimension: "pressure",
66736
- factor: 1.33322,
66737
- offset: 0
66738
- },
66739
- psi: {
66740
- dimension: "pressure",
66741
- factor: 68.9476,
66742
- offset: 0
66743
- },
66744
- "m/s": {
66745
- dimension: "speed",
66746
- factor: 1,
66747
- offset: 0
66748
- },
66749
- "km/h": {
66750
- dimension: "speed",
66751
- factor: 1 / 3.6,
66752
- offset: 0
66753
- },
66754
- mph: {
66755
- dimension: "speed",
66756
- factor: .44704,
66757
- offset: 0
66758
- },
66759
- kn: {
66760
- dimension: "speed",
66761
- factor: .514444,
66762
- offset: 0
66763
- },
66764
- "mm/h": {
66765
- dimension: "precipitation-rate",
66766
- factor: 1,
66767
- offset: 0
66768
- },
66769
- "in/h": {
66770
- dimension: "precipitation-rate",
66771
- factor: 25.4,
66772
- offset: 0
66773
- },
66774
- m: {
66775
- dimension: "length",
66776
- factor: 1,
66777
- offset: 0
66778
- },
66779
- mm: {
66780
- dimension: "length",
66781
- factor: .001,
66782
- offset: 0
66783
- },
66784
- cm: {
66785
- dimension: "length",
66786
- factor: .01,
66787
- offset: 0
66788
- },
66789
- km: {
66790
- dimension: "length",
66791
- factor: 1e3,
66792
- offset: 0
66793
- },
66794
- in: {
66795
- dimension: "length",
66796
- factor: .0254,
66797
- offset: 0
66798
- },
66799
- ft: {
66800
- dimension: "length",
66801
- factor: .3048,
66802
- offset: 0
66803
- },
66804
- mi: {
66805
- dimension: "length",
66806
- factor: 1609.344,
66807
- offset: 0
66808
- },
66809
- lx: {
66810
- dimension: "illuminance",
66811
- factor: 1,
66812
- offset: 0
66813
- },
66814
- "W/m²": {
66815
- dimension: "irradiance",
66816
- factor: 1,
66817
- offset: 0
66818
- },
66819
- W: {
66820
- dimension: "power",
66821
- factor: 1,
66822
- offset: 0
66823
- },
66824
- kW: {
66825
- dimension: "power",
66826
- factor: 1e3,
66827
- offset: 0
66828
- },
66829
- VA: {
66830
- dimension: "apparent-power",
66831
- factor: 1,
66832
- offset: 0
66833
- },
66834
- Wh: {
66835
- dimension: "energy",
66836
- factor: 1,
66837
- offset: 0
66838
- },
66839
- kWh: {
66840
- dimension: "energy",
66841
- factor: 1e3,
66842
- offset: 0
66843
- },
66844
- MWh: {
66845
- dimension: "energy",
66846
- factor: 1e6,
66847
- offset: 0
66848
- },
66849
- V: {
66850
- dimension: "voltage",
66851
- factor: 1,
66852
- offset: 0
66853
- },
66854
- mV: {
66855
- dimension: "voltage",
66856
- factor: .001,
66857
- offset: 0
66858
- },
66859
- A: {
66860
- dimension: "current",
66861
- factor: 1,
66862
- offset: 0
66863
- },
66864
- mA: {
66865
- dimension: "current",
66866
- factor: .001,
66867
- offset: 0
66868
- },
66869
- ppm: {
66870
- dimension: "concentration-volume",
66871
- factor: 1,
66872
- offset: 0
66873
- },
66874
- ppb: {
66875
- dimension: "concentration-volume",
66876
- factor: .001,
66877
- offset: 0
66878
- },
66879
- "µg/m³": {
66880
- dimension: "concentration-mass",
66881
- factor: 1,
66882
- offset: 0
66883
- },
66884
- "mg/m³": {
66885
- dimension: "concentration-mass",
66886
- factor: 1e3,
66887
- offset: 0
66888
- },
66889
- kg: {
66890
- dimension: "mass",
66891
- factor: 1,
66892
- offset: 0
66893
- },
66894
- g: {
66895
- dimension: "mass",
66896
- factor: .001,
66897
- offset: 0
66898
- },
66899
- lb: {
66900
- dimension: "mass",
66901
- factor: .453592,
66902
- offset: 0
66903
- },
66904
- oz: {
66905
- dimension: "mass",
66906
- factor: .0283495,
66907
- offset: 0
66908
- },
66909
- "%": {
66910
- dimension: "percentage",
66911
- factor: 1,
66912
- offset: 0
66913
- }
66914
- };
66915
- //#endregion
66916
- //#region src/units/convert.ts
66917
- /**
66918
- * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
66919
- *
66920
- * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
66921
- * from the `try*` variant) whenever a unit is unknown or the two units belong
66922
- * to different dimensions — cross-dimension conversion is deliberately
66923
- * impossible so a render override can never make the value and its unit label
66924
- * disagree.
66925
- *
66926
- * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
66927
- * calling here — the table keys only canonical spellings.
66928
- */
66929
- /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
66930
- * units + a machine-readable `reason` for callers that branch on the cause. */
66931
- var UnitConversionError = class extends Error {
66932
- from;
66933
- to;
66934
- reason;
66935
- constructor(from, to, reason) {
66936
- super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
66937
- this.name = "UnitConversionError";
66938
- this.from = from;
66939
- this.to = to;
66940
- this.reason = reason;
66941
- }
66942
- };
66943
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
66944
- function specFor(unit) {
66945
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
66946
- }
66947
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
66948
- function unitDimension(unit) {
66949
- return specFor(unit)?.dimension;
66950
- }
66951
- /** Value in `spec`'s unit → its dimension's canonical unit. */
66952
- function toCanonical(value, spec) {
66953
- return value * spec.factor + spec.offset;
66954
- }
66955
- /** Canonical-unit value → the unit described by `spec`. */
66956
- function fromCanonical(canonical, spec) {
66957
- return (canonical - spec.offset) / spec.factor;
66958
- }
66959
- /**
66960
- * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
66961
- * Throws `UnitConversionError` when either unit is unknown or the units belong
66962
- * to different dimensions.
66963
- */
66964
- function convertUnit(value, from, to) {
66965
- if (from === to) return value;
66966
- const fromSpec = specFor(from);
66967
- if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
66968
- const toSpec = specFor(to);
66969
- if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
66970
- if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
66971
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
66972
- }
66973
- /**
66974
- * Graceful variant for the render path: returns the converted value, or
66975
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
66976
- * Never throws — a render caller falls back to the raw value + source unit.
66977
- */
66978
- function tryConvertUnit(value, from, to) {
66979
- if (from === to) return value;
66980
- const fromSpec = specFor(from);
66981
- if (!fromSpec) return void 0;
66982
- const toSpec = specFor(to);
66983
- if (!toSpec) return void 0;
66984
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
66985
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
66986
- }
66987
- /**
66988
- * Whether `value` can be converted between `from` and `to` (same dimension,
66989
- * both known). Cheap boolean probe for the UI's same-dimension unit picker and
66990
- * the resolver's convertibility branch — compute ONCE so a unit label and its
66991
- * value math can never disagree.
66992
- */
66993
- function canConvertUnit(from, to) {
66994
- if (from === to) return true;
66995
- const fromDim = unitDimension(from);
66996
- if (fromDim === void 0) return false;
66997
- return fromDim === unitDimension(to);
66998
- }
66999
- /**
67000
- * Every known unit spelling in a dimension (table insertion order — canonical
67001
- * unit first). Powers the UI's same-dimension unit picker.
67002
- */
67003
- function unitsForDimension(dimension) {
67004
- const out = [];
67005
- for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
67006
- return out;
67007
- }
67008
- //#endregion
67009
67248
  //#region src/util/location-match.ts
67010
67249
  /**
67011
67250
  * Pure fuzzy matcher for adoption location import. Normalized
@@ -68554,6 +68793,9 @@ exports.ClusterModelChoiceSchema = ClusterModelChoiceSchema;
68554
68793
  exports.CollectionColumnSchema = CollectionColumnSchema;
68555
68794
  exports.CollectionIndexSchema = CollectionIndexSchema;
68556
68795
  exports.ColorStatusSchema = ColorStatusSchema;
68796
+ exports.ComposableCapabilitiesSchema = ComposableCapabilitiesSchema;
68797
+ exports.ComposableCapabilitySchema = ComposableCapabilitySchema;
68798
+ exports.ComposableFieldSchema = ComposableFieldSchema;
68557
68799
  exports.ComposerApplyInputSchema = ComposerApplyInputSchema;
68558
68800
  exports.ComposerApplyResultSchema = ComposerApplyResultSchema;
68559
68801
  exports.ComposerBlockSchema = ComposerBlockSchema;
@@ -68679,6 +68921,7 @@ exports.DISPLAY_ICON_GROUPS = DISPLAY_ICON_GROUPS;
68679
68921
  exports.DISPLAY_ICON_KEYS = DISPLAY_ICON_KEYS;
68680
68922
  exports.DISPLAY_ICON_OFFERED_KEYS = DISPLAY_ICON_OFFERED_KEYS;
68681
68923
  exports.DISPLAY_ICON_VOCABULARY = DISPLAY_ICON_VOCABULARY;
68924
+ exports.DOORBELL_RISE_DEBOUNCE_MS = DOORBELL_RISE_DEBOUNCE_MS;
68682
68925
  exports.DOORBELL_SOURCE_SLOT = DOORBELL_SOURCE_SLOT;
68683
68926
  exports.DataStoreEngineInfoSchema = EngineInfoSchema;
68684
68927
  exports.DayNightModeSchema = DayNightModeSchema;
@@ -68816,6 +69059,7 @@ exports.ExtensionSourceHealthSchema = ExtensionSourceHealthSchema;
68816
69059
  exports.ExtensionSourceSchema = ExtensionSourceSchema;
68817
69060
  exports.FEEDER_EXAMPLE_ADDON_ID = FEEDER_EXAMPLE_ADDON_ID;
68818
69061
  exports.FEEDER_EXAMPLE_HA_DEVICE_ID = FEEDER_EXAMPLE_HA_DEVICE_ID;
69062
+ exports.FFMPEG_PATH_ENV = require_composition.FFMPEG_PATH_ENV;
68819
69063
  exports.FIRST_LEVEL_MACRO_CLASSES = FIRST_LEVEL_MACRO_CLASSES;
68820
69064
  exports.FULL_IMAGE_BBOX = FULL_IMAGE_BBOX;
68821
69065
  exports.FailureContributionSchema = FailureContributionSchema;
@@ -69484,6 +69728,7 @@ exports.ServerUpdateActionResultSchema = ServerUpdateActionResultSchema;
69484
69728
  exports.ServerUpdateCheckResultSchema = ServerUpdateCheckResultSchema;
69485
69729
  exports.ServerUpdateOutcomeSchema = ServerUpdateOutcomeSchema;
69486
69730
  exports.ServerUpdateStateSchema = ServerUpdateStateSchema;
69731
+ exports.SetCameraSensorsInputSchema = SetCameraSensorsInputSchema;
69487
69732
  exports.SetLoggingSettingsInputSchema = SetLoggingSettingsInputSchema;
69488
69733
  exports.SetSiteLocationInputSchema = SetSiteLocationInputSchema;
69489
69734
  exports.SettingsPatchSchema = SettingsPatchSchema;
@@ -69732,6 +69977,7 @@ exports.buildAddonRouteProvider = buildAddonRouteProvider;
69732
69977
  exports.buildAudioArgs = require_composition.buildAudioArgs;
69733
69978
  exports.buildCapabilityLookup = buildCapabilityLookup;
69734
69979
  exports.buildDeviceAdminUrl = buildDeviceAdminUrl;
69980
+ exports.buildDoorbellCompositionFeature = buildDoorbellCompositionFeature;
69735
69981
  exports.buildEventKindDescriptor = buildEventKindDescriptor;
69736
69982
  exports.buildFfmpegArgs = require_composition.buildFfmpegArgs;
69737
69983
  exports.buildInputArgs = require_composition.buildInputArgs;
@@ -69740,6 +69986,7 @@ exports.buildNcTaxonomy = buildNcTaxonomy;
69740
69986
  exports.buildRoleScopes = buildRoleScopes;
69741
69987
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
69742
69988
  exports.buildVideoArgs = require_composition.buildVideoArgs;
69989
+ exports.builtCapabilityBlockers = builtCapabilityBlockers;
69743
69990
  exports.buttonCapability = buttonCapability;
69744
69991
  exports.cameraCredentialsCapability = cameraCredentialsCapability;
69745
69992
  exports.cameraGridLayoutCapability = cameraGridLayoutCapability;
@@ -69832,6 +70079,7 @@ exports.deriveBatteryPresence = deriveBatteryPresence;
69832
70079
  exports.deriveCameraSwitches = deriveCameraSwitches;
69833
70080
  exports.deriveDetailCropRect = deriveDetailCropRect;
69834
70081
  exports.deriveRecordingMode = deriveRecordingMode;
70082
+ exports.describeComposableCaps = describeComposableCaps;
69835
70083
  exports.describeCustomActions = require_sleep.describeCustomActions;
69836
70084
  exports.describeModelVariant = describeModelVariant;
69837
70085
  exports.describeOnboardClamp = describeOnboardClamp;
@@ -69859,6 +70107,7 @@ exports.deviceStatusCapability = deviceStatusCapability;
69859
70107
  exports.displayIconEntry = displayIconEntry;
69860
70108
  exports.displayIconsInGroup = displayIconsInGroup;
69861
70109
  exports.doorbellCapability = doorbellCapability;
70110
+ exports.doorbellFeatureSources = doorbellFeatureSources;
69862
70111
  exports.droppedConditionsForKind = droppedConditionsForKind;
69863
70112
  exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
69864
70113
  exports.egressTransportFromRequest = egressTransportFromRequest;
@@ -70024,6 +70273,7 @@ exports.maskUrlCredentials = maskUrlCredentials;
70024
70273
  exports.mayReadLocation = mayReadLocation;
70025
70274
  exports.mayWriteToLocation = mayWriteToLocation;
70026
70275
  exports.mediaPlayerCapability = mediaPlayerCapability;
70276
+ exports.memoizePinnedFfmpeg = memoizePinnedFfmpeg;
70027
70277
  exports.mergeSourceInfo = mergeSourceInfo;
70028
70278
  exports.meshNetworkCapability = meshNetworkCapability;
70029
70279
  exports.method = require_sleep.method;