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