@camstack/addon-admin-ui 1.2.281 → 1.2.283

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 (82) hide show
  1. package/dist/assets/{AddonCollectionPage-soKFqrfy.js → AddonCollectionPage-B8KnVGbd.js} +1 -1
  2. package/dist/assets/{Addons-F0FzCnMu.js → Addons-DnCJnioW.js} +1 -1
  3. package/dist/assets/{AdminTabs-BO-L1Uwf.js → AdminTabs-BI9XKTOX.js} +1 -1
  4. package/dist/assets/{AiProfiles-zu-Hlaaq.js → AiProfiles-CvEPh-Jv.js} +1 -1
  5. package/dist/assets/{BrokerForm-Bxm31chI.js → BrokerForm-C2yLLkgI.js} +1 -1
  6. package/dist/assets/{Build-CLw_kmCL.js → Build-CK40D9NM.js} +3 -3
  7. package/dist/assets/{Capabilities-CAcJw7Ta.js → Capabilities-yZMKb9tU.js} +1 -1
  8. package/dist/assets/{CapabilityBadges-7G92KTDz.js → CapabilityBadges-jZ91IXKK.js} +1 -1
  9. package/dist/assets/{Cluster-CU5tbF9f.js → Cluster-DHnTdzp4.js} +1 -1
  10. package/dist/assets/{Data-C7NowYTp.js → Data-BK1XvSCX.js} +1 -1
  11. package/dist/assets/{DetectionIntelligence-Sb9YakHV.js → DetectionIntelligence-JzCKQTez.js} +1 -1
  12. package/dist/assets/{DeviceAggregateSection-vzc3AAX1.js → DeviceAggregateSection-DjNgs-C7.js} +1 -1
  13. package/dist/assets/{DeviceDetail-aWZ8xrl0.js → DeviceDetail-DwXtp81y.js} +2 -2
  14. package/dist/assets/{Devices-DVA721yf.js → Devices-CdDLN1CB.js} +1 -1
  15. package/dist/assets/{EmbedPlayerPage-CL9PQZ-h.js → EmbedPlayerPage-PrkKKoTW.js} +1 -1
  16. package/dist/assets/{Events-BRBmSeVN.js → Events-DfglYEyk.js} +1 -1
  17. package/dist/assets/{FormBuilder-Crh3qn5F.js → FormBuilder-BW-aPxVU.js} +1 -1
  18. package/dist/assets/{Identity-CwUyo32W.js → Identity-DlA4U51D.js} +1 -1
  19. package/dist/assets/{IntegrationDetail-HdNE8EiS.js → IntegrationDetail-BykFe-Xp.js} +1 -1
  20. package/dist/assets/{Integrations-B29nDIby.js → Integrations-BsYx8Icg.js} +1 -1
  21. package/dist/assets/{Integrations-DurjsPb5.js → Integrations-eOCgWm5F.js} +1 -1
  22. package/dist/assets/{Locations-DgqBE9sA.js → Locations-PjonRey7.js} +2 -2
  23. package/dist/assets/{Logs-vEFw4yOo.js → Logs-jVPNAt3B.js} +1 -1
  24. package/dist/assets/{ModelLicenses-9EiSBNsc.js → ModelLicenses-CQdcxMre.js} +1 -1
  25. package/dist/assets/{MotionZonesSettings-BT2Ee0TE.js → MotionZonesSettings-CpXmhunH.js} +1 -1
  26. package/dist/assets/{MyAccess-oRoj9dOw.js → MyAccess-DOMS0jmj.js} +1 -1
  27. package/dist/assets/{Network-DUrWuwQa.js → Network-xa58rRWs.js} +1 -1
  28. package/dist/assets/{NodeAddonsSettingsPanel-BrZGHcyS.js → NodeAddonsSettingsPanel-BaVxUfF9.js} +1 -1
  29. package/dist/assets/{NodeInferenceDevices-sV7OFW3a.js → NodeInferenceDevices-U1ryObys.js} +1 -1
  30. package/dist/assets/{Notifications-BRR8PHMO.js → Notifications-DD-BLTTI.js} +1 -1
  31. package/dist/assets/{Pipeline-k9ihvZay.js → Pipeline-DWAK7pIF.js} +1 -1
  32. package/dist/assets/{Plates-DJeb-epZ.js → Plates-CbdsbMyb.js} +1 -1
  33. package/dist/assets/{PrivacyMaskSettings-DfnZlp-y.js → PrivacyMaskSettings-C8e6bEmU.js} +1 -1
  34. package/dist/assets/{ProviderIcon-CnA9st3F.js → ProviderIcon-hov2-9r6.js} +1 -1
  35. package/dist/assets/{Recordings-CFEBsgGk.js → Recordings-C8dQnQ1e.js} +1 -1
  36. package/dist/assets/{ScanNetworkModal-BNIusXca.js → ScanNetworkModal-CqCK2DD8.js} +1 -1
  37. package/dist/assets/{SceneMonitorEditor-DINVM61L.js → SceneMonitorEditor-CUrOtvdi.js} +1 -1
  38. package/dist/assets/{Scenes-DgYUyf94.js → Scenes-C4loq8MD.js} +1 -1
  39. package/dist/assets/{Settings-CvQAXxMJ.js → Settings-DrTAEWqc.js} +1 -1
  40. package/dist/assets/{Showroom-D1WOi_jx.js → Showroom-40hdhFz1.js} +1 -1
  41. package/dist/assets/{StorageLocationsPanel-Cg_ifRtu.js → StorageLocationsPanel-Dpg2k0xG.js} +1 -1
  42. package/dist/assets/{Terminal-BKc_cirX.js → Terminal-BILfwG-Y.js} +1 -1
  43. package/dist/assets/{Viewer-CNPU3Ckb.js → Viewer-BMvAUWDx.js} +1 -1
  44. package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-CLNns9AD.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-TGdvxvjh.js} +1 -1
  45. package/dist/assets/{archive-BX6oRdUb.js → archive-BFNkINv4.js} +1 -1
  46. package/dist/assets/{authed-media-JSbLFKLx.js → authed-media-BpnQU397.js} +1 -1
  47. package/dist/assets/{boxes-ZTtdYORX.js → boxes-Bf1IHNTV.js} +1 -1
  48. package/dist/assets/circle-slash-DPnEG60c.js +1 -0
  49. package/dist/assets/{debounced-invalidate-C9CC_Pf9.js → debounced-invalidate-K1AdA02y.js} +1 -1
  50. package/dist/assets/{device-integration-icon-DXW8q4NB.js → device-integration-icon-PTiDPGQB.js} +1 -1
  51. package/dist/assets/{dist-q3cwqCda.js → dist-DKnFXsMJ.js} +1 -1
  52. package/dist/assets/dist-DazA9LoV.js +349 -0
  53. package/dist/assets/{face-bbox-Cyjp9Pta.js → face-bbox-Da5VnAqL.js} +1 -1
  54. package/dist/assets/{hard-drive-download-tkymostV.js → hard-drive-download-DtcG5liH.js} +1 -1
  55. package/dist/assets/{hostInit-D3_CBmhe.js → hostInit-DsMDUzRk.js} +1 -1
  56. package/dist/assets/{index-DZL64fy8.js → index-0STeYzEm.js} +3 -3
  57. package/dist/assets/{layers-CUtjCIzl.js → layers-DFlfCddL.js} +1 -1
  58. package/dist/assets/{location-owned-usage-BR7bdmoF.js → location-owned-usage-CLbjRj98.js} +1 -1
  59. package/dist/assets/{mf-entry-bootstrap-0-53a6a051.js → mf-entry-bootstrap-0-66d3a68d.js} +2 -2
  60. package/dist/assets/{player-overlays-B-nupL9q.js → player-overlays-yHDaFW3f.js} +1 -1
  61. package/dist/assets/remoteEntry-C08_fTTi.js +1 -0
  62. package/dist/assets/{send-DrjP1k5U.js → send-DvnV0csj.js} +1 -1
  63. package/dist/assets/{settings-2-BX-2JFal.js → settings-2-B0b0kB_o.js} +1 -1
  64. package/dist/assets/{square-check-big-BsokJFdH.js → square-check-big-Bc37vIB_.js} +1 -1
  65. package/dist/assets/src-CAB6kHIG.js +349 -0
  66. package/dist/assets/src-D4OhLpzS.js +67 -0
  67. package/dist/assets/{use-scenes-feed-CseWp9ye.js → use-scenes-feed-BaT4HuAT.js} +1 -1
  68. package/dist/assets/{use-stationary-objects-D9s0BolH.js → use-stationary-objects-D05GDtuW.js} +1 -1
  69. package/dist/assets/useEventInvalidation-6Xto5MoF.js +1 -0
  70. package/dist/assets/{useNodeUrlSelection-p6hE5M3h.js → useNodeUrlSelection-Dqr41cEe.js} +1 -1
  71. package/dist/assets/user-round-DxZvUMoN.js +1 -0
  72. package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-DIS-S6ic.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-BCrN6LQo.js} +2 -2
  73. package/dist/index.html +5 -5
  74. package/dist/server/addon.js +1986 -1674
  75. package/package.json +1 -1
  76. package/dist/assets/circle-slash-Cv7ZekkW.js +0 -1
  77. package/dist/assets/dist-DgQGWpwv.js +0 -349
  78. package/dist/assets/remoteEntry-n4mLOejN.js +0 -1
  79. package/dist/assets/src-BP6gnsKM.js +0 -349
  80. package/dist/assets/src-D5oqRj-D.js +0 -67
  81. package/dist/assets/useEventInvalidation-CPWuXTCK.js +0 -1
  82. package/dist/assets/user-round-CbxyAAro.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { fileURLToPath } from "node:url";
3
- //#region ../types/dist/event-category-BVDXG4tB.mjs
3
+ //#region ../types/dist/event-category-C5xZWqz6.mjs
4
4
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
5
5
  EventCategory["SystemBoot"] = "system.boot";
6
6
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -364,6 +364,16 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
364
364
  */
365
365
  EventCategory["DeviceStateChanged"] = "device.state-changed";
366
366
  /**
367
+ * The NATIVE shadow of a cap a composition has claimed fields of moved
368
+ * (D663). Payload: `{ deviceId, capName, native }` — what the provider
369
+ * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
370
+ * the composer's native self-reads, and the public event routers refuse it
371
+ * (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
372
+ * truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
373
+ * `replace` claim, one per native write of that cap.
374
+ */
375
+ EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
376
+ /**
367
377
  * Frame occupancy for a camera CHANGED — a tracked object was gained or
368
378
  * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
369
379
  *
@@ -5361,7 +5371,7 @@ var ZodIssueCode = {
5361
5371
  var ZodFirstPartyTypeKind;
5362
5372
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5363
5373
  //#endregion
5364
- //#region ../types/dist/sleep-DC-wdyeS.mjs
5374
+ //#region ../types/dist/sleep-E6eD4yQS.mjs
5365
5375
  /**
5366
5376
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5367
5377
  * window to float samples (D455).
@@ -6917,6 +6927,10 @@ var ProfileRtspEntrySchema = object({
6917
6927
  codec: string().optional(),
6918
6928
  resolution: CamStreamResolutionSchema.optional()
6919
6929
  });
6930
+ object({
6931
+ field: string().min(1),
6932
+ on: literal("advance")
6933
+ });
6920
6934
  /**
6921
6935
  * Output schema shared by the contribution + live methods.
6922
6936
  *
@@ -7017,324 +7031,6 @@ var adminUiCapability = {
7017
7031
  getVersion: method(_void(), VersionOutputSchema$1, { auth: "admin" })
7018
7032
  }
7019
7033
  };
7020
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
7021
- DeviceType["Camera"] = "camera";
7022
- DeviceType["Hub"] = "hub";
7023
- DeviceType["Light"] = "light";
7024
- DeviceType["Siren"] = "siren";
7025
- DeviceType["Switch"] = "switch";
7026
- DeviceType["Sensor"] = "sensor";
7027
- DeviceType["Thermostat"] = "thermostat";
7028
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
7029
- * `climate-control` cap surface with `Thermostat` but renders a
7030
- * dedicated AC-appropriate control UI (mode chips, fan speed,
7031
- * independent vertical/horizontal swing). Sources: native Gree, and
7032
- * reusable by other AC integrations. */
7033
- DeviceType["Climate"] = "climate";
7034
- DeviceType["Button"] = "button";
7035
- /** Generic stateless event emitter — carries a device's EXACT declared
7036
- * event vocabulary verbatim (no normalization). Installed with the
7037
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
7038
- * HA bus events (e.g. `zha_event`, generic). */
7039
- DeviceType["EventEmitter"] = "event-emitter";
7040
- /** Firmware/software update entity — current vs available version,
7041
- * updatable flag, update state, and an install action. Installed with
7042
- * the `update` cap. Sources: Homematic firmware-update channels (and
7043
- * reusable by other providers, e.g. HA `update.*` entities). */
7044
- DeviceType["Update"] = "update";
7045
- DeviceType["Generic"] = "generic";
7046
- /** Generic notification delivery target (HA `notify.<service>`, future
7047
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
7048
- * endpoint; the `notifier` cap defines the send surface. */
7049
- DeviceType["Notifier"] = "notifier";
7050
- /** Pre-recorded action sequence with optional parameters
7051
- * (HA `script.*`). Runnable via `script-runner` cap. */
7052
- DeviceType["Script"] = "script";
7053
- /** Automation rule (HA `automation.*`) — enable/disable + manual
7054
- * trigger surface exposed via `automation-control` cap. */
7055
- DeviceType["Automation"] = "automation";
7056
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
7057
- DeviceType["Lock"] = "lock";
7058
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
7059
- * `valve.*`). `cover` cap with sub-roles for variant. */
7060
- DeviceType["Cover"] = "cover";
7061
- /** Pipe / water / gas valve with open/close/stop and optional
7062
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
7063
- * modelled on the same open/closed lifecycle. */
7064
- DeviceType["Valve"] = "valve";
7065
- /** Humidifier / dehumidifier with on/off + target humidity + mode
7066
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
7067
- * modelled on the same target / mode lifecycle. */
7068
- DeviceType["Humidifier"] = "humidifier";
7069
- /** Water heater / boiler with target temperature + operation mode +
7070
- * away mode (HA `water_heater.*`). `water-heater` cap — a
7071
- * climate-family actuator. */
7072
- DeviceType["WaterHeater"] = "water-heater";
7073
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
7074
- DeviceType["Fan"] = "fan";
7075
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
7076
- * the camera surface — those use `Camera`. `media-player` cap. */
7077
- DeviceType["MediaPlayer"] = "media-player";
7078
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
7079
- * `alarm-panel` cap. */
7080
- DeviceType["AlarmPanel"] = "alarm-panel";
7081
- /** Generic user-settable input (HA `number` / `input_number` / `select`
7082
- * / `input_select` / `text` / `input_text` / `input_datetime`).
7083
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
7084
- * TextControl / DateTimeControl. */
7085
- DeviceType["Control"] = "control";
7086
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
7087
- * `presence` cap. */
7088
- DeviceType["Presence"] = "presence";
7089
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
7090
- * `weather` cap. */
7091
- DeviceType["Weather"] = "weather";
7092
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
7093
- DeviceType["Vacuum"] = "vacuum";
7094
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
7095
- * `lawn-mower-control` cap. */
7096
- DeviceType["LawnMower"] = "lawn-mower";
7097
- /** Physical HA device group — parent container for entity-children
7098
- * adopted from a single HA device entry. Not renderable as a
7099
- * standalone device; exists only to anchor child entities. */
7100
- DeviceType["Container"] = "container";
7101
- /** Single still-image entity (HA `image.*`). Read-only display of an
7102
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
7103
- DeviceType["Image"] = "image";
7104
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
7105
- * level, battery, desiccant life, feeding state and manual-feed /
7106
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
7107
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
7108
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
7109
- * integrations sharing the same food/desiccant/hopper surface. */
7110
- DeviceType["PetFeeder"] = "pet-feeder";
7111
- return DeviceType;
7112
- }({});
7113
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
7114
- DeviceFeature["BatteryOperated"] = "battery-operated";
7115
- DeviceFeature["Rebootable"] = "rebootable";
7116
- /**
7117
- * Device supports an on-demand re-sync of its derived spec with its
7118
- * upstream source — drives the generic Re-sync button. The owning
7119
- * provider implements the action via the `device-adoption.resync` cap.
7120
- */
7121
- DeviceFeature["Resyncable"] = "resyncable";
7122
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
7123
- DeviceFeature["DoorbellButton"] = "doorbell-button";
7124
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
7125
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
7126
- /**
7127
- * Camera supports the on-firmware autotrack subsystem (subject-
7128
- * following). Distinct from `PanTiltZoom` because not every PTZ
7129
- * camera ships autotrack — the admin UI uses this flag to gate
7130
- * the autotrack toggle / settings card without re-deriving from
7131
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
7132
- * driver sets this feature when probe confirms the firmware
7133
- * surface, and registers the cap in the same code path.
7134
- */
7135
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
7136
- /**
7137
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
7138
- * motion detection automatically activates this device. Mirrors
7139
- * `motion-trigger` cap registration: drivers set this feature in the
7140
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
7141
- *
7142
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
7143
- * fast scalar without binding fetch), notifier rules, and `listAll`
7144
- * filters that want "all devices with on-motion behaviour".
7145
- */
7146
- DeviceFeature["MotionTrigger"] = "motion-trigger";
7147
- /** Light supports rgb-triplet color via `color` cap. */
7148
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
7149
- /** Light supports HSV color via `color` cap. */
7150
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
7151
- /** Light supports color-temperature (mired) via `color` cap. */
7152
- DeviceFeature["LightColorMired"] = "light-color-mired";
7153
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
7154
- * targetHigh). Gates the range slider UI. */
7155
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
7156
- /** Thermostat exposes target humidity and/or current humidity
7157
- * readings. Gates the humidity controls. */
7158
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
7159
- /** Thermostat exposes a fan-mode selector. */
7160
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
7161
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
7162
- DeviceFeature["ClimatePreset"] = "climate-preset";
7163
- /** Thermostat exposes a vertical louver swing toggle. Gates the
7164
- * vertical-swing switch in the climate UI. Independent of horizontal. */
7165
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
7166
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
7167
- * horizontal-swing switch in the climate UI. Independent of vertical. */
7168
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
7169
- /** Cover exposes intermediate position control (0..100). Gates the
7170
- * position slider UI. */
7171
- DeviceFeature["CoverPositionable"] = "cover-positionable";
7172
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
7173
- DeviceFeature["CoverTilt"] = "cover-tilt";
7174
- /** Valve exposes intermediate position control (0..100). Gates the
7175
- * position slider / drag surface UI. */
7176
- DeviceFeature["ValvePositionable"] = "valve-positionable";
7177
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
7178
- DeviceFeature["FanSpeed"] = "fan-speed";
7179
- /** Fan exposes a preset mode selector. */
7180
- DeviceFeature["FanPreset"] = "fan-preset";
7181
- /** Fan exposes blade direction (forward/reverse) — typical of
7182
- * ceiling fans. */
7183
- DeviceFeature["FanDirection"] = "fan-direction";
7184
- /** Fan exposes an oscillation toggle. */
7185
- DeviceFeature["FanOscillating"] = "fan-oscillating";
7186
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
7187
- * field on the UI lock-controls panel. */
7188
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
7189
- /** Lock supports a latch-release ("open door") action distinct from
7190
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
7191
- * `supported_features`. Gates the Open Door button in the UI. */
7192
- DeviceFeature["LockOpen"] = "lock-open";
7193
- /** Media player exposes a seek-to-position surface. */
7194
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
7195
- /** Media player exposes a volume-level setter. */
7196
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
7197
- /** Media player exposes a mute toggle distinct from volume=0. */
7198
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
7199
- /** Media player exposes a shuffle toggle. */
7200
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
7201
- /** Media player exposes a repeat mode (off / all / one). */
7202
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
7203
- /** Media player exposes a source / input selector. */
7204
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
7205
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
7206
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
7207
- /** Media player exposes next-track. */
7208
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
7209
- /** Media player exposes previous-track. */
7210
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
7211
- /** Media player exposes stop distinct from pause. */
7212
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
7213
- /** Alarm panel requires a PIN code on arm/disarm. */
7214
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
7215
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
7216
- * addition to a textual location. */
7217
- DeviceFeature["PresenceGps"] = "presence-gps";
7218
- /** Notifier accepts an inline / URL image attachment. */
7219
- DeviceFeature["NotifierImage"] = "notifier-image";
7220
- /** Notifier accepts a priority hint (high/normal/low). */
7221
- DeviceFeature["NotifierPriority"] = "notifier-priority";
7222
- /** Notifier accepts a free-form `data` payload for platform-specific
7223
- * fields. */
7224
- DeviceFeature["NotifierData"] = "notifier-data";
7225
- /** Notifier supports interactive action buttons / callbacks. */
7226
- DeviceFeature["NotifierActions"] = "notifier-actions";
7227
- /** Notifier supports per-call recipient targeting (multi-user). */
7228
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
7229
- /** Script runner accepts a variables map on each run invocation. */
7230
- DeviceFeature["ScriptVariables"] = "script-variables";
7231
- /** Automation `trigger` accepts a skipCondition flag — fires the
7232
- * automation's actions while bypassing its condition block. */
7233
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
7234
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
7235
- * map tile in the vacuum UI. */
7236
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
7237
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
7238
- * switches group. */
7239
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
7240
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
7241
- * mower map tile. */
7242
- DeviceFeature["MowerHasMap"] = "mower-has-map";
7243
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
7244
- * spots) — gates the Mow action + map selectors. */
7245
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
7246
- return DeviceFeature;
7247
- }({});
7248
- /**
7249
- * Semantic role a device plays within its parent. Populated by driver
7250
- * addons when creating accessory devices (Reolink siren/floodlight/
7251
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
7252
- * admin UI to pick icons, labels, and widgets — a `Switch` with
7253
- * `role: Floodlight` renders as a bulb with a brightness slider,
7254
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
7255
- *
7256
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
7257
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
7258
- */
7259
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7260
- DeviceRole["Siren"] = "siren";
7261
- DeviceRole["Floodlight"] = "floodlight";
7262
- DeviceRole["Spotlight"] = "spotlight";
7263
- DeviceRole["PirSensor"] = "pir-sensor";
7264
- DeviceRole["Chime"] = "chime";
7265
- DeviceRole["Autotrack"] = "autotrack";
7266
- DeviceRole["Nightvision"] = "nightvision";
7267
- DeviceRole["PrivacyMask"] = "privacy-mask";
7268
- DeviceRole["Doorbell"] = "doorbell";
7269
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
7270
- * real Switch device for UI rendering / export adapters. */
7271
- DeviceRole["BinaryHelper"] = "binary-helper";
7272
- /** Generic motion / occupancy / moving event source. Distinct from
7273
- * the camera accessory PirSensor role: that one is a camera child;
7274
- * this is a standalone HA / 3rd-party motion sensor. */
7275
- DeviceRole["MotionSensor"] = "motion-sensor";
7276
- DeviceRole["ContactSensor"] = "contact-sensor";
7277
- DeviceRole["LeakSensor"] = "leak-sensor";
7278
- DeviceRole["SmokeSensor"] = "smoke-sensor";
7279
- DeviceRole["COSensor"] = "co-sensor";
7280
- DeviceRole["GasSensor"] = "gas-sensor";
7281
- DeviceRole["TamperSensor"] = "tamper-sensor";
7282
- DeviceRole["VibrationSensor"] = "vibration-sensor";
7283
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
7284
- DeviceRole["SoundSensor"] = "sound-sensor";
7285
- /** Fallback for `binary_sensor` without a known `device_class`. */
7286
- DeviceRole["BinarySensor"] = "binary-sensor";
7287
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
7288
- DeviceRole["HumiditySensor"] = "humidity-sensor";
7289
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
7290
- DeviceRole["PressureSensor"] = "pressure-sensor";
7291
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
7292
- DeviceRole["WindSensor"] = "wind-sensor";
7293
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
7294
- DeviceRole["RainSensor"] = "rain-sensor";
7295
- /** UV index (weather-station `uv-sensor` cap). */
7296
- DeviceRole["UvSensor"] = "uv-sensor";
7297
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
7298
- * Distinct from AmbientLightSensor (lux). */
7299
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
7300
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
7301
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
7302
- DeviceRole["PowerSensor"] = "power-sensor";
7303
- DeviceRole["EnergySensor"] = "energy-sensor";
7304
- DeviceRole["VoltageSensor"] = "voltage-sensor";
7305
- DeviceRole["CurrentSensor"] = "current-sensor";
7306
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
7307
- /** Battery level (numeric % via `sensor` OR low-bool via
7308
- * `binary_sensor` — the cap distinguishes via the value type). */
7309
- DeviceRole["BatterySensor"] = "battery-sensor";
7310
- /** Fallback for `sensor` numeric without a known `device_class`. */
7311
- DeviceRole["NumericSensor"] = "numeric-sensor";
7312
- /** String / enum state (HA `sensor` with `state_class: enum` or
7313
- * `attributes.options`). */
7314
- DeviceRole["EnumSensor"] = "enum-sensor";
7315
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
7316
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
7317
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
7318
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
7319
- /** Last-resort fallback when nothing else matches. */
7320
- DeviceRole["GenericSensor"] = "generic-sensor";
7321
- DeviceRole["NumericControl"] = "numeric-control";
7322
- DeviceRole["SelectControl"] = "select-control";
7323
- DeviceRole["TextControl"] = "text-control";
7324
- DeviceRole["DateTimeControl"] = "datetime-control";
7325
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
7326
- * rich features (image, priority, channel routing). */
7327
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
7328
- /** Chat / messaging service (HA `notify.telegram_*`,
7329
- * `notify.discord_*`, etc.). */
7330
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
7331
- /** Email-based delivery (HA `notify.smtp`, etc.). */
7332
- DeviceRole["EmailNotifier"] = "email-notifier";
7333
- /** Fallback when the notifier service name doesn't match a known
7334
- * pattern. */
7335
- DeviceRole["GenericNotifier"] = "generic-notifier";
7336
- return DeviceRole;
7337
- }({});
7338
7034
  var StaticDirOutputSchema = object({ staticDir: string() });
