@camstack/system 1.2.321 → 1.2.323

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
  2. package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
  3. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
  4. package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
  5. package/dist/builtins/alerts/alerts.addon.js +1 -1
  6. package/dist/builtins/alerts/alerts.addon.mjs +1 -1
  7. package/dist/builtins/autotrack/index.js +1 -1
  8. package/dist/builtins/autotrack/index.mjs +1 -1
  9. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
  10. package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
  11. package/dist/builtins/camera-grid/addon.d.ts +13 -1
  12. package/dist/builtins/camera-grid/index.js +46 -12
  13. package/dist/builtins/camera-grid/index.mjs +46 -12
  14. package/dist/builtins/composer/composer.addon.js +2 -2
  15. package/dist/builtins/composer/composer.addon.mjs +2 -2
  16. package/dist/builtins/console-logging/index.js +1 -1
  17. package/dist/builtins/console-logging/index.mjs +1 -1
  18. package/dist/builtins/core-blocks/composition-api.d.ts +4 -2
  19. package/dist/builtins/core-blocks/core-blocks.addon.js +6 -4
  20. package/dist/builtins/core-blocks/core-blocks.addon.mjs +6 -4
  21. package/dist/builtins/device-manager/device-manager.addon.js +2 -2
  22. package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
  23. package/dist/builtins/doorbell/doorbell-composition-migration.d.ts +1 -13
  24. package/dist/builtins/doorbell/virtual-doorbell.addon.js +6 -64
  25. package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +5 -63
  26. package/dist/builtins/hub-forwarder/index.js +1 -1
  27. package/dist/builtins/hub-forwarder/index.mjs +1 -1
  28. package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
  29. package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
  30. package/dist/builtins/local-auth/local-auth.addon.js +1 -1
  31. package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
  32. package/dist/builtins/local-network/local-network.addon.js +1 -1
  33. package/dist/builtins/local-network/local-network.addon.mjs +1 -1
  34. package/dist/builtins/loki-logging/index.js +1 -1
  35. package/dist/builtins/loki-logging/index.mjs +1 -1
  36. package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
  37. package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
  38. package/dist/builtins/platform-probe/index.js +1 -1
  39. package/dist/builtins/platform-probe/index.mjs +1 -1
  40. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
  41. package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
  42. package/dist/builtins/snapshot/index.js +18 -5
  43. package/dist/builtins/snapshot/index.mjs +18 -5
  44. package/dist/builtins/snapshot/snapshot.addon.d.ts +3 -0
  45. package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
  46. package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
  47. package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
  48. package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
  49. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
  50. package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
  51. package/dist/builtins/system-config/system-config.addon.js +1 -1
  52. package/dist/builtins/system-config/system-config.addon.mjs +1 -1
  53. package/dist/builtins/winston-logging/index.js +1 -1
  54. package/dist/builtins/winston-logging/index.mjs +1 -1
  55. package/dist/{composition-sources-iDqQVPng.mjs → composition-sources-CeAPNF5Q.mjs} +1 -1
  56. package/dist/{composition-sources-gGrw35Lt.js → composition-sources-DkVxrXLm.js} +1 -1
  57. package/dist/{dist-BVHQ6TX1.mjs → dist-Bs6UyVGd.mjs} +2020 -1479
  58. package/dist/{dist-BMDYmNV3.js → dist-Bup0sTZb.js} +2043 -1490
  59. package/dist/index.js +1 -1
  60. package/dist/index.mjs +1 -1
  61. package/dist/{retired-settings-keys-FhjpCM5N.js → retired-settings-keys-Db0P4Yvn.js} +1 -1
  62. package/dist/{retired-settings-keys-BlQLUReM.mjs → retired-settings-keys-LROp1DXx.mjs} +1 -1
  63. package/package.json +1 -1
@@ -3512,7 +3512,7 @@ function errMsg(err) {
3512
3512
  return String(err);
3513
3513
  }
3514
3514
  //#endregion
3515
- //#region ../types/dist/composition-BK-GTFYb.mjs
3515
+ //#region ../types/dist/composition-Dq42XWp0.mjs
3516
3516
  var AUDIO_ENCODER_BY_CODEC = {
3517
3517
  opus: "libopus",
3518
3518
  aac: "aac",
@@ -3906,6 +3906,58 @@ function buildFfmpegArgs(inv) {
3906
3906
  ];
3907
3907
  }
3908
3908
  /**
3909
+ * The ffmpeg build every CamStack node downloads, per platform and arch.
3910
+ *
3911
+ * ## Why a download, on every node, including the container
3912
+ *
3913
+ * The version of ffmpeg used to be CODE: baked into the image, changeable only
3914
+ * by rebuilding and rolling it. Now it is DATA — this file states the release,
3915
+ * and a node that boots with a different one downloads it. Changing ffmpeg is a
3916
+ * framework bump, not an image rebuild.
3917
+ *
3918
+ * It also settles the licence question by removing it. FFmpeg's licence is
3919
+ * chosen at compile time: the core is LGPL 2.1+, `--enable-gpl` makes it GPL
3920
+ * (for x264/x265), `--enable-version3` makes that v3, and `--enable-nonfree`
3921
+ * makes the result undistributable. These are GPL v3 builds, and we do not
3922
+ * distribute them: the user's own node fetches the archive from the project
3923
+ * that publishes it, so we convey no GPL work and inherit none of the
3924
+ * obligations that come with conveying one. That is the whole reason the image
3925
+ * no longer installs ffmpeg at all — not a technical preference.
3926
+ *
3927
+ * ## Why jellyfin-ffmpeg
3928
+ *
3929
+ * One vendor covering linux-amd64, linux-arm64, darwin-arm64 and darwin-x64
3930
+ * from one release, with the widest hardware surface of anything maintained:
3931
+ * VAAPI, QSV, NVENC/NVDEC, AMF, Vulkan, OpenCL and DRM on amd64, plus Rockchip
3932
+ * MPP on arm64. It reaches the vendor libraries through dlopen trampolines,
3933
+ * so the binary is not linked against a driver stack it may not find.
3934
+ *
3935
+ * **What it still needs from the host, measured 2026-09-19.** On the Unraid
3936
+ * HOST, which has no libva, `-vaapi_device` aborts the process outright —
3937
+ * `implib-gen: libva-drm.so.2: failed to load library ... Assertion '0 &&
3938
+ * "Assertion in generated code"' failed`. Inside our container, which installs
3939
+ * libva plus the iHD driver, the same command encodes: verified end to end with
3940
+ * `testsrc → format=nv12,hwupload → h264_vaapi`. So the image drops ffmpeg and
3941
+ * KEEPS the Intel media stack; the drivers are the part a binary cannot bring.
3942
+ *
3943
+ * ## Why each artifact carries a checksum and a claim
3944
+ *
3945
+ * The download is now the only source, so `sha256` is verified before anything
3946
+ * is extracted — jellyfin publishes no checksums on its release assets, so
3947
+ * these are ours, taken at the version bump. And `expectedHwaccels` is what the
3948
+ * build is supposed to be able to do: a binary that probes short of its own
3949
+ * claim is a source that changed under us, which is precisely what went
3950
+ * unnoticed for months when a static build turned out to carry no VAAPI at all
3951
+ * (D536).
3952
+ */
3953
+ /** The pinned jellyfin-ffmpeg release. Changing this is the version change. */
3954
+ var FFMPEG_RELEASE = "8.1.2-5";
3955
+ var BASE = `https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v${FFMPEG_RELEASE}`;
3956
+ function portable(target) {
3957
+ return `${BASE}/jellyfin-ffmpeg_${FFMPEG_RELEASE}_portable_${target}-gpl.tar.xz`;
3958
+ }
3959
+ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac64");
3960
+ /**
3909
3961
  * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
3910
3962
  * valutatore puro del suo FRONTE.
3911
3963
  *
@@ -5359,6 +5411,26 @@ var BASE_LIVE_EGRESS_PROFILE = {
5359
5411
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
5360
5412
  ({ ...BASE_LIVE_EGRESS_PROFILE }), { ...BASE_LIVE_EGRESS_PROFILE.video };
5361
5413
  /**
5414
+ * Memoize `ensure` on SUCCESS only.
5415
+ *
5416
+ * A failure is not remembered: the next caller asks again, so a node whose
5417
+ * first download failed (no network at boot) recovers on the next spawn that
5418
+ * needs it — with no timer and no retry loop, because the ask is driven by
5419
+ * work that actually wants the binary.
5420
+ */
5421
+ function memoizePinnedFfmpeg(ensure) {
5422
+ let pending = null;
5423
+ return () => {
5424
+ if (pending !== null) return pending;
5425
+ const attempt = ensure();
5426
+ pending = attempt;
5427
+ attempt.catch(() => {
5428
+ if (pending === attempt) pending = null;
5429
+ });
5430
+ return attempt;
5431
+ };
5432
+ }
5433
+ /**
5362
5434
  * Deep wiring healthcheck — snapshot of active reachability probes across
5363
5435
  * every declared capability + widget of every installed plugin, on every
5364
5436
  * node. Produced by the backend `WiringHealthService` and surfaced via
@@ -11645,263 +11717,1156 @@ var CompositionBlockStateSchema = z.object({
11645
11717
  reason: z.string().optional(),
11646
11718
  fields: z.array(CompositionFieldStateSchema)
11647
11719
  });
11720
+ function problem(code, path, message) {
11721
+ return {
11722
+ code,
11723
+ path,
11724
+ message
11725
+ };
11726
+ }
11727
+ function refused(...problems) {
11728
+ return {
11729
+ problems,
11730
+ fields: [],
11731
+ features: [],
11732
+ events: []
11733
+ };
11734
+ }
11735
+ /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
11736
+ function compositionFieldAt(field) {
11737
+ const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
11738
+ return `features[${field.featureIndex}].${inner}.${field.path}`;
11739
+ }
11740
+ var NUMBER = {
11741
+ kind: "fixed",
11742
+ result: ["number"]
11743
+ };
11744
+ var BOOLEAN = {
11745
+ kind: "fixed",
11746
+ result: ["boolean"]
11747
+ };
11748
+ /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
11749
+ var BUILTIN_RESULT_KINDS = Object.freeze({
11750
+ min: NUMBER,
11751
+ max: NUMBER,
11752
+ abs: NUMBER,
11753
+ floor: NUMBER,
11754
+ ceil: NUMBER,
11755
+ sqrt: NUMBER,
11756
+ round: NUMBER,
11757
+ pow: NUMBER,
11758
+ clamp: NUMBER,
11759
+ avg: NUMBER,
11760
+ sum: NUMBER,
11761
+ age: NUMBER,
11762
+ convert: NUMBER,
11763
+ coalesce: { kind: "coalesce" },
11764
+ any: BOOLEAN,
11765
+ all: BOOLEAN,
11766
+ within: BOOLEAN,
11767
+ latest: {
11768
+ kind: "fixed",
11769
+ result: ["number", "null"]
11770
+ },
11771
+ number: {
11772
+ kind: "fixed",
11773
+ result: ["number", "null"]
11774
+ },
11775
+ rose: {
11776
+ kind: "fixed",
11777
+ result: ["number", "null"]
11778
+ },
11779
+ count: NUMBER
11780
+ });
11781
+ var KindInferenceError = class extends Error {};
11782
+ function kindOfValue(value) {
11783
+ if (value === null) return "null";
11784
+ if (typeof value === "number") return "number";
11785
+ if (typeof value === "string") return "string";
11786
+ return "boolean";
11787
+ }
11788
+ function only(kind) {
11789
+ return new Set([kind]);
11790
+ }
11791
+ function union(...sets) {
11792
+ return new Set(sets.flatMap((s) => [...s]));
11793
+ }
11794
+ function withoutNull(set) {
11795
+ return new Set([...set].filter((k) => k !== "null"));
11796
+ }
11797
+ function binaryKinds(op, left, right) {
11798
+ if (op === "+") {
11799
+ const all = [...left, ...right];
11800
+ if (all.every((k) => k === "string")) return only("string");
11801
+ if (all.every((k) => k === "number")) return only("number");
11802
+ return new Set(["number", "string"]);
11803
+ }
11804
+ if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
11805
+ return only("boolean");
11806
+ }
11807
+ /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
11808
+ function arityRange(builtin) {
11809
+ if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
11810
+ if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
11811
+ return `${builtin.minArgs}–${builtin.maxArgs}`;
11812
+ }
11813
+ function callKinds(callee, args) {
11814
+ const builtin = EXPRESSION_BUILTINS[callee];
11815
+ if (builtin !== void 0 && (args.length < builtin.minArgs || args.length > builtin.maxArgs)) throw new KindInferenceError(`${callee}: wrong number of arguments (${args.length}); expected ${arityRange(builtin)}`);
11816
+ const rule = BUILTIN_RESULT_KINDS[callee];
11817
+ if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
11818
+ if (rule.kind === "fixed") return new Set(rule.result);
11819
+ const nonNull = union(...args.map(withoutNull));
11820
+ return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
11821
+ }
11822
+ function kindsOf(node, bindings) {
11823
+ switch (node.kind) {
11824
+ case "literal": return only(kindOfValue(node.value));
11825
+ case "identifier": {
11826
+ if (node.name === "now") return only("number");
11827
+ const bound = bindings.get(node.name);
11828
+ if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
11829
+ return bound;
11830
+ }
11831
+ case "unary": return only(node.op === "!" ? "boolean" : "number");
11832
+ case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
11833
+ case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
11834
+ case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
11835
+ case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
11836
+ }
11837
+ }
11838
+ function nodeContainsCall(node, callee) {
11839
+ switch (node.kind) {
11840
+ case "literal":
11841
+ case "identifier": return false;
11842
+ case "unary": return nodeContainsCall(node.operand, callee);
11843
+ case "binary":
11844
+ case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
11845
+ case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
11846
+ case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
11847
+ }
11848
+ }
11849
+ function expressionContainsBuiltinCall(expr, callee) {
11850
+ const compiled = compileExpressionSafe(expr);
11851
+ return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
11852
+ }
11853
+ function inferExpressionKinds(input) {
11854
+ const compiled = compileExpressionSafe(input.expr);
11855
+ if (!compiled.ok) return {
11856
+ ok: false,
11857
+ error: compiled.error
11858
+ };
11859
+ try {
11860
+ return {
11861
+ ok: true,
11862
+ kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
11863
+ };
11864
+ } catch (err) {
11865
+ if (err instanceof KindInferenceError) return {
11866
+ ok: false,
11867
+ error: err.message
11868
+ };
11869
+ throw err;
11870
+ }
11871
+ }
11648
11872
  /**
11649
- * core-blocks — user-authored TypeScript, stored in the kernel and executed in
11650
- * its own process.
11651
- *
11652
- * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
11653
- *
11654
- * The first use is **owning devices without being a device provider**: a block
11655
- * declares devices under a system or custom integration and drives their state,
11656
- * with the same `ctx` an addon gets. Automations come later; nothing here
11657
- * models a trigger.
11658
- *
11659
- * **Stated plainly, because it does not change by being true:** a block has an
11660
- * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
11661
- * with no review step. What makes that survivable is not a sandbox, it is
11662
- * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
11663
- * so a block that throws or never returns is marked `failed` and visible
11664
- * instead of taking the hub with it (D6). Every method here is admin-only, and
11665
- * must stay so.
11666
- */
11667
- /**
11668
- * Prefix for a block's generated addon id AND its runner id — one addon, one
11669
- * process, so the two are the same string (D2).
11670
- *
11671
- * It lives on the CONTRACT rather than beside the code that materialises the
11672
- * package, because it is not an implementation detail: the operator surface
11673
- * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
11674
- * with no cap and no store of its own. Two copies of this string is one copy
11675
- * too many — a drift would show as a logs pane that is simply always empty.
11676
- */
11677
- var CORE_BLOCK_ADDON_PREFIX = "core-block-";
11678
- /**
11679
- * The builtin's own addon id — and, because of that, the OWNER of the
11680
- * integration every block's devices hang from.
11681
- *
11682
- * A block cannot own one. Every integration read is gated on the installed-addon
11683
- * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
11684
- * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
11685
- * null forever while the write succeeds — one invisible, undeletable row per
11686
- * start. `core-blocks` IS installed, so it reconciles one shared integration and
11687
- * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
11688
- *
11689
- * Exported from the contract, next to the runner prefix and for the same reason:
11690
- * a block reads it as
11691
- * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
11692
- * so a second copy of the string is a block that silently declares into nowhere.
11873
+ * The kind half of planning a composition field (D659): which kinds a source
11874
+ * can produce, whether they fit a target field, and the field's role.
11875
+ * Shared by the top-level planner (`validate-composition.ts`) and the item
11876
+ * planner (`composition-items.ts`). Internal to the composition module — NOT
11877
+ * in the barrel: these are planner internals, not a published contract.
11693
11878
  */
