@camstack/types 1.2.275 → 1.2.277

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,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-WOeeZ4FU.js");
2
+ const require_sleep = require("./sleep-DCbD8sdg.js");
3
3
  const require_event_category = require("./event-category-MbnB-6AN.js");
4
4
  const require_composition = require("./composition-DG4jVTfB.js");
5
5
  const require_err_msg = require("./err-msg-Dx2Kor0g.js");
@@ -2002,6 +2002,98 @@ var ArchivedDebugNoteQueryInputSchema = zod.z.object({
2002
2002
  limit: zod.z.number().int().min(1).max(1e3).optional()
2003
2003
  });
2004
2004
  //#endregion
2005
+ //#region src/interfaces/track-relations.ts
2006
+ /**
2007
+ * Ground truth for the tracking bench, as debug marks (D681, spec §4).
2008
+ *
2009
+ * Identity over live tracks, not boxes: "A and B are the same subject",
2010
+ * "A and B are different subjects", and one track's purity ("mixed" — two
2011
+ * subjects, optionally switching at an instant — or "not a subject"). Rows
2012
+ * reference tracks as PROVENANCE: they outlive the tracks by design.
2013
+ *
2014
+ * Bounded by COUNT, never by age (the D405 argument): ground truth is most
2015
+ * valuable when it is old enough for the pattern to have repeated.
2016
+ */
2017
+ var MAX_TRACK_RELATIONS = 2e4;
2018
+ var MAX_TRACK_PURITIES = 5e3;
2019
+ var TRACK_RELATIONS_DEFAULT_LIMIT = 500;
2020
+ var TrackRelationKindSchema = zod.z.enum(["same-subject", "different-subjects"]);
2021
+ var TrackSideSchema = zod.z.enum(["before", "after"]);
2022
+ var TrackPurityValueSchema = zod.z.enum(["mixed", "not-a-subject"]);
2023
+ var TrackRelationSchema = zod.z.object({
2024
+ id: zod.z.string(),
2025
+ deviceId: zod.z.number().int(),
2026
+ leftTrackId: zod.z.string(),
2027
+ rightTrackId: zod.z.string(),
2028
+ leftSide: TrackSideSchema.nullable(),
2029
+ rightSide: TrackSideSchema.nullable(),
2030
+ relation: TrackRelationKindSchema,
2031
+ presence: zod.z.literal("continuous").nullable(),
2032
+ leftStartedAt: zod.z.number().int().nullable(),
2033
+ rightStartedAt: zod.z.number().int().nullable(),
2034
+ createdAt: zod.z.number().int(),
2035
+ updatedAt: zod.z.number().int(),
2036
+ author: zod.z.string().nullable()
2037
+ });
2038
+ var TrackPuritySchema = zod.z.object({
2039
+ id: zod.z.string(),
2040
+ deviceId: zod.z.number().int(),
2041
+ sourceTrackId: zod.z.string(),
2042
+ purity: TrackPurityValueSchema,
2043
+ switchAtMs: zod.z.number().int().nullable(),
2044
+ startedAt: zod.z.number().int().nullable(),
2045
+ createdAt: zod.z.number().int(),
2046
+ updatedAt: zod.z.number().int(),
2047
+ author: zod.z.string().nullable()
2048
+ });
2049
+ var SetTrackRelationInputSchema = zod.z.object({
2050
+ deviceId: zod.z.number().int(),
2051
+ trackId: zod.z.string().min(1),
2052
+ otherTrackId: zod.z.string().min(1),
2053
+ relation: TrackRelationKindSchema,
2054
+ presence: zod.z.literal("continuous").optional(),
2055
+ trackSide: TrackSideSchema.optional(),
2056
+ otherSide: TrackSideSchema.optional()
2057
+ });
2058
+ var ClearTrackRelationInputSchema = zod.z.object({
2059
+ deviceId: zod.z.number().int(),
2060
+ trackId: zod.z.string().min(1),
2061
+ otherTrackId: zod.z.string().min(1)
2062
+ });
2063
+ var ClearTrackRelationResultSchema = zod.z.object({ cleared: zod.z.boolean() });
2064
+ var SetTrackPurityInputSchema = zod.z.object({
2065
+ deviceId: zod.z.number().int(),
2066
+ trackId: zod.z.string().min(1),
2067
+ purity: zod.z.enum([
2068
+ "pure",
2069
+ "mixed",
2070
+ "not-a-subject"
2071
+ ]),
2072
+ switchAtMs: zod.z.number().int().optional()
2073
+ });
2074
+ /** `null` when `pure` cleared the row. */
2075
+ var SetTrackPurityResultSchema = zod.z.object({ purity: TrackPuritySchema.nullable() });
2076
+ var ListTrackRelationsInputSchema = zod.z.object({
2077
+ deviceId: zod.z.number().int(),
2078
+ trackId: zod.z.string().optional(),
2079
+ since: zod.z.number().int().optional(),
2080
+ until: zod.z.number().int().optional(),
2081
+ limit: zod.z.number().int().min(1).max(5e3).optional()
2082
+ });
2083
+ var ListTrackRelationsResultSchema = zod.z.object({
2084
+ relations: zod.z.array(TrackRelationSchema).readonly(),
2085
+ purities: zod.z.array(TrackPuritySchema).readonly(),
2086
+ truncated: zod.z.boolean()
2087
+ });
2088
+ var TrackRelationRefusalSchema = zod.z.enum([
2089
+ "unknown-track",
2090
+ "cross-device",
2091
+ "self-relation",
2092
+ "contradiction",
2093
+ "switch-outside-track",
2094
+ "switch-needs-mixed"
2095
+ ]);
2096
+ //#endregion
2005
2097
  //#region src/interfaces/device-capabilities/camera.ts
2006
2098
  /** Friendly display labels for stream quality IDs. */
2007
2099
  var STREAM_QUALITY_LABELS = {
@@ -9790,251 +9882,1180 @@ var CompositionBlockStateSchema = zod.z.object({
9790
9882
  fields: zod.z.array(CompositionFieldStateSchema)
9791
9883
  });
9792
9884
  //#endregion
9793
- //#region src/capabilities/core-blocks.cap.ts
9794
- /**
9795
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
9796
- * its own process.
9797
- *
9798
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
9799
- *
9800
- * The first use is **owning devices without being a device provider**: a block
9801
- * declares devices under a system or custom integration and drives their state,
9802
- * with the same `ctx` an addon gets. Automations come later; nothing here
9803
- * models a trigger.
9804
- *
9805
- * **Stated plainly, because it does not change by being true:** a block has an
9806
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
9807
- * with no review step. What makes that survivable is not a sandbox, it is
9808
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
9809
- * so a block that throws or never returns is marked `failed` and visible
9810
- * instead of taking the hub with it (D6). Every method here is admin-only, and
9811
- * must stay so.
9812
- */
9813
- /**
9814
- * Prefix for a block's generated addon id AND its runner id — one addon, one
9815
- * process, so the two are the same string (D2).
9816
- *
9817
- * It lives on the CONTRACT rather than beside the code that materialises the
9818
- * package, because it is not an implementation detail: the operator surface
9819
- * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
9820
- * with no cap and no store of its own. Two copies of this string is one copy
9821
- * too many — a drift would show as a logs pane that is simply always empty.
9822
- */
9823
- var CORE_BLOCK_ADDON_PREFIX = "core-block-";
9885
+ //#region src/composition/plan-result.ts
9886
+ function problem(code, path, message) {
9887
+ return {
9888
+ code,
9889
+ path,
9890
+ message
9891
+ };
9892
+ }
9893
+ function refused(...problems) {
9894
+ return {
9895
+ problems,
9896
+ fields: [],
9897
+ features: [],
9898
+ events: []
9899
+ };
9900
+ }
9901
+ /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
9902
+ function compositionFieldAt(field) {
9903
+ const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
9904
+ return `features[${field.featureIndex}].${inner}.${field.path}`;
9905
+ }
9906
+ //#endregion
9907
+ //#region src/composition/expression-kinds.ts
9908
+ var NUMBER = {
9909
+ kind: "fixed",
9910
+ result: ["number"]
9911
+ };
9912
+ var BOOLEAN = {
9913
+ kind: "fixed",
9914
+ result: ["boolean"]
9915
+ };
9916
+ /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
9917
+ var BUILTIN_RESULT_KINDS = Object.freeze({
9918
+ min: NUMBER,
9919
+ max: NUMBER,
9920
+ abs: NUMBER,
9921
+ floor: NUMBER,
9922
+ ceil: NUMBER,
9923
+ sqrt: NUMBER,
9924
+ round: NUMBER,
9925
+ pow: NUMBER,
9926
+ clamp: NUMBER,
9927
+ avg: NUMBER,
9928
+ sum: NUMBER,
9929
+ age: NUMBER,
9930
+ convert: NUMBER,
9931
+ coalesce: { kind: "coalesce" },
9932
+ any: BOOLEAN,
9933
+ all: BOOLEAN,
9934
+ within: BOOLEAN,
9935
+ latest: {
9936
+ kind: "fixed",
9937
+ result: ["number", "null"]
9938
+ },
9939
+ number: {
9940
+ kind: "fixed",
9941
+ result: ["number", "null"]
9942
+ },
9943
+ rose: {
9944
+ kind: "fixed",
9945
+ result: ["number", "null"]
9946
+ },
9947
+ count: NUMBER
9948
+ });
9949
+ var KindInferenceError = class extends Error {};
9950
+ function kindOfValue(value) {
9951
+ if (value === null) return "null";
9952
+ if (typeof value === "number") return "number";
9953
+ if (typeof value === "string") return "string";
9954
+ return "boolean";
9955
+ }
9956
+ function only(kind) {
9957
+ return new Set([kind]);
9958
+ }
9959
+ function union(...sets) {
9960
+ return new Set(sets.flatMap((s) => [...s]));
9961
+ }
9962
+ function withoutNull(set) {
9963
+ return new Set([...set].filter((k) => k !== "null"));
9964
+ }
9965
+ function binaryKinds(op, left, right) {
9966
+ if (op === "+") {
9967
+ const all = [...left, ...right];
9968
+ if (all.every((k) => k === "string")) return only("string");
9969
+ if (all.every((k) => k === "number")) return only("number");
9970
+ return new Set(["number", "string"]);
9971
+ }
9972
+ if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
9973
+ return only("boolean");
9974
+ }
9975
+ /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
9976
+ function arityRange(builtin) {
9977
+ if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
9978
+ if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
9979
+ return `${builtin.minArgs}–${builtin.maxArgs}`;
9980
+ }
9981
+ function callKinds(callee, args) {
9982
+ const builtin = require_composition.EXPRESSION_BUILTINS[callee];
9983
+ 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)}`);
9984
+ const rule = BUILTIN_RESULT_KINDS[callee];
9985
+ if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
9986
+ if (rule.kind === "fixed") return new Set(rule.result);
9987
+ const nonNull = union(...args.map(withoutNull));
9988
+ return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
9989
+ }
9990
+ function kindsOf(node, bindings) {
9991
+ switch (node.kind) {
9992
+ case "literal": return only(kindOfValue(node.value));
9993
+ case "identifier": {
9994
+ if (node.name === "now") return only("number");
9995
+ const bound = bindings.get(node.name);
9996
+ if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
9997
+ return bound;
9998
+ }
9999
+ case "unary": return only(node.op === "!" ? "boolean" : "number");
10000
+ case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
10001
+ case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
10002
+ case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
10003
+ case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
10004
+ }
10005
+ }
10006
+ function nodeContainsCall(node, callee) {
10007
+ switch (node.kind) {
10008
+ case "literal":
10009
+ case "identifier": return false;
10010
+ case "unary": return nodeContainsCall(node.operand, callee);
10011
+ case "binary":
10012
+ case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
10013
+ case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
10014
+ case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
10015
+ }
10016
+ }
10017
+ function expressionContainsBuiltinCall(expr, callee) {
10018
+ const compiled = require_composition.compileExpressionSafe(expr);
10019
+ return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
10020
+ }
10021
+ function inferExpressionKinds(input) {
10022
+ const compiled = require_composition.compileExpressionSafe(input.expr);
10023
+ if (!compiled.ok) return {
10024
+ ok: false,
10025
+ error: compiled.error
10026
+ };
10027
+ try {
10028
+ return {
10029
+ ok: true,
10030
+ kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
10031
+ };
10032
+ } catch (err) {
10033
+ if (err instanceof KindInferenceError) return {
10034
+ ok: false,
10035
+ error: err.message
10036
+ };
10037
+ throw err;
10038
+ }
10039
+ }
10040
+ //#endregion
10041
+ //#region src/composition/field-kinds.ts
9824
10042
  /**
9825
- * The builtin's own addon id — and, because of that, the OWNER of the
9826
- * integration every block's devices hang from.
9827
- *
9828
- * A block cannot own one. Every integration read is gated on the installed-addon
9829
- * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
9830
- * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
9831
- * null forever while the write succeeds — one invisible, undeletable row per
9832
- * start. `core-blocks` IS installed, so it reconciles one shared integration and
9833
- * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
9834
- *
9835
- * Exported from the contract, next to the runner prefix and for the same reason:
9836
- * a block reads it as
9837
- * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
9838
- * so a second copy of the string is a block that silently declares into nowhere.
10043
+ * The kind half of planning a composition field (D659): which kinds a source
10044
+ * can produce, whether they fit a target field, and the field's role.
10045
+ * Shared by the top-level planner (`validate-composition.ts`) and the item
10046
+ * planner (`composition-items.ts`). Internal to the composition module — NOT
10047
+ * in the barrel: these are planner internals, not a published contract.
9839
10048
  */
9840
- var CORE_BLOCKS_ADDON_ID = "core-blocks";
9841
- /** The addon/runner id a block's process runs under. */
9842
- function coreBlockAddonId(blockId) {
9843
- return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
10049
+ function readKinds(read, lookupCap) {
10050
+ const def = lookupCap(read.cap);
10051
+ const schema = def?.runtimeState ?? def?.status?.schema;
10052
+ if (def === null || schema === void 0) return {
10053
+ ok: false,
10054
+ code: "source-field-unknown",
10055
+ message: `no capability \`${read.cap}\` with state to read`
10056
+ };
10057
+ const field = describeStateField(schema, read.fieldPath);
10058
+ if (field === null) return {
10059
+ ok: false,
10060
+ code: "source-field-unknown",
10061
+ message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
10062
+ };
10063
+ const base = field.kind === "enum" ? "string" : field.kind;
10064
+ return {
10065
+ ok: true,
10066
+ kinds: new Set(field.nullable ? [base, "null"] : [base])
10067
+ };
9844
10068
  }
