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