@camstack/types 1.2.432 → 1.2.434

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_sleep = require("./sleep-5zRztaHa.js");
3
- const require_event_category = require("./event-category-BhzwWQCH.js");
2
+ const require_sleep = require("./sleep-CO61AfwW.js");
3
+ const require_event_category = require("./event-category-E17zaHjN.js");
4
4
  const require_composition = require("./composition-CpAc0YfQ.js");
5
5
  const require_node_color = require("./node-color.js");
6
6
  const require_err_msg = require("./err-msg-U7lLUMNz.js");
@@ -8074,10 +8074,10 @@ var ExpressionSourceSchema = zod.z.object({
8074
8074
  *
8075
8075
  * Three vocabularies, each salvaged rather than invented:
8076
8076
  *
8077
- * - **Triggers** — the only genuinely new surface. Six members, none of which
8077
+ * - **Triggers** — the only genuinely new surface. Seven members, none of which
8078
8078
  * touches notification delivery. The NC keeps its four *pipeline* triggers;
8079
8079
  * these are device/time triggers and the two sets do not overlap.
8080
- * - **Conditions** — `ALL`/`ANY`/`NOT` over two leaf kinds, bounded. The
8080
+ * - **Conditions** — `ALL`/`ANY`/`NOT` over three leaf kinds, bounded. The
8081
8081
  * expression leaf is the bounded engine at `../expression/`, which is where
8082
8082
  * arithmetic between two device fields lives and the only place it lives.
8083
8083
  * - **Actions** — `NcRuleActionSchema`'s two steps, plus the `{kind:'code'}`
@@ -8124,6 +8124,14 @@ var AutomationConditionLeafSchema = zod.z.object({
8124
8124
  zod.z.array(zod.z.union([zod.z.string(), zod.z.number()]))
8125
8125
  ])
8126
8126
  });
8127
+ var AutomationConditionStationaryPresentSchema = zod.z.object({
8128
+ kind: zod.z.literal("stationary-present"),
8129
+ deviceId: zod.z.number().int().nonnegative(),
8130
+ /** Class names; a macro class matches every raw class that maps to it. */
8131
+ classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8132
+ /** The parked object must be in at least one of these zones. */
8133
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional()
8134
+ });
8127
8135
  /**
8128
8136
  * The expression leaf, declared as a plain object rather than an intersection
8129
8137
  * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
@@ -8157,7 +8165,8 @@ var AutomationConditionSchema = zod.z.lazy(() => zod.z.discriminatedUnion("kind"
8157
8165
  child: AutomationConditionSchema
8158
8166
  }),
8159
8167
  AutomationConditionLeafSchema,
8160
- AutomationConditionExpressionSchema
8168
+ AutomationConditionExpressionSchema,
8169
+ AutomationConditionStationaryPresentSchema
8161
8170
  ]));
8162
8171
  /**
8163
8172
  * What starts a run.
@@ -8211,6 +8220,17 @@ var AutomationTriggerSchema = zod.z.discriminatedUnion("kind", [
8211
8220
  /** Best-so-far confidence the track must reach. */
8212
8221
  minScore: zod.z.number().min(0).max(1).optional()
8213
8222
  }),
8223
+ zod.z.object({
8224
+ kind: zod.z.literal("stationary"),
8225
+ deviceIds: zod.z.array(zod.z.number().int().nonnegative()).min(1),
8226
+ /** Class names; a macro class matches like the detection trigger. */
8227
+ classes: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8228
+ /** The parked object must be in at least one of these zones. */
8229
+ zones: zod.z.array(zod.z.string().min(1)).min(1).optional(),
8230
+ /** `appeared`: it became parked. `departed`: the registry retired it.
8231
+ * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
8232
+ phase: zod.z.enum(["appeared", "departed"])
8233
+ }),
8214
8234
  zod.z.object({
8215
8235
  kind: zod.z.literal("interval"),
8216
8236
  minutes: zod.z.number().int().min(1).max(1440)
@@ -8323,9 +8343,12 @@ var AutomationRecipeSchema = zod.z.object({
8323
8343
  /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
8324
8344
  keepTraces: zod.z.number().int().min(1).max(100).optional()
8325
8345
  });
8346
+ function isLeaf(cond) {
8347
+ return cond.kind === "condition" || cond.kind === "expression" || cond.kind === "stationary-present";
8348
+ }
8326
8349
  /** Depth of a tree, root container = 1. A bare leaf is depth 1. */
8327
8350
  function conditionDepth(cond) {
8328
- if (cond.kind === "condition" || cond.kind === "expression") return 1;
8351
+ if (isLeaf(cond)) return 1;
8329
8352
  if (cond.kind === "not") return 1 + conditionDepth(cond.child);
8330
8353
  const children = cond.children;
8331
8354
  if (children.length === 0) return 1;
@@ -8338,14 +8361,14 @@ function conditionDepth(cond) {
8338
8361
  }
8339
8362
  /** Leaves only — containers do not count toward the leaf budget. */
8340
8363
  function countConditionLeaves(cond) {
8341
- if (cond.kind === "condition" || cond.kind === "expression") return 1;
8364
+ if (isLeaf(cond)) return 1;
8342
8365
  if (cond.kind === "not") return countConditionLeaves(cond.child);
8343
8366
  let total = 0;
8344
8367
  for (const child of cond.children) total += countConditionLeaves(child);
8345
8368
  return total;
8346
8369
  }
8347
8370
  function findEmptyContainer(cond) {
8348
- if (cond.kind === "condition" || cond.kind === "expression") return false;
8371
+ if (isLeaf(cond)) return false;
8349
8372
  if (cond.kind === "not") return findEmptyContainer(cond.child);
8350
8373
  if (cond.children.length === 0) return true;
8351
8374
  return cond.children.some(findEmptyContainer);
@@ -8504,6 +8527,32 @@ function emitTrigger(trigger, index) {
8504
8527
  },
8505
8528
  ),
8506
8529
  )
8530
+ }`;
8531
+ case "stationary": return ` // trigger ${index}: stationary — a parked object appeared / departed. Stateless:
8532
+ // the registry emits one event per transition. A dropped event only misses a fire;
8533
+ // the stationary-present CONDITION reads the registry, not this event.
8534
+ {
8535
+ const trigger = ${j(trigger)} as const
8536
+ disposers.push(
8537
+ ctx.eventBus.subscribe(
8538
+ { category: EventCategory.PipelineAnalyticsStationaryChanged },
8539
+ (event) => {
8540
+ const decision = evaluateStationaryTrigger(trigger, event.data)
8541
+ if (decision.kind === 'ignore') return
8542
+ if (decision.kind === 'reject') {
8543
+ ctx.logger.debug('stationary trigger rejected an event', {
8544
+ tags: { deviceId: decision.vars.deviceId },
8545
+ reason: decision.reason,
8546
+ })
8547
+ }
8548
+ void run(
8549
+ { kind: 'stationary', at: Date.now(), ...decision.vars },
8550
+ decision.reason,
8551
+ decision.kind === 'fire',
8552
+ )
8553
+ },
8554
+ ),
8555
+ )
8507
8556
  }`;
8508
8557
  }
8509
8558
  }
@@ -8570,6 +8619,7 @@ import {
8570
8619
  collectMirrorSources,
8571
8620
  coreBlockIdFromAddonId,
8572
8621
  evaluateConditionTree,
8622
+ evaluateStationaryTrigger,
8573
8623
  isSkippedTrace,
8574
8624
  mergeSkipTraces,
8575
8625
  resolveDeviceTarget,
@@ -9064,310 +9114,6 @@ ${recipe.triggers.map((t, i) => emitTrigger(t, i)).join("\n\n")}
9064
9114
  `;
9065
9115
  }