11694
- var CORE_BLOCKS_ADDON_ID = "core-blocks";
11695
- /** The addon/runner id a block's process runs under. */
11696
- function coreBlockAddonId(blockId) {
11697
- return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
11879
+ function readKinds(read, lookupCap) {
11880
+ const def = lookupCap(read.cap);
11881
+ const schema = def?.runtimeState ?? def?.status?.schema;
11882
+ if (def === null || schema === void 0) return {
11883
+ ok: false,
11884
+ code: "source-field-unknown",
11885
+ message: `no capability \`${read.cap}\` with state to read`
11886
+ };
11887
+ const field = describeStateField(schema, read.fieldPath);
11888
+ if (field === null) return {
11889
+ ok: false,
11890
+ code: "source-field-unknown",
11891
+ message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
11892
+ };
11893
+ const base = field.kind === "enum" ? "string" : field.kind;
11894
+ return {
11895
+ ok: true,
11896
+ kinds: new Set(field.nullable ? [base, "null"] : [base])
11897
+ };
11698
11898
  }
11699
- /** The block id behind a generated addon/runner id, or null when the id belongs
11700
- * to something else. The inverse of {@link coreBlockAddonId}. */
11701
- function coreBlockIdFromAddonId(addonId) {
11702
- if (!addonId.startsWith("core-block-")) return null;
11703
- const rest = addonId.slice(11);
11704
- return rest.length > 0 ? rest : null;
11899
+ function sourceKinds(source, lookupCap) {
11900
+ if (source.kind === "from") return readKinds(source, lookupCap);
11901
+ if (source.kind === "code") return {
11902
+ ok: false,
11903
+ code: "reserved-variant",
11904
+ message: "code fields arrive in slice 4"
11905
+ };
11906
+ const bindingKinds = /* @__PURE__ */ new Map();
11907
+ for (const [name, binding] of Object.entries(source.bindings)) {
11908
+ if (binding.kind === "literal") {
11909
+ bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
11910
+ continue;
11911
+ }
11912
+ const kinds = readKinds(binding, lookupCap);
11913
+ if (!kinds.ok) return {
11914
+ ...kinds,
11915
+ message: `binding \`${name}\`: ${kinds.message}`
11916
+ };
11917
+ bindingKinds.set(name, kinds.kinds);
11918
+ }
11919
+ const inferred = inferExpressionKinds({
11920
+ expr: source.expr,
11921
+ bindingKinds
11922
+ });
11923
+ return inferred.ok ? {
11924
+ ok: true,
11925
+ kinds: inferred.kinds
11926
+ } : {
11927
+ ok: false,
11928
+ code: "expression-invalid",
11929
+ message: inferred.error
11930
+ };
11931
+ }
11932
+ function compatibility(cap, target, kinds, at) {
11933
+ const allowed = target.kind === "enum" ? "string" : target.kind;
11934
+ const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
11935
+ const problems = [];
11936
+ if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
11937
+ if (kinds.has("null") && !target.nullable) problems.push(problem("may-be-null", at, `\`${cap}.${target.path}\` cannot be null and this source can produce null — wrap it in coalesce(…) or pick a non-null source`));
11938
+ return problems;
11705
11939
  }
11706
- /** Where a block runs. The operator chooses — a block driving a device on an
11707
- * agent is the reason placement is not fixed to the hub. */
11708
- var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
11709
- /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
11710
- * a failing block reads the same way a failing addon does. `degraded` is a
11711
- * composition on an existing device with a field handed back to the native
11712
- * provider while its source is unavailable (S1b, D663); the error names it. */
11713
- var CoreBlockStatusSchema = z.enum([
11714
- "stopped",
11715
- "starting",
11716
- "running",
11717
- "degraded",
11718
- "failed"
11719
- ]);
11720
- /** Client-authored fields. */
11721
- var CoreBlockInputSchema = z.object({
11722
- name: z.string().min(1).max(120),
11723
- /** TypeScript source. Compiled server-side before it is ever stored — a
11724
- * block that does not compile is a fork failure the operator would meet
11725
- * minutes later, in a log, instead of in the editor. */
11726
- code: z.string().max(2e5),
11727
- enabled: z.boolean(),
11728
- placement: CoreBlockPlacementSchema,
11729
- /**
11730
- * Integration the block's devices hang from. Absent = the system integration
11731
- * blocks share. A block may declare its own instead.
11732
- */
11733
- integrationId: z.string().optional()
11734
- });
11735
11940
  /**
11736
- * What a block IS (D659). Absent means `code` — every row stored before this
11737
- * union keeps working unchanged.
11738
- * - `code`: the TypeScript in `code`, compiled and run in its own runner;
11739
- * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
11740
- * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
11941
+ * A string a source produces WITHOUT reading any device — a field-level
11942
+ * `expression` source whose compiled AST is a bare string literal (`"red"`,
11943
+ * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
11944
+ * the kind level (a `from` read of an enum-typed source field, a computed
11945
+ * expression) is NOT checked against the target's `enumValues` here: the
11946
+ * kind inference already collapsed it to the coarse `'string'` kind, losing
11947
+ * whichever enum it actually came from, so membership can only be judged at
11948
+ * write time against the runtime-state schema. Known limitation — see the
11949
+ * fix report for Task 6 round 1.
11741
11950
  */
11742
- var CoreBlockSourceSchema = z.discriminatedUnion("kind", [
11743
- z.object({ kind: z.literal("code") }),
11744
- z.object({
11745
- kind: z.literal("recipe"),
11746
- recipe: AutomationRecipeSchema
11747
- }),
11748
- z.object({
11749
- kind: z.literal("composition"),
11750
- composition: CompositionSchema
11751
- })
11752
- ]);
11951
+ function literalStringValue(source) {
11952
+ if (source.kind !== "expression") return null;
11953
+ const compiled = compileExpressionSafe(source.expr);
11954
+ if (!compiled.ok) return null;
11955
+ const ast = compiled.parsed.ast;
11956
+ return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
11957
+ }
11753
11958
  /**
11754
- * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
11755
- *
11756
- * `name`: REQUIRED for a `new` target — the block's name IS the device's name
11757
- * (D659). ABSENT for an `existing` target — a customization has no name of its
11758
- * own; the server derives one (`customizationBlockName`, D663) and refuses a
11759
- * client-sent name by name.
11959
+ * An enum target whose source is a save-time-KNOWN string literal outside its
11960
+ * `enumValues` is refused now rather than left to fail the write later. A
11961
+ * dynamic source (a `from` read, a non-literal expression) is left to
11962
+ * runtime — see {@link literalStringValue}.
11760
11963
  */
11761
- var CompositionBlockInputSchema = z.object({
11762
- name: z.string().min(1).max(120).optional(),
11763
- enabled: z.boolean(),
11764
- composition: CompositionSchema
11765
- });
11766
- /** A stored block. */
11767
- var CoreBlockSchema = CoreBlockInputSchema.extend({
11768
- id: z.string(),
11769
- createdAt: z.number(),
11770
- updatedAt: z.number(),
11771
- /** Server-stamped author. */
11772
- createdBy: z.string(),
11773
- status: CoreBlockStatusSchema,
11774
- /**
11775
- * Why the block is not running, when it is not. The operator's ONLY window
11776
- * into a block that failed at load — a block that is silently absent is the
11777
- * failure mode this whole feature has to avoid.
11778
- */
11779
- lastError: z.string().optional(),
11780
- /** Ms epoch of the last state change. */
11781
- lastChangedAt: z.number(),
11782
- /** What the block is. Absent means `code` (D659). */
11783
- source: CoreBlockSourceSchema.optional()
11784
- });
11785
- function coreBlockSourceKind(block) {
11786
- return block.source?.kind ?? "code";
11964
+ function enumLiteralProblem(cap, target, source, at) {
11965
+ if (target.kind !== "enum" || target.enumValues === void 0) return null;
11966
+ const value = literalStringValue(source);
11967
+ if (value === null || target.enumValues.includes(value)) return null;
11968
+ const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
11969
+ return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
11787
11970
  }
11788
- function coreBlockComposition(block) {
11789
- return block.source?.kind === "composition" ? block.source.composition : null;
11971
+ function roleOf(source) {
11972
+ if (source.kind !== "expression") return "derived";
11973
+ if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
11974
+ if (fieldReads(source).length > 0) return "derived";
11975
+ const compiled = compileExpressionSafe(source.expr);
11976
+ return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
11790
11977
  }
11791
- /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
11792
- var CompositionStateReadSchema = z.discriminatedUnion("kind", [z.object({
11793
- kind: z.literal("answered"),
11794
- state: CompositionBlockStateSchema.nullable()
11795
- }), z.object({
11796
- kind: z.literal("unreachable"),
11797
- error: z.string()
11798
- })]);
11799
- /** A stored composition block and what the composer says about it. */
11800
- var CompositionViewSchema = z.object({
11801
- block: CoreBlockSchema,
11802
- state: CompositionStateReadSchema
11803
- });
11804
11978
  /**
11805
- * A read of the stored compositions. `not-loaded` while the `core_blocks`
11806
- * table has not been READ: an empty answer then would make "not yet known"
11807
- * look like "none", and invite the operator to recreate what exists (D315).
11979
+ * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
11980
+ * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
11981
+ * own offline, and D393 forbids a stale number, so on an existing target a
11982
+ * field that READS a source never gets `offline`.
11808
11983
  */
11809
- var CompositionsNotLoadedSchema = z.object({
11810
- state: z.literal("not-loaded"),
11811
- reason: z.string()
11812
- });
11813
- var CompositionListReadSchema = z.discriminatedUnion("state", [z.object({
11814
- state: z.literal("loaded"),
11815
- views: z.array(CompositionViewSchema)
11816
- }), CompositionsNotLoadedSchema]);
11817
- var CompositionGetReadSchema = z.discriminatedUnion("state", [z.object({
11818
- state: z.literal("loaded"),
11819
- view: CompositionViewSchema.nullable()
11820
- }), CompositionsNotLoadedSchema]);
11821
- /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
11822
- var CompositionCustomizationReadSchema = z.discriminatedUnion("state", [z.object({
11823
- state: z.literal("loaded"),
11824
- view: CompositionViewSchema.nullable()
11825
- }), CompositionsNotLoadedSchema]);
11826
- /** What a compile attempt produced. */
11827
- var CoreBlockCompileResultSchema = z.object({
11828
- ok: z.boolean(),
11829
- /** Present when `ok` is false — the first error, in the author's words. */
11830
- error: z.string().optional(),
11831
- line: z.number().optional(),
11832
- column: z.number().optional()
11833
- });
11834
- var coreBlocksCapability = {
11835
- name: "core-blocks",
11836
- scope: "system",
11837
- mode: "singleton",
11838
- methods: {
11839
- list: method(z.object({}), z.object({ blocks: z.array(CoreBlockSchema) }), { auth: "admin" }),
11840
- get: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
11841
- /**
11842
- * Create a block. The code is COMPILED first: storing source that does not
11843
- * compile turns an editor error into a fork failure the operator meets
11844
- * minutes later in a log.
11845
- */
11846
- create: method(z.object({ block: CoreBlockInputSchema }), z.object({ block: CoreBlockSchema }), {
11847
- kind: "mutation",
11848
- auth: "admin",
11849
- caller: "required"
11850
- }),
11851
- update: method(z.object({
11852
- blockId: z.string(),
11853
- block: CoreBlockInputSchema.partial()
11854
- }), z.object({ block: CoreBlockSchema }), {
11855
- kind: "mutation",
11856
- auth: "admin",
11857
- caller: "required"
11858
- }),
11859
- delete: method(z.object({ blockId: z.string() }), z.object({ success: z.literal(true) }), {
11860
- kind: "mutation",
11861
- auth: "admin"
11862
- }),
11863
- setEnabled: method(z.object({
11864
- blockId: z.string(),
11865
- enabled: z.boolean()
11866
- }), z.object({ block: CoreBlockSchema }), {
11867
- kind: "mutation",
11868
- auth: "admin"
11869
- }),
11870
- /**
11871
- * Stop this block's runner and let the supervisor bring it back.
11872
- *
11873
- * **It stores nothing.** There is no "restart requested" flag and no second
11874
- * lifecycle field: the supervisor already stops and respawns a runner when
11875
- * the code hash changes, and this exposes that same path deliberately
11876
- * (D62 — a switch writes the authority that already owned the function).
11877
- * The next reconcile pass remains the authority, so a restart that lost a
11878
- * race is corrected within one pass rather than leaving a stale flag behind.
11879
- *
11880
- * Refused for a DISABLED block: nothing would come back, and a button that
11881
- * silently does nothing is worse than one that says why.
11882
- */
11883
- restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
11884
- kind: "mutation",
11885
- auth: "admin"
11886
- }),
11887
- /**
11888
- * Type-check without saving — what the editor calls as the author types, so
11889
- * the compiler's verdict is the same one the server will reach.
11890
- */
11891
- compile: method(z.object({ code: z.string() }), CoreBlockCompileResultSchema, {
11892
- kind: "mutation",
11893
- auth: "admin"
11894
- }),
11895
- /**
11896
- * The declaration files the editor type-checks against.
11897
- *
11898
- * Served rather than bundled: the graph is 3.4 MB across 344 files, and
11899
- * bundling it would roughly double the admin remote. Served rather than
11900
- * hand-stubbed: a block runs with the same `ctx` an addon gets, so a stub
11901
- * would drift and the editor would confidently autocomplete methods that
11902
- * do not exist.
11903
- */
11904
- getTypeDefs: method(z.object({}), z.object({ libs: z.array(z.object({
11984
+ function unavailableFor(tf, role, mode, cap, at) {
11985
+ if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
11986
+ ok: true,
11987
+ unavailable: "release"
11988
+ };
11989
+ if (mode === "new" || role !== "derived") return {
11990
+ ok: true,
11991
+ unavailable: tf.nullable ? "null" : "offline"
11992
+ };
11993
+ if (tf.nullable) return {
11994
+ ok: true,
11995
+ unavailable: "null"
11996
+ };
11997
+ if (tf.enumValues?.includes("unknown") === true) return {
11998
+ ok: true,
11999
+ unavailable: "unknown"
12000
+ };
12001
+ return {
12002
+ ok: false,
12003
+ problem: problem("not-replaceable", at, `\`${cap}.${tf.path}\` reads a source but can say neither null nor 'unknown', and an ADDED capability has no native reading to fall back to (D393, D663)`)
12004
+ };
12005
+ }
12006
+ /** Item fields the composer writes itself, with the value it writes. */
12007
+ var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
12008
+ function reservedItemFields(itemArray) {
12009
+ return new Set([
12010
+ itemArray.keyField,
12011
+ ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
12012
+ ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
12013
+ ]);
12014
+ }
12015
+ /** A required leaf under an optional parent is required only once something writes that parent. */
12016
+ function leafRequired(itemArray, path, written) {
12017
+ const described = describeStateField(itemArray.itemSchema, path);
12018
+ if (described === null || !described.required) return false;
12019
+ return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
12020
+ }
12021
+ function missingLeaves({ capName, itemArray, reserved }, item, at) {
12022
+ const written = Object.keys(item.fields);
12023
+ return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
12024
+ if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
12025
+ if (!leafRequired(itemArray, leaf.path, written)) return [];
12026
+ const described = describeStateField(itemArray.itemSchema, leaf.path);
12027
+ if (described === null || described.nullable) return [];
12028
+ return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
12029
+ });
12030
+ }
12031
+ function planItemField(ctx, leaf) {
12032
+ const { capName, itemArray, featureIndex, lookupCap } = ctx;
12033
+ const { key, label, path, source } = leaf;
12034
+ const at = `features[${featureIndex}].items.${key}.fields.${path}`;
12035
+ if (ctx.reserved.has(path)) return refused(problem("item-field-reserved", at, `\`${capName}.${itemArray.path}[].${path}\` is set by the composer itself — it takes no source`));
12036
+ const tf = describeStateField(itemArray.itemSchema, path);
12037
+ if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
12038
+ const kinds = sourceKinds(source, lookupCap);
12039
+ if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
12040
+ const role = roleOf(source);
12041
+ const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
12042
+ if (!unavailable.ok) return refused(unavailable.problem);
12043
+ const plan = {
12044
+ cap: capName,
12045
+ path,
12046
+ item: {
12047
+ arrayPath: itemArray.path,
12048
+ key,
12049
+ label
12050
+ },
12051
+ featureIndex,
12052
+ kind: tf.kind,
12053
+ nullable: tf.nullable,
12054
+ required: tf.required,
12055
+ unavailable: unavailable.unavailable,
12056
+ role,
12057
+ source,
12058
+ reads: [...fieldReads(source)]
12059
+ };
12060
+ const enumProblem = enumLiteralProblem(capName, tf, source, at);
12061
+ return {
12062
+ problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
12063
+ fields: [plan],
12064
+ features: [],
12065
+ events: []
12066
+ };
12067
+ }
12068
+ function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
12069
+ const at = `features[${featureIndex}].items`;
12070
+ const itemArray = def.status?.itemArray;
12071
+ if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
12072
+ if (itemArray.path !== "items") return refused(problem("not-item-array", at, `\`${def.name}\`'s item array is at \`${itemArray.path}\`, not \`${COMPOSITION_ITEM_ARRAY_PATH}\` — not composable yet`));
12073
+ const ctx = {
12074
+ capName: def.name,
12075
+ itemArray,
12076
+ reserved: reservedItemFields(itemArray),
12077
+ featureIndex,
12078
+ lookupCap,
12079
+ mode
12080
+ };
12081
+ const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
12082
+ problems: missingLeaves(ctx, item, `${at}.${key}`),
12083
+ fields: [],
12084
+ features: [],
12085
+ events: []
12086
+ }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
12087
+ key,
12088
+ label: item.label,
12089
+ path,
12090
+ source
12091
+ }))]);
12092
+ return {
12093
+ problems: results.flatMap((r) => r.problems),
12094
+ fields: results.flatMap((r) => r.fields),
12095
+ features: [],
12096
+ events: []
12097
+ };
12098
+ }
12099
+ /**
12100
+ * The field graph across every composition, and the cycles in it (D659).
12101
+ *
12102
+ * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
12103
+ * edge points from a composed field to each field it reads. A cycle, direct or
12104
+ * through another composition, is refused at save by name (spec §2).
12105
+ *
12106
+ * The graph is built for ONE save: the CANDIDATE composition (the one being
12107
+ * validated) plus every STORED composition. The candidate replaces its own old
12108
+ * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
12109
+ * introduces or removes a cycle must never depend on array order. A DIFFERENT
12110
+ * block targeting the candidate's device is refused by name: one composition
12111
+ * per device (D663). Two different stored peers that (wrongly) target the same
12112
+ * device are corrupted state, not a cycle to resolve by picking one silently;
12113
+ * that is refused by name too (D391).
12114
+ *
12115
+ * A customization of an EXISTING device reads that device's NATIVE value for a
12116
+ * field it replaces or does not write ("Invert state" reads the `on` it
12117
+ * replaces): such a self-read is no edge, so it is never a cycle (D663).
12118
+ * Another composition reading that field reads the customizer's output.
12119
+ *
12120
+ * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
12121
+ * marks a node `done` after its first expansion) — not every elementary cycle
12122
+ * through a shared node, so the path it names may AVOID the candidate while
12123
+ * one through it goes unreported. What a save refuses is therefore decided per
12124
+ * strongly connected component (`findCandidateCycles`, C-1). A dotted
12125
+ * `fieldPath` is matched only as an exact string: a write to `state.value` and
12126
+ * a read of `state` would not connect (no nested field writes exist yet).
12127
+ *
12128
+ * An item leaf is its own node, `…#consumables.items[desiccant].status`
12129
+ * (`featureFieldEntries`), so two items sharing an inner path never merge. No
12130
+ * READ can name an array element (`describeStateField` never describes one),
12131
+ * so an item node is write-only: its edges to sources are what the check needs.
12132
+ */
12133
+ /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
12134
+ var CompositionGraphError = class extends Error {};
12135
+ /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
12136
+ function writtenFields(peer) {
12137
+ return new Set(peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
12138
+ }
12139
+ /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
12140
+ function readsComposedValue(peer, written, read) {
12141
+ if (compositionSourceKey(read.source) !== compositionSourceKey(peer.target)) return true;
12142
+ const field = `${read.cap}.${read.fieldPath}`;
12143
+ return written.has(field) && peer.replaced?.has(field) !== true;
12144
+ }
12145
+ function peerEdges(peer) {
12146
+ const written = writtenFields(peer);
12147
+ return peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
12148
+ source: peer.target,
12149
+ cap: feature.cap,
12150
+ fieldPath
12151
+ }), fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(compositionReadKey)]));
12152
+ }
12153
+ /**
12154
+ * Every peer that actually takes part in the graph: each stored peer minus the
12155
+ * candidate's own old version (same block id — the drop happens before any
12156
+ * edge is built, so its old edges never enter the map), plus the candidate
12157
+ * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
12158
+ * block targets the candidate's device, or two stored peers share a device.
12159
+ */
12160
+ function resolvedPeers(candidate, stored) {
12161
+ const candidateKey = compositionSourceKey(candidate.target);
12162
+ const survivors = /* @__PURE__ */ new Map();
12163
+ for (const peer of stored) {
12164
+ if (peer.blockId === candidate.blockId) continue;
12165
+ const key = compositionSourceKey(peer.target);
12166
+ if (key === candidateKey) throw new CompositionGraphError(`block \`${peer.blockId}\` and block \`${candidate.blockId}\` both target device \`${key}\` — one composition per device; delete or edit the existing one`);
12167
+ const clash = survivors.get(key);
12168
+ if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
12169
+ survivors.set(key, peer);
12170
+ }
12171
+ return [...survivors.values(), candidate];
12172
+ }
12173
+ function compositionEdges(candidate, stored) {
12174
+ const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
12175
+ return new Map(edges);
12176
+ }
12177
+ /** Tarjan's strongly connected components over every node of the graph (sources included). */
12178
+ function stronglyConnected(edges) {
12179
+ const index = /* @__PURE__ */ new Map();
12180
+ const low = /* @__PURE__ */ new Map();
12181
+ const onStack = /* @__PURE__ */ new Set();
12182
+ const stack = [];
12183
+ const components = [];
12184
+ const connect = (node) => {
12185
+ index.set(node, index.size);
12186
+ low.set(node, index.get(node) ?? 0);
12187
+ stack.push(node);
12188
+ onStack.add(node);
12189
+ for (const next of edges.get(node) ?? []) if (!index.has(next)) {
12190
+ connect(next);
12191
+ low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
12192
+ } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
12193
+ if (low.get(node) !== index.get(node)) return;
12194
+ const component = [];
12195
+ for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
12196
+ onStack.delete(member);
12197
+ component.push(member);
12198
+ if (member === node) break;
12199
+ }
12200
+ components.push(component);
12201
+ };
12202
+ for (const node of edges.keys()) if (!index.has(node)) connect(node);
12203
+ return components;
12204
+ }
12205
+ /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
12206
+ function cycleThrough(start, component, edges) {
12207
+ const parent = /* @__PURE__ */ new Map();
12208
+ const queue = [start];
12209
+ for (let head = 0; head < queue.length; head += 1) {
12210
+ const node = queue[head];
12211
+ if (node === void 0) break;
12212
+ for (const next of edges.get(node) ?? []) {
12213
+ if (next === start) {
12214
+ const chain = [];
12215
+ for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
12216
+ return [
12217
+ start,
12218
+ ...chain.reverse(),
12219
+ start
12220
+ ];
12221
+ }
12222
+ if (!component.has(next) || parent.has(next)) continue;
12223
+ parent.set(next, node);
12224
+ queue.push(next);
12225
+ }
12226
+ }
12227
+ return [start, start];
12228
+ }
12229
+ /**
12230
+ * The cycles THIS save closes, decided per strongly connected component — never
12231
+ * per reported DFS path, which names one back-edge per revisit and can report a
12232
+ * component's cycle by a path that avoids the candidate while the one through it
12233
+ * goes unreported. A component refuses when it holds a candidate node and a
12234
+ * cycle (more than one node, or a self-loop). Components without a candidate
12235
+ * node are not this save's cycles.
12236
+ */
12237
+ function findCandidateCycles(candidate, stored) {
12238
+ const edges = compositionEdges(candidate, stored);
12239
+ const prefix = `${compositionSourceKey(candidate.target)}#`;
12240
+ return stronglyConnected(edges).flatMap((members) => {
12241
+ const start = members.find((n) => n.startsWith(prefix));
12242
+ if (start === void 0) return [];
12243
+ const component = new Set(members);
12244
+ const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
12245
+ if (members.length === 1 && !selfLoop) return [];
12246
+ return [{
12247
+ path: cycleThrough(start, component, edges),
12248
+ component
12249
+ }];
12250
+ });
12251
+ }
12252
+ function buildCapabilityLookup(defs) {
12253
+ const byName = new Map(defs.map((d) => [d.name, d]));
12254
+ return (capName) => byName.get(capName) ?? null;
12255
+ }
12256
+ /** Caps every `BaseDevice` registers on itself; a second registration throws. */
12257
+ var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
12258
+ var PENDING_SELF = {
12259
+ addonId: COMPOSER_ADDON_ID,
12260
+ stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
12261
+ };
12262
+ function composableCap(feature, at, lookupCap) {
12263
+ const def = lookupCap(feature.cap);
12264
+ if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
12265
+ if (BASE_DEVICE_CAPS.has(def.name)) return problem("not-composable", `${at}.cap`, `\`${def.name}\` is registered by every device itself — it cannot be composed`);
12266
+ const state = def.runtimeState;
12267
+ if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
12268
+ return {
12269
+ def,
12270
+ state,
12271
+ fields: describeTopLevelStateFields(state)
12272
+ };
12273
+ }
12274
+ /**
12275
+ * A capability BUILT by the composition (a new device, or one ADDED to an
12276
+ * existing device) must fit the device type, serve no command, and have every
12277
+ * required field. A REPLACE needs none of it: the cap is bound (a fact), the
12278
+ * native keeps serving its methods, and only the named fields are owned (D663).
12279
+ */
12280
+ function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
12281
+ const problems = [];
12282
+ if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) problems.push(problem("wrong-device-type", `${at}.cap`, `\`${def.name}\` is not offered on a \`${deviceType}\` device (it allows ${def.deviceTypes.join(", ")})`));
12283
+ const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
12284
+ if (served.length > 0) problems.push(problem("has-commands", `${at}.cap`, `\`${def.name}\` has ${served.join(", ")}, which a composed device cannot serve until commands arrive (slice 3)`));
12285
+ const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
12286
+ for (const key of requiredTopLevelKeys(state)) {
12287
+ if (key === itemArrayPath) continue;
12288
+ if (!targetFields.has(key)) problems.push(problem("not-composable", `${at}.fields`, `\`${def.name}.${key}\` is not a scalar — nested fields arrive in a later slice`));
12289
+ else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
12290
+ }
12291
+ return problems;
12292
+ }
12293
+ function planTopLevelField({ def, fields }, field, mode, lookupCap) {
12294
+ const { featureIndex, path, source } = field;
12295
+ const fieldAt = `features[${featureIndex}].fields.${path}`;
12296
+ if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
12297
+ const tf = fields.get(path);
12298
+ if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
12299
+ const kinds = sourceKinds(source, lookupCap);
12300
+ if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
12301
+ const role = roleOf(source);
12302
+ const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
12303
+ if (!unavailable.ok) return refused(unavailable.problem);
12304
+ const plan = {
12305
+ cap: def.name,
12306
+ path,
12307
+ featureIndex,
12308
+ kind: tf.kind,
12309
+ nullable: tf.nullable,
12310
+ required: tf.required,
12311
+ unavailable: unavailable.unavailable,
12312
+ role,
12313
+ source,
12314
+ reads: [...fieldReads(source)]
12315
+ };
12316
+ const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
12317
+ return {
12318
+ problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
12319
+ fields: [plan],
12320
+ features: [],
12321
+ events: []
12322
+ };
12323
+ }
12324
+ function planFeature(feature, featureIndex, target, lookupCap) {
12325
+ const at = `features[${featureIndex}]`;
12326
+ const cap = composableCap(feature, at, lookupCap);
12327
+ if ("code" in cap) return refused(cap);
12328
+ const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
12329
+ if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
12330
+ const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
12331
+ featureIndex,
12332
+ path,
12333
+ source
12334
+ }, target.mode, lookupCap));
12335
+ const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
12336
+ const all = [...perField, ...items];
12337
+ const fields = all.flatMap((result) => result.fields);
12338
+ const featurePlan = {
12339
+ featureIndex,
12340
+ cap: cap.def.name,
12341
+ mode: target.mode
12342
+ };
12343
+ const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
12344
+ return {
12345
+ problems: [
12346
+ ...capProblems,
12347
+ ...all.flatMap((r) => r.problems),
12348
+ ...eventPlan.problems
12349
+ ],
12350
+ fields,
12351
+ features: [featurePlan],
12352
+ events: eventPlan.events
12353
+ };
12354
+ }
12355
+ /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
12356
+ function isNativeCap(target, cap) {
12357
+ const owner = target.nativeCaps.get(cap);
12358
+ return owner !== void 0 && owner !== "composer";
12359
+ }
12360
+ /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
12361
+ function planTargetOf(composition, target) {
12362
+ if (composition.target.kind === "new") return {
12363
+ kind: "new",
12364
+ type: composition.target.type
12365
+ };
12366
+ const key = compositionSourceKey(composition.target.device);
12367
+ if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
12368
+ if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
12369
+ if (target.kind === "unreadable") return problem("target-unreadable", "target", `could not read target device \`${key}\` (${target.error}) — the save is refused rather than stored unchecked`);
12370
+ return {
12371
+ kind: "existing",
12372
+ found: target
12373
+ };
12374
+ }
12375
+ function featureTargetOf(target, cap) {
12376
+ if (target.kind === "new") return {
12377
+ mode: "new",
12378
+ deviceType: target.type
12379
+ };
12380
+ return {
12381
+ mode: isNativeCap(target.found, cap) ? "replace" : "add",
12382
+ deviceType: target.found.type
12383
+ };
12384
+ }
12385
+ /**
12386
+ * The static plan. `target` describes an EXISTING target device (one read);
12387
+ * it is ignored for a new one. On an existing target each feature REPLACES a
12388
+ * cap the device has natively, else ADDS it (D663).
12389
+ */
12390
+ function planComposition(composition, lookupCap, target) {
12391
+ const planTarget = planTargetOf(composition, target);
12392
+ if ("code" in planTarget) return refused(planTarget);
12393
+ const results = composition.features.map((feature, index) => {
12394
+ const result = feature.kind === "passthrough" ? refused(problem("reserved-variant", `features[${index}]`, `whole-capability passthrough of \`${feature.cap}\` arrives in slice 3`)) : planFeature(feature, index, featureTargetOf(planTarget, feature.cap), lookupCap);
12395
+ return compositionFeatureEnabled(feature) ? result : {
12396
+ problems: result.problems,
12397
+ fields: [],
12398
+ features: [],
12399
+ events: []
12400
+ };
12401
+ });
12402
+ return {
12403
+ problems: results.flatMap((r) => r.problems),
12404
+ fields: results.flatMap((r) => r.fields),
12405
+ features: results.flatMap((r) => r.features),
12406
+ events: results.flatMap((r) => r.events)
12407
+ };
12408
+ }
12409
+ function compositionSourceRefs(composition) {
12410
+ const refs = composition.features.flatMap((f) => compositionFeatureEnabled(f) && f.kind === "fields" ? featureSources(f).flatMap(fieldReads).map((r) => r.source) : []);
12411
+ return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
12412
+ }
12413
+ /**
12414
+ * The candidate's device: the existing target itself, else `self`, else a
12415
+ * placeholder that cannot collide with a stored target. An existing target is
12416
+ * known even on create, so a second customization of it is refused on create too.
12417
+ */
12418
+ function candidateSelf(composition, ctx, plan) {
12419
+ return {
12420
+ ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
12421
+ native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
12422
+ replaced: replacedFieldKeys(plan)
12423
+ };
12424
+ }
12425
+ /**
12426
+ * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
12427
+ * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
12428
+ * derivation, so the graph reads both the same way.
12429
+ */
12430
+ function replacedFieldKeys(plan) {
12431
+ const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
12432
+ return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
12433
+ }
12434
+ /**
12435
+ * A read of the candidate's own device. Its own composed output (a new device's
12436
+ * field, an ADDED field) is the slice-1 own-field rule; on an existing target
12437
+ * every other field — replaced, or not composed at all — is the NATIVE value,
12438
+ * valid when the device serves that cap natively (D663, rule 5).
12439
+ */
12440
+ function selfReadProblems(read, at, fields, self) {
12441
+ const readKey = `${read.cap}.${read.fieldPath}`;
12442
+ if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
12443
+ if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
12444
+ if (isNativeCap(self.native, read.cap)) return [];
12445
+ return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
12446
+ }
12447
+ function sourceProblems(fields, ctx, self) {
12448
+ const selfKey = compositionSourceKey(self.ref);
12449
+ return fields.flatMap((field) => field.reads.flatMap((read) => {
12450
+ const at = compositionFieldAt(field);
12451
+ const key = compositionSourceKey(read.source);
12452
+ if (key === selfKey) return selfReadProblems(read, at, fields, self);
12453
+ const desc = ctx.sources.get(key);
12454
+ if (desc === void 0 || desc.kind === "unreadable") return [problem("source-unreadable", at, `could not read source \`${key}\` (${desc === void 0 ? "it was not read" : desc.error}) — the save is refused rather than stored unchecked`)];
12455
+ if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
12456
+ if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
12457
+ return [];
12458
+ }));
12459
+ }
12460
+ /**
12461
+ * A cycle's node keys are `compositionReadKey` strings:
12462
+ * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
12463
+ * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
12464
+ * back out so the refusal can point at that exact field, matching the
12465
+ * documented `features[N].fields.<path>` convention. A cap name has no `.`,
12466
+ * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
12467
+ * (`items[desiccant].status`) is matched through `compositionFieldKey`.
12468
+ */
12469
+ function candidateFieldOfNode(nodeKey, candidateKey) {
12470
+ const prefix = `${candidateKey}#`;
12471
+ if (!nodeKey.startsWith(prefix)) return null;
12472
+ const rest = nodeKey.slice(prefix.length);
12473
+ const dot = rest.indexOf(".");
12474
+ if (dot === -1) return null;
12475
+ return {
12476
+ cap: rest.slice(0, dot),
12477
+ fieldPath: rest.slice(dot + 1)
12478
+ };
12479
+ }
12480
+ /** The first node of a cycle's path that is one of THIS composition's own
12481
+ * planned fields, as a documented-convention path; `'features'` when the
12482
+ * cycle runs entirely through other stored compositions. */
12483
+ function cyclePath(nodePath, candidateKey, fields) {
12484
+ for (const nodeKey of nodePath) {
12485
+ const hit = candidateFieldOfNode(nodeKey, candidateKey);
12486
+ if (hit === null) continue;
12487
+ const hitKey = `${hit.cap}.${hit.fieldPath}`;
12488
+ const field = fields.find((f) => compositionFieldKey(f) === hitKey);
12489
+ if (field !== void 0) return compositionFieldAt(field);
12490
+ }
12491
+ return "features";
12492
+ }
12493
+ /** The first unread device any of `nodes` belongs to; null when none does. */
12494
+ function unreadDeviceOf(nodes, unread) {
12495
+ for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
12496
+ key,
12497
+ error
12498
+ };
12499
+ return null;
12500
+ }
12501
+ /**
12502
+ * Cycle check runs against the CANDIDATE (this composition, as block
12503
+ * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
12504
+ * decided per strongly connected component (`findCandidateCycles`): a cyclic
12505
+ * component holding a candidate node refuses, with one representative cycle
12506
+ * through it; one without is not this save's cycle. A
12507
+ * stored composition of the same block is its old version; another block on
12508
+ * the same device, or two stored peers sharing a device, make
12509
+ * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
12510
+ * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
12511
+ */
12512
+ function cycleProblems(composition, ctx, fields, self) {
12513
+ const candidate = {
12514
+ blockId: ctx.blockId ?? "(unsaved)",
12515
+ target: self.ref,
12516
+ composition,
12517
+ replaced: self.replaced
12518
+ };
12519
+ const candidateKey = compositionSourceKey(candidate.target);
12520
+ const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
12521
+ try {
12522
+ return findCandidateCycles(candidate, ctx.peers).map((c) => {
12523
+ const at = cyclePath(c.path, candidateKey, fields);
12524
+ const through = unreadDeviceOf([...c.component], unread);
12525
+ return through === null ? problem("cycle", at, `a field reads itself: ${c.path.join(" → ")}`) : problem("cycle", at, `possible cycle through \`${through.key}\` — its state could not be read (${through.error}): ${c.path.join(" → ")}`);
12526
+ });
12527
+ } catch (err) {
12528
+ if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
12529
+ throw err;
12530
+ }
12531
+ }
12532
+ function validateComposition(composition, ctx) {
12533
+ const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
12534
+ const self = candidateSelf(composition, ctx, plan);
12535
+ const cycles = cycleProblems(composition, ctx, plan.fields, self);
12536
+ const problems = [
12537
+ ...plan.problems,
12538
+ ...sourceProblems(plan.fields, ctx, self),
12539
+ ...cycles
12540
+ ];
12541
+ return {
12542
+ ok: problems.length === 0,
12543
+ problems,
12544
+ fields: [...plan.fields],
12545
+ features: [...plan.features],
12546
+ events: [...plan.events]
12547
+ };
12548
+ }
12549
+ /**
12550
+ * What a composition may BUILD on a device type (G2, D676): the caps a new
12551
+ * device of that type, or an existing one that lacks them, can be given. The
12552
+ * blockers are the validator's own refusal codes, and a spec compares this
12553
+ * listing with `planComposition` for every capability — the editor can never
12554
+ * offer a feature the save would refuse for the same reason.
12555
+ */
12556
+ var ComposableFieldSchema = z.object({
12557
+ path: z.string(),
12558
+ kind: CompositionFieldKindSchema,
12559
+ nullable: z.boolean(),
12560
+ required: z.boolean(),
12561
+ enumValues: z.array(z.string()).optional()
12562
+ });
12563
+ var ComposableCapabilitySchema = z.object({
12564
+ cap: z.string(),
12565
+ fields: z.array(ComposableFieldSchema),
12566
+ /** Present for an item-array cap (`consumables.items`). */
12567
+ itemArray: z.object({
12568
+ path: z.string(),
12569
+ keyField: z.string()
12570
+ }).optional()
12571
+ });
12572
+ var ComposableCapabilitiesSchema = z.object({ caps: z.array(ComposableCapabilitySchema) });
12573
+ /** Why a cap cannot be BUILT (new device, or added to an existing one) on `deviceType`; [] = it can. */
12574
+ function builtCapabilityBlockers(def, deviceType) {
12575
+ if (BASE_DEVICE_CAPS.has(def.name)) return ["not-composable"];
12576
+ const state = def.runtimeState;
12577
+ if (def.scope !== "device" || state === void 0) return ["not-composable"];
12578
+ const blockers = [];
12579
+ if (def.deviceTypes !== void 0 && !def.deviceTypes.includes(deviceType)) blockers.push("wrong-device-type");
12580
+ if (Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).length > 0) blockers.push("has-commands");
12581
+ const fields = describeTopLevelStateFields(state);
12582
+ const itemArrayPath = def.status?.itemArray?.path ?? null;
12583
+ if (requiredTopLevelKeys(state).some((key) => key !== itemArrayPath && !fields.has(key))) blockers.push("not-composable");
12584
+ if (planCompositionEvents(def, {
12585
+ featureIndex: 0,
12586
+ cap: def.name,
12587
+ mode: "new"
12588
+ }, []).problems.some((p) => p.code === "not-composable") && !blockers.includes("not-composable")) blockers.push("not-composable");
12589
+ return blockers;
12590
+ }
12591
+ /** Every cap with no blocker, sorted by name. */
12592
+ function describeComposableCaps(defs, deviceType) {
12593
+ return defs.filter((def) => builtCapabilityBlockers(def, deviceType).length === 0).map((def) => {
12594
+ const state = def.runtimeState;
12595
+ const fields = state === void 0 ? [] : [...describeTopLevelStateFields(state).values()];
12596
+ const itemArray = def.status?.itemArray;
12597
+ return {
12598
+ cap: def.name,
12599
+ fields: fields.map((f) => ({
12600
+ path: f.path,
12601
+ kind: f.kind,
12602
+ nullable: f.nullable,
12603
+ required: f.required,
12604
+ ...f.enumValues === void 0 ? {} : { enumValues: [...f.enumValues] }
12605
+ })),
12606
+ ...itemArray === void 0 ? {} : { itemArray: {
12607
+ path: itemArray.path,
12608
+ keyField: itemArray.keyField
12609
+ } }
12610
+ };
12611
+ }).toSorted((a, b) => a.cap.localeCompare(b.cap));
12612
+ }
12613
+ /**
12614
+ * core-blocks — user-authored TypeScript, stored in the kernel and executed in
12615
+ * its own process.
12616
+ *
12617
+ * Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
12618
+ *
12619
+ * The first use is **owning devices without being a device provider**: a block
12620
+ * declares devices under a system or custom integration and drives their state,
12621
+ * with the same `ctx` an addon gets. Automations come later; nothing here
12622
+ * models a trigger.
12623
+ *
12624
+ * **Stated plainly, because it does not change by being true:** a block has an
12625
+ * addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
12626
+ * with no review step. What makes that survivable is not a sandbox, it is
12627
+ * PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
12628
+ * so a block that throws or never returns is marked `failed` and visible
12629
+ * instead of taking the hub with it (D6). Every method here is admin-only, and
12630
+ * must stay so.
12631
+ */
12632
+ /**
12633
+ * Prefix for a block's generated addon id AND its runner id — one addon, one
12634
+ * process, so the two are the same string (D2).
12635
+ *
12636
+ * It lives on the CONTRACT rather than beside the code that materialises the
12637
+ * package, because it is not an implementation detail: the operator surface
12638
+ * reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
12639
+ * with no cap and no store of its own. Two copies of this string is one copy
12640
+ * too many — a drift would show as a logs pane that is simply always empty.
12641
+ */
12642
+ var CORE_BLOCK_ADDON_PREFIX = "core-block-";
12643
+ /**
12644
+ * The builtin's own addon id — and, because of that, the OWNER of the
12645
+ * integration every block's devices hang from.
12646
+ *
12647
+ * A block cannot own one. Every integration read is gated on the installed-addon
12648
+ * set (`addon-registry.service.ts` → `createFilteredRegistry`) and a block's
12649
+ * `core-block-<uuid>` is never in it, so its `getIntegrationByAddonId` answers
12650
+ * null forever while the write succeeds — one invisible, undeletable row per
12651
+ * start. `core-blocks` IS installed, so it reconciles one shared integration and
12652
+ * a block names it by id (`DeclaredDevicesSpec.integrationId`), minting nothing.
12653
+ *
12654
+ * Exported from the contract, next to the runner prefix and for the same reason:
12655
+ * a block reads it as
12656
+ * `ctx.api.integrations.getByAddonId.query({ addonId: CORE_BLOCKS_ADDON_ID })`,
12657
+ * so a second copy of the string is a block that silently declares into nowhere.
12658
+ */
12659
+ var CORE_BLOCKS_ADDON_ID = "core-blocks";
12660
+ /** The addon/runner id a block's process runs under. */
12661
+ function coreBlockAddonId(blockId) {
12662
+ return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
12663
+ }
12664
+ /** The block id behind a generated addon/runner id, or null when the id belongs
12665
+ * to something else. The inverse of {@link coreBlockAddonId}. */
12666
+ function coreBlockIdFromAddonId(addonId) {
12667
+ if (!addonId.startsWith("core-block-")) return null;
12668
+ const rest = addonId.slice(11);
12669
+ return rest.length > 0 ? rest : null;
12670
+ }
12671
+ /** Where a block runs. The operator chooses — a block driving a device on an
12672
+ * agent is the reason placement is not fixed to the hub. */
12673
+ var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
12674
+ /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
12675
+ * a failing block reads the same way a failing addon does. `degraded` is a
12676
+ * composition on an existing device with a field handed back to the native
12677
+ * provider while its source is unavailable (S1b, D663); the error names it. */
12678
+ var CoreBlockStatusSchema = z.enum([
12679
+ "stopped",
12680
+ "starting",
12681
+ "running",
12682
+ "degraded",
12683
+ "failed"
12684
+ ]);
12685
+ /** Client-authored fields. */
12686
+ var CoreBlockInputSchema = z.object({
12687
+ name: z.string().min(1).max(120),
12688
+ /** TypeScript source. Compiled server-side before it is ever stored — a
12689
+ * block that does not compile is a fork failure the operator would meet
12690
+ * minutes later, in a log, instead of in the editor. */
12691
+ code: z.string().max(2e5),
12692
+ enabled: z.boolean(),
12693
+ placement: CoreBlockPlacementSchema,
12694
+ /**
12695
+ * Integration the block's devices hang from. Absent = the system integration
12696
+ * blocks share. A block may declare its own instead.
12697
+ */
12698
+ integrationId: z.string().optional()
12699
+ });
12700
+ /**
12701
+ * What a block IS (D659). Absent means `code` — every row stored before this
12702
+ * union keeps working unchanged.
12703
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
12704
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
12705
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
12706
+ */
12707
+ var CoreBlockSourceSchema = z.discriminatedUnion("kind", [
12708
+ z.object({ kind: z.literal("code") }),
12709
+ z.object({
12710
+ kind: z.literal("recipe"),
12711
+ recipe: AutomationRecipeSchema
12712
+ }),
12713
+ z.object({
12714
+ kind: z.literal("composition"),
12715
+ composition: CompositionSchema
12716
+ })
12717
+ ]);
12718
+ /**
12719
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
12720
+ *
12721
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
12722
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
12723
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
12724
+ * client-sent name by name.
12725
+ */
12726
+ var CompositionBlockInputSchema = z.object({
12727
+ name: z.string().min(1).max(120).optional(),
12728
+ enabled: z.boolean(),
12729
+ composition: CompositionSchema
12730
+ });
12731
+ /** A stored block. */
12732
+ var CoreBlockSchema = CoreBlockInputSchema.extend({
12733
+ id: z.string(),
12734
+ createdAt: z.number(),
12735
+ updatedAt: z.number(),
12736
+ /** Server-stamped author. */
12737
+ createdBy: z.string(),
12738
+ status: CoreBlockStatusSchema,
12739
+ /**
12740
+ * Why the block is not running, when it is not. The operator's ONLY window
12741
+ * into a block that failed at load — a block that is silently absent is the
12742
+ * failure mode this whole feature has to avoid.
12743
+ */
12744
+ lastError: z.string().optional(),
12745
+ /** Ms epoch of the last state change. */
12746
+ lastChangedAt: z.number(),
12747
+ /** What the block is. Absent means `code` (D659). */
12748
+ source: CoreBlockSourceSchema.optional()
12749
+ });
12750
+ function coreBlockSourceKind(block) {
12751
+ return block.source?.kind ?? "code";
12752
+ }
12753
+ function coreBlockComposition(block) {
12754
+ return block.source?.kind === "composition" ? block.source.composition : null;
12755
+ }
12756
+ /** What the composer said, or that it could not be asked. Unknown never looks like absent (D315). */
12757
+ var CompositionStateReadSchema = z.discriminatedUnion("kind", [z.object({
12758
+ kind: z.literal("answered"),
12759
+ state: CompositionBlockStateSchema.nullable()
12760
+ }), z.object({
12761
+ kind: z.literal("unreachable"),
12762
+ error: z.string()
12763
+ })]);
12764
+ /** A stored composition block and what the composer says about it. */
12765
+ var CompositionViewSchema = z.object({
12766
+ block: CoreBlockSchema,
12767
+ state: CompositionStateReadSchema
12768
+ });
12769
+ /**
12770
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
12771
+ * table has not been READ: an empty answer then would make "not yet known"
12772
+ * look like "none", and invite the operator to recreate what exists (D315).
12773
+ */
12774
+ var CompositionsNotLoadedSchema = z.object({
12775
+ state: z.literal("not-loaded"),
12776
+ reason: z.string()
12777
+ });
12778
+ var CompositionListReadSchema = z.discriminatedUnion("state", [z.object({
12779
+ state: z.literal("loaded"),
12780
+ views: z.array(CompositionViewSchema)
12781
+ }), CompositionsNotLoadedSchema]);
12782
+ var CompositionGetReadSchema = z.discriminatedUnion("state", [z.object({
12783
+ state: z.literal("loaded"),
12784
+ view: CompositionViewSchema.nullable()
12785
+ }), CompositionsNotLoadedSchema]);
12786
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
12787
+ var CompositionCustomizationReadSchema = z.discriminatedUnion("state", [z.object({
12788
+ state: z.literal("loaded"),
12789
+ view: CompositionViewSchema.nullable()
12790
+ }), CompositionsNotLoadedSchema]);
12791
+ /** What a compile attempt produced. */
12792
+ var CoreBlockCompileResultSchema = z.object({
12793
+ ok: z.boolean(),
12794
+ /** Present when `ok` is false — the first error, in the author's words. */
12795
+ error: z.string().optional(),
12796
+ line: z.number().optional(),
12797
+ column: z.number().optional()
12798
+ });
12799
+ var coreBlocksCapability = {
12800
+ name: "core-blocks",
12801
+ scope: "system",
12802
+ mode: "singleton",
12803
+ methods: {
12804
+ list: method(z.object({}), z.object({ blocks: z.array(CoreBlockSchema) }), { auth: "admin" }),
12805
+ get: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema.nullable() }), { auth: "admin" }),
12806
+ /**
12807
+ * Create a block. The code is COMPILED first: storing source that does not
12808
+ * compile turns an editor error into a fork failure the operator meets
12809
+ * minutes later in a log.
12810
+ */
12811
+ create: method(z.object({ block: CoreBlockInputSchema }), z.object({ block: CoreBlockSchema }), {
12812
+ kind: "mutation",
12813
+ auth: "admin",
12814
+ caller: "required"
12815
+ }),
12816
+ update: method(z.object({
12817
+ blockId: z.string(),
12818
+ block: CoreBlockInputSchema.partial()
12819
+ }), z.object({ block: CoreBlockSchema }), {
12820
+ kind: "mutation",
12821
+ auth: "admin",
12822
+ caller: "required"
12823
+ }),
12824
+ delete: method(z.object({ blockId: z.string() }), z.object({ success: z.literal(true) }), {
12825
+ kind: "mutation",
12826
+ auth: "admin"
12827
+ }),
12828
+ setEnabled: method(z.object({
12829
+ blockId: z.string(),
12830
+ enabled: z.boolean()
12831
+ }), z.object({ block: CoreBlockSchema }), {
12832
+ kind: "mutation",
12833
+ auth: "admin"
12834
+ }),
12835
+ /**
12836
+ * Stop this block's runner and let the supervisor bring it back.
12837
+ *
12838
+ * **It stores nothing.** There is no "restart requested" flag and no second
12839
+ * lifecycle field: the supervisor already stops and respawns a runner when
12840
+ * the code hash changes, and this exposes that same path deliberately
12841
+ * (D62 — a switch writes the authority that already owned the function).
12842
+ * The next reconcile pass remains the authority, so a restart that lost a
12843
+ * race is corrected within one pass rather than leaving a stale flag behind.
12844
+ *
12845
+ * Refused for a DISABLED block: nothing would come back, and a button that
12846
+ * silently does nothing is worse than one that says why.
12847
+ */
12848
+ restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
12849
+ kind: "mutation",
12850
+ auth: "admin"
12851
+ }),
12852
+ /**
12853
+ * Type-check without saving — what the editor calls as the author types, so
12854
+ * the compiler's verdict is the same one the server will reach.
12855
+ */
12856
+ compile: method(z.object({ code: z.string() }), CoreBlockCompileResultSchema, {
12857
+ kind: "mutation",
12858
+ auth: "admin"
12859
+ }),
12860
+ /**
12861
+ * The declaration files the editor type-checks against.
12862
+ *
12863
+ * Served rather than bundled: the graph is 3.4 MB across 344 files, and
12864
+ * bundling it would roughly double the admin remote. Served rather than
12865
+ * hand-stubbed: a block runs with the same `ctx` an addon gets, so a stub
12866
+ * would drift and the editor would confidently autocomplete methods that
12867
+ * do not exist.
12868
+ */
12869
+ getTypeDefs: method(z.object({}), z.object({ libs: z.array(z.object({
11905
12870
  filePath: z.string(),
11906
12871
  content: z.string()
11907
12872
  })) }), { auth: "admin" }),
