@camstack/addon-post-analysis 1.2.664 → 1.2.666

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 (16) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/{clip-model-registry-TeNUAAPP.js → clip-model-registry-BIhYJRFt.js} +324 -132
  3. package/dist/{clip-model-registry-Ch9Sz-Qb.mjs → clip-model-registry-Uc_C1x81.mjs} +319 -133
  4. package/dist/embedding-encoder/index.js +1 -1
  5. package/dist/embedding-encoder/index.mjs +1 -1
  6. package/dist/pipeline-analytics/_stub.js +2 -2
  7. package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CXG7YPYq.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-FkeZoNXj.mjs} +2 -2
  8. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CnYZAdvw.mjs +26 -0
  9. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Br6eWzp_.mjs +26 -0
  10. package/dist/pipeline-analytics/{hostInit-BZ_D3_sF.mjs → hostInit-BBhgVoNl.mjs} +2 -2
  11. package/dist/pipeline-analytics/index.js +430 -46
  12. package/dist/pipeline-analytics/index.mjs +430 -46
  13. package/dist/pipeline-analytics/remoteEntry.js +1 -1
  14. package/package.json +2 -2
  15. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CIt_RDlm.mjs +0 -26
  16. package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-4TTlyJ66.mjs +0 -26
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-TeNUAAPP.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-BIhYJRFt.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -7354,6 +7354,18 @@ function sensorEdgeOf(kind, value) {
7354
7354
  if (HALTED.has(opening.state)) return "neither";
7355
7355
  if (OPENING.has(opening.state)) return "opening";
7356
7356
  }
7357
+ /**
7358
+ * The WORD a sensor change is told with — the state the device moved to, read off
7359
+ * the same slice the edge is: `unlocked`, `open`, `closed`… for a lock, cover, contact
7360
+ * or switch, else `raised` / `cleared` for a boolean sensor (water, smoke…). Undefined
7361
+ * when the slice cannot say: a sentence never invents a state (D393).
7362
+ */
7363
+ function sensorStateWordOf(kind, value, raised) {
7364
+ const opening = alarmOpeningFromSensorEvent(kind, value);
7365
+ if (opening.kind === "reading") return opening.state;
7366
+ if (raised === void 0) return void 0;
7367
+ return raised ? "raised" : "cleared";
7368
+ }
7357
7369
  //#endregion
7358
7370
  //#region src/notification-center/audio-rule-matcher.ts
7359
7371
  /**
@@ -7861,6 +7873,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
7861
7873
  const eventType = readSensorEventType(ev.value);
7862
7874
  const sensorEdge = sensorEdgeOf(ev.kind, ev.value);
7863
7875
  const sensorRaised = require_clip_model_registry.sensorRaisedOf(ev.kind, ev.value);
7876
+ const sensorState = sensorStateWordOf(ev.kind, ev.value, sensorRaised);
7864
7877
  return {
7865
7878
  kind: "device-event",
7866
7879
  recordId: String(ev.id),
@@ -7875,6 +7888,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
7875
7888
  ...eventType !== void 0 ? { eventType } : {},
7876
7889
  ...sensorEdge !== void 0 ? { sensorEdge } : {},
7877
7890
  ...sensorRaised !== void 0 ? { sensorRaised } : {},
7891
+ ...sensorState !== void 0 ? { sensorState } : {},
7878
7892
  ...linked !== void 0 ? {
7879
7893
  linkedSensorId: ev.sourceDeviceId,
7880
7894
  linkedCameraIds: [...linked.linkedCameraIds],
@@ -9213,6 +9227,17 @@ var en_default = {
9213
9227
  "alarm.combined.title": "Alarm: {{deviceName}}",
9214
9228
  "alarm.intrusion.title": "Intrusion — {{locations}}",
9215
9229
  "alarm.intrusion.body": "Intrusion: {{locations}}",
9230
+ "alarm.intrusion.body.withChanges": "Intrusion: {{locations}} — {{changes}}",
9231
+ "alarm.change.lock.unlocked": "{{device}} unlocked",
9232
+ "alarm.change.lock.unlocking": "{{device}} unlocking",
9233
+ "alarm.change.lock.jammed": "{{device}} jammed",
9234
+ "alarm.change.lock.door-open": "{{device}} open",
9235
+ "alarm.change.open": "{{device}} open",
9236
+ "alarm.change.opening": "{{device}} opening",
9237
+ "alarm.change.closed": "{{device}} closed",
9238
+ "alarm.change.on": "{{device}} on",
9239
+ "alarm.change.raised": "{{device}} triggered",
9240
+ "alarm.change.cleared": "{{device}} cleared",
9216
9241
  "system.alarm-arm-refused.title.home": "Arming home refused",
9217
9242
  "system.alarm-arm-refused.title.away": "Arming away refused",
9218
9243
  "system.alarm-arm-refused.title.night": "Arming night refused",
@@ -9630,6 +9655,17 @@ var it_default = {
9630
9655
  "alarm.combined.title": "Allarme: {{deviceName}}",
9631
9656
  "alarm.intrusion.title": "Intrusione — {{locations}}",
9632
9657
  "alarm.intrusion.body": "Intrusione: {{locations}}",
9658
+ "alarm.intrusion.body.withChanges": "Intrusione: {{locations}} — {{changes}}",
9659
+ "alarm.change.lock.unlocked": "{{device}} aperta",
9660
+ "alarm.change.lock.unlocking": "{{device}} in apertura",
9661
+ "alarm.change.lock.jammed": "{{device}} bloccata",
9662
+ "alarm.change.lock.door-open": "{{device}} aperta",
9663
+ "alarm.change.open": "{{device}} aperto",
9664
+ "alarm.change.opening": "{{device}} in apertura",
9665
+ "alarm.change.closed": "{{device}} chiuso",
9666
+ "alarm.change.on": "{{device}} acceso",
9667
+ "alarm.change.raised": "{{device}} attivato",
9668
+ "alarm.change.cleared": "{{device}} ripristinato",
9633
9669
  "system.alarm-arm-refused.title.home": "Inserimento Casa rifiutato",
9634
9670
  "system.alarm-arm-refused.title.away": "Inserimento Fuori rifiutato",
9635
9671
  "system.alarm-arm-refused.title.night": "Inserimento Notte rifiutato",
@@ -12284,6 +12320,16 @@ var NcStateSummaryTestSchema = require_clip_model_registry.object({
12284
12320
  }))
12285
12321
  });
12286
12322
  //#endregion
12323
+ //#region src/notification-center/template-defaults.ts
12324
+ function servedTemplateDefaults(texts) {
12325
+ return [{
12326
+ id: "appliance-cycle",
12327
+ kind: "sensor",
12328
+ title: texts.raw("appliance.finished.title") ?? "",
12329
+ body: texts.raw("appliance.finished.body") ?? ""
12330
+ }];
12331
+ }
12332
+ //#endregion
12287
12333
  //#region src/notification-center/clock-text.ts
12288
12334
  /**
12289
12335
  * `{{time}}` — hours and minutes in the NOTIFICATION language.
@@ -13682,7 +13728,10 @@ var ncActions = require_clip_model_registry.defineCustomActions({
13682
13728
  catalog: require_clip_model_registry.array(require_clip_model_registry.NcConditionDescriptorSchema),
13683
13729
  taxonomy: require_clip_model_registry.NcTaxonomySchema
13684
13730
  })),
13685
- "nc.getTemplateCatalog": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({ vars: require_clip_model_registry.array(require_clip_model_registry.NcTemplateVarDescriptorSchema) })),
13731
+ "nc.getTemplateCatalog": require_clip_model_registry.customAction(require_clip_model_registry.object({}), require_clip_model_registry.object({
13732
+ vars: require_clip_model_registry.array(require_clip_model_registry.NcTemplateVarDescriptorSchema),
13733
+ defaults: require_clip_model_registry.array(require_clip_model_registry.NcTemplateDefaultSchema)
13734
+ })),
13686
13735
  "nc.previewTemplate": require_clip_model_registry.customAction(require_clip_model_registry.NcTemplatePreviewInputSchema, require_clip_model_registry.NcTemplatePreviewSchema, { kind: "mutation" }),
13687
13736
  "nc.createRule": require_clip_model_registry.customAction(require_clip_model_registry.object({ rule: require_clip_model_registry.NcRuleInputSchema }), require_clip_model_registry.object({ rule: require_clip_model_registry.NcRuleSchema }), {
13688
13737
  kind: "mutation",
@@ -14462,7 +14511,10 @@ function makeNcActionHandlers(deps) {
14462
14511
  catalog: [...require_clip_model_registry.NC_CONDITION_CATALOG],
14463
14512
  taxonomy: require_clip_model_registry.NC_TAXONOMY
14464
14513
  }),
14465
- "nc.getTemplateCatalog": async () => ({ vars: require_clip_model_registry.servedTemplateVars() }),
14514
+ "nc.getTemplateCatalog": async () => ({
14515
+ vars: require_clip_model_registry.servedTemplateVars(),
14516
+ defaults: deps.previewTexts === void 0 ? [] : servedTemplateDefaults(deps.previewTexts)
14517
+ }),
14466
14518
  "nc.previewTemplate": async (input) => {
14467
14519
  const texts = deps.previewTexts;
14468
14520
  if (texts === void 0) throw new Error("nc.previewTemplate: text catalog not wired");
@@ -49951,7 +50003,7 @@ async function encodeRgbCropToJpeg(bytes, width, height) {
49951
50003
  * what lets the caller stop base64-ing purely to be understood.
49952
50004
  */