7339
7035
  var VersionOutputSchema = object({ version: string() });
7340
7036
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -7401,6 +7097,1129 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7401
7097
  kind: "mutation",
7402
7098
  auth: "admin"
7403
7099
  }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7100
+ //#endregion
7101
+ //#region ../types/dist/err-msg-DX6i_MY4.mjs
7102
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
7103
+ DeviceType["Camera"] = "camera";
7104
+ DeviceType["Hub"] = "hub";
7105
+ DeviceType["Light"] = "light";
7106
+ DeviceType["Siren"] = "siren";
7107
+ DeviceType["Switch"] = "switch";
7108
+ DeviceType["Sensor"] = "sensor";
7109
+ DeviceType["Thermostat"] = "thermostat";
7110
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
7111
+ * `climate-control` cap surface with `Thermostat` but renders a
7112
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
7113
+ * independent vertical/horizontal swing). Sources: native Gree, and
7114
+ * reusable by other AC integrations. */
7115
+ DeviceType["Climate"] = "climate";
7116
+ DeviceType["Button"] = "button";
7117
+ /** Generic stateless event emitter — carries a device's EXACT declared
7118
+ * event vocabulary verbatim (no normalization). Installed with the
7119
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
7120
+ * HA bus events (e.g. `zha_event`, generic). */
7121
+ DeviceType["EventEmitter"] = "event-emitter";
7122
+ /** Firmware/software update entity — current vs available version,
7123
+ * updatable flag, update state, and an install action. Installed with
7124
+ * the `update` cap. Sources: Homematic firmware-update channels (and
7125
+ * reusable by other providers, e.g. HA `update.*` entities). */
7126
+ DeviceType["Update"] = "update";
7127
+ DeviceType["Generic"] = "generic";
7128
+ /** Generic notification delivery target (HA `notify.<service>`, future
7129
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
7130
+ * endpoint; the `notifier` cap defines the send surface. */
7131
+ DeviceType["Notifier"] = "notifier";
7132
+ /** Pre-recorded action sequence with optional parameters
7133
+ * (HA `script.*`). Runnable via `script-runner` cap. */
7134
+ DeviceType["Script"] = "script";
7135
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
7136
+ * trigger surface exposed via `automation-control` cap. */
7137
+ DeviceType["Automation"] = "automation";
7138
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
7139
+ DeviceType["Lock"] = "lock";
7140
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
7141
+ * `valve.*`). `cover` cap with sub-roles for variant. */
7142
+ DeviceType["Cover"] = "cover";
7143
+ /** Pipe / water / gas valve with open/close/stop and optional
7144
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
7145
+ * modelled on the same open/closed lifecycle. */
7146
+ DeviceType["Valve"] = "valve";
7147
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
7148
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
7149
+ * modelled on the same target / mode lifecycle. */
7150
+ DeviceType["Humidifier"] = "humidifier";
7151
+ /** Water heater / boiler with target temperature + operation mode +
7152
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
7153
+ * climate-family actuator. */
7154
+ DeviceType["WaterHeater"] = "water-heater";
7155
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
7156
+ DeviceType["Fan"] = "fan";
7157
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
7158
+ * the camera surface — those use `Camera`. `media-player` cap. */
7159
+ DeviceType["MediaPlayer"] = "media-player";
7160
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
7161
+ * `alarm-panel` cap. */
7162
+ DeviceType["AlarmPanel"] = "alarm-panel";
7163
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
7164
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
7165
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
7166
+ * TextControl / DateTimeControl. */
7167
+ DeviceType["Control"] = "control";
7168
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
7169
+ * `presence` cap. */
7170
+ DeviceType["Presence"] = "presence";
7171
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
7172
+ * `weather` cap. */
7173
+ DeviceType["Weather"] = "weather";
7174
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
7175
+ DeviceType["Vacuum"] = "vacuum";
7176
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
7177
+ * `lawn-mower-control` cap. */
7178
+ DeviceType["LawnMower"] = "lawn-mower";
7179
+ /** Physical HA device group — parent container for entity-children
7180
+ * adopted from a single HA device entry. Not renderable as a
7181
+ * standalone device; exists only to anchor child entities. */
7182
+ DeviceType["Container"] = "container";
7183
+ /** Single still-image entity (HA `image.*`). Read-only display of an
7184
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
7185
+ DeviceType["Image"] = "image";
7186
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
7187
+ * level, battery, desiccant life, feeding state and manual-feed /
7188
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
7189
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
7190
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
7191
+ * integrations sharing the same food/desiccant/hopper surface. */
7192
+ DeviceType["PetFeeder"] = "pet-feeder";
7193
+ return DeviceType;
7194
+ }({});
7195
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
7196
+ DeviceFeature["BatteryOperated"] = "battery-operated";
7197
+ DeviceFeature["Rebootable"] = "rebootable";
7198
+ /**
7199
+ * Device supports an on-demand re-sync of its derived spec with its
7200
+ * upstream source — drives the generic Re-sync button. The owning
7201
+ * provider implements the action via the `device-adoption.resync` cap.
7202
+ */
7203
+ DeviceFeature["Resyncable"] = "resyncable";
7204
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
7205
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
7206
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
7207
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
7208
+ /**
7209
+ * Camera supports the on-firmware autotrack subsystem (subject-
7210
+ * following). Distinct from `PanTiltZoom` because not every PTZ
7211
+ * camera ships autotrack — the admin UI uses this flag to gate
7212
+ * the autotrack toggle / settings card without re-deriving from
7213
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
7214
+ * driver sets this feature when probe confirms the firmware
7215
+ * surface, and registers the cap in the same code path.
7216
+ */
7217
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
7218
+ /**
7219
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
7220
+ * motion detection automatically activates this device. Mirrors
7221
+ * `motion-trigger` cap registration: drivers set this feature in the
7222
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
7223
+ *
7224
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
7225
+ * fast scalar without binding fetch), notifier rules, and `listAll`
7226
+ * filters that want "all devices with on-motion behaviour".
7227
+ */
7228
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
7229
+ /** Light supports rgb-triplet color via `color` cap. */
7230
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
7231
+ /** Light supports HSV color via `color` cap. */
7232
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
7233
+ /** Light supports color-temperature (mired) via `color` cap. */
7234
+ DeviceFeature["LightColorMired"] = "light-color-mired";
7235
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
7236
+ * targetHigh). Gates the range slider UI. */
7237
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
7238
+ /** Thermostat exposes target humidity and/or current humidity
7239
+ * readings. Gates the humidity controls. */
7240
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
7241
+ /** Thermostat exposes a fan-mode selector. */
7242
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
7243
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
7244
+ DeviceFeature["ClimatePreset"] = "climate-preset";
7245
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
7246
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
7247
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
7248
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
7249
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
7250
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
7251
+ /** Cover exposes intermediate position control (0..100). Gates the
7252
+ * position slider UI. */
7253
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
7254
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
7255
+ DeviceFeature["CoverTilt"] = "cover-tilt";
7256
+ /** Valve exposes intermediate position control (0..100). Gates the
7257
+ * position slider / drag surface UI. */
7258
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
7259
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
7260
+ DeviceFeature["FanSpeed"] = "fan-speed";
7261
+ /** Fan exposes a preset mode selector. */
7262
+ DeviceFeature["FanPreset"] = "fan-preset";
7263
+ /** Fan exposes blade direction (forward/reverse) — typical of
7264
+ * ceiling fans. */
7265
+ DeviceFeature["FanDirection"] = "fan-direction";
7266
+ /** Fan exposes an oscillation toggle. */
7267
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
7268
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
7269
+ * field on the UI lock-controls panel. */
7270
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
7271
+ /** Lock supports a latch-release ("open door") action distinct from
7272
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
7273
+ * `supported_features`. Gates the Open Door button in the UI. */
7274
+ DeviceFeature["LockOpen"] = "lock-open";
7275
+ /** Media player exposes a seek-to-position surface. */
7276
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
7277
+ /** Media player exposes a volume-level setter. */
7278
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
7279
+ /** Media player exposes a mute toggle distinct from volume=0. */
7280
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
7281
+ /** Media player exposes a shuffle toggle. */
7282
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
7283
+ /** Media player exposes a repeat mode (off / all / one). */
7284
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
7285
+ /** Media player exposes a source / input selector. */
7286
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
7287
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
7288
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
7289
+ /** Media player exposes next-track. */
7290
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
7291
+ /** Media player exposes previous-track. */
7292
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
7293
+ /** Media player exposes stop distinct from pause. */
7294
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
7295
+ /** Alarm panel requires a PIN code on arm/disarm. */
7296
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
7297
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
7298
+ * addition to a textual location. */
7299
+ DeviceFeature["PresenceGps"] = "presence-gps";
7300
+ /** Notifier accepts an inline / URL image attachment. */
7301
+ DeviceFeature["NotifierImage"] = "notifier-image";
7302
+ /** Notifier accepts a priority hint (high/normal/low). */
7303
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
7304
+ /** Notifier accepts a free-form `data` payload for platform-specific
7305
+ * fields. */
7306
+ DeviceFeature["NotifierData"] = "notifier-data";
7307
+ /** Notifier supports interactive action buttons / callbacks. */
7308
+ DeviceFeature["NotifierActions"] = "notifier-actions";
7309
+ /** Notifier supports per-call recipient targeting (multi-user). */
7310
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
7311
+ /** Script runner accepts a variables map on each run invocation. */
7312
+ DeviceFeature["ScriptVariables"] = "script-variables";
7313
+ /** Automation `trigger` accepts a skipCondition flag — fires the
7314
+ * automation's actions while bypassing its condition block. */
7315
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
7316
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
7317
+ * map tile in the vacuum UI. */
7318
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
7319
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
7320
+ * switches group. */
7321
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
7322
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
7323
+ * mower map tile. */
7324
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
7325
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
7326
+ * spots) — gates the Mow action + map selectors. */
7327
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
7328
+ return DeviceFeature;
7329
+ }({});
7330
+ /**
7331
+ * Semantic role a device plays within its parent. Populated by driver
7332
+ * addons when creating accessory devices (Reolink siren/floodlight/
7333
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
7334
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
7335
+ * `role: Floodlight` renders as a bulb with a brightness slider,
7336
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
7337
+ *
7338
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
7339
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
7340
+ */
7341
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7342
+ DeviceRole["Siren"] = "siren";
7343
+ DeviceRole["Floodlight"] = "floodlight";
7344
+ DeviceRole["Spotlight"] = "spotlight";
7345
+ DeviceRole["PirSensor"] = "pir-sensor";
7346
+ DeviceRole["Chime"] = "chime";
7347
+ DeviceRole["Autotrack"] = "autotrack";
7348
+ DeviceRole["Nightvision"] = "nightvision";
7349
+ DeviceRole["PrivacyMask"] = "privacy-mask";
7350
+ DeviceRole["Doorbell"] = "doorbell";
7351
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
7352
+ * real Switch device for UI rendering / export adapters. */
7353
+ DeviceRole["BinaryHelper"] = "binary-helper";
7354
+ /** Generic motion / occupancy / moving event source. Distinct from
7355
+ * the camera accessory PirSensor role: that one is a camera child;
7356
+ * this is a standalone HA / 3rd-party motion sensor. */
7357
+ DeviceRole["MotionSensor"] = "motion-sensor";
7358
+ DeviceRole["ContactSensor"] = "contact-sensor";
7359
+ DeviceRole["LeakSensor"] = "leak-sensor";
7360
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
7361
+ DeviceRole["COSensor"] = "co-sensor";
7362
+ DeviceRole["GasSensor"] = "gas-sensor";
7363
+ DeviceRole["TamperSensor"] = "tamper-sensor";
7364
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
7365
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
7366
+ DeviceRole["SoundSensor"] = "sound-sensor";
7367
+ /** Fallback for `binary_sensor` without a known `device_class`. */
7368
+ DeviceRole["BinarySensor"] = "binary-sensor";
7369
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
7370
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
7371
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
7372
+ DeviceRole["PressureSensor"] = "pressure-sensor";
7373
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
7374
+ DeviceRole["WindSensor"] = "wind-sensor";
7375
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
7376
+ DeviceRole["RainSensor"] = "rain-sensor";
7377
+ /** UV index (weather-station `uv-sensor` cap). */
7378
+ DeviceRole["UvSensor"] = "uv-sensor";
7379
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
7380
+ * Distinct from AmbientLightSensor (lux). */
7381
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
7382
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
7383
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
7384
+ DeviceRole["PowerSensor"] = "power-sensor";
7385
+ DeviceRole["EnergySensor"] = "energy-sensor";
7386
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
7387
+ DeviceRole["CurrentSensor"] = "current-sensor";
7388
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
7389
+ /** Battery level (numeric % via `sensor` OR low-bool via
7390
+ * `binary_sensor` — the cap distinguishes via the value type). */
7391
+ DeviceRole["BatterySensor"] = "battery-sensor";
7392
+ /** Fallback for `sensor` numeric without a known `device_class`. */
7393
+ DeviceRole["NumericSensor"] = "numeric-sensor";
7394
+ /** String / enum state (HA `sensor` with `state_class: enum` or
7395
+ * `attributes.options`). */
7396
+ DeviceRole["EnumSensor"] = "enum-sensor";
7397
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
7398
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
7399
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
7400
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
7401
+ /** Last-resort fallback when nothing else matches. */
7402
+ DeviceRole["GenericSensor"] = "generic-sensor";
7403
+ DeviceRole["NumericControl"] = "numeric-control";
7404
+ DeviceRole["SelectControl"] = "select-control";
7405
+ DeviceRole["TextControl"] = "text-control";
7406
+ DeviceRole["DateTimeControl"] = "datetime-control";
7407
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
7408
+ * rich features (image, priority, channel routing). */
7409
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
7410
+ /** Chat / messaging service (HA `notify.telegram_*`,
7411
+ * `notify.discord_*`, etc.). */
7412
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
7413
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
7414
+ DeviceRole["EmailNotifier"] = "email-notifier";
7415
+ /** Fallback when the notifier service name doesn't match a known
7416
+ * pattern. */
7417
+ DeviceRole["GenericNotifier"] = "generic-notifier";
7418
+ return DeviceRole;
7419
+ }({});
7420
+ //#endregion
7421
+ //#region ../types/dist/composition-D5BQywfe.mjs
7422
+ /**
7423
+ * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
7424
+ * valutatore puro del suo FRONTE.
7425
+ *
7426
+ * Viveva dentro il builtin virtual-doorbell
7427
+ * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
7428
+ * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
7429
+ * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
7430
+ * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
7431
+ * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
7432
+ * SPOSTA qui e il doorbell la ri-esporta.
7433
+ *
7434
+ * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
7435
+ * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
7436
+ * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
7437
+ * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
7438
+ */
7439
+ /**
7440
+ * Known binary / switch source caps → the boolean slice field whose
7441
+ * false→true rise counts as ACTIVE. Every entry is "fire on active".
7442
+ * Sensors whose "active" reading is not a plain boolean (presence's string
7443
+ * state, connectivity's connected flag) are deliberately excluded — a
7444
+ * reconnect is not a doorbell press, and it is not a recording either.
7445
+ */
7446
+ var SOURCE_CAP_ACTIVE_FIELD = {
7447
+ contact: "entryOpen",
7448
+ binary: "on",
7449
+ switch: "on",
7450
+ motion: "detected",
7451
+ flood: "flooded",
7452
+ gas: "detected",
7453
+ smoke: "detected",
7454
+ "carbon-monoxide": "detected",
7455
+ vibration: "detected",
7456
+ tamper: "tampered"
7457
+ };
7458
+ /**
7459
+ * Error types for the safe expression engine. Two distinct classes so callers
7460
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
7461
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
7462
+ */
7463
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7464
+ * the failure is anchored to a character (author-facing inline feedback). */
7465
+ var ExpressionParseError = class extends Error {
7466
+ position;
7467
+ constructor(message, position) {
7468
+ super(message);
7469
+ this.name = "ExpressionParseError";
7470
+ this.position = position;
7471
+ }
7472
+ };
7473
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7474
+ * result, unknown builtin, step-budget exceeded). */
7475
+ var ExpressionEvalError = class extends Error {
7476
+ constructor(message) {
7477
+ super(message);
7478
+ this.name = "ExpressionEvalError";
7479
+ }
7480
+ };
7481
+ function asFiniteNumber(value, name, index) {
7482
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7483
+ return value;
7484
+ }
7485
+ function asString(value, name, index) {
7486
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
7487
+ return value;
7488
+ }
7489
+ function finiteResult(value, name) {
7490
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
7491
+ return value;
7492
+ }
7493
+ function allFiniteNumbers(args, name) {
7494
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
7495
+ }
7496
+ function asBoolean(value, name, index) {
7497
+ if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
7498
+ return value;
7499
+ }
7500
+ /** A plain decimal (optional sign, fraction, exponent) — never hex, never `Infinity`, never a unit suffix. */
7501
+ var DECIMAL = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
7502
+ /**
7503
+ * `number(x)`: a finite number, or a string that IS one, as a number; every
7504
+ * other value — `''`, `'unavailable'`, a boolean, null — is `null`
7505
+ * (unavailable, never 0: D393). The one bridge from a string-valued source (an
7506
+ * HA sensor with a unit and no device_class is an `enum-sensor`) to a number.
7507
+ */
7508
+ function toNumberOrNull(value) {
7509
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
7510
+ if (typeof value !== "string") return null;
7511
+ const trimmed = value.trim();
7512
+ if (!DECIMAL.test(trimmed)) return null;
7513
+ const parsed = Number(trimmed);
7514
+ return Number.isFinite(parsed) ? parsed : null;
7515
+ }
7516
+ var INF = Number.POSITIVE_INFINITY;
7517
+ var table = {
7518
+ min: {
7519
+ minArgs: 1,
7520
+ maxArgs: INF,
7521
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
7522
+ },
7523
+ max: {
7524
+ minArgs: 1,
7525
+ maxArgs: INF,
7526
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
7527
+ },
7528
+ abs: {
7529
+ minArgs: 1,
7530
+ maxArgs: 1,
7531
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
7532
+ },
7533
+ floor: {
7534
+ minArgs: 1,
7535
+ maxArgs: 1,
7536
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
7537
+ },
7538
+ ceil: {
7539
+ minArgs: 1,
7540
+ maxArgs: 1,
7541
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
7542
+ },
7543
+ sqrt: {
7544
+ minArgs: 1,
7545
+ maxArgs: 1,
7546
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
7547
+ },
7548
+ round: {
7549
+ minArgs: 1,
7550
+ maxArgs: 2,
7551
+ apply: (args) => {
7552
+ const x = asFiniteNumber(args[0], "round", 0);
7553
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
7554
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
7555
+ const factor = 10 ** digits;
7556
+ return finiteResult(Math.round(x * factor) / factor, "round");
7557
+ }
7558
+ },
7559
+ pow: {
7560
+ minArgs: 2,
7561
+ maxArgs: 2,
7562
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
7563
+ },
7564
+ clamp: {
7565
+ minArgs: 3,
7566
+ maxArgs: 3,
7567
+ apply: (args) => {
7568
+ const x = asFiniteNumber(args[0], "clamp", 0);
7569
+ const lo = asFiniteNumber(args[1], "clamp", 1);
7570
+ const hi = asFiniteNumber(args[2], "clamp", 2);
7571
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
7572
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
7573
+ }
7574
+ },
7575
+ avg: {
7576
+ minArgs: 1,
7577
+ maxArgs: INF,
7578
+ apply: (args) => {
7579
+ const nums = allFiniteNumbers(args, "avg");
7580
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
7581
+ }
7582
+ },
7583
+ sum: {
7584
+ minArgs: 1,
7585
+ maxArgs: INF,
7586
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
7587
+ },
7588
+ coalesce: {
7589
+ minArgs: 1,
7590
+ maxArgs: INF,
7591
+ apply: (args) => {
7592
+ for (const a of args) if (a !== null) return a;
7593
+ return null;
7594
+ }
7595
+ },
7596
+ age: {
7597
+ minArgs: 2,
7598
+ maxArgs: 2,
7599
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
7600
+ },
7601
+ convert: {
7602
+ minArgs: 3,
7603
+ maxArgs: 3,
7604
+ apply: (args, hooks) => {
7605
+ const x = asFiniteNumber(args[0], "convert", 0);
7606
+ const from = asString(args[1], "convert", 1).trim();
7607
+ const to = asString(args[2], "convert", 2).trim();
7608
+ if (hooks.convert) {
7609
+ const out = hooks.convert(x, from, to);
7610
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
7611
+ return finiteResult(out, "convert");
7612
+ }
7613
+ if (from === to) return x;
7614
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
7615
+ }
7616
+ },
7617
+ any: {
7618
+ minArgs: 1,
7619
+ maxArgs: INF,
7620
+ apply: (args) => args.map((a, i) => asBoolean(a, "any", i)).some((b) => b)
7621
+ },
7622
+ all: {
7623
+ minArgs: 1,
7624
+ maxArgs: INF,
7625
+ apply: (args) => args.map((a, i) => asBoolean(a, "all", i)).every((b) => b)
7626
+ },
7627
+ within: {
7628
+ minArgs: 2,
7629
+ maxArgs: 2,
7630
+ apply: (args, hooks) => {
7631
+ const windowMs = asFiniteNumber(args[1], "within", 1);
7632
+ if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
7633
+ const at = args[0];
7634
+ if (at === null) return false;
7635
+ const ts = asFiniteNumber(at, "within", 0);
7636
+ const now = hooks.now;
7637
+ if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
7638
+ const inside = now - ts <= windowMs;
7639
+ if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
7640
+ return inside;
7641
+ }
7642
+ },
7643
+ number: {
7644
+ minArgs: 1,
7645
+ maxArgs: 1,
7646
+ apply: (args) => toNumberOrNull(args[0])
7647
+ },
7648
+ latest: {
7649
+ minArgs: 1,
7650
+ maxArgs: INF,
7651
+ apply: (args) => {
7652
+ const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
7653
+ return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
7654
+ }
7655
+ },
7656
+ rose: {
7657
+ minArgs: 2,
7658
+ maxArgs: 3,
7659
+ apply: () => {
7660
+ throw new ExpressionEvalError("rose: stateful evaluation context is required");
7661
+ }
7662
+ },
7663
+ count: {
7664
+ minArgs: 1,
7665
+ maxArgs: 1,
7666
+ apply: () => {
7667
+ throw new ExpressionEvalError("count: stateful evaluation context is required");
7668
+ }
7669
+ }
7670
+ };
7671
+ Object.freeze(Object.assign(Object.create(null), table));
7672
+ /** The set of valid builtin names — used by the parser to reject unknown
7673
+ * callees at parse time (immediate author feedback). */
7674
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
7675
+ /**
7676
+ * Resource-bound constants for the safe expression engine.
7677
+ *
7678
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
7679
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
7680
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
7681
+ * work a single author-supplied expression can request, so a hostile or
7682
+ * accidental pathological string can never spend unbounded CPU/memory.
7683
+ */
7684
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7685
+ * rejected without allocation. */
7686
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7687
+ /** A legal binding / identifier name. */
7688
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7689
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
7690
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
7691
+ var RESERVED_BINDING_NAMES = new Set([
7692
+ "now",
7693
+ "true",
7694
+ "false",
7695
+ "null"
7696
+ ]);
7697
+ /**
7698
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
7699
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
7700
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
7701
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
7702
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
7703
+ * is a parse error with a source position, so member access / assignment /
7704
+ * template literals are lexically impossible.
7705
+ */
7706
+ var KEYWORDS = new Set([
7707
+ "true",
7708
+ "false",
7709
+ "null"
7710
+ ]);
7711
+ function isDigit(ch) {
7712
+ return ch >= "0" && ch <= "9";
7713
+ }
7714
+ function isIdentStart(ch) {
7715
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
7716
+ }
7717
+ function isIdentPart(ch) {
7718
+ return isIdentStart(ch) || isDigit(ch);
7719
+ }
7720
+ function isWhitespace(ch) {
7721
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
7722
+ }
7723
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
7724
+ * Throws `ExpressionParseError` on any illegal character or unterminated
7725
+ * string. */
7726
+ function tokenize(source) {
7727
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
7728
+ const tokens = [];
7729
+ let i = 0;
7730
+ const n = source.length;
7731
+ while (i < n) {
7732
+ const ch = source[i];
7733
+ if (isWhitespace(ch)) {
7734
+ i += 1;
7735
+ continue;
7736
+ }
7737
+ if (isDigit(ch)) {
7738
+ const start = i;
7739
+ while (i < n && isDigit(source[i])) i += 1;
7740
+ if (i < n && source[i] === ".") {
7741
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
7742
+ i += 1;
7743
+ while (i < n && isDigit(source[i])) i += 1;
7744
+ }
7745
+ const text = source.slice(start, i);
7746
+ const value = Number(text);
7747
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
7748
+ tokens.push({
7749
+ type: "number",
7750
+ value,
7751
+ pos: start
7752
+ });
7753
+ continue;
7754
+ }
7755
+ if (ch === "'" || ch === "\"") {
7756
+ const quote = ch;
7757
+ const start = i;
7758
+ i += 1;
7759
+ let out = "";
7760
+ let closed = false;
7761
+ while (i < n) {
7762
+ const c = source[i];
7763
+ if (c === "\\") {
7764
+ const next = i + 1 < n ? source[i + 1] : "";
7765
+ if (next === "\\" || next === "'" || next === "\"") {
7766
+ out += next;
7767
+ i += 2;
7768
+ continue;
7769
+ }
7770
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
7771
+ }
7772
+ if (c === quote) {
7773
+ closed = true;
7774
+ i += 1;
7775
+ break;
7776
+ }
7777
+ out += c;
7778
+ i += 1;
7779
+ }
7780
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
7781
+ tokens.push({
7782
+ type: "string",
7783
+ value: out,
7784
+ pos: start
7785
+ });
7786
+ continue;
7787
+ }
7788
+ if (isIdentStart(ch)) {
7789
+ const start = i;
7790
+ while (i < n && isIdentPart(source[i])) i += 1;
7791
+ const text = source.slice(start, i);
7792
+ if (KEYWORDS.has(text)) tokens.push({
7793
+ type: "keyword",
7794
+ keyword: keywordOf(text),
7795
+ pos: start
7796
+ });
7797
+ else tokens.push({
7798
+ type: "identifier",
7799
+ name: text,
7800
+ pos: start
7801
+ });
7802
+ continue;
7803
+ }
7804
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
7805
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
7806
+ tokens.push({
7807
+ type: "punct",
7808
+ punct: two,
7809
+ pos: i
7810
+ });
7811
+ i += 2;
7812
+ continue;
7813
+ }
7814
+ if (isSinglePunct(ch)) {
7815
+ tokens.push({
7816
+ type: "punct",
7817
+ punct: ch,
7818
+ pos: i
7819
+ });
7820
+ i += 1;
7821
+ continue;
7822
+ }
7823
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
7824
+ }
7825
+ tokens.push({
7826
+ type: "eof",
7827
+ pos: n
7828
+ });
7829
+ return tokens;
7830
+ }
7831
+ function keywordOf(text) {
7832
+ if (text === "true") return "true";
7833
+ if (text === "false") return "false";
7834
+ return "null";
7835
+ }
7836
+ function isSinglePunct(ch) {
7837
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
7838
+ }
7839
+ /**
7840
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
7841
+ *
7842
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
7843
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
7844
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
7845
+ * string validated against the builtin table at parse time, so an unknown
7846
+ * function is rejected immediately (author feedback) and a persisted expression
7847
+ * that references a since-removed builtin degrades at read.
7848
+ *
7849
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
7850
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
7851
+ */
7852
+ /** Binary/logical operator precedence (higher binds tighter). */
7853
+ var BINARY_PRECEDENCE = {
7854
+ "||": 1,
7855
+ "&&": 2,
7856
+ "==": 3,
7857
+ "!=": 3,
7858
+ "<": 4,
7859
+ "<=": 4,
7860
+ ">": 4,
7861
+ ">=": 4,
7862
+ "+": 5,
7863
+ "-": 5,
7864
+ "*": 6,
7865
+ "/": 6,
7866
+ "%": 6
7867
+ };
7868
+ function isLogicalOp(op) {
7869
+ return op === "&&" || op === "||";
7870
+ }
7871
+ function isBinaryOp(op) {
7872
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
7873
+ }
7874
+ var Parser = class {
7875
+ tokens;
7876
+ pos = 0;
7877
+ nodeCount = 0;
7878
+ identifiers = /* @__PURE__ */ new Set();
7879
+ callees = /* @__PURE__ */ new Set();
7880
+ constructor(tokens) {
7881
+ this.tokens = tokens;
7882
+ }
7883
+ parse() {
7884
+ const ast = this.parseTernary();
7885
+ const tok = this.peek();
7886
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
7887
+ return {
7888
+ ast,
7889
+ identifiers: this.identifiers,
7890
+ callees: this.callees,
7891
+ nodeCount: this.nodeCount
7892
+ };
7893
+ }
7894
+ peek() {
7895
+ return this.tokens[this.pos];
7896
+ }
7897
+ next() {
7898
+ return this.tokens[this.pos++];
7899
+ }
7900
+ /** Consume a punctuator token, erroring if the next token isn't it. */
7901
+ expectPunct(punct) {
7902
+ const tok = this.peek();
7903
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
7904
+ this.pos += 1;
7905
+ }
7906
+ matchPunct(punct) {
7907
+ const tok = this.peek();
7908
+ if (tok.type === "punct" && tok.punct === punct) {
7909
+ this.pos += 1;
7910
+ return true;
7911
+ }
7912
+ return false;
7913
+ }
7914
+ countNode() {
7915
+ this.nodeCount += 1;
7916
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
7917
+ }
7918
+ parseTernary() {
7919
+ const test = this.parseBinary(1);
7920
+ if (this.matchPunct("?")) {
7921
+ const consequent = this.parseTernary();
7922
+ this.expectPunct(":");
7923
+ const alternate = this.parseTernary();
7924
+ this.countNode();
7925
+ return {
7926
+ kind: "conditional",
7927
+ test,
7928
+ consequent,
7929
+ alternate
7930
+ };
7931
+ }
7932
+ return test;
7933
+ }
7934
+ parseBinary(minPrec) {
7935
+ let left = this.parseUnary();
7936
+ for (;;) {
7937
+ const tok = this.peek();
7938
+ if (tok.type !== "punct") break;
7939
+ const prec = BINARY_PRECEDENCE[tok.punct];
7940
+ if (prec === void 0 || prec < minPrec) break;
7941
+ const op = tok.punct;
7942
+ this.pos += 1;
7943
+ const right = this.parseBinary(prec + 1);
7944
+ this.countNode();
7945
+ if (isLogicalOp(op)) left = {
7946
+ kind: "logical",
7947
+ op,
7948
+ left,
7949
+ right
7950
+ };
7951
+ else if (isBinaryOp(op)) left = {
7952
+ kind: "binary",
7953
+ op,
7954
+ left,
7955
+ right
7956
+ };
7957
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
7958
+ }
7959
+ return left;
7960
+ }
7961
+ parseUnary() {
7962
+ const tok = this.peek();
7963
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
7964
+ const op = tok.punct;
7965
+ this.pos += 1;
7966
+ const operand = this.parseUnary();
7967
+ this.countNode();
7968
+ return {
7969
+ kind: "unary",
7970
+ op,
7971
+ operand
7972
+ };
7973
+ }
7974
+ return this.parsePrimary();
7975
+ }
7976
+ parsePrimary() {
7977
+ const tok = this.next();
7978
+ switch (tok.type) {
7979
+ case "number":
7980
+ this.countNode();
7981
+ return {
7982
+ kind: "literal",
7983
+ value: tok.value
7984
+ };
7985
+ case "string":
7986
+ this.countNode();
7987
+ return {
7988
+ kind: "literal",
7989
+ value: tok.value
7990
+ };
7991
+ case "keyword":
7992
+ this.countNode();
7993
+ return {
7994
+ kind: "literal",
7995
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
7996
+ };
7997
+ case "identifier": {
7998
+ const nextTok = this.peek();
7999
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
8000
+ this.identifiers.add(tok.name);
8001
+ this.countNode();
8002
+ return {
8003
+ kind: "identifier",
8004
+ name: tok.name
8005
+ };
8006
+ }
8007
+ case "punct":
8008
+ if (tok.punct === "(") {
8009
+ const inner = this.parseTernary();
8010
+ this.expectPunct(")");
8011
+ return inner;
8012
+ }
8013
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
8014
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
8015
+ }
8016
+ }
8017
+ parseCall(callee, pos) {
8018
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
8019
+ this.expectPunct("(");
8020
+ const args = [];
8021
+ if (!this.matchPunct(")")) for (;;) {
8022
+ args.push(this.parseTernary());
8023
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
8024
+ if (this.matchPunct(",")) continue;
8025
+ this.expectPunct(")");
8026
+ break;
8027
+ }
8028
+ this.callees.add(callee);
8029
+ this.countNode();
8030
+ return {
8031
+ kind: "call",
8032
+ callee,
8033
+ args
8034
+ };
8035
+ }
8036
+ };
8037
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
8038
+ * `ExpressionParseError` on any lexical or grammatical failure. */
8039
+ function parseExpression(source) {
8040
+ return new Parser(tokenize(source)).parse();
8041
+ }
8042
+ /**
8043
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
8044
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
8045
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
8046
+ * one per read on a hot resolve path.
8047
+ *
8048
+ * The cache is a module-level singleton: entries are pure, content-addressed
8049
+ * ASTs keyed by the raw source string, so sharing one instance across all
8050
+ * callers is safe and maximises hit rate.
8051
+ */
8052
+ var cache = /* @__PURE__ */ new Map();
8053
+ function getCached(source) {
8054
+ const hit = cache.get(source);
8055
+ if (hit !== void 0) {
8056
+ cache.delete(source);
8057
+ cache.set(source, hit);
8058
+ return hit;
8059
+ }
8060
+ let result;
8061
+ try {
8062
+ result = {
8063
+ ok: true,
8064
+ parsed: parseExpression(source)
8065
+ };
8066
+ } catch (err) {
8067
+ result = {
8068
+ ok: false,
8069
+ error: err instanceof ExpressionParseError ? err.message : String(err)
8070
+ };
8071
+ }
8072
+ cache.set(source, result);
8073
+ if (cache.size > 256) {
8074
+ const oldest = cache.keys().next().value;
8075
+ if (oldest !== void 0) cache.delete(oldest);
8076
+ }
8077
+ return result;
8078
+ }
8079
+ /** Compile `source`, returning a discriminated result instead of throwing.
8080
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
8081
+ function compileExpressionSafe(source) {
8082
+ return getCached(source);
8083
+ }
8084
+ Object.freeze({});
8085
+ /**
8086
+ * Author-time validation. Returns `null` when the source is valid, else a
8087
+ * human-readable error message. Checks: the expression compiles; binding count
8088
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
8089
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
8090
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
8091
+ */
8092
+ function validateExpressionSource(src) {
8093
+ const names = Object.keys(src.bindings);
8094
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
8095
+ for (const name of names) {
8096
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
8097
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
8098
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
8099
+ }
8100
+ const compiled = compileExpressionSafe(src.expr);
8101
+ if (!compiled.ok) return compiled.error;
8102
+ const bound = new Set(names);
8103
+ for (const id of compiled.parsed.identifiers) {
8104
+ if (id === "now") continue;
8105
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
8106
+ }
8107
+ return null;
8108
+ }
8109
+ /** A composed device's stableId is this prefix plus the block id. */
8110
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
8111
+ /** Longest snippet a `code` source may carry (slice 4). */
8112
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
8113
+ var CompositionSourceRefSchema = object({
8114
+ /** The owning addon; `stableId` is unique only within it. */
8115
+ addonId: string().min(1),
8116
+ stableId: string().min(1)
8117
+ });
8118
+ /** One field of one capability of one source device. */
8119
+ var CompositionFieldReadSchema = object({
8120
+ source: CompositionSourceRefSchema,
8121
+ cap: string().min(1),
8122
+ /** Dotted path into the source cap's runtime-state slice. */
8123
+ fieldPath: string().min(1)
8124
+ });
8125
+ /** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
8126
+ var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: literal("from") });
8127
+ var CompositionBindingSchema = discriminatedUnion("kind", [CompositionFromSourceSchema, object({
8128
+ kind: literal("literal"),
8129
+ value: union([
8130
+ string(),
8131
+ number(),
8132
+ boolean(),
8133
+ _null()
8134
+ ])
8135
+ })]);
8136
+ /**
8137
+ * `expression`: a formula over named bindings, in the salvaged expression
8138
+ * engine. Validated at parse by the SAME function every other consumer runs,
8139
+ * so the editor and the store cannot disagree.
8140
+ */
8141
+ var CompositionExpressionSourceSchema = object({
8142
+ kind: literal("expression"),
8143
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
8144
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
8145
+ }).superRefine((src, ctx) => {
8146
+ const err = validateExpressionSource(src);
8147
+ if (err !== null) ctx.addIssue({
8148
+ code: "custom",
8149
+ message: err,
8150
+ path: ["expr"]
8151
+ });
8152
+ });
8153
+ /** `code`: RESERVED for slice 4 (own runner, one-way eject). */
8154
+ var CompositionCodeSourceSchema = object({
8155
+ kind: literal("code"),
8156
+ code: string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
8157
+ });
8158
+ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
8159
+ CompositionFromSourceSchema,
8160
+ CompositionExpressionSourceSchema,
8161
+ CompositionCodeSourceSchema
8162
+ ]);
8163
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
8164
+ kind: literal("forward"),
8165
+ source: CompositionSourceRefSchema,
8166
+ cap: string().min(1),
8167
+ method: string().min(1)
8168
+ }), CompositionCodeSourceSchema]);
8169
+ /** An item key inside an item-array cap (`consumables.items[<key>]`). */
8170
+ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
8171
+ /**
8172
+ * One item of an item-array cap (D663), addressed through the cap's
8173
+ * `status.itemArray` descriptor. `fields` are paths INSIDE the item, nested
8174
+ * allowed (`remaining.value`); the key and label come from the entry itself.
8175
+ */
8176
+ var CompositionItemEntrySchema = object({
8177
+ label: string().min(1).max(80),
8178
+ fields: record(string().min(1), CompositionFieldSourceSchema)
8179
+ });
8180
+ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8181
+ kind: literal("fields"),
8182
+ cap: string().min(1),
8183
+ enabled: boolean().optional(),
8184
+ fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
8185
+ /**
8186
+ * Items of the cap's item array, keyed by item key (D663). Naming `items`
8187
+ * owns the WHOLE array field: the composed value holds exactly these items.
8188
+ */
8189
+ items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
8190
+ /** RESERVED for slice 3; refused by the validator when non-empty. */
8191
+ commands: record(string().min(1), CompositionCommandTargetSchema).optional()
8192
+ }), object({
8193
+ kind: literal("passthrough"),
8194
+ cap: string().min(1),
8195
+ enabled: boolean().optional(),
8196
+ source: CompositionSourceRefSchema
8197
+ })]);
8198
+ var CompositionSchema = object({
8199
+ target: discriminatedUnion("kind", [object({
8200
+ kind: literal("new"),
8201
+ type: _enum(DeviceType),
8202
+ role: _enum(DeviceRole).optional()
8203
+ }), object({
8204
+ kind: literal("existing"),
8205
+ device: CompositionSourceRefSchema
8206
+ })]),
8207
+ features: array(CompositionFeatureSchema).min(1).max(16)
8208
+ }).superRefine((composition, ctx) => {
8209
+ const seen = /* @__PURE__ */ new Set();
8210
+ composition.features.forEach((feature, index) => {
8211
+ if (seen.has(feature.cap)) ctx.addIssue({
8212
+ code: "custom",
8213
+ message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
8214
+ path: [
8215
+ "features",
8216
+ index,
8217
+ "cap"
8218
+ ]
8219
+ });
8220
+ seen.add(feature.cap);
8221
+ });
8222
+ });
7404
8223
  new Set([
7405
8224
  "track",
7406
8225
  "summary",
@@ -9161,1243 +9980,627 @@ var StorageDrainProgressSchema = object({
9161
9980
  * - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
9162
9981
  * (transitionally, the `<type>:default`-slugged row when several exist)
9163
9982
  * - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
9164
- *
9165
- * The orchestrator's `resolveRef(ref)` handles both cases.
9166
- */
9167
- var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
9168
- /**
9169
- * `StorageLocationDeclaration` — a single storage-location entry declared by
9170
- * an addon in its `package.json` under `camstack.storageLocations`.
9171
- *
9172
- * Design intent:
9173
- * - **Addon declares its needs** — each addon describes the logical storage
9174
- * slots it requires (e.g. `recordings`, `recordingsLow`) without caring
9175
- * about the physical path.
9176
- * - **Kernel aggregates** — at boot the kernel collects declarations from all
9177
- * installed addons, deduplicates by `id`, and exposes the union via the
9178
- * storage-locations settings surface.
9179
- * - **Orchestrator seeds** — for every declared `id` the orchestrator ensures
9180
- * at least one instance named `<id>:default` is present, using
9181
- * `defaultsTo` to inherit the resolved root from another location when the
9182
- * declaration is a derivative slot (e.g. `recordingsLow` defaults to
9183
- * `recordings`).
9184
- * - **ids are global** — `id` values are shared across the entire deployment;
9185
- * two addons declaring the same `id` must agree on `cardinality` (validated
9186
- * at kernel aggregation time, not here).
9187
- */
9188
- /**
9189
- * `StorageAccess` — how the service that DECLARED a storage-location kind
9190
- * actually reaches the bytes. It is the constraint that decides which
9191
- * `storage-provider`s may back a location of that kind.
9192
- *
9193
- * - `'local-path'` — the service asks `storage.resolve` for a path string and
9194
- * then does its own `node:fs` I/O on it (the recorder's segment writer, the
9195
- * post-analysis media roots). Only a provider that serves a genuine local
9196
- * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
9197
- * remote provider's `resolve` returns a path on the REMOTE host, and
9198
- * `fs.readdir` of it on this node either fails or — far worse — succeeds
9199
- * against a same-named local directory that is something else entirely.
9200
- *
9201
- * - `'cap-mediated'` — every byte travels through the `storage` cap
9202
- * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
9203
- * service never sees a path, so any provider can back it. `backups` is the
9204
- * one kind that qualifies today.
9205
- *
9206
- * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
9207
- * an EMERGENT property of how the recorder happened to be written. Nothing
9208
- * refused the configuration; the first write simply went somewhere wrong, and
9209
- * a recording write that goes wrong surfaces as a silent black window rather
9210
- * than an error (the read path does not `stat`). This turns that accident into
9211
- * a declared, enforced, testable refusal.
9212
- */
9213
- var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
9214
- var StorageLocationDeclarationSchema = object({
9215
- /**
9216
- * Global location identifier, e.g. `recordings` or `recordingsLow`.
9217
- * Must start with a lowercase letter and may contain letters, digits, and
9218
- * hyphens.
9219
- */
9220
- id: string().regex(/^[a-z][a-zA-Z0-9-]*$/, { message: "id must start with a lowercase letter and contain only letters, digits, or hyphens" }),
9221
- /** Human-readable name shown in the admin UI. */
9222
- displayName: string().min(1, { message: "displayName must not be empty" }),
9223
- /** Optional longer explanation of what data this location stores. */
9224
- description: string().optional(),
9225
- /**
9226
- * `single` — exactly one instance of this location is allowed system-wide
9227
- * (e.g. `logs`, `models`). The operator can edit it but not add more.
9228
- * `multi` — the operator may register several instances (e.g. a second
9229
- * `recordings` on a NAS for disk tiering); one is the default at any time.
9230
- */
9231
- cardinality: _enum(["single", "multi"]),
9232
- /**
9233
- * HOW the declaring service reaches the bytes — and therefore WHICH
9234
- * providers may back a location of this kind. See {@link StorageAccessSchema}
9235
- * and {@link STORAGE_ACCESS_FALLBACK}.
9236
- *
9237
- * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
9238
- * can only over-restrict (refuse a remote provider for a kind that might
9239
- * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
9240
- * permissive direction and is therefore never inferred — a repo guard
9241
- * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
9242
- * reached by omission.
9243
- */
9244
- access: StorageAccessSchema.optional(),
9245
- /**
9246
- * When set, the default instance for this location inherits its resolved
9247
- * root from the named location's default instance. Useful for derivative
9248
- * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
9249
- * configure the primary location.
9250
- */
9251
- defaultsTo: string().optional(),
9252
- /**
9253
- * Which node root the seeded `<id>:default` instance is placed under on a
9254
- * FRESH install:
9255
- * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
9256
- * the appData volume. Right for small/durable data (logs, models).
9257
- * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
9258
- * env is set, else falls back to the data root. Right for bulky, hot media
9259
- * (recordings, event media) that should stay off the appData disk.
9260
- * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
9261
- * `/backups` in the image) so archives live on their own mount rather than
9262
- * filling the appData disk. Falls back to the data root when unset.
9263
- *
9264
- * Only affects the seeded default's `basePath`; operators can repoint any
9265
- * location afterwards, and a `defaultsTo` slot inherits its parent's root
9266
- * regardless of this field. Absent (the common case) is treated as `'data'`.
9267
- */
9268
- defaultRoot: _enum([
9269
- "data",
9270
- "media",
9271
- "backup"
9272
- ]).optional()
9273
- });
9274
- var DecoderStatsSchema = object({
9275
- inputFps: number(),
9276
- outputFps: number(),
9277
- avgDecodeTimeMs: number(),
9278
- droppedFrames: number(),
9279
- /**
9280
- * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
9281
- * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
9282
- * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
9283
- * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
9284
- */
9285
- lagMs: number().optional(),
9286
- effectiveFps: number().optional(),
9287
- adaptiveFps: number().optional()
9288
- });
9289
- var DecoderSessionConfigSchema = object({
9290
- codec: string(),
9291
- maxFps: number().default(0),
9292
- outputFormat: _enum([
9293
- "jpeg",
9294
- "rgb",
9295
- "bgr",
9296
- "yuv420",
9297
- "gray"
9298
- ]).default("jpeg"),
9299
- scale: number().default(1),
9300
- width: number().optional(),
9301
- height: number().optional(),
9302
- /**
9303
- * Identifier of the camera this decoder session serves. Optional
9304
- * because the cap is generic (any caller could request decode), but
9305
- * stream-broker passes it so decoder logs include `deviceId` for
9306
- * per-camera filtering when diagnosing failures (e.g. node-av
9307
- * sendPacket errors on a single hung camera).
9308
- */
9309
- deviceId: number().int().nonnegative().optional(),
9310
- /**
9311
- * Free-form tag for log scoping. Stream-broker uses
9312
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
9313
- * on every line so `grep tag=broker:5/high` filters one camera
9314
- * profile cleanly.
9315
- */
9316
- tag: string().optional(),
9317
- /**
9318
- * Where the session delivers decoded frames (Phase 5 / D9):
9319
- *
9320
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
9321
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
9322
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
9323
- * into an OS shared-memory ring and drained as zero-pixel
9324
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
9325
- * other — `pullFrames` returns nothing for an `'shm'` session and
9326
- * `pullHandles` returns nothing for a `'callback'` session.
9327
- */
9328
- frameSink: _enum(["callback", "shm"]).default("callback"),
9329
- /**
9330
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
9331
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
9332
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
9333
- * stream-broker's `streamingDebug` gate — off by default so production logs
9334
- * stay quiet and the emit path pays zero per-frame cost when disabled.
9335
- */
9336
- debug: boolean().optional()
9337
- });
9338
- /**
9339
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
9340
- * an addon declares its channels in.
9341
- *
9342
- * ## Two axes, deliberately separated
9343
- *
9344
- * - **DECLARATION** — which channels exist. Only the addon knows:
9345
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
9346
- * baichuan/handshake. A hand-wired central list rots at the first addition,
9347
- * and rots silently. So a channel is declared where it is consulted, and the
9348
- * `log-channels` capability enumerates the declarations.
9349
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
9350
- * thing: the logging settings document on the `system` cap. Two authorities
9351
- * over the values is the exact defect
9352
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
9353
- * remove; re-introducing it from the cure side would be grotesque.
9354
- *
9355
- * Nothing in this file reads a clock, an env var or a store. The registry is
9356
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
9357
- * the hot path with a value somebody actually read, and by
9358
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
9359
- * never reaches here, so it can neither disarm an armed channel nor arm a
9360
- * disarmed one (D49).
9361
- *
9362
- * ## The canonical call shape
9363
- *
9364
- * ```ts
9365
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
9366
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
9367
- * }
9368
- * ```
9369
- *
9370
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
9371
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
9372
- * object literal is never constructed because it lives inside the branch. It
9373
- * is the same shape already proven in production at `stream-broker.ts:1650`,
9374
- * and the same discipline `LoggingGate.allowsDestination` uses for the
9375
- * destination floor (measured at 1.93 ns/call when off).
9376
- *
9377
- * ## Why a channel emits at `info`
9378
- *
9379
- * `loki-logging.addon.ts` pins the destination default at `info` and
9380
- * `loki-destination.ts` drops everything below it, so a line emitted at
9381
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
9382
- * minutes. A diagnostic that cannot be read an hour later is worse than no
9383
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
9384
- * emits at the channel's declared level, whose schema floor is `info`.
9385
- */
9386
- /**
9387
- * The level a channel writes at once armed.
9388
- *
9389
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
9390
- * not leave the process for Loki, and the whole point of arming a channel is
9391
- * to read it later.
9392
- */
9393
- var LogChannelLevelSchema = _enum([
9394
- "info",
9395
- "warn",
9396
- "error"
9397
- ]);
9398
- /**
9399
- * What an addon declares about one channel. No value, no state — a
9400
- * declaration is inert.
9401
- */
9402
- var LogChannelDescriptorSchema = object({
9403
- /**
9404
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
9405
- * the addon's short name so an operator reading a channel list can tell who
9406
- * owns it without a second lookup.
9407
- */
9408
- name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
9409
- /** One sentence: what the operator will SEE after arming it. */
9410
- description: string().min(1),
9411
- /** The level its lines are emitted at. Never below `info`. */
9412
- defaultLevel: LogChannelLevelSchema,
9413
- /**
9414
- * Whether this channel can be narrowed to a camera.
9415
- *
9416
- * `true` is a PROMISE with two halves, and both must hold: the gate is
9417
- * consulted with the numeric device id, AND every line the channel admits
9418
- * carries `tags: { deviceId }` with that same numeric id. The second half is
9419
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
9420
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
9421
- * the body is the only way to filter.
9422
- *
9423
- * A channel whose lines carry the device only in `meta` (or not at all) is
9424
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
9425
- * the operator narrows to one camera, sees nothing, and concludes the code
9426
- * path was never taken.
9427
- */
9428
- perDevice: boolean()
9429
- });
9430
- /**
9431
- * An armed window over one channel, as the document hands it to a mirror.
9432
- *
9433
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
9434
- * expires by itself, which is the one failure a boolean cannot avoid.
9435
- */
9436
- var LogChannelWindowSchema = object({
9437
- channel: string().min(1),
9438
- /** Epoch ms the window closes at. */
9439
- armedUntilMs: number(),
9440
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
9441
- deviceIds: array(number().int()).readonly().nullable()
9442
- });
9443
- var ClipModelMetaSchema = object({
9444
- /** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
9445
- textModelId: string().min(1),
9446
- /** Output dimension of BOTH towers. A vector of any other length is refused. */
9447
- embeddingDim: number().int().positive(),
9448
- /**
9449
- * The `vector-store` index this model's vectors live in. ONE feature space
9450
- * per index: two models share an index only when their vectors are ranked
9451
- * against each other, which for CLIP means never across families. MobileCLIP
9452
- * S1 and S2 share `object-clip` for history (separated by the row's
9453
- * `modelId`); any other model gets its own.
9454
- */
9455
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
9456
- /**
9457
- * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
9458
- * must be unique per tokenizer: every model's siblings land in ONE flat
9459
- * models directory, so two different tokenizers under the same name would
9460
- * silently overwrite each other (whichever model downloaded last wins, and
9461
- * the other one tokenises with the wrong vocabulary).
9462
- */
9463
- tokenizerFile: string().min(1),
9464
- /** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
9465
- contextLength: number().int().positive(),
9466
- /** Id the tokenizer pads with. */
9467
- padId: number().int().nonnegative(),
9468
- /**
9469
- * The search cosine floor at `strictness: 'loose'` — calibrated per model,
9470
- * because cosine bands are a property of the model: 0.2 keeps 96% of matched
9471
- * COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
9472
- */
9473
- searchMinScore: number().min(0).max(1)
9474
- });
9475
- var ClipSearchStrictnessSchema = _enum([
9476
- "loose",
9477
- "balanced",
9478
- "strict"
9479
- ]);
9480
- var MODEL_FORMATS = [
9481
- "onnx",
9482
- "coreml",
9483
- "openvino",
9484
- "tflite",
9485
- "pt",
9486
- "gguf"
9487
- ];
9488
- /**
9489
- * Multi-file format payload.
9490
- *
9491
- * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
9492
- * relative to the directory root — the downloader fetches each from
9493
- * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
9494
- * HuggingFace API (slower).
9495
- * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
9496
- * files fetched from the SAME remote directory as `url` and stored flat
9497
- * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
9498
- * weights next to `camstack-yolov9t.xml`.
9499
- */
9500
- var ModelFormatEntrySchema = object({
9501
- url: string(),
9502
- sizeMB: number(),
9503
- /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
9504
- isDirectory: boolean().optional(),
9505
- /** Multi-file payload (directory members or sibling files). */
9506
- files: array(string()).readonly().optional(),
9507
- /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
9508
- runtimes: array(_enum(["python"])).readonly().optional()
9509
- });
9510
- /**
9511
- * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
9512
- * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
9513
- */
9514
- var ModelExtraFileSchema = object({
9515
- url: string(),
9516
- filename: string(),
9517
- sizeMB: number()
9518
- });
9519
- /**
9520
- * Per-format payload map. Modelled as an explicit object (one optional key
9521
- * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
9522
- * record requires every key, but a catalog entry only ships a subset of
9523
- * formats.
9524
- */
9525
- var ModelFormatsSchema = object({
9526
- onnx: ModelFormatEntrySchema.optional(),
9527
- coreml: ModelFormatEntrySchema.optional(),
9528
- openvino: ModelFormatEntrySchema.optional(),
9529
- tflite: ModelFormatEntrySchema.optional(),
9530
- pt: ModelFormatEntrySchema.optional(),
9531
- gguf: ModelFormatEntrySchema.optional()
9532
- });
9533
- /**
9534
- * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
9535
- * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
9536
- * grouped Family→Tier→Variant picker renders identically in the config UI and
9537
- * in the pipeline/device steppers. The flat `id` stays the source of truth for
9538
- * resolution/download/persistence; this is a presentation overlay resolved back
9539
- * to an `id`.
9540
- */
9541
- var ModelVariantGroupSchema = object({
9542
- /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
9543
- family: string(),
9544
- /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
9545
- tier: string(),
9546
- /** Quantization axis. Omit ⇒ the fp32 base build. */
9547
- precision: _enum(["fp32", "int8"]).optional(),
9548
- /**
9549
- * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
9550
- * latency-optimized export (e.g. ReLU-activation variant) — the slot the
9551
- * future performance variants plug into.
9552
- */
9553
- optimization: _enum(["standard", "fast"]).optional(),
9554
- /**
9555
- * Input-resolution axis (square input side, px). Omit ⇒ the family's native
9556
- * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
9557
- * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
9558
- * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
9559
- * the group so the selector can offer it as a variant axis.
9560
- */
9561
- resolution: number().int().positive().optional()
9562
- });
9563
- var ModelProviderIdSchema = _enum([
9564
- "camstack",
9565
- "frigate",
9566
- "scrypted",
9567
- "custom"
9568
- ]);
9569
- /**
9570
- * The licence of a model, as a record rather than a bare SPDX string: a
9571
- * permissive CODE licence routinely sits on top of restrictive WEIGHTS or
9572
- * DATA, and the obligations (attribution, modification notices, source offers)
9573
- * are per upstream. A persisted custom-model row written when this field was a
9574
- * string still parses — the string is lifted into `{ weights }`.
9575
- *
9576
- * Every licence surface (the admin "Models and licenses" page,
9577
- * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
9578
- * these records by `scripts/gen-model-licenses.ts` (D661).
9579
- */
9580
- var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
9581
- /** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
9582
- weights: string().min(1),
9583
- /** SPDX id of the upstream code that defines / trained the network. */
9584
- code: string().min(1).optional(),
9585
- /** Training data whose terms add obligations (attribution, non-commercial). */
9586
- data: object({
9587
- name: string().min(1),
9588
- terms: string().min(1),
9589
- url: string().url().optional()
9590
- }).optional(),
9591
- /** The project the weights come from. Required on built-in entries (guard). */
9592
- upstream: object({
9593
- name: string().min(1),
9594
- url: string().url()
9595
- }).optional(),
9596
- /** Where the licence text governing the weights is published. */
9597
- url: string().url().optional(),
9598
- /** Attribution the licence REQUIRES, verbatim. */
9599
- attribution: string().min(1).optional(),
9600
- /** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
9601
- modifications: string().min(1).optional(),
9602
- /** Who serves the file a node downloads. */
9603
- hosting: _enum([
9604
- "camstack-hf",
9605
- "third-party",
9606
- "built-in"
9607
- ]).optional(),
9608
- /** True when the weights or their data forbid commercial use. */
9609
- nonCommercial: boolean().optional()
9610
- }));
9611
- var ModelCatalogEntrySchema = object({
9612
- id: string(),
9613
- name: string(),
9614
- description: string(),
9615
- formats: ModelFormatsSchema,
9616
- inputSize: object({
9617
- width: number(),
9618
- height: number()
9619
- }),
9620
- /**
9621
- * Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
9622
- * every detector / classifier / embedder. Set to 1 for a grayscale CTC text
9623
- * recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
9624
- * feeds a single-channel, EasyOCR-normalized tensor instead of the default
9625
- * 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
9626
- * Python inference pool.
9627
- */
9628
- inputChannels: number().int().positive().optional(),
9629
- labels: array(LabelDefinitionSchema).readonly(),
9630
- inputLayout: _enum(["nchw", "nhwc"]).optional(),
9631
- /**
9632
- * `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
9633
- * (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
9634
- * plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
9635
- * `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
9636
- */
9637
- inputNormalization: _enum([
9638
- "zero-one",
9639
- "imagenet",
9640
- "none",
9641
- "scrfd"
9642
- ]).optional(),
9643
- /**
9644
- * The model already applies softmax IN-GRAPH — its raw output is a
9645
- * probability distribution, not logits. When set, the `softmax`
9646
- * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
9647
- * probability vector collapses it toward uniform (top-1 score craters far
9648
- * below its true value, making every confidence gate meaningless). Absent ⇒
9649
- * the output is raw logits and the postprocessor applies softmax (the normal
9650
- * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
9651
- * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
9652
- */
9653
- outputProbabilities: boolean().optional(),
9654
- preprocessMode: _enum(["letterbox", "resize"]).optional(),
9655
- /**
9656
- * Per-MODEL postprocessor override. Absent ⇒ the step's own
9657
- * `StepDefinition.postprocessor` applies (the normal case — every model in a
9658
- * step shares its decode). Set it when a step hosts models with DIFFERENT raw
9659
- * output layouts under one slot: e.g. object-detection is `'yolo'` by default,
9660
- * but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
9661
- * 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
9662
- * by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
9663
- */
9664
- postprocessor: custom().optional(),
9983
+ *
9984
+ * The orchestrator's `resolveRef(ref)` handles both cases.
9985
+ */
9986
+ var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
9987
+ /**
9988
+ * `StorageLocationDeclaration` — a single storage-location entry declared by
9989
+ * an addon in its `package.json` under `camstack.storageLocations`.
9990
+ *
9991
+ * Design intent:
9992
+ * - **Addon declares its needs** — each addon describes the logical storage
9993
+ * slots it requires (e.g. `recordings`, `recordingsLow`) without caring
9994
+ * about the physical path.
9995
+ * - **Kernel aggregates** — at boot the kernel collects declarations from all
9996
+ * installed addons, deduplicates by `id`, and exposes the union via the
9997
+ * storage-locations settings surface.
9998
+ * - **Orchestrator seeds** — for every declared `id` the orchestrator ensures
9999
+ * at least one instance named `<id>:default` is present, using
10000
+ * `defaultsTo` to inherit the resolved root from another location when the
10001
+ * declaration is a derivative slot (e.g. `recordingsLow` defaults to
10002
+ * `recordings`).
10003
+ * - **ids are global** — `id` values are shared across the entire deployment;
10004
+ * two addons declaring the same `id` must agree on `cardinality` (validated
10005
+ * at kernel aggregation time, not here).
10006
+ */
10007
+ /**
10008
+ * `StorageAccess` — how the service that DECLARED a storage-location kind
10009
+ * actually reaches the bytes. It is the constraint that decides which
10010
+ * `storage-provider`s may back a location of that kind.
10011
+ *
10012
+ * - `'local-path'` — the service asks `storage.resolve` for a path string and
10013
+ * then does its own `node:fs` I/O on it (the recorder's segment writer, the
10014
+ * post-analysis media roots). Only a provider that serves a genuine local
10015
+ * filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
10016
+ * remote provider's `resolve` returns a path on the REMOTE host, and
10017
+ * `fs.readdir` of it on this node either fails or — far worse — succeeds
10018
+ * against a same-named local directory that is something else entirely.
10019
+ *
10020
+ * - `'cap-mediated'` — every byte travels through the `storage` cap
10021
+ * (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
10022
+ * service never sees a path, so any provider can back it. `backups` is the
10023
+ * one kind that qualifies today.
10024
+ *
10025
+ * Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
10026
+ * an EMERGENT property of how the recorder happened to be written. Nothing
10027
+ * refused the configuration; the first write simply went somewhere wrong, and
10028
+ * a recording write that goes wrong surfaces as a silent black window rather
10029
+ * than an error (the read path does not `stat`). This turns that accident into
10030
+ * a declared, enforced, testable refusal.
10031
+ */
10032
+ var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
10033
+ var StorageLocationDeclarationSchema = object({
9665
10034
  /**
9666
- * Per-MODEL default confidence floor. Absent ⇒ the step's
9667
- * `StepDefinition.defaultConfidence` applies. A score is a property of one
9668
- * model's output scale, not of the step slot it sits in: YuNet's
9669
- * `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
9670
- * face gallery with wheels and hands (D662, amends D645). An operator's
9671
- * explicit value still wins over it. Resolved in ONE place —
9672
- * `addon-pipeline/.../registry/effective-confidence.ts`.
10035
+ * Global location identifier, e.g. `recordings` or `recordingsLow`.
10036
+ * Must start with a lowercase letter and may contain letters, digits, and
10037
+ * hyphens.
9673
10038
  */
9674
- defaultConfidence: number().min(0).max(1).optional(),
10039
+ id: string().regex(/^[a-z][a-zA-Z0-9-]*$/, { message: "id must start with a lowercase letter and contain only letters, digits, or hyphens" }),
10040
+ /** Human-readable name shown in the admin UI. */
10041
+ displayName: string().min(1, { message: "displayName must not be empty" }),
10042
+ /** Optional longer explanation of what data this location stores. */
10043
+ description: string().optional(),
9675
10044
  /**
9676
- * When true, the executor produces a landmark-aligned crop (similarity warp
9677
- * onto the canonical template) before this step runs, instead of a plain
9678
- * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
9679
- * their embeddings are only discriminative on an aligned input. The face
9680
- * detector that produced the parent detail must emit 5 landmarks.
10045
+ * `single` — exactly one instance of this location is allowed system-wide
10046
+ * (e.g. `logs`, `models`). The operator can edit it but not add more.
10047
+ * `multi` — the operator may register several instances (e.g. a second
10048
+ * `recordings` on a NAS for disk tiering); one is the default at any time.
9681
10049
  */
9682
- faceAlignment: boolean().optional(),
10050
+ cardinality: _enum(["single", "multi"]),
9683
10051
  /**
9684
- * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
9685
- * Downloaded into the same modelsDir alongside the model file.
10052
+ * HOW the declaring service reaches the bytes — and therefore WHICH
10053
+ * providers may back a location of this kind. See {@link StorageAccessSchema}
10054
+ * and {@link STORAGE_ACCESS_FALLBACK}.
10055
+ *
10056
+ * Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
10057
+ * can only over-restrict (refuse a remote provider for a kind that might
10058
+ * have coped) and never under-restrict. Declaring `'cap-mediated'` is the
10059
+ * permissive direction and is therefore never inferred — a repo guard
10060
+ * (`scripts/check-storage-access-declarations.ts`) refuses to let it be
10061
+ * reached by omission.
9686
10062
  */
9687
- extraFiles: array(ModelExtraFileSchema).readonly().optional(),
10063
+ access: StorageAccessSchema.optional(),
9688
10064
  /**
9689
- * LEGACY entry — retained in the catalog so a persisted operator selection
9690
- * still RESOLVES (and can be re-activated), but hidden from the selectable
9691
- * model list and excluded from the auto format-default pick. Set on the
9692
- * superseded / consolidated models (older lineages, redundant fp16 IRs) so
9693
- * the active lineup stays the coherent curated ladder without deleting a
9694
- * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
9695
- * an explicit legacy id that has a build for the node's format.
10065
+ * When set, the default instance for this location inherits its resolved
10066
+ * root from the named location's default instance. Useful for derivative
10067
+ * slots (e.g. `recordingsLow` → `recordings`) so operators only need to
10068
+ * configure the primary location.
9696
10069
  */
9697
- legacy: boolean().optional(),
10070
+ defaultsTo: string().optional(),
9698
10071
  /**
9699
- * Measured quality/latency metadata — populated from the benchmark addon on
9700
- * the real node classes. Absent = not yet measured (most entries today; the
9701
- * catalog historically carried only `sizeMB`, a poor cross-architecture
9702
- * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
10072
+ * Which node root the seeded `<id>:default` instance is placed under on a
10073
+ * FRESH install:
10074
+ * - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
10075
+ * the appData volume. Right for small/durable data (logs, models).
10076
+ * - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
10077
+ * env is set, else falls back to the data root. Right for bulky, hot media
10078
+ * (recordings, event media) that should stay off the appData disk.
10079
+ * - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
10080
+ * `/backups` in the image) so archives live on their own mount rather than
10081
+ * filling the appData disk. Falls back to the data root when unset.
10082
+ *
10083
+ * Only affects the seeded default's `basePath`; operators can repoint any
10084
+ * location afterwards, and a `defaultsTo` slot inherits its parent's root
10085
+ * regardless of this field. Absent (the common case) is treated as `'data'`.
9703
10086
  */
9704
- metrics: object({
9705
- map50: number().optional(),
9706
- p95LatencyMs: record(string(), number()).optional()
9707
- }).optional(),
10087
+ defaultRoot: _enum([
10088
+ "data",
10089
+ "media",
10090
+ "backup"
10091
+ ]).optional()
10092
+ });
10093
+ var DecoderStatsSchema = object({
10094
+ inputFps: number(),
10095
+ outputFps: number(),
10096
+ avgDecodeTimeMs: number(),
10097
+ droppedFrames: number(),
9708
10098
  /**
9709
- * The model's licence record — see {@link ModelLicenseSchema}. Optional in
9710
- * the schema (a custom model's author may state none); REQUIRED and complete
9711
- * on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
10099
+ * Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
10100
+ * lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
10101
+ * the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
10102
+ * is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
9712
10103
  */
9713
- license: ModelLicenseSchema.optional(),
10104
+ lagMs: number().optional(),
10105
+ effectiveFps: number().optional(),
10106
+ adaptiveFps: number().optional()
10107
+ });
10108
+ var DecoderSessionConfigSchema = object({
10109
+ codec: string(),
10110
+ maxFps: number().default(0),
10111
+ outputFormat: _enum([
10112
+ "jpeg",
10113
+ "rgb",
10114
+ "bgr",
10115
+ "yuv420",
10116
+ "gray"
10117
+ ]).default("jpeg"),
10118
+ scale: number().default(1),
10119
+ width: number().optional(),
10120
+ height: number().optional(),
9714
10121
  /**
9715
- * Variant-selector grouping. The UI groups models by `family` + `tier` and
9716
- * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
9717
- * of a family's sizes and quantizations collapse into one grouped picker
9718
- * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
9719
- * (legacy / custom models) — never shown in the grouped selector. The flat
9720
- * `id` stays the source of truth for resolution/download/persistence; grouping
9721
- * is a presentation overlay resolved back to an `id`.
10122
+ * Identifier of the camera this decoder session serves. Optional
10123
+ * because the cap is generic (any caller could request decode), but
10124
+ * stream-broker passes it so decoder logs include `deviceId` for
10125
+ * per-camera filtering when diagnosing failures (e.g. node-av
10126
+ * sendPacket errors on a single hung camera).
9722
10127
  */
9723
- group: ModelVariantGroupSchema.optional(),
10128
+ deviceId: number().int().nonnegative().optional(),
9724
10129
  /**
9725
- * Catalog source for the pipeline stepper's provider-first picker. Absent on
9726
- * built-in CamStack entries (treated as `camstack`) and on registry rows
9727
- * persisted before this field existed (`inferModelProvider` fills those).
10130
+ * Free-form tag for log scoping. Stream-broker uses
10131
+ * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10132
+ * on every line so `grep tag=broker:5/high` filters one camera
10133
+ * profile cleanly.
9728
10134
  */
9729
- provider: ModelProviderIdSchema.optional(),
10135
+ tag: string().optional(),
9730
10136
  /**
9731
- * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9732
- * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9733
- * labels already ARE the CamStack macros (Scrypted identity map).
10137
+ * Where the session delivers decoded frames (Phase 5 / D9):
10138
+ *
10139
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
10140
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
10141
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
10142
+ * into an OS shared-memory ring and drained as zero-pixel
10143
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
10144
+ * other — `pullFrames` returns nothing for an `'shm'` session and
10145
+ * `pullHandles` returns nothing for a `'callback'` session.
9734
10146
  */
9735
- classMap: DetectionCatalogClassMapSchema.optional(),
10147
+ frameSink: _enum(["callback", "shm"]).default("callback"),
9736
10148
  /**
9737
- * The model's CLIP feature-space contract (text encoder, dimension, vector
9738
- * index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
9739
- * CLIP image encoder; its absence is what "not a CLIP model" means to the
9740
- * embedding encoder and the semantic-search store (D649).
10149
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10150
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10151
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10152
+ * stream-broker's `streamingDebug` gate — off by default so production logs
10153
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
9741
10154
  */
9742
- clip: ClipModelMetaSchema.optional()
9743
- });
9744
- var ConvertTargetSchema = discriminatedUnion("format", [object({
9745
- format: literal("openvino"),
9746
- precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
9747
- }), object({ format: literal("coreml") })]);
9748
- var ModelConvertMetadataSchema = object({
9749
- id: string().regex(/^[a-zA-Z0-9._-]+$/),
9750
- name: string(),
9751
- labels: array(LabelDefinitionSchema).readonly(),
9752
- inputSize: object({
9753
- width: number(),
9754
- height: number()
9755
- }),
9756
- inputLayout: _enum(["nchw", "nhwc"]).optional(),
9757
- inputNormalization: _enum([
9758
- "zero-one",
9759
- "imagenet",
9760
- "none",
9761
- "scrfd"
9762
- ]).optional(),
9763
- preprocessMode: _enum(["letterbox", "resize"]).optional(),
9764
- outputFormat: _enum([
9765
- "yolo",
9766
- "ssd",
9767
- "embedding",
9768
- "classification",
9769
- "ocr",
9770
- "segmentation"
9771
- ]),
9772
- faceAlignment: boolean().optional(),
9773
- classMap: DetectionCatalogClassMapSchema.optional()
9774
- });
9775
- var ConvertResultSchema = object({
9776
- entry: ModelCatalogEntrySchema,
9777
- artifacts: array(object({
9778
- format: _enum(MODEL_FORMATS),
9779
- precision: _enum(["fp16", "int8"]).optional(),
9780
- sizeMB: number(),
9781
- validated: boolean(),
9782
- files: array(string()).readonly()
9783
- })).readonly()
10155
+ debug: boolean().optional()
9784
10156
  });
9785
10157
  /**
9786
- * Error types for the safe expression engine. Two distinct classes so callers
9787
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
9788
- * failure — both are non-fatal to the host: read paths degrade to "skip link".
10158
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
10159
+ * an addon declares its channels in.
10160
+ *
10161
+ * ## Two axes, deliberately separated
10162
+ *
10163
+ * - **DECLARATION** — which channels exist. Only the addon knows:
10164
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
10165
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
10166
+ * and rots silently. So a channel is declared where it is consulted, and the
10167
+ * `log-channels` capability enumerates the declarations.
10168
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
10169
+ * thing: the logging settings document on the `system` cap. Two authorities
10170
+ * over the values is the exact defect
10171
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
10172
+ * remove; re-introducing it from the cure side would be grotesque.
10173
+ *
10174
+ * Nothing in this file reads a clock, an env var or a store. The registry is
10175
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
10176
+ * the hot path with a value somebody actually read, and by
10177
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
10178
+ * never reaches here, so it can neither disarm an armed channel nor arm a
10179
+ * disarmed one (D49).
10180
+ *
10181
+ * ## The canonical call shape
10182
+ *
10183
+ * ```ts
10184
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
10185
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
10186
+ * }
10187
+ * ```
10188
+ *
10189
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
10190
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
10191
+ * object literal is never constructed because it lives inside the branch. It
10192
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
10193
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
10194
+ * destination floor (measured at 1.93 ns/call when off).
10195
+ *
10196
+ * ## Why a channel emits at `info`
10197
+ *
10198
+ * `loki-logging.addon.ts` pins the destination default at `info` and
10199
+ * `loki-destination.ts` drops everything below it, so a line emitted at
10200
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
10201
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
10202
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
10203
+ * emits at the channel's declared level, whose schema floor is `info`.
9789
10204
  */
9790
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
9791
- * the failure is anchored to a character (author-facing inline feedback). */
9792
- var ExpressionParseError = class extends Error {
9793
- position;
9794
- constructor(message, position) {
9795
- super(message);
9796
- this.name = "ExpressionParseError";
9797
- this.position = position;
9798
- }
9799
- };
9800
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
9801
- * result, unknown builtin, step-budget exceeded). */
9802
- var ExpressionEvalError = class extends Error {
9803
- constructor(message) {
9804
- super(message);
9805
- this.name = "ExpressionEvalError";
9806
- }
9807
- };
9808
- function asFiniteNumber(value, name, index) {
9809
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
9810
- return value;
9811
- }
9812
- function asString$1(value, name, index) {
9813
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
9814
- return value;
9815
- }
9816
- function finiteResult(value, name) {
9817
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
9818
- return value;
9819
- }
9820
- function allFiniteNumbers(args, name) {
9821
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
9822
- }
9823
- function asBoolean$1(value, name, index) {
9824
- if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
9825
- return value;
9826
- }
9827
- var INF = Number.POSITIVE_INFINITY;
9828
- var table = {
9829
- min: {
9830
- minArgs: 1,
9831
- maxArgs: INF,
9832
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
9833
- },
9834
- max: {
9835
- minArgs: 1,
9836
- maxArgs: INF,
9837
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
9838
- },
9839
- abs: {
9840
- minArgs: 1,
9841
- maxArgs: 1,
9842
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
9843
- },
9844
- floor: {
9845
- minArgs: 1,
9846
- maxArgs: 1,
9847
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
9848
- },
9849
- ceil: {
9850
- minArgs: 1,
9851
- maxArgs: 1,
9852
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
9853
- },
9854
- sqrt: {
9855
- minArgs: 1,
9856
- maxArgs: 1,
9857
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
9858
- },
9859
- round: {
9860
- minArgs: 1,
9861
- maxArgs: 2,
9862
- apply: (args) => {
9863
- const x = asFiniteNumber(args[0], "round", 0);
9864
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
9865
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
9866
- const factor = 10 ** digits;
9867
- return finiteResult(Math.round(x * factor) / factor, "round");
9868
- }
9869
- },
9870
- pow: {
9871
- minArgs: 2,
9872
- maxArgs: 2,
9873
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
9874
- },
9875
- clamp: {
9876
- minArgs: 3,
9877
- maxArgs: 3,
9878
- apply: (args) => {
9879
- const x = asFiniteNumber(args[0], "clamp", 0);
9880
- const lo = asFiniteNumber(args[1], "clamp", 1);
9881
- const hi = asFiniteNumber(args[2], "clamp", 2);
9882
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
9883
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
9884
- }
9885
- },
9886
- avg: {
9887
- minArgs: 1,
9888
- maxArgs: INF,
9889
- apply: (args) => {
9890
- const nums = allFiniteNumbers(args, "avg");
9891
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
9892
- }
9893
- },
9894
- sum: {
9895
- minArgs: 1,
9896
- maxArgs: INF,
9897
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
9898
- },
9899
- coalesce: {
9900
- minArgs: 1,
9901
- maxArgs: INF,
9902
- apply: (args) => {
9903
- for (const a of args) if (a !== null) return a;
9904
- return null;
9905
- }
9906
- },
9907
- age: {
9908
- minArgs: 2,
9909
- maxArgs: 2,
9910
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
9911
- },
9912
- convert: {
9913
- minArgs: 3,
9914
- maxArgs: 3,
9915
- apply: (args, hooks) => {
9916
- const x = asFiniteNumber(args[0], "convert", 0);
9917
- const from = asString$1(args[1], "convert", 1).trim();
9918
- const to = asString$1(args[2], "convert", 2).trim();
9919
- if (hooks.convert) {
9920
- const out = hooks.convert(x, from, to);
9921
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
9922
- return finiteResult(out, "convert");
9923
- }
9924
- if (from === to) return x;
9925
- throw new ExpressionEvalError("convert: unit conversion table not installed");
9926
- }
9927
- },
9928
- any: {
9929
- minArgs: 1,
9930
- maxArgs: INF,
9931
- apply: (args) => args.map((a, i) => asBoolean$1(a, "any", i)).some((b) => b)
9932
- },
9933
- all: {
9934
- minArgs: 1,
9935
- maxArgs: INF,
9936
- apply: (args) => args.map((a, i) => asBoolean$1(a, "all", i)).every((b) => b)
9937
- },
9938
- within: {
9939
- minArgs: 2,
9940
- maxArgs: 2,
9941
- apply: (args, hooks) => {
9942
- const windowMs = asFiniteNumber(args[1], "within", 1);
9943
- if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
9944
- const at = args[0];
9945
- if (at === null) return false;
9946
- const ts = asFiniteNumber(at, "within", 0);
9947
- const now = hooks.now;
9948
- if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
9949
- const inside = now - ts <= windowMs;
9950
- if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
9951
- return inside;
9952
- }
9953
- },
9954
- latest: {
9955
- minArgs: 1,
9956
- maxArgs: INF,
9957
- apply: (args) => {
9958
- const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
9959
- return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
9960
- }
9961
- }
9962
- };
9963
- Object.freeze(Object.assign(Object.create(null), table));
9964
- /** The set of valid builtin names — used by the parser to reject unknown
9965
- * callees at parse time (immediate author feedback). */
9966
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
9967
10205
  /**
9968
- * Resource-bound constants for the safe expression engine.
10206
+ * The level a channel writes at once armed.
10207
+ *
10208
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
10209
+ * not leave the process for Loki, and the whole point of arming a channel is
10210
+ * to read it later.
10211
+ */
10212
+ var LogChannelLevelSchema = _enum([
10213
+ "info",
10214
+ "warn",
10215
+ "error"
10216
+ ]);
10217
+ /**
10218
+ * What an addon declares about one channel. No value, no state — a
10219
+ * declaration is inert.
10220
+ */
10221
+ var LogChannelDescriptorSchema = object({
10222
+ /**
10223
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
10224
+ * the addon's short name so an operator reading a channel list can tell who
10225
+ * owns it without a second lookup.
10226
+ */
10227
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
10228
+ /** One sentence: what the operator will SEE after arming it. */
10229
+ description: string().min(1),
10230
+ /** The level its lines are emitted at. Never below `info`. */
10231
+ defaultLevel: LogChannelLevelSchema,
10232
+ /**
10233
+ * Whether this channel can be narrowed to a camera.
10234
+ *
10235
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
10236
+ * consulted with the numeric device id, AND every line the channel admits
10237
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
10238
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
10239
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
10240
+ * the body is the only way to filter.
10241
+ *
10242
+ * A channel whose lines carry the device only in `meta` (or not at all) is
10243
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
10244
+ * the operator narrows to one camera, sees nothing, and concludes the code
10245
+ * path was never taken.
10246
+ */
10247
+ perDevice: boolean()
10248
+ });
10249
+ /**
10250
+ * An armed window over one channel, as the document hands it to a mirror.
9969
10251
  *
9970
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
9971
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
9972
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
9973
- * work a single author-supplied expression can request, so a hostile or
9974
- * accidental pathological string can never spend unbounded CPU/memory.
10252
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
10253
+ * expires by itself, which is the one failure a boolean cannot avoid.
9975
10254
  */
9976
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
9977
- * rejected without allocation. */
9978
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
9979
- /** A legal binding / identifier name. */
9980
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
9981
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
9982
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
9983
- var RESERVED_BINDING_NAMES = new Set([
9984
- "now",
9985
- "true",
9986
- "false",
9987
- "null"
10255
+ var LogChannelWindowSchema = object({
10256
+ channel: string().min(1),
10257
+ /** Epoch ms the window closes at. */
10258
+ armedUntilMs: number(),
10259
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
10260
+ deviceIds: array(number().int()).readonly().nullable()
10261
+ });
10262
+ var ClipModelMetaSchema = object({
10263
+ /** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
10264
+ textModelId: string().min(1),
10265
+ /** Output dimension of BOTH towers. A vector of any other length is refused. */
10266
+ embeddingDim: number().int().positive(),
10267
+ /**
10268
+ * The `vector-store` index this model's vectors live in. ONE feature space
10269
+ * per index: two models share an index only when their vectors are ranked
10270
+ * against each other, which for CLIP means never across families. MobileCLIP
10271
+ * S1 and S2 share `object-clip` for history (separated by the row's
10272
+ * `modelId`); any other model gets its own.
10273
+ */
10274
+ vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
10275
+ /**
10276
+ * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
10277
+ * must be unique per tokenizer: every model's siblings land in ONE flat
10278
+ * models directory, so two different tokenizers under the same name would
10279
+ * silently overwrite each other (whichever model downloaded last wins, and
10280
+ * the other one tokenises with the wrong vocabulary).
10281
+ */
10282
+ tokenizerFile: string().min(1),
10283
+ /** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
10284
+ contextLength: number().int().positive(),
10285
+ /** Id the tokenizer pads with. */
10286
+ padId: number().int().nonnegative(),
10287
+ /**
10288
+ * The search cosine floor at `strictness: 'loose'` — calibrated per model,
10289
+ * because cosine bands are a property of the model: 0.2 keeps 96% of matched
10290
+ * COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
10291
+ */
10292
+ searchMinScore: number().min(0).max(1)
10293
+ });
10294
+ var ClipSearchStrictnessSchema = _enum([
10295
+ "loose",
10296
+ "balanced",
10297
+ "strict"
9988
10298
  ]);
10299
+ var MODEL_FORMATS = [
10300
+ "onnx",
10301
+ "coreml",
10302
+ "openvino",
10303
+ "tflite",
10304
+ "pt",
10305
+ "gguf"
10306
+ ];
9989
10307
  /**
9990
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
9991
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
9992
- * single/double-quoted strings with a tiny escape set, identifiers, the three
9993
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
9994
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
9995
- * is a parse error with a source position, so member access / assignment /
9996
- * template literals are lexically impossible.
10308
+ * Multi-file format payload.
10309
+ *
10310
+ * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
10311
+ * relative to the directory root — the downloader fetches each from
10312
+ * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
10313
+ * HuggingFace API (slower).
10314
+ * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
10315
+ * files fetched from the SAME remote directory as `url` and stored flat
10316
+ * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
10317
+ * weights next to `camstack-yolov9t.xml`.
9997
10318
  */
9998
- var KEYWORDS = new Set([
9999
- "true",
10000
- "false",
10001
- "null"
10002
- ]);
10003
- function isDigit(ch) {
10004
- return ch >= "0" && ch <= "9";
10005
- }
10006
- function isIdentStart(ch) {
10007
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
10008
- }
10009
- function isIdentPart(ch) {
10010
- return isIdentStart(ch) || isDigit(ch);
10011
- }
10012
- function isWhitespace(ch) {
10013
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
10014
- }
10015
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
10016
- * Throws `ExpressionParseError` on any illegal character or unterminated
10017
- * string. */
10018
- function tokenize(source) {
10019
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
10020
- const tokens = [];
10021
- let i = 0;
10022
- const n = source.length;
10023
- while (i < n) {
10024
- const ch = source[i];
10025
- if (isWhitespace(ch)) {
10026
- i += 1;
10027
- continue;
10028
- }
10029
- if (isDigit(ch)) {
10030
- const start = i;
10031
- while (i < n && isDigit(source[i])) i += 1;
10032
- if (i < n && source[i] === ".") {
10033
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
10034
- i += 1;
10035
- while (i < n && isDigit(source[i])) i += 1;
10036
- }
10037
- const text = source.slice(start, i);
10038
- const value = Number(text);
10039
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
10040
- tokens.push({
10041
- type: "number",
10042
- value,
10043
- pos: start
10044
- });
10045
- continue;
10046
- }
10047
- if (ch === "'" || ch === "\"") {
10048
- const quote = ch;
10049
- const start = i;
10050
- i += 1;
10051
- let out = "";
10052
- let closed = false;
10053
- while (i < n) {
10054
- const c = source[i];
10055
- if (c === "\\") {
10056
- const next = i + 1 < n ? source[i + 1] : "";
10057
- if (next === "\\" || next === "'" || next === "\"") {
10058
- out += next;
10059
- i += 2;
10060
- continue;
10061
- }
10062
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
10063
- }
10064
- if (c === quote) {
10065
- closed = true;
10066
- i += 1;
10067
- break;
10068
- }
10069
- out += c;
10070
- i += 1;
10071
- }
10072
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
10073
- tokens.push({
10074
- type: "string",
10075
- value: out,
10076
- pos: start
10077
- });
10078
- continue;
10079
- }
10080
- if (isIdentStart(ch)) {
10081
- const start = i;
10082
- while (i < n && isIdentPart(source[i])) i += 1;
10083
- const text = source.slice(start, i);
10084
- if (KEYWORDS.has(text)) tokens.push({
10085
- type: "keyword",
10086
- keyword: keywordOf(text),
10087
- pos: start
10088
- });
10089
- else tokens.push({
10090
- type: "identifier",
10091
- name: text,
10092
- pos: start
10093
- });
10094
- continue;
10095
- }
10096
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
10097
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
10098
- tokens.push({
10099
- type: "punct",
10100
- punct: two,
10101
- pos: i
10102
- });
10103
- i += 2;
10104
- continue;
10105
- }
10106
- if (isSinglePunct(ch)) {
10107
- tokens.push({
10108
- type: "punct",
10109
- punct: ch,
10110
- pos: i
10111
- });
10112
- i += 1;
10113
- continue;
10114
- }
10115
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
10116
- }
10117
- tokens.push({
10118
- type: "eof",
10119
- pos: n
10120
- });
10121
- return tokens;
10122
- }
10123
- function keywordOf(text) {
10124
- if (text === "true") return "true";
10125
- if (text === "false") return "false";
10126
- return "null";
10127
- }
10128
- function isSinglePunct(ch) {
10129
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
10130
- }
10319
+ var ModelFormatEntrySchema = object({
10320
+ url: string(),
10321
+ sizeMB: number(),
10322
+ /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
10323
+ isDirectory: boolean().optional(),
10324
+ /** Multi-file payload (directory members or sibling files). */
10325
+ files: array(string()).readonly().optional(),
10326
+ /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
10327
+ runtimes: array(_enum(["python"])).readonly().optional()
10328
+ });
10131
10329
  /**
10132
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
10133
- *
10134
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
10135
- * → relational → additive → multiplicative → unary `! -` → call / primary.
10136
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
10137
- * string validated against the builtin table at parse time, so an unknown
10138
- * function is rejected immediately (author feedback) and a persisted expression
10139
- * that references a since-removed builtin degrades at read.
10140
- *
10141
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
10142
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
10330
+ * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
10331
+ * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
10143
10332
  */
10144
- /** Binary/logical operator precedence (higher binds tighter). */
10145
- var BINARY_PRECEDENCE = {
10146
- "||": 1,
10147
- "&&": 2,
10148
- "==": 3,
10149
- "!=": 3,
10150
- "<": 4,
10151
- "<=": 4,
10152
- ">": 4,
10153
- ">=": 4,
10154
- "+": 5,
10155
- "-": 5,
10156
- "*": 6,
10157
- "/": 6,
10158
- "%": 6
10159
- };
10160
- function isLogicalOp(op) {
10161
- return op === "&&" || op === "||";
10162
- }
10163
- function isBinaryOp(op) {
10164
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
10165
- }
10166
- var Parser = class {
10167
- tokens;
10168
- pos = 0;
10169
- nodeCount = 0;
10170
- identifiers = /* @__PURE__ */ new Set();
10171
- callees = /* @__PURE__ */ new Set();
10172
- constructor(tokens) {
10173
- this.tokens = tokens;
10174
- }
10175
- parse() {
10176
- const ast = this.parseTernary();
10177
- const tok = this.peek();
10178
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
10179
- return {
10180
- ast,
10181
- identifiers: this.identifiers,
10182
- callees: this.callees,
10183
- nodeCount: this.nodeCount
10184
- };
10185
- }
10186
- peek() {
10187
- return this.tokens[this.pos];
10188
- }
10189
- next() {
10190
- return this.tokens[this.pos++];
10191
- }
10192
- /** Consume a punctuator token, erroring if the next token isn't it. */
10193
- expectPunct(punct) {
10194
- const tok = this.peek();
10195
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
10196
- this.pos += 1;
10197
- }
10198
- matchPunct(punct) {
10199
- const tok = this.peek();
10200
- if (tok.type === "punct" && tok.punct === punct) {
10201
- this.pos += 1;
10202
- return true;
10203
- }
10204
- return false;
10205
- }
10206
- countNode() {
10207
- this.nodeCount += 1;
10208
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
10209
- }
10210
- parseTernary() {
10211
- const test = this.parseBinary(1);
10212
- if (this.matchPunct("?")) {
10213
- const consequent = this.parseTernary();
10214
- this.expectPunct(":");
10215
- const alternate = this.parseTernary();
10216
- this.countNode();
10217
- return {
10218
- kind: "conditional",
10219
- test,
10220
- consequent,
10221
- alternate
10222
- };
10223
- }
10224
- return test;
10225
- }
10226
- parseBinary(minPrec) {
10227
- let left = this.parseUnary();
10228
- for (;;) {
10229
- const tok = this.peek();
10230
- if (tok.type !== "punct") break;
10231
- const prec = BINARY_PRECEDENCE[tok.punct];
10232
- if (prec === void 0 || prec < minPrec) break;
10233
- const op = tok.punct;
10234
- this.pos += 1;
10235
- const right = this.parseBinary(prec + 1);
10236
- this.countNode();
10237
- if (isLogicalOp(op)) left = {
10238
- kind: "logical",
10239
- op,
10240
- left,
10241
- right
10242
- };
10243
- else if (isBinaryOp(op)) left = {
10244
- kind: "binary",
10245
- op,
10246
- left,
10247
- right
10248
- };
10249
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
10250
- }
10251
- return left;
10252
- }
10253
- parseUnary() {
10254
- const tok = this.peek();
10255
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
10256
- const op = tok.punct;
10257
- this.pos += 1;
10258
- const operand = this.parseUnary();
10259
- this.countNode();
10260
- return {
10261
- kind: "unary",
10262
- op,
10263
- operand
10264
- };
10265
- }
10266
- return this.parsePrimary();
10267
- }
10268
- parsePrimary() {
10269
- const tok = this.next();
10270
- switch (tok.type) {
10271
- case "number":
10272
- this.countNode();
10273
- return {
10274
- kind: "literal",
10275
- value: tok.value
10276
- };
10277
- case "string":
10278
- this.countNode();
10279
- return {
10280
- kind: "literal",
10281
- value: tok.value
10282
- };
10283
- case "keyword":
10284
- this.countNode();
10285
- return {
10286
- kind: "literal",
10287
- value: tok.keyword === "null" ? null : tok.keyword === "true"
10288
- };
10289
- case "identifier": {
10290
- const nextTok = this.peek();
10291
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
10292
- this.identifiers.add(tok.name);
10293
- this.countNode();
10294
- return {
10295
- kind: "identifier",
10296
- name: tok.name
10297
- };
10298
- }
10299
- case "punct":
10300
- if (tok.punct === "(") {
10301
- const inner = this.parseTernary();
10302
- this.expectPunct(")");
10303
- return inner;
10304
- }
10305
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
10306
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
10307
- }
10308
- }
10309
- parseCall(callee, pos) {
10310
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
10311
- this.expectPunct("(");
10312
- const args = [];
10313
- if (!this.matchPunct(")")) for (;;) {
10314
- args.push(this.parseTernary());
10315
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
10316
- if (this.matchPunct(",")) continue;
10317
- this.expectPunct(")");
10318
- break;
10319
- }
10320
- this.callees.add(callee);
10321
- this.countNode();
10322
- return {
10323
- kind: "call",
10324
- callee,
10325
- args
10326
- };
10327
- }
10328
- };
10329
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
10330
- * `ExpressionParseError` on any lexical or grammatical failure. */
10331
- function parseExpression(source) {
10332
- return new Parser(tokenize(source)).parse();
10333
- }
10333
+ var ModelExtraFileSchema = object({
10334
+ url: string(),
10335
+ filename: string(),
10336
+ sizeMB: number()
10337
+ });
10334
10338
  /**
10335
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
10336
- * by expr"). The cache stores BOTH successes and failures (negative caching),
10337
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
10338
- * one per read on a hot resolve path.
10339
- *
10340
- * The cache is a module-level singleton: entries are pure, content-addressed
10341
- * ASTs keyed by the raw source string, so sharing one instance across all
10342
- * callers is safe and maximises hit rate.
10339
+ * Per-format payload map. Modelled as an explicit object (one optional key
10340
+ * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
10341
+ * record requires every key, but a catalog entry only ships a subset of
10342
+ * formats.
10343
10343
  */
10344
- var cache = /* @__PURE__ */ new Map();
10345
- function getCached(source) {
10346
- const hit = cache.get(source);
10347
- if (hit !== void 0) {
10348
- cache.delete(source);
10349
- cache.set(source, hit);
10350
- return hit;
10351
- }
10352
- let result;
10353
- try {
10354
- result = {
10355
- ok: true,
10356
- parsed: parseExpression(source)
10357
- };
10358
- } catch (err) {
10359
- result = {
10360
- ok: false,
10361
- error: err instanceof ExpressionParseError ? err.message : String(err)
10362
- };
10363
- }
10364
- cache.set(source, result);
10365
- if (cache.size > 256) {
10366
- const oldest = cache.keys().next().value;
10367
- if (oldest !== void 0) cache.delete(oldest);
10368
- }
10369
- return result;
10370
- }
10371
- /** Compile `source`, returning a discriminated result instead of throwing.
10372
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
10373
- function compileExpressionSafe(source) {
10374
- return getCached(source);
10375
- }
10376
- Object.freeze({});
10344
+ var ModelFormatsSchema = object({
10345
+ onnx: ModelFormatEntrySchema.optional(),
10346
+ coreml: ModelFormatEntrySchema.optional(),
10347
+ openvino: ModelFormatEntrySchema.optional(),
10348
+ tflite: ModelFormatEntrySchema.optional(),
10349
+ pt: ModelFormatEntrySchema.optional(),
10350
+ gguf: ModelFormatEntrySchema.optional()
10351
+ });
10352
+ /**
10353
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
10354
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
10355
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
10356
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
10357
+ * resolution/download/persistence; this is a presentation overlay resolved back
10358
+ * to an `id`.
10359
+ */
10360
+ var ModelVariantGroupSchema = object({
10361
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
10362
+ family: string(),
10363
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
10364
+ tier: string(),
10365
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
10366
+ precision: _enum(["fp32", "int8"]).optional(),
10367
+ /**
10368
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
10369
+ * latency-optimized export (e.g. ReLU-activation variant) — the slot the
10370
+ * future performance variants plug into.
10371
+ */
10372
+ optimization: _enum(["standard", "fast"]).optional(),
10373
+ /**
10374
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
10375
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
10376
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
10377
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
10378
+ * the group so the selector can offer it as a variant axis.
10379
+ */
10380
+ resolution: number().int().positive().optional()
10381
+ });
10382
+ var ModelProviderIdSchema = _enum([
10383
+ "camstack",
10384
+ "frigate",
10385
+ "scrypted",
10386
+ "custom"
10387
+ ]);
10377
10388
  /**
10378
- * Author-time validation. Returns `null` when the source is valid, else a
10379
- * human-readable error message. Checks: the expression compiles; binding count
10380
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
10381
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
10382
- * FREE identifier of the AST is covered by a binding or the injected `now`.
10389
+ * The licence of a model, as a record rather than a bare SPDX string: a
10390
+ * permissive CODE licence routinely sits on top of restrictive WEIGHTS or
10391
+ * DATA, and the obligations (attribution, modification notices, source offers)
10392
+ * are per upstream. A persisted custom-model row written when this field was a
10393
+ * string still parses — the string is lifted into `{ weights }`.
10394
+ *
10395
+ * Every licence surface (the admin "Models and licenses" page,
10396
+ * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
10397
+ * these records by `scripts/gen-model-licenses.ts` (D661).
10383
10398
  */
10384
- function validateExpressionSource(src) {
10385
- const names = Object.keys(src.bindings);
10386
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
10387
- for (const name of names) {
10388
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
10389
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
10390
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
10391
- }
10392
- const compiled = compileExpressionSafe(src.expr);
10393
- if (!compiled.ok) return compiled.error;
10394
- const bound = new Set(names);
10395
- for (const id of compiled.parsed.identifiers) {
10396
- if (id === "now") continue;
10397
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
10398
- }
10399
- return null;
10400
- }
10399
+ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
10400
+ /** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
10401
+ weights: string().min(1),
10402
+ /** SPDX id of the upstream code that defines / trained the network. */
10403
+ code: string().min(1).optional(),
10404
+ /** Training data whose terms add obligations (attribution, non-commercial). */
10405
+ data: object({
10406
+ name: string().min(1),
10407
+ terms: string().min(1),
10408
+ url: string().url().optional()
10409
+ }).optional(),
10410
+ /** The project the weights come from. Required on built-in entries (guard). */
10411
+ upstream: object({
10412
+ name: string().min(1),
10413
+ url: string().url()
10414
+ }).optional(),
10415
+ /** Where the licence text governing the weights is published. */
10416
+ url: string().url().optional(),
10417
+ /** Attribution the licence REQUIRES, verbatim. */
10418
+ attribution: string().min(1).optional(),
10419
+ /** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
10420
+ modifications: string().min(1).optional(),
10421
+ /** Who serves the file a node downloads. */
10422
+ hosting: _enum([
10423
+ "camstack-hf",
10424
+ "third-party",
10425
+ "built-in"
10426
+ ]).optional(),
10427
+ /** True when the weights or their data forbid commercial use. */
10428
+ nonCommercial: boolean().optional()
10429
+ }));
10430
+ var ModelCatalogEntrySchema = object({
10431
+ id: string(),
10432
+ name: string(),
10433
+ description: string(),
10434
+ formats: ModelFormatsSchema,
10435
+ inputSize: object({
10436
+ width: number(),
10437
+ height: number()
10438
+ }),
10439
+ /**
10440
+ * Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
10441
+ * every detector / classifier / embedder. Set to 1 for a grayscale CTC text
10442
+ * recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
10443
+ * feeds a single-channel, EasyOCR-normalized tensor instead of the default
10444
+ * 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
10445
+ * Python inference pool.
10446
+ */
10447
+ inputChannels: number().int().positive().optional(),
10448
+ labels: array(LabelDefinitionSchema).readonly(),
10449
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10450
+ /**
10451
+ * `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
10452
+ * (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
10453
+ * plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
10454
+ * `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
10455
+ */
10456
+ inputNormalization: _enum([
10457
+ "zero-one",
10458
+ "imagenet",
10459
+ "none",
10460
+ "scrfd"
10461
+ ]).optional(),
10462
+ /**
10463
+ * The model already applies softmax IN-GRAPH — its raw output is a
10464
+ * probability distribution, not logits. When set, the `softmax`
10465
+ * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
10466
+ * probability vector collapses it toward uniform (top-1 score craters far
10467
+ * below its true value, making every confidence gate meaningless). Absent ⇒
10468
+ * the output is raw logits and the postprocessor applies softmax (the normal
10469
+ * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
10470
+ * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
10471
+ */
10472
+ outputProbabilities: boolean().optional(),
10473
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10474
+ /**
10475
+ * Per-MODEL postprocessor override. Absent ⇒ the step's own
10476
+ * `StepDefinition.postprocessor` applies (the normal case — every model in a
10477
+ * step shares its decode). Set it when a step hosts models with DIFFERENT raw
10478
+ * output layouts under one slot: e.g. object-detection is `'yolo'` by default,
10479
+ * but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
10480
+ * 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
10481
+ * by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
10482
+ */
10483
+ postprocessor: custom().optional(),
10484
+ /**
10485
+ * Per-MODEL default confidence floor. Absent ⇒ the step's
10486
+ * `StepDefinition.defaultConfidence` applies. A score is a property of one
10487
+ * model's output scale, not of the step slot it sits in: YuNet's
10488
+ * `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
10489
+ * face gallery with wheels and hands (D662, amends D645). An operator's
10490
+ * explicit value still wins over it. Resolved in ONE place —
10491
+ * `addon-pipeline/.../registry/effective-confidence.ts`.
10492
+ */
10493
+ defaultConfidence: number().min(0).max(1).optional(),
10494
+ /**
10495
+ * When true, the executor produces a landmark-aligned crop (similarity warp
10496
+ * onto the canonical template) before this step runs, instead of a plain
10497
+ * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
10498
+ * their embeddings are only discriminative on an aligned input. The face
10499
+ * detector that produced the parent detail must emit 5 landmarks.
10500
+ */
10501
+ faceAlignment: boolean().optional(),
10502
+ /**
10503
+ * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
10504
+ * Downloaded into the same modelsDir alongside the model file.
10505
+ */
10506
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
10507
+ /**
10508
+ * LEGACY entry — retained in the catalog so a persisted operator selection
10509
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
10510
+ * model list and excluded from the auto format-default pick. Set on the
10511
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
10512
+ * the active lineup stays the coherent curated ladder without deleting a
10513
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
10514
+ * an explicit legacy id that has a build for the node's format.
10515
+ */
10516
+ legacy: boolean().optional(),
10517
+ /**
10518
+ * Measured quality/latency metadata — populated from the benchmark addon on
10519
+ * the real node classes. Absent = not yet measured (most entries today; the
10520
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
10521
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
10522
+ */
10523
+ metrics: object({
10524
+ map50: number().optional(),
10525
+ p95LatencyMs: record(string(), number()).optional()
10526
+ }).optional(),
10527
+ /**
10528
+ * The model's licence record — see {@link ModelLicenseSchema}. Optional in
10529
+ * the schema (a custom model's author may state none); REQUIRED and complete
10530
+ * on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
10531
+ */
10532
+ license: ModelLicenseSchema.optional(),
10533
+ /**
10534
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
10535
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
10536
+ * of a family's sizes and quantizations collapse into one grouped picker
10537
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
10538
+ * (legacy / custom models) — never shown in the grouped selector. The flat
10539
+ * `id` stays the source of truth for resolution/download/persistence; grouping
10540
+ * is a presentation overlay resolved back to an `id`.
10541
+ */
10542
+ group: ModelVariantGroupSchema.optional(),
10543
+ /**
10544
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
10545
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
10546
+ * persisted before this field existed (`inferModelProvider` fills those).
10547
+ */
10548
+ provider: ModelProviderIdSchema.optional(),
10549
+ /**
10550
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
10551
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
10552
+ * labels already ARE the CamStack macros (Scrypted identity map).
10553
+ */
10554
+ classMap: DetectionCatalogClassMapSchema.optional(),
10555
+ /**
10556
+ * The model's CLIP feature-space contract (text encoder, dimension, vector
10557
+ * index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
10558
+ * CLIP image encoder; its absence is what "not a CLIP model" means to the
10559
+ * embedding encoder and the semantic-search store (D649).
10560
+ */
10561
+ clip: ClipModelMetaSchema.optional()
10562
+ });
10563
+ var ConvertTargetSchema = discriminatedUnion("format", [object({
10564
+ format: literal("openvino"),
10565
+ precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
10566
+ }), object({ format: literal("coreml") })]);
10567
+ var ModelConvertMetadataSchema = object({
10568
+ id: string().regex(/^[a-zA-Z0-9._-]+$/),
10569
+ name: string(),
10570
+ labels: array(LabelDefinitionSchema).readonly(),
10571
+ inputSize: object({
10572
+ width: number(),
10573
+ height: number()
10574
+ }),
10575
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10576
+ inputNormalization: _enum([
10577
+ "zero-one",
10578
+ "imagenet",
10579
+ "none",
10580
+ "scrfd"
10581
+ ]).optional(),
10582
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10583
+ outputFormat: _enum([
10584
+ "yolo",
10585
+ "ssd",
10586
+ "embedding",
10587
+ "classification",
10588
+ "ocr",
10589
+ "segmentation"
10590
+ ]),
10591
+ faceAlignment: boolean().optional(),
10592
+ classMap: DetectionCatalogClassMapSchema.optional()
10593
+ });
10594
+ var ConvertResultSchema = object({
10595
+ entry: ModelCatalogEntrySchema,
10596
+ artifacts: array(object({
10597
+ format: _enum(MODEL_FORMATS),
10598
+ precision: _enum(["fp16", "int8"]).optional(),
10599
+ sizeMB: number(),
10600
+ validated: boolean(),
10601
+ files: array(string()).readonly()
10602
+ })).readonly()
10603
+ });
10401
10604
  var ExpressionBindingSourceSchema = union([
10402
10605
  object({
10403
10606
  kind: literal("field").optional(),
@@ -12623,101 +12826,14 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
12623
12826
  }),
12624
12827
  lastChangedAt: number()
12625
12828
  });
