@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,4 @@
1
- //#region ../types/dist/event-category-BVDXG4tB.mjs
1
+ //#region ../types/dist/event-category-C5xZWqz6.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -362,6 +362,16 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
362
362
  */
363
363
  EventCategory["DeviceStateChanged"] = "device.state-changed";
364
364
  /**
365
+ * The NATIVE shadow of a cap a composition has claimed fields of moved
366
+ * (D663). Payload: `{ deviceId, capName, native }` — what the provider
367
+ * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
368
+ * the composer's native self-reads, and the public event routers refuse it
369
+ * (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
370
+ * truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
371
+ * `replace` claim, one per native write of that cap.
372
+ */
373
+ EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
374
+ /**
365
375
  * Frame occupancy for a camera CHANGED — a tracked object was gained or
366
376
  * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
367
377
  *
@@ -5359,7 +5369,7 @@ var ZodIssueCode = {
5359
5369
  var ZodFirstPartyTypeKind;
5360
5370
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5371
  //#endregion
5362
- //#region ../types/dist/sleep-BXv0ZcRa.mjs
5372
+ //#region ../types/dist/sleep-jkRQgtSc.mjs
5363
5373
  /**
5364
5374
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5375
  * window to float samples (D455).
@@ -7060,324 +7070,6 @@ function event(data) {
7060
7070
  var StaticDirOutputSchema$1 = object({ staticDir: string() });
7061
7071
  var VersionOutputSchema$1 = object({ version: string() });
7062
7072
  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
7073
  var StaticDirOutputSchema = object({ staticDir: string() });
7382
7074
  var VersionOutputSchema = object({ version: string() });
7383
7075
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -7445,7 +7137,325 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7445
7137
  auth: "admin"
7446
7138
  }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7447
7139
  //#endregion
7448
- //#region ../types/dist/err-msg-IQTHeDzc.mjs
7140
+ //#region ../types/dist/err-msg-DX6i_MY4.mjs
7141
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
7142
+ DeviceType["Camera"] = "camera";
7143
+ DeviceType["Hub"] = "hub";
7144
+ DeviceType["Light"] = "light";
7145
+ DeviceType["Siren"] = "siren";
7146
+ DeviceType["Switch"] = "switch";
7147
+ DeviceType["Sensor"] = "sensor";
7148
+ DeviceType["Thermostat"] = "thermostat";
7149
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
7150
+ * `climate-control` cap surface with `Thermostat` but renders a
7151
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
7152
+ * independent vertical/horizontal swing). Sources: native Gree, and
7153
+ * reusable by other AC integrations. */
7154
+ DeviceType["Climate"] = "climate";
7155
+ DeviceType["Button"] = "button";
7156
+ /** Generic stateless event emitter — carries a device's EXACT declared
7157
+ * event vocabulary verbatim (no normalization). Installed with the
7158
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
7159
+ * HA bus events (e.g. `zha_event`, generic). */
7160
+ DeviceType["EventEmitter"] = "event-emitter";
7161
+ /** Firmware/software update entity — current vs available version,
7162
+ * updatable flag, update state, and an install action. Installed with
7163
+ * the `update` cap. Sources: Homematic firmware-update channels (and
7164
+ * reusable by other providers, e.g. HA `update.*` entities). */
7165
+ DeviceType["Update"] = "update";
7166
+ DeviceType["Generic"] = "generic";
7167
+ /** Generic notification delivery target (HA `notify.<service>`, future
7168
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
7169
+ * endpoint; the `notifier` cap defines the send surface. */
7170
+ DeviceType["Notifier"] = "notifier";
7171
+ /** Pre-recorded action sequence with optional parameters
7172
+ * (HA `script.*`). Runnable via `script-runner` cap. */
7173
+ DeviceType["Script"] = "script";
7174
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
7175
+ * trigger surface exposed via `automation-control` cap. */
7176
+ DeviceType["Automation"] = "automation";
7177
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
7178
+ DeviceType["Lock"] = "lock";
7179
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
7180
+ * `valve.*`). `cover` cap with sub-roles for variant. */
7181
+ DeviceType["Cover"] = "cover";
7182
+ /** Pipe / water / gas valve with open/close/stop and optional
7183
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
7184
+ * modelled on the same open/closed lifecycle. */
7185
+ DeviceType["Valve"] = "valve";
7186
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
7187
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
7188
+ * modelled on the same target / mode lifecycle. */
7189
+ DeviceType["Humidifier"] = "humidifier";
7190
+ /** Water heater / boiler with target temperature + operation mode +
7191
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
7192
+ * climate-family actuator. */
7193
+ DeviceType["WaterHeater"] = "water-heater";
7194
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
7195
+ DeviceType["Fan"] = "fan";
7196
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
7197
+ * the camera surface — those use `Camera`. `media-player` cap. */
7198
+ DeviceType["MediaPlayer"] = "media-player";
7199
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
7200
+ * `alarm-panel` cap. */
7201
+ DeviceType["AlarmPanel"] = "alarm-panel";
7202
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
7203
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
7204
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
7205
+ * TextControl / DateTimeControl. */
7206
+ DeviceType["Control"] = "control";
7207
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
7208
+ * `presence` cap. */
7209
+ DeviceType["Presence"] = "presence";
7210
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
7211
+ * `weather` cap. */
7212
+ DeviceType["Weather"] = "weather";
7213
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
7214
+ DeviceType["Vacuum"] = "vacuum";
7215
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
7216
+ * `lawn-mower-control` cap. */
7217
+ DeviceType["LawnMower"] = "lawn-mower";
7218
+ /** Physical HA device group — parent container for entity-children
7219
+ * adopted from a single HA device entry. Not renderable as a
7220
+ * standalone device; exists only to anchor child entities. */
7221
+ DeviceType["Container"] = "container";
7222
+ /** Single still-image entity (HA `image.*`). Read-only display of an
7223
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
7224
+ DeviceType["Image"] = "image";
7225
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
7226
+ * level, battery, desiccant life, feeding state and manual-feed /
7227
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
7228
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
7229
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
7230
+ * integrations sharing the same food/desiccant/hopper surface. */
7231
+ DeviceType["PetFeeder"] = "pet-feeder";
7232
+ return DeviceType;
7233
+ }({});
7234
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
7235
+ DeviceFeature["BatteryOperated"] = "battery-operated";
7236
+ DeviceFeature["Rebootable"] = "rebootable";
7237
+ /**
7238
+ * Device supports an on-demand re-sync of its derived spec with its
7239
+ * upstream source — drives the generic Re-sync button. The owning
7240
+ * provider implements the action via the `device-adoption.resync` cap.
7241
+ */
7242
+ DeviceFeature["Resyncable"] = "resyncable";
7243
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
7244
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
7245
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
7246
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
7247
+ /**
7248
+ * Camera supports the on-firmware autotrack subsystem (subject-
7249
+ * following). Distinct from `PanTiltZoom` because not every PTZ
7250
+ * camera ships autotrack — the admin UI uses this flag to gate
7251
+ * the autotrack toggle / settings card without re-deriving from
7252
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
7253
+ * driver sets this feature when probe confirms the firmware
7254
+ * surface, and registers the cap in the same code path.
7255
+ */
7256
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
7257
+ /**
7258
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
7259
+ * motion detection automatically activates this device. Mirrors
7260
+ * `motion-trigger` cap registration: drivers set this feature in the
7261
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
7262
+ *
7263
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
7264
+ * fast scalar without binding fetch), notifier rules, and `listAll`
7265
+ * filters that want "all devices with on-motion behaviour".
7266
+ */
7267
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
7268
+ /** Light supports rgb-triplet color via `color` cap. */
7269
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
7270
+ /** Light supports HSV color via `color` cap. */
7271
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
7272
+ /** Light supports color-temperature (mired) via `color` cap. */
7273
+ DeviceFeature["LightColorMired"] = "light-color-mired";
7274
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
7275
+ * targetHigh). Gates the range slider UI. */
7276
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
7277
+ /** Thermostat exposes target humidity and/or current humidity
7278
+ * readings. Gates the humidity controls. */
7279
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
7280
+ /** Thermostat exposes a fan-mode selector. */
7281
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
7282
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
7283
+ DeviceFeature["ClimatePreset"] = "climate-preset";
7284
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
7285
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
7286
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
7287
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
7288
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
7289
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
7290
+ /** Cover exposes intermediate position control (0..100). Gates the
7291
+ * position slider UI. */
7292
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
7293
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
7294
+ DeviceFeature["CoverTilt"] = "cover-tilt";
7295
+ /** Valve exposes intermediate position control (0..100). Gates the
7296
+ * position slider / drag surface UI. */
7297
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
7298
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
7299
+ DeviceFeature["FanSpeed"] = "fan-speed";
7300
+ /** Fan exposes a preset mode selector. */
7301
+ DeviceFeature["FanPreset"] = "fan-preset";
7302
+ /** Fan exposes blade direction (forward/reverse) — typical of
7303
+ * ceiling fans. */
7304
+ DeviceFeature["FanDirection"] = "fan-direction";
7305
+ /** Fan exposes an oscillation toggle. */
7306
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
7307
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
7308
+ * field on the UI lock-controls panel. */
7309
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
7310
+ /** Lock supports a latch-release ("open door") action distinct from
7311
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
7312
+ * `supported_features`. Gates the Open Door button in the UI. */
7313
+ DeviceFeature["LockOpen"] = "lock-open";
7314
+ /** Media player exposes a seek-to-position surface. */
7315
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
7316
+ /** Media player exposes a volume-level setter. */
7317
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
7318
+ /** Media player exposes a mute toggle distinct from volume=0. */
7319
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
7320
+ /** Media player exposes a shuffle toggle. */
7321
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
7322
+ /** Media player exposes a repeat mode (off / all / one). */
7323
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
7324
+ /** Media player exposes a source / input selector. */
7325
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
7326
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
7327
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
7328
+ /** Media player exposes next-track. */
7329
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
7330
+ /** Media player exposes previous-track. */
7331
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
7332
+ /** Media player exposes stop distinct from pause. */
7333
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
7334
+ /** Alarm panel requires a PIN code on arm/disarm. */
7335
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
7336
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
7337
+ * addition to a textual location. */
7338
+ DeviceFeature["PresenceGps"] = "presence-gps";
7339
+ /** Notifier accepts an inline / URL image attachment. */
7340
+ DeviceFeature["NotifierImage"] = "notifier-image";
7341
+ /** Notifier accepts a priority hint (high/normal/low). */
7342
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
7343
+ /** Notifier accepts a free-form `data` payload for platform-specific
7344
+ * fields. */
7345
+ DeviceFeature["NotifierData"] = "notifier-data";
7346
+ /** Notifier supports interactive action buttons / callbacks. */
7347
+ DeviceFeature["NotifierActions"] = "notifier-actions";
7348
+ /** Notifier supports per-call recipient targeting (multi-user). */
7349
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
7350
+ /** Script runner accepts a variables map on each run invocation. */
7351
+ DeviceFeature["ScriptVariables"] = "script-variables";
7352
+ /** Automation `trigger` accepts a skipCondition flag — fires the
7353
+ * automation's actions while bypassing its condition block. */
7354
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
7355
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
7356
+ * map tile in the vacuum UI. */
7357
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
7358
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
7359
+ * switches group. */
7360
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
7361
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
7362
+ * mower map tile. */
7363
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
7364
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
7365
+ * spots) — gates the Mow action + map selectors. */
7366
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
7367
+ return DeviceFeature;
7368
+ }({});
7369
+ /**
7370
+ * Semantic role a device plays within its parent. Populated by driver
7371
+ * addons when creating accessory devices (Reolink siren/floodlight/
7372
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
7373
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
7374
+ * `role: Floodlight` renders as a bulb with a brightness slider,
7375
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
7376
+ *
7377
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
7378
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
7379
+ */
7380
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7381
+ DeviceRole["Siren"] = "siren";
7382
+ DeviceRole["Floodlight"] = "floodlight";
7383
+ DeviceRole["Spotlight"] = "spotlight";
7384
+ DeviceRole["PirSensor"] = "pir-sensor";
7385
+ DeviceRole["Chime"] = "chime";
7386
+ DeviceRole["Autotrack"] = "autotrack";
7387
+ DeviceRole["Nightvision"] = "nightvision";
7388
+ DeviceRole["PrivacyMask"] = "privacy-mask";
7389
+ DeviceRole["Doorbell"] = "doorbell";
7390
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
7391
+ * real Switch device for UI rendering / export adapters. */
7392
+ DeviceRole["BinaryHelper"] = "binary-helper";
7393
+ /** Generic motion / occupancy / moving event source. Distinct from
7394
+ * the camera accessory PirSensor role: that one is a camera child;
7395
+ * this is a standalone HA / 3rd-party motion sensor. */
7396
+ DeviceRole["MotionSensor"] = "motion-sensor";
7397
+ DeviceRole["ContactSensor"] = "contact-sensor";
7398
+ DeviceRole["LeakSensor"] = "leak-sensor";
7399
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
7400
+ DeviceRole["COSensor"] = "co-sensor";
7401
+ DeviceRole["GasSensor"] = "gas-sensor";
7402
+ DeviceRole["TamperSensor"] = "tamper-sensor";
7403
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
7404
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
7405
+ DeviceRole["SoundSensor"] = "sound-sensor";
7406
+ /** Fallback for `binary_sensor` without a known `device_class`. */
7407
+ DeviceRole["BinarySensor"] = "binary-sensor";
7408
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
7409
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
7410
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
7411
+ DeviceRole["PressureSensor"] = "pressure-sensor";
7412
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
7413
+ DeviceRole["WindSensor"] = "wind-sensor";
7414
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
7415
+ DeviceRole["RainSensor"] = "rain-sensor";
7416
+ /** UV index (weather-station `uv-sensor` cap). */
7417
+ DeviceRole["UvSensor"] = "uv-sensor";
7418
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
7419
+ * Distinct from AmbientLightSensor (lux). */
7420
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
7421
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
7422
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
7423
+ DeviceRole["PowerSensor"] = "power-sensor";
7424
+ DeviceRole["EnergySensor"] = "energy-sensor";
7425
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
7426
+ DeviceRole["CurrentSensor"] = "current-sensor";
7427
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
7428
+ /** Battery level (numeric % via `sensor` OR low-bool via
7429
+ * `binary_sensor` — the cap distinguishes via the value type). */
7430
+ DeviceRole["BatterySensor"] = "battery-sensor";
7431
+ /** Fallback for `sensor` numeric without a known `device_class`. */
7432
+ DeviceRole["NumericSensor"] = "numeric-sensor";
7433
+ /** String / enum state (HA `sensor` with `state_class: enum` or
7434
+ * `attributes.options`). */
7435
+ DeviceRole["EnumSensor"] = "enum-sensor";
7436
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
7437
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
7438
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
7439
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
7440
+ /** Last-resort fallback when nothing else matches. */
7441
+ DeviceRole["GenericSensor"] = "generic-sensor";
7442
+ DeviceRole["NumericControl"] = "numeric-control";
7443
+ DeviceRole["SelectControl"] = "select-control";
7444
+ DeviceRole["TextControl"] = "text-control";
7445
+ DeviceRole["DateTimeControl"] = "datetime-control";
7446
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
7447
+ * rich features (image, priority, channel routing). */
7448
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
7449
+ /** Chat / messaging service (HA `notify.telegram_*`,
7450
+ * `notify.discord_*`, etc.). */
7451
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
7452
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
7453
+ DeviceRole["EmailNotifier"] = "email-notifier";
7454
+ /** Fallback when the notifier service name doesn't match a known
7455
+ * pattern. */
7456
+ DeviceRole["GenericNotifier"] = "generic-notifier";
7457
+ return DeviceRole;
7458
+ }({});
7449
7459
  /**
7450
7460
  import { errMsg } from '@camstack/types'
7451
7461
  * Extract a human-readable message from an unknown error value.
@@ -7456,6 +7466,757 @@ function errMsg(err) {
7456
7466
  if (typeof err === "string") return err;
7457
7467
  return String(err);
7458
7468
  }
7469
+ //#endregion
7470
+ //#region ../types/dist/composition-CPFIlFfw.mjs
7471
+ /**
7472
+ * Error types for the safe expression engine. Two distinct classes so callers
7473
+ * can tell a compile-time (grammar) failure from a runtime (evaluation)
7474
+ * failure — both are non-fatal to the host: read paths degrade to "skip link".
7475
+ */
7476
+ /** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
7477
+ * the failure is anchored to a character (author-facing inline feedback). */
7478
+ var ExpressionParseError = class extends Error {
7479
+ position;
7480
+ constructor(message, position) {
7481
+ super(message);
7482
+ this.name = "ExpressionParseError";
7483
+ this.position = position;
7484
+ }
7485
+ };
7486
+ /** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
7487
+ * result, unknown builtin, step-budget exceeded). */
7488
+ var ExpressionEvalError = class extends Error {
7489
+ constructor(message) {
7490
+ super(message);
7491
+ this.name = "ExpressionEvalError";
7492
+ }
7493
+ };
7494
+ function asFiniteNumber(value, name, index) {
7495
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
7496
+ return value;
7497
+ }
7498
+ function asString(value, name, index) {
7499
+ if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
7500
+ return value;
7501
+ }
7502
+ function finiteResult(value, name) {
7503
+ if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
7504
+ return value;
7505
+ }
7506
+ function allFiniteNumbers(args, name) {
7507
+ return args.map((a, idx) => asFiniteNumber(a, name, idx));
7508
+ }
7509
+ function asBoolean(value, name, index) {
7510
+ if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
7511
+ return value;
7512
+ }
7513
+ /** A plain decimal (optional sign, fraction, exponent) — never hex, never `Infinity`, never a unit suffix. */
7514
+ var DECIMAL = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
7515
+ /**
7516
+ * `number(x)`: a finite number, or a string that IS one, as a number; every
7517
+ * other value — `''`, `'unavailable'`, a boolean, null — is `null`
7518
+ * (unavailable, never 0: D393). The one bridge from a string-valued source (an
7519
+ * HA sensor with a unit and no device_class is an `enum-sensor`) to a number.
7520
+ */
7521
+ function toNumberOrNull(value) {
7522
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
7523
+ if (typeof value !== "string") return null;
7524
+ const trimmed = value.trim();
7525
+ if (!DECIMAL.test(trimmed)) return null;
7526
+ const parsed = Number(trimmed);
7527
+ return Number.isFinite(parsed) ? parsed : null;
7528
+ }
7529
+ var INF = Number.POSITIVE_INFINITY;
7530
+ var table = {
7531
+ min: {
7532
+ minArgs: 1,
7533
+ maxArgs: INF,
7534
+ apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
7535
+ },
7536
+ max: {
7537
+ minArgs: 1,
7538
+ maxArgs: INF,
7539
+ apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
7540
+ },
7541
+ abs: {
7542
+ minArgs: 1,
7543
+ maxArgs: 1,
7544
+ apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
7545
+ },
7546
+ floor: {
7547
+ minArgs: 1,
7548
+ maxArgs: 1,
7549
+ apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
7550
+ },
7551
+ ceil: {
7552
+ minArgs: 1,
7553
+ maxArgs: 1,
7554
+ apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
7555
+ },
7556
+ sqrt: {
7557
+ minArgs: 1,
7558
+ maxArgs: 1,
7559
+ apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
7560
+ },
7561
+ round: {
7562
+ minArgs: 1,
7563
+ maxArgs: 2,
7564
+ apply: (args) => {
7565
+ const x = asFiniteNumber(args[0], "round", 0);
7566
+ const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
7567
+ if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
7568
+ const factor = 10 ** digits;
7569
+ return finiteResult(Math.round(x * factor) / factor, "round");
7570
+ }
7571
+ },
7572
+ pow: {
7573
+ minArgs: 2,
7574
+ maxArgs: 2,
7575
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
7576
+ },
7577
+ clamp: {
7578
+ minArgs: 3,
7579
+ maxArgs: 3,
7580
+ apply: (args) => {
7581
+ const x = asFiniteNumber(args[0], "clamp", 0);
7582
+ const lo = asFiniteNumber(args[1], "clamp", 1);
7583
+ const hi = asFiniteNumber(args[2], "clamp", 2);
7584
+ if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
7585
+ return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
7586
+ }
7587
+ },
7588
+ avg: {
7589
+ minArgs: 1,
7590
+ maxArgs: INF,
7591
+ apply: (args) => {
7592
+ const nums = allFiniteNumbers(args, "avg");
7593
+ return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
7594
+ }
7595
+ },
7596
+ sum: {
7597
+ minArgs: 1,
7598
+ maxArgs: INF,
7599
+ apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
7600
+ },
7601
+ coalesce: {
7602
+ minArgs: 1,
7603
+ maxArgs: INF,
7604
+ apply: (args) => {
7605
+ for (const a of args) if (a !== null) return a;
7606
+ return null;
7607
+ }
7608
+ },
7609
+ age: {
7610
+ minArgs: 2,
7611
+ maxArgs: 2,
7612
+ apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
7613
+ },
7614
+ convert: {
7615
+ minArgs: 3,
7616
+ maxArgs: 3,
7617
+ apply: (args, hooks) => {
7618
+ const x = asFiniteNumber(args[0], "convert", 0);
7619
+ const from = asString(args[1], "convert", 1).trim();
7620
+ const to = asString(args[2], "convert", 2).trim();
7621
+ if (hooks.convert) {
7622
+ const out = hooks.convert(x, from, to);
7623
+ if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
7624
+ return finiteResult(out, "convert");
7625
+ }
7626
+ if (from === to) return x;
7627
+ throw new ExpressionEvalError("convert: unit conversion table not installed");
7628
+ }
7629
+ },
7630
+ any: {
7631
+ minArgs: 1,
7632
+ maxArgs: INF,
7633
+ apply: (args) => args.map((a, i) => asBoolean(a, "any", i)).some((b) => b)
7634
+ },
7635
+ all: {
7636
+ minArgs: 1,
7637
+ maxArgs: INF,
7638
+ apply: (args) => args.map((a, i) => asBoolean(a, "all", i)).every((b) => b)
7639
+ },
7640
+ within: {
7641
+ minArgs: 2,
7642
+ maxArgs: 2,
7643
+ apply: (args, hooks) => {
7644
+ const windowMs = asFiniteNumber(args[1], "within", 1);
7645
+ if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
7646
+ const at = args[0];
7647
+ if (at === null) return false;
7648
+ const ts = asFiniteNumber(at, "within", 0);
7649
+ const now = hooks.now;
7650
+ if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
7651
+ const inside = now - ts <= windowMs;
7652
+ if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
7653
+ return inside;
7654
+ }
7655
+ },
7656
+ number: {
7657
+ minArgs: 1,
7658
+ maxArgs: 1,
7659
+ apply: (args) => toNumberOrNull(args[0])
7660
+ },
7661
+ latest: {
7662
+ minArgs: 1,
7663
+ maxArgs: INF,
7664
+ apply: (args) => {
7665
+ const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
7666
+ return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
7667
+ }
7668
+ }
7669
+ };
7670
+ Object.freeze(Object.assign(Object.create(null), table));
7671
+ /** The set of valid builtin names — used by the parser to reject unknown
7672
+ * callees at parse time (immediate author feedback). */
7673
+ var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
7674
+ /**
7675
+ * Resource-bound constants for the safe expression engine.
7676
+ *
7677
+ * Every bound is defense-in-depth: the grammar is non-Turing-complete (no
7678
+ * loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
7679
+ * O(nodeCount) by construction. These caps merely put a hard ceiling on the
7680
+ * work a single author-supplied expression can request, so a hostile or
7681
+ * accidental pathological string can never spend unbounded CPU/memory.
7682
+ */
7683
+ /** Max source length (chars) — checked BEFORE tokenizing so a huge string is
7684
+ * rejected without allocation. */
7685
+ var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
7686
+ /** A legal binding / identifier name. */
7687
+ var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
7688
+ /** Binding names an author may NOT use: `now` is auto-injected; the literal
7689
+ * keywords lex as values, not identifiers, so binding to them is meaningless. */
7690
+ var RESERVED_BINDING_NAMES = new Set([
7691
+ "now",
7692
+ "true",
7693
+ "false",
7694
+ "null"
7695
+ ]);
7696
+ /**
7697
+ * Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
7698
+ * zero-dependency. The grammar is deliberately boring: decimal numbers,
7699
+ * single/double-quoted strings with a tiny escape set, identifiers, the three
7700
+ * value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
7701
+ * outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
7702
+ * is a parse error with a source position, so member access / assignment /
7703
+ * template literals are lexically impossible.
7704
+ */
7705
+ var KEYWORDS = new Set([
7706
+ "true",
7707
+ "false",
7708
+ "null"
7709
+ ]);
7710
+ function isDigit(ch) {
7711
+ return ch >= "0" && ch <= "9";
7712
+ }
7713
+ function isIdentStart(ch) {
7714
+ return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
7715
+ }
7716
+ function isIdentPart(ch) {
7717
+ return isIdentStart(ch) || isDigit(ch);
7718
+ }
7719
+ function isWhitespace(ch) {
7720
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
7721
+ }
7722
+ /** Tokenize `source` into a flat token list ending with a single `eof` token.
7723
+ * Throws `ExpressionParseError` on any illegal character or unterminated
7724
+ * string. */
7725
+ function tokenize(source) {
7726
+ if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
7727
+ const tokens = [];
7728
+ let i = 0;
7729
+ const n = source.length;
7730
+ while (i < n) {
7731
+ const ch = source[i];
7732
+ if (isWhitespace(ch)) {
7733
+ i += 1;
7734
+ continue;
7735
+ }
7736
+ if (isDigit(ch)) {
7737
+ const start = i;
7738
+ while (i < n && isDigit(source[i])) i += 1;
7739
+ if (i < n && source[i] === ".") {
7740
+ if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
7741
+ i += 1;
7742
+ while (i < n && isDigit(source[i])) i += 1;
7743
+ }
7744
+ const text = source.slice(start, i);
7745
+ const value = Number(text);
7746
+ if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
7747
+ tokens.push({
7748
+ type: "number",
7749
+ value,
7750
+ pos: start
7751
+ });
7752
+ continue;
7753
+ }
7754
+ if (ch === "'" || ch === "\"") {
7755
+ const quote = ch;
7756
+ const start = i;
7757
+ i += 1;
7758
+ let out = "";
7759
+ let closed = false;
7760
+ while (i < n) {
7761
+ const c = source[i];
7762
+ if (c === "\\") {
7763
+ const next = i + 1 < n ? source[i + 1] : "";
7764
+ if (next === "\\" || next === "'" || next === "\"") {
7765
+ out += next;
7766
+ i += 2;
7767
+ continue;
7768
+ }
7769
+ throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
7770
+ }
7771
+ if (c === quote) {
7772
+ closed = true;
7773
+ i += 1;
7774
+ break;
7775
+ }
7776
+ out += c;
7777
+ i += 1;
7778
+ }
7779
+ if (!closed) throw new ExpressionParseError("unterminated string literal", start);
7780
+ tokens.push({
7781
+ type: "string",
7782
+ value: out,
7783
+ pos: start
7784
+ });
7785
+ continue;
7786
+ }
7787
+ if (isIdentStart(ch)) {
7788
+ const start = i;
7789
+ while (i < n && isIdentPart(source[i])) i += 1;
7790
+ const text = source.slice(start, i);
7791
+ if (KEYWORDS.has(text)) tokens.push({
7792
+ type: "keyword",
7793
+ keyword: keywordOf(text),
7794
+ pos: start
7795
+ });
7796
+ else tokens.push({
7797
+ type: "identifier",
7798
+ name: text,
7799
+ pos: start
7800
+ });
7801
+ continue;
7802
+ }
7803
+ const two = i + 1 < n ? source.slice(i, i + 2) : "";
7804
+ if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
7805
+ tokens.push({
7806
+ type: "punct",
7807
+ punct: two,
7808
+ pos: i
7809
+ });
7810
+ i += 2;
7811
+ continue;
7812
+ }
7813
+ if (isSinglePunct(ch)) {
7814
+ tokens.push({
7815
+ type: "punct",
7816
+ punct: ch,
7817
+ pos: i
7818
+ });
7819
+ i += 1;
7820
+ continue;
7821
+ }
7822
+ throw new ExpressionParseError(`unexpected character '${ch}'`, i);
7823
+ }
7824
+ tokens.push({
7825
+ type: "eof",
7826
+ pos: n
7827
+ });
7828
+ return tokens;
7829
+ }
7830
+ function keywordOf(text) {
7831
+ if (text === "true") return "true";
7832
+ if (text === "false") return "false";
7833
+ return "null";
7834
+ }
7835
+ function isSinglePunct(ch) {
7836
+ return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
7837
+ }
7838
+ /**
7839
+ * Pratt (precedence-climbing) parser for the safe expression mini-language.
7840
+ *
7841
+ * Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
7842
+ * → relational → additive → multiplicative → unary `! -` → call / primary.
7843
+ * Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
7844
+ * string validated against the builtin table at parse time, so an unknown
7845
+ * function is rejected immediately (author feedback) and a persisted expression
7846
+ * that references a since-removed builtin degrades at read.
7847
+ *
7848
+ * A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
7849
+ * arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
7850
+ */
7851
+ /** Binary/logical operator precedence (higher binds tighter). */
7852
+ var BINARY_PRECEDENCE = {
7853
+ "||": 1,
7854
+ "&&": 2,
7855
+ "==": 3,
7856
+ "!=": 3,
7857
+ "<": 4,
7858
+ "<=": 4,
7859
+ ">": 4,
7860
+ ">=": 4,
7861
+ "+": 5,
7862
+ "-": 5,
7863
+ "*": 6,
7864
+ "/": 6,
7865
+ "%": 6
7866
+ };
7867
+ function isLogicalOp(op) {
7868
+ return op === "&&" || op === "||";
7869
+ }
7870
+ function isBinaryOp(op) {
7871
+ return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
7872
+ }
7873
+ var Parser = class {
7874
+ tokens;
7875
+ pos = 0;
7876
+ nodeCount = 0;
7877
+ identifiers = /* @__PURE__ */ new Set();
7878
+ callees = /* @__PURE__ */ new Set();
7879
+ constructor(tokens) {
7880
+ this.tokens = tokens;
7881
+ }
7882
+ parse() {
7883
+ const ast = this.parseTernary();
7884
+ const tok = this.peek();
7885
+ if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
7886
+ return {
7887
+ ast,
7888
+ identifiers: this.identifiers,
7889
+ callees: this.callees,
7890
+ nodeCount: this.nodeCount
7891
+ };
7892
+ }
7893
+ peek() {
7894
+ return this.tokens[this.pos];
7895
+ }
7896
+ next() {
7897
+ return this.tokens[this.pos++];
7898
+ }
7899
+ /** Consume a punctuator token, erroring if the next token isn't it. */
7900
+ expectPunct(punct) {
7901
+ const tok = this.peek();
7902
+ if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
7903
+ this.pos += 1;
7904
+ }
7905
+ matchPunct(punct) {
7906
+ const tok = this.peek();
7907
+ if (tok.type === "punct" && tok.punct === punct) {
7908
+ this.pos += 1;
7909
+ return true;
7910
+ }
7911
+ return false;
7912
+ }
7913
+ countNode() {
7914
+ this.nodeCount += 1;
7915
+ if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
7916
+ }
7917
+ parseTernary() {
7918
+ const test = this.parseBinary(1);
7919
+ if (this.matchPunct("?")) {
7920
+ const consequent = this.parseTernary();
7921
+ this.expectPunct(":");
7922
+ const alternate = this.parseTernary();
7923
+ this.countNode();
7924
+ return {
7925
+ kind: "conditional",
7926
+ test,
7927
+ consequent,
7928
+ alternate
7929
+ };
7930
+ }
7931
+ return test;
7932
+ }
7933
+ parseBinary(minPrec) {
7934
+ let left = this.parseUnary();
7935
+ for (;;) {
7936
+ const tok = this.peek();
7937
+ if (tok.type !== "punct") break;
7938
+ const prec = BINARY_PRECEDENCE[tok.punct];
7939
+ if (prec === void 0 || prec < minPrec) break;
7940
+ const op = tok.punct;
7941
+ this.pos += 1;
7942
+ const right = this.parseBinary(prec + 1);
7943
+ this.countNode();
7944
+ if (isLogicalOp(op)) left = {
7945
+ kind: "logical",
7946
+ op,
7947
+ left,
7948
+ right
7949
+ };
7950
+ else if (isBinaryOp(op)) left = {
7951
+ kind: "binary",
7952
+ op,
7953
+ left,
7954
+ right
7955
+ };
7956
+ else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
7957
+ }
7958
+ return left;
7959
+ }
7960
+ parseUnary() {
7961
+ const tok = this.peek();
7962
+ if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
7963
+ const op = tok.punct;
7964
+ this.pos += 1;
7965
+ const operand = this.parseUnary();
7966
+ this.countNode();
7967
+ return {
7968
+ kind: "unary",
7969
+ op,
7970
+ operand
7971
+ };
7972
+ }
7973
+ return this.parsePrimary();
7974
+ }
7975
+ parsePrimary() {
7976
+ const tok = this.next();
7977
+ switch (tok.type) {
7978
+ case "number":
7979
+ this.countNode();
7980
+ return {
7981
+ kind: "literal",
7982
+ value: tok.value
7983
+ };
7984
+ case "string":
7985
+ this.countNode();
7986
+ return {
7987
+ kind: "literal",
7988
+ value: tok.value
7989
+ };
7990
+ case "keyword":
7991
+ this.countNode();
7992
+ return {
7993
+ kind: "literal",
7994
+ value: tok.keyword === "null" ? null : tok.keyword === "true"
7995
+ };
7996
+ case "identifier": {
7997
+ const nextTok = this.peek();
7998
+ if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
7999
+ this.identifiers.add(tok.name);
8000
+ this.countNode();
8001
+ return {
8002
+ kind: "identifier",
8003
+ name: tok.name
8004
+ };
8005
+ }
8006
+ case "punct":
8007
+ if (tok.punct === "(") {
8008
+ const inner = this.parseTernary();
8009
+ this.expectPunct(")");
8010
+ return inner;
8011
+ }
8012
+ throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
8013
+ case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
8014
+ }
8015
+ }
8016
+ parseCall(callee, pos) {
8017
+ if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
8018
+ this.expectPunct("(");
8019
+ const args = [];
8020
+ if (!this.matchPunct(")")) for (;;) {
8021
+ args.push(this.parseTernary());
8022
+ if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
8023
+ if (this.matchPunct(",")) continue;
8024
+ this.expectPunct(")");
8025
+ break;
8026
+ }
8027
+ this.callees.add(callee);
8028
+ this.countNode();
8029
+ return {
8030
+ kind: "call",
8031
+ callee,
8032
+ args
8033
+ };
8034
+ }
8035
+ };
8036
+ /** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
8037
+ * `ExpressionParseError` on any lexical or grammatical failure. */
8038
+ function parseExpression(source) {
8039
+ return new Parser(tokenize(source)).parse();
8040
+ }
8041
+ /**
8042
+ * LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
8043
+ * by expr"). The cache stores BOTH successes and failures (negative caching),
8044
+ * so a corrupt persisted string costs exactly one tokenize+parse total — not
8045
+ * one per read on a hot resolve path.
8046
+ *
8047
+ * The cache is a module-level singleton: entries are pure, content-addressed
8048
+ * ASTs keyed by the raw source string, so sharing one instance across all
8049
+ * callers is safe and maximises hit rate.
8050
+ */
8051
+ var cache = /* @__PURE__ */ new Map();
8052
+ function getCached(source) {
8053
+ const hit = cache.get(source);
8054
+ if (hit !== void 0) {
8055
+ cache.delete(source);
8056
+ cache.set(source, hit);
8057
+ return hit;
8058
+ }
8059
+ let result;
8060
+ try {
8061
+ result = {
8062
+ ok: true,
8063
+ parsed: parseExpression(source)
8064
+ };
8065
+ } catch (err) {
8066
+ result = {
8067
+ ok: false,
8068
+ error: err instanceof ExpressionParseError ? err.message : String(err)
8069
+ };
8070
+ }
8071
+ cache.set(source, result);
8072
+ if (cache.size > 256) {
8073
+ const oldest = cache.keys().next().value;
8074
+ if (oldest !== void 0) cache.delete(oldest);
8075
+ }
8076
+ return result;
8077
+ }
8078
+ /** Compile `source`, returning a discriminated result instead of throwing.
8079
+ * Used by read paths that must degrade rather than raise. LRU/negative-cached. */
8080
+ function compileExpressionSafe(source) {
8081
+ return getCached(source);
8082
+ }
8083
+ Object.freeze({});
8084
+ /**
8085
+ * Author-time validation. Returns `null` when the source is valid, else a
8086
+ * human-readable error message. Checks: the expression compiles; binding count
8087
+ * is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
8088
+ * is not reserved (`now`/keywords) and does not shadow a builtin; and every
8089
+ * FREE identifier of the AST is covered by a binding or the injected `now`.
8090
+ */
8091
+ function validateExpressionSource(src) {
8092
+ const names = Object.keys(src.bindings);
8093
+ if (names.length > 32) return `too many bindings (${names.length} > 32)`;
8094
+ for (const name of names) {
8095
+ if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
8096
+ if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
8097
+ if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
8098
+ }
8099
+ const compiled = compileExpressionSafe(src.expr);
8100
+ if (!compiled.ok) return compiled.error;
8101
+ const bound = new Set(names);
8102
+ for (const id of compiled.parsed.identifiers) {
8103
+ if (id === "now") continue;
8104
+ if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
8105
+ }
8106
+ return null;
8107
+ }
8108
+ /** A composed device's stableId is this prefix plus the block id. */
8109
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
8110
+ /** Longest snippet a `code` source may carry (slice 4). */
8111
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
8112
+ var CompositionSourceRefSchema = object({
8113
+ /** The owning addon; `stableId` is unique only within it. */
8114
+ addonId: string().min(1),
8115
+ stableId: string().min(1)
8116
+ });
8117
+ /** One field of one capability of one source device. */
8118
+ var CompositionFieldReadSchema = object({
8119
+ source: CompositionSourceRefSchema,
8120
+ cap: string().min(1),
8121
+ /** Dotted path into the source cap's runtime-state slice. */
8122
+ fieldPath: string().min(1)
8123
+ });
8124
+ /** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
8125
+ var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: literal("from") });
8126
+ var CompositionBindingSchema = discriminatedUnion("kind", [CompositionFromSourceSchema, object({
8127
+ kind: literal("literal"),
8128
+ value: union([
8129
+ string(),
8130
+ number(),
8131
+ boolean(),
8132
+ _null()
8133
+ ])
8134
+ })]);
8135
+ /**
8136
+ * `expression`: a formula over named bindings, in the salvaged expression
8137
+ * engine. Validated at parse by the SAME function every other consumer runs,
8138
+ * so the editor and the store cannot disagree.
8139
+ */
8140
+ var CompositionExpressionSourceSchema = object({
8141
+ kind: literal("expression"),
8142
+ expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
8143
+ bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
8144
+ }).superRefine((src, ctx) => {
8145
+ const err = validateExpressionSource(src);
8146
+ if (err !== null) ctx.addIssue({
8147
+ code: "custom",
8148
+ message: err,
8149
+ path: ["expr"]
8150
+ });
8151
+ });
8152
+ /** `code`: RESERVED for slice 4 (own runner, one-way eject). */
8153
+ var CompositionCodeSourceSchema = object({
8154
+ kind: literal("code"),
8155
+ code: string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
8156
+ });
8157
+ var CompositionFieldSourceSchema = discriminatedUnion("kind", [
8158
+ CompositionFromSourceSchema,
8159
+ CompositionExpressionSourceSchema,
8160
+ CompositionCodeSourceSchema
8161
+ ]);
8162
+ var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
8163
+ kind: literal("forward"),
8164
+ source: CompositionSourceRefSchema,
8165
+ cap: string().min(1),
8166
+ method: string().min(1)
8167
+ }), CompositionCodeSourceSchema]);
8168
+ /** An item key inside an item-array cap (`consumables.items[<key>]`). */
8169
+ var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
8170
+ /**
8171
+ * One item of an item-array cap (D663), addressed through the cap's
8172
+ * `status.itemArray` descriptor. `fields` are paths INSIDE the item, nested
8173
+ * allowed (`remaining.value`); the key and label come from the entry itself.
8174
+ */
8175
+ var CompositionItemEntrySchema = object({
8176
+ label: string().min(1).max(80),
8177
+ fields: record(string().min(1), CompositionFieldSourceSchema)
8178
+ });
8179
+ var CompositionFeatureSchema = discriminatedUnion("kind", [object({
8180
+ kind: literal("fields"),
8181
+ cap: string().min(1),
8182
+ fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
8183
+ /**
8184
+ * Items of the cap's item array, keyed by item key (D663). Naming `items`
8185
+ * owns the WHOLE array field: the composed value holds exactly these items.
8186
+ */
8187
+ items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
8188
+ /** RESERVED for slice 3; refused by the validator when non-empty. */
8189
+ commands: record(string().min(1), CompositionCommandTargetSchema).optional()
8190
+ }), object({
8191
+ kind: literal("passthrough"),
8192
+ cap: string().min(1),
8193
+ source: CompositionSourceRefSchema
8194
+ })]);
8195
+ var CompositionSchema = object({
8196
+ target: discriminatedUnion("kind", [object({
8197
+ kind: literal("new"),
8198
+ type: _enum(DeviceType),
8199
+ role: _enum(DeviceRole).optional()
8200
+ }), object({
8201
+ kind: literal("existing"),
8202
+ device: CompositionSourceRefSchema
8203
+ })]),
8204
+ features: array(CompositionFeatureSchema).min(1).max(16)
8205
+ }).superRefine((composition, ctx) => {
8206
+ const seen = /* @__PURE__ */ new Set();
8207
+ composition.features.forEach((feature, index) => {
8208
+ if (seen.has(feature.cap)) ctx.addIssue({
8209
+ code: "custom",
8210
+ message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
8211
+ path: [
8212
+ "features",
8213
+ index,
8214
+ "cap"
8215
+ ]
8216
+ });
8217
+ seen.add(feature.cap);
8218
+ });
8219
+ });
7459
8220
  new Set([
7460
8221
  "track",
7461
8222
  "summary",
@@ -9507,1023 +10268,480 @@ var DecoderSessionConfigSchema = object({
9507
10268
  */
9508
10269
  deviceId: number().int().nonnegative().optional(),
9509
10270
  /**
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).
10271
+ * Free-form tag for log scoping. Stream-broker uses
10272
+ * `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
10273
+ * on every line so `grep tag=broker:5/high` filters one camera
10274
+ * profile cleanly.
9875
10275
  */
9876
- provider: ModelProviderIdSchema.optional(),
10276
+ tag: string().optional(),
9877
10277
  /**
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).
10278
+ * Where the session delivers decoded frames (Phase 5 / D9):
10279
+ *
10280
+ * - `'callback'` (default) — the legacy pixel path: decoded frames are
10281
+ * buffered as `DecodedFrame`s and drained via `pullFrames`.
10282
+ * - `'shm'` — the shared-memory frame plane: decoded frames are written
10283
+ * into an OS shared-memory ring and drained as zero-pixel
10284
+ * `FrameHandle`s via `pullHandles`. A session is one mode or the
10285
+ * other — `pullFrames` returns nothing for an `'shm'` session and
10286
+ * `pullHandles` returns nothing for a `'callback'` session.
9881
10287
  */
9882
- classMap: DetectionCatalogClassMapSchema.optional(),
10288
+ frameSink: _enum(["callback", "shm"]).default("callback"),
9883
10289
  /**
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).
10290
+ * Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
10291
+ * a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
10292
+ * real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
10293
+ * stream-broker's `streamingDebug` gate — off by default so production logs
10294
+ * stay quiet and the emit path pays zero per-frame cost when disabled.
9888
10295
  */
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()
10296
+ debug: boolean().optional()
9931
10297
  });
9932
10298
  /**
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.
10299
+ * Per-component log CHANNELS — the gate a hot path consults, and the registry
10300
+ * an addon declares its channels in.
10301
+ *
10302
+ * ## Two axes, deliberately separated
10303
+ *
10304
+ * - **DECLARATION** — which channels exist. Only the addon knows:
10305
+ * `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
10306
+ * baichuan/handshake. A hand-wired central list rots at the first addition,
10307
+ * and rots silently. So a channel is declared where it is consulted, and the
10308
+ * `log-channels` capability enumerates the declarations.
10309
+ * - **VALUE** — at which level, for which scope, until when. That stays ONE
10310
+ * thing: the logging settings document on the `system` cap. Two authorities
10311
+ * over the values is the exact defect
10312
+ * `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
10313
+ * remove; re-introducing it from the cure side would be grotesque.
10314
+ *
10315
+ * Nothing in this file reads a clock, an env var or a store. The registry is
10316
+ * a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
10317
+ * the hot path with a value somebody actually read, and by
10318
+ * {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
10319
+ * never reaches here, so it can neither disarm an armed channel nor arm a
10320
+ * disarmed one (D49).
10321
+ *
10322
+ * ## The canonical call shape
10323
+ *
10324
+ * ```ts
10325
+ * if (CH_RTP.on && CH_RTP.wants(deviceId)) {
10326
+ * CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
10327
+ * }
10328
+ * ```
10329
+ *
10330
+ * `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
10331
+ * read. Disarmed, a call site costs one load and one branch, and the `extras`
10332
+ * object literal is never constructed because it lives inside the branch. It
10333
+ * is the same shape already proven in production at `stream-broker.ts:1650`,
10334
+ * and the same discipline `LoggingGate.allowsDestination` uses for the
10335
+ * destination floor (measured at 1.93 ns/call when off).
9960
10336
  *
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.
10337
+ * ## Why a channel emits at `info`
9966
10338
  *
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.
10339
+ * `loki-logging.addon.ts` pins the destination default at `info` and
10340
+ * `loki-destination.ts` drops everything below it, so a line emitted at
10341
+ * `debug` never reaches Loki and the hub's in-memory ring only holds ~35
10342
+ * minutes. A diagnostic that cannot be read an hour later is worse than no
10343
+ * diagnostic, because it looks done. {@link LogChannelGate.log} therefore
10344
+ * emits at the channel's declared level, whose schema floor is `info`.
9971
10345
  */
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
10346
  /**
10094
- * Resource-bound constants for the safe expression engine.
10347
+ * The level a channel writes at once armed.
10095
10348
  *
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.
10349
+ * `debug` is absent ON PURPOSE and not by omission: below `info` the line does
10350
+ * not leave the process for Loki, and the whole point of arming a channel is
10351
+ * to read it later.
10101
10352
  */
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"
10353
+ var LogChannelLevelSchema = _enum([
10354
+ "info",
10355
+ "warn",
10356
+ "error"
10114
10357
  ]);
10115
10358
  /**
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.
10359
+ * What an addon declares about one channel. No value, no state — a
10360
+ * declaration is inert.
10123
10361
  */
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
- }
10362
+ var LogChannelDescriptorSchema = object({
10363
+ /**
10364
+ * Dotted `area.thing`, unique across the workspace. `area` is conventionally
10365
+ * the addon's short name so an operator reading a channel list can tell who
10366
+ * owns it without a second lookup.
10367
+ */
10368
+ name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
10369
+ /** One sentence: what the operator will SEE after arming it. */
10370
+ description: string().min(1),
10371
+ /** The level its lines are emitted at. Never below `info`. */
10372
+ defaultLevel: LogChannelLevelSchema,
10373
+ /**
10374
+ * Whether this channel can be narrowed to a camera.
10375
+ *
10376
+ * `true` is a PROMISE with two halves, and both must hold: the gate is
10377
+ * consulted with the numeric device id, AND every line the channel admits
10378
+ * carries `tags: { deviceId }` with that same numeric id. The second half is
10379
+ * what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
10380
+ * keeps `deviceId` out of the stream labels for cardinality, so the tag in
10381
+ * the body is the only way to filter.
10382
+ *
10383
+ * A channel whose lines carry the device only in `meta` (or not at all) is
10384
+ * declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
10385
+ * the operator narrows to one camera, sees nothing, and concludes the code
10386
+ * path was never taken.
10387
+ */
10388
+ perDevice: boolean()
10389
+ });
10257
10390
  /**
10258
- * Pratt (precedence-climbing) parser for the safe expression mini-language.
10391
+ * An armed window over one channel, as the document hands it to a mirror.
10259
10392
  *
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.
10393
+ * A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
10394
+ * expires by itself, which is the one failure a boolean cannot avoid.
10395
+ */
10396
+ var LogChannelWindowSchema = object({
10397
+ channel: string().min(1),
10398
+ /** Epoch ms the window closes at. */
10399
+ armedUntilMs: number(),
10400
+ /** `null` = every camera. A non-empty list narrows to those numeric ids. */
10401
+ deviceIds: array(number().int()).readonly().nullable()
10402
+ });
10403
+ var ClipModelMetaSchema = object({
10404
+ /** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
10405
+ textModelId: string().min(1),
10406
+ /** Output dimension of BOTH towers. A vector of any other length is refused. */
10407
+ embeddingDim: number().int().positive(),
10408
+ /**
10409
+ * The `vector-store` index this model's vectors live in. ONE feature space
10410
+ * per index: two models share an index only when their vectors are ranked
10411
+ * against each other, which for CLIP means never across families. MobileCLIP
10412
+ * S1 and S2 share `object-clip` for history (separated by the row's
10413
+ * `modelId`); any other model gets its own.
10414
+ */
10415
+ vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
10416
+ /**
10417
+ * The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
10418
+ * must be unique per tokenizer: every model's siblings land in ONE flat
10419
+ * models directory, so two different tokenizers under the same name would
10420
+ * silently overwrite each other (whichever model downloaded last wins, and
10421
+ * the other one tokenises with the wrong vocabulary).
10422
+ */
10423
+ tokenizerFile: string().min(1),
10424
+ /** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
10425
+ contextLength: number().int().positive(),
10426
+ /** Id the tokenizer pads with. */
10427
+ padId: number().int().nonnegative(),
10428
+ /**
10429
+ * The search cosine floor at `strictness: 'loose'` — calibrated per model,
10430
+ * because cosine bands are a property of the model: 0.2 keeps 96% of matched
10431
+ * COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
10432
+ */
10433
+ searchMinScore: number().min(0).max(1)
10434
+ });
10435
+ var ClipSearchStrictnessSchema = _enum([
10436
+ "loose",
10437
+ "balanced",
10438
+ "strict"
10439
+ ]);
10440
+ var MODEL_FORMATS = [
10441
+ "onnx",
10442
+ "coreml",
10443
+ "openvino",
10444
+ "tflite",
10445
+ "pt",
10446
+ "gguf"
10447
+ ];
10448
+ /**
10449
+ * Multi-file format payload.
10266
10450
  *
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`.
10451
+ * - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
10452
+ * relative to the directory root — the downloader fetches each from
10453
+ * `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
10454
+ * HuggingFace API (slower).
10455
+ * - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
10456
+ * files fetched from the SAME remote directory as `url` and stored flat
10457
+ * alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
10458
+ * weights next to `camstack-yolov9t.xml`.
10269
10459
  */
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
- }
10460
+ var ModelFormatEntrySchema = object({
10461
+ url: string(),
10462
+ sizeMB: number(),
10463
+ /** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
10464
+ isDirectory: boolean().optional(),
10465
+ /** Multi-file payload (directory members or sibling files). */
10466
+ files: array(string()).readonly().optional(),
10467
+ /** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
10468
+ runtimes: array(_enum(["python"])).readonly().optional()
10469
+ });
10460
10470
  /**
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.
10471
+ * Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
10472
+ * The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
10469
10473
  */
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({});
10474
+ var ModelExtraFileSchema = object({
10475
+ url: string(),
10476
+ filename: string(),
10477
+ sizeMB: number()
10478
+ });
10503
10479
  /**
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`.
10480
+ * Per-format payload map. Modelled as an explicit object (one optional key
10481
+ * per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
10482
+ * record requires every key, but a catalog entry only ships a subset of
10483
+ * formats.
10509
10484
  */
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
- }
10485
+ var ModelFormatsSchema = object({
10486
+ onnx: ModelFormatEntrySchema.optional(),
10487
+ coreml: ModelFormatEntrySchema.optional(),
10488
+ openvino: ModelFormatEntrySchema.optional(),
10489
+ tflite: ModelFormatEntrySchema.optional(),
10490
+ pt: ModelFormatEntrySchema.optional(),
10491
+ gguf: ModelFormatEntrySchema.optional()
10492
+ });
10493
+ /**
10494
+ * Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
10495
+ * the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
10496
+ * grouped Family→Tier→Variant picker renders identically in the config UI and
10497
+ * in the pipeline/device steppers. The flat `id` stays the source of truth for
10498
+ * resolution/download/persistence; this is a presentation overlay resolved back
10499
+ * to an `id`.
10500
+ */
10501
+ var ModelVariantGroupSchema = object({
10502
+ /** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
10503
+ family: string(),
10504
+ /** Size within the family, e.g. `n` | `s` | `m` | `l`. */
10505
+ tier: string(),
10506
+ /** Quantization axis. Omit ⇒ the fp32 base build. */
10507
+ precision: _enum(["fp32", "int8"]).optional(),
10508
+ /**
10509
+ * Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
10510
+ * latency-optimized export (e.g. ReLU-activation variant) — the slot the
10511
+ * future performance variants plug into.
10512
+ */
10513
+ optimization: _enum(["standard", "fast"]).optional(),
10514
+ /**
10515
+ * Input-resolution axis (square input side, px). Omit ⇒ the family's native
10516
+ * resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
10517
+ * cheap latency lever — especially on Apple ANE and the Intel N100 — at a
10518
+ * small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
10519
+ * the group so the selector can offer it as a variant axis.
10520
+ */
10521
+ resolution: number().int().positive().optional()
10522
+ });
10523
+ var ModelProviderIdSchema = _enum([
10524
+ "camstack",
10525
+ "frigate",
10526
+ "scrypted",
10527
+ "custom"
10528
+ ]);
10529
+ /**
10530
+ * The licence of a model, as a record rather than a bare SPDX string: a
10531
+ * permissive CODE licence routinely sits on top of restrictive WEIGHTS or
10532
+ * DATA, and the obligations (attribution, modification notices, source offers)
10533
+ * are per upstream. A persisted custom-model row written when this field was a
10534
+ * string still parses — the string is lifted into `{ weights }`.
10535
+ *
10536
+ * Every licence surface (the admin "Models and licenses" page,
10537
+ * `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
10538
+ * these records by `scripts/gen-model-licenses.ts` (D661).
10539
+ */
10540
+ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
10541
+ /** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
10542
+ weights: string().min(1),
10543
+ /** SPDX id of the upstream code that defines / trained the network. */
10544
+ code: string().min(1).optional(),
10545
+ /** Training data whose terms add obligations (attribution, non-commercial). */
10546
+ data: object({
10547
+ name: string().min(1),
10548
+ terms: string().min(1),
10549
+ url: string().url().optional()
10550
+ }).optional(),
10551
+ /** The project the weights come from. Required on built-in entries (guard). */
10552
+ upstream: object({
10553
+ name: string().min(1),
10554
+ url: string().url()
10555
+ }).optional(),
10556
+ /** Where the licence text governing the weights is published. */
10557
+ url: string().url().optional(),
10558
+ /** Attribution the licence REQUIRES, verbatim. */
10559
+ attribution: string().min(1).optional(),
10560
+ /** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
10561
+ modifications: string().min(1).optional(),
10562
+ /** Who serves the file a node downloads. */
10563
+ hosting: _enum([
10564
+ "camstack-hf",
10565
+ "third-party",
10566
+ "built-in"
10567
+ ]).optional(),
10568
+ /** True when the weights or their data forbid commercial use. */
10569
+ nonCommercial: boolean().optional()
10570
+ }));
10571
+ var ModelCatalogEntrySchema = object({
10572
+ id: string(),
10573
+ name: string(),
10574
+ description: string(),
10575
+ formats: ModelFormatsSchema,
10576
+ inputSize: object({
10577
+ width: number(),
10578
+ height: number()
10579
+ }),
10580
+ /**
10581
+ * Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
10582
+ * every detector / classifier / embedder. Set to 1 for a grayscale CTC text
10583
+ * recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
10584
+ * feeds a single-channel, EasyOCR-normalized tensor instead of the default
10585
+ * 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
10586
+ * Python inference pool.
10587
+ */
10588
+ inputChannels: number().int().positive().optional(),
10589
+ labels: array(LabelDefinitionSchema).readonly(),
10590
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10591
+ /**
10592
+ * `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
10593
+ * (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
10594
+ * plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
10595
+ * `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
10596
+ */
10597
+ inputNormalization: _enum([
10598
+ "zero-one",
10599
+ "imagenet",
10600
+ "none",
10601
+ "scrfd"
10602
+ ]).optional(),
10603
+ /**
10604
+ * The model already applies softmax IN-GRAPH — its raw output is a
10605
+ * probability distribution, not logits. When set, the `softmax`
10606
+ * postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
10607
+ * probability vector collapses it toward uniform (top-1 score craters far
10608
+ * below its true value, making every confidence gate meaningless). Absent ⇒
10609
+ * the output is raw logits and the postprocessor applies softmax (the normal
10610
+ * case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
10611
+ * TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
10612
+ */
10613
+ outputProbabilities: boolean().optional(),
10614
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10615
+ /**
10616
+ * Per-MODEL postprocessor override. Absent ⇒ the step's own
10617
+ * `StepDefinition.postprocessor` applies (the normal case — every model in a
10618
+ * step shares its decode). Set it when a step hosts models with DIFFERENT raw
10619
+ * output layouts under one slot: e.g. object-detection is `'yolo'` by default,
10620
+ * but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
10621
+ * 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
10622
+ * by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
10623
+ */
10624
+ postprocessor: custom().optional(),
10625
+ /**
10626
+ * Per-MODEL default confidence floor. Absent ⇒ the step's
10627
+ * `StepDefinition.defaultConfidence` applies. A score is a property of one
10628
+ * model's output scale, not of the step slot it sits in: YuNet's
10629
+ * `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
10630
+ * face gallery with wheels and hands (D662, amends D645). An operator's
10631
+ * explicit value still wins over it. Resolved in ONE place —
10632
+ * `addon-pipeline/.../registry/effective-confidence.ts`.
10633
+ */
10634
+ defaultConfidence: number().min(0).max(1).optional(),
10635
+ /**
10636
+ * When true, the executor produces a landmark-aligned crop (similarity warp
10637
+ * onto the canonical template) before this step runs, instead of a plain
10638
+ * axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
10639
+ * their embeddings are only discriminative on an aligned input. The face
10640
+ * detector that produced the parent detail must emit 5 landmarks.
10641
+ */
10642
+ faceAlignment: boolean().optional(),
10643
+ /**
10644
+ * Auxiliary files required at runtime (labels JSON, charset dict, etc.).
10645
+ * Downloaded into the same modelsDir alongside the model file.
10646
+ */
10647
+ extraFiles: array(ModelExtraFileSchema).readonly().optional(),
10648
+ /**
10649
+ * LEGACY entry — retained in the catalog so a persisted operator selection
10650
+ * still RESOLVES (and can be re-activated), but hidden from the selectable
10651
+ * model list and excluded from the auto format-default pick. Set on the
10652
+ * superseded / consolidated models (older lineages, redundant fp16 IRs) so
10653
+ * the active lineup stays the coherent curated ladder without deleting a
10654
+ * model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
10655
+ * an explicit legacy id that has a build for the node's format.
10656
+ */
10657
+ legacy: boolean().optional(),
10658
+ /**
10659
+ * Measured quality/latency metadata — populated from the benchmark addon on
10660
+ * the real node classes. Absent = not yet measured (most entries today; the
10661
+ * catalog historically carried only `sizeMB`, a poor cross-architecture
10662
+ * speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
10663
+ */
10664
+ metrics: object({
10665
+ map50: number().optional(),
10666
+ p95LatencyMs: record(string(), number()).optional()
10667
+ }).optional(),
10668
+ /**
10669
+ * The model's licence record — see {@link ModelLicenseSchema}. Optional in
10670
+ * the schema (a custom model's author may state none); REQUIRED and complete
10671
+ * on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
10672
+ */
10673
+ license: ModelLicenseSchema.optional(),
10674
+ /**
10675
+ * Variant-selector grouping. The UI groups models by `family` + `tier` and
10676
+ * offers `precision` / `optimization` as variant axes WITHIN a tier — so all
10677
+ * of a family's sizes and quantizations collapse into one grouped picker
10678
+ * instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
10679
+ * (legacy / custom models) — never shown in the grouped selector. The flat
10680
+ * `id` stays the source of truth for resolution/download/persistence; grouping
10681
+ * is a presentation overlay resolved back to an `id`.
10682
+ */
10683
+ group: ModelVariantGroupSchema.optional(),
10684
+ /**
10685
+ * Catalog source for the pipeline stepper's provider-first picker. Absent on
10686
+ * built-in CamStack entries (treated as `camstack`) and on registry rows
10687
+ * persisted before this field existed (`inferModelProvider` fills those).
10688
+ */
10689
+ provider: ModelProviderIdSchema.optional(),
10690
+ /**
10691
+ * Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
10692
+ * applies (Frigate / COCO public catalog). Set on a custom model whose raw
10693
+ * labels already ARE the CamStack macros (Scrypted identity map).
10694
+ */
10695
+ classMap: DetectionCatalogClassMapSchema.optional(),
10696
+ /**
10697
+ * The model's CLIP feature-space contract (text encoder, dimension, vector
10698
+ * index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
10699
+ * CLIP image encoder; its absence is what "not a CLIP model" means to the
10700
+ * embedding encoder and the semantic-search store (D649).
10701
+ */
10702
+ clip: ClipModelMetaSchema.optional()
10703
+ });
10704
+ var ConvertTargetSchema = discriminatedUnion("format", [object({
10705
+ format: literal("openvino"),
10706
+ precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
10707
+ }), object({ format: literal("coreml") })]);
10708
+ var ModelConvertMetadataSchema = object({
10709
+ id: string().regex(/^[a-zA-Z0-9._-]+$/),
10710
+ name: string(),
10711
+ labels: array(LabelDefinitionSchema).readonly(),
10712
+ inputSize: object({
10713
+ width: number(),
10714
+ height: number()
10715
+ }),
10716
+ inputLayout: _enum(["nchw", "nhwc"]).optional(),
10717
+ inputNormalization: _enum([
10718
+ "zero-one",
10719
+ "imagenet",
10720
+ "none",
10721
+ "scrfd"
10722
+ ]).optional(),
10723
+ preprocessMode: _enum(["letterbox", "resize"]).optional(),
10724
+ outputFormat: _enum([
10725
+ "yolo",
10726
+ "ssd",
10727
+ "embedding",
10728
+ "classification",
10729
+ "ocr",
10730
+ "segmentation"
10731
+ ]),
10732
+ faceAlignment: boolean().optional(),
10733
+ classMap: DetectionCatalogClassMapSchema.optional()
10734
+ });
10735
+ var ConvertResultSchema = object({
10736
+ entry: ModelCatalogEntrySchema,
10737
+ artifacts: array(object({
10738
+ format: _enum(MODEL_FORMATS),
10739
+ precision: _enum(["fp16", "int8"]).optional(),
10740
+ sizeMB: number(),
10741
+ validated: boolean(),
10742
+ files: array(string()).readonly()
10743
+ })).readonly()
10744
+ });
10527
10745
  var ExpressionBindingSourceSchema = union([
10528
10746
  object({
10529
10747
  kind: literal("field").optional(),
@@ -10698,7 +10916,7 @@ var AutomationActionSchema = discriminatedUnion("kind", [
10698
10916
  code: string().min(1).max(2e4)
10699
10917
  })
10700
10918
  ]);
10701
- object({
10919
+ var AutomationRecipeSchema = object({
10702
10920
  triggers: array(AutomationTriggerSchema),
10703
10921
  conditions: AutomationConditionSchema.optional(),
10704
10922
  actions: array(AutomationActionSchema)
@@ -11185,12 +11403,32 @@ var AudioClassificationLabelSchema = object({
11185
11403
  originalClass: string().optional(),
11186
11404
  score: number()
11187
11405
  });
11406
+ /**
11407
+ * Did the classifier RUN on this window? (D660)
11408
+ *
11409
+ * `classified` with no labels means the model heard nothing above the
11410
+ * camera's floor. `not-classified` means nobody listened — and until D660 the
11411
+ * two were the same shape (`classification` absent), so a window the global
11412
+ * busy gate threw away read downstream exactly like a quiet one (D393).
11413
+ */
11414
+ var AudioClassifyOutcomeSchema = discriminatedUnion("state", [object({ state: literal("classified") }), object({
11415
+ state: literal("not-classified"),
11416
+ reason: _enum([
11417
+ "superseded",
11418
+ "error",
11419
+ "timeout",
11420
+ "disposed"
11421
+ ])
11422
+ })]);
11188
11423
  var AudioAnalysisResultSchema = object({
11189
11424
  level: AudioLevelSchema,
11425
+ /** Labels at or above the camera's floor. Absent = none — read `classifyOutcome` to know whether the model ran at all. */
11190
11426
  classification: object({
11191
11427
  labels: array(AudioClassificationLabelSchema).readonly(),
11192
11428
  inferenceMs: number()
11193
11429
  }).optional(),
11430
+ /** Whether the classifier ran on this window (D660). */
11431
+ classifyOutcome: AudioClassifyOutcomeSchema,
11194
11432
  timestamp: number()
11195
11433
  });
11196
11434
  var AudioAnalysisSettingsSchema = object({
@@ -12899,15 +13137,157 @@ var cameraStreamsCapability = {
12899
13137
  */
12900
13138
  durability: "session"
12901
13139
  };
13140
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
13141
+ var CompositionProblemSchema = object({
13142
+ code: _enum([
13143
+ "reserved-variant",
13144
+ "unknown-capability",
13145
+ "not-composable",
13146
+ "wrong-device-type",
13147
+ "has-commands",
13148
+ "unknown-field",
13149
+ "nested-field",
13150
+ "missing-field",
13151
+ "kind-mismatch",
13152
+ "may-be-null",
13153
+ "source-missing",
13154
+ "source-unreadable",
13155
+ "source-capability-missing",
13156
+ "source-field-unknown",
13157
+ "expression-invalid",
13158
+ "cycle",
13159
+ "duplicate-target",
13160
+ "not-item-array",
13161
+ "item-field-reserved",
13162
+ "target-missing",
13163
+ "target-unreadable",
13164
+ "not-replaceable"
13165
+ ]),
13166
+ path: string(),
13167
+ message: string()
13168
+ });
13169
+ var CompositionFieldKindSchema = _enum([
13170
+ "string",
13171
+ "number",
13172
+ "boolean",
13173
+ "enum"
13174
+ ]);
13175
+ /**
13176
+ * How a field says "unavailable" (D393, D663): `null` when its schema allows it;
13177
+ * `unknown` — the enum's own word — for an ADDED field that has one; `release`
13178
+ * for a REPLACED field of an existing device, whose claim is released so the
13179
+ * native provider's reading shows again; else the (composed) device goes offline.
13180
+ */
13181
+ var CompositionUnavailableModeSchema = _enum([
13182
+ "null",
13183
+ "offline",
13184
+ "unknown",
13185
+ "release"
13186
+ ]);
13187
+ /**
13188
+ * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
13189
+ * field of its cap changes value · `constant` reads nothing, computed once.
13190
+ */
13191
+ var CompositionFieldRoleSchema = _enum([
13192
+ "derived",
13193
+ "clock",
13194
+ "constant"
13195
+ ]);
13196
+ /** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
13197
+ var CompositionFieldItemSchema = object({
13198
+ arrayPath: string(),
13199
+ key: string(),
13200
+ label: string()
13201
+ });
13202
+ var CompositionFieldPlanSchema = object({
13203
+ cap: string(),
13204
+ /** Top-level field, or — when `item` is set — the dotted path inside that item. */
13205
+ path: string(),
13206
+ item: CompositionFieldItemSchema.optional(),
13207
+ /** Index into `composition.features` this field's capability came from —
13208
+ * lets a source/cycle problem name its path as `features[N].fields.<path>`
13209
+ * (the same convention `planComposition`'s own problems already use). */
13210
+ featureIndex: number().int().nonnegative(),
13211
+ kind: CompositionFieldKindSchema,
13212
+ nullable: boolean(),
13213
+ required: boolean(),
13214
+ unavailable: CompositionUnavailableModeSchema,
13215
+ role: CompositionFieldRoleSchema,
13216
+ source: CompositionFieldSourceSchema,
13217
+ reads: array(CompositionFieldReadSchema)
13218
+ });
13219
+ /**
13220
+ * What a feature does to its target (D663): `new` builds a capability on a new
13221
+ * device; on an existing device it `add`s a capability the device lacks, or
13222
+ * `replace`s named fields of one it has natively.
13223
+ */
13224
+ var CompositionFeatureModeSchema = _enum([
13225
+ "new",
13226
+ "add",
13227
+ "replace"
13228
+ ]);
13229
+ var CompositionFeaturePlanSchema = object({
13230
+ featureIndex: number().int().nonnegative(),
13231
+ cap: string(),
13232
+ mode: CompositionFeatureModeSchema
13233
+ });
13234
+ var CompositionValidationSchema = object({
13235
+ ok: boolean(),
13236
+ problems: array(CompositionProblemSchema),
13237
+ fields: array(CompositionFieldPlanSchema),
13238
+ features: array(CompositionFeaturePlanSchema)
13239
+ });
13240
+ /**
13241
+ * Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
13242
+ * running, but a replaced field is released to the native provider while its
13243
+ * source is unavailable (ruling S1b, D663) — the reason names each such field.
13244
+ */
13245
+ var CompositionRunStatusSchema = _enum([
13246
+ "stopped",
13247
+ "starting",
13248
+ "running",
13249
+ "degraded",
13250
+ "failed"
13251
+ ]);
13252
+ var CompositionFieldStateKindSchema = _enum([
13253
+ "fresh",
13254
+ "stale",
13255
+ "unavailable"
13256
+ ]);
13257
+ var CompositionFieldStateSchema = object({
13258
+ cap: string(),
13259
+ path: string(),
13260
+ /** Same as the plan's: two items' `status` are told apart by it. */
13261
+ item: CompositionFieldItemSchema.optional(),
13262
+ state: CompositionFieldStateKindSchema,
13263
+ /** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
13264
+ value: union([
13265
+ string(),
13266
+ number(),
13267
+ boolean(),
13268
+ _null()
13269
+ ]),
13270
+ reason: string().optional()
13271
+ });
13272
+ var CompositionBlockStateSchema = object({
13273
+ blockId: string(),
13274
+ deviceId: number().nullable(),
13275
+ status: CompositionRunStatusSchema,
13276
+ reason: string().optional(),
13277
+ fields: array(CompositionFieldStateSchema)
13278
+ });
12902
13279
  /** Where a block runs. The operator chooses — a block driving a device on an
12903
13280
  * agent is the reason placement is not fixed to the hub. */
12904
13281
  var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
12905
13282
  /** 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. */
13283
+ * a failing block reads the same way a failing addon does. `degraded` is a
13284
+ * composition on an existing device with a field handed back to the native
13285
+ * provider while its source is unavailable (S1b, D663); the error names it. */
12907
13286
  var CoreBlockStatusSchema = _enum([
12908
13287
  "stopped",
12909
13288
  "starting",
12910
13289
  "running",
13290
+ "degraded",
12911
13291
  "failed"
12912
13292
  ]);
12913
13293
  /** Client-authored fields. */
@@ -12925,6 +13305,37 @@ var CoreBlockInputSchema = object({
12925
13305
  */
12926
13306
  integrationId: string().optional()
12927
13307
  });
13308
+ /**
13309
+ * What a block IS (D659). Absent means `code` — every row stored before this
13310
+ * union keeps working unchanged.
13311
+ * - `code`: the TypeScript in `code`, compiled and run in its own runner;
13312
+ * - `recipe`: RESERVED (automations), stored shape only; refused at run by name;
13313
+ * - `composition`: declarative, run by the shared `composer`; `code` is `''`.
13314
+ */
13315
+ var CoreBlockSourceSchema = discriminatedUnion("kind", [
13316
+ object({ kind: literal("code") }),
13317
+ object({
13318
+ kind: literal("recipe"),
13319
+ recipe: AutomationRecipeSchema
13320
+ }),
13321
+ object({
13322
+ kind: literal("composition"),
13323
+ composition: CompositionSchema
13324
+ })
13325
+ ]);
13326
+ /**
13327
+ * The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
13328
+ *
13329
+ * `name`: REQUIRED for a `new` target — the block's name IS the device's name
13330
+ * (D659). ABSENT for an `existing` target — a customization has no name of its
13331
+ * own; the server derives one (`customizationBlockName`, D663) and refuses a
13332
+ * client-sent name by name.
13333
+ */
13334
+ var CompositionBlockInputSchema = object({
13335
+ name: string().min(1).max(120).optional(),
13336
+ enabled: boolean(),
13337
+ composition: CompositionSchema
13338
+ });
12928
13339
  /** A stored block. */
12929
13340
  var CoreBlockSchema = CoreBlockInputSchema.extend({
12930
13341
  id: string(),
@@ -12940,8 +13351,43 @@ var CoreBlockSchema = CoreBlockInputSchema.extend({
12940
13351
  */
12941
13352
  lastError: string().optional(),
12942
13353
  /** Ms epoch of the last state change. */
12943
- lastChangedAt: number()
13354
+ lastChangedAt: number(),
13355
+ /** What the block is. Absent means `code` (D659). */
13356
+ source: CoreBlockSourceSchema.optional()
13357
+ });
13358
+ /** A stored composition block and what the composer says about it. */
13359
+ var CompositionViewSchema = object({
13360
+ block: CoreBlockSchema,
13361
+ state: discriminatedUnion("kind", [object({
13362
+ kind: literal("answered"),
13363
+ state: CompositionBlockStateSchema.nullable()
13364
+ }), object({
13365
+ kind: literal("unreachable"),
13366
+ error: string()
13367
+ })])
12944
13368
  });
13369
+ /**
13370
+ * A read of the stored compositions. `not-loaded` while the `core_blocks`
13371
+ * table has not been READ: an empty answer then would make "not yet known"
13372
+ * look like "none", and invite the operator to recreate what exists (D315).
13373
+ */
13374
+ var CompositionsNotLoadedSchema = object({
13375
+ state: literal("not-loaded"),
13376
+ reason: string()
13377
+ });
13378
+ var CompositionListReadSchema = discriminatedUnion("state", [object({
13379
+ state: literal("loaded"),
13380
+ views: array(CompositionViewSchema)
13381
+ }), CompositionsNotLoadedSchema]);
13382
+ var CompositionGetReadSchema = discriminatedUnion("state", [object({
13383
+ state: literal("loaded"),
13384
+ view: CompositionViewSchema.nullable()
13385
+ }), CompositionsNotLoadedSchema]);
13386
+ /** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
13387
+ var CompositionCustomizationReadSchema = discriminatedUnion("state", [object({
13388
+ state: literal("loaded"),
13389
+ view: CompositionViewSchema.nullable()
13390
+ }), CompositionsNotLoadedSchema]);
12945
13391
  /** What a compile attempt produced. */
12946
13392
  var CoreBlockCompileResultSchema = object({
12947
13393
  ok: boolean(),
@@ -12979,7 +13425,75 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
12979
13425
  }), method(object({}), object({ libs: array(object({
12980
13426
  filePath: string(),
12981
13427
  content: string()
12982
- })) }), { auth: "admin" });
13428
+ })) }), { auth: "admin" }), method(object({ block: CompositionBlockInputSchema }), object({ block: CoreBlockSchema }), {
13429
+ kind: "mutation",
13430
+ auth: "admin",
13431
+ caller: "required"
13432
+ }), method(object({
13433
+ blockId: string(),
13434
+ expectedUpdatedAt: number(),
13435
+ block: CompositionBlockInputSchema.partial()
13436
+ }), object({ block: CoreBlockSchema }), {
13437
+ kind: "mutation",
13438
+ auth: "admin",
13439
+ caller: "required"
13440
+ }), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
13441
+ addonId: string().min(1),
13442
+ stableId: string().min(1)
13443
+ }), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
13444
+ composition: CompositionSchema,
13445
+ blockId: string().optional()
13446
+ }), CompositionValidationSchema, {
13447
+ kind: "mutation",
13448
+ auth: "admin",
13449
+ access: "view"
13450
+ });
13451
+ var ComposerBlockSchema = object({
13452
+ id: string().min(1),
13453
+ name: string().min(1).max(120),
13454
+ enabled: boolean(),
13455
+ updatedAt: number(),
13456
+ composition: CompositionSchema
13457
+ });
13458
+ /**
13459
+ * A stored composition row that no longer PARSES — a schema a release
13460
+ * tightened, a row written by a newer build. Sent by id so the composer keeps
13461
+ * its device as a placeholder and reports the block `failed` naming the parse
13462
+ * error. Leaving it out would read, twice, exactly like a delete (D49).
13463
+ */
13464
+ var ComposerUnreadableBlockSchema = object({
13465
+ id: string().min(1),
13466
+ /** The row's stored name column; null when even that is unreadable (the device keeps the name it has). */
13467
+ name: string().nullable(),
13468
+ /** The parse error, in the operator's words. */
13469
+ error: string()
13470
+ });
13471
+ method(object({
13472
+ /** The shared Blocks integration; null until `core-blocks` has reconciled it. */
13473
+ integrationId: string().nullable(),
13474
+ /** False when the `core_blocks` read failed: then nothing may be withdrawn (D49). */
13475
+ storeLoaded: boolean(),
13476
+ blocks: array(ComposerBlockSchema),
13477
+ /** Operator restarts: rebuild these runtimes and re-resolve their sources. Stores nothing (D62). */
13478
+ restartBlockIds: array(string()),
13479
+ /** Composition rows that exist but do not parse: held, never withdrawn, reported failed. */
13480
+ unreadableBlocks: array(ComposerUnreadableBlockSchema)
13481
+ }), object({
13482
+ blocks: array(object({
13483
+ blockId: string(),
13484
+ status: CompositionRunStatusSchema,
13485
+ reason: string().optional()
13486
+ })),
13487
+ /**
13488
+ * False when the composer declared and withdrew NOTHING (store not loaded,
13489
+ * integration unknown, its own rows unread or read empty once). An empty
13490
+ * apply the caller owes is paid off only by one the composer acted on (D49).
13491
+ */
13492
+ acted: boolean()
13493
+ }), {
13494
+ kind: "mutation",
13495
+ auth: "admin"
13496
+ }), method(object({ blockIds: array(string()) }), object({ states: array(CompositionBlockStateSchema) }), { auth: "admin" });
12983
13497
  /**
12984
13498
  * `custom-model-registry` — collection cap exposing operator-registered
12985
13499
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -14700,8 +15214,22 @@ var LinkedDeviceSchema = object({
14700
15214
  * The `linked-devices` extension slot's editable value (D648): the operator's
14701
15215
  * pick list, exactly what `_linkedDeviceIds` persists. Tree children are NOT
14702
15216
  * in it — they are always linked and are not a pick.
14703
- */
14704
- var LinkedDevicesSlotValueSchema = object({ deviceIds: array(number().int().positive()) });
15217
+ *
15218
+ * `trackedDeviceIds` (D655) is the tracked-event opt-in, exactly what
15219
+ * `_linkedDeviceTrackedIds` persists: which linked devices materialize a
15220
+ * synthetic tracked event on the camera. A read always carries it AS STORED
15221
+ * (an absent key reads as `[]`, which is what the resolver makes of it —
15222
+ * NONE). On a write it is optional: absent leaves the opt-in untouched, so a
15223
+ * card built before D655 cannot erase it by saving the pick list. A present
15224
+ * list REPLACES the opt-in; each id must be a pick, a device the camera links
15225
+ * by structure (the row's `impliedSources`: tree children, a picked
15226
+ * container's child) or already stored — so what was read can always be
15227
+ * written back unchanged, and a NEW opt-in outside the linked set is refused.
15228
+ */
15229
+ var LinkedDevicesSlotValueSchema = object({
15230
+ deviceIds: array(number().int().positive()),
15231
+ trackedDeviceIds: array(number().int().positive()).optional()
15232
+ });
14705
15233
  DeviceType.Camera;
14706
15234
  /** One camera's resolved linked set, tagged with the camera it belongs to.
14707
15235
  * The batch answer needs the tag; the single-device answer already has it
@@ -15205,6 +15733,52 @@ method(object({
15205
15733
  deviceIds: array(number()).readonly(),
15206
15734
  caps: array(string()).readonly().optional()
15207
15735
  }), record(string(), record(string(), unknown().nullable())));
15736
+ /**
15737
+ * Per-field ownership (D663). A composition may CLAIM fields of a native
15738
+ * cap's runtime-state slice on an existing device: `replace` overlays the
15739
+ * claimed fields on the native slice, `add` builds a slice the device does
15740
+ * not natively have. Declared here — once — so the hub mirror (Task 5) and
15741
+ * the claim methods (Task 6) share one union.
15742
+ */
15743
+ var FieldClaimModeSchema = _enum(["replace", "add"]);
15744
+ /** What a claim, patch or release answers. A refusal always names its reason. */
15745
+ var ClaimOutcomeSchema = discriminatedUnion("ok", [object({
15746
+ ok: literal(true),
15747
+ changed: boolean()
15748
+ }), object({
15749
+ ok: literal(false),
15750
+ code: _enum([
15751
+ "owned-by-other",
15752
+ "not-claimed",
15753
+ "field-not-claimed",
15754
+ "invalid-slice",
15755
+ "unknown-device",
15756
+ "row-unreadable",
15757
+ "migration-in-flight"
15758
+ ]),
15759
+ message: string()
15760
+ })]);
15761
+ /** One claim as `listClaims` reports it. `fields` is the PLANNED set, explicit (ruling S5). */
15762
+ var FieldClaimSchema = object({
15763
+ deviceId: number(),
15764
+ capName: string(),
15765
+ owner: string().min(1),
15766
+ mode: FieldClaimModeSchema,
15767
+ fields: array(string().min(1)).min(1)
15768
+ });
15769
+ /**
15770
+ * What `listClaims` answers. The claims INDEX is a listing projection loaded
15771
+ * from the settings store at boot; until a load has succeeded it is
15772
+ * `not-loaded`, never `[]` — an unanswered question must not look like an
15773
+ * empty answer (D315).
15774
+ */
15775
+ var ClaimsListingSchema = discriminatedUnion("state", [object({
15776
+ state: literal("loaded"),
15777
+ claims: array(FieldClaimSchema)
15778
+ }), object({
15779
+ state: literal("not-loaded"),
15780
+ reason: string()
15781
+ })]);
15208
15782
  method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
15209
15783
  deviceId: number(),
15210
15784
  capName: string()
@@ -15212,7 +15786,35 @@ method(object({ deviceId: number() }), record(string(), record(string(), unknown
15212
15786
  deviceId: number(),
15213
15787
  capName: string(),
15214
15788
  slice: record(string(), unknown())
15215
- }), _void(), { kind: "mutation" }), object({
15789
+ }), _void(), { kind: "mutation" }), method(object({
15790
+ deviceId: number(),
15791
+ capName: string(),
15792
+ owner: string().min(1),
15793
+ mode: FieldClaimModeSchema,
15794
+ fields: array(string().min(1)).min(1),
15795
+ values: record(string(), unknown())
15796
+ }), ClaimOutcomeSchema, {
15797
+ kind: "mutation",
15798
+ auth: "admin"
15799
+ }), method(object({
15800
+ deviceId: number(),
15801
+ capName: string(),
15802
+ owner: string().min(1),
15803
+ values: record(string(), unknown())
15804
+ }), ClaimOutcomeSchema, {
15805
+ kind: "mutation",
15806
+ auth: "admin"
15807
+ }), method(object({
15808
+ deviceId: number(),
15809
+ capName: string(),
15810
+ owner: string().min(1)
15811
+ }), ClaimOutcomeSchema, {
15812
+ kind: "mutation",
15813
+ auth: "admin"
15814
+ }), method(object({ ownerPrefix: string().optional() }), ClaimsListingSchema, { auth: "admin" }), method(object({
15815
+ deviceId: number(),
15816
+ capName: string()
15817
+ }), record(string(), unknown()).nullable(), { auth: "admin" }), object({
15216
15818
  deviceId: number(),
15217
15819
  capName: string(),
15218
15820
  slice: record(string(), unknown())
@@ -19184,8 +19786,8 @@ var NcTestResultSchema = object({
19184
19786
  className: string().optional(),
19185
19787
  label: string().optional()
19186
19788
  });
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
19789
+ /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is
19790
+ * `z.infer` of this schema — one shape, no twin. `mixed` is DERIVED
19189
19791
  * (a stored system rule whose kinds span families, D637) and never authored. */
19190
19792
  var NcRuleKindSchema = _enum([
19191
19793
  "detection",
@@ -21100,6 +21702,13 @@ var AudioEventSchema = object({
21100
21702
  originalClass: string().optional(),
21101
21703
  score: number()
21102
21704
  }).optional(),
21705
+ /**
21706
+ * Whether the classifier RAN on the window behind this row (D660). Absent
21707
+ * on rows written before D660 — unknown, NOT classified: until then a
21708
+ * window the analyzer's busy gate threw away was stored exactly like one
21709
+ * that heard nothing above the floor.
21710
+ */
21711
+ classifyOutcome: AudioClassifyOutcomeSchema.optional(),
21103
21712
  /** Populated by B5 (recording playback URL for this event). */
21104
21713
  mediaUrl: string().optional()
21105
21714
  });
@@ -21812,7 +22421,15 @@ var RebuildObjectEmbeddingsInput = object({
21812
22421
  * force is logged at start and finish so a deliberately slow pass reads
21813
22422
  * differently from a stalled one.
21814
22423
  */
21815
- pacingMs: number().int().nonnegative().optional()
22424
+ pacingMs: number().int().nonnegative().optional(),
22425
+ /**
22426
+ * Re-embed ONLY the tracks whose vector is ALREADY in the pinned model's
22427
+ * space (D664) — the in-space skip inverted. It exists to redo vectors
22428
+ * written from the wrong box, which a normal pass would skip as done
22429
+ * forever. Requires `maxTracks`: a forced pass without a bound is refused
22430
+ * (`force-requires-max-tracks`).
22431
+ */
22432
+ force: boolean().optional()
21816
22433
  });
21817
22434
  /**
21818
22435
  * Result of emptying the CLIP index.
@@ -21848,6 +22465,7 @@ var WipeObjectEmbeddingsResultSchema = object({
21848
22465
  * - `reads-disagree` — the two reads named different choices.
21849
22466
  * - `not-a-clip-model` — the CONFIRMED id has no CLIP metadata, so there is no
21850
22467
  * index to fill and no dimension to check against (D649).
22468
+ * - `force-requires-max-tracks` — `force: true` with no `maxTracks` (D664).
21851
22469
  */
21852
22470
  var RebuildObjectEmbeddingsRefusalSchema = _enum([
21853
22471
  "stores-unavailable",
@@ -21855,7 +22473,8 @@ var RebuildObjectEmbeddingsRefusalSchema = _enum([
21855
22473
  "method-unavailable",
21856
22474
  "read-failed",
21857
22475
  "reads-disagree",
21858
- "not-a-clip-model"
22476
+ "not-a-clip-model",
22477
+ "force-requires-max-tracks"
21859
22478
  ]);
21860
22479
  /**
21861
22480
  * Acknowledgement that a rebuild STARTED.
@@ -21880,11 +22499,32 @@ var RebuildStatusSchema = object({
21880
22499
  running: boolean(),
21881
22500
  scanned: number(),
21882
22501
  rebuilt: number(),
21883
- /** Tracks whose key frame is gone — nothing to re-embed from. */
22502
+ /** Tracks whose key frame is gone — nothing to re-embed from. Pixels ONLY:
22503
+ * `missingEvent` is its own field since D664, never summed in. */
21884
22504
  missingKeyFrame: number(),
21885
- /** Tracks with no usable detection box. */
22505
+ /** Tracks with no object event at all (D664). */
22506
+ missingEvent: number(),
22507
+ /** Tracks with no box measured on the pixels to cut — see `boxSource.refused`. */
21886
22508
  missingBbox: number(),
21887
22509
  /**
22510
+ * Which rule placed each judged track's box (D664): the key frame row's own
22511
+ * `subjectBox`, `track.positions` at exactly its instant (older key frames),
22512
+ * the event box (a key frame recovered from the recording, at the event's
22513
+ * instant), or `refused` by name — the reasons are in the tagged log lines.
22514
+ */
22515
+ boxSource: object({
22516
+ subjectBox: number(),
22517
+ positions: number(),
22518
+ event: number(),
22519
+ refused: number()
22520
+ }),
22521
+ /** Tracks skipped because their vector is already in the pinned space. */
22522
+ alreadyInSpace: number(),
22523
+ /** Whether the current / last pass was a `force` pass; `null` before any (D664). */
22524
+ force: boolean().nullable(),
22525
+ /** In-space tracks a `force` pass selected for re-embedding. */
22526
+ forcedInSpace: number(),
22527
+ /**
21888
22528
  * Tracks an executing node REFUSED rather than broke on — an unreadable key
21889
22529
  * frame, a step that threw. Separate from `failed` because the remedy is
21890
22530
  * different, and because a whole camera silently contributing zero vectors
@@ -22389,7 +23029,8 @@ var PipelineModelOptionSchema = object({
22389
23029
  })),
22390
23030
  group: ModelVariantGroupSchema.optional(),
22391
23031
  legacy: boolean().optional(),
22392
- provider: ModelProviderIdSchema.optional()
23032
+ provider: ModelProviderIdSchema.optional(),
23033
+ defaultConfidence: number().optional()
22393
23034
  });
22394
23035
  var ConfigFieldBridge = custom();
22395
23036
  var PipelineAddonSchemaSchema = object({
@@ -22691,7 +23332,14 @@ method(_void(), array(PipelineEngineChoiceSchema)), method(_void(), PipelineEngi
22691
23332
  height: number()
22692
23333
  }), { kind: "mutation" }), method(object({
22693
23334
  stepId: string(),
22694
- frameId: number().int()
23335
+ frameId: number().int(),
23336
+ /**
23337
+ * The model to time — REQUIRED (D658). Without it the pool answered the
23338
+ * step's ACTIVE variant (the first loaded after boot), so a benchmark
23339
+ * of model B silently timed model A. Callers pass the `rootModelId`
23340
+ * their warmup `runPipeline` reported: the model that actually ran.
23341
+ */
23342
+ modelId: string().min(1)
22695
23343
  }), record(string(), unknown()), { kind: "mutation" }), method(object({ frameId: number().int() }), _void(), { kind: "mutation" }), method(object({ deviceKey: string().optional() }), EffectiveTuningSchema), method(_void(), array(object({
22696
23344
  engineKey: string(),
22697
23345
  engine: PipelineEngineChoiceSchema,
@@ -26704,7 +27352,6 @@ var doorbellCapability = {
26704
27352
  kind: "wrapper",
26705
27353
  defaultActive: false,
26706
27354
  deviceTypes: [DeviceType.Button, DeviceType.Camera],
26707
- exposesDeviceSettings: true,
26708
27355
  extensionSlots: [DOORBELL_SOURCE_SLOT],
26709
27356
  methods: {},
26710
27357
  events: {
@@ -26754,7 +27401,7 @@ var ExtensionSourceSchema = object({
26754
27401
  /**
26755
27402
  * How one picked source stands. `missing` — no such device (deleted, or an id
26756
27403
  * 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
27404
+ * (a source owned by another addon); `unavailable` — it exists but is not a
26758
27405
  * valid source now (a container, which is never a pick). A source in any of
26759
27406
  * these states is SHOWN, never silently dropped or re-pointed.
26760
27407
  */
@@ -26794,13 +27441,19 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
26794
27441
  slot: ExtensionSlotIdSchema,
26795
27442
  /** Names the store written (D62), e.g. `virtual-doorbell:doorbellSources`. */
26796
27443
  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
27444
  activation: ExtensionSlotActivationSchema.nullable(),
26802
27445
  value: ExtensionSlotValueSchema,
26803
- sources: array(ExtensionSlotSourceSchema)
27446
+ sources: array(ExtensionSlotSourceSchema),
27447
+ /**
27448
+ * Devices the owner links to the host ON ITS OWN, by structure, and that
27449
+ * are therefore not in `value` (D655): a camera's device-tree children, and
27450
+ * the child a picked container stands for (D356 — a container is never a
27451
+ * pick, its children are). Read-only; the editor shows them and may offer
27452
+ * per-device options on them (the linked-devices tracked-event opt-in).
27453
+ * Absent when the owner links nothing beyond the picks, and from an owner
27454
+ * older than D655.
27455
+ */
27456
+ impliedSources: array(ExtensionSlotSourceSchema).optional()
26804
27457
  }), object({
26805
27458
  state: literal("unreachable"),
26806
27459
  addonId: string(),
@@ -26821,7 +27474,8 @@ var ExtensionCandidatesSchema = object({ candidates: array(object({
26821
27474
  /**
26822
27475
  * What a write did. `none` — applied in place. `host` / `parent` — the owner
26823
27476
  * 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).
27477
+ * write changed what the host REGISTERS. A slot never adds a capability to its
27478
+ * host — that is a composition (D663) — so today's owners answer `none`.
26825
27479
  */
26826
27480
  var ExtensionSetResultSchema = object({
26827
27481
  applied: literal(true),
@@ -28101,7 +28755,9 @@ var batteryCapability = {
28101
28755
  DeviceType.Camera,
28102
28756
  DeviceType.Sensor,
28103
28757
  DeviceType.Button,
28104
- DeviceType.Switch
28758
+ DeviceType.Switch,
28759
+ DeviceType.PetFeeder,
28760
+ DeviceType.Container
28105
28761
  ],
28106
28762
  methods: {
28107
28763
  /**
@@ -28128,6 +28784,19 @@ wakeForStream: method(object({
28128
28784
  awoke: boolean(),
28129
28785
  durationMs: number()
28130
28786
  }), { kind: "mutation" }) },
28787
+ /**
28788
+ * A composed provider (battery ADDED to a pet feeder or container that has
28789
+ * no wake surface) answers `wakeForStream` with the same "unavailable"
28790
+ * result the contract already defines above: "Returns `awoke: false` when
28791
+ * … the cap surface is unavailable." (D663).
28792
+ */
28793
+ composedMethods: { wakeForStream: {
28794
+ kind: "answer",
28795
+ value: {
28796
+ awoke: false,
28797
+ durationMs: 0
28798
+ }
28799
+ } },
28131
28800
  events: {
28132
28801
  /**
28133
28802
  * Emitted whenever the cached status changes (firmware push OR
@@ -28143,7 +28812,7 @@ onStatusChanged: { data: object({
28143
28812
  schema: BatteryStatusSchema,
28144
28813
  kind: "push",
28145
28814
  empty: {
28146
- percentage: 0,
28815
+ percentage: null,
28147
28816
  charging: "unknown",
28148
28817
  sleeping: false,
28149
28818
  lastUpdated: 0
@@ -28885,6 +29554,19 @@ var connectivityCapability = {
28885
29554
  * provider populates it by guessing (no HA inference). The UI renders a
28886
29555
  * "No consumables reported" placeholder when `items` is empty.
28887
29556
  */
29557
+ /** Units a remaining-life counter is reported in. Closed: the panel renders
29558
+ * each. */
29559
+ var ConsumableRemainingUnitSchema = _enum([
29560
+ "days",
29561
+ "hours",
29562
+ "cycles"
29563
+ ]);
29564
+ /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
29565
+ * days). `value` null = unknown (D393). */
29566
+ var ConsumableRemainingSchema = object({
29567
+ value: number().min(0).nullable(),
29568
+ unit: ConsumableRemainingUnitSchema
29569
+ });
28888
29570
  /** A single consumable item. Either a continuous `level` (remaining
28889
29571
  * life %) or a discrete `status` may be known — both may be null when a
28890
29572
  * provider only knows the item exists. `level` and `status` are not
@@ -28901,7 +29583,10 @@ var ConsumableItemSchema = object({
28901
29583
  /** Ms epoch of the last replace, when known. */
28902
29584
  lastResetAt: number().nullable(),
28903
29585
  /** Whether `reset()` is meaningful for this item. */
28904
- resettable: boolean()
29586
+ resettable: boolean(),
29587
+ /** Remaining life as a count (days/hours/cycles), when a provider reports
29588
+ * it that way instead of — or alongside — `level`. Absent = not reported. */
29589
+ remaining: ConsumableRemainingSchema.optional()
28905
29590
  });
28906
29591
  var ConsumablesStatusSchema = object({
28907
29592
  items: array(ConsumableItemSchema),
@@ -28931,6 +29616,16 @@ reset: method(object({
28931
29616
  kind: "mutation",
28932
29617
  auth: "admin"
28933
29618
  }) },
29619
+ /**
29620
+ * A composed consumable (item added to a device with no native consumables
29621
+ * provider) refuses `reset` by name: "Only meaningful when the item's
29622
+ * `resettable` is true" — a composed item is never resettable until
29623
+ * commands (`forward`) arrive in slice 3 (D663).
29624
+ */
29625
+ composedMethods: { reset: {
29626
+ kind: "refuse",
29627
+ reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
29628
+ } },
28934
29629
  status: {
28935
29630
  schema: ConsumablesStatusSchema,
28936
29631
  kind: "push",
@@ -38043,6 +38738,42 @@ var zoneRulesCapability = {
38043
38738
  */
38044
38739
  durability: "restored"
38045
38740
  };
38741
+ var NUMBER = {
38742
+ kind: "fixed",
38743
+ result: ["number"]
38744
+ };
38745
+ var BOOLEAN = {
38746
+ kind: "fixed",
38747
+ result: ["boolean"]
38748
+ };
38749
+ Object.freeze({
38750
+ min: NUMBER,
38751
+ max: NUMBER,
38752
+ abs: NUMBER,
38753
+ floor: NUMBER,
38754
+ ceil: NUMBER,
38755
+ sqrt: NUMBER,
38756
+ round: NUMBER,
38757
+ pow: NUMBER,
38758
+ clamp: NUMBER,
38759
+ avg: NUMBER,
38760
+ sum: NUMBER,
38761
+ age: NUMBER,
38762
+ convert: NUMBER,
38763
+ coalesce: { kind: "coalesce" },
38764
+ any: BOOLEAN,
38765
+ all: BOOLEAN,
38766
+ within: BOOLEAN,
38767
+ latest: {
38768
+ kind: "fixed",
38769
+ result: ["number", "null"]
38770
+ },
38771
+ number: {
38772
+ kind: "fixed",
38773
+ result: ["number", "null"]
38774
+ }
38775
+ });
38776
+ `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
38046
38777
  /**
38047
38778
  * Accessory device helpers — shared across drivers.
38048
38779
  *
@@ -38186,38 +38917,31 @@ var DeviceConfig = class DeviceConfig {
38186
38917
  }));
38187
38918
  }
38188
38919
  };
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
38920
  var DeviceRuntimeState = class DeviceRuntimeState {
38205
38921
  writer;
38922
+ onWriteFailed;
38206
38923
  /** In-flight writer promises tracked so `flush()` can await them. */
38207
38924
  pendingWrites = /* @__PURE__ */ new Set();
38925
+ /**
38926
+ * Caps whose last write the hub refused. The equality gate in
38927
+ * `applyCapWrite` is bypassed for these, so the SAME value is re-sent on the
38928
+ * next write instead of being swallowed forever; a landing write clears it.
38929
+ */
38930
+ failedWrites = /* @__PURE__ */ new Set();
38208
38931
  /** Per-cap committed slice — after schema validation when known. */
38209
38932
  slices;
38210
38933
  /** Per-cap registered schema (set by `installCapSchema`). */
38211
38934
  schemas = /* @__PURE__ */ new Map();
38212
38935
  listeners = /* @__PURE__ */ new Set();
38213
38936
  capListeners = /* @__PURE__ */ new Map();
38214
- constructor(initial, writer) {
38937
+ constructor(initial, writer, onWriteFailed) {
38215
38938
  this.writer = writer;
38939
+ this.onWriteFailed = onWriteFailed;
38216
38940
  this.slices = /* @__PURE__ */ new Map();
38217
38941
  for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
38218
38942
  }
38219
- static fromInitial(initial, writer) {
38220
- return new DeviceRuntimeState(initial, writer);
38943
+ static fromInitial(initial, writer, onWriteFailed) {
38944
+ return new DeviceRuntimeState(initial, writer, onWriteFailed);
38221
38945
  }
38222
38946
  installCapSchema(capName, schema) {
38223
38947
  const existing = this.schemas.get(capName);
@@ -38261,10 +38985,15 @@ var DeviceRuntimeState = class DeviceRuntimeState {
38261
38985
  ...value
38262
38986
  } : { ...value };
38263
38987
  const parsed = schema.parse(next);
38264
- if (shallowEqual(current, parsed)) return;
38988
+ if (shallowEqual(current, parsed) && !this.failedWrites.has(capName)) return;
38265
38989
  this.slices.set(capName, parsed);
38266
38990
  this.fireListeners([capName]);
38267
- const writePromise = this.writer(capName, { ...parsed }).catch(() => {});
38991
+ const writePromise = this.writer(capName, { ...parsed }).then(() => {
38992
+ this.failedWrites.delete(capName);
38993
+ }).catch((err) => {
38994
+ this.failedWrites.add(capName);
38995
+ this.onWriteFailed?.(capName, err instanceof Error ? err.message : String(err));
38996
+ });
38268
38997
  this.pendingWrites.add(writePromise);
38269
38998
  writePromise.finally(() => {
38270
38999
  this.pendingWrites.delete(writePromise);
@@ -38541,7 +39270,16 @@ var BaseDevice = class {
38541
39270
  });
38542
39271
  };
38543
39272
  const initial = ctx.initialRuntimeState ?? {};
38544
- this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer);
39273
+ const onWriteFailed = (cap, error) => {
39274
+ ctx.logger.warn("runtime-state write refused by the hub — the slice is not mirrored; it is re-sent on the next write", {
39275
+ tags: { deviceId: ctx.id },
39276
+ meta: {
39277
+ cap,
39278
+ error
39279
+ }
39280
+ });
39281
+ };
39282
+ this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer, onWriteFailed);
38545
39283
  ctx.bindRuntimeState?.(this.runtimeState);
38546
39284
  ctx.registerNativeCap?.(deviceStatusCapability, {});
38547
39285
  const seed = {
@@ -41809,6 +42547,18 @@ Object.freeze({
41809
42547
  addonId: null,
41810
42548
  access: "create"
41811
42549
  },
42550
+ "composer.applyBlocks": {
42551
+ capName: "composer",
42552
+ capScope: "system",
42553
+ addonId: null,
42554
+ access: "create"
42555
+ },
42556
+ "composer.getStates": {
42557
+ capName: "composer",
42558
+ capScope: "system",
42559
+ addonId: null,
42560
+ access: "view"
42561
+ },
41812
42562
  "connectionTest.describeTest": {
41813
42563
  capName: "connection-test",
41814
42564
  capScope: "system",
@@ -41869,6 +42619,12 @@ Object.freeze({
41869
42619
  addonId: null,
41870
42620
  access: "create"
41871
42621
  },
42622
+ "coreBlocks.createComposition": {
42623
+ capName: "core-blocks",
42624
+ capScope: "system",
42625
+ addonId: null,
42626
+ access: "create"
42627
+ },
41872
42628
  "coreBlocks.delete": {
41873
42629
  capName: "core-blocks",
41874
42630
  capScope: "system",
@@ -41881,6 +42637,18 @@ Object.freeze({
41881
42637
  addonId: null,
41882
42638
  access: "view"
41883
42639
  },
42640
+ "coreBlocks.getComposition": {
42641
+ capName: "core-blocks",
42642
+ capScope: "system",
42643
+ addonId: null,
42644
+ access: "view"
42645
+ },
42646
+ "coreBlocks.getCustomization": {
42647
+ capName: "core-blocks",
42648
+ capScope: "system",
42649
+ addonId: null,
42650
+ access: "view"
42651
+ },
41884
42652
  "coreBlocks.getTypeDefs": {
41885
42653
  capName: "core-blocks",
41886
42654
  capScope: "system",
@@ -41893,6 +42661,12 @@ Object.freeze({
41893
42661
  addonId: null,
41894
42662
  access: "view"
41895
42663
  },
42664
+ "coreBlocks.listCompositions": {
42665
+ capName: "core-blocks",
42666
+ capScope: "system",
42667
+ addonId: null,
42668
+ access: "view"
42669
+ },
41896
42670
  "coreBlocks.restart": {
41897
42671
  capName: "core-blocks",
41898
42672
  capScope: "system",
@@ -41911,6 +42685,18 @@ Object.freeze({
41911
42685
  addonId: null,
41912
42686
  access: "create"
41913
42687
  },
42688
+ "coreBlocks.updateComposition": {
42689
+ capName: "core-blocks",
42690
+ capScope: "system",
42691
+ addonId: null,
42692
+ access: "create"
42693
+ },
42694
+ "coreBlocks.validateComposition": {
42695
+ capName: "core-blocks",
42696
+ capScope: "system",
42697
+ addonId: null,
42698
+ access: "view"
42699
+ },
41914
42700
  "cover.close": {
41915
42701
  capName: "cover",
41916
42702
  capScope: "device",
@@ -42397,12 +43183,6 @@ Object.freeze({
42397
43183
  addonId: null,
42398
43184
  access: "create"
42399
43185
  },
42400
- "deviceManager.applyDeviceSettingsPatch": {
42401
- capName: "device-manager",
42402
- capScope: "system",
42403
- addonId: null,
42404
- access: "create"
42405
- },
42406
43186
  "deviceManager.applyInitialMeta": {
42407
43187
  capName: "device-manager",
42408
43188
  capScope: "system",
@@ -42505,12 +43285,6 @@ Object.freeze({
42505
43285
  addonId: null,
42506
43286
  access: "view"
42507
43287
  },
42508
- "deviceManager.getDeviceLiveContribution": {
42509
- capName: "device-manager",
42510
- capScope: "system",
42511
- addonId: null,
42512
- access: "view"
42513
- },
42514
43288
  "deviceManager.getDeviceLiveInfoAggregate": {
42515
43289
  capName: "device-manager",
42516
43290
  capScope: "system",
@@ -42523,12 +43297,6 @@ Object.freeze({
42523
43297
  addonId: null,
42524
43298
  access: "view"
42525
43299
  },
42526
- "deviceManager.getDeviceSettingsContribution": {
42527
- capName: "device-manager",
42528
- capScope: "system",
42529
- addonId: null,
42530
- access: "view"
42531
- },
42532
43300
  "deviceManager.getDeviceStatusAggregate": {
42533
43301
  capName: "device-manager",
42534
43302
  capScope: "system",
@@ -43003,6 +43771,12 @@ Object.freeze({
43003
43771
  addonId: null,
43004
43772
  access: "create"
43005
43773
  },
43774
+ "deviceState.claimFields": {
43775
+ capName: "device-state",
43776
+ capScope: "system",
43777
+ addonId: null,
43778
+ access: "create"
43779
+ },
43006
43780
  "deviceState.getAllSnapshots": {
43007
43781
  capName: "device-state",
43008
43782
  capScope: "system",
@@ -43015,38 +43789,44 @@ Object.freeze({
43015
43789
  addonId: null,
43016
43790
  access: "view"
43017
43791
  },
43018
- "deviceState.getSnapshot": {
43792
+ "deviceState.getNativeCapSlice": {
43019
43793
  capName: "device-state",
43020
43794
  capScope: "system",
43021
43795
  addonId: null,
43022
43796
  access: "view"
43023
43797
  },
43024
- "deviceState.setCapSlice": {
43798
+ "deviceState.getSnapshot": {
43025
43799
  capName: "device-state",
43026
43800
  capScope: "system",
43027
43801
  addonId: null,
43028
- access: "create"
43802
+ access: "view"
43029
43803
  },
43030
- "deviceStatus.getStatus": {
43031
- capName: "device-status",
43032
- capScope: "device",
43804
+ "deviceState.listClaims": {
43805
+ capName: "device-state",
43806
+ capScope: "system",
43033
43807
  addonId: null,
43034
43808
  access: "view"
43035
43809
  },
43036
- "doorbell.applyDeviceSettingsPatch": {
43037
- capName: "doorbell",
43038
- capScope: "device",
43810
+ "deviceState.patchOwnedFields": {
43811
+ capName: "device-state",
43812
+ capScope: "system",
43039
43813
  addonId: null,
43040
43814
  access: "create"
43041
43815
  },
43042
- "doorbell.getDeviceLiveContribution": {
43043
- capName: "doorbell",
43044
- capScope: "device",
43816
+ "deviceState.releaseClaim": {
43817
+ capName: "device-state",
43818
+ capScope: "system",
43045
43819
  addonId: null,
43046
- access: "view"
43820
+ access: "create"
43047
43821
  },
43048
- "doorbell.getDeviceSettingsContribution": {
43049
- capName: "doorbell",
43822
+ "deviceState.setCapSlice": {
43823
+ capName: "device-state",
43824
+ capScope: "system",
43825
+ addonId: null,
43826
+ access: "create"
43827
+ },
43828
+ "deviceStatus.getStatus": {
43829
+ capName: "device-status",
43050
43830
  capScope: "device",
43051
43831
  addonId: null,
43052
43832
  access: "view"
@@ -48554,16 +49334,36 @@ Object.freeze({
48554
49334
  form: "single",
48555
49335
  optional: false
48556
49336
  }],
49337
+ "deviceState.claimFields": [{
49338
+ name: "deviceId",
49339
+ form: "single",
49340
+ optional: false
49341
+ }],
48557
49342
  "deviceState.getCapSlice": [{
48558
49343
  name: "deviceId",
48559
49344
  form: "single",
48560
49345
  optional: false
48561
49346
  }],
49347
+ "deviceState.getNativeCapSlice": [{
49348
+ name: "deviceId",
49349
+ form: "single",
49350
+ optional: false
49351
+ }],
48562
49352
  "deviceState.getSnapshot": [{
48563
49353
  name: "deviceId",
48564
49354
  form: "single",
48565
49355
  optional: false
48566
49356
  }],
49357
+ "deviceState.patchOwnedFields": [{
49358
+ name: "deviceId",
49359
+ form: "single",
49360
+ optional: false
49361
+ }],
49362
+ "deviceState.releaseClaim": [{
49363
+ name: "deviceId",
49364
+ form: "single",
49365
+ optional: false
49366
+ }],
48567
49367
  "deviceState.setCapSlice": [{
48568
49368
  name: "deviceId",
48569
49369
  form: "single",
@@ -51167,4 +51967,4 @@ function bestLocationMatch(externalName, existing, threshold = .8) {
51167
51967
  var MB = 1024 * 1024;
51168
51968
  1024 * MB, 3072 * MB;
51169
51969
  //#endregion
51170
- export { parseAdminBase as $, contactCapability as A, union as At, gasCapability as B, buildAddonRouteProvider as C, array as Ct, climateControlCapability as D, object as Dt, carbonMonoxideCapability as E, number as Et, deviceExportCapability as F, lockControlCapability as G, humiditySensorCapability as H, enumSensorCapability as I, normalizeUnit as J, mediaPlayerCapability as K, eventEmitterCapability as L, coverCapability as M, url as Mt, deviceAdminLinkCapability as N, EventCategory as Nt, colorCapability as O, record as Ot, deviceAdoptionCapability as P, oauthIntegrationCapability as Q, fanControlCapability as R, brokerCapability as S, _enum as St, buttonCapability as T, literal as Tt, imageCapability as U, humidifierCapability as V, lawnMowerControlCapability as W, notifierCapability as X, notificationOutputCapability as Y, numericSensorCapability as Z, automationControlCapability as _, DeviceFeature as _t, COCO_TO_MACRO as a, smokeCapability as at, binaryCapability as b, createEvent as bt, FanDirectionSchema as c, temperatureSensorCapability as ct, TargetSchema as d, valveCapability as dt, powerMeterCapability as et, accessoriesCapability as f, vibrationCapability as ft, ambientLightSensorCapability as g, BaseAddon as gt, alarmPanelCapability as h, errMsg as ht, CAMERA_SWITCH_ORDER as i, scriptRunnerCapability as it, controlCapability as j, unknown as jt, connectivityCapability as k, string as kt, HvacModeSchema as l, updateCapability as lt, airQualitySensorCapability as m, weatherCapability as mt, BaseDevice as n, presenceCapability as nt, CameraSwitchIdSchema as o, switchCapability as ot, addonRoutesCapability as p, waterHeaterCapability as pt, motionCapability as q, BaseDeviceProvider as r, pressureSensorCapability as rt, EnumSensorDateTimeFormatSchema as s, tamperCapability as st, AlarmArmModeSchema as t, prepareNotification as tt, MediaPlayerRepeatSchema as u, vacuumControlCapability as ut, batteryCapability as v, DeviceRole as vt, buildDeviceAdminUrl as w, discriminatedUnion as wt, brightnessCapability as x, nodePin as xt, bestLocationMatch as y, DeviceType as yt, floodCapability as z };
51970
+ export { parseAdminBase as $, contactCapability as A, union as At, gasCapability as B, buildAddonRouteProvider as C, array as Ct, climateControlCapability as D, object as Dt, carbonMonoxideCapability as E, number as Et, deviceExportCapability as F, lockControlCapability as G, humiditySensorCapability as H, enumSensorCapability as I, normalizeUnit as J, mediaPlayerCapability as K, eventEmitterCapability as L, coverCapability as M, url as Mt, deviceAdminLinkCapability as N, EventCategory as Nt, colorCapability as O, record as Ot, deviceAdoptionCapability as P, oauthIntegrationCapability as Q, fanControlCapability as R, brokerCapability as S, _enum as St, buttonCapability as T, literal as Tt, imageCapability as U, humidifierCapability as V, lawnMowerControlCapability as W, notifierCapability as X, notificationOutputCapability as Y, numericSensorCapability as Z, automationControlCapability as _, DeviceType as _t, COCO_TO_MACRO as a, smokeCapability as at, binaryCapability as b, createEvent as bt, FanDirectionSchema as c, temperatureSensorCapability as ct, TargetSchema as d, valveCapability as dt, powerMeterCapability as et, accessoriesCapability as f, vibrationCapability as ft, ambientLightSensorCapability as g, DeviceRole as gt, alarmPanelCapability as h, DeviceFeature as ht, CAMERA_SWITCH_ORDER as i, scriptRunnerCapability as it, controlCapability as j, unknown as jt, connectivityCapability as k, string as kt, HvacModeSchema as l, updateCapability as lt, airQualitySensorCapability as m, weatherCapability as mt, BaseDevice as n, presenceCapability as nt, CameraSwitchIdSchema as o, switchCapability as ot, addonRoutesCapability as p, waterHeaterCapability as pt, motionCapability as q, BaseDeviceProvider as r, pressureSensorCapability as rt, EnumSensorDateTimeFormatSchema as s, tamperCapability as st, AlarmArmModeSchema as t, prepareNotification as tt, MediaPlayerRepeatSchema as u, vacuumControlCapability as ut, batteryCapability as v, errMsg as vt, buildDeviceAdminUrl as w, discriminatedUnion as wt, brightnessCapability as x, nodePin as xt, bestLocationMatch as y, BaseAddon as yt, floodCapability as z };