49953
50005
  async function decodeJpegToRgb(jpeg) {
49954
- const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).raw().toBuffer({ resolveWithObject: true });
50006
+ const { data, info } = await (0, sharp.default)(typeof jpeg === "string" ? Buffer.from(jpeg, "base64") : jpeg).removeAlpha().toColourspace("srgb").raw().toBuffer({ resolveWithObject: true });
49955
50007
  return {
49956
50008
  bytes: data,
49957
50009
  width: info.width,
@@ -53037,6 +53089,42 @@ function collectPackageZoneIds(rules) {
53037
53089
  }
53038
53090
  return ids;
53039
53091
  }
53092
+ /**
53093
+ * How much the package zone box grows on every side when asking whether motion
53094
+ * reached it. Mirrors the D144 dilation of the pipeline's burst trigger
53095
+ * (`PACKAGE_ZONE_DILATION` in addon-pipeline, not importable from here): about
53096
+ * one adult's width beside a doormat, the place a courier stands.
53097
+ */
53098
+ var PACKAGE_ZONE_MOTION_DILATION = .5;
53099
+ /**
53100
+ * A motion region covering more than this fraction of the frame is a light
53101
+ * change or the weather, not a person: it never counts as a deliverer.
53102
+ */
53103
+ var PACKAGE_ZONE_MOTION_MAX_REGION_FRAC = .8;
53104
+ /**
53105
+ * Did analyzer motion reach the dilated package zone with a person-sized
53106
+ * region? Unknown geometry answers false: this stamp only ever BACKS the open
53107
+ * of a candidate, so failing closed keeps the light-wedge guard intact.
53108
+ */
53109
+ function delivererMotionNearPackageZone(input) {
53110
+ const { zoneRect, frameWidth, frameHeight } = input;
53111
+ if (zoneRect === null || frameWidth <= 0 || frameHeight <= 0) return false;
53112
+ const dx = zoneRect.w * PACKAGE_ZONE_MOTION_DILATION;
53113
+ const dy = zoneRect.h * PACKAGE_ZONE_MOTION_DILATION;
53114
+ const left = Math.max(0, zoneRect.x - dx);
53115
+ const top = Math.max(0, zoneRect.y - dy);
53116
+ const right = Math.min(1, zoneRect.x + zoneRect.w + dx);
53117
+ const bottom = Math.min(1, zoneRect.y + zoneRect.h + dy);
53118
+ for (const region of input.regions) {
53119
+ const x = region.bbox.x / frameWidth;
53120
+ const y = region.bbox.y / frameHeight;
53121
+ const w = region.bbox.w / frameWidth;
53122
+ const h = region.bbox.h / frameHeight;
53123
+ if (w * h > PACKAGE_ZONE_MOTION_MAX_REGION_FRAC) continue;
53124
+ if (x < right && left < x + w && y < bottom && top < y + h) return true;
53125
+ }
53126
+ return false;
53127
+ }
53040
53128
  //#endregion
53041
53129
  //#region src/pipeline-analytics/pipeline/package/package-collector-mirror.ts
53042
53130
  /**
@@ -53982,6 +54070,15 @@ var PACKAGE_STILLNESS_TTL_MS = 5 * 6e4;
53982
54070
  */
53983
54071
  var PACKAGE_STILLNESS_ASSOC_IOU = .3;
53984
54072
  /**
54073
+ * Lowest score of a below-threshold box that may CONTINUE an open candidate
54074
+ * (D665: a box under the floor may continue, never open). Observed on a real
54075
+ * parcel after the day to IR flip: 0.35 to 0.50.
54076
+ */
54077
+ var PACKAGE_STILLNESS_CONTINUATION_MIN_SCORE = .3;
54078
+ /** Above-floor sightings a candidate needs before a sub-floor box may count:
54079
+ * one confident look plus a doormat-shaped reading is not a parcel. */
54080
+ var PACKAGE_CONTINUATION_MIN_REAL_HITS = 2;
54081
+ /**
53985
54082
  * Two absence reads closer than this are ONE look, not two. A settle burst
53986
54083
  * runs the package step 5 s apart and a poll 45 s apart, so a real second
53987
54084
  * look is always further away; two frames of one moment (a re-dispatch, a
@@ -54031,6 +54128,7 @@ var PackageStillnessTracker = class {
54031
54128
  const next = [...this.byDevice.get(input.deviceId) ?? []];
54032
54129
  const consumed = /* @__PURE__ */ new Set();
54033
54130
  const promoted = [];