9845
- /** The block id behind a generated addon/runner id, or null when the id belongs
9846
- * to something else. The inverse of {@link coreBlockAddonId}. */
9847
- function coreBlockIdFromAddonId(addonId) {
9848
- if (!addonId.startsWith("core-block-")) return null;
9849
- const rest = addonId.slice(11);
9850
- return rest.length > 0 ? rest : null;
10069
+ function sourceKinds(source, lookupCap) {
10070
+ if (source.kind === "from") return readKinds(source, lookupCap);
10071
+ if (source.kind === "code") return {
10072
+ ok: false,
10073
+ code: "reserved-variant",
10074
+ message: "code fields arrive in slice 4"
10075
+ };
10076
+ const bindingKinds = /* @__PURE__ */ new Map();
10077
+ for (const [name, binding] of Object.entries(source.bindings)) {
10078
+ if (binding.kind === "literal") {
10079
+ bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
10080
+ continue;
10081
+ }
10082
+ const kinds = readKinds(binding, lookupCap);
10083
+ if (!kinds.ok) return {
10084
+ ...kinds,
10085
+ message: `binding \`${name}\`: ${kinds.message}`
10086
+ };
10087
+ bindingKinds.set(name, kinds.kinds);
10088
+ }
10089
+ const inferred = inferExpressionKinds({
10090
+ expr: source.expr,
10091
+ bindingKinds
10092
+ });
10093
+ return inferred.ok ? {
10094
+ ok: true,
10095
+ kinds: inferred.kinds
10096
+ } : {
10097
+ ok: false,
10098
+ code: "expression-invalid",
10099
+ message: inferred.error
10100
+ };
10101
+ }
10102
+ function compatibility(cap, target, kinds, at) {
10103
+ const allowed = target.kind === "enum" ? "string" : target.kind;
10104
+ const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
10105
+ const problems = [];
10106
+ if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
10107
+ 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`));
10108
+ return problems;
9851
10109
  }
9852
- /** Where a block runs. The operator chooses — a block driving a device on an
9853
- * agent is the reason placement is not fixed to the hub. */
9854
- var CoreBlockPlacementSchema = zod.z.union([zod.z.literal("hub"), zod.z.string().min(1)]);
9855
- /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
9856
- * a failing block reads the same way a failing addon does. `degraded` is a
9857
- * composition on an existing device with a field handed back to the native
9858
- * provider while its source is unavailable (S1b, D663); the error names it. */
9859
- var CoreBlockStatusSchema = zod.z.enum([
9860
- "stopped",
9861
- "starting",
9862
- "running",
9863
- "degraded",
9864
- "failed"
9865
- ]);
9866
- /** Client-authored fields. */
9867
- var CoreBlockInputSchema = zod.z.object({
9868
- name: zod.z.string().min(1).max(120),
9869
- /** TypeScript source. Compiled server-side before it is ever stored — a
9870
- * block that does not compile is a fork failure the operator would meet
9871
- * minutes later, in a log, instead of in the editor. */
9872
- code: zod.z.string().max(2e5),
9873
- enabled: zod.z.boolean(),
9874
- placement: CoreBlockPlacementSchema,
9875
- /**
9876
- * Integration the block's devices hang from. Absent = the system integration
9877
- * blocks share. A block may declare its own instead.
9878
- */
9879
- integrationId: zod.z.string().optional()
9880
- });
9881
10110
  /**
9882
- * What a block IS (D659). Absent means `code` — every row stored before this
9883
- * union keeps working unchanged.
9884
- * - `code`: the TypeScript in `code`, compiled and run in its own runner;
9885
- * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
9886
- * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
10111
+ * A string a source produces WITHOUT reading any device — a field-level
10112
+ * `expression` source whose compiled AST is a bare string literal (`"red"`,
10113
+ * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
10114
+ * the kind level (a `from` read of an enum-typed source field, a computed
10115
+ * expression) is NOT checked against the target's `enumValues` here: the
10116
+ * kind inference already collapsed it to the coarse `'string'` kind, losing
10117
+ * whichever enum it actually came from, so membership can only be judged at
10118
+ * write time against the runtime-state schema. Known limitation — see the
10119
+ * fix report for Task 6 round 1.
9887
10120
  */
9888
- var CoreBlockSourceSchema = zod.z.discriminatedUnion("kind", [
9889
- zod.z.object({ kind: zod.z.literal("code") }),
9890
- zod.z.object({
9891
- kind: zod.z.literal("recipe"),
9892
- recipe: AutomationRecipeSchema
9893
- }),
9894
- zod.z.object({
9895
- kind: zod.z.literal("composition"),
9896
- composition: require_composition.CompositionSchema
9897
- })
9898
- ]);
10121
+ function literalStringValue(source) {
10122
+ if (source.kind !== "expression") return null;
10123
+ const compiled = require_composition.compileExpressionSafe(source.expr);
10124
+ if (!compiled.ok) return null;
10125
+ const ast = compiled.parsed.ast;
10126
+ return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
10127
+ }
9899
10128
  /**
9900
- * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
9901
- *
9902
- * `name`: REQUIRED for a `new` target — the block's name IS the device's name
9903
- * (D659). ABSENT for an `existing` target — a customization has no name of its
9904
- * own; the server derives one (`customizationBlockName`, D663) and refuses a
9905
- * client-sent name by name.
10129
+ * An enum target whose source is a save-time-KNOWN string literal outside its
10130
+ * `enumValues` is refused now rather than left to fail the write later. A
10131
+ * dynamic source (a `from` read, a non-literal expression) is left to
10132
+ * runtime — see {@link literalStringValue}.
9906
10133
  */
9907
- var CompositionBlockInputSchema = zod.z.object({
9908
- name: zod.z.string().min(1).max(120).optional(),
9909
- enabled: zod.z.boolean(),
9910
- composition: require_composition.CompositionSchema
9911
- });
9912
- /** A stored block. */
9913
- var CoreBlockSchema = CoreBlockInputSchema.extend({
9914
- id: zod.z.string(),
9915
- createdAt: zod.z.number(),
9916
- updatedAt: zod.z.number(),
9917
- /** Server-stamped author. */
9918
- createdBy: zod.z.string(),
9919
- status: CoreBlockStatusSchema,
9920
- /**
9921
- * Why the block is not running, when it is not. The operator's ONLY window
9922
- * into a block that failed at load — a block that is silently absent is the
9923
- * failure mode this whole feature has to avoid.
9924
- */
9925
- lastError: zod.z.string().optional(),
9926
- /** Ms epoch of the last state change. */
9927
- lastChangedAt: zod.z.number(),
9928
- /** What the block is. Absent means `code` (D659). */
9929
- source: CoreBlockSourceSchema.optional()
9930
- });
9931
- function coreBlockSourceKind(block) {
9932
- return block.source?.kind ?? "code";
10134
+ function enumLiteralProblem(cap, target, source, at) {
10135
+ if (target.kind !== "enum" || target.enumValues === void 0) return null;
10136
+ const value = literalStringValue(source);
10137
+ if (value === null || target.enumValues.includes(value)) return null;
10138
+ const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
10139
+ return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
9933
10140
  }
9934
- function coreBlockComposition(block) {
9935
- return block.source?.kind === "composition" ? block.source.composition : null;
10141
+ function roleOf(source) {
10142
+ if (source.kind !== "expression") return "derived";
10143
+ if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
10144
+ if (require_composition.fieldReads(source).length > 0) return "derived";
10145
+ const compiled = require_composition.compileExpressionSafe(source.expr);
10146
+ return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
9936
10147
  }
9937
- /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
9938
- var CompositionStateReadSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
9939
- kind: zod.z.literal("answered"),
9940
- state: CompositionBlockStateSchema.nullable()
9941
- }), zod.z.object({
9942
- kind: zod.z.literal("unreachable"),
9943
- error: zod.z.string()
9944
- })]);
9945
- /** A stored composition block and what the composer says about it. */
9946
- var CompositionViewSchema = zod.z.object({
9947
- block: CoreBlockSchema,
9948
- state: CompositionStateReadSchema
9949
- });
9950
10148
  /**
9951
- * A read of the stored compositions. `not-loaded` while the `core_blocks`
9952
- * table has not been READ: an empty answer then would make "not yet known"
9953
- * look like "none", and invite the operator to recreate what exists (D315).
10149
+ * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
10150
+ * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
10151
+ * own offline, and D393 forbids a stale number, so on an existing target a
10152
+ * field that READS a source never gets `offline`.
9954
10153
  */
9955
- var CompositionsNotLoadedSchema = zod.z.object({
9956
- state: zod.z.literal("not-loaded"),
9957
- reason: zod.z.string()
9958
- });
9959
- var CompositionListReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9960
- state: zod.z.literal("loaded"),
9961
- views: zod.z.array(CompositionViewSchema)
9962
- }), CompositionsNotLoadedSchema]);
9963
- var CompositionGetReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9964
- state: zod.z.literal("loaded"),
9965
- view: CompositionViewSchema.nullable()
9966
- }), CompositionsNotLoadedSchema]);
9967
- /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
9968
- var CompositionCustomizationReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
9969
- state: zod.z.literal("loaded"),
9970
- view: CompositionViewSchema.nullable()
9971
- }), CompositionsNotLoadedSchema]);
9972
- /** What a compile attempt produced. */
9973
- var CoreBlockCompileResultSchema = zod.z.object({
9974
- ok: zod.z.boolean(),
9975
- /** Present when `ok` is false — the first error, in the author's words. */
9976
- error: zod.z.string().optional(),
9977
- line: zod.z.number().optional(),
9978
- column: zod.z.number().optional()
9979
- });
9980
- var coreBlocksCapability = {
9981
- name: "core-blocks",
9982
- scope: "system",
9983
- mode: "singleton",
9984
- methods: {
9985
- list: require_sleep.method(zod.z.object({}), zod.z.object({ blocks: zod.z.array(CoreBlockSchema) }), { auth: "admin" }),
9986
- get: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
9987
- /**
9988
- * Create a block. The code is COMPILED first: storing source that does not
9989
- * compile turns an editor error into a fork failure the operator meets
9990
- * minutes later in a log.
9991
- */
9992
- create: require_sleep.method(zod.z.object({ block: CoreBlockInputSchema }), zod.z.object({ block: CoreBlockSchema }), {
9993
- kind: "mutation",
9994
- auth: "admin",
9995
- caller: "required"
9996
- }),
9997
- update: require_sleep.method(zod.z.object({
9998
- blockId: zod.z.string(),
9999
- block: CoreBlockInputSchema.partial()
10000
- }), zod.z.object({ block: CoreBlockSchema }), {
10001
- kind: "mutation",
10002
- auth: "admin",
10003
- caller: "required"
10004
- }),
10005
- delete: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ success: zod.z.literal(true) }), {
10006
- kind: "mutation",
10007
- auth: "admin"
10008
- }),
10009
- setEnabled: require_sleep.method(zod.z.object({
10010
- blockId: zod.z.string(),
10011
- enabled: zod.z.boolean()
10012
- }), zod.z.object({ block: CoreBlockSchema }), {
10013
- kind: "mutation",
10014
- auth: "admin"
10015
- }),
10016
- /**
10017
- * Stop this block's runner and let the supervisor bring it back.
10018
- *
10019
- * **It stores nothing.** There is no "restart requested" flag and no second
10020
- * lifecycle field: the supervisor already stops and respawns a runner when
10021
- * the code hash changes, and this exposes that same path deliberately
10022
- * (D62 — a switch writes the authority that already owned the function).
10023
- * The next reconcile pass remains the authority, so a restart that lost a
10024
- * race is corrected within one pass rather than leaving a stale flag behind.
10025
- *
10026
- * Refused for a DISABLED block: nothing would come back, and a button that
10027
- * silently does nothing is worse than one that says why.
10028
- */
10029
- restart: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema }), {
10030
- kind: "mutation",
10031
- auth: "admin"
10032
- }),
10033
- /**
10034
- * Type-check without saving — what the editor calls as the author types, so
10035
- * the compiler's verdict is the same one the server will reach.
10036
- */
10037
- compile: require_sleep.method(zod.z.object({ code: zod.z.string() }), CoreBlockCompileResultSchema, {
10154
+ function unavailableFor(tf, role, mode, cap, at) {
10155
+ if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
10156
+ ok: true,
10157
+ unavailable: "release"
10158
+ };
10159
+ if (mode === "new" || role !== "derived") return {
10160
+ ok: true,
10161
+ unavailable: tf.nullable ? "null" : "offline"
10162
+ };
10163
+ if (tf.nullable) return {
10164
+ ok: true,
10165
+ unavailable: "null"
10166
+ };
10167
+ if (tf.enumValues?.includes("unknown") === true) return {
10168
+ ok: true,
10169
+ unavailable: "unknown"
10170
+ };
10171
+ return {
10172
+ ok: false,
10173
+ 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)`)
10174
+ };
10175
+ }
10176
+ //#endregion
10177
+ //#region src/composition/composition-items.ts
10178
+ /** Item fields the composer writes itself, with the value it writes. */
10179
+ var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
10180
+ function reservedItemFields(itemArray) {
10181
+ return new Set([
10182
+ itemArray.keyField,
10183
+ ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
10184
+ ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
10185
+ ]);
10186
+ }
10187
+ /** A required leaf under an optional parent is required only once something writes that parent. */
10188
+ function leafRequired(itemArray, path, written) {
10189
+ const described = describeStateField(itemArray.itemSchema, path);
10190
+ if (described === null || !described.required) return false;
10191
+ return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
10192
+ }
10193
+ function missingLeaves({ capName, itemArray, reserved }, item, at) {
10194
+ const written = Object.keys(item.fields);
10195
+ return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
10196
+ if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
10197
+ if (!leafRequired(itemArray, leaf.path, written)) return [];
10198
+ const described = describeStateField(itemArray.itemSchema, leaf.path);
10199
+ if (described === null || described.nullable) return [];
10200
+ return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
10201
+ });
10202
+ }
10203
+ function planItemField(ctx, leaf) {
10204
+ const { capName, itemArray, featureIndex, lookupCap } = ctx;
10205
+ const { key, label, path, source } = leaf;
10206
+ const at = `features[${featureIndex}].items.${key}.fields.${path}`;
10207
+ 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`));
10208
+ const tf = describeStateField(itemArray.itemSchema, path);
10209
+ if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
10210
+ const kinds = sourceKinds(source, lookupCap);
10211
+ if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
10212
+ const role = roleOf(source);
10213
+ const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
10214
+ if (!unavailable.ok) return refused(unavailable.problem);
10215
+ const plan = {
10216
+ cap: capName,
10217
+ path,
10218
+ item: {
10219
+ arrayPath: itemArray.path,
10220
+ key,
10221
+ label
10222
+ },
10223
+ featureIndex,
10224
+ kind: tf.kind,
10225
+ nullable: tf.nullable,
10226
+ required: tf.required,
10227
+ unavailable: unavailable.unavailable,
10228
+ role,
10229
+ source,
10230
+ reads: [...require_composition.fieldReads(source)]
10231
+ };
10232
+ const enumProblem = enumLiteralProblem(capName, tf, source, at);
10233
+ return {
10234
+ problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
10235
+ fields: [plan],
10236
+ features: [],
10237
+ events: []
10238
+ };
10239
+ }
10240
+ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
10241
+ const at = `features[${featureIndex}].items`;
10242
+ const itemArray = def.status?.itemArray;
10243
+ if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
10244
+ 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`));
10245
+ const ctx = {
10246
+ capName: def.name,
10247
+ itemArray,
10248
+ reserved: reservedItemFields(itemArray),
10249
+ featureIndex,
10250
+ lookupCap,
10251
+ mode
10252
+ };
10253
+ const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
10254
+ problems: missingLeaves(ctx, item, `${at}.${key}`),
10255
+ fields: [],
10256
+ features: [],
10257
+ events: []
10258
+ }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
10259
+ key,
10260
+ label: item.label,
10261
+ path,
10262
+ source
10263
+ }))]);
10264
+ return {
10265
+ problems: results.flatMap((r) => r.problems),
10266
+ fields: results.flatMap((r) => r.fields),
10267
+ features: [],
10268
+ events: []
10269
+ };
10270
+ }
10271
+ //#endregion
10272
+ //#region src/composition/composition-graph.ts
10273
+ /**
10274
+ * The field graph across every composition, and the cycles in it (D659).
10275
+ *
10276
+ * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
10277
+ * edge points from a composed field to each field it reads. A cycle, direct or
10278
+ * through another composition, is refused at save by name (spec §2).
10279
+ *
10280
+ * The graph is built for ONE save: the CANDIDATE composition (the one being
10281
+ * validated) plus every STORED composition. The candidate replaces its own old
10282
+ * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
10283
+ * introduces or removes a cycle must never depend on array order. A DIFFERENT
10284
+ * block targeting the candidate's device is refused by name: one composition
10285
+ * per device (D663). Two different stored peers that (wrongly) target the same
10286
+ * device are corrupted state, not a cycle to resolve by picking one silently;
10287
+ * that is refused by name too (D391).
10288
+ *
10289
+ * A customization of an EXISTING device reads that device's NATIVE value for a
10290
+ * field it replaces or does not write ("Invert state" reads the `on` it
10291
+ * replaces): such a self-read is no edge, so it is never a cycle (D663).
10292
+ * Another composition reading that field reads the customizer's output.
10293
+ *
10294
+ * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
10295
+ * marks a node `done` after its first expansion) — not every elementary cycle
10296
+ * through a shared node, so the path it names may AVOID the candidate while
10297
+ * one through it goes unreported. What a save refuses is therefore decided per
10298
+ * strongly connected component (`findCandidateCycles`, C-1). A dotted
10299
+ * `fieldPath` is matched only as an exact string: a write to `state.value` and
10300
+ * a read of `state` would not connect (no nested field writes exist yet).
10301
+ *
10302
+ * An item leaf is its own node, `…#consumables.items[desiccant].status`
10303
+ * (`featureFieldEntries`), so two items sharing an inner path never merge. No
10304
+ * READ can name an array element (`describeStateField` never describes one),
10305
+ * so an item node is write-only: its edges to sources are what the check needs.
10306
+ */
10307
+ /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
10308
+ var CompositionGraphError = class extends Error {};
10309
+ /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
10310
+ function writtenFields(peer) {
10311
+ return new Set(peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
10312
+ }
10313
+ /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
10314
+ function readsComposedValue(peer, written, read) {
10315
+ if (require_composition.compositionSourceKey(read.source) !== require_composition.compositionSourceKey(peer.target)) return true;
10316
+ const field = `${read.cap}.${read.fieldPath}`;
10317
+ return written.has(field) && peer.replaced?.has(field) !== true;
10318
+ }
10319
+ function peerEdges(peer) {
10320
+ const written = writtenFields(peer);
10321
+ return peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath, source }) => [require_composition.compositionReadKey({
10322
+ source: peer.target,
10323
+ cap: feature.cap,
10324
+ fieldPath
10325
+ }), require_composition.fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(require_composition.compositionReadKey)]));
10326
+ }
10327
+ /**
10328
+ * Every peer that actually takes part in the graph: each stored peer minus the
10329
+ * candidate's own old version (same block id — the drop happens before any
10330
+ * edge is built, so its old edges never enter the map), plus the candidate
10331
+ * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
10332
+ * block targets the candidate's device, or two stored peers share a device.
10333
+ */
10334
+ function resolvedPeers(candidate, stored) {
10335
+ const candidateKey = require_composition.compositionSourceKey(candidate.target);
10336
+ const survivors = /* @__PURE__ */ new Map();
10337
+ for (const peer of stored) {
10338
+ if (peer.blockId === candidate.blockId) continue;
10339
+ const key = require_composition.compositionSourceKey(peer.target);
10340
+ 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`);
10341
+ const clash = survivors.get(key);
10342
+ if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
10343
+ survivors.set(key, peer);
10344
+ }
10345
+ return [...survivors.values(), candidate];
10346
+ }
10347
+ function compositionEdges(candidate, stored) {
10348
+ const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
10349
+ return new Map(edges);
10350
+ }
10351
+ function findCompositionCycles(candidate, stored) {
10352
+ const edges = compositionEdges(candidate, stored);
10353
+ const marks = /* @__PURE__ */ new Map();
10354
+ const found = /* @__PURE__ */ new Map();
10355
+ const visit = (node, stack) => {
10356
+ const mark = marks.get(node);
10357
+ if (mark === "done") return;
10358
+ if (mark === "visiting") {
10359
+ const path = [...stack.slice(stack.indexOf(node)), node];
10360
+ const id = [...new Set(path)].toSorted().join("|");
10361
+ if (!found.has(id)) found.set(id, { path });
10362
+ return;
10363
+ }
10364
+ marks.set(node, "visiting");
10365
+ for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
10366
+ marks.set(node, "done");
10367
+ };
10368
+ for (const node of edges.keys()) visit(node, []);
10369
+ return [...found.values()];
10370
+ }
10371
+ /** Tarjan's strongly connected components over every node of the graph (sources included). */
10372
+ function stronglyConnected(edges) {
10373
+ const index = /* @__PURE__ */ new Map();
10374
+ const low = /* @__PURE__ */ new Map();
10375
+ const onStack = /* @__PURE__ */ new Set();
10376
+ const stack = [];
10377
+ const components = [];
10378
+ const connect = (node) => {
10379
+ index.set(node, index.size);
10380
+ low.set(node, index.get(node) ?? 0);
10381
+ stack.push(node);
10382
+ onStack.add(node);
10383
+ for (const next of edges.get(node) ?? []) if (!index.has(next)) {
10384
+ connect(next);
10385
+ low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
10386
+ } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
10387
+ if (low.get(node) !== index.get(node)) return;
10388
+ const component = [];
10389
+ for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
10390
+ onStack.delete(member);
10391
+ component.push(member);
10392
+ if (member === node) break;
10393
+ }
10394
+ components.push(component);
10395
+ };
10396
+ for (const node of edges.keys()) if (!index.has(node)) connect(node);
10397
+ return components;
10398
+ }
10399
+ /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
10400
+ function cycleThrough(start, component, edges) {
10401
+ const parent = /* @__PURE__ */ new Map();
10402
+ const queue = [start];
10403
+ for (let head = 0; head < queue.length; head += 1) {
10404
+ const node = queue[head];
10405
+ if (node === void 0) break;
10406
+ for (const next of edges.get(node) ?? []) {
10407
+ if (next === start) {
10408
+ const chain = [];
10409
+ for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
10410
+ return [
10411
+ start,
10412
+ ...chain.reverse(),
10413
+ start
10414
+ ];
10415
+ }
10416
+ if (!component.has(next) || parent.has(next)) continue;
10417
+ parent.set(next, node);
10418
+ queue.push(next);
10419
+ }
10420
+ }
10421
+ return [start, start];
10422
+ }
10423
+ /**
10424
+ * The cycles THIS save closes, decided per strongly connected component — never
10425
+ * per reported DFS path, which names one back-edge per revisit and can report a
10426
+ * component's cycle by a path that avoids the candidate while the one through it
10427
+ * goes unreported. A component refuses when it holds a candidate node and a
10428
+ * cycle (more than one node, or a self-loop). Components without a candidate
10429
+ * node are not this save's cycles.
10430
+ */
10431
+ function findCandidateCycles(candidate, stored) {
10432
+ const edges = compositionEdges(candidate, stored);
10433
+ const prefix = `${require_composition.compositionSourceKey(candidate.target)}#`;
10434
+ return stronglyConnected(edges).flatMap((members) => {
10435
+ const start = members.find((n) => n.startsWith(prefix));
10436
+ if (start === void 0) return [];
10437
+ const component = new Set(members);
10438
+ const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
10439
+ if (members.length === 1 && !selfLoop) return [];
10440
+ return [{
10441
+ path: cycleThrough(start, component, edges),
10442
+ component
10443
+ }];
10444
+ });
10445
+ }
10446
+ //#endregion
10447
+ //#region src/composition/validate-composition.ts
10448
+ function buildCapabilityLookup(defs) {
10449
+ const byName = new Map(defs.map((d) => [d.name, d]));
10450
+ return (capName) => byName.get(capName) ?? null;
10451
+ }
10452
+ /** Caps every `BaseDevice` registers on itself; a second registration throws. */
10453
+ var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
10454
+ var PENDING_SELF = {
10455
+ addonId: require_composition.COMPOSER_ADDON_ID,
10456
+ stableId: `${require_composition.COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
10457
+ };
10458
+ /** The candidate's block id before its first save. Stored block ids are `randomUUID()`, so none can carry it. */
10459
+ var PENDING_BLOCK_ID = "(unsaved)";
10460
+ function composableCap(feature, at, lookupCap) {
10461
+ const def = lookupCap(feature.cap);
10462
+ if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
10463
+ 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`);
10464
+ const state = def.runtimeState;
10465
+ if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
10466
+ return {
10467
+ def,
10468
+ state,
10469
+ fields: describeTopLevelStateFields(state)
10470
+ };
10471
+ }
10472
+ /**
10473
+ * A capability BUILT by the composition (a new device, or one ADDED to an
10474
+ * existing device) must fit the device type, serve no command, and have every
10475
+ * required field. A REPLACE needs none of it: the cap is bound (a fact), the
10476
+ * native keeps serving its methods, and only the named fields are owned (D663).
10477
+ */
10478
+ function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
10479
+ const problems = [];
10480
+ 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(", ")})`));
10481
+ const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
10482
+ 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)`));
10483
+ const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
10484
+ for (const key of requiredTopLevelKeys(state)) {
10485
+ if (key === itemArrayPath) continue;
10486
+ 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`));
10487
+ else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
10488
+ }
10489
+ return problems;
10490
+ }
10491
+ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
10492
+ const { featureIndex, path, source } = field;
10493
+ const fieldAt = `features[${featureIndex}].fields.${path}`;
10494
+ if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
10495
+ const tf = fields.get(path);
10496
+ if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
10497
+ const kinds = sourceKinds(source, lookupCap);
10498
+ if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
10499
+ const role = roleOf(source);
10500
+ const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
10501
+ if (!unavailable.ok) return refused(unavailable.problem);
10502
+ const plan = {
10503
+ cap: def.name,
10504
+ path,
10505
+ featureIndex,
10506
+ kind: tf.kind,
10507
+ nullable: tf.nullable,
10508
+ required: tf.required,
10509
+ unavailable: unavailable.unavailable,
10510
+ role,
10511
+ source,
10512
+ reads: [...require_composition.fieldReads(source)]
10513
+ };
10514
+ const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
10515
+ return {
10516
+ problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
10517
+ fields: [plan],
10518
+ features: [],
10519
+ events: []
10520
+ };
10521
+ }
10522
+ function planFeature(feature, featureIndex, target, lookupCap) {
10523
+ const at = `features[${featureIndex}]`;
10524
+ const cap = composableCap(feature, at, lookupCap);
10525
+ if ("code" in cap) return refused(cap);
10526
+ const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
10527
+ if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
10528
+ const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
10529
+ featureIndex,
10530
+ path,
10531
+ source
10532
+ }, target.mode, lookupCap));
10533
+ const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
10534
+ const all = [...perField, ...items];
10535
+ const fields = all.flatMap((result) => result.fields);
10536
+ const featurePlan = {
10537
+ featureIndex,
10538
+ cap: cap.def.name,
10539
+ mode: target.mode
10540
+ };
10541
+ const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
10542
+ return {
10543
+ problems: [
10544
+ ...capProblems,
10545
+ ...all.flatMap((r) => r.problems),
10546
+ ...eventPlan.problems
10547
+ ],
10548
+ fields,
10549
+ features: [featurePlan],
10550
+ events: eventPlan.events
10551
+ };
10552
+ }
10553
+ /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
10554
+ function isNativeCap(target, cap) {
10555
+ const owner = target.nativeCaps.get(cap);
10556
+ return owner !== void 0 && owner !== "composer";
10557
+ }
10558
+ /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
10559
+ function planTargetOf(composition, target) {
10560
+ if (composition.target.kind === "new") return {
10561
+ kind: "new",
10562
+ type: composition.target.type
10563
+ };
10564
+ const key = require_composition.compositionSourceKey(composition.target.device);
10565
+ if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
10566
+ if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
10567
+ 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`);
10568
+ return {
10569
+ kind: "existing",
10570
+ found: target
10571
+ };
10572
+ }
10573
+ function featureTargetOf(target, cap) {
10574
+ if (target.kind === "new") return {
10575
+ mode: "new",
10576
+ deviceType: target.type
10577
+ };
10578
+ return {
10579
+ mode: isNativeCap(target.found, cap) ? "replace" : "add",
10580
+ deviceType: target.found.type
10581
+ };
10582
+ }
10583
+ /**
10584
+ * The static plan. `target` describes an EXISTING target device (one read);
10585
+ * it is ignored for a new one. On an existing target each feature REPLACES a
10586
+ * cap the device has natively, else ADDS it (D663).
10587
+ */
10588
+ function planComposition(composition, lookupCap, target) {
10589
+ const planTarget = planTargetOf(composition, target);
10590
+ if ("code" in planTarget) return refused(planTarget);
10591
+ const results = composition.features.map((feature, index) => {
10592
+ 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);
10593
+ return require_composition.compositionFeatureEnabled(feature) ? result : {
10594
+ problems: result.problems,
10595
+ fields: [],
10596
+ features: [],
10597
+ events: []
10598
+ };
10599
+ });
10600
+ return {
10601
+ problems: results.flatMap((r) => r.problems),
10602
+ fields: results.flatMap((r) => r.fields),
10603
+ features: results.flatMap((r) => r.features),
10604
+ events: results.flatMap((r) => r.events)
10605
+ };
10606
+ }
10607
+ function compositionSourceRefs(composition) {
10608
+ 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) : []);
10609
+ return [...new Map(refs.map((r) => [require_composition.compositionSourceKey(r), r])).values()];
10610
+ }
10611
+ /**
10612
+ * The candidate's device: the existing target itself, else `self`, else a
10613
+ * placeholder that cannot collide with a stored target. An existing target is
10614
+ * known even on create, so a second customization of it is refused on create too.
10615
+ */
10616
+ function candidateSelf(composition, ctx, plan) {
10617
+ return {
10618
+ ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
10619
+ native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
10620
+ replaced: replacedFieldKeys(plan)
10621
+ };
10622
+ }
10623
+ /**
10624
+ * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
10625
+ * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
10626
+ * derivation, so the graph reads both the same way.
10627
+ */
10628
+ function replacedFieldKeys(plan) {
10629
+ const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
10630
+ return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
10631
+ }
10632
+ /**
10633
+ * A read of the candidate's own device. Its own composed output (a new device's
10634
+ * field, an ADDED field) is the slice-1 own-field rule; on an existing target
10635
+ * every other field — replaced, or not composed at all — is the NATIVE value,
10636
+ * valid when the device serves that cap natively (D663, rule 5).
10637
+ */
10638
+ function selfReadProblems(read, at, fields, self) {
10639
+ const readKey = `${read.cap}.${read.fieldPath}`;
10640
+ if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
10641
+ if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
10642
+ if (isNativeCap(self.native, read.cap)) return [];
10643
+ return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
10644
+ }
10645
+ function sourceProblems(fields, ctx, self) {
10646
+ const selfKey = require_composition.compositionSourceKey(self.ref);
10647
+ return fields.flatMap((field) => field.reads.flatMap((read) => {
10648
+ const at = compositionFieldAt(field);
10649
+ const key = require_composition.compositionSourceKey(read.source);
10650
+ if (key === selfKey) return selfReadProblems(read, at, fields, self);
10651
+ const desc = ctx.sources.get(key);
10652
+ 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`)];
10653
+ if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
10654
+ if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
10655
+ return [];
10656
+ }));
10657
+ }
10658
+ /**
10659
+ * A cycle's node keys are `compositionReadKey` strings:
10660
+ * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
10661
+ * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
10662
+ * back out so the refusal can point at that exact field, matching the
10663
+ * documented `features[N].fields.<path>` convention. A cap name has no `.`,
10664
+ * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
10665
+ * (`items[desiccant].status`) is matched through `compositionFieldKey`.
10666
+ */
10667
+ function candidateFieldOfNode(nodeKey, candidateKey) {
10668
+ const prefix = `${candidateKey}#`;
10669
+ if (!nodeKey.startsWith(prefix)) return null;
10670
+ const rest = nodeKey.slice(prefix.length);
10671
+ const dot = rest.indexOf(".");
10672
+ if (dot === -1) return null;
10673
+ return {
10674
+ cap: rest.slice(0, dot),
10675
+ fieldPath: rest.slice(dot + 1)
10676
+ };
10677
+ }
10678
+ /** The first node of a cycle's path that is one of THIS composition's own
10679
+ * planned fields, as a documented-convention path; `'features'` when the
10680
+ * cycle runs entirely through other stored compositions. */
10681
+ function cyclePath(nodePath, candidateKey, fields) {
10682
+ for (const nodeKey of nodePath) {
10683
+ const hit = candidateFieldOfNode(nodeKey, candidateKey);
10684
+ if (hit === null) continue;
10685
+ const hitKey = `${hit.cap}.${hit.fieldPath}`;
10686
+ const field = fields.find((f) => compositionFieldKey(f) === hitKey);
10687
+ if (field !== void 0) return compositionFieldAt(field);
10688
+ }
10689
+ return "features";
10690
+ }
10691
+ /** The first unread device any of `nodes` belongs to; null when none does. */
10692
+ function unreadDeviceOf(nodes, unread) {
10693
+ for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
10694
+ key,
10695
+ error
10696
+ };
10697
+ return null;
10698
+ }
10699
+ /**
10700
+ * Cycle check runs against the CANDIDATE (this composition, as block
10701
+ * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
10702
+ * decided per strongly connected component (`findCandidateCycles`): a cyclic
10703
+ * component holding a candidate node refuses, with one representative cycle
10704
+ * through it; one without is not this save's cycle. A
10705
+ * stored composition of the same block is its old version; another block on
10706
+ * the same device, or two stored peers sharing a device, make
10707
+ * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
10708
+ * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
10709
+ */
10710
+ function cycleProblems(composition, ctx, fields, self) {
10711
+ const candidate = {
10712
+ blockId: ctx.blockId ?? "(unsaved)",
10713
+ target: self.ref,
10714
+ composition,
10715
+ replaced: self.replaced
10716
+ };
10717
+ const candidateKey = require_composition.compositionSourceKey(candidate.target);
10718
+ const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
10719
+ try {
10720
+ return findCandidateCycles(candidate, ctx.peers).map((c) => {
10721
+ const at = cyclePath(c.path, candidateKey, fields);
10722
+ const through = unreadDeviceOf([...c.component], unread);
10723
+ 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(" → ")}`);
10724
+ });
10725
+ } catch (err) {
10726
+ if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
10727
+ throw err;
10728
+ }
10729
+ }
10730
+ function validateComposition(composition, ctx) {
10731
+ const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
10732
+ const self = candidateSelf(composition, ctx, plan);
10733
+ const cycles = cycleProblems(composition, ctx, plan.fields, self);
10734
+ const problems = [
10735
+ ...plan.problems,
10736
+ ...sourceProblems(plan.fields, ctx, self),
10737
+ ...cycles
10738
+ ];
10739
+ return {
10740
+ ok: problems.length === 0,
10741
+ problems,
10742
+ fields: [...plan.fields],
10743
+ features: [...plan.features],
10744
+ events: [...plan.events]
10745
+ };
10746
+ }
10747
+ //#endregion
10748
+ //#region src/composition/composable-caps.ts
10749
+ /**
10750
+ * What a composition may BUILD on a device type (G2, D676): the caps a new
10751
+ * device of that type, or an existing one that lacks them, can be given. The
10752
+ * blockers are the validator's own refusal codes, and a spec compares this
10753
+ * listing with `planComposition` for every capability — the editor can never
10754
+ * offer a feature the save would refuse for the same reason.
10755
+ */
10756
+ var ComposableFieldSchema = zod.z.object({
10757
+ path: zod.z.string(),
10758
+ kind: CompositionFieldKindSchema,
10759
+ nullable: zod.z.boolean(),
10760
+ required: zod.z.boolean(),
10761
+ enumValues: zod.z.array(zod.z.string()).optional()
10762
+ });
10763
+ var ComposableCapabilitySchema = zod.z.object({
10764
+ cap: zod.z.string(),
10765
+ fields: zod.z.array(ComposableFieldSchema),
10766
+ /** Present for an item-array cap (`consumables.items`). */
10767
+ itemArray: zod.z.object({
10768
+ path: zod.z.string(),
10769
+ keyField: zod.z.string()
10770
+ }).optional()
10771
+ });
10772
+ var ComposableCapabilitiesSchema = zod.z.object({ caps: zod.z.array(ComposableCapabilitySchema) });
10773
+ /** Why a cap cannot be BUILT (new device, or added to an existing one) on `deviceType`; [] = it can. */
10774
+ function builtCapabilityBlockers(def, deviceType) {
10775
+ if (BASE_DEVICE_CAPS.has(def.name)) return ["not-composable"];
10776
+ const state = def.runtimeState;
10777
+ if (def.scope !== "device" || state === void 0) return ["not-composable"];
10778
+ const blockers = [];
10779
+ if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) blockers.push("wrong-device-type");
10780
+ if (Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).length > 0) blockers.push("has-commands");
10781
+ const fields = describeTopLevelStateFields(state);
10782
+ const itemArrayPath = def.status?.itemArray?.path ?? null;
10783
+ if (requiredTopLevelKeys(state).some((key) => key !== itemArrayPath && !fields.has(key))) blockers.push("not-composable");
10784
+ if (planCompositionEvents(def, {
10785
+ featureIndex: 0,
10786
+ cap: def.name,
10787
+ mode: "new"
10788
+ }, []).problems.some((p) => p.code === "not-composable") && !blockers.includes("not-composable")) blockers.push("not-composable");
10789
+ return blockers;
10790
+ }
10791
+ /** Every cap with no blocker, sorted by name. */
10792
+ function describeComposableCaps(defs, deviceType) {
10793
+ return defs.filter((def) => builtCapabilityBlockers(def, deviceType).length === 0).map((def) => {
10794
+ const state = def.runtimeState;
10795
+ const fields = state === void 0 ? [] : [...describeTopLevelStateFields(state).values()];
10796
+ const itemArray = def.status?.itemArray;
10797
+ return {
10798
+ cap: def.name,
10799
+ fields: fields.map((f) => ({
10800
+ path: f.path,
10801
+ kind: f.kind,
10802
+ nullable: f.nullable,
10803
+ required: f.required,
10804
+ ...f.enumValues === void 0 ? {} : { enumValues: [...f.enumValues] }
10805
+ })),
10806
+ ...itemArray === void 0 ? {} : { itemArray: {
10807
+ path: itemArray.path,
10808
+ keyField: itemArray.keyField
10809
+ } }
10810
+ };
10811
+ }).toSorted((a, b) => a.cap.localeCompare(b.cap));
10812
+ }
10813
+ //#endregion
10814
+ //#region src/capabilities/core-blocks.cap.ts
10815
+ /**
10816
+ * core-blocks — user-authored TypeScript, stored in the kernel and executed in
10817
+ * its own process.
10818
+ *
10819
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
10820
+ *
10821
+ * The first use is **owning devices without being a device provider**: a block
10822
+ * declares devices under a system or custom integration and drives their state,
10823
+ * with the same `ctx` an addon gets. Automations come later; nothing here
10824
+ * models a trigger.
10825
+ *
10826
+ * **Stated plainly, because it does not change by being true:** a block has an
10827
+ * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
10828
+ * with no review step. What makes that survivable is not a sandbox, it is
10829
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
10830
+ * so a block that throws or never returns is marked `failed` and visible
10831
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
10832
+ * must stay so.
10833
+ */
10834
+ /**
10835
+ * Prefix for a block's generated addon id AND its runner id — one addon, one
10836
+ * process, so the two are the same string (D2).
10837
+ *
10838
+ * It lives on the CONTRACT rather than beside the code that materialises the
10839
+ * package, because it is not an implementation detail: the operator surface
10840
+ * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
10841
+ * with no cap and no store of its own. Two copies of this string is one copy
10842
+ * too many — a drift would show as a logs pane that is simply always empty.
10843
+ */
10844
+ var CORE_BLOCK_ADDON_PREFIX = "core-block-";
10845
+ /**
10846
+ * The builtin's own addon id — and, because of that, the OWNER of the
10847
+ * integration every block's devices hang from.
10848
+ *
10849
+ * A block cannot own one. Every integration read is gated on the installed-addon
10850
+ * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
10851
+ * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
10852
+ * null forever while the write succeeds — one invisible, undeletable row per
10853
+ * start. `core-blocks` IS installed, so it reconciles one shared integration and
10854
+ * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
10855
+ *
10856
+ * Exported from the contract, next to the runner prefix and for the same reason:
10857
+ * a block reads it as
10858
+ * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
10859
+ * so a second copy of the string is a block that silently declares into nowhere.
10860
+ */
10861
+ var CORE_BLOCKS_ADDON_ID = "core-blocks";
10862
+ /** The addon/runner id a block's process runs under. */
10863
+ function coreBlockAddonId(blockId) {
10864
+ return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
10865
+ }
10866
+ /** The block id behind a generated addon/runner id, or null when the id belongs
10867
+ * to something else. The inverse of {@link coreBlockAddonId}. */
10868
+ function coreBlockIdFromAddonId(addonId) {
10869
+ if (!addonId.startsWith("core-block-")) return null;
10870
+ const rest = addonId.slice(11);
10871
+ return rest.length > 0 ? rest : null;
10872
+ }
10873
+ /** Where a block runs. The operator chooses — a block driving a device on an
10874
+ * agent is the reason placement is not fixed to the hub. */
10875
+ var CoreBlockPlacementSchema = zod.z.union([zod.z.literal("hub"), zod.z.string().min(1)]);
10876
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
10877
+ * a failing block reads the same way a failing addon does. `degraded` is a
10878
+ * composition on an existing device with a field handed back to the native
10879
+ * provider while its source is unavailable (S1b, D663); the error names it. */
10880
+ var CoreBlockStatusSchema = zod.z.enum([
10881
+ "stopped",
10882
+ "starting",
10883
+ "running",
10884
+ "degraded",
10885
+ "failed"
10886
+ ]);
10887
+ /** Client-authored fields. */
10888
+ var CoreBlockInputSchema = zod.z.object({
10889
+ name: zod.z.string().min(1).max(120),
10890
+ /** TypeScript source. Compiled server-side before it is ever stored — a
10891
+ * block that does not compile is a fork failure the operator would meet
10892
+ * minutes later, in a log, instead of in the editor. */
10893
+ code: zod.z.string().max(2e5),
10894
+ enabled: zod.z.boolean(),
10895
+ placement: CoreBlockPlacementSchema,
10896
+ /**
10897
+ * Integration the block's devices hang from. Absent = the system integration
10898
+ * blocks share. A block may declare its own instead.
10899
+ */
10900
+ integrationId: zod.z.string().optional()
10901
+ });
10902
+ /**
10903
+ * What a block IS (D659). Absent means `code` — every row stored before this
10904
+ * union keeps working unchanged.
10905
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
10906
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
10907
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
10908
+ */
10909
+ var CoreBlockSourceSchema = zod.z.discriminatedUnion("kind", [
10910
+ zod.z.object({ kind: zod.z.literal("code") }),
10911
+ zod.z.object({
10912
+ kind: zod.z.literal("recipe"),
10913
+ recipe: AutomationRecipeSchema
10914
+ }),
10915
+ zod.z.object({
10916
+ kind: zod.z.literal("composition"),
10917
+ composition: require_composition.CompositionSchema
10918
+ })
10919
+ ]);
10920
+ /**
10921
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
10922
+ *
10923
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
10924
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
10925
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
10926
+ * client-sent name by name.
10927
+ */
10928
+ var CompositionBlockInputSchema = zod.z.object({
10929
+ name: zod.z.string().min(1).max(120).optional(),
10930
+ enabled: zod.z.boolean(),
10931
+ composition: require_composition.CompositionSchema
10932
+ });
10933
+ /** A stored block. */
10934
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
10935
+ id: zod.z.string(),
10936
+ createdAt: zod.z.number(),
10937
+ updatedAt: zod.z.number(),
10938
+ /** Server-stamped author. */
10939
+ createdBy: zod.z.string(),
10940
+ status: CoreBlockStatusSchema,
10941
+ /**
10942
+ * Why the block is not running, when it is not. The operator's ONLY window
10943
+ * into a block that failed at load — a block that is silently absent is the
10944
+ * failure mode this whole feature has to avoid.
10945
+ */
10946
+ lastError: zod.z.string().optional(),
10947
+ /** Ms epoch of the last state change. */
10948
+ lastChangedAt: zod.z.number(),
10949
+ /** What the block is. Absent means `code` (D659). */
10950
+ source: CoreBlockSourceSchema.optional()
10951
+ });
10952
+ function coreBlockSourceKind(block) {
10953
+ return block.source?.kind ?? "code";
10954
+ }
10955
+ function coreBlockComposition(block) {
10956
+ return block.source?.kind === "composition" ? block.source.composition : null;
10957
+ }
10958
+ /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
10959
+ var CompositionStateReadSchema = zod.z.discriminatedUnion("kind", [zod.z.object({
10960
+ kind: zod.z.literal("answered"),
10961
+ state: CompositionBlockStateSchema.nullable()
10962
+ }), zod.z.object({
10963
+ kind: zod.z.literal("unreachable"),
10964
+ error: zod.z.string()
10965
+ })]);
10966
+ /** A stored composition block and what the composer says about it. */
10967
+ var CompositionViewSchema = zod.z.object({
10968
+ block: CoreBlockSchema,
10969
+ state: CompositionStateReadSchema
10970
+ });
10971
+ /**
10972
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
10973
+ * table has not been READ: an empty answer then would make "not yet known"
10974
+ * look like "none", and invite the operator to recreate what exists (D315).
10975
+ */
10976
+ var CompositionsNotLoadedSchema = zod.z.object({
10977
+ state: zod.z.literal("not-loaded"),
10978
+ reason: zod.z.string()
10979
+ });
10980
+ var CompositionListReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10981
+ state: zod.z.literal("loaded"),
10982
+ views: zod.z.array(CompositionViewSchema)
10983
+ }), CompositionsNotLoadedSchema]);
10984
+ var CompositionGetReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10985
+ state: zod.z.literal("loaded"),
10986
+ view: CompositionViewSchema.nullable()
10987
+ }), CompositionsNotLoadedSchema]);
10988
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
10989
+ var CompositionCustomizationReadSchema = zod.z.discriminatedUnion("state", [zod.z.object({
10990
+ state: zod.z.literal("loaded"),
10991
+ view: CompositionViewSchema.nullable()
10992
+ }), CompositionsNotLoadedSchema]);
10993
+ /** What a compile attempt produced. */
10994
+ var CoreBlockCompileResultSchema = zod.z.object({
10995
+ ok: zod.z.boolean(),
10996
+ /** Present when `ok` is false — the first error, in the author's words. */
10997
+ error: zod.z.string().optional(),
10998
+ line: zod.z.number().optional(),
10999
+ column: zod.z.number().optional()
11000
+ });
11001
+ var coreBlocksCapability = {
11002
+ name: "core-blocks",
11003
+ scope: "system",
11004
+ mode: "singleton",
11005
+ methods: {
11006
+ list: require_sleep.method(zod.z.object({}), zod.z.object({ blocks: zod.z.array(CoreBlockSchema) }), { auth: "admin" }),
11007
+ get: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
11008
+ /**
11009
+ * Create a block. The code is COMPILED first: storing source that does not
11010
+ * compile turns an editor error into a fork failure the operator meets
11011
+ * minutes later in a log.
11012
+ */
11013
+ create: require_sleep.method(zod.z.object({ block: CoreBlockInputSchema }), zod.z.object({ block: CoreBlockSchema }), {
11014
+ kind: "mutation",
11015
+ auth: "admin",
11016
+ caller: "required"
11017
+ }),
11018
+ update: require_sleep.method(zod.z.object({
11019
+ blockId: zod.z.string(),
11020
+ block: CoreBlockInputSchema.partial()
11021
+ }), zod.z.object({ block: CoreBlockSchema }), {
11022
+ kind: "mutation",
11023
+ auth: "admin",
11024
+ caller: "required"
11025
+ }),
11026
+ delete: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ success: zod.z.literal(true) }), {
11027
+ kind: "mutation",
11028
+ auth: "admin"
11029
+ }),
11030
+ setEnabled: require_sleep.method(zod.z.object({
11031
+ blockId: zod.z.string(),
11032
+ enabled: zod.z.boolean()
11033
+ }), zod.z.object({ block: CoreBlockSchema }), {
11034
+ kind: "mutation",
11035
+ auth: "admin"
11036
+ }),
11037
+ /**
11038
+ * Stop this block's runner and let the supervisor bring it back.
11039
+ *
11040
+ * **It stores nothing.** There is no "restart requested" flag and no second
11041
+ * lifecycle field: the supervisor already stops and respawns a runner when
11042
+ * the code hash changes, and this exposes that same path deliberately
11043
+ * (D62 — a switch writes the authority that already owned the function).
11044
+ * The next reconcile pass remains the authority, so a restart that lost a
11045
+ * race is corrected within one pass rather than leaving a stale flag behind.
11046
+ *
11047
+ * Refused for a DISABLED block: nothing would come back, and a button that
11048
+ * silently does nothing is worse than one that says why.
11049
+ */
11050
+ restart: require_sleep.method(zod.z.object({ blockId: zod.z.string() }), zod.z.object({ block: CoreBlockSchema }), {
11051
+ kind: "mutation",
11052
+ auth: "admin"
11053
+ }),
11054
+ /**
11055
+ * Type-check without saving — what the editor calls as the author types, so
11056
+ * the compiler's verdict is the same one the server will reach.
11057
+ */
11058
+ compile: require_sleep.method(zod.z.object({ code: zod.z.string() }), CoreBlockCompileResultSchema, {
10038
11059
  kind: "mutation",
10039
11060
  auth: "admin"
10040
11061
  }),
@@ -10087,6 +11108,14 @@ var coreBlocksCapability = {
10087
11108
  stableId: zod.z.string().min(1)
10088
11109
  }), CompositionCustomizationReadSchema, { auth: "admin" }),
10089
11110
  /**
11111
+ * The capabilities a composition may BUILD on a device of `deviceType`
11112
+ * (a new device, or added to an existing one), with their fields — the
11113
+ * editor's feature list and the presets' availability (G2, D676). Built
11114
+ * from the validator's own blockers; a replace of a BOUND cap is not
11115
+ * listed here (the device's bindings say what it has).
11116
+ */
11117
+ listComposableCaps: require_sleep.method(zod.z.object({ deviceType: zod.z.enum(require_err_msg.DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }),
11118
+ /**
10090
11119
  * Validate without saving — the same verdict the save reaches. A mutation
10091
11120
  * for its body size; it writes nothing, so its access is `view`.
10092
11121
  */
@@ -23077,6 +24106,25 @@ var pipelineAnalyticsCapability = {
23077
24106
  */
23078
24107
  listArchivedDebugNotes: require_sleep.method(ArchivedDebugNoteQueryInputSchema, zod.z.array(ArchivedDebugNoteSchema).readonly(), { kind: "query" }),
23079
24108
  /**
24109
+ * Tracking-bench ground truth (D681): "this track and that one are the
24110
+ * same subject" / "different subjects". One row per unordered pair —
24111
+ * re-marking replaces it — and a mark that contradicts an existing chain
24112
+ * is REFUSED with the chain named. Writing a mark also sets `debug` on
24113
+ * both tracks. `protected`, matching `setTrackFlags`: marking is done from
24114
+ * the viewer's track detail by an authenticated non-admin session.
24115
+ */
24116
+ setTrackRelation: require_sleep.method(SetTrackRelationInputSchema, TrackRelationSchema, { kind: "mutation" }),
24117
+ /** Remove the mark between two tracks. */
24118
+ clearTrackRelation: require_sleep.method(ClearTrackRelationInputSchema, ClearTrackRelationResultSchema, { kind: "mutation" }),
24119
+ /**
24120
+ * A track that is not ONE subject: `mixed` (two subjects, optionally
24121
+ * switching at `switchAtMs`) or `not-a-subject`. `pure` clears the mark
24122
+ * and answers `{ purity: null }` (D681).
24123
+ */
24124
+ setTrackPurity: require_sleep.method(SetTrackPurityInputSchema, SetTrackPurityResultSchema, { kind: "mutation" }),
24125
+ /** A camera's marks, optionally one track's, windowed by track start. */
24126
+ listTrackRelations: require_sleep.method(ListTrackRelationsInputSchema, ListTrackRelationsResultSchema, { kind: "query" }),
24127
+ /**
23080
24128
  * Birth decisions — one row per resolved confirmation-gate verdict (D409).
23081
24129
  *
23082
24130
  * The READ half of a measurement, and only that: it gates nothing and no
@@ -26310,6 +27358,16 @@ var NodeInferenceDevicesSchema = zod.z.object({
26310
27358
  * Strictly the control plane: never sees a frame, never calls inference,
26311
27359
  * never drains queues. The runner does that work locally on each agent.
26312
27360
  */
27361
+ /**
27362
+ * The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
27363
+ * camera's attached sensors (D672). `config: null` removes the attachment —
27364
+ * the camera reads `not-configured` again, never `unreadable`.
27365
+ */
27366
+ var SetCameraSensorsInputSchema = zod.z.object({
27367
+ deviceId: zod.z.number().int().nonnegative(),
27368
+ /** `null` removes the attachment: the camera goes back to `not-configured`. */
27369
+ config: CameraSensorsConfigSchema.nullable()
27370
+ });
26313
27371
  var pipelineOrchestratorCapability = {
26314
27372
  name: "pipeline-orchestrator",
26315
27373
  scope: "system",
@@ -26775,6 +27833,18 @@ var pipelineOrchestratorCapability = {
26775
27833
  */
26776
27834
  getCameraStatuses: require_sleep.method(zod.z.object({ deviceIds: zod.z.array(zod.z.number()).optional() }), zod.z.array(CameraStatusSchema).readonly()),
26777
27835
  /**
27836
+ * Attach sensors to a camera, or detach them with `config: null` (D672
27837
+ * configuration, G5/D676). Written to the orchestrator's per-device store
27838
+ * under the same key `applyDeviceSettingsPatch` writes, and announced as a
27839
+ * settings change so the camera-sensors bridge refreshes. Refused by name
27840
+ * — nothing written — for a device that cannot be read, does not exist,
27841
+ * is not a camera, or names itself as its own sensor.
27842
+ */
27843
+ setCameraSensors: require_sleep.method(SetCameraSensorsInputSchema, zod.z.object({ success: zod.z.literal(true) }), {
27844
+ kind: "mutation",
27845
+ auth: "admin"
27846
+ }),
27847
+ /**
26778
27848
  * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
26779
27849
  * the addon process and returns immediately with the job snapshot — the
26780
27850
  * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
@@ -44680,569 +45750,366 @@ function createDeviceExtensionProvider(options) {
44680
45750
  };
44681
45751
  }
44682
45752
  //#endregion
44683
- //#region src/composition/composition-graph.ts
44684
- /**
44685
- * The field graph across every composition, and the cycles in it (D659).
44686
- *
44687
- * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
44688
- * edge points from a composed field to each field it reads. A cycle, direct or
44689
- * through another composition, is refused at save by name (spec §2).
44690
- *
44691
- * The graph is built for ONE save: the CANDIDATE composition (the one being
44692
- * validated) plus every STORED composition. The candidate replaces its own old
44693
- * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
44694
- * introduces or removes a cycle must never depend on array order. A DIFFERENT
44695
- * block targeting the candidate's device is refused by name: one composition
44696
- * per device (D663). Two different stored peers that (wrongly) target the same
44697
- * device are corrupted state, not a cycle to resolve by picking one silently;
44698
- * that is refused by name too (D391).
44699
- *
44700
- * A customization of an EXISTING device reads that device's NATIVE value for a
44701
- * field it replaces or does not write ("Invert state" reads the `on` it
44702
- * replaces): such a self-read is no edge, so it is never a cycle (D663).
44703
- * Another composition reading that field reads the customizer's output.
44704
- *
44705
- * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
44706
- * marks a node `done` after its first expansion) — not every elementary cycle
44707
- * through a shared node, so the path it names may AVOID the candidate while
44708
- * one through it goes unreported. What a save refuses is therefore decided per
44709
- * strongly connected component (`findCandidateCycles`, C-1). A dotted
44710
- * `fieldPath` is matched only as an exact string: a write to `state.value` and
44711
- * a read of `state` would not connect (no nested field writes exist yet).
44712
- *
44713
- * An item leaf is its own node, `…#consumables.items[desiccant].status`
44714
- * (`featureFieldEntries`), so two items sharing an inner path never merge. No
44715
- * READ can name an array element (`describeStateField` never describes one),
44716
- * so an item node is write-only: its edges to sources are what the check needs.
44717
- */
44718
- /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
44719
- var CompositionGraphError = class extends Error {};
44720
- /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
44721
- function writtenFields(peer) {
44722
- return new Set(peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
44723
- }
44724
- /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
44725
- function readsComposedValue(peer, written, read) {
44726
- if (require_composition.compositionSourceKey(read.source) !== require_composition.compositionSourceKey(peer.target)) return true;
44727
- const field = `${read.cap}.${read.fieldPath}`;
44728
- return written.has(field) && peer.replaced?.has(field) !== true;
44729
- }
44730
- function peerEdges(peer) {
44731
- const written = writtenFields(peer);
44732
- return peer.composition.features.flatMap((feature) => !require_composition.compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : require_composition.featureFieldEntries(feature).map(({ fieldPath, source }) => [require_composition.compositionReadKey({
44733
- source: peer.target,
44734
- cap: feature.cap,
44735
- fieldPath
44736
- }), require_composition.fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(require_composition.compositionReadKey)]));
44737
- }
44738
- /**
44739
- * Every peer that actually takes part in the graph: each stored peer minus the
44740
- * candidate's own old version (same block id — the drop happens before any
44741
- * edge is built, so its old edges never enter the map), plus the candidate
44742
- * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
44743
- * block targets the candidate's device, or two stored peers share a device.
44744
- */
44745
- function resolvedPeers(candidate, stored) {
44746
- const candidateKey = require_composition.compositionSourceKey(candidate.target);
44747
- const survivors = /* @__PURE__ */ new Map();
44748
- for (const peer of stored) {
44749
- if (peer.blockId === candidate.blockId) continue;
44750
- const key = require_composition.compositionSourceKey(peer.target);
44751
- 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`);
44752
- const clash = survivors.get(key);
44753
- if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
44754
- survivors.set(key, peer);
44755
- }
44756
- return [...survivors.values(), candidate];
44757
- }
44758
- function compositionEdges(candidate, stored) {
44759
- const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
44760
- return new Map(edges);
44761
- }
44762
- function findCompositionCycles(candidate, stored) {
44763
- const edges = compositionEdges(candidate, stored);
44764
- const marks = /* @__PURE__ */ new Map();
44765
- const found = /* @__PURE__ */ new Map();
44766
- const visit = (node, stack) => {
44767
- const mark = marks.get(node);
44768
- if (mark === "done") return;
44769
- if (mark === "visiting") {
44770
- const path = [...stack.slice(stack.indexOf(node)), node];
44771
- const id = [...new Set(path)].toSorted().join("|");
44772
- if (!found.has(id)) found.set(id, { path });
44773
- return;
44774
- }
44775
- marks.set(node, "visiting");
44776
- for (const next of edges.get(node) ?? []) visit(next, [...stack, node]);
44777
- marks.set(node, "done");
44778
- };
44779
- for (const node of edges.keys()) visit(node, []);
44780
- return [...found.values()];
44781
- }
44782
- /** Tarjan's strongly connected components over every node of the graph (sources included). */
44783
- function stronglyConnected(edges) {
44784
- const index = /* @__PURE__ */ new Map();
44785
- const low = /* @__PURE__ */ new Map();
44786
- const onStack = /* @__PURE__ */ new Set();
44787
- const stack = [];
44788
- const components = [];
44789
- const connect = (node) => {
44790
- index.set(node, index.size);
44791
- low.set(node, index.get(node) ?? 0);
44792
- stack.push(node);
44793
- onStack.add(node);
44794
- for (const next of edges.get(node) ?? []) if (!index.has(next)) {
44795
- connect(next);
44796
- low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
44797
- } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
44798
- if (low.get(node) !== index.get(node)) return;
44799
- const component = [];
44800
- for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
44801
- onStack.delete(member);
44802
- component.push(member);
44803
- if (member === node) break;
44804
- }
44805
- components.push(component);
44806
- };
44807
- for (const node of edges.keys()) if (!index.has(node)) connect(node);
44808
- return components;
44809
- }
44810
- /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
44811
- function cycleThrough(start, component, edges) {
44812
- const parent = /* @__PURE__ */ new Map();
44813
- const queue = [start];
44814
- for (let head = 0; head < queue.length; head += 1) {
44815
- const node = queue[head];
44816
- if (node === void 0) break;
44817
- for (const next of edges.get(node) ?? []) {
44818
- if (next === start) {
44819
- const chain = [];
44820
- for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
44821
- return [
44822
- start,
44823
- ...chain.reverse(),
44824
- start
44825
- ];
44826
- }
44827
- if (!component.has(next) || parent.has(next)) continue;
44828
- parent.set(next, node);
44829
- queue.push(next);
44830
- }
44831
- }
44832
- return [start, start];
44833
- }
45753
+ //#region src/units/unit-table.ts
44834
45754
  /**
44835
- * The cycles THIS save closes, decided per strongly connected component — never
44836
- * per reported DFS path, which names one back-edge per revisit and can report a
44837
- * component's cycle by a path that avoids the candidate while the one through it
44838
- * goes unreported. A component refuses when it holds a candidate node and a
44839
- * cycle (more than one node, or a self-loop). Components without a candidate
44840
- * node are not this save's cycles.
45755
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
45756
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
45757
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
45758
+ * decimals). K derives from `°C = K − 273.15`.
44841
45759
  */
44842
- function findCandidateCycles(candidate, stored) {
44843
- const edges = compositionEdges(candidate, stored);
44844
- const prefix = `${require_composition.compositionSourceKey(candidate.target)}#`;
44845
- return stronglyConnected(edges).flatMap((members) => {
44846
- const start = members.find((n) => n.startsWith(prefix));
44847
- if (start === void 0) return [];
44848
- const component = new Set(members);
44849
- const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
44850
- if (members.length === 1 && !selfLoop) return [];
44851
- return [{
44852
- path: cycleThrough(start, component, edges),
44853
- component
44854
- }];
44855
- });
44856
- }
44857
- //#endregion
44858
- //#region src/composition/plan-result.ts
44859
- function problem(code, path, message) {
44860
- return {
44861
- code,
44862
- path,
44863
- message
44864
- };
44865
- }
44866
- function refused(...problems) {
44867
- return {
44868
- problems,
44869
- fields: [],
44870
- features: [],
44871
- events: []
44872
- };
44873
- }
44874
- /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
44875
- function compositionFieldAt(field) {
44876
- const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
44877
- return `features[${field.featureIndex}].${inner}.${field.path}`;
44878
- }
44879
- //#endregion
44880
- //#region src/composition/expression-kinds.ts
44881
- var NUMBER = {
44882
- kind: "fixed",
44883
- result: ["number"]
44884
- };
44885
- var BOOLEAN = {
44886
- kind: "fixed",
44887
- result: ["boolean"]
44888
- };
44889
- /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
44890
- var BUILTIN_RESULT_KINDS = Object.freeze({
44891
- min: NUMBER,
44892
- max: NUMBER,
44893
- abs: NUMBER,
44894
- floor: NUMBER,
44895
- ceil: NUMBER,
44896
- sqrt: NUMBER,
44897
- round: NUMBER,
44898
- pow: NUMBER,
44899
- clamp: NUMBER,
44900
- avg: NUMBER,
44901
- sum: NUMBER,
44902
- age: NUMBER,
44903
- convert: NUMBER,
44904
- coalesce: { kind: "coalesce" },
44905
- any: BOOLEAN,
44906
- all: BOOLEAN,
44907
- within: BOOLEAN,
44908
- latest: {
44909
- kind: "fixed",
44910
- result: ["number", "null"]
45760
+ var UNIT_TABLE = {
45761
+ "°C": {
45762
+ dimension: "temperature",
45763
+ factor: 1,
45764
+ offset: 0
45765
+ },
45766
+ "°F": {
45767
+ dimension: "temperature",
45768
+ factor: 5 / 9,
45769
+ offset: -160 / 9
45770
+ },
45771
+ K: {
45772
+ dimension: "temperature",
45773
+ factor: 1,
45774
+ offset: -273.15
45775
+ },
45776
+ hPa: {
45777
+ dimension: "pressure",
45778
+ factor: 1,
45779
+ offset: 0
45780
+ },
45781
+ kPa: {
45782
+ dimension: "pressure",
45783
+ factor: 10,
45784
+ offset: 0
45785
+ },
45786
+ Pa: {
45787
+ dimension: "pressure",
45788
+ factor: .01,
45789
+ offset: 0
45790
+ },
45791
+ mbar: {
45792
+ dimension: "pressure",
45793
+ factor: 1,
45794
+ offset: 0
45795
+ },
45796
+ bar: {
45797
+ dimension: "pressure",
45798
+ factor: 1e3,
45799
+ offset: 0
45800
+ },
45801
+ inHg: {
45802
+ dimension: "pressure",
45803
+ factor: 33.8639,
45804
+ offset: 0
45805
+ },
45806
+ mmHg: {
45807
+ dimension: "pressure",
45808
+ factor: 1.33322,
45809
+ offset: 0
45810
+ },
45811
+ psi: {
45812
+ dimension: "pressure",
45813
+ factor: 68.9476,
45814
+ offset: 0
45815
+ },
45816
+ "m/s": {
45817
+ dimension: "speed",
45818
+ factor: 1,
45819
+ offset: 0
45820
+ },
45821
+ "km/h": {
45822
+ dimension: "speed",
45823
+ factor: 1 / 3.6,
45824
+ offset: 0
45825
+ },
45826
+ mph: {
45827
+ dimension: "speed",
45828
+ factor: .44704,
45829
+ offset: 0
45830
+ },
45831
+ kn: {
45832
+ dimension: "speed",
45833
+ factor: .514444,
45834
+ offset: 0
45835
+ },
45836
+ "mm/h": {
45837
+ dimension: "precipitation-rate",
45838
+ factor: 1,
45839
+ offset: 0
45840
+ },
45841
+ "in/h": {
45842
+ dimension: "precipitation-rate",
45843
+ factor: 25.4,
45844
+ offset: 0
45845
+ },
45846
+ m: {
45847
+ dimension: "length",
45848
+ factor: 1,
45849
+ offset: 0
45850
+ },
45851
+ mm: {
45852
+ dimension: "length",
45853
+ factor: .001,
45854
+ offset: 0
45855
+ },
45856
+ cm: {
45857
+ dimension: "length",
45858
+ factor: .01,
45859
+ offset: 0
45860
+ },
45861
+ km: {
45862
+ dimension: "length",
45863
+ factor: 1e3,
45864
+ offset: 0
44911
45865
  },
44912
- number: {
44913
- kind: "fixed",
44914
- result: ["number", "null"]
45866
+ in: {
45867
+ dimension: "length",
45868
+ factor: .0254,
45869
+ offset: 0
44915
45870
  },
44916
- rose: {
44917
- kind: "fixed",
44918
- result: ["number", "null"]
45871
+ ft: {
45872
+ dimension: "length",
45873
+ factor: .3048,
45874
+ offset: 0
44919
45875
  },
44920
- count: NUMBER
44921
- });
44922
- var KindInferenceError = class extends Error {};
44923
- function kindOfValue(value) {
44924
- if (value === null) return "null";
44925
- if (typeof value === "number") return "number";
44926
- if (typeof value === "string") return "string";
44927
- return "boolean";
44928
- }
44929
- function only(kind) {
44930
- return new Set([kind]);
44931
- }
44932
- function union(...sets) {
44933
- return new Set(sets.flatMap((s) => [...s]));
44934
- }
44935
- function withoutNull(set) {
44936
- return new Set([...set].filter((k) => k !== "null"));
44937
- }
44938
- function binaryKinds(op, left, right) {
44939
- if (op === "+") {
44940
- const all = [...left, ...right];
44941
- if (all.every((k) => k === "string")) return only("string");
44942
- if (all.every((k) => k === "number")) return only("number");
44943
- return new Set(["number", "string"]);
45876
+ mi: {
45877
+ dimension: "length",
45878
+ factor: 1609.344,
45879
+ offset: 0
45880
+ },
45881
+ lx: {
45882
+ dimension: "illuminance",
45883
+ factor: 1,
45884
+ offset: 0
45885
+ },
45886
+ "W/m²": {
45887
+ dimension: "irradiance",
45888
+ factor: 1,
45889
+ offset: 0
45890
+ },
45891
+ W: {
45892
+ dimension: "power",
45893
+ factor: 1,
45894
+ offset: 0
45895
+ },
45896
+ kW: {
45897
+ dimension: "power",
45898
+ factor: 1e3,
45899
+ offset: 0
45900
+ },
45901
+ VA: {
45902
+ dimension: "apparent-power",
45903
+ factor: 1,
45904
+ offset: 0
45905
+ },
45906
+ Wh: {
45907
+ dimension: "energy",
45908
+ factor: 1,
45909
+ offset: 0
45910
+ },
45911
+ kWh: {
45912
+ dimension: "energy",
45913
+ factor: 1e3,
45914
+ offset: 0
45915
+ },
45916
+ MWh: {
45917
+ dimension: "energy",
45918
+ factor: 1e6,
45919
+ offset: 0
45920
+ },
45921
+ V: {
45922
+ dimension: "voltage",
45923
+ factor: 1,
45924
+ offset: 0
45925
+ },
45926
+ mV: {
45927
+ dimension: "voltage",
45928
+ factor: .001,
45929
+ offset: 0
45930
+ },
45931
+ A: {
45932
+ dimension: "current",
45933
+ factor: 1,
45934
+ offset: 0
45935
+ },
45936
+ mA: {
45937
+ dimension: "current",
45938
+ factor: .001,
45939
+ offset: 0
45940
+ },
45941
+ ppm: {
45942
+ dimension: "concentration-volume",
45943
+ factor: 1,
45944
+ offset: 0
45945
+ },
45946
+ ppb: {
45947
+ dimension: "concentration-volume",
45948
+ factor: .001,
45949
+ offset: 0
45950
+ },
45951
+ "µg/m³": {
45952
+ dimension: "concentration-mass",
45953
+ factor: 1,
45954
+ offset: 0
45955
+ },
45956
+ "mg/m³": {
45957
+ dimension: "concentration-mass",
45958
+ factor: 1e3,
45959
+ offset: 0
45960
+ },
45961
+ kg: {
45962
+ dimension: "mass",
45963
+ factor: 1,
45964
+ offset: 0
45965
+ },
45966
+ g: {
45967
+ dimension: "mass",
45968
+ factor: .001,
45969
+ offset: 0
45970
+ },
45971
+ lb: {
45972
+ dimension: "mass",
45973
+ factor: .453592,
45974
+ offset: 0
45975
+ },
45976
+ oz: {
45977
+ dimension: "mass",
45978
+ factor: .0283495,
45979
+ offset: 0
45980
+ },
45981
+ "%": {
45982
+ dimension: "percentage",
45983
+ factor: 1,
45984
+ offset: 0
44944
45985
  }
44945
- if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
44946
- return only("boolean");
44947
- }
44948
- /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
44949
- function arityRange(builtin) {
44950
- if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
44951
- if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
44952
- return `${builtin.minArgs}–${builtin.maxArgs}`;
44953
- }
44954
- function callKinds(callee, args) {
44955
- const builtin = require_composition.EXPRESSION_BUILTINS[callee];
44956
- 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)}`);
44957
- const rule = BUILTIN_RESULT_KINDS[callee];
44958
- if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
44959
- if (rule.kind === "fixed") return new Set(rule.result);
44960
- const nonNull = union(...args.map(withoutNull));
44961
- return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
44962
- }
44963
- function kindsOf(node, bindings) {
44964
- switch (node.kind) {
44965
- case "literal": return only(kindOfValue(node.value));
44966
- case "identifier": {
44967
- if (node.name === "now") return only("number");
44968
- const bound = bindings.get(node.name);
44969
- if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
44970
- return bound;
44971
- }
44972
- case "unary": return only(node.op === "!" ? "boolean" : "number");
44973
- case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
44974
- case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
44975
- case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
44976
- case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
45986
+ };
45987
+ //#endregion
45988
+ //#region src/units/convert.ts
45989
+ /**
45990
+ * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
45991
+ *
45992
+ * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
45993
+ * from the `try*` variant) whenever a unit is unknown or the two units belong
45994
+ * to different dimensions — cross-dimension conversion is deliberately
45995
+ * impossible so a render override can never make the value and its unit label
45996
+ * disagree.
45997
+ *
45998
+ * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
45999
+ * calling here — the table keys only canonical spellings.
46000
+ */
46001
+ /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
46002
+ * units + a machine-readable `reason` for callers that branch on the cause. */
46003
+ var UnitConversionError = class extends Error {
46004
+ from;
46005
+ to;
46006
+ reason;
46007
+ constructor(from, to, reason) {
46008
+ super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
46009
+ this.name = "UnitConversionError";
46010
+ this.from = from;
46011
+ this.to = to;
46012
+ this.reason = reason;
44977
46013
  }
46014
+ };
46015
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
46016
+ function specFor(unit) {
46017
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
44978
46018
  }
44979
- function nodeContainsCall(node, callee) {
44980
- switch (node.kind) {
44981
- case "literal":
44982
- case "identifier": return false;
44983
- case "unary": return nodeContainsCall(node.operand, callee);
44984
- case "binary":
44985
- case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
44986
- case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
44987
- case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
44988
- }
46019
+ /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
46020
+ function unitDimension(unit) {
46021
+ return specFor(unit)?.dimension;
44989
46022
  }
44990
- function expressionContainsBuiltinCall(expr, callee) {
44991
- const compiled = require_composition.compileExpressionSafe(expr);
44992
- return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
46023
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
46024
+ function toCanonical(value, spec) {
46025
+ return value * spec.factor + spec.offset;
44993
46026
  }
44994
- function inferExpressionKinds(input) {
44995
- const compiled = require_composition.compileExpressionSafe(input.expr);
44996
- if (!compiled.ok) return {
44997
- ok: false,
44998
- error: compiled.error
44999
- };
45000
- try {
45001
- return {
45002
- ok: true,
45003
- kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
45004
- };
45005
- } catch (err) {
45006
- if (err instanceof KindInferenceError) return {
45007
- ok: false,
45008
- error: err.message
45009
- };
45010
- throw err;
45011
- }
46027
+ /** Canonical-unit value → the unit described by `spec`. */
46028
+ function fromCanonical(canonical, spec) {
46029
+ return (canonical - spec.offset) / spec.factor;
45012
46030
  }
45013
- //#endregion
45014
- //#region src/composition/field-kinds.ts
45015
46031
  /**
45016
- * The kind half of planning a composition field (D659): which kinds a source
45017
- * can produce, whether they fit a target field, and the field's role.
45018
- * Shared by the top-level planner (`validate-composition.ts`) and the item
45019
- * planner (`composition-items.ts`). Internal to the composition module — NOT
45020
- * in the barrel: these are planner internals, not a published contract.
46032
+ * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
46033
+ * Throws `UnitConversionError` when either unit is unknown or the units belong
46034
+ * to different dimensions.
45021
46035
  */
45022
- function readKinds(read, lookupCap) {
45023
- const def = lookupCap(read.cap);
45024
- const schema = def?.runtimeState ?? def?.status?.schema;
45025
- if (def === null || schema === void 0) return {
45026
- ok: false,
45027
- code: "source-field-unknown",
45028
- message: `no capability \`${read.cap}\` with state to read`
45029
- };
45030
- const field = describeStateField(schema, read.fieldPath);
45031
- if (field === null) return {
45032
- ok: false,
45033
- code: "source-field-unknown",
45034
- message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
45035
- };
45036
- const base = field.kind === "enum" ? "string" : field.kind;
45037
- return {
45038
- ok: true,
45039
- kinds: new Set(field.nullable ? [base, "null"] : [base])
45040
- };
45041
- }
45042
- function sourceKinds(source, lookupCap) {
45043
- if (source.kind === "from") return readKinds(source, lookupCap);
45044
- if (source.kind === "code") return {
45045
- ok: false,
45046
- code: "reserved-variant",
45047
- message: "code fields arrive in slice 4"
45048
- };
45049
- const bindingKinds = /* @__PURE__ */ new Map();
45050
- for (const [name, binding] of Object.entries(source.bindings)) {
45051
- if (binding.kind === "literal") {
45052
- bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
45053
- continue;
45054
- }
45055
- const kinds = readKinds(binding, lookupCap);
45056
- if (!kinds.ok) return {
45057
- ...kinds,
45058
- message: `binding \`${name}\`: ${kinds.message}`
45059
- };
45060
- bindingKinds.set(name, kinds.kinds);
45061
- }
45062
- const inferred = inferExpressionKinds({
45063
- expr: source.expr,
45064
- bindingKinds
45065
- });
45066
- return inferred.ok ? {
45067
- ok: true,
45068
- kinds: inferred.kinds
45069
- } : {
45070
- ok: false,
45071
- code: "expression-invalid",
45072
- message: inferred.error
45073
- };
45074
- }
45075
- function compatibility(cap, target, kinds, at) {
45076
- const allowed = target.kind === "enum" ? "string" : target.kind;
45077
- const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
45078
- const problems = [];
45079
- if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
45080
- 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`));
45081
- return problems;
46036
+ function convertUnit(value, from, to) {
46037
+ if (from === to) return value;
46038
+ const fromSpec = specFor(from);
46039
+ if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
46040
+ const toSpec = specFor(to);
46041
+ if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
46042
+ if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
46043
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45082
46044
  }
45083
46045
  /**
45084
- * A string a source produces WITHOUT reading any device — a field-level
45085
- * `expression` source whose compiled AST is a bare string literal (`"red"`,
45086
- * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
45087
- * the kind level (a `from` read of an enum-typed source field, a computed
45088
- * expression) is NOT checked against the target's `enumValues` here: the
45089
- * kind inference already collapsed it to the coarse `'string'` kind, losing
45090
- * whichever enum it actually came from, so membership can only be judged at
45091
- * write time against the runtime-state schema. Known limitation — see the
45092
- * fix report for Task 6 round 1.
46046
+ * Graceful variant for the render path: returns the converted value, or
46047
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
46048
+ * Never throws — a render caller falls back to the raw value + source unit.
45093
46049
  */
45094
- function literalStringValue(source) {
45095
- if (source.kind !== "expression") return null;
45096
- const compiled = require_composition.compileExpressionSafe(source.expr);
45097
- if (!compiled.ok) return null;
45098
- const ast = compiled.parsed.ast;
45099
- return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
46050
+ function tryConvertUnit(value, from, to) {
46051
+ if (from === to) return value;
46052
+ const fromSpec = specFor(from);
46053
+ if (!fromSpec) return void 0;
46054
+ const toSpec = specFor(to);
46055
+ if (!toSpec) return void 0;
46056
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
46057
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45100
46058
  }
45101
46059
  /**
45102
- * An enum target whose source is a save-time-KNOWN string literal outside its
45103
- * `enumValues` is refused now rather than left to fail the write later. A
45104
- * dynamic source (a `from` read, a non-literal expression) is left to
45105
- * runtime — see {@link literalStringValue}.
46060
+ * Whether `value` can be converted between `from` and `to` (same dimension,
46061
+ * both known). Cheap boolean probe for the UI's same-dimension unit picker and
46062
+ * the resolver's convertibility branch — compute ONCE so a unit label and its
46063
+ * value math can never disagree.
45106
46064
  */
45107
- function enumLiteralProblem(cap, target, source, at) {
45108
- if (target.kind !== "enum" || target.enumValues === void 0) return null;
45109
- const value = literalStringValue(source);
45110
- if (value === null || target.enumValues.includes(value)) return null;
45111
- const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
45112
- return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
45113
- }
45114
- function roleOf(source) {
45115
- if (source.kind !== "expression") return "derived";
45116
- if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
45117
- if (require_composition.fieldReads(source).length > 0) return "derived";
45118
- const compiled = require_composition.compileExpressionSafe(source.expr);
45119
- return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
46065
+ function canConvertUnit(from, to) {
46066
+ if (from === to) return true;
46067
+ const fromDim = unitDimension(from);
46068
+ if (fromDim === void 0) return false;
46069
+ return fromDim === unitDimension(to);
45120
46070
  }
45121
46071
  /**
45122
- * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
45123
- * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
45124
- * own offline, and D393 forbids a stale number, so on an existing target a
45125
- * field that READS a source never gets `offline`.
46072
+ * Every known unit spelling in a dimension (table insertion order — canonical
46073
+ * unit first). Powers the UI's same-dimension unit picker.
45126
46074
  */
45127
- function unavailableFor(tf, role, mode, cap, at) {
45128
- if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
45129
- ok: true,
45130
- unavailable: "release"
45131
- };
45132
- if (mode === "new" || role !== "derived") return {
45133
- ok: true,
45134
- unavailable: tf.nullable ? "null" : "offline"
45135
- };
45136
- if (tf.nullable) return {
45137
- ok: true,
45138
- unavailable: "null"
45139
- };
45140
- if (tf.enumValues?.includes("unknown") === true) return {
45141
- ok: true,
45142
- unavailable: "unknown"
45143
- };
45144
- return {
45145
- ok: false,
45146
- 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)`)
45147
- };
45148
- }
45149
- //#endregion
45150
- //#region src/composition/composition-items.ts
45151
- /** Item fields the composer writes itself, with the value it writes. */
45152
- var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
45153
- function reservedItemFields(itemArray) {
45154
- return new Set([
45155
- itemArray.keyField,
45156
- ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
45157
- ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
45158
- ]);
45159
- }
45160
- /** A required leaf under an optional parent is required only once something writes that parent. */
45161
- function leafRequired(itemArray, path, written) {
45162
- const described = describeStateField(itemArray.itemSchema, path);
45163
- if (described === null || !described.required) return false;
45164
- return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
45165
- }
45166
- function missingLeaves({ capName, itemArray, reserved }, item, at) {
45167
- const written = Object.keys(item.fields);
45168
- return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
45169
- if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
45170
- if (!leafRequired(itemArray, leaf.path, written)) return [];
45171
- const described = describeStateField(itemArray.itemSchema, leaf.path);
45172
- if (described === null || described.nullable) return [];
45173
- return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
45174
- });
45175
- }
45176
- function planItemField(ctx, leaf) {
45177
- const { capName, itemArray, featureIndex, lookupCap } = ctx;
45178
- const { key, label, path, source } = leaf;
45179
- const at = `features[${featureIndex}].items.${key}.fields.${path}`;
45180
- 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`));
45181
- const tf = describeStateField(itemArray.itemSchema, path);
45182
- if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
45183
- const kinds = sourceKinds(source, lookupCap);
45184
- if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
45185
- const role = roleOf(source);
45186
- const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
45187
- if (!unavailable.ok) return refused(unavailable.problem);
45188
- const plan = {
45189
- cap: capName,
45190
- path,
45191
- item: {
45192
- arrayPath: itemArray.path,
45193
- key,
45194
- label
45195
- },
45196
- featureIndex,
45197
- kind: tf.kind,
45198
- nullable: tf.nullable,
45199
- required: tf.required,
45200
- unavailable: unavailable.unavailable,
45201
- role,
45202
- source,
45203
- reads: [...require_composition.fieldReads(source)]
45204
- };
45205
- const enumProblem = enumLiteralProblem(capName, tf, source, at);
45206
- return {
45207
- problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
45208
- fields: [plan],
45209
- features: [],
45210
- events: []
45211
- };
45212
- }
45213
- function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
45214
- const at = `features[${featureIndex}].items`;
45215
- const itemArray = def.status?.itemArray;
45216
- if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
45217
- 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`));
45218
- const ctx = {
45219
- capName: def.name,
45220
- itemArray,
45221
- reserved: reservedItemFields(itemArray),
45222
- featureIndex,
45223
- lookupCap,
45224
- mode
45225
- };
45226
- const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
45227
- problems: missingLeaves(ctx, item, `${at}.${key}`),
45228
- fields: [],
45229
- features: [],
45230
- events: []
45231
- }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
45232
- key,
45233
- label: item.label,
45234
- path,
45235
- source
45236
- }))]);
45237
- return {
45238
- problems: results.flatMap((r) => r.problems),
45239
- fields: results.flatMap((r) => r.fields),
45240
- features: [],
45241
- events: []
45242
- };
46075
+ function unitsForDimension(dimension) {
46076
+ const out = [];
46077
+ for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
46078
+ return out;
45243
46079
  }