12626
- /** A composed device's stableId is this prefix plus the block id. */
12627
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12628
- /** Longest snippet a `code` source may carry (slice 4). */
12629
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12630
- var CompositionSourceRefSchema = object({
12631
- /** The owning addon; `stableId` is unique only within it. */
12632
- addonId: string().min(1),
12633
- stableId: string().min(1)
12634
- });
12635
- /** One field of one capability of one source device. */
12636
- var CompositionFieldReadSchema = object({
12637
- source: CompositionSourceRefSchema,
12638
- cap: string().min(1),
12639
- /** Dotted path into the source cap's runtime-state slice. */
12640
- fieldPath: string().min(1)
12641
- });
12642
- /** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
12643
- var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: literal("from") });
12644
- var CompositionBindingSchema = discriminatedUnion("kind", [CompositionFromSourceSchema, object({
12645
- kind: literal("literal"),
12646
- value: union([
12647
- string(),
12648
- number(),
12649
- boolean(),
12650
- _null()
12651
- ])
12652
- })]);
12653
- /**
12654
- * `expression`: a formula over named bindings, in the salvaged expression
12655
- * engine. Validated at parse by the SAME function every other consumer runs,
12656
- * so the editor and the store cannot disagree.
12657
- */
12658
- var CompositionExpressionSourceSchema = object({
12659
- kind: literal("expression"),
12660
- expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
12661
- bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
12662
- }).superRefine((src, ctx) => {
12663
- const err = validateExpressionSource(src);
12664
- if (err !== null) ctx.addIssue({
12665
- code: "custom",
12666
- message: err,
12667
- path: ["expr"]
12668
- });
12669
- });
12670
- /** `code`: RESERVED for slice 4 (own runner, one-way eject). */
12671
- var CompositionCodeSourceSchema = object({
12672
- kind: literal("code"),
12673
- code: string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
12674
- });
12675
- var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12676
- CompositionFromSourceSchema,
12677
- CompositionExpressionSourceSchema,
12678
- CompositionCodeSourceSchema
12679
- ]);
12680
- var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12681
- kind: literal("forward"),
12682
- source: CompositionSourceRefSchema,
12683
- cap: string().min(1),
12684
- method: string().min(1)
12685
- }), CompositionCodeSourceSchema]);
12686
- var CompositionFeatureSchema = discriminatedUnion("kind", [object({
12687
- kind: literal("fields"),
12688
- cap: string().min(1),
12689
- fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
12690
- /** RESERVED for slice 3; refused by the validator when non-empty. */
12691
- commands: record(string().min(1), CompositionCommandTargetSchema).optional()
12692
- }), object({
12693
- kind: literal("passthrough"),
12694
- cap: string().min(1),
12695
- source: CompositionSourceRefSchema
12696
- })]);
12697
- var CompositionSchema = object({
12698
- target: discriminatedUnion("kind", [object({
12699
- kind: literal("new"),
12700
- type: _enum(DeviceType),
12701
- role: _enum(DeviceRole).optional()
12702
- }), object({
12703
- kind: literal("existing"),
12704
- device: CompositionSourceRefSchema
12705
- })]),
12706
- features: array(CompositionFeatureSchema).min(1).max(16)
12707
- }).superRefine((composition, ctx) => {
12708
- const seen = /* @__PURE__ */ new Set();
12709
- composition.features.forEach((feature, index) => {
12710
- if (seen.has(feature.cap)) ctx.addIssue({
12711
- code: "custom",
12712
- message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
12713
- path: [
12714
- "features",
12715
- index,
12716
- "cap"
12717
- ]
12718
- });
12719
- seen.add(feature.cap);
12720
- });
12829
+ EventCategory.DoorbellOnPressed;
12830
+ var CompositionEventPlanSchema = object({
12831
+ featureIndex: number().int().nonnegative(),
12832
+ cap: string(),
12833
+ eventName: string(),
12834
+ category: _enum(EventCategory),
12835
+ stateField: string(),
12836
+ fieldKey: string()
12721
12837
  });
12722
12838
  /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
12723
12839
  var CompositionProblemSchema = object({
@@ -12738,7 +12854,12 @@ var CompositionProblemSchema = object({
12738
12854
  "source-field-unknown",
12739
12855
  "expression-invalid",
12740
12856
  "cycle",
12741
- "duplicate-target"
12857
+ "duplicate-target",
12858
+ "not-item-array",
12859
+ "item-field-reserved",
12860
+ "target-missing",
12861
+ "target-unreadable",
12862
+ "not-replaceable"
12742
12863
  ]),
12743
12864
  path: string(),
12744
12865
  message: string()
@@ -12749,20 +12870,40 @@ var CompositionFieldKindSchema = _enum([
12749
12870
  "boolean",
12750
12871
  "enum"
12751
12872
  ]);
12752
- /** How a field says "unavailable": `null` when its schema allows it, else the device goes offline (D393). */
12753
- var CompositionUnavailableModeSchema = _enum(["null", "offline"]);
12873
+ /**
12874
+ * How a field says "unavailable" (D393, D663): `null` when its schema allows it;
12875
+ * `unknown` — the enum's own word — for an ADDED field that has one; `release`
12876
+ * for a REPLACED field of an existing device, whose claim is released so the
12877
+ * native provider's reading shows again; else the (composed) device goes offline.
12878
+ */
12879
+ var CompositionUnavailableModeSchema = _enum([
12880
+ "null",
12881
+ "offline",
12882
+ "unknown",
12883
+ "release"
12884
+ ]);
12754
12885
  /**
12755
12886
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
12756
- * field of its cap changes value · `constant` reads nothing, computed once.
12887
+ * field of its cap changes value · `constant` reads nothing, computed once ·
12888
+ * `accumulator` advances from the target's seed/current output.
12757
12889
  */
12758
12890
  var CompositionFieldRoleSchema = _enum([
12759
12891
  "derived",
12760
12892
  "clock",
12761
- "constant"
12893
+ "constant",
12894
+ "accumulator"
12762
12895
  ]);
12896
+ /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
12897
+ var CompositionFieldItemSchema = object({
12898
+ arrayPath: string(),
12899
+ key: string(),
12900
+ label: string()
12901
+ });
12763
12902
  var CompositionFieldPlanSchema = object({
12764
12903
  cap: string(),
12904
+ /** Top-level field, or — when `item` is set — the dotted path inside that item. */
12765
12905
  path: string(),
12906
+ item: CompositionFieldItemSchema.optional(),
12766
12907
  /** Index into `composition.features` this field's capability came from —
12767
12908
  * lets a source/cycle problem name its path as `features[N].fields.<path>`
12768
12909
  * (the same convention `planComposition`'s own problems already use). */
@@ -12775,16 +12916,39 @@ var CompositionFieldPlanSchema = object({
12775
12916
  source: CompositionFieldSourceSchema,
12776
12917
  reads: array(CompositionFieldReadSchema)
12777
12918
  });
12919
+ /**
12920
+ * What a feature does to its target (D663): `new` builds a capability on a new
12921
+ * device; on an existing device it `add`s a capability the device lacks, or
12922
+ * `replace`s named fields of one it has natively.
12923
+ */
12924
+ var CompositionFeatureModeSchema = _enum([
12925
+ "new",
12926
+ "add",
12927
+ "replace"
12928
+ ]);
12929
+ var CompositionFeaturePlanSchema = object({
12930
+ /** Index in the stored feature array; disabled features are omitted rather than re-indexed. */
12931
+ featureIndex: number().int().nonnegative(),
12932
+ cap: string(),
12933
+ mode: CompositionFeatureModeSchema
12934
+ });
12778
12935
  var CompositionValidationSchema = object({
12779
12936
  ok: boolean(),
12780
12937
  problems: array(CompositionProblemSchema),
12781
- fields: array(CompositionFieldPlanSchema)
12938
+ fields: array(CompositionFieldPlanSchema),
12939
+ features: array(CompositionFeaturePlanSchema),
12940
+ events: array(CompositionEventPlanSchema)
12782
12941
  });
12783
- /** Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts` (Task 11). */
12942
+ /**
12943
+ * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
12944
+ * running, but a replaced field is released to the native provider while its
12945
+ * source is unavailable (ruling S1b, D663) — the reason names each such field.
12946
+ */
12784
12947
  var CompositionRunStatusSchema = _enum([
12785
12948
  "stopped",
12786
12949
  "starting",
12787
12950
  "running",
12951
+ "degraded",
12788
12952
  "failed"
12789
12953
  ]);
12790
12954
  var CompositionFieldStateKindSchema = _enum([
@@ -12795,6 +12959,8 @@ var CompositionFieldStateKindSchema = _enum([
12795
12959
  var CompositionFieldStateSchema = object({
12796
12960
  cap: string(),
12797
12961
  path: string(),
12962
+ /** Same as the plan's: two items' `status` are told apart by it. */
12963
+ item: CompositionFieldItemSchema.optional(),
12798
12964
  state: CompositionFieldStateKindSchema,
12799
12965
  /** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
12800
12966
  value: union([
@@ -12816,11 +12982,14 @@ var CompositionBlockStateSchema = object({
12816
12982
  * agent is the reason placement is not fixed to the hub. */
12817
12983
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
12818
12984
  /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
12819
- * a failing block reads the same way a failing addon does. */
12985
+ * a failing block reads the same way a failing addon does. `degraded` is a
12986
+ * composition on an existing device with a field handed back to the native
12987
+ * provider while its source is unavailable (S1b, D663); the error names it. */
12820
12988
  var CoreBlockStatusSchema = _enum([
12821
12989
  "stopped",
12822
12990
  "starting",
12823
12991
  "running",
12992
+ "degraded",
12824
12993
  "failed"
12825
12994
  ]);
12826
12995
  /** Client-authored fields. */
@@ -12856,9 +13025,16 @@ var CoreBlockSourceSchema = discriminatedUnion("kind", [
12856
13025
  composition: CompositionSchema
12857
13026
  })
12858
13027
  ]);
12859
- /** The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7). */
13028
+ /**
13029
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
13030
+ *
13031
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
13032
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
13033
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
13034
+ * client-sent name by name.
13035
+ */
12860
13036
  var CompositionBlockInputSchema = object({
12861
- name: string().min(1).max(120),
13037
+ name: string().min(1).max(120).optional(),
12862
13038
  enabled: boolean(),
12863
13039
  composition: CompositionSchema
12864
13040
  });
@@ -12909,6 +13085,11 @@ var CompositionGetReadSchema = discriminatedUnion("state", [object({
12909
13085
  state: literal("loaded"),
12910
13086
  view: CompositionViewSchema.nullable()
12911
13087
  }), CompositionsNotLoadedSchema]);
13088
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
13089
+ var CompositionCustomizationReadSchema = discriminatedUnion("state", [object({
13090
+ state: literal("loaded"),
13091
+ view: CompositionViewSchema.nullable()
13092
+ }), CompositionsNotLoadedSchema]);
12912
13093
  /** What a compile attempt produced. */
12913
13094
  var CoreBlockCompileResultSchema = object({
12914
13095
  ok: boolean(),
@@ -12959,6 +13140,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
12959
13140
  auth: "admin",
12960
13141
  caller: "required"
12961
13142
  }), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
13143
+ addonId: string().min(1),
13144
+ stableId: string().min(1)
13145
+ }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
12962
13146
  composition: CompositionSchema,
12963
13147
  blockId: string().optional()
12964
13148
  }), CompositionValidationSchema, {
@@ -14829,6 +15013,52 @@ method(object({
14829
15013
  deviceIds: array(number()).readonly(),
14830
15014
  caps: array(string()).readonly().optional()
14831
15015
  }), record(string(), record(string(), unknown().nullable())));
15016
+ /**
15017
+ * Per-field ownership (D663). A composition may CLAIM fields of a native
15018
+ * cap's runtime-state slice on an existing device: `replace` overlays the
15019
+ * claimed fields on the native slice, `add` builds a slice the device does
15020
+ * not natively have. Declared here — once — so the hub mirror (Task 5) and
15021
+ * the claim methods (Task 6) share one union.
15022
+ */
15023
+ var FieldClaimModeSchema = _enum(["replace", "add"]);
15024
+ /** What a claim, patch or release answers. A refusal always names its reason. */
15025
+ var ClaimOutcomeSchema = discriminatedUnion("ok", [object({
15026
+ ok: literal(true),
15027
+ changed: boolean()
15028
+ }), object({
15029
+ ok: literal(false),
15030
+ code: _enum([
15031
+ "owned-by-other",
15032
+ "not-claimed",
15033
+ "field-not-claimed",
15034
+ "invalid-slice",
15035
+ "unknown-device",
15036
+ "row-unreadable",
15037
+ "migration-in-flight"
15038
+ ]),
15039
+ message: string()
15040
+ })]);
15041
+ /** One claim as `listClaims` reports it. `fields` is the PLANNED set, explicit (ruling S5). */
15042
+ var FieldClaimSchema = object({
15043
+ deviceId: number(),
15044
+ capName: string(),
15045
+ owner: string().min(1),
15046
+ mode: FieldClaimModeSchema,
15047
+ fields: array(string().min(1)).min(1)
15048
+ });
15049
+ /**
15050
+ * What `listClaims` answers. The claims INDEX is a listing projection loaded
15051
+ * from the settings store at boot; until a load has succeeded it is
15052
+ * `not-loaded`, never `[]` — an unanswered question must not look like an
15053
+ * empty answer (D315).
15054
+ */
15055
+ var ClaimsListingSchema = discriminatedUnion("state", [object({
15056
+ state: literal("loaded"),
15057
+ claims: array(FieldClaimSchema)
15058
+ }), object({
15059
+ state: literal("not-loaded"),
15060
+ reason: string()
15061
+ })]);
14832
15062
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
14833
15063
  deviceId: number(),
14834
15064
  capName: string()
@@ -14836,7 +15066,35 @@ method(object({ deviceId: number() }), record(string(), record(string(), unknown
14836
15066
  deviceId: number(),
14837
15067
  capName: string(),
14838
15068
  slice: record(string(), unknown())
14839
- }), _void(), { kind: "mutation" }), object({
15069
+ }), _void(), { kind: "mutation" }), method(object({
15070
+ deviceId: number(),
15071
+ capName: string(),
15072
+ owner: string().min(1),
15073
+ mode: FieldClaimModeSchema,
15074
+ fields: array(string().min(1)).min(1),
15075
+ values: record(string(), unknown())
15076
+ }), ClaimOutcomeSchema, {
15077
+ kind: "mutation",
15078
+ auth: "admin"
15079
+ }), method(object({
15080
+ deviceId: number(),
15081
+ capName: string(),
15082
+ owner: string().min(1),
15083
+ values: record(string(), unknown())
15084
+ }), ClaimOutcomeSchema, {
15085
+ kind: "mutation",
15086
+ auth: "admin"
15087
+ }), method(object({
15088
+ deviceId: number(),
15089
+ capName: string(),
15090
+ owner: string().min(1)
15091
+ }), ClaimOutcomeSchema, {
15092
+ kind: "mutation",
15093
+ auth: "admin"
15094
+ }), method(object({ ownerPrefix: string().optional() }), ClaimsListingSchema, { auth: "admin" }), method(object({
15095
+ deviceId: number(),
15096
+ capName: string()
15097
+ }), record(string(), unknown()).nullable(), { auth: "admin" }), object({
14840
15098
  deviceId: number(),
14841
15099
  capName: string(),
14842
15100
  slice: record(string(), unknown())
@@ -26134,42 +26392,6 @@ DeviceType.Camera, method(object({
26134
26392
  kind: "query",
26135
26393
  auth: "protected"
26136
26394
  });
26137
- /**
26138
- * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
26139
- * valutatore puro del suo FRONTE.
26140
- *
26141
- * Viveva dentro il builtin virtual-doorbell
26142
- * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
26143
- * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
26144
- * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
26145
- * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
26146
- * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
26147
- * SPOSTA qui e il doorbell la ri-esporta.
26148
- *
26149
- * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
26150
- * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
26151
- * regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
26152
- * booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
26153
- */
26154
- /**
26155
- * Known binary / switch source caps → the boolean slice field whose
26156
- * false→true rise counts as ACTIVE. Every entry is "fire on active".
26157
- * Sensors whose "active" reading is not a plain boolean (presence's string
26158
- * state, connectivity's connected flag) are deliberately excluded — a
26159
- * reconnect is not a doorbell press, and it is not a recording either.
26160
- */
26161
- var SOURCE_CAP_ACTIVE_FIELD = {
26162
- contact: "entryOpen",
26163
- binary: "on",
26164
- switch: "on",
26165
- motion: "detected",
26166
- flood: "flooded",
26167
- gas: "detected",
26168
- smoke: "detected",
26169
- "carbon-monoxide": "detected",
26170
- vibration: "detected",
26171
- tamper: "tampered"
26172
- };
26173
26395
  object({
26174
26396
  /** Ms epoch of the last press. null = never observed since this provider started. */
26175
26397
  lastPressedAt: number().nullable(),
@@ -26209,7 +26431,7 @@ var ExtensionSourceSchema = object({
26209
26431
  /**
26210
26432
  * How one picked source stands. `missing` — no such device (deleted, or an id
26211
26433
  * that never existed); `foreign` — a device the slot's owner cannot read from
26212
- * (slice 2's cross-owner graft source); `unavailable` — it exists but is not a
26434
+ * (a source owned by another addon); `unavailable` — it exists but is not a
26213
26435
  * valid source now (a container, which is never a pick). A source in any of
26214
26436
  * these states is SHOWN, never silently dropped or re-pointed.
26215
26437
  */
@@ -26249,10 +26471,6 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
26249
26471
  slot: ExtensionSlotIdSchema,
26250
26472
  /** Names the store written (D62), e.g. `virtual-doorbell:doorbellSources`. */
26251
26473
  authority: string(),
26252
- /** graft: its switch. association: null — removing every source is "off". */
26253
- enabled: boolean().nullable(),
26254
- /** graft: the cap registered on the host. association: null. */
26255
- projects: string().nullable(),
26256
26474
  activation: ExtensionSlotActivationSchema.nullable(),
26257
26475
  value: ExtensionSlotValueSchema,
26258
26476
  sources: array(ExtensionSlotSourceSchema),
@@ -26286,7 +26504,8 @@ var ExtensionCandidatesSchema = object({ candidates: array(object({
26286
26504
  /**
26287
26505
  * What a write did. `none` — applied in place. `host` / `parent` — the owner
26288
26506
  * reloaded the host (or, for an accessory child, its parent) because the
26289
- * write changed what the host REGISTERS (a graft switched on or off, slice 2).
26507
+ * write changed what the host REGISTERS. A slot never adds a capability to its
26508
+ * host — that is a composition (D663) — so today's owners answer `none`.
26290
26509
  */
26291
26510
  var ExtensionSetResultSchema = object({
26292
26511
  applied: literal(true),
@@ -27353,7 +27572,7 @@ var BatteryStatusSchema = object({
27353
27572
  */
27354
27573
  binary: boolean().optional()
27355
27574
  });
27356
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, method(object({
27575
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, method(object({
27357
27576
  deviceId: number(),
27358
27577
  /** Bound on the wait. Sensible range 3000–10000ms. */
27359
27578
  timeoutMs: number().int().min(500).max(3e4).default(8e3)
@@ -27854,6 +28073,31 @@ object({
27854
28073
  lastChangedAt: number()
27855
28074
  });
27856
28075
  DeviceType.Sensor;
28076
+ /**
28077
+ * Generic device-consumables capability — surfaces a device's
28078
+ * maintenance items (vacuum filters/brushes, replaceable cartridges,
28079
+ * descaling cycles, …) with their remaining life and an optional
28080
+ * "Replaced" reset action. Device-agnostic: any provider that knows its
28081
+ * device tracks consumables can register it; the cap declares no
28082
+ * vocabulary of its own — the provider names each item verbatim.
28083
+ *
28084
+ * Like `childLayout`, the cap is INERT until a provider sets items: no
28085
+ * provider populates it by guessing (no HA inference). The UI renders a
28086
+ * "No consumables reported" placeholder when `items` is empty.
28087
+ */
28088
+ /** Units a remaining-life counter is reported in. Closed: the panel renders
28089
+ * each. */
28090
+ var ConsumableRemainingUnitSchema = _enum([
28091
+ "days",
28092
+ "hours",
28093
+ "cycles"
28094
+ ]);
28095
+ /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
28096
+ * days). `value` null = unknown (D393). */
28097
+ var ConsumableRemainingSchema = object({
28098
+ value: number().min(0).nullable(),
28099
+ unit: ConsumableRemainingUnitSchema
28100
+ });
27857
28101
  var ConsumablesStatusSchema = object({
27858
28102
  items: array(object({
27859
28103
  /** Stable id, e.g. 'main-brush'. */
@@ -27867,7 +28111,10 @@ var ConsumablesStatusSchema = object({
27867
28111
  /** Ms epoch of the last replace, when known. */
27868
28112
  lastResetAt: number().nullable(),
27869
28113
  /** Whether `reset()` is meaningful for this item. */
27870
- resettable: boolean()
28114
+ resettable: boolean(),
28115
+ /** Remaining life as a count (days/hours/cycles), when a provider reports
28116
+ * it that way instead of — or alongside — `level`. Absent = not reported. */
28117
+ remaining: ConsumableRemainingSchema.optional()
27871
28118
  })),
27872
28119
  lastChangedAt: number()
27873
28120
  });
@@ -34888,7 +35135,16 @@ Object.freeze({
34888
35135
  latest: {
34889
35136
  kind: "fixed",
34890
35137
  result: ["number", "null"]
34891
- }
35138
+ },
35139
+ number: {
35140
+ kind: "fixed",
35141
+ result: ["number", "null"]
35142
+ },
35143
+ rose: {
35144
+ kind: "fixed",
35145
+ result: ["number", "null"]
35146
+ },
35147
+ count: NUMBER
34892
35148
  });
34893
35149
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
34894
35150
  /**
@@ -37295,6 +37551,12 @@ Object.freeze({
37295
37551
  addonId: null,
37296
37552
  access: "view"
37297
37553
  },
37554
+ "coreBlocks.getCustomization": {
37555
+ capName: "core-blocks",
37556
+ capScope: "system",
37557
+ addonId: null,
37558
+ access: "view"
37559
+ },
37298
37560
  "coreBlocks.getTypeDefs": {
37299
37561
  capName: "core-blocks",
37300
37562
  capScope: "system",
@@ -38417,6 +38679,12 @@ Object.freeze({
38417
38679
  addonId: null,
38418
38680
  access: "create"
38419
38681
  },
38682
+ "deviceState.claimFields": {
38683
+ capName: "device-state",
38684
+ capScope: "system",
38685
+ addonId: null,
38686
+ access: "create"
38687
+ },
38420
38688
  "deviceState.getAllSnapshots": {
38421
38689
  capName: "device-state",
38422
38690
  capScope: "system",
@@ -38429,12 +38697,36 @@ Object.freeze({
38429
38697
  addonId: null,
38430
38698
  access: "view"
38431
38699
  },
38700
+ "deviceState.getNativeCapSlice": {
38701
+ capName: "device-state",
38702
+ capScope: "system",
38703
+ addonId: null,
38704
+ access: "view"
38705
+ },
38432
38706
  "deviceState.getSnapshot": {
38433
38707
  capName: "device-state",
38434
38708
  capScope: "system",
38435
38709
  addonId: null,
38436
38710
  access: "view"
38437
38711
  },
38712
+ "deviceState.listClaims": {
38713
+ capName: "device-state",
38714
+ capScope: "system",
38715
+ addonId: null,
38716
+ access: "view"
38717
+ },
38718
+ "deviceState.patchOwnedFields": {
38719
+ capName: "device-state",
38720
+ capScope: "system",
38721
+ addonId: null,
38722
+ access: "create"
38723
+ },
38724
+ "deviceState.releaseClaim": {
38725
+ capName: "device-state",
38726
+ capScope: "system",
38727
+ addonId: null,
38728
+ access: "create"
38729
+ },
38438
38730
  "deviceState.setCapSlice": {
38439
38731
  capName: "device-state",
38440
38732
  capScope: "system",
@@ -43950,16 +44242,36 @@ Object.freeze({
43950
44242
  form: "single",
43951
44243
  optional: false
43952
44244
  }],
44245
+ "deviceState.claimFields": [{
44246
+ name: "deviceId",
44247
+ form: "single",
44248
+ optional: false
44249
+ }],
43953
44250
  "deviceState.getCapSlice": [{
43954
44251
  name: "deviceId",
43955
44252
  form: "single",
43956
44253
  optional: false
43957
44254
  }],
44255
+ "deviceState.getNativeCapSlice": [{
44256
+ name: "deviceId",
44257
+ form: "single",
44258
+ optional: false
44259
+ }],
43958
44260
  "deviceState.getSnapshot": [{
43959
44261
  name: "deviceId",
43960
44262
  form: "single",
43961
44263
  optional: false
43962
44264
  }],
44265
+ "deviceState.patchOwnedFields": [{
44266
+ name: "deviceId",
44267
+ form: "single",
44268
+ optional: false
44269
+ }],
44270
+ "deviceState.releaseClaim": [{
44271
+ name: "deviceId",
44272
+ form: "single",
44273
+ optional: false
44274
+ }],
43963
44275
  "deviceState.setCapSlice": [{
43964
44276
  name: "deviceId",
43965
44277
  form: "single",
@@ -46322,7 +46634,7 @@ var AdminUIAddon = class extends BaseAddon {
46322
46634
  capability: adminUiCapability,
46323
46635
  provider: {
46324
46636
  getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
46325
- getVersion: async () => ({ version: "1.2.281" })
46637
+ getVersion: async () => ({ version: "1.2.283" })
46326
46638
  }
46327
46639
  }];
46328
46640
  }