@camstack/addon-post-analysis 1.2.280 → 1.2.282

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.
@@ -29,6 +29,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  enumerable: true
30
30
  }) : target, mod));
31
31
  //#endregion
32
+ let node_crypto = require("node:crypto");
32
33
  //#region ../types/dist/event-category-BVDXG4tB.mjs
33
34
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
34
35
  EventCategory["SystemBoot"] = "system.boot";
@@ -5390,7 +5391,7 @@ var ZodIssueCode = {
5390
5391
  var ZodFirstPartyTypeKind;
5391
5392
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5392
5393
  //#endregion
5393
- //#region ../types/dist/sleep-D_4W7xuQ.mjs
5394
+ //#region ../types/dist/sleep-DU7mu4wb.mjs
5394
5395
  /**
5395
5396
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5396
5397
  * window to float samples (D455).
@@ -7598,6 +7599,39 @@ function sleep(ms) {
7598
7599
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
7599
7600
  }
7600
7601
  //#endregion
7602
+ //#region ../types/dist/canonical-hash-CPK2Dy60.mjs
7603
+ /**
7604
+ * Deterministic SHA-256 hash of an arbitrary serialisable value. The
7605
+ * canonical form sorts object keys alphabetically at every depth so two
7606
+ * structurally-equal inputs with different key insertion orders produce
7607
+ * the same hash. Returns a 64-char lowercase hex digest.
7608
+ *
7609
+ * Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
7610
+ * accessory-rebuild work when the upstream shape is byte-identical to
7611
+ * the last applied state — preventing user-visible "re-discovery"
7612
+ * notifications on every addon-runner respawn. Each respawn re-fires
7613
+ * `DeviceBindingsChanged` for every cap registration, which without
7614
+ * this guard would propagate redundant pushes.
7615
+ *
7616
+ * Note: this is a SYMPTOMATIC fix layered on top of the binding-change
7617
+ * subscription. The proper fix is a single "device ready" lifecycle
7618
+ * barrier so exports react only when the full cap set has landed —
7619
+ * tracked separately for post-HA-integration work.
7620
+ */
7621
+ function canonicalHash(value) {
7622
+ const canonical = JSON.stringify(value, replaceWithSortedKeys);
7623
+ return (0, node_crypto.createHash)("sha256").update(canonical ?? "").digest("hex");
7624
+ }
7625
+ function replaceWithSortedKeys(_key, value) {
7626
+ if (value && typeof value === "object" && !Array.isArray(value)) {
7627
+ const obj = value;
7628
+ const out = {};
7629
+ for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
7630
+ return out;
7631
+ }
7632
+ return value;
7633
+ }
7634
+ //#endregion
7601
7635
  //#region ../types/dist/err-msg-IQTHeDzc.mjs
7602
7636
  /**
7603
7637
  import { errMsg } from '@camstack/types'
@@ -18175,6 +18209,16 @@ var NcCrossingSchema = _enum([
18175
18209
  "exit",
18176
18210
  "any"
18177
18211
  ]);
18212
+ /**
18213
+ * Which edge of a BOOLEAN sensor fires a device-event rule (D640).
18214
+ * - `raised` — the sensor went high: water detected, smoke, open, tampered.
18215
+ * - `cleared` — it went back: dry, clear, closed.
18216
+ * - absent — both edges, which is what every rule authored before this did.
18217
+ *
18218
+ * There is no third member for "both": absence already says it, and a value
18219
+ * that means the same as absence is a second way to write one rule.
18220
+ */
18221
+ var NcSensorTransitionSchema = _enum(["raised", "cleared"]);
18178
18222
  /** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
18179
18223
  var NcZoneConditionSchema = object({
18180
18224
  ids: array(string().min(1)).min(1),
@@ -18364,6 +18408,20 @@ var NcConditionsSchema = object({
18364
18408
  scene: NcSceneConditionSchema.optional(),
18365
18409
  /** Device scope — absent = all devices. */
18366
18410
  devices: array(number()).optional(),
18411
+ /**
18412
+ * Device EXCLUSION (D641) — "every device except these". Judged on the SAME
18413
+ * subject ids {@link devices} is judged on (the camera, the sensor that
18414
+ * fired, a linked set's sensor and row cameras), so a sensor fanned out per
18415
+ * linked camera is excluded exactly where an include would have tested it.
18416
+ * `min(1)`: an empty exclusion excludes nothing and is written as absent.
18417
+ *
18418
+ * Not for `system-event` rules — those carry their own
18419
+ * `systemEvent.excludeDeviceIds` (D636), and the hub refuses this key there.
18420
+ * Editors present the two lists as ONE field (All | Only these | All except
18421
+ * these); the engine evaluates each independently, so a legacy rule
18422
+ * carrying both still means "these, minus those".
18423
+ */
18424
+ devicesExclude: array(number()).min(1).optional(),
18367
18425
  /** Detector class names (any overlap with the record's class set). */
18368
18426
  classes: array(string().min(1)).optional(),
18369
18427
  /** Veto classes — any overlap fails the rule. */
@@ -18497,6 +18555,16 @@ var NcConditionsSchema = object({
18497
18555
  */
18498
18556
  sensorKinds: array(string().min(1)).optional(),
18499
18557
  /**
18558
+ * DEVICE-EVENT only (D640). Fire on one edge of a boolean sensor —
18559
+ * {@link NcSensorTransitionSchema}. Which field of the slice means "raised"
18560
+ * is `@camstack/types` → `notification/sensor-raised.ts`. A reading the
18561
+ * engine cannot place (a kind with no raise, an unreadable slice) PASSES:
18562
+ * the condition fails open towards notifying, never towards silence. Editors
18563
+ * offer it only when every chosen {@link sensorKinds} member has a raise
18564
+ * (`sensorTransitionOffered`), and the hub refuses it on any other delivery.
18565
+ */
18566
+ sensorTransition: NcSensorTransitionSchema.optional(),
18567
+ /**
18500
18568
  * PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
18501
18569
  * (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
18502
18570
  * when the subject's phase does not match (a subject always carries a phase
@@ -18996,9 +19064,29 @@ var NcTestResultSchema = object({
18996
19064
  className: string().optional(),
18997
19065
  label: string().optional()
18998
19066
  });
18999
- /** The five rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts`
19000
- * is the same union; the template-vars spec pins the two together. */
19067
+ /** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is the
19068
+ * same union; the template-vars spec pins the two together. `mixed` is DERIVED
19069
+ * (a stored system rule whose kinds span families, D637) and never authored. */
19001
19070
  var NcRuleKindSchema = _enum([
19071
+ "detection",
19072
+ "sensor",
19073
+ "occupancy",
19074
+ "sound",
19075
+ "device-events",
19076
+ "system",
19077
+ "alarm",
19078
+ "mixed"
19079
+ ]);
19080
+ /**
19081
+ * The kinds a SERVED template-variable descriptor may name: the five that
19082
+ * existed before D637, frozen. `getTemplateCatalog` ships `kinds` to every
19083
+ * installed viewer, and a viewer at `@camstack/types` 1.2.25x parses them with a
19084
+ * strict five-member enum — one `device-events` in one descriptor fails the
19085
+ * whole catalog parse. So the WIRE says `system`, and the MATCH widens it:
19086
+ * `templateVarKindApplies` reads a `system` descriptor as applying to every
19087
+ * system rule kind (`device-events`, `system`, `alarm`, `mixed`).
19088
+ */
19089
+ var NcTemplateVarKindSchema = _enum([
19002
19090
  "detection",
19003
19091
  "sensor",
19004
19092
  "occupancy",
@@ -19036,7 +19124,7 @@ var NcTemplateVarGroupSchema = _enum([
19036
19124
  *
19037
19125
  * `families` / `fields` / `kinds` / `deliveries` / `systemEventKinds` say WHERE
19038
19126
  * it has a value; absent = no restriction on that axis. `systemEventKinds` is
19039
- * read only for a `system` rule. `dynamic` declares a FAMILY of names
19127
+ * read only for a system-event rule (any family, or `mixed`). `dynamic` declares a FAMILY of names
19040
19128
  * (`count_<class>`): the descriptor's `name` is the example member.
19041
19129
  */
19042
19130
  var NcTemplateVarDescriptorSchema = object({
@@ -19047,7 +19135,7 @@ var NcTemplateVarDescriptorSchema = object({
19047
19135
  group: NcTemplateVarGroupSchema,
19048
19136
  families: array(NcTemplateFamilySchema).min(1),
19049
19137
  fields: array(NcTemplateFieldSchema).optional(),
19050
- kinds: array(NcRuleKindSchema).optional(),
19138
+ kinds: array(NcTemplateVarKindSchema).optional(),
19051
19139
  deliveries: array(NcDeliverySchema).optional(),
19052
19140
  systemEventKinds: array(NcSystemEventKindSchema).optional(),
19053
19141
  dynamic: object({
@@ -19095,6 +19183,7 @@ var NcConditionDescriptorSchema = object({
19095
19183
  /** Editor widget the UI renders — never hardcode per-condition forms. */
19096
19184
  valueType: _enum([
19097
19185
  "deviceIdList",
19186
+ "deviceIdExcludeList",
19098
19187
  "stringList",
19099
19188
  "number01",
19100
19189
  "number",
@@ -19105,6 +19194,7 @@ var NcConditionDescriptorSchema = object({
19105
19194
  "plateMatcher",
19106
19195
  "packagePhase",
19107
19196
  "crossingSelect",
19197
+ "sensorTransition",
19108
19198
  "polygonDraw",
19109
19199
  "occupancy",
19110
19200
  "audio",
@@ -19303,6 +19393,21 @@ var NC_CONDITION_CATALOG = [
19303
19393
  phase: "P1",
19304
19394
  description: "Restrict the rule to these devices; absent = all devices. A sensor event matches either the SENSOR that fired or the camera it is attributed to, so a rule may name either one."
19305
19395
  },
19396
+ {
19397
+ id: "devicesExclude",
19398
+ group: "scope",
19399
+ label: "Excluded devices",
19400
+ valueType: "deviceIdExcludeList",
19401
+ operator: "notIn",
19402
+ appliesTo: [
19403
+ "immediate",
19404
+ "track-end",
19405
+ "device-event",
19406
+ "package-event"
19407
+ ],
19408
+ phase: "P1",
19409
+ description: "Every device except these. Judged on the same device ids as Devices: a sensor event is excluded by the SENSOR that fired or by the camera it is attributed to."
19410
+ },
19306
19411
  {
19307
19412
  id: "classes",
19308
19413
  group: "class",
@@ -19559,6 +19664,33 @@ var NC_CONDITION_CATALOG = [
19559
19664
  phase: "P1",
19560
19665
  description: "Raw device event-type tokens (e.g. doorbell press / press_long) from the event-emitter slice; absent on doorbell-pulse / passive sensors."
19561
19666
  },
19667
+ {
19668
+ id: "sensorTransition",
19669
+ group: "device",
19670
+ label: "Notify when",
19671
+ valueType: "sensorTransition",
19672
+ options: [
19673
+ {
19674
+ value: "",
19675
+ label: "It triggers and when it clears",
19676
+ hint: "Every change: water detected, and back to dry."
19677
+ },
19678
+ {
19679
+ value: "raised",
19680
+ label: "Only when it triggers",
19681
+ hint: "Water detected, smoke, a contact opening — never the return to normal."
19682
+ },
19683
+ {
19684
+ value: "cleared",
19685
+ label: "Only when it clears",
19686
+ hint: "Back to dry, smoke gone, a contact closing — never the alert itself."
19687
+ }
19688
+ ],
19689
+ operator: "in",
19690
+ appliesTo: ["device-event"],
19691
+ phase: "P2",
19692
+ description: "Fire on one edge of a boolean sensor. Offered only when every chosen sensor kind raises and clears; a reading that cannot be placed still notifies."
19693
+ },
19562
19694
  {
19563
19695
  id: "packagePhase",
19564
19696
  group: "package",
@@ -19742,6 +19874,28 @@ var NcHistoryFilterSchema = object({
19742
19874
  limit: number().int().min(1).max(500).default(100)
19743
19875
  });
19744
19876
  /**
19877
+ * Every rule's last run, in one answer.
19878
+ *
19879
+ * A rule ABSENT from `runs` produced no delivery inside `horizonMs` — how far
19880
+ * back this answer looked, in ms before the hub's now. It is the outbox prune
19881
+ * window (7 days by default, operator-configurable), NARROWED when the read hit
19882
+ * its row bound: the answer never claims a window it did not read. `null` means
19883
+ * the operator keeps the outbox forever AND the read reached its end, so absent
19884
+ * means "never on record". A UI derives its "not in the last N days" from this
19885
+ * value, never from a constant — and only when the call ANSWERED (D315).
19886
+ */
19887
+ var NcRuleLastRunsSchema = object({
19888
+ runs: array(object({
19889
+ ruleId: string(),
19890
+ /** Fire time of the newest row (`createdAt`, epoch ms). */
19891
+ lastAt: number(),
19892
+ status: NcHistoryStatusSchema,
19893
+ /** The camera/device of that row (a digest row files under its first camera). */
19894
+ deviceId: number()
19895
+ })),
19896
+ horizonMs: number().nonnegative().nullable()
19897
+ });
19898
+ /**
19745
19899
  * What a snooze covers. Broader scopes win when several overlap, so one window
19746
19900
  * leaves ONE digest rather than a rule snooze and a whole-feed snooze both
19747
19901
  * summarising the same silence.
@@ -19923,6 +20077,14 @@ var NcAlarmModeCoverageSchema = object({
19923
20077
  /** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
19924
20078
  deviceIds: array(number().int()),
19925
20079
  /**
20080
+ * D641: the ids NOT covered although `allDevices` is true — every unscoped
20081
+ * covering rule EXCLUDES them (`conditions.devicesExclude`) and no named rule
20082
+ * covers them. Always `[]` when `allDevices` is false. Excluded by the rules,
20083
+ * which is not the same as skipped by a switch: never in `skippedDevices`.
20084
+ * Defaulted so an answer computed before this field parses as "none".
20085
+ */
20086
+ allDevicesExcept: array(number().int()).default([]),
20087
+ /**
19926
20088
  * Devices this mode NAMES but cannot actually arm, each with the switch that
19927
20089
  * excludes it.
19928
20090
  *
@@ -20146,6 +20308,15 @@ var notificationRulesCapability = {
20146
20308
  */
20147
20309
  getHistory: method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }),
20148
20310
  /**
20311
+ * The newest delivery of every rule, in ONE bounded read of the outbox —
20312
+ * the "Last run" column. Never a `getHistory` per rule. A rule with no
20313
+ * delivery inside `horizonMs` is absent from `runs`; see
20314
+ * {@link NcRuleLastRunsSchema} for what absent means. A store that cannot
20315
+ * answer FAILS the call rather than answering "nothing ran". Admin-only,
20316
+ * like `getHistory`, of which it is a projection.
20317
+ */
20318
+ getRuleLastRuns: method(object({}), NcRuleLastRunsSchema, { auth: "admin" }),
20319
+ /**
20149
20320
  * Mint a fresh, externally-reachable URL for one artefact a history row
20150
20321
  * named in {@link NcHistoryEntrySchema.shape.artifactIds} — the SAME
20151
20322
  * signed-link mechanism the dispatcher uses to attach media to an
@@ -33200,7 +33371,30 @@ var RenameNodeResultSchema = object({
33200
33371
  nodeId: string(),
33201
33372
  name: string()
33202
33373
  });
33203
- method(_void(), array(TopologyNodeSchema).readonly(), { auth: "admin" }), method(NodeAddonInputSchema, SuccessSchema, {
33374
+ /**
33375
+ * The cheap node list. A consumer's rule for a STORED node id (a rule's
33376
+ * `nodeIds`) that is missing from `nodes`, whatever `complete` says: render it
33377
+ * as a removable chip labelled unknown, never drop it (D49). Even a complete
33378
+ * list omits a node that joined and left between two history captures, so a
33379
+ * missing id means "not known to the hub", never "removed". Only the operator
33380
+ * removes it.
33381
+ *
33382
+ * `complete` changes only the WORDING: `false` = no history read has ever
33383
+ * answered, so the list may be missing offline nodes the hub does know (D315).
33384
+ *
33385
+ * `name` is the history row's name (≤ 30 s behind a rename), or the id when
33386
+ * the history has none.
33387
+ */
33388
+ var NodeDirectorySchema = object({
33389
+ nodes: array(object({
33390
+ id: string(),
33391
+ name: string(),
33392
+ isHub: boolean(),
33393
+ isOnline: boolean()
33394
+ })).readonly(),
33395
+ complete: boolean()
33396
+ });
33397
+ method(_void(), array(TopologyNodeSchema).readonly(), { auth: "admin" }), method(_void(), NodeDirectorySchema, { auth: "admin" }), method(NodeAddonInputSchema, SuccessSchema, {
33204
33398
  kind: "mutation",
33205
33399
  auth: "admin"
33206
33400
  }), method(NodeAddonInputSchema, SuccessSchema, {
@@ -44726,6 +44920,12 @@ Object.freeze({
44726
44920
  addonId: null,
44727
44921
  access: "create"
44728
44922
  },
44923
+ "nodes.directory": {
44924
+ capName: "nodes",
44925
+ capScope: "system",
44926
+ addonId: null,
44927
+ access: "view"
44928
+ },
44729
44929
  "nodes.executeQuery": {
44730
44930
  capName: "nodes",
44731
44931
  capScope: "system",
@@ -44894,6 +45094,12 @@ Object.freeze({
44894
45094
  addonId: null,
44895
45095
  access: "view"
44896
45096
  },
45097
+ "notificationRules.getRuleLastRuns": {
45098
+ capName: "notification-rules",
45099
+ capScope: "system",
45100
+ addonId: null,
45101
+ access: "view"
45102
+ },
44897
45103
  "notificationRules.getTemplateCatalog": {
44898
45104
  capName: "notification-rules",
44899
45105
  capScope: "system",
@@ -50510,6 +50716,253 @@ function audioModeOf(value) {
50510
50716
  return null;
50511
50717
  }
50512
50718
  new Set(["devices", "classes"]);
50719
+ /** Every filter, for the callers that must cover all four (guards, tests). */
50720
+ var NC_SYSTEM_EVENT_FILTER_KEYS = [
50721
+ "deviceIds",
50722
+ "deviceTypes",
50723
+ "nodeIds",
50724
+ "packageNames"
50725
+ ];
50726
+ /**
50727
+ * Does `filter` say anything about `kind`?
50728
+ *
50729
+ * `false` means the filter is not part of this kind's vocabulary: the engine
50730
+ * ignores it and an editor hides its input. It never means "excluded".
50731
+ */
50732
+ function systemEventFilterApplies(kind, filter) {
50733
+ switch (filter) {
50734
+ case "deviceIds": return kind.startsWith("device-") || kind.startsWith("stream-") || kind === "detection-blind" || kind === "alarm-triggered";
50735
+ case "deviceTypes": return kind.startsWith("device-") || kind === "detection-blind";
50736
+ case "nodeIds": return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "wrapper-update-available" || kind === "server-updated";
50737
+ case "packageNames": return kind.endsWith("update-available") || kind === "addon-updated" || kind === "server-updated";
50738
+ }
50739
+ }
50740
+ /**
50741
+ * WHICH FAMILY every system-event kind belongs to, and which narrowing filters
50742
+ * each family may carry: one table, read by the hub validator, by `ruleKindOf`
50743
+ * and by both rule editors (D637).
50744
+ *
50745
+ * A system rule used to be one editor for thirty-one kinds and five filters.
50746
+ * "Eventi di sistema" carried battery kinds under a liveness `deviceTypes` filter
50747
+ * and went silent for two weeks (D636). The three families are three questions:
50748
+ * about devices, about the installation, and about the alarm panel. A rule
50749
+ * answers one of them. The engine never needed to know, and it still does not:
50750
+ * a mixed stored rule keeps firing. Only AUTHORING is strict.
50751
+ *
50752
+ * The legacy `camera-*` tail has no family (`null`). It is parsed, never produced,
50753
+ * never offered.
50754
+ */
50755
+ var NC_SYSTEM_EVENT_FAMILIES = [
50756
+ "device-events",
50757
+ "system",
50758
+ "alarm"
50759
+ ];
50760
+ function isSystemEventFamily(value) {
50761
+ return NC_SYSTEM_EVENT_FAMILIES.some((family) => family === value);
50762
+ }
50763
+ /** Total over the enum: a kind added to the cap without a family fails tsc here. */
50764
+ var NC_SYSTEM_EVENT_FAMILY = {
50765
+ "device-online": "device-events",
50766
+ "device-offline": "device-events",
50767
+ "device-disabled": "device-events",
50768
+ "device-enabled": "device-events",
50769
+ "device-battery-low": "device-events",
50770
+ "device-battery-normal": "device-events",
50771
+ "device-consumable-low": "device-events",
50772
+ "device-consumable-normal": "device-events",
50773
+ "stream-online": "device-events",
50774
+ "stream-offline": "device-events",
50775
+ "detection-blind": "device-events",
50776
+ "node-online": "system",
50777
+ "node-offline": "system",
50778
+ "node-inference-unavailable": "system",
50779
+ "addon-crash-loop": "system",
50780
+ "addon-update-available": "system",
50781
+ "addon-updated": "system",
50782
+ "server-update-available": "system",
50783
+ "server-updated": "system",
50784
+ "wrapper-update-available": "system",
50785
+ "backup-completed": "system",
50786
+ "export-completed": "system",
50787
+ "alarm-triggered": "alarm",
50788
+ "alarm-armed": "alarm",
50789
+ "alarm-disarmed": "alarm",
50790
+ "alarm-arming": "alarm",
50791
+ "alarm-arm-refused": "alarm",
50792
+ "alarm-pending": "alarm",
50793
+ "alarm-rearmed": "alarm",
50794
+ "alarm-sensor-bypass": "alarm",
50795
+ "alarm-check-failed": "alarm",
50796
+ "camera-online": null,
50797
+ "camera-offline": null,
50798
+ "camera-disabled": null,
50799
+ "camera-enabled": null
50800
+ };
50801
+ new Map(Object.entries(NC_SYSTEM_EVENT_FAMILY));
50802
+ var NC_SYSTEM_EVENT_CONDITION_FILTER_KEYS = [...NC_SYSTEM_EVENT_FILTER_KEYS, "excludeDeviceIds"];
50803
+ /**
50804
+ * The filters a family may carry. The four base filters are the union of
50805
+ * `systemEventFilterApplies` over the family's kinds (the spec pins that
50806
+ * equality). `excludeDeviceIds` is a device-events filter only: an alarm is
50807
+ * narrowed to the camera that triggered it and excludes nothing.
50808
+ */
50809
+ var NC_SYSTEM_EVENT_FAMILY_FILTERS = {
50810
+ "device-events": [
50811
+ "deviceIds",
50812
+ "excludeDeviceIds",
50813
+ "deviceTypes"
50814
+ ],
50815
+ system: ["nodeIds", "packageNames"],
50816
+ alarm: ["deviceIds"]
50817
+ };
50818
+ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
50819
+ {
50820
+ id: "device-state",
50821
+ family: "device-events",
50822
+ label: "Device state",
50823
+ kinds: [
50824
+ "device-offline",
50825
+ "device-online",
50826
+ "device-disabled",
50827
+ "device-enabled"
50828
+ ]
50829
+ },
50830
+ {
50831
+ id: "streams",
50832
+ family: "device-events",
50833
+ label: "Streams",
50834
+ kinds: ["stream-offline", "stream-online"]
50835
+ },
50836
+ {
50837
+ id: "battery",
50838
+ family: "device-events",
50839
+ label: "Battery",
50840
+ kinds: ["device-battery-low", "device-battery-normal"]
50841
+ },
50842
+ {
50843
+ id: "consumables",
50844
+ family: "device-events",
50845
+ label: "Consumables",
50846
+ kinds: ["device-consumable-low", "device-consumable-normal"]
50847
+ },
50848
+ {
50849
+ id: "detection-blind",
50850
+ family: "device-events",
50851
+ label: "Detecting nothing",
50852
+ kinds: ["detection-blind"]
50853
+ },
50854
+ {
50855
+ id: "nodes",
50856
+ family: "system",
50857
+ label: "Nodes",
50858
+ kinds: [
50859
+ "node-offline",
50860
+ "node-online",
50861
+ "node-inference-unavailable"
50862
+ ]
50863
+ },
50864
+ {
50865
+ id: "addons",
50866
+ family: "system",
50867
+ label: "Addons",
50868
+ kinds: [
50869
+ "addon-crash-loop",
50870
+ "addon-update-available",
50871
+ "addon-updated"
50872
+ ]
50873
+ },
50874
+ {
50875
+ id: "updates",
50876
+ family: "system",
50877
+ label: "Server & container updates",
50878
+ kinds: [
50879
+ "server-update-available",
50880
+ "server-updated",
50881
+ "wrapper-update-available"
50882
+ ]
50883
+ },
50884
+ {
50885
+ id: "backups-exports",
50886
+ family: "system",
50887
+ label: "Backups & exports",
50888
+ kinds: ["backup-completed", "export-completed"]
50889
+ },
50890
+ {
50891
+ id: "alarm",
50892
+ family: "alarm",
50893
+ label: "Alarm",
50894
+ kinds: NC_ALARM_SYSTEM_EVENT_KINDS
50895
+ }
50896
+ ];
50897
+ function systemEventKindsOfFamily(family) {
50898
+ return NC_SYSTEM_EVENT_KIND_GROUP_SPECS.filter((g) => g.family === family).flatMap((g) => g.kinds);
50899
+ }
50900
+ /**
50901
+ * Why the hub refuses this condition on a new or updated rule. Empty = accepted.
50902
+ * Families are reported in `NC_SYSTEM_EVENT_FAMILIES` order, and filters in
50903
+ * `NC_SYSTEM_EVENT_CONDITION_FILTER_KEYS` order, so a refusal reads the same
50904
+ * every time.
50905
+ */
50906
+ function systemEventFamilyProblems(condition) {
50907
+ if (condition === void 0 || condition.kinds.length === 0) return [{ code: "no-kinds" }];
50908
+ const problems = [];
50909
+ const present = /* @__PURE__ */ new Set();
50910
+ for (const kind of condition.kinds) {
50911
+ const family = NC_SYSTEM_EVENT_FAMILY[kind];
50912
+ if (family === null) problems.push({
50913
+ code: "legacy-kind",
50914
+ kind
50915
+ });
50916
+ else present.add(family);
50917
+ }
50918
+ const families = NC_SYSTEM_EVENT_FAMILIES.filter((f) => present.has(f));
50919
+ if (families.length > 1) problems.push({
50920
+ code: "mixed-families",
50921
+ families
50922
+ });
50923
+ const [family] = families;
50924
+ if (families.length !== 1 || family === void 0) return problems;
50925
+ const allowed = NC_SYSTEM_EVENT_FAMILY_FILTERS[family];
50926
+ for (const filter of NC_SYSTEM_EVENT_CONDITION_FILTER_KEYS) if (condition[filter] !== void 0 && !allowed.includes(filter)) problems.push({
50927
+ code: "filter-outside-family",
50928
+ filter,
50929
+ family
50930
+ });
50931
+ return problems;
50932
+ }
50933
+ function describeSystemEventFamilyProblem(problem) {
50934
+ switch (problem.code) {
50935
+ case "no-kinds": return "a system-event rule needs at least one event kind";
50936
+ case "legacy-kind": return `'${problem.kind}' is a legacy kind and cannot be authored`;
50937
+ case "mixed-families": return `event kinds span ${problem.families.join(" + ")} — one rule per family`;
50938
+ case "filter-outside-family": return `filter '${problem.filter}' is not allowed on a ${problem.family} rule`;
50939
+ }
50940
+ }
50941
+ /** Every kind a `system-event` rule can derive — the three families and `mixed`. */
50942
+ var NC_SYSTEM_RULE_KINDS = [
50943
+ "device-events",
50944
+ "system",
50945
+ "alarm",
50946
+ "mixed"
50947
+ ];
50948
+ /** Is this a kind a `system-event` rule derives (any family, or `mixed`)? */
50949
+ function isSystemRuleKind(kind) {
50950
+ return NC_SYSTEM_RULE_KINDS.some((k) => k === kind);
50951
+ }
50952
+ /** The family a kind authors, or undefined (a camera kind, or `mixed`). */
50953
+ function systemFamilyOfKind(kind) {
50954
+ return isSystemEventFamily(kind) ? kind : void 0;
50955
+ }
50956
+ /** The delivery a system rule rides — named once, compared everywhere. */
50957
+ var NC_SYSTEM_DELIVERY = "system-event";
50958
+ /**
50959
+ * Is this a SYSTEM rule — one about the installation rather than about anything
50960
+ * a camera saw? Takes a plain `string` so a rule carrying a delivery this build
50961
+ * has never heard of still gets an answer instead of a type error.
50962
+ */
50963
+ function isSystemDelivery(delivery) {
50964
+ return delivery === NC_SYSTEM_DELIVERY;
50965
+ }
50513
50966
  NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
50514
50967
  var WEEKDAY_TO_DAY = {
50515
50968
  Sun: 0,
@@ -50571,6 +51024,66 @@ function isScheduleActive(schedule, atMs) {
50571
51024
  }
50572
51025
  return schedule.invert === true ? !inside : inside;
50573
51026
  }
51027
+ var ARMED_WORD_PREFIX = "armed_";
51028
+ var ALARM_PANEL_CAP = "alarm-panel";
51029
+ var ALARM_PANEL_TRIGGER_METHOD = "trigger";
51030
+ /**
51031
+ * A rule belongs to the alarm when its `deviceState` gate names ANY device's
51032
+ * `armed_*` word, or an ENABLED `onTrigger` sequence pulls the alarm panel's
51033
+ * trigger (D630).
51034
+ */
51035
+ function ruleBelongsToAlarm(rule) {
51036
+ if ((rule.conditions.deviceState?.states ?? []).some((s) => s.trim().toLowerCase().startsWith(ARMED_WORD_PREFIX))) return true;
51037
+ return (rule.onTrigger ?? []).some((sequence) => sequence.enabled && sequence.actions.some((step) => step.kind === "cap" && step.cap === ALARM_PANEL_CAP && step.method === ALARM_PANEL_TRIGGER_METHOD));
51038
+ }
51039
+ /**
51040
+ * WHICH WAY A BOOLEAN SENSOR MOVED — read off the one slice a sensor event
51041
+ * persists (D640 "a sensor rule fires on the raise, the clear, or both").
51042
+ *
51043
+ * A device-event rule over a flood sensor notified on BOTH edges: water
51044
+ * detected, and back to dry. `NcConditions.sensorTransition` lets the rule
51045
+ * name one edge; this file answers, for one `SensorEvent`, which edge it is.
51046
+ *
51047
+ * NOT A SECOND TABLE. The "which boolean of this cap counts as HIGH" question
51048
+ * already has one answer, `SOURCE_CAP_ACTIVE_FIELD`
51049
+ * (`catalogs/sensor-active-state.ts`, the doorbell's and the recorder's edge
51050
+ * table), and a sensor event names its kind, not its cap. So the table here is
51051
+ * DERIVED: event kind → cap (`EVENT_KIND_BY_CAP`) → that cap's active field.
51052
+ * Only kinds whose taxonomy category is `sensor` take part — a switch has an
51053
+ * `on` boolean too, but "it triggers / it clears" is not a sentence about a
51054
+ * light, and offering it there would be a knob with a misleading label.
51055
+ * `presence` is a WORD (`home` / `not_home` / a zone), not a boolean, and is
51056
+ * absent from the source table, so it is absent here.
51057
+ *
51058
+ * `undefined` = never guessed: a kind with no raise (a doorbell press, a
51059
+ * button, a cover), or a slice whose field is missing or not a boolean. The
51060
+ * engine then does NOT filter — a flood alert must not go silent because one
51061
+ * read came back unreadable (the same stance as the alarm's `sensorEdgeOf`).
51062
+ *
51063
+ * Pure, RN-safe: the viewer imports the support rule from here.
51064
+ */
51065
+ /** Event kind → the boolean field of its cap slice that means "raised". */
51066
+ var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.entries(EVENT_KIND_BY_CAP).flatMap(([capName, descriptor]) => {
51067
+ const field = SOURCE_CAP_ACTIVE_FIELD[capName];
51068
+ if (field === void 0 || descriptor.category !== "sensor") return [];
51069
+ return [[descriptor.kind, field]];
51070
+ })));
51071
+ /** The sensor kinds `sensorTransition` means something for. */
51072
+ var NC_SENSOR_TRANSITION_KINDS = Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
51073
+ function normaliseKind(kind) {
51074
+ return kind.trim().toLowerCase();
51075
+ }
51076
+ /**
51077
+ * true = the sensor is raised (water, smoke, open, tampered…), false = it
51078
+ * cleared, undefined = this kind has no raise or the slice cannot say.
51079
+ */
51080
+ function sensorRaisedOf(kind, value) {
51081
+ if (value === null) return void 0;
51082
+ const field = NC_SENSOR_RAISED_FIELD_BY_KIND[normaliseKind(kind)];
51083
+ if (field === void 0) return void 0;
51084
+ const raw = Object.entries(value).find(([key]) => key === field)?.[1];
51085
+ return typeof raw === "boolean" ? raw : void 0;
51086
+ }
50574
51087
  var RULE_BODY_ONLY = ["body"];
50575
51088
  var RULE_TEXT = ["title", "body"];
50576
51089
  var BATTERY = ["device-battery-low", "device-battery-normal"];
@@ -51308,10 +51821,38 @@ var CAPTION_VARS = {
51308
51821
  function onAxis(declared, asked) {
51309
51822
  return declared === void 0 || asked === void 0 || declared.includes(asked);
51310
51823
  }
51311
- function onSystemAxis(d, ctx) {
51312
- if (ctx.kind !== "system" || d.systemEventKinds === void 0) return true;
51824
+ /**
51825
+ * Does a descriptor declared for `declared` kinds have a value for a rule of
51826
+ * `kind`?
51827
+ *
51828
+ * `system` on the WIRE stands for every system rule kind (D637): the served
51829
+ * catalog keeps the five pre-D637 tokens so installed viewers still parse it
51830
+ * (`NcTemplateVarKindSchema`), and the family split lives here, in the match.
51831
+ * Exported so the admin's local interpreter of the served catalog asks the
51832
+ * same question instead of restating it.
51833
+ */
51834
+ function templateVarKindApplies(declared, kind) {
51835
+ if (declared === void 0 || kind === void 0) return true;
51836
+ if (declared.some((k) => k === kind)) return true;
51837
+ return isSystemRuleKind(kind) && declared.includes("system");
51838
+ }
51839
+ /**
51840
+ * Is a descriptor that names `systemEventKinds` on the event-kind axis of a
51841
+ * system rule? Read only for a system rule kind; any other kind ignores it.
51842
+ *
51843
+ * Event kinds chosen: the descriptor must speak about one of them. None chosen
51844
+ * yet: a FAMILY kind watches its own family's event kinds (a device-events rule
51845
+ * is never offered the alarm's `{{mode}}`), and `mixed` watches all of them.
51846
+ */
51847
+ function templateVarSystemEventApplies(d, ctx) {
51848
+ if (!isSystemRuleKind(ctx.kind) || d.systemEventKinds === void 0) return true;
51849
+ const declared = d.systemEventKinds;
51313
51850
  const asked = ctx.systemEventKinds ?? [];
51314
- return asked.length === 0 || asked.some((k) => d.systemEventKinds?.includes(k) === true);
51851
+ if (asked.length > 0) return asked.some((k) => declared.includes(k));
51852
+ const family = ctx.kind === void 0 ? void 0 : systemFamilyOfKind(ctx.kind);
51853
+ if (family === void 0) return true;
51854
+ const own = systemEventKindsOfFamily(family);
51855
+ return declared.some((k) => own.includes(k));
51315
51856
  }
51316
51857
  /**
51317
51858
  * The variables that have a value in `ctx`, in catalog order. Pure.
@@ -51325,27 +51866,13 @@ function templateVarsFor(ctx) {
51325
51866
  const allowed = CAPTION_VARS[ctx.field];
51326
51867
  return NC_TEMPLATE_VARS.filter((d) => d.families.includes(ctx.family) && allowed.includes(d.name) && d.dynamic === void 0);
51327
51868
  }
51328
- return NC_TEMPLATE_VARS.filter((d) => d.families.includes(ctx.family) && onAxis(d.fields, ctx.field) && onAxis(d.kinds, ctx.kind) && onAxis(d.deliveries, ctx.delivery) && onSystemAxis(d, ctx));
51869
+ return NC_TEMPLATE_VARS.filter((d) => d.families.includes(ctx.family) && onAxis(d.fields, ctx.field) && templateVarKindApplies(d.kinds, ctx.kind) && onAxis(d.deliveries, ctx.delivery) && templateVarSystemEventApplies(d, ctx));
51329
51870
  }
51330
51871
  /** True when `name` is supplied in `ctx`, dynamic families (`count_<class>`) included. */
51331
51872
  function isKnownTemplateVar(name, ctx) {
51332
51873
  return templateVarsFor(ctx).some((d) => d.dynamic !== void 0 ? name.startsWith(d.dynamic.prefix) && name.length > d.dynamic.prefix.length : d.name === name);
51333
51874
  }
51334
51875
  /**
51335
- * Does `filter` say anything about `kind`?
51336
- *
51337
- * `false` means the filter is not part of this kind's vocabulary: the engine
51338
- * ignores it and an editor hides its input. It never means "excluded".
51339
- */
51340
- function systemEventFilterApplies(kind, filter) {
51341
- switch (filter) {
51342
- case "deviceIds": return kind.startsWith("device-") || kind.startsWith("stream-") || kind === "detection-blind" || kind === "alarm-triggered" || kind === "export-completed";
51343
- case "deviceTypes": return kind.startsWith("device-") || kind === "detection-blind";
51344
- case "nodeIds": return kind.startsWith("node-") || kind === "addon-crash-loop" || kind === "addon-update-available" || kind === "addon-updated" || kind === "server-update-available" || kind === "wrapper-update-available" || kind === "server-updated";
51345
- case "packageNames": return kind.endsWith("update-available") || kind === "addon-updated" || kind === "server-updated";
51346
- }
51347
- }
51348
- /**
51349
51876
  * TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
51350
51877
  *
51351
51878
  * Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
@@ -52547,6 +53074,12 @@ Object.defineProperty(exports, "NC_DEFAULT_SNOOZE_MINUTES", {
52547
53074
  return NC_DEFAULT_SNOOZE_MINUTES;
52548
53075
  }
52549
53076
  });
53077
+ Object.defineProperty(exports, "NC_SENSOR_TRANSITION_KINDS", {
53078
+ enumerable: true,
53079
+ get: function() {
53080
+ return NC_SENSOR_TRANSITION_KINDS;
53081
+ }
53082
+ });
52550
53083
  Object.defineProperty(exports, "NC_SNOOZE_MAX_MINUTES", {
52551
53084
  enumerable: true,
52552
53085
  get: function() {
@@ -52583,6 +53116,12 @@ Object.defineProperty(exports, "NcConditionDescriptorSchema", {
52583
53116
  return NcConditionDescriptorSchema;
52584
53117
  }
52585
53118
  });
53119
+ Object.defineProperty(exports, "NcConditionsSchema", {
53120
+ enumerable: true,
53121
+ get: function() {
53122
+ return NcConditionsSchema;
53123
+ }
53124
+ });
52586
53125
  Object.defineProperty(exports, "NcRuleInputSchema", {
52587
53126
  enumerable: true,
52588
53127
  get: function() {
@@ -52823,6 +53362,12 @@ Object.defineProperty(exports, "buildEventKindDescriptor", {
52823
53362
  return buildEventKindDescriptor;
52824
53363
  }
52825
53364
  });
53365
+ Object.defineProperty(exports, "canonicalHash", {
53366
+ enumerable: true,
53367
+ get: function() {
53368
+ return canonicalHash;
53369
+ }
53370
+ });
52826
53371
  Object.defineProperty(exports, "cosineSimilarity", {
52827
53372
  enumerable: true,
52828
53373
  get: function() {
@@ -52853,6 +53398,12 @@ Object.defineProperty(exports, "deriveRecordingMode", {
52853
53398
  return deriveRecordingMode;
52854
53399
  }
52855
53400
  });
53401
+ Object.defineProperty(exports, "describeSystemEventFamilyProblem", {
53402
+ enumerable: true,
53403
+ get: function() {
53404
+ return describeSystemEventFamilyProblem;
53405
+ }
53406
+ });
52856
53407
  Object.defineProperty(exports, "discriminatedUnion", {
52857
53408
  enumerable: true,
52858
53409
  get: function() {
@@ -52955,6 +53506,12 @@ Object.defineProperty(exports, "isSourceCap", {
52955
53506
  return isSourceCap;
52956
53507
  }
52957
53508
  });
53509
+ Object.defineProperty(exports, "isSystemDelivery", {
53510
+ enumerable: true,
53511
+ get: function() {
53512
+ return isSystemDelivery;
53513
+ }
53514
+ });
52958
53515
  Object.defineProperty(exports, "kebabToCamel", {
52959
53516
  enumerable: true,
52960
53517
  get: function() {
@@ -53063,12 +53620,24 @@ Object.defineProperty(exports, "resolvePoolMemoryPolicy", {
53063
53620
  return resolvePoolMemoryPolicy;
53064
53621
  }
53065
53622
  });
53623
+ Object.defineProperty(exports, "ruleBelongsToAlarm", {
53624
+ enumerable: true,
53625
+ get: function() {
53626
+ return ruleBelongsToAlarm;
53627
+ }
53628
+ });
53066
53629
  Object.defineProperty(exports, "sceneMonitorCapability", {
53067
53630
  enumerable: true,
53068
53631
  get: function() {
53069
53632
  return sceneMonitorCapability;
53070
53633
  }
53071
53634
  });
53635
+ Object.defineProperty(exports, "sensorRaisedOf", {
53636
+ enumerable: true,
53637
+ get: function() {
53638
+ return sensorRaisedOf;
53639
+ }
53640
+ });
53072
53641
  Object.defineProperty(exports, "sleep", {
53073
53642
  enumerable: true,
53074
53643
  get: function() {
@@ -53093,6 +53662,12 @@ Object.defineProperty(exports, "subKindsOf", {
53093
53662
  return subKindsOf;
53094
53663
  }
53095
53664
  });
53665
+ Object.defineProperty(exports, "systemEventFamilyProblems", {
53666
+ enumerable: true,
53667
+ get: function() {
53668
+ return systemEventFamilyProblems;
53669
+ }
53670
+ });
53096
53671
  Object.defineProperty(exports, "systemEventFilterApplies", {
53097
53672
  enumerable: true,
53098
53673
  get: function() {