@@ -11941,6 +12906,14 @@ var coreBlocksCapability = {
11941
12906
  stableId: z.string().min(1)
11942
12907
  }), CompositionCustomizationReadSchema, { auth: "admin" }),
11943
12908
  /**
12909
+ * The capabilities a composition may BUILD on a device of `deviceType`
12910
+ * (a new device, or added to an existing one), with their fields — the
12911
+ * editor's feature list and the presets' availability (G2, D676). Built
12912
+ * from the validator's own blockers; a replace of a BOUND cap is not
12913
+ * listed here (the device's bindings say what it has).
12914
+ */
12915
+ listComposableCaps: method(z.object({ deviceType: z.enum(DeviceType) }), ComposableCapabilitiesSchema, { auth: "admin" }),
12916
+ /**
11944
12917
  * Validate without saving — the same verdict the save reaches. A mutation
11945
12918
  * for its body size; it writes nothing, so its access is `view`.
11946
12919
  */
@@ -20920,7 +21893,7 @@ function cameraSensorRefKey(ref) {
20920
21893
  }
20921
21894
  /** CS-1 accepts exactly one mode; CS-2 (D673) widens it to confirm / replace. */
20922
21895
  var CameraSensorsModeSchema = z.enum(["in-addition"]);
20923
- z.object({
21896
+ var CameraSensorsConfigSchema = z.object({
20924
21897
  mode: CameraSensorsModeSchema,
20925
21898
  sensors: z.array(CameraSensorRefSchema).min(1).max(8)
20926
21899
  }).superRefine((value, ctx) => {
@@ -26107,6 +27080,16 @@ var NodeInferenceDevicesSchema = z.object({
26107
27080
  * Strictly the control plane: never sees a frame, never calls inference,
26108
27081
  * never drains queues. The runner does that work locally on each agent.
26109
27082
  */
27083
+ /**
27084
+ * The input of `setCameraSensors` (G5, D676): the ONE typed write path of a
27085
+ * camera's attached sensors (D672). `config: null` removes the attachment —
27086
+ * the camera reads `not-configured` again, never `unreadable`.
27087
+ */
27088
+ var SetCameraSensorsInputSchema = z.object({
27089
+ deviceId: z.number().int().nonnegative(),
27090
+ /** `null` removes the attachment: the camera goes back to `not-configured`. */
27091
+ config: CameraSensorsConfigSchema.nullable()
27092
+ });
26110
27093
  var pipelineOrchestratorCapability = {
26111
27094
  name: "pipeline-orchestrator",
26112
27095
  scope: "system",
@@ -26572,6 +27555,18 @@ var pipelineOrchestratorCapability = {
26572
27555
  */
26573
27556
  getCameraStatuses: method(z.object({ deviceIds: z.array(z.number()).optional() }), z.array(CameraStatusSchema).readonly()),
26574
27557
  /**
27558
+ * Attach sensors to a camera, or detach them with `config: null` (D672
27559
+ * configuration, G5/D676). Written to the orchestrator's per-device store
27560
+ * under the same key `applyDeviceSettingsPatch` writes, and announced as a
27561
+ * settings change so the camera-sensors bridge refreshes. Refused by name
27562
+ * — nothing written — for a device that cannot be read, does not exist,
27563
+ * is not a camera, or names itself as its own sensor.
27564
+ */
27565
+ setCameraSensors: method(SetCameraSensorsInputSchema, z.object({ success: z.literal(true) }), {
27566
+ kind: "mutation",
27567
+ auth: "admin"
27568
+ }),
27569
+ /**
26575
27570
  * Disk-wins fleet reconcile after a recordings wipe. Starts the walk in
26576
27571
  * the addon process and returns immediately with the job snapshot — the
26577
27572
  * work outlives the tRPC/UDS 60s timeout. Poll `getReconcileFromDiskStatus`.
@@ -42569,978 +43564,736 @@ var WeatherStatusSchema = z.object({
42569
43564
  /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
42570
43565
  pressureUnit: z.string().nullable(),
42571
43566
  /** Wind speed in the reported unit. Null when not provided. */
42572
- windSpeed: z.number().nullable(),
42573
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
42574
- windSpeedUnit: z.string().nullable(),
42575
- /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
42576
- windBearing: z.number().nullable(),
42577
- /** Ms epoch when the slice was last updated. */
42578
- lastFetchedAt: z.number()
42579
- });
42580
- var weatherCapability = {
42581
- name: "weather",
42582
- scope: "device",
42583
- deviceNative: true,
42584
- mode: "singleton",
42585
- deviceTypes: [DeviceType.Weather],
42586
- methods: {},
42587
- status: {
42588
- schema: WeatherStatusSchema,
42589
- kind: "push"
42590
- },
42591
- /**
42592
- * Runtime-state slice — mirrored by the kernel. The UI reads the
42593
- * current conditions directly from the slice on each weather push.
42594
- */
42595
- runtimeState: WeatherStatusSchema,
42596
- /**
42597
- * Runtime-state durability: **session** — a forecast is stale the moment the hub is down; the provider re-fetches on connect.
42598
- *
42599
- * See `RuntimeStateDurability`. Enforced by
42600
- * `scripts/check-runtime-state-durability.ts`.
42601
- */
42602
- durability: "session"
42603
- };
42604
- /**
42605
- * Per-zone occupancy aggregation produced by the analytics frame
42606
- * processor on every inference result. Covers the full combinatorial
42607
- * matrix the operator UI needs: total objects everywhere, total
42608
- * objects per zone, single class everywhere, single class per zone,
42609
- * objects outside any zone.
42610
- *
42611
- * Counts are derived from the analytics tracker (tracked detections
42612
- * with stable trackIds), not raw detector hits — this filters out
42613
- * one-off detector flickers and gives counts that match what the user
42614
- * sees on the live overlay.
42615
- *
42616
- * Overlap policy: a detection that intersects two zones counts in
42617
- * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
42618
- * `frame` aggregate de-duplicates trivially since it's frame-wide.
42619
- * `unzoned` counts only detections that landed in zero zones.
42620
- */
42621
- var PerScopeBreakdownSchema = z.object({
42622
- /** Total tracked objects in this scope (frame / zone / unzoned). */
42623
- totalObjects: z.number().int().nonnegative(),
42624
- /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
42625
- byClass: z.record(z.string(), z.number().int().nonnegative()),
42626
- /**
42627
- * Of `totalObjects`, how many are STANDING — objects the census holds,
42628
- * present but not going anywhere (a parked car, a bin, a statue).
42629
- *
42630
- * The operator's question about a scene has two halves and they answer
42631
- * different things: "what is parked here" is stable for hours and must not
42632
- * flap, while "who is walking through" is the thing an alert is about. One
42633
- * number could only serve one of them, and `totalObjects` served the first
42634
- * badly — a passer-by moved it and every occupancy rule saw a change.
42635
- *
42636
- * `standing + transient === totalObjects`, always. The total keeps its old
42637
- * meaning exactly, so no rule written against it changes behaviour.
42638
- *
42639
- * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
42640
- * this field cannot say, and a reader that folds absence in as 0 reports "no
42641
- * parked objects here" about a camera whose lot is full.
42642
- */
42643
- standingObjects: z.number().int().nonnegative().optional(),
42644
- /** Of `totalObjects`, how many are passing through. See `standingObjects` —
42645
- * same absence rule. */
42646
- transientObjects: z.number().int().nonnegative().optional()
42647
- });
42648
- var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
42649
- zoneId: z.string(),
42650
- zoneName: z.string(),
42651
- /** TrackIds of objects currently inside this zone — for cross-reference
42652
- * with the per-track detail panel and live overlay. */
42653
- trackIds: z.array(z.string()).readonly()
42654
- });
42655
- /**
42656
- * A parked ("stationary") object surfaced alongside occupancy — an object that
42657
- * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
42658
- * told to forget it so it stops re-spawning tracks/events), but it IS still
42659
- * physically present, so it keeps counting toward `frame` occupancy and is
42660
- * listed here so the UI can show it in a dedicated "Stationary" section instead
42661
- * of flooding the live event feed.
42662
- */
42663
- var StationaryObjectSchema = z.object({
42664
- id: z.string(),
42665
- className: z.string(),
42666
- bbox: z.object({
42667
- x: z.number(),
42668
- y: z.number(),
42669
- w: z.number(),
42670
- h: z.number()
42671
- }),
42672
- frameWidth: z.number().int().nonnegative(),
42673
- frameHeight: z.number().int().nonnegative(),
42674
- /** When the source track was first seen. */
42675
- firstSeenAt: z.number().int(),
42676
- /** When the object was recognised as parked (promotion time). */
42677
- becameStationaryAt: z.number().int(),
42678
- /** Last frame a detection confirmed the object is still there. */
42679
- lastConfirmedAt: z.number().int(),
42680
- /** Enrichment label carried from the source track (identity / plate). */
42681
- label: z.string().optional(),
42682
- /** Native-resolution key-frame media key for the parked object's best image. */
42683
- keyFrameMediaId: z.number().int().optional()
42684
- });
42685
- var CameraOccupancySnapshotSchema = z.object({
42686
- /** Frame timestamp of the inference result that produced this snapshot. */
42687
- ts: z.number().int(),
42688
- /** Frame width/height in pixels — let the UI normalize bbox coords. */
42689
- frameWidth: z.number().int().nonnegative(),
42690
- frameHeight: z.number().int().nonnegative(),
42691
- /** Per-zone breakdown — one entry per defined zone (user + onboard). */
42692
- zones: z.array(ZoneScopeBreakdownSchema).readonly(),
42693
- /** Frame-wide aggregate (everywhere, regardless of zone membership).
42694
- * INCLUDES currently-confirmed stationary objects (they are still present). */
42695
- frame: PerScopeBreakdownSchema,
42696
- /** Detections that landed outside every zone. Empty when no zones defined. */
42697
- unzoned: PerScopeBreakdownSchema,
42698
- /** Parked objects on this camera (additive — absent on legacy snapshots).
42699
- * Surfaced separately so the UI shows them in a dedicated section rather
42700
- * than as repeated tracks/events. */
42701
- stationaryObjects: z.array(StationaryObjectSchema).readonly().optional()
42702
- });
42703
- /**
42704
- * One camera's row in a `getCurrentSnapshotBatch` answer.
42705
- *
42706
- * THREE outcomes, and the single-camera method could only express two of them
42707
- * because `snapshot: null` was already spoken for:
42708
- *
42709
- * - `read: 'read'`, `snapshot` present — the live occupancy reading.
42710
- * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
42711
- * yet: no frame since boot, and no parked-object registry to hydrate from.
42712
- * - `read: 'unreadable'` — the owner could not answer for this
42713
- * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
42714
- *
42715
- * Collapsing the last two is the failure this field exists to prevent: a
42716
- * hydration that threw would otherwise render as an empty Stationary section,
42717
- * which is a definite claim about a camera nobody could read.
42718
- */
42719
- var CameraOccupancySnapshotForDeviceSchema = z.object({
42720
- deviceId: z.number(),
42721
- read: z.enum(["read", "unreadable"]),
42722
- snapshot: CameraOccupancySnapshotSchema.nullable()
42723
- });
42724
- /**
42725
- * Time-series resolution. The history methods return one bucket per
42726
- * step over the requested range. Smaller resolutions cost more
42727
- * memory + bandwidth; bound to discrete steps so caller cannot ask
42728
- * for arbitrary fractional buckets.
42729
- */
42730
- var HistoryResolutionEnum = z.enum([
42731
- "minute",
42732
- "5min",
42733
- "hour"
42734
- ]);
42735
- var HistoryRangeSchema = z.object({
42736
- /** Range start (epoch ms, inclusive). */
42737
- from: z.number().int(),
42738
- /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
42739
- to: z.number().int(),
42740
- resolution: HistoryResolutionEnum
42741
- });
42742
- var HistoryPointSchema = z.object({
42743
- /** Bucket midpoint (epoch ms). */
42744
- ts: z.number().int(),
42745
- /** Object count averaged over the bucket (rounded to nearest integer). */
42746
- count: z.number().int().nonnegative()
42747
- });
42748
- /**
42749
- * Zone Analytics capability — spatial occupancy aggregations over
42750
- * tracked detections. Hosted by `addon-pipeline-analytics`.
42751
- *
42752
- * Live snapshot is mirrored into the `zone-occupancy` device-state
42753
- * slice on every frame; the cap method `getCurrentSnapshot` is a
42754
- * direct read for callers that don't want to subscribe.
42755
- *
42756
- * History methods are sourced from a 60-minute rolling in-memory
42757
- * ring of frame-level samples (sub-second resolution). Queries
42758
- * outside the ring window return empty arrays — durable storage
42759
- * is a follow-up (Step 4b: persisted samples → SQL store).
42760
- *
42761
- * Every history method takes an optional `className` filter:
42762
- * - omitted → bucket count includes ALL classes in scope
42763
- * - present → bucket count restricted to that class
42764
- *
42765
- * The `getZoneHistory` / `getCameraHistory` / `getUnzonedHistory`
42766
- * triple covers the three spatial scopes (per-zone, frame-wide,
42767
- * outside-any-zone). Crossing them with `className?` gives the full
42768
- * combinatorial coverage the operator UI requested.
42769
- *
42770
- * ## ⚠️ The three history methods are DEPRECATED
42771
- *
42772
- * They are served from a 60-MINUTE in-memory ring. It dies with the process,
42773
- * so after a restart they report nothing about a night that did happen, and any
42774
- * window past an hour is silently clamped — a caller cannot tell a clamp from
42775
- * an empty scene. The presets in the occupancy chart existed to hide that
42776
- * horizon rather than to answer a question.
42777
- *
42778
- * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
42779
- * instead. Those rows are already written for every camera, ~60 bytes per
42780
- * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
42781
- * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
42782
- * function the writer uses, so the two cannot drift.
42783
- *
42784
- * The ring stays until these three go, and it is the only thing left reading
42785
- * it. Do not add a fourth caller.
42786
- */
42787
- var zoneAnalyticsCapability = {
42788
- name: "zone-analytics",
42789
- scope: "device",
42790
- mode: "singleton",
42791
- deviceTypes: [DeviceType.Camera],
42792
- methods: {
42793
- /** Latest computed occupancy snapshot for this camera. Null when
42794
- * the analytics pipeline hasn't seen a frame for this device yet
42795
- * (no inference result emitted since boot or since binding was
42796
- * activated). */
42797
- getCurrentSnapshot: method(z.object({ deviceId: z.number() }), CameraOccupancySnapshotSchema.nullable()),
42798
- /**
42799
- * The same snapshot, for a SET of cameras, in one round trip.
42800
- *
42801
- * The Events page's Stationary section polls this every 15s for every
42802
- * selected camera. Fanned out client-side that is one query per camera to
42803
- * read a `Map.get` at the owner — the answer costs nothing, the round trip
42804
- * costs everything. Batched, N transports become one and the per-device
42805
- * work is unchanged.
42806
- *
42807
- * Every requested deviceId gets a row, tagged `read` — see
42808
- * {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
42809
- * not answer for is `'unreadable'`, never an empty reading.
42810
- */
42811
- getCurrentSnapshotBatch: method(z.object({ deviceIds: z.array(z.number()).min(1).max(200) }), z.array(CameraOccupancySnapshotForDeviceSchema).readonly()),
42812
- /** Time-series object count inside one zone. `className` optional —
42813
- * omit to count every class in the zone. */
42814
- getZoneHistory: method(z.object({
42815
- deviceId: z.number(),
42816
- zoneId: z.string(),
42817
- className: z.string().optional()
42818
- }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly()),
42819
- /** Time-series frame-wide object count (everywhere). */
42820
- getCameraHistory: method(z.object({
42821
- deviceId: z.number(),
42822
- className: z.string().optional()
42823
- }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly()),
42824
- /** Time-series count of objects outside every zone. */
42825
- getUnzonedHistory: method(z.object({
42826
- deviceId: z.number(),
42827
- className: z.string().optional()
42828
- }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly())
42829
- },
42830
- /**
42831
- * Runtime-state slice — the latest occupancy snapshot mirrored by
42832
- * the analytics frame processor on every inference result. Consumers
42833
- * read via `device.state.zoneAnalytics.value` and stay in sync
42834
- * automatically; the explicit `getCurrentSnapshot` cap method is
42835
- * still useful for one-off polls without a subscription.
42836
- */
42837
- runtimeState: CameraOccupancySnapshotSchema,
42838
- /**
42839
- * Runtime-state durability: **session** — per-frame analytics; with `audio-metrics` it is ~90 % of the offered write rate. Re-derived on the next frame.
42840
- *
42841
- * See `RuntimeStateDurability`. Enforced by
42842
- * `scripts/check-runtime-state-durability.ts`.
42843
- */
42844
- durability: "session"
42845
- };
42846
- /**
42847
- * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
42848
- * cap so a single CRUD surface backs every consumer; each stage has
42849
- * its own dev-state mirror slice (`motion-zone-rules`,
42850
- * `detection-zone-rules`, …) so consumer addons subscribe independently.
42851
- *
42852
- * Extend the enum here when a new gating consumer comes online (audio
42853
- * gating, alert filtering, …) — no other surface needs to change.
42854
- */
42855
- var ZoneRuleStageEnum = z.enum([
42856
- "motion",
42857
- "detection",
42858
- "package"
42859
- ]);
42860
- /**
42861
- * Zone rules capability — per-camera CRUD over the {@link ZoneRule}
42862
- * arrays that decide how each pipeline stage uses the polygon zones.
42863
- *
42864
- * Hosted by `addon-pipeline-orchestrator` alongside the zones provider
42865
- * so the operator has a single hub-side source of truth for both
42866
- * geometry and behaviour. Per-stage rules are stored under the
42867
- * `zoneRules.<stage>` key in the orchestrator's per-device store and
42868
- * mirrored to the device-state slice `<stage>-zone-rules` on every
42869
- * mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
42870
- * subscribe to that slice and refresh their gating without
42871
- * round-tripping the cap.
42872
- *
42873
- * Sets are bulk-replace — the operator UI sends the new rule list
42874
- * wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
42875
- * naturally in the rule editor without per-rule mutation chatter.
42876
- */
42877
- var zoneRulesCapability = {
42878
- name: "zone-rules",
42879
- scope: "device",
42880
- mode: "singleton",
42881
- deviceTypes: [DeviceType.Camera],
42882
- methods: {
42883
- /** Read the full rule list for a given stage (empty when no rules
42884
- * are defined yet). */
42885
- listRules: method(z.object({
42886
- deviceId: z.number(),
42887
- stage: ZoneRuleStageEnum
42888
- }), z.array(ZoneRuleSchema).readonly()),
42889
- /** Bulk-replace the rule list for one stage. The provider validates
42890
- * each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
42891
- * thresholds in range) and rejects the whole patch if any entry
42892
- * is invalid — partial writes are a configuration footgun. */
42893
- setRules: method(z.object({
42894
- deviceId: z.number(),
42895
- stage: ZoneRuleStageEnum,
42896
- rules: z.array(ZoneRuleSchema).readonly()
42897
- }), z.void(), {
42898
- kind: "mutation",
42899
- auth: "admin"
42900
- })
43567
+ windSpeed: z.number().nullable(),
43568
+ /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
43569
+ windSpeedUnit: z.string().nullable(),
43570
+ /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
43571
+ windBearing: z.number().nullable(),
43572
+ /** Ms epoch when the slice was last updated. */
43573
+ lastFetchedAt: z.number()
43574
+ });
43575
+ var weatherCapability = {
43576
+ name: "weather",
43577
+ scope: "device",
43578
+ deviceNative: true,
43579
+ mode: "singleton",
43580
+ deviceTypes: [DeviceType.Weather],
43581
+ methods: {},
43582
+ status: {
43583
+ schema: WeatherStatusSchema,
43584
+ kind: "push"
42901
43585
  },
42902
43586
  /**
42903
- * Runtime-state slice — every stage mirrored together so consumers
42904
- * see one reactive handle (`device.state.zoneRules.value`) instead
42905
- * of one per stage. Bulk-replace mutations on any stage write the full
42906
- * `{motion, detection, package}` shape, so subscribers always get the
42907
- * complete current set. Consumers that only care about one stage
42908
- * just read the matching property.
42909
- *
42910
- * `package` backs the package-drop detector — a package zone is a
42911
- * `ZoneRule` on the `'package'` stage referencing drawn polygons
42912
- * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
42913
- * The orchestrator provider writes this stage as a first-class slice
42914
- * (Phase 4): every mutation mirrors the full `{motion, detection,
42915
- * package}` shape, so consumers read the current package rules directly
42916
- * off `device.state.zoneRules.value.package`.
43587
+ * Runtime-state slice — mirrored by the kernel. The UI reads the
43588
+ * current conditions directly from the slice on each weather push.
42917
43589
  */
42918
- runtimeState: z.object({
42919
- motion: z.array(ZoneRuleSchema).readonly(),
42920
- detection: z.array(ZoneRuleSchema).readonly(),
42921
- package: z.array(ZoneRuleSchema).readonly()
42922
- }),
43590
+ runtimeState: WeatherStatusSchema,
42923
43591
  /**
42924
- * Runtime-state durability: **restored** — operator intent, mutation-only, same argument as `zones`.
43592
+ * Runtime-state durability: **session** — a forecast is stale the moment the hub is down; the provider re-fetches on connect.
42925
43593
  *
42926
43594
  * See `RuntimeStateDurability`. Enforced by
42927
43595
  * `scripts/check-runtime-state-durability.ts`.
42928
43596
  */
42929
- durability: "restored"
43597
+ durability: "session"
42930
43598
  };
42931
- function isValueOf(slot, value) {
42932
- return value.slot === slot;
42933
- }
42934
- /** Bind slot `slot` to its owner's functions. See the module docblock. */
42935
- function defineExtensionSlotProvider(slot, impl) {
42936
- return {
42937
- slot,
42938
- list: async (deviceId) => {
42939
- const listing = await impl.list(deviceId);
42940
- if (listing === null) return null;
42941
- const value = listing.value;
42942
- return {
42943
- ...listing,
42944
- value
42945
- };
42946
- },
42947
- set: async (deviceId, value) => {
42948
- if (!isValueOf(slot, value)) throw new Error(`device-extension: a "${value.slot}" value was sent to slot "${slot}" — refused`);
42949
- return await impl.set(deviceId, value);
42950
- },
42951
- candidates: (deviceId) => impl.candidates(deviceId)
42952
- };
42953
- }
42954
- /** `<source>:<slotId>`. */
42955
- function extensionSlotRef(source, slot) {
42956
- return `${source}:${slot}`;
42957
- }
42958
- /** Build the `device-extension` provider one owner registers. */
42959
- function createDeviceExtensionProvider(options) {
42960
- const { source, label, slots, hostTypeOf, logger } = options;
42961
- const offered = (slot, hostType) => hostType !== null && EXTENSION_SLOTS[slot].hostDeviceTypes.some((t) => t === hostType);
42962
- const resolve = async (deviceId, slotRef) => {
42963
- const prefix = `${source}:`;
42964
- const slot = slotRef.startsWith(prefix) ? slots.find((s) => s.slot === slotRef.slice(prefix.length)) : void 0;
42965
- if (slot === void 0) throw new Error(`device-extension: "${slotRef}" is not a slot of "${source}" — it serves ${slots.map((s) => extensionSlotRef(source, s.slot)).join(", ") || "(none)"}`);
42966
- const hostType = await hostTypeOf(deviceId);
42967
- if (!offered(slot.slot, hostType)) throw new Error(`device-extension: slot "${slot.slot}" is not offered on device ${deviceId} (type ${hostType ?? "unknown"}; offered on ${EXTENSION_SLOTS[slot.slot].hostDeviceTypes.join(", ")})`);
42968
- return slot;
42969
- };
42970
- return {
42971
- listSources: async () => [{
42972
- source,
42973
- label,
42974
- availability: { state: "ok" }
42975
- }],
42976
- listSlots: async (input) => {
42977
- const hostType = await hostTypeOf(input.deviceId);
42978
- const rows = [];
42979
- for (const slot of slots) {
42980
- if (!offered(slot.slot, hostType)) continue;
42981
- const listing = await slot.list(input.deviceId);
42982
- if (listing === null) continue;
42983
- rows.push({
42984
- state: "ok",
42985
- slotRef: extensionSlotRef(source, slot.slot),
42986
- slot: slot.slot,
42987
- authority: listing.authority,
42988
- activation: listing.activation,
42989
- value: listing.value,
42990
- sources: [...listing.sources],
42991
- ...listing.impliedSources !== void 0 ? { impliedSources: [...listing.impliedSources] } : {}
42992
- });
42993
- }
42994
- return rows;
42995
- },
42996
- listCandidates: async (input) => {
42997
- return { candidates: [...await (await resolve(input.deviceId, input.slotRef)).candidates(input.deviceId)] };
42998
- },
42999
- setSlot: async (input) => {
43000
- const result = await (await resolve(input.deviceId, input.slotRef)).set(input.deviceId, input.value);
43001
- logger.info("device-extension: slot written", {
43002
- tags: { deviceId: input.deviceId },
43003
- meta: {
43004
- slotRef: input.slotRef,
43005
- reload: result.reload
43006
- }
43007
- });
43008
- return result;
43009
- }
43010
- };
43011
- }
43012
43599
  /**
43013
- * The field graph across every composition, and the cycles in it (D659).
43014
- *
43015
- * A node is ONE field of ONE device (`<addon>/<stableId>#<cap>.<field>`); an
43016
- * edge points from a composed field to each field it reads. A cycle, direct or
43017
- * through another composition, is refused at save by name (spec §2).
43018
- *
43019
- * The graph is built for ONE save: the CANDIDATE composition (the one being
43020
- * validated) plus every STORED composition. The candidate replaces its own old
43021
- * version, identified BY BLOCK ID wherever it sits in `stored` — whether a save
43022
- * introduces or removes a cycle must never depend on array order. A DIFFERENT
43023
- * block targeting the candidate's device is refused by name: one composition
43024
- * per device (D663). Two different stored peers that (wrongly) target the same
43025
- * device are corrupted state, not a cycle to resolve by picking one silently;
43026
- * that is refused by name too (D391).
43027
- *
43028
- * A customization of an EXISTING device reads that device's NATIVE value for a
43029
- * field it replaces or does not write ("Invert state" reads the `on` it
43030
- * replaces): such a self-read is no edge, so it is never a cycle (D663).
43031
- * Another composition reading that field reads the customizer's output.
43600
+ * Per-zone occupancy aggregation produced by the analytics frame
43601
+ * processor on every inference result. Covers the full combinatorial
43602
+ * matrix the operator UI needs: total objects everywhere, total
43603
+ * objects per zone, single class everywhere, single class per zone,
43604
+ * objects outside any zone.
43032
43605
  *
43033
- * `findCompositionCycles` reports at least one cycle per cycle-SET (the DFS
43034
- * marks a node `done` after its first expansion) — not every elementary cycle
43035
- * through a shared node, so the path it names may AVOID the candidate while
43036
- * one through it goes unreported. What a save refuses is therefore decided per
43037
- * strongly connected component (`findCandidateCycles`, C-1). A dotted
43038
- * `fieldPath` is matched only as an exact string: a write to `state.value` and
43039
- * a read of `state` would not connect (no nested field writes exist yet).
43606
+ * Counts are derived from the analytics tracker (tracked detections
43607
+ * with stable trackIds), not raw detector hits — this filters out
43608
+ * one-off detector flickers and gives counts that match what the user
43609
+ * sees on the live overlay.
43040
43610
  *
43041
- * An item leaf is its own node, `…#consumables.items[desiccant].status`
43042
- * (`featureFieldEntries`), so two items sharing an inner path never merge. No
43043
- * READ can name an array element (`describeStateField` never describes one),
43044
- * so an item node is write-only: its edges to sources are what the check needs.
43045
- */
43046
- /** Two blocks' compositions target the same device — refused by name, never silently resolved. */
43047
- var CompositionGraphError = class extends Error {};
43048
- /** `"cap.field"` of every field a peer writes — the same spelling as `compositionFieldKey`. */
43049
- function writtenFields(peer) {
43050
- return new Set(peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath }) => `${feature.cap}.${fieldPath}`)));
43051
- }
43052
- /** A read of the peer's OWN device is an edge only when it reads the peer's own output (rule 5, D663). */
43053
- function readsComposedValue(peer, written, read) {
43054
- if (compositionSourceKey(read.source) !== compositionSourceKey(peer.target)) return true;
43055
- const field = `${read.cap}.${read.fieldPath}`;
43056
- return written.has(field) && peer.replaced?.has(field) !== true;
43057
- }
43058
- function peerEdges(peer) {
43059
- const written = writtenFields(peer);
43060
- return peer.composition.features.flatMap((feature) => !compositionFeatureEnabled(feature) || feature.kind !== "fields" ? [] : featureFieldEntries(feature).map(({ fieldPath, source }) => [compositionReadKey({
43061
- source: peer.target,
43062
- cap: feature.cap,
43063
- fieldPath
43064
- }), fieldReads(source).filter((read) => readsComposedValue(peer, written, read)).map(compositionReadKey)]));
43065
- }
43066
- /**
43067
- * Every peer that actually takes part in the graph: each stored peer minus the
43068
- * candidate's own old version (same block id — the drop happens before any
43069
- * edge is built, so its old edges never enter the map), plus the candidate
43070
- * itself, exactly once. Throws {@link CompositionGraphError} when a DIFFERENT
43071
- * block targets the candidate's device, or two stored peers share a device.
43611
+ * Overlap policy: a detection that intersects two zones counts in
43612
+ * BOTH zones' `byClass` and `totalObjects` (count-in-each). The
43613
+ * `frame` aggregate de-duplicates trivially since it's frame-wide.
43614
+ * `unzoned` counts only detections that landed in zero zones.
43072
43615
  */
43073
- function resolvedPeers(candidate, stored) {
43074
- const candidateKey = compositionSourceKey(candidate.target);
43075
- const survivors = /* @__PURE__ */ new Map();
43076
- for (const peer of stored) {
43077
- if (peer.blockId === candidate.blockId) continue;
43078
- const key = compositionSourceKey(peer.target);
43079
- 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`);
43080
- const clash = survivors.get(key);
43081
- if (clash !== void 0) throw new CompositionGraphError(`two stored compositions (\`${clash.blockId}\`, \`${peer.blockId}\`) both target device \`${key}\` — refusing to build the field graph`);
43082
- survivors.set(key, peer);
43083
- }
43084
- return [...survivors.values(), candidate];
43085
- }
43086
- function compositionEdges(candidate, stored) {
43087
- const edges = resolvedPeers(candidate, stored).flatMap(peerEdges);
43088
- return new Map(edges);
43089
- }
43090
- /** Tarjan's strongly connected components over every node of the graph (sources included). */
43091
- function stronglyConnected(edges) {
43092
- const index = /* @__PURE__ */ new Map();
43093
- const low = /* @__PURE__ */ new Map();
43094
- const onStack = /* @__PURE__ */ new Set();
43095
- const stack = [];
43096
- const components = [];
43097
- const connect = (node) => {
43098
- index.set(node, index.size);
43099
- low.set(node, index.get(node) ?? 0);
43100
- stack.push(node);
43101
- onStack.add(node);
43102
- for (const next of edges.get(node) ?? []) if (!index.has(next)) {
43103
- connect(next);
43104
- low.set(node, Math.min(low.get(node) ?? 0, low.get(next) ?? 0));
43105
- } else if (onStack.has(next)) low.set(node, Math.min(low.get(node) ?? 0, index.get(next) ?? 0));
43106
- if (low.get(node) !== index.get(node)) return;
43107
- const component = [];
43108
- for (let member = stack.pop(); member !== void 0; member = stack.pop()) {
43109
- onStack.delete(member);
43110
- component.push(member);
43111
- if (member === node) break;
43112
- }
43113
- components.push(component);
43114
- };
43115
- for (const node of edges.keys()) if (!index.has(node)) connect(node);
43116
- return components;
43117
- }
43118
- /** The shortest walk from `start` back to itself inside `component` (one exists: the component has a cycle). */
43119
- function cycleThrough(start, component, edges) {
43120
- const parent = /* @__PURE__ */ new Map();
43121
- const queue = [start];
43122
- for (let head = 0; head < queue.length; head += 1) {
43123
- const node = queue[head];
43124
- if (node === void 0) break;
43125
- for (const next of edges.get(node) ?? []) {
43126
- if (next === start) {
43127
- const chain = [];
43128
- for (let at = node; at !== void 0 && at !== start; at = parent.get(at)) chain.push(at);
43129
- return [
43130
- start,
43131
- ...chain.reverse(),
43132
- start
43133
- ];
43134
- }
43135
- if (!component.has(next) || parent.has(next)) continue;
43136
- parent.set(next, node);
43137
- queue.push(next);
43138
- }
43139
- }
43140
- return [start, start];
43141
- }
43616
+ var PerScopeBreakdownSchema = z.object({
43617
+ /** Total tracked objects in this scope (frame / zone / unzoned). */
43618
+ totalObjects: z.number().int().nonnegative(),
43619
+ /** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
43620
+ byClass: z.record(z.string(), z.number().int().nonnegative()),
43621
+ /**
43622
+ * Of `totalObjects`, how many are STANDING — objects the census holds,
43623
+ * present but not going anywhere (a parked car, a bin, a statue).
43624
+ *
43625
+ * The operator's question about a scene has two halves and they answer
43626
+ * different things: "what is parked here" is stable for hours and must not
43627
+ * flap, while "who is walking through" is the thing an alert is about. One
43628
+ * number could only serve one of them, and `totalObjects` served the first
43629
+ * badly — a passer-by moved it and every occupancy rule saw a change.
43630
+ *
43631
+ * `standing + transient === totalObjects`, always. The total keeps its old
43632
+ * meaning exactly, so no rule written against it changes behaviour.
43633
+ *
43634
+ * ABSENT means UNKNOWN, never zero (D393): a snapshot from a node older than
43635
+ * this field cannot say, and a reader that folds absence in as 0 reports "no
43636
+ * parked objects here" about a camera whose lot is full.
43637
+ */
43638
+ standingObjects: z.number().int().nonnegative().optional(),
43639
+ /** Of `totalObjects`, how many are passing through. See `standingObjects` —
43640
+ * same absence rule. */
43641
+ transientObjects: z.number().int().nonnegative().optional()
43642
+ });
43643
+ var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
43644
+ zoneId: z.string(),
43645
+ zoneName: z.string(),
43646
+ /** TrackIds of objects currently inside this zone — for cross-reference
43647
+ * with the per-track detail panel and live overlay. */
43648
+ trackIds: z.array(z.string()).readonly()
43649
+ });
43142
43650
  /**
43143
- * The cycles THIS save closes, decided per strongly connected component — never
43144
- * per reported DFS path, which names one back-edge per revisit and can report a
43145
- * component's cycle by a path that avoids the candidate while the one through it
43146
- * goes unreported. A component refuses when it holds a candidate node and a
43147
- * cycle (more than one node, or a self-loop). Components without a candidate
43148
- * node are not this save's cycles.
43651
+ * A parked ("stationary") object surfaced alongside occupancy — an object that
43652
+ * settled and stopped moving. It is NO LONGER a tracked object (the tracker was
43653
+ * told to forget it so it stops re-spawning tracks/events), but it IS still
43654
+ * physically present, so it keeps counting toward `frame` occupancy and is
43655
+ * listed here so the UI can show it in a dedicated "Stationary" section instead
43656
+ * of flooding the live event feed.
43149
43657
  */
43150
- function findCandidateCycles(candidate, stored) {
43151
- const edges = compositionEdges(candidate, stored);
43152
- const prefix = `${compositionSourceKey(candidate.target)}#`;
43153
- return stronglyConnected(edges).flatMap((members) => {
43154
- const start = members.find((n) => n.startsWith(prefix));
43155
- if (start === void 0) return [];
43156
- const component = new Set(members);
43157
- const selfLoop = members.length === 1 && (edges.get(start) ?? []).includes(start);
43158
- if (members.length === 1 && !selfLoop) return [];
43159
- return [{
43160
- path: cycleThrough(start, component, edges),
43161
- component
43162
- }];
43163
- });
43164
- }
43165
- function problem(code, path, message) {
43166
- return {
43167
- code,
43168
- path,
43169
- message
43170
- };
43171
- }
43172
- function refused(...problems) {
43173
- return {
43174
- problems,
43175
- fields: [],
43176
- features: [],
43177
- events: []
43178
- };
43179
- }
43180
- /** Where a planned field sits in the composition: `features[N].fields.<path>` or `features[N].items.<key>.fields.<path>`. */
43181
- function compositionFieldAt(field) {
43182
- const inner = field.item === void 0 ? "fields" : `items.${field.item.key}.fields`;
43183
- return `features[${field.featureIndex}].${inner}.${field.path}`;
43184
- }
43185
- var NUMBER = {
43186
- kind: "fixed",
43187
- result: ["number"]
43188
- };
43189
- var BOOLEAN = {
43190
- kind: "fixed",
43191
- result: ["boolean"]
43192
- };
43193
- /** One rule per builtin. `expression-kinds.spec.ts` fails for a builtin missing here. */
43194
- var BUILTIN_RESULT_KINDS = Object.freeze({
43195
- min: NUMBER,
43196
- max: NUMBER,
43197
- abs: NUMBER,
43198
- floor: NUMBER,
43199
- ceil: NUMBER,
43200
- sqrt: NUMBER,
43201
- round: NUMBER,
43202
- pow: NUMBER,
43203
- clamp: NUMBER,
43204
- avg: NUMBER,
43205
- sum: NUMBER,
43206
- age: NUMBER,
43207
- convert: NUMBER,
43208
- coalesce: { kind: "coalesce" },
43209
- any: BOOLEAN,
43210
- all: BOOLEAN,
43211
- within: BOOLEAN,
43212
- latest: {
43213
- kind: "fixed",
43214
- result: ["number", "null"]
43215
- },
43216
- number: {
43217
- kind: "fixed",
43218
- result: ["number", "null"]
43219
- },
43220
- rose: {
43221
- kind: "fixed",
43222
- result: ["number", "null"]
43223
- },
43224
- count: NUMBER
43658
+ var StationaryObjectSchema = z.object({
43659
+ id: z.string(),
43660
+ className: z.string(),
43661
+ bbox: z.object({
43662
+ x: z.number(),
43663
+ y: z.number(),
43664
+ w: z.number(),
43665
+ h: z.number()
43666
+ }),
43667
+ frameWidth: z.number().int().nonnegative(),
43668
+ frameHeight: z.number().int().nonnegative(),
43669
+ /** When the source track was first seen. */
43670
+ firstSeenAt: z.number().int(),
43671
+ /** When the object was recognised as parked (promotion time). */
43672
+ becameStationaryAt: z.number().int(),
43673
+ /** Last frame a detection confirmed the object is still there. */
43674
+ lastConfirmedAt: z.number().int(),
43675
+ /** Enrichment label carried from the source track (identity / plate). */
43676
+ label: z.string().optional(),
43677
+ /** Native-resolution key-frame media key for the parked object's best image. */
43678
+ keyFrameMediaId: z.number().int().optional()
43679
+ });
43680
+ var CameraOccupancySnapshotSchema = z.object({
43681
+ /** Frame timestamp of the inference result that produced this snapshot. */
43682
+ ts: z.number().int(),
43683
+ /** Frame width/height in pixels — let the UI normalize bbox coords. */
43684
+ frameWidth: z.number().int().nonnegative(),
43685
+ frameHeight: z.number().int().nonnegative(),
43686
+ /** Per-zone breakdown — one entry per defined zone (user + onboard). */
43687
+ zones: z.array(ZoneScopeBreakdownSchema).readonly(),
43688
+ /** Frame-wide aggregate (everywhere, regardless of zone membership).
43689
+ * INCLUDES currently-confirmed stationary objects (they are still present). */
43690
+ frame: PerScopeBreakdownSchema,
43691
+ /** Detections that landed outside every zone. Empty when no zones defined. */
43692
+ unzoned: PerScopeBreakdownSchema,
43693
+ /** Parked objects on this camera (additive — absent on legacy snapshots).
43694
+ * Surfaced separately so the UI shows them in a dedicated section rather
43695
+ * than as repeated tracks/events. */
43696
+ stationaryObjects: z.array(StationaryObjectSchema).readonly().optional()
43225
43697
  });
43226
- var KindInferenceError = class extends Error {};
43227
- function kindOfValue(value) {
43228
- if (value === null) return "null";
43229
- if (typeof value === "number") return "number";
43230
- if (typeof value === "string") return "string";
43231
- return "boolean";
43232
- }
43233
- function only(kind) {
43234
- return new Set([kind]);
43235
- }
43236
- function union(...sets) {
43237
- return new Set(sets.flatMap((s) => [...s]));
43238
- }
43239
- function withoutNull(set) {
43240
- return new Set([...set].filter((k) => k !== "null"));
43241
- }
43242
- function binaryKinds(op, left, right) {
43243
- if (op === "+") {
43244
- const all = [...left, ...right];
43245
- if (all.every((k) => k === "string")) return only("string");
43246
- if (all.every((k) => k === "number")) return only("number");
43247
- return new Set(["number", "string"]);
43248
- }
43249
- if (op === "-" || op === "*" || op === "/" || op === "%") return only("number");
43250
- return only("boolean");
43251
- }
43252
- /** "exactly N" / "at least N" / "N–M", for an arity refusal that says what IS allowed. */
43253
- function arityRange(builtin) {
43254
- if (builtin.minArgs === builtin.maxArgs) return `exactly ${builtin.minArgs}`;
43255
- if (builtin.maxArgs === Number.POSITIVE_INFINITY) return `at least ${builtin.minArgs}`;
43256
- return `${builtin.minArgs}–${builtin.maxArgs}`;
43257
- }
43258
- function callKinds(callee, args) {
43259
- const builtin = EXPRESSION_BUILTINS[callee];
43260
- 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)}`);
43261
- const rule = BUILTIN_RESULT_KINDS[callee];
43262
- if (rule === void 0) throw new KindInferenceError(`no result-kind rule for builtin '${callee}'`);
43263
- if (rule.kind === "fixed") return new Set(rule.result);
43264
- const nonNull = union(...args.map(withoutNull));
43265
- return args.every((a) => a.has("null")) ? union(nonNull, only("null")) : nonNull;
43266
- }
43267
- function kindsOf(node, bindings) {
43268
- switch (node.kind) {
43269
- case "literal": return only(kindOfValue(node.value));
43270
- case "identifier": {
43271
- if (node.name === "now") return only("number");
43272
- const bound = bindings.get(node.name);
43273
- if (bound === void 0) throw new KindInferenceError(`unbound identifier '${node.name}'`);
43274
- return bound;
43275
- }
43276
- case "unary": return only(node.op === "!" ? "boolean" : "number");
43277
- case "binary": return binaryKinds(node.op, kindsOf(node.left, bindings), kindsOf(node.right, bindings));
43278
- case "logical": return union(kindsOf(node.left, bindings), kindsOf(node.right, bindings));
43279
- case "conditional": return union(kindsOf(node.consequent, bindings), kindsOf(node.alternate, bindings));
43280
- case "call": return callKinds(node.callee, node.args.map((a) => kindsOf(a, bindings)));
43281
- }
43282
- }
43283
- function nodeContainsCall(node, callee) {
43284
- switch (node.kind) {
43285
- case "literal":
43286
- case "identifier": return false;
43287
- case "unary": return nodeContainsCall(node.operand, callee);
43288
- case "binary":
43289
- case "logical": return nodeContainsCall(node.left, callee) || nodeContainsCall(node.right, callee);
43290
- case "conditional": return nodeContainsCall(node.test, callee) || nodeContainsCall(node.consequent, callee) || nodeContainsCall(node.alternate, callee);
43291
- case "call": return node.callee === callee || node.args.some((arg) => nodeContainsCall(arg, callee));
43292
- }
43293
- }
43294
- function expressionContainsBuiltinCall(expr, callee) {
43295
- const compiled = compileExpressionSafe(expr);
43296
- return compiled.ok && nodeContainsCall(compiled.parsed.ast, callee);
43297
- }
43298
- function inferExpressionKinds(input) {
43299
- const compiled = compileExpressionSafe(input.expr);
43300
- if (!compiled.ok) return {
43301
- ok: false,
43302
- error: compiled.error
43303
- };
43304
- try {
43305
- return {
43306
- ok: true,
43307
- kinds: kindsOf(compiled.parsed.ast, input.bindingKinds)
43308
- };
43309
- } catch (err) {
43310
- if (err instanceof KindInferenceError) return {
43311
- ok: false,
43312
- error: err.message
43313
- };
43314
- throw err;
43315
- }
43316
- }
43317
43698
  /**
43318
- * The kind half of planning a composition field (D659): which kinds a source
43319
- * can produce, whether they fit a target field, and the field's role.
43320
- * Shared by the top-level planner (`validate-composition.ts`) and the item
43321
- * planner (`composition-items.ts`). Internal to the composition module — NOT
43322
- * in the barrel: these are planner internals, not a published contract.
43699
+ * One camera's row in a `getCurrentSnapshotBatch` answer.
43700
+ *
43701
+ * THREE outcomes, and the single-camera method could only express two of them
43702
+ * because `snapshot: null` was already spoken for:
43703
+ *
43704
+ * - `read: 'read'`, `snapshot` present — the live occupancy reading.
43705
+ * - `read: 'read'`, `snapshot: null` — read, and this camera has no reading
43706
+ * yet: no frame since boot, and no parked-object registry to hydrate from.
43707
+ * - `read: 'unreadable'` — the owner could not answer for this
43708
+ * camera. `snapshot` is null, and it does NOT mean "nothing parked here".
43709
+ *
43710
+ * Collapsing the last two is the failure this field exists to prevent: a
43711
+ * hydration that threw would otherwise render as an empty Stationary section,
43712
+ * which is a definite claim about a camera nobody could read.
43323
43713
  */
43324
- function readKinds(read, lookupCap) {
43325
- const def = lookupCap(read.cap);
43326
- const schema = def?.runtimeState ?? def?.status?.schema;
43327
- if (def === null || schema === void 0) return {
43328
- ok: false,
43329
- code: "source-field-unknown",
43330
- message: `no capability \`${read.cap}\` with state to read`
43331
- };
43332
- const field = describeStateField(schema, read.fieldPath);
43333
- if (field === null) return {
43334
- ok: false,
43335
- code: "source-field-unknown",
43336
- message: `\`${read.cap}\` has no field \`${read.fieldPath}\``
43337
- };
43338
- const base = field.kind === "enum" ? "string" : field.kind;
43339
- return {
43340
- ok: true,
43341
- kinds: new Set(field.nullable ? [base, "null"] : [base])
43342
- };
43343
- }
43344
- function sourceKinds(source, lookupCap) {
43345
- if (source.kind === "from") return readKinds(source, lookupCap);
43346
- if (source.kind === "code") return {
43347
- ok: false,
43348
- code: "reserved-variant",
43349
- message: "code fields arrive in slice 4"
43350
- };
43351
- const bindingKinds = /* @__PURE__ */ new Map();
43352
- for (const [name, binding] of Object.entries(source.bindings)) {
43353
- if (binding.kind === "literal") {
43354
- bindingKinds.set(name, new Set([kindOfValue(binding.value)]));
43355
- continue;
43356
- }
43357
- const kinds = readKinds(binding, lookupCap);
43358
- if (!kinds.ok) return {
43359
- ...kinds,
43360
- message: `binding \`${name}\`: ${kinds.message}`
43361
- };
43362
- bindingKinds.set(name, kinds.kinds);
43363
- }
43364
- const inferred = inferExpressionKinds({
43365
- expr: source.expr,
43366
- bindingKinds
43367
- });
43368
- return inferred.ok ? {
43369
- ok: true,
43370
- kinds: inferred.kinds
43371
- } : {
43372
- ok: false,
43373
- code: "expression-invalid",
43374
- message: inferred.error
43375
- };
43376
- }
43377
- function compatibility(cap, target, kinds, at) {
43378
- const allowed = target.kind === "enum" ? "string" : target.kind;
43379
- const offending = [...kinds].filter((k) => k !== "null" && k !== allowed);
43380
- const problems = [];
43381
- if (offending.length > 0) problems.push(problem("kind-mismatch", at, `\`${cap}.${target.path}\` is a ${target.kind}; this source can produce ${offending.join(" or ")}`));
43382
- 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`));
43383
- return problems;
43384
- }
43714
+ var CameraOccupancySnapshotForDeviceSchema = z.object({
43715
+ deviceId: z.number(),
43716
+ read: z.enum(["read", "unreadable"]),
43717
+ snapshot: CameraOccupancySnapshotSchema.nullable()
43718
+ });
43385
43719
  /**
43386
- * A string a source produces WITHOUT reading any device — a field-level
43387
- * `expression` source whose compiled AST is a bare string literal (`"red"`,
43388
- * never `a` or `a + "x"`). Anything else that can only produce `'string'` at
43389
- * the kind level (a `from` read of an enum-typed source field, a computed
43390
- * expression) is NOT checked against the target's `enumValues` here: the
43391
- * kind inference already collapsed it to the coarse `'string'` kind, losing
43392
- * whichever enum it actually came from, so membership can only be judged at
43393
- * write time against the runtime-state schema. Known limitation — see the
43394
- * fix report for Task 6 round 1.
43720
+ * Time-series resolution. The history methods return one bucket per
43721
+ * step over the requested range. Smaller resolutions cost more
43722
+ * memory + bandwidth; bound to discrete steps so caller cannot ask
43723
+ * for arbitrary fractional buckets.
43395
43724
  */
43396
- function literalStringValue(source) {
43397
- if (source.kind !== "expression") return null;
43398
- const compiled = compileExpressionSafe(source.expr);
43399
- if (!compiled.ok) return null;
43400
- const ast = compiled.parsed.ast;
43401
- return ast.kind === "literal" && typeof ast.value === "string" ? ast.value : null;
43402
- }
43725
+ var HistoryResolutionEnum = z.enum([
43726
+ "minute",
43727
+ "5min",
43728
+ "hour"
43729
+ ]);
43730
+ var HistoryRangeSchema = z.object({
43731
+ /** Range start (epoch ms, inclusive). */
43732
+ from: z.number().int(),
43733
+ /** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
43734
+ to: z.number().int(),
43735
+ resolution: HistoryResolutionEnum
43736
+ });
43737
+ var HistoryPointSchema = z.object({
43738
+ /** Bucket midpoint (epoch ms). */
43739
+ ts: z.number().int(),
43740
+ /** Object count averaged over the bucket (rounded to nearest integer). */
43741
+ count: z.number().int().nonnegative()
43742
+ });
43403
43743
  /**
43404
- * An enum target whose source is a save-time-KNOWN string literal outside its
43405
- * `enumValues` is refused now rather than left to fail the write later. A
43406
- * dynamic source (a `from` read, a non-literal expression) is left to
43407
- * runtime — see {@link literalStringValue}.
43744
+ * Zone Analytics capability — spatial occupancy aggregations over
43745
+ * tracked detections. Hosted by `addon-pipeline-analytics`.
43746
+ *
43747
+ * Live snapshot is mirrored into the `zone-occupancy` device-state
43748
+ * slice on every frame; the cap method `getCurrentSnapshot` is a
43749
+ * direct read for callers that don't want to subscribe.
43750
+ *
43751
+ * History methods are sourced from a 60-minute rolling in-memory
43752
+ * ring of frame-level samples (sub-second resolution). Queries
43753
+ * outside the ring window return empty arrays — durable storage
43754
+ * is a follow-up (Step 4b: persisted samples → SQL store).
43755
+ *
43756
+ * Every history method takes an optional `className` filter:
43757
+ * - omitted → bucket count includes ALL classes in scope
43758
+ * - present → bucket count restricted to that class
43759
+ *
43760
+ * The `getZoneHistory` / `getCameraHistory` / `getUnzonedHistory`
43761
+ * triple covers the three spatial scopes (per-zone, frame-wide,
43762
+ * outside-any-zone). Crossing them with `className?` gives the full
43763
+ * combinatorial coverage the operator UI requested.
43764
+ *
43765
+ * ## ⚠️ The three history methods are DEPRECATED
43766
+ *
43767
+ * They are served from a 60-MINUTE in-memory ring. It dies with the process,
43768
+ * so after a restart they report nothing about a night that did happen, and any
43769
+ * window past an hour is silently clamped — a caller cannot tell a clamp from
43770
+ * an empty scene. The presets in the occupancy chart existed to hide that
43771
+ * horizon rather than to answer a question.
43772
+ *
43773
+ * Use `pipelineAnalytics.readLongTermSeries({ series: 'occupancy', scope })`
43774
+ * instead. Those rows are already written for every camera, ~60 bytes per
43775
+ * (camera, scope, 5-minute bucket), kept for a year, and survive a restart.
43776
+ * Spell the scope with `occupancyScope(zoneId?, className?)` — the same
43777
+ * function the writer uses, so the two cannot drift.
43778
+ *
43779
+ * The ring stays until these three go, and it is the only thing left reading
43780
+ * it. Do not add a fourth caller.
43408
43781
  */
43409
- function enumLiteralProblem(cap, target, source, at) {
43410
- if (target.kind !== "enum" || target.enumValues === void 0) return null;
43411
- const value = literalStringValue(source);
43412
- if (value === null || target.enumValues.includes(value)) return null;
43413
- const allowed = target.enumValues.map((v) => `\`${v}\``).join(", ");
43414
- return problem("kind-mismatch", at, `\`${cap}.${target.path}\` accepts only ${allowed}; this source is the literal \`${value}\``);
43415
- }
43416
- function roleOf(source) {
43417
- if (source.kind !== "expression") return "derived";
43418
- if (expressionContainsBuiltinCall(source.expr, "count")) return "accumulator";
43419
- if (fieldReads(source).length > 0) return "derived";
43420
- const compiled = compileExpressionSafe(source.expr);
43421
- return compiled.ok && compiled.parsed.identifiers.has("now") ? "clock" : "constant";
43422
- }
43782
+ var zoneAnalyticsCapability = {
43783
+ name: "zone-analytics",
43784
+ scope: "device",
43785
+ mode: "singleton",
43786
+ deviceTypes: [DeviceType.Camera],
43787
+ methods: {
43788
+ /** Latest computed occupancy snapshot for this camera. Null when
43789
+ * the analytics pipeline hasn't seen a frame for this device yet
43790
+ * (no inference result emitted since boot or since binding was
43791
+ * activated). */
43792
+ getCurrentSnapshot: method(z.object({ deviceId: z.number() }), CameraOccupancySnapshotSchema.nullable()),
43793
+ /**
43794
+ * The same snapshot, for a SET of cameras, in one round trip.
43795
+ *
43796
+ * The Events page's Stationary section polls this every 15s for every
43797
+ * selected camera. Fanned out client-side that is one query per camera to
43798
+ * read a `Map.get` at the owner — the answer costs nothing, the round trip
43799
+ * costs everything. Batched, N transports become one and the per-device
43800
+ * work is unchanged.
43801
+ *
43802
+ * Every requested deviceId gets a row, tagged `read` — see
43803
+ * {@link CameraOccupancySnapshotForDeviceSchema}. A camera the owner could
43804
+ * not answer for is `'unreadable'`, never an empty reading.
43805
+ */
43806
+ getCurrentSnapshotBatch: method(z.object({ deviceIds: z.array(z.number()).min(1).max(200) }), z.array(CameraOccupancySnapshotForDeviceSchema).readonly()),
43807
+ /** Time-series object count inside one zone. `className` optional —
43808
+ * omit to count every class in the zone. */
43809
+ getZoneHistory: method(z.object({
43810
+ deviceId: z.number(),
43811
+ zoneId: z.string(),
43812
+ className: z.string().optional()
43813
+ }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly()),
43814
+ /** Time-series frame-wide object count (everywhere). */
43815
+ getCameraHistory: method(z.object({
43816
+ deviceId: z.number(),
43817
+ className: z.string().optional()
43818
+ }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly()),
43819
+ /** Time-series count of objects outside every zone. */
43820
+ getUnzonedHistory: method(z.object({
43821
+ deviceId: z.number(),
43822
+ className: z.string().optional()
43823
+ }).extend(HistoryRangeSchema.shape), z.array(HistoryPointSchema).readonly())
43824
+ },
43825
+ /**
43826
+ * Runtime-state slice — the latest occupancy snapshot mirrored by
43827
+ * the analytics frame processor on every inference result. Consumers
43828
+ * read via `device.state.zoneAnalytics.value` and stay in sync
43829
+ * automatically; the explicit `getCurrentSnapshot` cap method is
43830
+ * still useful for one-off polls without a subscription.
43831
+ */
43832
+ runtimeState: CameraOccupancySnapshotSchema,
43833
+ /**
43834
+ * Runtime-state durability: **session** — per-frame analytics; with `audio-metrics` it is ~90 % of the offered write rate. Re-derived on the next frame.
43835
+ *
43836
+ * See `RuntimeStateDurability`. Enforced by
43837
+ * `scripts/check-runtime-state-durability.ts`.
43838
+ */
43839
+ durability: "session"
43840
+ };
43423
43841
  /**
43424
- * How a planned field says "unavailable", per feature mode and AFTER `roleOf`
43425
- * (D393, D663 rulings S1 + S1b). The composer cannot take a device it does not
43426
- * own offline, and D393 forbids a stale number, so on an existing target a
43427
- * field that READS a source never gets `offline`.
43842
+ * Stages a {@link ZoneRule} can apply to. Discriminator on the rules
43843
+ * cap so a single CRUD surface backs every consumer; each stage has
43844
+ * its own dev-state mirror slice (`motion-zone-rules`,
43845
+ * `detection-zone-rules`, …) so consumer addons subscribe independently.
43846
+ *
43847
+ * Extend the enum here when a new gating consumer comes online (audio
43848
+ * gating, alert filtering, …) — no other surface needs to change.
43428
43849
  */
43429
- function unavailableFor(tf, role, mode, cap, at) {
43430
- if (mode === "replace" && (role === "derived" || role === "accumulator")) return {
43431
- ok: true,
43432
- unavailable: "release"
43433
- };
43434
- if (mode === "new" || role !== "derived") return {
43435
- ok: true,
43436
- unavailable: tf.nullable ? "null" : "offline"
43437
- };
43438
- if (tf.nullable) return {
43439
- ok: true,
43440
- unavailable: "null"
43441
- };
43442
- if (tf.enumValues?.includes("unknown") === true) return {
43443
- ok: true,
43444
- unavailable: "unknown"
43445
- };
43446
- return {
43447
- ok: false,
43448
- 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)`)
43449
- };
43450
- }
43451
- /** Item fields the composer writes itself, with the value it writes. */
43452
- var COMPOSED_ITEM_FIXED_FIELDS = { resettable: false };
43453
- function reservedItemFields(itemArray) {
43454
- return new Set([
43455
- itemArray.keyField,
43456
- ...itemArray.labelField === void 0 ? [] : [itemArray.labelField],
43457
- ...Object.keys(COMPOSED_ITEM_FIXED_FIELDS)
43458
- ]);
43459
- }
43460
- /** A required leaf under an optional parent is required only once something writes that parent. */
43461
- function leafRequired(itemArray, path, written) {
43462
- const described = describeStateField(itemArray.itemSchema, path);
43463
- if (described === null || !described.required) return false;
43464
- return optionalAncestors(itemArray.itemSchema, path).every((parent) => written.some((w) => w.startsWith(`${parent}.`)));
43465
- }
43466
- function missingLeaves({ capName, itemArray, reserved }, item, at) {
43467
- const written = Object.keys(item.fields);
43468
- return enumerateSchemaFields(itemArray.itemSchema).flatMap((leaf) => {
43469
- if (reserved.has(leaf.path) || leaf.path in item.fields) return [];
43470
- if (!leafRequired(itemArray, leaf.path, written)) return [];
43471
- const described = describeStateField(itemArray.itemSchema, leaf.path);
43472
- if (described === null || described.nullable) return [];
43473
- return [problem("missing-field", `${at}.fields.${leaf.path}`, `\`${capName}.${itemArray.path}[].${leaf.path}\` is required and has no source`)];
43474
- });
43850
+ var ZoneRuleStageEnum = z.enum([
43851
+ "motion",
43852
+ "detection",
43853
+ "package"
43854
+ ]);
43855
+ /**
43856
+ * Zone rules capability — per-camera CRUD over the {@link ZoneRule}
43857
+ * arrays that decide how each pipeline stage uses the polygon zones.
43858
+ *
43859
+ * Hosted by `addon-pipeline-orchestrator` alongside the zones provider
43860
+ * so the operator has a single hub-side source of truth for both
43861
+ * geometry and behaviour. Per-stage rules are stored under the
43862
+ * `zoneRules.<stage>` key in the orchestrator's per-device store and
43863
+ * mirrored to the device-state slice `<stage>-zone-rules` on every
43864
+ * mutation; consumer addons (analytics, motion-wasm, pipeline-executor)
43865
+ * subscribe to that slice and refresh their gating without
43866
+ * round-tripping the cap.
43867
+ *
43868
+ * Sets are bulk-replace — the operator UI sends the new rule list
43869
+ * wholesale, so reordering / batch enable-toggle / drag-drop CRUD lives
43870
+ * naturally in the rule editor without per-rule mutation chatter.
43871
+ */
43872
+ var zoneRulesCapability = {
43873
+ name: "zone-rules",
43874
+ scope: "device",
43875
+ mode: "singleton",
43876
+ deviceTypes: [DeviceType.Camera],
43877
+ methods: {
43878
+ /** Read the full rule list for a given stage (empty when no rules
43879
+ * are defined yet). */
43880
+ listRules: method(z.object({
43881
+ deviceId: z.number(),
43882
+ stage: ZoneRuleStageEnum
43883
+ }), z.array(ZoneRuleSchema).readonly()),
43884
+ /** Bulk-replace the rule list for one stage. The provider validates
43885
+ * each entry against {@link ZoneRuleSchema} (zoneIds non-empty,
43886
+ * thresholds in range) and rejects the whole patch if any entry
43887
+ * is invalid — partial writes are a configuration footgun. */
43888
+ setRules: method(z.object({
43889
+ deviceId: z.number(),
43890
+ stage: ZoneRuleStageEnum,
43891
+ rules: z.array(ZoneRuleSchema).readonly()
43892
+ }), z.void(), {
43893
+ kind: "mutation",
43894
+ auth: "admin"
43895
+ })
43896
+ },
43897
+ /**
43898
+ * Runtime-state slice — every stage mirrored together so consumers
43899
+ * see one reactive handle (`device.state.zoneRules.value`) instead
43900
+ * of one per stage. Bulk-replace mutations on any stage write the full
43901
+ * `{motion, detection, package}` shape, so subscribers always get the
43902
+ * complete current set. Consumers that only care about one stage
43903
+ * just read the matching property.
43904
+ *
43905
+ * `package` backs the package-drop detector — a package zone is a
43906
+ * `ZoneRule` on the `'package'` stage referencing drawn polygons
43907
+ * (see docs/superpowers/specs/2026-07-17-package-zones-design.md §3.1).
43908
+ * The orchestrator provider writes this stage as a first-class slice
43909
+ * (Phase 4): every mutation mirrors the full `{motion, detection,
43910
+ * package}` shape, so consumers read the current package rules directly
43911
+ * off `device.state.zoneRules.value.package`.
43912
+ */
43913
+ runtimeState: z.object({
43914
+ motion: z.array(ZoneRuleSchema).readonly(),
43915
+ detection: z.array(ZoneRuleSchema).readonly(),
43916
+ package: z.array(ZoneRuleSchema).readonly()
43917
+ }),
43918
+ /**
43919
+ * Runtime-state durability: **restored** — operator intent, mutation-only, same argument as `zones`.
43920
+ *
43921
+ * See `RuntimeStateDurability`. Enforced by
43922
+ * `scripts/check-runtime-state-durability.ts`.
43923
+ */
43924
+ durability: "restored"
43925
+ };
43926
+ function isValueOf(slot, value) {
43927
+ return value.slot === slot;
43475
43928
  }