9066
9116
  //#endregion
9067
- //#region src/automation/condition-eval.ts
9068
- /**
9069
- * Evaluating an automation's condition tree against the ConditionMirror.
9070
- *
9071
- * D49: a trigger is a one-shot event, so its gate must never hang on a read
9072
- * that can fail. Every field a condition leaf names is therefore kept in
9073
- * memory — filled at start, updated off device state-changed events, reconciled
9074
- * on a timer — and evaluation reads that memory ONLY. A value the mirror does
9075
- * not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
9076
- * `pass`. A failed refresh changes nothing: the previous value stays.
9077
- *
9078
- * The evaluator returns a node per tree position (containers, leaves, and the
9079
- * ones a short-circuit never reached) with a stable path and a human reason, so
9080
- * the editor can paint each block.
9081
- */
9082
- function collectInto(cond, out) {
9083
- switch (cond.kind) {
9084
- case "all":
9085
- case "any":
9086
- for (const child of cond.children) collectInto(child, out);
9087
- return;
9088
- case "not":
9089
- collectInto(cond.child, out);
9090
- return;
9091
- case "condition":
9092
- out.push({
9093
- kind: "device",
9094
- deviceId: cond.deviceId,
9095
- cap: cond.cap
9096
- });
9097
- return;
9098
- case "expression":
9099
- for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
9100
- kind: "stable",
9101
- stableId: binding.sourceStableId,
9102
- cap: binding.cap
9103
- });
9104
- return;
9105
- }
9106
- }
9107
- /** Every (device, cap) a tree reads, deduplicated. */
9108
- function collectMirrorSources(cond) {
9109
- if (cond === void 0) return [];
9110
- const all = [];
9111
- collectInto(cond, all);
9112
- const seen = /* @__PURE__ */ new Set();
9113
- return all.filter((s) => {
9114
- const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
9115
- if (seen.has(key)) return false;
9116
- seen.add(key);
9117
- return true;
9118
- });
9119
- }
9120
- function sliceKey(deviceId, cap) {
9121
- return `${deviceId}|${cap}`;
9122
- }
9123
- function pick$1(slice, fieldPath) {
9124
- let cursor = slice;
9125
- for (const part of fieldPath.split(".")) {
9126
- if (cursor === null || typeof cursor !== "object") return void 0;
9127
- cursor = Reflect.get(cursor, part);
9128
- }
9129
- return cursor;
9130
- }
9131
- function isSliceObject(value) {
9132
- return typeof value === "object" && value !== null;
9133
- }
9134
- var ConditionMirror = class {
9135
- sources;
9136
- slices = /* @__PURE__ */ new Map();
9137
- stableIds = /* @__PURE__ */ new Map();
9138
- watched;
9139
- constructor(sources) {
9140
- this.sources = sources;
9141
- this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
9142
- }
9143
- /** A `device.state-changed` event. Slices nobody reads are ignored. */
9144
- apply(deviceId, cap, slice) {
9145
- const key = sliceKey(deviceId, cap);
9146
- if (!isSliceObject(slice)) return;
9147
- if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
9148
- this.slices.set(key, slice);
9149
- }
9150
- isStableTarget(deviceId, cap) {
9151
- for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
9152
- return false;
9153
- }
9154
- read(deviceId, cap, fieldPath) {
9155
- const slice = this.slices.get(sliceKey(deviceId, cap));
9156
- return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
9157
- }
9158
- readStable(stableId, cap, fieldPath) {
9159
- const deviceId = this.stableIds.get(stableId);
9160
- return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
9161
- }
9162
- /** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
9163
- async refresh(fetchers) {
9164
- let updated = 0;
9165
- let kept = 0;
9166
- if (this.sources.some((s) => s.kind === "stable")) try {
9167
- this.stableIds = await fetchers.fetchStableIds();
9168
- } catch {}
9169
- const targets = /* @__PURE__ */ new Map();
9170
- for (const s of this.sources) {
9171
- const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
9172
- if (deviceId === void 0) {
9173
- kept += 1;
9174
- continue;
9175
- }
9176
- targets.set(sliceKey(deviceId, s.cap), {
9177
- deviceId,
9178
- cap: s.cap
9179
- });
9180
- }
9181
- for (const [key, target] of targets) try {
9182
- const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
9183
- if (isSliceObject(slice)) {
9184
- this.slices.set(key, slice);
9185
- updated += 1;
9186
- } else kept += 1;
9187
- } catch {
9188
- kept += 1;
9189
- }
9190
- return {
9191
- updated,
9192
- kept
9193
- };
9194
- }
9195
- };
9196
- var OPERATOR_SYMBOL = {
9197
- eq: "=",
9198
- ne: "≠",
9199
- gt: ">",
9200
- gte: "≥",
9201
- lt: "<",
9202
- lte: "≤",
9203
- contains: "contains",
9204
- in: "in"
9205
- };
9206
- function compare(actual, operator, expected) {
9207
- switch (operator) {
9208
- case "eq": return actual === expected;
9209
- case "ne": return actual !== expected;
9210
- case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
9211
- case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
9212
- case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
9213
- case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
9214
- case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
9215
- case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
9216
- }
9217
- }
9218
- function toTraceValue(value) {
9219
- if (value === void 0 || value === null) return null;
9220
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9221
- if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
9222
- return JSON.stringify(value);
9223
- }
9224
- function show(value) {
9225
- return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
9226
- }
9227
- function markSkipped(cond, path, why, out) {
9228
- out.push({
9229
- path,
9230
- kind: cond.kind,
9231
- outcome: "not-evaluated",
9232
- reason: why
9233
- });
9234
- if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
9235
- markSkipped(c, `${path}.${i}`, why, out);
9236
- });
9237
- else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
9238
- }
9239
- function evalNode(cond, path, reader, now, out) {
9240
- switch (cond.kind) {
9241
- case "condition": {
9242
- const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
9243
- const base = {
9244
- path,
9245
- kind: cond.kind,
9246
- deviceId: cond.deviceId,
9247
- cap: cond.cap,
9248
- fieldPath: cond.fieldPath,
9249
- operator: cond.operator,
9250
- expected: toTraceValue(cond.value),
9251
- observed: toTraceValue(observed)
9252
- };
9253
- if (observed === void 0) {
9254
- out.push({
9255
- ...base,
9256
- outcome: "fail",
9257
- reason: `${cond.fieldPath} value unknown → fail`
9258
- });
9259
- return false;
9260
- }
9261
- const ok = compare(observed, cond.operator, cond.value);
9262
- out.push({
9263
- ...base,
9264
- outcome: ok ? "pass" : "fail",
9265
- reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
9266
- });
9267
- return ok;
9268
- }
9269
- case "expression": {
9270
- const values = {};
9271
- for (const [alias, binding] of Object.entries(cond.bindings)) {
9272
- if (binding.kind === "literal") {
9273
- values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
9274
- continue;
9275
- }
9276
- const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
9277
- if (raw === void 0) {
9278
- out.push({
9279
- path,
9280
- kind: cond.kind,
9281
- outcome: "fail",
9282
- reason: `${alias} value unknown → fail`
9283
- });
9284
- return false;
9285
- }
9286
- values[alias] = require_composition.toExpressionValue(raw) ?? null;
9287
- }
9288
- const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
9289
- if (!result.ok) {
9290
- out.push({
9291
- path,
9292
- kind: cond.kind,
9293
- outcome: "fail",
9294
- reason: `${cond.expr}: ${result.error} → fail`
9295
- });
9296
- return false;
9297
- }
9298
- const ok = result.value === true;
9299
- out.push({
9300
- path,
9301
- kind: cond.kind,
9302
- outcome: ok ? "pass" : "fail",
9303
- reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
9304
- });
9305
- return ok;
9306
- }
9307
- case "not": {
9308
- const at = out.length;
9309
- out.push({
9310
- path,
9311
- kind: "not",
9312
- outcome: "not-evaluated",
9313
- reason: ""
9314
- });
9315
- const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
9316
- const ok = !inner;
9317
- out.splice(at, 1, {
9318
- path,
9319
- kind: "not",
9320
- outcome: ok ? "pass" : "fail",
9321
- reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
9322
- });
9323
- return ok;
9324
- }
9325
- case "all":
9326
- case "any": {
9327
- const isAll = cond.kind === "all";
9328
- const at = out.length;
9329
- out.push({
9330
- path,
9331
- kind: cond.kind,
9332
- outcome: "not-evaluated",
9333
- reason: ""
9334
- });
9335
- let decided = null;
9336
- for (let i = 0; i < cond.children.length; i += 1) {
9337
- const child = cond.children[i];
9338
- if (child === void 0) continue;
9339
- if (decided !== null) {
9340
- markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
9341
- continue;
9342
- }
9343
- const ok = evalNode(child, `${path}.${i}`, reader, now, out);
9344
- if (isAll ? !ok : ok) decided = i;
9345
- }
9346
- const ok = isAll ? decided === null : decided !== null;
9347
- const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
9348
- out.splice(at, 1, {
9349
- path,
9350
- kind: cond.kind,
9351
- outcome: ok ? "pass" : "fail",
9352
- reason
9353
- });
9354
- return ok;
9355
- }
9356
- }
9357
- }
9358
- /** No condition tree is a pass with no nodes. */
9359
- function evaluateConditionTree(cond, reader, now) {
9360
- if (cond === void 0) return {
9361
- pass: true,
9362
- nodes: []
9363
- };
9364
- const nodes = [];
9365
- return {
9366
- pass: evalNode(cond, "conditions", reader, now, nodes),
9367
- nodes
9368
- };
9369
- }
9370
- //#endregion
9371
9117
  //#region src/catalogs/coco-classmap.ts
