@camstack/addon-post-analysis 1.2.241 → 1.2.243

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.
@@ -5359,7 +5359,7 @@ var ZodIssueCode = {
5359
5359
  var ZodFirstPartyTypeKind;
5360
5360
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5361
5361
  //#endregion
5362
- //#region ../types/dist/sleep-BEyvfshj.mjs
5362
+ //#region ../types/dist/sleep-B9qx0cEV.mjs
5363
5363
  /**
5364
5364
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5365
5365
  * window to float samples (D455).
@@ -7303,6 +7303,28 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7303
7303
  DeviceRole["GenericNotifier"] = "generic-notifier";
7304
7304
  return DeviceRole;
7305
7305
  }({});
7306
+ /**
7307
+ * Identity — preserves literal types for downstream inference.
7308
+ *
7309
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7310
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7311
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7312
+ * Shape validity is enforced separately by the `customAction(...)` helper
7313
+ * whose return type is already a `CustomActionSpec<...>`.
7314
+ */
7315
+ function defineCustomActions(spec) {
7316
+ return spec;
7317
+ }
7318
+ function customAction(input, output, options) {
7319
+ return {
7320
+ input,
7321
+ output,
7322
+ kind: options?.kind ?? "query",
7323
+ auth: options?.auth ?? "protected",
7324
+ scope: options?.scope ?? { kind: "system" },
7325
+ ...options?.caller ? { caller: "required" } : {}
7326
+ };
7327
+ }
7306
7328
  var StaticDirOutputSchema = object({ staticDir: string() });
7307
7329
  var VersionOutputSchema = object({ version: string() });
7308
7330
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12146,6 +12168,21 @@ var streamSignalsCapability = {
12146
12168
  mode: "singleton",
12147
12169
  deviceTypes: Object.values(DeviceType),
12148
12170
  runtimeState: StreamSignalsStatusSchema,
12171
+ /**
12172
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12173
+ *
12174
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12175
+ * not an operator choice: the provider re-declares it on every registration,
12176
+ * so losing it loses nothing and persisting it would freeze an answer the
12177
+ * camera is entitled to change. Measured the same day on the sibling case —
12178
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12179
+ * class the camera really detected stayed missing for the life of the row
12180
+ * because the fix could not reach it.
12181
+ *
12182
+ * See `RuntimeStateDurability`. Enforced by
12183
+ * `scripts/check-runtime-state-durability.ts`.
12184
+ */
12185
+ durability: "session",
12149
12186
  methods: {
12150
12187
  /**
12151
12188
  * What this device can emit. Empty is a valid and common answer — most
@@ -13124,28 +13161,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13124
13161
  content: string()
13125
13162
  })) }), { auth: "admin" });
13126
13163
  /**
13127
- * Identity — preserves literal types for downstream inference.
13128
- *
13129
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13130
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13131
- * the broader unions declared on `CustomActionSpec`'s default generics.
13132
- * Shape validity is enforced separately by the `customAction(...)` helper
13133
- * whose return type is already a `CustomActionSpec<...>`.
13134
- */
13135
- function defineCustomActions(spec) {
13136
- return spec;
13137
- }
13138
- function customAction(input, output, options) {
13139
- return {
13140
- input,
13141
- output,
13142
- kind: options?.kind ?? "query",
13143
- auth: options?.auth ?? "protected",
13144
- scope: options?.scope ?? { kind: "system" },
13145
- ...options?.caller ? { caller: "required" } : {}
13146
- };
13147
- }
13148
- /**
13149
13164
  * `custom-model-registry` — collection cap exposing operator-registered
13150
13165
  * custom detection models. Each provider (today: `addon-model-studio`)
13151
13166
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -46398,4 +46413,4 @@ function vectorDimFromBase64(encoded) {
46398
46413
  return Math.floor(Buffer.from(encoded, "base64").byteLength / 4);
46399
46414
  }
46400
46415
  //#endregion
46401
- export { TimelapseRuleSchema as $, boolean as $t, NcConditionDescriptorSchema as A, plateGalleryCapability as At, NcTaxonomySchema as B, videoclipsCapability as Bt, MOTION_CLOSE_AFTER_MS as C, isSourceCap as Ct, NC_DEFAULT_SNOOZE_MINUTES as D, parseProcStatus as Dt, NC_CONDITION_CATALOG as E, notificationRulesCapability as Et, NcScheduleSchema as F, sceneMonitorCapability as Ft, SCENE_CONFIRM_DEFAULT_TIMEOUT_MS as G, DeviceType as Gt, PoolMemoryWatchdog as H, errMsg as Ht, NcSnoozeInputSchema as I, storageOccupancyCapability as It, SCENE_DIVERGED as J, isDeviceScopedCap as Jt, SCENE_DEFAULT_ANCHOR_THRESHOLD as K, createEvent as Kt, NcSnoozeSchema as L, subKindsOf as Lt, NcRulePatchSchema as M, readTimelapseGeneratedAt as Mt, NcRuleSchema as N, resolveLocationMode as Nt, NC_SNOOZE_MAX_MINUTES as O, pickClusterStepModels as Ot, NcRuleTargetSchema as P, resolvePoolMemoryPolicy as Pt, TimelapseRulePatchSchema as Q, array as Qt, NcSnoozeSuppressedSchema as R, systemEventFilterApplies as Rt, MAX_BIRTH_LATENCY_PROXY_MS as S, isScheduleActive as St, NC_ALARM_SYSTEM_EVENT_KINDS as T, mayWriteToLocation as Tt, RECORDING_EXPORT_MAX_READ_BYTES as U, BaseAddon as Ut, OpsLogEntrySchema as V, zoneAnalyticsCapability as Vt, RetrainStatusSchema as W, CamProfileSchema as Wt, TIMELAPSE_DENSE_FLOOR_SEC as X, sleep as Xt, SceneMonitorSchema as Y, nodePin as Yt, TimelapseRuleInputSchema as Z, _enum as Zt, FULL_IMAGE_BBOX as _, failureContributionCapability as _t, CLUSTER_MODEL_SCOPED_STEPS as a, record as an, audioMetricsCapability as at, MAX_ARCHIVED_DEBUG_NOTES as b, isEventOwnerType as bt, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as c, EventCategory as cn, cosineSimilarity as ct, DETECTION_PIPELINE_CAP_NAME as d, deriveRecordingMode as dt, discriminatedUnion as en, TrackSourceSchema as et, DeclaredDevices as f, embeddingEncoderCapability as ft, FIRST_LEVEL_MACRO_CLASSES as g, faceGalleryCapability as gt, EVENT_PAD_MS as h, evictionPolicyOfLocation as ht, BirthDecisionRecordSchema as i, partialRecord as in, assertTimelapseCadences as it, NcRuleInputSchema as j, readDeviceStateFrom as jt, NC_TAXONOMY as k, pipelineAnalyticsCapability as kt, DEFAULT_TIMELAPSE_PREVIEW_TEXT as l, customAction as lt, EVENT_OWNER_TYPES as m, evaluateSensorEdge as mt, ArchivedDebugNoteSchema as n, number as nn, addonWidgetsSourceCapability as nt, COCO_TO_MACRO as o, string as on, audioModeOf as ot, EVENT_KIND_BY_CAP as p, encodeVectorBase64 as pt, SCENE_DEFAULT_UNCOVERED_POLICY as q, hydrateSchema as qt, BaseDevice as r, object as rn, alarmPanelCapability as rt, DEFAULT_EVENT_COLOR as s, unknown as sn, buildEventKindDescriptor as st, AUDIO_MACRO_LABELS as t, literal as tn, VISIT_MERGE_GAP_MS as tt, DETECTION_MACRO_CLASSES as u, defineCustomActions as ut, FailureCounters as v, hfModelUrl as vt, MediaFileKindEnum as w, kebabToCamel as wt, MAX_BIRTH_DECISION_RECORDS as x, isMediaOwnerType as xt, LabelAttributionSchema as y, isDetectionMacroClass as yt, NcSystemEventKindSchema as z, vectorDimFromBase64 as zt };
46416
+ export { TimelapseRuleSchema as $, boolean as $t, NcConditionDescriptorSchema as A, readTimelapseGeneratedAt as At, NcTaxonomySchema as B, errMsg as Bt, MOTION_CLOSE_AFTER_MS as C, mayWriteToLocation as Ct, NC_DEFAULT_SNOOZE_MINUTES as D, pipelineAnalyticsCapability as Dt, NC_CONDITION_CATALOG as E, pickClusterStepModels as Et, NcScheduleSchema as F, subKindsOf as Ft, SCENE_CONFIRM_DEFAULT_TIMEOUT_MS as G, customAction as Gt, PoolMemoryWatchdog as H, CamProfileSchema as Ht, NcSnoozeInputSchema as I, systemEventFilterApplies as It, SCENE_DIVERGED as J, isDeviceScopedCap as Jt, SCENE_DEFAULT_ANCHOR_THRESHOLD as K, defineCustomActions as Kt, NcSnoozeSchema as L, vectorDimFromBase64 as Lt, NcRulePatchSchema as M, resolvePoolMemoryPolicy as Mt, NcRuleSchema as N, sceneMonitorCapability as Nt, NC_SNOOZE_MAX_MINUTES as O, plateGalleryCapability as Ot, NcRuleTargetSchema as P, storageOccupancyCapability as Pt, TimelapseRulePatchSchema as Q, array as Qt, NcSnoozeSuppressedSchema as R, videoclipsCapability as Rt, MAX_BIRTH_LATENCY_PROXY_MS as S, kebabToCamel as St, NC_ALARM_SYSTEM_EVENT_KINDS as T, parseProcStatus as Tt, RECORDING_EXPORT_MAX_READ_BYTES as U, DeviceType as Ut, OpsLogEntrySchema as V, BaseAddon as Vt, RetrainStatusSchema as W, createEvent as Wt, TIMELAPSE_DENSE_FLOOR_SEC as X, sleep as Xt, SceneMonitorSchema as Y, nodePin as Yt, TimelapseRuleInputSchema as Z, _enum as Zt, FULL_IMAGE_BBOX as _, isDetectionMacroClass as _t, CLUSTER_MODEL_SCOPED_STEPS as a, record as an, audioMetricsCapability as at, MAX_ARCHIVED_DEBUG_NOTES as b, isScheduleActive as bt, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as c, EventCategory as cn, cosineSimilarity as ct, DETECTION_PIPELINE_CAP_NAME as d, encodeVectorBase64 as dt, discriminatedUnion as en, TrackSourceSchema as et, DeclaredDevices as f, evaluateSensorEdge as ft, FIRST_LEVEL_MACRO_CLASSES as g, hfModelUrl as gt, EVENT_PAD_MS as h, failureContributionCapability as ht, BirthDecisionRecordSchema as i, partialRecord as in, assertTimelapseCadences as it, NcRuleInputSchema as j, resolveLocationMode as jt, NC_TAXONOMY as k, readDeviceStateFrom as kt, DEFAULT_TIMELAPSE_PREVIEW_TEXT as l, deriveRecordingMode as lt, EVENT_OWNER_TYPES as m, faceGalleryCapability as mt, ArchivedDebugNoteSchema as n, number as nn, addonWidgetsSourceCapability as nt, COCO_TO_MACRO as o, string as on, audioModeOf as ot, EVENT_KIND_BY_CAP as p, evictionPolicyOfLocation as pt, SCENE_DEFAULT_UNCOVERED_POLICY as q, hydrateSchema as qt, BaseDevice as r, object as rn, alarmPanelCapability as rt, DEFAULT_EVENT_COLOR as s, unknown as sn, buildEventKindDescriptor as st, AUDIO_MACRO_LABELS as t, literal as tn, VISIT_MERGE_GAP_MS as tt, DETECTION_MACRO_CLASSES as u, embeddingEncoderCapability as ut, FailureCounters as v, isEventOwnerType as vt, MediaFileKindEnum as w, notificationRulesCapability as wt, MAX_BIRTH_DECISION_RECORDS as x, isSourceCap as xt, LabelAttributionSchema as y, isMediaOwnerType as yt, NcSystemEventKindSchema as z, zoneAnalyticsCapability as zt };
@@ -5390,7 +5390,7 @@ var ZodIssueCode = {
5390
5390
  var ZodFirstPartyTypeKind;
5391
5391
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5392
5392
  //#endregion
5393
- //#region ../types/dist/sleep-BEyvfshj.mjs
5393
+ //#region ../types/dist/sleep-B9qx0cEV.mjs
5394
5394
  /**
5395
5395
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5396
5396
  * window to float samples (D455).
@@ -7334,6 +7334,28 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7334
7334
  DeviceRole["GenericNotifier"] = "generic-notifier";
7335
7335
  return DeviceRole;
7336
7336
  }({});
7337
+ /**
7338
+ * Identity — preserves literal types for downstream inference.
7339
+ *
7340
+ * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
7341
+ * TypeScript does not widen each entry's literal `kind`/`auth` fields to
7342
+ * the broader unions declared on `CustomActionSpec`'s default generics.
7343
+ * Shape validity is enforced separately by the `customAction(...)` helper
7344
+ * whose return type is already a `CustomActionSpec<...>`.
7345
+ */
7346
+ function defineCustomActions(spec) {
7347
+ return spec;
7348
+ }
7349
+ function customAction(input, output, options) {
7350
+ return {
7351
+ input,
7352
+ output,
7353
+ kind: options?.kind ?? "query",
7354
+ auth: options?.auth ?? "protected",
7355
+ scope: options?.scope ?? { kind: "system" },
7356
+ ...options?.caller ? { caller: "required" } : {}
7357
+ };
7358
+ }
7337
7359
  var StaticDirOutputSchema = object({ staticDir: string() });
7338
7360
  var VersionOutputSchema = object({ version: string() });
7339
7361
  method(_void(), StaticDirOutputSchema, { auth: "admin" }), method(_void(), VersionOutputSchema, { auth: "admin" });
@@ -12177,6 +12199,21 @@ var streamSignalsCapability = {
12177
12199
  mode: "singleton",
12178
12200
  deviceTypes: Object.values(DeviceType),
12179
12201
  runtimeState: StreamSignalsStatusSchema,
12202
+ /**
12203
+ * Runtime-state durability: **session** — mirrored in RAM, never written.
12204
+ *
12205
+ * The slice holds what the DEVICE says it can emit. That is a probed fact,
12206
+ * not an operator choice: the provider re-declares it on every registration,
12207
+ * so losing it loses nothing and persisting it would freeze an answer the
12208
+ * camera is entitled to change. Measured the same day on the sibling case —
12209
+ * `native-object-detection.supportedClasses` was persisted, and a firmware
12210
+ * class the camera really detected stayed missing for the life of the row
12211
+ * because the fix could not reach it.
12212
+ *
12213
+ * See `RuntimeStateDurability`. Enforced by
12214
+ * `scripts/check-runtime-state-durability.ts`.
12215
+ */
12216
+ durability: "session",
12180
12217
  methods: {
12181
12218
  /**
12182
12219
  * What this device can emit. Empty is a valid and common answer — most
@@ -13155,28 +13192,6 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
13155
13192
  content: string()
13156
13193
  })) }), { auth: "admin" });
13157
13194
  /**
13158
- * Identity — preserves literal types for downstream inference.
13159
- *
13160
- * The constraint is `Record<string, unknown>` (not `CustomActionsSpec`) so
13161
- * TypeScript does not widen each entry's literal `kind`/`auth` fields to
13162
- * the broader unions declared on `CustomActionSpec`'s default generics.
13163
- * Shape validity is enforced separately by the `customAction(...)` helper
13164
- * whose return type is already a `CustomActionSpec<...>`.
13165
- */
13166
- function defineCustomActions(spec) {
13167
- return spec;
13168
- }
13169
- function customAction(input, output, options) {
13170
- return {
13171
- input,
13172
- output,
13173
- kind: options?.kind ?? "query",
13174
- auth: options?.auth ?? "protected",
13175
- scope: options?.scope ?? { kind: "system" },
13176
- ...options?.caller ? { caller: "required" } : {}
13177
- };
13178
- }
13179
- /**
13180
13195
  * `custom-model-registry` — collection cap exposing operator-registered
13181
13196
  * custom detection models. Each provider (today: `addon-model-studio`)
13182
13197
  * contributes a list of `CustomModelDescriptor`s; the hub auto-concatenates
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DjW7k4lv.js");
5
+ const require_dist = require("../dist-DxZAf0Hs.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs);
8
8
  let node_path = require("node:path");
@@ -1,4 +1,4 @@
1
- import { Dt as parseProcStatus, H as PoolMemoryWatchdog, Pt as resolvePoolMemoryPolicy, Ut as BaseAddon, ft as embeddingEncoderCapability, vt as hfModelUrl } from "../dist-CCw4SKRp.mjs";
1
+ import { H as PoolMemoryWatchdog, Mt as resolvePoolMemoryPolicy, Tt as parseProcStatus, Vt as BaseAddon, gt as hfModelUrl, ut as embeddingEncoderCapability } from "../dist-C-1xTyB-.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import * as path$1 from "node:path";
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.193",
21
+ version: "1.2.195",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.158",
36
+ version: "1.2.160",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.193",
39
+ version: "1.2.195",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.158",
84
+ version: "1.2.160",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-DjW7k4lv.js");
5
+ const require_dist = require("../dist-DxZAf0Hs.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -55625,7 +55625,8 @@ function buildOverlayDetections(input) {
55625
55625
  ...det,
55626
55626
  track: {
55627
55627
  trackId: t.trackId,
55628
- zones: t.zones
55628
+ zones: t.zones,
55629
+ ...t.state !== void 0 ? { movementState: t.state } : {}
55629
55630
  },
55630
55631
  labels: hasName && !alreadyLabeled ? [...det.labels, {
55631
55632
  label: name,
@@ -57583,6 +57584,10 @@ function centroidInside(b, p) {
57583
57584
  }
57584
57585
  /** Parked-box coverage (intersection / entry area) that counts as swallowed. */
57585
57586
  var SWALLOW_MIN_COVERAGE = .5;
57587
+ /** Fraction of a detection's OWN area that must lie inside a parked entry for
57588
+ * the entry to swallow it. Deliberately near-total: a fragment of a parked
57589
+ * object does not stick out of it. */
57590
+ var FRAGMENT_MIN_CONTAINMENT = .9;
57586
57591
  function isSceneSizedBox(bbox, frameWidth, frameHeight) {
57587
57592
  const frame = frameWidth * frameHeight;
57588
57593
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
@@ -57602,6 +57607,29 @@ function detectionSwallowsEntry(det, parked) {
57602
57607
  if (!centroidInside(det, centroid(parked))) return false;
57603
57608
  return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
57604
57609
  }
57610
+ /**
57611
+ * True when `det` is a FRAGMENT of the parked object — much smaller than the
57612
+ * entry and sitting (almost) wholly inside it.
57613
+ *
57614
+ * The mirror of {@link detectionSwallowsEntry}, and the case neither IoU
57615
+ * threshold can see. Measured on 617: a 34x137 box inside a 452x420 parked
57616
+ * motorcycle scores IoU 0.024 — BELOW `wakeAssocIou` (0.1), so the filter read
57617
+ * it as "no detection overlaps this entry" and let it open a track, accumulate
57618
+ * its own evidence and promote into a registry entry of its own. The camera
57619
+ * then reported two parked vehicles where there was one motorcycle.
57620
+ *
57621
+ * The size bound is the reciprocal of {@link SWALLOW_AREA_RATIO}, so the two
57622
+ * swallow rules meet without overlapping: a box between 1/1.35 and 1.35 of the
57623
+ * entry's area, contained, already scores IoU >= 0.74 and is answered by
57624
+ * `suppressIou`. Nothing is a fragment and a near-identical box at once.
57625
+ */
57626
+ function entrySwallowsDetection(parked, det) {
57627
+ const entryArea = parked.w * parked.h;
57628
+ const detArea = det.w * det.h;
57629
+ if (entryArea <= 0 || detArea <= 0) return false;
57630
+ if (detArea > entryArea / 1.35) return false;
57631
+ return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
57632
+ }
57605
57633
  function parkedEntryHoldsDetection(entry, detBbox, config) {
57606
57634
  const overlap = iou(entry.bbox, detBbox);
57607
57635
  if (overlap >= config.suppressIou) return {
@@ -57616,6 +57644,10 @@ function parkedEntryHoldsDetection(entry, detBbox, config) {
57616
57644
  strong: false,
57617
57645
  holds: false
57618
57646
  };
57647
+ if (entrySwallowsDetection(entry.bbox, detBbox)) return {
57648
+ strong: false,
57649
+ holds: true
57650
+ };
57619
57651
  return {
57620
57652
  strong: false,
57621
57653
  holds: overlap >= config.wakeAssocIou && centroidInside(entry.bbox, centroid(detBbox))
@@ -57634,6 +57666,34 @@ function bboxMatchesParkedEntry(bbox, className, entries, suppressIou = SUPPRESS
57634
57666
  }
57635
57667
  return false;
57636
57668
  }
57669
+ /**
57670
+ * The live entry already censusing this spot, if any — the ONE answer to "is
57671
+ * this place taken?", asked at BIRTH.
57672
+ *
57673
+ * It is deliberately the same predicate the per-frame filter uses
57674
+ * ({@link parkedEntryHoldsDetection}): a box the filter would suppress as a
57675
+ * parked object's own must not be able to become a second parked object. Device
57676
+ * 617 held four rows for one car and a fifth for a fragment of a motorcycle
57677
+ * because the birth path asked nothing at all.
57678
+ */
57679
+ function findParkedOccupant(entries, className, bbox, suppressIou = SUPPRESS_IOU, wakeAssocIou = WAKE_ASSOC_IOU) {
57680
+ const cls = normaliseStationaryClass(className);
57681
+ const config = {
57682
+ suppressIou,
57683
+ wakeAssocIou
57684
+ };
57685
+ let best;
57686
+ for (const e of entries) {
57687
+ if (normaliseStationaryClass(e.className) !== cls) continue;
57688
+ if (!parkedEntryHoldsDetection(e, bbox, config).holds) continue;
57689
+ const overlap = iou(e.bbox, bbox);
57690
+ if (best === void 0 || overlap > best.overlap) best = {
57691
+ entry: e,
57692
+ overlap
57693
+ };
57694
+ }
57695
+ return best?.entry;
57696
+ }
57637
57697
  /** Best-shot veto: current crop is a parked object the trail did not start on. */
57638
57698
  function thumbnailWouldShowParkedNotSubject(input) {
57639
57699
  if (!bboxMatchesParkedEntry(input.currentBbox, input.className, input.entries)) return false;
@@ -58619,85 +58679,6 @@ var StartedBirthRegistry = class {
58619
58679
  //#endregion
58620
58680
  //#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
58621
58681
  /**
58622
- * stationary-evidence — how a parked object EARNS a registry entry.
58623
- *
58624
- * ## The criterion this replaces, and why it was measuring the wrong thing
58625
- *
58626
- * Until 2026-08-28 an object became stationary by TRACK CONTINUITY: one track
58627
- * had to stay alive and barely move for `PROMOTION_WINDOW_MS` (30 s). That
58628
- * rewards an object that is *seen well and often* — precisely the opposite of
58629
- * what the operator needs. Measured on device 617 (the parking lot), 24 h:
58630
- *
58631
- * - three cars are parked in view; the registry held exactly ONE entry;
58632
- * - the two it missed were re-discovered as **140 and 112 separate tracks**,
58633
- * median life **6.8 s**, p90 29 s. Not one of them could ever reach 30 s.
58634
- * - those two objects alone produced **13 494 of 18 402** vehicle
58635
- * observations (73%) — the whole re-spawn flood the registry exists to stop.
58636
- *
58637
- * The track dies; the OBJECT does not. So the evidence must outlive the track.
58638
- *
58639
- * ## The new criterion: a RATIO, not a count
58640
- *
58641
- * "The same thing, in the same place, most of the times it was seen."
58642
- *
58643
- * N — observations that AGREED with the candidate's anchor box (IoU ≥
58644
- * {@link StationaryEvidencePolicy.agreeIou}).
58645
- * M — observations OFFERED by a track once it bound to this candidate,
58646
- * agreeing or not.
58647
- *
58648
- * A count would rescue nothing: a badly-lit car at night is seen rarely, and
58649
- * any floor high enough to exclude a passer-by would exclude it too. A RATIO
58650
- * asks a question a rarely-seen object can still answer well — 8 sightings out
58651
- * of 10 in the same spot is stationary even though 10 is a small number.
58652
- *
58653
- * The dilution is the whole point, and it is why M counts a bound track's
58654
- * observations even after it walks away: a car that drives THROUGH a spot binds
58655
- * for a few frames and then contributes dozens of disagreeing ones. Measured on
58656
- * 617 the two populations do not overlap:
58657
- *
58658
- * | candidate | N / M | ratio |
58659
- * | ---------------------------- | ----------- | ----- |
58660
- * | parked car A (missed today) | 6405 / 6467 | 0.99 |
58661
- * | parked car B (missed today) | 343 / 343 | 1.00 |
58662
- * | car crossing the lot | 5 / 70 | 0.07 |
58663
- * | car turning through the lot | 14 / 91 | 0.15 |
58664
- * | car manoeuvring | 12 / 41 | 0.29 |
58665
- *
58666
- * The threshold sits in the empty band between 0.29 and 0.99.
58667
- *
58668
- * ## The span is what a ratio cannot say
58669
- *
58670
- * A ratio alone promotes a car that paused for four seconds — its handful of
58671
- * observations all agree. So evidence must also STRETCH: the agreeing
58672
- * observations must span {@link StationaryEvidencePolicy.minSpanMs}. Measured
58673
- * on 617, real parked cars span 2 min to 24 h; the two momentary stops in the
58674
- * same day spanned 4.2 s and 8.0 s. Setting the span to
58675
- * `DEFAULT_MAX_SESSION_HOLD_MS` also buys a structural guarantee: no single
58676
- * motion session, however long it is held open, can promote on its own.
58677
- *
58678
- * ## ONE ledger, two producers
58679
- *
58680
- * Evidence is anchored to a BOX, not to a track and not to a session, so an
58681
- * object confirmed half by an active motion session and half by a periodic
58682
- * burst still promotes. Occasion A (the session) is implemented and is
58683
- * sufficient on its own — it is free, because the pipeline is already decoding
58684
- * and already inferring. Occasion B (the burst) is a precaution for the cases
58685
- * where A could not have fired at all (hub restarted, camera offline, motion
58686
- * detection failed, object already parked at boot); it is NOT implemented yet,
58687
- * and {@link StationaryEvidenceSource} is the seam it will arrive through.
58688
- *
58689
- * ## Identity is IoU. There is no grid.
58690
- *
58691
- * The predecessor mechanism (`phantom-cells.ts`) quantises a point to a 32 px
58692
- * cell, and its write path passes a CENTROID while its read path passes a
58693
- * top-left CORNER — at 32 px every box bigger than 32 px lands in a different
58694
- * cell on read, and its unit test never crosses that boundary because it calls
58695
- * both halves with the same literal coordinates. This module has no such
58696
- * boundary to get wrong: a candidate is found by overlap against the boxes a
58697
- * device already holds, bounded by {@link EVIDENCE_MAX_CANDIDATES_PER_DEVICE}.
58698
- * Device 617 — the busiest lot in the fleet — holds 8.
58699
- */
58700
- /**
58701
58682
  * `vehicle` and its kin. Every number below is read off device 617 over 24 h
58702
58683
  * (18 402 vehicle observations, 290 tracks) with the three parked cars the
58703
58684
  * operator can see out of the window as the bench.
@@ -58719,11 +58700,11 @@ var VEHICLE_EVIDENCE_POLICY = {
58719
58700
  agreeIou: .5,
58720
58701
  minAgreed: 8,
58721
58702
  minAgreementRatio: .6,
58722
- minSpanMs: 12e4,
58723
- promotesByDefault: true
58703
+ minSpanMs: 12e4
58724
58704
  };
58725
58705
  /**
58726
- * `person`, and it ships OFF.
58706
+ * `person`, promoted like every other class since 2026-09-14 — and still the
58707
+ * strictest numbers in the file, because these are MEASURED, not cautious.
58727
58708
  *
58728
58709
  * This is not caution, it is a measurement. Replaying 24 h of person
58729
58710
  * observations from devices 617, 1443, 590 and 592 through this exact ledger:
@@ -58742,17 +58723,24 @@ var VEHICLE_EVIDENCE_POLICY = {
58742
58723
  *
58743
58724
  * Promoting a person is a BLINDING decision (D49 read in the direction that
58744
58725
  * destroys work: for a car the destructive read is the RETIREMENT — the flood
58745
- * comes back; for a person it is the BIRTH — we stop seeing them). So the
58746
- * class is available, tuned as strictly as the data allows, and switched off
58747
- * until an operator turns it on for a camera where they know what is standing
58748
- * there.
58726
+ * comes back; for a person it is the BIRTH — we stop seeing them). It used to
58727
+ * ship OFF behind a per-camera switch for that reason; the switch is gone —
58728
+ * an opt-in nobody turns on produces nothing, and it was producing nothing.
58729
+ *
58730
+ * What makes that affordable now is that agreement is no longer overlap alone:
58731
+ * `observationAgrees` requires the TRACKER to call the subject still, so a
58732
+ * person who merely pauses inside the anchor no longer feeds the candidate.
58733
+ * The statue and the poster still promote — which is what you want, they ARE
58734
+ * furniture — while the real person who stops for a moment no longer does.
58735
+ *
58736
+ * The thresholds stay where the measurement put them. They are the difference
58737
+ * between 22 promotions a day and 10.
58749
58738
  */
58750
58739
  var PERSON_EVIDENCE_POLICY = {
58751
58740
  agreeIou: .5,
58752
58741
  minAgreed: 40,
58753
58742
  minAgreementRatio: .95,
58754
- minSpanMs: 18e5,
58755
- promotesByDefault: false
58743
+ minSpanMs: 18e5
58756
58744
  };
58757
58745
  /** Classes with no policy of their own fall back to the vehicle numbers —
58758
58746
  * `animal`, `package` and friends are all "an object that ended up somewhere",
@@ -58771,7 +58759,7 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
58771
58759
  * Pure, and ordered cheapest-question-first so the reason a candidate was
58772
58760
  * refused is the FIRST thing it failed, not the last.
58773
58761
  */
58774
- function evaluateStationaryEvidence(row, policy, classEnabled) {
58762
+ function evaluateStationaryEvidence(row, policy) {
58775
58763
  const windowSeen = row.windowSeen + row.prevWindowSeen;
58776
58764
  const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
58777
58765
  const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
@@ -58782,11 +58770,6 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
58782
58770
  ratio,
58783
58771
  spanMs
58784
58772
  };
58785
- if (!classEnabled) return {
58786
- kind: "accumulating",
58787
- shortfall: "class-not-promoted",
58788
- ...base
58789
- };
58790
58773
  if (row.agreed < policy.minAgreed) return {
58791
58774
  kind: "accumulating",
58792
58775
  shortfall: "too-few-agreements",
@@ -58861,6 +58844,32 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
58861
58844
  };
58862
58845
  }
58863
58846
  /**
58847
+ * Does this observation count as EVIDENCE that the object is parked?
58848
+ *
58849
+ * Two questions, deliberately kept apart:
58850
+ *
58851
+ * - IDENTITY — is this the same object in the same place? That is the IoU,
58852
+ * and it is the only notion of sameness in this module.
58853
+ * - STILLNESS — is it going anywhere? That is the TRACKER's verdict, and
58854
+ * nothing here re-derives it.
58855
+ *
58856
+ * Before this, `agrees` was the IoU alone, so a long run of overlaps WAS the
58857
+ * stillness argument — a second derivation of a fact `StateAnalyzer` already
58858
+ * computes per track, slower and able to disagree with it. Now the ledger adds
58859
+ * only what the analyzer cannot know: that the same still object has been here
58860
+ * across a succession of tracks.
58861
+ *
58862
+ * `entering` is the tracker's undecided window — five frames that begin EVERY
58863
+ * track, including a parked car's re-detection. It is not disagreement, and
58864
+ * counting it as such would punish a candidate for the tracker's own warm-up.
58865
+ * An ABSENT verdict likewise falls back to overlap alone: a path that never had
58866
+ * an opinion must not be silently reclassified.
58867
+ */
58868
+ function observationAgrees(overlap, agreeIou, movementState) {
58869
+ if (overlap < agreeIou) return false;
58870
+ return movementState !== "moving" && movementState !== "left";
58871
+ }
58872
+ /**
58864
58873
  * Fold one observation into a candidate.
58865
58874
  *
58866
58875
  * `agrees` is decided by the CALLER, because M is what makes the ratio mean
@@ -59163,7 +59172,7 @@ var StationaryEvidenceLedger = class {
59163
59172
  * slow or failing store can never change what this frame decides. The
59164
59173
  * durable half is fired behind the verdict.
59165
59174
  */
59166
- observe(observation, classEnabled) {
59175
+ observe(observation) {
59167
59176
  const policy = evidencePolicyFor(observation.className);
59168
59177
  this.expire(observation.deviceId, observation.timestamp);
59169
59178
  const bindKey = observation.trackId === void 0 ? void 0 : `${observation.deviceId}:${observation.trackId}`;
@@ -59211,18 +59220,18 @@ var StationaryEvidenceLedger = class {
59211
59220
  this.capDevice(observation.deviceId);
59212
59221
  return {
59213
59222
  row,
59214
- verdict: evaluateStationaryEvidence(row, policy, classEnabled),
59223
+ verdict: evaluateStationaryEvidence(row, policy),
59215
59224
  promoted: false
59216
59225
  };
59217
59226
  }
59218
59227
  if (bindKey !== void 0) this.binding.set(bindKey, row.key);
59219
59228
  }
59220
- const agrees = overlap >= policy.agreeIou;
59229
+ const agrees = observationAgrees(overlap, policy.agreeIou, observation.movementState);
59221
59230
  const next = foldObservation(row, observation, agrees, overlap);
59222
59231
  this.ledger.stage(next);
59223
59232
  this.dirty.add(next.key);
59224
59233
  if (created) this.capDevice(observation.deviceId);
59225
- const verdict = evaluateStationaryEvidence(next, policy, classEnabled);
59234
+ const verdict = evaluateStationaryEvidence(next, policy);
59226
59235
  if (verdict.kind !== "promote") return {
59227
59236
  row: next,
59228
59237
  verdict,
@@ -59670,8 +59679,32 @@ var StationaryObjectRegistry = class {
59670
59679
  });
59671
59680
  }
59672
59681
  }
59673
- /** Promote a parked track into a persisted stationary entry. */
59674
- async promote(entry) {
59682
+ /**
59683
+ * Promote a parked track into a persisted stationary entry — or, when the
59684
+ * spot is already censused, CONFIRM the entry sitting on it.
59685
+ *
59686
+ * The occupancy question is asked here and nowhere else, with the same
59687
+ * predicate the per-frame filter uses (`findParkedOccupant`): a box the
59688
+ * filter would suppress as a parked object's own can never become a second
59689
+ * parked object. Until 2026-09-14 this method minted whatever it was handed,
59690
+ * and device 617 reported EIGHT parked vehicles — four of them one car, the
59691
+ * same box to the decimal, promoted again roughly once an hour because the
59692
+ * weak-continuation path (D410) keeps feeding the ledger from a track riding
59693
+ * a parked object. The re-promotion is not the anomaly and is not worth
59694
+ * suppressing: it is a fresh, independent confirmation that the thing is
59695
+ * still there. It just must not produce a neighbour.
59696
+ *
59697
+ * A merge never DOWNGRADES the tenant: it advances the clock and adopts an
59698
+ * identity the tenant lacks (the 617 duplicates arrived unlabelled while the
59699
+ * row they belonged to was already `Car`), and it never overwrites one it
59700
+ * has.
59701
+ */
59702
+ async promote(entry, config) {
59703
+ const occupant = findParkedOccupant(this.list(entry.deviceId), entry.className, entry.bbox, (config ?? this.matchConfig).suppressIou, (config ?? this.matchConfig).wakeAssocIou);
59704
+ if (occupant !== void 0) return {
59705
+ outcome: "merged",
59706
+ entry: this.absorbInto(occupant, entry)
59707
+ };
59675
59708
  this.indexEntry(entry.deviceId, entry.id);
59676
59709
  this.dirty.delete(entry.id);
59677
59710
  await this.ledger.put(entry);
@@ -59680,6 +59713,33 @@ var StationaryObjectRegistry = class {
59680
59713
  entry,
59681
59714
  timestamp: entry.becameStationaryAt
59682
59715
  });
59716
+ return {
59717
+ outcome: "born",
59718
+ entry
59719
+ };
59720
+ }
59721
+ /** Fold a refused promotion into the entry that already holds its spot. */
59722
+ absorbInto(occupant, refused) {
59723
+ const merged = {
59724
+ ...occupant,
59725
+ lastConfirmedAt: Math.max(occupant.lastConfirmedAt, refused.becameStationaryAt),
59726
+ ...occupant.label === void 0 && refused.label !== void 0 ? { label: refused.label } : {},
59727
+ ...occupant.keyFrameMediaId === void 0 && refused.keyFrameMediaId !== void 0 ? { keyFrameMediaId: refused.keyFrameMediaId } : {}
59728
+ };
59729
+ this.ledger.stage(merged);
59730
+ this.dirty.add(merged.id);
59731
+ this.logger.info("stationary promotion absorbed — the spot is already censused", {
59732
+ tags: { deviceId: occupant.deviceId },
59733
+ meta: {
59734
+ deviceId: occupant.deviceId,
59735
+ entryId: occupant.id,
59736
+ className: occupant.className,
59737
+ refusedTrackId: refused.sourceTrackId,
59738
+ parkedForMs: refused.becameStationaryAt - occupant.becameStationaryAt,
59739
+ gainedLabel: occupant.label === void 0 && refused.label !== void 0
59740
+ }
59741
+ });
59742
+ return merged;
59683
59743
  }
59684
59744
  /**
59685
59745
  * Retire entries unconfirmed for longer than the TTL of OBSERVED time, and
@@ -69950,18 +70010,6 @@ var StationarySettingsSchema = require_dist.object({
69950
70010
  * session open (also the minimum track age). Creates nothing — birth is
69951
70011
  * decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
69952
70012
  sessionStillnessWindowMs: require_dist.number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
69953
- /**
69954
- * Let a motionless PERSON become a stationary entry on this camera.
69955
- *
69956
- * Off, and it is a measurement rather than caution. Replaying 24 h of person
69957
- * observations from devices 617/1443/590/592 through the evidence ledger
69958
- * produces 6-22 entries a day depending on how strictly it is tuned, and none
69959
- * of them can be tuned to zero: device 592 alone yielded eleven candidates at
69960
- * a perfect agreement ratio between 02:00 and 05:00. A stationary entry
69961
- * SUPPRESSES its object, so a wrong one makes the camera blind to a person —
69962
- * turn this on only for a camera where you know what is standing there.
69963
- */
69964
- promotePersons: require_dist.boolean().default(false),
69965
70013
  /** Observed-time TTL: retire an entry unconfirmed for this long (measured on
69966
70014
  * frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
69967
70015
  entryTtlMs: require_dist.number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
@@ -69981,7 +70029,6 @@ function resolveStationarySettings(raw) {
69981
70029
  suppressIou: pick("suppressIou"),
69982
70030
  wakeAssocIou: pick("wakeAssocIou"),
69983
70031
  sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
69984
- promotePersons: pick("promotePersons"),
69985
70032
  entryTtlMs: pick("entryTtlMs")
69986
70033
  };
69987
70034
  }
@@ -70517,13 +70564,6 @@ function buildDetectionSettingsSections() {
70517
70564
  unit: "s",
70518
70565
  displayScale: 1e3
70519
70566
  },
70520
- {
70521
- type: "boolean",
70522
- key: "promotePersons",
70523
- label: "Also park motionless people",
70524
- description: "Off by default. A stationary entry SUPPRESSES its object, so a wrong one makes this camera blind to a person standing there. Replaying a day of real person detections produced entries on every camera tried, at every threshold tried — turn this on only where you know what is standing still.",
70525
- default: STATIONARY_DEFAULTS.promotePersons
70526
- },
70527
70567
  {
70528
70568
  type: "slider",
70529
70569
  key: "entryTtlMs",
@@ -73867,11 +73907,7 @@ var ZoneAnalyticsProvider = class {
73867
73907
  });
73868
73908
  }
73869
73909
  this.snapshots.set(input.deviceId, snapshot);
73870
- const noteOccupancy = this.ctx.onOccupancySample;
73871
- if (noteOccupancy !== void 0) {
73872
- noteOccupancy(input.deviceId, total, input.timestamp);
73873
- for (const zone of snapshot.zones) noteOccupancy(input.deviceId, zone.totalObjects, input.timestamp, zone.zoneId);
73874
- }
73910
+ this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
73875
73911
  this.appendHistory(input.deviceId, snapshot);
73876
73912
  this.emitSnapshot(input.deviceId, snapshot);
73877
73913
  this.sliceThrottle.push(input.deviceId, snapshot);
@@ -73938,11 +73974,30 @@ var ZoneAnalyticsProvider = class {
73938
73974
  });
73939
73975
  if (snapshot) {
73940
73976
  this.appendHistory(deviceId, snapshot);
73977
+ this.noteLtsSamples(deviceId, snapshot, now);
73941
73978
  this.emitSnapshot(deviceId, snapshot);
73942
73979
  this.sliceThrottle.push(deviceId, snapshot);
73943
73980
  }
73944
73981
  }
73945
73982
  }
73983
+ /**
73984
+ * Append this snapshot to the long-term occupancy series — frame-wide first
73985
+ * (`scope: ''`), then one sample per zone.
73986
+ *
73987
+ * Shared by BOTH producers on purpose. It used to live inline in
73988
+ * `recordFrame`, so the baseline sampler — the only producer a camera with no
73989
+ * frames flowing has — wrote the RAM ring, the NC watcher and the state slice
73990
+ * but never LTS. The long-term series therefore had a hole exactly where the
73991
+ * scene was nothing but parked objects: the one span an occupancy history is
73992
+ * asked about. The RAM ring showed a flat line over the same minutes, which
73993
+ * is what made the gap invisible.
73994
+ */
73995
+ noteLtsSamples(deviceId, snapshot, timestamp) {
73996
+ const note = this.ctx.onOccupancySample;
73997
+ if (note === void 0) return;
73998
+ note(deviceId, snapshot.frame.totalObjects, timestamp);
73999
+ for (const zone of snapshot.zones) note(deviceId, zone.totalObjects, timestamp, zone.zoneId);
74000
+ }
73946
74001
  /** Push one snapshot to the occupancy tap, isolating any consumer throw from
73947
74002
  * the frame path (occupancy notifications must never break analytics). */
73948
74003
  emitSnapshot(deviceId, snapshot) {
@@ -78277,7 +78332,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78277
78332
  }
78278
78333
  });
78279
78334
  }
78280
- await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps);
78335
+ await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps, data.occasion ?? "session");
78281
78336
  }
78282
78337
  /**
78283
78338
  * Run one detection frame through the analysis layers for a given
@@ -78287,7 +78342,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78287
78342
  * tracking/zone/event state never crosses between sources. Emits the
78288
78343
  * SAME canonical events, distinguished only by `source`.
78289
78344
  */
78290
- async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps) {
78345
+ async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
78291
78346
  if (this.shuttingDown) return;
78292
78347
  if (!await this.bindingCache.isActive(deviceId)) {
78293
78348
  const now = Date.now();
@@ -78695,7 +78750,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78695
78750
  const evidence = this.stationaryEvidence;
78696
78751
  for (const t of result.tracked) {
78697
78752
  if (packageClasses.has(t.className)) continue;
78698
- const classEnabled = t.className === "person" ? stationarySettings.promotePersons : evidencePolicyFor(t.className).promotesByDefault;
78699
78753
  const active = this.trackStore?.peekActive(t.trackId) ?? null;
78700
78754
  const observedLabel = active === null ? void 0 : displayLabel(active);
78701
78755
  const observedKeyFrame = this.residents.keyFrameMediaId(t.trackId);
@@ -78704,12 +78758,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78704
78758
  className: t.className,
78705
78759
  bbox: { ...t.bbox },
78706
78760
  timestamp: result.timestamp,
78707
- source: "session",
78761
+ source: occasion,
78708
78762
  trackId: t.trackId,
78709
78763
  ...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
78710
- ...observedLabel !== void 0 ? { label: observedLabel } : {}
78764
+ ...observedLabel !== void 0 ? { label: observedLabel } : {},
78765
+ movementState: t.state
78711
78766
  };
78712
- const outcome = evidence.observe(observation, classEnabled);
78767
+ const outcome = evidence.observe(observation);
78713
78768
  if (outcome === void 0 || !outcome.promoted) continue;
78714
78769
  this.birthStationaryFromEvidence({
78715
78770
  deviceId,
@@ -81812,7 +81867,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
81812
81867
  ...label !== void 0 ? { label } : {},
81813
81868
  ...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {}
81814
81869
  };
81815
- if (plausible) this.stationaryRegistry?.promote(entry);
81870
+ if (plausible) {
81871
+ const gate = this.stationarySettingsFromCache(deviceId);
81872
+ this.stationaryRegistry?.promote(entry, {
81873
+ suppressIou: gate.suppressIou,
81874
+ wakeAssocIou: gate.wakeAssocIou
81875
+ });
81876
+ }
81816
81877
  processor.dropTrack(track.trackId);
81817
81878
  this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
81818
81879
  this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
@@ -1,4 +1,4 @@
1
- import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as plateGalleryCapability, B as NcTaxonomySchema, Bt as videoclipsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as isSourceCap, D as NC_DEFAULT_SNOOZE_MINUTES, E as NC_CONDITION_CATALOG, Et as notificationRulesCapability, F as NcScheduleSchema, Ft as sceneMonitorCapability, Gt as DeviceType, Ht as errMsg$1, I as NcSnoozeInputSchema, It as storageOccupancyCapability, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as createEvent, L as NcSnoozeSchema, Lt as subKindsOf, M as NcRulePatchSchema, Mt as readTimelapseGeneratedAt, N as NcRuleSchema, Nt as resolveLocationMode, Ot as pickClusterStepModels, P as NcRuleTargetSchema, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as systemEventFilterApplies, St as isScheduleActive, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as mayWriteToLocation, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as BaseAddon, V as OpsLogEntrySchema, Vt as zoneAnalyticsCapability, W as RetrainStatusSchema, Wt as CamProfileSchema, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as failureContributionCapability, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isEventOwnerType, cn as EventCategory, ct as cosineSimilarity$1, dt as deriveRecordingMode, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, g as FIRST_LEVEL_MACRO_CLASSES, gt as faceGalleryCapability, h as EVENT_PAD_MS, ht as evictionPolicyOfLocation, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readDeviceStateFrom, k as NC_TAXONOMY, kt as pipelineAnalyticsCapability, lt as customAction, m as EVENT_OWNER_TYPES, mt as evaluateSensorEdge, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as encodeVectorBase64, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, ut as defineCustomActions, v as FailureCounters, w as MediaFileKindEnum, wt as kebabToCamel, x as MAX_BIRTH_DECISION_RECORDS, xt as isMediaOwnerType, y as LabelAttributionSchema, yt as isDetectionMacroClass, z as NcSystemEventKindSchema, zt as vectorDimFromBase64 } from "../dist-CCw4SKRp.mjs";
1
+ import { $ as TimelapseRuleSchema, $t as boolean, A as NcConditionDescriptorSchema, At as readTimelapseGeneratedAt, B as NcTaxonomySchema, Bt as errMsg$1, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pipelineAnalyticsCapability, E as NC_CONDITION_CATALOG, Et as pickClusterStepModels, F as NcScheduleSchema, Ft as subKindsOf, Gt as customAction, Ht as CamProfileSchema, I as NcSnoozeInputSchema, It as systemEventFilterApplies, J as SCENE_DIVERGED, Jt as isDeviceScopedCap, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as defineCustomActions, L as NcSnoozeSchema, Lt as vectorDimFromBase64, M as NcRulePatchSchema, N as NcRuleSchema, Nt as sceneMonitorCapability, Ot as plateGalleryCapability, P as NcRuleTargetSchema, Pt as storageOccupancyCapability, Q as TimelapseRulePatchSchema, Qt as array, R as NcSnoozeSuppressedSchema, Rt as videoclipsCapability, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as DeviceType, V as OpsLogEntrySchema, Vt as BaseAddon, W as RetrainStatusSchema, Wt as createEvent, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as sleep$1, Y as SceneMonitorSchema, Yt as nodePin, Z as TimelapseRuleInputSchema, Zt as _enum, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as record, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as EventCategory, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as discriminatedUnion, et as TrackSourceSchema, f as DeclaredDevices, ft as evaluateSensorEdge, g as FIRST_LEVEL_MACRO_CLASSES, h as EVENT_PAD_MS, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as partialRecord, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as resolveLocationMode, k as NC_TAXONOMY, kt as readDeviceStateFrom, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as number, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as string, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as hydrateSchema, r as BaseDevice, rn as object, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as unknown, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as literal, tt as VISIT_MERGE_GAP_MS, u as DETECTION_MACRO_CLASSES, v as FailureCounters, vt as isEventOwnerType, w as MediaFileKindEnum, wt as notificationRulesCapability, x as MAX_BIRTH_DECISION_RECORDS, xt as isSourceCap, y as LabelAttributionSchema, yt as isMediaOwnerType, z as NcSystemEventKindSchema, zt as zoneAnalyticsCapability } from "../dist-C-1xTyB-.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -55551,7 +55551,8 @@ function buildOverlayDetections(input) {
55551
55551
  ...det,
55552
55552
  track: {
55553
55553
  trackId: t.trackId,
55554
- zones: t.zones
55554
+ zones: t.zones,
55555
+ ...t.state !== void 0 ? { movementState: t.state } : {}
55555
55556
  },
55556
55557
  labels: hasName && !alreadyLabeled ? [...det.labels, {
55557
55558
  label: name,
@@ -57509,6 +57510,10 @@ function centroidInside(b, p) {
57509
57510
  }
57510
57511
  /** Parked-box coverage (intersection / entry area) that counts as swallowed. */
57511
57512
  var SWALLOW_MIN_COVERAGE = .5;
57513
+ /** Fraction of a detection's OWN area that must lie inside a parked entry for
57514
+ * the entry to swallow it. Deliberately near-total: a fragment of a parked
57515
+ * object does not stick out of it. */
57516
+ var FRAGMENT_MIN_CONTAINMENT = .9;
57512
57517
  function isSceneSizedBox(bbox, frameWidth, frameHeight) {
57513
57518
  const frame = frameWidth * frameHeight;
57514
57519
  return frame > 0 && bbox.w * bbox.h / frame >= .55;
@@ -57528,6 +57533,29 @@ function detectionSwallowsEntry(det, parked) {
57528
57533
  if (!centroidInside(det, centroid(parked))) return false;
57529
57534
  return intersectionArea$2(parked, det) / entryArea >= SWALLOW_MIN_COVERAGE;
57530
57535
  }
57536
+ /**
57537
+ * True when `det` is a FRAGMENT of the parked object — much smaller than the
57538
+ * entry and sitting (almost) wholly inside it.
57539
+ *
57540
+ * The mirror of {@link detectionSwallowsEntry}, and the case neither IoU
57541
+ * threshold can see. Measured on 617: a 34x137 box inside a 452x420 parked
57542
+ * motorcycle scores IoU 0.024 — BELOW `wakeAssocIou` (0.1), so the filter read
57543
+ * it as "no detection overlaps this entry" and let it open a track, accumulate
57544
+ * its own evidence and promote into a registry entry of its own. The camera
57545
+ * then reported two parked vehicles where there was one motorcycle.
57546
+ *
57547
+ * The size bound is the reciprocal of {@link SWALLOW_AREA_RATIO}, so the two
57548
+ * swallow rules meet without overlapping: a box between 1/1.35 and 1.35 of the
57549
+ * entry's area, contained, already scores IoU >= 0.74 and is answered by
57550
+ * `suppressIou`. Nothing is a fragment and a near-identical box at once.
57551
+ */
57552
+ function entrySwallowsDetection(parked, det) {
57553
+ const entryArea = parked.w * parked.h;
57554
+ const detArea = det.w * det.h;
57555
+ if (entryArea <= 0 || detArea <= 0) return false;
57556
+ if (detArea > entryArea / 1.35) return false;
57557
+ return intersectionArea$2(parked, det) / detArea >= FRAGMENT_MIN_CONTAINMENT;
57558
+ }
57531
57559
  function parkedEntryHoldsDetection(entry, detBbox, config) {
57532
57560
  const overlap = iou(entry.bbox, detBbox);
57533
57561
  if (overlap >= config.suppressIou) return {
@@ -57542,6 +57570,10 @@ function parkedEntryHoldsDetection(entry, detBbox, config) {
57542
57570
  strong: false,
57543
57571
  holds: false
57544
57572
  };
57573
+ if (entrySwallowsDetection(entry.bbox, detBbox)) return {
57574
+ strong: false,
57575
+ holds: true
57576
+ };
57545
57577
  return {
57546
57578
  strong: false,
57547
57579
  holds: overlap >= config.wakeAssocIou && centroidInside(entry.bbox, centroid(detBbox))
@@ -57560,6 +57592,34 @@ function bboxMatchesParkedEntry(bbox, className, entries, suppressIou = SUPPRESS
57560
57592
  }
57561
57593
  return false;
57562
57594
  }
57595
+ /**
57596
+ * The live entry already censusing this spot, if any — the ONE answer to "is
57597
+ * this place taken?", asked at BIRTH.
57598
+ *
57599
+ * It is deliberately the same predicate the per-frame filter uses
57600
+ * ({@link parkedEntryHoldsDetection}): a box the filter would suppress as a
57601
+ * parked object's own must not be able to become a second parked object. Device
57602
+ * 617 held four rows for one car and a fifth for a fragment of a motorcycle
57603
+ * because the birth path asked nothing at all.
57604
+ */
57605
+ function findParkedOccupant(entries, className, bbox, suppressIou = SUPPRESS_IOU, wakeAssocIou = WAKE_ASSOC_IOU) {
57606
+ const cls = normaliseStationaryClass(className);
57607
+ const config = {
57608
+ suppressIou,
57609
+ wakeAssocIou
57610
+ };
57611
+ let best;
57612
+ for (const e of entries) {
57613
+ if (normaliseStationaryClass(e.className) !== cls) continue;
57614
+ if (!parkedEntryHoldsDetection(e, bbox, config).holds) continue;
57615
+ const overlap = iou(e.bbox, bbox);
57616
+ if (best === void 0 || overlap > best.overlap) best = {
57617
+ entry: e,
57618
+ overlap
57619
+ };
57620
+ }
57621
+ return best?.entry;
57622
+ }
57563
57623
  /** Best-shot veto: current crop is a parked object the trail did not start on. */
57564
57624
  function thumbnailWouldShowParkedNotSubject(input) {
57565
57625
  if (!bboxMatchesParkedEntry(input.currentBbox, input.className, input.entries)) return false;
@@ -58545,85 +58605,6 @@ var StartedBirthRegistry = class {
58545
58605
  //#endregion
58546
58606
  //#region src/pipeline-analytics/pipeline/stationary/stationary-evidence.ts
58547
58607
  /**
58548
- * stationary-evidence — how a parked object EARNS a registry entry.
58549
- *
58550
- * ## The criterion this replaces, and why it was measuring the wrong thing
58551
- *
58552
- * Until 2026-08-28 an object became stationary by TRACK CONTINUITY: one track
58553
- * had to stay alive and barely move for `PROMOTION_WINDOW_MS` (30 s). That
58554
- * rewards an object that is *seen well and often* — precisely the opposite of
58555
- * what the operator needs. Measured on device 617 (the parking lot), 24 h:
58556
- *
58557
- * - three cars are parked in view; the registry held exactly ONE entry;
58558
- * - the two it missed were re-discovered as **140 and 112 separate tracks**,
58559
- * median life **6.8 s**, p90 29 s. Not one of them could ever reach 30 s.
58560
- * - those two objects alone produced **13 494 of 18 402** vehicle
58561
- * observations (73%) — the whole re-spawn flood the registry exists to stop.
58562
- *
58563
- * The track dies; the OBJECT does not. So the evidence must outlive the track.
58564
- *
58565
- * ## The new criterion: a RATIO, not a count
58566
- *
58567
- * "The same thing, in the same place, most of the times it was seen."
58568
- *
58569
- * N — observations that AGREED with the candidate's anchor box (IoU ≥
58570
- * {@link StationaryEvidencePolicy.agreeIou}).
58571
- * M — observations OFFERED by a track once it bound to this candidate,
58572
- * agreeing or not.
58573
- *
58574
- * A count would rescue nothing: a badly-lit car at night is seen rarely, and
58575
- * any floor high enough to exclude a passer-by would exclude it too. A RATIO
58576
- * asks a question a rarely-seen object can still answer well — 8 sightings out
58577
- * of 10 in the same spot is stationary even though 10 is a small number.
58578
- *
58579
- * The dilution is the whole point, and it is why M counts a bound track's
58580
- * observations even after it walks away: a car that drives THROUGH a spot binds
58581
- * for a few frames and then contributes dozens of disagreeing ones. Measured on
58582
- * 617 the two populations do not overlap:
58583
- *
58584
- * | candidate | N / M | ratio |
58585
- * | ---------------------------- | ----------- | ----- |
58586
- * | parked car A (missed today) | 6405 / 6467 | 0.99 |
58587
- * | parked car B (missed today) | 343 / 343 | 1.00 |
58588
- * | car crossing the lot | 5 / 70 | 0.07 |
58589
- * | car turning through the lot | 14 / 91 | 0.15 |
58590
- * | car manoeuvring | 12 / 41 | 0.29 |
58591
- *
58592
- * The threshold sits in the empty band between 0.29 and 0.99.
58593
- *
58594
- * ## The span is what a ratio cannot say
58595
- *
58596
- * A ratio alone promotes a car that paused for four seconds — its handful of
58597
- * observations all agree. So evidence must also STRETCH: the agreeing
58598
- * observations must span {@link StationaryEvidencePolicy.minSpanMs}. Measured
58599
- * on 617, real parked cars span 2 min to 24 h; the two momentary stops in the
58600
- * same day spanned 4.2 s and 8.0 s. Setting the span to
58601
- * `DEFAULT_MAX_SESSION_HOLD_MS` also buys a structural guarantee: no single
58602
- * motion session, however long it is held open, can promote on its own.
58603
- *
58604
- * ## ONE ledger, two producers
58605
- *
58606
- * Evidence is anchored to a BOX, not to a track and not to a session, so an
58607
- * object confirmed half by an active motion session and half by a periodic
58608
- * burst still promotes. Occasion A (the session) is implemented and is
58609
- * sufficient on its own — it is free, because the pipeline is already decoding
58610
- * and already inferring. Occasion B (the burst) is a precaution for the cases
58611
- * where A could not have fired at all (hub restarted, camera offline, motion
58612
- * detection failed, object already parked at boot); it is NOT implemented yet,
58613
- * and {@link StationaryEvidenceSource} is the seam it will arrive through.
58614
- *
58615
- * ## Identity is IoU. There is no grid.
58616
- *
58617
- * The predecessor mechanism (`phantom-cells.ts`) quantises a point to a 32 px
58618
- * cell, and its write path passes a CENTROID while its read path passes a
58619
- * top-left CORNER — at 32 px every box bigger than 32 px lands in a different
58620
- * cell on read, and its unit test never crosses that boundary because it calls
58621
- * both halves with the same literal coordinates. This module has no such
58622
- * boundary to get wrong: a candidate is found by overlap against the boxes a
58623
- * device already holds, bounded by {@link EVIDENCE_MAX_CANDIDATES_PER_DEVICE}.
58624
- * Device 617 — the busiest lot in the fleet — holds 8.
58625
- */
58626
- /**
58627
58608
  * `vehicle` and its kin. Every number below is read off device 617 over 24 h
58628
58609
  * (18 402 vehicle observations, 290 tracks) with the three parked cars the
58629
58610
  * operator can see out of the window as the bench.
@@ -58645,11 +58626,11 @@ var VEHICLE_EVIDENCE_POLICY = {
58645
58626
  agreeIou: .5,
58646
58627
  minAgreed: 8,
58647
58628
  minAgreementRatio: .6,
58648
- minSpanMs: 12e4,
58649
- promotesByDefault: true
58629
+ minSpanMs: 12e4
58650
58630
  };
58651
58631
  /**
58652
- * `person`, and it ships OFF.
58632
+ * `person`, promoted like every other class since 2026-09-14 — and still the
58633
+ * strictest numbers in the file, because these are MEASURED, not cautious.
58653
58634
  *
58654
58635
  * This is not caution, it is a measurement. Replaying 24 h of person
58655
58636
  * observations from devices 617, 1443, 590 and 592 through this exact ledger:
@@ -58668,17 +58649,24 @@ var VEHICLE_EVIDENCE_POLICY = {
58668
58649
  *
58669
58650
  * Promoting a person is a BLINDING decision (D49 read in the direction that
58670
58651
  * destroys work: for a car the destructive read is the RETIREMENT — the flood
58671
- * comes back; for a person it is the BIRTH — we stop seeing them). So the
58672
- * class is available, tuned as strictly as the data allows, and switched off
58673
- * until an operator turns it on for a camera where they know what is standing
58674
- * there.
58652
+ * comes back; for a person it is the BIRTH — we stop seeing them). It used to
58653
+ * ship OFF behind a per-camera switch for that reason; the switch is gone —
58654
+ * an opt-in nobody turns on produces nothing, and it was producing nothing.
58655
+ *
58656
+ * What makes that affordable now is that agreement is no longer overlap alone:
58657
+ * `observationAgrees` requires the TRACKER to call the subject still, so a
58658
+ * person who merely pauses inside the anchor no longer feeds the candidate.
58659
+ * The statue and the poster still promote — which is what you want, they ARE
58660
+ * furniture — while the real person who stops for a moment no longer does.
58661
+ *
58662
+ * The thresholds stay where the measurement put them. They are the difference
58663
+ * between 22 promotions a day and 10.
58675
58664
  */
58676
58665
  var PERSON_EVIDENCE_POLICY = {
58677
58666
  agreeIou: .5,
58678
58667
  minAgreed: 40,
58679
58668
  minAgreementRatio: .95,
58680
- minSpanMs: 18e5,
58681
- promotesByDefault: false
58669
+ minSpanMs: 18e5
58682
58670
  };
58683
58671
  /** Classes with no policy of their own fall back to the vehicle numbers —
58684
58672
  * `animal`, `package` and friends are all "an object that ended up somewhere",
@@ -58697,7 +58685,7 @@ function stationaryEvidenceKey(deviceId, className, anchorId) {
58697
58685
  * Pure, and ordered cheapest-question-first so the reason a candidate was
58698
58686
  * refused is the FIRST thing it failed, not the last.
58699
58687
  */
58700
- function evaluateStationaryEvidence(row, policy, classEnabled) {
58688
+ function evaluateStationaryEvidence(row, policy) {
58701
58689
  const windowSeen = row.windowSeen + row.prevWindowSeen;
58702
58690
  const windowAgreed = row.windowAgreed + row.prevWindowAgreed;
58703
58691
  const ratio = windowSeen > 0 ? windowAgreed / windowSeen : 0;
@@ -58708,11 +58696,6 @@ function evaluateStationaryEvidence(row, policy, classEnabled) {
58708
58696
  ratio,
58709
58697
  spanMs
58710
58698
  };
58711
- if (!classEnabled) return {
58712
- kind: "accumulating",
58713
- shortfall: "class-not-promoted",
58714
- ...base
58715
- };
58716
58699
  if (row.agreed < policy.minAgreed) return {
58717
58700
  kind: "accumulating",
58718
58701
  shortfall: "too-few-agreements",
@@ -58787,6 +58770,32 @@ function matchEvidenceCandidate(rows, className, bbox, agreeIou) {
58787
58770
  };
58788
58771
  }
58789
58772
  /**
58773
+ * Does this observation count as EVIDENCE that the object is parked?
58774
+ *
58775
+ * Two questions, deliberately kept apart:
58776
+ *
58777
+ * - IDENTITY — is this the same object in the same place? That is the IoU,
58778
+ * and it is the only notion of sameness in this module.
58779
+ * - STILLNESS — is it going anywhere? That is the TRACKER's verdict, and
58780
+ * nothing here re-derives it.
58781
+ *
58782
+ * Before this, `agrees` was the IoU alone, so a long run of overlaps WAS the
58783
+ * stillness argument — a second derivation of a fact `StateAnalyzer` already
58784
+ * computes per track, slower and able to disagree with it. Now the ledger adds
58785
+ * only what the analyzer cannot know: that the same still object has been here
58786
+ * across a succession of tracks.
58787
+ *
58788
+ * `entering` is the tracker's undecided window — five frames that begin EVERY
58789
+ * track, including a parked car's re-detection. It is not disagreement, and
58790
+ * counting it as such would punish a candidate for the tracker's own warm-up.
58791
+ * An ABSENT verdict likewise falls back to overlap alone: a path that never had
58792
+ * an opinion must not be silently reclassified.
58793
+ */
58794
+ function observationAgrees(overlap, agreeIou, movementState) {
58795
+ if (overlap < agreeIou) return false;
58796
+ return movementState !== "moving" && movementState !== "left";
58797
+ }
58798
+ /**
58790
58799
  * Fold one observation into a candidate.
58791
58800
  *
58792
58801
  * `agrees` is decided by the CALLER, because M is what makes the ratio mean
@@ -59089,7 +59098,7 @@ var StationaryEvidenceLedger = class {
59089
59098
  * slow or failing store can never change what this frame decides. The
59090
59099
  * durable half is fired behind the verdict.
59091
59100
  */
59092
- observe(observation, classEnabled) {
59101
+ observe(observation) {
59093
59102
  const policy = evidencePolicyFor(observation.className);
59094
59103
  this.expire(observation.deviceId, observation.timestamp);
59095
59104
  const bindKey = observation.trackId === void 0 ? void 0 : `${observation.deviceId}:${observation.trackId}`;
@@ -59137,18 +59146,18 @@ var StationaryEvidenceLedger = class {
59137
59146
  this.capDevice(observation.deviceId);
59138
59147
  return {
59139
59148
  row,
59140
- verdict: evaluateStationaryEvidence(row, policy, classEnabled),
59149
+ verdict: evaluateStationaryEvidence(row, policy),
59141
59150
  promoted: false
59142
59151
  };
59143
59152
  }
59144
59153
  if (bindKey !== void 0) this.binding.set(bindKey, row.key);
59145
59154
  }
59146
- const agrees = overlap >= policy.agreeIou;
59155
+ const agrees = observationAgrees(overlap, policy.agreeIou, observation.movementState);
59147
59156
  const next = foldObservation(row, observation, agrees, overlap);
59148
59157
  this.ledger.stage(next);
59149
59158
  this.dirty.add(next.key);
59150
59159
  if (created) this.capDevice(observation.deviceId);
59151
- const verdict = evaluateStationaryEvidence(next, policy, classEnabled);
59160
+ const verdict = evaluateStationaryEvidence(next, policy);
59152
59161
  if (verdict.kind !== "promote") return {
59153
59162
  row: next,
59154
59163
  verdict,
@@ -59596,8 +59605,32 @@ var StationaryObjectRegistry = class {
59596
59605
  });
59597
59606
  }
59598
59607
  }
59599
- /** Promote a parked track into a persisted stationary entry. */
59600
- async promote(entry) {
59608
+ /**
59609
+ * Promote a parked track into a persisted stationary entry — or, when the
59610
+ * spot is already censused, CONFIRM the entry sitting on it.
59611
+ *
59612
+ * The occupancy question is asked here and nowhere else, with the same
59613
+ * predicate the per-frame filter uses (`findParkedOccupant`): a box the
59614
+ * filter would suppress as a parked object's own can never become a second
59615
+ * parked object. Until 2026-09-14 this method minted whatever it was handed,
59616
+ * and device 617 reported EIGHT parked vehicles — four of them one car, the
59617
+ * same box to the decimal, promoted again roughly once an hour because the
59618
+ * weak-continuation path (D410) keeps feeding the ledger from a track riding
59619
+ * a parked object. The re-promotion is not the anomaly and is not worth
59620
+ * suppressing: it is a fresh, independent confirmation that the thing is
59621
+ * still there. It just must not produce a neighbour.
59622
+ *
59623
+ * A merge never DOWNGRADES the tenant: it advances the clock and adopts an
59624
+ * identity the tenant lacks (the 617 duplicates arrived unlabelled while the
59625
+ * row they belonged to was already `Car`), and it never overwrites one it
59626
+ * has.
59627
+ */
59628
+ async promote(entry, config) {
59629
+ const occupant = findParkedOccupant(this.list(entry.deviceId), entry.className, entry.bbox, (config ?? this.matchConfig).suppressIou, (config ?? this.matchConfig).wakeAssocIou);
59630
+ if (occupant !== void 0) return {
59631
+ outcome: "merged",
59632
+ entry: this.absorbInto(occupant, entry)
59633
+ };
59601
59634
  this.indexEntry(entry.deviceId, entry.id);
59602
59635
  this.dirty.delete(entry.id);
59603
59636
  await this.ledger.put(entry);
@@ -59606,6 +59639,33 @@ var StationaryObjectRegistry = class {
59606
59639
  entry,
59607
59640
  timestamp: entry.becameStationaryAt
59608
59641
  });
59642
+ return {
59643
+ outcome: "born",
59644
+ entry
59645
+ };
59646
+ }
59647
+ /** Fold a refused promotion into the entry that already holds its spot. */
59648
+ absorbInto(occupant, refused) {
59649
+ const merged = {
59650
+ ...occupant,
59651
+ lastConfirmedAt: Math.max(occupant.lastConfirmedAt, refused.becameStationaryAt),
59652
+ ...occupant.label === void 0 && refused.label !== void 0 ? { label: refused.label } : {},
59653
+ ...occupant.keyFrameMediaId === void 0 && refused.keyFrameMediaId !== void 0 ? { keyFrameMediaId: refused.keyFrameMediaId } : {}
59654
+ };
59655
+ this.ledger.stage(merged);
59656
+ this.dirty.add(merged.id);
59657
+ this.logger.info("stationary promotion absorbed — the spot is already censused", {
59658
+ tags: { deviceId: occupant.deviceId },
59659
+ meta: {
59660
+ deviceId: occupant.deviceId,
59661
+ entryId: occupant.id,
59662
+ className: occupant.className,
59663
+ refusedTrackId: refused.sourceTrackId,
59664
+ parkedForMs: refused.becameStationaryAt - occupant.becameStationaryAt,
59665
+ gainedLabel: occupant.label === void 0 && refused.label !== void 0
59666
+ }
59667
+ });
59668
+ return merged;
59609
59669
  }
59610
59670
  /**
59611
59671
  * Retire entries unconfirmed for longer than the TTL of OBSERVED time, and
@@ -69876,18 +69936,6 @@ var StationarySettingsSchema = object({
69876
69936
  * session open (also the minimum track age). Creates nothing — birth is
69877
69937
  * decided by repeated evidence. `SESSION_STILLNESS_WINDOW_MS`. */
69878
69938
  sessionStillnessWindowMs: number().int().min(5e3).max(12e4).default(SESSION_STILLNESS_WINDOW_MS),
69879
- /**
69880
- * Let a motionless PERSON become a stationary entry on this camera.
69881
- *
69882
- * Off, and it is a measurement rather than caution. Replaying 24 h of person
69883
- * observations from devices 617/1443/590/592 through the evidence ledger
69884
- * produces 6-22 entries a day depending on how strictly it is tuned, and none
69885
- * of them can be tuned to zero: device 592 alone yielded eleven candidates at
69886
- * a perfect agreement ratio between 02:00 and 05:00. A stationary entry
69887
- * SUPPRESSES its object, so a wrong one makes the camera blind to a person —
69888
- * turn this on only for a camera where you know what is standing there.
69889
- */
69890
- promotePersons: boolean().default(false),
69891
69939
  /** Observed-time TTL: retire an entry unconfirmed for this long (measured on
69892
69940
  * frames-flowing time, not wall-clock). `ENTRY_TTL_MS`. */
69893
69941
  entryTtlMs: number().int().min(6e4).max(18e5).default(ENTRY_TTL_MS)
@@ -69907,7 +69955,6 @@ function resolveStationarySettings(raw) {
69907
69955
  suppressIou: pick("suppressIou"),
69908
69956
  wakeAssocIou: pick("wakeAssocIou"),
69909
69957
  sessionStillnessWindowMs: pick("sessionStillnessWindowMs"),
69910
- promotePersons: pick("promotePersons"),
69911
69958
  entryTtlMs: pick("entryTtlMs")
69912
69959
  };
69913
69960
  }
@@ -70443,13 +70490,6 @@ function buildDetectionSettingsSections() {
70443
70490
  unit: "s",
70444
70491
  displayScale: 1e3
70445
70492
  },
70446
- {
70447
- type: "boolean",
70448
- key: "promotePersons",
70449
- label: "Also park motionless people",
70450
- description: "Off by default. A stationary entry SUPPRESSES its object, so a wrong one makes this camera blind to a person standing there. Replaying a day of real person detections produced entries on every camera tried, at every threshold tried — turn this on only where you know what is standing still.",
70451
- default: STATIONARY_DEFAULTS.promotePersons
70452
- },
70453
70493
  {
70454
70494
  type: "slider",
70455
70495
  key: "entryTtlMs",
@@ -73793,11 +73833,7 @@ var ZoneAnalyticsProvider = class {
73793
73833
  });
73794
73834
  }
73795
73835
  this.snapshots.set(input.deviceId, snapshot);
73796
- const noteOccupancy = this.ctx.onOccupancySample;
73797
- if (noteOccupancy !== void 0) {
73798
- noteOccupancy(input.deviceId, total, input.timestamp);
73799
- for (const zone of snapshot.zones) noteOccupancy(input.deviceId, zone.totalObjects, input.timestamp, zone.zoneId);
73800
- }
73836
+ this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
73801
73837
  this.appendHistory(input.deviceId, snapshot);
73802
73838
  this.emitSnapshot(input.deviceId, snapshot);
73803
73839
  this.sliceThrottle.push(input.deviceId, snapshot);
@@ -73864,11 +73900,30 @@ var ZoneAnalyticsProvider = class {
73864
73900
  });
73865
73901
  if (snapshot) {
73866
73902
  this.appendHistory(deviceId, snapshot);
73903
+ this.noteLtsSamples(deviceId, snapshot, now);
73867
73904
  this.emitSnapshot(deviceId, snapshot);
73868
73905
  this.sliceThrottle.push(deviceId, snapshot);
73869
73906
  }
73870
73907
  }
73871
73908
  }
73909
+ /**
73910
+ * Append this snapshot to the long-term occupancy series — frame-wide first
73911
+ * (`scope: ''`), then one sample per zone.
73912
+ *
73913
+ * Shared by BOTH producers on purpose. It used to live inline in
73914
+ * `recordFrame`, so the baseline sampler — the only producer a camera with no
73915
+ * frames flowing has — wrote the RAM ring, the NC watcher and the state slice
73916
+ * but never LTS. The long-term series therefore had a hole exactly where the
73917
+ * scene was nothing but parked objects: the one span an occupancy history is
73918
+ * asked about. The RAM ring showed a flat line over the same minutes, which
73919
+ * is what made the gap invisible.
73920
+ */
73921
+ noteLtsSamples(deviceId, snapshot, timestamp) {
73922
+ const note = this.ctx.onOccupancySample;
73923
+ if (note === void 0) return;
73924
+ note(deviceId, snapshot.frame.totalObjects, timestamp);
73925
+ for (const zone of snapshot.zones) note(deviceId, zone.totalObjects, timestamp, zone.zoneId);
73926
+ }
73872
73927
  /** Push one snapshot to the occupancy tap, isolating any consumer throw from
73873
73928
  * the frame path (occupancy notifications must never break analytics). */
73874
73929
  emitSnapshot(deviceId, snapshot) {
@@ -78203,7 +78258,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78203
78258
  }
78204
78259
  });
78205
78260
  }
78206
- await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps);
78261
+ await this.processFrame(deviceId, frame, data.source ?? "pipeline", data.frameHandle, data.detailSteps, data.occasion ?? "session");
78207
78262
  }
78208
78263
  /**
78209
78264
  * Run one detection frame through the analysis layers for a given
@@ -78213,7 +78268,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78213
78268
  * tracking/zone/event state never crosses between sources. Emits the
78214
78269
  * SAME canonical events, distinguished only by `source`.
78215
78270
  */
78216
- async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps) {
78271
+ async processFrame(deviceId, frame, source, rawFrameHandle, detailSteps, occasion = "session") {
78217
78272
  if (this.shuttingDown) return;
78218
78273
  if (!await this.bindingCache.isActive(deviceId)) {
78219
78274
  const now = Date.now();
@@ -78621,7 +78676,6 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78621
78676
  const evidence = this.stationaryEvidence;
78622
78677
  for (const t of result.tracked) {
78623
78678
  if (packageClasses.has(t.className)) continue;
78624
- const classEnabled = t.className === "person" ? stationarySettings.promotePersons : evidencePolicyFor(t.className).promotesByDefault;
78625
78679
  const active = this.trackStore?.peekActive(t.trackId) ?? null;
78626
78680
  const observedLabel = active === null ? void 0 : displayLabel(active);
78627
78681
  const observedKeyFrame = this.residents.keyFrameMediaId(t.trackId);
@@ -78630,12 +78684,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78630
78684
  className: t.className,
78631
78685
  bbox: { ...t.bbox },
78632
78686
  timestamp: result.timestamp,
78633
- source: "session",
78687
+ source: occasion,
78634
78688
  trackId: t.trackId,
78635
78689
  ...observedKeyFrame !== void 0 ? { keyFrameMediaId: observedKeyFrame } : {},
78636
- ...observedLabel !== void 0 ? { label: observedLabel } : {}
78690
+ ...observedLabel !== void 0 ? { label: observedLabel } : {},
78691
+ movementState: t.state
78637
78692
  };
78638
- const outcome = evidence.observe(observation, classEnabled);
78693
+ const outcome = evidence.observe(observation);
78639
78694
  if (outcome === void 0 || !outcome.promoted) continue;
78640
78695
  this.birthStationaryFromEvidence({
78641
78696
  deviceId,
@@ -81738,7 +81793,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
81738
81793
  ...label !== void 0 ? { label } : {},
81739
81794
  ...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {}
81740
81795
  };
81741
- if (plausible) this.stationaryRegistry?.promote(entry);
81796
+ if (plausible) {
81797
+ const gate = this.stationarySettingsFromCache(deviceId);
81798
+ this.stationaryRegistry?.promote(entry, {
81799
+ suppressIou: gate.suppressIou,
81800
+ wakeAssocIou: gate.wakeAssocIou
81801
+ });
81802
+ }
81742
81803
  processor.dropTrack(track.trackId);
81743
81804
  this.trackStore?.closeActive(track.trackId, "stationary-promotion").catch((err) => {
81744
81805
  this.ctx.logger.warn("stationary promotion: closing the source track failed — its media, events and vector are now unowned", {
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CG4H07lP.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-zMqSbyyP.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.241",
3
+ "version": "1.2.243",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",