@camstack/addon-post-analysis 1.2.291 → 1.2.293
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{clip-model-registry-CTY5co6P.js → clip-model-registry-BAuTHS_s.js} +2323 -2033
- package/dist/{clip-model-registry-Cudz--Ql.mjs → clip-model-registry-D2zqXamW.mjs} +2324 -2034
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CBHxwCSj.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DW6-zerK.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Boh18P08.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Bs5QFgmh.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-DSKT2dE4.mjs → hostInit-CAJh3aX8.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +445 -45
- package/dist/pipeline-analytics/index.mjs +445 -45
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +1 -1
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-2OwTV_D2.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-DAAKQ3sC.mjs +0 -26
|
@@ -30,7 +30,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
30
30
|
}) : target, mod));
|
|
31
31
|
//#endregion
|
|
32
32
|
let node_crypto = require("node:crypto");
|
|
33
|
-
//#region ../types/dist/event-category-
|
|
33
|
+
//#region ../types/dist/event-category-C5xZWqz6.mjs
|
|
34
34
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
35
35
|
EventCategory["SystemBoot"] = "system.boot";
|
|
36
36
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -394,6 +394,16 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
394
394
|
*/
|
|
395
395
|
EventCategory["DeviceStateChanged"] = "device.state-changed";
|
|
396
396
|
/**
|
|
397
|
+
* The NATIVE shadow of a cap a composition has claimed fields of moved
|
|
398
|
+
* (D663). Payload: `{ deviceId, capName, native }` — what the provider
|
|
399
|
+
* wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
|
|
400
|
+
* the composer's native self-reads, and the public event routers refuse it
|
|
401
|
+
* (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
|
|
402
|
+
* truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
|
|
403
|
+
* `replace` claim, one per native write of that cap.
|
|
404
|
+
*/
|
|
405
|
+
EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
|
|
406
|
+
/**
|
|
397
407
|
* Frame occupancy for a camera CHANGED — a tracked object was gained or
|
|
398
408
|
* lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
|
|
399
409
|
*
|
|
@@ -5391,7 +5401,7 @@ var ZodIssueCode = {
|
|
|
5391
5401
|
var ZodFirstPartyTypeKind;
|
|
5392
5402
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5393
5403
|
//#endregion
|
|
5394
|
-
//#region ../types/dist/sleep-
|
|
5404
|
+
//#region ../types/dist/sleep-jkRQgtSc.mjs
|
|
5395
5405
|
/**
|
|
5396
5406
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5397
5407
|
* window to float samples (D455).
|
|
@@ -7092,324 +7102,6 @@ function event(data) {
|
|
|
7092
7102
|
var StaticDirOutputSchema$1 = object({ staticDir: string() });
|
|
7093
7103
|
var VersionOutputSchema$1 = object({ version: string() });
|
|
7094
7104
|
method(_void(), StaticDirOutputSchema$1, { auth: "admin" }), method(_void(), VersionOutputSchema$1, { auth: "admin" });
|
|
7095
|
-
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
7096
|
-
DeviceType["Camera"] = "camera";
|
|
7097
|
-
DeviceType["Hub"] = "hub";
|
|
7098
|
-
DeviceType["Light"] = "light";
|
|
7099
|
-
DeviceType["Siren"] = "siren";
|
|
7100
|
-
DeviceType["Switch"] = "switch";
|
|
7101
|
-
DeviceType["Sensor"] = "sensor";
|
|
7102
|
-
DeviceType["Thermostat"] = "thermostat";
|
|
7103
|
-
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
7104
|
-
* `climate-control` cap surface with `Thermostat` but renders a
|
|
7105
|
-
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
7106
|
-
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
7107
|
-
* reusable by other AC integrations. */
|
|
7108
|
-
DeviceType["Climate"] = "climate";
|
|
7109
|
-
DeviceType["Button"] = "button";
|
|
7110
|
-
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
7111
|
-
* event vocabulary verbatim (no normalization). Installed with the
|
|
7112
|
-
* `event-emitter` cap. Sources: HA `event.*` entities (structured) and
|
|
7113
|
-
* HA bus events (e.g. `zha_event`, generic). */
|
|
7114
|
-
DeviceType["EventEmitter"] = "event-emitter";
|
|
7115
|
-
/** Firmware/software update entity — current vs available version,
|
|
7116
|
-
* updatable flag, update state, and an install action. Installed with
|
|
7117
|
-
* the `update` cap. Sources: Homematic firmware-update channels (and
|
|
7118
|
-
* reusable by other providers, e.g. HA `update.*` entities). */
|
|
7119
|
-
DeviceType["Update"] = "update";
|
|
7120
|
-
DeviceType["Generic"] = "generic";
|
|
7121
|
-
/** Generic notification delivery target (HA `notify.<service>`, future
|
|
7122
|
-
* Telegram / Discord / ntfy / SMTP, …). One device per delivery
|
|
7123
|
-
* endpoint; the `notifier` cap defines the send surface. */
|
|
7124
|
-
DeviceType["Notifier"] = "notifier";
|
|
7125
|
-
/** Pre-recorded action sequence with optional parameters
|
|
7126
|
-
* (HA `script.*`). Runnable via `script-runner` cap. */
|
|
7127
|
-
DeviceType["Script"] = "script";
|
|
7128
|
-
/** Automation rule (HA `automation.*`) — enable/disable + manual
|
|
7129
|
-
* trigger surface exposed via `automation-control` cap. */
|
|
7130
|
-
DeviceType["Automation"] = "automation";
|
|
7131
|
-
/** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
|
|
7132
|
-
DeviceType["Lock"] = "lock";
|
|
7133
|
-
/** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
|
|
7134
|
-
* `valve.*`). `cover` cap with sub-roles for variant. */
|
|
7135
|
-
DeviceType["Cover"] = "cover";
|
|
7136
|
-
/** Pipe / water / gas valve with open/close/stop and optional
|
|
7137
|
-
* position (HA `valve.*`). `valve` cap — a cover-sibling actuator
|
|
7138
|
-
* modelled on the same open/closed lifecycle. */
|
|
7139
|
-
DeviceType["Valve"] = "valve";
|
|
7140
|
-
/** Humidifier / dehumidifier with on/off + target humidity + mode
|
|
7141
|
-
* (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
|
|
7142
|
-
* modelled on the same target / mode lifecycle. */
|
|
7143
|
-
DeviceType["Humidifier"] = "humidifier";
|
|
7144
|
-
/** Water heater / boiler with target temperature + operation mode +
|
|
7145
|
-
* away mode (HA `water_heater.*`). `water-heater` cap — a
|
|
7146
|
-
* climate-family actuator. */
|
|
7147
|
-
DeviceType["WaterHeater"] = "water-heater";
|
|
7148
|
-
/** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
|
|
7149
|
-
DeviceType["Fan"] = "fan";
|
|
7150
|
-
/** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
|
|
7151
|
-
* the camera surface — those use `Camera`. `media-player` cap. */
|
|
7152
|
-
DeviceType["MediaPlayer"] = "media-player";
|
|
7153
|
-
/** Security panel / alarm system (HA `alarm_control_panel.*`).
|
|
7154
|
-
* `alarm-panel` cap. */
|
|
7155
|
-
DeviceType["AlarmPanel"] = "alarm-panel";
|
|
7156
|
-
/** Generic user-settable input (HA `number` / `input_number` / `select`
|
|
7157
|
-
* / `input_select` / `text` / `input_text` / `input_datetime`).
|
|
7158
|
-
* Sub-type via `DeviceRole`: NumericControl / SelectControl /
|
|
7159
|
-
* TextControl / DateTimeControl. */
|
|
7160
|
-
DeviceType["Control"] = "control";
|
|
7161
|
-
/** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
|
|
7162
|
-
* `presence` cap. */
|
|
7163
|
-
DeviceType["Presence"] = "presence";
|
|
7164
|
-
/** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
|
|
7165
|
-
* `weather` cap. */
|
|
7166
|
-
DeviceType["Weather"] = "weather";
|
|
7167
|
-
/** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
|
|
7168
|
-
DeviceType["Vacuum"] = "vacuum";
|
|
7169
|
-
/** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
|
|
7170
|
-
* `lawn-mower-control` cap. */
|
|
7171
|
-
DeviceType["LawnMower"] = "lawn-mower";
|
|
7172
|
-
/** Physical HA device group — parent container for entity-children
|
|
7173
|
-
* adopted from a single HA device entry. Not renderable as a
|
|
7174
|
-
* standalone device; exists only to anchor child entities. */
|
|
7175
|
-
DeviceType["Container"] = "container";
|
|
7176
|
-
/** Single still-image entity (HA `image.*`). Read-only display of an
|
|
7177
|
-
* `entity_picture` signed URL the browser loads directly. `image` cap. */
|
|
7178
|
-
DeviceType["Image"] = "image";
|
|
7179
|
-
/** Smart pet feeder — cloud-connected food dispenser with a bowl food
|
|
7180
|
-
* level, battery, desiccant life, feeding state and manual-feed /
|
|
7181
|
-
* call-pet / maintenance actions. Installed with the `pet-feeder` cap;
|
|
7182
|
-
* dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
|
|
7183
|
-
* native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
|
|
7184
|
-
* integrations sharing the same food/desiccant/hopper surface. */
|
|
7185
|
-
DeviceType["PetFeeder"] = "pet-feeder";
|
|
7186
|
-
return DeviceType;
|
|
7187
|
-
}({});
|
|
7188
|
-
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
7189
|
-
DeviceFeature["BatteryOperated"] = "battery-operated";
|
|
7190
|
-
DeviceFeature["Rebootable"] = "rebootable";
|
|
7191
|
-
/**
|
|
7192
|
-
* Device supports an on-demand re-sync of its derived spec with its
|
|
7193
|
-
* upstream source — drives the generic Re-sync button. The owning
|
|
7194
|
-
* provider implements the action via the `device-adoption.resync` cap.
|
|
7195
|
-
*/
|
|
7196
|
-
DeviceFeature["Resyncable"] = "resyncable";
|
|
7197
|
-
DeviceFeature["NativeSnapshot"] = "native-snapshot";
|
|
7198
|
-
DeviceFeature["DoorbellButton"] = "doorbell-button";
|
|
7199
|
-
DeviceFeature["TwoWayAudio"] = "two-way-audio";
|
|
7200
|
-
DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
|
|
7201
|
-
/**
|
|
7202
|
-
* Camera supports the on-firmware autotrack subsystem (subject-
|
|
7203
|
-
* following). Distinct from `PanTiltZoom` because not every PTZ
|
|
7204
|
-
* camera ships autotrack — the admin UI uses this flag to gate
|
|
7205
|
-
* the autotrack toggle / settings card without re-deriving from
|
|
7206
|
-
* the cap registry. Mirrors `ptz-autotrack` cap registration:
|
|
7207
|
-
* driver sets this feature when probe confirms the firmware
|
|
7208
|
-
* surface, and registers the cap in the same code path.
|
|
7209
|
-
*/
|
|
7210
|
-
DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
|
|
7211
|
-
/**
|
|
7212
|
-
* Accessory exposes a "trigger on motion" toggle — the parent camera's
|
|
7213
|
-
* motion detection automatically activates this device. Mirrors
|
|
7214
|
-
* `motion-trigger` cap registration: drivers set this feature in the
|
|
7215
|
-
* same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
|
|
7216
|
-
*
|
|
7217
|
-
* Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
|
|
7218
|
-
* fast scalar without binding fetch), notifier rules, and `listAll`
|
|
7219
|
-
* filters that want "all devices with on-motion behaviour".
|
|
7220
|
-
*/
|
|
7221
|
-
DeviceFeature["MotionTrigger"] = "motion-trigger";
|
|
7222
|
-
/** Light supports rgb-triplet color via `color` cap. */
|
|
7223
|
-
DeviceFeature["LightColorRgb"] = "light-color-rgb";
|
|
7224
|
-
/** Light supports HSV color via `color` cap. */
|
|
7225
|
-
DeviceFeature["LightColorHsv"] = "light-color-hsv";
|
|
7226
|
-
/** Light supports color-temperature (mired) via `color` cap. */
|
|
7227
|
-
DeviceFeature["LightColorMired"] = "light-color-mired";
|
|
7228
|
-
/** Thermostat supports a `heat_cool` dual setpoint (targetLow +
|
|
7229
|
-
* targetHigh). Gates the range slider UI. */
|
|
7230
|
-
DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
|
|
7231
|
-
/** Thermostat exposes target humidity and/or current humidity
|
|
7232
|
-
* readings. Gates the humidity controls. */
|
|
7233
|
-
DeviceFeature["ClimateHumidity"] = "climate-humidity";
|
|
7234
|
-
/** Thermostat exposes a fan-mode selector. */
|
|
7235
|
-
DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
|
|
7236
|
-
/** Thermostat exposes preset modes (eco / away / sleep / vendor). */
|
|
7237
|
-
DeviceFeature["ClimatePreset"] = "climate-preset";
|
|
7238
|
-
/** Thermostat exposes a vertical louver swing toggle. Gates the
|
|
7239
|
-
* vertical-swing switch in the climate UI. Independent of horizontal. */
|
|
7240
|
-
DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
|
|
7241
|
-
/** Thermostat exposes a horizontal louver swing toggle. Gates the
|
|
7242
|
-
* horizontal-swing switch in the climate UI. Independent of vertical. */
|
|
7243
|
-
DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
|
|
7244
|
-
/** Cover exposes intermediate position control (0..100). Gates the
|
|
7245
|
-
* position slider UI. */
|
|
7246
|
-
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
7247
|
-
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
7248
|
-
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
7249
|
-
/** Valve exposes intermediate position control (0..100). Gates the
|
|
7250
|
-
* position slider / drag surface UI. */
|
|
7251
|
-
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
7252
|
-
/** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
|
|
7253
|
-
DeviceFeature["FanSpeed"] = "fan-speed";
|
|
7254
|
-
/** Fan exposes a preset mode selector. */
|
|
7255
|
-
DeviceFeature["FanPreset"] = "fan-preset";
|
|
7256
|
-
/** Fan exposes blade direction (forward/reverse) — typical of
|
|
7257
|
-
* ceiling fans. */
|
|
7258
|
-
DeviceFeature["FanDirection"] = "fan-direction";
|
|
7259
|
-
/** Fan exposes an oscillation toggle. */
|
|
7260
|
-
DeviceFeature["FanOscillating"] = "fan-oscillating";
|
|
7261
|
-
/** Lock requires a PIN code on lock/unlock. Gates the code-entry
|
|
7262
|
-
* field on the UI lock-controls panel. */
|
|
7263
|
-
DeviceFeature["LockPinRequired"] = "lock-pin-required";
|
|
7264
|
-
/** Lock supports a latch-release ("open door") action distinct from
|
|
7265
|
-
* unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
|
|
7266
|
-
* `supported_features`. Gates the Open Door button in the UI. */
|
|
7267
|
-
DeviceFeature["LockOpen"] = "lock-open";
|
|
7268
|
-
/** Media player exposes a seek-to-position surface. */
|
|
7269
|
-
DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
|
|
7270
|
-
/** Media player exposes a volume-level setter. */
|
|
7271
|
-
DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
|
|
7272
|
-
/** Media player exposes a mute toggle distinct from volume=0. */
|
|
7273
|
-
DeviceFeature["MediaPlayerMute"] = "media-player-mute";
|
|
7274
|
-
/** Media player exposes a shuffle toggle. */
|
|
7275
|
-
DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
|
|
7276
|
-
/** Media player exposes a repeat mode (off / all / one). */
|
|
7277
|
-
DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
|
|
7278
|
-
/** Media player exposes a source / input selector. */
|
|
7279
|
-
DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
|
|
7280
|
-
/** Media player exposes a play-arbitrary-media surface (URL / id). */
|
|
7281
|
-
DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
|
|
7282
|
-
/** Media player exposes next-track. */
|
|
7283
|
-
DeviceFeature["MediaPlayerNext"] = "media-player-next";
|
|
7284
|
-
/** Media player exposes previous-track. */
|
|
7285
|
-
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
7286
|
-
/** Media player exposes stop distinct from pause. */
|
|
7287
|
-
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
7288
|
-
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
7289
|
-
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
7290
|
-
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
7291
|
-
* addition to a textual location. */
|
|
7292
|
-
DeviceFeature["PresenceGps"] = "presence-gps";
|
|
7293
|
-
/** Notifier accepts an inline / URL image attachment. */
|
|
7294
|
-
DeviceFeature["NotifierImage"] = "notifier-image";
|
|
7295
|
-
/** Notifier accepts a priority hint (high/normal/low). */
|
|
7296
|
-
DeviceFeature["NotifierPriority"] = "notifier-priority";
|
|
7297
|
-
/** Notifier accepts a free-form `data` payload for platform-specific
|
|
7298
|
-
* fields. */
|
|
7299
|
-
DeviceFeature["NotifierData"] = "notifier-data";
|
|
7300
|
-
/** Notifier supports interactive action buttons / callbacks. */
|
|
7301
|
-
DeviceFeature["NotifierActions"] = "notifier-actions";
|
|
7302
|
-
/** Notifier supports per-call recipient targeting (multi-user). */
|
|
7303
|
-
DeviceFeature["NotifierRecipients"] = "notifier-recipients";
|
|
7304
|
-
/** Script runner accepts a variables map on each run invocation. */
|
|
7305
|
-
DeviceFeature["ScriptVariables"] = "script-variables";
|
|
7306
|
-
/** Automation `trigger` accepts a skipCondition flag — fires the
|
|
7307
|
-
* automation's actions while bypassing its condition block. */
|
|
7308
|
-
DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
|
|
7309
|
-
/** Robot vacuum exposes a live cleaning map (image child). Gates the
|
|
7310
|
-
* map tile in the vacuum UI. */
|
|
7311
|
-
DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
|
|
7312
|
-
/** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
|
|
7313
|
-
* switches group. */
|
|
7314
|
-
DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
|
|
7315
|
-
/** Robot mower exposes a live mowing map (SVG image child). Gates the
|
|
7316
|
-
* mower map tile. */
|
|
7317
|
-
DeviceFeature["MowerHasMap"] = "mower-has-map";
|
|
7318
|
-
/** Robot mower exposes targeted mowing (all-area / zones / edges /
|
|
7319
|
-
* spots) — gates the Mow action + map selectors. */
|
|
7320
|
-
DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
|
|
7321
|
-
return DeviceFeature;
|
|
7322
|
-
}({});
|
|
7323
|
-
/**
|
|
7324
|
-
* Semantic role a device plays within its parent. Populated by driver
|
|
7325
|
-
* addons when creating accessory devices (Reolink siren/floodlight/
|
|
7326
|
-
* PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
|
|
7327
|
-
* admin UI to pick icons, labels, and widgets — a `Switch` with
|
|
7328
|
-
* `role: Floodlight` renders as a bulb with a brightness slider,
|
|
7329
|
-
* whereas a `Switch` with `role: Siren` renders as a klaxon.
|
|
7330
|
-
*
|
|
7331
|
-
* Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
|
|
7332
|
-
* sqlite as a nullable TEXT column — old rows keep working unchanged.
|
|
7333
|
-
*/
|
|
7334
|
-
var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
7335
|
-
DeviceRole["Siren"] = "siren";
|
|
7336
|
-
DeviceRole["Floodlight"] = "floodlight";
|
|
7337
|
-
DeviceRole["Spotlight"] = "spotlight";
|
|
7338
|
-
DeviceRole["PirSensor"] = "pir-sensor";
|
|
7339
|
-
DeviceRole["Chime"] = "chime";
|
|
7340
|
-
DeviceRole["Autotrack"] = "autotrack";
|
|
7341
|
-
DeviceRole["Nightvision"] = "nightvision";
|
|
7342
|
-
DeviceRole["PrivacyMask"] = "privacy-mask";
|
|
7343
|
-
DeviceRole["Doorbell"] = "doorbell";
|
|
7344
|
-
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
7345
|
-
* real Switch device for UI rendering / export adapters. */
|
|
7346
|
-
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
7347
|
-
/** Generic motion / occupancy / moving event source. Distinct from
|
|
7348
|
-
* the camera accessory PirSensor role: that one is a camera child;
|
|
7349
|
-
* this is a standalone HA / 3rd-party motion sensor. */
|
|
7350
|
-
DeviceRole["MotionSensor"] = "motion-sensor";
|
|
7351
|
-
DeviceRole["ContactSensor"] = "contact-sensor";
|
|
7352
|
-
DeviceRole["LeakSensor"] = "leak-sensor";
|
|
7353
|
-
DeviceRole["SmokeSensor"] = "smoke-sensor";
|
|
7354
|
-
DeviceRole["COSensor"] = "co-sensor";
|
|
7355
|
-
DeviceRole["GasSensor"] = "gas-sensor";
|
|
7356
|
-
DeviceRole["TamperSensor"] = "tamper-sensor";
|
|
7357
|
-
DeviceRole["VibrationSensor"] = "vibration-sensor";
|
|
7358
|
-
DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
|
|
7359
|
-
DeviceRole["SoundSensor"] = "sound-sensor";
|
|
7360
|
-
/** Fallback for `binary_sensor` without a known `device_class`. */
|
|
7361
|
-
DeviceRole["BinarySensor"] = "binary-sensor";
|
|
7362
|
-
DeviceRole["TemperatureSensor"] = "temperature-sensor";
|
|
7363
|
-
DeviceRole["HumiditySensor"] = "humidity-sensor";
|
|
7364
|
-
DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
|
|
7365
|
-
DeviceRole["PressureSensor"] = "pressure-sensor";
|
|
7366
|
-
/** Wind speed or direction (weather-station `wind-sensor` cap). */
|
|
7367
|
-
DeviceRole["WindSensor"] = "wind-sensor";
|
|
7368
|
-
/** Rain accumulation or rate (weather-station `rain-sensor` cap). */
|
|
7369
|
-
DeviceRole["RainSensor"] = "rain-sensor";
|
|
7370
|
-
/** UV index (weather-station `uv-sensor` cap). */
|
|
7371
|
-
DeviceRole["UvSensor"] = "uv-sensor";
|
|
7372
|
-
/** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
|
|
7373
|
-
* Distinct from AmbientLightSensor (lux). */
|
|
7374
|
-
DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
|
|
7375
|
-
/** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
|
|
7376
|
-
DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
|
|
7377
|
-
DeviceRole["PowerSensor"] = "power-sensor";
|
|
7378
|
-
DeviceRole["EnergySensor"] = "energy-sensor";
|
|
7379
|
-
DeviceRole["VoltageSensor"] = "voltage-sensor";
|
|
7380
|
-
DeviceRole["CurrentSensor"] = "current-sensor";
|
|
7381
|
-
DeviceRole["AirQualitySensor"] = "air-quality-sensor";
|
|
7382
|
-
/** Battery level (numeric % via `sensor` OR low-bool via
|
|
7383
|
-
* `binary_sensor` — the cap distinguishes via the value type). */
|
|
7384
|
-
DeviceRole["BatterySensor"] = "battery-sensor";
|
|
7385
|
-
/** Fallback for `sensor` numeric without a known `device_class`. */
|
|
7386
|
-
DeviceRole["NumericSensor"] = "numeric-sensor";
|
|
7387
|
-
/** String / enum state (HA `sensor` with `state_class: enum` or
|
|
7388
|
-
* `attributes.options`). */
|
|
7389
|
-
DeviceRole["EnumSensor"] = "enum-sensor";
|
|
7390
|
-
/** Date / timestamp state (HA `sensor` with `device_class: timestamp`
|
|
7391
|
-
* or `date`). The slice carries the raw ISO string verbatim (hosted on
|
|
7392
|
-
* the `enum-sensor` cap); the UI renders it locale-formatted. */
|
|
7393
|
-
DeviceRole["DateTimeSensor"] = "datetime-sensor";
|
|
7394
|
-
/** Last-resort fallback when nothing else matches. */
|
|
7395
|
-
DeviceRole["GenericSensor"] = "generic-sensor";
|
|
7396
|
-
DeviceRole["NumericControl"] = "numeric-control";
|
|
7397
|
-
DeviceRole["SelectControl"] = "select-control";
|
|
7398
|
-
DeviceRole["TextControl"] = "text-control";
|
|
7399
|
-
DeviceRole["DateTimeControl"] = "datetime-control";
|
|
7400
|
-
/** Mobile push notifier (HA `notify.mobile_app_*`) — supports
|
|
7401
|
-
* rich features (image, priority, channel routing). */
|
|
7402
|
-
DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
|
|
7403
|
-
/** Chat / messaging service (HA `notify.telegram_*`,
|
|
7404
|
-
* `notify.discord_*`, etc.). */
|
|
7405
|
-
DeviceRole["MessagingNotifier"] = "messaging-notifier";
|
|
7406
|
-
/** Email-based delivery (HA `notify.smtp`, etc.). */
|
|
7407
|
-
DeviceRole["EmailNotifier"] = "email-notifier";
|
|
7408
|
-
/** Fallback when the notifier service name doesn't match a known
|
|
7409
|
-
* pattern. */
|
|
7410
|
-
DeviceRole["GenericNotifier"] = "generic-notifier";
|
|
7411
|
-
return DeviceRole;
|
|
7412
|
-
}({});
|
|
7413
7105
|
/**
|
|
7414
7106
|
* Identity — preserves literal types for downstream inference.
|
|
7415
7107
|
*
|
|
@@ -7634,7 +7326,337 @@ function sleep(ms) {
|
|
|
7634
7326
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7635
7327
|
}
|
|
7636
7328
|
//#endregion
|
|
7637
|
-
//#region ../types/dist/
|
|
7329
|
+
//#region ../types/dist/err-msg-DX6i_MY4.mjs
|
|
7330
|
+
var DeviceType = /* @__PURE__ */ function(DeviceType) {
|
|
7331
|
+
DeviceType["Camera"] = "camera";
|
|
7332
|
+
DeviceType["Hub"] = "hub";
|
|
7333
|
+
DeviceType["Light"] = "light";
|
|
7334
|
+
DeviceType["Siren"] = "siren";
|
|
7335
|
+
DeviceType["Switch"] = "switch";
|
|
7336
|
+
DeviceType["Sensor"] = "sensor";
|
|
7337
|
+
DeviceType["Thermostat"] = "thermostat";
|
|
7338
|
+
/** Air-conditioner / heat-pump climate device (HVAC) — shares the
|
|
7339
|
+
* `climate-control` cap surface with `Thermostat` but renders a
|
|
7340
|
+
* dedicated AC-appropriate control UI (mode chips, fan speed,
|
|
7341
|
+
* independent vertical/horizontal swing). Sources: native Gree, and
|
|
7342
|
+
* reusable by other AC integrations. */
|
|
7343
|
+
DeviceType["Climate"] = "climate";
|
|
7344
|
+
DeviceType["Button"] = "button";
|
|
7345
|
+
/** Generic stateless event emitter — carries a device's EXACT declared
|
|
7346
|
+
* event vocabulary verbatim (no normalization). Installed with the
|
|
7347
|
+
* `event-emitter` cap. Sources: HA `event.*` entities (structured) and
|
|
7348
|
+
* HA bus events (e.g. `zha_event`, generic). */
|
|
7349
|
+
DeviceType["EventEmitter"] = "event-emitter";
|
|
7350
|
+
/** Firmware/software update entity — current vs available version,
|
|
7351
|
+
* updatable flag, update state, and an install action. Installed with
|
|
7352
|
+
* the `update` cap. Sources: Homematic firmware-update channels (and
|
|
7353
|
+
* reusable by other providers, e.g. HA `update.*` entities). */
|
|
7354
|
+
DeviceType["Update"] = "update";
|
|
7355
|
+
DeviceType["Generic"] = "generic";
|
|
7356
|
+
/** Generic notification delivery target (HA `notify.<service>`, future
|
|
7357
|
+
* Telegram / Discord / ntfy / SMTP, …). One device per delivery
|
|
7358
|
+
* endpoint; the `notifier` cap defines the send surface. */
|
|
7359
|
+
DeviceType["Notifier"] = "notifier";
|
|
7360
|
+
/** Pre-recorded action sequence with optional parameters
|
|
7361
|
+
* (HA `script.*`). Runnable via `script-runner` cap. */
|
|
7362
|
+
DeviceType["Script"] = "script";
|
|
7363
|
+
/** Automation rule (HA `automation.*`) — enable/disable + manual
|
|
7364
|
+
* trigger surface exposed via `automation-control` cap. */
|
|
7365
|
+
DeviceType["Automation"] = "automation";
|
|
7366
|
+
/** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
|
|
7367
|
+
DeviceType["Lock"] = "lock";
|
|
7368
|
+
/** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
|
|
7369
|
+
* `valve.*`). `cover` cap with sub-roles for variant. */
|
|
7370
|
+
DeviceType["Cover"] = "cover";
|
|
7371
|
+
/** Pipe / water / gas valve with open/close/stop and optional
|
|
7372
|
+
* position (HA `valve.*`). `valve` cap — a cover-sibling actuator
|
|
7373
|
+
* modelled on the same open/closed lifecycle. */
|
|
7374
|
+
DeviceType["Valve"] = "valve";
|
|
7375
|
+
/** Humidifier / dehumidifier with on/off + target humidity + mode
|
|
7376
|
+
* (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
|
|
7377
|
+
* modelled on the same target / mode lifecycle. */
|
|
7378
|
+
DeviceType["Humidifier"] = "humidifier";
|
|
7379
|
+
/** Water heater / boiler with target temperature + operation mode +
|
|
7380
|
+
* away mode (HA `water_heater.*`). `water-heater` cap — a
|
|
7381
|
+
* climate-family actuator. */
|
|
7382
|
+
DeviceType["WaterHeater"] = "water-heater";
|
|
7383
|
+
/** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
|
|
7384
|
+
DeviceType["Fan"] = "fan";
|
|
7385
|
+
/** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
|
|
7386
|
+
* the camera surface — those use `Camera`. `media-player` cap. */
|
|
7387
|
+
DeviceType["MediaPlayer"] = "media-player";
|
|
7388
|
+
/** Security panel / alarm system (HA `alarm_control_panel.*`).
|
|
7389
|
+
* `alarm-panel` cap. */
|
|
7390
|
+
DeviceType["AlarmPanel"] = "alarm-panel";
|
|
7391
|
+
/** Generic user-settable input (HA `number` / `input_number` / `select`
|
|
7392
|
+
* / `input_select` / `text` / `input_text` / `input_datetime`).
|
|
7393
|
+
* Sub-type via `DeviceRole`: NumericControl / SelectControl /
|
|
7394
|
+
* TextControl / DateTimeControl. */
|
|
7395
|
+
DeviceType["Control"] = "control";
|
|
7396
|
+
/** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
|
|
7397
|
+
* `presence` cap. */
|
|
7398
|
+
DeviceType["Presence"] = "presence";
|
|
7399
|
+
/** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
|
|
7400
|
+
* `weather` cap. */
|
|
7401
|
+
DeviceType["Weather"] = "weather";
|
|
7402
|
+
/** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
|
|
7403
|
+
DeviceType["Vacuum"] = "vacuum";
|
|
7404
|
+
/** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
|
|
7405
|
+
* `lawn-mower-control` cap. */
|
|
7406
|
+
DeviceType["LawnMower"] = "lawn-mower";
|
|
7407
|
+
/** Physical HA device group — parent container for entity-children
|
|
7408
|
+
* adopted from a single HA device entry. Not renderable as a
|
|
7409
|
+
* standalone device; exists only to anchor child entities. */
|
|
7410
|
+
DeviceType["Container"] = "container";
|
|
7411
|
+
/** Single still-image entity (HA `image.*`). Read-only display of an
|
|
7412
|
+
* `entity_picture` signed URL the browser loads directly. `image` cap. */
|
|
7413
|
+
DeviceType["Image"] = "image";
|
|
7414
|
+
/** Smart pet feeder — cloud-connected food dispenser with a bowl food
|
|
7415
|
+
* level, battery, desiccant life, feeding state and manual-feed /
|
|
7416
|
+
* call-pet / maintenance actions. Installed with the `pet-feeder` cap;
|
|
7417
|
+
* dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
|
|
7418
|
+
* native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
|
|
7419
|
+
* integrations sharing the same food/desiccant/hopper surface. */
|
|
7420
|
+
DeviceType["PetFeeder"] = "pet-feeder";
|
|
7421
|
+
return DeviceType;
|
|
7422
|
+
}({});
|
|
7423
|
+
var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
7424
|
+
DeviceFeature["BatteryOperated"] = "battery-operated";
|
|
7425
|
+
DeviceFeature["Rebootable"] = "rebootable";
|
|
7426
|
+
/**
|
|
7427
|
+
* Device supports an on-demand re-sync of its derived spec with its
|
|
7428
|
+
* upstream source — drives the generic Re-sync button. The owning
|
|
7429
|
+
* provider implements the action via the `device-adoption.resync` cap.
|
|
7430
|
+
*/
|
|
7431
|
+
DeviceFeature["Resyncable"] = "resyncable";
|
|
7432
|
+
DeviceFeature["NativeSnapshot"] = "native-snapshot";
|
|
7433
|
+
DeviceFeature["DoorbellButton"] = "doorbell-button";
|
|
7434
|
+
DeviceFeature["TwoWayAudio"] = "two-way-audio";
|
|
7435
|
+
DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
|
|
7436
|
+
/**
|
|
7437
|
+
* Camera supports the on-firmware autotrack subsystem (subject-
|
|
7438
|
+
* following). Distinct from `PanTiltZoom` because not every PTZ
|
|
7439
|
+
* camera ships autotrack — the admin UI uses this flag to gate
|
|
7440
|
+
* the autotrack toggle / settings card without re-deriving from
|
|
7441
|
+
* the cap registry. Mirrors `ptz-autotrack` cap registration:
|
|
7442
|
+
* driver sets this feature when probe confirms the firmware
|
|
7443
|
+
* surface, and registers the cap in the same code path.
|
|
7444
|
+
*/
|
|
7445
|
+
DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
|
|
7446
|
+
/**
|
|
7447
|
+
* Accessory exposes a "trigger on motion" toggle — the parent camera's
|
|
7448
|
+
* motion detection automatically activates this device. Mirrors
|
|
7449
|
+
* `motion-trigger` cap registration: drivers set this feature in the
|
|
7450
|
+
* same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
|
|
7451
|
+
*
|
|
7452
|
+
* Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
|
|
7453
|
+
* fast scalar without binding fetch), notifier rules, and `listAll`
|
|
7454
|
+
* filters that want "all devices with on-motion behaviour".
|
|
7455
|
+
*/
|
|
7456
|
+
DeviceFeature["MotionTrigger"] = "motion-trigger";
|
|
7457
|
+
/** Light supports rgb-triplet color via `color` cap. */
|
|
7458
|
+
DeviceFeature["LightColorRgb"] = "light-color-rgb";
|
|
7459
|
+
/** Light supports HSV color via `color` cap. */
|
|
7460
|
+
DeviceFeature["LightColorHsv"] = "light-color-hsv";
|
|
7461
|
+
/** Light supports color-temperature (mired) via `color` cap. */
|
|
7462
|
+
DeviceFeature["LightColorMired"] = "light-color-mired";
|
|
7463
|
+
/** Thermostat supports a `heat_cool` dual setpoint (targetLow +
|
|
7464
|
+
* targetHigh). Gates the range slider UI. */
|
|
7465
|
+
DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
|
|
7466
|
+
/** Thermostat exposes target humidity and/or current humidity
|
|
7467
|
+
* readings. Gates the humidity controls. */
|
|
7468
|
+
DeviceFeature["ClimateHumidity"] = "climate-humidity";
|
|
7469
|
+
/** Thermostat exposes a fan-mode selector. */
|
|
7470
|
+
DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
|
|
7471
|
+
/** Thermostat exposes preset modes (eco / away / sleep / vendor). */
|
|
7472
|
+
DeviceFeature["ClimatePreset"] = "climate-preset";
|
|
7473
|
+
/** Thermostat exposes a vertical louver swing toggle. Gates the
|
|
7474
|
+
* vertical-swing switch in the climate UI. Independent of horizontal. */
|
|
7475
|
+
DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
|
|
7476
|
+
/** Thermostat exposes a horizontal louver swing toggle. Gates the
|
|
7477
|
+
* horizontal-swing switch in the climate UI. Independent of vertical. */
|
|
7478
|
+
DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
|
|
7479
|
+
/** Cover exposes intermediate position control (0..100). Gates the
|
|
7480
|
+
* position slider UI. */
|
|
7481
|
+
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
7482
|
+
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
7483
|
+
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
7484
|
+
/** Valve exposes intermediate position control (0..100). Gates the
|
|
7485
|
+
* position slider / drag surface UI. */
|
|
7486
|
+
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
7487
|
+
/** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
|
|
7488
|
+
DeviceFeature["FanSpeed"] = "fan-speed";
|
|
7489
|
+
/** Fan exposes a preset mode selector. */
|
|
7490
|
+
DeviceFeature["FanPreset"] = "fan-preset";
|
|
7491
|
+
/** Fan exposes blade direction (forward/reverse) — typical of
|
|
7492
|
+
* ceiling fans. */
|
|
7493
|
+
DeviceFeature["FanDirection"] = "fan-direction";
|
|
7494
|
+
/** Fan exposes an oscillation toggle. */
|
|
7495
|
+
DeviceFeature["FanOscillating"] = "fan-oscillating";
|
|
7496
|
+
/** Lock requires a PIN code on lock/unlock. Gates the code-entry
|
|
7497
|
+
* field on the UI lock-controls panel. */
|
|
7498
|
+
DeviceFeature["LockPinRequired"] = "lock-pin-required";
|
|
7499
|
+
/** Lock supports a latch-release ("open door") action distinct from
|
|
7500
|
+
* unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
|
|
7501
|
+
* `supported_features`. Gates the Open Door button in the UI. */
|
|
7502
|
+
DeviceFeature["LockOpen"] = "lock-open";
|
|
7503
|
+
/** Media player exposes a seek-to-position surface. */
|
|
7504
|
+
DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
|
|
7505
|
+
/** Media player exposes a volume-level setter. */
|
|
7506
|
+
DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
|
|
7507
|
+
/** Media player exposes a mute toggle distinct from volume=0. */
|
|
7508
|
+
DeviceFeature["MediaPlayerMute"] = "media-player-mute";
|
|
7509
|
+
/** Media player exposes a shuffle toggle. */
|
|
7510
|
+
DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
|
|
7511
|
+
/** Media player exposes a repeat mode (off / all / one). */
|
|
7512
|
+
DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
|
|
7513
|
+
/** Media player exposes a source / input selector. */
|
|
7514
|
+
DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
|
|
7515
|
+
/** Media player exposes a play-arbitrary-media surface (URL / id). */
|
|
7516
|
+
DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
|
|
7517
|
+
/** Media player exposes next-track. */
|
|
7518
|
+
DeviceFeature["MediaPlayerNext"] = "media-player-next";
|
|
7519
|
+
/** Media player exposes previous-track. */
|
|
7520
|
+
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
7521
|
+
/** Media player exposes stop distinct from pause. */
|
|
7522
|
+
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
7523
|
+
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
7524
|
+
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
7525
|
+
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
7526
|
+
* addition to a textual location. */
|
|
7527
|
+
DeviceFeature["PresenceGps"] = "presence-gps";
|
|
7528
|
+
/** Notifier accepts an inline / URL image attachment. */
|
|
7529
|
+
DeviceFeature["NotifierImage"] = "notifier-image";
|
|
7530
|
+
/** Notifier accepts a priority hint (high/normal/low). */
|
|
7531
|
+
DeviceFeature["NotifierPriority"] = "notifier-priority";
|
|
7532
|
+
/** Notifier accepts a free-form `data` payload for platform-specific
|
|
7533
|
+
* fields. */
|
|
7534
|
+
DeviceFeature["NotifierData"] = "notifier-data";
|
|
7535
|
+
/** Notifier supports interactive action buttons / callbacks. */
|
|
7536
|
+
DeviceFeature["NotifierActions"] = "notifier-actions";
|
|
7537
|
+
/** Notifier supports per-call recipient targeting (multi-user). */
|
|
7538
|
+
DeviceFeature["NotifierRecipients"] = "notifier-recipients";
|
|
7539
|
+
/** Script runner accepts a variables map on each run invocation. */
|
|
7540
|
+
DeviceFeature["ScriptVariables"] = "script-variables";
|
|
7541
|
+
/** Automation `trigger` accepts a skipCondition flag — fires the
|
|
7542
|
+
* automation's actions while bypassing its condition block. */
|
|
7543
|
+
DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
|
|
7544
|
+
/** Robot vacuum exposes a live cleaning map (image child). Gates the
|
|
7545
|
+
* map tile in the vacuum UI. */
|
|
7546
|
+
DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
|
|
7547
|
+
/** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
|
|
7548
|
+
* switches group. */
|
|
7549
|
+
DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
|
|
7550
|
+
/** Robot mower exposes a live mowing map (SVG image child). Gates the
|
|
7551
|
+
* mower map tile. */
|
|
7552
|
+
DeviceFeature["MowerHasMap"] = "mower-has-map";
|
|
7553
|
+
/** Robot mower exposes targeted mowing (all-area / zones / edges /
|
|
7554
|
+
* spots) — gates the Mow action + map selectors. */
|
|
7555
|
+
DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
|
|
7556
|
+
return DeviceFeature;
|
|
7557
|
+
}({});
|
|
7558
|
+
/**
|
|
7559
|
+
* Semantic role a device plays within its parent. Populated by driver
|
|
7560
|
+
* addons when creating accessory devices (Reolink siren/floodlight/
|
|
7561
|
+
* PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
|
|
7562
|
+
* admin UI to pick icons, labels, and widgets — a `Switch` with
|
|
7563
|
+
* `role: Floodlight` renders as a bulb with a brightness slider,
|
|
7564
|
+
* whereas a `Switch` with `role: Siren` renders as a klaxon.
|
|
7565
|
+
*
|
|
7566
|
+
* Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
|
|
7567
|
+
* sqlite as a nullable TEXT column — old rows keep working unchanged.
|
|
7568
|
+
*/
|
|
7569
|
+
var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
7570
|
+
DeviceRole["Siren"] = "siren";
|
|
7571
|
+
DeviceRole["Floodlight"] = "floodlight";
|
|
7572
|
+
DeviceRole["Spotlight"] = "spotlight";
|
|
7573
|
+
DeviceRole["PirSensor"] = "pir-sensor";
|
|
7574
|
+
DeviceRole["Chime"] = "chime";
|
|
7575
|
+
DeviceRole["Autotrack"] = "autotrack";
|
|
7576
|
+
DeviceRole["Nightvision"] = "nightvision";
|
|
7577
|
+
DeviceRole["PrivacyMask"] = "privacy-mask";
|
|
7578
|
+
DeviceRole["Doorbell"] = "doorbell";
|
|
7579
|
+
/** Virtual HA toggle (input_boolean.*) — distinguishable from a
|
|
7580
|
+
* real Switch device for UI rendering / export adapters. */
|
|
7581
|
+
DeviceRole["BinaryHelper"] = "binary-helper";
|
|
7582
|
+
/** Generic motion / occupancy / moving event source. Distinct from
|
|
7583
|
+
* the camera accessory PirSensor role: that one is a camera child;
|
|
7584
|
+
* this is a standalone HA / 3rd-party motion sensor. */
|
|
7585
|
+
DeviceRole["MotionSensor"] = "motion-sensor";
|
|
7586
|
+
DeviceRole["ContactSensor"] = "contact-sensor";
|
|
7587
|
+
DeviceRole["LeakSensor"] = "leak-sensor";
|
|
7588
|
+
DeviceRole["SmokeSensor"] = "smoke-sensor";
|
|
7589
|
+
DeviceRole["COSensor"] = "co-sensor";
|
|
7590
|
+
DeviceRole["GasSensor"] = "gas-sensor";
|
|
7591
|
+
DeviceRole["TamperSensor"] = "tamper-sensor";
|
|
7592
|
+
DeviceRole["VibrationSensor"] = "vibration-sensor";
|
|
7593
|
+
DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
|
|
7594
|
+
DeviceRole["SoundSensor"] = "sound-sensor";
|
|
7595
|
+
/** Fallback for `binary_sensor` without a known `device_class`. */
|
|
7596
|
+
DeviceRole["BinarySensor"] = "binary-sensor";
|
|
7597
|
+
DeviceRole["TemperatureSensor"] = "temperature-sensor";
|
|
7598
|
+
DeviceRole["HumiditySensor"] = "humidity-sensor";
|
|
7599
|
+
DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
|
|
7600
|
+
DeviceRole["PressureSensor"] = "pressure-sensor";
|
|
7601
|
+
/** Wind speed or direction (weather-station `wind-sensor` cap). */
|
|
7602
|
+
DeviceRole["WindSensor"] = "wind-sensor";
|
|
7603
|
+
/** Rain accumulation or rate (weather-station `rain-sensor` cap). */
|
|
7604
|
+
DeviceRole["RainSensor"] = "rain-sensor";
|
|
7605
|
+
/** UV index (weather-station `uv-sensor` cap). */
|
|
7606
|
+
DeviceRole["UvSensor"] = "uv-sensor";
|
|
7607
|
+
/** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
|
|
7608
|
+
* Distinct from AmbientLightSensor (lux). */
|
|
7609
|
+
DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
|
|
7610
|
+
/** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
|
|
7611
|
+
DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
|
|
7612
|
+
DeviceRole["PowerSensor"] = "power-sensor";
|
|
7613
|
+
DeviceRole["EnergySensor"] = "energy-sensor";
|
|
7614
|
+
DeviceRole["VoltageSensor"] = "voltage-sensor";
|
|
7615
|
+
DeviceRole["CurrentSensor"] = "current-sensor";
|
|
7616
|
+
DeviceRole["AirQualitySensor"] = "air-quality-sensor";
|
|
7617
|
+
/** Battery level (numeric % via `sensor` OR low-bool via
|
|
7618
|
+
* `binary_sensor` — the cap distinguishes via the value type). */
|
|
7619
|
+
DeviceRole["BatterySensor"] = "battery-sensor";
|
|
7620
|
+
/** Fallback for `sensor` numeric without a known `device_class`. */
|
|
7621
|
+
DeviceRole["NumericSensor"] = "numeric-sensor";
|
|
7622
|
+
/** String / enum state (HA `sensor` with `state_class: enum` or
|
|
7623
|
+
* `attributes.options`). */
|
|
7624
|
+
DeviceRole["EnumSensor"] = "enum-sensor";
|
|
7625
|
+
/** Date / timestamp state (HA `sensor` with `device_class: timestamp`
|
|
7626
|
+
* or `date`). The slice carries the raw ISO string verbatim (hosted on
|
|
7627
|
+
* the `enum-sensor` cap); the UI renders it locale-formatted. */
|
|
7628
|
+
DeviceRole["DateTimeSensor"] = "datetime-sensor";
|
|
7629
|
+
/** Last-resort fallback when nothing else matches. */
|
|
7630
|
+
DeviceRole["GenericSensor"] = "generic-sensor";
|
|
7631
|
+
DeviceRole["NumericControl"] = "numeric-control";
|
|
7632
|
+
DeviceRole["SelectControl"] = "select-control";
|
|
7633
|
+
DeviceRole["TextControl"] = "text-control";
|
|
7634
|
+
DeviceRole["DateTimeControl"] = "datetime-control";
|
|
7635
|
+
/** Mobile push notifier (HA `notify.mobile_app_*`) — supports
|
|
7636
|
+
* rich features (image, priority, channel routing). */
|
|
7637
|
+
DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
|
|
7638
|
+
/** Chat / messaging service (HA `notify.telegram_*`,
|
|
7639
|
+
* `notify.discord_*`, etc.). */
|
|
7640
|
+
DeviceRole["MessagingNotifier"] = "messaging-notifier";
|
|
7641
|
+
/** Email-based delivery (HA `notify.smtp`, etc.). */
|
|
7642
|
+
DeviceRole["EmailNotifier"] = "email-notifier";
|
|
7643
|
+
/** Fallback when the notifier service name doesn't match a known
|
|
7644
|
+
* pattern. */
|
|
7645
|
+
DeviceRole["GenericNotifier"] = "generic-notifier";
|
|
7646
|
+
return DeviceRole;
|
|
7647
|
+
}({});
|
|
7648
|
+
/**
|
|
7649
|
+
import { errMsg } from '@camstack/types'
|
|
7650
|
+
* Extract a human-readable message from an unknown error value.
|
|
7651
|
+
* Replaces the ubiquitous `errMsg(err)` pattern.
|
|
7652
|
+
*/
|
|
7653
|
+
function errMsg(err) {
|
|
7654
|
+
if (err instanceof Error) return err.message;
|
|
7655
|
+
if (typeof err === "string") return err;
|
|
7656
|
+
return String(err);
|
|
7657
|
+
}
|
|
7658
|
+
//#endregion
|
|
7659
|
+
//#region ../types/dist/composition-CPFIlFfw.mjs
|
|
7638
7660
|
/**
|
|
7639
7661
|
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7640
7662
|
* canonical form sorts object keys alphabetically at every depth so two
|
|
@@ -7666,162 +7688,899 @@ function replaceWithSortedKeys(_key, value) {
|
|
|
7666
7688
|
}
|
|
7667
7689
|
return value;
|
|
7668
7690
|
}
|
|
7669
|
-
//#endregion
|
|
7670
|
-
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7671
7691
|
/**
|
|
7672
|
-
|
|
7673
|
-
*
|
|
7674
|
-
*
|
|
7692
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
7693
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
7694
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
7675
7695
|
*/
|
|
7676
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7696
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
7697
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
7698
|
+
var ExpressionParseError = class extends Error {
|
|
7699
|
+
position;
|
|
7700
|
+
constructor(message, position) {
|
|
7701
|
+
super(message);
|
|
7702
|
+
this.name = "ExpressionParseError";
|
|
7703
|
+
this.position = position;
|
|
7704
|
+
}
|
|
7705
|
+
};
|
|
7706
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
7707
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
7708
|
+
var ExpressionEvalError = class extends Error {
|
|
7709
|
+
constructor(message) {
|
|
7710
|
+
super(message);
|
|
7711
|
+
this.name = "ExpressionEvalError";
|
|
7712
|
+
}
|
|
7713
|
+
};
|
|
7714
|
+
function asFiniteNumber(value, name, index) {
|
|
7715
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
7716
|
+
return value;
|
|
7680
7717
|
}
|
|
7681
|
-
|
|
7682
|
-
"
|
|
7683
|
-
|
|
7684
|
-
"face",
|
|
7685
|
-
"identity",
|
|
7686
|
-
"plate",
|
|
7687
|
-
"vehicle",
|
|
7688
|
-
"scene",
|
|
7689
|
-
"motion",
|
|
7690
|
-
"object",
|
|
7691
|
-
"audio",
|
|
7692
|
-
"mosaic"
|
|
7693
|
-
]);
|
|
7694
|
-
/**
|
|
7695
|
-
* Is this string a declared media owner type?
|
|
7696
|
-
*
|
|
7697
|
-
* Exported because the `event-media` plane's path parser needs it (D482): a
|
|
7698
|
-
* path segment is an arbitrary string until something narrows it, and a
|
|
7699
|
-
* hand-written list at the door would drift from {@link MEDIA_OWNER_TYPES} the
|
|
7700
|
-
* day an owner kind is added — the failure this module's own docblock already
|
|
7701
|
-
* records for `vehicle` and `scene`.
|
|
7702
|
-
*/
|
|
7703
|
-
function isMediaOwnerType(value) {
|
|
7704
|
-
return OWNER_TYPES.has(value);
|
|
7718
|
+
function asString(value, name, index) {
|
|
7719
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
7720
|
+
return value;
|
|
7705
7721
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
* does not re-spell the list and drift from it.
|
|
7710
|
-
*/
|
|
7711
|
-
var EVENT_OWNER_TYPES = [
|
|
7712
|
-
"motion",
|
|
7713
|
-
"object",
|
|
7714
|
-
"audio"
|
|
7715
|
-
];
|
|
7716
|
-
/**
|
|
7717
|
-
* The same list as a Zod enum, for the cap inputs that must NAME the table
|
|
7718
|
-
* (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
|
|
7719
|
-
* `(table, id)`).
|
|
7720
|
-
*
|
|
7721
|
-
* Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
|
|
7722
|
-
* table would otherwise be accepted by the codec and refused at the door, with
|
|
7723
|
-
* nothing failing until a caller asked.
|
|
7724
|
-
*/
|
|
7725
|
-
var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
|
|
7726
|
-
/** The same list as a Zod enum, for the cap input that carries it. */
|
|
7727
|
-
var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
|
|
7728
|
-
var EVENT_OWNER_TYPE_SET = new Set(EVENT_OWNER_TYPES);
|
|
7729
|
-
/** True when this key's owner is one of the three event tables. */
|
|
7730
|
-
function isEventOwnerType(ownerType) {
|
|
7731
|
-
return EVENT_OWNER_TYPE_SET.has(ownerType);
|
|
7722
|
+
function finiteResult(value, name) {
|
|
7723
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
7724
|
+
return value;
|
|
7732
7725
|
}
|
|
7733
|
-
|
|
7734
|
-
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
"main",
|
|
7743
|
-
"high"
|
|
7744
|
-
]).optional(),
|
|
7745
|
-
/**
|
|
7746
|
-
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
7747
|
-
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
7748
|
-
* it, or it ships a stream that does not match its own advertisement — the
|
|
7749
|
-
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
7750
|
-
* silently. Optional because a browser negotiates the level itself.
|
|
7751
|
-
*/
|
|
7752
|
-
level: string().optional(),
|
|
7753
|
-
width: number().int().positive().optional(),
|
|
7754
|
-
height: number().int().positive().optional(),
|
|
7755
|
-
fps: number().positive().optional(),
|
|
7756
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7757
|
-
gopFrames: number().int().positive().optional(),
|
|
7758
|
-
bf: number().int().min(0).optional(),
|
|
7759
|
-
preset: _enum([
|
|
7760
|
-
"ultrafast",
|
|
7761
|
-
"superfast",
|
|
7762
|
-
"veryfast",
|
|
7763
|
-
"faster",
|
|
7764
|
-
"fast",
|
|
7765
|
-
"medium"
|
|
7766
|
-
]).optional(),
|
|
7767
|
-
tune: _enum([
|
|
7768
|
-
"zerolatency",
|
|
7769
|
-
"film",
|
|
7770
|
-
"animation"
|
|
7771
|
-
]).optional(),
|
|
7772
|
-
/**
|
|
7773
|
-
* ONE slice per access unit.
|
|
7774
|
-
*
|
|
7775
|
-
* `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
|
|
7776
|
-
* the encoder as five NAL slices that share one RTP timestamp and carry one
|
|
7777
|
-
* marker bit. A libwebrtc depacketiser sees five frame-starts and one
|
|
7778
|
-
* frame-end per frame: the first pictures render and the video then freezes
|
|
7779
|
-
* for good while the audio, on its own plane, plays on. Measured on this hub
|
|
7780
|
-
* 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
|
|
7781
|
-
*
|
|
7782
|
-
* A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
|
|
7783
|
-
* `outputArgs` on purpose: an opaque array is part of the sharing key, so two
|
|
7784
|
-
* consumers meaning the same thing spelled differently would stop sharing one
|
|
7785
|
-
* child. Absent means "whatever the encoder does" — today's behaviour.
|
|
7786
|
-
*/
|
|
7787
|
-
singleSlicePerFrame: boolean().optional()
|
|
7788
|
-
}),
|
|
7789
|
-
audio: union([literal("passthrough"), object({
|
|
7790
|
-
codec: _enum([
|
|
7791
|
-
"opus",
|
|
7792
|
-
"aac",
|
|
7793
|
-
"pcmu",
|
|
7794
|
-
"pcma",
|
|
7795
|
-
"copy"
|
|
7796
|
-
]),
|
|
7797
|
-
bitrateKbps: number().int().positive().optional(),
|
|
7798
|
-
sampleRateHz: number().int().positive().optional(),
|
|
7799
|
-
channels: union([literal(1), literal(2)]).optional()
|
|
7800
|
-
})]),
|
|
7801
|
-
/**
|
|
7802
|
-
* ffmpeg input-side args, inserted between the fixed global flags
|
|
7803
|
-
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
7804
|
-
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
7805
|
-
* used demuxer/format options.
|
|
7806
|
-
*/
|
|
7807
|
-
inputArgs: array(string()).optional(),
|
|
7808
|
-
/**
|
|
7809
|
-
* ffmpeg output-side args, inserted between the encode block and
|
|
7810
|
-
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
7811
|
-
* filters, codec-specific overrides. Free-text array.
|
|
7812
|
-
*/
|
|
7813
|
-
outputArgs: array(string()).optional()
|
|
7814
|
-
});
|
|
7726
|
+
function allFiniteNumbers(args, name) {
|
|
7727
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
7728
|
+
}
|
|
7729
|
+
function asBoolean(value, name, index) {
|
|
7730
|
+
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
7731
|
+
return value;
|
|
7732
|
+
}
|
|
7733
|
+
/** A plain decimal (optional sign, fraction, exponent) — never hex, never `Infinity`, never a unit suffix. */
|
|
7734
|
+
var DECIMAL = /^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$/;
|
|
7815
7735
|
/**
|
|
7816
|
-
*
|
|
7817
|
-
*
|
|
7818
|
-
* (
|
|
7736
|
+
* `number(x)`: a finite number, or a string that IS one, as a number; every
|
|
7737
|
+
* other value — `''`, `'unavailable'`, a boolean, null — is `null`
|
|
7738
|
+
* (unavailable, never 0: D393). The one bridge from a string-valued source (an
|
|
7739
|
+
* HA sensor with a unit and no device_class is an `enum-sensor`) to a number.
|
|
7819
7740
|
*/
|
|
7820
|
-
|
|
7821
|
-
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7741
|
+
function toNumberOrNull(value) {
|
|
7742
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
7743
|
+
if (typeof value !== "string") return null;
|
|
7744
|
+
const trimmed = value.trim();
|
|
7745
|
+
if (!DECIMAL.test(trimmed)) return null;
|
|
7746
|
+
const parsed = Number(trimmed);
|
|
7747
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
7748
|
+
}
|
|
7749
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
7750
|
+
var table = {
|
|
7751
|
+
min: {
|
|
7752
|
+
minArgs: 1,
|
|
7753
|
+
maxArgs: INF,
|
|
7754
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
7755
|
+
},
|
|
7756
|
+
max: {
|
|
7757
|
+
minArgs: 1,
|
|
7758
|
+
maxArgs: INF,
|
|
7759
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
7760
|
+
},
|
|
7761
|
+
abs: {
|
|
7762
|
+
minArgs: 1,
|
|
7763
|
+
maxArgs: 1,
|
|
7764
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
7765
|
+
},
|
|
7766
|
+
floor: {
|
|
7767
|
+
minArgs: 1,
|
|
7768
|
+
maxArgs: 1,
|
|
7769
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
7770
|
+
},
|
|
7771
|
+
ceil: {
|
|
7772
|
+
minArgs: 1,
|
|
7773
|
+
maxArgs: 1,
|
|
7774
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
7775
|
+
},
|
|
7776
|
+
sqrt: {
|
|
7777
|
+
minArgs: 1,
|
|
7778
|
+
maxArgs: 1,
|
|
7779
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
7780
|
+
},
|
|
7781
|
+
round: {
|
|
7782
|
+
minArgs: 1,
|
|
7783
|
+
maxArgs: 2,
|
|
7784
|
+
apply: (args) => {
|
|
7785
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
7786
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
7787
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
7788
|
+
const factor = 10 ** digits;
|
|
7789
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
7790
|
+
}
|
|
7791
|
+
},
|
|
7792
|
+
pow: {
|
|
7793
|
+
minArgs: 2,
|
|
7794
|
+
maxArgs: 2,
|
|
7795
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
7796
|
+
},
|
|
7797
|
+
clamp: {
|
|
7798
|
+
minArgs: 3,
|
|
7799
|
+
maxArgs: 3,
|
|
7800
|
+
apply: (args) => {
|
|
7801
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
7802
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
7803
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
7804
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
7805
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
7806
|
+
}
|
|
7807
|
+
},
|
|
7808
|
+
avg: {
|
|
7809
|
+
minArgs: 1,
|
|
7810
|
+
maxArgs: INF,
|
|
7811
|
+
apply: (args) => {
|
|
7812
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
7813
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
7814
|
+
}
|
|
7815
|
+
},
|
|
7816
|
+
sum: {
|
|
7817
|
+
minArgs: 1,
|
|
7818
|
+
maxArgs: INF,
|
|
7819
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
7820
|
+
},
|
|
7821
|
+
coalesce: {
|
|
7822
|
+
minArgs: 1,
|
|
7823
|
+
maxArgs: INF,
|
|
7824
|
+
apply: (args) => {
|
|
7825
|
+
for (const a of args) if (a !== null) return a;
|
|
7826
|
+
return null;
|
|
7827
|
+
}
|
|
7828
|
+
},
|
|
7829
|
+
age: {
|
|
7830
|
+
minArgs: 2,
|
|
7831
|
+
maxArgs: 2,
|
|
7832
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
7833
|
+
},
|
|
7834
|
+
convert: {
|
|
7835
|
+
minArgs: 3,
|
|
7836
|
+
maxArgs: 3,
|
|
7837
|
+
apply: (args, hooks) => {
|
|
7838
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
7839
|
+
const from = asString(args[1], "convert", 1).trim();
|
|
7840
|
+
const to = asString(args[2], "convert", 2).trim();
|
|
7841
|
+
if (hooks.convert) {
|
|
7842
|
+
const out = hooks.convert(x, from, to);
|
|
7843
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
7844
|
+
return finiteResult(out, "convert");
|
|
7845
|
+
}
|
|
7846
|
+
if (from === to) return x;
|
|
7847
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
7848
|
+
}
|
|
7849
|
+
},
|
|
7850
|
+
any: {
|
|
7851
|
+
minArgs: 1,
|
|
7852
|
+
maxArgs: INF,
|
|
7853
|
+
apply: (args) => args.map((a, i) => asBoolean(a, "any", i)).some((b) => b)
|
|
7854
|
+
},
|
|
7855
|
+
all: {
|
|
7856
|
+
minArgs: 1,
|
|
7857
|
+
maxArgs: INF,
|
|
7858
|
+
apply: (args) => args.map((a, i) => asBoolean(a, "all", i)).every((b) => b)
|
|
7859
|
+
},
|
|
7860
|
+
within: {
|
|
7861
|
+
minArgs: 2,
|
|
7862
|
+
maxArgs: 2,
|
|
7863
|
+
apply: (args, hooks) => {
|
|
7864
|
+
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
7865
|
+
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
7866
|
+
const at = args[0];
|
|
7867
|
+
if (at === null) return false;
|
|
7868
|
+
const ts = asFiniteNumber(at, "within", 0);
|
|
7869
|
+
const now = hooks.now;
|
|
7870
|
+
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
7871
|
+
const inside = now - ts <= windowMs;
|
|
7872
|
+
if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
|
|
7873
|
+
return inside;
|
|
7874
|
+
}
|
|
7875
|
+
},
|
|
7876
|
+
number: {
|
|
7877
|
+
minArgs: 1,
|
|
7878
|
+
maxArgs: 1,
|
|
7879
|
+
apply: (args) => toNumberOrNull(args[0])
|
|
7880
|
+
},
|
|
7881
|
+
latest: {
|
|
7882
|
+
minArgs: 1,
|
|
7883
|
+
maxArgs: INF,
|
|
7884
|
+
apply: (args) => {
|
|
7885
|
+
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
|
|
7886
|
+
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7889
|
+
};
|
|
7890
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
7891
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
7892
|
+
* callees at parse time (immediate author feedback). */
|
|
7893
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
7894
|
+
/**
|
|
7895
|
+
* Resource-bound constants for the safe expression engine.
|
|
7896
|
+
*
|
|
7897
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
7898
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
7899
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
7900
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
7901
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
7902
|
+
*/
|
|
7903
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
7904
|
+
* rejected without allocation. */
|
|
7905
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
7906
|
+
/** A legal binding / identifier name. */
|
|
7907
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
7908
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
7909
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
7910
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
7911
|
+
"now",
|
|
7912
|
+
"true",
|
|
7913
|
+
"false",
|
|
7914
|
+
"null"
|
|
7915
|
+
]);
|
|
7916
|
+
/**
|
|
7917
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
7918
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
7919
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
7920
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
7921
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
7922
|
+
* is a parse error with a source position, so member access / assignment /
|
|
7923
|
+
* template literals are lexically impossible.
|
|
7924
|
+
*/
|
|
7925
|
+
var KEYWORDS = new Set([
|
|
7926
|
+
"true",
|
|
7927
|
+
"false",
|
|
7928
|
+
"null"
|
|
7929
|
+
]);
|
|
7930
|
+
function isDigit(ch) {
|
|
7931
|
+
return ch >= "0" && ch <= "9";
|
|
7932
|
+
}
|
|
7933
|
+
function isIdentStart(ch) {
|
|
7934
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
7935
|
+
}
|
|
7936
|
+
function isIdentPart(ch) {
|
|
7937
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
7938
|
+
}
|
|
7939
|
+
function isWhitespace(ch) {
|
|
7940
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
7941
|
+
}
|
|
7942
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
7943
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
7944
|
+
* string. */
|
|
7945
|
+
function tokenize(source) {
|
|
7946
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
7947
|
+
const tokens = [];
|
|
7948
|
+
let i = 0;
|
|
7949
|
+
const n = source.length;
|
|
7950
|
+
while (i < n) {
|
|
7951
|
+
const ch = source[i];
|
|
7952
|
+
if (isWhitespace(ch)) {
|
|
7953
|
+
i += 1;
|
|
7954
|
+
continue;
|
|
7955
|
+
}
|
|
7956
|
+
if (isDigit(ch)) {
|
|
7957
|
+
const start = i;
|
|
7958
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
7959
|
+
if (i < n && source[i] === ".") {
|
|
7960
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
7961
|
+
i += 1;
|
|
7962
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
7963
|
+
}
|
|
7964
|
+
const text = source.slice(start, i);
|
|
7965
|
+
const value = Number(text);
|
|
7966
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
7967
|
+
tokens.push({
|
|
7968
|
+
type: "number",
|
|
7969
|
+
value,
|
|
7970
|
+
pos: start
|
|
7971
|
+
});
|
|
7972
|
+
continue;
|
|
7973
|
+
}
|
|
7974
|
+
if (ch === "'" || ch === "\"") {
|
|
7975
|
+
const quote = ch;
|
|
7976
|
+
const start = i;
|
|
7977
|
+
i += 1;
|
|
7978
|
+
let out = "";
|
|
7979
|
+
let closed = false;
|
|
7980
|
+
while (i < n) {
|
|
7981
|
+
const c = source[i];
|
|
7982
|
+
if (c === "\\") {
|
|
7983
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
7984
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
7985
|
+
out += next;
|
|
7986
|
+
i += 2;
|
|
7987
|
+
continue;
|
|
7988
|
+
}
|
|
7989
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
7990
|
+
}
|
|
7991
|
+
if (c === quote) {
|
|
7992
|
+
closed = true;
|
|
7993
|
+
i += 1;
|
|
7994
|
+
break;
|
|
7995
|
+
}
|
|
7996
|
+
out += c;
|
|
7997
|
+
i += 1;
|
|
7998
|
+
}
|
|
7999
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
8000
|
+
tokens.push({
|
|
8001
|
+
type: "string",
|
|
8002
|
+
value: out,
|
|
8003
|
+
pos: start
|
|
8004
|
+
});
|
|
8005
|
+
continue;
|
|
8006
|
+
}
|
|
8007
|
+
if (isIdentStart(ch)) {
|
|
8008
|
+
const start = i;
|
|
8009
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
8010
|
+
const text = source.slice(start, i);
|
|
8011
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
8012
|
+
type: "keyword",
|
|
8013
|
+
keyword: keywordOf(text),
|
|
8014
|
+
pos: start
|
|
8015
|
+
});
|
|
8016
|
+
else tokens.push({
|
|
8017
|
+
type: "identifier",
|
|
8018
|
+
name: text,
|
|
8019
|
+
pos: start
|
|
8020
|
+
});
|
|
8021
|
+
continue;
|
|
8022
|
+
}
|
|
8023
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
8024
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
8025
|
+
tokens.push({
|
|
8026
|
+
type: "punct",
|
|
8027
|
+
punct: two,
|
|
8028
|
+
pos: i
|
|
8029
|
+
});
|
|
8030
|
+
i += 2;
|
|
8031
|
+
continue;
|
|
8032
|
+
}
|
|
8033
|
+
if (isSinglePunct(ch)) {
|
|
8034
|
+
tokens.push({
|
|
8035
|
+
type: "punct",
|
|
8036
|
+
punct: ch,
|
|
8037
|
+
pos: i
|
|
8038
|
+
});
|
|
8039
|
+
i += 1;
|
|
8040
|
+
continue;
|
|
8041
|
+
}
|
|
8042
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
8043
|
+
}
|
|
8044
|
+
tokens.push({
|
|
8045
|
+
type: "eof",
|
|
8046
|
+
pos: n
|
|
8047
|
+
});
|
|
8048
|
+
return tokens;
|
|
8049
|
+
}
|
|
8050
|
+
function keywordOf(text) {
|
|
8051
|
+
if (text === "true") return "true";
|
|
8052
|
+
if (text === "false") return "false";
|
|
8053
|
+
return "null";
|
|
8054
|
+
}
|
|
8055
|
+
function isSinglePunct(ch) {
|
|
8056
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
8057
|
+
}
|
|
8058
|
+
/**
|
|
8059
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
8060
|
+
*
|
|
8061
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
8062
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
8063
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
8064
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
8065
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
8066
|
+
* that references a since-removed builtin degrades at read.
|
|
8067
|
+
*
|
|
8068
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
8069
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
8070
|
+
*/
|
|
8071
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
8072
|
+
var BINARY_PRECEDENCE = {
|
|
8073
|
+
"||": 1,
|
|
8074
|
+
"&&": 2,
|
|
8075
|
+
"==": 3,
|
|
8076
|
+
"!=": 3,
|
|
8077
|
+
"<": 4,
|
|
8078
|
+
"<=": 4,
|
|
8079
|
+
">": 4,
|
|
8080
|
+
">=": 4,
|
|
8081
|
+
"+": 5,
|
|
8082
|
+
"-": 5,
|
|
8083
|
+
"*": 6,
|
|
8084
|
+
"/": 6,
|
|
8085
|
+
"%": 6
|
|
8086
|
+
};
|
|
8087
|
+
function isLogicalOp(op) {
|
|
8088
|
+
return op === "&&" || op === "||";
|
|
8089
|
+
}
|
|
8090
|
+
function isBinaryOp(op) {
|
|
8091
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
8092
|
+
}
|
|
8093
|
+
var Parser = class {
|
|
8094
|
+
tokens;
|
|
8095
|
+
pos = 0;
|
|
8096
|
+
nodeCount = 0;
|
|
8097
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
8098
|
+
callees = /* @__PURE__ */ new Set();
|
|
8099
|
+
constructor(tokens) {
|
|
8100
|
+
this.tokens = tokens;
|
|
8101
|
+
}
|
|
8102
|
+
parse() {
|
|
8103
|
+
const ast = this.parseTernary();
|
|
8104
|
+
const tok = this.peek();
|
|
8105
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
8106
|
+
return {
|
|
8107
|
+
ast,
|
|
8108
|
+
identifiers: this.identifiers,
|
|
8109
|
+
callees: this.callees,
|
|
8110
|
+
nodeCount: this.nodeCount
|
|
8111
|
+
};
|
|
8112
|
+
}
|
|
8113
|
+
peek() {
|
|
8114
|
+
return this.tokens[this.pos];
|
|
8115
|
+
}
|
|
8116
|
+
next() {
|
|
8117
|
+
return this.tokens[this.pos++];
|
|
8118
|
+
}
|
|
8119
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
8120
|
+
expectPunct(punct) {
|
|
8121
|
+
const tok = this.peek();
|
|
8122
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
8123
|
+
this.pos += 1;
|
|
8124
|
+
}
|
|
8125
|
+
matchPunct(punct) {
|
|
8126
|
+
const tok = this.peek();
|
|
8127
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
8128
|
+
this.pos += 1;
|
|
8129
|
+
return true;
|
|
8130
|
+
}
|
|
8131
|
+
return false;
|
|
8132
|
+
}
|
|
8133
|
+
countNode() {
|
|
8134
|
+
this.nodeCount += 1;
|
|
8135
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
8136
|
+
}
|
|
8137
|
+
parseTernary() {
|
|
8138
|
+
const test = this.parseBinary(1);
|
|
8139
|
+
if (this.matchPunct("?")) {
|
|
8140
|
+
const consequent = this.parseTernary();
|
|
8141
|
+
this.expectPunct(":");
|
|
8142
|
+
const alternate = this.parseTernary();
|
|
8143
|
+
this.countNode();
|
|
8144
|
+
return {
|
|
8145
|
+
kind: "conditional",
|
|
8146
|
+
test,
|
|
8147
|
+
consequent,
|
|
8148
|
+
alternate
|
|
8149
|
+
};
|
|
8150
|
+
}
|
|
8151
|
+
return test;
|
|
8152
|
+
}
|
|
8153
|
+
parseBinary(minPrec) {
|
|
8154
|
+
let left = this.parseUnary();
|
|
8155
|
+
for (;;) {
|
|
8156
|
+
const tok = this.peek();
|
|
8157
|
+
if (tok.type !== "punct") break;
|
|
8158
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
8159
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
8160
|
+
const op = tok.punct;
|
|
8161
|
+
this.pos += 1;
|
|
8162
|
+
const right = this.parseBinary(prec + 1);
|
|
8163
|
+
this.countNode();
|
|
8164
|
+
if (isLogicalOp(op)) left = {
|
|
8165
|
+
kind: "logical",
|
|
8166
|
+
op,
|
|
8167
|
+
left,
|
|
8168
|
+
right
|
|
8169
|
+
};
|
|
8170
|
+
else if (isBinaryOp(op)) left = {
|
|
8171
|
+
kind: "binary",
|
|
8172
|
+
op,
|
|
8173
|
+
left,
|
|
8174
|
+
right
|
|
8175
|
+
};
|
|
8176
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
8177
|
+
}
|
|
8178
|
+
return left;
|
|
8179
|
+
}
|
|
8180
|
+
parseUnary() {
|
|
8181
|
+
const tok = this.peek();
|
|
8182
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
8183
|
+
const op = tok.punct;
|
|
8184
|
+
this.pos += 1;
|
|
8185
|
+
const operand = this.parseUnary();
|
|
8186
|
+
this.countNode();
|
|
8187
|
+
return {
|
|
8188
|
+
kind: "unary",
|
|
8189
|
+
op,
|
|
8190
|
+
operand
|
|
8191
|
+
};
|
|
8192
|
+
}
|
|
8193
|
+
return this.parsePrimary();
|
|
8194
|
+
}
|
|
8195
|
+
parsePrimary() {
|
|
8196
|
+
const tok = this.next();
|
|
8197
|
+
switch (tok.type) {
|
|
8198
|
+
case "number":
|
|
8199
|
+
this.countNode();
|
|
8200
|
+
return {
|
|
8201
|
+
kind: "literal",
|
|
8202
|
+
value: tok.value
|
|
8203
|
+
};
|
|
8204
|
+
case "string":
|
|
8205
|
+
this.countNode();
|
|
8206
|
+
return {
|
|
8207
|
+
kind: "literal",
|
|
8208
|
+
value: tok.value
|
|
8209
|
+
};
|
|
8210
|
+
case "keyword":
|
|
8211
|
+
this.countNode();
|
|
8212
|
+
return {
|
|
8213
|
+
kind: "literal",
|
|
8214
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
8215
|
+
};
|
|
8216
|
+
case "identifier": {
|
|
8217
|
+
const nextTok = this.peek();
|
|
8218
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
8219
|
+
this.identifiers.add(tok.name);
|
|
8220
|
+
this.countNode();
|
|
8221
|
+
return {
|
|
8222
|
+
kind: "identifier",
|
|
8223
|
+
name: tok.name
|
|
8224
|
+
};
|
|
8225
|
+
}
|
|
8226
|
+
case "punct":
|
|
8227
|
+
if (tok.punct === "(") {
|
|
8228
|
+
const inner = this.parseTernary();
|
|
8229
|
+
this.expectPunct(")");
|
|
8230
|
+
return inner;
|
|
8231
|
+
}
|
|
8232
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
8233
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
8234
|
+
}
|
|
8235
|
+
}
|
|
8236
|
+
parseCall(callee, pos) {
|
|
8237
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
8238
|
+
this.expectPunct("(");
|
|
8239
|
+
const args = [];
|
|
8240
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
8241
|
+
args.push(this.parseTernary());
|
|
8242
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
8243
|
+
if (this.matchPunct(",")) continue;
|
|
8244
|
+
this.expectPunct(")");
|
|
8245
|
+
break;
|
|
8246
|
+
}
|
|
8247
|
+
this.callees.add(callee);
|
|
8248
|
+
this.countNode();
|
|
8249
|
+
return {
|
|
8250
|
+
kind: "call",
|
|
8251
|
+
callee,
|
|
8252
|
+
args
|
|
8253
|
+
};
|
|
8254
|
+
}
|
|
8255
|
+
};
|
|
8256
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
8257
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
8258
|
+
function parseExpression(source) {
|
|
8259
|
+
return new Parser(tokenize(source)).parse();
|
|
8260
|
+
}
|
|
8261
|
+
/**
|
|
8262
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
8263
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
8264
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
8265
|
+
* one per read on a hot resolve path.
|
|
8266
|
+
*
|
|
8267
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
8268
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
8269
|
+
* callers is safe and maximises hit rate.
|
|
8270
|
+
*/
|
|
8271
|
+
var cache = /* @__PURE__ */ new Map();
|
|
8272
|
+
function getCached(source) {
|
|
8273
|
+
const hit = cache.get(source);
|
|
8274
|
+
if (hit !== void 0) {
|
|
8275
|
+
cache.delete(source);
|
|
8276
|
+
cache.set(source, hit);
|
|
8277
|
+
return hit;
|
|
8278
|
+
}
|
|
8279
|
+
let result;
|
|
8280
|
+
try {
|
|
8281
|
+
result = {
|
|
8282
|
+
ok: true,
|
|
8283
|
+
parsed: parseExpression(source)
|
|
8284
|
+
};
|
|
8285
|
+
} catch (err) {
|
|
8286
|
+
result = {
|
|
8287
|
+
ok: false,
|
|
8288
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
8289
|
+
};
|
|
8290
|
+
}
|
|
8291
|
+
cache.set(source, result);
|
|
8292
|
+
if (cache.size > 256) {
|
|
8293
|
+
const oldest = cache.keys().next().value;
|
|
8294
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
8295
|
+
}
|
|
8296
|
+
return result;
|
|
8297
|
+
}
|
|
8298
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
8299
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
8300
|
+
function compileExpressionSafe(source) {
|
|
8301
|
+
return getCached(source);
|
|
8302
|
+
}
|
|
8303
|
+
Object.freeze({});
|
|
8304
|
+
/**
|
|
8305
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
8306
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
8307
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
8308
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
8309
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
8310
|
+
*/
|
|
8311
|
+
function validateExpressionSource(src) {
|
|
8312
|
+
const names = Object.keys(src.bindings);
|
|
8313
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
8314
|
+
for (const name of names) {
|
|
8315
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
8316
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
8317
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
8318
|
+
}
|
|
8319
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
8320
|
+
if (!compiled.ok) return compiled.error;
|
|
8321
|
+
const bound = new Set(names);
|
|
8322
|
+
for (const id of compiled.parsed.identifiers) {
|
|
8323
|
+
if (id === "now") continue;
|
|
8324
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
8325
|
+
}
|
|
8326
|
+
return null;
|
|
8327
|
+
}
|
|
8328
|
+
/** A composed device's stableId is this prefix plus the block id. */
|
|
8329
|
+
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
8330
|
+
/** Longest snippet a `code` source may carry (slice 4). */
|
|
8331
|
+
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
8332
|
+
var CompositionSourceRefSchema = object({
|
|
8333
|
+
/** The owning addon; `stableId` is unique only within it. */
|
|
8334
|
+
addonId: string().min(1),
|
|
8335
|
+
stableId: string().min(1)
|
|
8336
|
+
});
|
|
8337
|
+
/** One field of one capability of one source device. */
|
|
8338
|
+
var CompositionFieldReadSchema = object({
|
|
8339
|
+
source: CompositionSourceRefSchema,
|
|
8340
|
+
cap: string().min(1),
|
|
8341
|
+
/** Dotted path into the source cap's runtime-state slice. */
|
|
8342
|
+
fieldPath: string().min(1)
|
|
8343
|
+
});
|
|
8344
|
+
/** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
|
|
8345
|
+
var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: literal("from") });
|
|
8346
|
+
var CompositionBindingSchema = discriminatedUnion("kind", [CompositionFromSourceSchema, object({
|
|
8347
|
+
kind: literal("literal"),
|
|
8348
|
+
value: union([
|
|
8349
|
+
string(),
|
|
8350
|
+
number(),
|
|
8351
|
+
boolean(),
|
|
8352
|
+
_null()
|
|
8353
|
+
])
|
|
8354
|
+
})]);
|
|
8355
|
+
/**
|
|
8356
|
+
* `expression`: a formula over named bindings, in the salvaged expression
|
|
8357
|
+
* engine. Validated at parse by the SAME function every other consumer runs,
|
|
8358
|
+
* so the editor and the store cannot disagree.
|
|
8359
|
+
*/
|
|
8360
|
+
var CompositionExpressionSourceSchema = object({
|
|
8361
|
+
kind: literal("expression"),
|
|
8362
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
8363
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
|
|
8364
|
+
}).superRefine((src, ctx) => {
|
|
8365
|
+
const err = validateExpressionSource(src);
|
|
8366
|
+
if (err !== null) ctx.addIssue({
|
|
8367
|
+
code: "custom",
|
|
8368
|
+
message: err,
|
|
8369
|
+
path: ["expr"]
|
|
8370
|
+
});
|
|
8371
|
+
});
|
|
8372
|
+
/** `code`: RESERVED for slice 4 (own runner, one-way eject). */
|
|
8373
|
+
var CompositionCodeSourceSchema = object({
|
|
8374
|
+
kind: literal("code"),
|
|
8375
|
+
code: string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
|
|
8376
|
+
});
|
|
8377
|
+
var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
8378
|
+
CompositionFromSourceSchema,
|
|
8379
|
+
CompositionExpressionSourceSchema,
|
|
8380
|
+
CompositionCodeSourceSchema
|
|
8381
|
+
]);
|
|
8382
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
8383
|
+
kind: literal("forward"),
|
|
8384
|
+
source: CompositionSourceRefSchema,
|
|
8385
|
+
cap: string().min(1),
|
|
8386
|
+
method: string().min(1)
|
|
8387
|
+
}), CompositionCodeSourceSchema]);
|
|
8388
|
+
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
8389
|
+
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
8390
|
+
/**
|
|
8391
|
+
* One item of an item-array cap (D663), addressed through the cap's
|
|
8392
|
+
* `status.itemArray` descriptor. `fields` are paths INSIDE the item, nested
|
|
8393
|
+
* allowed (`remaining.value`); the key and label come from the entry itself.
|
|
8394
|
+
*/
|
|
8395
|
+
var CompositionItemEntrySchema = object({
|
|
8396
|
+
label: string().min(1).max(80),
|
|
8397
|
+
fields: record(string().min(1), CompositionFieldSourceSchema)
|
|
8398
|
+
});
|
|
8399
|
+
var CompositionFeatureSchema = discriminatedUnion("kind", [object({
|
|
8400
|
+
kind: literal("fields"),
|
|
8401
|
+
cap: string().min(1),
|
|
8402
|
+
fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
|
|
8403
|
+
/**
|
|
8404
|
+
* Items of the cap's item array, keyed by item key (D663). Naming `items`
|
|
8405
|
+
* owns the WHOLE array field: the composed value holds exactly these items.
|
|
8406
|
+
*/
|
|
8407
|
+
items: record(string().regex(COMPOSITION_ITEM_KEY_RE), CompositionItemEntrySchema).refine((items) => Object.keys(items).length <= 16, { message: `at most 16 items` }).optional(),
|
|
8408
|
+
/** RESERVED for slice 3; refused by the validator when non-empty. */
|
|
8409
|
+
commands: record(string().min(1), CompositionCommandTargetSchema).optional()
|
|
8410
|
+
}), object({
|
|
8411
|
+
kind: literal("passthrough"),
|
|
8412
|
+
cap: string().min(1),
|
|
8413
|
+
source: CompositionSourceRefSchema
|
|
8414
|
+
})]);
|
|
8415
|
+
var CompositionSchema = object({
|
|
8416
|
+
target: discriminatedUnion("kind", [object({
|
|
8417
|
+
kind: literal("new"),
|
|
8418
|
+
type: _enum(DeviceType),
|
|
8419
|
+
role: _enum(DeviceRole).optional()
|
|
8420
|
+
}), object({
|
|
8421
|
+
kind: literal("existing"),
|
|
8422
|
+
device: CompositionSourceRefSchema
|
|
8423
|
+
})]),
|
|
8424
|
+
features: array(CompositionFeatureSchema).min(1).max(16)
|
|
8425
|
+
}).superRefine((composition, ctx) => {
|
|
8426
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8427
|
+
composition.features.forEach((feature, index) => {
|
|
8428
|
+
if (seen.has(feature.cap)) ctx.addIssue({
|
|
8429
|
+
code: "custom",
|
|
8430
|
+
message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
|
|
8431
|
+
path: [
|
|
8432
|
+
"features",
|
|
8433
|
+
index,
|
|
8434
|
+
"cap"
|
|
8435
|
+
]
|
|
8436
|
+
});
|
|
8437
|
+
seen.add(feature.cap);
|
|
8438
|
+
});
|
|
8439
|
+
});
|
|
8440
|
+
var OWNER_TYPES = new Set([
|
|
8441
|
+
"track",
|
|
8442
|
+
"summary",
|
|
8443
|
+
"face",
|
|
8444
|
+
"identity",
|
|
8445
|
+
"plate",
|
|
8446
|
+
"vehicle",
|
|
8447
|
+
"scene",
|
|
8448
|
+
"motion",
|
|
8449
|
+
"object",
|
|
8450
|
+
"audio",
|
|
8451
|
+
"mosaic"
|
|
8452
|
+
]);
|
|
8453
|
+
/**
|
|
8454
|
+
* Is this string a declared media owner type?
|
|
8455
|
+
*
|
|
8456
|
+
* Exported because the `event-media` plane's path parser needs it (D482): a
|
|
8457
|
+
* path segment is an arbitrary string until something narrows it, and a
|
|
8458
|
+
* hand-written list at the door would drift from {@link MEDIA_OWNER_TYPES} the
|
|
8459
|
+
* day an owner kind is added — the failure this module's own docblock already
|
|
8460
|
+
* records for `vehicle` and `scene`.
|
|
8461
|
+
*/
|
|
8462
|
+
function isMediaOwnerType(value) {
|
|
8463
|
+
return OWNER_TYPES.has(value);
|
|
8464
|
+
}
|
|
8465
|
+
/**
|
|
8466
|
+
* The owner types that are event tables, in the order the event-media resolver
|
|
8467
|
+
* should consider them. Exported so a consumer asking "is this key an event?"
|
|
8468
|
+
* does not re-spell the list and drift from it.
|
|
8469
|
+
*/
|
|
8470
|
+
var EVENT_OWNER_TYPES = [
|
|
8471
|
+
"motion",
|
|
8472
|
+
"object",
|
|
8473
|
+
"audio"
|
|
8474
|
+
];
|
|
8475
|
+
/**
|
|
8476
|
+
* The same list as a Zod enum, for the cap inputs that must NAME the table
|
|
8477
|
+
* (D482: `getEventMedia` / `listEventMedia` take an owner, and an owner is
|
|
8478
|
+
* `(table, id)`).
|
|
8479
|
+
*
|
|
8480
|
+
* Built FROM {@link EVENT_OWNER_TYPES} rather than re-spelled: a fourth event
|
|
8481
|
+
* table would otherwise be accepted by the codec and refused at the door, with
|
|
8482
|
+
* nothing failing until a caller asked.
|
|
8483
|
+
*/
|
|
8484
|
+
var EventOwnerTypeSchema = _enum(EVENT_OWNER_TYPES);
|
|
8485
|
+
/** The same list as a Zod enum, for the cap input that carries it. */
|
|
8486
|
+
var MediaPresenceOwnerKindSchema = _enum([...EVENT_OWNER_TYPES, "track"]);
|
|
8487
|
+
var EVENT_OWNER_TYPE_SET = new Set(EVENT_OWNER_TYPES);
|
|
8488
|
+
/** True when this key's owner is one of the three event tables. */
|
|
8489
|
+
function isEventOwnerType(ownerType) {
|
|
8490
|
+
return EVENT_OWNER_TYPE_SET.has(ownerType);
|
|
8491
|
+
}
|
|
8492
|
+
var EncodeProfileSchema = object({
|
|
8493
|
+
video: object({
|
|
8494
|
+
codec: _enum([
|
|
8495
|
+
"h264",
|
|
8496
|
+
"h265",
|
|
8497
|
+
"copy"
|
|
8498
|
+
]),
|
|
8499
|
+
profile: _enum([
|
|
8500
|
+
"baseline",
|
|
8501
|
+
"main",
|
|
8502
|
+
"high"
|
|
8503
|
+
]).optional(),
|
|
8504
|
+
/**
|
|
8505
|
+
* `-level`, e.g. `'3.1'`. A consumer that ADVERTISES a level in its SDP
|
|
8506
|
+
* (`profile-level-id=42e01f` is Baseline 3.1) must constrain the encoder to
|
|
8507
|
+
* it, or it ships a stream that does not match its own advertisement — the
|
|
8508
|
+
* defect class that kept HomeKit black for a year and that Alexa carried
|
|
8509
|
+
* silently. Optional because a browser negotiates the level itself.
|
|
8510
|
+
*/
|
|
8511
|
+
level: string().optional(),
|
|
8512
|
+
width: number().int().positive().optional(),
|
|
8513
|
+
height: number().int().positive().optional(),
|
|
8514
|
+
fps: number().positive().optional(),
|
|
8515
|
+
bitrateKbps: number().int().positive().optional(),
|
|
8516
|
+
gopFrames: number().int().positive().optional(),
|
|
8517
|
+
bf: number().int().min(0).optional(),
|
|
8518
|
+
preset: _enum([
|
|
8519
|
+
"ultrafast",
|
|
8520
|
+
"superfast",
|
|
8521
|
+
"veryfast",
|
|
8522
|
+
"faster",
|
|
8523
|
+
"fast",
|
|
8524
|
+
"medium"
|
|
8525
|
+
]).optional(),
|
|
8526
|
+
tune: _enum([
|
|
8527
|
+
"zerolatency",
|
|
8528
|
+
"film",
|
|
8529
|
+
"animation"
|
|
8530
|
+
]).optional(),
|
|
8531
|
+
/**
|
|
8532
|
+
* ONE slice per access unit.
|
|
8533
|
+
*
|
|
8534
|
+
* `-tune zerolatency` turns on x264's sliced threads, and a frame then leaves
|
|
8535
|
+
* the encoder as five NAL slices that share one RTP timestamp and carry one
|
|
8536
|
+
* marker bit. A libwebrtc depacketiser sees five frame-starts and one
|
|
8537
|
+
* frame-end per frame: the first pictures render and the video then freezes
|
|
8538
|
+
* for good while the audio, on its own plane, plays on. Measured on this hub
|
|
8539
|
+
* 2026-09-13 against the Echo, and reduced to one slice by this flag alone.
|
|
8540
|
+
*
|
|
8541
|
+
* A NAMED field and not a raw flag, because {@link EgressEncodeSchema} omits
|
|
8542
|
+
* `outputArgs` on purpose: an opaque array is part of the sharing key, so two
|
|
8543
|
+
* consumers meaning the same thing spelled differently would stop sharing one
|
|
8544
|
+
* child. Absent means "whatever the encoder does" — today's behaviour.
|
|
8545
|
+
*/
|
|
8546
|
+
singleSlicePerFrame: boolean().optional()
|
|
8547
|
+
}),
|
|
8548
|
+
audio: union([literal("passthrough"), object({
|
|
8549
|
+
codec: _enum([
|
|
8550
|
+
"opus",
|
|
8551
|
+
"aac",
|
|
8552
|
+
"pcmu",
|
|
8553
|
+
"pcma",
|
|
8554
|
+
"copy"
|
|
8555
|
+
]),
|
|
8556
|
+
bitrateKbps: number().int().positive().optional(),
|
|
8557
|
+
sampleRateHz: number().int().positive().optional(),
|
|
8558
|
+
channels: union([literal(1), literal(2)]).optional()
|
|
8559
|
+
})]),
|
|
8560
|
+
/**
|
|
8561
|
+
* ffmpeg input-side args, inserted between the fixed global flags
|
|
8562
|
+
* (`-hide_banner -loglevel error`) and `-i pipe:0`. Free-text array
|
|
8563
|
+
* — the widget surfaces a textarea + suggestion chips for the most-
|
|
8564
|
+
* used demuxer/format options.
|
|
8565
|
+
*/
|
|
8566
|
+
inputArgs: array(string()).optional(),
|
|
8567
|
+
/**
|
|
8568
|
+
* ffmpeg output-side args, inserted between the encode block and
|
|
8569
|
+
* the final `-f <muxer> pipe:1`. Use for muxer options, bitstream
|
|
8570
|
+
* filters, codec-specific overrides. Free-text array.
|
|
8571
|
+
*/
|
|
8572
|
+
outputArgs: array(string()).optional()
|
|
8573
|
+
});
|
|
8574
|
+
/**
|
|
8575
|
+
* The shape every live egress starts from: H.264 Baseline 3.1 at 720p25.
|
|
8576
|
+
* Baseline because it is the one profile every consumer in this repo decodes
|
|
8577
|
+
* (Echo, iOS, an old browser); 3.1 because that is what the SDPs advertise.
|
|
8578
|
+
*/
|
|
8579
|
+
var BASE_LIVE_EGRESS_PROFILE = {
|
|
8580
|
+
video: {
|
|
8581
|
+
codec: "h264",
|
|
8582
|
+
profile: "baseline",
|
|
8583
|
+
level: "3.1",
|
|
7825
8584
|
width: 1280,
|
|
7826
8585
|
height: 720,
|
|
7827
8586
|
fps: 25,
|
|
@@ -9539,1504 +10298,888 @@ function isStorageLocationMode(value) {
|
|
|
9539
10298
|
/** May this location be written to? */
|
|
9540
10299
|
function mayWriteToLocation(location) {
|
|
9541
10300
|
return modeMayWrite(resolveLocationMode(location));
|
|
9542
|
-
}
|
|
9543
|
-
/** What eviction may do to this location. */
|
|
9544
|
-
function evictionPolicyOfLocation(location) {
|
|
9545
|
-
return evictionPolicyForMode(resolveLocationMode(location));
|
|
9546
|
-
}
|
|
9547
|
-
/**
|
|
9548
|
-
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
9549
|
-
* storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
|
|
9550
|
-
* so the persisted record schema and the consumer-facing cap can both consume it
|
|
9551
|
-
* without forming a circular import. The `storage` cap re-exports it
|
|
9552
|
-
* verbatim for back-compat.
|
|
9553
|
-
*
|
|
9554
|
-
* This Zod schema is the **authoritative source** for `StorageLocationType`.
|
|
9555
|
-
* The TS alias in `./storage.ts` re-exports `z.infer<typeof
|
|
9556
|
-
* StorageLocationTypeSchema>` so the wire surface (cap) and the legacy
|
|
9557
|
-
* `IStorageProvider` interface stay in lockstep.
|
|
9558
|
-
*
|
|
9559
|
-
* The type is now an **open string** (not a closed enum) — addons declare
|
|
9560
|
-
* their own location kinds via `StorageLocationDeclaration.id`. The regex
|
|
9561
|
-
* enforces a safe id format: lowercase-start, alphanumeric + hyphens.
|
|
9562
|
-
*/
|
|
9563
|
-
var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
|
|
9564
|
-
/**
|
|
9565
|
-
* Persisted record for a storage location instance. Operators can register
|
|
9566
|
-
* multiple instances for multi-cardinality types (e.g. two `backups`
|
|
9567
|
-
* locations with different `providerId`s). Cardinality is now declared per
|
|
9568
|
-
* location via `StorageLocationDeclaration.cardinality` — the static
|
|
9569
|
-
* `STORAGE_LOCATION_CARDINALITY` map has been removed.
|
|
9570
|
-
*
|
|
9571
|
-
* `id` is a stable namespaced string of the form `<type>:<slug>`.
|
|
9572
|
-
* The seed names its first instance `<type>:default` — a NAME, not a flag.
|
|
9573
|
-
* There is no default location any more (D383): `enabled` is the whole write
|
|
9574
|
-
* model, and a bare type ref resolves to the sole location of the type, or —
|
|
9575
|
-
* transitionally, only while legacy NULL-stamped rows exist — to the row whose
|
|
9576
|
-
* slug is `default`.
|
|
9577
|
-
*
|
|
9578
|
-
* `isSystem` is a legacy persisted flag. Seed still creates the initial
|
|
9579
|
-
* `<type>:default` locations; the flag is no longer a lock, a badge, or a
|
|
9580
|
-
* prune selector. New writes leave it false. Deletion is gated on uniqueness
|
|
9581
|
-
* / last-enabled, not on this bit.
|
|
9582
|
-
*/
|
|
9583
|
-
var StorageLocationSchema = object({
|
|
9584
|
-
id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
|
|
9585
|
-
type: string(),
|
|
9586
|
-
displayName: string().min(1),
|
|
9587
|
-
providerId: string().min(1),
|
|
9588
|
-
config: record(string(), unknown()),
|
|
9589
|
-
/**
|
|
9590
|
-
* Cluster node this location physically lives on. REQUIRED for node-local
|
|
9591
|
-
* providers (filesystem — the path exists on one node's disk), null/absent
|
|
9592
|
-
* for node-agnostic providers (S3/SFTP/WebDAV, reachable from any node).
|
|
9593
|
-
* `'hub'` is the hub node. Validated against the provider's `nodeLocal`
|
|
9594
|
-
* flag at upsert time, not here (the schema is provider-agnostic).
|
|
9595
|
-
*/
|
|
9596
|
-
nodeId: string().optional(),
|
|
9597
|
-
isSystem: boolean().default(false),
|
|
9598
|
-
/**
|
|
9599
|
-
* THE write switch, and the only one (D383). `enabled: true` means every
|
|
9600
|
-
* consumer that chooses a write target for this type may write here, and all
|
|
9601
|
-
* enabled locations of a type are used TOGETHER; `false` means read-only —
|
|
9602
|
-
* still read, still played back, still age-swept, still drained, never
|
|
9603
|
-
* written.
|
|
9604
|
-
*
|
|
9605
|
-
* OPTIONAL only for the wire: an upsert that omits it means "leave what is
|
|
9606
|
-
* stored" on an update and "born inert unless it is the first location of its
|
|
9607
|
-
* type" on a create. On a PERSISTED row absence is legacy and it means
|
|
9608
|
-
* enabled — {@link isLocationEnabled} is the one place that says so, and the
|
|
9609
|
-
* orchestrator stamps every flagless row `true` once at hydrate so absence
|
|
9610
|
-
* stops existing rather than being re-derived on every read.
|
|
9611
|
-
*/
|
|
9612
|
-
enabled: boolean().optional(),
|
|
9613
|
-
/**
|
|
9614
|
-
* THE state of this location (D385), and the only authority on what may be
|
|
9615
|
-
* written, read or evicted here. Interpreted in exactly one place —
|
|
9616
|
-
* `storage-location-mode.ts` — which also folds the legacy
|
|
9617
|
-
* `enabled` / `config.readOnly` pair into a mode so an old row is never
|
|
9618
|
-
* ambiguous.
|
|
9619
|
-
*
|
|
9620
|
-
* OPTIONAL only for the wire and for rows written before D385: absence is
|
|
9621
|
-
* resolved by `resolveLocationMode`, and the orchestrator stamps every
|
|
9622
|
-
* unstamped row ONCE at hydrate so absence stops existing rather than being
|
|
9623
|
-
* re-derived on every read. `enabled` survives one release as a DERIVED
|
|
9624
|
-
* mirror (`mode === 'active'`); `withLocationMode` is the only writer of
|
|
9625
|
-
* either, so the two cannot disagree.
|
|
9626
|
-
*/
|
|
9627
|
-
mode: StorageLocationModeSchema.optional(),
|
|
9628
|
-
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
9629
|
-
* for node-local locations it can reach) — never persisted, absent when the
|
|
9630
|
-
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
9631
|
-
capacity: object({
|
|
9632
|
-
totalBytes: number(),
|
|
9633
|
-
availableBytes: number()
|
|
9634
|
-
}).nullable().optional(),
|
|
9635
|
-
/**
|
|
9636
|
-
* How much of that volume CamStack ITSELF holds on this location (D388) —
|
|
9637
|
-
* COMPUTED at read time from the `storage-occupancy` providers' own figures,
|
|
9638
|
-
* never persisted, never a filesystem walk.
|
|
9639
|
-
*
|
|
9640
|
-
* **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
|
|
9641
|
-
* location yet — nobody stores here, the owning addon is down, or the first
|
|
9642
|
-
* refresh has not completed. A UI must omit the segment rather than draw it
|
|
9643
|
-
* at zero, which would claim we occupy nothing (D315). It is an OBJECT and
|
|
9644
|
-
* not a bare number precisely so that a `?? 0` on the consuming side has to
|
|
9645
|
-
* be spelled out loud instead of appearing by accident.
|
|
9646
|
-
*
|
|
9647
|
-
* `measuredAtMs` is the OLDEST contributing measurement, so it is honest
|
|
9648
|
-
* about the whole figure rather than about its freshest part.
|
|
9649
|
-
*/
|
|
9650
|
-
owned: object({
|
|
9651
|
-
bytes: number().int().nonnegative(),
|
|
9652
|
-
measuredAtMs: number().int().nonnegative()
|
|
9653
|
-
}).optional(),
|
|
9654
|
-
createdAt: number(),
|
|
9655
|
-
updatedAt: number()
|
|
9656
|
-
});
|
|
9657
|
-
object({ isDefault: boolean().optional() });
|
|
9658
|
-
/**
|
|
9659
|
-
* How far a `drain` has got (D386) — the read a UI renders, and nothing more.
|
|
9660
|
-
*
|
|
9661
|
-
* `estimatedEmptyAtMs` is derived from the growth the ratchet has actually
|
|
9662
|
-
* OBSERVED and is `null` when it has observed none. Never a fabricated date: a
|
|
9663
|
-
* drain with no observed growth has no honest ETA, and inventing one is how an
|
|
9664
|
-
* operator learns not to believe the screen.
|
|
9665
|
-
*/
|
|
9666
|
-
var StorageDrainProgressSchema = object({
|
|
9667
|
-
locationId: string(),
|
|
9668
|
-
startedAtMs: number(),
|
|
9669
|
-
startBytes: number(),
|
|
9670
|
-
bytesRemaining: number(),
|
|
9671
|
-
drained: boolean(),
|
|
9672
|
-
estimatedEmptyAtMs: number().nullable()
|
|
9673
|
-
});
|
|
9674
|
-
/**
|
|
9675
|
-
* Reference accepted by consumer-facing `api.storage.*` calls.
|
|
9676
|
-
* Either:
|
|
9677
|
-
* - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
|
|
9678
|
-
* (transitionally, the `<type>:default`-slugged row when several exist)
|
|
9679
|
-
* - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
|
|
9680
|
-
*
|
|
9681
|
-
* The orchestrator's `resolveRef(ref)` handles both cases.
|
|
9682
|
-
*/
|
|
9683
|
-
var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
|
|
9684
|
-
/**
|
|
9685
|
-
* `StorageLocationDeclaration` — a single storage-location entry declared by
|
|
9686
|
-
* an addon in its `package.json` under `camstack.storageLocations`.
|
|
9687
|
-
*
|
|
9688
|
-
* Design intent:
|
|
9689
|
-
* - **Addon declares its needs** — each addon describes the logical storage
|
|
9690
|
-
* slots it requires (e.g. `recordings`, `recordingsLow`) without caring
|
|
9691
|
-
* about the physical path.
|
|
9692
|
-
* - **Kernel aggregates** — at boot the kernel collects declarations from all
|
|
9693
|
-
* installed addons, deduplicates by `id`, and exposes the union via the
|
|
9694
|
-
* storage-locations settings surface.
|
|
9695
|
-
* - **Orchestrator seeds** — for every declared `id` the orchestrator ensures
|
|
9696
|
-
* at least one instance named `<id>:default` is present, using
|
|
9697
|
-
* `defaultsTo` to inherit the resolved root from another location when the
|
|
9698
|
-
* declaration is a derivative slot (e.g. `recordingsLow` defaults to
|
|
9699
|
-
* `recordings`).
|
|
9700
|
-
* - **ids are global** — `id` values are shared across the entire deployment;
|
|
9701
|
-
* two addons declaring the same `id` must agree on `cardinality` (validated
|
|
9702
|
-
* at kernel aggregation time, not here).
|
|
9703
|
-
*/
|
|
9704
|
-
/**
|
|
9705
|
-
* `StorageAccess` — how the service that DECLARED a storage-location kind
|
|
9706
|
-
* actually reaches the bytes. It is the constraint that decides which
|
|
9707
|
-
* `storage-provider`s may back a location of that kind.
|
|
9708
|
-
*
|
|
9709
|
-
* - `'local-path'` — the service asks `storage.resolve` for a path string and
|
|
9710
|
-
* then does its own `node:fs` I/O on it (the recorder's segment writer, the
|
|
9711
|
-
* post-analysis media roots). Only a provider that serves a genuine local
|
|
9712
|
-
* filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
|
|
9713
|
-
* remote provider's `resolve` returns a path on the REMOTE host, and
|
|
9714
|
-
* `fs.readdir` of it on this node either fails or — far worse — succeeds
|
|
9715
|
-
* against a same-named local directory that is something else entirely.
|
|
9716
|
-
*
|
|
9717
|
-
* - `'cap-mediated'` — every byte travels through the `storage` cap
|
|
9718
|
-
* (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
|
|
9719
|
-
* service never sees a path, so any provider can back it. `backups` is the
|
|
9720
|
-
* one kind that qualifies today.
|
|
9721
|
-
*
|
|
9722
|
-
* Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
|
|
9723
|
-
* an EMERGENT property of how the recorder happened to be written. Nothing
|
|
9724
|
-
* refused the configuration; the first write simply went somewhere wrong, and
|
|
9725
|
-
* a recording write that goes wrong surfaces as a silent black window rather
|
|
9726
|
-
* than an error (the read path does not `stat`). This turns that accident into
|
|
9727
|
-
* a declared, enforced, testable refusal.
|
|
9728
|
-
*/
|
|
9729
|
-
var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
|
|
9730
|
-
var StorageLocationDeclarationSchema = object({
|
|
9731
|
-
/**
|
|
9732
|
-
* Global location identifier, e.g. `recordings` or `recordingsLow`.
|
|
9733
|
-
* Must start with a lowercase letter and may contain letters, digits, and
|
|
9734
|
-
* hyphens.
|
|
9735
|
-
*/
|
|
9736
|
-
id: string().regex(/^[a-z][a-zA-Z0-9-]*$/, { message: "id must start with a lowercase letter and contain only letters, digits, or hyphens" }),
|
|
9737
|
-
/** Human-readable name shown in the admin UI. */
|
|
9738
|
-
displayName: string().min(1, { message: "displayName must not be empty" }),
|
|
9739
|
-
/** Optional longer explanation of what data this location stores. */
|
|
9740
|
-
description: string().optional(),
|
|
9741
|
-
/**
|
|
9742
|
-
* `single` — exactly one instance of this location is allowed system-wide
|
|
9743
|
-
* (e.g. `logs`, `models`). The operator can edit it but not add more.
|
|
9744
|
-
* `multi` — the operator may register several instances (e.g. a second
|
|
9745
|
-
* `recordings` on a NAS for disk tiering); one is the default at any time.
|
|
9746
|
-
*/
|
|
9747
|
-
cardinality: _enum(["single", "multi"]),
|
|
9748
|
-
/**
|
|
9749
|
-
* HOW the declaring service reaches the bytes — and therefore WHICH
|
|
9750
|
-
* providers may back a location of this kind. See {@link StorageAccessSchema}
|
|
9751
|
-
* and {@link STORAGE_ACCESS_FALLBACK}.
|
|
9752
|
-
*
|
|
9753
|
-
* Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
|
|
9754
|
-
* can only over-restrict (refuse a remote provider for a kind that might
|
|
9755
|
-
* have coped) and never under-restrict. Declaring `'cap-mediated'` is the
|
|
9756
|
-
* permissive direction and is therefore never inferred — a repo guard
|
|
9757
|
-
* (`scripts/check-storage-access-declarations.ts`) refuses to let it be
|
|
9758
|
-
* reached by omission.
|
|
9759
|
-
*/
|
|
9760
|
-
access: StorageAccessSchema.optional(),
|
|
9761
|
-
/**
|
|
9762
|
-
* When set, the default instance for this location inherits its resolved
|
|
9763
|
-
* root from the named location's default instance. Useful for derivative
|
|
9764
|
-
* slots (e.g. `recordingsLow` → `recordings`) so operators only need to
|
|
9765
|
-
* configure the primary location.
|
|
9766
|
-
*/
|
|
9767
|
-
defaultsTo: string().optional(),
|
|
9768
|
-
/**
|
|
9769
|
-
* Which node root the seeded `<id>:default` instance is placed under on a
|
|
9770
|
-
* FRESH install:
|
|
9771
|
-
* - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
|
|
9772
|
-
* the appData volume. Right for small/durable data (logs, models).
|
|
9773
|
-
* - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
|
|
9774
|
-
* env is set, else falls back to the data root. Right for bulky, hot media
|
|
9775
|
-
* (recordings, event media) that should stay off the appData disk.
|
|
9776
|
-
* - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
|
|
9777
|
-
* `/backups` in the image) so archives live on their own mount rather than
|
|
9778
|
-
* filling the appData disk. Falls back to the data root when unset.
|
|
9779
|
-
*
|
|
9780
|
-
* Only affects the seeded default's `basePath`; operators can repoint any
|
|
9781
|
-
* location afterwards, and a `defaultsTo` slot inherits its parent's root
|
|
9782
|
-
* regardless of this field. Absent (the common case) is treated as `'data'`.
|
|
9783
|
-
*/
|
|
9784
|
-
defaultRoot: _enum([
|
|
9785
|
-
"data",
|
|
9786
|
-
"media",
|
|
9787
|
-
"backup"
|
|
9788
|
-
]).optional()
|
|
9789
|
-
});
|
|
9790
|
-
var DecoderStatsSchema = object({
|
|
9791
|
-
inputFps: number(),
|
|
9792
|
-
outputFps: number(),
|
|
9793
|
-
avgDecodeTimeMs: number(),
|
|
9794
|
-
droppedFrames: number(),
|
|
9795
|
-
/**
|
|
9796
|
-
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
9797
|
-
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
9798
|
-
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
9799
|
-
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
9800
|
-
*/
|
|
9801
|
-
lagMs: number().optional(),
|
|
9802
|
-
effectiveFps: number().optional(),
|
|
9803
|
-
adaptiveFps: number().optional()
|
|
9804
|
-
});
|
|
9805
|
-
var DecoderSessionConfigSchema = object({
|
|
9806
|
-
codec: string(),
|
|
9807
|
-
maxFps: number().default(0),
|
|
9808
|
-
outputFormat: _enum([
|
|
9809
|
-
"jpeg",
|
|
9810
|
-
"rgb",
|
|
9811
|
-
"bgr",
|
|
9812
|
-
"yuv420",
|
|
9813
|
-
"gray"
|
|
9814
|
-
]).default("jpeg"),
|
|
9815
|
-
scale: number().default(1),
|
|
9816
|
-
width: number().optional(),
|
|
9817
|
-
height: number().optional(),
|
|
9818
|
-
/**
|
|
9819
|
-
* Identifier of the camera this decoder session serves. Optional
|
|
9820
|
-
* because the cap is generic (any caller could request decode), but
|
|
9821
|
-
* stream-broker passes it so decoder logs include `deviceId` for
|
|
9822
|
-
* per-camera filtering when diagnosing failures (e.g. node-av
|
|
9823
|
-
* sendPacket errors on a single hung camera).
|
|
9824
|
-
*/
|
|
9825
|
-
deviceId: number().int().nonnegative().optional(),
|
|
9826
|
-
/**
|
|
9827
|
-
* Free-form tag for log scoping. Stream-broker uses
|
|
9828
|
-
* `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
|
|
9829
|
-
* on every line so `grep tag=broker:5/high` filters one camera
|
|
9830
|
-
* profile cleanly.
|
|
9831
|
-
*/
|
|
9832
|
-
tag: string().optional(),
|
|
9833
|
-
/**
|
|
9834
|
-
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
9835
|
-
*
|
|
9836
|
-
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
9837
|
-
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
9838
|
-
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
9839
|
-
* into an OS shared-memory ring and drained as zero-pixel
|
|
9840
|
-
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
9841
|
-
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
9842
|
-
* `pullHandles` returns nothing for a `'callback'` session.
|
|
9843
|
-
*/
|
|
9844
|
-
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
9845
|
-
/**
|
|
9846
|
-
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
9847
|
-
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
9848
|
-
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
9849
|
-
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
9850
|
-
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
9851
|
-
*/
|
|
9852
|
-
debug: boolean().optional()
|
|
9853
|
-
});
|
|
9854
|
-
/**
|
|
9855
|
-
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
9856
|
-
* an addon declares its channels in.
|
|
9857
|
-
*
|
|
9858
|
-
* ## Two axes, deliberately separated
|
|
9859
|
-
*
|
|
9860
|
-
* - **DECLARATION** — which channels exist. Only the addon knows:
|
|
9861
|
-
* `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
|
|
9862
|
-
* baichuan/handshake. A hand-wired central list rots at the first addition,
|
|
9863
|
-
* and rots silently. So a channel is declared where it is consulted, and the
|
|
9864
|
-
* `log-channels` capability enumerates the declarations.
|
|
9865
|
-
* - **VALUE** — at which level, for which scope, until when. That stays ONE
|
|
9866
|
-
* thing: the logging settings document on the `system` cap. Two authorities
|
|
9867
|
-
* over the values is the exact defect
|
|
9868
|
-
* `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
|
|
9869
|
-
* remove; re-introducing it from the cure side would be grotesque.
|
|
9870
|
-
*
|
|
9871
|
-
* Nothing in this file reads a clock, an env var or a store. The registry is
|
|
9872
|
-
* a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
|
|
9873
|
-
* the hot path with a value somebody actually read, and by
|
|
9874
|
-
* {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
|
|
9875
|
-
* never reaches here, so it can neither disarm an armed channel nor arm a
|
|
9876
|
-
* disarmed one (D49).
|
|
9877
|
-
*
|
|
9878
|
-
* ## The canonical call shape
|
|
9879
|
-
*
|
|
9880
|
-
* ```ts
|
|
9881
|
-
* if (CH_RTP.on && CH_RTP.wants(deviceId)) {
|
|
9882
|
-
* CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
|
|
9883
|
-
* }
|
|
9884
|
-
* ```
|
|
9885
|
-
*
|
|
9886
|
-
* `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
|
|
9887
|
-
* read. Disarmed, a call site costs one load and one branch, and the `extras`
|
|
9888
|
-
* object literal is never constructed because it lives inside the branch. It
|
|
9889
|
-
* is the same shape already proven in production at `stream-broker.ts:1650`,
|
|
9890
|
-
* and the same discipline `LoggingGate.allowsDestination` uses for the
|
|
9891
|
-
* destination floor (measured at 1.93 ns/call when off).
|
|
9892
|
-
*
|
|
9893
|
-
* ## Why a channel emits at `info`
|
|
9894
|
-
*
|
|
9895
|
-
* `loki-logging.addon.ts` pins the destination default at `info` and
|
|
9896
|
-
* `loki-destination.ts` drops everything below it, so a line emitted at
|
|
9897
|
-
* `debug` never reaches Loki and the hub's in-memory ring only holds ~35
|
|
9898
|
-
* minutes. A diagnostic that cannot be read an hour later is worse than no
|
|
9899
|
-
* diagnostic, because it looks done. {@link LogChannelGate.log} therefore
|
|
9900
|
-
* emits at the channel's declared level, whose schema floor is `info`.
|
|
9901
|
-
*/
|
|
9902
|
-
/**
|
|
9903
|
-
* The level a channel writes at once armed.
|
|
9904
|
-
*
|
|
9905
|
-
* `debug` is absent ON PURPOSE and not by omission: below `info` the line does
|
|
9906
|
-
* not leave the process for Loki, and the whole point of arming a channel is
|
|
9907
|
-
* to read it later.
|
|
9908
|
-
*/
|
|
9909
|
-
var LogChannelLevelSchema = _enum([
|
|
9910
|
-
"info",
|
|
9911
|
-
"warn",
|
|
9912
|
-
"error"
|
|
9913
|
-
]);
|
|
9914
|
-
/**
|
|
9915
|
-
* What an addon declares about one channel. No value, no state — a
|
|
9916
|
-
* declaration is inert.
|
|
9917
|
-
*/
|
|
9918
|
-
var LogChannelDescriptorSchema = object({
|
|
9919
|
-
/**
|
|
9920
|
-
* Dotted `area.thing`, unique across the workspace. `area` is conventionally
|
|
9921
|
-
* the addon's short name so an operator reading a channel list can tell who
|
|
9922
|
-
* owns it without a second lookup.
|
|
9923
|
-
*/
|
|
9924
|
-
name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
|
|
9925
|
-
/** One sentence: what the operator will SEE after arming it. */
|
|
9926
|
-
description: string().min(1),
|
|
9927
|
-
/** The level its lines are emitted at. Never below `info`. */
|
|
9928
|
-
defaultLevel: LogChannelLevelSchema,
|
|
9929
|
-
/**
|
|
9930
|
-
* Whether this channel can be narrowed to a camera.
|
|
9931
|
-
*
|
|
9932
|
-
* `true` is a PROMISE with two halves, and both must hold: the gate is
|
|
9933
|
-
* consulted with the numeric device id, AND every line the channel admits
|
|
9934
|
-
* carries `tags: { deviceId }` with that same numeric id. The second half is
|
|
9935
|
-
* what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
|
|
9936
|
-
* keeps `deviceId` out of the stream labels for cardinality, so the tag in
|
|
9937
|
-
* the body is the only way to filter.
|
|
9938
|
-
*
|
|
9939
|
-
* A channel whose lines carry the device only in `meta` (or not at all) is
|
|
9940
|
-
* declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
|
|
9941
|
-
* the operator narrows to one camera, sees nothing, and concludes the code
|
|
9942
|
-
* path was never taken.
|
|
9943
|
-
*/
|
|
9944
|
-
perDevice: boolean()
|
|
9945
|
-
});
|
|
9946
|
-
/**
|
|
9947
|
-
* An armed window over one channel, as the document hands it to a mirror.
|
|
9948
|
-
*
|
|
9949
|
-
* A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
|
|
9950
|
-
* expires by itself, which is the one failure a boolean cannot avoid.
|
|
9951
|
-
*/
|
|
9952
|
-
var LogChannelWindowSchema = object({
|
|
9953
|
-
channel: string().min(1),
|
|
9954
|
-
/** Epoch ms the window closes at. */
|
|
9955
|
-
armedUntilMs: number(),
|
|
9956
|
-
/** `null` = every camera. A non-empty list narrows to those numeric ids. */
|
|
9957
|
-
deviceIds: array(number().int()).readonly().nullable()
|
|
9958
|
-
});
|
|
10301
|
+
}
|
|
10302
|
+
/** What eviction may do to this location. */
|
|
10303
|
+
function evictionPolicyOfLocation(location) {
|
|
10304
|
+
return evictionPolicyForMode(resolveLocationMode(location));
|
|
10305
|
+
}
|
|
9959
10306
|
/**
|
|
9960
|
-
*
|
|
10307
|
+
* `StorageLocationType` — an addon-declared id that identifies the *kind* of
|
|
10308
|
+
* storage a location serves. Defined here (not in `capabilities/storage.cap.ts`)
|
|
10309
|
+
* so the persisted record schema and the consumer-facing cap can both consume it
|
|
10310
|
+
* without forming a circular import. The `storage` cap re-exports it
|
|
10311
|
+
* verbatim for back-compat.
|
|
9961
10312
|
*
|
|
9962
|
-
*
|
|
9963
|
-
*
|
|
9964
|
-
*
|
|
9965
|
-
|
|
9966
|
-
var MAX_KEYS = 1024;
|
|
9967
|
-
/**
|
|
9968
|
-
* Where reasons past {@link MAX_REASONS_PER_KEY} go.
|
|
10313
|
+
* This Zod schema is the **authoritative source** for `StorageLocationType`.
|
|
10314
|
+
* The TS alias in `./storage.ts` re-exports `z.infer<typeof
|
|
10315
|
+
* StorageLocationTypeSchema>` so the wire surface (cap) and the legacy
|
|
10316
|
+
* `IStorageProvider` interface stay in lockstep.
|
|
9969
10317
|
*
|
|
9970
|
-
*
|
|
9971
|
-
*
|
|
10318
|
+
* The type is now an **open string** (not a closed enum) — addons declare
|
|
10319
|
+
* their own location kinds via `StorageLocationDeclaration.id`. The regex
|
|
10320
|
+
* enforces a safe id format: lowercase-start, alphanumeric + hyphens.
|
|
9972
10321
|
*/
|
|
9973
|
-
var
|
|
9974
|
-
/** `deviceId` + `family` + optional `variant`, flattened into the map key. */
|
|
9975
|
-
function counterKey(deviceId, family, variant) {
|
|
9976
|
-
return variant === void 0 ? `${deviceId}${family}` : `${deviceId}${family}${variant}`;
|
|
9977
|
-
}
|
|
10322
|
+
var StorageLocationTypeSchema = string().regex(/^[a-z][a-zA-Z0-9-]*$/);
|
|
9978
10323
|
/**
|
|
9979
|
-
*
|
|
10324
|
+
* Persisted record for a storage location instance. Operators can register
|
|
10325
|
+
* multiple instances for multi-cardinality types (e.g. two `backups`
|
|
10326
|
+
* locations with different `providerId`s). Cardinality is now declared per
|
|
10327
|
+
* location via `StorageLocationDeclaration.cardinality` — the static
|
|
10328
|
+
* `STORAGE_LOCATION_CARDINALITY` map has been removed.
|
|
9980
10329
|
*
|
|
9981
|
-
*
|
|
9982
|
-
*
|
|
10330
|
+
* `id` is a stable namespaced string of the form `<type>:<slug>`.
|
|
10331
|
+
* The seed names its first instance `<type>:default` — a NAME, not a flag.
|
|
10332
|
+
* There is no default location any more (D383): `enabled` is the whole write
|
|
10333
|
+
* model, and a bare type ref resolves to the sole location of the type, or —
|
|
10334
|
+
* transitionally, only while legacy NULL-stamped rows exist — to the row whose
|
|
10335
|
+
* slug is `default`.
|
|
10336
|
+
*
|
|
10337
|
+
* `isSystem` is a legacy persisted flag. Seed still creates the initial
|
|
10338
|
+
* `<type>:default` locations; the flag is no longer a lock, a badge, or a
|
|
10339
|
+
* prune selector. New writes leave it false. Deletion is gated on uniqueness
|
|
10340
|
+
* / last-enabled, not on this bit.
|
|
9983
10341
|
*/
|
|
9984
|
-
var
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9989
|
-
|
|
9990
|
-
this.maxKeys = maxKeys;
|
|
9991
|
-
this.maxReasons = maxReasons;
|
|
9992
|
-
}
|
|
10342
|
+
var StorageLocationSchema = object({
|
|
10343
|
+
id: string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/),
|
|
10344
|
+
type: string(),
|
|
10345
|
+
displayName: string().min(1),
|
|
10346
|
+
providerId: string().min(1),
|
|
10347
|
+
config: record(string(), unknown()),
|
|
9993
10348
|
/**
|
|
9994
|
-
*
|
|
9995
|
-
*
|
|
9996
|
-
*
|
|
9997
|
-
*
|
|
9998
|
-
*
|
|
10349
|
+
* Cluster node this location physically lives on. REQUIRED for node-local
|
|
10350
|
+
* providers (filesystem — the path exists on one node's disk), null/absent
|
|
10351
|
+
* for node-agnostic providers (S3/SFTP/WebDAV, reachable from any node).
|
|
10352
|
+
* `'hub'` is the hub node. Validated against the provider's `nodeLocal`
|
|
10353
|
+
* flag at upsert time, not here (the schema is provider-agnostic).
|
|
9999
10354
|
*/
|
|
10000
|
-
|
|
10001
|
-
|
|
10002
|
-
}
|
|
10003
|
-
/** Counters currently held. */
|
|
10004
|
-
get size() {
|
|
10005
|
-
return this.counters.size;
|
|
10006
|
-
}
|
|
10355
|
+
nodeId: string().optional(),
|
|
10356
|
+
isSystem: boolean().default(false),
|
|
10007
10357
|
/**
|
|
10008
|
-
*
|
|
10358
|
+
* THE write switch, and the only one (D383). `enabled: true` means every
|
|
10359
|
+
* consumer that chooses a write target for this type may write here, and all
|
|
10360
|
+
* enabled locations of a type are used TOGETHER; `false` means read-only —
|
|
10361
|
+
* still read, still played back, still age-swept, still drained, never
|
|
10362
|
+
* written.
|
|
10009
10363
|
*
|
|
10010
|
-
*
|
|
10011
|
-
*
|
|
10012
|
-
*
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
const key = counterKey(observation.deviceId, observation.family, observation.variant);
|
|
10017
|
-
let counter = this.counters.get(key);
|
|
10018
|
-
if (counter === void 0) {
|
|
10019
|
-
if (this.counters.size >= this.maxKeys) {
|
|
10020
|
-
this.refused += 1;
|
|
10021
|
-
return;
|
|
10022
|
-
}
|
|
10023
|
-
counter = {
|
|
10024
|
-
deviceId: observation.deviceId,
|
|
10025
|
-
family: observation.family,
|
|
10026
|
-
variant: observation.variant,
|
|
10027
|
-
sinceMs: nowMs,
|
|
10028
|
-
attempts: 0,
|
|
10029
|
-
succeeded: 0,
|
|
10030
|
-
reasons: /* @__PURE__ */ new Map()
|
|
10031
|
-
};
|
|
10032
|
-
this.counters.set(key, counter);
|
|
10033
|
-
}
|
|
10034
|
-
counter.attempts += 1;
|
|
10035
|
-
if (observation.reason === void 0) {
|
|
10036
|
-
counter.succeeded += 1;
|
|
10037
|
-
return;
|
|
10038
|
-
}
|
|
10039
|
-
const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
|
|
10040
|
-
counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
|
|
10041
|
-
}
|
|
10042
|
-
/** Read every counter. Never mutates — see the module docblock. */
|
|
10043
|
-
snapshot(nowMs) {
|
|
10044
|
-
const out = [];
|
|
10045
|
-
for (const counter of this.counters.values()) out.push({
|
|
10046
|
-
deviceId: counter.deviceId,
|
|
10047
|
-
family: counter.family,
|
|
10048
|
-
...counter.variant !== void 0 ? { variant: counter.variant } : {},
|
|
10049
|
-
sinceMs: counter.sinceMs,
|
|
10050
|
-
atMs: nowMs,
|
|
10051
|
-
attempts: counter.attempts,
|
|
10052
|
-
succeeded: counter.succeeded,
|
|
10053
|
-
reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
|
|
10054
|
-
reason,
|
|
10055
|
-
count
|
|
10056
|
-
})).toSorted((a, b) => b.count - a.count)
|
|
10057
|
-
});
|
|
10058
|
-
return out;
|
|
10059
|
-
}
|
|
10060
|
-
/** Drop everything (host disposal). */
|
|
10061
|
-
clear() {
|
|
10062
|
-
this.counters.clear();
|
|
10063
|
-
}
|
|
10064
|
-
};
|
|
10065
|
-
var ClipModelMetaSchema = object({
|
|
10066
|
-
/** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
|
|
10067
|
-
textModelId: string().min(1),
|
|
10068
|
-
/** Output dimension of BOTH towers. A vector of any other length is refused. */
|
|
10069
|
-
embeddingDim: number().int().positive(),
|
|
10070
|
-
/**
|
|
10071
|
-
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
10072
|
-
* per index: two models share an index only when their vectors are ranked
|
|
10073
|
-
* against each other, which for CLIP means never across families. MobileCLIP
|
|
10074
|
-
* S1 and S2 share `object-clip` for history (separated by the row's
|
|
10075
|
-
* `modelId`); any other model gets its own.
|
|
10364
|
+
* OPTIONAL only for the wire: an upsert that omits it means "leave what is
|
|
10365
|
+
* stored" on an update and "born inert unless it is the first location of its
|
|
10366
|
+
* type" on a create. On a PERSISTED row absence is legacy and it means
|
|
10367
|
+
* enabled — {@link isLocationEnabled} is the one place that says so, and the
|
|
10368
|
+
* orchestrator stamps every flagless row `true` once at hydrate so absence
|
|
10369
|
+
* stops existing rather than being re-derived on every read.
|
|
10076
10370
|
*/
|
|
10077
|
-
|
|
10371
|
+
enabled: boolean().optional(),
|
|
10078
10372
|
/**
|
|
10079
|
-
*
|
|
10080
|
-
*
|
|
10081
|
-
*
|
|
10082
|
-
*
|
|
10083
|
-
*
|
|
10373
|
+
* THE state of this location (D385), and the only authority on what may be
|
|
10374
|
+
* written, read or evicted here. Interpreted in exactly one place —
|
|
10375
|
+
* `storage-location-mode.ts` — which also folds the legacy
|
|
10376
|
+
* `enabled` / `config.readOnly` pair into a mode so an old row is never
|
|
10377
|
+
* ambiguous.
|
|
10378
|
+
*
|
|
10379
|
+
* OPTIONAL only for the wire and for rows written before D385: absence is
|
|
10380
|
+
* resolved by `resolveLocationMode`, and the orchestrator stamps every
|
|
10381
|
+
* unstamped row ONCE at hydrate so absence stops existing rather than being
|
|
10382
|
+
* re-derived on every read. `enabled` survives one release as a DERIVED
|
|
10383
|
+
* mirror (`mode === 'active'`); `withLocationMode` is the only writer of
|
|
10384
|
+
* either, so the two cannot disagree.
|
|
10084
10385
|
*/
|
|
10085
|
-
|
|
10086
|
-
/**
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10386
|
+
mode: StorageLocationModeSchema.optional(),
|
|
10387
|
+
/** COMPUTED at read time by the orchestrator (statfs of the backing volume
|
|
10388
|
+
* for node-local locations it can reach) — never persisted, absent when the
|
|
10389
|
+
* volume is remote/unreachable. The single capacity truth every UI reads. */
|
|
10390
|
+
capacity: object({
|
|
10391
|
+
totalBytes: number(),
|
|
10392
|
+
availableBytes: number()
|
|
10393
|
+
}).nullable().optional(),
|
|
10090
10394
|
/**
|
|
10091
|
-
*
|
|
10092
|
-
*
|
|
10093
|
-
*
|
|
10395
|
+
* How much of that volume CamStack ITSELF holds on this location (D388) —
|
|
10396
|
+
* COMPUTED at read time from the `storage-occupancy` providers' own figures,
|
|
10397
|
+
* never persisted, never a filesystem walk.
|
|
10398
|
+
*
|
|
10399
|
+
* **ABSENT MEANS UNKNOWN, never zero.** No provider has reported for this
|
|
10400
|
+
* location yet — nobody stores here, the owning addon is down, or the first
|
|
10401
|
+
* refresh has not completed. A UI must omit the segment rather than draw it
|
|
10402
|
+
* at zero, which would claim we occupy nothing (D315). It is an OBJECT and
|
|
10403
|
+
* not a bare number precisely so that a `?? 0` on the consuming side has to
|
|
10404
|
+
* be spelled out loud instead of appearing by accident.
|
|
10405
|
+
*
|
|
10406
|
+
* `measuredAtMs` is the OLDEST contributing measurement, so it is honest
|
|
10407
|
+
* about the whole figure rather than about its freshest part.
|
|
10094
10408
|
*/
|
|
10095
|
-
|
|
10409
|
+
owned: object({
|
|
10410
|
+
bytes: number().int().nonnegative(),
|
|
10411
|
+
measuredAtMs: number().int().nonnegative()
|
|
10412
|
+
}).optional(),
|
|
10413
|
+
createdAt: number(),
|
|
10414
|
+
updatedAt: number()
|
|
10096
10415
|
});
|
|
10097
|
-
|
|
10098
|
-
"loose",
|
|
10099
|
-
"balanced",
|
|
10100
|
-
"strict"
|
|
10101
|
-
]);
|
|
10102
|
-
var CLIP_STRICTNESS_FACTOR = {
|
|
10103
|
-
loose: 1,
|
|
10104
|
-
balanced: 1.25,
|
|
10105
|
-
strict: 1.5
|
|
10106
|
-
};
|
|
10107
|
-
/**
|
|
10108
|
-
* The cosine floor a search runs with.
|
|
10109
|
-
*
|
|
10110
|
-
* An explicit `minScore` is honoured verbatim: it is a number a caller chose,
|
|
10111
|
-
* and silently rescaling it would make the same URL mean different things on
|
|
10112
|
-
* two installs. Everything else is relative to the model.
|
|
10113
|
-
*/
|
|
10114
|
-
function resolveClipSearchMinScore(meta, request) {
|
|
10115
|
-
if (request.minScore !== void 0) return request.minScore;
|
|
10116
|
-
const factor = CLIP_STRICTNESS_FACTOR[request.strictness ?? "loose"];
|
|
10117
|
-
return Math.min(1, Math.round(meta.searchMinScore * factor * 1e3) / 1e3);
|
|
10118
|
-
}
|
|
10119
|
-
var MODEL_FORMATS = [
|
|
10120
|
-
"onnx",
|
|
10121
|
-
"coreml",
|
|
10122
|
-
"openvino",
|
|
10123
|
-
"tflite",
|
|
10124
|
-
"pt",
|
|
10125
|
-
"gguf"
|
|
10126
|
-
];
|
|
10416
|
+
object({ isDefault: boolean().optional() });
|
|
10127
10417
|
/**
|
|
10128
|
-
*
|
|
10418
|
+
* How far a `drain` has got (D386) — the read a UI renders, and nothing more.
|
|
10129
10419
|
*
|
|
10130
|
-
*
|
|
10131
|
-
*
|
|
10132
|
-
*
|
|
10133
|
-
*
|
|
10134
|
-
* - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
|
|
10135
|
-
* files fetched from the SAME remote directory as `url` and stored flat
|
|
10136
|
-
* alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
|
|
10137
|
-
* weights next to `camstack-yolov9t.xml`.
|
|
10138
|
-
*/
|
|
10139
|
-
var ModelFormatEntrySchema = object({
|
|
10140
|
-
url: string(),
|
|
10141
|
-
sizeMB: number(),
|
|
10142
|
-
/** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
|
|
10143
|
-
isDirectory: boolean().optional(),
|
|
10144
|
-
/** Multi-file payload (directory members or sibling files). */
|
|
10145
|
-
files: array(string()).readonly().optional(),
|
|
10146
|
-
/** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
|
|
10147
|
-
runtimes: array(_enum(["python"])).readonly().optional()
|
|
10148
|
-
});
|
|
10149
|
-
/**
|
|
10150
|
-
* Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
|
|
10151
|
-
* The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
|
|
10420
|
+
* `estimatedEmptyAtMs` is derived from the growth the ratchet has actually
|
|
10421
|
+
* OBSERVED and is `null` when it has observed none. Never a fabricated date: a
|
|
10422
|
+
* drain with no observed growth has no honest ETA, and inventing one is how an
|
|
10423
|
+
* operator learns not to believe the screen.
|
|
10152
10424
|
*/
|
|
10153
|
-
var
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10425
|
+
var StorageDrainProgressSchema = object({
|
|
10426
|
+
locationId: string(),
|
|
10427
|
+
startedAtMs: number(),
|
|
10428
|
+
startBytes: number(),
|
|
10429
|
+
bytesRemaining: number(),
|
|
10430
|
+
drained: boolean(),
|
|
10431
|
+
estimatedEmptyAtMs: number().nullable()
|
|
10157
10432
|
});
|
|
10158
10433
|
/**
|
|
10159
|
-
*
|
|
10160
|
-
*
|
|
10161
|
-
*
|
|
10162
|
-
*
|
|
10434
|
+
* Reference accepted by consumer-facing `api.storage.*` calls.
|
|
10435
|
+
* Either:
|
|
10436
|
+
* - a `StorageLocationType` (e.g. `'backups'`) → the sole location of that type
|
|
10437
|
+
* (transitionally, the `<type>:default`-slugged row when several exist)
|
|
10438
|
+
* - a fully-qualified id (e.g. `'backups:nas-01'`) → addresses a specific instance
|
|
10439
|
+
*
|
|
10440
|
+
* The orchestrator's `resolveRef(ref)` handles both cases.
|
|
10163
10441
|
*/
|
|
10164
|
-
var
|
|
10165
|
-
onnx: ModelFormatEntrySchema.optional(),
|
|
10166
|
-
coreml: ModelFormatEntrySchema.optional(),
|
|
10167
|
-
openvino: ModelFormatEntrySchema.optional(),
|
|
10168
|
-
tflite: ModelFormatEntrySchema.optional(),
|
|
10169
|
-
pt: ModelFormatEntrySchema.optional(),
|
|
10170
|
-
gguf: ModelFormatEntrySchema.optional()
|
|
10171
|
-
});
|
|
10442
|
+
var StorageLocationRefSchema = union([StorageLocationTypeSchema, string().regex(/^[a-z][a-zA-Z0-9-]*:[a-zA-Z0-9-]+$/)]);
|
|
10172
10443
|
/**
|
|
10173
|
-
*
|
|
10174
|
-
*
|
|
10175
|
-
*
|
|
10176
|
-
*
|
|
10177
|
-
*
|
|
10178
|
-
*
|
|
10444
|
+
* `StorageLocationDeclaration` — a single storage-location entry declared by
|
|
10445
|
+
* an addon in its `package.json` under `camstack.storageLocations`.
|
|
10446
|
+
*
|
|
10447
|
+
* Design intent:
|
|
10448
|
+
* - **Addon declares its needs** — each addon describes the logical storage
|
|
10449
|
+
* slots it requires (e.g. `recordings`, `recordingsLow`) without caring
|
|
10450
|
+
* about the physical path.
|
|
10451
|
+
* - **Kernel aggregates** — at boot the kernel collects declarations from all
|
|
10452
|
+
* installed addons, deduplicates by `id`, and exposes the union via the
|
|
10453
|
+
* storage-locations settings surface.
|
|
10454
|
+
* - **Orchestrator seeds** — for every declared `id` the orchestrator ensures
|
|
10455
|
+
* at least one instance named `<id>:default` is present, using
|
|
10456
|
+
* `defaultsTo` to inherit the resolved root from another location when the
|
|
10457
|
+
* declaration is a derivative slot (e.g. `recordingsLow` defaults to
|
|
10458
|
+
* `recordings`).
|
|
10459
|
+
* - **ids are global** — `id` values are shared across the entire deployment;
|
|
10460
|
+
* two addons declaring the same `id` must agree on `cardinality` (validated
|
|
10461
|
+
* at kernel aggregation time, not here).
|
|
10179
10462
|
*/
|
|
10180
|
-
var ModelVariantGroupSchema = object({
|
|
10181
|
-
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
10182
|
-
family: string(),
|
|
10183
|
-
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
10184
|
-
tier: string(),
|
|
10185
|
-
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
10186
|
-
precision: _enum(["fp32", "int8"]).optional(),
|
|
10187
|
-
/**
|
|
10188
|
-
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
10189
|
-
* latency-optimized export (e.g. ReLU-activation variant) — the slot the
|
|
10190
|
-
* future performance variants plug into.
|
|
10191
|
-
*/
|
|
10192
|
-
optimization: _enum(["standard", "fast"]).optional(),
|
|
10193
|
-
/**
|
|
10194
|
-
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
10195
|
-
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
10196
|
-
* cheap latency lever — especially on Apple ANE and the Intel N100 — at a
|
|
10197
|
-
* small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
|
|
10198
|
-
* the group so the selector can offer it as a variant axis.
|
|
10199
|
-
*/
|
|
10200
|
-
resolution: number().int().positive().optional()
|
|
10201
|
-
});
|
|
10202
|
-
var ModelProviderIdSchema = _enum([
|
|
10203
|
-
"camstack",
|
|
10204
|
-
"frigate",
|
|
10205
|
-
"scrypted",
|
|
10206
|
-
"custom"
|
|
10207
|
-
]);
|
|
10208
10463
|
/**
|
|
10209
|
-
*
|
|
10210
|
-
*
|
|
10211
|
-
*
|
|
10212
|
-
* are per upstream. A persisted custom-model row written when this field was a
|
|
10213
|
-
* string still parses — the string is lifted into `{ weights }`.
|
|
10464
|
+
* `StorageAccess` — how the service that DECLARED a storage-location kind
|
|
10465
|
+
* actually reaches the bytes. It is the constraint that decides which
|
|
10466
|
+
* `storage-provider`s may back a location of that kind.
|
|
10214
10467
|
*
|
|
10215
|
-
*
|
|
10216
|
-
* `
|
|
10217
|
-
*
|
|
10468
|
+
* - `'local-path'` — the service asks `storage.resolve` for a path string and
|
|
10469
|
+
* then does its own `node:fs` I/O on it (the recorder's segment writer, the
|
|
10470
|
+
* post-analysis media roots). Only a provider that serves a genuine local
|
|
10471
|
+
* filesystem (`getProviderInfo().nodeLocal === true`) can satisfy that: a
|
|
10472
|
+
* remote provider's `resolve` returns a path on the REMOTE host, and
|
|
10473
|
+
* `fs.readdir` of it on this node either fails or — far worse — succeeds
|
|
10474
|
+
* against a same-named local directory that is something else entirely.
|
|
10475
|
+
*
|
|
10476
|
+
* - `'cap-mediated'` — every byte travels through the `storage` cap
|
|
10477
|
+
* (`read`/`write`, or `beginUpload`/`writeChunk`/`finalizeUpload`). The
|
|
10478
|
+
* service never sees a path, so any provider can back it. `backups` is the
|
|
10479
|
+
* one kind that qualifies today.
|
|
10480
|
+
*
|
|
10481
|
+
* Before this existed, `recordings` was unreachable by SFTP/S3/WebDAV only as
|
|
10482
|
+
* an EMERGENT property of how the recorder happened to be written. Nothing
|
|
10483
|
+
* refused the configuration; the first write simply went somewhere wrong, and
|
|
10484
|
+
* a recording write that goes wrong surfaces as a silent black window rather
|
|
10485
|
+
* than an error (the read path does not `stat`). This turns that accident into
|
|
10486
|
+
* a declared, enforced, testable refusal.
|
|
10218
10487
|
*/
|
|
10219
|
-
var
|
|
10220
|
-
|
|
10221
|
-
weights: string().min(1),
|
|
10222
|
-
/** SPDX id of the upstream code that defines / trained the network. */
|
|
10223
|
-
code: string().min(1).optional(),
|
|
10224
|
-
/** Training data whose terms add obligations (attribution, non-commercial). */
|
|
10225
|
-
data: object({
|
|
10226
|
-
name: string().min(1),
|
|
10227
|
-
terms: string().min(1),
|
|
10228
|
-
url: string().url().optional()
|
|
10229
|
-
}).optional(),
|
|
10230
|
-
/** The project the weights come from. Required on built-in entries (guard). */
|
|
10231
|
-
upstream: object({
|
|
10232
|
-
name: string().min(1),
|
|
10233
|
-
url: string().url()
|
|
10234
|
-
}).optional(),
|
|
10235
|
-
/** Where the licence text governing the weights is published. */
|
|
10236
|
-
url: string().url().optional(),
|
|
10237
|
-
/** Attribution the licence REQUIRES, verbatim. */
|
|
10238
|
-
attribution: string().min(1).optional(),
|
|
10239
|
-
/** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
|
|
10240
|
-
modifications: string().min(1).optional(),
|
|
10241
|
-
/** Who serves the file a node downloads. */
|
|
10242
|
-
hosting: _enum([
|
|
10243
|
-
"camstack-hf",
|
|
10244
|
-
"third-party",
|
|
10245
|
-
"built-in"
|
|
10246
|
-
]).optional(),
|
|
10247
|
-
/** True when the weights or their data forbid commercial use. */
|
|
10248
|
-
nonCommercial: boolean().optional()
|
|
10249
|
-
}));
|
|
10250
|
-
var ModelCatalogEntrySchema = object({
|
|
10251
|
-
id: string(),
|
|
10252
|
-
name: string(),
|
|
10253
|
-
description: string(),
|
|
10254
|
-
formats: ModelFormatsSchema,
|
|
10255
|
-
inputSize: object({
|
|
10256
|
-
width: number(),
|
|
10257
|
-
height: number()
|
|
10258
|
-
}),
|
|
10259
|
-
/**
|
|
10260
|
-
* Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
|
|
10261
|
-
* every detector / classifier / embedder. Set to 1 for a grayscale CTC text
|
|
10262
|
-
* recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
|
|
10263
|
-
* feeds a single-channel, EasyOCR-normalized tensor instead of the default
|
|
10264
|
-
* 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
|
|
10265
|
-
* Python inference pool.
|
|
10266
|
-
*/
|
|
10267
|
-
inputChannels: number().int().positive().optional(),
|
|
10268
|
-
labels: array(LabelDefinitionSchema).readonly(),
|
|
10269
|
-
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
10270
|
-
/**
|
|
10271
|
-
* `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
|
|
10272
|
-
* (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
|
|
10273
|
-
* plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
|
|
10274
|
-
* `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
|
|
10275
|
-
*/
|
|
10276
|
-
inputNormalization: _enum([
|
|
10277
|
-
"zero-one",
|
|
10278
|
-
"imagenet",
|
|
10279
|
-
"none",
|
|
10280
|
-
"scrfd"
|
|
10281
|
-
]).optional(),
|
|
10282
|
-
/**
|
|
10283
|
-
* The model already applies softmax IN-GRAPH — its raw output is a
|
|
10284
|
-
* probability distribution, not logits. When set, the `softmax`
|
|
10285
|
-
* postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
|
|
10286
|
-
* probability vector collapses it toward uniform (top-1 score craters far
|
|
10287
|
-
* below its true value, making every confidence gate meaningless). Absent ⇒
|
|
10288
|
-
* the output is raw logits and the postprocessor applies softmax (the normal
|
|
10289
|
-
* case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
|
|
10290
|
-
* TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
|
|
10291
|
-
*/
|
|
10292
|
-
outputProbabilities: boolean().optional(),
|
|
10293
|
-
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
10294
|
-
/**
|
|
10295
|
-
* Per-MODEL postprocessor override. Absent ⇒ the step's own
|
|
10296
|
-
* `StepDefinition.postprocessor` applies (the normal case — every model in a
|
|
10297
|
-
* step shares its decode). Set it when a step hosts models with DIFFERENT raw
|
|
10298
|
-
* output layouts under one slot: e.g. object-detection is `'yolo'` by default,
|
|
10299
|
-
* but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
|
|
10300
|
-
* 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
|
|
10301
|
-
* by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
|
|
10302
|
-
*/
|
|
10303
|
-
postprocessor: custom().optional(),
|
|
10488
|
+
var StorageAccessSchema = _enum(["local-path", "cap-mediated"]);
|
|
10489
|
+
var StorageLocationDeclarationSchema = object({
|
|
10304
10490
|
/**
|
|
10305
|
-
*
|
|
10306
|
-
*
|
|
10307
|
-
*
|
|
10308
|
-
* `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
|
|
10309
|
-
* face gallery with wheels and hands (D662, amends D645). An operator's
|
|
10310
|
-
* explicit value still wins over it. Resolved in ONE place —
|
|
10311
|
-
* `addon-pipeline/.../registry/effective-confidence.ts`.
|
|
10491
|
+
* Global location identifier, e.g. `recordings` or `recordingsLow`.
|
|
10492
|
+
* Must start with a lowercase letter and may contain letters, digits, and
|
|
10493
|
+
* hyphens.
|
|
10312
10494
|
*/
|
|
10313
|
-
|
|
10495
|
+
id: string().regex(/^[a-z][a-zA-Z0-9-]*$/, { message: "id must start with a lowercase letter and contain only letters, digits, or hyphens" }),
|
|
10496
|
+
/** Human-readable name shown in the admin UI. */
|
|
10497
|
+
displayName: string().min(1, { message: "displayName must not be empty" }),
|
|
10498
|
+
/** Optional longer explanation of what data this location stores. */
|
|
10499
|
+
description: string().optional(),
|
|
10314
10500
|
/**
|
|
10315
|
-
*
|
|
10316
|
-
*
|
|
10317
|
-
*
|
|
10318
|
-
*
|
|
10319
|
-
* detector that produced the parent detail must emit 5 landmarks.
|
|
10501
|
+
* `single` — exactly one instance of this location is allowed system-wide
|
|
10502
|
+
* (e.g. `logs`, `models`). The operator can edit it but not add more.
|
|
10503
|
+
* `multi` — the operator may register several instances (e.g. a second
|
|
10504
|
+
* `recordings` on a NAS for disk tiering); one is the default at any time.
|
|
10320
10505
|
*/
|
|
10321
|
-
|
|
10506
|
+
cardinality: _enum(["single", "multi"]),
|
|
10322
10507
|
/**
|
|
10323
|
-
*
|
|
10324
|
-
*
|
|
10508
|
+
* HOW the declaring service reaches the bytes — and therefore WHICH
|
|
10509
|
+
* providers may back a location of this kind. See {@link StorageAccessSchema}
|
|
10510
|
+
* and {@link STORAGE_ACCESS_FALLBACK}.
|
|
10511
|
+
*
|
|
10512
|
+
* Absent means `'local-path'`. That default is FAIL-CLOSED on purpose: it
|
|
10513
|
+
* can only over-restrict (refuse a remote provider for a kind that might
|
|
10514
|
+
* have coped) and never under-restrict. Declaring `'cap-mediated'` is the
|
|
10515
|
+
* permissive direction and is therefore never inferred — a repo guard
|
|
10516
|
+
* (`scripts/check-storage-access-declarations.ts`) refuses to let it be
|
|
10517
|
+
* reached by omission.
|
|
10325
10518
|
*/
|
|
10326
|
-
|
|
10519
|
+
access: StorageAccessSchema.optional(),
|
|
10327
10520
|
/**
|
|
10328
|
-
*
|
|
10329
|
-
*
|
|
10330
|
-
*
|
|
10331
|
-
*
|
|
10332
|
-
* the active lineup stays the coherent curated ladder without deleting a
|
|
10333
|
-
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
10334
|
-
* an explicit legacy id that has a build for the node's format.
|
|
10521
|
+
* When set, the default instance for this location inherits its resolved
|
|
10522
|
+
* root from the named location's default instance. Useful for derivative
|
|
10523
|
+
* slots (e.g. `recordingsLow` → `recordings`) so operators only need to
|
|
10524
|
+
* configure the primary location.
|
|
10335
10525
|
*/
|
|
10336
|
-
|
|
10526
|
+
defaultsTo: string().optional(),
|
|
10337
10527
|
/**
|
|
10338
|
-
*
|
|
10339
|
-
*
|
|
10340
|
-
*
|
|
10341
|
-
*
|
|
10528
|
+
* Which node root the seeded `<id>:default` instance is placed under on a
|
|
10529
|
+
* FRESH install:
|
|
10530
|
+
* - `'data'` (default) — the node's data dir (`CAMSTACK_DATA` / boot dir),
|
|
10531
|
+
* the appData volume. Right for small/durable data (logs, models).
|
|
10532
|
+
* - `'media'` — the dedicated media volume (`CAMSTACK_MEDIA_ROOT`) when that
|
|
10533
|
+
* env is set, else falls back to the data root. Right for bulky, hot media
|
|
10534
|
+
* (recordings, event media) that should stay off the appData disk.
|
|
10535
|
+
* - `'backup'` — the dedicated backup volume (`CAMSTACK_BACKUP_ROOT`, default
|
|
10536
|
+
* `/backups` in the image) so archives live on their own mount rather than
|
|
10537
|
+
* filling the appData disk. Falls back to the data root when unset.
|
|
10538
|
+
*
|
|
10539
|
+
* Only affects the seeded default's `basePath`; operators can repoint any
|
|
10540
|
+
* location afterwards, and a `defaultsTo` slot inherits its parent's root
|
|
10541
|
+
* regardless of this field. Absent (the common case) is treated as `'data'`.
|
|
10342
10542
|
*/
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10543
|
+
defaultRoot: _enum([
|
|
10544
|
+
"data",
|
|
10545
|
+
"media",
|
|
10546
|
+
"backup"
|
|
10547
|
+
]).optional()
|
|
10548
|
+
});
|
|
10549
|
+
var DecoderStatsSchema = object({
|
|
10550
|
+
inputFps: number(),
|
|
10551
|
+
outputFps: number(),
|
|
10552
|
+
avgDecodeTimeMs: number(),
|
|
10553
|
+
droppedFrames: number(),
|
|
10347
10554
|
/**
|
|
10348
|
-
*
|
|
10349
|
-
*
|
|
10350
|
-
*
|
|
10555
|
+
* Pull-mode adaptive-fps telemetry (optional — only pull sessions run the
|
|
10556
|
+
* lag-driven controller; push sessions omit these). `lagMs` is the EWMA of
|
|
10557
|
+
* the decoder's real-time drift (rising = falling behind live); `adaptiveFps`
|
|
10558
|
+
* is the current lag-throttled emit rate (≤ `effectiveFps` ceiling).
|
|
10351
10559
|
*/
|
|
10352
|
-
|
|
10560
|
+
lagMs: number().optional(),
|
|
10561
|
+
effectiveFps: number().optional(),
|
|
10562
|
+
adaptiveFps: number().optional()
|
|
10563
|
+
});
|
|
10564
|
+
var DecoderSessionConfigSchema = object({
|
|
10565
|
+
codec: string(),
|
|
10566
|
+
maxFps: number().default(0),
|
|
10567
|
+
outputFormat: _enum([
|
|
10568
|
+
"jpeg",
|
|
10569
|
+
"rgb",
|
|
10570
|
+
"bgr",
|
|
10571
|
+
"yuv420",
|
|
10572
|
+
"gray"
|
|
10573
|
+
]).default("jpeg"),
|
|
10574
|
+
scale: number().default(1),
|
|
10575
|
+
width: number().optional(),
|
|
10576
|
+
height: number().optional(),
|
|
10353
10577
|
/**
|
|
10354
|
-
*
|
|
10355
|
-
*
|
|
10356
|
-
*
|
|
10357
|
-
*
|
|
10358
|
-
*
|
|
10359
|
-
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
10360
|
-
* is a presentation overlay resolved back to an `id`.
|
|
10578
|
+
* Identifier of the camera this decoder session serves. Optional
|
|
10579
|
+
* because the cap is generic (any caller could request decode), but
|
|
10580
|
+
* stream-broker passes it so decoder logs include `deviceId` for
|
|
10581
|
+
* per-camera filtering when diagnosing failures (e.g. node-av
|
|
10582
|
+
* sendPacket errors on a single hung camera).
|
|
10361
10583
|
*/
|
|
10362
|
-
|
|
10584
|
+
deviceId: number().int().nonnegative().optional(),
|
|
10363
10585
|
/**
|
|
10364
|
-
*
|
|
10365
|
-
*
|
|
10366
|
-
*
|
|
10586
|
+
* Free-form tag for log scoping. Stream-broker uses
|
|
10587
|
+
* `broker:<deviceId>/<profile>`. Decoder session logger surfaces it
|
|
10588
|
+
* on every line so `grep tag=broker:5/high` filters one camera
|
|
10589
|
+
* profile cleanly.
|
|
10367
10590
|
*/
|
|
10368
|
-
|
|
10591
|
+
tag: string().optional(),
|
|
10369
10592
|
/**
|
|
10370
|
-
*
|
|
10371
|
-
*
|
|
10372
|
-
*
|
|
10593
|
+
* Where the session delivers decoded frames (Phase 5 / D9):
|
|
10594
|
+
*
|
|
10595
|
+
* - `'callback'` (default) — the legacy pixel path: decoded frames are
|
|
10596
|
+
* buffered as `DecodedFrame`s and drained via `pullFrames`.
|
|
10597
|
+
* - `'shm'` — the shared-memory frame plane: decoded frames are written
|
|
10598
|
+
* into an OS shared-memory ring and drained as zero-pixel
|
|
10599
|
+
* `FrameHandle`s via `pullHandles`. A session is one mode or the
|
|
10600
|
+
* other — `pullFrames` returns nothing for an `'shm'` session and
|
|
10601
|
+
* `pullHandles` returns nothing for a `'callback'` session.
|
|
10373
10602
|
*/
|
|
10374
|
-
|
|
10603
|
+
frameSink: _enum(["callback", "shm"]).default("callback"),
|
|
10375
10604
|
/**
|
|
10376
|
-
*
|
|
10377
|
-
*
|
|
10378
|
-
*
|
|
10379
|
-
*
|
|
10605
|
+
* Per-camera decoder DEBUG facility. When `true`, a pull-mode session emits
|
|
10606
|
+
* a throttled (~1Hz) structured `decoder debug` line (effective/adaptive fps,
|
|
10607
|
+
* real-time lag, dropped-frame delta, avg decode time, hwaccel). Mirrors the
|
|
10608
|
+
* stream-broker's `streamingDebug` gate — off by default so production logs
|
|
10609
|
+
* stay quiet and the emit path pays zero per-frame cost when disabled.
|
|
10380
10610
|
*/
|
|
10381
|
-
|
|
10382
|
-
});
|
|
10383
|
-
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
10384
|
-
format: literal("openvino"),
|
|
10385
|
-
precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
|
|
10386
|
-
}), object({ format: literal("coreml") })]);
|
|
10387
|
-
var ModelConvertMetadataSchema = object({
|
|
10388
|
-
id: string().regex(/^[a-zA-Z0-9._-]+$/),
|
|
10389
|
-
name: string(),
|
|
10390
|
-
labels: array(LabelDefinitionSchema).readonly(),
|
|
10391
|
-
inputSize: object({
|
|
10392
|
-
width: number(),
|
|
10393
|
-
height: number()
|
|
10394
|
-
}),
|
|
10395
|
-
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
10396
|
-
inputNormalization: _enum([
|
|
10397
|
-
"zero-one",
|
|
10398
|
-
"imagenet",
|
|
10399
|
-
"none",
|
|
10400
|
-
"scrfd"
|
|
10401
|
-
]).optional(),
|
|
10402
|
-
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
10403
|
-
outputFormat: _enum([
|
|
10404
|
-
"yolo",
|
|
10405
|
-
"ssd",
|
|
10406
|
-
"embedding",
|
|
10407
|
-
"classification",
|
|
10408
|
-
"ocr",
|
|
10409
|
-
"segmentation"
|
|
10410
|
-
]),
|
|
10411
|
-
faceAlignment: boolean().optional(),
|
|
10412
|
-
classMap: DetectionCatalogClassMapSchema.optional()
|
|
10413
|
-
});
|
|
10414
|
-
var ConvertResultSchema = object({
|
|
10415
|
-
entry: ModelCatalogEntrySchema,
|
|
10416
|
-
artifacts: array(object({
|
|
10417
|
-
format: _enum(MODEL_FORMATS),
|
|
10418
|
-
precision: _enum(["fp16", "int8"]).optional(),
|
|
10419
|
-
sizeMB: number(),
|
|
10420
|
-
validated: boolean(),
|
|
10421
|
-
files: array(string()).readonly()
|
|
10422
|
-
})).readonly()
|
|
10611
|
+
debug: boolean().optional()
|
|
10423
10612
|
});
|
|
10424
10613
|
/**
|
|
10425
|
-
*
|
|
10426
|
-
*
|
|
10427
|
-
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
10428
|
-
*/
|
|
10429
|
-
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
10430
|
-
* the failure is anchored to a character (author-facing inline feedback). */
|
|
10431
|
-
var ExpressionParseError = class extends Error {
|
|
10432
|
-
position;
|
|
10433
|
-
constructor(message, position) {
|
|
10434
|
-
super(message);
|
|
10435
|
-
this.name = "ExpressionParseError";
|
|
10436
|
-
this.position = position;
|
|
10437
|
-
}
|
|
10438
|
-
};
|
|
10439
|
-
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
10440
|
-
* result, unknown builtin, step-budget exceeded). */
|
|
10441
|
-
var ExpressionEvalError = class extends Error {
|
|
10442
|
-
constructor(message) {
|
|
10443
|
-
super(message);
|
|
10444
|
-
this.name = "ExpressionEvalError";
|
|
10445
|
-
}
|
|
10446
|
-
};
|
|
10447
|
-
function asFiniteNumber(value, name, index) {
|
|
10448
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
10449
|
-
return value;
|
|
10450
|
-
}
|
|
10451
|
-
function asString$1(value, name, index) {
|
|
10452
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
10453
|
-
return value;
|
|
10454
|
-
}
|
|
10455
|
-
function finiteResult(value, name) {
|
|
10456
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
10457
|
-
return value;
|
|
10458
|
-
}
|
|
10459
|
-
function allFiniteNumbers(args, name) {
|
|
10460
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
10461
|
-
}
|
|
10462
|
-
function asBoolean$1(value, name, index) {
|
|
10463
|
-
if (typeof value !== "boolean") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a boolean`);
|
|
10464
|
-
return value;
|
|
10465
|
-
}
|
|
10466
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
10467
|
-
var table = {
|
|
10468
|
-
min: {
|
|
10469
|
-
minArgs: 1,
|
|
10470
|
-
maxArgs: INF,
|
|
10471
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
10472
|
-
},
|
|
10473
|
-
max: {
|
|
10474
|
-
minArgs: 1,
|
|
10475
|
-
maxArgs: INF,
|
|
10476
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
10477
|
-
},
|
|
10478
|
-
abs: {
|
|
10479
|
-
minArgs: 1,
|
|
10480
|
-
maxArgs: 1,
|
|
10481
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
10482
|
-
},
|
|
10483
|
-
floor: {
|
|
10484
|
-
minArgs: 1,
|
|
10485
|
-
maxArgs: 1,
|
|
10486
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
10487
|
-
},
|
|
10488
|
-
ceil: {
|
|
10489
|
-
minArgs: 1,
|
|
10490
|
-
maxArgs: 1,
|
|
10491
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
10492
|
-
},
|
|
10493
|
-
sqrt: {
|
|
10494
|
-
minArgs: 1,
|
|
10495
|
-
maxArgs: 1,
|
|
10496
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
10497
|
-
},
|
|
10498
|
-
round: {
|
|
10499
|
-
minArgs: 1,
|
|
10500
|
-
maxArgs: 2,
|
|
10501
|
-
apply: (args) => {
|
|
10502
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
10503
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
10504
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
10505
|
-
const factor = 10 ** digits;
|
|
10506
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
10507
|
-
}
|
|
10508
|
-
},
|
|
10509
|
-
pow: {
|
|
10510
|
-
minArgs: 2,
|
|
10511
|
-
maxArgs: 2,
|
|
10512
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
10513
|
-
},
|
|
10514
|
-
clamp: {
|
|
10515
|
-
minArgs: 3,
|
|
10516
|
-
maxArgs: 3,
|
|
10517
|
-
apply: (args) => {
|
|
10518
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
10519
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
10520
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
10521
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
10522
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
10523
|
-
}
|
|
10524
|
-
},
|
|
10525
|
-
avg: {
|
|
10526
|
-
minArgs: 1,
|
|
10527
|
-
maxArgs: INF,
|
|
10528
|
-
apply: (args) => {
|
|
10529
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
10530
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
10531
|
-
}
|
|
10532
|
-
},
|
|
10533
|
-
sum: {
|
|
10534
|
-
minArgs: 1,
|
|
10535
|
-
maxArgs: INF,
|
|
10536
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
10537
|
-
},
|
|
10538
|
-
coalesce: {
|
|
10539
|
-
minArgs: 1,
|
|
10540
|
-
maxArgs: INF,
|
|
10541
|
-
apply: (args) => {
|
|
10542
|
-
for (const a of args) if (a !== null) return a;
|
|
10543
|
-
return null;
|
|
10544
|
-
}
|
|
10545
|
-
},
|
|
10546
|
-
age: {
|
|
10547
|
-
minArgs: 2,
|
|
10548
|
-
maxArgs: 2,
|
|
10549
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
10550
|
-
},
|
|
10551
|
-
convert: {
|
|
10552
|
-
minArgs: 3,
|
|
10553
|
-
maxArgs: 3,
|
|
10554
|
-
apply: (args, hooks) => {
|
|
10555
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
10556
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
10557
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
10558
|
-
if (hooks.convert) {
|
|
10559
|
-
const out = hooks.convert(x, from, to);
|
|
10560
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
10561
|
-
return finiteResult(out, "convert");
|
|
10562
|
-
}
|
|
10563
|
-
if (from === to) return x;
|
|
10564
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
10565
|
-
}
|
|
10566
|
-
},
|
|
10567
|
-
any: {
|
|
10568
|
-
minArgs: 1,
|
|
10569
|
-
maxArgs: INF,
|
|
10570
|
-
apply: (args) => args.map((a, i) => asBoolean$1(a, "any", i)).some((b) => b)
|
|
10571
|
-
},
|
|
10572
|
-
all: {
|
|
10573
|
-
minArgs: 1,
|
|
10574
|
-
maxArgs: INF,
|
|
10575
|
-
apply: (args) => args.map((a, i) => asBoolean$1(a, "all", i)).every((b) => b)
|
|
10576
|
-
},
|
|
10577
|
-
within: {
|
|
10578
|
-
minArgs: 2,
|
|
10579
|
-
maxArgs: 2,
|
|
10580
|
-
apply: (args, hooks) => {
|
|
10581
|
-
const windowMs = asFiniteNumber(args[1], "within", 1);
|
|
10582
|
-
if (windowMs < 0) throw new ExpressionEvalError("within: the window must not be negative");
|
|
10583
|
-
const at = args[0];
|
|
10584
|
-
if (at === null) return false;
|
|
10585
|
-
const ts = asFiniteNumber(at, "within", 0);
|
|
10586
|
-
const now = hooks.now;
|
|
10587
|
-
if (now === void 0 || !Number.isFinite(now)) throw new ExpressionEvalError("within: no clock was supplied to this evaluation");
|
|
10588
|
-
const inside = now - ts <= windowMs;
|
|
10589
|
-
if (inside) hooks.noteDeadline?.(ts + windowMs + 1);
|
|
10590
|
-
return inside;
|
|
10591
|
-
}
|
|
10592
|
-
},
|
|
10593
|
-
latest: {
|
|
10594
|
-
minArgs: 1,
|
|
10595
|
-
maxArgs: INF,
|
|
10596
|
-
apply: (args) => {
|
|
10597
|
-
const present = args.flatMap((a, i) => a === null ? [] : [asFiniteNumber(a, "latest", i)]);
|
|
10598
|
-
return present.length === 0 ? null : finiteResult(Math.max(...present), "latest");
|
|
10599
|
-
}
|
|
10600
|
-
}
|
|
10601
|
-
};
|
|
10602
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
10603
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
10604
|
-
* callees at parse time (immediate author feedback). */
|
|
10605
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
10606
|
-
/**
|
|
10607
|
-
* Resource-bound constants for the safe expression engine.
|
|
10614
|
+
* Per-component log CHANNELS — the gate a hot path consults, and the registry
|
|
10615
|
+
* an addon declares its channels in.
|
|
10608
10616
|
*
|
|
10609
|
-
*
|
|
10610
|
-
*
|
|
10611
|
-
*
|
|
10612
|
-
*
|
|
10613
|
-
*
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
*
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
*
|
|
10622
|
-
var
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
*
|
|
10630
|
-
*
|
|
10631
|
-
*
|
|
10632
|
-
*
|
|
10633
|
-
*
|
|
10634
|
-
*
|
|
10635
|
-
*
|
|
10636
|
-
|
|
10637
|
-
|
|
10638
|
-
|
|
10639
|
-
|
|
10640
|
-
|
|
10641
|
-
|
|
10642
|
-
|
|
10643
|
-
|
|
10644
|
-
|
|
10645
|
-
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
|
|
10650
|
-
}
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
}
|
|
10654
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
10655
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
10656
|
-
* string. */
|
|
10657
|
-
function tokenize(source) {
|
|
10658
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
10659
|
-
const tokens = [];
|
|
10660
|
-
let i = 0;
|
|
10661
|
-
const n = source.length;
|
|
10662
|
-
while (i < n) {
|
|
10663
|
-
const ch = source[i];
|
|
10664
|
-
if (isWhitespace(ch)) {
|
|
10665
|
-
i += 1;
|
|
10666
|
-
continue;
|
|
10667
|
-
}
|
|
10668
|
-
if (isDigit(ch)) {
|
|
10669
|
-
const start = i;
|
|
10670
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10671
|
-
if (i < n && source[i] === ".") {
|
|
10672
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
10673
|
-
i += 1;
|
|
10674
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10675
|
-
}
|
|
10676
|
-
const text = source.slice(start, i);
|
|
10677
|
-
const value = Number(text);
|
|
10678
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
10679
|
-
tokens.push({
|
|
10680
|
-
type: "number",
|
|
10681
|
-
value,
|
|
10682
|
-
pos: start
|
|
10683
|
-
});
|
|
10684
|
-
continue;
|
|
10685
|
-
}
|
|
10686
|
-
if (ch === "'" || ch === "\"") {
|
|
10687
|
-
const quote = ch;
|
|
10688
|
-
const start = i;
|
|
10689
|
-
i += 1;
|
|
10690
|
-
let out = "";
|
|
10691
|
-
let closed = false;
|
|
10692
|
-
while (i < n) {
|
|
10693
|
-
const c = source[i];
|
|
10694
|
-
if (c === "\\") {
|
|
10695
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
10696
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
10697
|
-
out += next;
|
|
10698
|
-
i += 2;
|
|
10699
|
-
continue;
|
|
10700
|
-
}
|
|
10701
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
10702
|
-
}
|
|
10703
|
-
if (c === quote) {
|
|
10704
|
-
closed = true;
|
|
10705
|
-
i += 1;
|
|
10706
|
-
break;
|
|
10707
|
-
}
|
|
10708
|
-
out += c;
|
|
10709
|
-
i += 1;
|
|
10710
|
-
}
|
|
10711
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
10712
|
-
tokens.push({
|
|
10713
|
-
type: "string",
|
|
10714
|
-
value: out,
|
|
10715
|
-
pos: start
|
|
10716
|
-
});
|
|
10717
|
-
continue;
|
|
10718
|
-
}
|
|
10719
|
-
if (isIdentStart(ch)) {
|
|
10720
|
-
const start = i;
|
|
10721
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
10722
|
-
const text = source.slice(start, i);
|
|
10723
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
10724
|
-
type: "keyword",
|
|
10725
|
-
keyword: keywordOf(text),
|
|
10726
|
-
pos: start
|
|
10727
|
-
});
|
|
10728
|
-
else tokens.push({
|
|
10729
|
-
type: "identifier",
|
|
10730
|
-
name: text,
|
|
10731
|
-
pos: start
|
|
10732
|
-
});
|
|
10733
|
-
continue;
|
|
10734
|
-
}
|
|
10735
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
10736
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
10737
|
-
tokens.push({
|
|
10738
|
-
type: "punct",
|
|
10739
|
-
punct: two,
|
|
10740
|
-
pos: i
|
|
10741
|
-
});
|
|
10742
|
-
i += 2;
|
|
10743
|
-
continue;
|
|
10744
|
-
}
|
|
10745
|
-
if (isSinglePunct(ch)) {
|
|
10746
|
-
tokens.push({
|
|
10747
|
-
type: "punct",
|
|
10748
|
-
punct: ch,
|
|
10749
|
-
pos: i
|
|
10750
|
-
});
|
|
10751
|
-
i += 1;
|
|
10752
|
-
continue;
|
|
10753
|
-
}
|
|
10754
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
10755
|
-
}
|
|
10756
|
-
tokens.push({
|
|
10757
|
-
type: "eof",
|
|
10758
|
-
pos: n
|
|
10759
|
-
});
|
|
10760
|
-
return tokens;
|
|
10761
|
-
}
|
|
10762
|
-
function keywordOf(text) {
|
|
10763
|
-
if (text === "true") return "true";
|
|
10764
|
-
if (text === "false") return "false";
|
|
10765
|
-
return "null";
|
|
10766
|
-
}
|
|
10767
|
-
function isSinglePunct(ch) {
|
|
10768
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
10769
|
-
}
|
|
10617
|
+
* ## Two axes, deliberately separated
|
|
10618
|
+
*
|
|
10619
|
+
* - **DECLARATION** — which channels exist. Only the addon knows:
|
|
10620
|
+
* `stream-broker` knows webrtc/ICE/RTP, `provider-reolink` knows
|
|
10621
|
+
* baichuan/handshake. A hand-wired central list rots at the first addition,
|
|
10622
|
+
* and rots silently. So a channel is declared where it is consulted, and the
|
|
10623
|
+
* `log-channels` capability enumerates the declarations.
|
|
10624
|
+
* - **VALUE** — at which level, for which scope, until when. That stays ONE
|
|
10625
|
+
* thing: the logging settings document on the `system` cap. Two authorities
|
|
10626
|
+
* over the values is the exact defect
|
|
10627
|
+
* `docs/design/plans/2026-08-26-logging-per-componente.md` was written to
|
|
10628
|
+
* remove; re-introducing it from the cure side would be grotesque.
|
|
10629
|
+
*
|
|
10630
|
+
* Nothing in this file reads a clock, an env var or a store. The registry is
|
|
10631
|
+
* a MIRROR: it is moved only by {@link LogChannelRegistry.apply}, called off
|
|
10632
|
+
* the hot path with a value somebody actually read, and by
|
|
10633
|
+
* {@link LogChannelRegistry.tick}, called on a timer. A store read that fails
|
|
10634
|
+
* never reaches here, so it can neither disarm an armed channel nor arm a
|
|
10635
|
+
* disarmed one (D49).
|
|
10636
|
+
*
|
|
10637
|
+
* ## The canonical call shape
|
|
10638
|
+
*
|
|
10639
|
+
* ```ts
|
|
10640
|
+
* if (CH_RTP.on && CH_RTP.wants(deviceId)) {
|
|
10641
|
+
* CH_RTP.log(logger, 'rtp subscriber added', { tags: { deviceId }, meta: { ssrc } })
|
|
10642
|
+
* }
|
|
10643
|
+
* ```
|
|
10644
|
+
*
|
|
10645
|
+
* `on` is a plain boolean FIELD — never a getter — and it is the FIRST thing
|
|
10646
|
+
* read. Disarmed, a call site costs one load and one branch, and the `extras`
|
|
10647
|
+
* object literal is never constructed because it lives inside the branch. It
|
|
10648
|
+
* is the same shape already proven in production at `stream-broker.ts:1650`,
|
|
10649
|
+
* and the same discipline `LoggingGate.allowsDestination` uses for the
|
|
10650
|
+
* destination floor (measured at 1.93 ns/call when off).
|
|
10651
|
+
*
|
|
10652
|
+
* ## Why a channel emits at `info`
|
|
10653
|
+
*
|
|
10654
|
+
* `loki-logging.addon.ts` pins the destination default at `info` and
|
|
10655
|
+
* `loki-destination.ts` drops everything below it, so a line emitted at
|
|
10656
|
+
* `debug` never reaches Loki and the hub's in-memory ring only holds ~35
|
|
10657
|
+
* minutes. A diagnostic that cannot be read an hour later is worse than no
|
|
10658
|
+
* diagnostic, because it looks done. {@link LogChannelGate.log} therefore
|
|
10659
|
+
* emits at the channel's declared level, whose schema floor is `info`.
|
|
10660
|
+
*/
|
|
10770
10661
|
/**
|
|
10771
|
-
*
|
|
10662
|
+
* The level a channel writes at once armed.
|
|
10772
10663
|
*
|
|
10773
|
-
*
|
|
10774
|
-
*
|
|
10775
|
-
*
|
|
10776
|
-
|
|
10777
|
-
|
|
10778
|
-
|
|
10664
|
+
* `debug` is absent ON PURPOSE and not by omission: below `info` the line does
|
|
10665
|
+
* not leave the process for Loki, and the whole point of arming a channel is
|
|
10666
|
+
* to read it later.
|
|
10667
|
+
*/
|
|
10668
|
+
var LogChannelLevelSchema = _enum([
|
|
10669
|
+
"info",
|
|
10670
|
+
"warn",
|
|
10671
|
+
"error"
|
|
10672
|
+
]);
|
|
10673
|
+
/**
|
|
10674
|
+
* What an addon declares about one channel. No value, no state — a
|
|
10675
|
+
* declaration is inert.
|
|
10676
|
+
*/
|
|
10677
|
+
var LogChannelDescriptorSchema = object({
|
|
10678
|
+
/**
|
|
10679
|
+
* Dotted `area.thing`, unique across the workspace. `area` is conventionally
|
|
10680
|
+
* the addon's short name so an operator reading a channel list can tell who
|
|
10681
|
+
* owns it without a second lookup.
|
|
10682
|
+
*/
|
|
10683
|
+
name: string().min(3).regex(/^[a-z0-9-]+(\.[a-z0-9-]+)+$/, "a channel name is dotted lower-kebab, e.g. area.thing"),
|
|
10684
|
+
/** One sentence: what the operator will SEE after arming it. */
|
|
10685
|
+
description: string().min(1),
|
|
10686
|
+
/** The level its lines are emitted at. Never below `info`. */
|
|
10687
|
+
defaultLevel: LogChannelLevelSchema,
|
|
10688
|
+
/**
|
|
10689
|
+
* Whether this channel can be narrowed to a camera.
|
|
10690
|
+
*
|
|
10691
|
+
* `true` is a PROMISE with two halves, and both must hold: the gate is
|
|
10692
|
+
* consulted with the numeric device id, AND every line the channel admits
|
|
10693
|
+
* carries `tags: { deviceId }` with that same numeric id. The second half is
|
|
10694
|
+
* what makes `| json | deviceId="617"` work in Loki — `loki-payload.ts`
|
|
10695
|
+
* keeps `deviceId` out of the stream labels for cardinality, so the tag in
|
|
10696
|
+
* the body is the only way to filter.
|
|
10697
|
+
*
|
|
10698
|
+
* A channel whose lines carry the device only in `meta` (or not at all) is
|
|
10699
|
+
* declared `false`. Declaring it `true` anyway would be a lie the UI repeats:
|
|
10700
|
+
* the operator narrows to one camera, sees nothing, and concludes the code
|
|
10701
|
+
* path was never taken.
|
|
10702
|
+
*/
|
|
10703
|
+
perDevice: boolean()
|
|
10704
|
+
});
|
|
10705
|
+
/**
|
|
10706
|
+
* An armed window over one channel, as the document hands it to a mirror.
|
|
10779
10707
|
*
|
|
10780
|
-
* A
|
|
10781
|
-
*
|
|
10708
|
+
* A window is a DEADLINE, never a flag (ADR-0244): a channel somebody forgot
|
|
10709
|
+
* expires by itself, which is the one failure a boolean cannot avoid.
|
|
10782
10710
|
*/
|
|
10783
|
-
|
|
10784
|
-
|
|
10785
|
-
|
|
10786
|
-
|
|
10787
|
-
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10791
|
-
|
|
10792
|
-
|
|
10793
|
-
|
|
10794
|
-
|
|
10795
|
-
|
|
10796
|
-
|
|
10797
|
-
|
|
10798
|
-
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10711
|
+
var LogChannelWindowSchema = object({
|
|
10712
|
+
channel: string().min(1),
|
|
10713
|
+
/** Epoch ms the window closes at. */
|
|
10714
|
+
armedUntilMs: number(),
|
|
10715
|
+
/** `null` = every camera. A non-empty list narrows to those numeric ids. */
|
|
10716
|
+
deviceIds: array(number().int()).readonly().nullable()
|
|
10717
|
+
});
|
|
10718
|
+
/**
|
|
10719
|
+
* Distinct (device, family, variant) counters one instance will hold.
|
|
10720
|
+
*
|
|
10721
|
+
* A large fleet x the handful of families any single addon reports, with
|
|
10722
|
+
* slack. At ~200 B per counter this is a ~100 KB ceiling on a process that
|
|
10723
|
+
* already declares an RSS budget in the gigabytes.
|
|
10724
|
+
*/
|
|
10725
|
+
var MAX_KEYS = 1024;
|
|
10726
|
+
/**
|
|
10727
|
+
* Where reasons past {@link MAX_REASONS_PER_KEY} go.
|
|
10728
|
+
*
|
|
10729
|
+
* They are FOLDED, never dropped: `attempts - succeeded` must always equal the
|
|
10730
|
+
* sum of the reason counts, or the ratio stops adding up.
|
|
10731
|
+
*/
|
|
10732
|
+
var OVERFLOW_REASON = "other";
|
|
10733
|
+
/** `deviceId` + `family` + optional `variant`, flattened into the map key. */
|
|
10734
|
+
function counterKey(deviceId, family, variant) {
|
|
10735
|
+
return variant === void 0 ? `${deviceId}${family}` : `${deviceId}${family}${variant}`;
|
|
10804
10736
|
}
|
|
10805
|
-
|
|
10806
|
-
|
|
10807
|
-
|
|
10808
|
-
|
|
10809
|
-
|
|
10810
|
-
|
|
10811
|
-
|
|
10812
|
-
|
|
10813
|
-
|
|
10814
|
-
|
|
10815
|
-
|
|
10816
|
-
|
|
10817
|
-
|
|
10818
|
-
|
|
10819
|
-
ast,
|
|
10820
|
-
identifiers: this.identifiers,
|
|
10821
|
-
callees: this.callees,
|
|
10822
|
-
nodeCount: this.nodeCount
|
|
10823
|
-
};
|
|
10824
|
-
}
|
|
10825
|
-
peek() {
|
|
10826
|
-
return this.tokens[this.pos];
|
|
10827
|
-
}
|
|
10828
|
-
next() {
|
|
10829
|
-
return this.tokens[this.pos++];
|
|
10830
|
-
}
|
|
10831
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
10832
|
-
expectPunct(punct) {
|
|
10833
|
-
const tok = this.peek();
|
|
10834
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
10835
|
-
this.pos += 1;
|
|
10836
|
-
}
|
|
10837
|
-
matchPunct(punct) {
|
|
10838
|
-
const tok = this.peek();
|
|
10839
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
10840
|
-
this.pos += 1;
|
|
10841
|
-
return true;
|
|
10842
|
-
}
|
|
10843
|
-
return false;
|
|
10737
|
+
/**
|
|
10738
|
+
* A bounded set of per-camera, cumulative failure counters.
|
|
10739
|
+
*
|
|
10740
|
+
* One instance per contributing subsystem. `note` is O(1) and allocation-free
|
|
10741
|
+
* on the steady path; `snapshot` reads without mutating anything.
|
|
10742
|
+
*/
|
|
10743
|
+
var FailureCounters = class {
|
|
10744
|
+
maxKeys;
|
|
10745
|
+
maxReasons;
|
|
10746
|
+
counters = /* @__PURE__ */ new Map();
|
|
10747
|
+
refused = 0;
|
|
10748
|
+
constructor(maxKeys = MAX_KEYS, maxReasons = 16) {
|
|
10749
|
+
this.maxKeys = maxKeys;
|
|
10750
|
+
this.maxReasons = maxReasons;
|
|
10844
10751
|
}
|
|
10845
|
-
|
|
10846
|
-
|
|
10847
|
-
|
|
10752
|
+
/**
|
|
10753
|
+
* Counters refused because {@link MAX_KEYS} was already held.
|
|
10754
|
+
*
|
|
10755
|
+
* Cumulative for the life of the instance: a bound that bit is a fact about
|
|
10756
|
+
* the deployment, and a surface that hid it would under-report a fleet
|
|
10757
|
+
* precisely when the fleet got large enough to matter.
|
|
10758
|
+
*/
|
|
10759
|
+
get keysRefused() {
|
|
10760
|
+
return this.refused;
|
|
10848
10761
|
}
|
|
10849
|
-
|
|
10850
|
-
|
|
10851
|
-
|
|
10852
|
-
const consequent = this.parseTernary();
|
|
10853
|
-
this.expectPunct(":");
|
|
10854
|
-
const alternate = this.parseTernary();
|
|
10855
|
-
this.countNode();
|
|
10856
|
-
return {
|
|
10857
|
-
kind: "conditional",
|
|
10858
|
-
test,
|
|
10859
|
-
consequent,
|
|
10860
|
-
alternate
|
|
10861
|
-
};
|
|
10862
|
-
}
|
|
10863
|
-
return test;
|
|
10762
|
+
/** Counters currently held. */
|
|
10763
|
+
get size() {
|
|
10764
|
+
return this.counters.size;
|
|
10864
10765
|
}
|
|
10865
|
-
|
|
10866
|
-
|
|
10867
|
-
|
|
10868
|
-
|
|
10869
|
-
|
|
10870
|
-
|
|
10871
|
-
|
|
10872
|
-
|
|
10873
|
-
|
|
10874
|
-
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10766
|
+
/**
|
|
10767
|
+
* Fold one observation in.
|
|
10768
|
+
*
|
|
10769
|
+
* A non-positive or non-integer `deviceId` is REFUSED rather than bucketed:
|
|
10770
|
+
* see the module docblock — an entry that cannot name its camera is worse
|
|
10771
|
+
* than no entry.
|
|
10772
|
+
*/
|
|
10773
|
+
note(observation, nowMs) {
|
|
10774
|
+
if (!Number.isInteger(observation.deviceId) || observation.deviceId <= 0) return;
|
|
10775
|
+
const key = counterKey(observation.deviceId, observation.family, observation.variant);
|
|
10776
|
+
let counter = this.counters.get(key);
|
|
10777
|
+
if (counter === void 0) {
|
|
10778
|
+
if (this.counters.size >= this.maxKeys) {
|
|
10779
|
+
this.refused += 1;
|
|
10780
|
+
return;
|
|
10781
|
+
}
|
|
10782
|
+
counter = {
|
|
10783
|
+
deviceId: observation.deviceId,
|
|
10784
|
+
family: observation.family,
|
|
10785
|
+
variant: observation.variant,
|
|
10786
|
+
sinceMs: nowMs,
|
|
10787
|
+
attempts: 0,
|
|
10788
|
+
succeeded: 0,
|
|
10789
|
+
reasons: /* @__PURE__ */ new Map()
|
|
10887
10790
|
};
|
|
10888
|
-
|
|
10791
|
+
this.counters.set(key, counter);
|
|
10889
10792
|
}
|
|
10890
|
-
|
|
10891
|
-
|
|
10892
|
-
|
|
10893
|
-
|
|
10894
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
10895
|
-
const op = tok.punct;
|
|
10896
|
-
this.pos += 1;
|
|
10897
|
-
const operand = this.parseUnary();
|
|
10898
|
-
this.countNode();
|
|
10899
|
-
return {
|
|
10900
|
-
kind: "unary",
|
|
10901
|
-
op,
|
|
10902
|
-
operand
|
|
10903
|
-
};
|
|
10793
|
+
counter.attempts += 1;
|
|
10794
|
+
if (observation.reason === void 0) {
|
|
10795
|
+
counter.succeeded += 1;
|
|
10796
|
+
return;
|
|
10904
10797
|
}
|
|
10905
|
-
|
|
10798
|
+
const reason = counter.reasons.has(observation.reason) || counter.reasons.size < this.maxReasons ? observation.reason : OVERFLOW_REASON;
|
|
10799
|
+
counter.reasons.set(reason, (counter.reasons.get(reason) ?? 0) + 1);
|
|
10906
10800
|
}
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
10911
|
-
|
|
10912
|
-
|
|
10913
|
-
|
|
10914
|
-
|
|
10915
|
-
|
|
10916
|
-
|
|
10917
|
-
|
|
10918
|
-
|
|
10919
|
-
|
|
10920
|
-
|
|
10921
|
-
|
|
10922
|
-
|
|
10923
|
-
|
|
10924
|
-
return {
|
|
10925
|
-
kind: "literal",
|
|
10926
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
10927
|
-
};
|
|
10928
|
-
case "identifier": {
|
|
10929
|
-
const nextTok = this.peek();
|
|
10930
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
10931
|
-
this.identifiers.add(tok.name);
|
|
10932
|
-
this.countNode();
|
|
10933
|
-
return {
|
|
10934
|
-
kind: "identifier",
|
|
10935
|
-
name: tok.name
|
|
10936
|
-
};
|
|
10937
|
-
}
|
|
10938
|
-
case "punct":
|
|
10939
|
-
if (tok.punct === "(") {
|
|
10940
|
-
const inner = this.parseTernary();
|
|
10941
|
-
this.expectPunct(")");
|
|
10942
|
-
return inner;
|
|
10943
|
-
}
|
|
10944
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
10945
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
10946
|
-
}
|
|
10801
|
+
/** Read every counter. Never mutates — see the module docblock. */
|
|
10802
|
+
snapshot(nowMs) {
|
|
10803
|
+
const out = [];
|
|
10804
|
+
for (const counter of this.counters.values()) out.push({
|
|
10805
|
+
deviceId: counter.deviceId,
|
|
10806
|
+
family: counter.family,
|
|
10807
|
+
...counter.variant !== void 0 ? { variant: counter.variant } : {},
|
|
10808
|
+
sinceMs: counter.sinceMs,
|
|
10809
|
+
atMs: nowMs,
|
|
10810
|
+
attempts: counter.attempts,
|
|
10811
|
+
succeeded: counter.succeeded,
|
|
10812
|
+
reasons: [...counter.reasons.entries()].map(([reason, count]) => ({
|
|
10813
|
+
reason,
|
|
10814
|
+
count
|
|
10815
|
+
})).toSorted((a, b) => b.count - a.count)
|
|
10816
|
+
});
|
|
10817
|
+
return out;
|
|
10947
10818
|
}
|
|
10948
|
-
|
|
10949
|
-
|
|
10950
|
-
this.
|
|
10951
|
-
const args = [];
|
|
10952
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
10953
|
-
args.push(this.parseTernary());
|
|
10954
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
10955
|
-
if (this.matchPunct(",")) continue;
|
|
10956
|
-
this.expectPunct(")");
|
|
10957
|
-
break;
|
|
10958
|
-
}
|
|
10959
|
-
this.callees.add(callee);
|
|
10960
|
-
this.countNode();
|
|
10961
|
-
return {
|
|
10962
|
-
kind: "call",
|
|
10963
|
-
callee,
|
|
10964
|
-
args
|
|
10965
|
-
};
|
|
10819
|
+
/** Drop everything (host disposal). */
|
|
10820
|
+
clear() {
|
|
10821
|
+
this.counters.clear();
|
|
10966
10822
|
}
|
|
10967
10823
|
};
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10971
|
-
|
|
10824
|
+
var ClipModelMetaSchema = object({
|
|
10825
|
+
/** The paired text encoder's catalog id (the embedding-encoder's text catalog). */
|
|
10826
|
+
textModelId: string().min(1),
|
|
10827
|
+
/** Output dimension of BOTH towers. A vector of any other length is refused. */
|
|
10828
|
+
embeddingDim: number().int().positive(),
|
|
10829
|
+
/**
|
|
10830
|
+
* The `vector-store` index this model's vectors live in. ONE feature space
|
|
10831
|
+
* per index: two models share an index only when their vectors are ranked
|
|
10832
|
+
* against each other, which for CLIP means never across families. MobileCLIP
|
|
10833
|
+
* S1 and S2 share `object-clip` for history (separated by the row's
|
|
10834
|
+
* `modelId`); any other model gets its own.
|
|
10835
|
+
*/
|
|
10836
|
+
vectorIndex: string().regex(/^object-clip(-[a-z0-9][a-z0-9-]*)?$/),
|
|
10837
|
+
/**
|
|
10838
|
+
* The text encoder's tokenizer, a declared SIBLING file of its onnx. The name
|
|
10839
|
+
* must be unique per tokenizer: every model's siblings land in ONE flat
|
|
10840
|
+
* models directory, so two different tokenizers under the same name would
|
|
10841
|
+
* silently overwrite each other (whichever model downloaded last wins, and
|
|
10842
|
+
* the other one tokenises with the wrong vocabulary).
|
|
10843
|
+
*/
|
|
10844
|
+
tokenizerFile: string().min(1),
|
|
10845
|
+
/** Token window the text graph takes (SigLIP2: 64; CLIP BPE: 77). */
|
|
10846
|
+
contextLength: number().int().positive(),
|
|
10847
|
+
/** Id the tokenizer pads with. */
|
|
10848
|
+
padId: number().int().nonnegative(),
|
|
10849
|
+
/**
|
|
10850
|
+
* The search cosine floor at `strictness: 'loose'` — calibrated per model,
|
|
10851
|
+
* because cosine bands are a property of the model: 0.2 keeps 96% of matched
|
|
10852
|
+
* COCO caption-image pairs on MobileCLIP S1 and 2.7% on SigLIP2.
|
|
10853
|
+
*/
|
|
10854
|
+
searchMinScore: number().min(0).max(1)
|
|
10855
|
+
});
|
|
10856
|
+
var ClipSearchStrictnessSchema = _enum([
|
|
10857
|
+
"loose",
|
|
10858
|
+
"balanced",
|
|
10859
|
+
"strict"
|
|
10860
|
+
]);
|
|
10861
|
+
var CLIP_STRICTNESS_FACTOR = {
|
|
10862
|
+
loose: 1,
|
|
10863
|
+
balanced: 1.25,
|
|
10864
|
+
strict: 1.5
|
|
10865
|
+
};
|
|
10866
|
+
/**
|
|
10867
|
+
* The cosine floor a search runs with.
|
|
10868
|
+
*
|
|
10869
|
+
* An explicit `minScore` is honoured verbatim: it is a number a caller chose,
|
|
10870
|
+
* and silently rescaling it would make the same URL mean different things on
|
|
10871
|
+
* two installs. Everything else is relative to the model.
|
|
10872
|
+
*/
|
|
10873
|
+
function resolveClipSearchMinScore(meta, request) {
|
|
10874
|
+
if (request.minScore !== void 0) return request.minScore;
|
|
10875
|
+
const factor = CLIP_STRICTNESS_FACTOR[request.strictness ?? "loose"];
|
|
10876
|
+
return Math.min(1, Math.round(meta.searchMinScore * factor * 1e3) / 1e3);
|
|
10972
10877
|
}
|
|
10878
|
+
var MODEL_FORMATS = [
|
|
10879
|
+
"onnx",
|
|
10880
|
+
"coreml",
|
|
10881
|
+
"openvino",
|
|
10882
|
+
"tflite",
|
|
10883
|
+
"pt",
|
|
10884
|
+
"gguf"
|
|
10885
|
+
];
|
|
10886
|
+
/**
|
|
10887
|
+
* Multi-file format payload.
|
|
10888
|
+
*
|
|
10889
|
+
* - Directory formats (`isDirectory: true`, e.g. `.mlpackage`): files
|
|
10890
|
+
* relative to the directory root — the downloader fetches each from
|
|
10891
|
+
* `{url}/{file}` into `{modelDir}/{file}`. If omitted, it probes the
|
|
10892
|
+
* HuggingFace API (slower).
|
|
10893
|
+
* - Single-file formats (no `isDirectory`, e.g. OpenVINO IR): sibling
|
|
10894
|
+
* files fetched from the SAME remote directory as `url` and stored flat
|
|
10895
|
+
* alongside the main file — e.g. `['camstack-yolov9t.bin']` for the IR
|
|
10896
|
+
* weights next to `camstack-yolov9t.xml`.
|
|
10897
|
+
*/
|
|
10898
|
+
var ModelFormatEntrySchema = object({
|
|
10899
|
+
url: string(),
|
|
10900
|
+
sizeMB: number(),
|
|
10901
|
+
/** Whether this format is a directory bundle (e.g., .mlpackage) rather than a single file */
|
|
10902
|
+
isDirectory: boolean().optional(),
|
|
10903
|
+
/** Multi-file payload (directory members or sibling files). */
|
|
10904
|
+
files: array(string()).readonly().optional(),
|
|
10905
|
+
/** Runtime(s) that can use this format. If omitted, inferred from ModelFormat key */
|
|
10906
|
+
runtimes: array(_enum(["python"])).readonly().optional()
|
|
10907
|
+
});
|
|
10908
|
+
/**
|
|
10909
|
+
* Extra file that must be downloaded alongside the model (e.g., labels JSON, dict.txt).
|
|
10910
|
+
* The downloader fetches from `url` and saves to `{modelsDir}/{filename}`.
|
|
10911
|
+
*/
|
|
10912
|
+
var ModelExtraFileSchema = object({
|
|
10913
|
+
url: string(),
|
|
10914
|
+
filename: string(),
|
|
10915
|
+
sizeMB: number()
|
|
10916
|
+
});
|
|
10917
|
+
/**
|
|
10918
|
+
* Per-format payload map. Modelled as an explicit object (one optional key
|
|
10919
|
+
* per `ModelFormat`) rather than `z.record(enum, …)` — zod v4's enum-keyed
|
|
10920
|
+
* record requires every key, but a catalog entry only ships a subset of
|
|
10921
|
+
* formats.
|
|
10922
|
+
*/
|
|
10923
|
+
var ModelFormatsSchema = object({
|
|
10924
|
+
onnx: ModelFormatEntrySchema.optional(),
|
|
10925
|
+
coreml: ModelFormatEntrySchema.optional(),
|
|
10926
|
+
openvino: ModelFormatEntrySchema.optional(),
|
|
10927
|
+
tflite: ModelFormatEntrySchema.optional(),
|
|
10928
|
+
pt: ModelFormatEntrySchema.optional(),
|
|
10929
|
+
gguf: ModelFormatEntrySchema.optional()
|
|
10930
|
+
});
|
|
10931
|
+
/**
|
|
10932
|
+
* Variant-selector grouping axes. Shared by the full `ModelCatalogEntry` and by
|
|
10933
|
+
* the reduced `PipelineModelOption` returned in `pipeline.getSchema()` so the
|
|
10934
|
+
* grouped Family→Tier→Variant picker renders identically in the config UI and
|
|
10935
|
+
* in the pipeline/device steppers. The flat `id` stays the source of truth for
|
|
10936
|
+
* resolution/download/persistence; this is a presentation overlay resolved back
|
|
10937
|
+
* to an `id`.
|
|
10938
|
+
*/
|
|
10939
|
+
var ModelVariantGroupSchema = object({
|
|
10940
|
+
/** Top-level family, e.g. `yolo26` (later `d-fine`, `rf-detr`). */
|
|
10941
|
+
family: string(),
|
|
10942
|
+
/** Size within the family, e.g. `n` | `s` | `m` | `l`. */
|
|
10943
|
+
tier: string(),
|
|
10944
|
+
/** Quantization axis. Omit ⇒ the fp32 base build. */
|
|
10945
|
+
precision: _enum(["fp32", "int8"]).optional(),
|
|
10946
|
+
/**
|
|
10947
|
+
* Speed-optimization axis. Omit ⇒ the standard build. `fast` marks a
|
|
10948
|
+
* latency-optimized export (e.g. ReLU-activation variant) — the slot the
|
|
10949
|
+
* future performance variants plug into.
|
|
10950
|
+
*/
|
|
10951
|
+
optimization: _enum(["standard", "fast"]).optional(),
|
|
10952
|
+
/**
|
|
10953
|
+
* Input-resolution axis (square input side, px). Omit ⇒ the family's native
|
|
10954
|
+
* resolution (640 for yolo26). Reduced-input builds (320 / 256) are a big,
|
|
10955
|
+
* cheap latency lever — especially on Apple ANE and the Intel N100 — at a
|
|
10956
|
+
* small-object accuracy cost. Mirrors the model's `inputSize` but lifted onto
|
|
10957
|
+
* the group so the selector can offer it as a variant axis.
|
|
10958
|
+
*/
|
|
10959
|
+
resolution: number().int().positive().optional()
|
|
10960
|
+
});
|
|
10961
|
+
var ModelProviderIdSchema = _enum([
|
|
10962
|
+
"camstack",
|
|
10963
|
+
"frigate",
|
|
10964
|
+
"scrypted",
|
|
10965
|
+
"custom"
|
|
10966
|
+
]);
|
|
10973
10967
|
/**
|
|
10974
|
-
*
|
|
10975
|
-
*
|
|
10976
|
-
*
|
|
10977
|
-
*
|
|
10968
|
+
* The licence of a model, as a record rather than a bare SPDX string: a
|
|
10969
|
+
* permissive CODE licence routinely sits on top of restrictive WEIGHTS or
|
|
10970
|
+
* DATA, and the obligations (attribution, modification notices, source offers)
|
|
10971
|
+
* are per upstream. A persisted custom-model row written when this field was a
|
|
10972
|
+
* string still parses — the string is lifted into `{ weights }`.
|
|
10978
10973
|
*
|
|
10979
|
-
*
|
|
10980
|
-
*
|
|
10981
|
-
*
|
|
10982
|
-
*/
|
|
10983
|
-
var cache = /* @__PURE__ */ new Map();
|
|
10984
|
-
function getCached(source) {
|
|
10985
|
-
const hit = cache.get(source);
|
|
10986
|
-
if (hit !== void 0) {
|
|
10987
|
-
cache.delete(source);
|
|
10988
|
-
cache.set(source, hit);
|
|
10989
|
-
return hit;
|
|
10990
|
-
}
|
|
10991
|
-
let result;
|
|
10992
|
-
try {
|
|
10993
|
-
result = {
|
|
10994
|
-
ok: true,
|
|
10995
|
-
parsed: parseExpression(source)
|
|
10996
|
-
};
|
|
10997
|
-
} catch (err) {
|
|
10998
|
-
result = {
|
|
10999
|
-
ok: false,
|
|
11000
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
11001
|
-
};
|
|
11002
|
-
}
|
|
11003
|
-
cache.set(source, result);
|
|
11004
|
-
if (cache.size > 256) {
|
|
11005
|
-
const oldest = cache.keys().next().value;
|
|
11006
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
11007
|
-
}
|
|
11008
|
-
return result;
|
|
11009
|
-
}
|
|
11010
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
11011
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
11012
|
-
function compileExpressionSafe(source) {
|
|
11013
|
-
return getCached(source);
|
|
11014
|
-
}
|
|
11015
|
-
Object.freeze({});
|
|
11016
|
-
/**
|
|
11017
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
11018
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
11019
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
11020
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
11021
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
10974
|
+
* Every licence surface (the admin "Models and licenses" page,
|
|
10975
|
+
* `THIRD_PARTY_MODELS.md`, the Hugging Face model table) is GENERATED from
|
|
10976
|
+
* these records by `scripts/gen-model-licenses.ts` (D661).
|
|
11022
10977
|
*/
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11028
|
-
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
10978
|
+
var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { weights: value } : value, object({
|
|
10979
|
+
/** SPDX id of the WEIGHTS' terms, a `LicenseRef-*` for non-SPDX terms, or `UNKNOWN`. */
|
|
10980
|
+
weights: string().min(1),
|
|
10981
|
+
/** SPDX id of the upstream code that defines / trained the network. */
|
|
10982
|
+
code: string().min(1).optional(),
|
|
10983
|
+
/** Training data whose terms add obligations (attribution, non-commercial). */
|
|
10984
|
+
data: object({
|
|
10985
|
+
name: string().min(1),
|
|
10986
|
+
terms: string().min(1),
|
|
10987
|
+
url: string().url().optional()
|
|
10988
|
+
}).optional(),
|
|
10989
|
+
/** The project the weights come from. Required on built-in entries (guard). */
|
|
10990
|
+
upstream: object({
|
|
10991
|
+
name: string().min(1),
|
|
10992
|
+
url: string().url()
|
|
10993
|
+
}).optional(),
|
|
10994
|
+
/** Where the licence text governing the weights is published. */
|
|
10995
|
+
url: string().url().optional(),
|
|
10996
|
+
/** Attribution the licence REQUIRES, verbatim. */
|
|
10997
|
+
attribution: string().min(1).optional(),
|
|
10998
|
+
/** What CamStack changed. Required when `hosting` is `camstack-hf` (guard). */
|
|
10999
|
+
modifications: string().min(1).optional(),
|
|
11000
|
+
/** Who serves the file a node downloads. */
|
|
11001
|
+
hosting: _enum([
|
|
11002
|
+
"camstack-hf",
|
|
11003
|
+
"third-party",
|
|
11004
|
+
"built-in"
|
|
11005
|
+
]).optional(),
|
|
11006
|
+
/** True when the weights or their data forbid commercial use. */
|
|
11007
|
+
nonCommercial: boolean().optional()
|
|
11008
|
+
}));
|
|
11009
|
+
var ModelCatalogEntrySchema = object({
|
|
11010
|
+
id: string(),
|
|
11011
|
+
name: string(),
|
|
11012
|
+
description: string(),
|
|
11013
|
+
formats: ModelFormatsSchema,
|
|
11014
|
+
inputSize: object({
|
|
11015
|
+
width: number(),
|
|
11016
|
+
height: number()
|
|
11017
|
+
}),
|
|
11018
|
+
/**
|
|
11019
|
+
* Channel count of the model input tensor. Omit ⇒ 3 (RGB), the default for
|
|
11020
|
+
* every detector / classifier / embedder. Set to 1 for a grayscale CTC text
|
|
11021
|
+
* recognizer (EasyOCR VGG plate-OCR: input `[N,1,H,W]`) so the preprocess
|
|
11022
|
+
* feeds a single-channel, EasyOCR-normalized tensor instead of the default
|
|
11023
|
+
* 3-channel RGB one. Threaded through `PoolModelConfig.inputChannels` to the
|
|
11024
|
+
* Python inference pool.
|
|
11025
|
+
*/
|
|
11026
|
+
inputChannels: number().int().positive().optional(),
|
|
11027
|
+
labels: array(LabelDefinitionSchema).readonly(),
|
|
11028
|
+
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
11029
|
+
/**
|
|
11030
|
+
* `'scrfd'` applies InsightFace's SCRFD input contract, `(x - 127.5) / 128`
|
|
11031
|
+
* (upstream `insightface/model_zoo/scrfd.py`), instead of the historical
|
|
11032
|
+
* plain `/255`. Measured against COCO GT: 188 → 196 faces found on the
|
|
11033
|
+
* `scrfd-2.5g` catalog entry (2026-09-26 model-replacement spike, §2.4-2).
|
|
11034
|
+
*/
|
|
11035
|
+
inputNormalization: _enum([
|
|
11036
|
+
"zero-one",
|
|
11037
|
+
"imagenet",
|
|
11038
|
+
"none",
|
|
11039
|
+
"scrfd"
|
|
11040
|
+
]).optional(),
|
|
11041
|
+
/**
|
|
11042
|
+
* The model already applies softmax IN-GRAPH — its raw output is a
|
|
11043
|
+
* probability distribution, not logits. When set, the `softmax`
|
|
11044
|
+
* postprocessor must NOT re-apply softmax: re-softmaxing an already-normalised
|
|
11045
|
+
* probability vector collapses it toward uniform (top-1 score craters far
|
|
11046
|
+
* below its true value, making every confidence gate meaningless). Absent ⇒
|
|
11047
|
+
* the output is raw logits and the postprocessor applies softmax (the normal
|
|
11048
|
+
* case). Set on the Google AIY Birds `bird-classifier` (softmax baked into the
|
|
11049
|
+
* TF graph). Threaded to the Python pool via `PoolModelConfig.outputProbabilities`.
|
|
11050
|
+
*/
|
|
11051
|
+
outputProbabilities: boolean().optional(),
|
|
11052
|
+
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
11053
|
+
/**
|
|
11054
|
+
* Per-MODEL postprocessor override. Absent ⇒ the step's own
|
|
11055
|
+
* `StepDefinition.postprocessor` applies (the normal case — every model in a
|
|
11056
|
+
* step shares its decode). Set it when a step hosts models with DIFFERENT raw
|
|
11057
|
+
* output layouts under one slot: e.g. object-detection is `'yolo'` by default,
|
|
11058
|
+
* but a Coral SSD MobileNet build emits the `TFLite_Detection_PostProcess`
|
|
11059
|
+
* 4-tensor layout and needs `'ssd'`. Threaded into `PoolModelConfig.postprocessor`
|
|
11060
|
+
* by the engine factory (`modelEntry.postprocessor ?? def.postprocessor`).
|
|
11061
|
+
*/
|
|
11062
|
+
postprocessor: custom().optional(),
|
|
11063
|
+
/**
|
|
11064
|
+
* Per-MODEL default confidence floor. Absent ⇒ the step's
|
|
11065
|
+
* `StepDefinition.defaultConfidence` applies. A score is a property of one
|
|
11066
|
+
* model's output scale, not of the step slot it sits in: YuNet's
|
|
11067
|
+
* `sqrt(cls·obj)` is not SCRFD's score, and inheriting SCRFD's 0.5 filled the
|
|
11068
|
+
* face gallery with wheels and hands (D662, amends D645). An operator's
|
|
11069
|
+
* explicit value still wins over it. Resolved in ONE place —
|
|
11070
|
+
* `addon-pipeline/.../registry/effective-confidence.ts`.
|
|
11071
|
+
*/
|
|
11072
|
+
defaultConfidence: number().min(0).max(1).optional(),
|
|
11073
|
+
/**
|
|
11074
|
+
* When true, the executor produces a landmark-aligned crop (similarity warp
|
|
11075
|
+
* onto the canonical template) before this step runs, instead of a plain
|
|
11076
|
+
* axis-aligned bbox crop. Required for face-recognition embedders (ArcFace):
|
|
11077
|
+
* their embeddings are only discriminative on an aligned input. The face
|
|
11078
|
+
* detector that produced the parent detail must emit 5 landmarks.
|
|
11079
|
+
*/
|
|
11080
|
+
faceAlignment: boolean().optional(),
|
|
11081
|
+
/**
|
|
11082
|
+
* Auxiliary files required at runtime (labels JSON, charset dict, etc.).
|
|
11083
|
+
* Downloaded into the same modelsDir alongside the model file.
|
|
11084
|
+
*/
|
|
11085
|
+
extraFiles: array(ModelExtraFileSchema).readonly().optional(),
|
|
11086
|
+
/**
|
|
11087
|
+
* LEGACY entry — retained in the catalog so a persisted operator selection
|
|
11088
|
+
* still RESOLVES (and can be re-activated), but hidden from the selectable
|
|
11089
|
+
* model list and excluded from the auto format-default pick. Set on the
|
|
11090
|
+
* superseded / consolidated models (older lineages, redundant fp16 IRs) so
|
|
11091
|
+
* the active lineup stays the coherent curated ladder without deleting a
|
|
11092
|
+
* model anyone may still be pinned to. `resolveModelForFormat` keeps honoring
|
|
11093
|
+
* an explicit legacy id that has a build for the node's format.
|
|
11094
|
+
*/
|
|
11095
|
+
legacy: boolean().optional(),
|
|
11096
|
+
/**
|
|
11097
|
+
* Measured quality/latency metadata — populated from the benchmark addon on
|
|
11098
|
+
* the real node classes. Absent = not yet measured (most entries today; the
|
|
11099
|
+
* catalog historically carried only `sizeMB`, a poor cross-architecture
|
|
11100
|
+
* speed proxy). `p95LatencyMs` is keyed by node class (e.g. `n100`, `mac`).
|
|
11101
|
+
*/
|
|
11102
|
+
metrics: object({
|
|
11103
|
+
map50: number().optional(),
|
|
11104
|
+
p95LatencyMs: record(string(), number()).optional()
|
|
11105
|
+
}).optional(),
|
|
11106
|
+
/**
|
|
11107
|
+
* The model's licence record — see {@link ModelLicenseSchema}. Optional in
|
|
11108
|
+
* the schema (a custom model's author may state none); REQUIRED and complete
|
|
11109
|
+
* on every built-in catalog entry (`scripts/check-catalog-licence.ts`).
|
|
11110
|
+
*/
|
|
11111
|
+
license: ModelLicenseSchema.optional(),
|
|
11112
|
+
/**
|
|
11113
|
+
* Variant-selector grouping. The UI groups models by `family` + `tier` and
|
|
11114
|
+
* offers `precision` / `optimization` as variant axes WITHIN a tier — so all
|
|
11115
|
+
* of a family's sizes and quantizations collapse into one grouped picker
|
|
11116
|
+
* instead of a flat list of `yolo26s`, `yolo26s-int8`, … Absent ⇒ ungrouped
|
|
11117
|
+
* (legacy / custom models) — never shown in the grouped selector. The flat
|
|
11118
|
+
* `id` stays the source of truth for resolution/download/persistence; grouping
|
|
11119
|
+
* is a presentation overlay resolved back to an `id`.
|
|
11120
|
+
*/
|
|
11121
|
+
group: ModelVariantGroupSchema.optional(),
|
|
11122
|
+
/**
|
|
11123
|
+
* Catalog source for the pipeline stepper's provider-first picker. Absent on
|
|
11124
|
+
* built-in CamStack entries (treated as `camstack`) and on registry rows
|
|
11125
|
+
* persisted before this field existed (`inferModelProvider` fills those).
|
|
11126
|
+
*/
|
|
11127
|
+
provider: ModelProviderIdSchema.optional(),
|
|
11128
|
+
/**
|
|
11129
|
+
* Per-MODEL class map override. Absent ⇒ the step's `StepDefinition.classMap`
|
|
11130
|
+
* applies (Frigate / COCO public catalog). Set on a custom model whose raw
|
|
11131
|
+
* labels already ARE the CamStack macros (Scrypted identity map).
|
|
11132
|
+
*/
|
|
11133
|
+
classMap: DetectionCatalogClassMapSchema.optional(),
|
|
11134
|
+
/**
|
|
11135
|
+
* The model's CLIP feature-space contract (text encoder, dimension, vector
|
|
11136
|
+
* index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
|
|
11137
|
+
* CLIP image encoder; its absence is what "not a CLIP model" means to the
|
|
11138
|
+
* embedding encoder and the semantic-search store (D649).
|
|
11139
|
+
*/
|
|
11140
|
+
clip: ClipModelMetaSchema.optional()
|
|
11141
|
+
});
|
|
11142
|
+
var ConvertTargetSchema = discriminatedUnion("format", [object({
|
|
11143
|
+
format: literal("openvino"),
|
|
11144
|
+
precisions: array(_enum(["fp16", "int8"])).min(1).readonly()
|
|
11145
|
+
}), object({ format: literal("coreml") })]);
|
|
11146
|
+
var ModelConvertMetadataSchema = object({
|
|
11147
|
+
id: string().regex(/^[a-zA-Z0-9._-]+$/),
|
|
11148
|
+
name: string(),
|
|
11149
|
+
labels: array(LabelDefinitionSchema).readonly(),
|
|
11150
|
+
inputSize: object({
|
|
11151
|
+
width: number(),
|
|
11152
|
+
height: number()
|
|
11153
|
+
}),
|
|
11154
|
+
inputLayout: _enum(["nchw", "nhwc"]).optional(),
|
|
11155
|
+
inputNormalization: _enum([
|
|
11156
|
+
"zero-one",
|
|
11157
|
+
"imagenet",
|
|
11158
|
+
"none",
|
|
11159
|
+
"scrfd"
|
|
11160
|
+
]).optional(),
|
|
11161
|
+
preprocessMode: _enum(["letterbox", "resize"]).optional(),
|
|
11162
|
+
outputFormat: _enum([
|
|
11163
|
+
"yolo",
|
|
11164
|
+
"ssd",
|
|
11165
|
+
"embedding",
|
|
11166
|
+
"classification",
|
|
11167
|
+
"ocr",
|
|
11168
|
+
"segmentation"
|
|
11169
|
+
]),
|
|
11170
|
+
faceAlignment: boolean().optional(),
|
|
11171
|
+
classMap: DetectionCatalogClassMapSchema.optional()
|
|
11172
|
+
});
|
|
11173
|
+
var ConvertResultSchema = object({
|
|
11174
|
+
entry: ModelCatalogEntrySchema,
|
|
11175
|
+
artifacts: array(object({
|
|
11176
|
+
format: _enum(MODEL_FORMATS),
|
|
11177
|
+
precision: _enum(["fp16", "int8"]).optional(),
|
|
11178
|
+
sizeMB: number(),
|
|
11179
|
+
validated: boolean(),
|
|
11180
|
+
files: array(string()).readonly()
|
|
11181
|
+
})).readonly()
|
|
11182
|
+
});
|
|
11040
11183
|
var ExpressionBindingSourceSchema = union([
|
|
11041
11184
|
object({
|
|
11042
11185
|
kind: literal("field").optional(),
|
|
@@ -13387,102 +13530,6 @@ var cameraStreamsCapability = {
|
|
|
13387
13530
|
function kebabToCamel(s) {
|
|
13388
13531
|
return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
13389
13532
|
}
|
|
13390
|
-
/** A composed device's stableId is this prefix plus the block id. */
|
|
13391
|
-
var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
|
|
13392
|
-
/** Longest snippet a `code` source may carry (slice 4). */
|
|
13393
|
-
var MAX_COMPOSITION_CODE_LENGTH = 2e4;
|
|
13394
|
-
var CompositionSourceRefSchema = object({
|
|
13395
|
-
/** The owning addon; `stableId` is unique only within it. */
|
|
13396
|
-
addonId: string().min(1),
|
|
13397
|
-
stableId: string().min(1)
|
|
13398
|
-
});
|
|
13399
|
-
/** One field of one capability of one source device. */
|
|
13400
|
-
var CompositionFieldReadSchema = object({
|
|
13401
|
-
source: CompositionSourceRefSchema,
|
|
13402
|
-
cap: string().min(1),
|
|
13403
|
-
/** Dotted path into the source cap's runtime-state slice. */
|
|
13404
|
-
fieldPath: string().min(1)
|
|
13405
|
-
});
|
|
13406
|
-
/** `from`: copy one source field verbatim. It is also an expression's `from` binding. */
|
|
13407
|
-
var CompositionFromSourceSchema = CompositionFieldReadSchema.extend({ kind: literal("from") });
|
|
13408
|
-
var CompositionBindingSchema = discriminatedUnion("kind", [CompositionFromSourceSchema, object({
|
|
13409
|
-
kind: literal("literal"),
|
|
13410
|
-
value: union([
|
|
13411
|
-
string(),
|
|
13412
|
-
number(),
|
|
13413
|
-
boolean(),
|
|
13414
|
-
_null()
|
|
13415
|
-
])
|
|
13416
|
-
})]);
|
|
13417
|
-
/**
|
|
13418
|
-
* `expression`: a formula over named bindings, in the salvaged expression
|
|
13419
|
-
* engine. Validated at parse by the SAME function every other consumer runs,
|
|
13420
|
-
* so the editor and the store cannot disagree.
|
|
13421
|
-
*/
|
|
13422
|
-
var CompositionExpressionSourceSchema = object({
|
|
13423
|
-
kind: literal("expression"),
|
|
13424
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
13425
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), CompositionBindingSchema)
|
|
13426
|
-
}).superRefine((src, ctx) => {
|
|
13427
|
-
const err = validateExpressionSource(src);
|
|
13428
|
-
if (err !== null) ctx.addIssue({
|
|
13429
|
-
code: "custom",
|
|
13430
|
-
message: err,
|
|
13431
|
-
path: ["expr"]
|
|
13432
|
-
});
|
|
13433
|
-
});
|
|
13434
|
-
/** `code`: RESERVED for slice 4 (own runner, one-way eject). */
|
|
13435
|
-
var CompositionCodeSourceSchema = object({
|
|
13436
|
-
kind: literal("code"),
|
|
13437
|
-
code: string().min(1).max(MAX_COMPOSITION_CODE_LENGTH)
|
|
13438
|
-
});
|
|
13439
|
-
var CompositionFieldSourceSchema = discriminatedUnion("kind", [
|
|
13440
|
-
CompositionFromSourceSchema,
|
|
13441
|
-
CompositionExpressionSourceSchema,
|
|
13442
|
-
CompositionCodeSourceSchema
|
|
13443
|
-
]);
|
|
13444
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
|
|
13445
|
-
kind: literal("forward"),
|
|
13446
|
-
source: CompositionSourceRefSchema,
|
|
13447
|
-
cap: string().min(1),
|
|
13448
|
-
method: string().min(1)
|
|
13449
|
-
}), CompositionCodeSourceSchema]);
|
|
13450
|
-
var CompositionFeatureSchema = discriminatedUnion("kind", [object({
|
|
13451
|
-
kind: literal("fields"),
|
|
13452
|
-
cap: string().min(1),
|
|
13453
|
-
fields: record(string().min(1), CompositionFieldSourceSchema).refine((fields) => Object.keys(fields).length <= 32, { message: `at most 32 fields per capability` }),
|
|
13454
|
-
/** RESERVED for slice 3; refused by the validator when non-empty. */
|
|
13455
|
-
commands: record(string().min(1), CompositionCommandTargetSchema).optional()
|
|
13456
|
-
}), object({
|
|
13457
|
-
kind: literal("passthrough"),
|
|
13458
|
-
cap: string().min(1),
|
|
13459
|
-
source: CompositionSourceRefSchema
|
|
13460
|
-
})]);
|
|
13461
|
-
var CompositionSchema = object({
|
|
13462
|
-
target: discriminatedUnion("kind", [object({
|
|
13463
|
-
kind: literal("new"),
|
|
13464
|
-
type: _enum(DeviceType),
|
|
13465
|
-
role: _enum(DeviceRole).optional()
|
|
13466
|
-
}), object({
|
|
13467
|
-
kind: literal("existing"),
|
|
13468
|
-
device: CompositionSourceRefSchema
|
|
13469
|
-
})]),
|
|
13470
|
-
features: array(CompositionFeatureSchema).min(1).max(16)
|
|
13471
|
-
}).superRefine((composition, ctx) => {
|
|
13472
|
-
const seen = /* @__PURE__ */ new Set();
|
|
13473
|
-
composition.features.forEach((feature, index) => {
|
|
13474
|
-
if (seen.has(feature.cap)) ctx.addIssue({
|
|
13475
|
-
code: "custom",
|
|
13476
|
-
message: `capability \`${feature.cap}\` is composed twice — one feature per capability`,
|
|
13477
|
-
path: [
|
|
13478
|
-
"features",
|
|
13479
|
-
index,
|
|
13480
|
-
"cap"
|
|
13481
|
-
]
|
|
13482
|
-
});
|
|
13483
|
-
seen.add(feature.cap);
|
|
13484
|
-
});
|
|
13485
|
-
});
|
|
13486
13533
|
/** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
|
|
13487
13534
|
var CompositionProblemSchema = object({
|
|
13488
13535
|
code: _enum([
|
|
@@ -13502,7 +13549,12 @@ var CompositionProblemSchema = object({
|
|
|
13502
13549
|
"source-field-unknown",
|
|
13503
13550
|
"expression-invalid",
|
|
13504
13551
|
"cycle",
|
|
13505
|
-
"duplicate-target"
|
|
13552
|
+
"duplicate-target",
|
|
13553
|
+
"not-item-array",
|
|
13554
|
+
"item-field-reserved",
|
|
13555
|
+
"target-missing",
|
|
13556
|
+
"target-unreadable",
|
|
13557
|
+
"not-replaceable"
|
|
13506
13558
|
]),
|
|
13507
13559
|
path: string(),
|
|
13508
13560
|
message: string()
|
|
@@ -13513,8 +13565,18 @@ var CompositionFieldKindSchema = _enum([
|
|
|
13513
13565
|
"boolean",
|
|
13514
13566
|
"enum"
|
|
13515
13567
|
]);
|
|
13516
|
-
/**
|
|
13517
|
-
|
|
13568
|
+
/**
|
|
13569
|
+
* How a field says "unavailable" (D393, D663): `null` when its schema allows it;
|
|
13570
|
+
* `unknown` — the enum's own word — for an ADDED field that has one; `release`
|
|
13571
|
+
* for a REPLACED field of an existing device, whose claim is released so the
|
|
13572
|
+
* native provider's reading shows again; else the (composed) device goes offline.
|
|
13573
|
+
*/
|
|
13574
|
+
var CompositionUnavailableModeSchema = _enum([
|
|
13575
|
+
"null",
|
|
13576
|
+
"offline",
|
|
13577
|
+
"unknown",
|
|
13578
|
+
"release"
|
|
13579
|
+
]);
|
|
13518
13580
|
/**
|
|
13519
13581
|
* `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
|
|
13520
13582
|
* field of its cap changes value · `constant` reads nothing, computed once.
|
|
@@ -13524,9 +13586,17 @@ var CompositionFieldRoleSchema = _enum([
|
|
|
13524
13586
|
"clock",
|
|
13525
13587
|
"constant"
|
|
13526
13588
|
]);
|
|
13589
|
+
/** The item a planned field belongs to; its `path` is then the dotted path INSIDE the item. */
|
|
13590
|
+
var CompositionFieldItemSchema = object({
|
|
13591
|
+
arrayPath: string(),
|
|
13592
|
+
key: string(),
|
|
13593
|
+
label: string()
|
|
13594
|
+
});
|
|
13527
13595
|
var CompositionFieldPlanSchema = object({
|
|
13528
13596
|
cap: string(),
|
|
13597
|
+
/** Top-level field, or — when `item` is set — the dotted path inside that item. */
|
|
13529
13598
|
path: string(),
|
|
13599
|
+
item: CompositionFieldItemSchema.optional(),
|
|
13530
13600
|
/** Index into `composition.features` this field's capability came from —
|
|
13531
13601
|
* lets a source/cycle problem name its path as `features[N].fields.<path>`
|
|
13532
13602
|
* (the same convention `planComposition`'s own problems already use). */
|
|
@@ -13539,16 +13609,37 @@ var CompositionFieldPlanSchema = object({
|
|
|
13539
13609
|
source: CompositionFieldSourceSchema,
|
|
13540
13610
|
reads: array(CompositionFieldReadSchema)
|
|
13541
13611
|
});
|
|
13612
|
+
/**
|
|
13613
|
+
* What a feature does to its target (D663): `new` builds a capability on a new
|
|
13614
|
+
* device; on an existing device it `add`s a capability the device lacks, or
|
|
13615
|
+
* `replace`s named fields of one it has natively.
|
|
13616
|
+
*/
|
|
13617
|
+
var CompositionFeatureModeSchema = _enum([
|
|
13618
|
+
"new",
|
|
13619
|
+
"add",
|
|
13620
|
+
"replace"
|
|
13621
|
+
]);
|
|
13622
|
+
var CompositionFeaturePlanSchema = object({
|
|
13623
|
+
featureIndex: number().int().nonnegative(),
|
|
13624
|
+
cap: string(),
|
|
13625
|
+
mode: CompositionFeatureModeSchema
|
|
13626
|
+
});
|
|
13542
13627
|
var CompositionValidationSchema = object({
|
|
13543
13628
|
ok: boolean(),
|
|
13544
13629
|
problems: array(CompositionProblemSchema),
|
|
13545
|
-
fields: array(CompositionFieldPlanSchema)
|
|
13630
|
+
fields: array(CompositionFieldPlanSchema),
|
|
13631
|
+
features: array(CompositionFeaturePlanSchema)
|
|
13546
13632
|
});
|
|
13547
|
-
/**
|
|
13633
|
+
/**
|
|
13634
|
+
* Mirrors `CoreBlockStatus`, pinned by `core-blocks-source.spec.ts`. `degraded`:
|
|
13635
|
+
* running, but a replaced field is released to the native provider while its
|
|
13636
|
+
* source is unavailable (ruling S1b, D663) — the reason names each such field.
|
|
13637
|
+
*/
|
|
13548
13638
|
var CompositionRunStatusSchema = _enum([
|
|
13549
13639
|
"stopped",
|
|
13550
13640
|
"starting",
|
|
13551
13641
|
"running",
|
|
13642
|
+
"degraded",
|
|
13552
13643
|
"failed"
|
|
13553
13644
|
]);
|
|
13554
13645
|
var CompositionFieldStateKindSchema = _enum([
|
|
@@ -13559,6 +13650,8 @@ var CompositionFieldStateKindSchema = _enum([
|
|
|
13559
13650
|
var CompositionFieldStateSchema = object({
|
|
13560
13651
|
cap: string(),
|
|
13561
13652
|
path: string(),
|
|
13653
|
+
/** Same as the plan's: two items' `status` are told apart by it. */
|
|
13654
|
+
item: CompositionFieldItemSchema.optional(),
|
|
13562
13655
|
state: CompositionFieldStateKindSchema,
|
|
13563
13656
|
/** `null` whenever `state === 'unavailable'`; never a plausible stand-in (D393). */
|
|
13564
13657
|
value: union([
|
|
@@ -13580,11 +13673,14 @@ var CompositionBlockStateSchema = object({
|
|
|
13580
13673
|
* agent is the reason placement is not fixed to the hub. */
|
|
13581
13674
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
13582
13675
|
/** What a block's process is doing. Mirrors the addon runner's own lifecycle so
|
|
13583
|
-
* a failing block reads the same way a failing addon does.
|
|
13676
|
+
* a failing block reads the same way a failing addon does. `degraded` is a
|
|
13677
|
+
* composition on an existing device with a field handed back to the native
|
|
13678
|
+
* provider while its source is unavailable (S1b, D663); the error names it. */
|
|
13584
13679
|
var CoreBlockStatusSchema = _enum([
|
|
13585
13680
|
"stopped",
|
|
13586
13681
|
"starting",
|
|
13587
13682
|
"running",
|
|
13683
|
+
"degraded",
|
|
13588
13684
|
"failed"
|
|
13589
13685
|
]);
|
|
13590
13686
|
/** Client-authored fields. */
|
|
@@ -13620,9 +13716,16 @@ var CoreBlockSourceSchema = discriminatedUnion("kind", [
|
|
|
13620
13716
|
composition: CompositionSchema
|
|
13621
13717
|
})
|
|
13622
13718
|
]);
|
|
13623
|
-
/**
|
|
13719
|
+
/**
|
|
13720
|
+
* The client-authored half of a composition block. No `code`, no placement (hub-only; Phase 7).
|
|
13721
|
+
*
|
|
13722
|
+
* `name`: REQUIRED for a `new` target — the block's name IS the device's name
|
|
13723
|
+
* (D659). ABSENT for an `existing` target — a customization has no name of its
|
|
13724
|
+
* own; the server derives one (`customizationBlockName`, D663) and refuses a
|
|
13725
|
+
* client-sent name by name.
|
|
13726
|
+
*/
|
|
13624
13727
|
var CompositionBlockInputSchema = object({
|
|
13625
|
-
name: string().min(1).max(120),
|
|
13728
|
+
name: string().min(1).max(120).optional(),
|
|
13626
13729
|
enabled: boolean(),
|
|
13627
13730
|
composition: CompositionSchema
|
|
13628
13731
|
});
|
|
@@ -13673,6 +13776,11 @@ var CompositionGetReadSchema = discriminatedUnion("state", [object({
|
|
|
13673
13776
|
state: literal("loaded"),
|
|
13674
13777
|
view: CompositionViewSchema.nullable()
|
|
13675
13778
|
}), CompositionsNotLoadedSchema]);
|
|
13779
|
+
/** The customization of one existing device, found by its TARGET, or `not-loaded` (D315). */
|
|
13780
|
+
var CompositionCustomizationReadSchema = discriminatedUnion("state", [object({
|
|
13781
|
+
state: literal("loaded"),
|
|
13782
|
+
view: CompositionViewSchema.nullable()
|
|
13783
|
+
}), CompositionsNotLoadedSchema]);
|
|
13676
13784
|
/** What a compile attempt produced. */
|
|
13677
13785
|
var CoreBlockCompileResultSchema = object({
|
|
13678
13786
|
ok: boolean(),
|
|
@@ -13723,6 +13831,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
13723
13831
|
auth: "admin",
|
|
13724
13832
|
caller: "required"
|
|
13725
13833
|
}), method(object({ blockId: string() }), CompositionGetReadSchema, { auth: "admin" }), method(object({}), CompositionListReadSchema, { auth: "admin" }), method(object({
|
|
13834
|
+
addonId: string().min(1),
|
|
13835
|
+
stableId: string().min(1)
|
|
13836
|
+
}), CompositionCustomizationReadSchema, { auth: "admin" }), method(object({
|
|
13726
13837
|
composition: CompositionSchema,
|
|
13727
13838
|
blockId: string().optional()
|
|
13728
13839
|
}), CompositionValidationSchema, {
|
|
@@ -15700,6 +15811,52 @@ method(object({
|
|
|
15700
15811
|
deviceIds: array(number()).readonly(),
|
|
15701
15812
|
caps: array(string()).readonly().optional()
|
|
15702
15813
|
}), record(string(), record(string(), unknown().nullable())));
|
|
15814
|
+
/**
|
|
15815
|
+
* Per-field ownership (D663). A composition may CLAIM fields of a native
|
|
15816
|
+
* cap's runtime-state slice on an existing device: `replace` overlays the
|
|
15817
|
+
* claimed fields on the native slice, `add` builds a slice the device does
|
|
15818
|
+
* not natively have. Declared here — once — so the hub mirror (Task 5) and
|
|
15819
|
+
* the claim methods (Task 6) share one union.
|
|
15820
|
+
*/
|
|
15821
|
+
var FieldClaimModeSchema = _enum(["replace", "add"]);
|
|
15822
|
+
/** What a claim, patch or release answers. A refusal always names its reason. */
|
|
15823
|
+
var ClaimOutcomeSchema = discriminatedUnion("ok", [object({
|
|
15824
|
+
ok: literal(true),
|
|
15825
|
+
changed: boolean()
|
|
15826
|
+
}), object({
|
|
15827
|
+
ok: literal(false),
|
|
15828
|
+
code: _enum([
|
|
15829
|
+
"owned-by-other",
|
|
15830
|
+
"not-claimed",
|
|
15831
|
+
"field-not-claimed",
|
|
15832
|
+
"invalid-slice",
|
|
15833
|
+
"unknown-device",
|
|
15834
|
+
"row-unreadable",
|
|
15835
|
+
"migration-in-flight"
|
|
15836
|
+
]),
|
|
15837
|
+
message: string()
|
|
15838
|
+
})]);
|
|
15839
|
+
/** One claim as `listClaims` reports it. `fields` is the PLANNED set, explicit (ruling S5). */
|
|
15840
|
+
var FieldClaimSchema = object({
|
|
15841
|
+
deviceId: number(),
|
|
15842
|
+
capName: string(),
|
|
15843
|
+
owner: string().min(1),
|
|
15844
|
+
mode: FieldClaimModeSchema,
|
|
15845
|
+
fields: array(string().min(1)).min(1)
|
|
15846
|
+
});
|
|
15847
|
+
/**
|
|
15848
|
+
* What `listClaims` answers. The claims INDEX is a listing projection loaded
|
|
15849
|
+
* from the settings store at boot; until a load has succeeded it is
|
|
15850
|
+
* `not-loaded`, never `[]` — an unanswered question must not look like an
|
|
15851
|
+
* empty answer (D315).
|
|
15852
|
+
*/
|
|
15853
|
+
var ClaimsListingSchema = discriminatedUnion("state", [object({
|
|
15854
|
+
state: literal("loaded"),
|
|
15855
|
+
claims: array(FieldClaimSchema)
|
|
15856
|
+
}), object({
|
|
15857
|
+
state: literal("not-loaded"),
|
|
15858
|
+
reason: string()
|
|
15859
|
+
})]);
|
|
15703
15860
|
method(object({ deviceId: number() }), record(string(), record(string(), unknown()))), method(object({
|
|
15704
15861
|
deviceId: number(),
|
|
15705
15862
|
capName: string()
|
|
@@ -15707,7 +15864,35 @@ method(object({ deviceId: number() }), record(string(), record(string(), unknown
|
|
|
15707
15864
|
deviceId: number(),
|
|
15708
15865
|
capName: string(),
|
|
15709
15866
|
slice: record(string(), unknown())
|
|
15710
|
-
}), _void(), { kind: "mutation" }), object({
|
|
15867
|
+
}), _void(), { kind: "mutation" }), method(object({
|
|
15868
|
+
deviceId: number(),
|
|
15869
|
+
capName: string(),
|
|
15870
|
+
owner: string().min(1),
|
|
15871
|
+
mode: FieldClaimModeSchema,
|
|
15872
|
+
fields: array(string().min(1)).min(1),
|
|
15873
|
+
values: record(string(), unknown())
|
|
15874
|
+
}), ClaimOutcomeSchema, {
|
|
15875
|
+
kind: "mutation",
|
|
15876
|
+
auth: "admin"
|
|
15877
|
+
}), method(object({
|
|
15878
|
+
deviceId: number(),
|
|
15879
|
+
capName: string(),
|
|
15880
|
+
owner: string().min(1),
|
|
15881
|
+
values: record(string(), unknown())
|
|
15882
|
+
}), ClaimOutcomeSchema, {
|
|
15883
|
+
kind: "mutation",
|
|
15884
|
+
auth: "admin"
|
|
15885
|
+
}), method(object({
|
|
15886
|
+
deviceId: number(),
|
|
15887
|
+
capName: string(),
|
|
15888
|
+
owner: string().min(1)
|
|
15889
|
+
}), ClaimOutcomeSchema, {
|
|
15890
|
+
kind: "mutation",
|
|
15891
|
+
auth: "admin"
|
|
15892
|
+
}), method(object({ ownerPrefix: string().optional() }), ClaimsListingSchema, { auth: "admin" }), method(object({
|
|
15893
|
+
deviceId: number(),
|
|
15894
|
+
capName: string()
|
|
15895
|
+
}), record(string(), unknown()).nullable(), { auth: "admin" }), object({
|
|
15711
15896
|
deviceId: number(),
|
|
15712
15897
|
capName: string(),
|
|
15713
15898
|
slice: record(string(), unknown())
|
|
@@ -29019,7 +29204,7 @@ var ExtensionSourceSchema = object({
|
|
|
29019
29204
|
/**
|
|
29020
29205
|
* How one picked source stands. `missing` — no such device (deleted, or an id
|
|
29021
29206
|
* that never existed); `foreign` — a device the slot's owner cannot read from
|
|
29022
|
-
* (
|
|
29207
|
+
* (a source owned by another addon); `unavailable` — it exists but is not a
|
|
29023
29208
|
* valid source now (a container, which is never a pick). A source in any of
|
|
29024
29209
|
* these states is SHOWN, never silently dropped or re-pointed.
|
|
29025
29210
|
*/
|
|
@@ -29059,10 +29244,6 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
|
|
|
29059
29244
|
slot: ExtensionSlotIdSchema,
|
|
29060
29245
|
/** Names the store written (D62), e.g. `virtual-doorbell:doorbellSources`. */
|
|
29061
29246
|
authority: string(),
|
|
29062
|
-
/** graft: its switch. association: null — removing every source is "off". */
|
|
29063
|
-
enabled: boolean().nullable(),
|
|
29064
|
-
/** graft: the cap registered on the host. association: null. */
|
|
29065
|
-
projects: string().nullable(),
|
|
29066
29247
|
activation: ExtensionSlotActivationSchema.nullable(),
|
|
29067
29248
|
value: ExtensionSlotValueSchema,
|
|
29068
29249
|
sources: array(ExtensionSlotSourceSchema),
|
|
@@ -29096,7 +29277,8 @@ var ExtensionCandidatesSchema = object({ candidates: array(object({
|
|
|
29096
29277
|
/**
|
|
29097
29278
|
* What a write did. `none` — applied in place. `host` / `parent` — the owner
|
|
29098
29279
|
* reloaded the host (or, for an accessory child, its parent) because the
|
|
29099
|
-
* write changed what the host REGISTERS
|
|
29280
|
+
* write changed what the host REGISTERS. A slot never adds a capability to its
|
|
29281
|
+
* host — that is a composition (D663) — so today's owners answer `none`.
|
|
29100
29282
|
*/
|
|
29101
29283
|
var ExtensionSetResultSchema = object({
|
|
29102
29284
|
applied: literal(true),
|
|
@@ -30304,7 +30486,9 @@ var batteryCapability = {
|
|
|
30304
30486
|
DeviceType.Camera,
|
|
30305
30487
|
DeviceType.Sensor,
|
|
30306
30488
|
DeviceType.Button,
|
|
30307
|
-
DeviceType.Switch
|
|
30489
|
+
DeviceType.Switch,
|
|
30490
|
+
DeviceType.PetFeeder,
|
|
30491
|
+
DeviceType.Container
|
|
30308
30492
|
],
|
|
30309
30493
|
methods: {
|
|
30310
30494
|
/**
|
|
@@ -30331,6 +30515,19 @@ wakeForStream: method(object({
|
|
|
30331
30515
|
awoke: boolean(),
|
|
30332
30516
|
durationMs: number()
|
|
30333
30517
|
}), { kind: "mutation" }) },
|
|
30518
|
+
/**
|
|
30519
|
+
* A composed provider (battery ADDED to a pet feeder or container that has
|
|
30520
|
+
* no wake surface) answers `wakeForStream` with the same "unavailable"
|
|
30521
|
+
* result the contract already defines above: "Returns `awoke: false` when
|
|
30522
|
+
* … the cap surface is unavailable." (D663).
|
|
30523
|
+
*/
|
|
30524
|
+
composedMethods: { wakeForStream: {
|
|
30525
|
+
kind: "answer",
|
|
30526
|
+
value: {
|
|
30527
|
+
awoke: false,
|
|
30528
|
+
durationMs: 0
|
|
30529
|
+
}
|
|
30530
|
+
} },
|
|
30334
30531
|
events: {
|
|
30335
30532
|
/**
|
|
30336
30533
|
* Emitted whenever the cached status changes (firmware push OR
|
|
@@ -30346,7 +30543,7 @@ onStatusChanged: { data: object({
|
|
|
30346
30543
|
schema: BatteryStatusSchema,
|
|
30347
30544
|
kind: "push",
|
|
30348
30545
|
empty: {
|
|
30349
|
-
percentage:
|
|
30546
|
+
percentage: null,
|
|
30350
30547
|
charging: "unknown",
|
|
30351
30548
|
sleeping: false,
|
|
30352
30549
|
lastUpdated: 0
|
|
@@ -31071,6 +31268,19 @@ var connectivityCapability = {
|
|
|
31071
31268
|
* provider populates it by guessing (no HA inference). The UI renders a
|
|
31072
31269
|
* "No consumables reported" placeholder when `items` is empty.
|
|
31073
31270
|
*/
|
|
31271
|
+
/** Units a remaining-life counter is reported in. Closed: the panel renders
|
|
31272
|
+
* each. */
|
|
31273
|
+
var ConsumableRemainingUnitSchema = _enum([
|
|
31274
|
+
"days",
|
|
31275
|
+
"hours",
|
|
31276
|
+
"cycles"
|
|
31277
|
+
]);
|
|
31278
|
+
/** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
|
|
31279
|
+
* days). `value` null = unknown (D393). */
|
|
31280
|
+
var ConsumableRemainingSchema = object({
|
|
31281
|
+
value: number().min(0).nullable(),
|
|
31282
|
+
unit: ConsumableRemainingUnitSchema
|
|
31283
|
+
});
|
|
31074
31284
|
/** A single consumable item. Either a continuous `level` (remaining
|
|
31075
31285
|
* life %) or a discrete `status` may be known — both may be null when a
|
|
31076
31286
|
* provider only knows the item exists. `level` and `status` are not
|
|
@@ -31087,7 +31297,10 @@ var ConsumableItemSchema = object({
|
|
|
31087
31297
|
/** Ms epoch of the last replace, when known. */
|
|
31088
31298
|
lastResetAt: number().nullable(),
|
|
31089
31299
|
/** Whether `reset()` is meaningful for this item. */
|
|
31090
|
-
resettable: boolean()
|
|
31300
|
+
resettable: boolean(),
|
|
31301
|
+
/** Remaining life as a count (days/hours/cycles), when a provider reports
|
|
31302
|
+
* it that way instead of — or alongside — `level`. Absent = not reported. */
|
|
31303
|
+
remaining: ConsumableRemainingSchema.optional()
|
|
31091
31304
|
});
|
|
31092
31305
|
var ConsumablesStatusSchema = object({
|
|
31093
31306
|
items: array(ConsumableItemSchema),
|
|
@@ -31117,6 +31330,16 @@ reset: method(object({
|
|
|
31117
31330
|
kind: "mutation",
|
|
31118
31331
|
auth: "admin"
|
|
31119
31332
|
}) },
|
|
31333
|
+
/**
|
|
31334
|
+
* A composed consumable (item added to a device with no native consumables
|
|
31335
|
+
* provider) refuses `reset` by name: "Only meaningful when the item's
|
|
31336
|
+
* `resettable` is true" — a composed item is never resettable until
|
|
31337
|
+
* commands (`forward`) arrive in slice 3 (D663).
|
|
31338
|
+
*/
|
|
31339
|
+
composedMethods: { reset: {
|
|
31340
|
+
kind: "refuse",
|
|
31341
|
+
reason: "a composed consumable is not resettable until commands (forward) arrive — slice 3"
|
|
31342
|
+
} },
|
|
31120
31343
|
status: {
|
|
31121
31344
|
schema: ConsumablesStatusSchema,
|
|
31122
31345
|
kind: "push",
|
|
@@ -40427,6 +40650,10 @@ Object.freeze({
|
|
|
40427
40650
|
latest: {
|
|
40428
40651
|
kind: "fixed",
|
|
40429
40652
|
result: ["number", "null"]
|
|
40653
|
+
},
|
|
40654
|
+
number: {
|
|
40655
|
+
kind: "fixed",
|
|
40656
|
+
result: ["number", "null"]
|
|
40430
40657
|
}
|
|
40431
40658
|
});
|
|
40432
40659
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -40626,38 +40853,31 @@ var DeviceConfig = class DeviceConfig {
|
|
|
40626
40853
|
}));
|
|
40627
40854
|
}
|
|
40628
40855
|
};
|
|
40629
|
-
/**
|
|
40630
|
-
* Concrete implementation. Routes every successful write through
|
|
40631
|
-
* `writer(capName, slice)` — the kernel hooks this up to
|
|
40632
|
-
* `device-state.setCapSlice`, the canonical cross-layer write
|
|
40633
|
-
* entrypoint, which handles disk persistence (debounced on the hub)
|
|
40634
|
-
* and mirror updates.
|
|
40635
|
-
*
|
|
40636
|
-
* Schema validation runs in-process before the writer is called —
|
|
40637
|
-
* the round-trip should never carry an invalid slice. `flush()`
|
|
40638
|
-
* awaits any in-flight writer promises so shutdown is lossless.
|
|
40639
|
-
*
|
|
40640
|
-
* `initial` is the persisted blob loaded at boot. Slices for caps
|
|
40641
|
-
* whose schema hasn't been installed yet are kept in-memory verbatim
|
|
40642
|
-
* and validated when the cap registers later.
|
|
40643
|
-
*/
|
|
40644
40856
|
var DeviceRuntimeState = class DeviceRuntimeState {
|
|
40645
40857
|
writer;
|
|
40858
|
+
onWriteFailed;
|
|
40646
40859
|
/** In-flight writer promises tracked so `flush()` can await them. */
|
|
40647
40860
|
pendingWrites = /* @__PURE__ */ new Set();
|
|
40861
|
+
/**
|
|
40862
|
+
* Caps whose last write the hub refused. The equality gate in
|
|
40863
|
+
* `applyCapWrite` is bypassed for these, so the SAME value is re-sent on the
|
|
40864
|
+
* next write instead of being swallowed forever; a landing write clears it.
|
|
40865
|
+
*/
|
|
40866
|
+
failedWrites = /* @__PURE__ */ new Set();
|
|
40648
40867
|
/** Per-cap committed slice — after schema validation when known. */
|
|
40649
40868
|
slices;
|
|
40650
40869
|
/** Per-cap registered schema (set by `installCapSchema`). */
|
|
40651
40870
|
schemas = /* @__PURE__ */ new Map();
|
|
40652
40871
|
listeners = /* @__PURE__ */ new Set();
|
|
40653
40872
|
capListeners = /* @__PURE__ */ new Map();
|
|
40654
|
-
constructor(initial, writer) {
|
|
40873
|
+
constructor(initial, writer, onWriteFailed) {
|
|
40655
40874
|
this.writer = writer;
|
|
40875
|
+
this.onWriteFailed = onWriteFailed;
|
|
40656
40876
|
this.slices = /* @__PURE__ */ new Map();
|
|
40657
40877
|
for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
|
|
40658
40878
|
}
|
|
40659
|
-
static fromInitial(initial, writer) {
|
|
40660
|
-
return new DeviceRuntimeState(initial, writer);
|
|
40879
|
+
static fromInitial(initial, writer, onWriteFailed) {
|
|
40880
|
+
return new DeviceRuntimeState(initial, writer, onWriteFailed);
|
|
40661
40881
|
}
|
|
40662
40882
|
installCapSchema(capName, schema) {
|
|
40663
40883
|
const existing = this.schemas.get(capName);
|
|
@@ -40701,10 +40921,15 @@ var DeviceRuntimeState = class DeviceRuntimeState {
|
|
|
40701
40921
|
...value
|
|
40702
40922
|
} : { ...value };
|
|
40703
40923
|
const parsed = schema.parse(next);
|
|
40704
|
-
if (shallowEqual(current, parsed)) return;
|
|
40924
|
+
if (shallowEqual(current, parsed) && !this.failedWrites.has(capName)) return;
|
|
40705
40925
|
this.slices.set(capName, parsed);
|
|
40706
40926
|
this.fireListeners([capName]);
|
|
40707
|
-
const writePromise = this.writer(capName, { ...parsed }).
|
|
40927
|
+
const writePromise = this.writer(capName, { ...parsed }).then(() => {
|
|
40928
|
+
this.failedWrites.delete(capName);
|
|
40929
|
+
}).catch((err) => {
|
|
40930
|
+
this.failedWrites.add(capName);
|
|
40931
|
+
this.onWriteFailed?.(capName, err instanceof Error ? err.message : String(err));
|
|
40932
|
+
});
|
|
40708
40933
|
this.pendingWrites.add(writePromise);
|
|
40709
40934
|
writePromise.finally(() => {
|
|
40710
40935
|
this.pendingWrites.delete(writePromise);
|
|
@@ -40981,7 +41206,16 @@ var BaseDevice = class {
|
|
|
40981
41206
|
});
|
|
40982
41207
|
};
|
|
40983
41208
|
const initial = ctx.initialRuntimeState ?? {};
|
|
40984
|
-
|
|
41209
|
+
const onWriteFailed = (cap, error) => {
|
|
41210
|
+
ctx.logger.warn("runtime-state write refused by the hub — the slice is not mirrored; it is re-sent on the next write", {
|
|
41211
|
+
tags: { deviceId: ctx.id },
|
|
41212
|
+
meta: {
|
|
41213
|
+
cap,
|
|
41214
|
+
error
|
|
41215
|
+
}
|
|
41216
|
+
});
|
|
41217
|
+
};
|
|
41218
|
+
this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer, onWriteFailed);
|
|
40985
41219
|
ctx.bindRuntimeState?.(this.runtimeState);
|
|
40986
41220
|
ctx.registerNativeCap?.(deviceStatusCapability, {});
|
|
40987
41221
|
const seed = {
|
|
@@ -43927,6 +44161,12 @@ Object.freeze({
|
|
|
43927
44161
|
addonId: null,
|
|
43928
44162
|
access: "view"
|
|
43929
44163
|
},
|
|
44164
|
+
"coreBlocks.getCustomization": {
|
|
44165
|
+
capName: "core-blocks",
|
|
44166
|
+
capScope: "system",
|
|
44167
|
+
addonId: null,
|
|
44168
|
+
access: "view"
|
|
44169
|
+
},
|
|
43930
44170
|
"coreBlocks.getTypeDefs": {
|
|
43931
44171
|
capName: "core-blocks",
|
|
43932
44172
|
capScope: "system",
|
|
@@ -45049,6 +45289,12 @@ Object.freeze({
|
|
|
45049
45289
|
addonId: null,
|
|
45050
45290
|
access: "create"
|
|
45051
45291
|
},
|
|
45292
|
+
"deviceState.claimFields": {
|
|
45293
|
+
capName: "device-state",
|
|
45294
|
+
capScope: "system",
|
|
45295
|
+
addonId: null,
|
|
45296
|
+
access: "create"
|
|
45297
|
+
},
|
|
45052
45298
|
"deviceState.getAllSnapshots": {
|
|
45053
45299
|
capName: "device-state",
|
|
45054
45300
|
capScope: "system",
|
|
@@ -45061,12 +45307,36 @@ Object.freeze({
|
|
|
45061
45307
|
addonId: null,
|
|
45062
45308
|
access: "view"
|
|
45063
45309
|
},
|
|
45310
|
+
"deviceState.getNativeCapSlice": {
|
|
45311
|
+
capName: "device-state",
|
|
45312
|
+
capScope: "system",
|
|
45313
|
+
addonId: null,
|
|
45314
|
+
access: "view"
|
|
45315
|
+
},
|
|
45064
45316
|
"deviceState.getSnapshot": {
|
|
45065
45317
|
capName: "device-state",
|
|
45066
45318
|
capScope: "system",
|
|
45067
45319
|
addonId: null,
|
|
45068
45320
|
access: "view"
|
|
45069
45321
|
},
|
|
45322
|
+
"deviceState.listClaims": {
|
|
45323
|
+
capName: "device-state",
|
|
45324
|
+
capScope: "system",
|
|
45325
|
+
addonId: null,
|
|
45326
|
+
access: "view"
|
|
45327
|
+
},
|
|
45328
|
+
"deviceState.patchOwnedFields": {
|
|
45329
|
+
capName: "device-state",
|
|
45330
|
+
capScope: "system",
|
|
45331
|
+
addonId: null,
|
|
45332
|
+
access: "create"
|
|
45333
|
+
},
|
|
45334
|
+
"deviceState.releaseClaim": {
|
|
45335
|
+
capName: "device-state",
|
|
45336
|
+
capScope: "system",
|
|
45337
|
+
addonId: null,
|
|
45338
|
+
access: "create"
|
|
45339
|
+
},
|
|
45070
45340
|
"deviceState.setCapSlice": {
|
|
45071
45341
|
capName: "device-state",
|
|
45072
45342
|
capScope: "system",
|
|
@@ -50582,16 +50852,36 @@ Object.freeze({
|
|
|
50582
50852
|
form: "single",
|
|
50583
50853
|
optional: false
|
|
50584
50854
|
}],
|
|
50855
|
+
"deviceState.claimFields": [{
|
|
50856
|
+
name: "deviceId",
|
|
50857
|
+
form: "single",
|
|
50858
|
+
optional: false
|
|
50859
|
+
}],
|
|
50585
50860
|
"deviceState.getCapSlice": [{
|
|
50586
50861
|
name: "deviceId",
|
|
50587
50862
|
form: "single",
|
|
50588
50863
|
optional: false
|
|
50589
50864
|
}],
|
|
50865
|
+
"deviceState.getNativeCapSlice": [{
|
|
50866
|
+
name: "deviceId",
|
|
50867
|
+
form: "single",
|
|
50868
|
+
optional: false
|
|
50869
|
+
}],
|
|
50590
50870
|
"deviceState.getSnapshot": [{
|
|
50591
50871
|
name: "deviceId",
|
|
50592
50872
|
form: "single",
|
|
50593
50873
|
optional: false
|
|
50594
50874
|
}],
|
|
50875
|
+
"deviceState.patchOwnedFields": [{
|
|
50876
|
+
name: "deviceId",
|
|
50877
|
+
form: "single",
|
|
50878
|
+
optional: false
|
|
50879
|
+
}],
|
|
50880
|
+
"deviceState.releaseClaim": [{
|
|
50881
|
+
name: "deviceId",
|
|
50882
|
+
form: "single",
|
|
50883
|
+
optional: false
|
|
50884
|
+
}],
|
|
50595
50885
|
"deviceState.setCapSlice": [{
|
|
50596
50886
|
name: "deviceId",
|
|
50597
50887
|
form: "single",
|