@camstack/types 1.2.451 → 1.2.453

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/addon/base-addon.d.ts +27 -0
  3. package/dist/addon.js +8 -3
  4. package/dist/addon.mjs +8 -3
  5. package/dist/automation/condition-eval.d.ts +20 -5
  6. package/dist/automation/detection-subject.d.ts +154 -0
  7. package/dist/automation/detection-trigger.d.ts +16 -6
  8. package/dist/automation/index.d.ts +10 -4
  9. package/dist/automation/legacy-recipe.d.ts +130 -0
  10. package/dist/automation/normalize-units.d.ts +4 -3
  11. package/dist/automation/recipe.d.ts +286 -142
  12. package/dist/automation/stationary-trigger.d.ts +4 -4
  13. package/dist/automation/step-runner.d.ts +3 -1
  14. package/dist/automation/trace.d.ts +51 -6
  15. package/dist/automation/when.d.ts +295 -0
  16. package/dist/capabilities/actions.cap.d.ts +10 -0
  17. package/dist/capabilities/addon-settings.cap.d.ts +18 -0
  18. package/dist/capabilities/core-blocks.cap.d.ts +2546 -51
  19. package/dist/capabilities/device-discovery.cap.d.ts +2 -0
  20. package/dist/capabilities/device-manager.cap.d.ts +16 -1
  21. package/dist/capabilities/device-provider.cap.d.ts +165 -0
  22. package/dist/capabilities/index.d.ts +3 -3
  23. package/dist/capabilities/integrations.cap.d.ts +1 -0
  24. package/dist/capabilities/keychain.cap.d.ts +12 -0
  25. package/dist/capabilities/local-network.cap.d.ts +198 -0
  26. package/dist/capabilities/storage-admin.cap.d.ts +155 -0
  27. package/dist/capabilities/terminal-session.cap.d.ts +7 -0
  28. package/dist/client.js +8 -1
  29. package/dist/client.mjs +8 -1
  30. package/dist/device/base-device-provider.d.ts +17 -0
  31. package/dist/generated/addon-api.d.ts +84 -0
  32. package/dist/generated/collection-array-methods.d.ts +1 -1
  33. package/dist/generated/method-access-map.d.ts +1 -1
  34. package/dist/generated/system-proxy.d.ts +3 -3
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.js +1969 -706
  37. package/dist/index.mjs +1934 -703
  38. package/dist/interfaces/addon.d.ts +16 -0
  39. package/dist/interfaces/config-ui-secrets.d.ts +5 -3
  40. package/dist/interfaces/config-ui.d.ts +34 -1
  41. package/dist/{sleep-BSPhInRm.js → sleep-CewR0zRz.js} +125 -6
  42. package/dist/{sleep-63gM_1JR.mjs → sleep-CeyhrcOk.mjs} +125 -6
  43. package/dist/vendor-login-proxy/cookie-jar.d.ts +30 -0
  44. package/dist/vendor-login-proxy/index.d.ts +1 -0
  45. package/dist/vendor-login-proxy/login-proxy.d.ts +6 -0
  46. package/dist/vendor-login-proxy/url-rewrite.d.ts +9 -2
  47. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-BSPhInRm.js");
2
+ const require_sleep = require("./sleep-CewR0zRz.js");
3
3
  const require_event_category = require("./event-category-E17zaHjN.js");
4
4
  const require_composition = require("./composition-tP-vAKqU.js");
5
5
  const require_node_color = require("./node-color.js");
@@ -8845,47 +8845,664 @@ function paramMetaOf(field) {
8845
8845
  return metaOf(field) ?? metaOf(unwrapSchema(field));
8846
8846
  }
8847
8847
  //#endregion
8848
- //#region src/automation/recipe.ts
8848
+ //#region src/catalogs/coco-classmap.ts
8849
+ var COCO_80_LABELS = [
8850
+ {
8851
+ id: "person",
8852
+ name: "Person"
8853
+ },
8854
+ {
8855
+ id: "bicycle",
8856
+ name: "Bicycle"
8857
+ },
8858
+ {
8859
+ id: "car",
8860
+ name: "Car"
8861
+ },
8862
+ {
8863
+ id: "motorcycle",
8864
+ name: "Motorcycle"
8865
+ },
8866
+ {
8867
+ id: "airplane",
8868
+ name: "Airplane"
8869
+ },
8870
+ {
8871
+ id: "bus",
8872
+ name: "Bus"
8873
+ },
8874
+ {
8875
+ id: "train",
8876
+ name: "Train"
8877
+ },
8878
+ {
8879
+ id: "truck",
8880
+ name: "Truck"
8881
+ },
8882
+ {
8883
+ id: "boat",
8884
+ name: "Boat"
8885
+ },
8886
+ {
8887
+ id: "traffic light",
8888
+ name: "Traffic Light"
8889
+ },
8890
+ {
8891
+ id: "fire hydrant",
8892
+ name: "Fire Hydrant"
8893
+ },
8894
+ {
8895
+ id: "stop sign",
8896
+ name: "Stop Sign"
8897
+ },
8898
+ {
8899
+ id: "parking meter",
8900
+ name: "Parking Meter"
8901
+ },
8902
+ {
8903
+ id: "bench",
8904
+ name: "Bench"
8905
+ },
8906
+ {
8907
+ id: "bird",
8908
+ name: "Bird"
8909
+ },
8910
+ {
8911
+ id: "cat",
8912
+ name: "Cat"
8913
+ },
8914
+ {
8915
+ id: "dog",
8916
+ name: "Dog"
8917
+ },
8918
+ {
8919
+ id: "horse",
8920
+ name: "Horse"
8921
+ },
8922
+ {
8923
+ id: "sheep",
8924
+ name: "Sheep"
8925
+ },
8926
+ {
8927
+ id: "cow",
8928
+ name: "Cow"
8929
+ },
8930
+ {
8931
+ id: "elephant",
8932
+ name: "Elephant"
8933
+ },
8934
+ {
8935
+ id: "bear",
8936
+ name: "Bear"
8937
+ },
8938
+ {
8939
+ id: "zebra",
8940
+ name: "Zebra"
8941
+ },
8942
+ {
8943
+ id: "giraffe",
8944
+ name: "Giraffe"
8945
+ },
8946
+ {
8947
+ id: "backpack",
8948
+ name: "Backpack"
8949
+ },
8950
+ {
8951
+ id: "umbrella",
8952
+ name: "Umbrella"
8953
+ },
8954
+ {
8955
+ id: "handbag",
8956
+ name: "Handbag"
8957
+ },
8958
+ {
8959
+ id: "tie",
8960
+ name: "Tie"
8961
+ },
8962
+ {
8963
+ id: "suitcase",
8964
+ name: "Suitcase"
8965
+ },
8966
+ {
8967
+ id: "frisbee",
8968
+ name: "Frisbee"
8969
+ },
8970
+ {
8971
+ id: "skis",
8972
+ name: "Skis"
8973
+ },
8974
+ {
8975
+ id: "snowboard",
8976
+ name: "Snowboard"
8977
+ },
8978
+ {
8979
+ id: "sports ball",
8980
+ name: "Sports Ball"
8981
+ },
8982
+ {
8983
+ id: "kite",
8984
+ name: "Kite"
8985
+ },
8986
+ {
8987
+ id: "baseball bat",
8988
+ name: "Baseball Bat"
8989
+ },
8990
+ {
8991
+ id: "baseball glove",
8992
+ name: "Baseball Glove"
8993
+ },
8994
+ {
8995
+ id: "skateboard",
8996
+ name: "Skateboard"
8997
+ },
8998
+ {
8999
+ id: "surfboard",
9000
+ name: "Surfboard"
9001
+ },
9002
+ {
9003
+ id: "tennis racket",
9004
+ name: "Tennis Racket"
9005
+ },
9006
+ {
9007
+ id: "bottle",
9008
+ name: "Bottle"
9009
+ },
9010
+ {
9011
+ id: "wine glass",
9012
+ name: "Wine Glass"
9013
+ },
9014
+ {
9015
+ id: "cup",
9016
+ name: "Cup"
9017
+ },
9018
+ {
9019
+ id: "fork",
9020
+ name: "Fork"
9021
+ },
9022
+ {
9023
+ id: "knife",
9024
+ name: "Knife"
9025
+ },
9026
+ {
9027
+ id: "spoon",
9028
+ name: "Spoon"
9029
+ },
9030
+ {
9031
+ id: "bowl",
9032
+ name: "Bowl"
9033
+ },
9034
+ {
9035
+ id: "banana",
9036
+ name: "Banana"
9037
+ },
9038
+ {
9039
+ id: "apple",
9040
+ name: "Apple"
9041
+ },
9042
+ {
9043
+ id: "sandwich",
9044
+ name: "Sandwich"
9045
+ },
9046
+ {
9047
+ id: "orange",
9048
+ name: "Orange"
9049
+ },
9050
+ {
9051
+ id: "broccoli",
9052
+ name: "Broccoli"
9053
+ },
9054
+ {
9055
+ id: "carrot",
9056
+ name: "Carrot"
9057
+ },
9058
+ {
9059
+ id: "hot dog",
9060
+ name: "Hot Dog"
9061
+ },
9062
+ {
9063
+ id: "pizza",
9064
+ name: "Pizza"
9065
+ },
9066
+ {
9067
+ id: "donut",
9068
+ name: "Donut"
9069
+ },
9070
+ {
9071
+ id: "cake",
9072
+ name: "Cake"
9073
+ },
9074
+ {
9075
+ id: "chair",
9076
+ name: "Chair"
9077
+ },
9078
+ {
9079
+ id: "couch",
9080
+ name: "Couch"
9081
+ },
9082
+ {
9083
+ id: "potted plant",
9084
+ name: "Potted Plant"
9085
+ },
9086
+ {
9087
+ id: "bed",
9088
+ name: "Bed"
9089
+ },
9090
+ {
9091
+ id: "dining table",
9092
+ name: "Dining Table"
9093
+ },
9094
+ {
9095
+ id: "toilet",
9096
+ name: "Toilet"
9097
+ },
9098
+ {
9099
+ id: "tv",
9100
+ name: "TV"
9101
+ },
9102
+ {
9103
+ id: "laptop",
9104
+ name: "Laptop"
9105
+ },
9106
+ {
9107
+ id: "mouse",
9108
+ name: "Mouse"
9109
+ },
9110
+ {
9111
+ id: "remote",
9112
+ name: "Remote"
9113
+ },
9114
+ {
9115
+ id: "keyboard",
9116
+ name: "Keyboard"
9117
+ },
9118
+ {
9119
+ id: "cell phone",
9120
+ name: "Cell Phone"
9121
+ },
9122
+ {
9123
+ id: "microwave",
9124
+ name: "Microwave"
9125
+ },
9126
+ {
9127
+ id: "oven",
9128
+ name: "Oven"
9129
+ },
9130
+ {
9131
+ id: "toaster",
9132
+ name: "Toaster"
9133
+ },
9134
+ {
9135
+ id: "sink",
9136
+ name: "Sink"
9137
+ },
9138
+ {
9139
+ id: "refrigerator",
9140
+ name: "Refrigerator"
9141
+ },
9142
+ {
9143
+ id: "book",
9144
+ name: "Book"
9145
+ },
9146
+ {
9147
+ id: "clock",
9148
+ name: "Clock"
9149
+ },
9150
+ {
9151
+ id: "vase",
9152
+ name: "Vase"
9153
+ },
9154
+ {
9155
+ id: "scissors",
9156
+ name: "Scissors"
9157
+ },
9158
+ {
9159
+ id: "teddy bear",
9160
+ name: "Teddy Bear"
9161
+ },
9162
+ {
9163
+ id: "hair drier",
9164
+ name: "Hair Drier"
9165
+ },
9166
+ {
9167
+ id: "toothbrush",
9168
+ name: "Toothbrush"
9169
+ }
9170
+ ];
9171
+ var MACRO_LABELS = [
9172
+ {
9173
+ id: "person",
9174
+ name: "Person"
9175
+ },
9176
+ {
9177
+ id: "vehicle",
9178
+ name: "Vehicle"
9179
+ },
9180
+ {
9181
+ id: "animal",
9182
+ name: "Animal"
9183
+ },
9184
+ {
9185
+ id: "package",
9186
+ name: "Package"
9187
+ }
9188
+ ];
8849
9189
  /**
8850
- * The automation RECIPE — what a visually-authored automation is, before it
8851
- * becomes code.
9190
+ * The class names a per-class COUNT can ever be keyed on — the macro ids, and
9191
+ * only those.
8852
9192
  *
8853
- * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
8854
- * Phase 6. A recipe is not an engine and never evaluates anything: it is a
8855
- * declarative description that {@link generateAutomationBlock} compiles to the
8856
- * TypeScript of an ordinary core block. There is therefore no second rule
8857
- * engine, no second store and no second runtime — the block runner is the
8858
- * runtime, `core_blocks` is the store (§2.1).
9193
+ * `preserveOriginal: false` below is the whole reason this set exists: the
9194
+ * executor maps each raw COCO label onto its macro and DROPS everything else,
9195
+ * so a tracked detection's `className` is always one of {@link MACRO_LABELS}.
9196
+ * Any surface that indexes counts by class name — the ZoneAnalytics occupancy
9197
+ * snapshot's `byClass`, and therefore every occupancy rule — can match nothing
9198
+ * else. Derived from the catalog, never hand-listed, so a new macro joins it by
9199
+ * being declared once.
9200
+ *
9201
+ * Cost of not having it, 2026-08-12: the notification rule editor offered the
9202
+ * full taxonomy for an occupancy class (occupancy is the one condition that
9203
+ * does not go through the engine's macro-expanding `expandClassSelector`), two
9204
+ * live rules were authored on `car`, and both counters read 0 forever.
9205
+ */
9206
+ var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
9207
+ /** True when `className` is a macro the detector can actually emit (and hence a
9208
+ * class a count can be keyed on) — see {@link DETECTION_MACRO_CLASSES}. */
9209
+ function isDetectionMacroClass(className) {
9210
+ return DETECTION_MACRO_CLASSES.has(className.trim().toLowerCase());
9211
+ }
9212
+ var COCO_TO_MACRO = {
9213
+ mapping: {
9214
+ person: "person",
9215
+ bicycle: "vehicle",
9216
+ car: "vehicle",
9217
+ motorcycle: "vehicle",
9218
+ airplane: "vehicle",
9219
+ bus: "vehicle",
9220
+ train: "vehicle",
9221
+ truck: "vehicle",
9222
+ boat: "vehicle",
9223
+ bird: "animal",
9224
+ cat: "animal",
9225
+ dog: "animal",
9226
+ horse: "animal",
9227
+ sheep: "animal",
9228
+ cow: "animal",
9229
+ elephant: "animal",
9230
+ bear: "animal",
9231
+ zebra: "animal",
9232
+ giraffe: "animal",
9233
+ suitcase: "package",
9234
+ backpack: "package",
9235
+ handbag: "package"
9236
+ },
9237
+ preserveOriginal: false
9238
+ };
9239
+ //#endregion
9240
+ //#region src/automation/detection-subject.ts
9241
+ /**
9242
+ * WHAT a detection-shaped leaf is about: one MACRO class (person, vehicle,
9243
+ * animal, package) plus an OPTIONAL label that narrows it.
8859
9244
  *
8860
- * Three vocabularies, each salvaged rather than invented:
9245
+ * The taxonomy is `COCO_TO_MACRO` (and `CLASS_MAP_MACRO_TARGETS`) — never a
9246
+ * second list. The label that fits depends on the macro class:
8861
9247
  *
8862
- * - **Triggers** — the only genuinely new surface. Seven members, none of which
8863
- * touches notification delivery. The NC keeps its four *pipeline* triggers;
8864
- * these are device/time triggers and the two sets do not overlap.
8865
- * - **Conditions** — `ALL`/`ANY`/`NOT` over three leaf kinds, bounded. The
8866
- * expression leaf is the bounded engine at `../expression/`, which is where
8867
- * arithmetic between two device fields lives and the only place it lives.
8868
- * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
8869
- * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
8870
- * this had to diverge from "verbatim", and why.
9248
+ * - `animal` → a sub-class (`dog`, `cat`, …);
9249
+ * - `person` → a known face (an identity of the `face-gallery`);
9250
+ * - `vehicle` → a plate (its text) or a sub-class (`car`, `truck`, …);
9251
+ * - `package` → a sub-class.
9252
+ *
9253
+ * `other` exists only so a class the taxonomy does not know (a custom model's
9254
+ * `forklift`) survives the conversion of a stored recipe without widening.
9255
+ *
9256
+ * A label is decided LATE (D683): face recognition and LPR land after the
9257
+ * detection starts. The track-lifecycle `update` event carries the newly
9258
+ * resolved `label` / `plateText`, which is why a leaf with a label fires when
9259
+ * the subject GETS it — not at the first detection.
9260
+ */
9261
+ var SubClassLabelSchema = zod.z.object({
9262
+ kind: zod.z.literal("sub-class"),
9263
+ className: zod.z.string().min(1)
9264
+ });
9265
+ var IdentityLabelSchema = zod.z.object({
9266
+ kind: zod.z.literal("identity"),
9267
+ /** The `face-gallery` identity id (stable across a rename). */
9268
+ identityId: zod.z.string().min(1),
9269
+ /** The identity's name — what a track-lifecycle event carries as `label`. */
9270
+ name: zod.z.string().min(1)
9271
+ });
9272
+ var PlateLabelSchema = zod.z.object({
9273
+ kind: zod.z.literal("plate"),
9274
+ text: zod.z.string().min(1)
9275
+ });
9276
+ function macroOf$1(className) {
9277
+ return COCO_TO_MACRO.mapping[className.toLowerCase()] ?? className.toLowerCase();
9278
+ }
9279
+ /** A sub-class label must belong to the macro it is under. */
9280
+ function subClassBelongs(macro, label) {
9281
+ return label === void 0 || macroOf$1(label.className) === macro;
9282
+ }
9283
+ var subClassOf = (macro) => SubClassLabelSchema.refine((l) => subClassBelongs(macro, l), { message: `that class is not a kind of ${macro} in the detection taxonomy` });
9284
+ var DetectionSubjectSchema = zod.z.discriminatedUnion("macro", [
9285
+ zod.z.object({
9286
+ macro: zod.z.literal("animal"),
9287
+ label: subClassOf("animal").optional()
9288
+ }),
9289
+ zod.z.object({
9290
+ macro: zod.z.literal("person"),
9291
+ label: IdentityLabelSchema.optional()
9292
+ }),
9293
+ zod.z.object({
9294
+ macro: zod.z.literal("vehicle"),
9295
+ label: zod.z.discriminatedUnion("kind", [SubClassLabelSchema.refine((l) => subClassBelongs("vehicle", l), { message: "that class is not a kind of vehicle in the detection taxonomy" }), PlateLabelSchema]).optional()
9296
+ }),
9297
+ zod.z.object({
9298
+ macro: zod.z.literal("package"),
9299
+ label: subClassOf("package").optional()
9300
+ }),
9301
+ zod.z.object({
9302
+ macro: zod.z.literal("other"),
9303
+ className: zod.z.string().min(1)
9304
+ })
9305
+ ]);
9306
+ /**
9307
+ * The subject of a PARKED object. The stationary registry's event carries a
9308
+ * class and a zone, no face and no plate — an identity or plate label there
9309
+ * could never match, so the schema refuses it instead of letting it sit dead.
9310
+ */
9311
+ var StationarySubjectSchema = DetectionSubjectSchema.refine((s) => s.macro === "other" || s.label === void 0 || s.label.kind === "sub-class", { message: "a parked object carries no face or plate — only a class can narrow it" });
9312
+ /**
9313
+ * Where an editor lists the labels of a macro class. Sub-classes come from the
9314
+ * taxonomy (`subClassesOf`); identities and plates come from the galleries'
9315
+ * own methods, so a picker reads the cap and never a copy of it.
9316
+ */
9317
+ var DETECTION_LABEL_SOURCES = {
9318
+ animal: [{ kind: "sub-class" }],
9319
+ person: [{
9320
+ kind: "cap",
9321
+ cap: "face-gallery",
9322
+ method: "listIdentities",
9323
+ label: "identity"
9324
+ }],
9325
+ vehicle: [{
9326
+ kind: "cap",
9327
+ cap: "plate-gallery",
9328
+ method: "listPlates",
9329
+ label: "plate"
9330
+ }, { kind: "sub-class" }],
9331
+ package: [{ kind: "sub-class" }]
9332
+ };
9333
+ /** The raw classes the taxonomy files under a macro class. */
9334
+ function subClassesOf(macro) {
9335
+ return Object.entries(COCO_TO_MACRO.mapping).filter(([, m]) => m === macro).map(([raw]) => raw);
9336
+ }
9337
+ /**
9338
+ * A stored class string as a subject: a macro name stays a macro; a raw class
9339
+ * the taxonomy files under a macro becomes that macro plus the sub-class label
9340
+ * (`dog` → animal / dog); anything else is `other`, kept verbatim.
9341
+ */
9342
+ function subjectFromClassName(className) {
9343
+ const name = className.trim().toLowerCase();
9344
+ const macro = COCO_TO_MACRO.mapping[name];
9345
+ if (new Set(Object.values(COCO_TO_MACRO.mapping)).has(name)) return macroOnly(name);
9346
+ if (macro === void 0) return {
9347
+ macro: "other",
9348
+ className
9349
+ };
9350
+ const label = {
9351
+ kind: "sub-class",
9352
+ className: name
9353
+ };
9354
+ switch (macro) {
9355
+ case "animal": return {
9356
+ macro: "animal",
9357
+ label
9358
+ };
9359
+ case "vehicle": return {
9360
+ macro: "vehicle",
9361
+ label
9362
+ };
9363
+ case "package": return {
9364
+ macro: "package",
9365
+ label
9366
+ };
9367
+ default: return {
9368
+ macro: "other",
9369
+ className
9370
+ };
9371
+ }
9372
+ }
9373
+ function macroOnly(macro) {
9374
+ switch (macro) {
9375
+ case "animal": return { macro: "animal" };
9376
+ case "person": return { macro: "person" };
9377
+ case "vehicle": return { macro: "vehicle" };
9378
+ case "package": return { macro: "package" };
9379
+ default: return {
9380
+ macro: "other",
9381
+ className: macro
9382
+ };
9383
+ }
9384
+ }
9385
+ function normalisePlate(text) {
9386
+ return text.toUpperCase().replace(/[^A-Z0-9]/g, "");
9387
+ }
9388
+ /** A human name for a subject, for a reason line. */
9389
+ function describeSubject(subject) {
9390
+ if (subject.macro === "other") return subject.className;
9391
+ const label = subject.label;
9392
+ if (label === void 0) return subject.macro;
9393
+ switch (label.kind) {
9394
+ case "sub-class": return `${subject.macro} ${label.className}`;
9395
+ case "identity": return `${subject.macro} ${label.name}`;
9396
+ case "plate": return `${subject.macro} plate ${label.text}`;
9397
+ }
9398
+ }
9399
+ /** Does one observation satisfy one subject, and which label did it match? */
9400
+ function subjectMatches(subject, observed) {
9401
+ const className = observed.className.toLowerCase();
9402
+ if (subject.macro === "other") return className === subject.className.toLowerCase() ? {
9403
+ matches: true,
9404
+ label: ""
9405
+ } : {
9406
+ matches: false,
9407
+ reason: `class ${observed.className || "unknown"} is not ${subject.className}`
9408
+ };
9409
+ if (macroOf$1(className) !== subject.macro) return {
9410
+ matches: false,
9411
+ reason: `class ${observed.className || "unknown"} is not a ${subject.macro}`
9412
+ };
9413
+ const label = subject.label;
9414
+ if (label === void 0) return {
9415
+ matches: true,
9416
+ label: ""
9417
+ };
9418
+ switch (label.kind) {
9419
+ case "sub-class": {
9420
+ const wanted = label.className.toLowerCase();
9421
+ return [
9422
+ className,
9423
+ ...observed.classes ?? [],
9424
+ observed.label ?? ""
9425
+ ].map((c) => c.toLowerCase()).includes(wanted) ? {
9426
+ matches: true,
9427
+ label: label.className
9428
+ } : {
9429
+ matches: false,
9430
+ reason: `${subject.macro} ${label.className} not seen yet`
9431
+ };
9432
+ }
9433
+ case "identity": {
9434
+ const byId = observed.identityId !== void 0 && observed.identityId === label.identityId;
9435
+ const byName = observed.label !== void 0 && observed.label.toLowerCase() === label.name.toLowerCase();
9436
+ return byId || byName ? {
9437
+ matches: true,
9438
+ label: label.name
9439
+ } : {
9440
+ matches: false,
9441
+ reason: `face ${label.name} not recognised yet${observed.label === void 0 || observed.label === "" ? "" : ` (seen as ${observed.label})`}`
9442
+ };
9443
+ }
9444
+ case "plate": {
9445
+ const text = observed.plateText ?? observed.label ?? "";
9446
+ return text !== "" && normalisePlate(text) === normalisePlate(label.text) ? {
9447
+ matches: true,
9448
+ label: label.text
9449
+ } : {
9450
+ matches: false,
9451
+ reason: `plate ${label.text} not read yet${text === "" ? "" : ` (read ${text})`}`
9452
+ };
9453
+ }
9454
+ }
9455
+ }
9456
+ /** The first subject an observation satisfies, else why the last one did not. */
9457
+ function anySubjectMatches(subjects, observed) {
9458
+ let lastReason = "no subject listed";
9459
+ for (const subject of subjects) {
9460
+ const verdict = subjectMatches(subject, observed);
9461
+ if (verdict.matches) return verdict;
9462
+ lastReason = verdict.reason;
9463
+ }
9464
+ return {
9465
+ matches: false,
9466
+ reason: subjects.length === 1 ? lastReason : `none of [${subjects.map(describeSubject).join(", ")}] matches (${observed.className || "unknown"})`
9467
+ };
9468
+ }
9469
+ //#endregion
9470
+ //#region src/automation/when.ts
9471
+ /**
9472
+ * The `when` tree — the ONE place an automation says when it runs.
9473
+ *
9474
+ * Containers are `all` / `any` / `not`. Leaves come in two kinds, told apart by
9475
+ * what they ARE, not by which section of a form they sit in:
9476
+ *
9477
+ * - **event leaves (instants)** — something HAPPENS: a device field *becomes*
9478
+ * a value, a device event, a schedule tick, an interval, a manual run, a
9479
+ * detection, a parked object appearing or leaving;
9480
+ * - **state leaves (durations)** — something IS true: a device field *is*
9481
+ * compared to a value, a parked object is present, an expression holds.
8871
9482
  *
8872
- * Bounds are a guard, not a paragraph (§3.2.1): depth ≤ 3, ≤ 32 leaves. They are
8873
- * enforced by {@link validateRecipeBounds} AND by the schema, so a recipe that
8874
- * arrives over the wire cannot skip the check that the editor applies.
9483
+ * Firing rule (ADR, "one tree"):
9484
+ * 1. A tree with event leaves fires when ONE of them occurs AND the whole tree
9485
+ * is true at that instant. The event that occurred is true; every other
9486
+ * event leaf is false.
9487
+ * 2. A tree of state leaves only fires when it goes false → true. The first
9488
+ * evaluation seeds the state, so it never fires because it was already true.
9489
+ * 3. Two event leaves that can only be satisfied together are refused at
9490
+ * author time ({@link validateWhenTree}).
8875
9491
  */