54131
+ const continuedLog = [];
54034
54132
  for (const s of input.sightings) {
54035
54133
  const index = this.bestMatch(next, s, consumed);
54036
54134
  if (index < 0) {
@@ -54043,17 +54141,33 @@ var PackageStillnessTracker = class {
54043
54141
  if (prev === void 0) continue;
54044
54142
  consumed.add(prev.id);
54045
54143
  const updated = confirm(prev, input, s);
54046
- const minHits = input.minHits ?? this.config.minHits;
54047
- if (!(updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs)) {
54048
- next[index] = updated;
54049
- continue;
54050
- }
54051
- const parked = {
54052
- ...updated,
54053
- promotedAt: input.timestamp
54144
+ const parked = this.promoteWhenReady(updated, input);
54145
+ next[index] = parked ?? updated;
54146
+ if (parked !== null) promoted.push(toEntry(parked));
54147
+ }
54148
+ if (input.looked === true && input.continuationBoxes !== void 0) for (let i = 0; i < next.length; i += 1) {
54149
+ const c = next[i];
54150
+ if (c === void 0 || c.promotedAt !== null || consumed.has(c.id)) continue;
54151
+ if (c.realHits < PACKAGE_CONTINUATION_MIN_REAL_HITS) continue;
54152
+ const box = this.continuationFor(c, input.continuationBoxes);
54153
+ if (box === null) continue;
54154
+ consumed.add(c.id);
54155
+ const continued = {
54156
+ ...c,
54157
+ lastSeenAt: input.timestamp,
54158
+ hits: c.hits + 1,
54159
+ absenceReads: [],
54160
+ lastLook: "present",
54161
+ earlyUnbackedReported: false
54054
54162
  };
54055
- next[index] = parked;
54056
- promoted.push(toEntry(parked));
54163
+ continuedLog.push({
54164
+ id: c.id,
54165
+ hits: continued.hits,
54166
+ score: box.score
54167
+ });
54168
+ const parked = this.promoteWhenReady(continued, input);
54169
+ next[i] = parked ?? continued;
54170
+ if (parked !== null) promoted.push(toEntry(parked));
54057
54171
  }
54058
54172
  const present = input.rawPackageBoxes !== void 0 ? withRawPresence(next, consumed, input.rawPackageBoxes, input.timestamp, this.config.assocIou) : next;
54059
54173
  const looked = input.looked === true && input.rawPackageBoxes !== void 0 ? withLook(present, consumed, input, input.rawPackageBoxes, this.config.assocIou) : present;
@@ -54089,7 +54203,8 @@ var PackageStillnessTracker = class {
54089
54203
  ...input.lastActivityAt !== void 0 ? { lastActivityAt: input.lastActivityAt } : {}
54090
54204
  }),
54091
54205
  earlyDepartureIds: [...early],
54092
- earlyEvidenceUnbacked: unbacked
54206
+ earlyEvidenceUnbacked: unbacked,
54207
+ continued: continuedLog
54093
54208
  };
54094
54209
  }
54095
54210
  /** Why complete early evidence may NOT conclude, or null when it may. */
@@ -54116,7 +54231,8 @@ var PackageStillnessTracker = class {
54116
54231
  ...input.lastActivityAt !== void 0 ? { lastActivityAt: input.lastActivityAt } : {}
54117
54232
  }),
54118
54233
  earlyDepartureIds: [],
54119
- earlyEvidenceUnbacked: []
54234
+ earlyEvidenceUnbacked: [],
54235
+ continued: []
54120
54236
  };
54121
54237
  }
54122
54238
  /** The devices holding a candidate — what a sweep has to visit. */
