@camstack/addon-provider-homeassistant 1.2.145 → 1.2.146

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.
@@ -1,4 +1,5 @@
1
- //#region ../types/dist/event-category-BVDXG4tB.mjs
1
+ require("node:crypto");
2
+ //#region ../types/dist/event-category-C5xZWqz6.mjs
2
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
4
  EventCategory["SystemBoot"] = "system.boot";
4
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -362,6 +363,16 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
362
363
  */
363
364
  EventCategory["DeviceStateChanged"] = "device.state-changed";
364
365
  /**
366
+ * The NATIVE shadow of a cap a composition has claimed fields of moved
367
+ * (D663). Payload: `{ deviceId, capName, native }` — what the provider
368
+ * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
369
+ * the composer's native self-reads, and the public event routers refuse it
370
+ * (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
371
+ * truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
372
+ * `replace` claim, one per native write of that cap.
373
+ */
374
+ EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
375
+ /**
365
376
  * Frame occupancy for a camera CHANGED — a tracked object was gained or
366
377
  * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
367
378
  *
@@ -5359,7 +5370,7 @@ var ZodIssueCode = {
5359
5370
  var ZodFirstPartyTypeKind;
5360
5371
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5372
  //#endregion
5362
- //#region ../types/dist/sleep-BXv0ZcRa.mjs
5373
+ //#region ../types/dist/sleep-jkRQgtSc.mjs
5363
5374
  /**
5364
5375
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5376
  * window to float samples (D455).
@@ -7060,324 +7071,6 @@ function event(data) {
7060
7071
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
7061
7072
  var VersionOutputSchema$1 = object({ version: string() });
7062
7073
  method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
7063
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
7064
- DeviceType["Camera"] = "camera";
7065
- DeviceType["Hub"] = "hub";
7066
- DeviceType["Light"] = "light";
7067
- DeviceType["Siren"] = "siren";
7068
- DeviceType["Switch"] = "switch";
7069
- DeviceType["Sensor"] = "sensor";
7070
- DeviceType["Thermostat"] = "thermostat";
7071
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
7072
- * `climate-control` cap surface with `Thermostat` but renders a
7073
- * dedicated AC-appropriate control UI (mode chips, fan speed,
7074
- * independent vertical/horizontal swing). Sources: native Gree, and
7075
- * reusable by other AC integrations. */
7076
- DeviceType["Climate"] = "climate";
7077
- DeviceType["Button"] = "button";
7078
- /** Generic stateless event emitter — carries a device's EXACT declared
7079
- * event vocabulary verbatim (no normalization). Installed with the
7080
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
7081
- * HA bus events (e.g. `zha_event`, generic). */
7082
- DeviceType["EventEmitter"] = "event-emitter";
7083
- /** Firmware/software update entity — current vs available version,
7084
- * updatable flag, update state, and an install action. Installed with
7085
- * the `update` cap. Sources: Homematic firmware-update channels (and
7086
- * reusable by other providers, e.g. HA `update.*` entities). */
7087
- DeviceType["Update"] = "update";
7088
- DeviceType["Generic"] = "generic";
7089
- /** Generic notification delivery target (HA `notify.<service>`, future
7090
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
7091
- * endpoint; the `notifier` cap defines the send surface. */
7092
- DeviceType["Notifier"] = "notifier";
7093
- /** Pre-recorded action sequence with optional parameters
7094
- * (HA `script.*`). Runnable via `script-runner` cap. */
7095
- DeviceType["Script"] = "script";
7096
- /** Automation rule (HA `automation.*`) — enable/disable + manual
7097
- * trigger surface exposed via `automation-control` cap. */
7098
- DeviceType["Automation"] = "automation";
7099
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
7100
- DeviceType["Lock"] = "lock";
7101
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
7102
- * `valve.*`). `cover` cap with sub-roles for variant. */
7103
- DeviceType["Cover"] = "cover";
7104
- /** Pipe / water / gas valve with open/close/stop and optional
7105
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
7106
- * modelled on the same open/closed lifecycle. */
7107
- DeviceType["Valve"] = "valve";
7108
- /** Humidifier / dehumidifier with on/off + target humidity + mode
7109
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
7110
- * modelled on the same target / mode lifecycle. */
7111
- DeviceType["Humidifier"] = "humidifier";
7112
- /** Water heater / boiler with target temperature + operation mode +
7113
- * away mode (HA `water_heater.*`). `water-heater` cap — a
7114
- * climate-family actuator. */
7115
- DeviceType["WaterHeater"] = "water-heater";
7116
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
7117
- DeviceType["Fan"] = "fan";
7118
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
7119
- * the camera surface — those use `Camera`. `media-player` cap. */
7120
- DeviceType["MediaPlayer"] = "media-player";
7121
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
7122
- * `alarm-panel` cap. */
7123
- DeviceType["AlarmPanel"] = "alarm-panel";
7124
- /** Generic user-settable input (HA `number` / `input_number` / `select`
7125
- * / `input_select` / `text` / `input_text` / `input_datetime`).
7126
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
7127
- * TextControl / DateTimeControl. */
7128
- DeviceType["Control"] = "control";
7129
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
7130
- * `presence` cap. */
7131
- DeviceType["Presence"] = "presence";
7132
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
7133
- * `weather` cap. */
7134
- DeviceType["Weather"] = "weather";
7135
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
7136
- DeviceType["Vacuum"] = "vacuum";
7137
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
7138
- * `lawn-mower-control` cap. */
7139
- DeviceType["LawnMower"] = "lawn-mower";
7140
- /** Physical HA device group — parent container for entity-children
7141
- * adopted from a single HA device entry. Not renderable as a
7142
- * standalone device; exists only to anchor child entities. */
7143
- DeviceType["Container"] = "container";
7144
- /** Single still-image entity (HA `image.*`). Read-only display of an
7145
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
7146
- DeviceType["Image"] = "image";
7147
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
7148
- * level, battery, desiccant life, feeding state and manual-feed /
7149
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
7150
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
7151
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
7152
- * integrations sharing the same food/desiccant/hopper surface. */
7153
- DeviceType["PetFeeder"] = "pet-feeder";
7154
- return DeviceType;
7155
- }({});
7156
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
7157
- DeviceFeature["BatteryOperated"] = "battery-operated";
7158
- DeviceFeature["Rebootable"] = "rebootable";
7159
- /**
7160
- * Device supports an on-demand re-sync of its derived spec with its
7161
- * upstream source — drives the generic Re-sync button. The owning
7162
- * provider implements the action via the `device-adoption.resync` cap.
7163
- */
7164
- DeviceFeature["Resyncable"] = "resyncable";
7165
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
7166
- DeviceFeature["DoorbellButton"] = "doorbell-button";
7167
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
7168
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
7169
- /**
7170
- * Camera supports the on-firmware autotrack subsystem (subject-
7171
- * following). Distinct from `PanTiltZoom` because not every PTZ
7172
- * camera ships autotrack — the admin UI uses this flag to gate
7173
- * the autotrack toggle / settings card without re-deriving from
7174
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
7175
- * driver sets this feature when probe confirms the firmware
7176
- * surface, and registers the cap in the same code path.
7177
- */
7178
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
7179
- /**
7180
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
7181
- * motion detection automatically activates this device. Mirrors
7182
- * `motion-trigger` cap registration: drivers set this feature in the
7183
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
7184
- *
7185
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
7186
- * fast scalar without binding fetch), notifier rules, and `listAll`
7187
- * filters that want "all devices with on-motion behaviour".
7188
- */
7189
- DeviceFeature["MotionTrigger"] = "motion-trigger";
7190
- /** Light supports rgb-triplet color via `color` cap. */
7191
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
7192
- /** Light supports HSV color via `color` cap. */
7193
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
7194
- /** Light supports color-temperature (mired) via `color` cap. */
7195
- DeviceFeature["LightColorMired"] = "light-color-mired";
7196
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
7197
- * targetHigh). Gates the range slider UI. */
7198
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
7199
- /** Thermostat exposes target humidity and/or current humidity
7200
- * readings. Gates the humidity controls. */
7201
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
7202
- /** Thermostat exposes a fan-mode selector. */
7203
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
7204
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
7205
- DeviceFeature["ClimatePreset"] = "climate-preset";
7206
- /** Thermostat exposes a vertical louver swing toggle. Gates the
7207
- * vertical-swing switch in the climate UI. Independent of horizontal. */
7208
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
7209
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
7210
- * horizontal-swing switch in the climate UI. Independent of vertical. */
7211
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
7212
- /** Cover exposes intermediate position control (0..100). Gates the
7213
- * position slider UI. */
7214
- DeviceFeature["CoverPositionable"] = "cover-positionable";
7215
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
7216
- DeviceFeature["CoverTilt"] = "cover-tilt";
7217
- /** Valve exposes intermediate position control (0..100). Gates the
7218
- * position slider / drag surface UI. */
7219
- DeviceFeature["ValvePositionable"] = "valve-positionable";
7220
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
7221
- DeviceFeature["FanSpeed"] = "fan-speed";
7222
- /** Fan exposes a preset mode selector. */
7223
- DeviceFeature["FanPreset"] = "fan-preset";
7224
- /** Fan exposes blade direction (forward/reverse) — typical of
7225
- * ceiling fans. */
7226
- DeviceFeature["FanDirection"] = "fan-direction";
7227
- /** Fan exposes an oscillation toggle. */
7228
- DeviceFeature["FanOscillating"] = "fan-oscillating";
7229
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
7230
- * field on the UI lock-controls panel. */
7231
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
7232
- /** Lock supports a latch-release ("open door") action distinct from
7233
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
7234
- * `supported_features`. Gates the Open Door button in the UI. */
7235
- DeviceFeature["LockOpen"] = "lock-open";
7236
- /** Media player exposes a seek-to-position surface. */
7237
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
7238
- /** Media player exposes a volume-level setter. */
7239
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
7240
- /** Media player exposes a mute toggle distinct from volume=0. */
7241
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
7242
- /** Media player exposes a shuffle toggle. */
7243
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
7244
- /** Media player exposes a repeat mode (off / all / one). */
7245
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
7246
- /** Media player exposes a source / input selector. */
7247
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
7248
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
7249
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
7250
- /** Media player exposes next-track. */
7251
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
7252
- /** Media player exposes previous-track. */
7253
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
7254
- /** Media player exposes stop distinct from pause. */
7255
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
7256
- /** Alarm panel requires a PIN code on arm/disarm. */
7257
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
7258
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
7259
- * addition to a textual location. */
7260
- DeviceFeature["PresenceGps"] = "presence-gps";
7261
- /** Notifier accepts an inline / URL image attachment. */
7262
- DeviceFeature["NotifierImage"] = "notifier-image";
7263
- /** Notifier accepts a priority hint (high/normal/low). */
7264
- DeviceFeature["NotifierPriority"] = "notifier-priority";
7265
- /** Notifier accepts a free-form `data` payload for platform-specific
7266
- * fields. */
7267
- DeviceFeature["NotifierData"] = "notifier-data";
7268
- /** Notifier supports interactive action buttons / callbacks. */
7269
- DeviceFeature["NotifierActions"] = "notifier-actions";
7270
- /** Notifier supports per-call recipient targeting (multi-user). */
7271
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
7272
- /** Script runner accepts a variables map on each run invocation. */
7273
- DeviceFeature["ScriptVariables"] = "script-variables";
7274
- /** Automation `trigger` accepts a skipCondition flag — fires the
7275
- * automation's actions while bypassing its condition block. */
7276
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
7277
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
7278
- * map tile in the vacuum UI. */
7279
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
7280
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
7281
- * switches group. */
7282
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
7283
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
7284
- * mower map tile. */
7285
- DeviceFeature["MowerHasMap"] = "mower-has-map";
7286
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
7287
- * spots) — gates the Mow action + map selectors. */
7288
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
7289
- return DeviceFeature;
7290
- }({});
7291
- /**
7292
- * Semantic role a device plays within its parent. Populated by driver
7293
- * addons when creating accessory devices (Reolink siren/floodlight/
7294
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
7295
- * admin UI to pick icons, labels, and widgets — a `Switch` with
7296
- * `role: Floodlight` renders as a bulb with a brightness slider,
7297
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
7298
- *
7299
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
7300
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
7301
- */
7302
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7303
- DeviceRole["Siren"] = "siren";
7304
- DeviceRole["Floodlight"] = "floodlight";
7305
- DeviceRole["Spotlight"] = "spotlight";
7306
- DeviceRole["PirSensor"] = "pir-sensor";
7307
- DeviceRole["Chime"] = "chime";
7308
- DeviceRole["Autotrack"] = "autotrack";
7309
- DeviceRole["Nightvision"] = "nightvision";
7310
- DeviceRole["PrivacyMask"] = "privacy-mask";
7311
- DeviceRole["Doorbell"] = "doorbell";
7312
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
7313
- * real Switch device for UI rendering / export adapters. */
7314
- DeviceRole["BinaryHelper"] = "binary-helper";
7315
- /** Generic motion / occupancy / moving event source. Distinct from
7316
- * the camera accessory PirSensor role: that one is a camera child;
7317
- * this is a standalone HA / 3rd-party motion sensor. */
7318
- DeviceRole["MotionSensor"] = "motion-sensor";
7319
- DeviceRole["ContactSensor"] = "contact-sensor";
7320
- DeviceRole["LeakSensor"] = "leak-sensor";
7321
- DeviceRole["SmokeSensor"] = "smoke-sensor";
7322
- DeviceRole["COSensor"] = "co-sensor";
7323
- DeviceRole["GasSensor"] = "gas-sensor";
7324
- DeviceRole["TamperSensor"] = "tamper-sensor";
7325
- DeviceRole["VibrationSensor"] = "vibration-sensor";
7326
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
7327
- DeviceRole["SoundSensor"] = "sound-sensor";
7328
- /** Fallback for `binary_sensor` without a known `device_class`. */
7329
- DeviceRole["BinarySensor"] = "binary-sensor";
7330
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
7331
- DeviceRole["HumiditySensor"] = "humidity-sensor";
7332
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
7333
- DeviceRole["PressureSensor"] = "pressure-sensor";
7334
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
7335
- DeviceRole["WindSensor"] = "wind-sensor";
7336
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
7337
- DeviceRole["RainSensor"] = "rain-sensor";
7338
- /** UV index (weather-station `uv-sensor` cap). */
7339
- DeviceRole["UvSensor"] = "uv-sensor";
7340
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
7341
- * Distinct from AmbientLightSensor (lux). */
7342
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
7343
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
7344
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
7345
- DeviceRole["PowerSensor"] = "power-sensor";
7346
- DeviceRole["EnergySensor"] = "energy-sensor";
7347
- DeviceRole["VoltageSensor"] = "voltage-sensor";
7348
- DeviceRole["CurrentSensor"] = "current-sensor";
7349
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
7350
- /** Battery level (numeric % via `sensor` OR low-bool via
7351
- * `binary_sensor` — the cap distinguishes via the value type). */
7352
- DeviceRole["BatterySensor"] = "battery-sensor";
7353
- /** Fallback for `sensor` numeric without a known `device_class`. */
7354
- DeviceRole["NumericSensor"] = "numeric-sensor";
7355
- /** String / enum state (HA `sensor` with `state_class: enum` or
7356
- * `attributes.options`). */
7357
- DeviceRole["EnumSensor"] = "enum-sensor";
7358
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
7359
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
7360
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
7361
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
7362
- /** Last-resort fallback when nothing else matches. */
7363
- DeviceRole["GenericSensor"] = "generic-sensor";
7364
- DeviceRole["NumericControl"] = "numeric-control";
7365
- DeviceRole["SelectControl"] = "select-control";
7366
- DeviceRole["TextControl"] = "text-control";
7367
- DeviceRole["DateTimeControl"] = "datetime-control";
7368
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
7369
- * rich features (image, priority, channel routing). */
7370
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
7371
- /** Chat / messaging service (HA `notify.telegram_*`,
7372
- * `notify.discord_*`, etc.). */
7373
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
7374
- /** Email-based delivery (HA `notify.smtp`, etc.). */
7375
- DeviceRole["EmailNotifier"] = "email-notifier";
7376
- /** Fallback when the notifier service name doesn't match a known
7377
- * pattern. */
7378
- DeviceRole["GenericNotifier"] = "generic-notifier";
7379
- return DeviceRole;
7380
- }({});
7381
7074
  var StaticDirOutputSchema = object({ staticDir: string() });
7382
7075
  var VersionOutputSchema = object({ version: string() });
7383
7076
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -7445,7 +7138,325 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7445
7138
  auth: "admin"
7446
7139
  }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7447
7140
  //#endregion