43476
- function planItemField(ctx, leaf) {
43477
- const { capName, itemArray, featureIndex, lookupCap } = ctx;
43478
- const { key, label, path, source } = leaf;
43479
- const at = `features[${featureIndex}].items.${key}.fields.${path}`;
43480
- 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`));
43481
- const tf = describeStateField(itemArray.itemSchema, path);
43482
- if (tf === null) return refused(problem("unknown-field", at, `a \`${capName}\` item has no field \`${path}\``));
43483
- const kinds = sourceKinds(source, lookupCap);
43484
- if (!kinds.ok) return refused(problem(kinds.code, at, kinds.message));
43485
- const role = roleOf(source);
43486
- const unavailable = unavailableFor(tf, role, ctx.mode, capName, at);
43487
- if (!unavailable.ok) return refused(unavailable.problem);
43488
- const plan = {
43489
- cap: capName,
43490
- path,
43491
- item: {
43492
- arrayPath: itemArray.path,
43493
- key,
43494
- label
43495
- },
43496
- featureIndex,
43497
- kind: tf.kind,
43498
- nullable: tf.nullable,
43499
- required: tf.required,
43500
- unavailable: unavailable.unavailable,
43501
- role,
43502
- source,
43503
- reads: [...fieldReads(source)]
43504
- };
43505
- const enumProblem = enumLiteralProblem(capName, tf, source, at);
43929
+ /** Bind slot `slot` to its owner's functions. See the module docblock. */
43930
+ function defineExtensionSlotProvider(slot, impl) {
43506
43931
  return {
43507
- problems: [...compatibility(capName, tf, kinds.kinds, at), ...enumProblem === null ? [] : [enumProblem]],
43508
- fields: [plan],
43509
- features: [],
43510
- events: []
43932
+ slot,
43933
+ list: async (deviceId) => {
43934
+ const listing = await impl.list(deviceId);
43935
+ if (listing === null) return null;
43936
+ const value = listing.value;
43937
+ return {
43938
+ ...listing,
43939
+ value
43940
+ };
43941
+ },
43942
+ set: async (deviceId, value) => {
43943
+ if (!isValueOf(slot, value)) throw new Error(`device-extension: a "${value.slot}" value was sent to slot "${slot}" — refused`);
43944
+ return await impl.set(deviceId, value);
43945
+ },
43946
+ candidates: (deviceId) => impl.candidates(deviceId)
43511
43947
  };
43512
43948
  }