@@ -54180,6 +54296,43 @@ var PackageStillnessTracker = class {
54180
54296
  /** Best same-class candidate over the association floor, ignoring any already
54181
54297
  * consumed by an earlier sighting in THIS frame (two boxes never collapse
54182
54298
  * into one parcel). */
54299
+ continuationFor(c, boxes) {
54300
+ let best = null;
54301
+ let bestIou = this.config.assocIou;
54302
+ for (const b of boxes) {
54303
+ if (b.score < PACKAGE_STILLNESS_CONTINUATION_MIN_SCORE) continue;
54304
+ const overlap = iou$2(c.bbox, b.bbox);
54305
+ if (overlap >= bestIou) {
54306
+ bestIou = overlap;
54307
+ best = b;
54308
+ }
54309
+ }
54310
+ return best;
54311
+ }
54312
+ promoteWhenReady(candidate, input) {
54313
+ const updated = this.backedLate(candidate, input);
54314
+ const minHits = input.minHits ?? this.config.minHits;
54315
+ return updated.promotedAt === null && updated.activityBackedAtOpen && updated.hits >= minHits && input.timestamp - updated.firstSeenAt >= input.minDwellMs ? {
54316
+ ...updated,
54317
+ promotedAt: input.timestamp
54318
+ } : null;
54319
+ }
54320
+ /**
54321
+ * Zone motion learned AFTER the open still backs the candidate when its stamp
54322
+ * lies in the window around the FIRST sighting (the same slack a pick-up
54323
+ * gets): the backing stays anchored at the open, a later passer-by never
54324
+ * validates scenery (D1003).
54325
+ */
54326
+ backedLate(c, input) {
54327
+ if (c.activityBackedAtOpen || c.promotedAt !== null) return c;
54328
+ const at = input.lastDelivererMotionAt;
54329
+ const windowMs = input.activityWindowMs ?? 0;
54330
+ if (at === void 0 || windowMs <= 0) return c;
54331
+ return at >= c.firstSeenAt - windowMs && at <= c.firstSeenAt + this.config.roundGapMs ? {
54332
+ ...c,
54333
+ activityBackedAtOpen: true
54334
+ } : c;
54335
+ }
54183
54336
  bestMatch(candidates, s, consumed) {
54184
54337
  let bestIndex = -1;
54185
54338
  let bestIou = this.config.assocIou;
@@ -54196,7 +54349,7 @@ var PackageStillnessTracker = class {
54196
54349
  }
54197
54350
  openCandidate(input, s) {
54198
54351
  const windowMs = input.activityWindowMs ?? 0;
54199
- const activityBackedAtOpen = windowMs <= 0 || input.lastActivityAt !== void 0 && input.timestamp - input.lastActivityAt <= windowMs;
54352
+ const activityBackedAtOpen = windowMs <= 0 || input.lastActivityAt !== void 0 && input.timestamp - input.lastActivityAt <= windowMs || input.lastDelivererMotionAt !== void 0 && input.timestamp - input.lastDelivererMotionAt <= windowMs;
54200
54353
  return {
54201
54354
  id: this.newId(),
54202
54355
  deviceId: input.deviceId,
@@ -54207,6 +54360,7 @@ var PackageStillnessTracker = class {
54207
54360
  firstSeenAt: input.timestamp,
54208
54361
  lastSeenAt: input.timestamp,
54209
54362
  hits: 1,
54363
+ realHits: 1,
54210
54364
  promotedAt: null,
54211
54365
  absenceReads: [],
54212
54366
  lastLook: null,
@@ -54229,6 +54383,7 @@ function confirm(prev, input, s) {
54229
54383
  frameHeight: input.frameHeight,
54230
54384
  lastSeenAt: input.timestamp,
54231
54385
  hits: prev.hits + 1,
54386
+ realHits: prev.realHits + 1,
54232
54387
  absenceReads: [],
54233
54388
  lastLook: "present",
54234
54389
  earlyUnbackedReported: false,
@@ -54765,6 +54920,9 @@ var FrameState = class {
54765
54920
  * which would let the streetlight wedge vouch for its own delivery.
54766
54921
  */
54767
54922
  lastNonPackageActivityAt = /* @__PURE__ */ new Map();
54923
+ /** Last analyzer motion near a package drop-off zone, per device (D1003).
54924
+ * Backs the OPEN of a candidate only; never a pick-up witness. */
54925
+ lastPackageZoneMotionAt = /* @__PURE__ */ new Map();
54768
54926
  /**
54769
54927
  * WHO was in the drop-off zone, and when (D404). The pick-up is CONCLUDED
54770
54928
  * minutes after the fact, so the collector must be remembered while it is
@@ -58124,6 +58282,7 @@ function nextPackageLookDropEpisodes(previous, drops) {
58124
58282
  //#region src/pipeline-analytics/pipeline/package/package-look-evidence.ts
58125
58283
  function packageLookEvidence(input) {
58126
58284
  const raw = [];
58285
+ const continuation = [];
58127
58286
  for (const d of input.frame.detections) {
58128
58287
  if (d.kind !== "first-level" || !input.packageClasses.has(d.macroClass)) continue;
58129
58288
  raw.push({
@@ -58136,11 +58295,16 @@ function packageLookEvidence(input) {
58136
58295
  for (const d of input.frame.discarded ?? []) {
58137
58296
  if (!input.packageClasses.has(d.macroClass)) continue;
58138
58297
  const [x1, y1, x2, y2] = d.bbox;
58139
- raw.push({
58298
+ const box = {
58140
58299
  x: x1,
58141
58300
  y: y1,
58142
58301
  w: x2 - x1,
58143
58302
  h: y2 - y1
58303
+ };
58304
+ raw.push(box);
58305
+ if (d.reason === "below-threshold") continuation.push({
58306
+ bbox: box,
58307
+ score: d.score
58144
58308
  });
58145
58309
  }
58146
58310
  return {
@@ -58152,6 +58316,7 @@ function packageLookEvidence(input) {
58152
58316
  h: t.bbox.h
58153
58317
  })),
58154
58318
  rawPackageBoxes: raw,
58319
+ continuationBoxes: continuation,
58155
58320
  earlyPickup: input.earlyPickupEnabled
58156
58321
  };
58157
58322
  }
@@ -58339,6 +58504,13 @@ function clamp01(value) {
58339
58504
  }
58340
58505
  //#endregion
58341
58506
  //#region src/pipeline-analytics/pipeline/package/package-scene-diff.ts
58507
+ /**
58508
+ * Zone-crop scene difference for a packages rule (D931).
58509
+ *
58510
+ * A uniform brightness shift is removed by subtracting the median luma delta,
58511
+ * so a shadow that lifts the whole crop leaves no blob. What remains is a
58512
+ * connected region. The package model is not consulted here.
58513
+ */
58342
58514
  /** Residual, after the median shift, that counts as a changed pixel. */
58343
58515
  var PACKAGE_SCENE_DIFF_LUMA_THRESHOLD = 28;
58344
58516
  /** A changed region this large, relative to the zone, is the zone itself. */
@@ -58353,12 +58525,26 @@ var PACKAGE_SCENE_DIFF_SETTLE_MS = 3e3;
58353
58525
  var PACKAGE_SCENE_DIFF_LOOK_GAP_MS = 2e3;
58354
58526
  /** Confidence stamped on a scene-diff sighting the model did not box. */
58355
58527
  var PACKAGE_SCENE_DIFF_CONFIDENCE = .5;
58528
+ /** True when the crop was shot in infrared (no colour). */
58529
+ function isIrCrop(crop) {
58530
+ return crop.chromaSpread < IR_CHROMA_THRESHOLD;
58531
+ }
58356
58532
  function diffZoneLuma(input) {
58357
58533
  const { before, after } = input;
58358
58534
  if (before.width !== after.width || before.height !== after.height || before.luma.length !== after.luma.length) return {
58359
58535
  blobs: [],
58360
58536
  rejections: [{ reason: "crop-mismatch" }]
58361
58537
  };
58538
+ if (isIrCrop(before) !== isIrCrop(after)) return {
58539
+ blobs: [],
58540
+ rejections: [{
58541
+ reason: "lighting-mode-changed",
58542
+ chroma: {
58543
+ before: before.chromaSpread,
58544
+ after: after.chromaSpread
58545
+ }
58546
+ }]
58547
+ };
58362
58548
  const median = medianDelta(before.luma, after.luma);
58363
58549
  const mask = residualMask(before.luma, after.luma, median);
58364
58550
  const zonePx = before.width * before.height;
@@ -58573,6 +58759,7 @@ function okCrop(rgb, width, height, jpeg) {
58573
58759
  width,
58574
58760
  height,
58575
58761
  luma: lumaFromRgb(rgb, width, height),
58762
+ chromaSpread: chromaSpreadFromRgb(rgb, width, height),
58576
58763
  jpeg: new Uint8Array(jpeg)
58577
58764
  };
58578
58765
  }
@@ -58589,6 +58776,46 @@ function lumaFromRgb(bytes, width, height) {
58589
58776
  }
58590
58777
  return luma;
58591
58778
  }
58779
+ /** The grid `IR_CHROMA_THRESHOLD` was calibrated on (`chromaSpread`, scene-crop.ts). */
58780
+ var CHROMA_GRID = 32;
58781
+ /**
58782
+ * Mean of max(r,g,b) - min(r,g,b), 0..255, measured on a 32x32 box-averaged
58783
+ * copy of the crop: the same measure and grid as `chromaSpread` in
58784
+ * `scene-crop.ts` (which needs sharp), so `IR_CHROMA_THRESHOLD` means the same
58785
+ * thing. Averaging first matters: sensor noise and JPEG chroma speckle on an IR
58786
+ * frame cancel out, where per-pixel they read as colour.
58787
+ */
58788
+ function chromaSpreadFromRgb(bytes, width, height) {
58789
+ if (width <= 0 || height <= 0 || bytes.length < width * height * 3) return 0;
58790
+ const gw = Math.min(CHROMA_GRID, width);
58791
+ const gh = Math.min(CHROMA_GRID, height);
58792
+ const sum = new Float64Array(gw * gh * 3);
58793
+ const count = new Float64Array(gw * gh);
58794
+ for (let y = 0; y < height; y++) {
58795
+ const cy = Math.min(gh - 1, Math.floor(y * gh / height));
58796
+ for (let x = 0; x < width; x++) {
58797
+ const cx = Math.min(gw - 1, Math.floor(x * gw / width));
58798
+ const cell = cy * gw + cx;
58799
+ const o = (y * width + x) * 3;
58800
+ sum[cell * 3] = (sum[cell * 3] ?? 0) + (bytes[o] ?? 0);
58801
+ sum[cell * 3 + 1] = (sum[cell * 3 + 1] ?? 0) + (bytes[o + 1] ?? 0);
58802
+ sum[cell * 3 + 2] = (sum[cell * 3 + 2] ?? 0) + (bytes[o + 2] ?? 0);
58803
+ count[cell] = (count[cell] ?? 0) + 1;
58804
+ }
58805
+ }
58806
+ let total = 0;
58807
+ let cells = 0;
58808
+ for (let cell = 0; cell < gw * gh; cell++) {
58809
+ const n = count[cell] ?? 0;
58810
+ if (n === 0) continue;
58811
+ const r = (sum[cell * 3] ?? 0) / n;
58812
+ const g = (sum[cell * 3 + 1] ?? 0) / n;
58813
+ const b = (sum[cell * 3 + 2] ?? 0) / n;
58814
+ total += Math.max(r, g, b) - Math.min(r, g, b);
58815
+ cells += 1;
58816
+ }
58817
+ return cells === 0 ? 0 : total / cells;
58818
+ }
58592
58819
  //#endregion
58593
58820
  //#region src/pipeline-analytics/pipeline/package/package-scene-step.ts
58594
58821
  /**
@@ -58688,6 +58915,14 @@ function compareAfterLeave(prev, input) {
58688
58915
  minAreaFrac: input.minAreaFrac,
58689
58916
  occluders: input.occluders
58690
58917
  });
58918
+ if (diff.rejections.some((r) => r.reason === "lighting-mode-changed")) return {
58919
+ state: {
58920
+ ...rememberClear(prev, input).state,
58921
+ lastDiffAt: input.timestamp
58922
+ },
58923
+ sightings: [],
58924
+ rejections: diff.rejections
58925
+ };
58691
58926
  const blocked = diff.rejections.some((r) => r.reason === "overlaps-person" || r.reason === "overlaps-vehicle");
58692
58927
  if (diff.blobs.length === 0) {
58693
58928
  if (blocked) return {
@@ -58731,6 +58966,20 @@ function confirmPending(prev, input) {
58731
58966
  sightings: [],
58732
58967
  rejections: []
58733
58968
  };
58969
+ if (isIrCrop(prev.after) !== isIrCrop(input.crop)) return {
58970
+ state: {
58971
+ ...rememberClear(prev, input).state,
58972
+ lastDiffAt: input.timestamp
58973
+ },
58974
+ sightings: [],
58975
+ rejections: [{
58976
+ reason: "lighting-mode-changed",
58977
+ chroma: {
58978
+ before: prev.after.chromaSpread,
58979
+ after: input.crop.chromaSpread
58980
+ }
58981
+ }]
58982
+ };
58734
58983
  const sightings = [];
58735
58984
  const rejections = [];
58736
58985
  const still = [];
@@ -58777,7 +59026,7 @@ function judgeBlob(prev, input, blob) {
58777
59026
  bbox: blob.bbox
58778
59027
  } };
58779
59028
  const stable = meanAbsInBox(after, crop, blob.bbox) <= PACKAGE_SCENE_DIFF_STABLE_MEAN;
58780
- const present = meanAbsInBox(before, crop, blob.bbox) >= PACKAGE_SCENE_DIFF_LUMA_THRESHOLD;
59029
+ const present = isIrCrop(before) !== isIrCrop(crop) || meanAbsInBox(before, crop, blob.bbox) >= PACKAGE_SCENE_DIFF_LUMA_THRESHOLD;
58781
59030
  if (!stable || !present) return { rejection: {
58782
59031
  reason: "unstable",
58783
59032
  bbox: blob.bbox,
@@ -58945,7 +59194,8 @@ var PackageSceneCoordinator = class {
58945
59194
  const crop = {
58946
59195
  width: fetched.width,
58947
59196
  height: fetched.height,
58948
- luma: fetched.luma
59197
+ luma: fetched.luma,
59198
+ chromaSpread: fetched.chromaSpread
58949
59199
  };
58950
59200
  const stepped = stepPackageScene({
58951
59201
  deviceId,
@@ -59011,6 +59261,10 @@ var PackageSceneCoordinator = class {
59011
59261
  tags: { deviceId },
59012
59262
  meta: {
59013
59263
  reason: rejection.reason,
59264
+ ...rejection.chroma !== void 0 ? {
59265
+ chromaBefore: Number(rejection.chroma.before.toFixed(2)),
59266
+ chromaAfter: Number(rejection.chroma.after.toFixed(2))
59267
+ } : {},
59014
59268
  ...rejection.areaFrac !== void 0 ? { areaFrac: Number(rejection.areaFrac.toFixed(5)) } : {}
59015
59269
  }
59016
59270
  });
@@ -59119,14 +59373,8 @@ function planPackageSceneFrame(deviceId, input) {
59119
59373
  frameWidth: input.frameWidth,
59120
59374
  frameHeight: input.frameHeight
59121
59375
  });
59122
- const sceneRect = sceneZones.size === 0 ? null : packageZoneCropRect({
59123
- zones: input.zones,
59124
- packageZoneIds: sceneZones,
59125
- frameWidth: input.frameWidth,
59126
- frameHeight: input.frameHeight
59127
- });
59128
59376
  return {
59129
- sightings: sceneRect === null ? input.sightings : withoutSceneCenters(input, sceneRect),
59377
+ sightings: input.sightings,
59130
59378
  note: {
59131
59379
  deviceId,
59132
59380
  timestamp: input.timestamp,
@@ -59169,15 +59417,6 @@ function occludersOf(tracked) {
59169
59417
  }
59170
59418
  return out;
59171
59419
  }
59172
- function withoutSceneCenters(input, rect) {
59173
- if (input.frameWidth <= 0 || input.frameHeight <= 0) return input.sightings;
59174
- return input.sightings.filter((sighting) => !centerInside(sighting.bbox, rect, input));
59175
- }
59176
- function centerInside(bbox, rect, input) {
59177
- const cx = (bbox.x + bbox.w / 2) / input.frameWidth;
59178
- const cy = (bbox.y + bbox.h / 2) / input.frameHeight;
59179
- return cx >= rect.x && cx < rect.x + rect.w && cy >= rect.y && cy < rect.y + rect.h;
59180
- }
59181
59420
  //#endregion
59182
59421
  //#region src/pipeline-analytics/pipeline/package/package-llm-confirm.ts
59183
59422
  /**
@@ -71157,6 +71396,7 @@ Object.freeze({
71157
71396
  "addon-pages-source": ["listPages"],
71158
71397
  "addon-routes": ["getRoutes"],
71159
71398
  "addon-widgets-source": ["listWidgets"],
71399
+ "board-key-source": ["listSources"],
71160
71400
  "broker": ["list", "listProviders"],
71161
71401
  "calendar-source": ["listCalendars", "listEvents"],
71162
71402
  "custom-model-registry": ["listModels"],
@@ -71287,8 +71527,8 @@ Object.freeze({
71287
71527
  * ## Caps with no boolean level pass through untouched
71288
71528
  *
71289
71529
  * `EVENT_KIND_BY_CAP` covers sixteen caps; `SOURCE_CAP_ACTIVE_FIELD` covers the
71290
- * ten that have a boolean whose edge means something. `button`, `presence`,
71291
- * `enum-sensor` and `event-emitter` are NOT levels — their ARRIVAL is the
71530
+ * ten that have a boolean whose edge means something. `button`, `presence`
71531
+ * and `event-emitter` are NOT levels (`enum-sensor` is a stampless word level, see `UNSTAMPED_WORD_FIELD`) — their ARRIVAL is the
71292
71532
  * event, and three presses in a row are three markers. Gating them would
71293
71533
  * re-ship the failure of 2026-08-08, when camera 101's press produced a row, a
71294
71534
  * ring, a matched rule and no marker. (`doorbell` is not a level either, but
@@ -71414,6 +71654,15 @@ var WORD_LEVEL_FIELD = {
71414
71654
  * running for an hour at boot did not just start).
71415
71655
  */
71416
71656
  var BOOLEAN_LEVEL_FIELD = { "running-state": "running" };
71657
+ /**
71658
+ * Caps whose level is a WORD with NO change stamp: cap → field. `enum-sensor`
71659
+ * carries `{ value, lastFetchedAt }`, and `lastFetchedAt` moves on every
71660
+ * provider refresh, so it says nothing about when the word changed. Gated by
71661
+ * the word alone: the first sighting seeds, only a different word passes, and
71662
+ * a null / missing value is dropped and never remembered (planning#396: such
71663
+ * slices passed as `changed` many times a minute and could match an NC rule).
71664
+ */
71665
+ var UNSTAMPED_WORD_FIELD = { "enum-sensor": "value" };
71417
71666
  /** The word a word-level cap rests in — its `false`, for the first-sighting rule. */
71418
71667
  var WORD_LEVEL_INACTIVE = {
71419
71668
  cover: "closed",
@@ -71466,6 +71715,8 @@ var SensorTransitionGate = class {
71466
71715
  */
71467
71716
  observe(deviceId, capName, slice) {
71468
71717
  if (slice["lastChangedAt"] === 0) return this.drop(deviceId, capName, "cold-slice");
71718
+ const unstampedField = UNSTAMPED_WORD_FIELD[capName];
71719
+ if (unstampedField !== void 0) return this.observeUnstampedWord(deviceId, capName, slice, unstampedField);
71469
71720
  const wordField = WORD_LEVEL_FIELD[capName];
71470
71721
  if (wordField !== void 0) return this.observeWord(deviceId, capName, slice, wordField);
71471
71722
  const levelField = BOOLEAN_LEVEL_FIELD[capName];
@@ -71536,11 +71787,25 @@ var SensorTransitionGate = class {
71536
71787
  observeWord(deviceId, capName, slice, wordField) {
71537
71788
  const raw = slice[wordField];
71538
71789
  if (typeof raw !== "string" || raw.length === 0) return this.drop(deviceId, capName, "non-word-value");
71790
+ const stamp = slice["lastChangedAt"];
71791
+ if (typeof stamp !== "number" || !Number.isFinite(stamp) || stamp <= 0) return this.drop(deviceId, capName, "cold-slice");
71539
71792
  const key = `${deviceId}:${capName}`;
71540
71793
  const prior = this.lastWords.get(key);
71541
71794
  this.lastWords.set(key, raw);
71542
71795
  if (prior === void 0) return this.firstWordSighting(deviceId, capName, slice, raw);
71543
71796
  if (prior === raw) return this.drop(deviceId, capName, "no-change");
71797
+ if (stamp < this.startedAtMs) return this.drop(deviceId, capName, "baseline-seeded-stale-active");
71798
+ return { changed: true };
71799
+ }
71800
+ /** A word-level cap with no change stamp: only a DIFFERENT word is a transition. */
71801
+ observeUnstampedWord(deviceId, capName, slice, field) {
71802
+ const raw = slice[field];
71803
+ if (typeof raw !== "string" || raw.length === 0) return this.drop(deviceId, capName, "non-word-value");
71804
+ const key = `${deviceId}:${capName}`;
71805
+ const prior = this.lastWords.get(key);
71806
+ this.lastWords.set(key, raw);
71807
+ if (prior === void 0) return this.drop(deviceId, capName, "baseline-seeded-inactive");
71808
+ if (prior === raw) return this.drop(deviceId, capName, "no-change");
71544
71809
  return { changed: true };
71545
71810
  }
71546
71811
  /**
@@ -72685,7 +72950,7 @@ function buildGlobalSettingsSchema(cap) {
72685
72950
  type: "slider",
72686
72951
  key: "packageDropActivityWindowSec",
72687
72952
  label: "Deliverer window",
72688
- description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Motion elsewhere on the camera does not count. 0 disables the requirement.",
72953
+ description: "A delivery only counts if a person or vehicle was in the configured package drop-off zone within this many seconds before the package appeared — and a pick-up only with somebody in that zone when it vanished. Person-sized motion beside the drop-off zone also counts when nobody was tracked. Motion elsewhere on the camera does not count. 0 disables the requirement.",
72689
72954
  min: 0,
72690
72955
  max: 1800,
72691
72956
  step: 30,
@@ -81616,11 +81881,11 @@ function unique(values) {
81616
81881
  * therefore keeps the alarm's own sentence, which is also what every row
81617
81882
  * enqueued before this deploy carries.
81618
81883
  */
81619
- function combinedAlarmText(kind, source) {
81884
+ function combinedAlarmText(kind, source, followers = []) {
81620
81885
  if (kind !== "alarm-triggered" || source === void 0) return null;
81621
81886
  return {
81622
81887
  titleKey: source.className !== void 0 ? "alarm.combined.title" : "system.alarm-triggered.title",
81623
- bodyKey: "alarm.intrusion.body"
81888
+ bodyKey: [source, ...followers].some((ref) => ref.sensorChange !== void 0) ? "alarm.intrusion.body.withChanges" : "alarm.intrusion.body"
81624
81889
  };
81625
81890
  }
81626
81891
  /**
@@ -81635,7 +81900,7 @@ function combinedAlarmText(kind, source) {
81635
81900
  */
81636
81901
  function alarmTextKeys(transition, body) {
81637
81902
  const { kind, source } = transition;
81638
- const combined = combinedAlarmText(kind, source);
81903
+ const combined = combinedAlarmText(kind, source, transition.followers);
81639
81904
  if (combined !== null) return combined;
81640
81905
  const baseTitle = transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`;
81641
81906
  const titleKey = body?.titleKeySuffix !== void 0 ? `${baseTitle}.${body.titleKeySuffix}` : baseTitle;
@@ -87205,6 +87470,13 @@ var NC_STILL_WAIT_MAX_AGE_MS = 3e4;
87205
87470
  function canStillGainMedia(entry) {
87206
87471
  return entry.recordKind === "object-event";
87207
87472
  }
87473
+ /** The variables that carry a finished cycle's run summary. */
87474
+ var CYCLE_SUMMARY_VARS = [
87475
+ "duration",
87476
+ "energy",
87477
+ "afterDuration",
87478
+ "usedEnergy"
87479
+ ];
87208
87480
  var NcDispatcher = class {
87209
87481
  deps;
87210
87482
  targetCache = null;
@@ -88108,6 +88380,7 @@ var NcDispatcher = class {
88108
88380
  }
88109
88381
  /** Rules already warned about a deprecated variable in this process — once each. */
88110
88382
  deprecatedVarWarned = /* @__PURE__ */ new Set();
88383
+ cycleSummaryWarned = /* @__PURE__ */ new Set();
88111
88384
  /**
88112
88385
  * A rule template written by an older client may still say `{{camera}}`. It
88113
88386
  * renders (the alias), and the rule is named once so the operator can edit it.
@@ -88127,6 +88400,27 @@ var NcDispatcher = class {
88127
88400
  });
88128
88401
  }
88129
88402
  /**
88403
+ * A FINISHED-cycle row whose own template places none of the four cycle
88404
+ * variables delivers no run summary (D391: accepted, produced less). Said
88405
+ * once per rule; the operator's text is never touched.
88406
+ */
88407
+ warnCycleSummaryDropped(entry) {
88408
+ const deviceId = entry.payload.subject.applianceCycle?.deviceId;
88409
+ const template = entry.payload.template;
88410
+ if (deviceId === void 0 || template === void 0) return;
88411
+ if (this.cycleSummaryWarned.has(entry.ruleId)) return;
88412
+ const used = new Set([...templatePlaceholders(template.title ?? ""), ...templatePlaceholders(template.body ?? "")]);
88413
+ if (CYCLE_SUMMARY_VARS.some((name) => used.has(name))) return;
88414
+ this.cycleSummaryWarned.add(entry.ruleId);
88415
+ this.deps.logger.warn("appliance cycle finished but the rule template places no run summary — the message carries no duration or energy", {
88416
+ tags: {
88417
+ deviceId,
88418
+ ruleId: entry.ruleId
88419
+ },
88420
+ meta: { placeholders: [...CYCLE_SUMMARY_VARS] }
88421
+ });
88422
+ }
88423
+ /**
88130
88424
  * The facts of a finished appliance cycle; unknown on every other row and on
88131
88425
  * any failure (a part that cannot be read is omitted, never a blocked send).
88132
88426
  */
@@ -88171,6 +88465,7 @@ var NcDispatcher = class {
88171
88465
  const cycleFacts = await this.applianceCycleFacts(entry);
88172
88466
  const vars = buildTemplateVars(this.texts, entry, deviceName, zoneLabels, void 0, cycleFacts);
88173
88467
  this.warnDeprecatedVars(entry);
88468
+ this.warnCycleSummaryDropped(entry);
88174
88469
  const systemCount = systemEvent?.textCount !== void 0 ? { count: systemEvent.textCount } : {};
88175
88470
  const title = renderRowTitle(this.texts, entry, vars);
88176
88471
  const authoredBody = entry.payload.template?.body;
@@ -89429,10 +89724,31 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone, cycle
89429
89724
  latestVersion: typeof systemEvent?.data["latestVersion"] === "string" ? systemEvent.data["latestVersion"] : "",
89430
89725
  ...systemEvent?.deviceId !== void 0 ? { deviceName: String(systemEvent.deviceId) } : {},
89431
89726
  ...systemEvent?.nodeId !== void 0 && (systemEvent.kind === "node-online" || systemEvent.kind === "node-offline") ? { nodeName: systemEvent.nodeId } : {},
89727
+ ...systemEvent?.alarmSource !== void 0 ? { changes: alarmChangesText(texts, systemEvent.alarmSource, systemEvent.alarmFollowers) } : {},
89432
89728
  ...systemEventParamsForTemplate(systemEvent?.textParams)
89433
89729
  };
89434
89730
  }
89435
89731
  /**
89732
+ * `{{changes}}` of the combined alarm body: what each sensor of the incident did, in
89733
+ * arrival order and one phrase per device — "Laundry lock unlocked". The phrase is a
89734
+ * catalog key per kind and state (`alarm.change.<kind>.<state>`), then per state
89735
+ * (`alarm.change.<state>`), then the bare name: a state nobody worded never drops the device.
89736
+ */
89737
+ function alarmChangesText(texts, source, followers) {
89738
+ const phrases = [];
89739
+ for (const ref of [source, ...followers ?? []]) {
89740
+ const change = ref.sensorChange;
89741
+ if (change === void 0) continue;
89742
+ const key = [`alarm.change.${change.kind}.${change.state}`, `alarm.change.${change.state}`].find((candidate) => texts.has(candidate));
89743
+ const phrase = key === void 0 ? change.name : texts.text({
89744
+ key,
89745
+ vars: { device: change.name }
89746
+ });
89747
+ if (!phrases.includes(phrase)) phrases.push(phrase);
89748
+ }
89749
+ return phrases.join(", ");
89750
+ }
89751
+ /**
89436
89752
  * Which catalog key supplies the DEFAULT body for a row.
89437
89753
  *
89438
89754
  * A system event names its own at intake (the variant selection — profiled or
@@ -102153,7 +102469,10 @@ function buildProvider(host) {
102153
102469
  catalog: [...require_clip_model_registry.NC_CONDITION_CATALOG],
102154
102470
  taxonomy: require_clip_model_registry.NC_TAXONOMY
102155
102471
  }),
102156
- getTemplateCatalog: async () => ({ vars: require_clip_model_registry.servedTemplateVars() }),
102472
+ getTemplateCatalog: async () => ({
102473
+ vars: require_clip_model_registry.servedTemplateVars(),
102474
+ defaults: servedTemplateDefaults(host.texts)
102475
+ }),
102157
102476
  previewTemplate: async (input) => previewTemplate(host.texts, input),
102158
102477
  getHistory: async ({ filter }) => {
102159
102478
  const entries = await host.outbox.queryHistory({
@@ -105715,7 +106034,7 @@ function groupedAlarmPhaseKey(ruleId, trackId) {
105715
106034
  * combined alarm notification would then arrive with no picture, which is the
105716
106035
  * one thing it exists to fix.
105717
106036
  */
105718
- function fireRefFor(rule, subject, kind) {
106037
+ function fireRefFor(rule, subject, kind, sensorName) {
105719
106038
  const hasEventMedia = kind === "object-event" || kind === "package-event";
105720
106039
  const className = subject.classNames[0];
105721
106040
  const linkedOwners = subject.trackId === void 0 && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(rule.media)) : void 0;
@@ -105725,6 +106044,11 @@ function fireRefFor(rule, subject, kind) {
105725
106044
  ruleName: rule.name,
105726
106045
  deviceId: subject.deviceId,
105727
106046
  ...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
106047
+ ...subject.sensorKind !== void 0 && subject.sensorState !== void 0 && sensorName !== void 0 ? { sensorChange: {
106048
+ name: sensorName,
106049
+ kind: subject.sensorKind,
106050
+ state: subject.sensorState
106051
+ } } : {},
105728
106052
  recordId: subject.recordId,
105729
106053
  ...trackId !== void 0 ? { trackId } : {},
105730
106054
  ...linkedOwners !== void 0 && linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
@@ -105736,6 +106060,12 @@ function fireRefFor(rule, subject, kind) {
105736
106060
  ...subject.confidence !== void 0 ? { confidence: subject.confidence } : {}
105737
106061
  };
105738
106062
  }
106063
+ /** The directory name of the sensor a match was about; absent for a camera subject. */
106064
+ function sensorNameOf(host, subject) {
106065
+ if (subject.source !== "sensor") return void 0;
106066
+ const name = host.deviceDirectory.get(subject.sourceDeviceId ?? subject.deviceId)?.name.trim();
106067
+ return name !== void 0 && name.length > 0 ? name : void 0;
106068
+ }
105739
106069
  /**
105740
106070
  * Run this rule's onTrigger / onTriggerOff bindings for this match, and —
105741
106071
  * when the rule is gated on `armed_*` — auto-trigger the CamStack
@@ -105757,7 +106087,7 @@ function runRuleActions(host, rule, subject, kind, controls) {
105757
106087
  deviceId: subject.deviceId,
105758
106088
  bindings,
105759
106089
  lookup: (id) => host.sequences.get(id),
105760
- origin: fireRefFor(rule, subject, kind)
106090
+ origin: fireRefFor(rule, subject, kind, sensorNameOf(host, subject))
105761
106091
  });
105762
106092
  return phase;
105763
106093
  }
@@ -105781,7 +106111,7 @@ function autoTriggerAlarmPanel(host, rule, subject, kind) {
105781
106111
  });
105782
106112
  return NO_ALARM_PHASE;
105783
106113
  }
105784
- const origin = fireRefFor(rule, subject, kind);
106114
+ const origin = fireRefFor(rule, subject, kind, sensorNameOf(host, subject));
105785
106115
  const phase = panel.classifyRuleMatch(origin);
105786
106116
  try {
105787
106117
  panel.triggerFromRule(origin);
@@ -125090,6 +125420,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125090
125420
  this.packageDropDetector?.forgetDevice(data.deviceId);
125091
125421
  this.packageScene?.forget(data.deviceId);
125092
125422
  this.frameState.lastNonPackageActivityAt.delete(data.deviceId);
125423
+ this.frameState.lastPackageZoneMotionAt.delete(data.deviceId);
125093
125424
  this.frameState.packageCollectors.forgetDevice(data.deviceId);
125094
125425
  this.frameState.packageOversizeLogAt.delete(data.deviceId);
125095
125426
  this.frameState.packageLookFrames.delete(data.deviceId);
@@ -125114,6 +125445,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125114
125445
  this.packageDropDetector?.forgetDevice(deviceId);
125115
125446
  this.packageScene?.forget(deviceId);
125116
125447
  this.frameState.lastNonPackageActivityAt.delete(deviceId);
125448
+ this.frameState.lastPackageZoneMotionAt.delete(deviceId);
125117
125449
  this.frameState.packageCollectors.forgetDevice(deviceId);
125118
125450
  this.frameState.packageOversizeLogAt.delete(deviceId);
125119
125451
  this.frameState.packageLookFrames.delete(deviceId);
@@ -127686,6 +128018,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127686
128018
  detected,
127687
128019
  atMs: timestamp
127688
128020
  });
128021
+ if (detected) await this.stampPackageZoneMotion(deviceId, payload, timestamp);
127689
128022
  if (detected) this.eventStore?.noteMotionSample(deviceId, payload.regionCount, timestamp);
127690
128023
  await this.motionEpisodes?.observe(deviceId, "analyzer", detected, timestamp, {
127691
128024
  regionCount: payload.regionCount,
@@ -127704,6 +128037,28 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
127704
128037
  });
127705
128038
  }
127706
128039
  /**
128040
+ * Stamp analyzer motion that reached a package drop-off zone (D1003): the
128041
+ * courier who never became an L1 track still backs the OPEN of a candidate.
128042
+ * Onboard motion carries no regions or dimensions and never stamps.
128043
+ */
128044
+ async stampPackageZoneMotion(deviceId, payload, timestamp) {
128045
+ const proxy = await this.ensureProxy(deviceId);
128046
+ const packageZoneIds = collectPackageZoneIds(proxy?.state.zoneRules.value?.package ?? []);
128047
+ if (packageZoneIds.size === 0) return;
128048
+ const zoneRect = packageZoneCropRect({
128049
+ zones: proxy?.state.zones.value?.zones ?? [],
128050
+ packageZoneIds,
128051
+ frameWidth: payload.frameWidth,
128052
+ frameHeight: payload.frameHeight
128053
+ });
128054
+ if (delivererMotionNearPackageZone({
128055
+ regions: payload.regions,
128056
+ frameWidth: payload.frameWidth,
128057
+ frameHeight: payload.frameHeight,
128058
+ zoneRect
128059
+ })) this.frameState.lastPackageZoneMotionAt.set(deviceId, timestamp);
128060
+ }
128061
+ /**
127707
128062
  * Persist firmware-driven (onboard / device-activity) motion events into
127708
128063
  * the SAME episode tracker as {@link handleMotionAnalysis}, keyed per
127709
128064
  * `(deviceId, source)` so the two paths can never share — or fight over —
@@ -128036,6 +128391,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128036
128391
  encodeJpeg: request.encodeJpeg
128037
128392
  }, request.pin);
128038
128393
  }
128394
+ zoneMotionStamp(deviceId) {
128395
+ const at = this.frameState.lastPackageZoneMotionAt.get(deviceId);
128396
+ return at === void 0 ? {} : { lastDelivererMotionAt: at };
128397
+ }
128039
128398
  deliverPackageSceneSightings(delivery) {
128040
128399
  const settings = this.packageDropSettingsFromCache(delivery.deviceId);
128041
128400
  const lastActivityAt = this.frameState.lastNonPackageActivityAt.get(delivery.deviceId);
@@ -128049,6 +128408,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128049
128408
  minDwellMs: settings.packageDropDwellSec * 1e3,
128050
128409
  activityWindowMs: settings.packageDropActivityWindowSec * 1e3,
128051
128410
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {},
128411
+ ...this.zoneMotionStamp(delivery.deviceId),
128052
128412
  looked: false
128053
128413
  });
128054
128414
  this.bridgePackageStillness({
@@ -128234,6 +128594,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128234
128594
  minDwellMs: input.settings.packageDropDwellSec * 1e3,
128235
128595
  activityWindowMs: input.settings.packageDropActivityWindowSec * 1e3,
128236
128596
  ...lastActivityAt !== void 0 ? { lastActivityAt } : {},
128597
+ ...this.zoneMotionStamp(input.deviceId),
128237
128598
  ...lookEvidence
128238
128599
  });
128239
128600
  this.bridgePackageStillness({
@@ -128429,6 +128790,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128429
128790
  });
128430
128791
  }).catch(() => void 0);
128431
128792
  }
128793
+ for (const c of outcome.continued) this.ctx.logger.info("package candidate continued by a sub-floor box", {
128794
+ tags,
128795
+ meta: {
128796
+ entryId: c.id,
128797
+ hits: c.hits,
128798
+ score: c.score,
128799
+ via: input.via
128800
+ }
128801
+ });
128432
128802
  for (const u of outcome.earlyEvidenceUnbacked) this.ctx.logger.info("early pick-up evidence complete — nobody around, left to the TTL", {
128433
128803
  tags,
128434
128804
  meta: {
@@ -128456,6 +128826,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
128456
128826
  observedDwellMs,
128457
128827
  requiredDwellMs,
128458
128828
  activityBackedAtOpen: ex.activityBackedAtOpen,
128829
+ lastZoneMotionAt: this.frameState.lastPackageZoneMotionAt.get(input.deviceId) ?? null,
128459
128830
  reasons
128460
128831
  }
128461
128832
  });
@@ -129229,6 +129600,19 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
129229
129600
  }
129230
129601
  const verdict = this.sensorTransitions.observe(data.deviceId, data.capName, { ...slice });
129231
129602
  this.flushSensorTransitionDrops();
129603
+ if (verdict.changed) {
129604
+ const entries = Object.entries(slice);
129605
+ const field = (key) => entries.find(([k]) => k === key)?.[1];
129606
+ this.ctx.logger.info("sensor slice passed the transition gate", {
129607
+ tags: { deviceId: data.deviceId },
129608
+ meta: {
129609
+ capName: data.capName,
129610
+ state: field("state") ?? field("value") ?? field("entryOpen") ?? field("active") ?? null,
129611
+ lastChangedAt: field("lastChangedAt") ?? null,
129612
+ verdict: "changed"
129613
+ }
129614
+ });
129615
+ }
129232
129616
  return verdict.changed;
129233
129617
  }
129234
129618
  /**