45244
46080
  //#endregion
45245
46081
  //#region src/composition/evaluate-field.ts
46082
+ /**
46083
+ * One composed field, evaluated from what its sources say NOW (D659).
46084
+ *
46085
+ * Pure. The composer calls it on every source change, and the slice-5 editor
46086
+ * will call it for its live preview, so both see the same answer.
46087
+ *
46088
+ * The rules (spec §2):
46089
+ * - a source that is offline or unreadable keeps its last value and is marked
46090
+ * STALE; the field is stale and keeps computing (D49);
46091
+ * - a source that is GONE makes the field UNAVAILABLE, `fatal`, and never a
46092
+ * plausible number (D393);
46093
+ * - a source that is unavailable but NOT gone (e.g. a cap unregistered at
46094
+ * runtime, R7b) makes the field UNAVAILABLE, non-fatal, `value: null`: it
46095
+ * may come back on its own, no block failure is raised for it;
46096
+ * - an expression that fails on a value makes the field unavailable for that
46097
+ * value, and NOT fatal: the next value may compute.
46098
+ *
46099
+ * `confirmed` (D663, D49): whether a NON-fatal unavailability is an ANSWER — a
46100
+ * cap the provider unregistered and a re-read agreed, a field the source
46101
+ * reported without, an expression that failed on a value — or only the
46102
+ * absence of one (a failed read, a first or empty read, a source not tracked
46103
+ * yet). Only a confirmed unavailability may hand a `release`-mode field back
46104
+ * to the native provider; an unconfirmed one holds it on its last value.
46105
+ * Absent means NOT confirmed. Fatal is always confirmed.
46106
+ *
46107
+ * When an expression has more than one unavailable binding, a FATAL one always
46108
+ * wins the outcome, regardless of position: every binding is scanned before
46109
+ * the field is judged, so an earlier non-fatal binding (e.g. a cap that is
46110
+ * momentarily unregistered) can never hide a later fatal one (a deleted
46111
+ * source) behind an early return.
46112
+ */
45246
46113
  function describeRead(read) {
45247
46114
  return `${require_composition.compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
45248
46115
  }
@@ -45309,7 +46176,10 @@ function expressionOutcome(source, read, now, stateful) {
45309
46176
  }
45310
46177
  const deadlines = [];
45311
46178
  const result = require_composition.evaluateExpressionSource(source.expr, values, now, {
45312
- hooks: { noteDeadline: (at) => deadlines.push(at) },
46179
+ hooks: {
46180
+ noteDeadline: (at) => deadlines.push(at),
46181
+ convert: (value, from, to) => tryConvertUnit(value, normalizeUnit(from) ?? from, normalizeUnit(to) ?? to) ?? null
46182
+ },
45313
46183
  ...stateful === void 0 ? {} : { invokeStateful: (invocation) => require_composition.evaluateStatefulExpressionCall(stateful.callPathPrefix === void 0 ? invocation : {
45314
46184
  ...invocation,
45315
46185
  path: `${stateful.callPathPrefix}@${invocation.path}`
@@ -45548,304 +46418,71 @@ function feederCustomizationExample(sources) {
45548
46418
  });
45549
46419
  }
45550
46420
  //#endregion
45551
- //#region src/composition/validate-composition.ts
45552
- function buildCapabilityLookup(defs) {
45553
- const byName = new Map(defs.map((d) => [d.name, d]));
45554
- return (capName) => byName.get(capName) ?? null;
45555
- }
45556
- /** Caps every `BaseDevice` registers on itself; a second registration throws. */
45557
- var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
45558
- var PENDING_SELF = {
45559
- addonId: require_composition.COMPOSER_ADDON_ID,
45560
- stableId: `${require_composition.COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
45561
- };
45562
- /** The candidate's block id before its first save. Stored block ids are `randomUUID()`, so none can carry it. */
45563
- var PENDING_BLOCK_ID = "(unsaved)";
45564
- function composableCap(feature, at, lookupCap) {
45565
- const def = lookupCap(feature.cap);
45566
- if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
45567
- 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`);
45568
- const state = def.runtimeState;
45569
- if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
45570
- return {
45571
- def,
45572
- state,
45573
- fields: describeTopLevelStateFields(state)
45574
- };
45575
- }
45576
- /**
45577
- * A capability BUILT by the composition (a new device, or one ADDED to an
45578
- * existing device) must fit the device type, serve no command, and have every
45579
- * required field. A REPLACE needs none of it: the cap is bound (a fact), the
45580
- * native keeps serving its methods, and only the named fields are owned (D663).
45581
- */
45582
- function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
45583
- const problems = [];
45584
- 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(", ")})`));
45585
- const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
45586
- 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)`));
45587
- const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
45588
- for (const key of requiredTopLevelKeys(state)) {
45589
- if (key === itemArrayPath) continue;
45590
- 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`));
45591
- else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
45592
- }
45593
- return problems;
45594
- }
45595
- function planTopLevelField({ def, fields }, field, mode, lookupCap) {
45596
- const { featureIndex, path, source } = field;
45597
- const fieldAt = `features[${featureIndex}].fields.${path}`;
45598
- if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
45599
- const tf = fields.get(path);
45600
- if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
45601
- const kinds = sourceKinds(source, lookupCap);
45602
- if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
45603
- const role = roleOf(source);
45604
- const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
45605
- if (!unavailable.ok) return refused(unavailable.problem);
45606
- const plan = {
45607
- cap: def.name,
45608
- path,
45609
- featureIndex,
45610
- kind: tf.kind,
45611
- nullable: tf.nullable,
45612
- required: tf.required,
45613
- unavailable: unavailable.unavailable,
45614
- role,
45615
- source,
45616
- reads: [...require_composition.fieldReads(source)]
45617
- };
45618
- const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
45619
- return {
45620
- problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
45621
- fields: [plan],
45622
- features: [],
45623
- events: []
45624
- };
45625
- }
45626
- function planFeature(feature, featureIndex, target, lookupCap) {
45627
- const at = `features[${featureIndex}]`;
45628
- const cap = composableCap(feature, at, lookupCap);
45629
- if ("code" in cap) return refused(cap);
45630
- const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
45631
- if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
45632
- const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
45633
- featureIndex,
45634
- path,
45635
- source
45636
- }, target.mode, lookupCap));
45637
- const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
45638
- const all = [...perField, ...items];
45639
- const fields = all.flatMap((result) => result.fields);
45640
- const featurePlan = {
45641
- featureIndex,
45642
- cap: cap.def.name,
45643
- mode: target.mode
45644
- };
45645
- const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
45646
- return {
45647
- problems: [
45648
- ...capProblems,
45649
- ...all.flatMap((r) => r.problems),
45650
- ...eventPlan.problems
45651
- ],
45652
- fields,
45653
- features: [featurePlan],
45654
- events: eventPlan.events
45655
- };
45656
- }
45657
- /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
45658
- function isNativeCap(target, cap) {
45659
- const owner = target.nativeCaps.get(cap);
45660
- return owner !== void 0 && owner !== "composer";
45661
- }
45662
- /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
45663
- function planTargetOf(composition, target) {
45664
- if (composition.target.kind === "new") return {
45665
- kind: "new",
45666
- type: composition.target.type
45667
- };
45668
- const key = require_composition.compositionSourceKey(composition.target.device);
45669
- if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
45670
- if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
45671
- 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`);
45672
- return {
45673
- kind: "existing",
45674
- found: target
45675
- };
45676
- }
45677
- function featureTargetOf(target, cap) {
45678
- if (target.kind === "new") return {
45679
- mode: "new",
45680
- deviceType: target.type
45681
- };
45682
- return {
45683
- mode: isNativeCap(target.found, cap) ? "replace" : "add",
45684
- deviceType: target.found.type
45685
- };
46421
+ //#region src/composition/presets/doorbell-feature.ts
46422
+ /**
46423
+ * The doorbell of a camera as a customization (D671). ONE builder: the
46424
+ * virtual-doorbell migration and the admin UI's "Use as doorbell" preset both
46425
+ * call it, so a preset can never write a shape the migration would refuse as
46426
+ * "an unrelated existing block".
46427
+ */
46428
+ /** A rise closer than this to the previous one is the same press (D671). */
46429
+ var DOORBELL_RISE_DEBOUNCE_MS = 2e3;
46430
+ /** Every source cap of `source` that `SOURCE_CAP_ACTIVE_FIELD` knows AND the device has bound. */
46431
+ function doorbellFeatureSources(source, boundCaps) {
46432
+ 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) => {
46433
+ const activeField = require_composition.SOURCE_CAP_ACTIVE_FIELD[cap];
46434
+ const changedAtField = require_composition.SOURCE_CAP_CHANGED_AT_FIELD[cap];
46435
+ return activeField === void 0 || changedAtField === void 0 ? [] : [{
46436
+ source,
46437
+ cap,
46438
+ activeField,
46439
+ changedAtField
46440
+ }];
46441
+ });
45686
46442
  }
45687
- /**
45688
- * The static plan. `target` describes an EXISTING target device (one read);
45689
- * it is ignored for a new one. On an existing target each feature REPLACES a
45690
- * cap the device has natively, else ADDS it (D663).
45691
- */
45692
- function planComposition(composition, lookupCap, target) {
45693
- const planTarget = planTargetOf(composition, target);
45694
- if ("code" in planTarget) return refused(planTarget);
45695
- const results = composition.features.map((feature, index) => {
45696
- 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);
45697
- return require_composition.compositionFeatureEnabled(feature) ? result : {
45698
- problems: result.problems,
45699
- fields: [],
45700
- features: [],
45701
- events: []
46443
+ /** The D671 feature, byte for byte what the migration writes. Throws on zero sources. */
46444
+ function buildDoorbellCompositionFeature(input) {
46445
+ const sources = [...input.sources].toSorted((a, b) => `${a.source.addonId}/${a.source.stableId}#${a.cap}`.localeCompare(`${b.source.addonId}/${b.source.stableId}#${b.cap}`));
46446
+ if (sources.length === 0) throw new Error("doorbell composition needs at least one source");
46447
+ const edgeBindings = {};
46448
+ sources.forEach((entry, index) => {
46449
+ edgeBindings[`active${index}`] = {
46450
+ kind: "from",
46451
+ source: entry.source,
46452
+ cap: entry.cap,
46453
+ fieldPath: entry.activeField
46454
+ };
46455
+ edgeBindings[`changed${index}`] = {
46456
+ kind: "from",
46457
+ source: entry.source,
46458
+ cap: entry.cap,
46459
+ fieldPath: entry.changedAtField
45702
46460
  };
45703
46461
  });
45704
46462
  return {
45705
- problems: results.flatMap((r) => r.problems),
45706
- fields: results.flatMap((r) => r.fields),
45707
- features: results.flatMap((r) => r.features),
45708
- events: results.flatMap((r) => r.events)
45709
- };
45710
- }
45711
- function compositionSourceRefs(composition) {
45712
- 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) : []);
45713
- return [...new Map(refs.map((r) => [require_composition.compositionSourceKey(r), r])).values()];
45714
- }
45715
- /**
45716
- * The candidate's device: the existing target itself, else `self`, else a
45717
- * placeholder that cannot collide with a stored target. An existing target is
45718
- * known even on create, so a second customization of it is refused on create too.
45719
- */
45720
- function candidateSelf(composition, ctx, plan) {
45721
- return {
45722
- ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
45723
- native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
45724
- replaced: replacedFieldKeys(plan)
45725
- };
45726
- }
45727
- /**
45728
- * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
45729
- * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
45730
- * derivation, so the graph reads both the same way.
45731
- */
45732
- function replacedFieldKeys(plan) {
45733
- const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
45734
- return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
45735
- }
45736
- /**
45737
- * A read of the candidate's own device. Its own composed output (a new device's
45738
- * field, an ADDED field) is the slice-1 own-field rule; on an existing target
45739
- * every other field — replaced, or not composed at all — is the NATIVE value,
45740
- * valid when the device serves that cap natively (D663, rule 5).
45741
- */
45742
- function selfReadProblems(read, at, fields, self) {
45743
- const readKey = `${read.cap}.${read.fieldPath}`;
45744
- if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
45745
- if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
45746
- if (isNativeCap(self.native, read.cap)) return [];
45747
- return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
45748
- }
45749
- function sourceProblems(fields, ctx, self) {
45750
- const selfKey = require_composition.compositionSourceKey(self.ref);
45751
- return fields.flatMap((field) => field.reads.flatMap((read) => {
45752
- const at = compositionFieldAt(field);
45753
- const key = require_composition.compositionSourceKey(read.source);
45754
- if (key === selfKey) return selfReadProblems(read, at, fields, self);
45755
- const desc = ctx.sources.get(key);
45756
- 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`)];
45757
- if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
45758
- if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
45759
- return [];
45760
- }));
45761
- }
45762
- /**
45763
- * A cycle's node keys are `compositionReadKey` strings:
45764
- * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
45765
- * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
45766
- * back out so the refusal can point at that exact field, matching the
45767
- * documented `features[N].fields.<path>` convention. A cap name has no `.`,
45768
- * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
45769
- * (`items[desiccant].status`) is matched through `compositionFieldKey`.
45770
- */
45771
- function candidateFieldOfNode(nodeKey, candidateKey) {
45772
- const prefix = `${candidateKey}#`;
45773
- if (!nodeKey.startsWith(prefix)) return null;
45774
- const rest = nodeKey.slice(prefix.length);
45775
- const dot = rest.indexOf(".");
45776
- if (dot === -1) return null;
45777
- return {
45778
- cap: rest.slice(0, dot),
45779
- fieldPath: rest.slice(dot + 1)
45780
- };
45781
- }
45782
- /** The first node of a cycle's path that is one of THIS composition's own
45783
- * planned fields, as a documented-convention path; `'features'` when the
45784
- * cycle runs entirely through other stored compositions. */
45785
- function cyclePath(nodePath, candidateKey, fields) {
45786
- for (const nodeKey of nodePath) {
45787
- const hit = candidateFieldOfNode(nodeKey, candidateKey);
45788
- if (hit === null) continue;
45789
- const hitKey = `${hit.cap}.${hit.fieldPath}`;
45790
- const field = fields.find((f) => compositionFieldKey(f) === hitKey);
45791
- if (field !== void 0) return compositionFieldAt(field);
45792
- }
45793
- return "features";
45794
- }
45795
- /** The first unread device any of `nodes` belongs to; null when none does. */
45796
- function unreadDeviceOf(nodes, unread) {
45797
- for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
45798
- key,
45799
- error
45800
- };
45801
- return null;
45802
- }
45803
- /**
45804
- * Cycle check runs against the CANDIDATE (this composition, as block
45805
- * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
45806
- * decided per strongly connected component (`findCandidateCycles`): a cyclic
45807
- * component holding a candidate node refuses, with one representative cycle
45808
- * through it; one without is not this save's cycle. A
45809
- * stored composition of the same block is its old version; another block on
45810
- * the same device, or two stored peers sharing a device, make
45811
- * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
45812
- * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
45813
- */
45814
- function cycleProblems(composition, ctx, fields, self) {
45815
- const candidate = {
45816
- blockId: ctx.blockId ?? "(unsaved)",
45817
- target: self.ref,
45818
- composition,
45819
- replaced: self.replaced
45820
- };
45821
- const candidateKey = require_composition.compositionSourceKey(candidate.target);
45822
- const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
45823
- try {
45824
- return findCandidateCycles(candidate, ctx.peers).map((c) => {
45825
- const at = cyclePath(c.path, candidateKey, fields);
45826
- const through = unreadDeviceOf([...c.component], unread);
45827
- 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(" → ")}`);
45828
- });
45829
- } catch (err) {
45830
- if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
45831
- throw err;
45832
- }
45833
- }
45834
- function validateComposition(composition, ctx) {
45835
- const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
45836
- const self = candidateSelf(composition, ctx, plan);
45837
- const cycles = cycleProblems(composition, ctx, plan.fields, self);
45838
- const problems = [
45839
- ...plan.problems,
45840
- ...sourceProblems(plan.fields, ctx, self),
45841
- ...cycles
45842
- ];
45843
- return {
45844
- ok: problems.length === 0,
45845
- problems,
45846
- fields: [...plan.fields],
45847
- features: [...plan.features],
45848
- events: [...plan.events]
46463
+ kind: "fields",
46464
+ cap: "doorbell",
46465
+ enabled: input.enabled,
46466
+ fields: {
46467
+ lastPressedAt: {
46468
+ kind: "expression",
46469
+ expr: `latest(${sources.map((_, index) => `rose(active${index}, changed${index}, ${DOORBELL_RISE_DEBOUNCE_MS})`).join(", ")})`,
46470
+ bindings: edgeBindings
46471
+ },
46472
+ pressCountSinceStart: {
46473
+ kind: "expression",
46474
+ expr: "count(lastPressedAt)",
46475
+ bindings: { lastPressedAt: {
46476
+ kind: "from",
46477
+ source: {
46478
+ addonId: input.camera.addonId,
46479
+ stableId: input.camera.stableId
46480
+ },
46481
+ cap: "doorbell",
46482
+ fieldPath: "lastPressedAt"
46483
+ } }
46484
+ }
46485
+ }
45849
46486
  };
45850
46487
  }
45851
46488
  //#endregion
@@ -52971,6 +53608,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
52971
53608
  addonId: null,
52972
53609
  access: "view"
52973
53610
  },
53611
+ "coreBlocks.listComposableCaps": {
53612
+ capName: "core-blocks",
53613
+ capScope: "system",
53614
+ addonId: null,
53615
+ access: "view"
53616
+ },
52974
53617
  "coreBlocks.listCompositions": {
52975
53618
  capName: "core-blocks",
52976
53619
  capScope: "system",
@@ -55785,6 +56428,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
55785
56428
  addonId: null,
55786
56429
  access: "create"
55787
56430
  },
56431
+ "pipelineAnalytics.clearTrackRelation": {
56432
+ capName: "pipeline-analytics",
56433
+ capScope: "device",
56434
+ addonId: null,
56435
+ access: "delete"
56436
+ },
55788
56437
  "pipelineAnalytics.clearTracks": {
55789
56438
  capName: "pipeline-analytics",
55790
56439
  capScope: "device",
@@ -56037,6 +56686,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
56037
56686
  addonId: null,
56038
56687
  access: "view"
56039
56688
  },
56689
+ "pipelineAnalytics.listTrackRelations": {
56690
+ capName: "pipeline-analytics",
56691
+ capScope: "device",
56692
+ addonId: null,
56693
+ access: "view"
56694
+ },
56040
56695
  "pipelineAnalytics.listTracks": {
56041
56696
  capName: "pipeline-analytics",
56042
56697
  capScope: "device",
@@ -56151,6 +56806,18 @@ var METHOD_ACCESS_MAP = Object.freeze({
56151
56806
  addonId: null,
56152
56807
  access: "create"
56153
56808
  },
56809
+ "pipelineAnalytics.setTrackPurity": {
56810
+ capName: "pipeline-analytics",
56811
+ capScope: "device",
56812
+ addonId: null,
56813
+ access: "create"
56814
+ },
56815
+ "pipelineAnalytics.setTrackRelation": {
56816
+ capName: "pipeline-analytics",
56817
+ capScope: "device",
56818
+ addonId: null,
56819
+ access: "create"
56820
+ },
56154
56821
  "pipelineAnalytics.startStorageMigrationMove": {
56155
56822
  capName: "pipeline-analytics",
56156
56823
  capScope: "device",
@@ -56655,6 +57322,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
56655
57322
  addonId: null,
56656
57323
  access: "create"
56657
57324
  },
57325
+ "pipelineOrchestrator.setCameraSensors": {
57326
+ capName: "pipeline-orchestrator",
57327
+ capScope: "system",
57328
+ addonId: null,
57329
+ access: "create"
57330
+ },
56658
57331
  "pipelineOrchestrator.setCameraStepOverride": {
56659
57332
  capName: "pipeline-orchestrator",
56660
57333
  capScope: "system",
@@ -60400,6 +61073,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
60400
61073
  form: "single",
60401
61074
  optional: false
60402
61075
  }],
61076
+ "pipelineAnalytics.clearTrackRelation": [{
61077
+ name: "deviceId",
61078
+ form: "single",
61079
+ optional: false
61080
+ }],
60403
61081
  "pipelineAnalytics.clearTracks": [{
60404
61082
  name: "deviceId",
60405
61083
  form: "single",
@@ -60550,6 +61228,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
60550
61228
  form: "single",
60551
61229
  optional: false
60552
61230
  }],
61231
+ "pipelineAnalytics.listTrackRelations": [{
61232
+ name: "deviceId",
61233
+ form: "single",
61234
+ optional: false
61235
+ }],
60553
61236
  "pipelineAnalytics.listTracks": [{
60554
61237
  name: "deviceId",
60555
61238
  form: "single",
@@ -60620,6 +61303,16 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
60620
61303
  form: "single",
60621
61304
  optional: false
60622
61305
  }],
61306
+ "pipelineAnalytics.setTrackPurity": [{
61307
+ name: "deviceId",
61308
+ form: "single",
61309
+ optional: false
61310
+ }],
61311
+ "pipelineAnalytics.setTrackRelation": [{
61312
+ name: "deviceId",
61313
+ form: "single",
61314
+ optional: false
61315
+ }],
60623
61316
  "pipelineAnalytics.wipeAllAnalytics": [{
60624
61317
  name: "deviceId",
60625
61318
  form: "single",
@@ -60700,6 +61393,11 @@ var METHOD_DEVICE_SELECTORS = Object.freeze({
60700
61393
  form: "single",
60701
61394
  optional: false
60702
61395
  }],
61396
+ "pipelineOrchestrator.setCameraSensors": [{
61397
+ name: "deviceId",
61398
+ form: "single",
61399
+ optional: false
61400
+ }],
60703
61401
  "pipelineOrchestrator.setCameraStepOverride": [{
60704
61402
  name: "deviceId",
60705
61403
  form: "single",
@@ -61522,6 +62220,7 @@ var SYSTEM_SCOPE_DEVICE_METHODS = [
61522
62220
  "pipelineOrchestrator.getPipelineDevicePin",
61523
62221
  "pipelineOrchestrator.resolvePipeline",
61524
62222
  "pipelineOrchestrator.setCameraPipelineForAgent",
62223
+ "pipelineOrchestrator.setCameraSensors",
61525
62224
  "pipelineOrchestrator.setCameraStepOverride",
61526
62225
  "pipelineOrchestrator.setCameraStepToggle",
61527
62226
  "pipelineOrchestrator.setCameraSwitch",
@@ -62174,6 +62873,7 @@ function createSystemProxy(api) {
62174
62873
  getComposition: (input) => dispatch("coreBlocks", "getComposition", "query", input),
62175
62874
  listCompositions: (input) => dispatch("coreBlocks", "listCompositions", "query", input),
62176
62875
  getCustomization: (input) => dispatch("coreBlocks", "getCustomization", "query", input),
62876
+ listComposableCaps: (input) => dispatch("coreBlocks", "listComposableCaps", "query", input),
62177
62877
  validateComposition: (input) => dispatch("coreBlocks", "validateComposition", "mutation", input)
62178
62878
  },
62179
62879
  decoder: {
@@ -62505,6 +63205,7 @@ function createSystemProxy(api) {
62505
63205
  getNodeInferenceDevices: (input) => dispatch("pipelineOrchestrator", "getNodeInferenceDevices", "query", input),
62506
63206
  resetNodePipelineDefaults: (input) => dispatch("pipelineOrchestrator", "resetNodePipelineDefaults", "mutation", input),
62507
63207
  getCameraStatuses: (input) => dispatch("pipelineOrchestrator", "getCameraStatuses", "query", input),
63208
+ setCameraSensors: (input) => dispatch("pipelineOrchestrator", "setCameraSensors", "mutation", input),
62508
63209
  reconcileFromDisk: (input) => dispatch("pipelineOrchestrator", "reconcileFromDisk", "mutation", input),
62509
63210
  getReconcileFromDiskStatus: (input) => dispatch("pipelineOrchestrator", "getReconcileFromDiskStatus", "query", input),
62510
63211
  listTemplates: (input) => dispatch("pipelineOrchestrator", "listTemplates", "query", input),
@@ -66699,334 +67400,6 @@ var DEVICE_TYPE_INFO = { ["camera"]: {
66699
67400
  icon: "camera"
66700
67401
  } };
66701
67402
  //#endregion
66702
- //#region src/units/unit-table.ts
66703
- /**
66704
- * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
66705
- * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
66706
- * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
66707
- * decimals). K derives from `°C = K − 273.15`.
66708
- */
66709
- var UNIT_TABLE = {
66710
- "°C": {
66711
- dimension: "temperature",
66712
- factor: 1,
66713
- offset: 0
66714
- },
66715
- "°F": {
66716
- dimension: "temperature",
66717
- factor: 5 / 9,
66718
- offset: -160 / 9
66719
- },
66720
- K: {
66721
- dimension: "temperature",
66722
- factor: 1,
66723
- offset: -273.15
66724
- },
66725
- hPa: {
66726
- dimension: "pressure",
66727
- factor: 1,
66728
- offset: 0
66729
- },
66730
- kPa: {
66731
- dimension: "pressure",
66732
- factor: 10,
66733
- offset: 0
66734
- },
66735
- Pa: {
66736
- dimension: "pressure",
66737
- factor: .01,
66738
- offset: 0
66739
- },
66740
- mbar: {
66741
- dimension: "pressure",
66742
- factor: 1,
66743
- offset: 0
66744
- },
66745
- bar: {
66746
- dimension: "pressure",
66747
- factor: 1e3,
66748
- offset: 0
66749
- },
66750
- inHg: {
66751
- dimension: "pressure",
66752
- factor: 33.8639,
66753
- offset: 0
66754
- },
66755
- mmHg: {
66756
- dimension: "pressure",
66757
- factor: 1.33322,
66758
- offset: 0
66759
- },
66760
- psi: {
66761
- dimension: "pressure",
66762
- factor: 68.9476,
66763
- offset: 0
66764
- },
66765
- "m/s": {
66766
- dimension: "speed",
66767
- factor: 1,
66768
- offset: 0
66769
- },
66770
- "km/h": {
66771
- dimension: "speed",
66772
- factor: 1 / 3.6,
66773
- offset: 0
66774
- },
66775
- mph: {
66776
- dimension: "speed",
66777
- factor: .44704,
66778
- offset: 0
66779
- },
66780
- kn: {
66781
- dimension: "speed",
66782
- factor: .514444,
66783
- offset: 0
66784
- },
66785
- "mm/h": {
66786
- dimension: "precipitation-rate",
66787
- factor: 1,
66788
- offset: 0
66789
- },
66790
- "in/h": {
66791
- dimension: "precipitation-rate",
66792
- factor: 25.4,
66793
- offset: 0
66794
- },
66795
- m: {
66796
- dimension: "length",
66797
- factor: 1,
66798
- offset: 0
66799
- },
66800
- mm: {
66801
- dimension: "length",
66802
- factor: .001,
66803
- offset: 0
66804
- },
66805
- cm: {
66806
- dimension: "length",
66807
- factor: .01,
66808
- offset: 0
66809
- },
66810
- km: {
66811
- dimension: "length",
66812
- factor: 1e3,
66813
- offset: 0
66814
- },
66815
- in: {
66816
- dimension: "length",
66817
- factor: .0254,
66818
- offset: 0
66819
- },
66820
- ft: {
66821
- dimension: "length",
66822
- factor: .3048,
66823
- offset: 0
66824
- },
66825
- mi: {
66826
- dimension: "length",
66827
- factor: 1609.344,
66828
- offset: 0
66829
- },
66830
- lx: {
66831
- dimension: "illuminance",
66832
- factor: 1,
66833
- offset: 0
66834
- },
66835
- "W/m²": {
66836
- dimension: "irradiance",
66837
- factor: 1,
66838
- offset: 0
66839
- },
66840
- W: {
66841
- dimension: "power",
66842
- factor: 1,
66843
- offset: 0
66844
- },
66845
- kW: {
66846
- dimension: "power",
66847
- factor: 1e3,
66848
- offset: 0
66849
- },
66850
- VA: {
66851
- dimension: "apparent-power",
66852
- factor: 1,
66853
- offset: 0
66854
- },
66855
- Wh: {
66856
- dimension: "energy",
66857
- factor: 1,
66858
- offset: 0
66859
- },
66860
- kWh: {
66861
- dimension: "energy",
66862
- factor: 1e3,
66863
- offset: 0
66864
- },
66865
- MWh: {
66866
- dimension: "energy",
66867
- factor: 1e6,
66868
- offset: 0
66869
- },
66870
- V: {
66871
- dimension: "voltage",
66872
- factor: 1,
66873
- offset: 0
66874
- },
66875
- mV: {
66876
- dimension: "voltage",
66877
- factor: .001,
66878
- offset: 0
66879
- },
66880
- A: {
66881
- dimension: "current",
66882
- factor: 1,
66883
- offset: 0
66884
- },
66885
- mA: {
66886
- dimension: "current",
66887
- factor: .001,
66888
- offset: 0
66889
- },
66890
- ppm: {
66891
- dimension: "concentration-volume",
66892
- factor: 1,
66893
- offset: 0
66894
- },
66895
- ppb: {
66896
- dimension: "concentration-volume",
66897
- factor: .001,
66898
- offset: 0
66899
- },
66900
- "µg/m³": {
66901
- dimension: "concentration-mass",
66902
- factor: 1,
66903
- offset: 0
66904
- },
66905
- "mg/m³": {
66906
- dimension: "concentration-mass",
66907
- factor: 1e3,
66908
- offset: 0
66909
- },
66910
- kg: {
66911
- dimension: "mass",
66912
- factor: 1,
66913
- offset: 0
66914
- },
66915
- g: {
66916
- dimension: "mass",
66917
- factor: .001,
66918
- offset: 0
66919
- },
66920
- lb: {
66921
- dimension: "mass",
66922
- factor: .453592,
66923
- offset: 0
66924
- },
66925
- oz: {
66926
- dimension: "mass",
66927
- factor: .0283495,
66928
- offset: 0
66929
- },
66930
- "%": {
66931
- dimension: "percentage",
66932
- factor: 1,
66933
- offset: 0
66934
- }
66935
- };
66936
- //#endregion
66937
- //#region src/units/convert.ts
66938
- /**
66939
- * Pure unit-conversion API over `UNIT_TABLE`. No dependencies, no side effects.
66940
- *
66941
- * Conversion is refused (throws `UnitConversionError`, or returns `undefined`
66942
- * from the `try*` variant) whenever a unit is unknown or the two units belong
66943
- * to different dimensions — cross-dimension conversion is deliberately
66944
- * impossible so a render override can never make the value and its unit label
66945
- * disagree.
66946
- *
66947
- * Callers pass units through `normalizeUnit` (`../device/source-info.ts`) BEFORE
66948
- * calling here — the table keys only canonical spellings.
66949
- */
66950
- /** Thrown by `convertUnit` when a conversion is refused. Carries the offending
66951
- * units + a machine-readable `reason` for callers that branch on the cause. */
66952
- var UnitConversionError = class extends Error {
66953
- from;
66954
- to;
66955
- reason;
66956
- constructor(from, to, reason) {
66957
- super(`[units] cannot convert '${from}' → '${to}': ${reason}`);
66958
- this.name = "UnitConversionError";
66959
- this.from = from;
66960
- this.to = to;
66961
- this.reason = reason;
66962
- }
66963
- };
66964
- /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
66965
- function specFor(unit) {
66966
- return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
66967
- }
66968
- /** The dimension a unit belongs to, or `undefined` for an unknown unit. */
66969
- function unitDimension(unit) {
66970
- return specFor(unit)?.dimension;
66971
- }
66972
- /** Value in `spec`'s unit → its dimension's canonical unit. */
66973
- function toCanonical(value, spec) {
66974
- return value * spec.factor + spec.offset;
66975
- }
66976
- /** Canonical-unit value → the unit described by `spec`. */
66977
- function fromCanonical(canonical, spec) {
66978
- return (canonical - spec.offset) / spec.factor;
66979
- }
66980
- /**
66981
- * Convert `value` from unit `from` to unit `to`. Identity when `from === to`.
66982
- * Throws `UnitConversionError` when either unit is unknown or the units belong
66983
- * to different dimensions.
66984
- */
66985
- function convertUnit(value, from, to) {
66986
- if (from === to) return value;
66987
- const fromSpec = specFor(from);
66988
- if (!fromSpec) throw new UnitConversionError(from, to, "unknown-from");
66989
- const toSpec = specFor(to);
66990
- if (!toSpec) throw new UnitConversionError(from, to, "unknown-to");
66991
- if (fromSpec.dimension !== toSpec.dimension) throw new UnitConversionError(from, to, "cross-dimension");
66992
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
66993
- }
66994
- /**
66995
- * Graceful variant for the render path: returns the converted value, or
66996
- * `undefined` when the conversion is refused (unknown unit / cross-dimension).
66997
- * Never throws — a render caller falls back to the raw value + source unit.
66998
- */
66999
- function tryConvertUnit(value, from, to) {
67000
- if (from === to) return value;
67001
- const fromSpec = specFor(from);
67002
- if (!fromSpec) return void 0;
67003
- const toSpec = specFor(to);
67004
- if (!toSpec) return void 0;
67005
- if (fromSpec.dimension !== toSpec.dimension) return void 0;
67006
- return fromCanonical(toCanonical(value, fromSpec), toSpec);
67007
- }
67008
- /**
67009
- * Whether `value` can be converted between `from` and `to` (same dimension,
67010
- * both known). Cheap boolean probe for the UI's same-dimension unit picker and
67011
- * the resolver's convertibility branch — compute ONCE so a unit label and its
67012
- * value math can never disagree.
67013
- */
67014
- function canConvertUnit(from, to) {
67015
- if (from === to) return true;
67016
- const fromDim = unitDimension(from);
67017
- if (fromDim === void 0) return false;
67018
- return fromDim === unitDimension(to);
67019
- }
67020
- /**
67021
- * Every known unit spelling in a dimension (table insertion order — canonical
67022
- * unit first). Powers the UI's same-dimension unit picker.
67023
- */
67024
- function unitsForDimension(dimension) {
67025
- const out = [];
67026
- for (const [unit, spec] of Object.entries(UNIT_TABLE)) if (spec.dimension === dimension) out.push(unit);
67027
- return out;
67028
- }
67029
- //#endregion
67030
67403
  //#region src/util/location-match.ts
67031
67404
  /**
67032
67405
  * Pure fuzzy matcher for adoption location import. Normalized
@@ -68551,6 +68924,8 @@ exports.CarbonMonoxideStatusSchema = CarbonMonoxideStatusSchema;
68551
68924
  exports.ChargingStatus = require_err_msg.ChargingStatus;
68552
68925
  exports.ClaimOutcomeSchema = ClaimOutcomeSchema;
68553
68926
  exports.ClaimsListingSchema = ClaimsListingSchema;
68927
+ exports.ClearTrackRelationInputSchema = ClearTrackRelationInputSchema;
68928
+ exports.ClearTrackRelationResultSchema = ClearTrackRelationResultSchema;
68554
68929
  exports.ClientNetworkStatsSchema = ClientNetworkStatsSchema;
68555
68930
  exports.ClimateControlStatusSchema = ClimateControlStatusSchema;
68556
68931
  exports.ClipBytesOfferSchema = ClipBytesOfferSchema;
@@ -68575,6 +68950,9 @@ exports.ClusterModelChoiceSchema = ClusterModelChoiceSchema;
68575
68950
  exports.CollectionColumnSchema = CollectionColumnSchema;
68576
68951
  exports.CollectionIndexSchema = CollectionIndexSchema;
68577
68952
  exports.ColorStatusSchema = ColorStatusSchema;
68953
+ exports.ComposableCapabilitiesSchema = ComposableCapabilitiesSchema;
68954
+ exports.ComposableCapabilitySchema = ComposableCapabilitySchema;
68955
+ exports.ComposableFieldSchema = ComposableFieldSchema;
68578
68956
  exports.ComposerApplyInputSchema = ComposerApplyInputSchema;
68579
68957
  exports.ComposerApplyResultSchema = ComposerApplyResultSchema;
68580
68958
  exports.ComposerBlockSchema = ComposerBlockSchema;
@@ -68700,6 +69078,7 @@ exports.DISPLAY_ICON_GROUPS = DISPLAY_ICON_GROUPS;
68700
69078
  exports.DISPLAY_ICON_KEYS = DISPLAY_ICON_KEYS;
68701
69079
  exports.DISPLAY_ICON_OFFERED_KEYS = DISPLAY_ICON_OFFERED_KEYS;
68702
69080
  exports.DISPLAY_ICON_VOCABULARY = DISPLAY_ICON_VOCABULARY;
69081
+ exports.DOORBELL_RISE_DEBOUNCE_MS = DOORBELL_RISE_DEBOUNCE_MS;
68703
69082
  exports.DOORBELL_SOURCE_SLOT = DOORBELL_SOURCE_SLOT;
68704
69083
  exports.DataStoreEngineInfoSchema = EngineInfoSchema;
68705
69084
  exports.DayNightModeSchema = DayNightModeSchema;
@@ -68920,6 +69299,8 @@ exports.LegacyStorageLocationDefaultSchema = LegacyStorageLocationDefaultSchema;
68920
69299
  exports.LinkedDeviceSchema = LinkedDeviceSchema;
68921
69300
  exports.LinkedDevicesModeSchema = LinkedDevicesModeSchema;
68922
69301
  exports.LinkedDevicesSlotValueSchema = LinkedDevicesSlotValueSchema;
69302
+ exports.ListTrackRelationsInputSchema = ListTrackRelationsInputSchema;
69303
+ exports.ListTrackRelationsResultSchema = ListTrackRelationsResultSchema;
68923
69304
  exports.LlmDefaultSchema = LlmDefaultSchema;
68924
69305
  exports.LlmDefaultSelectorSchema = LlmDefaultSelectorSchema;
68925
69306
  exports.LlmDownloadProgressSchema = LlmDownloadProgressSchema;
@@ -68995,6 +69376,8 @@ exports.MAX_KEYS = MAX_KEYS;
68995
69376
  exports.MAX_REASONS_PER_KEY = MAX_REASONS_PER_KEY;
68996
69377
  exports.MAX_SENSOR_TRIGGER_DEVICES = MAX_SENSOR_TRIGGER_DEVICES;
68997
69378
  exports.MAX_TRACK_DEBUG_NOTE_LEN = MAX_TRACK_DEBUG_NOTE_LEN;
69379
+ exports.MAX_TRACK_PURITIES = MAX_TRACK_PURITIES;
69380
+ exports.MAX_TRACK_RELATIONS = MAX_TRACK_RELATIONS;
68998
69381
  exports.MEDIA_OWNER_TYPES = MEDIA_OWNER_TYPES;
68999
69382
  exports.MEDIA_PRESENCE_OWNER_KINDS = MEDIA_PRESENCE_OWNER_KINDS;
69000
69383
  exports.METHOD_ACCESS_MAP = METHOD_ACCESS_MAP;
@@ -69506,8 +69889,12 @@ exports.ServerUpdateActionResultSchema = ServerUpdateActionResultSchema;
69506
69889
  exports.ServerUpdateCheckResultSchema = ServerUpdateCheckResultSchema;
69507
69890
  exports.ServerUpdateOutcomeSchema = ServerUpdateOutcomeSchema;
69508
69891
  exports.ServerUpdateStateSchema = ServerUpdateStateSchema;
69892
+ exports.SetCameraSensorsInputSchema = SetCameraSensorsInputSchema;
69509
69893
  exports.SetLoggingSettingsInputSchema = SetLoggingSettingsInputSchema;
69510
69894
  exports.SetSiteLocationInputSchema = SetSiteLocationInputSchema;
69895
+ exports.SetTrackPurityInputSchema = SetTrackPurityInputSchema;
69896
+ exports.SetTrackPurityResultSchema = SetTrackPurityResultSchema;
69897
+ exports.SetTrackRelationInputSchema = SetTrackRelationInputSchema;
69511
69898
  exports.SettingsPatchSchema = SettingsPatchSchema;
69512
69899
  exports.SettingsRecordSchema = SettingsRecordSchema;
69513
69900
  exports.SettingsSchemaWithValuesSchema = SettingsSchemaWithValuesSchema;
@@ -69592,6 +69979,7 @@ exports.SystemMirror = SystemMirror;
69592
69979
  exports.TAXONOMY_COLORS = TAXONOMY_COLORS;
69593
69980
  exports.TIMELAPSE_DENSE_FLOOR_SEC = TIMELAPSE_DENSE_FLOOR_SEC;
69594
69981
  exports.TIMEZONES = TIMEZONES;
69982
+ exports.TRACK_RELATIONS_DEFAULT_LIMIT = TRACK_RELATIONS_DEFAULT_LIMIT;
69595
69983
  exports.TRANSCODE_DOWN_MAX_BITRATE_KBPS = TRANSCODE_DOWN_MAX_BITRATE_KBPS;
69596
69984
  exports.TRANSCODE_DOWN_MAX_HEIGHT = TRANSCODE_DOWN_MAX_HEIGHT;
69597
69985
  exports.TamperStatusSchema = TamperStatusSchema;
@@ -69624,7 +70012,13 @@ exports.TrackEnvelopeSchema = TrackEnvelopeSchema;
69624
70012
  exports.TrackFlagsPatchSchema = TrackFlagsPatchSchema;
69625
70013
  exports.TrackFlagsSchema = TrackFlagsSchema;
69626
70014
  exports.TrackProjectionSchema = TrackProjectionSchema;
70015
+ exports.TrackPuritySchema = TrackPuritySchema;
70016
+ exports.TrackPurityValueSchema = TrackPurityValueSchema;
70017
+ exports.TrackRelationKindSchema = TrackRelationKindSchema;
70018
+ exports.TrackRelationRefusalSchema = TrackRelationRefusalSchema;
70019
+ exports.TrackRelationSchema = TrackRelationSchema;
69627
70020
  exports.TrackSchema = TrackSchema;
70021
+ exports.TrackSideSchema = TrackSideSchema;
69628
70022
  exports.TrackSourceSchema = TrackSourceSchema;
69629
70023
  exports.TrackStateSchema = TrackStateSchema;
69630
70024
  exports.TrackZoneFilterSchema = TrackZoneFilterSchema;
@@ -69754,6 +70148,7 @@ exports.buildAddonRouteProvider = buildAddonRouteProvider;
69754
70148
  exports.buildAudioArgs = require_composition.buildAudioArgs;
69755
70149
  exports.buildCapabilityLookup = buildCapabilityLookup;
69756
70150
  exports.buildDeviceAdminUrl = buildDeviceAdminUrl;
70151
+ exports.buildDoorbellCompositionFeature = buildDoorbellCompositionFeature;
69757
70152
  exports.buildEventKindDescriptor = buildEventKindDescriptor;
69758
70153
  exports.buildFfmpegArgs = require_composition.buildFfmpegArgs;
69759
70154
  exports.buildInputArgs = require_composition.buildInputArgs;
@@ -69762,6 +70157,7 @@ exports.buildNcTaxonomy = buildNcTaxonomy;
69762
70157
  exports.buildRoleScopes = buildRoleScopes;
69763
70158
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
69764
70159
  exports.buildVideoArgs = require_composition.buildVideoArgs;
70160
+ exports.builtCapabilityBlockers = builtCapabilityBlockers;
69765
70161
  exports.buttonCapability = buttonCapability;
69766
70162
  exports.cameraCredentialsCapability = cameraCredentialsCapability;
69767
70163
  exports.cameraGridLayoutCapability = cameraGridLayoutCapability;
@@ -69854,6 +70250,7 @@ exports.deriveBatteryPresence = deriveBatteryPresence;
69854
70250
  exports.deriveCameraSwitches = deriveCameraSwitches;
69855
70251
  exports.deriveDetailCropRect = deriveDetailCropRect;
69856
70252
  exports.deriveRecordingMode = deriveRecordingMode;
70253
+ exports.describeComposableCaps = describeComposableCaps;
69857
70254
  exports.describeCustomActions = require_sleep.describeCustomActions;
69858
70255
  exports.describeModelVariant = describeModelVariant;
69859
70256
  exports.describeOnboardClamp = describeOnboardClamp;
@@ -69881,6 +70278,7 @@ exports.deviceStatusCapability = deviceStatusCapability;
69881
70278
  exports.displayIconEntry = displayIconEntry;
69882
70279
  exports.displayIconsInGroup = displayIconsInGroup;
69883
70280
  exports.doorbellCapability = doorbellCapability;
70281
+ exports.doorbellFeatureSources = doorbellFeatureSources;
69884
70282
  exports.droppedConditionsForKind = droppedConditionsForKind;
69885
70283
  exports.egressTranscodeSharingKey = egressTranscodeSharingKey;
69886
70284
  exports.egressTransportFromRequest = egressTransportFromRequest;