9372
9118
  var COCO_80_LABELS = [
9373
9119
  {
@@ -9770,7 +9516,7 @@ function detectionClassMatches(wanted, actual) {
9770
9516
  if (w === a) return true;
9771
9517
  return MACRO_NAMES.has(w) && macroOf(a) === w;
9772
9518
  }
9773
- function isPhase(value) {
9519
+ function isPhase$1(value) {
9774
9520
  return value === "start" || value === "update" || value === "end";
9775
9521
  }
9776
9522
  var DetectionTriggerFilter = class {
@@ -9785,7 +9531,7 @@ var DetectionTriggerFilter = class {
9785
9531
  }
9786
9532
  evaluate(event, now) {
9787
9533
  const { deviceId, trackId, phase } = event;
9788
- if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase(phase)) return {
9534
+ if (typeof deviceId !== "number" || typeof trackId !== "string" || !isPhase$1(phase)) return {
9789
9535
  kind: "ignore",
9790
9536
  why: "malformed track-lifecycle event"
9791
9537
  };
@@ -9856,6 +9602,433 @@ var DetectionTriggerFilter = class {
9856
9602
  }
9857
9603
  };
9858
9604
  //#endregion
9605
+ //#region src/automation/condition-eval.ts
9606
+ /**
9607
+ * Evaluating an automation's condition tree against the ConditionMirror.
9608
+ *
9609
+ * D49: a trigger is a one-shot event, so its gate must never hang on a read
9610
+ * that can fail. Every field a condition leaf names is therefore kept in
9611
+ * memory — filled at start, updated off device state-changed events, reconciled
9612
+ * on a timer — and evaluation reads that memory ONLY. A value the mirror does
9613
+ * not hold is UNKNOWN, which is `fail` with a reason, never a throw and never a
9614
+ * `pass`. A failed refresh changes nothing: the previous value stays.
9615
+ *
9616
+ * The evaluator returns a node per tree position (containers, leaves, and the
9617
+ * ones a short-circuit never reached) with a stable path and a human reason, so
9618
+ * the editor can paint each block.
9619
+ */
9620
+ /** The cap whose runtime-state slice IS the occupancy snapshot, parked objects included. */
9621
+ var ZONE_ANALYTICS_CAP = "zone-analytics";
9622
+ function collectInto(cond, out) {
9623
+ switch (cond.kind) {
9624
+ case "all":
9625
+ case "any":
9626
+ for (const child of cond.children) collectInto(child, out);
9627
+ return;
9628
+ case "not":
9629
+ collectInto(cond.child, out);
9630
+ return;
9631
+ case "condition":
9632
+ out.push({
9633
+ kind: "device",
9634
+ deviceId: cond.deviceId,
9635
+ cap: cond.cap
9636
+ });
9637
+ return;
9638
+ case "stationary-present":
9639
+ out.push({
9640
+ kind: "device",
9641
+ deviceId: cond.deviceId,
9642
+ cap: ZONE_ANALYTICS_CAP
9643
+ });
9644
+ return;
9645
+ case "expression":
9646
+ for (const binding of Object.values(cond.bindings)) if (binding.kind === "global") out.push({
9647
+ kind: "stable",
9648
+ stableId: binding.sourceStableId,
9649
+ cap: binding.cap
9650
+ });
9651
+ return;
9652
+ }
9653
+ }
9654
+ /** Every (device, cap) a tree reads, deduplicated. */
9655
+ function collectMirrorSources(cond) {
9656
+ if (cond === void 0) return [];
9657
+ const all = [];
9658
+ collectInto(cond, all);
9659
+ const seen = /* @__PURE__ */ new Set();
9660
+ return all.filter((s) => {
9661
+ const key = s.kind === "device" ? `d|${s.deviceId}|${s.cap}` : `s|${s.stableId}|${s.cap}`;
9662
+ if (seen.has(key)) return false;
9663
+ seen.add(key);
9664
+ return true;
9665
+ });
9666
+ }
9667
+ function sliceKey(deviceId, cap) {
9668
+ return `${deviceId}|${cap}`;
9669
+ }
9670
+ function pick$1(slice, fieldPath) {
9671
+ let cursor = slice;
9672
+ for (const part of fieldPath.split(".")) {
9673
+ if (cursor === null || typeof cursor !== "object") return void 0;
9674
+ cursor = Reflect.get(cursor, part);
9675
+ }
9676
+ return cursor;
9677
+ }
9678
+ function isSliceObject(value) {
9679
+ return typeof value === "object" && value !== null;
9680
+ }
9681
+ var ConditionMirror = class {
9682
+ sources;
9683
+ slices = /* @__PURE__ */ new Map();
9684
+ stableIds = /* @__PURE__ */ new Map();
9685
+ watched;
9686
+ constructor(sources) {
9687
+ this.sources = sources;
9688
+ this.watched = new Set(sources.filter((s) => s.kind === "device").map((s) => sliceKey(s.deviceId, s.cap)));
9689
+ }
9690
+ /** A `device.state-changed` event. Slices nobody reads are ignored. */
9691
+ apply(deviceId, cap, slice) {
9692
+ const key = sliceKey(deviceId, cap);
9693
+ if (!isSliceObject(slice)) return;
9694
+ if (!this.watched.has(key) && !this.isStableTarget(deviceId, cap)) return;
9695
+ this.slices.set(key, slice);
9696
+ }
9697
+ isStableTarget(deviceId, cap) {
9698
+ for (const s of this.sources) if (s.kind === "stable" && s.cap === cap && this.stableIds.get(s.stableId) === deviceId) return true;
9699
+ return false;
9700
+ }
9701
+ read(deviceId, cap, fieldPath) {
9702
+ const slice = this.slices.get(sliceKey(deviceId, cap));
9703
+ return slice === void 0 ? void 0 : pick$1(slice, fieldPath);
9704
+ }
9705
+ readStable(stableId, cap, fieldPath) {
9706
+ const deviceId = this.stableIds.get(stableId);
9707
+ return deviceId === void 0 ? void 0 : this.read(deviceId, cap, fieldPath);
9708
+ }
9709
+ /** Fill / reconcile. A read that fails or answers nothing keeps the old value. */
9710
+ async refresh(fetchers) {
9711
+ let updated = 0;
9712
+ let kept = 0;
9713
+ if (this.sources.some((s) => s.kind === "stable")) try {
9714
+ this.stableIds = await fetchers.fetchStableIds();
9715
+ } catch {}
9716
+ const targets = /* @__PURE__ */ new Map();
9717
+ for (const s of this.sources) {
9718
+ const deviceId = s.kind === "device" ? s.deviceId : this.stableIds.get(s.stableId);
9719
+ if (deviceId === void 0) {
9720
+ kept += 1;
9721
+ continue;
9722
+ }
9723
+ targets.set(sliceKey(deviceId, s.cap), {
9724
+ deviceId,
9725
+ cap: s.cap
9726
+ });
9727
+ }
9728
+ for (const [key, target] of targets) try {
9729
+ const slice = await fetchers.fetchSlice(target.deviceId, target.cap);
9730
+ if (isSliceObject(slice)) {
9731
+ this.slices.set(key, slice);
9732
+ updated += 1;
9733
+ } else kept += 1;
9734
+ } catch {
9735
+ kept += 1;
9736
+ }
9737
+ return {
9738
+ updated,
9739
+ kept
9740
+ };
9741
+ }
9742
+ };
9743
+ var OPERATOR_SYMBOL = {
9744
+ eq: "=",
9745
+ ne: "≠",
9746
+ gt: ">",
9747
+ gte: "≥",
9748
+ lt: "<",
9749
+ lte: "≤",
9750
+ contains: "contains",
9751
+ in: "in"
9752
+ };
9753
+ function compare(actual, operator, expected) {
9754
+ switch (operator) {
9755
+ case "eq": return actual === expected;
9756
+ case "ne": return actual !== expected;
9757
+ case "gt": return typeof actual === "number" && typeof expected === "number" && actual > expected;
9758
+ case "gte": return typeof actual === "number" && typeof expected === "number" && actual >= expected;
9759
+ case "lt": return typeof actual === "number" && typeof expected === "number" && actual < expected;
9760
+ case "lte": return typeof actual === "number" && typeof expected === "number" && actual <= expected;
9761
+ case "contains": return typeof actual === "string" && typeof expected === "string" && actual.includes(expected);
9762
+ case "in": return Array.isArray(expected) && expected.some((v) => v === actual);
9763
+ }
9764
+ }
9765
+ function toTraceValue(value) {
9766
+ if (value === void 0 || value === null) return null;
9767
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
9768
+ if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) return value;
9769
+ return JSON.stringify(value);
9770
+ }
9771
+ function show(value) {
9772
+ return Array.isArray(value) ? `[${value.join(", ")}]` : String(value);
9773
+ }
9774
+ function markSkipped(cond, path, why, out) {
9775
+ out.push({
9776
+ path,
9777
+ kind: cond.kind,
9778
+ outcome: "not-evaluated",
9779
+ reason: why
9780
+ });
9781
+ if (cond.kind === "all" || cond.kind === "any") cond.children.forEach((c, i) => {
9782
+ markSkipped(c, `${path}.${i}`, why, out);
9783
+ });
9784
+ else if (cond.kind === "not") markSkipped(cond.child, `${path}.0`, why, out);
9785
+ }
9786
+ function parkedObjectsOf(value) {
9787
+ if (!Array.isArray(value)) return [];
9788
+ const rows = [];
9789
+ for (const item of value) {
9790
+ const id = Reflect.get(Object(item), "id");
9791
+ const className = Reflect.get(Object(item), "className");
9792
+ if (typeof id === "string" && typeof className === "string") rows.push({
9793
+ id,
9794
+ className
9795
+ });
9796
+ }
9797
+ return rows;
9798
+ }
9799
+ /** `zoneId`s whose `trackIds` list the parked object (`stationary:<id>`, as the snapshot builds it). */
9800
+ function zonesOfParked(zones, parkedId) {
9801
+ if (!Array.isArray(zones)) return [];
9802
+ const out = [];
9803
+ for (const zone of zones) {
9804
+ const zoneId = Reflect.get(Object(zone), "zoneId");
9805
+ const trackIds = Reflect.get(Object(zone), "trackIds");
9806
+ if (typeof zoneId === "string" && Array.isArray(trackIds) && trackIds.includes(`stationary:${parkedId}`)) out.push(zoneId);
9807
+ }
9808
+ return out;
9809
+ }
9810
+ /**
9811
+ * Is a matching parked object there NOW? Read off the mirrored zone-analytics
9812
+ * snapshot: `frame` is always present in a snapshot, so its absence means the
9813
+ * slice was never read (UNKNOWN, a fail that says so), while a snapshot without
9814
+ * `stationaryObjects` is a definite "nothing parked" (the provider omits an
9815
+ * empty list).
9816
+ */
9817
+ function judgeStationaryPresent(cond, reader) {
9818
+ if (reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "frame") === void 0) return {
9819
+ pass: false,
9820
+ why: `device ${cond.deviceId} occupancy snapshot unknown`
9821
+ };
9822
+ const parked = parkedObjectsOf(reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "stationaryObjects"));
9823
+ if (parked.length === 0) return {
9824
+ pass: false,
9825
+ why: `no parked objects on device ${cond.deviceId}`
9826
+ };
9827
+ const zones = reader.read(cond.deviceId, ZONE_ANALYTICS_CAP, "zones");
9828
+ for (const obj of parked) {
9829
+ if (cond.classes !== void 0 && !cond.classes.some((c) => detectionClassMatches(c, obj.className))) continue;
9830
+ if (cond.zones !== void 0) {
9831
+ const inZones = zonesOfParked(zones, obj.id);
9832
+ if (!cond.zones.some((z) => inZones.includes(z))) continue;
9833
+ }
9834
+ return {
9835
+ pass: true,
9836
+ why: `parked ${obj.className} (${obj.id}) on device ${cond.deviceId}`
9837
+ };
9838
+ }
9839
+ return {
9840
+ pass: false,
9841
+ why: `${parked.length} parked on device ${cond.deviceId}, none matching the class/zone filter`
9842
+ };
9843
+ }
9844
+ function evalNode(cond, path, reader, now, out) {
9845
+ switch (cond.kind) {
9846
+ case "condition": {
9847
+ const observed = reader.read(cond.deviceId, cond.cap, cond.fieldPath);
9848
+ const base = {
9849
+ path,
9850
+ kind: cond.kind,
9851
+ deviceId: cond.deviceId,
9852
+ cap: cond.cap,
9853
+ fieldPath: cond.fieldPath,
9854
+ operator: cond.operator,
9855
+ expected: toTraceValue(cond.value),
9856
+ observed: toTraceValue(observed)
9857
+ };
9858
+ if (observed === void 0) {
9859
+ out.push({
9860
+ ...base,
9861
+ outcome: "fail",
9862
+ reason: `${cond.fieldPath} value unknown → fail`
9863
+ });
9864
+ return false;
9865
+ }
9866
+ const ok = compare(observed, cond.operator, cond.value);
9867
+ out.push({
9868
+ ...base,
9869
+ outcome: ok ? "pass" : "fail",
9870
+ reason: `${cond.fieldPath} ${show(observed)}, expected ${OPERATOR_SYMBOL[cond.operator]} ${show(cond.value)} → ${ok ? "pass" : "fail"}`
9871
+ });
9872
+ return ok;
9873
+ }
9874
+ case "stationary-present": {
9875
+ const verdict = judgeStationaryPresent(cond, reader);
9876
+ out.push({
9877
+ path,
9878
+ kind: cond.kind,
9879
+ outcome: verdict.pass ? "pass" : "fail",
9880
+ reason: `${verdict.why} → ${verdict.pass ? "pass" : "fail"}`,
9881
+ deviceId: cond.deviceId,
9882
+ cap: ZONE_ANALYTICS_CAP
9883
+ });
9884
+ return verdict.pass;
9885
+ }
9886
+ case "expression": {
9887
+ const values = {};
9888
+ for (const [alias, binding] of Object.entries(cond.bindings)) {
9889
+ if (binding.kind === "literal") {
9890
+ values[alias] = require_composition.toExpressionValue(binding.value) ?? null;
9891
+ continue;
9892
+ }
9893
+ const raw = binding.kind === "global" ? reader.readStable(binding.sourceStableId, binding.cap, binding.fieldPath) : void 0;
9894
+ if (raw === void 0) {
9895
+ out.push({
9896
+ path,
9897
+ kind: cond.kind,
9898
+ outcome: "fail",
9899
+ reason: `${alias} value unknown → fail`
9900
+ });
9901
+ return false;
9902
+ }
9903
+ values[alias] = require_composition.toExpressionValue(raw) ?? null;
9904
+ }
9905
+ const result = require_composition.evaluateExpressionSource(cond.expr, values, now);
9906
+ if (!result.ok) {
9907
+ out.push({
9908
+ path,
9909
+ kind: cond.kind,
9910
+ outcome: "fail",
9911
+ reason: `${cond.expr}: ${result.error} → fail`
9912
+ });
9913
+ return false;
9914
+ }
9915
+ const ok = result.value === true;
9916
+ out.push({
9917
+ path,
9918
+ kind: cond.kind,
9919
+ outcome: ok ? "pass" : "fail",
9920
+ reason: `${cond.expr} with ${JSON.stringify(values)} → ${ok ? "pass" : "fail"}`
9921
+ });
9922
+ return ok;
9923
+ }
9924
+ case "not": {
9925
+ const at = out.length;
9926
+ out.push({
9927
+ path,
9928
+ kind: "not",
9929
+ outcome: "not-evaluated",
9930
+ reason: ""
9931
+ });
9932
+ const inner = evalNode(cond.child, `${path}.0`, reader, now, out);
9933
+ const ok = !inner;
9934
+ out.splice(at, 1, {
9935
+ path,
9936
+ kind: "not",
9937
+ outcome: ok ? "pass" : "fail",
9938
+ reason: `not (${inner ? "pass" : "fail"}) → ${ok ? "pass" : "fail"}`
9939
+ });
9940
+ return ok;
9941
+ }
9942
+ case "all":
9943
+ case "any": {
9944
+ const isAll = cond.kind === "all";
9945
+ const at = out.length;
9946
+ out.push({
9947
+ path,
9948
+ kind: cond.kind,
9949
+ outcome: "not-evaluated",
9950
+ reason: ""
9951
+ });
9952
+ let decided = null;
9953
+ for (let i = 0; i < cond.children.length; i += 1) {
9954
+ const child = cond.children[i];
9955
+ if (child === void 0) continue;
9956
+ if (decided !== null) {
9957
+ markSkipped(child, `${path}.${i}`, `short-circuited by ${path}.${decided}`, out);
9958
+ continue;
9959
+ }
9960
+ const ok = evalNode(child, `${path}.${i}`, reader, now, out);
9961
+ if (isAll ? !ok : ok) decided = i;
9962
+ }
9963
+ const ok = isAll ? decided === null : decided !== null;
9964
+ const reason = isAll ? ok ? `all ${cond.children.length} passed → pass` : `${path}.${decided} failed → fail` : ok ? `${path}.${decided} passed → pass` : `none of ${cond.children.length} passed → fail`;
9965
+ out.splice(at, 1, {
9966
+ path,
9967
+ kind: cond.kind,
9968
+ outcome: ok ? "pass" : "fail",
9969
+ reason
9970
+ });
9971
+ return ok;
9972
+ }
9973
+ }
9974
+ }
9975
+ /** No condition tree is a pass with no nodes. */
9976
+ function evaluateConditionTree(cond, reader, now) {
9977
+ if (cond === void 0) return {
9978
+ pass: true,
9979
+ nodes: []
9980
+ };
9981
+ const nodes = [];
9982
+ return {
9983
+ pass: evalNode(cond, "conditions", reader, now, nodes),
9984
+ nodes
9985
+ };
9986
+ }
9987
+ //#endregion
9988
+ //#region src/automation/stationary-trigger.ts
9989
+ function isPhase(value) {
9990
+ return value === "appeared" || value === "departed";
9991
+ }
9992
+ function evaluateStationaryTrigger(trigger, event) {
9993
+ const { deviceId, entryId, phase } = event;
9994
+ if (typeof deviceId !== "number" || typeof entryId !== "string" || !isPhase(phase)) return {
9995
+ kind: "ignore",
9996
+ why: "malformed stationary-changed event"
9997
+ };
9998
+ if (!trigger.deviceIds.includes(deviceId)) return {
9999
+ kind: "ignore",
10000
+ why: `device ${deviceId} is not one of the trigger's devices`
10001
+ };
10002
+ if (phase !== trigger.phase) return {
10003
+ kind: "ignore",
10004
+ why: `phase ${phase}, the trigger wants ${trigger.phase}`
10005
+ };
10006
+ const className = typeof event.className === "string" ? event.className : "";
10007
+ const zones = Array.isArray(event.zones) ? event.zones.filter((z) => typeof z === "string") : [];
10008
+ const vars = {
10009
+ deviceId,
10010
+ className,
10011
+ zones,
10012
+ phase,
10013
+ entryId
10014
+ };
10015
+ if (trigger.classes !== void 0 && !trigger.classes.some((c) => detectionClassMatches(c, className))) return {
10016
+ kind: "reject",
10017
+ reason: `class ${className || "unknown"} not in [${trigger.classes.join(", ")}]`,
10018
+ vars
10019
+ };
10020
+ if (trigger.zones !== void 0 && !trigger.zones.some((z) => zones.includes(z))) return {
10021
+ kind: "reject",
10022
+ reason: `zone mismatch: in [${zones.join(", ")}], wanted [${trigger.zones.join(", ")}]`,
10023
+ vars
10024
+ };
10025
+ return {
10026
+ kind: "fire",
10027
+ reason: `${className} ${phase} on device ${deviceId}`,
10028
+ vars
10029
+ };
10030
+ }
10031
+ //#endregion
9859
10032
  //#region src/automation/run-gate.ts
9860
10033
  var MINUTE_MS$1 = 6e4;
9861
10034
  function minutes(ms) {
@@ -10032,7 +10205,8 @@ var AutomationConditionTraceNodeSchema = zod.z.object({
10032
10205
  "any",
10033
10206
  "not",
10034
10207
  "condition",
10035
- "expression"
10208
+ "expression",
10209
+ "stationary-present"
10036
10210
  ]),
10037
10211
  outcome: TraceNodeOutcomeSchema,
10038
10212
  reason: zod.z.string(),
@@ -10159,7 +10333,7 @@ function appendTrace(ring, trace, keep) {
10159
10333
  }
10160
10334
  /**
10161
10335
  * The trigger stage of a trace from the bag a trigger handed to `run()`:
10162
- * primitives only (a device-state `to` that is an object is left out), minus the
10336
+ * primitives and short lists of them (a device-state `to` that is an object is left out), minus the
10163
10337
  * bookkeeping keys `kind` and `at`.
10164
10338
  */
10165
10339
  function buildTriggerTrace(trigger, outcome, reason) {
@@ -10167,6 +10341,7 @@ function buildTriggerTrace(trigger, outcome, reason) {
10167
10341
  for (const [key, value] of Object.entries(trigger)) {
10168
10342
  if (key === "kind" || key === "at") continue;
10169
10343
  if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) vars[key] = value;
10344
+ else if (Array.isArray(value) && value.every((v) => typeof v === "string" || typeof v === "number")) vars[key] = value;
10170
10345
  }
10171
10346
  return {
10172
10347
  kind: typeof trigger["kind"] === "string" ? trigger["kind"] : "unknown",
@@ -113507,7 +113682,41 @@ function packagePickerItems(packages) {
113507
113682
  return [...pinned, ...addons];
113508
113683
  }
113509
113684
  //#endregion
113685
+ //#region src/i18n/humanize-id.ts
113686
+ /**
113687
+ * Make a raw wire id readable when no dictionary knows it: `delivery_truck`
113688
+ * becomes "Delivery truck", `open-door` becomes "Open door". It invents no
113689
+ * translation — the words stay the id's own, only separators and the first
113690
+ * letter change. An empty id stays empty.
113691
+ */
113692
+ function humanizeId(id) {
113693
+ const spaced = id.replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
113694
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
113695
+ }
113696
+ //#endregion
113510
113697
  //#region src/i18n/state-labels.ts
113698
+ /**
113699
+ * ONE dictionary of human labels for the state words the rule editors show
113700
+ * (R7, D690).
113701
+ *
113702
+ * A device-state gate offers the words the engine compares against —
113703
+ * `armed_away`, `not_home`, `unlocking` — and both rule editors printed them
113704
+ * verbatim. The words are the WIRE vocabulary and must stay what they are (a
113705
+ * rule stores them); what the operator reads is a label, and two editors that
113706
+ * each translate on their own are two editors that disagree. Both the admin UI
113707
+ * and the viewer resolve through {@link stateLabel}.
113708
+ *
113709
+ * Keys are the raw values themselves, flat: the vocabularies are disjoint where
113710
+ * they differ and agree where they overlap (`open` is the same word on a cover
113711
+ * and on a contact). An OPEN vocabulary (a presence zone the operator named,
113712
+ * a cover state a new integration invents) falls back to the raw word — never
113713
+ * an exception, never an empty chip.
113714
+ *
113715
+ * Covered, and held to their sources by the spec: the alarm panel's states,
113716
+ * the cover's, the lock's, the boolean readers' words
113717
+ * (`DEVICE_STATE_READERS`), presence's canonical `home`/`not_home`, and the
113718
+ * scene gate's verdicts and read modes.
113719
+ */
113511
113720
  /** Every key the dictionary answers for, in a stable order. */
113512
113721
  var STATE_LABEL_KEYS = [
113513
113722
  "disarmed",
@@ -113668,11 +113877,23 @@ function isStateLabelKey(key) {
113668
113877
  * dictionary does not know it. Never throws.
113669
113878
  */
113670
113879
  function stateLabel(key, locale) {
113671
- return isStateLabelKey(key) ? STATE_LABELS[key][locale] : key;
113880
+ return isStateLabelKey(key) ? STATE_LABELS[key][locale] : humanizeId(key);
113672
113881
  }
113673
113882
  //#endregion
113674
113883
  //#region src/i18n/event-kind-labels.ts
113675
113884
  /**
113885
+ * ONE dictionary of human labels for event kinds (detection macros/subs,
113886
+ * sensors, audio macros, …) — the same vocabulary the viewer filters and the
113887
+ * notification centre speak (R7).
113888
+ *
113889
+ * Wire ids stay raw (`cat`, `animal`, `audio-dog`). What a human reads is a
113890
+ * label, and a surface that translates on its own is a surface that disagrees
113891
+ * with the next one. Admin UI, viewer and NC resolve through {@link eventKindLabel}.
113892
+ *
113893
+ * Keys match {@link EVENT_TAXONOMY}. An OPEN kind (a custom model class) falls
113894
+ * back to the raw word — never an exception, never an empty chip.
113895
+ */
113896
+ /**
113676
113897
  * Every kind the dictionary answers for. Order is stable; the set is held to
113677
113898
  * {@link EVENT_TAXONOMY} by the spec.
113678
113899
  */
@@ -114042,7 +114263,7 @@ function isEventKindLabelKey(key) {
114042
114263
  */
114043
114264
  function eventKindLabel(kind, locale) {
114044
114265
  const key = kind.trim().toLowerCase();
114045
- return isEventKindLabelKey(key) ? EVENT_KIND_LABELS[key][locale] : kind;
114266
+ return isEventKindLabelKey(key) ? EVENT_KIND_LABELS[key][locale] : humanizeId(kind);
114046
114267
  }
114047
114268
  //#endregion
114048
114269
  //#region src/i18n/translation.ts
@@ -121775,6 +121996,7 @@ exports.evaluateRose = require_composition.evaluateRose;
121775
121996
  exports.evaluateSensorEdge = require_composition.evaluateSensorEdge;
121776
121997
  exports.evaluateSensorLevelEdge = require_composition.evaluateSensorLevelEdge;
121777
121998
  exports.evaluateStatefulExpressionCall = require_composition.evaluateStatefulExpressionCall;
121999
+ exports.evaluateStationaryTrigger = evaluateStationaryTrigger;
121778
122000
  exports.evaluateStatistic = require_composition.evaluateStatistic;
121779
122001
  exports.evaluateZoneRules = evaluateZoneRules;
121780
122002
  exports.event = require_sleep.event;