43513
- function planItems(def, feature, featureIndex, lookupCap, mode = "new") {
43514
- const at = `features[${featureIndex}].items`;
43515
- const itemArray = def.status?.itemArray;
43516
- if (itemArray === void 0) return refused(problem("not-item-array", at, `\`${def.name}\` has no item array — it takes no \`items\``));
43517
- if (itemArray.path !== "items") return refused(problem("not-item-array", at, `\`${def.name}\`'s item array is at \`${itemArray.path}\`, not \`${COMPOSITION_ITEM_ARRAY_PATH}\` — not composable yet`));
43518
- const ctx = {
43519
- capName: def.name,
43520
- itemArray,
43521
- reserved: reservedItemFields(itemArray),
43522
- featureIndex,
43523
- lookupCap,
43524
- mode
43949
+ /** `<source>:<slotId>`. */
43950
+ function extensionSlotRef(source, slot) {
43951
+ return `${source}:${slot}`;
43952
+ }
43953
+ /** Build the `device-extension` provider one owner registers. */
43954
+ function createDeviceExtensionProvider(options) {
43955
+ const { source, label, slots, hostTypeOf, logger } = options;
43956
+ const offered = (slot, hostType) => hostType !== null && EXTENSION_SLOTS[slot].hostDeviceTypes.some((t) => t === hostType);
43957
+ const resolve = async (deviceId, slotRef) => {
43958
+ const prefix = `${source}:`;
43959
+ const slot = slotRef.startsWith(prefix) ? slots.find((s) => s.slot === slotRef.slice(prefix.length)) : void 0;
43960
+ if (slot === void 0) throw new Error(`device-extension: "${slotRef}" is not a slot of "${source}" — it serves ${slots.map((s) => extensionSlotRef(source, s.slot)).join(", ") || "(none)"}`);
43961
+ const hostType = await hostTypeOf(deviceId);
43962
+ if (!offered(slot.slot, hostType)) throw new Error(`device-extension: slot "${slot.slot}" is not offered on device ${deviceId} (type ${hostType ?? "unknown"}; offered on ${EXTENSION_SLOTS[slot.slot].hostDeviceTypes.join(", ")})`);
43963
+ return slot;
43525
43964
  };
43526
- const results = Object.entries(feature.items ?? {}).flatMap(([key, item]) => [{
43527
- problems: missingLeaves(ctx, item, `${at}.${key}`),
43528
- fields: [],
43529
- features: [],
43530
- events: []
43531
- }, ...Object.entries(item.fields).map(([path, source]) => planItemField(ctx, {
43532
- key,
43533
- label: item.label,
43534
- path,
43535
- source
43536
- }))]);
43537
43965
  return {
43538
- problems: results.flatMap((r) => r.problems),
43539
- fields: results.flatMap((r) => r.fields),
43540
- features: [],
43541
- events: []
43966
+ listSources: async () => [{
43967
+ source,
43968
+ label,
43969
+ availability: { state: "ok" }
43970
+ }],
43971
+ listSlots: async (input) => {
43972
+ const hostType = await hostTypeOf(input.deviceId);
43973
+ const rows = [];
43974
+ for (const slot of slots) {
43975
+ if (!offered(slot.slot, hostType)) continue;
43976
+ const listing = await slot.list(input.deviceId);
43977
+ if (listing === null) continue;
43978
+ rows.push({
43979
+ state: "ok",
43980
+ slotRef: extensionSlotRef(source, slot.slot),
43981
+ slot: slot.slot,
43982
+ authority: listing.authority,
43983
+ activation: listing.activation,
43984
+ value: listing.value,
43985
+ sources: [...listing.sources],
43986
+ ...listing.impliedSources !== void 0 ? { impliedSources: [...listing.impliedSources] } : {}
43987
+ });
43988
+ }
43989
+ return rows;
43990
+ },
43991
+ listCandidates: async (input) => {
43992
+ return { candidates: [...await (await resolve(input.deviceId, input.slotRef)).candidates(input.deviceId)] };
43993
+ },
43994
+ setSlot: async (input) => {
43995
+ const result = await (await resolve(input.deviceId, input.slotRef)).set(input.deviceId, input.value);
43996
+ logger.info("device-extension: slot written", {
43997
+ tags: { deviceId: input.deviceId },
43998
+ meta: {
43999
+ slotRef: input.slotRef,
44000
+ reload: result.reload
44001
+ }
44002
+ });
44003
+ return result;
44004
+ }
43542
44005
  };
43543
44006
  }
44007
+ /**
44008
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
44009
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
44010
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
44011
+ * decimals). K derives from `°C = K − 273.15`.
44012
+ */
44013
+ var UNIT_TABLE = {
44014
+ "°C": {
44015
+ dimension: "temperature",
44016
+ factor: 1,
44017
+ offset: 0
44018
+ },
44019
+ "°F": {
44020
+ dimension: "temperature",
44021
+ factor: 5 / 9,
44022
+ offset: -160 / 9
44023
+ },
44024
+ K: {
44025
+ dimension: "temperature",
44026
+ factor: 1,
44027
+ offset: -273.15
44028
+ },
44029
+ hPa: {
44030
+ dimension: "pressure",
44031
+ factor: 1,
44032
+ offset: 0
44033
+ },
44034
+ kPa: {
44035
+ dimension: "pressure",
44036
+ factor: 10,
44037
+ offset: 0
44038
+ },
44039
+ Pa: {
44040
+ dimension: "pressure",
44041
+ factor: .01,
44042
+ offset: 0
44043
+ },
44044
+ mbar: {
44045
+ dimension: "pressure",
44046
+ factor: 1,
44047
+ offset: 0
44048
+ },
44049
+ bar: {
44050
+ dimension: "pressure",
44051
+ factor: 1e3,
44052
+ offset: 0
44053
+ },
44054
+ inHg: {
44055
+ dimension: "pressure",
44056
+ factor: 33.8639,
44057
+ offset: 0
44058
+ },
44059
+ mmHg: {
44060
+ dimension: "pressure",
44061
+ factor: 1.33322,
44062
+ offset: 0
44063
+ },
44064
+ psi: {
44065
+ dimension: "pressure",
44066
+ factor: 68.9476,
44067
+ offset: 0
44068
+ },
44069
+ "m/s": {
44070
+ dimension: "speed",
44071
+ factor: 1,
44072
+ offset: 0
44073
+ },
44074
+ "km/h": {
44075
+ dimension: "speed",
44076
+ factor: 1 / 3.6,
44077
+ offset: 0
44078
+ },
44079
+ mph: {
44080
+ dimension: "speed",
44081
+ factor: .44704,
44082
+ offset: 0
44083
+ },
44084
+ kn: {
44085
+ dimension: "speed",
44086
+ factor: .514444,
44087
+ offset: 0
44088
+ },
44089
+ "mm/h": {
44090
+ dimension: "precipitation-rate",
44091
+ factor: 1,
44092
+ offset: 0
44093
+ },
44094
+ "in/h": {
44095
+ dimension: "precipitation-rate",
44096
+ factor: 25.4,
44097
+ offset: 0
44098
+ },
44099
+ m: {
44100
+ dimension: "length",
44101
+ factor: 1,
44102
+ offset: 0
44103
+ },
44104
+ mm: {
44105
+ dimension: "length",
44106
+ factor: .001,
44107
+ offset: 0
44108
+ },
44109
+ cm: {
44110
+ dimension: "length",
44111
+ factor: .01,
44112
+ offset: 0
44113
+ },
44114
+ km: {
44115
+ dimension: "length",
44116
+ factor: 1e3,
44117
+ offset: 0
44118
+ },
44119
+ in: {
44120
+ dimension: "length",
44121
+ factor: .0254,
44122
+ offset: 0
44123
+ },
44124
+ ft: {
44125
+ dimension: "length",
44126
+ factor: .3048,
44127
+ offset: 0
44128
+ },
44129
+ mi: {
44130
+ dimension: "length",
44131
+ factor: 1609.344,
44132
+ offset: 0
44133
+ },
44134
+ lx: {
44135
+ dimension: "illuminance",
44136
+ factor: 1,
44137
+ offset: 0
44138
+ },
44139
+ "W/m²": {
44140
+ dimension: "irradiance",
44141
+ factor: 1,
44142
+ offset: 0
44143
+ },
44144
+ W: {
44145
+ dimension: "power",
44146
+ factor: 1,
44147
+ offset: 0
44148
+ },
44149
+ kW: {
44150
+ dimension: "power",
44151
+ factor: 1e3,
44152
+ offset: 0
44153
+ },
44154
+ VA: {
44155
+ dimension: "apparent-power",
44156
+ factor: 1,
44157
+ offset: 0
44158
+ },
44159
+ Wh: {
44160
+ dimension: "energy",
44161
+ factor: 1,
44162
+ offset: 0
44163
+ },
44164
+ kWh: {
44165
+ dimension: "energy",
44166
+ factor: 1e3,
44167
+ offset: 0
44168
+ },
44169
+ MWh: {
44170
+ dimension: "energy",
44171
+ factor: 1e6,
44172
+ offset: 0
44173
+ },
44174
+ V: {
44175
+ dimension: "voltage",
44176
+ factor: 1,
44177
+ offset: 0
44178
+ },
44179
+ mV: {
44180
+ dimension: "voltage",
44181
+ factor: .001,
44182
+ offset: 0
44183
+ },
44184
+ A: {
44185
+ dimension: "current",
44186
+ factor: 1,
44187
+ offset: 0
44188
+ },
44189
+ mA: {
44190
+ dimension: "current",
44191
+ factor: .001,
44192
+ offset: 0
44193
+ },
44194
+ ppm: {
44195
+ dimension: "concentration-volume",
44196
+ factor: 1,
44197
+ offset: 0
44198
+ },
44199
+ ppb: {
44200
+ dimension: "concentration-volume",
44201
+ factor: .001,
44202
+ offset: 0
44203
+ },
44204
+ "µg/m³": {
44205
+ dimension: "concentration-mass",
44206
+ factor: 1,
44207
+ offset: 0
44208
+ },
44209
+ "mg/m³": {
44210
+ dimension: "concentration-mass",
44211
+ factor: 1e3,
44212
+ offset: 0
44213
+ },
44214
+ kg: {
44215
+ dimension: "mass",
44216
+ factor: 1,
44217
+ offset: 0
44218
+ },
44219
+ g: {
44220
+ dimension: "mass",
44221
+ factor: .001,
44222
+ offset: 0
44223
+ },
44224
+ lb: {
44225
+ dimension: "mass",
44226
+ factor: .453592,
44227
+ offset: 0
44228
+ },
44229
+ oz: {
44230
+ dimension: "mass",
44231
+ factor: .0283495,
44232
+ offset: 0
44233
+ },
44234
+ "%": {
44235
+ dimension: "percentage",
44236
+ factor: 1,
44237
+ offset: 0
44238
+ }
44239
+ };
44240
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
44241
+ function specFor(unit) {
44242
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
44243
+ }
44244
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
44245
+ function toCanonical(value, spec) {
44246
+ return value * spec.factor + spec.offset;
44247
+ }
44248
+ /** Canonical-unit value → the unit described by `spec`. */
44249
+ function fromCanonical(canonical, spec) {
44250
+ return (canonical - spec.offset) / spec.factor;
44251
+ }
44252
+ /**
44253
+ * Graceful variant for the render path: returns the converted value, or
44254
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
44255
+ * Never throws — a render caller falls back to the raw value + source unit.
44256
+ */
44257
+ function tryConvertUnit(value, from, to) {
44258
+ if (from === to) return value;
44259
+ const fromSpec = specFor(from);
44260
+ if (!fromSpec) return void 0;
44261
+ const toSpec = specFor(to);
44262
+ if (!toSpec) return void 0;
44263
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
44264
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
44265
+ }
44266
+ /**
44267
+ * One composed field, evaluated from what its sources say NOW (D659).
44268
+ *
44269
+ * Pure. The composer calls it on every source change, and the slice-5 editor
44270
+ * will call it for its live preview, so both see the same answer.
44271
+ *
44272
+ * The rules (spec §2):
44273
+ * - a source that is offline or unreadable keeps its last value and is marked
44274
+ * STALE; the field is stale and keeps computing (D49);
44275
+ * - a source that is GONE makes the field UNAVAILABLE, `fatal`, and never a
44276
+ * plausible number (D393);
44277
+ * - a source that is unavailable but NOT gone (e.g. a cap unregistered at
44278
+ * runtime, R7b) makes the field UNAVAILABLE, non-fatal, `value: null`: it
44279
+ * may come back on its own, no block failure is raised for it;
44280
+ * - an expression that fails on a value makes the field unavailable for that
44281
+ * value, and NOT fatal: the next value may compute.
44282
+ *
44283
+ * `confirmed` (D663, D49): whether a NON-fatal unavailability is an ANSWER — a
44284
+ * cap the provider unregistered and a re-read agreed, a field the source
44285
+ * reported without, an expression that failed on a value — or only the
44286
+ * absence of one (a failed read, a first or empty read, a source not tracked
44287
+ * yet). Only a confirmed unavailability may hand a `release`-mode field back
44288
+ * to the native provider; an unconfirmed one holds it on its last value.
44289
+ * Absent means NOT confirmed. Fatal is always confirmed.
44290
+ *
44291
+ * When an expression has more than one unavailable binding, a FATAL one always
44292
+ * wins the outcome, regardless of position: every binding is scanned before
44293
+ * the field is judged, so an earlier non-fatal binding (e.g. a cap that is
44294
+ * momentarily unregistered) can never hide a later fatal one (a deleted
44295
+ * source) behind an early return.
44296
+ */
43544
44297
  function describeRead(read) {
43545
44298
  return `${compositionSourceKey(read.source)} ${read.cap}.${read.fieldPath}`;
43546
44299
  }
@@ -43607,7 +44360,10 @@ function expressionOutcome(source, read, now, stateful) {
43607
44360
  }
43608
44361
  const deadlines = [];
43609
44362
  const result = evaluateExpressionSource(source.expr, values, now, {
43610
- hooks: { noteDeadline: (at) => deadlines.push(at) },
44363
+ hooks: {
44364
+ noteDeadline: (at) => deadlines.push(at),
44365
+ convert: (value, from, to) => tryConvertUnit(value, normalizeUnit(from) ?? from, normalizeUnit(to) ?? to) ?? null
44366
+ },
43611
44367
  ...stateful === void 0 ? {} : { invokeStateful: (invocation) => evaluateStatefulExpressionCall(stateful.callPathPrefix === void 0 ? invocation : {
43612
44368
  ...invocation,
43613
44369
  path: `${stateful.callPathPrefix}@${invocation.path}`
@@ -43645,301 +44401,70 @@ function evaluateCompositionField(input) {
43645
44401
  };
43646
44402
  }
43647
44403
  }
43648
- function buildCapabilityLookup(defs) {
43649
- const byName = new Map(defs.map((d) => [d.name, d]));
43650
- return (capName) => byName.get(capName) ?? null;
43651
- }
43652
- /** Caps every `BaseDevice` registers on itself; a second registration throws. */
43653
- var BASE_DEVICE_CAPS = new Set(["device-status", "feature-probe"]);
43654
- var PENDING_SELF = {
43655
- addonId: COMPOSER_ADDON_ID,
43656
- stableId: `${COMPOSED_DEVICE_STABLE_ID_PREFIX}(unsaved)`
43657
- };
43658
- function composableCap(feature, at, lookupCap) {
43659
- const def = lookupCap(feature.cap);
43660
- if (def === null) return problem("unknown-capability", `${at}.cap`, `no capability is named \`${feature.cap}\``);
43661
- 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`);
43662
- const state = def.runtimeState;
43663
- if (def.scope !== "device" || state === void 0) return problem("not-composable", `${at}.cap`, `\`${def.name}\` has no per-device state to compose`);
43664
- return {
43665
- def,
43666
- state,
43667
- fields: describeTopLevelStateFields(state)
43668
- };
43669
- }
43670
44404
  /**
43671
- * A capability BUILT by the composition (a new device, or one ADDED to an
43672
- * existing device) must fit the device type, serve no command, and have every
43673
- * required field. A REPLACE needs none of it: the cap is bound (a fact), the
43674
- * native keeps serving its methods, and only the named fields are owned (D663).
44405
+ * The doorbell of a camera as a customization (D671). ONE builder: the
44406
+ * virtual-doorbell migration and the admin UI's "Use as doorbell" preset both
44407
+ * call it, so a preset can never write a shape the migration would refuse as
44408
+ * "an unrelated existing block".
43675
44409
  */
43676
- function builtCapProblems({ def, state, fields: targetFields }, feature, at, deviceType) {
43677
- const problems = [];
43678
- 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(", ")})`));
43679
- const served = Object.entries(def.methods).filter(([name, m]) => m.systemOnly !== true && def.composedMethods?.[name] === void 0).map(([name]) => `\`${name}\``);
43680
- 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)`));
43681
- const itemArrayPath = feature.items === void 0 ? null : def.status?.itemArray?.path ?? null;
43682
- for (const key of requiredTopLevelKeys(state)) {
43683
- if (key === itemArrayPath) continue;
43684
- 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`));
43685
- else if (!(key in feature.fields)) problems.push(problem("missing-field", `${at}.fields.${key}`, `\`${def.name}.${key}\` is required and has no source`));
43686
- }
43687
- return problems;
43688
- }
43689
- function planTopLevelField({ def, fields }, field, mode, lookupCap) {
43690
- const { featureIndex, path, source } = field;
43691
- const fieldAt = `features[${featureIndex}].fields.${path}`;
43692
- if (path.includes(".")) return refused(problem("nested-field", fieldAt, `\`${path}\` is nested — nested fields arrive in a later slice`));
43693
- const tf = fields.get(path);
43694
- if (tf === void 0) return refused(problem("unknown-field", fieldAt, `\`${def.name}\` has no field \`${path}\``));
43695
- const kinds = sourceKinds(source, lookupCap);
43696
- if (!kinds.ok) return refused(problem(kinds.code, fieldAt, kinds.message));
43697
- const role = roleOf(source);
43698
- const unavailable = unavailableFor(tf, role, mode, def.name, fieldAt);
43699
- if (!unavailable.ok) return refused(unavailable.problem);
43700
- const plan = {
43701
- cap: def.name,
43702
- path,
43703
- featureIndex,
43704
- kind: tf.kind,
43705
- nullable: tf.nullable,
43706
- required: tf.required,
43707
- unavailable: unavailable.unavailable,
43708
- role,
43709
- source,
43710
- reads: [...fieldReads(source)]
43711
- };
43712
- const enumProblem = enumLiteralProblem(def.name, tf, source, fieldAt);
43713
- return {
43714
- problems: [...compatibility(def.name, tf, kinds.kinds, fieldAt), ...enumProblem === null ? [] : [enumProblem]],
43715
- fields: [plan],
43716
- features: [],
43717
- events: []
43718
- };
43719
- }
43720
- function planFeature(feature, featureIndex, target, lookupCap) {
43721
- const at = `features[${featureIndex}]`;
43722
- const cap = composableCap(feature, at, lookupCap);
43723
- if ("code" in cap) return refused(cap);
43724
- const capProblems = target.mode === "replace" ? [] : [...builtCapProblems(cap, feature, at, target.deviceType)];
43725
- if (feature.commands !== void 0 && Object.keys(feature.commands).length > 0) capProblems.push(problem("reserved-variant", `${at}.commands`, "commands (`forward`) arrive in slice 3"));
43726
- const perField = Object.entries(feature.fields).map(([path, source]) => planTopLevelField(cap, {
43727
- featureIndex,
43728
- path,
43729
- source
43730
- }, target.mode, lookupCap));
43731
- const items = feature.items === void 0 ? [] : [planItems(cap.def, feature, featureIndex, lookupCap, target.mode)];
43732
- const all = [...perField, ...items];
43733
- const fields = all.flatMap((result) => result.fields);
43734
- const featurePlan = {
43735
- featureIndex,
43736
- cap: cap.def.name,
43737
- mode: target.mode
43738
- };
43739
- const eventPlan = planCompositionEvents(cap.def, featurePlan, fields);
43740
- return {
43741
- problems: [
43742
- ...capProblems,
43743
- ...all.flatMap((r) => r.problems),
43744
- ...eventPlan.problems
43745
- ],
43746
- fields,
43747
- features: [featurePlan],
43748
- events: eventPlan.events
43749
- };
43750
- }
43751
- /** A cap the device serves itself; one the composer grafted (an earlier ADD) is not native. */
43752
- function isNativeCap(target, cap) {
43753
- const owner = target.nativeCaps.get(cap);
43754
- return owner !== void 0 && owner !== "composer";
43755
- }
43756
- /** The target read, or the refusal that replaces the whole plan when it could not be (D391). */
43757
- function planTargetOf(composition, target) {
43758
- if (composition.target.kind === "new") return {
43759
- kind: "new",
43760
- type: composition.target.type
43761
- };
43762
- const key = compositionSourceKey(composition.target.device);
43763
- if (target === void 0) return problem("target-unreadable", "target", `target device \`${key}\` was not read — the save is refused rather than stored unchecked`);
43764
- if (target.kind === "absent") return problem("target-missing", "target", `target device \`${key}\` does not exist`);
43765
- 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`);
43766
- return {
43767
- kind: "existing",
43768
- found: target
43769
- };
43770
- }
43771
- function featureTargetOf(target, cap) {
43772
- if (target.kind === "new") return {
43773
- mode: "new",
43774
- deviceType: target.type
43775
- };
43776
- return {
43777
- mode: isNativeCap(target.found, cap) ? "replace" : "add",
43778
- deviceType: target.found.type
43779
- };
44410
+ /** A rise closer than this to the previous one is the same press (D671). */
44411
+ var DOORBELL_RISE_DEBOUNCE_MS = 2e3;
44412
+ /** Every source cap of `source` that `SOURCE_CAP_ACTIVE_FIELD` knows AND the device has bound. */
44413
+ function doorbellFeatureSources(source, boundCaps) {
44414
+ return Object.keys(SOURCE_CAP_ACTIVE_FIELD).filter((cap) => Object.hasOwn(SOURCE_CAP_CHANGED_AT_FIELD, cap) && boundCaps.includes(cap)).toSorted().flatMap((cap) => {
44415
+ const activeField = SOURCE_CAP_ACTIVE_FIELD[cap];
44416
+ const changedAtField = SOURCE_CAP_CHANGED_AT_FIELD[cap];
44417
+ return activeField === void 0 || changedAtField === void 0 ? [] : [{
44418
+ source,
44419
+ cap,
44420
+ activeField,
44421
+ changedAtField
44422
+ }];
44423
+ });
43780
44424
  }
43781
- /**
43782
- * The static plan. `target` describes an EXISTING target device (one read);
43783
- * it is ignored for a new one. On an existing target each feature REPLACES a
43784
- * cap the device has natively, else ADDS it (D663).
43785
- */
43786
- function planComposition(composition, lookupCap, target) {
43787
- const planTarget = planTargetOf(composition, target);
43788
- if ("code" in planTarget) return refused(planTarget);
43789
- const results = composition.features.map((feature, index) => {
43790
- 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);
43791
- return compositionFeatureEnabled(feature) ? result : {
43792
- problems: result.problems,
43793
- fields: [],
43794
- features: [],
43795
- events: []
44425
+ /** The D671 feature, byte for byte what the migration writes. Throws on zero sources. */
44426
+ function buildDoorbellCompositionFeature(input) {
44427
+ const sources = [...input.sources].toSorted((a, b) => `${a.source.addonId}/${a.source.stableId}#${a.cap}`.localeCompare(`${b.source.addonId}/${b.source.stableId}#${b.cap}`));
44428
+ if (sources.length === 0) throw new Error("doorbell composition needs at least one source");
44429
+ const edgeBindings = {};
44430
+ sources.forEach((entry, index) => {
44431
+ edgeBindings[`active${index}`] = {
44432
+ kind: "from",
44433
+ source: entry.source,
44434
+ cap: entry.cap,
44435
+ fieldPath: entry.activeField
44436
+ };
44437
+ edgeBindings[`changed${index}`] = {
44438
+ kind: "from",
44439
+ source: entry.source,
44440
+ cap: entry.cap,
44441
+ fieldPath: entry.changedAtField
43796
44442
  };
43797
44443
  });
43798
44444
  return {
43799
- problems: results.flatMap((r) => r.problems),
43800
- fields: results.flatMap((r) => r.fields),
43801
- features: results.flatMap((r) => r.features),
43802
- events: results.flatMap((r) => r.events)
43803
- };
43804
- }
43805
- function compositionSourceRefs(composition) {
43806
- const refs = composition.features.flatMap((f) => compositionFeatureEnabled(f) && f.kind === "fields" ? featureSources(f).flatMap(fieldReads).map((r) => r.source) : []);
43807
- return [...new Map(refs.map((r) => [compositionSourceKey(r), r])).values()];
43808
- }
43809
- /**
43810
- * The candidate's device: the existing target itself, else `self`, else a
43811
- * placeholder that cannot collide with a stored target. An existing target is
43812
- * known even on create, so a second customization of it is refused on create too.
43813
- */
43814
- function candidateSelf(composition, ctx, plan) {
43815
- return {
43816
- ref: composition.target.kind === "existing" ? composition.target.device : ctx.self ?? PENDING_SELF,
43817
- native: composition.target.kind === "existing" && ctx.target?.kind === "found" ? ctx.target : null,
43818
- replaced: replacedFieldKeys(plan)
43819
- };
43820
- }
43821
- /**
43822
- * `compositionFieldKey` of every field a REPLACE feature of `plan` owns — the
43823
- * candidate's own, and a stored peer's `CompositionPeer.replaced` (D663): one
43824
- * derivation, so the graph reads both the same way.
43825
- */
43826
- function replacedFieldKeys(plan) {
43827
- const replacing = new Set(plan.features.filter((f) => f.mode === "replace").map((f) => f.featureIndex));
43828
- return new Set(plan.fields.filter((f) => replacing.has(f.featureIndex)).map(compositionFieldKey));
43829
- }
43830
- /**
43831
- * A read of the candidate's own device. Its own composed output (a new device's
43832
- * field, an ADDED field) is the slice-1 own-field rule; on an existing target
43833
- * every other field — replaced, or not composed at all — is the NATIVE value,
43834
- * valid when the device serves that cap natively (D663, rule 5).
43835
- */
43836
- function selfReadProblems(read, at, fields, self) {
43837
- const readKey = `${read.cap}.${read.fieldPath}`;
43838
- if (fields.some((f) => compositionFieldKey(f) === readKey && !self.replaced.has(readKey))) return [];
43839
- if (self.native === null) return [problem("source-field-unknown", at, `this device has no composed field \`${read.cap}.${read.fieldPath}\``)];
43840
- if (isNativeCap(self.native, read.cap)) return [];
43841
- return [problem("source-capability-missing", at, `the target device (device ${self.native.deviceId}) has no native \`${read.cap}\``)];
43842
- }
43843
- function sourceProblems(fields, ctx, self) {
43844
- const selfKey = compositionSourceKey(self.ref);
43845
- return fields.flatMap((field) => field.reads.flatMap((read) => {
43846
- const at = compositionFieldAt(field);
43847
- const key = compositionSourceKey(read.source);
43848
- if (key === selfKey) return selfReadProblems(read, at, fields, self);
43849
- const desc = ctx.sources.get(key);
43850
- 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`)];
43851
- if (desc.kind === "absent") return [problem("source-missing", at, `source \`${key}\` does not exist`)];
43852
- if (!desc.caps.has(read.cap)) return [problem("source-capability-missing", at, `source \`${key}\` (device ${desc.deviceId}) has no \`${read.cap}\``)];
43853
- return [];
43854
- }));
43855
- }
43856
- /**
43857
- * A cycle's node keys are `compositionReadKey` strings:
43858
- * `<addonId>/<stableId>#<cap>.<fieldPath>`. When a node belongs to the
43859
- * CANDIDATE itself (this composition being saved), pull its `cap`/`fieldPath`
43860
- * back out so the refusal can point at that exact field, matching the
43861
- * documented `features[N].fields.<path>` convention. A cap name has no `.`,
43862
- * so splitting `rest` on the FIRST `.` is exact; an item leaf's remainder
43863
- * (`items[desiccant].status`) is matched through `compositionFieldKey`.
43864
- */
43865
- function candidateFieldOfNode(nodeKey, candidateKey) {
43866
- const prefix = `${candidateKey}#`;
43867
- if (!nodeKey.startsWith(prefix)) return null;
43868
- const rest = nodeKey.slice(prefix.length);
43869
- const dot = rest.indexOf(".");
43870
- if (dot === -1) return null;
43871
- return {
43872
- cap: rest.slice(0, dot),
43873
- fieldPath: rest.slice(dot + 1)
43874
- };
43875
- }
43876
- /** The first node of a cycle's path that is one of THIS composition's own
43877
- * planned fields, as a documented-convention path; `'features'` when the
43878
- * cycle runs entirely through other stored compositions. */
43879
- function cyclePath(nodePath, candidateKey, fields) {
43880
- for (const nodeKey of nodePath) {
43881
- const hit = candidateFieldOfNode(nodeKey, candidateKey);
43882
- if (hit === null) continue;
43883
- const hitKey = `${hit.cap}.${hit.fieldPath}`;
43884
- const field = fields.find((f) => compositionFieldKey(f) === hitKey);
43885
- if (field !== void 0) return compositionFieldAt(field);
43886
- }
43887
- return "features";
43888
- }
43889
- /** The first unread device any of `nodes` belongs to; null when none does. */
43890
- function unreadDeviceOf(nodes, unread) {
43891
- for (const [key, error] of unread) if (nodes.some((node) => node.startsWith(`${key}#`))) return {
43892
- key,
43893
- error
43894
- };
43895
- return null;
43896
- }
43897
- /**
43898
- * Cycle check runs against the CANDIDATE (this composition, as block
43899
- * `ctx.blockId`, writing `self.ref`) plus every OTHER stored composition,
43900
- * decided per strongly connected component (`findCandidateCycles`): a cyclic
43901
- * component holding a candidate node refuses, with one representative cycle
43902
- * through it; one without is not this save's cycle. A
43903
- * stored composition of the same block is its old version; another block on
43904
- * the same device, or two stored peers sharing a device, make
43905
- * `compositionEdges` throw `CompositionGraphError` — refused BY NAME as
43906
- * `duplicate-target` (D391, D663) instead of escaping as an uncaught throw.
43907
- */
43908
- function cycleProblems(composition, ctx, fields, self) {
43909
- const candidate = {
43910
- blockId: ctx.blockId ?? "(unsaved)",
43911
- target: self.ref,
43912
- composition,
43913
- replaced: self.replaced
43914
- };
43915
- const candidateKey = compositionSourceKey(candidate.target);
43916
- const unread = ctx.unreadTargets ?? /* @__PURE__ */ new Map();
43917
- try {
43918
- return findCandidateCycles(candidate, ctx.peers).map((c) => {
43919
- const at = cyclePath(c.path, candidateKey, fields);
43920
- const through = unreadDeviceOf([...c.component], unread);
43921
- 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(" → ")}`);
43922
- });
43923
- } catch (err) {
43924
- if (err instanceof CompositionGraphError) return [problem("duplicate-target", "features", err.message)];
43925
- throw err;
43926
- }
43927
- }
43928
- function validateComposition(composition, ctx) {
43929
- const plan = planComposition(composition, ctx.lookupCap, ctx.target ?? void 0);
43930
- const self = candidateSelf(composition, ctx, plan);
43931
- const cycles = cycleProblems(composition, ctx, plan.fields, self);
43932
- const problems = [
43933
- ...plan.problems,
43934
- ...sourceProblems(plan.fields, ctx, self),
43935
- ...cycles
43936
- ];
43937
- return {
43938
- ok: problems.length === 0,
43939
- problems,
43940
- fields: [...plan.fields],
43941
- features: [...plan.features],
43942
- events: [...plan.events]
44445
+ kind: "fields",
44446
+ cap: "doorbell",
44447
+ enabled: input.enabled,
44448
+ fields: {
44449
+ lastPressedAt: {
44450
+ kind: "expression",
44451
+ expr: `latest(${sources.map((_, index) => `rose(active${index}, changed${index}, ${DOORBELL_RISE_DEBOUNCE_MS})`).join(", ")})`,
44452
+ bindings: edgeBindings
44453
+ },
44454
+ pressCountSinceStart: {
44455
+ kind: "expression",
44456
+ expr: "count(lastPressedAt)",
44457
+ bindings: { lastPressedAt: {
44458
+ kind: "from",
44459
+ source: {
44460
+ addonId: input.camera.addonId,
44461
+ stableId: input.camera.stableId
44462
+ },
44463
+ cap: "doorbell",
44464
+ fieldPath: "lastPressedAt"
44465
+ } }
44466
+ }
44467
+ }
43943
44468
  };
43944
44469
  }
43945
44470
  /**
@@ -43974,7 +44499,6 @@ var RUNTIME_DEFAULTS = {
43974
44499
  models: "camstack-data/models"
43975
44500
  },
43976
44501
  providers: [],
43977
- "ffmpeg.binaryPath": "ffmpeg",
43978
44502
  "ffmpeg.hwAccel": "auto",
43979
44503
  "ffmpeg.threadCount": 0,
43980
44504
  "auth.tokenExpiry": "30d"
@@ -47960,6 +48484,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
47960
48484
  addonId: null,
47961
48485
  access: "view"
47962
48486
  },
48487
+ "coreBlocks.listComposableCaps": {
48488
+ capName: "core-blocks",
48489
+ capScope: "system",
48490
+ addonId: null,
48491
+ access: "view"
48492
+ },
47963
48493
  "coreBlocks.listCompositions": {
47964
48494
  capName: "core-blocks",
47965
48495
  capScope: "system",
@@ -51644,6 +52174,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
51644
52174
  addonId: null,
51645
52175
  access: "create"
51646
52176
  },
52177
+ "pipelineOrchestrator.setCameraSensors": {
52178
+ capName: "pipeline-orchestrator",
52179
+ capScope: "system",
52180
+ addonId: null,
52181
+ access: "create"
52182
+ },
51647
52183
  "pipelineOrchestrator.setCameraStepOverride": {
51648
52184
  capName: "pipeline-orchestrator",
51649
52185
  capScope: "system",
@@ -55354,6 +55890,11 @@ Object.freeze({
55354
55890
  form: "single",
55355
55891
  optional: false
55356
55892
  }],
55893
+ "pipelineOrchestrator.setCameraSensors": [{
55894
+ name: "deviceId",
55895
+ form: "single",
55896
+ optional: false
55897
+ }],
55357
55898
  "pipelineOrchestrator.setCameraStepOverride": [{
55358
55899
  name: "deviceId",
55359
55900
  form: "single",
@@ -57457,4 +57998,4 @@ function enumerateInferenceDevices(hw) {
57457
57998
  return out;
57458
57999
  }
57459
58000
  //#endregion
57460
- export { coreBlockAddonId as $, asNumber as $n, ptzAutotrackCapability as $t, STORAGE_ACCESS_FALLBACK as A, cloneStatefulExpressionMemory as An, isSameAddonId as At, addonWidgetsCapability as B, toExpressionValue as Bn, looseSchema as Bt, METHOD_ACCESS_MAP as C, withLocationMode as Cn, foldSnapshotByFunction as Ct, REDACTED_SECRET as D, SOURCE_CAP_ACTIVE_FIELD as Dn, isCollectionArrayMethod as Dt, RECORDING_SOURCE_CAMSTACK_ADDON as E, SOURCE_CAPS as En, isArrayOutputSchema as Et, StorageLocationTypeSchema as F, compositionSliceKey as Fn, lifecycleJobSchema as Ft, batteryCapability as G, BaseAddon as Gn, motionCapability as Gt, authProviderCapability as H, DeviceRole as Hn, mayWriteToLocation as Ht, StorageMigrationJobSchema as I, compositionSourceKey as In, localNetworkCapability as It, cameraGridLayoutCapability as J, DEVICE_STATUS_METHOD as Jn, objectInputDeclaresRequiredDeviceId as Jt, buildCapabilityLookup as K, DATAPLANE_SECRET_HEADER as Kn, normalizeUnit as Kt, UserRecordSchema as L, createStatefulExpressionMemory as Ln, locationIconKey as Lt, ScopedTokenSchema as M, composedDeviceStableId as Mn, kebabToCamel as Mt, StorageCleanupJobSchema as N, compositionFeatureEnabled as Nn, legacyAutoLinkIds as Nt, RUNTIME_DEFAULTS as O, SOURCE_CAP_CHANGED_AT_FIELD as On, isLocationEnabled as Ot, StorageLocationModeSchema as P, compositionItemFieldPath as Pn, legacyModeOf as Pt, compositionSourceRefs as Q, asJsonObject as Qn, procedureAuthKey as Qt, addonPagesCapability as R, evaluateSensorEdge as Rn, logDestinationCapability as Rt, LocationIconIdSchema as S, vectorStoreCapability as Sn, filesystemBrowseCapability as St, MOTION_CLOSE_AFTER_MS as T, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as Tn, gbToBytes as Tt, backupCapability as U, DeviceType as Un, metricsProviderCapability as Ut, alertsCapability as V, DeviceFeature as Vn, mayReadLocation as Vt, bareAddonId as W, errMsg as Wn, modeMayWrite as Wt, composerCapability as X, ReadinessTimeoutError as Xn, planComposition as Xt, collectSecretConfigKeys as Y, ReadinessRegistry as Yn, parseStreamParamsFormPatch as Yt, compositionFieldKey as Z, WELL_KNOWN_TAB_MAP as Zn, platformProbeCapability as Zt, FIRST_LEVEL_MACRO_CLASSES as _, streamQualityLabel as _n, enumerateSchemaFields as _t, BatteryStatusSchema as a, runtimeStatePolicyFor as an, hydrateSchema as ar, dataStoreProviderCapability as at, LOG_LEVEL_RANK as b, vectorDimFromBase64 as bn, expandContainer as bt, COMPOSED_ITEM_FIXED_FIELDS as c, scoreRuntimes as cn, nodePin as cr, defineExtensionSlotProvider as ct, CORE_BLOCK_ADDON_PREFIX as d, snapshotCapability as dn, readinessKey as dr, deviceManagerCapability as dt, reducePoints as en, asString$1 as er, coreBlockComposition as et, CoreBlockSchema as f, storageCapability as fn, resolveCapMount as fr, deviceStateCapability as ft, DeviceStatusSchema as g, streamCatalogCapability as gn, enumerateItemArrayFields as gt, DeclaredDevices as h, storageProviderCapability as hn, EventCategory as hr, enumerateInferenceDevices as ht, BaseDevice as i, resolveMethodAuth as in, expandCapMethods as ir, createDeviceExtensionProvider as it, STREAM_PROFILE_META as j, composedDeviceRef as jn, isVoidInput as jt, RecordingStatusSchema as k, buildFfmpegArgs as kn, isObjectInput as kt, COMPOSER_RUNNER_ID as l, settingsStoreCapability as ln, parseJsonObject as lr, deriveBatteryPresence as lt, DERIVED_CAMERA_SILENCED_CAP_NAMES as m, storageOccupancyCapability as mn, sleep as mr, doorbellCapability as mt, AlertSchema as n, resolveBucketMs as nn, emitDownForOwnedCaps as nr, coreBlockSourceKind as nt, CAMERA_SWITCH_ORDER as o, scopesAllowAddon as on, isDeviceConfigCap as or, declarationOwnerNodeId as ot, DAY_NAMES as p, storageMigrationCapability as pn, scopeKey as pr, deviceStatusCapability as pt, buildStreamParamsConfigSchema as q, DEVICE_SETTINGS_CONTRIBUTION_METHODS as qn, objectInputDeclaresAddonId as qt, ApiKeyRecordSchema as r, resolveLocationMode as rn, emitReadiness as rr, coreBlocksCapability as rt, CAP_NAMES_WITH_STATUS as s, scopesAllowDeviceCap as sn, isEvent as sr, decodeVectorBase64 as st, ALL_CAPABILITY_DEFINITIONS as t, replacedFieldKeys as tn, createEvent as tr, coreBlockIdFromAddonId as tt, CORE_BLOCKS_ADDON_ID as u, siteZoneKey as un, parseJsonUnknown as ur, deviceExtensionCapability as ut, FieldClaimModeSchema as v, userManagementCapability as vn, evaluateCompositionField as vt, MINUTES_PER_DAY as w, COMPOSER_ADDON_ID as wn, formatMinutes as wt, LegacyStorageLocationDefaultSchema as x, vectorIndexDimConflictError as xn, extractNestedAddonId as xt, FieldClaimSchema as y, validateComposition as yn, evictionPolicyOfLocation as yt, addonSettingsCapability as z, isSourceCap as zn, logLevelAtMost as zt };
58001
+ export { compositionSourceRefs as $, WELL_KNOWN_TAB_MAP as $n, parseStreamParamsFormPatch as $t, STORAGE_ACCESS_FALLBACK as A, SOURCE_CAPS as An, isCollectionArrayMethod as At, addonWidgetsCapability as B, evaluateSensorEdge as Bn, locationIconKey as Bt, METHOD_ACCESS_MAP as C, validateComposition as Cn, expandContainer as Ct, REDACTED_SECRET as D, withLocationMode as Dn, formatMinutes as Dt, RECORDING_SOURCE_CAMSTACK_ADDON as E, vectorStoreCapability as En, foldSnapshotByFunction as Et, StorageLocationTypeSchema as F, compositionFeatureEnabled as Fn, kebabToCamel as Ft, batteryCapability as G, DeviceType as Gn, mayWriteToLocation as Gt, authProviderCapability as H, toExpressionValue as Hn, logLevelAtMost as Ht, StorageMigrationJobSchema as I, compositionItemFieldPath as In, legacyAutoLinkIds as It, buildStreamParamsConfigSchema as J, DATAPLANE_SECRET_HEADER as Jn, modeMayWrite as Jt, buildCapabilityLookup as K, errMsg as Kn, memoizePinnedFfmpeg as Kt, UserRecordSchema as L, compositionSliceKey as Ln, legacyModeOf as Lt, ScopedTokenSchema as M, cloneStatefulExpressionMemory as Mn, isObjectInput as Mt, StorageCleanupJobSchema as N, composedDeviceRef as Nn, isSameAddonId as Nt, RUNTIME_DEFAULTS as O, COMPOSER_ADDON_ID as On, gbToBytes as Ot, StorageLocationModeSchema as P, composedDeviceStableId as Pn, isVoidInput as Pt, compositionFieldKey as Q, ReadinessTimeoutError as Qn, objectInputDeclaresRequiredDeviceId as Qt, addonPagesCapability as R, compositionSourceKey as Rn, lifecycleJobSchema as Rt, LocationIconIdSchema as S, userManagementCapability as Sn, evictionPolicyOfLocation as St, MOTION_CLOSE_AFTER_MS as T, vectorIndexDimConflictError as Tn, filesystemBrowseCapability as Tt, backupCapability as U, DeviceFeature as Un, looseSchema as Ut, alertsCapability as V, isSourceCap as Vn, logDestinationCapability as Vt, bareAddonId as W, DeviceRole as Wn, mayReadLocation as Wt, collectSecretConfigKeys as X, DEVICE_STATUS_METHOD as Xn, normalizeUnit as Xt, cameraGridLayoutCapability as Y, DEVICE_SETTINGS_CONTRIBUTION_METHODS as Yn, motionCapability as Yt, composerCapability as Z, ReadinessRegistry as Zn, objectInputDeclaresAddonId as Zt, FIRST_LEVEL_MACRO_CLASSES as _, storageMigrationCapability as _n, EventCategory as _r, doorbellFeatureSources as _t, BatteryStatusSchema as a, replacedFieldKeys as an, emitReadiness as ar, createDeviceExtensionProvider as at, LOG_LEVEL_RANK as b, streamCatalogCapability as bn, enumerateSchemaFields as bt, COMPOSED_ITEM_FIXED_FIELDS as c, resolveMethodAuth as cn, isDeviceConfigCap as cr, decodeVectorBase64 as ct, CORE_BLOCK_ADDON_PREFIX as d, scopesAllowDeviceCap as dn, parseJsonObject as dr, describeComposableCaps as dt, planComposition as en, asJsonObject as er, coreBlockAddonId as et, CoreBlockSchema as f, scoreRuntimes as fn, parseJsonUnknown as fr, deviceExtensionCapability as ft, DeviceStatusSchema as g, storageCapability as gn, sleep as gr, doorbellCapability as gt, DeclaredDevices as h, snapshotCapability as hn, scopeKey as hr, deviceStatusCapability as ht, BaseDevice as i, reducePoints as in, emitDownForOwnedCaps as ir, coreBlocksCapability as it, STREAM_PROFILE_META as j, buildFfmpegArgs as jn, isLocationEnabled as jt, RecordingStatusSchema as k, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as kn, isArrayOutputSchema as kt, COMPOSER_RUNNER_ID as l, runtimeStatePolicyFor as ln, isEvent as lr, defineExtensionSlotProvider as lt, DERIVED_CAMERA_SILENCED_CAP_NAMES as m, siteZoneKey as mn, resolveCapMount as mr, deviceStateCapability as mt, AlertSchema as n, procedureAuthKey as nn, asString$1 as nr, coreBlockIdFromAddonId as nt, CAMERA_SWITCH_ORDER as o, resolveBucketMs as on, expandCapMethods as or, dataStoreProviderCapability as ot, DAY_NAMES as p, settingsStoreCapability as pn, readinessKey as pr, deviceManagerCapability as pt, buildDoorbellCompositionFeature as q, BaseAddon as qn, metricsProviderCapability as qt, ApiKeyRecordSchema as r, ptzAutotrackCapability as rn, createEvent as rr, coreBlockSourceKind as rt, CAP_NAMES_WITH_STATUS as s, resolveLocationMode as sn, hydrateSchema as sr, declarationOwnerNodeId as st, ALL_CAPABILITY_DEFINITIONS as t, platformProbeCapability as tn, asNumber as tr, coreBlockComposition as tt, CORE_BLOCKS_ADDON_ID as u, scopesAllowAddon as un, nodePin as ur, deriveBatteryPresence as ut, FieldClaimModeSchema as v, storageOccupancyCapability as vn, enumerateInferenceDevices as vt, MINUTES_PER_DAY as w, vectorDimFromBase64 as wn, extractNestedAddonId as wt, LegacyStorageLocationDefaultSchema as x, streamQualityLabel as xn, evaluateCompositionField as xt, FieldClaimSchema as y, storageProviderCapability as yn, enumerateItemArrayFields as yt, addonSettingsCapability as z, createStatefulExpressionMemory as zn, localNetworkCapability as zt };