8876
9492
  /**
8877
- * Depth of a condition tree, counting the root container as level 1.
9493
+ * Depth of the `when` tree, counting the root container as level 1. The old
9494
+ * condition tree was bounded at 3 under a flat list of triggers; the trigger
9495
+ * list is now the tree's top `any`, one level, so the bound is 3 + 1 and a
9496
+ * converted recipe never exceeds it.
8878
9497
  *
8879
- * Three is enough and is not arbitrary: `NOT` over a group is always rewritable
8880
- * by De Morgan (`not(any(a,b))` ≡ `all(not a, not b)`), so the shapes that would
8881
- * need a fourth level have an equivalent at three. An editor that refuses depth
8882
- * 4 should offer that rewrite rather than refusing blankly.
9498
+ * `NOT` over a group is always rewritable by De Morgan, so the shapes that
9499
+ * would need a fifth level have an equivalent at four.
8883
9500
  */
8884
- var MAX_CONDITION_DEPTH = 3;
9501
+ var MAX_WHEN_DEPTH = 4;
8885
9502
  /** Leaves per automation. A UI that grows without a bound becomes a language. */
8886
- var MAX_CONDITION_LEAVES = 32;
8887
- /** How a leaf compares a device field to a value. Derived from the field's
8888
- * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
9503
+ var MAX_WHEN_LEAVES = 32;
9504
+ /** How a state leaf compares a device field to a value. Derived from the
9505
+ * field's `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
8889
9506
  var AutomationConditionOperatorSchema = zod.z.enum([
8890
9507
  "eq",
8891
9508
  "ne",
@@ -8896,12 +9513,76 @@ var AutomationConditionOperatorSchema = zod.z.enum([
8896
9513
  "contains",
8897
9514
  "in"
8898
9515
  ]);
8899
- var AutomationConditionLeafSchema = zod.z.object({
8900
- kind: zod.z.literal("condition"),
9516
+ /**
9517
+ * D8 compliance, and it is the reason a "becomes" leaf is not merely an event
9518
+ * subscription: it evaluates against the **state mirror**, which is reconciled,
9519
+ * and an event only WAKES the evaluation. A dropped event therefore DELAYS a
9520
+ * fire; it does not lose it.
9521
+ */
9522
+ var DeviceBecomesLeafSchema = zod.z.object({
9523
+ kind: zod.z.literal("device-becomes"),
9524
+ deviceId: zod.z.number().int().nonnegative(),
9525
+ cap: zod.z.string().min(1),
9526
+ fieldPath: zod.z.string().min(1),
9527
+ /** Fire when the field takes this value. Omit to fire on any change. */
9528
+ becomes: zod.z.union([
9529
+ zod.z.string(),
9530
+ zod.z.number(),
9531
+ zod.z.boolean()
9532
+ ]).optional(),
9533
+ /** Only on a CHANGE of value, not on every re-report. */
9534
+ edge: zod.z.boolean().optional(),
9535
+ /** The new value must hold this long before it counts. */
9536
+ forMs: zod.z.number().int().min(0).max(864e5).optional(),
9537
+ /** Collapse a burst into one occurrence. */
9538
+ debounceMs: zod.z.number().int().min(0).max(6e5).optional()
9539
+ });
9540
+ var DeviceEventLeafSchema = zod.z.object({
9541
+ kind: zod.z.literal("device-event"),
9542
+ /** An `EventCategory` value. */
9543
+ category: zod.z.string().min(1),
9544
+ deviceId: zod.z.number().int().nonnegative().optional()
9545
+ });
9546
+ var ScheduleLeafSchema = zod.z.object({
9547
+ kind: zod.z.literal("schedule"),
9548
+ cron: zod.z.string().min(1).max(120)
9549
+ });
9550
+ var ManualLeafSchema = zod.z.object({ kind: zod.z.literal("manual") });
9551
+ var IntervalLeafSchema = zod.z.object({
9552
+ kind: zod.z.literal("interval"),
9553
+ minutes: zod.z.number().int().min(1).max(1440)
9554
+ });
9555
+ var DetectionLeafSchema = zod.z.object({
9556
+ kind: zod.z.literal("detection"),
9557
+ /** One macro class each, optionally narrowed by a label; a track of any listed subject fires. */
9558
+ subjects: zod.z.array(DetectionSubjectSchema).min(1),
9559
+ deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
9560
+ /** The track must have visited at least one of these zones. */
9561
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9562
+ /** Best-so-far confidence the track must reach. */
9563
+ minScore: zod.z.number().min(0).max(1).optional()
9564
+ });
9565
+ var StationaryLeafSchema = zod.z.object({
9566
+ kind: zod.z.literal("stationary"),
9567
+ deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
9568
+ subjects: zod.z.array(StationarySubjectSchema).min(1).optional(),
9569
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9570
+ /** `appeared`: it became parked. `departed`: the registry retired it. */
9571
+ phase: zod.z.enum(["appeared", "departed"])
9572
+ });
9573
+ /** "is": one device field compared to one value, NOW. */
9574
+ var DeviceIsLeafSchema = zod.z.object({
9575
+ kind: zod.z.literal("device-is"),
8901
9576
  deviceId: zod.z.number().int().nonnegative(),
8902
9577
  cap: zod.z.string().min(1),
8903
9578
  fieldPath: zod.z.string().min(1),
8904
9579
  operator: AutomationConditionOperatorSchema,
9580
+ /**
9581
+ * The value. A number on a quantity field is stored in the field's base unit
9582
+ * (D944); a quantity value is the WRITE form, which the save boundary
9583
+ * replaces with the converted number and `unit`. A leaf that still carries
9584
+ * one is never evaluated (it fails).
9585
+ */
8905
9586
  value: zod.z.union([
8906
9587
  zod.z.string(),
8907
9588
  zod.z.number(),
@@ -8909,24 +9590,26 @@ var AutomationConditionLeafSchema = zod.z.object({
8909
9590
  zod.z.array(zod.z.union([zod.z.string(), zod.z.number()])),
8910
9591
  QuantityValueSchema
8911
9592
  ]),
9593
+ /** The unit the person picked for `value` (display only; `value` is in the base unit). */
8912
9594
  unit: zod.z.string().min(1).optional()
8913
9595
  });
8914
- var AutomationConditionStationaryPresentSchema = zod.z.object({
9596
+ /**
9597
+ * True when the camera currently has a PARKED object matching `subjects` in at
9598
+ * least one of `zones`. Reads the zone-analytics snapshot through the same
9599
+ * mirror as every other state leaf, so a dropped event never decides it (D49).
9600
+ */
9601
+ var StationaryPresentLeafSchema = zod.z.object({
8915
9602
  kind: zod.z.literal("stationary-present"),
8916
9603
  deviceId: zod.z.number().int().nonnegative(),
8917
- /** Class names; a macro class matches every raw class that maps to it. */
8918
- classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8919
- /** The parked object must be in at least one of these zones. */
9604
+ subjects: zod.z.array(StationarySubjectSchema).min(1).optional(),
8920
9605
  zones: zod.z.array(zod.z.string().min(1)).min(1).optional()
8921
9606
  });
8922
9607
  /**
8923
- * The expression leaf, declared as a plain object rather than an intersection
8924
- * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
8925
- * read `kind` off each option, and an intersection hides it. The author-time
8926
- * validation is the SAME function `ExpressionSourceSchema` runs, so the two
8927
- * cannot drift — an expression that one accepts, the other accepts.
9608
+ * The escape hatch: the bounded expression engine over named device bindings.
9609
+ * Declared as a plain object so a discriminated union can read `kind`; the
9610
+ * author-time validation is the SAME function `ExpressionSourceSchema` runs.
8928
9611
  */
8929
- var AutomationConditionExpressionSchema = zod.z.object({
9612
+ var ExpressionLeafSchema = zod.z.object({
8930
9613
  kind: zod.z.literal("expression"),
8931
9614
  expr: zod.z.string().min(1).max(require_composition.MAX_EXPRESSION_SOURCE_LENGTH),
8932
9615
  bindings: zod.z.record(zod.z.string().regex(require_composition.EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
@@ -8938,55 +9621,180 @@ var AutomationConditionExpressionSchema = zod.z.object({
8938
9621
  path: ["expr"]
8939
9622
  });
8940
9623
  });
8941
- var AutomationConditionSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind", [
9624
+ var AutomationWhenSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind", [
8942
9625
  zod.z.object({
8943
9626
  kind: zod.z.literal("all"),
8944
- children: zod.z.array(AutomationConditionSchema)
9627
+ children: zod.z.array(AutomationWhenSchema)
8945
9628
  }),
8946
9629
  zod.z.object({
8947
9630
  kind: zod.z.literal("any"),
8948
- children: zod.z.array(AutomationConditionSchema)
9631
+ children: zod.z.array(AutomationWhenSchema)
8949
9632
  }),
8950
9633
  zod.z.object({
8951
9634
  kind: zod.z.literal("not"),
8952
- child: AutomationConditionSchema
9635
+ child: AutomationWhenSchema
8953
9636
  }),
8954
- AutomationConditionLeafSchema,
8955
- AutomationConditionExpressionSchema,
8956
- AutomationConditionStationaryPresentSchema
9637
+ DeviceBecomesLeafSchema,
9638
+ DeviceEventLeafSchema,
9639
+ ScheduleLeafSchema,
9640
+ ManualLeafSchema,
9641
+ IntervalLeafSchema,
9642
+ DetectionLeafSchema,
9643
+ StationaryLeafSchema,
9644
+ DeviceIsLeafSchema,
9645
+ StationaryPresentLeafSchema,
9646
+ ExpressionLeafSchema
8957
9647
  ]));
9648
+ var EVENT_KINDS = new Set([
9649
+ "device-becomes",
9650
+ "device-event",
9651
+ "schedule",
9652
+ "manual",
9653
+ "interval",
9654
+ "detection",
9655
+ "stationary"
9656
+ ]);
9657
+ function isEventLeaf(node) {
9658
+ return EVENT_KINDS.has(node.kind);
9659
+ }
9660
+ function isLeaf(node) {
9661
+ return node.kind !== "all" && node.kind !== "any" && node.kind !== "not";
9662
+ }
9663
+ /** Depth of a tree, root container = 1. A bare leaf is depth 1. */
9664
+ function whenDepth(node) {
9665
+ if (isLeaf(node)) return 1;
9666
+ if (node.kind === "not") return 1 + whenDepth(node.child);
9667
+ let deepest = 0;
9668
+ for (const child of node.children) deepest = Math.max(deepest, whenDepth(child));
9669
+ return 1 + deepest;
9670
+ }
9671
+ /** Leaves only — containers do not count toward the leaf budget. */
9672
+ function countWhenLeaves(node) {
9673
+ if (isLeaf(node)) return 1;
9674
+ if (node.kind === "not") return countWhenLeaves(node.child);
9675
+ let total = 0;
9676
+ for (const child of node.children) total += countWhenLeaves(child);
9677
+ return total;
9678
+ }
9679
+ /** Every leaf with its path (`when`, `when.0`, `when.0.1`, `when.0` under a `not`). */
9680
+ function forEachLeaf(node, visit, path = "when") {
9681
+ if (isLeaf(node)) {
9682
+ visit(node, path);
9683
+ return;
9684
+ }
9685
+ if (node.kind === "not") {
9686
+ forEachLeaf(node.child, visit, `${path}.0`);
9687
+ return;
9688
+ }
9689
+ node.children.forEach((child, i) => {
9690
+ forEachLeaf(child, visit, `${path}.${i}`);
9691
+ });
9692
+ }
9693
+ function hasEventLeaves(node) {
9694
+ let found = false;
9695
+ forEachLeaf(node, (leaf) => {
9696
+ if (isEventLeaf(leaf)) found = true;
9697
+ });
9698
+ return found;
9699
+ }
9700
+ function hasEmptyContainer(node) {
9701
+ if (isLeaf(node)) return false;
9702
+ if (node.kind === "not") return hasEmptyContainer(node.child);
9703
+ if (node.children.length === 0) return true;
9704
+ return node.children.some(hasEmptyContainer);
9705
+ }
9706
+ var LEAF_WORDS = {
9707
+ "device-becomes": "a device value that becomes",
9708
+ "device-event": "a device event",
9709
+ schedule: "a schedule",
9710
+ manual: "a manual run",
9711
+ interval: "an interval",
9712
+ detection: "a detection",
9713
+ stationary: "a parked object appearing or leaving"
9714
+ };
9715
+ function say(kind) {
9716
+ return LEAF_WORDS[kind] ?? kind;
9717
+ }
8958
9718
  /**
8959
- * What starts a run.
8960
- *
8961
- * D8 compliance, and it is the reason `device-state` is not merely an event
8962
- * subscription: the trigger evaluates against the **state mirror**, which is
8963
- * reconciled, and an event only WAKES the evaluation. A dropped event therefore
8964
- * DELAYS a trigger; it does not lose it. `schedule` uses `croner` — the one
8965
- * already in the repo — because `setInterval(24h)` drifts and "at 23:30" does
8966
- * not.
9719
+ * The cheapest set of event leaves that makes `node` true, with negation pushed
9720
+ * down to the leaves first (De Morgan), so `not(any(A, B))` and `all(not A,
9721
+ * not B)` are the same shape to the check. A negated event is its own error: an
9722
+ * event is an instant, "the event did not occur" is true at every other moment.
8967
9723
  */
8968
- var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
9724
+ function eventNeed(node, path, negated) {
9725
+ if (isLeaf(node)) {
9726
+ if (!isEventLeaf(node)) return { paths: [] };
9727
+ if (negated) return { error: `"not" is applied to ${say(node.kind)} (${path}) — an event is an instant, so "it did not happen" is true at every other moment and can never start a run. Negate a state ("is") instead, or remove the "not"` };
9728
+ return { paths: [{
9729
+ path,
9730
+ kind: node.kind
9731
+ }] };
9732
+ }
9733
+ if (node.kind === "not") return eventNeed(node.child, `${path}.0`, !negated);
9734
+ const conjunctive = node.kind === "all" !== negated;
9735
+ const parts = [];
9736
+ for (let i = 0; i < node.children.length; i += 1) {
9737
+ const child = node.children[i];
9738
+ if (child === void 0) continue;
9739
+ const need = eventNeed(child, `${path}.${i}`, negated);
9740
+ if ("error" in need) return need;
9741
+ parts.push(need);
9742
+ }
9743
+ if (conjunctive) return { paths: parts.flatMap((p) => p.paths) };
9744
+ let best;
9745
+ for (const part of parts) if (best === void 0 || part.paths.length < best.paths.length) best = part;
9746
+ return best ?? { paths: [] };
9747
+ }
9748
+ /**
9749
+ * Author-time bounds and sense check of a `when` tree. Returns `null` when it
9750
+ * is legal, else a message the editor shows verbatim.
9751
+ */
9752
+ function validateWhenTree(when) {
9753
+ if (hasEmptyContainer(when)) return "a group is empty — group existing rows instead of leaving a container to fill";
9754
+ const depth = whenDepth(when);
9755
+ if (depth > 4) return `the rules nest ${depth} levels deep — the limit is 4. "not (any of …)" is the same as "all of (not …)", which is one level shallower`;
9756
+ const leaves = countWhenLeaves(when);
9757
+ if (leaves > 32) return `the rules use ${leaves} rows — the limit is 32`;
9758
+ const need = eventNeed(when, "when", false);
9759
+ if ("error" in need) return need.error;
9760
+ const [first, second] = need.paths;
9761
+ if (first !== void 0 && second !== void 0) return `${say(first.kind)} (${first.path}) and ${say(second.kind)} (${second.path}) would both have to happen at the same instant, and an event happens at one moment — this can never run. Use "any of" to start on either, or turn one of them into a state ("is") that must already hold`;
9762
+ return null;
9763
+ }
9764
+ //#endregion
9765
+ //#region src/automation/legacy-recipe.ts
9766
+ /**
9767
+ * The recipe shape before the single `when` tree: a flat list of triggers
9768
+ * (OR'd, each an instant) and an optional condition tree. Stored recipes —
9769
+ * the `AUTOMATION_RECIPE` literal of every block saved so far, and whatever an
9770
+ * older client still posts — are in this shape, so it stays READABLE here, and
9771
+ * {@link convertLegacyRecipe} turns it into the new one, losslessly and one
9772
+ * way. Nothing writes this shape any more.
9773
+ *
9774
+ * `{ triggers: [t1…tn], conditions?: C }`
9775
+ * → `when = all(any(t1…tn), C)`, or `any(t1…tn)` without `C`;
9776
+ * a single trigger is not wrapped in `any`.
9777
+ * a `condition` leaf → a `device-is` leaf (the "is" verb);
9778
+ * a `device-state` trigger → a `device-becomes` leaf (the "becomes" verb);
9779
+ * `classes: ['dog']` → the macro `animal` with the label `dog`.
9780
+ */
9781
+ var LegacyTriggerSchema = zod.z.discriminatedUnion("kind", [
8969
9782
  zod.z.object({
8970
9783
  kind: zod.z.literal("device-state"),
8971
9784
  deviceId: zod.z.number().int().nonnegative(),
8972
9785
  cap: zod.z.string().min(1),
8973
9786
  fieldPath: zod.z.string().min(1),
8974
- /** Fire when the field takes this value. Omit to fire on any change. */
8975
9787
  becomes: zod.z.union([
8976
9788
  zod.z.string(),
8977
9789
  zod.z.number(),
8978
9790
  zod.z.boolean()
8979
9791
  ]).optional(),
8980
- /** Only on a CHANGE of value, not on every re-report. */
8981
9792
  edge: zod.z.boolean().optional(),
8982
- /** The condition must hold this long before the run starts. */
8983
9793
  forMs: zod.z.number().int().min(0).max(864e5).optional(),
8984
- /** Collapse a burst into one run. */
8985
9794
  debounceMs: zod.z.number().int().min(0).max(6e5).optional()
8986
9795
  }),
8987
9796
  zod.z.object({
8988
9797
  kind: zod.z.literal("device-event"),
8989
- /** An `EventCategory` value. */
8990
9798
  category: zod.z.string().min(1),
8991
9799
  deviceId: zod.z.number().int().nonnegative().optional()
8992
9800
  }),
@@ -8997,25 +9805,16 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
8997
9805
  zod.z.object({ kind: zod.z.literal("manual") }),
8998
9806
  zod.z.object({
8999
9807
  kind: zod.z.literal("detection"),
9000
- /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
9001
- * matches every raw class that maps to it; any other name matches
9002
- * exactly. */
9003
9808
  classes: zod.z.array(zod.z.string().min(1)).min(1),
9004
9809
  deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
9005
- /** The track must have visited at least one of these zones. */
9006
9810
  zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9007
- /** Best-so-far confidence the track must reach. */
9008
9811
  minScore: zod.z.number().min(0).max(1).optional()
9009
9812
  }),
9010
9813
  zod.z.object({
9011
9814
  kind: zod.z.literal("stationary"),
9012
9815
  deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
9013
- /** Class names; a macro class matches like the detection trigger. */
9014
9816
  classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9015
- /** The parked object must be in at least one of these zones. */
9016
9817
  zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9017
- /** `appeared`: it became parked. `departed`: the registry retired it.
9018
- * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
9019
9818
  phase: zod.z.enum(["appeared", "departed"])
9020
9819
  }),
9021
9820
  zod.z.object({
@@ -9023,6 +9822,145 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
9023
9822
  minutes: zod.z.number().int().min(1).max(1440)
9024
9823
  })
9025
9824
  ]);
9825
+ var LegacyConditionLeafSchema = zod.z.object({
9826
+ kind: zod.z.literal("condition"),
9827
+ deviceId: zod.z.number().int().nonnegative(),
9828
+ cap: zod.z.string().min(1),
9829
+ fieldPath: zod.z.string().min(1),
9830
+ operator: AutomationConditionOperatorSchema,
9831
+ value: zod.z.union([
9832
+ zod.z.string(),
9833
+ zod.z.number(),
9834
+ zod.z.boolean(),
9835
+ zod.z.array(zod.z.union([zod.z.string(), zod.z.number()])),
9836
+ QuantityValueSchema
9837
+ ]),
9838
+ unit: zod.z.string().min(1).optional()
9839
+ });
9840
+ var LegacyStationaryPresentSchema = zod.z.object({
9841
+ kind: zod.z.literal("stationary-present"),
9842
+ deviceId: zod.z.number().int().nonnegative(),
9843
+ classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
9844
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional()
9845
+ });
9846
+ var LegacyExpressionSchema = zod.z.object({
9847
+ kind: zod.z.literal("expression"),
9848
+ expr: zod.z.string().min(1).max(require_composition.MAX_EXPRESSION_SOURCE_LENGTH),
9849
+ bindings: zod.z.record(zod.z.string().regex(require_composition.EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
9850
+ }).superRefine((src, ctx) => {
9851
+ const err = require_composition.validateExpressionSource(src);
9852
+ if (err !== null) ctx.addIssue({
9853
+ code: "custom",
9854
+ message: err,
9855
+ path: ["expr"]
9856
+ });
9857
+ });
9858
+ var LegacyConditionSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind", [
9859
+ zod.z.object({
9860
+ kind: zod.z.literal("all"),
9861
+ children: zod.z.array(LegacyConditionSchema)
9862
+ }),
9863
+ zod.z.object({
9864
+ kind: zod.z.literal("any"),
9865
+ children: zod.z.array(LegacyConditionSchema)
9866
+ }),
9867
+ zod.z.object({
9868
+ kind: zod.z.literal("not"),
9869
+ child: LegacyConditionSchema
9870
+ }),
9871
+ LegacyConditionLeafSchema,
9872
+ LegacyExpressionSchema,
9873
+ LegacyStationaryPresentSchema
9874
+ ]));
9875
+ function convertTrigger(trigger) {
9876
+ switch (trigger.kind) {
9877
+ case "device-state": return {
9878
+ ...trigger,
9879
+ kind: "device-becomes"
9880
+ };
9881
+ case "detection": {
9882
+ const { classes, ...rest } = trigger;
9883
+ return {
9884
+ ...rest,
9885
+ subjects: classes.map(subjectFromClassName)
9886
+ };
9887
+ }
9888
+ case "stationary": {
9889
+ const { classes, ...rest } = trigger;
9890
+ return {
9891
+ ...rest,
9892
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
9893
+ };
9894
+ }
9895
+ default: return trigger;
9896
+ }
9897
+ }
9898
+ /** An old condition tree as a `when` subtree: `condition` → "is", classes → subjects. */
9899
+ function convertLegacyCondition(condition) {
9900
+ switch (condition.kind) {
9901
+ case "condition": return {
9902
+ ...condition,
9903
+ kind: "device-is"
9904
+ };
9905
+ case "stationary-present": {
9906
+ const { classes, ...rest } = condition;
9907
+ return {
9908
+ ...rest,
9909
+ ...classes === void 0 ? {} : { subjects: classes.map(subjectFromClassName) }
9910
+ };
9911
+ }
9912
+ case "expression": return condition;
9913
+ case "not": return {
9914
+ kind: "not",
9915
+ child: convertLegacyCondition(condition.child)
9916
+ };
9917
+ case "all":
9918
+ case "any": return {
9919
+ kind: condition.kind,
9920
+ children: condition.children.map(convertLegacyCondition)
9921
+ };
9922
+ }
9923
+ }
9924
+ /** `all(any(t1…tn), C)`, or `any(t1…tn)`; a single trigger is not wrapped in `any`. */
9925
+ function convertLegacyWhen(half) {
9926
+ const leaves = half.triggers.map(convertTrigger);
9927
+ const first = leaves[0];
9928
+ const triggers = leaves.length === 1 && first !== void 0 ? first : {
9929
+ kind: "any",
9930
+ children: leaves
9931
+ };
9932
+ if (half.conditions === void 0) return triggers;
9933
+ return {
9934
+ kind: "all",
9935
+ children: [triggers, convertLegacyCondition(half.conditions)]
9936
+ };
9937
+ }
9938
+ //#endregion
9939
+ //#region src/automation/recipe.ts
9940
+ /**
9941
+ * The automation RECIPE — what a visually-authored automation is, before it
9942
+ * becomes code.
9943
+ *
9944
+ * This is the data half of `docs/design/2026-08-08-automation-scripts-custom-devices.md`
9945
+ * Phase 6. A recipe is not an engine and never evaluates anything: it is a
9946
+ * declarative description that {@link generateAutomationBlock} compiles to the
9947
+ * TypeScript of an ordinary core block. There is therefore no second rule
9948
+ * engine, no second store and no second runtime — the block runner is the
9949
+ * runtime, `core_blocks` is the store (§2.1).
9950
+ *
9951
+ * Two parts:
9952
+ *
9953
+ * - **`when`** — ONE tree (`./when.ts`) of all/any/not over event leaves
9954
+ * (instants) and state leaves (durations). It replaces the flat trigger list
9955
+ * plus the separate condition tree; the firing rule is in that file and in
9956
+ * ADR "one tree".
9957
+ * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
9958
+ * fragment of §3.2.2. See {@link AutomationActionSchema} for the ONE place
9959
+ * this had to diverge from "verbatim", and why.
9960
+ *
9961
+ * A recipe stored in the old shape (`triggers` + `conditions`) still LOADS:
9962
+ * {@link AutomationRecipeSchema} (the parse-anywhere schema) converts it on parse. Nothing writes it.
9963
+ */
9026
9964
  /**
9027
9965
  * One action step.
9028
9966
  *
@@ -9128,61 +10066,49 @@ var AutomationRunModeSchema = zod.z.discriminatedUnion("mode", [
9128
10066
  /** Run traces kept per automation when the recipe does not say. */
9129
10067
  var DEFAULT_TRACE_KEEP = 20;
9130
10068
  var MAX_TRACE_KEEP = 100;
9131
- var AutomationRecipeSchema = zod.z.object({
9132
- triggers: zod.z.array(AutomationTriggerSchema),
9133
- conditions: AutomationConditionSchema.optional(),
10069
+ var CurrentAutomationRecipeSchema = zod.z.object({
10070
+ when: AutomationWhenSchema,
9134
10071
  actions: zod.z.array(AutomationActionSchema),
9135
10072
  runMode: AutomationRunModeSchema.optional(),
9136
10073
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
9137
10074
  keepTraces: zod.z.number().int().min(1).max(100).optional()
9138
10075
  });
9139
- function isLeaf(cond) {
9140
- return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
9141
- }
9142
- /** Depth of a tree, root container = 1. A bare leaf is depth 1. */
9143
- function conditionDepth(cond) {
9144
- if (isLeaf(cond)) return 1;
9145
- if (cond.kind === "not") return 1 + conditionDepth(cond.child);
9146
- const children = cond.children;
9147
- if (children.length === 0) return 1;
9148
- let deepest = 0;
9149
- for (const child of children) {
9150
- const d = conditionDepth(child);
9151
- if (d > deepest) deepest = d;
9152
- }
9153
- return 1 + deepest;
9154
- }
9155
- /** Leaves only — containers do not count toward the leaf budget. */
9156
- function countConditionLeaves(cond) {
9157
- if (isLeaf(cond)) return 1;
9158
- if (cond.kind === "not") return countConditionLeaves(cond.child);
9159
- let total = 0;
9160
- for (const child of cond.children) total += countConditionLeaves(child);
9161
- return total;
9162
- }
9163
- function findEmptyContainer(cond) {
9164
- if (isLeaf(cond)) return false;
9165
- if (cond.kind === "not") return findEmptyContainer(cond.child);
9166
- if (cond.children.length === 0) return true;
9167
- return cond.children.some(findEmptyContainer);
10076
+ /** A recipe as it was stored before the single tree. Read-only: nothing writes this. */
10077
+ var LegacyAutomationRecipeSchema = zod.z.object({
10078
+ triggers: zod.z.array(LegacyTriggerSchema),
10079
+ conditions: LegacyConditionSchema.optional(),
10080
+ actions: zod.z.array(AutomationActionSchema),
10081
+ runMode: AutomationRunModeSchema.optional(),
10082
+ keepTraces: zod.z.number().int().min(1).max(100).optional()
10083
+ });
10084
+ /** The old shape as the new one, lossless. */
10085
+ function convertLegacyRecipe(legacy) {
10086
+ const { triggers, conditions, ...rest } = legacy;
10087
+ return {
10088
+ when: convertLegacyWhen({
10089
+ triggers,
10090
+ conditions
10091
+ }),
10092
+ ...rest
10093
+ };
9168
10094
  }
9169
10095
  /**
9170
- * Author-time bounds check. Returns `null` when the recipe is legal, else a
9171
- * message naming the bound it broke — the editor shows it verbatim, so it has
9172
- * to read like something an operator can act on.
10096
+ * Where recipes are PARSED (the core-blocks cap, the MCP tool, the literal a
10097
+ * saved block carries): the new shape as it is, or the old shape converted to
10098
+ * it. The output type is always the new one.
10099
+ */
10100
+ var AutomationRecipeSchema = zod.z.union([CurrentAutomationRecipeSchema, LegacyAutomationRecipeSchema.transform(convertLegacyRecipe)]);
10101
+ /** Bounds, shared by `when` and the old names the guards still know. */
10102
+ var MAX_CONDITION_DEPTH = 4;
10103
+ var MAX_CONDITION_LEAVES = 32;
10104
+ /**
10105
+ * Author-time check. Returns `null` when the recipe is legal, else a message
10106
+ * naming what is wrong — the editor shows it verbatim, so it has to read like
10107
+ * something an operator can act on.
9173
10108
  */
9174
10109
  function validateRecipeBounds(recipe) {
9175
- if (recipe.triggers.length === 0) return "an automation needs at least one trigger — without one nothing can start a run";
9176
10110
  if (recipe.actions.length === 0) return "an automation needs at least one action — a rule that does nothing is not a rule";
9177
- const cond = recipe.conditions;
9178
- if (cond !== void 0) {
9179
- if (findEmptyContainer(cond)) return "a condition group is empty — group existing rows instead of leaving a container to fill";
9180
- const depth = conditionDepth(cond);
9181
- if (depth > 3) return `conditions nest ${depth} levels deep — the limit is 3. "not (any of …)" is the same as "all of (not …)", which is one level shallower`;
9182
- const leaves = countConditionLeaves(cond);
9183
- if (leaves > 32) return `conditions use ${leaves} leaves — the limit is 32`;
9184
- }
9185
- return null;
10111
+ return validateWhenTree(recipe.when);
9186
10112
  }
9187
10113
  //#endregion
9188
10114
  //#region src/automation/codegen.ts
@@ -9206,9 +10132,10 @@ function stepMetaOf(action) {
9206
10132
  function j(value) {
9207
10133
  return JSON.stringify(value);
9208
10134
  }
9209
- function emitTrigger(trigger, index) {
10135
+ function emitLeaf(trigger, path) {
10136
+ const at = j(path);
9210
10137
  switch (trigger.kind) {
9211
- case "schedule": return ` // trigger ${index}: schedule — armed to the next minute boundary and
10138
+ case "schedule": return ` // event leaf ${path}: schedule — armed to the next minute boundary and
9212
10139
  // RE-ARMED from the wall clock every tick, so it cannot drift the way a
9213
10140
  // setInterval(24h) does. No cron library: a block cannot resolve one.
9214
10141
  {
@@ -9216,7 +10143,7 @@ function emitTrigger(trigger, index) {
9216
10143
  const arm = (): void => {
9217
10144
  timer = setTimeout(() => {
9218
10145
  if (cronMatches(${j(trigger.cron)}, new Date())) {
9219
- void run({ kind: 'schedule', at: Date.now() })
10146
+ void run({ kind: 'schedule', leaf: ${at}, at: Date.now() })
9220
10147
  }
9221
10148
  arm()
9222
10149
  }, nextMinuteBoundary())
@@ -9224,13 +10151,14 @@ function emitTrigger(trigger, index) {
9224
10151
  arm()
9225
10152
  disposers.push(() => { if (timer !== undefined) clearTimeout(timer) })
9226
10153
  }`;
9227
- case "device-event": return ` // trigger ${index}: device event
10154
+ case "device-event": return ` // event leaf ${path}: device event
9228
10155
  disposers.push(
9229
10156
  ctx.eventBus.subscribe(
9230
10157
  { category: ${j(trigger.category)}${trigger.deviceId === void 0 ? "" : `, deviceId: ${j(trigger.deviceId)}`} },
9231
10158
  (event) => {
9232
10159
  void run({
9233
10160
  kind: 'device-event',
10161
+ leaf: ${at},
9234
10162
  at: Date.now(),
9235
10163
  deviceId: event.deviceId,
9236
10164
  category: ${j(trigger.category)},
@@ -9238,9 +10166,9 @@ function emitTrigger(trigger, index) {
9238
10166
  },
9239
10167
  ),
9240
10168
  )`;
9241
- case "device-state": {
10169
+ case "device-becomes": {
9242
10170
  const becomes = trigger.becomes === void 0 ? "undefined" : j(trigger.becomes);
9243
- return ` // trigger ${index}: device state — the mirror is the authority, the
10171
+ return ` // event leaf ${path}: device state — the mirror is the authority, the
9244
10172
  // event only WAKES the evaluation, so a dropped event delays and never loses.
9245
10173
  {
9246
10174
  let previous: unknown = undefined
@@ -9261,7 +10189,8 @@ function emitTrigger(trigger, index) {
9261
10189
  }
9262
10190
  heldSince = undefined
9263
10191
  void run({
9264
- kind: 'device-state',
10192
+ kind: 'device-becomes',
10193
+ leaf: ${at},
9265
10194
  at: Date.now(),
9266
10195
  deviceId: ${j(trigger.deviceId)},
9267
10196
  cap: ${j(trigger.cap)},
@@ -9280,23 +10209,23 @@ function emitTrigger(trigger, index) {
9280
10209
  disposers.push(() => clearInterval(reconcile))
9281
10210
  }`;
9282
10211
  }
9283
- case "manual": return ` // trigger ${index}: manual — fired by automation-control.trigger
10212
+ case "manual": return ` // event leaf ${path}: manual — fired by automation-control.trigger
9284
10213
  // ("Run actions"), which runs the THEN half with an EMPTY vars bag. A manual
9285
10214
  // run bypasses only the cooldown wait, never the single/queued limits.`;
9286
- case "interval": return ` // trigger ${index}: interval — every ${trigger.minutes} min, first run jittered so
10215
+ case "interval": return ` // event leaf ${path}: interval — every ${trigger.minutes} min, first run jittered so
9287
10216
  // blocks restarted together do not all fire on the same second.
9288
10217
  {
9289
10218
  let interval: ReturnType<typeof setInterval> | undefined
9290
10219
  const first = setTimeout(() => {
9291
- void run({ kind: 'interval', at: Date.now() })
9292
- interval = setInterval(() => { void run({ kind: 'interval', at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
10220
+ void run({ kind: 'interval', leaf: ${at}, at: Date.now() })
10221
+ interval = setInterval(() => { void run({ kind: 'interval', leaf: ${at}, at: Date.now() }) }, ${j(trigger.minutes * 6e4)})
9293
10222
  }, Math.floor(Math.random() * Math.min(${j(trigger.minutes * 6e4)}, 30_000)) + 1)
9294
10223
  disposers.push(() => {
9295
10224
  clearTimeout(first)
9296
10225
  if (interval !== undefined) clearInterval(interval)
9297
10226
  })
9298
10227
  }`;
9299
- case "detection": return ` // trigger ${index}: detection — ONE fire per (device, track); a rejected event is
10228
+ case "detection": return ` // event leaf ${path}: detection — ONE fire per (device, track); a rejected event is
9300
10229
  // traced with its reason and never fires.
9301
10230
  {
9302
10231
  const filter = new DetectionTriggerFilter(${j(trigger)})
@@ -9313,7 +10242,7 @@ function emitTrigger(trigger, index) {
9313
10242
  })
9314
10243
  }
9315
10244
  void run(
9316
- { kind: 'detection', at: Date.now(), ...decision.vars },
10245
+ { kind: 'detection', leaf: ${at}, at: Date.now(), ...decision.vars },
9317
10246
  decision.reason,
9318
10247
  decision.kind === 'fire',
9319
10248
  )
@@ -9321,9 +10250,9 @@ function emitTrigger(trigger, index) {
9321
10250
  ),
9322
10251
  )
9323
10252
  }`;
9324
- case "stationary": return ` // trigger ${index}: stationary — a parked object appeared / departed. Stateless:
10253
+ case "stationary": return ` // event leaf ${path}: stationary — a parked object appeared / departed. Stateless:
9325
10254
  // the registry emits one event per transition. A dropped event only misses a fire;
9326
- // the stationary-present CONDITION reads the registry, not this event.
10255
+ // the stationary-present STATE leaf reads the registry, not this event.
9327
10256
  {
9328
10257
  const trigger = ${j(trigger)} as const
9329
10258
  disposers.push(
@@ -9339,7 +10268,7 @@ function emitTrigger(trigger, index) {
9339
10268
  })
9340
10269
  }
9341
10270
  void run(
9342
- { kind: 'stationary', at: Date.now(), ...decision.vars },
10271
+ { kind: 'stationary', leaf: ${at}, at: Date.now(), ...decision.vars },
9343
10272
  decision.reason,
9344
10273
  decision.kind === 'fire',
9345
10274
  )
@@ -9361,11 +10290,11 @@ ${body}
9361
10290
  case "cap":
9362
10291
  if (Array.isArray(action.deviceId)) return step(` for (const deviceId of ${j(action.deviceId)}) {
9363
10292
  scope.setDeviceId(deviceId)
9364
- await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
10293
+ await actuate(scope, deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)
9365
10294
  }`);
9366
10295
  return step(` const deviceId = ${typeof action.deviceId === "number" ? j(action.deviceId) : `resolveTarget(vars, ${j(action.deviceId.$var)})`}
9367
10296
  scope.setDeviceId(deviceId)
9368
- await actuate(deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
10297
+ await actuate(scope, deviceId, ${j(action.cap)}, ${j(action.method)}, ${j(action.args ?? {})}, vars)`);
9369
10298
  case "action": return step(` // an Action run, awaited: the step takes THAT run's outcome
9370
10299
  const result = await ctx.api.actions.run.mutate({
9371
10300
  actionId: ${j(action.actionId)},
@@ -9395,6 +10324,13 @@ function generateAutomationBlock(input) {
9395
10324
  const { name, recipe } = input;
9396
10325
  const runMode = recipe.runMode ?? { mode: "single" };
9397
10326
  const stepMeta = recipe.actions.map(stepMetaOf);
10327
+ const eventLeaves = [];
10328
+ forEachLeaf(recipe.when, (leaf, path) => {
10329
+ if (isEventLeaf(leaf)) eventLeaves.push({
10330
+ leaf,
10331
+ path
10332
+ });
10333
+ });
9398
10334
  return `import type {
9399
10335
  AddonContext,
9400
10336
  AutomationConditionTraceNode,
@@ -9402,6 +10338,7 @@ function generateAutomationBlock(input) {
9402
10338
  AutomationRunVerdict,
9403
10339
  AutomationStepTrace,
9404
10340
  RunnableStep,
10341
+ StepScope,
9405
10342
  } from '@camstack/types'
9406
10343
  import {
9407
10344
  ConditionMirror,
@@ -9409,9 +10346,9 @@ import {
9409
10346
  EventCategory,
9410
10347
  RunGate,
9411
10348
  buildTriggerTrace,
9412
- collectMirrorSources,
10349
+ collectWhenMirrorSources,
9413
10350
  coreBlockIdFromAddonId,
9414
- evaluateConditionTree,
10351
+ evaluateWhenTree,
9415
10352
  evaluateStationaryTrigger,
9416
10353
  isSkippedTrace,
9417
10354
  mergeSkipTraces,
@@ -9439,7 +10376,11 @@ const RUN_MODE = ${j(runMode)} as const
9439
10376
  /** Run traces kept (a ring, held in the hub's runs ledger). */
9440
10377
  const KEEP_TRACES = ${recipe.keepTraces ?? 20}
9441
10378
 
9442
- const CONDITIONS = ${recipe.conditions === void 0 ? "undefined" : "AUTOMATION_RECIPE.conditions"}
10379
+ /** The ONE tree: event leaves (instants) and state leaves (durations). */
10380
+ const WHEN = AUTOMATION_RECIPE.when
10381
+
10382
+ /** No event leaf: the automation fires when the tree goes false → true, never because it was already true. */
10383
+ const STATE_ONLY = ${!hasEventLeaves(recipe.when)}
9443
10384
 
9444
10385
  /** Skips that repeat are folded into one trace; it goes to the hub after this. */
9445
10386
  const SKIP_FLUSH_MS = 5_000
@@ -9498,7 +10439,8 @@ function matchField(field: string, value: number): boolean {
9498
10439
  export default function block(ctx: AddonContext) {
9499
10440
  const disposers: Array<() => void> = []
9500
10441
  const gate = new RunGate(RUN_MODE)
9501
- const mirror = new ConditionMirror(collectMirrorSources(CONDITIONS))
10442
+ const MIRROR_SOURCES = collectWhenMirrorSources(WHEN)
10443
+ const mirror = new ConditionMirror(MIRROR_SOURCES)
9502
10444
  const BLOCK_ID = coreBlockIdFromAddonId(ctx.id)
9503
10445
  /** The cooldown stamp: memory is the authority at fire time (D49); the hub ledger is read once at start. */
9504
10446
  let lastRunStartedAt: number | null = null
@@ -9664,6 +10606,7 @@ export default function block(ctx: AddonContext) {
9664
10606
  }
9665
10607
 
9666
10608
  async function actuate(
10609
+ scope: StepScope,
9667
10610
  deviceId: number,
9668
10611
  cap: string,
9669
10612
  method: string,
@@ -9673,6 +10616,7 @@ export default function block(ctx: AddonContext) {
9673
10616
  const substituted = substitute(args, vars)
9674
10617
  const resolved: Record<string, unknown> =
9675
10618
  substituted !== null && typeof substituted === 'object' ? { ...substituted } : {}
10619
+ scope.setArgs(resolved)
9676
10620
  // Forked-addon ctx.api namespaces are tRPC Proxies over a noop function
9677
10621
  // (typeof === 'function'), not plain objects — reject only null/undefined.
9678
10622
  const client: unknown = Reflect.get(ctx.api, capToRouterKey(cap))
@@ -9761,10 +10705,13 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
9761
10705
  let nodes: AutomationConditionTraceNode[] = []
9762
10706
  let steps: AutomationStepTrace[] = []
9763
10707
  try {
9764
- const evaluation = evaluateConditionTree(CONDITIONS, mirror, Date.now())
10708
+ const firedLeaf = trigger['leaf']
10709
+ const evaluation = evaluateWhenTree(WHEN, mirror, Date.now(), {
10710
+ ...(typeof firedLeaf === 'string' ? { firedPath: firedLeaf } : {}),
10711
+ })
9765
10712
  nodes = [...evaluation.nodes]
9766
10713
  for (const node of nodes) {
9767
- if (node.kind === 'condition' && node.observed === null && node.deviceId !== undefined) {
10714
+ if (node.kind === 'device-is' && node.observed === null && node.deviceId !== undefined) {
9768
10715
  ctx.logger.warn('condition leaf value unknown — treated as false', {
9769
10716
  tags: { deviceId: node.deviceId },
9770
10717
  path: node.path,
@@ -9775,7 +10722,7 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
9775
10722
  }
9776
10723
  if (!evaluation.pass) {
9777
10724
  outcome = 'skipped-conditions'
9778
- ctx.logger.debug('automation did not run — conditions were false', { trigger, ...deviceTags(trigger) })
10725
+ ctx.logger.debug('automation did not run — the rule tree was false at that instant', { trigger, ...deviceTags(trigger) })
9779
10726
  return
9780
10727
  }
9781
10728
  const letThrough = stampUnknown && RUN_MODE.mode === 'cooldown'
@@ -9874,9 +10821,30 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
9874
10821
  const ready: Promise<void> = (async () => {
9875
10822
  await loadState()
9876
10823
  await refreshMirror()
10824
+ stateWatching = true
10825
+ evaluateStateTree()
9877
10826
  })()
9878
10827
 
9879
- if (CONDITIONS !== undefined) {
10828
+ /**
10829
+ * Rule 2 — a tree of states only. It fires when the tree goes false → true.
10830
+ * The FIRST evaluation only seeds \`treeWasTrue\` (a tree that is already true
10831
+ * when the hub or runner starts must not fire), and an evaluation that read an
10832
+ * unknown value neither seeds nor moves it: unknown is not false.
10833
+ */
10834
+ let treeWasTrue: boolean | undefined
10835
+ let stateWatching = false
10836
+ function evaluateStateTree(): void {
10837
+ if (!STATE_ONLY || !stateWatching || stopped) return
10838
+ const evaluation = evaluateWhenTree(WHEN, mirror, Date.now())
10839
+ if (evaluation.unknown) return
10840
+ const was = treeWasTrue
10841
+ treeWasTrue = evaluation.pass
10842
+ if (was === false && evaluation.pass) {
10843
+ void run({ kind: 'state', leaf: 'when', at: Date.now() }, 'the rules became true', true)
10844
+ }
10845
+ }
10846
+
10847
+ if (MIRROR_SOURCES.length > 0) {
9880
10848
  disposers.push(
9881
10849
  ctx.eventBus.subscribe({ category: EventCategory.DeviceStateChanged }, (event) => {
9882
10850
  const data = event.data
@@ -9884,14 +10852,17 @@ ${recipe.actions.map((a, i) => emitAction(a, i)).join("\n")}
9884
10852
  const capName = data['capName']
9885
10853
  if (typeof deviceId === 'number' && typeof capName === 'string') {
9886
10854
  mirror.apply(deviceId, capName, data['slice'])
10855
+ evaluateStateTree()
9887
10856
  }
9888
10857
  }),
9889
10858
  )
9890
- const mirrorReconcile = setInterval(() => { void refreshMirror() }, RECONCILE_MS)
10859
+ const mirrorReconcile = setInterval(() => {
10860
+ void refreshMirror().then(evaluateStateTree)
10861
+ }, RECONCILE_MS)
9891
10862
  disposers.push(() => clearInterval(mirrorReconcile))
9892
10863
  }
9893
10864
 
9894
- ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
10865
+ ${eventLeaves.map(({ leaf, path }) => emitLeaf(leaf, path)).join("\n\n")}
9895
10866
 
9896
10867
  return () => {
9897
10868
  stopped = true
@@ -9907,494 +10878,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
9907
10878
  `;
9908
10879
  }
9909
10880
  //#endregion
9910
- //#region src/catalogs/coco-classmap.ts
9911
- var COCO_80_LABELS = [
9912
- {
9913
- id: "person",
9914
- name: "Person"
9915
- },
9916
- {
9917
- id: "bicycle",
9918
- name: "Bicycle"
9919
- },
9920
- {
9921
- id: "car",
9922
- name: "Car"
9923
- },
9924
- {
9925
- id: "motorcycle",
9926
- name: "Motorcycle"
9927
- },
9928
- {
9929
- id: "airplane",
9930
- name: "Airplane"
9931
- },
9932
- {
9933
- id: "bus",
9934
- name: "Bus"
9935
- },
9936
- {
9937
- id: "train",
9938
- name: "Train"
9939
- },
9940
- {
9941
- id: "truck",
9942
- name: "Truck"
9943
- },
9944
- {
9945
- id: "boat",
9946
- name: "Boat"
9947
- },
9948
- {
9949
- id: "traffic light",
9950
- name: "Traffic Light"
9951
- },
9952
- {
9953
- id: "fire hydrant",
9954
- name: "Fire Hydrant"
9955
- },
9956
- {
9957
- id: "stop sign",
9958
- name: "Stop Sign"
9959
- },
9960
- {
9961
- id: "parking meter",
9962
- name: "Parking Meter"
9963
- },
9964
- {
9965
- id: "bench",
9966
- name: "Bench"
9967
- },
9968
- {
9969
- id: "bird",
9970
- name: "Bird"
9971
- },
9972
- {
9973
- id: "cat",
9974
- name: "Cat"
9975
- },
9976
- {
9977
- id: "dog",
9978
- name: "Dog"
9979
- },
9980
- {
9981
- id: "horse",
9982
- name: "Horse"
9983
- },
9984
- {
9985
- id: "sheep",
9986
- name: "Sheep"
9987
- },
9988
- {
9989
- id: "cow",
9990
- name: "Cow"
9991
- },
9992
- {
9993
- id: "elephant",
9994
- name: "Elephant"
9995
- },
9996
- {
9997
- id: "bear",
9998
- name: "Bear"
9999
- },
10000
- {
10001
- id: "zebra",
10002
- name: "Zebra"
10003
- },
10004
- {
10005
- id: "giraffe",
10006
- name: "Giraffe"
10007
- },
10008
- {
10009
- id: "backpack",
10010
- name: "Backpack"
10011
- },
10012
- {
10013
- id: "umbrella",
10014
- name: "Umbrella"
10015
- },
10016
- {
10017
- id: "handbag",
10018
- name: "Handbag"
10019
- },
10020
- {
10021
- id: "tie",
10022
- name: "Tie"
10023
- },
10024
- {
10025
- id: "suitcase",
10026
- name: "Suitcase"
10027
- },
10028
- {
10029
- id: "frisbee",
10030
- name: "Frisbee"
10031
- },
10032
- {
10033
- id: "skis",
10034
- name: "Skis"
10035
- },
10036
- {
10037
- id: "snowboard",
10038
- name: "Snowboard"
10039
- },
10040
- {
10041
- id: "sports ball",
10042
- name: "Sports Ball"
10043
- },
10044
- {
10045
- id: "kite",
10046
- name: "Kite"
10047
- },
10048
- {
10049
- id: "baseball bat",
10050
- name: "Baseball Bat"
10051
- },
10052
- {
10053
- id: "baseball glove",
10054
- name: "Baseball Glove"
10055
- },
10056
- {
10057
- id: "skateboard",
10058
- name: "Skateboard"
10059
- },
10060
- {
10061
- id: "surfboard",
10062
- name: "Surfboard"
10063
- },
10064
- {
10065
- id: "tennis racket",
10066
- name: "Tennis Racket"
10067
- },
10068
- {
10069
- id: "bottle",
10070
- name: "Bottle"
10071
- },
10072
- {
10073
- id: "wine glass",
10074
- name: "Wine Glass"
10075
- },
10076
- {
10077
- id: "cup",
10078
- name: "Cup"
10079
- },
10080
- {
10081
- id: "fork",
10082
- name: "Fork"
10083
- },
10084
- {
10085
- id: "knife",
10086
- name: "Knife"
10087
- },
10088
- {
10089
- id: "spoon",
10090
- name: "Spoon"
10091
- },
10092
- {
10093
- id: "bowl",
10094
- name: "Bowl"
10095
- },
10096
- {
10097
- id: "banana",
10098
- name: "Banana"
10099
- },
10100
- {
10101
- id: "apple",
10102
- name: "Apple"
10103
- },
10104
- {
10105
- id: "sandwich",
10106
- name: "Sandwich"
10107
- },
10108
- {
10109
- id: "orange",
10110
- name: "Orange"
10111
- },
10112
- {
10113
- id: "broccoli",
10114
- name: "Broccoli"
10115
- },
10116
- {
10117
- id: "carrot",
10118
- name: "Carrot"
10119
- },
10120
- {
10121
- id: "hot dog",
10122
- name: "Hot Dog"
10123
- },
10124
- {
10125
- id: "pizza",
10126
- name: "Pizza"
10127
- },
10128
- {
10129
- id: "donut",
10130
- name: "Donut"
10131
- },
10132
- {
10133
- id: "cake",
10134
- name: "Cake"
10135
- },
10136
- {
10137
- id: "chair",
10138
- name: "Chair"
10139
- },
10140
- {
10141
- id: "couch",
10142
- name: "Couch"
10143
- },
10144
- {
10145
- id: "potted plant",
10146
- name: "Potted Plant"
10147
- },
10148
- {
10149
- id: "bed",
10150
- name: "Bed"
10151
- },
10152
- {
10153
- id: "dining table",
10154
- name: "Dining Table"
10155
- },
10156
- {
10157
- id: "toilet",
10158
- name: "Toilet"
10159
- },
10160
- {
10161
- id: "tv",
10162
- name: "TV"
10163
- },
10164
- {
10165
- id: "laptop",
10166
- name: "Laptop"
10167
- },
10168
- {
10169
- id: "mouse",
10170
- name: "Mouse"
10171
- },
10172
- {
10173
- id: "remote",
10174
- name: "Remote"
10175
- },
10176
- {
10177
- id: "keyboard",
10178
- name: "Keyboard"
10179
- },
10180
- {
10181
- id: "cell phone",
10182
- name: "Cell Phone"
10183
- },
10184
- {
10185
- id: "microwave",
10186
- name: "Microwave"
10187
- },
10188
- {
10189
- id: "oven",
10190
- name: "Oven"
10191
- },
10192
- {
10193
- id: "toaster",
10194
- name: "Toaster"
10195
- },
10196
- {
10197
- id: "sink",
10198
- name: "Sink"
10199
- },
10200
- {
10201
- id: "refrigerator",
10202
- name: "Refrigerator"
10203
- },
10204
- {
10205
- id: "book",
10206
- name: "Book"
10207
- },
10208
- {
10209
- id: "clock",
10210
- name: "Clock"
10211
- },
10212
- {
10213
- id: "vase",
10214
- name: "Vase"
10215
- },
10216
- {
10217
- id: "scissors",
10218
- name: "Scissors"
10219
- },
10220
- {
10221
- id: "teddy bear",
10222
- name: "Teddy Bear"
10223
- },
10224
- {
10225
- id: "hair drier",
10226
- name: "Hair Drier"
10227
- },
10228
- {
10229
- id: "toothbrush",
10230
- name: "Toothbrush"
10231
- }
10232
- ];
10233
- var MACRO_LABELS = [
10234
- {
10235
- id: "person",
10236
- name: "Person"
10237
- },
10238
- {
10239
- id: "vehicle",
10240
- name: "Vehicle"
10241
- },
10242
- {
10243
- id: "animal",
10244
- name: "Animal"
10245
- },
10246
- {
10247
- id: "package",
10248
- name: "Package"
10249
- }
10250
- ];
10251
- /**
10252
- * The class names a per-class COUNT can ever be keyed on — the macro ids, and
10253
- * only those.
10254
- *
10255
- * `preserveOriginal: false` below is the whole reason this set exists: the
10256
- * executor maps each raw COCO label onto its macro and DROPS everything else,
10257
- * so a tracked detection's `className` is always one of {@link MACRO_LABELS}.
10258
- * Any surface that indexes counts by class name — the ZoneAnalytics occupancy
10259
- * snapshot's `byClass`, and therefore every occupancy rule — can match nothing
10260
- * else. Derived from the catalog, never hand-listed, so a new macro joins it by
10261
- * being declared once.
10262
- *
10263
- * Cost of not having it, 2026-08-12: the notification rule editor offered the
10264
- * full taxonomy for an occupancy class (occupancy is the one condition that
10265
- * does not go through the engine's macro-expanding `expandClassSelector`), two
10266
- * live rules were authored on `car`, and both counters read 0 forever.
10267
- */
10268
- var DETECTION_MACRO_CLASSES = new Set(MACRO_LABELS.map((l) => l.id));
10269
- /** True when `className` is a macro the detector can actually emit (and hence a
10270
- * class a count can be keyed on) — see {@link DETECTION_MACRO_CLASSES}. */
10271
- function isDetectionMacroClass(className) {
10272
- return DETECTION_MACRO_CLASSES.has(className.trim().toLowerCase());
10273
- }
10274
- var COCO_TO_MACRO = {
10275
- mapping: {
10276
- person: "person",
10277
- bicycle: "vehicle",
10278
- car: "vehicle",
10279
- motorcycle: "vehicle",
10280
- airplane: "vehicle",
10281
- bus: "vehicle",
10282
- train: "vehicle",
10283
- truck: "vehicle",
10284
- boat: "vehicle",
10285
- bird: "animal",
10286
- cat: "animal",
10287
- dog: "animal",
10288
- horse: "animal",
10289
- sheep: "animal",
10290
- cow: "animal",
10291
- elephant: "animal",
10292
- bear: "animal",
10293
- zebra: "animal",
10294
- giraffe: "animal",
10295
- suitcase: "package",
10296
- backpack: "package",
10297
- handbag: "package"
10298
- },
10299
- preserveOriginal: false
10300
- };
10301
- var MACRO_NAMES = new Set(Object.values(COCO_TO_MACRO.mapping));
10302
- function macroOf(className) {
10303
- return COCO_TO_MACRO.mapping[className] ?? className;
10304
- }
10305
- /** `animal` matches a dog; `dog` matches only a dog (a cat is also an animal). */
10306
- function detectionClassMatches(wanted, actual) {
10307
- const w = wanted.toLowerCase();
10308
- const a = actual.toLowerCase();
10309
- if (w === a) return true;
10310
- return MACRO_NAMES.has(w) && macroOf(a) === w;
10311
- }
10312
- function isPhase$1(value) {
10313
- return value === "start" || value === "update" || value === "end";
10314
- }
10315
- var DetectionTriggerFilter = class {
10316
- trigger;
10317
- /** `${deviceId}|${trackId}` → fired at. Insertion-ordered, so the oldest is first. */
10318
- fired = /* @__PURE__ */ new Map();
10319
- constructor(trigger) {
10320
- this.trigger = trigger;
10321
- }
10322
- get firedCount() {
10323
- return this.fired.size;
10324
- }
10325
- evaluate(event, now) {
10326
- const { deviceId, trackId, phase } = event;
10327
- if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase$1(phase)) return {
10328
- kind: "ignore",
10329
- why: "malformed track-lifecycle event"
10330
- };
10331
- if (!this.trigger.deviceIds.includes(deviceId)) return {
10332
- kind: "ignore",
10333
- why: `device ${deviceId} is not one of the trigger's devices`
10334
- };
10335
- const key = `${deviceId}|${trackId}`;
10336
- if (phase === "end") {
10337
- this.fired.delete(key);
10338
- return {
10339
- kind: "ignore",
10340
- why: "track ended"
10341
- };
10342
- }
10343
- this.expire(now);
10344
- const className = typeof event.bestClassName === "string" ? event.bestClassName : "";
10345
- const score = typeof event.bestConfidence === "number" ? event.bestConfidence : 0;
10346
- const vars = {
10347
- deviceId,
10348
- className,
10349
- trackId,
10350
- score
10351
- };
10352
- if (this.fired.has(key)) return {
10353
- kind: "reject",
10354
- reason: `already fired for track ${trackId}`,
10355
- vars
10356
- };
10357
- if (!this.trigger.classes.some((c) => detectionClassMatches(c, className))) return {
10358
- kind: "reject",
10359
- reason: `class ${className || "unknown"} not in [${this.trigger.classes.join(", ")}]`,
10360
- vars
10361
- };
10362
- if (this.trigger.minScore !== void 0 && score < this.trigger.minScore) return {
10363
- kind: "reject",
10364
- reason: `score ${score} < ${this.trigger.minScore}`,
10365
- vars
10366
- };
10367
- if (this.trigger.zones !== void 0) {
10368
- const visited = Array.isArray(event.zonesVisited) ? event.zonesVisited.filter((z) => typeof z === "string") : [];
10369
- if (!this.trigger.zones.some((z) => visited.includes(z))) return {
10370
- kind: "reject",
10371
- reason: `zone mismatch: visited [${visited.join(", ")}], wanted [${this.trigger.zones.join(", ")}]`,
10372
- vars
10373
- };
10374
- }
10375
- this.remember(key, now);
10376
- return {
10377
- kind: "fire",
10378
- reason: `${className} ${score} on device ${deviceId}`,
10379
- vars
10380
- };
10381
- }
10382
- remember(key, now) {
10383
- this.fired.set(key, now);
10384
- while (this.fired.size > 512) {
10385
- const oldest = this.fired.keys().next();
10386
- if (oldest.done === true) break;
10387
- this.fired.delete(oldest.value);
10388
- }
10389
- }
10390
- expire(now) {
10391
- for (const [key, at] of this.fired) {
10392
- if (now - at < 9e5) break;
10393
- this.fired.delete(key);
10394
- }
10395
- }
10396
- };
10397
- //#endregion
10398
10881
  //#region src/automation/condition-eval.ts
10399
10882
  /**
10400
10883
  * Evaluating an automation's condition tree against the ConditionMirror.
@@ -10421,7 +10904,7 @@ function collectInto(cond, out) {
10421
10904
  case "not":
10422
10905
  collectInto(cond.child, out);
10423
10906
  return;
10424
- case "condition":
10907
+ case "device-is":
10425
10908
  out.push({
10426
10909
  kind: "device",
10427
10910
  deviceId: cond.deviceId,
@@ -10442,10 +10925,11 @@ function collectInto(cond, out) {
10442
10925
  cap: binding.cap
10443
10926
  });
10444
10927
  return;
10928
+ default: return;
10445
10929
  }
10446
10930
  }
10447
- /** Every (device, cap) a tree reads, deduplicated. */
10448
- function collectMirrorSources(cond) {
10931
+ /** Every (device, cap) a `when` tree's STATE leaves read, deduplicated. */
10932
+ function collectWhenMirrorSources(cond) {
10449
10933
  if (cond === void 0) return [];
10450
10934
  const all = [];
10451
10935
  collectInto(cond, all);
@@ -10457,6 +10941,10 @@ function collectMirrorSources(cond) {
10457
10941
  return true;
10458
10942
  });
10459
10943
  }
10944
+ /** Legacy name, for blocks generated before the single tree: an old condition tree. */
10945
+ function collectMirrorSources(cond) {
10946
+ return collectWhenMirrorSources(cond === void 0 ? void 0 : convertLegacyCondition(cond));
10947
+ }
10460
10948
  function sliceKey(deviceId, cap) {
10461
10949
  return `${deviceId}|${cap}`;
10462
10950
  }
@@ -10610,7 +11098,8 @@ function zonesOfParked(zones, parkedId) {
10610
11098
  function judgeStationaryPresent(cond, reader) {
10611
11099
  if (reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "frame") === void 0) return {
10612
11100
  pass: false,
10613
- why: `device ${cond.deviceId} occupancy snapshot unknown`
11101
+ why: `device ${cond.deviceId} occupancy snapshot unknown`,
11102
+ unknown: true
10614
11103
  };
10615
11104
  const parked = parkedObjectsOf(reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "stationaryObjects"));
10616
11105
  if (parked.length === 0) return {
@@ -10619,7 +11108,7 @@ function judgeStationaryPresent(cond, reader) {
10619
11108
  };
10620
11109
  const zones = reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "zones");
10621
11110
  for (const obj of parked) {
10622
- if (cond.classes !== void 0 && !cond.classes.some((c) => detectionClassMatches(c, obj.className))) continue;
11111
+ if (cond.subjects !== void 0 && !anySubjectMatches(cond.subjects, { className: obj.className }).matches) continue;
10623
11112
  if (cond.zones !== void 0) {
10624
11113
  const inZones = zonesOfParked(zones, obj.id);
10625
11114
  if (!cond.zones.some((z) => inZones.includes(z))) continue;
@@ -10634,9 +11123,20 @@ function judgeStationaryPresent(cond, reader) {
10634
11123
  why: `${parked.length} parked on device ${cond.deviceId}, none matching the class/zone filter`
10635
11124
  };
10636
11125
  }
10637
- function evalNode(cond, path, reader, now, out) {
11126
+ function evalEventLeaf(cond, path, state, out) {
11127
+ const occurred = state.firedPath === path;
11128
+ out.push({
11129
+ path,
11130
+ kind: cond.kind,
11131
+ outcome: occurred ? "pass" : "fail",
11132
+ reason: occurred ? `${cond.kind} occurred → pass` : `${cond.kind} is an instant and did not occur now → fail`
11133
+ });
11134
+ return occurred;
11135
+ }
11136
+ function evalNode(cond, path, reader, now, out, state) {
11137
+ if (isEventLeaf(cond)) return evalEventLeaf(cond, path, state, out);
10638
11138
  switch (cond.kind) {
10639
- case "condition": {
11139
+ case "device-is": {
10640
11140
  const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
10641
11141
  const base = {
10642
11142
  path,
@@ -10657,6 +11157,7 @@ function evalNode(cond, path, reader, now, out) {
10657
11157
  return false;
10658
11158
  }
10659
11159
  if (observed === void 0) {
11160
+ state.unknown = true;
10660
11161
  out.push({
10661
11162
  ...base,
10662
11163
  outcome: "fail",
@@ -10674,6 +11175,7 @@ function evalNode(cond, path, reader, now, out) {
10674
11175
  }
10675
11176
  case "stationary-present": {
10676
11177
  const verdict = judgeStationaryPresent(cond, reader);
11178
+ if (verdict.unknown === true) state.unknown = true;
10677
11179
  out.push({
10678
11180
  path,
10679
11181
  kind: cond.kind,
@@ -10699,6 +11201,7 @@ function evalNode(cond, path, reader, now, out) {
10699
11201
  outcome: "fail",
10700
11202
  reason: `${alias} value unknown → fail`
10701
11203
  });
11204
+ state.unknown = true;
10702
11205
  return false;
10703
11206
  }
10704
11207
  values[alias] = require_composition.toExpressionValue(raw) ?? null;
@@ -10730,7 +11233,7 @@ function evalNode(cond, path, reader, now, out) {
10730
11233
  outcome: "not-evaluated",
10731
11234
  reason: ""
10732
11235
  });
10733
- const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
11236
+ const inner = evalNode(cond.child, `${path}.0`, reader, now, out, state);
10734
11237
  const ok = !inner;
10735
11238
  out.splice(at, 1, {
10736
11239
  path,
@@ -10758,7 +11261,7 @@ function evalNode(cond, path, reader, now, out) {
10758
11261
  markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
10759
11262
  continue;
10760
11263
  }
10761
- const ok = evalNode(child, `${path}.${i}`, reader, now, out);
11264
+ const ok = evalNode(child, `${path}.${i}`, reader, now, out, state);
10762
11265
  if (isAll ? !ok : ok) decided = i;
10763
11266
  }
10764
11267
  const ok = isAll ? decided === null : decided !== null;
@@ -10773,20 +11276,167 @@ function evalNode(cond, path, reader, now, out) {
10773
11276
  }
10774
11277
  }
10775
11278
  }
10776
- /** No condition tree is a pass with no nodes. */
11279
+ /** Evaluate a `when` tree: the fired event leaf is true, every other event leaf false. */
11280
+ function evaluateWhenTree(when, reader, now, options = {}) {
11281
+ const nodes = [];
11282
+ const state = {
11283
+ firedPath: options.firedPath,
11284
+ unknown: false
11285
+ };
11286
+ return {
11287
+ pass: evalNode(when, options.rootPath ?? "when", reader, now, nodes, state),
11288
+ unknown: state.unknown,
11289
+ nodes
11290
+ };
11291
+ }
11292
+ /** Legacy name, for blocks generated before the single tree. No tree is a pass with no nodes. */
10777
11293
  function evaluateConditionTree(cond, reader, now) {
10778
11294
  if (cond === void 0) return {
10779
11295
  pass: true,
11296
+ unknown: false,
10780
11297
  nodes: []
10781
11298
  };
10782
- const nodes = [];
10783
- return {
10784
- pass: evalNode(cond, "conditions", reader, now, nodes),
10785
- nodes
10786
- };
11299
+ return evaluateWhenTree(convertLegacyCondition(cond), reader, now, { rootPath: "conditions" });
11300
+ }
11301
+ //#endregion
11302
+ //#region src/automation/detection-trigger.ts
11303
+ /**
11304
+ * The `detection` trigger's per-event decision: does this track-lifecycle
11305
+ * event start a run, and if not, WHY not.
11306
+ *
11307
+ * Source: `EventCategory.PipelineAnalyticsTrackLifecycle` — payload
11308
+ * {@link PipelineAnalyticsTrackLifecyclePayload} (`deviceId`, `trackId`,
11309
+ * `phase` start|update|end, `bestClassName`, `bestConfidence`,
11310
+ * `zonesVisited`). The event carries the device in its DATA, not on its
11311
+ * `source`, so the filter reads `data.deviceId`.
11312
+ *
11313
+ * ONE fire per `(deviceId, trackId)`: a track fires the first time it satisfies
11314
+ * the subject, score and zone conditions (which may be on an `update`, once its
11315
+ * best confidence has risen, or once its face or plate has been resolved — D683
11316
+ * decides identity late, so a leaf with a label fires on the update that
11317
+ * carries it, never on the first detection), and never again. The fired set is bounded and
11318
+ * forgets a track on its `end` or after {@link FIRED_TTL_MS}, so a track whose
11319
+ * `end` was dropped (events are telemetry, D8) cannot pin memory.
11320
+ */
11321
+ /** The subjects a trigger asks for, whichever shape it has. */
11322
+ function subjectsOf(trigger) {
11323
+ return "subjects" in trigger ? trigger.subjects : trigger.classes.map(subjectFromClassName);
11324
+ }
11325
+ var MACRO_NAMES = new Set(Object.values(COCO_TO_MACRO.mapping));
11326
+ function macroOf(className) {
11327
+ return COCO_TO_MACRO.mapping[className] ?? className;
11328
+ }
11329
+ /** `animal` matches a dog; `dog` matches only a dog (a cat is also an animal). */
11330
+ function detectionClassMatches(wanted, actual) {
11331
+ const w = wanted.toLowerCase();
11332
+ const a = actual.toLowerCase();
11333
+ if (w === a) return true;
11334
+ return MACRO_NAMES.has(w) && macroOf(a) === w;
10787
11335
  }
11336
+ function isPhase$1(value) {
11337
+ return value === "start" || value === "update" || value === "end";
11338
+ }
11339
+ var DetectionTriggerFilter = class {
11340
+ trigger;
11341
+ /** `${deviceId}|${trackId}` → fired at. Insertion-ordered, so the oldest is first. */
11342
+ fired = /* @__PURE__ */ new Map();
11343
+ subjects;
11344
+ constructor(trigger) {
11345
+ this.trigger = trigger;
11346
+ this.subjects = subjectsOf(trigger);
11347
+ }
11348
+ get firedCount() {
11349
+ return this.fired.size;
11350
+ }
11351
+ evaluate(event, now) {
11352
+ const { deviceId, trackId, phase } = event;
11353
+ if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase$1(phase)) return {
11354
+ kind: "ignore",
11355
+ why: "malformed track-lifecycle event"
11356
+ };
11357
+ if (!this.trigger.deviceIds.includes(deviceId)) return {
11358
+ kind: "ignore",
11359
+ why: `device ${deviceId} is not one of the trigger's devices`
11360
+ };
11361
+ const key = `${deviceId}|${trackId}`;
11362
+ if (phase === "end") {
11363
+ this.fired.delete(key);
11364
+ return {
11365
+ kind: "ignore",
11366
+ why: "track ended"
11367
+ };
11368
+ }
11369
+ this.expire(now);
11370
+ const className = typeof event.bestClassName === "string" ? event.bestClassName : "";
11371
+ const score = typeof event.bestConfidence === "number" ? event.bestConfidence : 0;
11372
+ const vars = {
11373
+ deviceId,
11374
+ className,
11375
+ trackId,
11376
+ score,
11377
+ label: ""
11378
+ };
11379
+ if (this.fired.has(key)) return {
11380
+ kind: "reject",
11381
+ reason: `already fired for track ${trackId}`,
11382
+ vars
11383
+ };
11384
+ const verdict = anySubjectMatches(this.subjects, {
11385
+ className,
11386
+ ...Array.isArray(event.classes) ? { classes: event.classes.filter((c) => typeof c === "string") } : {},
11387
+ ...typeof event.label === "string" ? { label: event.label } : {},
11388
+ ...typeof event.identityId === "string" ? { identityId: event.identityId } : {},
11389
+ ...typeof event.plateText === "string" ? { plateText: event.plateText } : {}
11390
+ });
11391
+ if (!verdict.matches) return {
11392
+ kind: "reject",
11393
+ reason: verdict.reason,
11394
+ vars
11395
+ };
11396
+ const matched = {
11397
+ ...vars,
11398
+ label: verdict.label
11399
+ };
11400
+ if (this.trigger.minScore !== void 0 && score < this.trigger.minScore) return {
11401
+ kind: "reject",
11402
+ reason: `score ${score} < ${this.trigger.minScore}`,
11403
+ vars: matched
11404
+ };
11405
+ if (this.trigger.zones !== void 0) {
11406
+ const visited = Array.isArray(event.zonesVisited) ? event.zonesVisited.filter((z) => typeof z === "string") : [];
11407
+ if (!this.trigger.zones.some((z) => visited.includes(z))) return {
11408
+ kind: "reject",
11409
+ reason: `zone mismatch: visited [${visited.join(", ")}], wanted [${this.trigger.zones.join(", ")}]`,
11410
+ vars: matched
11411
+ };
11412
+ }
11413
+ this.remember(key, now);
11414
+ return {
11415
+ kind: "fire",
11416
+ reason: `${verdict.label === "" ? className : verdict.label} ${score} on device ${deviceId}`,
11417
+ vars: matched
11418
+ };
11419
+ }
11420
+ remember(key, now) {
11421
+ this.fired.set(key, now);
11422
+ while (this.fired.size > 512) {
11423
+ const oldest = this.fired.keys().next();
11424
+ if (oldest.done === true) break;
11425
+ this.fired.delete(oldest.value);
11426
+ }
11427
+ }
11428
+ expire(now) {
11429
+ for (const [key, at] of this.fired) {
11430
+ if (now - at < 9e5) break;
11431
+ this.fired.delete(key);
11432
+ }
11433
+ }
11434
+ };
10788
11435
  //#endregion
10789
11436
  //#region src/automation/stationary-trigger.ts
11437
+ function isLegacyStationary(trigger) {
11438
+ return "classes" in trigger;
11439
+ }
10790
11440
  function isPhase(value) {
10791
11441
  return value === "appeared" || value === "departed";
10792
11442
  }
@@ -10813,11 +11463,15 @@ function evaluateStationaryTrigger(trigger, event) {
10813
11463
  phase,
10814
11464
  entryId
10815
11465
  };
10816
- if (trigger.classes !== void 0 && !trigger.classes.some((c) => detectionClassMatches(c, className))) return {
10817
- kind: "reject",
10818
- reason: `class ${className || "unknown"} not in [${trigger.classes.join(", ")}]`,
10819
- vars
10820
- };
11466
+ const subjects = isLegacyStationary(trigger) ? trigger.classes?.map(subjectFromClassName) : trigger.subjects;
11467
+ if (subjects !== void 0) {
11468
+ const verdict = anySubjectMatches(subjects, { className });
11469
+ if (!verdict.matches) return {
11470
+ kind: "reject",
11471
+ reason: verdict.reason,
11472
+ vars
11473
+ };
11474
+ }
10821
11475
  if (trigger.zones !== void 0 && !trigger.zones.some((z) => zones.includes(z))) return {
10822
11476
  kind: "reject",
10823
11477
  reason: `zone mismatch: in [${zones.join(", ")}], wanted [${trigger.zones.join(", ")}]`,
@@ -10956,11 +11610,12 @@ var RunGate = class {
10956
11610
  * Run traces — what an automation (or, later, an Action run) DID, node by node,
10957
11611
  * so the editor can paint every block green or red with its reason.
10958
11612
  *
10959
- * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
10960
- * every condition node (containers and leaves, including the ones a
10961
- * short-circuit never reached), the run-mode verdict and every action step —
10962
- * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
10963
- * for an Action run). Skipped
11613
+ * A trace is data, not a log: it covers the WHOLE tree — the event leaf that
11614
+ * fired, every node of the `when` tree (containers and leaves, including the
11615
+ * ones a short-circuit never reached), the run-mode verdict and every action
11616
+ * step — each addressed by a stable path (`when.0.1`, `actions.2`, or `steps.N`
11617
+ * for an Action run). Traces written before the single tree carry
11618
+ * `conditions.*` paths and still parse. Skipped
10964
11619
  * evaluations are traced too, and identical consecutive skips collapse into a
10965
11620
  * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
10966
11621
  *
@@ -10992,6 +11647,8 @@ var TraceNodeOutcomeSchema = zod.z.enum([
10992
11647
  ]);
10993
11648
  var AutomationTriggerTraceSchema = zod.z.object({
10994
11649
  kind: zod.z.string().min(1),
11650
+ /** The event leaf that fired (`when`, `when.0.1`); absent on traces written before the single tree. */
11651
+ path: zod.z.string().min(1).optional(),
10995
11652
  outcome: zod.z.enum(["pass", "fail"]),
10996
11653
  /** Why an event was rejected ("class person not in [dog]") or what fired. */
10997
11654
  reason: zod.z.string(),
@@ -10999,15 +11656,23 @@ var AutomationTriggerTraceSchema = zod.z.object({
10999
11656
  vars: zod.z.record(zod.z.string(), TraceValueSchema)
11000
11657
  });
11001
11658
  var AutomationConditionTraceNodeSchema = zod.z.object({
11002
- /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
11659
+ /** `when` for the root, `when.<i>` for its i-th child, … (`conditions…` on an old trace). */
11003
11660
  path: zod.z.string().min(1),
11004
11661
  kind: zod.z.enum([
11005
11662
  "all",
11006
11663
  "any",
11007
11664
  "not",
11008
11665
  "condition",
11666
+ "device-is",
11009
11667
  "expression",
11010
- "stationary-present"
11668
+ "stationary-present",
11669
+ "device-becomes",
11670
+ "device-event",
11671
+ "schedule",
11672
+ "interval",
11673
+ "manual",
11674
+ "detection",
11675
+ "stationary"
11011
11676
  ]),
11012
11677
  outcome: TraceNodeOutcomeSchema,
11013
11678
  reason: zod.z.string(),
@@ -11041,6 +11706,12 @@ var AutomationStepTraceSchema = zod.z.object({
11041
11706
  deviceId: zod.z.number().int().optional(),
11042
11707
  startedAt: zod.z.number().optional(),
11043
11708
  endedAt: zod.z.number().optional(),
11709
+ /**
11710
+ * The parameters the step SENT, after `{{vars.x}}` substitution — what the
11711
+ * device was actually asked. Bounded like trigger `vars`; a secret parameter
11712
+ * is shown as {@link REDACTED_ARG}. Absent on traces written before it existed.
11713
+ */
11714
+ args: zod.z.record(zod.z.string(), TraceValueSchema).optional(),
11044
11715
  outcome: zod.z.enum([
11045
11716
  "ok",
11046
11717
  "error",
@@ -11132,20 +11803,53 @@ function appendTrace(ring, trace, keep) {
11132
11803
  }
11133
11804
  return next;
11134
11805
  }
11806
+ /** What a secret parameter is shown as in a trace. */
11807
+ var REDACTED_ARG = "[redacted]";
11808
+ /** Parameters kept per step trace; the rest are counted in `…`. */
11809
+ var MAX_TRACE_ARGS = 32;
11810
+ /** A string value is cut here. */
11811
+ var MAX_TRACE_ARG_LENGTH = 200;
11812
+ /** A parameter whose NAME says it is a credential. */
11813
+ var SECRET_ARG_NAME = /pass(word|phrase|code)?|secret|token|api[-_]?key|credential|\bpin\b|auth/i;
11814
+ function isSecretArg(name, value) {
11815
+ return SECRET_ARG_NAME.test(name) || require_sleep.isSecretMarkerLike(value);
11816
+ }
11817
+ /**
11818
+ * The parameters a step sent, as a trace may show them: primitives and short
11819
+ * lists of them (anything deeper is its JSON text, cut), at most
11820
+ * {@link MAX_TRACE_ARGS} of them. No cap method declares a parameter secret, so
11821
+ * a parameter is redacted by what it is called or by being a keychain marker,
11822
+ * and `secretKeys` names the ones a caller knows about.
11823
+ */
11824
+ function buildStepArgsTrace(args, secretKeys = []) {
11825
+ const out = {};
11826
+ for (const [key, value] of Object.entries(args).slice(0, 32)) if (secretKeys.includes(key) || isSecretArg(key, value)) out[key] = REDACTED_ARG;
11827
+ else if (typeof value === "string") out[key] = value.length > 200 ? `${value.slice(0, 200)}…` : value;
11828
+ else if (typeof value === "number" || typeof value === "boolean" || value === null) out[key] = value;
11829
+ else if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) out[key] = value;
11830
+ else if (value !== void 0) {
11831
+ const text = JSON.stringify(value) ?? "";
11832
+ out[key] = text.length > 200 ? `${text.slice(0, 200)}…` : text;
11833
+ }
11834
+ return out;
11835
+ }
11135
11836
  /**
11136
11837
  * The trigger stage of a trace from the bag a trigger handed to `run()`:
11137
11838
  * primitives and short lists of them (a device-state `to` that is an object is left out), minus the
11138
- * bookkeeping keys `kind` and `at`.
11839
+ * bookkeeping keys `kind`, `at` and `leaf` (the fired leaf's path, which moves to `path`).
11139
11840
  */
11140
11841
  function buildTriggerTrace(trigger, outcome, reason) {
11141
11842
  const vars = {};
11142
11843
  for (const [key, value] of Object.entries(trigger)) {
11143
- if (key === "kind" || key === "at") continue;
11844
+ if (key === "kind" || key === "at" || key === "leaf") continue;
11144
11845
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
11145
11846
  else if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) vars[key] = value;
11146
11847
  }
11848
+ const kind = typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown";
11849
+ const path = typeof trigger["leaf"] === "string" ? trigger["leaf"] : void 0;
11147
11850
  return {
11148
- kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
11851
+ kind,
11852
+ ...path === void 0 ? {} : { path },
11149
11853
  outcome,
11150
11854
  reason,
11151
11855
  vars
@@ -11383,14 +12087,22 @@ async function runSteps(steps, options) {
11383
12087
  }
11384
12088
  const startedAt = options.now();
11385
12089
  let deviceId = step.meta.deviceId;
11386
- const scope = { setDeviceId: (id) => {
11387
- deviceId = id;
11388
- } };
12090
+ let sent;
12091
+ const scope = {
12092
+ setDeviceId: (id) => {
12093
+ deviceId = id;
12094
+ },
12095
+ setArgs: (args) => {
12096
+ sent = args;
12097
+ }
12098
+ };
12099
+ const argsTrace = () => sent === void 0 ? {} : { args: buildStepArgsTrace(sent) };
11389
12100
  try {
11390
12101
  await step.run(scope);
11391
12102
  traces.push({
11392
12103
  ...step.meta,
11393
12104
  ...deviceId === void 0 ? {} : { deviceId },
12105
+ ...argsTrace(),
11394
12106
  path,
11395
12107
  startedAt,
11396
12108
  endedAt: options.now(),
@@ -11408,6 +12120,7 @@ async function runSteps(steps, options) {
11408
12120
  traces.push({
11409
12121
  ...step.meta,
11410
12122
  ...deviceId === void 0 ? {} : { deviceId },
12123
+ ...argsTrace(),
11411
12124
  path,
11412
12125
  startedAt,
11413
12126
  endedAt: options.now(),
@@ -11494,6 +12207,7 @@ function buildActionSteps(steps, vars, deps) {
11494
12207
  const targets = typeof step.deviceId === "number" ? [step.deviceId] : Array.isArray(step.deviceId) ? step.deviceId : [resolveDeviceTarget(vars, step.deviceId.$var)];
11495
12208
  for (const deviceId of targets) {
11496
12209
  scope.setDeviceId(deviceId);
12210
+ scope.setArgs(args);
11497
12211
  await deps.invokeCap({
11498
12212
  deviceId,
11499
12213
  cap: step.cap,
@@ -11633,9 +12347,9 @@ function normalizeLeaf(leaf, resolver, path) {
11633
12347
  };
11634
12348
  }
11635
12349
  /** Normalise every leaf of a condition tree. Throws {@link QuantityUnitError}. */
11636
- function normalizeConditionUnits(condition, resolver, path = "conditions") {
12350
+ function normalizeConditionUnits(condition, resolver, path = "when") {
11637
12351
  switch (condition.kind) {
11638
- case "condition": {
12352
+ case "device-is": {
11639
12353
  const { leaf, warnings } = normalizeLeaf(condition, resolver, path);
11640
12354
  return {
11641
12355
  value: leaf,
@@ -11728,17 +12442,17 @@ function normalizeActionUnits(actions, resolver, path = "steps") {
11728
12442
  warnings
11729
12443
  };
11730
12444
  }
11731
- /** Normalise a whole recipe: its condition tree and its actions. */
12445
+ /** Normalise a whole recipe: its `when` tree and its actions. */
11732
12446
  function normalizeRecipeUnits(recipe, resolver) {
11733
- const conditions = recipe.conditions === void 0 ? void 0 : normalizeConditionUnits(recipe.conditions, resolver, "conditions");
12447
+ const when = normalizeConditionUnits(recipe.when, resolver, "when");
11734
12448
  const actions = normalizeActionUnits(recipe.actions, resolver, "actions");
11735
12449
  return {
11736
12450
  value: {
11737
12451
  ...recipe,
11738
- ...conditions === void 0 ? {} : { conditions: conditions.value },
12452
+ when: when.value,
11739
12453
  actions: actions.value
11740
12454
  },
11741
- warnings: [...conditions?.warnings ?? [], ...actions.warnings]
12455
+ warnings: [...when.warnings, ...actions.warnings]
11742
12456
  };
11743
12457
  }
11744
12458
  //#endregion
@@ -11761,7 +12475,7 @@ function normalizeRecipeUnits(recipe, resolver) {
11761
12475
  */
11762
12476
  function stampCondition(condition, resolver) {
11763
12477
  switch (condition.kind) {
11764
- case "condition": {
12478
+ case "device-is": {
11765
12479
  if (typeof condition.value !== "number" || condition.unit !== void 0) return condition;
11766
12480
  const quantity = resolver.fieldQuantity(condition.cap, condition.fieldPath);
11767
12481
  if (quantity === void 0 || unitsForQuantity(quantity).length < 2) return condition;
@@ -11804,7 +12518,7 @@ function stampAction(action, resolver) {
11804
12518
  function prepareRecipeUnits(recipe, resolver) {
11805
12519
  const stamped = {
11806
12520
  ...recipe,
11807
- ...recipe.conditions === void 0 ? {} : { conditions: stampCondition(recipe.conditions, resolver) },
12521
+ when: stampCondition(recipe.when, resolver),
11808
12522
  actions: recipe.actions.map((action) => stampAction(action, resolver))
11809
12523
  };
11810
12524
  try {
@@ -12609,6 +13323,25 @@ var addonSettingsCapability = {
12609
13323
  addonId: zod.z.string(),
12610
13324
  nodeId: zod.z.string().optional()
12611
13325
  }), SettingsSchemaWithValuesSchema.nullable()),
13326
+ getDeclaredSettingsKeys: require_sleep.method(zod.z.object({
13327
+ addonId: zod.z.string(),
13328
+ nodeId: zod.z.string().optional()
13329
+ }), zod.z.object({ keys: zod.z.array(zod.z.string()) }).nullable(), { auth: "admin" }),
13330
+ unsetStoredSettingsKeys: require_sleep.method(zod.z.object({
13331
+ addonId: zod.z.string(),
13332
+ nodeId: zod.z.string().optional(),
13333
+ keys: zod.z.array(zod.z.string())
13334
+ }), zod.z.object({
13335
+ removed: zod.z.array(zod.z.string()),
13336
+ refused: zod.z.array(zod.z.object({
13337
+ key: zod.z.string(),
13338
+ reason: zod.z.string()
13339
+ })),
13340
+ secretsCleared: zod.z.number()
13341
+ }), {
13342
+ kind: "mutation",
13343
+ auth: "admin"
13344
+ }),
12612
13345
  getDeviceSettings: require_sleep.method(zod.z.object({
12613
13346
  addonId: zod.z.string(),
12614
13347
  deviceId: zod.z.number(),
@@ -20747,6 +21480,8 @@ var StorageKindSchema = zod.z.object({
20747
21480
  "orphaned",
20748
21481
  "dropped"
20749
21482
  ]),
21483
+ /** An owner declared this kind since the engine started. */
21484
+ declaredThisBoot: zod.z.boolean(),
20750
21485
  /** `null` when the caller asked for no counts (`counts: false`). */
20751
21486
  rows: zod.z.number().int().nonnegative().nullable(),
20752
21487
  /** Rows x the average size of a sample; null for an empty kind. */
@@ -20758,6 +21493,51 @@ var StorageKindSchema = zod.z.object({
20758
21493
  /** The whole declaration, secret paths and entry descriptor included (the entries service builds its handle from it). */
20759
21494
  declaration: require_sleep.RecordKindDeclarationSchema
20760
21495
  });
21496
+ zod.z.enum([
21497
+ "orphaned",
21498
+ "stale",
21499
+ "unknown"
21500
+ ]);
21501
+ /** A record kind nobody declared since its owner started, or whose owner is gone. */
21502
+ var StaleKindFindingSchema = zod.z.object({
21503
+ owner: zod.z.string(),
21504
+ kind: zod.z.string(),
21505
+ /** `unknown`: the owner could not be judged (not running, or its state unreadable) - never dropped. */
21506
+ verdict: zod.z.enum(["orphaned", "unknown"]),
21507
+ reason: zod.z.string(),
21508
+ rows: zod.z.number().int().nonnegative().nullable(),
21509
+ /** local-auth's tables are never dropped by this audit. */
21510
+ protected: zod.z.boolean()
21511
+ });
21512
+ /** A stored global-settings key the running addon no longer declares. */
21513
+ var StaleSettingsKeyFindingSchema = zod.z.object({
21514
+ addonId: zod.z.string(),
21515
+ /** The stored key as written, a per-node `<key>@<nodeId>` suffix included. */
21516
+ key: zod.z.string(),
21517
+ /**
21518
+ * Keychain refs whose ONLY marker sits under this key: in use by stale data,
21519
+ * not orphans yet. Removing the key releases them.
21520
+ */
21521
+ secretRefs: zod.z.array(zod.z.string())
21522
+ });
21523
+ /** An addon the audit could not judge, with the reason. Never reported as stale. */
21524
+ var StaleUnknownSchema = zod.z.object({
21525
+ subject: zod.z.string(),
21526
+ reason: zod.z.string()
21527
+ });
21528
+ var StaleDataReportSchema = zod.z.object({
21529
+ /** Epoch ms of the last finished scan; `null` before the first. */
21530
+ scannedAt: zod.z.number().nullable(),
21531
+ scanning: zod.z.boolean(),
21532
+ trigger: zod.z.enum([
21533
+ "boot",
21534
+ "interval",
21535
+ "manual"
21536
+ ]).nullable(),
21537
+ kinds: zod.z.array(StaleKindFindingSchema),
21538
+ settingsKeys: zod.z.array(StaleSettingsKeyFindingSchema),
21539
+ unknown: zod.z.array(StaleUnknownSchema)
21540
+ });
20761
21541
  /**
20762
21542
  * storage-admin - the operator's maintenance reach into the databases: list
20763
21543
  * their tables and record kinds, read a kind for diagnosis. Read-only: a table
@@ -20794,7 +21574,49 @@ var storageAdminCapability = {
20794
21574
  readRecords: require_sleep.method(RecordQueryInputSchema.extend({
20795
21575
  owner: zod.z.string().min(1),
20796
21576
  kind: require_sleep.RecordKindNameSchema
20797
- }), RecordQueryResultSchema, { auth: "admin" })
21577
+ }), RecordQueryResultSchema, { auth: "admin" }),
21578
+ /**
21579
+ * The last stale-data scan (read-only detection): record kinds left behind
21580
+ * and settings keys no running addon declares. Held in memory; a scan runs
21581
+ * at hub boot, every 24 h and on `scanStaleData`.
21582
+ */
21583
+ staleDataReport: require_sleep.method(zod.z.object({}), StaleDataReportSchema, { auth: "admin" }),
21584
+ /** Scan now. Detection only: it logs findings (owner and key, never values) and changes nothing. */
21585
+ scanStaleData: require_sleep.method(zod.z.object({}), StaleDataReportSchema, {
21586
+ kind: "mutation",
21587
+ auth: "admin"
21588
+ }),
21589
+ /**
21590
+ * Drop an ORPHANED kind: its rows, their keychain values, then mark it
21591
+ * dropped. The verdict is re-checked at call time; anything not still
21592
+ * orphaned (or a protected owner) is refused.
21593
+ */
21594
+ dropOrphanedKind: require_sleep.method(zod.z.object({
21595
+ owner: zod.z.string().min(1),
21596
+ kind: require_sleep.RecordKindNameSchema
21597
+ }), zod.z.object({
21598
+ deleted: zod.z.number().int().nonnegative(),
21599
+ secretsReleased: zod.z.number().int().nonnegative()
21600
+ }), {
21601
+ kind: "mutation",
21602
+ auth: "admin",
21603
+ caller: "required"
21604
+ }),
21605
+ /**
21606
+ * Unset one stale settings key on a running addon (an owner-side write that
21607
+ * also clears the keychain values the key held). Re-checked at call time.
21608
+ */
21609
+ unsetStaleSettingsKey: require_sleep.method(zod.z.object({
21610
+ addonId: zod.z.string().min(1),
21611
+ key: zod.z.string().min(1)
21612
+ }), zod.z.object({
21613
+ removed: zod.z.boolean(),
21614
+ secretsCleared: zod.z.number().int().nonnegative()
21615
+ }), {
21616
+ kind: "mutation",
21617
+ auth: "admin",
21618
+ caller: "required"
21619
+ })
20798
21620
  }
20799
21621
  };
20800
21622
  //#endregion
@@ -20974,6 +21796,19 @@ var keychainCapability = {
20974
21796
  kind: "mutation",
20975
21797
  auth: "admin"
20976
21798
  }),
21799
+ /**
21800
+ * Release the values rows of a DROPPED record kind held, whatever the kind's
21801
+ * owner (stale-data audit). Reserved to `sqlite-settings`, the engine that
21802
+ * just deleted the rows: only refs scoped `row/records:<kind>/...` are
21803
+ * accepted, and the audit names the operator it acted for. Answers a count.
21804
+ */
21805
+ releaseRecordRefs: require_sleep.method(zod.z.object({
21806
+ refs: RefListSchema,
21807
+ onBehalfOf: zod.z.string().min(1)
21808
+ }), zod.z.object({ released: zod.z.number().int() }), {
21809
+ kind: "mutation",
21810
+ auth: "admin"
21811
+ }),
20977
21812
  /** The caller's own refs, metadata only. */
20978
21813
  listMeta: require_sleep.method(zod.z.object({}), zod.z.array(KeychainSecretMetaSchema).readonly(), { auth: "admin" }),
20979
21814
  /** Whether values can be served — the save path refuses a write while locked. */
@@ -22294,7 +23129,9 @@ var DiscoveredChildDeviceSchema = zod.z.lazy(() => zod.z.object({
22294
23129
  /** Upstream-system identity + rendering hints for the candidate. */
22295
23130
  sourceInfo: SourceInfoSchema.optional(),
22296
23131
  /** Nested entity-children for accordion preview — integrations populate, Hubs omit. */
22297
- children: zod.z.array(DiscoveredChildDeviceSchema).readonly().optional()
23132
+ children: zod.z.array(DiscoveredChildDeviceSchema).readonly().optional(),
23133
+ /** `childNativeId` of the gateway candidate in the same list this device sits behind. */
23134
+ parentStableId: zod.z.string().optional()
22298
23135
  }));
22299
23136
  /**
22300
23137
  * Status block — populated by the integration after each
@@ -42900,6 +43737,79 @@ var DiscoveredDeviceSchema = zod.z.object({
42900
43737
  metadata: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
42901
43738
  });
42902
43739
  /**
43740
+ * HOW a provider can be found on the network. The provider only DECLARES the
43741
+ * mode; the `local-network` scan engine runs it once for every provider that
43742
+ * asked (one multicast browse, one host sweep) and hands the raw hits back to
43743
+ * `classifyDiscoveryHits`. A provider never opens a discovery socket itself,
43744
+ * except through a `custom` mode.
43745
+ */
43746
+ var DiscoveryModeSchema = zod.z.discriminatedUnion("kind", [
43747
+ zod.z.object({
43748
+ kind: zod.z.literal("mdns"),
43749
+ /** DNS-SD service type, e.g. `_esphomelib._tcp`. */
43750
+ serviceType: zod.z.string().min(1),
43751
+ /** Regular expression an instance name must match (case-insensitive). */
43752
+ instancePattern: zod.z.string().optional()
43753
+ }),
43754
+ zod.z.object({
43755
+ kind: zod.z.literal("ws-discovery"),
43756
+ /** Probe types (e.g. `dn:NetworkVideoTransmitter`); omitted = any device. */
43757
+ types: zod.z.array(zod.z.string()).optional()
43758
+ }),
43759
+ zod.z.object({
43760
+ kind: zod.z.literal("ssdp"),
43761
+ searchTarget: zod.z.string().min(1)
43762
+ }),
43763
+ zod.z.object({
43764
+ kind: zod.z.literal("udp-broadcast"),
43765
+ port: zod.z.number().int().min(1).max(65535),
43766
+ payloadHex: zod.z.string().regex(/^([0-9a-fA-F]{2})*$/),
43767
+ /** Port replies arrive on, when the device answers somewhere other than the source port. */
43768
+ listenPort: zod.z.number().int().min(1).max(65535).optional()
43769
+ }),
43770
+ zod.z.object({
43771
+ kind: zod.z.literal("http-probe"),
43772
+ port: zod.z.number().int().min(1).max(65535),
43773
+ path: zod.z.string().startsWith("/"),
43774
+ method: zod.z.enum(["GET", "POST"]).default("GET"),
43775
+ bodyText: zod.z.string().optional(),
43776
+ contentType: zod.z.string().optional(),
43777
+ tls: zod.z.boolean().default(false),
43778
+ /** Status codes that count as a hit. Omitted = any HTTP answer. */
43779
+ acceptStatus: zod.z.array(zod.z.number().int()).optional()
43780
+ }),
43781
+ zod.z.object({
43782
+ kind: zod.z.literal("custom"),
43783
+ /** Why the engine cannot do it (an allowlisted exception, e.g. a vendor UDP protocol). */
43784
+ reason: zod.z.string().min(1)
43785
+ })
43786
+ ]);
43787
+ /** What the engine saw on the wire for ONE declared mode; the provider classifies it. */
43788
+ var DiscoveryHitSchema = zod.z.object({
43789
+ /** Index into the provider's own `getDiscoveryModes()` array. */
43790
+ modeIndex: zod.z.number().int().min(0),
43791
+ host: zod.z.string(),
43792
+ port: zod.z.number().int(),
43793
+ http: zod.z.object({
43794
+ status: zod.z.number().int(),
43795
+ headers: zod.z.record(zod.z.string(), zod.z.string()),
43796
+ /** At most 16 KiB. */
43797
+ bodyText: zod.z.string()
43798
+ }).optional(),
43799
+ mdns: zod.z.object({
43800
+ instance: zod.z.string(),
43801
+ host: zod.z.string(),
43802
+ port: zod.z.number().int(),
43803
+ txt: zod.z.record(zod.z.string(), zod.z.string())
43804
+ }).optional(),
43805
+ udp: zod.z.object({ payloadHex: zod.z.string() }).optional(),
43806
+ wsd: zod.z.object({
43807
+ xaddrs: zod.z.array(zod.z.string()),
43808
+ types: zod.z.array(zod.z.string()),
43809
+ scopes: zod.z.array(zod.z.string())
43810
+ }).optional()
43811
+ });
43812
+ /**
42903
43813
  * Candidate handed back from discovery and accepted by
42904
43814
  * `adoptDiscoveredDevice`. Shape mirrors the in-process
42905
43815
  * `DiscoveredDevice` interface used by `DeviceDiscovery`.
@@ -42934,7 +43844,17 @@ var DiscoveryCandidateSchema = zod.z.object({
42934
43844
  /** Numeric id of the device this candidate was matched to. Set with `alreadyOnboarded`. */
42935
43845
  onboardedDeviceId: zod.z.number().optional(),
42936
43846
  /** Operator-facing name of the matched device, so the UI can say WHICH one it is. */
42937
- onboardedName: zod.z.string().optional()
43847
+ onboardedName: zod.z.string().optional(),
43848
+ /**
43849
+ * This candidate is an INTEGRATION to set up (a gateway, a hub, a controller),
43850
+ * not a device to adopt. Replaces the `prefilledConfig.setupIntegration` marker,
43851
+ * which is still read for one release.
43852
+ */
43853
+ setupIntegration: zod.z.boolean().optional(),
43854
+ /** `stableId` of the gateway candidate this device was found BEHIND. */
43855
+ parentStableId: zod.z.string().optional(),
43856
+ /** `false` = shown for context but not pickable. Default true. */
43857
+ selectable: zod.z.boolean().optional()
42938
43858
  });
42939
43859
  /**
42940
43860
  * Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
@@ -43031,6 +43951,24 @@ var deviceProviderCapability = {
43031
43951
  auth: "admin"
43032
43952
  }),
43033
43953
  /**
43954
+ * How this provider is found on the network. `[]` = the provider has no
43955
+ * declarative mode and keeps answering `discoverDevices` itself.
43956
+ */
43957
+ getDiscoveryModes: require_sleep.method(zod.z.object({}), zod.z.array(DiscoveryModeSchema)),
43958
+ /**
43959
+ * Turn the engine's raw hits into candidates. The provider may make
43960
+ * follow-up UNICAST requests to a hit host (an authenticated call, an
43961
+ * identity check) and never sweeps a subnet. `params` is the per-scan form
43962
+ * from `getDiscoveryParamsSchema`.
43963
+ */
43964
+ classifyDiscoveryHits: require_sleep.method(zod.z.object({
43965
+ hits: zod.z.array(DiscoveryHitSchema),
43966
+ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
43967
+ }), zod.z.array(DiscoveryCandidateSchema), {
43968
+ kind: "mutation",
43969
+ auth: "admin"
43970
+ }),
43971
+ /**
43034
43972
  * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
43035
43973
  * provider accepts (e.g. Gree's broadcast address for a different subnet).
43036
43974
  * `null` when the provider takes no extra scan params — the generic
@@ -44388,20 +45326,26 @@ var deviceManagerCapability = {
44388
45326
  label: zod.z.string()
44389
45327
  })).readonly() }), { auth: "admin" }),
44390
45328
  /** Scan EVERY discovery-capable provider; returns candidates grouped by addon. */
44391
- discoverAllProviders: require_sleep.method(zod.z.object({}), zod.z.object({ groups: zod.z.array(zod.z.object({
44392
- addonId: zod.z.string(),
44393
- label: zod.z.string(),
44394
- candidates: zod.z.array(DiscoveryCandidateSchema).readonly(),
44395
- error: zod.z.string().nullable()
44396
- })).readonly() }), {
44397
- kind: "mutation",
44398
- auth: "admin"
44399
- }),
45329
+ discoverAllProviders: require_sleep.method(
45330
+ /** `subnets`: extra CIDRs for THIS scan, on top of the hub's own and the saved list. */
45331
+ zod.z.object({ subnets: zod.z.array(zod.z.string()).optional() }),
45332
+ zod.z.object({ groups: zod.z.array(zod.z.object({
45333
+ addonId: zod.z.string(),
45334
+ label: zod.z.string(),
45335
+ candidates: zod.z.array(DiscoveryCandidateSchema).readonly(),
45336
+ error: zod.z.string().nullable()
45337
+ })).readonly() }),
45338
+ {
45339
+ kind: "mutation",
45340
+ auth: "admin"
45341
+ }
45342
+ ),
44400
45343
  /** Scan ONE provider with optional provider-specific params (subnet /
44401
45344
  * broadcast addr) — the per-integration "Scan network" surface. */
44402
45345
  discoverProvider: require_sleep.method(zod.z.object({
44403
45346
  addonId: zod.z.string(),
44404
- params: zod.z.record(zod.z.string(), zod.z.unknown()).optional()
45347
+ params: zod.z.record(zod.z.string(), zod.z.unknown()).optional(),
45348
+ subnets: zod.z.array(zod.z.string()).optional()
44405
45349
  }), zod.z.object({ candidates: zod.z.array(DiscoveryCandidateSchema).readonly() }), {
44406
45350
  kind: "mutation",
44407
45351
  auth: "admin"
@@ -62504,6 +63448,12 @@ var TerminalInstanceInfoSchema = zod.z.object({
62504
63448
  instanceId: zod.z.string(),
62505
63449
  cameraStableId: zod.z.string(),
62506
63450
  nodeId: zod.z.string(),
63451
+ /**
63452
+ * False while the node the instance is bound to is not in the cluster
63453
+ * topology (forgotten, renamed, re-paired). The instance is kept; the
63454
+ * operator re-points it with `updateInstance({ targetNodeId })`.
63455
+ */
63456
+ nodeInCluster: zod.z.boolean(),
62507
63457
  profileId: zod.z.string(),
62508
63458
  profileLabel: zod.z.string(),
62509
63459
  name: zod.z.string(),
@@ -62570,6 +63520,8 @@ var terminalSessionCapability = {
62570
63520
  }),
62571
63521
  updateInstance: require_sleep.method(zod.z.object({
62572
63522
  instanceId: zod.z.string().min(1),
63523
+ /** Re-point the instance to another node; the profile must exist there. */
63524
+ targetNodeId: zod.z.string().min(1).max(256).optional(),
62573
63525
  name: zod.z.string().trim().min(1).max(160).optional(),
62574
63526
  executable: zod.z.string().max(1024).optional(),
62575
63527
  args: zod.z.array(zod.z.string().max(2048)).max(64).optional(),
@@ -70961,7 +71913,9 @@ var integrationsCapability = {
70961
71913
  id: zod.z.string(),
70962
71914
  settings: zod.z.record(zod.z.string(), zod.z.unknown()),
70963
71915
  /** Keys the form's schema declares secret (by type) — keychain-stored (D787). */
70964
- secretKeys: zod.z.array(zod.z.string()).optional()
71916
+ secretKeys: zod.z.array(zod.z.string()).optional(),
71917
+ /** The integration's own addon applies the change in-process and asks the hub not to respawn it. */
71918
+ skipRestart: zod.z.boolean().optional()
70965
71919
  }), zod.z.object({ success: zod.z.boolean() }), {
70966
71920
  kind: "mutation",
70967
71921
  auth: "admin"
@@ -71607,6 +72561,39 @@ var TlsStatusSchema = zod.z.object({
71607
72561
  hasCa: zod.z.boolean()
71608
72562
  }).nullable()
71609
72563
  });
72564
+ /** One provider's ask of the scan engine: the modes it declared. */
72565
+ var ScanRequestSchema = zod.z.object({
72566
+ providerId: zod.z.string().min(1),
72567
+ modes: zod.z.array(DiscoveryModeSchema)
72568
+ });
72569
+ var ScanErrorSchema = zod.z.object({
72570
+ /** Absent for a failure that is the scan's own (a bad subnet, a socket that would not open). */
72571
+ providerId: zod.z.string().optional(),
72572
+ /** The mode family that failed (`mdns`, `http-probe`, …). */
72573
+ mode: zod.z.string().optional(),
72574
+ message: zod.z.string()
72575
+ });
72576
+ /** What one scan did, per mode family; the engine logs the same numbers once. */
72577
+ var ScanStatsSchema = zod.z.object({
72578
+ hostsProbed: zod.z.number().int(),
72579
+ hits: zod.z.number().int(),
72580
+ timeouts: zod.z.number().int(),
72581
+ byMode: zod.z.record(zod.z.string(), zod.z.object({
72582
+ probed: zod.z.number().int(),
72583
+ hits: zod.z.number().int(),
72584
+ timeouts: zod.z.number().int()
72585
+ }))
72586
+ });
72587
+ var ScanNetworkResultSchema = zod.z.object({
72588
+ hits: zod.z.array(zod.z.object({
72589
+ providerId: zod.z.string(),
72590
+ hit: DiscoveryHitSchema
72591
+ })),
72592
+ /** CIDRs actually covered: the hub's own /24s, the saved list and the per-call list. */
72593
+ subnetsScanned: zod.z.array(zod.z.string()),
72594
+ errors: zod.z.array(ScanErrorSchema),
72595
+ stats: ScanStatsSchema
72596
+ });
71610
72597
  var localNetworkCapability = {
71611
72598
  name: "local-network",
71612
72599
  scope: "system",
@@ -71820,6 +72807,34 @@ var localNetworkCapability = {
71820
72807
  kind: "mutation",
71821
72808
  auth: "admin"
71822
72809
  }),
72810
+ /**
72811
+ * The LAN discovery engine. Every provider that declared discovery modes
72812
+ * (`device-provider.getDiscoveryModes`) is scanned in ONE pass: a single
72813
+ * mDNS browse for the union of service types, one WS-Discovery probe, one
72814
+ * SSDP search per target and one sweep that visits each host once and runs
72815
+ * every http-probe of every provider. Subnets scanned = the hub's own
72816
+ * non-internal IPv4 /24s ∪ the saved list (`getScanSubnets`) ∪ `subnets`.
72817
+ * A bad CIDR, or more than 4096 hosts in total, refuses the scan.
72818
+ */
72819
+ scanNetwork: require_sleep.method(zod.z.object({
72820
+ requests: zod.z.array(ScanRequestSchema),
72821
+ subnets: zod.z.array(zod.z.string()).optional()
72822
+ }), ScanNetworkResultSchema, {
72823
+ kind: "mutation",
72824
+ auth: "admin",
72825
+ timeoutMs: 9e4
72826
+ }),
72827
+ /** Extra subnets every scan covers besides the hub's own, as CIDR strings. */
72828
+ getScanSubnets: require_sleep.method(zod.z.void(), zod.z.object({ subnets: zod.z.array(zod.z.string()) }), { auth: "admin" }),
72829
+ /**
72830
+ * Replace the saved extra subnets (a cluster setting). Each entry is IPv4
72831
+ * CIDR, at most a /22; the list is refused when it is invalid or its hosts
72832
+ * would exceed the scan cap.
72833
+ */
72834
+ setScanSubnets: require_sleep.method(zod.z.object({ subnets: zod.z.array(zod.z.string()) }), zod.z.object({ subnets: zod.z.array(zod.z.string()) }), {
72835
+ kind: "mutation",
72836
+ auth: "admin"
72837
+ }),
71823
72838
  /** Return to the generated local CA. The uploaded certificate stays stored (D873). */
71824
72839
  revertToGeneratedCertificate: require_sleep.method(zod.z.void(), TlsStatusSchema, {
71825
72840
  kind: "mutation",
@@ -88643,6 +89658,17 @@ var BaseDeviceProvider = class extends require_sleep.BaseAddon {
88643
89658
  async discoverDevices(_input) {
88644
89659
  return [];
88645
89660
  }
89661
+ /**
89662
+ * How the scan engine finds this provider. Default `[]`: the provider is
89663
+ * not migrated and keeps answering `discoverDevices` itself.
89664
+ */
89665
+ async getDiscoveryModes() {
89666
+ return [];
89667
+ }
89668
+ /** Turn the scan engine's raw hits into candidates. Default: none. */
89669
+ async classifyDiscoveryHits(_input) {
89670
+ return [];
89671
+ }
88646
89672
  /** Extra per-scan input form (e.g. a broadcast address for another subnet).
88647
89673
  * Null = no extra params. Override in providers that support scoped scans. */
88648
89674
  async getDiscoveryParamsSchema() {
@@ -90333,6 +91359,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90333
91359
  access: "create",
90334
91360
  role: "admin"
90335
91361
  },
91362
+ "addonSettings.getDeclaredSettingsKeys": {
91363
+ capName: "addon-settings",
91364
+ capScope: "system",
91365
+ addonId: null,
91366
+ access: "view",
91367
+ role: "admin"
91368
+ },
90336
91369
  "addonSettings.getDeviceSettings": {
90337
91370
  capName: "addon-settings",
90338
91371
  capScope: "system",
@@ -90354,6 +91387,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
90354
91387
  access: "view",
90355
91388
  role: "admin"
90356
91389
  },
91390
+ "addonSettings.unsetStoredSettingsKeys": {
91391
+ capName: "addon-settings",
91392
+ capScope: "system",
91393
+ addonId: null,
91394
+ access: "create",
91395
+ role: "admin"
91396
+ },
90357
91397
  "addonSettings.updateDeviceSettings": {
90358
91398
  capName: "addon-settings",
90359
91399
  capScope: "system",
@@ -92769,6 +93809,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
92769
93809
  access: "create",
92770
93810
  role: "admin"
92771
93811
  },
93812
+ "deviceProvider.classifyDiscoveryHits": {
93813
+ capName: "device-provider",
93814
+ capScope: "system",
93815
+ addonId: null,
93816
+ access: "create",
93817
+ role: "admin"
93818
+ },
92772
93819
  "deviceProvider.createDevice": {
92773
93820
  capName: "device-provider",
92774
93821
  capScope: "system",
@@ -92797,6 +93844,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
92797
93844
  access: "view",
92798
93845
  role: "admin"
92799
93846
  },
93847
+ "deviceProvider.getDiscoveryModes": {
93848
+ capName: "device-provider",
93849
+ capScope: "system",
93850
+ addonId: null,
93851
+ access: "view",
93852
+ role: "admin"
93853
+ },
92800
93854
  "deviceProvider.getDiscoveryParamsSchema": {
92801
93855
  capName: "device-provider",
92802
93856
  capScope: "system",
@@ -93574,6 +94628,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
93574
94628
  access: "create",
93575
94629
  role: "admin"
93576
94630
  },
94631
+ "keychain.releaseRecordRefs": {
94632
+ capName: "keychain",
94633
+ capScope: "system",
94634
+ addonId: null,
94635
+ access: "create",
94636
+ role: "admin"
94637
+ },
93577
94638
  "keychain.resolve": {
93578
94639
  capName: "keychain",
93579
94640
  capScope: "system",
@@ -93994,6 +95055,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
93994
95055
  access: "view",
93995
95056
  role: "admin"
93996
95057
  },
95058
+ "localNetwork.getScanSubnets": {
95059
+ capName: "local-network",
95060
+ capScope: "system",
95061
+ addonId: null,
95062
+ access: "view",
95063
+ role: "admin"
95064
+ },
93997
95065
  "localNetwork.getTlsStatus": {
93998
95066
  capName: "local-network",
93999
95067
  capScope: "system",
@@ -94043,6 +95111,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
94043
95111
  access: "create",
94044
95112
  role: "admin"
94045
95113
  },
95114
+ "localNetwork.scanNetwork": {
95115
+ capName: "local-network",
95116
+ capScope: "system",
95117
+ addonId: null,
95118
+ access: "create",
95119
+ role: "admin"
95120
+ },
94046
95121
  "localNetwork.setAddressRole": {
94047
95122
  capName: "local-network",
94048
95123
  capScope: "system",
@@ -94064,6 +95139,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
94064
95139
  access: "create",
94065
95140
  role: "admin"
94066
95141
  },
95142
+ "localNetwork.setScanSubnets": {
95143
+ capName: "local-network",
95144
+ capScope: "system",
95145
+ addonId: null,
95146
+ access: "create",
95147
+ role: "admin"
95148
+ },
94067
95149
  "localNetwork.setViewerEndpoints": {
94068
95150
  capName: "local-network",
94069
95151
  capScope: "system",
@@ -98285,6 +99367,13 @@ var METHOD_ACCESS_MAP = Object.freeze({
98285
99367
  access: "create",
98286
99368
  role: "admin"
98287
99369
  },
99370
+ "storageAdmin.dropOrphanedKind": {
99371
+ capName: "storage-admin",
99372
+ capScope: "system",
99373
+ addonId: null,
99374
+ access: "delete",
99375
+ role: "admin"
99376
+ },
98288
99377
  "storageAdmin.listKinds": {
98289
99378
  capName: "storage-admin",
98290
99379
  capScope: "system",
@@ -98306,6 +99395,27 @@ var METHOD_ACCESS_MAP = Object.freeze({
98306
99395
  access: "view",
98307
99396
  role: "admin"
98308
99397
  },
99398
+ "storageAdmin.scanStaleData": {
99399
+ capName: "storage-admin",
99400
+ capScope: "system",
99401
+ addonId: null,
99402
+ access: "create",
99403
+ role: "admin"
99404
+ },
99405
+ "storageAdmin.staleDataReport": {
99406
+ capName: "storage-admin",
99407
+ capScope: "system",
99408
+ addonId: null,
99409
+ access: "view",
99410
+ role: "admin"
99411
+ },
99412
+ "storageAdmin.unsetStaleSettingsKey": {
99413
+ capName: "storage-admin",
99414
+ capScope: "system",
99415
+ addonId: null,
99416
+ access: "create",
99417
+ role: "admin"
99418
+ },
98309
99419
  "storageEvictable.evict": {
98310
99420
  capName: "storage-evictable",
98311
99421
  capScope: "system",
@@ -111511,7 +112621,9 @@ function createSystemProxy(api) {
111511
112621
  addonSettings: {
111512
112622
  getGlobalSettings: (input) => dispatch("addonSettings", "getGlobalSettings", "query", input),
111513
112623
  updateGlobalSettings: (input) => dispatch("addonSettings", "updateGlobalSettings", "mutation", input),
111514
- getIntegrationSettings: (input) => dispatch("addonSettings", "getIntegrationSettings", "query", input)
112624
+ getIntegrationSettings: (input) => dispatch("addonSettings", "getIntegrationSettings", "query", input),
112625
+ getDeclaredSettingsKeys: (input) => dispatch("addonSettings", "getDeclaredSettingsKeys", "query", input),
112626
+ unsetStoredSettingsKeys: (input) => dispatch("addonSettings", "unsetStoredSettingsKeys", "mutation", input)
111515
112627
  },
111516
112628
  addonWidgets: { listWidgets: (input) => dispatch("addonWidgets", "listWidgets", "query", input) },
111517
112629
  alerts: {
@@ -111726,6 +112838,8 @@ function createSystemProxy(api) {
111726
112838
  reloadDevice: (input) => dispatch("deviceProvider", "reloadDevice", "mutation", input),
111727
112839
  supportsDiscovery: (input) => dispatch("deviceProvider", "supportsDiscovery", "query", input),
111728
112840
  discoverDevices: (input) => dispatch("deviceProvider", "discoverDevices", "mutation", input),
112841
+ getDiscoveryModes: (input) => dispatch("deviceProvider", "getDiscoveryModes", "query", input),
112842
+ classifyDiscoveryHits: (input) => dispatch("deviceProvider", "classifyDiscoveryHits", "mutation", input),
111729
112843
  getDiscoveryParamsSchema: (input) => dispatch("deviceProvider", "getDiscoveryParamsSchema", "query", input),
111730
112844
  getManualCreationType: (input) => dispatch("deviceProvider", "getManualCreationType", "query", input),
111731
112845
  adoptDiscoveredDevice: (input) => dispatch("deviceProvider", "adoptDiscoveredDevice", "mutation", input),
@@ -111831,6 +112945,9 @@ function createSystemProxy(api) {
111831
112945
  uploadCertificate: (input) => dispatch("localNetwork", "uploadCertificate", "mutation", input),
111832
112946
  downloadCa: (input) => dispatch("localNetwork", "downloadCa", "query", input),
111833
112947
  useStoredUploadedCertificate: (input) => dispatch("localNetwork", "useStoredUploadedCertificate", "mutation", input),
112948
+ scanNetwork: (input) => dispatch("localNetwork", "scanNetwork", "mutation", input),
112949
+ getScanSubnets: (input) => dispatch("localNetwork", "getScanSubnets", "query", input),
112950
+ setScanSubnets: (input) => dispatch("localNetwork", "setScanSubnets", "mutation", input),
111834
112951
  revertToGeneratedCertificate: (input) => dispatch("localNetwork", "revertToGeneratedCertificate", "mutation", input)
111835
112952
  },
111836
112953
  meshNetwork: {
@@ -121367,12 +122484,22 @@ function rewriteBody(body, kind, currentUpstream, route) {
121367
122484
  /**
121368
122485
  * A vendor `Set-Cookie` for the browser: no `Domain` (the cookie belongs to the hub
121369
122486
  * origin), `Path` narrowed to this attempt's prefix, `Secure` kept (the hub is HTTPS).
121370
- */
121371
- function rewriteSetCookie(cookie, route) {
121372
- const kept = cookie.split(";").map((part) => part.trim()).filter((part, index) => {
122487
+ * `browserSafe` (the server keeps its own jar, so this copy only serves the page's script):
122488
+ * also no `Secure` / `SameSite`, which a plain-http hub makes the browser refuse, and no
122489
+ * `__Host-` / `__Secure-` cookie, whose rules a narrowed Path breaks.
122490
+ */
122491
+ function rewriteSetCookie(cookie, route, options = {}) {
122492
+ const parts = cookie.split(";").map((part) => part.trim());
122493
+ const dropped = options.browserSafe === true ? [
122494
+ "domain",
122495
+ "path",
122496
+ "secure",
122497
+ "samesite"
122498
+ ] : ["domain", "path"];
122499
+ const kept = parts.filter((part, index) => {
121373
122500
  if (index === 0) return true;
121374
122501
  const name = part.split("=")[0]?.trim().toLowerCase() ?? "";
121375
- return name !== "domain" && name !== "path";
122502
+ return !dropped.includes(name);
121376
122503
  });
121377
122504
  kept.push(`Path=${route.basePath}/`);
121378
122505
  return kept.join("; ");
@@ -121383,10 +122510,10 @@ function rewriteSetCookie(cookie, route) {
121383
122510
  * one, so a past-dated set that only deleted the other domain's copy would delete the
121384
122511
  * live one the same response just set: that deletion is dropped.
121385
122512
  */
121386
- function rewriteSetCookies(cookies, route) {
122513
+ function rewriteSetCookies(cookies, route, options = {}) {
121387
122514
  const now = Date.now();
121388
122515
  const liveNames = new Set(cookies.filter((c) => !isExpiredCookie(c, now)).map(cookieName));
121389
- return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).map((c) => rewriteSetCookie(c, route));
122516
+ return cookies.filter((c) => !(isExpiredCookie(c, now) && liveNames.has(cookieName(c)))).filter((c) => options.browserSafe !== true || !/^__(?:host|secure)-/i.test(cookieName(c))).map((c) => rewriteSetCookie(c, route, options));
121390
122517
  }
121391
122518
  function cookieName(cookie) {
121392
122519
  return cookie.split(";")[0]?.split("=")[0]?.trim() ?? "";
@@ -121447,6 +122574,101 @@ function filterForwardedCookies(cookieHeader) {
121447
122574
  return cookieHeader.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !part.toLowerCase().startsWith("camstack_")).join("; ");
121448
122575
  }
121449
122576
  //#endregion
122577
+ //#region src/vendor-login-proxy/cookie-jar.ts
122578
+ /**
122579
+ * Server-side cookie jar of ONE vendor sign-in attempt (D955).
122580
+ *
122581
+ * A vendor login (an identity provider's cookie/JS check page) refuses a flow that arrives
122582
+ * without the cookies it set a step earlier. Relying on the browser to store them under the
122583
+ * hub origin loses them whenever the browser refuses the rewritten cookie (a `Secure` cookie
122584
+ * on a plain-http hub, a `__Host-` name with a narrowed Path, a `SameSite=None` without
122585
+ * `Secure`). The proxy therefore keeps every vendor cookie itself and sends them upstream,
122586
+ * whatever the browser did. Values never leave this object except as the upstream `Cookie`
122587
+ * header; nothing here logs.
122588
+ */
122589
+ function attributeValue(attrs, name) {
122590
+ const found = attrs.find((a) => a.split("=")[0]?.trim().toLowerCase() === name);
122591
+ if (found === void 0) return void 0;
122592
+ const eq = found.indexOf("=");
122593
+ return eq === -1 ? "" : found.slice(eq + 1).trim();
122594
+ }
122595
+ function hasFlag(attrs, name) {
122596
+ return attrs.some((a) => a.toLowerCase() === name);
122597
+ }
122598
+ function expiryOf(attrs, nowMs) {
122599
+ const maxAge = attributeValue(attrs, "max-age");
122600
+ if (maxAge !== void 0 && maxAge.length > 0 && Number.isFinite(Number(maxAge))) return nowMs + Number(maxAge) * 1e3;
122601
+ const expires = attributeValue(attrs, "expires");
122602
+ if (expires === void 0) return null;
122603
+ const at = Date.parse(expires);
122604
+ return Number.isFinite(at) ? at : null;
122605
+ }
122606
+ function defaultPath(requestPath) {
122607
+ const last = requestPath.lastIndexOf("/");
122608
+ return last <= 0 ? "/" : requestPath.slice(0, last);
122609
+ }
122610
+ function pathMatches(cookiePath, requestPath) {
122611
+ if (requestPath === cookiePath) return true;
122612
+ if (!requestPath.startsWith(cookiePath)) return false;
122613
+ return cookiePath.endsWith("/") || requestPath.charAt(cookiePath.length) === "/";
122614
+ }
122615
+ function hostMatches(cookie, host) {
122616
+ if (cookie.domain === null) return cookie.originHost === host;
122617
+ const bare = hostWithoutPort(host);
122618
+ return bare === cookie.domain || bare.endsWith(`.${cookie.domain}`);
122619
+ }
122620
+ var VendorCookieJar = class {
122621
+ #cookies = /* @__PURE__ */ new Map();
122622
+ /** Store (or delete, when expired) the cookies of one upstream response. */
122623
+ store(setCookies, origin, nowMs) {
122624
+ for (const raw of setCookies) {
122625
+ const [first, ...rest] = raw.split(";").map((part) => part.trim());
122626
+ const eq = first?.indexOf("=") ?? -1;
122627
+ if (first === void 0 || eq <= 0) continue;
122628
+ const name = first.slice(0, eq);
122629
+ const attrs = rest;
122630
+ const domainAttr = attributeValue(attrs, "domain");
122631
+ const domain = domainAttr === void 0 || domainAttr.length === 0 ? null : domainAttr.replace(/^\./, "").toLowerCase();
122632
+ const pathAttr = attributeValue(attrs, "path");
122633
+ const path = pathAttr !== void 0 && pathAttr.startsWith("/") ? pathAttr : defaultPath(origin.pathname);
122634
+ const originBare = hostWithoutPort(origin.host);
122635
+ if (domain !== null && originBare !== domain && !originBare.endsWith(`.${domain}`)) continue;
122636
+ const expiresAtMs = expiryOf(attrs, nowMs);
122637
+ const key = `${name}|${domain ?? origin.host}|${path}`;
122638
+ if (expiresAtMs !== null && expiresAtMs <= nowMs) {
122639
+ this.#cookies.delete(key);
122640
+ continue;
122641
+ }
122642
+ this.#cookies.set(key, {
122643
+ name,
122644
+ value: first.slice(eq + 1),
122645
+ originHost: origin.host,
122646
+ domain,
122647
+ path,
122648
+ secure: hasFlag(attrs, "secure"),
122649
+ expiresAtMs
122650
+ });
122651
+ }
122652
+ }
122653
+ /** The `Cookie` header value for a request to `host` + `pathname`, or null when none applies. */
122654
+ headerFor(target, nowMs) {
122655
+ const secureOk = target.scheme === "https" || isLoopbackHost(target.host);
122656
+ const matching = [...this.#cookies.values()].filter((c) => (c.expiresAtMs === null || c.expiresAtMs > nowMs) && (!c.secure || secureOk) && hostMatches(c, target.host) && pathMatches(c.path, target.pathname)).sort((a, b) => b.path.length - a.path.length);
122657
+ if (matching.length === 0) return null;
122658
+ return matching.map((c) => `${c.name}=${c.value}`).join("; ");
122659
+ }
122660
+ };
122661
+ /** Cookie names carried by a `Cookie` header value. */
122662
+ function cookieHeaderNames(header) {
122663
+ return new Set(header.split(";").map((part) => part.split("=")[0]?.trim() ?? "").filter((name) => name.length > 0));
122664
+ }
122665
+ /** `browser` cookies not shadowed by the jar's, then the jar's: the jar is the truth. */
122666
+ function mergeCookieHeaders(browser, jar) {
122667
+ if (jar === null) return browser;
122668
+ const shadowed = cookieHeaderNames(jar);
122669
+ return [...browser.split(";").map((part) => part.trim()).filter((part) => part.length > 0 && !shadowed.has(part.split("=")[0]?.trim() ?? "")), jar].join("; ");
122670
+ }
122671
+ //#endregion
121450
122672
  //#region src/vendor-login-proxy/flow-trace.ts
121451
122673
  /** A short stable label for the attempt: enough to group lines, useless as a capability. */
121452
122674
  function attemptLabel(attemptId) {
@@ -121645,6 +122867,7 @@ var VendorLoginProxy = class {
121645
122867
  this.#attempts.set(spec.attemptId, {
121646
122868
  expiresAtMs: spec.expiresAtMs,
121647
122869
  upstreams: spec.upstreams,
122870
+ jar: spec.serverCookieJar === true ? new VendorCookieJar() : null,
121648
122871
  evaluate: async (view) => {
121649
122872
  const verdict = await spec.terminal(view);
121650
122873
  if (verdict.kind === "continue") return verdict;
@@ -121741,7 +122964,7 @@ var VendorLoginProxy = class {
121741
122964
  body = Buffer.from(rewriteFormBody(formBodyRaw, route), "utf8");
121742
122965
  } else body = read;
121743
122966
  }
121744
- const headers = this.#requestHeaders(req, route, upstream);
122967
+ const headers = this.#requestHeaders(req, route, upstream, attempt.jar, target);
121745
122968
  const upstreamRes = await this.#fetch(target, {
121746
122969
  method,
121747
122970
  headers,
@@ -121750,6 +122973,10 @@ var VendorLoginProxy = class {
121750
122973
  });
121751
122974
  const location = upstreamRes.headers.get("location");
121752
122975
  const setCookies = upstreamRes.headers.getSetCookie();
122976
+ attempt.jar?.store(setCookies, {
122977
+ host: upstream.host,
122978
+ pathname: target.pathname
122979
+ }, this.#now());
121753
122980
  const kind = bodyKindOf(upstreamRes.headers.get("content-type") ?? "");
121754
122981
  const declaredLength = Number(upstreamRes.headers.get("content-length") ?? "0");
121755
122982
  const text = kind !== null && !(declaredLength > MAX_TEXT_BODY_BYTES) && method !== "HEAD" ? await upstreamRes.text() : null;
@@ -121797,7 +123024,7 @@ var VendorLoginProxy = class {
121797
123024
  upstreamRes.headers.forEach((value, name) => {
121798
123025
  if (!DROP_RESPONSE_HEADERS.has(name.toLowerCase())) out[name] = value;
121799
123026
  });
121800
- if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route);
123027
+ if (setCookies.length > 0) out["set-cookie"] = rewriteSetCookies(setCookies, route, { browserSafe: attempt.jar !== null });
121801
123028
  if (location !== null) out["location"] = rewriteLocation(location, upstream, route);
121802
123029
  out["cache-control"] = "no-store";
121803
123030
  if (text !== null && kind !== null) {
@@ -121819,15 +123046,19 @@ var VendorLoginProxy = class {
121819
123046
  ...flowTraceMeta(input)
121820
123047
  } });
121821
123048
  }
121822
- #requestHeaders(req, route, upstream) {
123049
+ #requestHeaders(req, route, upstream, jar, target) {
121823
123050
  const headers = {};
121824
123051
  for (const [name, value] of Object.entries(req.headers)) {
121825
123052
  const lower = name.toLowerCase();
121826
123053
  if (DROP_REQUEST_HEADERS.has(lower) || value === void 0) continue;
121827
123054
  headers[lower] = Array.isArray(value) ? value.join(", ") : value;
121828
123055
  }
121829
- const cookie = filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? "");
121830
- if (cookie.length > 0) headers["cookie"] = cookie;
123056
+ const merged = mergeCookieHeaders(filterForwardedCookies(firstHeader(req.headers["cookie"]) ?? ""), jar?.headerFor({
123057
+ host: upstream.host,
123058
+ pathname: target.pathname,
123059
+ scheme: upstream.scheme
123060
+ }, this.#now()) ?? null);
123061
+ if (merged.length > 0) headers["cookie"] = merged;
121831
123062
  if (req.headers["origin"] !== void 0) headers["origin"] = `${upstream.scheme}://${upstream.host}`;
121832
123063
  const referer = firstHeader(req.headers["referer"]);
121833
123064
  if (referer !== void 0) {
@@ -123159,7 +124390,6 @@ exports.AutoUpdateSettingsSchema = AutoUpdateSettingsSchema;
123159
124390
  exports.AutomationActionSchema = AutomationActionSchema;
123160
124391
  exports.AutomationCapStepSchema = AutomationCapStepSchema;
123161
124392
  exports.AutomationConditionOperatorSchema = AutomationConditionOperatorSchema;
123162
- exports.AutomationConditionSchema = AutomationConditionSchema;
123163
124393
  exports.AutomationConditionTraceNodeSchema = AutomationConditionTraceNodeSchema;
123164
124394
  exports.AutomationControlStatusSchema = AutomationControlStatusSchema;
123165
124395
  exports.AutomationRecipeSchema = AutomationRecipeSchema;
@@ -123169,9 +124399,9 @@ exports.AutomationRunTraceSchema = AutomationRunTraceSchema;
123169
124399
  exports.AutomationRunVerdictSchema = AutomationRunVerdictSchema;
123170
124400
  exports.AutomationStateDocSchema = AutomationStateDocSchema;
123171
124401
  exports.AutomationStepTraceSchema = AutomationStepTraceSchema;
123172
- exports.AutomationTriggerSchema = AutomationTriggerSchema;
123173
124402
  exports.AutomationTriggerTraceSchema = AutomationTriggerTraceSchema;
123174
124403
  exports.AutomationWaitStepSchema = AutomationWaitStepSchema;
124404
+ exports.AutomationWhenSchema = AutomationWhenSchema;
123175
124405
  exports.AvailableIntegrationTypeSchema = AvailableIntegrationTypeSchema;
123176
124406
  exports.BASE_DEVICE_CAPS = BASE_DEVICE_CAPS;
123177
124407
  exports.BASE_LIVE_EGRESS_PROFILE = BASE_LIVE_EGRESS_PROFILE;
@@ -123472,6 +124702,7 @@ exports.CreateIntegrationInputSchema = CreateIntegrationInputSchema;
123472
124702
  exports.CreateUserInputSchema = CreateUserInputSchema;
123473
124703
  exports.CredentialScopeSchema = CredentialScopeSchema;
123474
124704
  exports.CredentialTokenClaimsSchema = CredentialTokenClaimsSchema;
124705
+ exports.CurrentAutomationRecipeSchema = CurrentAutomationRecipeSchema;
123475
124706
  exports.CustomActionInputSchema = CustomActionInputSchema;
123476
124707
  exports.CustomModelDescriptorSchema = CustomModelDescriptorSchema;
123477
124708
  exports.CustomizeAssignmentStatusSchema = CustomizeAssignmentStatusSchema;
@@ -123524,6 +124755,7 @@ exports.DETAIL_CROP_PADDING_FIELD = DETAIL_CROP_PADDING_FIELD;
123524
124755
  exports.DETAIL_CROP_PADDING_KEY = DETAIL_CROP_PADDING_KEY;
123525
124756
  exports.DETAIL_CROP_SECTION_ID = DETAIL_CROP_SECTION_ID;
123526
124757
  exports.DETAIL_CROP_SQUARE_KEY = DETAIL_CROP_SQUARE_KEY;
124758
+ exports.DETECTION_LABEL_SOURCES = DETECTION_LABEL_SOURCES;
123527
124759
  exports.DETECTION_MACRO_CLASSES = DETECTION_MACRO_CLASSES;
123528
124760
  exports.DETECTION_PIPELINE_CAP_NAME = DETECTION_PIPELINE_CAP_NAME;
123529
124761
  exports.DETECTOR_ACCURACY = DETECTOR_ACCURACY;
@@ -123587,6 +124819,7 @@ exports.DetailCropConventionSchema = DetailCropConventionSchema;
123587
124819
  exports.DetectionCatalogClassMapSchema = DetectionCatalogClassMapSchema;
123588
124820
  exports.DetectionSourceSchema = DetectionSourceSchema;
123589
124821
  exports.DetectionSourcesSchema = DetectionSourcesSchema;
124822
+ exports.DetectionSubjectSchema = DetectionSubjectSchema;
123590
124823
  exports.DetectionTriggerFilter = DetectionTriggerFilter;
123591
124824
  exports.DeviceAdminLinkSchema = DeviceAdminLinkSchema;
123592
124825
  exports.DeviceCaStatusSchema = DeviceCaStatusSchema;
@@ -123638,6 +124871,8 @@ exports.DiscoveredChildDeviceSchema = DiscoveredChildDeviceSchema;
123638
124871
  exports.DiscoveredChildStatusSchema = DiscoveredChildStatusSchema;
123639
124872
  exports.DiscoveredDeviceSchema = DiscoveredDeviceSchema;
123640
124873
  exports.DiscoveredTargetSchema = DiscoveredTargetSchema;
124874
+ exports.DiscoveryHitSchema = DiscoveryHitSchema;
124875
+ exports.DiscoveryModeSchema = DiscoveryModeSchema;
123641
124876
  exports.DishwasherAccessSchema = DishwasherAccessSchema;
123642
124877
  exports.DishwasherAvailabilitySchema = DishwasherAvailabilitySchema;
123643
124878
  exports.DishwasherOperationStateSchema = DishwasherOperationStateSchema;
@@ -123935,7 +125170,10 @@ exports.LedgerWalkRefusalSchema = LedgerWalkRefusalSchema;
123935
125170
  exports.LedgerWalkReportSchema = LedgerWalkReportSchema;
123936
125171
  exports.LedgerWalkSkipCountsSchema = LedgerWalkSkipCountsSchema;
123937
125172
  exports.LedgerWalkSkipReasonSchema = LedgerWalkSkipReasonSchema;
125173
+ exports.LegacyAutomationRecipeSchema = LegacyAutomationRecipeSchema;
125174
+ exports.LegacyConditionSchema = LegacyConditionSchema;
123938
125175
  exports.LegacyStorageLocationDefaultSchema = LegacyStorageLocationDefaultSchema;
125176
+ exports.LegacyTriggerSchema = LegacyTriggerSchema;
123939
125177
  exports.LinkedDeviceSchema = LinkedDeviceSchema;
123940
125178
  exports.LinkedDevicesModeSchema = LinkedDevicesModeSchema;
123941
125179
  exports.LinkedDevicesSlotValueSchema = LinkedDevicesSlotValueSchema;
@@ -124038,10 +125276,14 @@ exports.MAX_REMOTE_BINDINGS = MAX_REMOTE_BINDINGS;
124038
125276
  exports.MAX_REMOTE_CUSTOM_BUTTONS = MAX_REMOTE_CUSTOM_BUTTONS;
124039
125277
  exports.MAX_REQUEUED_BUCKETS_PER_SERIES = MAX_REQUEUED_BUCKETS_PER_SERIES;
124040
125278
  exports.MAX_SENSOR_TRIGGER_DEVICES = MAX_SENSOR_TRIGGER_DEVICES;
125279
+ exports.MAX_TRACE_ARGS = MAX_TRACE_ARGS;
125280
+ exports.MAX_TRACE_ARG_LENGTH = MAX_TRACE_ARG_LENGTH;
124041
125281
  exports.MAX_TRACE_KEEP = MAX_TRACE_KEEP;
124042
125282
  exports.MAX_TRACK_DEBUG_NOTE_LEN = MAX_TRACK_DEBUG_NOTE_LEN;
124043
125283
  exports.MAX_TRACK_PURITIES = MAX_TRACK_PURITIES;
124044
125284
  exports.MAX_TRACK_RELATIONS = MAX_TRACK_RELATIONS;
125285
+ exports.MAX_WHEN_DEPTH = MAX_WHEN_DEPTH;
125286
+ exports.MAX_WHEN_LEAVES = MAX_WHEN_LEAVES;
124045
125287
  exports.MEDIA_OWNER_TYPES = MEDIA_OWNER_TYPES;
124046
125288
  exports.MEDIA_PRESENCE_OWNER_KINDS = MEDIA_PRESENCE_OWNER_KINDS;
124047
125289
  exports.METHOD_ACCESS_MAP = METHOD_ACCESS_MAP;
@@ -124597,6 +125839,7 @@ exports.RECOVERY_MIN_INTERVAL_MS = RECOVERY_MIN_INTERVAL_MS;
124597
125839
  exports.RECOVERY_MIN_UNCOVERED_FRACTION = RECOVERY_MIN_UNCOVERED_FRACTION;
124598
125840
  exports.RECOVERY_MOTION_MAX_AGE_MS = RECOVERY_MOTION_MAX_AGE_MS;
124599
125841
  exports.RECOVERY_VIEW_PX = RECOVERY_VIEW_PX;
125842
+ exports.REDACTED_ARG = REDACTED_ARG;
124600
125843
  exports.REDACTED_SECRET = require_sleep.REDACTED_SECRET;
124601
125844
  exports.REFERENCE_COMPUTE = REFERENCE_COMPUTE;
124602
125845
  exports.REFERENCE_MATCH_FRACTION = REFERENCE_MATCH_FRACTION;
@@ -124921,10 +126164,14 @@ exports.SnapshotImageSchema = SnapshotImageSchema;
124921
126164
  exports.SourceInfoSchema = SourceInfoSchema;
124922
126165
  exports.SpatialDetectionSchema = SpatialDetectionSchema;
124923
126166
  exports.SsoBridgeClaimsSchema = SsoBridgeClaimsSchema;
126167
+ exports.StaleDataReportSchema = StaleDataReportSchema;
124924
126168
  exports.StaleDevicePinSchema = StaleDevicePinSchema;
126169
+ exports.StaleKindFindingSchema = StaleKindFindingSchema;
126170
+ exports.StaleSettingsKeyFindingSchema = StaleSettingsKeyFindingSchema;
124925
126171
  exports.StartEmbeddedInputSchema = StartEmbeddedInputSchema;
124926
126172
  exports.StartEnrollmentOutputSchema = StartEnrollmentOutputSchema;
124927
126173
  exports.StationaryObjectSchema = StationaryObjectSchema;
126174
+ exports.StationarySubjectSchema = StationarySubjectSchema;
124928
126175
  exports.StatisticHelperSpecSchema = require_composition.StatisticHelperSpecSchema;
124929
126176
  exports.StatisticWindowSchema = require_composition.StatisticWindowSchema;
124930
126177
  exports.StorageAbortUploadInputSchema = AbortUploadInputSchema;
@@ -125233,6 +126480,7 @@ exports.allDoActions = allDoActions;
125233
126480
  exports.allowedSuppliedEffect = allowedSuppliedEffect;
125234
126481
  exports.ambientLightSensorCapability = ambientLightSensorCapability;
125235
126482
  exports.annotateClipsWithEventLabels = annotateClipsWithEventLabels;
126483
+ exports.anySubjectMatches = anySubjectMatches;
125236
126484
  exports.appFlowLocation = appFlowLocation;
125237
126485
  exports.appendTrace = appendTrace;
125238
126486
  exports.applianceDeviceType = require_composition.applianceDeviceType;
@@ -125300,6 +126548,7 @@ exports.buildPlusCatalogClassMap = buildPlusCatalogClassMap;
125300
126548
  exports.buildRoleScopes = buildRoleScopes;
125301
126549
  exports.buildSecretRef = require_sleep.buildSecretRef;
125302
126550
  exports.buildSessionLostAlert = buildSessionLostAlert;
126551
+ exports.buildStepArgsTrace = buildStepArgsTrace;
125303
126552
  exports.buildStreamParamsConfigSchema = buildStreamParamsConfigSchema;
125304
126553
  exports.buildSubjectPath = buildSubjectPath;
125305
126554
  exports.buildTriggerTrace = buildTriggerTrace;
@@ -125365,6 +126614,7 @@ exports.collectModelBoundKeys = collectModelBoundKeys;
125365
126614
  exports.collectSecretConfigKeys = require_sleep.collectSecretConfigKeys;
125366
126615
  exports.collectSecretLeaves = require_sleep.collectSecretLeaves;
125367
126616
  exports.collectSecretMarkerRefs = require_sleep.collectSecretMarkerRefs;
126617
+ exports.collectWhenMirrorSources = collectWhenMirrorSources;
125368
126618
  exports.collectZodSecretPaths = require_sleep.collectZodSecretPaths;
125369
126619
  exports.colorCapability = colorCapability;
125370
126620
  exports.colorForKind = colorForKind;
@@ -125394,7 +126644,6 @@ exports.compositionSourceKey = require_composition.compositionSourceKey;
125394
126644
  exports.compositionSourceRefs = compositionSourceRefs;
125395
126645
  exports.computeCapacityShares = computeCapacityShares;
125396
126646
  exports.computeScores = computeScores;
125397
- exports.conditionDepth = conditionDepth;
125398
126647
  exports.conditionExclusionReason = conditionExclusionReason;
125399
126648
  exports.conditionVisibleForKind = conditionVisibleForKind;
125400
126649
  exports.connectionTestCapability = connectionTestCapability;
@@ -125406,6 +126655,9 @@ exports.consumablesCapability = consumablesCapability;
125406
126655
  exports.contactCapability = contactCapability;
125407
126656
  exports.containingParentIndex = containingParentIndex;
125408
126657
  exports.controlCapability = controlCapability;
126658
+ exports.convertLegacyCondition = convertLegacyCondition;
126659
+ exports.convertLegacyRecipe = convertLegacyRecipe;
126660
+ exports.convertLegacyWhen = convertLegacyWhen;
125409
126661
  exports.convertPresetFeature = convertPresetFeature;
125410
126662
  exports.convertQuantityInput = convertQuantityInput;
125411
126663
  exports.convertUnit = convertUnit;
@@ -125417,8 +126669,8 @@ exports.coreBlockIdFromAddonId = coreBlockIdFromAddonId;
125417
126669
  exports.coreBlockSourceKind = coreBlockSourceKind;
125418
126670
  exports.coreBlocksCapability = coreBlocksCapability;
125419
126671
  exports.cosineSimilarity = cosineSimilarity;
125420
- exports.countConditionLeaves = countConditionLeaves;
125421
126672
  exports.countRecoverySkips = countRecoverySkips;
126673
+ exports.countWhenLeaves = countWhenLeaves;
125422
126674
  exports.coverCapability = coverCapability;
125423
126675
  exports.createAlarmProfile = createAlarmProfile;
125424
126676
  exports.createDeviceExtensionProvider = createDeviceExtensionProvider;
@@ -125484,6 +126736,7 @@ exports.describeOnboardRefusal = describeOnboardRefusal;
125484
126736
  exports.describeRootSupplies = describeRootSupplies;
125485
126737
  exports.describeRootSupply = describeRootSupply;
125486
126738
  exports.describeStateField = describeStateField;
126739
+ exports.describeSubject = describeSubject;
125487
126740
  exports.describeSystemEventDeliveryProblem = describeSystemEventDeliveryProblem;
125488
126741
  exports.describeSystemEventFamilyProblem = describeSystemEventFamilyProblem;
125489
126742
  exports.describeTopLevelStateFields = describeTopLevelStateFields;
@@ -125560,6 +126813,7 @@ exports.evaluateSensorLevelEdge = require_composition.evaluateSensorLevelEdge;
125560
126813
  exports.evaluateStatefulExpressionCall = require_composition.evaluateStatefulExpressionCall;
125561
126814
  exports.evaluateStationaryTrigger = evaluateStationaryTrigger;
125562
126815
  exports.evaluateStatistic = require_composition.evaluateStatistic;
126816
+ exports.evaluateWhenTree = evaluateWhenTree;
125563
126817
  exports.evaluateZoneRules = evaluateZoneRules;
125564
126818
  exports.event = require_sleep.event;
125565
126819
  exports.eventEmitterCapability = eventEmitterCapability;
@@ -125602,6 +126856,7 @@ exports.findCompositionCycles = findCompositionCycles;
125602
126856
  exports.findTimezone = findTimezone;
125603
126857
  exports.floodCapability = floodCapability;
125604
126858
  exports.foldSnapshotByFunction = foldSnapshotByFunction;
126859
+ exports.forEachLeaf = forEachLeaf;
125605
126860
  exports.foreignCapacityCells = foreignCapacityCells;
125606
126861
  exports.forgetKnownSecret = require_sleep.forgetKnownSecret;
125607
126862
  exports.formatDeviceRevoker = formatDeviceRevoker;
@@ -125635,6 +126890,7 @@ exports.groupActiveCapability = groupActiveCapability;
125635
126890
  exports.groupCommandsCapability = groupCommandsCapability;
125636
126891
  exports.groupStatusCapability = groupStatusCapability;
125637
126892
  exports.groupTemplate = groupTemplate;
126893
+ exports.hasEventLeaves = hasEventLeaves;
125638
126894
  exports.hasModelBoundOverrides = hasModelBoundOverrides;
125639
126895
  exports.hasMotionTrigger = hasMotionTrigger;
125640
126896
  exports.hasUrlScheme = hasUrlScheme;
@@ -125705,6 +126961,7 @@ exports.isDrawableSubjectPath = isDrawableSubjectPath;
125705
126961
  exports.isEmbeddingModelFailed = isEmbeddingModelFailed;
125706
126962
  exports.isEvent = require_sleep.isEvent;
125707
126963
  exports.isEventKindLabelKey = isEventKindLabelKey;
126964
+ exports.isEventLeaf = isEventLeaf;
125708
126965
  exports.isEventOwnerType = isEventOwnerType;
125709
126966
  exports.isFirstLevelMacroClass = isFirstLevelMacroClass;
125710
126967
  exports.isForwardInputRef = require_composition.isForwardInputRef;
@@ -125716,6 +126973,7 @@ exports.isIsolatedBuiltin = isIsolatedBuiltin;
125716
126973
  exports.isKeychainStoredField = require_sleep.isKeychainStoredField;
125717
126974
  exports.isKnownTemplateVar = isKnownTemplateVar;
125718
126975
  exports.isLabelSupplyStepId = isLabelSupplyStepId;
126976
+ exports.isLeaf = isLeaf;
125719
126977
  exports.isLocationEnabled = isLocationEnabled;
125720
126978
  exports.isMediaOwnerType = isMediaOwnerType;
125721
126979
  exports.isModelScopedStepSettingKey = isModelScopedStepSettingKey;
@@ -126220,8 +127478,11 @@ exports.streamQualityLabel = streamQualityLabel;
126220
127478
  exports.streamSignalsCapability = streamSignalsCapability;
126221
127479
  exports.stripRetiredBandPreBufferSec = stripRetiredBandPreBufferSec;
126222
127480
  exports.strippedOfLegacyReadOnly = strippedOfLegacyReadOnly;
127481
+ exports.subClassesOf = subClassesOf;
126223
127482
  exports.subKindsOf = subKindsOf;
127483
+ exports.subjectFromClassName = subjectFromClassName;
126224
127484
  exports.subjectHasPath = subjectHasPath;
127485
+ exports.subjectMatches = subjectMatches;
126225
127486
  exports.subjectPathSampleFromBox = subjectPathSampleFromBox;
126226
127487
  exports.subjectPathSvgPoints = subjectPathSvgPoints;
126227
127488
  exports.summarisePrivacyAudio = summarisePrivacyAudio;
@@ -126303,6 +127564,7 @@ exports.validateCustomAddress = validateCustomAddress;
126303
127564
  exports.validateExpressionSource = require_composition.validateExpressionSource;
126304
127565
  exports.validatePalette = validatePalette;
126305
127566
  exports.validateRecipeBounds = validateRecipeBounds;
127567
+ exports.validateWhenTree = validateWhenTree;
126306
127568
  exports.valveCapability = valveCapability;
126307
127569
  exports.vectorDimFromBase64 = vectorDimFromBase64;
126308
127570
  exports.vectorIndexDimConflictError = vectorIndexDimConflictError;
@@ -126324,6 +127586,7 @@ exports.webrtcSessionCapability = webrtcSessionCapability;
126324
127586
  exports.weeklyMaskIndex = weeklyMaskIndex;
126325
127587
  exports.weeklyMaskIsUniform = weeklyMaskIsUniform;
126326
127588
  exports.weeklyMaskToWindows = weeklyMaskToWindows;
127589
+ exports.whenDepth = whenDepth;
126327
127590
  exports.widenDeviceConstructor = widenDeviceConstructor;
126328
127591
  exports.windowsToWeeklyMask = windowsToWeeklyMask;
126329
127592
  exports.wiringAddonHealthSchema = wiringAddonHealthSchema;