7448
- //#region ../types/dist/err-msg-IQTHeDzc.mjs
7141
+ //#region ../types/dist/err-msg-DX6i_MY4.mjs
7142
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
7143
+ DeviceType["Camera"] = "camera";
7144
+ DeviceType["Hub"] = "hub";
7145
+ DeviceType["Light"] = "light";
7146
+ DeviceType["Siren"] = "siren";
7147
+ DeviceType["Switch"] = "switch";
7148
+ DeviceType["Sensor"] = "sensor";
7149
+ DeviceType["Thermostat"] = "thermostat";
7150
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
7151
+ * `climate-control` cap surface with `Thermostat` but renders a
7152
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
7153
+ * independent vertical/horizontal swing). Sources: native Gree, and
7154
+ * reusable by other AC integrations. */
7155
+ DeviceType["Climate"] = "climate";
7156
+ DeviceType["Button"] = "button";
7157
+ /** Generic stateless event emitter — carries a device's EXACT declared
7158
+ * event vocabulary verbatim (no normalization). Installed with the
7159
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
7160
+ * HA bus events (e.g. `zha_event`, generic). */
7161
+ DeviceType["EventEmitter"] = "event-emitter";
7162
+ /** Firmware/software update entity — current vs available version,
7163
+ * updatable flag, update state, and an install action. Installed with
7164
+ * the `update` cap. Sources: Homematic firmware-update channels (and
7165
+ * reusable by other providers, e.g. HA `update.*` entities). */
7166
+ DeviceType["Update"] = "update";
7167
+ DeviceType["Generic"] = "generic";
7168
+ /** Generic notification delivery target (HA `notify.<service>`, future
7169
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
7170
+ * endpoint; the `notifier` cap defines the send surface. */
7171
+ DeviceType["Notifier"] = "notifier";
7172
+ /** Pre-recorded action sequence with optional parameters
7173
+ * (HA `script.*`). Runnable via `script-runner` cap. */
7174
+ DeviceType["Script"] = "script";
7175
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
7176
+ * trigger surface exposed via `automation-control` cap. */
7177
+ DeviceType["Automation"] = "automation";
7178
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
7179
+ DeviceType["Lock"] = "lock";
7180
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
7181
+ * `valve.*`). `cover` cap with sub-roles for variant. */
7182
+ DeviceType["Cover"] = "cover";
7183
+ /** Pipe / water / gas valve with open/close/stop and optional
7184
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
7185
+ * modelled on the same open/closed lifecycle. */
7186
+ DeviceType["Valve"] = "valve";
7187
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
7188
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
7189
+ * modelled on the same target / mode lifecycle. */
7190
+ DeviceType["Humidifier"] = "humidifier";
7191
+ /** Water heater / boiler with target temperature + operation mode +
7192
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
7193
+ * climate-family actuator. */
7194
+ DeviceType["WaterHeater"] = "water-heater";
7195
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
7196
+ DeviceType["Fan"] = "fan";
7197
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
7198
+ * the camera surface — those use `Camera`. `media-player` cap. */
7199
+ DeviceType["MediaPlayer"] = "media-player";
7200
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
7201
+ * `alarm-panel` cap. */
7202
+ DeviceType["AlarmPanel"] = "alarm-panel";
7203
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
7204
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
7205
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
7206
+ * TextControl / DateTimeControl. */
7207
+ DeviceType["Control"] = "control";
7208
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
7209
+ * `presence` cap. */
7210
+ DeviceType["Presence"] = "presence";
7211
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
7212
+ * `weather` cap. */
7213
+ DeviceType["Weather"] = "weather";
7214
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
7215
+ DeviceType["Vacuum"] = "vacuum";
7216
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
7217
+ * `lawn-mower-control` cap. */
7218
+ DeviceType["LawnMower"] = "lawn-mower";
7219
+ /** Physical HA device group — parent container for entity-children
7220
+ * adopted from a single HA device entry. Not renderable as a
7221
+ * standalone device; exists only to anchor child entities. */
7222
+ DeviceType["Container"] = "container";
7223
+ /** Single still-image entity (HA `image.*`). Read-only display of an
7224
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
7225
+ DeviceType["Image"] = "image";
7226
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
7227
+ * level, battery, desiccant life, feeding state and manual-feed /
7228
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
7229
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
7230
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
7231
+ * integrations sharing the same food/desiccant/hopper surface. */
7232
+ DeviceType["PetFeeder"] = "pet-feeder";
7233
+ return DeviceType;
7234
+ }({});
7235
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
7236
+ DeviceFeature["BatteryOperated"] = "battery-operated";
7237
+ DeviceFeature["Rebootable"] = "rebootable";
7238
+ /**
7239
+ * Device supports an on-demand re-sync of its derived spec with its
7240
+ * upstream source — drives the generic Re-sync button. The owning
7241
+ * provider implements the action via the `device-adoption.resync` cap.
7242
+ */
7243
+ DeviceFeature["Resyncable"] = "resyncable";
7244
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
7245
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
7246
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
7247
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
7248
+ /**
7249
+ * Camera supports the on-firmware autotrack subsystem (subject-
7250
+ * following). Distinct from `PanTiltZoom` because not every PTZ
7251
+ * camera ships autotrack — the admin UI uses this flag to gate
7252
+ * the autotrack toggle / settings card without re-deriving from
7253
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
7254
+ * driver sets this feature when probe confirms the firmware
7255
+ * surface, and registers the cap in the same code path.
7256
+ */
7257
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
7258
+ /**
7259
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
7260
+ * motion detection automatically activates this device. Mirrors
7261
+ * `motion-trigger` cap registration: drivers set this feature in the
7262
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
7263
+ *
7264
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
7265
+ * fast scalar without binding fetch), notifier rules, and `listAll`
7266
+ * filters that want "all devices with on-motion behaviour".
7267
+ */
7268
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
7269
+ /** Light supports rgb-triplet color via `color` cap. */
7270
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
7271
+ /** Light supports HSV color via `color` cap. */
7272
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
7273
+ /** Light supports color-temperature (mired) via `color` cap. */
7274
+ DeviceFeature["LightColorMired"] = "light-color-mired";
7275
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
7276
+ * targetHigh). Gates the range slider UI. */
7277
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
7278
+ /** Thermostat exposes target humidity and/or current humidity
7279
+ * readings. Gates the humidity controls. */
7280
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
7281
+ /** Thermostat exposes a fan-mode selector. */
7282
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
7283
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
7284
+ DeviceFeature["ClimatePreset"] = "climate-preset";
7285
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
7286
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
7287
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
7288
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
7289
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
7290
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
7291
+ /** Cover exposes intermediate position control (0..100). Gates the
7292
+ * position slider UI. */
7293
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
7294
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
7295
+ DeviceFeature["CoverTilt"] = "cover-tilt";
7296
+ /** Valve exposes intermediate position control (0..100). Gates the
7297
+ * position slider / drag surface UI. */
7298
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
7299
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
7300
+ DeviceFeature["FanSpeed"] = "fan-speed";
7301
+ /** Fan exposes a preset mode selector. */
7302
+ DeviceFeature["FanPreset"] = "fan-preset";
7303
+ /** Fan exposes blade direction (forward/reverse) — typical of
7304
+ * ceiling fans. */
7305
+ DeviceFeature["FanDirection"] = "fan-direction";
7306
+ /** Fan exposes an oscillation toggle. */
7307
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
7308
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
7309
+ * field on the UI lock-controls panel. */
7310
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
7311
+ /** Lock supports a latch-release ("open door") action distinct from
7312
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
7313
+ * `supported_features`. Gates the Open Door button in the UI. */
7314
+ DeviceFeature["LockOpen"] = "lock-open";
7315
+ /** Media player exposes a seek-to-position surface. */
7316
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
7317
+ /** Media player exposes a volume-level setter. */
7318
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
7319
+ /** Media player exposes a mute toggle distinct from volume=0. */
7320
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
7321
+ /** Media player exposes a shuffle toggle. */
7322
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
7323
+ /** Media player exposes a repeat mode (off / all / one). */
7324
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
7325
+ /** Media player exposes a source / input selector. */
7326
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
7327
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
7328
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
7329
+ /** Media player exposes next-track. */
7330
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
7331
+ /** Media player exposes previous-track. */
7332
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
7333
+ /** Media player exposes stop distinct from pause. */
7334
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
7335
+ /** Alarm panel requires a PIN code on arm/disarm. */
7336
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
7337
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
7338
+ * addition to a textual location. */
7339
+ DeviceFeature["PresenceGps"] = "presence-gps";
7340
+ /** Notifier accepts an inline / URL image attachment. */
7341
+ DeviceFeature["NotifierImage"] = "notifier-image";
7342
+ /** Notifier accepts a priority hint (high/normal/low). */
7343
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
7344
+ /** Notifier accepts a free-form `data` payload for platform-specific
7345
+ * fields. */
7346
+ DeviceFeature["NotifierData"] = "notifier-data";
7347
+ /** Notifier supports interactive action buttons / callbacks. */
7348
+ DeviceFeature["NotifierActions"] = "notifier-actions";
7349
+ /** Notifier supports per-call recipient targeting (multi-user). */
7350
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
7351
+ /** Script runner accepts a variables map on each run invocation. */
7352
+ DeviceFeature["ScriptVariables"] = "script-variables";
7353
+ /** Automation `trigger` accepts a skipCondition flag — fires the
7354
+ * automation's actions while bypassing its condition block. */
7355
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
7356
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
7357
+ * map tile in the vacuum UI. */
7358
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
7359
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
7360
+ * switches group. */
7361
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
7362
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
7363
+ * mower map tile. */
7364
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
7365
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
7366
+ * spots) — gates the Mow action + map selectors. */
7367
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
7368
+ return DeviceFeature;
7369
+ }({});
7370
+ /**
7371
+ * Semantic role a device plays within its parent. Populated by driver
7372
+ * addons when creating accessory devices (Reolink siren/floodlight/
7373
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
7374
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
7375
+ * `role: Floodlight` renders as a bulb with a brightness slider,
7376
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
7377
+ *
7378
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
7379
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
7380
+ */
7381
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7382
+ DeviceRole["Siren"] = "siren";
7383
+ DeviceRole["Floodlight"] = "floodlight";
7384
+ DeviceRole["Spotlight"] = "spotlight";
7385
+ DeviceRole["PirSensor"] = "pir-sensor";
7386
+ DeviceRole["Chime"] = "chime";
7387
+ DeviceRole["Autotrack"] = "autotrack";
7388
+ DeviceRole["Nightvision"] = "nightvision";
7389
+ DeviceRole["PrivacyMask"] = "privacy-mask";
7390
+ DeviceRole["Doorbell"] = "doorbell";
7391
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
7392
+ * real Switch device for UI rendering / export adapters. */
7393
+ DeviceRole["BinaryHelper"] = "binary-helper";
7394
+ /** Generic motion / occupancy / moving event source. Distinct from
7395
+ * the camera accessory PirSensor role: that one is a camera child;
7396
+ * this is a standalone HA / 3rd-party motion sensor. */
7397
+ DeviceRole["MotionSensor"] = "motion-sensor";
7398
+ DeviceRole["ContactSensor"] = "contact-sensor";
7399
+ DeviceRole["LeakSensor"] = "leak-sensor";
7400
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
7401
+ DeviceRole["COSensor"] = "co-sensor";
7402
+ DeviceRole["GasSensor"] = "gas-sensor";
7403
+ DeviceRole["TamperSensor"] = "tamper-sensor";
7404
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
7405
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
7406
+ DeviceRole["SoundSensor"] = "sound-sensor";
7407
+ /** Fallback for `binary_sensor` without a known `device_class`. */
7408
+ DeviceRole["BinarySensor"] = "binary-sensor";
7409
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
7410
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
7411
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
7412
+ DeviceRole["PressureSensor"] = "pressure-sensor";
7413
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
7414
+ DeviceRole["WindSensor"] = "wind-sensor";
7415
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
7416
+ DeviceRole["RainSensor"] = "rain-sensor";
7417
+ /** UV index (weather-station `uv-sensor` cap). */
7418
+ DeviceRole["UvSensor"] = "uv-sensor";
7419
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
7420
+ * Distinct from AmbientLightSensor (lux). */
7421
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
7422
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
7423
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
7424
+ DeviceRole["PowerSensor"] = "power-sensor";
7425
+ DeviceRole["EnergySensor"] = "energy-sensor";
7426
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
7427
+ DeviceRole["CurrentSensor"] = "current-sensor";
7428
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
7429
+ /** Battery level (numeric % via `sensor` OR low-bool via
7430
+ * `binary_sensor` — the cap distinguishes via the value type). */
7431
+ DeviceRole["BatterySensor"] = "battery-sensor";
7432
+ /** Fallback for `sensor` numeric without a known `device_class`. */
7433
+ DeviceRole["NumericSensor"] = "numeric-sensor";
7434
+ /** String / enum state (HA `sensor` with `state_class: enum` or
7435
+ * `attributes.options`). */
7436
+ DeviceRole["EnumSensor"] = "enum-sensor";
7437
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
7438
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
7439
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
7440
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
7441
+ /** Last-resort fallback when nothing else matches. */
7442
+ DeviceRole["GenericSensor"] = "generic-sensor";
7443
+ DeviceRole["NumericControl"] = "numeric-control";
7444
+ DeviceRole["SelectControl"] = "select-control";
7445
+ DeviceRole["TextControl"] = "text-control";
7446
+ DeviceRole["DateTimeControl"] = "datetime-control";
7447
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
7448
+ * rich features (image, priority, channel routing). */
7449
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
7450
+ /** Chat / messaging service (HA `notify.telegram_*`,
7451
+ * `notify.discord_*`, etc.). */
7452
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
7453
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
7454
+ DeviceRole["EmailNotifier"] = "email-notifier";
7455
+ /** Fallback when the notifier service name doesn't match a known
7456
+ * pattern. */
7457
+ DeviceRole["GenericNotifier"] = "generic-notifier";
7458
+ return DeviceRole;
7459
+ }({});
7449
7460
  /**
7450
7461
  import { errMsg } from '@camstack/types'
7451
7462
  * Extract a human-readable message from an unknown error value.
@@ -7456,6 +7467,757 @@ function errMsg(err) {
7456
7467
  if (typeof err === "string") return err;
7457
7468
  return String(err);
7458
7469
  }
7470
+ //#endregion
7471
+ //#region ../types/dist/composition-CPFIlFfw.mjs
7472
+ /**
7473
+ * Error types for the safe expression engine. Two distinct classes so callers
7474
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
7475
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
7476
+ */
7477
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7478
+ * the failure is anchored to a character (author-facing inline feedback). */
7479
+ var ExpressionParseError = class extends Error {
7480
+ position;
7481
+ constructor(message, position) {
7482
+ super(message);
7483
+ this.name = "ExpressionParseError";
7484
+ this.position = position;
7485
+ }
7486
+ };
7487
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7488
+ * result, unknown builtin, step-budget exceeded). */
7489
+ var ExpressionEvalError = class extends Error {
7490
+ constructor(message) {
7491
+ super(message);
7492
+ this.name = "ExpressionEvalError";
7493
+ }
7494
+ };
7495
+ function asFiniteNumber(value, name, index) {
7496
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7497
+ return value;
7498
+ }
7499
+ function asString(value, name, index) {
7500
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
7501
+ return value;
7502
+ }
7503
+ function finiteResult(value, name) {
7504
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
7505
+ return value;
7506
+ }
7507
+ function allFiniteNumbers(args, name) {
7508
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
7509
+ }
7510
+ function asBoolean(value, name, index) {
7511
+ if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
7512
+ return value;
7513
+ }
7514
+ /** A plain decimal (optional sign, fraction, exponent) — never hex, never `Infinity`, never a unit suffix. */
7515
+ var DECIMAL = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
7516
+ /**
7517
+ * `number(x)`: a finite number, or a string that IS one, as a number; every
7518
+ * other value — `''`, `'unavailable'`, a boolean, null — is `null`
7519
+ * (unavailable, never 0: D393). The one bridge from a string-valued source (an
7520
+ * HA sensor with a unit and no device_class is an `enum-sensor`) to a number.
7521
+ */
7522
+ function toNumberOrNull(value) {
7523
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
7524
+ if (typeof value !== "string") return null;
7525
+ const trimmed = value.trim();
7526
+ if (!DECIMAL.test(trimmed)) return null;
7527
+ const parsed = Number(trimmed);
7528
+ return Number.isFinite(parsed) ? parsed : null;
7529
+ }
7530
+ var INF = Number.POSITIVE_INFINITY;
7531
+ var table = {
7532
+ min: {
7533
+ minArgs: 1,
7534
+ maxArgs: INF,
7535
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
7536
+ },
7537
+ max: {
7538
+ minArgs: 1,
7539
+ maxArgs: INF,
7540
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
7541
+ },
7542
+ abs: {
7543
+ minArgs: 1,
7544
+ maxArgs: 1,
7545
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
7546
+ },
7547
+ floor: {
7548
+ minArgs: 1,
7549
+ maxArgs: 1,
7550
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
7551
+ },
7552
+ ceil: {
7553
+ minArgs: 1,
7554
+ maxArgs: 1,
7555
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
7556
+ },
7557
+ sqrt: {
7558
+ minArgs: 1,
7559
+ maxArgs: 1,
7560
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
7561
+ },
7562
+ round: {
7563
+ minArgs: 1,
7564
+ maxArgs: 2,
7565
+ apply: (args) => {
7566
+ const x = asFiniteNumber(args[0], "round", 0);
7567
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
7568
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
7569
+ const factor = 10 ** digits;
7570
+ return finiteResult(Math.round(x * factor) / factor, "round");
7571
+ }
7572
+ },
7573
+ pow: {
7574
+ minArgs: 2,
7575
+ maxArgs: 2,
7576
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
7577
+ },
7578
+ clamp: {
7579
+ minArgs: 3,
7580
+ maxArgs: 3,
7581
+ apply: (args) => {
7582
+ const x = asFiniteNumber(args[0], "clamp", 0);
7583
+ const lo = asFiniteNumber(args[1], "clamp", 1);
7584
+ const hi = asFiniteNumber(args[2], "clamp", 2);
7585
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
7586
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
7587
+ }
7588
+ },
7589
+ avg: {
7590
+ minArgs: 1,
7591
+ maxArgs: INF,
7592
+ apply: (args) => {
7593
+ const nums = allFiniteNumbers(args, "avg");
7594
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
7595
+ }
7596
+ },
7597
+ sum: {
7598
+ minArgs: 1,
7599
+ maxArgs: INF,
7600
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
7601
+ },
7602
+ coalesce: {
7603
+ minArgs: 1,
7604
+ maxArgs: INF,
7605
+ apply: (args) => {
7606
+ for (const a of args) if (a !== null) return a;
7607
+ return null;
7608
+ }
7609
+ },
7610
+ age: {
7611
+ minArgs: 2,
7612
+ maxArgs: 2,
7613
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
7614
+ },
7615
+ convert: {
7616
+ minArgs: 3,
7617
+ maxArgs: 3,
7618
+ apply: (args, hooks) => {
7619
+ const x = asFiniteNumber(args[0], "convert", 0);
7620
+ const from = asString(args[1], "convert", 1).trim();
7621
+ const to = asString(args[2], "convert", 2).trim();
7622
+ if (hooks.convert) {
7623
+ const out = hooks.convert(x, from, to);
7624
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
7625
+ return finiteResult(out, "convert");
7626
+ }
7627
+ if (from === to) return x;
7628
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
7629
+ }
7630
+ },
7631
+ any: {
7632
+ minArgs: 1,
7633
+ maxArgs: INF,
7634
+ apply: (args) => args.map((a, i) => asBoolean(a, "any", i)).some((b) => b)
7635
+ },
7636
+ all: {
7637
+ minArgs: 1,
7638
+ maxArgs: INF,
7639
+ apply: (args) => args.map((a, i) => asBoolean(a, "all", i)).every((b) => b)
7640
+ },
7641
+ within: {
7642
+ minArgs: 2,
7643
+ maxArgs: 2,
7644
+ apply: (args, hooks) => {
7645
+ const windowMs = asFiniteNumber(args[1], "within", 1);
7646
+ if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
7647
+ const at = args[0];
7648
+ if (at === null) return false;
7649
+ const ts = asFiniteNumber(at, "within", 0);
7650
+ const now = hooks.now;
7651
+ if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
7652
+ const inside = now - ts <= windowMs;
7653
+ if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
7654
+ return inside;
7655
+ }
7656
+ },
7657
+ number: {
7658
+ minArgs: 1,
7659
+ maxArgs: 1,
7660
+ apply: (args) => toNumberOrNull(args[0])
7661
+ },
7662
+ latest: {
7663
+ minArgs: 1,
7664
+ maxArgs: INF,
7665
+ apply: (args) => {
7666
+ const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
7667
+ return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
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
+ fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
8184
+ /**
8185
+ * Items of the cap's item array, keyed by item key (D663). Naming `items`
8186
+ * owns the WHOLE array field: the composed value holds exactly these items.
8187
+ */
8188
+ items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
8189
+ /** RESERVED for slice 3; refused by the validator when non-empty. */
8190
+ commands: record(string().min(1), CompositionCommandTargetSchema).optional()
8191
+ }), object({
8192
+ kind: literal("passthrough"),
8193
+ cap: string().min(1),
8194
+ source: CompositionSourceRefSchema
8195
+ })]);
8196
+ var CompositionSchema = object({
8197
+ target: discriminatedUnion("kind", [object({
8198
+ kind: literal("new"),
8199
+ type: _enum(DeviceType),
8200
+ role: _enum(DeviceRole).optional()
8201
+ }), object({
8202
+ kind: literal("existing"),
8203
+ device: CompositionSourceRefSchema
8204
+ })]),
8205
+ features: array(CompositionFeatureSchema).min(1).max(16)
8206
+ }).superRefine((composition, ctx) => {
8207
+ const seen = /* @__PURE__ */ new Set();
8208
+ composition.features.forEach((feature, index) => {
8209
+ if (seen.has(feature.cap)) ctx.addIssue({
8210
+ code: "custom",
8211
+ message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
8212
+ path: [
8213
+ "features",
8214
+ index,
8215
+ "cap"
8216
+ ]
8217
+ });
8218
+ seen.add(feature.cap);
8219
+ });
8220
+ });
7459
8221
  new Set([
7460
8222
  "track",
7461
8223
  "summary",
@@ -9507,1023 +10269,480 @@ var DecoderSessionConfigSchema = object({
9507
10269
  */
9508
10270
  deviceId: number().int().nonnegative().optional(),
9509
10271
  /**
9510
- * Free-form tag for log scoping. Stream-broker uses
9511
- * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
9512
- * on every line so `grep tag=broker:5/high` filters one camera
9513
- * profile cleanly.
9514
- */
9515
- tag: string().optional(),
9516
- /**
9517
- * Where the session delivers decoded frames (Phase 5 / D9):
9518
- *
9519
- * - `'callback'` (default) — the legacy pixel path: decoded frames are
9520
- * buffered as `DecodedFrame`s and drained via `pullFrames`.
9521
- * - `'shm'` — the shared-memory frame plane: decoded frames are written
9522
- * into an OS shared-memory ring and drained as zero-pixel
9523
- * `FrameHandle`s via `pullHandles`. A session is one mode or the
9524
- * other — `pullFrames` returns nothing for an `'shm'` session and
9525
- * `pullHandles` returns nothing for a `'callback'` session.
9526
- */
9527
- frameSink: _enum(["callback", "shm"]).default("callback"),
9528
- /**
9529
- * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
9530
- * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
9531
- * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
9532
- * stream-broker's `streamingDebug` gate — off by default so production logs
9533
- * stay quiet and the emit path pays zero per-frame cost when disabled.
9534
- */
9535
- debug: boolean().optional()
9536
- });
9537
- /**
9538
- * Per-component log CHANNELS — the gate a hot path consults, and the registry
9539
- * an addon declares its channels in.
9540
- *
9541
- * ## Two axes, deliberately separated
9542
- *
9543
- * - **DECLARATION** — which channels exist. Only the addon knows:
9544
- * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
9545
- * baichuan/handshake. A hand-wired central list rots at the first addition,
9546
- * and rots silently. So a channel is declared where it is consulted, and the
9547
- * `log-channels` capability enumerates the declarations.
9548
- * - **VALUE** — at which level, for which scope, until when. That stays ONE
9549
- * thing: the logging settings document on the `system` cap. Two authorities
9550
- * over the values is the exact defect
9551
- * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
9552
- * remove; re-introducing it from the cure side would be grotesque.
9553
- *
9554
- * Nothing in this file reads a clock, an env var or a store. The registry is
9555
- * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
9556
- * the hot path with a value somebody actually read, and by
9557
- * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
9558
- * never reaches here, so it can neither disarm an armed channel nor arm a
9559
- * disarmed one (D49).
9560
- *
9561
- * ## The canonical call shape
9562
- *
9563
- * ```ts
9564
- * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
9565
- * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
9566
- * }
9567
- * ```
9568
- *
9569
- * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
9570
- * read. Disarmed, a call site costs one load and one branch, and the `extras`
9571
- * object literal is never constructed because it lives inside the branch. It
9572
- * is the same shape already proven in production at `stream-broker.ts:1650`,
9573
- * and the same discipline `LoggingGate.allowsDestination` uses for the
9574
- * destination floor (measured at 1.93 ns/call when off).
9575
- *
9576
- * ## Why a channel emits at `info`
9577
- *
9578
- * `loki-logging.addon.ts` pins the destination default at `info` and
9579
- * `loki-destination.ts` drops everything below it, so a line emitted at
9580
- * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
9581
- * minutes. A diagnostic that cannot be read an hour later is worse than no
9582
- * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
9583
- * emits at the channel's declared level, whose schema floor is `info`.
9584
- */
9585
- /**
9586
- * The level a channel writes at once armed.
9587
- *
9588
- * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
9589
- * not leave the process for Loki, and the whole point of arming a channel is
9590
- * to read it later.
9591
- */
9592
- var LogChannelLevelSchema = _enum([
9593
- "info",
9594
- "warn",
9595
- "error"
9596
- ]);
9597
- /**
9598
- * What an addon declares about one channel. No value, no state — a
9599
- * declaration is inert.
9600
- */
9601
- var LogChannelDescriptorSchema = object({
9602
- /**
9603
- * Dotted `area.thing`, unique across the workspace. `area` is conventionally
9604
- * the addon's short name so an operator reading a channel list can tell who
9605
- * owns it without a second lookup.
9606
- */
9607
- name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
9608
- /** One sentence: what the operator will SEE after arming it. */
9609
- description: string().min(1),
9610
- /** The level its lines are emitted at. Never below `info`. */
9611
- defaultLevel: LogChannelLevelSchema,
9612
- /**
9613
- * Whether this channel can be narrowed to a camera.
9614
- *
9615
- * `true` is a PROMISE with two halves, and both must hold: the gate is
9616
- * consulted with the numeric device id, AND every line the channel admits
9617
- * carries `tags: { deviceId }` with that same numeric id. The second half is
9618
- * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
9619
- * keeps `deviceId` out of the stream labels for cardinality, so the tag in
9620
- * the body is the only way to filter.
9621
- *
9622
- * A channel whose lines carry the device only in `meta` (or not at all) is
9623
- * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
9624
- * the operator narrows to one camera, sees nothing, and concludes the code
9625
- * path was never taken.
9626
- */
9627
- perDevice: boolean()
9628
- });
9629
- /**
9630
- * An armed window over one channel, as the document hands it to a mirror.
9631
- *
9632
- * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
9633
- * expires by itself, which is the one failure a boolean cannot avoid.
9634
- */
9635
- var LogChannelWindowSchema = object({
9636
- channel: string().min(1),
9637
- /** Epoch ms the window closes at. */
9638
- armedUntilMs: number(),
9639
- /** `null` = every camera. A non-empty list narrows to those numeric ids. */
9640
- deviceIds: array(number().int()).readonly().nullable()
9641
- });
9642
- var ClipModelMetaSchema = object({
9643
- /** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
9644
- textModelId: string().min(1),
9645
- /** Output dimension of BOTH towers. A vector of any other length is refused. */
9646
- embeddingDim: number().int().positive(),
9647
- /**
9648
- * The `vector-store` index this model's vectors live in. ONE feature space
9649
- * per index: two models share an index only when their vectors are ranked
9650
- * against each other, which for CLIP means never across families. MobileCLIP
9651
- * S1 and S2 share `object-clip` for history (separated by the row's
9652
- * `modelId`); any other model gets its own.
9653
- */
9654
- vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
9655
- /**
9656
- * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
9657
- * must be unique per tokenizer: every model's siblings land in ONE flat
9658
- * models directory, so two different tokenizers under the same name would
9659
- * silently overwrite each other (whichever model downloaded last wins, and
9660
- * the other one tokenises with the wrong vocabulary).
9661
- */
9662
- tokenizerFile: string().min(1),
9663
- /** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
9664
- contextLength: number().int().positive(),
9665
- /** Id the tokenizer pads with. */
9666
- padId: number().int().nonnegative(),
9667
- /**
9668
- * The search cosine floor at `strictness: 'loose'` — calibrated per model,
9669
- * because cosine bands are a property of the model: 0.2 keeps 96% of matched
9670
- * COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
9671
- */
9672
- searchMinScore: number().min(0).max(1)
9673
- });
9674
- var ClipSearchStrictnessSchema = _enum([
9675
- "loose",
9676
- "balanced",
9677
- "strict"
9678
- ]);
9679
- var MODEL_FORMATS = [
9680
- "onnx",
9681
- "coreml",
9682
- "openvino",
9683
- "tflite",
9684
- "pt",
9685
- "gguf"
9686
- ];
9687
- /**
9688
- * Multi-file format payload.
9689
- *
9690
- * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
9691
- * relative to the directory root — the downloader fetches each from
9692
- * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
9693
- * HuggingFace API (slower).
9694
- * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
9695
- * files fetched from the SAME remote directory as `url` and stored flat
9696
- * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
9697
- * weights next to `camstack-yolov9t.xml`.
9698
- */
9699
- var ModelFormatEntrySchema = object({
9700
- url: string(),
9701
- sizeMB: number(),
9702
- /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
9703
- isDirectory: boolean().optional(),
9704
- /** Multi-file payload (directory members or sibling files). */
9705
- files: array(string()).readonly().optional(),
9706
- /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
9707
- runtimes: array(_enum(["python"])).readonly().optional()
9708
- });
9709
- /**
9710
- * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
9711
- * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
9712
- */
9713
- var ModelExtraFileSchema = object({
9714
- url: string(),
9715
- filename: string(),
9716
- sizeMB: number()
9717
- });
9718
- /**
9719
- * Per-format payload map. Modelled as an explicit object (one optional key
9720
- * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
9721
- * record requires every key, but a catalog entry only ships a subset of
9722
- * formats.
9723
- */
9724
- var ModelFormatsSchema = object({
9725
- onnx: ModelFormatEntrySchema.optional(),
9726
- coreml: ModelFormatEntrySchema.optional(),
9727
- openvino: ModelFormatEntrySchema.optional(),
9728
- tflite: ModelFormatEntrySchema.optional(),
9729
- pt: ModelFormatEntrySchema.optional(),
9730
- gguf: ModelFormatEntrySchema.optional()
9731
- });
9732
- /**
9733
- * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
9734
- * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
9735
- * grouped Family→Tier→Variant picker renders identically in the config UI and
9736
- * in the pipeline/device steppers. The flat `id` stays the source of truth for
9737
- * resolution/download/persistence; this is a presentation overlay resolved back
9738
- * to an `id`.
9739
- */
9740
- var ModelVariantGroupSchema = object({
9741
- /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
9742
- family: string(),
9743
- /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
9744
- tier: string(),
9745
- /** Quantization axis. Omit ⇒ the fp32 base build. */
9746
- precision: _enum(["fp32", "int8"]).optional(),
9747
- /**
9748
- * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
9749
- * latency-optimized export (e.g. ReLU-activation variant) — the slot the
9750
- * future performance variants plug into.
9751
- */
9752
- optimization: _enum(["standard", "fast"]).optional(),
9753
- /**
9754
- * Input-resolution axis (square input side, px). Omit ⇒ the family's native
9755
- * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
9756
- * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
9757
- * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
9758
- * the group so the selector can offer it as a variant axis.
9759
- */
9760
- resolution: number().int().positive().optional()
9761
- });
9762
- var ModelProviderIdSchema = _enum([
9763
- "camstack",
9764
- "frigate",
9765
- "scrypted",
9766
- "custom"
9767
- ]);
9768
- var ModelCatalogEntrySchema = object({
9769
- id: string(),
9770
- name: string(),
9771
- description: string(),
9772
- formats: ModelFormatsSchema,
9773
- inputSize: object({
9774
- width: number(),
9775
- height: number()
9776
- }),
9777
- /**
9778
- * Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
9779
- * every detector / classifier / embedder. Set to 1 for a grayscale CTC text
9780
- * recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
9781
- * feeds a single-channel, EasyOCR-normalized tensor instead of the default
9782
- * 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
9783
- * Python inference pool.
9784
- */
9785
- inputChannels: number().int().positive().optional(),
9786
- labels: array(LabelDefinitionSchema).readonly(),
9787
- inputLayout: _enum(["nchw", "nhwc"]).optional(),
9788
- /**
9789
- * `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
9790
- * (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
9791
- * plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
9792
- * `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
9793
- */
9794
- inputNormalization: _enum([
9795
- "zero-one",
9796
- "imagenet",
9797
- "none",
9798
- "scrfd"
9799
- ]).optional(),
9800
- /**
9801
- * The model already applies softmax IN-GRAPH — its raw output is a
9802
- * probability distribution, not logits. When set, the `softmax`
9803
- * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
9804
- * probability vector collapses it toward uniform (top-1 score craters far
9805
- * below its true value, making every confidence gate meaningless). Absent ⇒
9806
- * the output is raw logits and the postprocessor applies softmax (the normal
9807
- * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
9808
- * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
9809
- */
9810
- outputProbabilities: boolean().optional(),
9811
- preprocessMode: _enum(["letterbox", "resize"]).optional(),
9812
- /**
9813
- * Per-MODEL postprocessor override. Absent ⇒ the step's own
9814
- * `StepDefinition.postprocessor` applies (the normal case — every model in a
9815
- * step shares its decode). Set it when a step hosts models with DIFFERENT raw
9816
- * output layouts under one slot: e.g. object-detection is `'yolo'` by default,
9817
- * but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
9818
- * 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
9819
- * by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
9820
- */
9821
- postprocessor: custom().optional(),
9822
- /**
9823
- * When true, the executor produces a landmark-aligned crop (similarity warp
9824
- * onto the canonical template) before this step runs, instead of a plain
9825
- * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
9826
- * their embeddings are only discriminative on an aligned input. The face
9827
- * detector that produced the parent detail must emit 5 landmarks.
9828
- */
9829
- faceAlignment: boolean().optional(),
9830
- /**
9831
- * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
9832
- * Downloaded into the same modelsDir alongside the model file.
9833
- */
9834
- extraFiles: array(ModelExtraFileSchema).readonly().optional(),
9835
- /**
9836
- * LEGACY entry — retained in the catalog so a persisted operator selection
9837
- * still RESOLVES (and can be re-activated), but hidden from the selectable
9838
- * model list and excluded from the auto format-default pick. Set on the
9839
- * superseded / consolidated models (older lineages, redundant fp16 IRs) so
9840
- * the active lineup stays the coherent curated ladder without deleting a
9841
- * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
9842
- * an explicit legacy id that has a build for the node's format.
9843
- */
9844
- legacy: boolean().optional(),
9845
- /**
9846
- * Measured quality/latency metadata — populated from the benchmark addon on
9847
- * the real node classes. Absent = not yet measured (most entries today; the
9848
- * catalog historically carried only `sizeMB`, a poor cross-architecture
9849
- * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
9850
- */
9851
- metrics: object({
9852
- map50: number().optional(),
9853
- p95LatencyMs: record(string(), number()).optional()
9854
- }).optional(),
9855
- /**
9856
- * SPDX-ish license id of the model weights (e.g. `AGPL-3.0` for Ultralytics
9857
- * YOLO26, `GPL-3.0` for YOLOv9, `Apache-2.0` for D-FINE/RF-DETR). Matters for
9858
- * the retraining addon and any future commercial distribution.
9859
- */
9860
- license: string().optional(),
9861
- /**
9862
- * Variant-selector grouping. The UI groups models by `family` + `tier` and
9863
- * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
9864
- * of a family's sizes and quantizations collapse into one grouped picker
9865
- * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
9866
- * (legacy / custom models) — never shown in the grouped selector. The flat
9867
- * `id` stays the source of truth for resolution/download/persistence; grouping
9868
- * is a presentation overlay resolved back to an `id`.
9869
- */
9870
- group: ModelVariantGroupSchema.optional(),
9871
- /**
9872
- * Catalog source for the pipeline stepper's provider-first picker. Absent on
9873
- * built-in CamStack entries (treated as `camstack`) and on registry rows
9874
- * persisted before this field existed (`inferModelProvider` fills those).
10272
+ * Free-form tag for log scoping. Stream-broker uses
10273
+ * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10274
+ * on every line so `grep tag=broker:5/high` filters one camera
10275
+ * profile cleanly.
9875
10276
  */
9876
- provider: ModelProviderIdSchema.optional(),
10277
+ tag: string().optional(),
9877
10278
  /**
9878
- * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
9879
- * applies (Frigate / COCO public catalog). Set on a custom model whose raw
9880
- * labels already ARE the CamStack macros (Scrypted identity map).
10279
+ * Where the session delivers decoded frames (Phase 5 / D9):
10280
+ *
10281
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
10282
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
10283
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
10284
+ * into an OS shared-memory ring and drained as zero-pixel
10285
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
10286
+ * other — `pullFrames` returns nothing for an `'shm'` session and
10287
+ * `pullHandles` returns nothing for a `'callback'` session.
9881
10288
  */
9882
- classMap: DetectionCatalogClassMapSchema.optional(),
10289
+ frameSink: _enum(["callback", "shm"]).default("callback"),
9883
10290
  /**
9884
- * The model's CLIP feature-space contract (text encoder, dimension, vector
9885
- * index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
9886
- * CLIP image encoder; its absence is what "not a CLIP model" means to the
9887
- * embedding encoder and the semantic-search store (D649).
10291
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10292
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10293
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10294
+ * stream-broker's `streamingDebug` gate — off by default so production logs
10295
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
9888
10296
  */
9889
- clip: ClipModelMetaSchema.optional()
9890
- });
9891
- var ConvertTargetSchema = discriminatedUnion("format", [object({
9892
- format: literal("openvino"),
9893
- precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
9894
- }), object({ format: literal("coreml") })]);
9895
- var ModelConvertMetadataSchema = object({
9896
- id: string().regex(/^[a-zA-Z0-9._-]+$/),
9897
- name: string(),
9898
- labels: array(LabelDefinitionSchema).readonly(),
9899
- inputSize: object({
9900
- width: number(),
9901
- height: number()
9902
- }),
9903
- inputLayout: _enum(["nchw", "nhwc"]).optional(),
9904
- inputNormalization: _enum([
9905
- "zero-one",
9906
- "imagenet",
9907
- "none",
9908
- "scrfd"
9909
- ]).optional(),
9910
- preprocessMode: _enum(["letterbox", "resize"]).optional(),
9911
- outputFormat: _enum([
9912
- "yolo",
9913
- "ssd",
9914
- "embedding",
9915
- "classification",
9916
- "ocr",
9917
- "segmentation"
9918
- ]),
9919
- faceAlignment: boolean().optional(),
9920
- classMap: DetectionCatalogClassMapSchema.optional()
9921
- });
9922
- var ConvertResultSchema = object({
9923
- entry: ModelCatalogEntrySchema,
9924
- artifacts: array(object({
9925
- format: _enum(MODEL_FORMATS),
9926
- precision: _enum(["fp16", "int8"]).optional(),
9927
- sizeMB: number(),
9928
- validated: boolean(),
9929
- files: array(string()).readonly()
9930
- })).readonly()
10297
+ debug: boolean().optional()
9931
10298
  });
9932
10299
  /**
9933
- * Error types for the safe expression engine. Two distinct classes so callers
9934
- * can tell a compile-time (grammar) failure from a runtime (evaluation)
9935
- * failure — both are non-fatal to the host: read paths degrade to "skip link".
9936
- */
9937
- /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
9938
- * the failure is anchored to a character (author-facing inline feedback). */
9939
- var ExpressionParseError = class extends Error {
9940
- position;
9941
- constructor(message, position) {
9942
- super(message);
9943
- this.name = "ExpressionParseError";
9944
- this.position = position;
9945
- }
9946
- };
9947
- /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
9948
- * result, unknown builtin, step-budget exceeded). */
9949
- var ExpressionEvalError = class extends Error {
9950
- constructor(message) {
9951
- super(message);
9952
- this.name = "ExpressionEvalError";
9953
- }
9954
- };
9955
- /**
9956
- * Frozen, null-prototype builtin function table for the expression engine
9957
- * (spec §4 rule 4). The table is the SOLE surface of callable functions: the
9958
- * parser rejects any callee not in it, and the evaluator gates each call on an
9959
- * own-property check against it.
10300
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
10301
+ * an addon declares its channels in.
10302
+ *
10303
+ * ## Two axes, deliberately separated
10304
+ *
10305
+ * - **DECLARATION** — which channels exist. Only the addon knows:
10306
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
10307
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
10308
+ * and rots silently. So a channel is declared where it is consulted, and the
10309
+ * `log-channels` capability enumerates the declarations.
10310
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
10311
+ * thing: the logging settings document on the `system` cap. Two authorities
10312
+ * over the values is the exact defect
10313
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
10314
+ * remove; re-introducing it from the cure side would be grotesque.
10315
+ *
10316
+ * Nothing in this file reads a clock, an env var or a store. The registry is
10317
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
10318
+ * the hot path with a value somebody actually read, and by
10319
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
10320
+ * never reaches here, so it can neither disarm an armed channel nor arm a
10321
+ * disarmed one (D49).
10322
+ *
10323
+ * ## The canonical call shape
10324
+ *
10325
+ * ```ts
10326
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
10327
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
10328
+ * }
10329
+ * ```
10330
+ *
10331
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
10332
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
10333
+ * object literal is never constructed because it lives inside the branch. It
10334
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
10335
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
10336
+ * destination floor (measured at 1.93 ns/call when off).
9960
10337
  *
9961
- * Because the object has a NULL prototype AND is `Object.freeze`d:
9962
- * - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
9963
- * - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
9964
- * (there is no `Object.prototype` in the chain), so those names are not
9965
- * callable — they are simply "unknown function" at parse time.
10338
+ * ## Why a channel emits at `info`
9966
10339
  *
9967
- * Every numeric argument is validated as a finite number and every numeric
9968
- * RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
9969
- * (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
9970
- * closed rather than emitting a garbage value.
10340
+ * `loki-logging.addon.ts` pins the destination default at `info` and
10341
+ * `loki-destination.ts` drops everything below it, so a line emitted at
10342
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
10343
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
10344
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
10345
+ * emits at the channel's declared level, whose schema floor is `info`.
9971
10346
  */
9972
- function asFiniteNumber(value, name, index) {
9973
- if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
9974
- return value;
9975
- }
9976
- function asString$1(value, name, index) {
9977
- if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
9978
- return value;
9979
- }
9980
- function finiteResult(value, name) {
9981
- if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
9982
- return value;
9983
- }
9984
- function allFiniteNumbers(args, name) {
9985
- return args.map((a, idx) => asFiniteNumber(a, name, idx));
9986
- }
9987
- var INF = Number.POSITIVE_INFINITY;
9988
- var table = {
9989
- min: {
9990
- minArgs: 1,
9991
- maxArgs: INF,
9992
- apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
9993
- },
9994
- max: {
9995
- minArgs: 1,
9996
- maxArgs: INF,
9997
- apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
9998
- },
9999
- abs: {
10000
- minArgs: 1,
10001
- maxArgs: 1,
10002
- apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
10003
- },
10004
- floor: {
10005
- minArgs: 1,
10006
- maxArgs: 1,
10007
- apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
10008
- },
10009
- ceil: {
10010
- minArgs: 1,
10011
- maxArgs: 1,
10012
- apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
10013
- },
10014
- sqrt: {
10015
- minArgs: 1,
10016
- maxArgs: 1,
10017
- apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
10018
- },
10019
- round: {
10020
- minArgs: 1,
10021
- maxArgs: 2,
10022
- apply: (args) => {
10023
- const x = asFiniteNumber(args[0], "round", 0);
10024
- const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
10025
- if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
10026
- const factor = 10 ** digits;
10027
- return finiteResult(Math.round(x * factor) / factor, "round");
10028
- }
10029
- },
10030
- pow: {
10031
- minArgs: 2,
10032
- maxArgs: 2,
10033
- apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
10034
- },
10035
- clamp: {
10036
- minArgs: 3,
10037
- maxArgs: 3,
10038
- apply: (args) => {
10039
- const x = asFiniteNumber(args[0], "clamp", 0);
10040
- const lo = asFiniteNumber(args[1], "clamp", 1);
10041
- const hi = asFiniteNumber(args[2], "clamp", 2);
10042
- if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
10043
- return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
10044
- }
10045
- },
10046
- avg: {
10047
- minArgs: 1,
10048
- maxArgs: INF,
10049
- apply: (args) => {
10050
- const nums = allFiniteNumbers(args, "avg");
10051
- return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
10052
- }
10053
- },
10054
- sum: {
10055
- minArgs: 1,
10056
- maxArgs: INF,
10057
- apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
10058
- },
10059
- coalesce: {
10060
- minArgs: 1,
10061
- maxArgs: INF,
10062
- apply: (args) => {
10063
- for (const a of args) if (a !== null) return a;
10064
- return null;
10065
- }
10066
- },
10067
- age: {
10068
- minArgs: 2,
10069
- maxArgs: 2,
10070
- apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
10071
- },
10072
- convert: {
10073
- minArgs: 3,
10074
- maxArgs: 3,
10075
- apply: (args, hooks) => {
10076
- const x = asFiniteNumber(args[0], "convert", 0);
10077
- const from = asString$1(args[1], "convert", 1).trim();
10078
- const to = asString$1(args[2], "convert", 2).trim();
10079
- if (hooks.convert) {
10080
- const out = hooks.convert(x, from, to);
10081
- if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
10082
- return finiteResult(out, "convert");
10083
- }
10084
- if (from === to) return x;
10085
- throw new ExpressionEvalError("convert: unit conversion table not installed");
10086
- }
10087
- }
10088
- };
10089
- Object.freeze(Object.assign(Object.create(null), table));
10090
- /** The set of valid builtin names — used by the parser to reject unknown
10091
- * callees at parse time (immediate author feedback). */
10092
- var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
10093
10347
  /**
10094
- * Resource-bound constants for the safe expression engine.
10348
+ * The level a channel writes at once armed.
10095
10349
  *
10096
- * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
10097
- * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
10098
- * O(nodeCount) by construction. These caps merely put a hard ceiling on the
10099
- * work a single author-supplied expression can request, so a hostile or
10100
- * accidental pathological string can never spend unbounded CPU/memory.
10350
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
10351
+ * not leave the process for Loki, and the whole point of arming a channel is
10352
+ * to read it later.
10101
10353
  */
10102
- /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
10103
- * rejected without allocation. */
10104
- var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
10105
- /** A legal binding / identifier name. */
10106
- var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
10107
- /** Binding names an author may NOT use: `now` is auto-injected; the literal
10108
- * keywords lex as values, not identifiers, so binding to them is meaningless. */
10109
- var RESERVED_BINDING_NAMES = new Set([
10110
- "now",
10111
- "true",
10112
- "false",
10113
- "null"
10354
+ var LogChannelLevelSchema = _enum([
10355
+ "info",
10356
+ "warn",
10357
+ "error"
10114
10358
  ]);
10115
10359
  /**
10116
- * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
10117
- * zero-dependency. The grammar is deliberately boring: decimal numbers,
10118
- * single/double-quoted strings with a tiny escape set, identifiers, the three
10119
- * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
10120
- * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
10121
- * is a parse error with a source position, so member access / assignment /
10122
- * template literals are lexically impossible.
10360
+ * What an addon declares about one channel. No value, no state — a
10361
+ * declaration is inert.
10123
10362
  */
10124
- var KEYWORDS = new Set([
10125
- "true",
10126
- "false",
10127
- "null"
10128
- ]);
10129
- function isDigit(ch) {
10130
- return ch >= "0" && ch <= "9";
10131
- }
10132
- function isIdentStart(ch) {
10133
- return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
10134
- }
10135
- function isIdentPart(ch) {
10136
- return isIdentStart(ch) || isDigit(ch);
10137
- }
10138
- function isWhitespace(ch) {
10139
- return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
10140
- }
10141
- /** Tokenize `source` into a flat token list ending with a single `eof` token.
10142
- * Throws `ExpressionParseError` on any illegal character or unterminated
10143
- * string. */
10144
- function tokenize(source) {
10145
- if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
10146
- const tokens = [];
10147
- let i = 0;
10148
- const n = source.length;
10149
- while (i < n) {
10150
- const ch = source[i];
10151
- if (isWhitespace(ch)) {
10152
- i += 1;
10153
- continue;
10154
- }
10155
- if (isDigit(ch)) {
10156
- const start = i;
10157
- while (i < n && isDigit(source[i])) i += 1;
10158
- if (i < n && source[i] === ".") {
10159
- if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
10160
- i += 1;
10161
- while (i < n && isDigit(source[i])) i += 1;
10162
- }
10163
- const text = source.slice(start, i);
10164
- const value = Number(text);
10165
- if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
10166
- tokens.push({
10167
- type: "number",
10168
- value,
10169
- pos: start
10170
- });
10171
- continue;
10172
- }
10173
- if (ch === "'" || ch === "\"") {
10174
- const quote = ch;
10175
- const start = i;
10176
- i += 1;
10177
- let out = "";
10178
- let closed = false;
10179
- while (i < n) {
10180
- const c = source[i];
10181
- if (c === "\\") {
10182
- const next = i + 1 < n ? source[i + 1] : "";
10183
- if (next === "\\" || next === "'" || next === "\"") {
10184
- out += next;
10185
- i += 2;
10186
- continue;
10187
- }
10188
- throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
10189
- }
10190
- if (c === quote) {
10191
- closed = true;
10192
- i += 1;
10193
- break;
10194
- }
10195
- out += c;
10196
- i += 1;
10197
- }
10198
- if (!closed) throw new ExpressionParseError("unterminated string literal", start);
10199
- tokens.push({
10200
- type: "string",
10201
- value: out,
10202
- pos: start
10203
- });
10204
- continue;
10205
- }
10206
- if (isIdentStart(ch)) {
10207
- const start = i;
10208
- while (i < n && isIdentPart(source[i])) i += 1;
10209
- const text = source.slice(start, i);
10210
- if (KEYWORDS.has(text)) tokens.push({
10211
- type: "keyword",
10212
- keyword: keywordOf(text),
10213
- pos: start
10214
- });
10215
- else tokens.push({
10216
- type: "identifier",
10217
- name: text,
10218
- pos: start
10219
- });
10220
- continue;
10221
- }
10222
- const two = i + 1 < n ? source.slice(i, i + 2) : "";
10223
- if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
10224
- tokens.push({
10225
- type: "punct",
10226
- punct: two,
10227
- pos: i
10228
- });
10229
- i += 2;
10230
- continue;
10231
- }
10232
- if (isSinglePunct(ch)) {
10233
- tokens.push({
10234
- type: "punct",
10235
- punct: ch,
10236
- pos: i
10237
- });
10238
- i += 1;
10239
- continue;
10240
- }
10241
- throw new ExpressionParseError(`unexpected character '${ch}'`, i);
10242
- }
10243
- tokens.push({
10244
- type: "eof",
10245
- pos: n
10246
- });
10247
- return tokens;
10248
- }
10249
- function keywordOf(text) {
10250
- if (text === "true") return "true";
10251
- if (text === "false") return "false";
10252
- return "null";
10253
- }
10254
- function isSinglePunct(ch) {
10255
- return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
10256
- }
10363
+ var LogChannelDescriptorSchema = object({
10364
+ /**
10365
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
10366
+ * the addon's short name so an operator reading a channel list can tell who
10367
+ * owns it without a second lookup.
10368
+ */
10369
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
10370
+ /** One sentence: what the operator will SEE after arming it. */
10371
+ description: string().min(1),
10372
+ /** The level its lines are emitted at. Never below `info`. */
10373
+ defaultLevel: LogChannelLevelSchema,
10374
+ /**
10375
+ * Whether this channel can be narrowed to a camera.
10376
+ *
10377
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
10378
+ * consulted with the numeric device id, AND every line the channel admits
10379
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
10380
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
10381
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
10382
+ * the body is the only way to filter.
10383
+ *
10384
+ * A channel whose lines carry the device only in `meta` (or not at all) is
10385
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
10386
+ * the operator narrows to one camera, sees nothing, and concludes the code
10387
+ * path was never taken.
10388
+ */
10389
+ perDevice: boolean()
10390
+ });
10257
10391
  /**
10258
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
10392
+ * An armed window over one channel, as the document hands it to a mirror.
10259
10393
  *
10260
- * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
10261
- * → relational → additive → multiplicative → unary `! -` → call / primary.
10262
- * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
10263
- * string validated against the builtin table at parse time, so an unknown
10264
- * function is rejected immediately (author feedback) and a persisted expression
10265
- * that references a since-removed builtin degrades at read.
10394
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
10395
+ * expires by itself, which is the one failure a boolean cannot avoid.
10396
+ */
10397
+ var LogChannelWindowSchema = object({
10398
+ channel: string().min(1),
10399
+ /** Epoch ms the window closes at. */
10400
+ armedUntilMs: number(),
10401
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
10402
+ deviceIds: array(number().int()).readonly().nullable()
10403
+ });
10404
+ var ClipModelMetaSchema = object({
10405
+ /** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
10406
+ textModelId: string().min(1),
10407
+ /** Output dimension of BOTH towers. A vector of any other length is refused. */
10408
+ embeddingDim: number().int().positive(),
10409
+ /**
10410
+ * The `vector-store` index this model's vectors live in. ONE feature space
10411
+ * per index: two models share an index only when their vectors are ranked
10412
+ * against each other, which for CLIP means never across families. MobileCLIP
10413
+ * S1 and S2 share `object-clip` for history (separated by the row's
10414
+ * `modelId`); any other model gets its own.
10415
+ */
10416
+ vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
10417
+ /**
10418
+ * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
10419
+ * must be unique per tokenizer: every model's siblings land in ONE flat
10420
+ * models directory, so two different tokenizers under the same name would
10421
+ * silently overwrite each other (whichever model downloaded last wins, and
10422
+ * the other one tokenises with the wrong vocabulary).
10423
+ */
10424
+ tokenizerFile: string().min(1),
10425
+ /** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
10426
+ contextLength: number().int().positive(),
10427
+ /** Id the tokenizer pads with. */
10428
+ padId: number().int().nonnegative(),
10429
+ /**
10430
+ * The search cosine floor at `strictness: 'loose'` — calibrated per model,
10431
+ * because cosine bands are a property of the model: 0.2 keeps 96% of matched
10432
+ * COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
10433
+ */
10434
+ searchMinScore: number().min(0).max(1)
10435
+ });
10436
+ var ClipSearchStrictnessSchema = _enum([
10437
+ "loose",
10438
+ "balanced",
10439
+ "strict"
10440
+ ]);
10441
+ var MODEL_FORMATS = [
10442
+ "onnx",
10443
+ "coreml",
10444
+ "openvino",
10445
+ "tflite",
10446
+ "pt",
10447
+ "gguf"
10448
+ ];
10449
+ /**
10450
+ * Multi-file format payload.
10266
10451
  *
10267
- * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
10268
- * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
10452
+ * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
10453
+ * relative to the directory root — the downloader fetches each from
10454
+ * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
10455
+ * HuggingFace API (slower).
10456
+ * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
10457
+ * files fetched from the SAME remote directory as `url` and stored flat
10458
+ * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
10459
+ * weights next to `camstack-yolov9t.xml`.
10269
10460
  */
10270
- /** Binary/logical operator precedence (higher binds tighter). */
10271
- var BINARY_PRECEDENCE = {
10272
- "||": 1,
10273
- "&&": 2,
10274
- "==": 3,
10275
- "!=": 3,
10276
- "<": 4,
10277
- "<=": 4,
10278
- ">": 4,
10279
- ">=": 4,
10280
- "+": 5,
10281
- "-": 5,
10282
- "*": 6,
10283
- "/": 6,
10284
- "%": 6
10285
- };
10286
- function isLogicalOp(op) {
10287
- return op === "&&" || op === "||";
10288
- }
10289
- function isBinaryOp(op) {
10290
- return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
10291
- }
10292
- var Parser = class {
10293
- tokens;
10294
- pos = 0;
10295
- nodeCount = 0;
10296
- identifiers = /* @__PURE__ */ new Set();
10297
- callees = /* @__PURE__ */ new Set();
10298
- constructor(tokens) {
10299
- this.tokens = tokens;
10300
- }
10301
- parse() {
10302
- const ast = this.parseTernary();
10303
- const tok = this.peek();
10304
- if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
10305
- return {
10306
- ast,
10307
- identifiers: this.identifiers,
10308
- callees: this.callees,
10309
- nodeCount: this.nodeCount
10310
- };
10311
- }
10312
- peek() {
10313
- return this.tokens[this.pos];
10314
- }
10315
- next() {
10316
- return this.tokens[this.pos++];
10317
- }
10318
- /** Consume a punctuator token, erroring if the next token isn't it. */
10319
- expectPunct(punct) {
10320
- const tok = this.peek();
10321
- if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
10322
- this.pos += 1;
10323
- }
10324
- matchPunct(punct) {
10325
- const tok = this.peek();
10326
- if (tok.type === "punct" && tok.punct === punct) {
10327
- this.pos += 1;
10328
- return true;
10329
- }
10330
- return false;
10331
- }
10332
- countNode() {
10333
- this.nodeCount += 1;
10334
- if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
10335
- }
10336
- parseTernary() {
10337
- const test = this.parseBinary(1);
10338
- if (this.matchPunct("?")) {
10339
- const consequent = this.parseTernary();
10340
- this.expectPunct(":");
10341
- const alternate = this.parseTernary();
10342
- this.countNode();
10343
- return {
10344
- kind: "conditional",
10345
- test,
10346
- consequent,
10347
- alternate
10348
- };
10349
- }
10350
- return test;
10351
- }
10352
- parseBinary(minPrec) {
10353
- let left = this.parseUnary();
10354
- for (;;) {
10355
- const tok = this.peek();
10356
- if (tok.type !== "punct") break;
10357
- const prec = BINARY_PRECEDENCE[tok.punct];
10358
- if (prec === void 0 || prec < minPrec) break;
10359
- const op = tok.punct;
10360
- this.pos += 1;
10361
- const right = this.parseBinary(prec + 1);
10362
- this.countNode();
10363
- if (isLogicalOp(op)) left = {
10364
- kind: "logical",
10365
- op,
10366
- left,
10367
- right
10368
- };
10369
- else if (isBinaryOp(op)) left = {
10370
- kind: "binary",
10371
- op,
10372
- left,
10373
- right
10374
- };
10375
- else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
10376
- }
10377
- return left;
10378
- }
10379
- parseUnary() {
10380
- const tok = this.peek();
10381
- if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
10382
- const op = tok.punct;
10383
- this.pos += 1;
10384
- const operand = this.parseUnary();
10385
- this.countNode();
10386
- return {
10387
- kind: "unary",
10388
- op,
10389
- operand
10390
- };
10391
- }
10392
- return this.parsePrimary();
10393
- }
10394
- parsePrimary() {
10395
- const tok = this.next();
10396
- switch (tok.type) {
10397
- case "number":
10398
- this.countNode();
10399
- return {
10400
- kind: "literal",
10401
- value: tok.value
10402
- };
10403
- case "string":
10404
- this.countNode();
10405
- return {
10406
- kind: "literal",
10407
- value: tok.value
10408
- };
10409
- case "keyword":
10410
- this.countNode();
10411
- return {
10412
- kind: "literal",
10413
- value: tok.keyword === "null" ? null : tok.keyword === "true"
10414
- };
10415
- case "identifier": {
10416
- const nextTok = this.peek();
10417
- if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
10418
- this.identifiers.add(tok.name);
10419
- this.countNode();
10420
- return {
10421
- kind: "identifier",
10422
- name: tok.name
10423
- };
10424
- }
10425
- case "punct":
10426
- if (tok.punct === "(") {
10427
- const inner = this.parseTernary();
10428
- this.expectPunct(")");
10429
- return inner;
10430
- }
10431
- throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
10432
- case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
10433
- }
10434
- }
10435
- parseCall(callee, pos) {
10436
- if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
10437
- this.expectPunct("(");
10438
- const args = [];
10439
- if (!this.matchPunct(")")) for (;;) {
10440
- args.push(this.parseTernary());
10441
- if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
10442
- if (this.matchPunct(",")) continue;
10443
- this.expectPunct(")");
10444
- break;
10445
- }
10446
- this.callees.add(callee);
10447
- this.countNode();
10448
- return {
10449
- kind: "call",
10450
- callee,
10451
- args
10452
- };
10453
- }
10454
- };
10455
- /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
10456
- * `ExpressionParseError` on any lexical or grammatical failure. */
10457
- function parseExpression(source) {
10458
- return new Parser(tokenize(source)).parse();
10459
- }
10461
+ var ModelFormatEntrySchema = object({
10462
+ url: string(),
10463
+ sizeMB: number(),
10464
+ /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
10465
+ isDirectory: boolean().optional(),
10466
+ /** Multi-file payload (directory members or sibling files). */
10467
+ files: array(string()).readonly().optional(),
10468
+ /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
10469
+ runtimes: array(_enum(["python"])).readonly().optional()
10470
+ });
10460
10471
  /**
10461
- * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
10462
- * by expr"). The cache stores BOTH successes and failures (negative caching),
10463
- * so a corrupt persisted string costs exactly one tokenize+parse total — not
10464
- * one per read on a hot resolve path.
10465
- *
10466
- * The cache is a module-level singleton: entries are pure, content-addressed
10467
- * ASTs keyed by the raw source string, so sharing one instance across all
10468
- * callers is safe and maximises hit rate.
10472
+ * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
10473
+ * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
10469
10474
  */
10470
- var cache = /* @__PURE__ */ new Map();
10471
- function getCached(source) {
10472
- const hit = cache.get(source);
10473
- if (hit !== void 0) {
10474
- cache.delete(source);
10475
- cache.set(source, hit);
10476
- return hit;
10477
- }
10478
- let result;
10479
- try {
10480
- result = {
10481
- ok: true,
10482
- parsed: parseExpression(source)
10483
- };
10484
- } catch (err) {
10485
- result = {
10486
- ok: false,
10487
- error: err instanceof ExpressionParseError ? err.message : String(err)
10488
- };
10489
- }
10490
- cache.set(source, result);
10491
- if (cache.size > 256) {
10492
- const oldest = cache.keys().next().value;
10493
- if (oldest !== void 0) cache.delete(oldest);
10494
- }
10495
- return result;
10496
- }
10497
- /** Compile `source`, returning a discriminated result instead of throwing.
10498
- * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
10499
- function compileExpressionSafe(source) {
10500
- return getCached(source);
10501
- }
10502
- Object.freeze({});
10475
+ var ModelExtraFileSchema = object({
10476
+ url: string(),
10477
+ filename: string(),
10478
+ sizeMB: number()
10479
+ });
10503
10480
  /**
10504
- * Author-time validation. Returns `null` when the source is valid, else a
10505
- * human-readable error message. Checks: the expression compiles; binding count
10506
- * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
10507
- * is not reserved (`now`/keywords) and does not shadow a builtin; and every
10508
- * FREE identifier of the AST is covered by a binding or the injected `now`.
10481
+ * Per-format payload map. Modelled as an explicit object (one optional key
10482
+ * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
10483
+ * record requires every key, but a catalog entry only ships a subset of
10484
+ * formats.
10509
10485
  */
10510
- function validateExpressionSource(src) {
10511
- const names = Object.keys(src.bindings);
10512
- if (names.length > 32) return `too many bindings (${names.length} > 32)`;
10513
- for (const name of names) {
10514
- if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
10515
- if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
10516
- if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
10517
- }
10518
- const compiled = compileExpressionSafe(src.expr);
10519
- if (!compiled.ok) return compiled.error;
10520
- const bound = new Set(names);
10521
- for (const id of compiled.parsed.identifiers) {
10522
- if (id === "now") continue;
10523
- if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
10524
- }
10525
- return null;
10526
- }
10486
+ var ModelFormatsSchema = object({
10487
+ onnx: ModelFormatEntrySchema.optional(),
10488
+ coreml: ModelFormatEntrySchema.optional(),
10489
+ openvino: ModelFormatEntrySchema.optional(),
10490
+ tflite: ModelFormatEntrySchema.optional(),
10491
+ pt: ModelFormatEntrySchema.optional(),
10492
+ gguf: ModelFormatEntrySchema.optional()
10493
+ });
10494
+ /**
10495
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
10496
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
10497
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
10498
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
10499
+ * resolution/download/persistence; this is a presentation overlay resolved back
10500
+ * to an `id`.
10501
+ */
10502
+ var ModelVariantGroupSchema = object({
10503
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
10504
+ family: string(),
10505
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
10506
+ tier: string(),
10507
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
10508
+ precision: _enum(["fp32", "int8"]).optional(),
10509
+ /**
10510
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
10511
+ * latency-optimized export (e.g. ReLU-activation variant) — the slot the
10512
+ * future performance variants plug into.
10513
+ */
10514
+ optimization: _enum(["standard", "fast"]).optional(),
10515
+ /**
10516
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
10517
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
10518
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
10519
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
10520
+ * the group so the selector can offer it as a variant axis.
10521
+ */
10522
+ resolution: number().int().positive().optional()
10523
+ });
10524
+ var ModelProviderIdSchema = _enum([
10525
+ "camstack",
10526
+ "frigate",
10527
+ "scrypted",
10528
+ "custom"
10529
+ ]);
10530
+ /**
10531
+ * The licence of a model, as a record rather than a bare SPDX string: a
10532
+ * permissive CODE licence routinely sits on top of restrictive WEIGHTS or
10533
+ * DATA, and the obligations (attribution, modification notices, source offers)
10534
+ * are per upstream. A persisted custom-model row written when this field was a
10535
+ * string still parses — the string is lifted into `{ weights }`.
10536
+ *
10537
+ * Every licence surface (the admin "Models and licenses" page,
10538
+ * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
10539
+ * these records by `scripts/gen-model-licenses.ts` (D661).
10540
+ */
10541
+ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
10542
+ /** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
10543
+ weights: string().min(1),
10544
+ /** SPDX id of the upstream code that defines / trained the network. */
10545
+ code: string().min(1).optional(),
10546
+ /** Training data whose terms add obligations (attribution, non-commercial). */
10547
+ data: object({
10548
+ name: string().min(1),
10549
+ terms: string().min(1),
10550
+ url: string().url().optional()
10551
+ }).optional(),
10552
+ /** The project the weights come from. Required on built-in entries (guard). */
10553
+ upstream: object({
10554
+ name: string().min(1),
10555
+ url: string().url()
10556
+ }).optional(),
10557
+ /** Where the licence text governing the weights is published. */
10558
+ url: string().url().optional(),
10559
+ /** Attribution the licence REQUIRES, verbatim. */
10560
+ attribution: string().min(1).optional(),
10561
+ /** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
10562
+ modifications: string().min(1).optional(),
10563
+ /** Who serves the file a node downloads. */
10564
+ hosting: _enum([
10565
+ "camstack-hf",
10566
+ "third-party",
10567
+ "built-in"
10568
+ ]).optional(),
10569
+ /** True when the weights or their data forbid commercial use. */
10570
+ nonCommercial: boolean().optional()
10571
+ }));
10572
+ var ModelCatalogEntrySchema = object({
10573
+ id: string(),
10574
+ name: string(),
10575
+ description: string(),
10576
+ formats: ModelFormatsSchema,
10577
+ inputSize: object({
10578
+ width: number(),
10579
+ height: number()
10580
+ }),
10581
+ /**
10582
+ * Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
10583
+ * every detector / classifier / embedder. Set to 1 for a grayscale CTC text
10584
+ * recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
10585
+ * feeds a single-channel, EasyOCR-normalized tensor instead of the default
10586
+ * 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
10587
+ * Python inference pool.
10588
+ */
10589
+ inputChannels: number().int().positive().optional(),
10590
+ labels: array(LabelDefinitionSchema).readonly(),
10591
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10592
+ /**
10593
+ * `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
10594
+ * (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
10595
+ * plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
10596
+ * `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
10597
+ */
10598
+ inputNormalization: _enum([
10599
+ "zero-one",
10600
+ "imagenet",
10601
+ "none",
10602
+ "scrfd"
10603
+ ]).optional(),
10604
+ /**
10605
+ * The model already applies softmax IN-GRAPH — its raw output is a
10606
+ * probability distribution, not logits. When set, the `softmax`
10607
+ * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
10608
+ * probability vector collapses it toward uniform (top-1 score craters far
10609
+ * below its true value, making every confidence gate meaningless). Absent ⇒
10610
+ * the output is raw logits and the postprocessor applies softmax (the normal
10611
+ * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
10612
+ * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
10613
+ */
10614
+ outputProbabilities: boolean().optional(),
10615
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10616
+ /**
10617
+ * Per-MODEL postprocessor override. Absent ⇒ the step's own
10618
+ * `StepDefinition.postprocessor` applies (the normal case — every model in a
10619
+ * step shares its decode). Set it when a step hosts models with DIFFERENT raw
10620
+ * output layouts under one slot: e.g. object-detection is `'yolo'` by default,
10621
+ * but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
10622
+ * 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
10623
+ * by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
10624
+ */
10625
+ postprocessor: custom().optional(),
10626
+ /**
10627
+ * Per-MODEL default confidence floor. Absent ⇒ the step's
10628
+ * `StepDefinition.defaultConfidence` applies. A score is a property of one
10629
+ * model's output scale, not of the step slot it sits in: YuNet's
10630
+ * `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
10631
+ * face gallery with wheels and hands (D662, amends D645). An operator's
10632
+ * explicit value still wins over it. Resolved in ONE place —
10633
+ * `addon-pipeline/.../registry/effective-confidence.ts`.
10634
+ */
10635
+ defaultConfidence: number().min(0).max(1).optional(),
10636
+ /**
10637
+ * When true, the executor produces a landmark-aligned crop (similarity warp
10638
+ * onto the canonical template) before this step runs, instead of a plain
10639
+ * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
10640
+ * their embeddings are only discriminative on an aligned input. The face
10641
+ * detector that produced the parent detail must emit 5 landmarks.
10642
+ */
10643
+ faceAlignment: boolean().optional(),
10644
+ /**
10645
+ * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
10646
+ * Downloaded into the same modelsDir alongside the model file.
10647
+ */
10648
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
10649
+ /**
10650
+ * LEGACY entry — retained in the catalog so a persisted operator selection
10651
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
10652
+ * model list and excluded from the auto format-default pick. Set on the
10653
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
10654
+ * the active lineup stays the coherent curated ladder without deleting a
10655
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
10656
+ * an explicit legacy id that has a build for the node's format.
10657
+ */
10658
+ legacy: boolean().optional(),
10659
+ /**
10660
+ * Measured quality/latency metadata — populated from the benchmark addon on
10661
+ * the real node classes. Absent = not yet measured (most entries today; the
10662
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
10663
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
10664
+ */
10665
+ metrics: object({
10666
+ map50: number().optional(),
10667
+ p95LatencyMs: record(string(), number()).optional()
10668
+ }).optional(),
10669
+ /**
10670
+ * The model's licence record — see {@link ModelLicenseSchema}. Optional in
10671
+ * the schema (a custom model's author may state none); REQUIRED and complete
10672
+ * on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
10673
+ */
10674
+ license: ModelLicenseSchema.optional(),
10675
+ /**
10676
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
10677
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
10678
+ * of a family's sizes and quantizations collapse into one grouped picker
10679
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
10680
+ * (legacy / custom models) — never shown in the grouped selector. The flat
10681
+ * `id` stays the source of truth for resolution/download/persistence; grouping
10682
+ * is a presentation overlay resolved back to an `id`.
10683
+ */
10684
+ group: ModelVariantGroupSchema.optional(),
10685
+ /**
10686
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
10687
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
10688
+ * persisted before this field existed (`inferModelProvider` fills those).
10689
+ */
10690
+ provider: ModelProviderIdSchema.optional(),
10691
+ /**
10692
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
10693
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
10694
+ * labels already ARE the CamStack macros (Scrypted identity map).
10695
+ */
10696
+ classMap: DetectionCatalogClassMapSchema.optional(),
10697
+ /**
10698
+ * The model's CLIP feature-space contract (text encoder, dimension, vector
10699
+ * index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
10700
+ * CLIP image encoder; its absence is what "not a CLIP model" means to the
10701
+ * embedding encoder and the semantic-search store (D649).
10702
+ */
10703
+ clip: ClipModelMetaSchema.optional()
10704
+ });
10705
+ var ConvertTargetSchema = discriminatedUnion("format", [object({
10706
+ format: literal("openvino"),
10707
+ precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
10708
+ }), object({ format: literal("coreml") })]);
10709
+ var ModelConvertMetadataSchema = object({
10710
+ id: string().regex(/^[a-zA-Z0-9._-]+$/),
10711
+ name: string(),
10712
+ labels: array(LabelDefinitionSchema).readonly(),
10713
+ inputSize: object({
10714
+ width: number(),
10715
+ height: number()
10716
+ }),
10717
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10718
+ inputNormalization: _enum([
10719
+ "zero-one",
10720
+ "imagenet",
10721
+ "none",
10722
+ "scrfd"
10723
+ ]).optional(),
10724
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10725
+ outputFormat: _enum([
10726
+ "yolo",
10727
+ "ssd",
10728
+ "embedding",
10729
+ "classification",
10730
+ "ocr",
10731
+ "segmentation"
10732
+ ]),
10733
+ faceAlignment: boolean().optional(),
10734
+ classMap: DetectionCatalogClassMapSchema.optional()
10735
+ });
10736
+ var ConvertResultSchema = object({
10737
+ entry: ModelCatalogEntrySchema,
10738
+ artifacts: array(object({
10739
+ format: _enum(MODEL_FORMATS),
10740
+ precision: _enum(["fp16", "int8"]).optional(),
10741
+ sizeMB: number(),
10742
+ validated: boolean(),
10743
+ files: array(string()).readonly()
10744
+ })).readonly()
10745
+ });
10527
10746
  var ExpressionBindingSourceSchema = union([
10528
10747
  object({
10529
10748
  kind: literal("field").optional(),
@@ -10698,7 +10917,7 @@ var AutomationActionSchema = discriminatedUnion("kind", [
10698
10917
  code: string().min(1).max(2e4)
10699
10918
  })
10700
10919
  ]);
10701
- object({
10920
+ var AutomationRecipeSchema = object({
10702
10921
  triggers: array(AutomationTriggerSchema),
10703
10922
  conditions: AutomationConditionSchema.optional(),
10704
10923
  actions: array(AutomationActionSchema)
@@ -11185,12 +11404,32 @@ var AudioClassificationLabelSchema = object({
11185
11404
  originalClass: string().optional(),
11186
11405
  score: number()
11187
11406
  });
11407
+ /**
11408
+ * Did the classifier RUN on this window? (D660)
11409
+ *
11410
+ * `classified` with no labels means the model heard nothing above the
11411
+ * camera's floor. `not-classified` means nobody listened — and until D660 the
11412
+ * two were the same shape (`classification` absent), so a window the global
11413
+ * busy gate threw away read downstream exactly like a quiet one (D393).
11414
+ */
11415
+ var AudioClassifyOutcomeSchema = discriminatedUnion("state", [object({ state: literal("classified") }), object({
11416
+ state: literal("not-classified"),
11417
+ reason: _enum([
11418
+ "superseded",
11419
+ "error",
11420
+ "timeout",
11421
+ "disposed"
11422
+ ])
11423
+ })]);
11188
11424
  var AudioAnalysisResultSchema = object({
11189
11425
  level: AudioLevelSchema,
11426
+ /** Labels at or above the camera's floor. Absent = none — read `classifyOutcome` to know whether the model ran at all. */
11190
11427
  classification: object({
11191
11428
  labels: array(AudioClassificationLabelSchema).readonly(),
11192
11429
  inferenceMs: number()
11193
11430
  }).optional(),
11431
+ /** Whether the classifier ran on this window (D660). */
11432
+ classifyOutcome: AudioClassifyOutcomeSchema,
11194
11433
  timestamp: number()
11195
11434
  });
11196
11435
  var AudioAnalysisSettingsSchema = object({
@@ -12899,15 +13138,157 @@ var cameraStreamsCapability = {
12899
13138
  */
12900
13139
  durability: "session"
12901
13140
  };
13141
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
13142
+ var CompositionProblemSchema = object({
13143
+ code: _enum([
13144
+ "reserved-variant",
13145
+ "unknown-capability",
13146
+ "not-composable",
13147
+ "wrong-device-type",
13148
+ "has-commands",
13149
+ "unknown-field",
13150
+ "nested-field",
13151
+ "missing-field",
13152
+ "kind-mismatch",
13153
+ "may-be-null",
13154
+ "source-missing",
13155
+ "source-unreadable",
13156
+ "source-capability-missing",
13157
+ "source-field-unknown",
13158
+ "expression-invalid",
13159
+ "cycle",
13160
+ "duplicate-target",
13161
+ "not-item-array",
13162
+ "item-field-reserved",
13163
+ "target-missing",
13164
+ "target-unreadable",
13165
+ "not-replaceable"
13166
+ ]),
13167
+ path: string(),
13168
+ message: string()
13169
+ });
13170
+ var CompositionFieldKindSchema = _enum([
13171
+ "string",
13172
+ "number",
13173
+ "boolean",
13174
+ "enum"
13175
+ ]);
13176
+ /**
13177
+ * How a field says "unavailable" (D393, D663): `null` when its schema allows it;
13178
+ * `unknown` — the enum's own word — for an ADDED field that has one; `release`
13179
+ * for a REPLACED field of an existing device, whose claim is released so the
13180
+ * native provider's reading shows again; else the (composed) device goes offline.
13181
+ */
13182
+ var CompositionUnavailableModeSchema = _enum([
13183
+ "null",
13184
+ "offline",
13185
+ "unknown",
13186
+ "release"
13187
+ ]);
13188
+ /**
13189
+ * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
13190
+ * field of its cap changes value · `constant` reads nothing, computed once.
13191
+ */
13192
+ var CompositionFieldRoleSchema = _enum([
13193
+ "derived",
13194
+ "clock",
13195
+ "constant"
13196
+ ]);
13197
+ /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
13198
+ var CompositionFieldItemSchema = object({
13199
+ arrayPath: string(),
13200
+ key: string(),
13201
+ label: string()
13202
+ });
13203
+ var CompositionFieldPlanSchema = object({
13204
+ cap: string(),
13205
+ /** Top-level field, or — when `item` is set — the dotted path inside that item. */
13206
+ path: string(),
13207
+ item: CompositionFieldItemSchema.optional(),
13208
+ /** Index into `composition.features` this field's capability came from —
13209
+ * lets a source/cycle problem name its path as `features[N].fields.<path>`
13210
+ * (the same convention `planComposition`'s own problems already use). */
13211
+ featureIndex: number().int().nonnegative(),
13212
+ kind: CompositionFieldKindSchema,
13213
+ nullable: boolean(),
13214
+ required: boolean(),
13215
+ unavailable: CompositionUnavailableModeSchema,
13216
+ role: CompositionFieldRoleSchema,
13217
+ source: CompositionFieldSourceSchema,
13218
+ reads: array(CompositionFieldReadSchema)
13219
+ });
13220
+ /**
13221
+ * What a feature does to its target (D663): `new` builds a capability on a new
13222
+ * device; on an existing device it `add`s a capability the device lacks, or
13223
+ * `replace`s named fields of one it has natively.
13224
+ */
13225
+ var CompositionFeatureModeSchema = _enum([
13226
+ "new",
13227
+ "add",
13228
+ "replace"
13229
+ ]);
13230
+ var CompositionFeaturePlanSchema = object({
13231
+ featureIndex: number().int().nonnegative(),
13232
+ cap: string(),
13233
+ mode: CompositionFeatureModeSchema
13234
+ });
13235
+ var CompositionValidationSchema = object({
13236
+ ok: boolean(),
13237
+ problems: array(CompositionProblemSchema),
13238
+ fields: array(CompositionFieldPlanSchema),
13239
+ features: array(CompositionFeaturePlanSchema)
13240
+ });
13241
+ /**
13242
+ * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
13243
+ * running, but a replaced field is released to the native provider while its
13244
+ * source is unavailable (ruling S1b, D663) — the reason names each such field.
13245
+ */
13246
+ var CompositionRunStatusSchema = _enum([
13247
+ "stopped",
13248
+ "starting",
13249
+ "running",
13250
+ "degraded",
13251
+ "failed"
13252
+ ]);
13253
+ var CompositionFieldStateKindSchema = _enum([
13254
+ "fresh",
13255
+ "stale",
13256
+ "unavailable"
13257
+ ]);
13258
+ var CompositionFieldStateSchema = object({
13259
+ cap: string(),
13260
+ path: string(),
13261
+ /** Same as the plan's: two items' `status` are told apart by it. */
13262
+ item: CompositionFieldItemSchema.optional(),
13263
+ state: CompositionFieldStateKindSchema,
13264
+ /** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
13265
+ value: union([
13266
+ string(),
13267
+ number(),
13268
+ boolean(),
13269
+ _null()
13270
+ ]),
13271
+ reason: string().optional()
13272
+ });
13273
+ var CompositionBlockStateSchema = object({
13274
+ blockId: string(),
13275
+ deviceId: number().nullable(),
13276
+ status: CompositionRunStatusSchema,
13277
+ reason: string().optional(),
13278
+ fields: array(CompositionFieldStateSchema)
13279
+ });
12902
13280
  /** Where a block runs. The operator chooses — a block driving a device on an
12903
13281
  * agent is the reason placement is not fixed to the hub. */
12904
13282
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
12905
13283
  /** What a block's process is doing. Mirrors the addon runner's own lifecycle so
12906
- * a failing block reads the same way a failing addon does. */
13284
+ * a failing block reads the same way a failing addon does. `degraded` is a
13285
+ * composition on an existing device with a field handed back to the native
13286
+ * provider while its source is unavailable (S1b, D663); the error names it. */
12907
13287
  var CoreBlockStatusSchema = _enum([
12908
13288
  "stopped",
12909
13289
  "starting",
12910
13290
  "running",
13291
+ "degraded",
12911
13292
  "failed"
12912
13293
  ]);
12913
13294
  /** Client-authored fields. */
@@ -12925,6 +13306,37 @@ var CoreBlockInputSchema = object({
12925
13306
  */
12926
13307
  integrationId: string().optional()
12927
13308
  });
13309
+ /**
13310
+ * What a block IS (D659). Absent means `code` — every row stored before this
13311
+ * union keeps working unchanged.
13312
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
13313
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
13314
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
13315
+ */
13316
+ var CoreBlockSourceSchema = discriminatedUnion("kind", [
13317
+ object({ kind: literal("code") }),
13318
+ object({
13319
+ kind: literal("recipe"),
13320
+ recipe: AutomationRecipeSchema
13321
+ }),
13322
+ object({
13323
+ kind: literal("composition"),
13324
+ composition: CompositionSchema
13325
+ })
13326
+ ]);
13327
+ /**
13328
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
13329
+ *
13330
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
13331
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
13332
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
13333
+ * client-sent name by name.
13334
+ */
13335
+ var CompositionBlockInputSchema = object({
13336
+ name: string().min(1).max(120).optional(),
13337
+ enabled: boolean(),
13338
+ composition: CompositionSchema
13339
+ });
12928
13340
  /** A stored block. */
12929
13341
  var CoreBlockSchema = CoreBlockInputSchema.extend({
12930
13342
  id: string(),
@@ -12940,8 +13352,43 @@ var CoreBlockSchema = CoreBlockInputSchema.extend({
12940
13352
  */
12941
13353
  lastError: string().optional(),
12942
13354
  /** Ms epoch of the last state change. */
12943
- lastChangedAt: number()
13355
+ lastChangedAt: number(),
13356
+ /** What the block is. Absent means `code` (D659). */
13357
+ source: CoreBlockSourceSchema.optional()
13358
+ });
13359
+ /** A stored composition block and what the composer says about it. */
13360
+ var CompositionViewSchema = object({
13361
+ block: CoreBlockSchema,
13362
+ state: discriminatedUnion("kind", [object({
13363
+ kind: literal("answered"),
13364
+ state: CompositionBlockStateSchema.nullable()
13365
+ }), object({
13366
+ kind: literal("unreachable"),
13367
+ error: string()
13368
+ })])
12944
13369
  });
13370
+ /**
13371
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
13372
+ * table has not been READ: an empty answer then would make "not yet known"
13373
+ * look like "none", and invite the operator to recreate what exists (D315).
13374
+ */
13375
+ var CompositionsNotLoadedSchema = object({
13376
+ state: literal("not-loaded"),
13377
+ reason: string()
13378
+ });
13379
+ var CompositionListReadSchema = discriminatedUnion("state", [object({
13380
+ state: literal("loaded"),
13381
+ views: array(CompositionViewSchema)
13382
+ }), CompositionsNotLoadedSchema]);
13383
+ var CompositionGetReadSchema = discriminatedUnion("state", [object({
13384
+ state: literal("loaded"),
13385
+ view: CompositionViewSchema.nullable()
13386
+ }), CompositionsNotLoadedSchema]);
13387
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
13388
+ var CompositionCustomizationReadSchema = discriminatedUnion("state", [object({
13389
+ state: literal("loaded"),
13390
+ view: CompositionViewSchema.nullable()
13391
+ }), CompositionsNotLoadedSchema]);
12945
13392
  /** What a compile attempt produced. */
12946
13393
  var CoreBlockCompileResultSchema = object({
12947
13394
  ok: boolean(),
@@ -12979,7 +13426,75 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
12979
13426
  }), method(object({}), object({ libs: array(object({
12980
13427
  filePath: string(),
12981
13428
  content: string()
12982
- })) }), { auth: "admin" });
13429
+ })) }), { auth: "admin" }), method(object({ block: CompositionBlockInputSchema }), object({ block: CoreBlockSchema }), {
13430
+ kind: "mutation",
13431
+ auth: "admin",
13432
+ caller: "required"
13433
+ }), method(object({
13434
+ blockId: string(),
13435
+ expectedUpdatedAt: number(),
13436
+ block: CompositionBlockInputSchema.partial()
13437
+ }), object({ block: CoreBlockSchema }), {
13438
+ kind: "mutation",
13439
+ auth: "admin",
13440
+ caller: "required"
13441
+ }), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
13442
+ addonId: string().min(1),
13443
+ stableId: string().min(1)
13444
+ }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
13445
+ composition: CompositionSchema,
13446
+ blockId: string().optional()
13447
+ }), CompositionValidationSchema, {
13448
+ kind: "mutation",
13449
+ auth: "admin",
13450
+ access: "view"
13451
+ });
13452
+ var ComposerBlockSchema = object({
13453
+ id: string().min(1),
13454
+ name: string().min(1).max(120),
13455
+ enabled: boolean(),
13456
+ updatedAt: number(),
13457
+ composition: CompositionSchema
13458
+ });
13459
+ /**
13460
+ * A stored composition row that no longer PARSES — a schema a release
13461
+ * tightened, a row written by a newer build. Sent by id so the composer keeps
13462
+ * its device as a placeholder and reports the block `failed` naming the parse
13463
+ * error. Leaving it out would read, twice, exactly like a delete (D49).
13464
+ */
13465
+ var ComposerUnreadableBlockSchema = object({
13466
+ id: string().min(1),
13467
+ /** The row's stored name column; null when even that is unreadable (the device keeps the name it has). */
13468
+ name: string().nullable(),
13469
+ /** The parse error, in the operator's words. */
13470
+ error: string()
13471
+ });
13472
+ method(object({
13473
+ /** The shared Blocks integration; null until `core-blocks` has reconciled it. */
13474
+ integrationId: string().nullable(),
13475
+ /** False when the `core_blocks` read failed: then nothing may be withdrawn (D49). */
13476
+ storeLoaded: boolean(),
13477
+ blocks: array(ComposerBlockSchema),
13478
+ /** Operator restarts: rebuild these runtimes and re-resolve their sources. Stores nothing (D62). */
13479
+ restartBlockIds: array(string()),
13480
+ /** Composition rows that exist but do not parse: held, never withdrawn, reported failed. */
13481
+ unreadableBlocks: array(ComposerUnreadableBlockSchema)
13482
+ }), object({
13483
+ blocks: array(object({
13484
+ blockId: string(),
13485
+ status: CompositionRunStatusSchema,
13486
+ reason: string().optional()
13487
+ })),
13488
+ /**
13489
+ * False when the composer declared and withdrew NOTHING (store not loaded,
13490
+ * integration unknown, its own rows unread or read empty once). An empty
13491
+ * apply the caller owes is paid off only by one the composer acted on (D49).
13492
+ */
13493
+ acted: boolean()
13494
+ }), {
13495
+ kind: "mutation",
13496
+ auth: "admin"
13497
+ }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
12983
13498
  /**
12984
13499
  * `custom-model-registry` — collection cap exposing operator-registered
12985
13500
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -14700,8 +15215,22 @@ var LinkedDeviceSchema = object({
14700
15215
  * The `linked-devices` extension slot's editable value (D648): the operator's
14701
15216
  * pick list, exactly what `_linkedDeviceIds` persists. Tree children are NOT
14702
15217
  * in it — they are always linked and are not a pick.
14703
- */
14704
- var LinkedDevicesSlotValueSchema = object({ deviceIds: array(number().int().positive()) });
15218
+ *
15219
+ * `trackedDeviceIds` (D655) is the tracked-event opt-in, exactly what
15220
+ * `_linkedDeviceTrackedIds` persists: which linked devices materialize a
15221
+ * synthetic tracked event on the camera. A read always carries it AS STORED
15222
+ * (an absent key reads as `[]`, which is what the resolver makes of it —
15223
+ * NONE). On a write it is optional: absent leaves the opt-in untouched, so a
15224
+ * card built before D655 cannot erase it by saving the pick list. A present
15225
+ * list REPLACES the opt-in; each id must be a pick, a device the camera links
15226
+ * by structure (the row's `impliedSources`: tree children, a picked
15227
+ * container's child) or already stored — so what was read can always be
15228
+ * written back unchanged, and a NEW opt-in outside the linked set is refused.
15229
+ */
15230
+ var LinkedDevicesSlotValueSchema = object({
15231
+ deviceIds: array(number().int().positive()),
15232
+ trackedDeviceIds: array(number().int().positive()).optional()
15233
+ });
14705
15234
  DeviceType.Camera;
14706
15235
  /** One camera's resolved linked set, tagged with the camera it belongs to.
14707
15236
  * The batch answer needs the tag; the single-device answer already has it
@@ -15205,6 +15734,52 @@ method(object({
15205
15734
  deviceIds: array(number()).readonly(),
15206
15735
  caps: array(string()).readonly().optional()
15207
15736
  }), record(string(), record(string(), unknown().nullable())));
15737
+ /**
15738
+ * Per-field ownership (D663). A composition may CLAIM fields of a native
15739
+ * cap's runtime-state slice on an existing device: `replace` overlays the
15740
+ * claimed fields on the native slice, `add` builds a slice the device does
15741
+ * not natively have. Declared here — once — so the hub mirror (Task 5) and
15742
+ * the claim methods (Task 6) share one union.
15743
+ */
15744
+ var FieldClaimModeSchema = _enum(["replace", "add"]);
15745
+ /** What a claim, patch or release answers. A refusal always names its reason. */
15746
+ var ClaimOutcomeSchema = discriminatedUnion("ok", [object({
15747
+ ok: literal(true),
15748
+ changed: boolean()
15749
+ }), object({
15750
+ ok: literal(false),
15751
+ code: _enum([
15752
+ "owned-by-other",
15753
+ "not-claimed",
15754
+ "field-not-claimed",
15755
+ "invalid-slice",
15756
+ "unknown-device",
15757
+ "row-unreadable",
15758
+ "migration-in-flight"
15759
+ ]),
15760
+ message: string()
15761
+ })]);
15762
+ /** One claim as `listClaims` reports it. `fields` is the PLANNED set, explicit (ruling S5). */
15763
+ var FieldClaimSchema = object({
15764
+ deviceId: number(),
15765
+ capName: string(),
15766
+ owner: string().min(1),
15767
+ mode: FieldClaimModeSchema,
15768
+ fields: array(string().min(1)).min(1)
15769
+ });
15770
+ /**
15771
+ * What `listClaims` answers. The claims INDEX is a listing projection loaded
15772
+ * from the settings store at boot; until a load has succeeded it is
15773
+ * `not-loaded`, never `[]` — an unanswered question must not look like an
15774
+ * empty answer (D315).
15775
+ */
15776
+ var ClaimsListingSchema = discriminatedUnion("state", [object({
15777
+ state: literal("loaded"),
15778
+ claims: array(FieldClaimSchema)
15779
+ }), object({
15780
+ state: literal("not-loaded"),
15781
+ reason: string()
15782
+ })]);
15208
15783
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
15209
15784
  deviceId: number(),
15210
15785
  capName: string()
@@ -15212,7 +15787,35 @@ method(object({ deviceId: number() }), record(string(), record(string(), unknown
15212
15787
  deviceId: number(),
15213
15788
  capName: string(),
15214
15789
  slice: record(string(), unknown())
15215
- }), _void(), { kind: "mutation" }), object({
15790
+ }), _void(), { kind: "mutation" }), method(object({
15791
+ deviceId: number(),
15792
+ capName: string(),
15793
+ owner: string().min(1),
15794
+ mode: FieldClaimModeSchema,
15795
+ fields: array(string().min(1)).min(1),
15796
+ values: record(string(), unknown())
15797
+ }), ClaimOutcomeSchema, {
15798
+ kind: "mutation",
15799
+ auth: "admin"
15800
+ }), method(object({
15801
+ deviceId: number(),
15802
+ capName: string(),
15803
+ owner: string().min(1),
15804
+ values: record(string(), unknown())
15805
+ }), ClaimOutcomeSchema, {
15806
+ kind: "mutation",
15807
+ auth: "admin"
15808
+ }), method(object({
15809
+ deviceId: number(),
15810
+ capName: string(),
15811
+ owner: string().min(1)
15812
+ }), ClaimOutcomeSchema, {
15813
+ kind: "mutation",
15814
+ auth: "admin"
15815
+ }), method(object({ ownerPrefix: string().optional() }), ClaimsListingSchema, { auth: "admin" }), method(object({
15816
+ deviceId: number(),
15817
+ capName: string()
15818
+ }), record(string(), unknown()).nullable(), { auth: "admin" }), object({
15216
15819
  deviceId: number(),
15217
15820
  capName: string(),
15218
15821
  slice: record(string(), unknown())
@@ -19184,8 +19787,8 @@ var NcTestResultSchema = object({
19184
19787
  className: string().optional(),
19185
19788
  label: string().optional()
19186
19789
  });
19187
- /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is the
19188
- * same union; the template-vars spec pins the two together. `mixed` is DERIVED
19790
+ /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is
19791
+ * `z.infer` of this schema — one shape, no twin. `mixed` is DERIVED
19189
19792
  * (a stored system rule whose kinds span families, D637) and never authored. */
19190
19793
  var NcRuleKindSchema = _enum([
19191
19794
  "detection",
@@ -21100,6 +21703,13 @@ var AudioEventSchema = object({
21100
21703
  originalClass: string().optional(),
21101
21704
  score: number()
21102
21705
  }).optional(),
21706
+ /**
21707
+ * Whether the classifier RAN on the window behind this row (D660). Absent
21708
+ * on rows written before D660 — unknown, NOT classified: until then a
21709
+ * window the analyzer's busy gate threw away was stored exactly like one
21710
+ * that heard nothing above the floor.
21711
+ */
21712
+ classifyOutcome: AudioClassifyOutcomeSchema.optional(),
21103
21713
  /** Populated by B5 (recording playback URL for this event). */
21104
21714
  mediaUrl: string().optional()
21105
21715
  });
@@ -21812,7 +22422,15 @@ var RebuildObjectEmbeddingsInput = object({
21812
22422
  * force is logged at start and finish so a deliberately slow pass reads
21813
22423
  * differently from a stalled one.
21814
22424
  */
21815
- pacingMs: number().int().nonnegative().optional()
22425
+ pacingMs: number().int().nonnegative().optional(),
22426
+ /**
22427
+ * Re-embed ONLY the tracks whose vector is ALREADY in the pinned model's
22428
+ * space (D664) — the in-space skip inverted. It exists to redo vectors
22429
+ * written from the wrong box, which a normal pass would skip as done
22430
+ * forever. Requires `maxTracks`: a forced pass without a bound is refused
22431
+ * (`force-requires-max-tracks`).
22432
+ */
22433
+ force: boolean().optional()
21816
22434
  });
21817
22435
  /**
21818
22436
  * Result of emptying the CLIP index.
@@ -21848,6 +22466,7 @@ var WipeObjectEmbeddingsResultSchema = object({
21848
22466
  * - `reads-disagree` — the two reads named different choices.
21849
22467
  * - `not-a-clip-model` — the CONFIRMED id has no CLIP metadata, so there is no
21850
22468
  * index to fill and no dimension to check against (D649).
22469
+ * - `force-requires-max-tracks` — `force: true` with no `maxTracks` (D664).
21851
22470
  */
21852
22471
  var RebuildObjectEmbeddingsRefusalSchema = _enum([
21853
22472
  "stores-unavailable",
@@ -21855,7 +22474,8 @@ var RebuildObjectEmbeddingsRefusalSchema = _enum([
21855
22474
  "method-unavailable",
21856
22475
  "read-failed",
21857
22476
  "reads-disagree",
21858
- "not-a-clip-model"
22477
+ "not-a-clip-model",
22478
+ "force-requires-max-tracks"
21859
22479
  ]);
21860
22480
  /**
21861
22481
  * Acknowledgement that a rebuild STARTED.
@@ -21880,11 +22500,32 @@ var RebuildStatusSchema = object({
21880
22500
  running: boolean(),
21881
22501
  scanned: number(),
21882
22502
  rebuilt: number(),
21883
- /** Tracks whose key frame is gone — nothing to re-embed from. */
22503
+ /** Tracks whose key frame is gone — nothing to re-embed from. Pixels ONLY:
22504
+ * `missingEvent` is its own field since D664, never summed in. */
21884
22505
  missingKeyFrame: number(),
21885
- /** Tracks with no usable detection box. */
22506
+ /** Tracks with no object event at all (D664). */
22507
+ missingEvent: number(),
22508
+ /** Tracks with no box measured on the pixels to cut — see `boxSource.refused`. */
21886
22509
  missingBbox: number(),
21887
22510
  /**
22511
+ * Which rule placed each judged track's box (D664): the key frame row's own
22512
+ * `subjectBox`, `track.positions` at exactly its instant (older key frames),
22513
+ * the event box (a key frame recovered from the recording, at the event's
22514
+ * instant), or `refused` by name — the reasons are in the tagged log lines.
22515
+ */
22516
+ boxSource: object({
22517
+ subjectBox: number(),
22518
+ positions: number(),
22519
+ event: number(),
22520
+ refused: number()
22521
+ }),
22522
+ /** Tracks skipped because their vector is already in the pinned space. */
22523
+ alreadyInSpace: number(),
22524
+ /** Whether the current / last pass was a `force` pass; `null` before any (D664). */
22525
+ force: boolean().nullable(),
22526
+ /** In-space tracks a `force` pass selected for re-embedding. */
22527
+ forcedInSpace: number(),
22528
+ /**
21888
22529
  * Tracks an executing node REFUSED rather than broke on — an unreadable key
21889
22530
  * frame, a step that threw. Separate from `failed` because the remedy is
21890
22531
  * different, and because a whole camera silently contributing zero vectors
@@ -22389,7 +23030,8 @@ var PipelineModelOptionSchema = object({
22389
23030
  })),
22390
23031
  group: ModelVariantGroupSchema.optional(),
22391
23032
  legacy: boolean().optional(),
22392
- provider: ModelProviderIdSchema.optional()
23033
+ provider: ModelProviderIdSchema.optional(),
23034
+ defaultConfidence: number().optional()
22393
23035
  });
22394
23036
  var ConfigFieldBridge = custom();
22395
23037
  var PipelineAddonSchemaSchema = object({
@@ -22691,7 +23333,14 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
22691
23333
  height: number()
22692
23334
  }), { kind: "mutation" }), method(object({
22693
23335
  stepId: string(),
22694
- frameId: number().int()
23336
+ frameId: number().int(),
23337
+ /**
23338
+ * The model to time — REQUIRED (D658). Without it the pool answered the
23339
+ * step's ACTIVE variant (the first loaded after boot), so a benchmark
23340
+ * of model B silently timed model A. Callers pass the `rootModelId`
23341
+ * their warmup `runPipeline` reported: the model that actually ran.
23342
+ */
23343
+ modelId: string().min(1)
22695
23344
  }), record(string(), unknown()), { kind: "mutation" }), method(object({ frameId: number().int() }), _void(), { kind: "mutation" }), method(object({ deviceKey: string().optional() }), EffectiveTuningSchema), method(_void(), array(object({
22696
23345
  engineKey: string(),
22697
23346
  engine: PipelineEngineChoiceSchema,
@@ -26704,7 +27353,6 @@ var doorbellCapability = {
26704
27353
  kind: "wrapper",
26705
27354
  defaultActive: false,
26706
27355
  deviceTypes: [DeviceType.Button, DeviceType.Camera],
26707
- exposesDeviceSettings: true,
26708
27356
  extensionSlots: [DOORBELL_SOURCE_SLOT],
26709
27357
  methods: {},
26710
27358
  events: {
@@ -26754,7 +27402,7 @@ var ExtensionSourceSchema = object({
26754
27402
  /**
26755
27403
  * How one picked source stands. `missing` — no such device (deleted, or an id
26756
27404
  * that never existed); `foreign` — a device the slot's owner cannot read from
26757
- * (slice 2's cross-owner graft source); `unavailable` — it exists but is not a
27405
+ * (a source owned by another addon); `unavailable` — it exists but is not a
26758
27406
  * valid source now (a container, which is never a pick). A source in any of
26759
27407
  * these states is SHOWN, never silently dropped or re-pointed.
26760
27408
  */
@@ -26794,13 +27442,19 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
26794
27442
  slot: ExtensionSlotIdSchema,
26795
27443
  /** Names the store written (D62), e.g. `virtual-doorbell:doorbellSources`. */
26796
27444
  authority: string(),
26797
- /** graft: its switch. association: null — removing every source is "off". */
26798
- enabled: boolean().nullable(),
26799
- /** graft: the cap registered on the host. association: null. */
26800
- projects: string().nullable(),
26801
27445
  activation: ExtensionSlotActivationSchema.nullable(),
26802
27446
  value: ExtensionSlotValueSchema,
26803
- sources: array(ExtensionSlotSourceSchema)
27447
+ sources: array(ExtensionSlotSourceSchema),
27448
+ /**
27449
+ * Devices the owner links to the host ON ITS OWN, by structure, and that
27450
+ * are therefore not in `value` (D655): a camera's device-tree children, and
27451
+ * the child a picked container stands for (D356 — a container is never a
27452
+ * pick, its children are). Read-only; the editor shows them and may offer
27453
+ * per-device options on them (the linked-devices tracked-event opt-in).
27454
+ * Absent when the owner links nothing beyond the picks, and from an owner
27455
+ * older than D655.
27456
+ */
27457
+ impliedSources: array(ExtensionSlotSourceSchema).optional()
26804
27458
  }), object({
26805
27459
  state: literal("unreachable"),
26806
27460
  addonId: string(),
@@ -26821,7 +27475,8 @@ var ExtensionCandidatesSchema = object({ candidates: array(object({
26821
27475
  /**
26822
27476
  * What a write did. `none` — applied in place. `host` / `parent` — the owner
26823
27477
  * reloaded the host (or, for an accessory child, its parent) because the
26824
- * write changed what the host REGISTERS (a graft switched on or off, slice 2).
27478
+ * write changed what the host REGISTERS. A slot never adds a capability to its
27479
+ * host — that is a composition (D663) — so today's owners answer `none`.
26825
27480
  */
26826
27481
  var ExtensionSetResultSchema = object({
26827
27482
  applied: literal(true),
@@ -28101,7 +28756,9 @@ var batteryCapability = {
28101
28756
  DeviceType.Camera,
28102
28757
  DeviceType.Sensor,
28103
28758
  DeviceType.Button,
28104
- DeviceType.Switch
28759
+ DeviceType.Switch,
28760
+ DeviceType.PetFeeder,
28761
+ DeviceType.Container
28105
28762
  ],
28106
28763
  methods: {
28107
28764
  /**
@@ -28128,6 +28785,19 @@ wakeForStream: method(object({
28128
28785
  awoke: boolean(),
28129
28786
  durationMs: number()
28130
28787
  }), { kind: "mutation" }) },
28788
+ /**
28789
+ * A composed provider (battery ADDED to a pet feeder or container that has
28790
+ * no wake surface) answers `wakeForStream` with the same "unavailable"
28791
+ * result the contract already defines above: "Returns `awoke: false` when
28792
+ * … the cap surface is unavailable." (D663).
28793
+ */
28794
+ composedMethods: { wakeForStream: {
28795
+ kind: "answer",
28796
+ value: {
28797
+ awoke: false,
28798
+ durationMs: 0
28799
+ }
28800
+ } },
28131
28801
  events: {
28132
28802
  /**
28133
28803
  * Emitted whenever the cached status changes (firmware push OR
@@ -28143,7 +28813,7 @@ onStatusChanged: { data: object({
28143
28813
  schema: BatteryStatusSchema,
28144
28814
  kind: "push",
28145
28815
  empty: {
28146
- percentage: 0,
28816
+ percentage: null,
28147
28817
  charging: "unknown",
28148
28818
  sleeping: false,
28149
28819
  lastUpdated: 0
@@ -28885,6 +29555,19 @@ var connectivityCapability = {
28885
29555
  * provider populates it by guessing (no HA inference). The UI renders a
28886
29556
  * "No consumables reported" placeholder when `items` is empty.
28887
29557
  */
29558
+ /** Units a remaining-life counter is reported in. Closed: the panel renders
29559
+ * each. */
29560
+ var ConsumableRemainingUnitSchema = _enum([
29561
+ "days",
29562
+ "hours",
29563
+ "cycles"
29564
+ ]);
29565
+ /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
29566
+ * days). `value` null = unknown (D393). */
29567
+ var ConsumableRemainingSchema = object({
29568
+ value: number().min(0).nullable(),
29569
+ unit: ConsumableRemainingUnitSchema
29570
+ });
28888
29571
  /** A single consumable item. Either a continuous `level` (remaining
28889
29572
  * life %) or a discrete `status` may be known — both may be null when a
28890
29573
  * provider only knows the item exists. `level` and `status` are not
@@ -28901,7 +29584,10 @@ var ConsumableItemSchema = object({
28901
29584
  /** Ms epoch of the last replace, when known. */
28902
29585
  lastResetAt: number().nullable(),
28903
29586
  /** Whether `reset()` is meaningful for this item. */
28904
- resettable: boolean()
29587
+ resettable: boolean(),
29588
+ /** Remaining life as a count (days/hours/cycles), when a provider reports
29589
+ * it that way instead of — or alongside — `level`. Absent = not reported. */
29590
+ remaining: ConsumableRemainingSchema.optional()
28905
29591
  });
28906
29592
  var ConsumablesStatusSchema = object({
28907
29593
  items: array(ConsumableItemSchema),
@@ -28931,6 +29617,16 @@ reset: method(object({
28931
29617
  kind: "mutation",
28932
29618
  auth: "admin"
28933
29619
  }) },
29620
+ /**
29621
+ * A composed consumable (item added to a device with no native consumables
29622
+ * provider) refuses `reset` by name: "Only meaningful when the item's
29623
+ * `resettable` is true" — a composed item is never resettable until
29624
+ * commands (`forward`) arrive in slice 3 (D663).
29625
+ */
29626
+ composedMethods: { reset: {
29627
+ kind: "refuse",
29628
+ reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
29629
+ } },
28934
29630
  status: {
28935
29631
  schema: ConsumablesStatusSchema,
28936
29632
  kind: "push",
@@ -38043,6 +38739,42 @@ var zoneRulesCapability = {
38043
38739
  */
38044
38740
  durability: "restored"
38045
38741
  };
38742
+ var NUMBER = {
38743
+ kind: "fixed",
38744
+ result: ["number"]
38745
+ };
38746
+ var BOOLEAN = {
38747
+ kind: "fixed",
38748
+ result: ["boolean"]
38749
+ };
38750
+ Object.freeze({
38751
+ min: NUMBER,
38752
+ max: NUMBER,
38753
+ abs: NUMBER,
38754
+ floor: NUMBER,
38755
+ ceil: NUMBER,
38756
+ sqrt: NUMBER,
38757
+ round: NUMBER,
38758
+ pow: NUMBER,
38759
+ clamp: NUMBER,
38760
+ avg: NUMBER,
38761
+ sum: NUMBER,
38762
+ age: NUMBER,
38763
+ convert: NUMBER,
38764
+ coalesce: { kind: "coalesce" },
38765
+ any: BOOLEAN,
38766
+ all: BOOLEAN,
38767
+ within: BOOLEAN,
38768
+ latest: {
38769
+ kind: "fixed",
38770
+ result: ["number", "null"]
38771
+ },
38772
+ number: {
38773
+ kind: "fixed",
38774
+ result: ["number", "null"]
38775
+ }
38776
+ });
38777
+ `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
38046
38778
  /**
38047
38779
  * Accessory device helpers — shared across drivers.
38048
38780
  *
@@ -38186,38 +38918,31 @@ var DeviceConfig = class DeviceConfig {
38186
38918
  }));
38187
38919
  }
38188
38920
  };
38189
- /**
38190
- * Concrete implementation. Routes every successful write through
38191
- * `writer(capName, slice)` — the kernel hooks this up to
38192
- * `device-state.setCapSlice`, the canonical cross-layer write
38193
- * entrypoint, which handles disk persistence (debounced on the hub)
38194
- * and mirror updates.
38195
- *
38196
- * Schema validation runs in-process before the writer is called —
38197
- * the round-trip should never carry an invalid slice. `flush()`
38198
- * awaits any in-flight writer promises so shutdown is lossless.
38199
- *
38200
- * `initial` is the persisted blob loaded at boot. Slices for caps
38201
- * whose schema hasn't been installed yet are kept in-memory verbatim
38202
- * and validated when the cap registers later.
38203
- */
38204
38921
  var DeviceRuntimeState = class DeviceRuntimeState {
38205
38922
  writer;
38923
+ onWriteFailed;
38206
38924
  /** In-flight writer promises tracked so `flush()` can await them. */
38207
38925
  pendingWrites = /* @__PURE__ */ new Set();
38926
+ /**
38927
+ * Caps whose last write the hub refused. The equality gate in
38928
+ * `applyCapWrite` is bypassed for these, so the SAME value is re-sent on the
38929
+ * next write instead of being swallowed forever; a landing write clears it.
38930
+ */
38931
+ failedWrites = /* @__PURE__ */ new Set();
38208
38932
  /** Per-cap committed slice — after schema validation when known. */
38209
38933
  slices;
38210
38934
  /** Per-cap registered schema (set by `installCapSchema`). */
38211
38935
  schemas = /* @__PURE__ */ new Map();
38212
38936
  listeners = /* @__PURE__ */ new Set();
38213
38937
  capListeners = /* @__PURE__ */ new Map();
38214
- constructor(initial, writer) {
38938
+ constructor(initial, writer, onWriteFailed) {
38215
38939
  this.writer = writer;
38940
+ this.onWriteFailed = onWriteFailed;
38216
38941
  this.slices = /* @__PURE__ */ new Map();
38217
38942
  for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
38218
38943
  }
38219
- static fromInitial(initial, writer) {
38220
- return new DeviceRuntimeState(initial, writer);
38944
+ static fromInitial(initial, writer, onWriteFailed) {
38945
+ return new DeviceRuntimeState(initial, writer, onWriteFailed);
38221
38946
  }
38222
38947
  installCapSchema(capName, schema) {
38223
38948
  const existing = this.schemas.get(capName);
@@ -38261,10 +38986,15 @@ var DeviceRuntimeState = class DeviceRuntimeState {
38261
38986
  ...value
38262
38987
  } : { ...value };
38263
38988
  const parsed = schema.parse(next);
38264
- if (shallowEqual(current, parsed)) return;
38989
+ if (shallowEqual(current, parsed) && !this.failedWrites.has(capName)) return;
38265
38990
  this.slices.set(capName, parsed);
38266
38991
  this.fireListeners([capName]);
38267
- const writePromise = this.writer(capName, { ...parsed }).catch(() => {});
38992
+ const writePromise = this.writer(capName, { ...parsed }).then(() => {
38993
+ this.failedWrites.delete(capName);
38994
+ }).catch((err) => {
38995
+ this.failedWrites.add(capName);
38996
+ this.onWriteFailed?.(capName, err instanceof Error ? err.message : String(err));
38997
+ });
38268
38998
  this.pendingWrites.add(writePromise);
38269
38999
  writePromise.finally(() => {
38270
39000
  this.pendingWrites.delete(writePromise);
@@ -38541,7 +39271,16 @@ var BaseDevice = class {
38541
39271
  });
38542
39272
  };
38543
39273
  const initial = ctx.initialRuntimeState ?? {};
38544
- this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer);
39274
+ const onWriteFailed = (cap, error) => {
39275
+ ctx.logger.warn("runtime-state write refused by the hub — the slice is not mirrored; it is re-sent on the next write", {
39276
+ tags: { deviceId: ctx.id },
39277
+ meta: {
39278
+ cap,
39279
+ error
39280
+ }
39281
+ });
39282
+ };
39283
+ this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer, onWriteFailed);
38545
39284
  ctx.bindRuntimeState?.(this.runtimeState);
38546
39285
  ctx.registerNativeCap?.(deviceStatusCapability, {});
38547
39286
  const seed = {
@@ -41809,6 +42548,18 @@ Object.freeze({
41809
42548
  addonId: null,
41810
42549
  access: "create"
41811
42550
  },
42551
+ "composer.applyBlocks": {
42552
+ capName: "composer",
42553
+ capScope: "system",
42554
+ addonId: null,
42555
+ access: "create"
42556
+ },
42557
+ "composer.getStates": {
42558
+ capName: "composer",
42559
+ capScope: "system",
42560
+ addonId: null,
42561
+ access: "view"
42562
+ },
41812
42563
  "connectionTest.describeTest": {
41813
42564
  capName: "connection-test",
41814
42565
  capScope: "system",
@@ -41869,6 +42620,12 @@ Object.freeze({
41869
42620
  addonId: null,
41870
42621
  access: "create"
41871
42622
  },
42623
+ "coreBlocks.createComposition": {
42624
+ capName: "core-blocks",
42625
+ capScope: "system",
42626
+ addonId: null,
42627
+ access: "create"
42628
+ },
41872
42629
  "coreBlocks.delete": {
41873
42630
  capName: "core-blocks",
41874
42631
  capScope: "system",
@@ -41881,6 +42638,18 @@ Object.freeze({
41881
42638
  addonId: null,
41882
42639
  access: "view"
41883
42640
  },
42641
+ "coreBlocks.getComposition": {
42642
+ capName: "core-blocks",
42643
+ capScope: "system",
42644
+ addonId: null,
42645
+ access: "view"
42646
+ },
42647
+ "coreBlocks.getCustomization": {
42648
+ capName: "core-blocks",
42649
+ capScope: "system",
42650
+ addonId: null,
42651
+ access: "view"
42652
+ },
41884
42653
  "coreBlocks.getTypeDefs": {
41885
42654
  capName: "core-blocks",
41886
42655
  capScope: "system",
@@ -41893,6 +42662,12 @@ Object.freeze({
41893
42662
  addonId: null,
41894
42663
  access: "view"
41895
42664
  },
42665
+ "coreBlocks.listCompositions": {
42666
+ capName: "core-blocks",
42667
+ capScope: "system",
42668
+ addonId: null,
42669
+ access: "view"
42670
+ },
41896
42671
  "coreBlocks.restart": {
41897
42672
  capName: "core-blocks",
41898
42673
  capScope: "system",
@@ -41911,6 +42686,18 @@ Object.freeze({
41911
42686
  addonId: null,
41912
42687
  access: "create"
41913
42688
  },
42689
+ "coreBlocks.updateComposition": {
42690
+ capName: "core-blocks",
42691
+ capScope: "system",
42692
+ addonId: null,
42693
+ access: "create"
42694
+ },
42695
+ "coreBlocks.validateComposition": {
42696
+ capName: "core-blocks",
42697
+ capScope: "system",
42698
+ addonId: null,
42699
+ access: "view"
42700
+ },
41914
42701
  "cover.close": {
41915
42702
  capName: "cover",
41916
42703
  capScope: "device",
@@ -42397,12 +43184,6 @@ Object.freeze({
42397
43184
  addonId: null,
42398
43185
  access: "create"
42399
43186
  },
42400
- "deviceManager.applyDeviceSettingsPatch": {
42401
- capName: "device-manager",
42402
- capScope: "system",
42403
- addonId: null,
42404
- access: "create"
42405
- },
42406
43187
  "deviceManager.applyInitialMeta": {
42407
43188
  capName: "device-manager",
42408
43189
  capScope: "system",
@@ -42505,12 +43286,6 @@ Object.freeze({
42505
43286
  addonId: null,
42506
43287
  access: "view"
42507
43288
  },
42508
- "deviceManager.getDeviceLiveContribution": {
42509
- capName: "device-manager",
42510
- capScope: "system",
42511
- addonId: null,
42512
- access: "view"
42513
- },
42514
43289
  "deviceManager.getDeviceLiveInfoAggregate": {
42515
43290
  capName: "device-manager",
42516
43291
  capScope: "system",
@@ -42523,12 +43298,6 @@ Object.freeze({
42523
43298
  addonId: null,
42524
43299
  access: "view"
42525
43300
  },
42526
- "deviceManager.getDeviceSettingsContribution": {
42527
- capName: "device-manager",
42528
- capScope: "system",
42529
- addonId: null,
42530
- access: "view"
42531
- },
42532
43301
  "deviceManager.getDeviceStatusAggregate": {
42533
43302
  capName: "device-manager",
42534
43303
  capScope: "system",
@@ -43003,6 +43772,12 @@ Object.freeze({
43003
43772
  addonId: null,
43004
43773
  access: "create"
43005
43774
  },
43775
+ "deviceState.claimFields": {
43776
+ capName: "device-state",
43777
+ capScope: "system",
43778
+ addonId: null,
43779
+ access: "create"
43780
+ },
43006
43781
  "deviceState.getAllSnapshots": {
43007
43782
  capName: "device-state",
43008
43783
  capScope: "system",
@@ -43015,38 +43790,44 @@ Object.freeze({
43015
43790
  addonId: null,
43016
43791
  access: "view"
43017
43792
  },
43018
- "deviceState.getSnapshot": {
43793
+ "deviceState.getNativeCapSlice": {
43019
43794
  capName: "device-state",
43020
43795
  capScope: "system",
43021
43796
  addonId: null,
43022
43797
  access: "view"
43023
43798
  },
43024
- "deviceState.setCapSlice": {
43799
+ "deviceState.getSnapshot": {
43025
43800
  capName: "device-state",
43026
43801
  capScope: "system",
43027
43802
  addonId: null,
43028
- access: "create"
43803
+ access: "view"
43029
43804
  },
43030
- "deviceStatus.getStatus": {
43031
- capName: "device-status",
43032
- capScope: "device",
43805
+ "deviceState.listClaims": {
43806
+ capName: "device-state",
43807
+ capScope: "system",
43033
43808
  addonId: null,
43034
43809
  access: "view"
43035
43810
  },
43036
- "doorbell.applyDeviceSettingsPatch": {
43037
- capName: "doorbell",
43038
- capScope: "device",
43811
+ "deviceState.patchOwnedFields": {
43812
+ capName: "device-state",
43813
+ capScope: "system",
43039
43814
  addonId: null,
43040
43815
  access: "create"
43041
43816
  },
43042
- "doorbell.getDeviceLiveContribution": {
43043
- capName: "doorbell",
43044
- capScope: "device",
43817
+ "deviceState.releaseClaim": {
43818
+ capName: "device-state",
43819
+ capScope: "system",
43045
43820
  addonId: null,
43046
- access: "view"
43821
+ access: "create"
43047
43822
  },
43048
- "doorbell.getDeviceSettingsContribution": {
43049
- capName: "doorbell",
43823
+ "deviceState.setCapSlice": {
43824
+ capName: "device-state",
43825
+ capScope: "system",
43826
+ addonId: null,
43827
+ access: "create"
43828
+ },
43829
+ "deviceStatus.getStatus": {
43830
+ capName: "device-status",
43050
43831
  capScope: "device",
43051
43832
  addonId: null,
43052
43833
  access: "view"
@@ -48554,16 +49335,36 @@ Object.freeze({
48554
49335
  form: "single",
48555
49336
  optional: false
48556
49337
  }],
49338
+ "deviceState.claimFields": [{
49339
+ name: "deviceId",
49340
+ form: "single",
49341
+ optional: false
49342
+ }],
48557
49343
  "deviceState.getCapSlice": [{
48558
49344
  name: "deviceId",
48559
49345
  form: "single",
48560
49346
  optional: false
48561
49347
  }],
49348
+ "deviceState.getNativeCapSlice": [{
49349
+ name: "deviceId",
49350
+ form: "single",
49351
+ optional: false
49352
+ }],
48562
49353
  "deviceState.getSnapshot": [{
48563
49354
  name: "deviceId",
48564
49355
  form: "single",
48565
49356
  optional: false
48566
49357
  }],
49358
+ "deviceState.patchOwnedFields": [{
49359
+ name: "deviceId",
49360
+ form: "single",
49361
+ optional: false
49362
+ }],
49363
+ "deviceState.releaseClaim": [{
49364
+ name: "deviceId",
49365
+ form: "single",
49366
+ optional: false
49367
+ }],
48567
49368
  "deviceState.setCapSlice": [{
48568
49369
  name: "deviceId",
48569
49370
  form: "single",