@camstack/addon-post-analysis 1.2.248 → 1.2.249

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.
@@ -20663,13 +20663,26 @@ var TrackAudioLabelSchema = object({
20663
20663
  * - `audio` — an audio event on the camera itself that was anomalous for
20664
20664
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20665
20665
  *
20666
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
20667
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
20668
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
20669
+ * detector.
20670
+ *
20671
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
20672
+ * a persisted onboard track failed `safeParse` on read and came back with NO
20673
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
20674
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
20675
+ * could not name.
20676
+ *
20666
20677
  * The spatial subsystems (tracker association, occupancy count, re-id /
20667
20678
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20668
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20669
- * check silently readmits every source added after it was written.
20679
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
20680
+ * `!== 'sensor'` check silently readmits every source added after it was
20681
+ * written.
20670
20682
  */
20671
20683
  var TrackSourceSchema = _enum([
20672
20684
  "pipeline",
20685
+ "onboard",
20673
20686
  "sensor",
20674
20687
  "audio"
20675
20688
  ]);
@@ -23402,13 +23415,39 @@ var MotionSourceEnum = _enum([
23402
23415
  */
23403
23416
  var MotionSourcesSchema = array(MotionSourceEnum);
23404
23417
  /**
23405
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
23406
- * post-analysis already tags every detection with (`DetectionSource`) rather
23407
- * than a second spelling of the same two ideas — the value an operator picks
23408
- * here is the value that comes back on the track, the overlay and the debug
23409
- * row.
23418
+ * Which root detector a camera runs — EXACTLY ONE.
23419
+ *
23420
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
23421
+ * with (`DetectionSource`) rather than a second spelling of the same two
23422
+ * ideas: the value an operator picks here is the value that comes back on the
23423
+ * track, the overlay and the debug row.
23424
+ *
23425
+ * ## Why one, and why it is still an array
23426
+ *
23427
+ * Two roots on one camera is a capability nobody asked for and the operator has
23428
+ * since ruled out. It was never free: both planes feed the SAME per-device
23429
+ * stationary registry, both can promote the same parked object, and the two
23430
+ * detectors disagree about the same box by construction — which is the
23431
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
23432
+ *
23433
+ * The ARRAY shape survives because the stored settings and the attach payload
23434
+ * already speak it on every camera of every fleet, and a cap input that
23435
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
23436
+ * in the one direction that costs an operator their cameras. So the wire stays
23437
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
23438
+ * element rather than refused, and `max(1)` is what every consumer can then
23439
+ * rely on.
23440
+ *
23441
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
23442
+ * a camera an operator left with no root detector — and `planDetectionSources`
23443
+ * has always said so: inventing a root for them is how a default acts without
23444
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
23445
+ * suite caught it.
23446
+ *
23447
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
23448
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
23410
23449
  */
23411
- var DetectionSourcesSchema = array(DetectionSourceSchema);
23450
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
23412
23451
  /**
23413
23452
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
23414
23453
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -20694,13 +20694,26 @@ var TrackAudioLabelSchema = object({
20694
20694
  * - `audio` — an audio event on the camera itself that was anomalous for
20695
20695
  * THAT camera, loud, and heard while nothing visual was happening (D62).
20696
20696
  *
20697
+ * - `onboard` — the CAMERA's own firmware, paired with a decoded frame in
20698
+ * that frame's pixel space. A real root detector and a SPATIAL one: its
20699
+ * boxes are the same kind of fact `pipeline`'s are, produced by a different
20700
+ * detector.
20701
+ *
20702
+ * `onboard` was missing here while `DetectionSourceSchema` already had it, so
20703
+ * a persisted onboard track failed `safeParse` on read and came back with NO
20704
+ * source at all — which `isSpatialTrack` then admitted by accident, through the
20705
+ * `undefined` arm rather than by anybody's decision, and which `excludeSources`
20706
+ * could not name.
20707
+ *
20697
20708
  * The spatial subsystems (tracker association, occupancy count, re-id /
20698
20709
  * embedding, resurrection) MUST skip every synthetic source. Test for that
20699
- * with `isSpatialTrack`, which allow-lists `pipeline` — a `!== 'sensor'`
20700
- * check silently readmits every source added after it was written.
20710
+ * with `isSpatialTrack`, which allow-lists the spatial sources explicitly — a
20711
+ * `!== 'sensor'` check silently readmits every source added after it was
20712
+ * written.
20701
20713
  */
20702
20714
  var TrackSourceSchema = _enum([
20703
20715
  "pipeline",
20716
+ "onboard",
20704
20717
  "sensor",
20705
20718
  "audio"
20706
20719
  ]);
@@ -23433,13 +23446,39 @@ var MotionSourceEnum = _enum([
23433
23446
  */
23434
23447
  var MotionSourcesSchema = array(MotionSourceEnum);
23435
23448
  /**
23436
- * Which root detectors a camera runs. Deliberately the SAME vocabulary
23437
- * post-analysis already tags every detection with (`DetectionSource`) rather
23438
- * than a second spelling of the same two ideas — the value an operator picks
23439
- * here is the value that comes back on the track, the overlay and the debug
23440
- * row.
23449
+ * Which root detector a camera runs — EXACTLY ONE.
23450
+ *
23451
+ * Deliberately the SAME vocabulary post-analysis already tags every detection
23452
+ * with (`DetectionSource`) rather than a second spelling of the same two
23453
+ * ideas: the value an operator picks here is the value that comes back on the
23454
+ * track, the overlay and the debug row.
23455
+ *
23456
+ * ## Why one, and why it is still an array
23457
+ *
23458
+ * Two roots on one camera is a capability nobody asked for and the operator has
23459
+ * since ruled out. It was never free: both planes feed the SAME per-device
23460
+ * stationary registry, both can promote the same parked object, and the two
23461
+ * detectors disagree about the same box by construction — which is the
23462
+ * disagreement D505 had to arbitrate. Removing the case removes the arbitration.
23463
+ *
23464
+ * The ARRAY shape survives because the stored settings and the attach payload
23465
+ * already speak it on every camera of every fleet, and a cap input that
23466
+ * suddenly refuses a stored value makes the camera un-attachable — a fail-closed
23467
+ * in the one direction that costs an operator their cameras. So the wire stays
23468
+ * tolerant and the TYPE is exact: a longer list is truncated to its first
23469
+ * element rather than refused, and `max(1)` is what every consumer can then
23470
+ * rely on.
23471
+ *
23472
+ * AT MOST one, not exactly one. The EMPTY list is a legal, deliberate pick —
23473
+ * a camera an operator left with no root detector — and `planDetectionSources`
23474
+ * has always said so: inventing a root for them is how a default acts without
23475
+ * anybody choosing it. Tightening this to `length(1)` broke that, and the
23476
+ * suite caught it.
23477
+ *
23478
+ * Measured before tightening (2026-09-15, this fleet, 6 h of attaches): 3327
23479
+ * `["pipeline"]` and 2 `["onboard"]`. Not one camera had two.
23441
23480
  */
23442
- var DetectionSourcesSchema = array(DetectionSourceSchema);
23481
+ var DetectionSourcesSchema = preprocess((value) => Array.isArray(value) && value.length > 1 ? value.slice(0, 1) : value, array(DetectionSourceSchema).max(1));
23443
23482
  /**
23444
23483
  * Input shape for `pipeline-runner.reportMotion` cap method. Exported
23445
23484
  * so cap-side consumers (the orchestrator forward, the runner addon's
@@ -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-DmB50uQ-.js");
5
+ const require_dist = require("../dist-CdLfCStM.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 { Et as parseProcStatus, H as PoolMemoryWatchdog, Ht as BaseAddon, Nt as resolvePoolMemoryPolicy, gt as hfModelUrl, ut as embeddingEncoderCapability } from "../dist-KRjxHIQ4.mjs";
1
+ import { Et as parseProcStatus, H as PoolMemoryWatchdog, Ht as BaseAddon, Nt as resolvePoolMemoryPolicy, gt as hfModelUrl, ut as embeddingEncoderCapability } from "../dist-C7xHFnGJ.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";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.106",
6
+ version: "1.2.107",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.200",
21
+ version: "1.2.201",
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.165",
36
+ version: "1.2.166",
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.200",
39
+ version: "1.2.201",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.106",
48
+ version: "1.2.107",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.165",
84
+ version: "1.2.166",
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-DmB50uQ-.js");
5
+ const require_dist = require("../dist-CdLfCStM.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");
@@ -15088,20 +15088,23 @@ var OccupancyWatcher = class {
15088
15088
  * this tick (usually none). Every watched key is evaluated for `deviceId`;
15089
15089
  * fail-closed keys (absent zone) are skipped and hold no state.
15090
15090
  */
15091
- observe(deviceId, snapshot, now) {
15091
+ observe(deviceId, snapshot, now, evidence = "scene") {
15092
15092
  const edges = [];
15093
15093
  for (const spec of this.watched.values()) {
15094
15094
  const resolved = resolveOccupancyScope(snapshot, spec);
15095
15095
  if (resolved === null) continue;
15096
15096
  const key = occupancyKey(deviceId, spec.zoneId, spec.className, spec.threshold);
15097
15097
  this.reportImpossibleScope(deviceId, key, spec);
15098
+ const blindCensus = evidence === "census" && resolved.count < spec.threshold;
15098
15099
  const existing = this.states.get(key);
15099
15100
  if (existing === void 0) {
15101
+ if (blindCensus) continue;
15100
15102
  const state = seedState(deviceId, spec, resolved, now);
15101
15103
  this.states.set(key, state);
15102
15104
  this.seeded.set(key, stateToRow(key, state, now));
15103
15105
  continue;
15104
15106
  }
15107
+ if (blindCensus) continue;
15105
15108
  const edge = step(existing, spec, resolved, now);
15106
15109
  if (edge !== null) edges.push(edge);
15107
15110
  }
@@ -24730,12 +24733,12 @@ var NotificationCenter = class NotificationCenter {
24730
24733
  * COMMITTED edge is persisted (rare — only on a confirmed flip, never
24731
24734
  * per-frame) and routed to {@link onOccupancyEdge}.
24732
24735
  */
24733
- observeOccupancy(deviceId, snapshot) {
24736
+ observeOccupancy(deviceId, snapshot, evidence = "scene") {
24734
24737
  if (!this.evaluationActive || !this.occupancyEnabled) return;
24735
24738
  const now = this.now();
24736
24739
  let edges;
24737
24740
  try {
24738
- edges = this.occupancyWatcher.observe(deviceId, snapshot, now);
24741
+ edges = this.occupancyWatcher.observe(deviceId, snapshot, now, evidence);
24739
24742
  } catch (err) {
24740
24743
  this.logger.debug("occupancy observe failed", {
24741
24744
  tags: { deviceId },
@@ -24756,7 +24759,8 @@ var NotificationCenter = class NotificationCenter {
24756
24759
  previousCount: edge.previousCount,
24757
24760
  occupied: edge.occupied,
24758
24761
  observedAt: edge.observedAt ?? -1,
24759
- observedLagMs: edge.observedAt !== void 0 ? edge.timestamp - edge.observedAt : -1
24762
+ observedLagMs: edge.observedAt !== void 0 ? edge.timestamp - edge.observedAt : -1,
24763
+ committedBy: evidence
24760
24764
  }
24761
24765
  });
24762
24766
  this.persistOccupancyEdge(edge, now);
@@ -61830,6 +61834,49 @@ function hasKeyFrame(source) {
61830
61834
  return source.keyFrame !== null && source.bbox !== null;
61831
61835
  }
61832
61836
  //#endregion
61837
+ //#region src/pipeline-analytics/pipeline/stationary-planes.ts
61838
+ /**
61839
+ * WHICH detection planes carry the stationary machinery.
61840
+ *
61841
+ * A plane qualifies when it produces OBJECT BOXES on decoded frames — because
61842
+ * that is what the registry needs to decide an object is parked: a box, a
61843
+ * class, and an observed clock that only advances while frames flow.
61844
+ *
61845
+ * Both root detectors qualify. `pipeline` is the ML tree; `onboard` is the
61846
+ * camera's own firmware, and the boxes it produces are paired with a decoded
61847
+ * frame in that frame's pixel space — the same plane, entered by a different
61848
+ * door. `sensor` and `audio` do not: they carry no geometry to park.
61849
+ *
61850
+ * It used to be `source === 'pipeline'`, in five places, each explaining that
61851
+ * the gate "runs on that plane". The reason given was WHERE the flood it was
61852
+ * written for had been measured (617, the ML plane), never an argument that the
61853
+ * firmware's boxes were unfit. The cost of the accident, on device 640: a still
61854
+ * object is never promoted, so it holds `hasLiveTrack` true for as long as it
61855
+ * sits there, and every detection session runs to its 120 s cap instead of
61856
+ * closing on cooldown — `closed at the max-hold cap with a subject still
61857
+ * present`. The camera's lazy stream is then torn down and rebuilt on the next
61858
+ * detection, on a battery camera.
61859
+ *
61860
+ * One predicate and not five comparisons, because five copies of a rule drift:
61861
+ * this repo has already paid for that with two authorities on one function
61862
+ * (D62).
61863
+ */
61864
+ /** Does this detection plane run the stationary registry, gate and promotion? */
61865
+ function planeRunsStationary(source) {
61866
+ return source === "pipeline" || source === "onboard";
61867
+ }
61868
+ //#endregion
61869
+ //#region src/pipeline-analytics/pipeline/onboard-birth-authority.ts
61870
+ /**
61871
+ * May a suppression verdict RETRACT this birth?
61872
+ *
61873
+ * `false` means hold: keep the track, log the held verdict, and let the
61874
+ * ordinary lifetime decide. It never means "confirmed".
61875
+ */
61876
+ function suppressionMayRetract(source) {
61877
+ return source !== "onboard";
61878
+ }
61879
+ //#endregion
61833
61880
  //#region src/pipeline-analytics/rebuild-source-loader.ts
61834
61881
  /**
61835
61882
  * Assemble one track's rebuild inputs, reading at most ONE blob.
@@ -65600,9 +65647,10 @@ var PlateStore = class {
65600
65647
  * `PlateColumnPatch` widens the nullable media columns to accept `null`,
65601
65648
  * which is how a caller CLEARS one (`assignPlate` nulls a crop whose blob it
65602
65649
  * just deleted, so the reown that follows cannot try to move a resource that
65603
- * is gone). It used to be expressed as `null as unknown as string` at the
65604
- * call site — a cast that stopped being even nominally true when the column
65605
- * became a number (D482), and which the type can simply say instead.
65650
+ * is gone). It used to be expressed at the call site as a `null` double-cast
65651
+ * through `unknown` to `string` — a cast that stopped being even nominally
65652
+ * true when the column became a number (D482), and which the type can simply
65653
+ * say instead.
65606
65654
  */
65607
65655
  async update(plateId, data) {
65608
65656
  try {
@@ -74803,7 +74851,7 @@ var ZoneAnalyticsProvider = class {
74803
74851
  this.snapshots.set(input.deviceId, snapshot);
74804
74852
  this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
74805
74853
  this.appendHistory(input.deviceId, snapshot);
74806
- this.emitSnapshot(input.deviceId, snapshot);
74854
+ this.emitSnapshot(input.deviceId, snapshot, "scene");
74807
74855
  this.sliceThrottle.push(input.deviceId, snapshot);
74808
74856
  }
74809
74857
  /** Drop a device's snapshot + history. Called on device removal. */
@@ -74869,7 +74917,7 @@ var ZoneAnalyticsProvider = class {
74869
74917
  if (snapshot) {
74870
74918
  this.appendHistory(deviceId, snapshot);
74871
74919
  this.noteLtsSamples(deviceId, snapshot, now);
74872
- this.emitSnapshot(deviceId, snapshot);
74920
+ this.emitSnapshot(deviceId, snapshot, "census");
74873
74921
  this.sliceThrottle.push(deviceId, snapshot);
74874
74922
  }
74875
74923
  }
@@ -74898,10 +74946,10 @@ var ZoneAnalyticsProvider = class {
74898
74946
  }
74899
74947
  /** Push one snapshot to the occupancy tap, isolating any consumer throw from
74900
74948
  * the frame path (occupancy notifications must never break analytics). */
74901
- emitSnapshot(deviceId, snapshot) {
74949
+ emitSnapshot(deviceId, snapshot, evidence) {
74902
74950
  if (!this.ctx.onSnapshot) return;
74903
74951
  try {
74904
- this.ctx.onSnapshot(deviceId, snapshot);
74952
+ this.ctx.onSnapshot(deviceId, snapshot, evidence);
74905
74953
  } catch (err) {
74906
74954
  this.ctx.logger.debug("zone-analytics occupancy tap failed", {
74907
74955
  tags: { deviceId },
@@ -77521,7 +77569,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
77521
77569
  listStationaryObjects: (deviceId) => this.stationaryRegistry?.listViews(deviceId) ?? [],
77522
77570
  listStationaryDeviceIds: () => this.stationaryRegistry?.deviceIds() ?? [],
77523
77571
  resolveZones: (deviceId) => this.resolveDeviceZones(deviceId),
77524
- onSnapshot: (deviceId, snapshot) => this.notificationCenter?.observeOccupancy(deviceId, snapshot)
77572
+ onSnapshot: (deviceId, snapshot, evidence) => this.notificationCenter?.observeOccupancy(deviceId, snapshot, evidence)
77525
77573
  });
77526
77574
  this.zoneAnalytics = zoneAnalytics;
77527
77575
  const audioMetrics = new AudioMetricsProvider({
@@ -79362,7 +79410,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
79362
79410
  return;
79363
79411
  }
79364
79412
  this.dropoutSkipsByKey.set(key, 0);
79365
- if (source === "pipeline" && this.stationaryRegistry) {
79413
+ if (planeRunsStationary(source) && this.stationaryRegistry) {
79366
79414
  await this.resolveDeviceStationarySettings(deviceId);
79367
79415
  await this.resolveDevicePackageDropSettings(deviceId);
79368
79416
  await this.resolveDeviceSummarySettings(deviceId);
@@ -79422,7 +79470,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
79422
79470
  });
79423
79471
  this.trackStore?.markRiderFolded(fold.trackId);
79424
79472
  }
79425
- if (source === "pipeline") this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
79473
+ if (planeRunsStationary(source)) this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
79426
79474
  if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0)) this.stationaryRegistry.applyFrameOutcome({
79427
79475
  deviceId,
79428
79476
  confirmed: result.stationaryConfirmed,
@@ -79596,8 +79644,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
79596
79644
  firstSeen: this.trackStore?.peekActive(id)?.firstSeen ?? null
79597
79645
  });
79598
79646
  if (!outcome.confirmed.has(id)) {
79647
+ if (!suppressionMayRetract(source)) {
79648
+ log.info("birth suppression held — the gate is not this root detector", {
79649
+ tags: { deviceId },
79650
+ meta: {
79651
+ trackId: id,
79652
+ className: t.className,
79653
+ source
79654
+ }
79655
+ });
79656
+ continue;
79657
+ }
79599
79658
  this.suppressedBirths.reject(key, id);
79600
79659
  this.trackStore?.dropActive(id);
79660
+ this.clearLiveTrackState(deviceId, id);
79601
79661
  log.info("birth suppressed — track record retracted", { meta: {
79602
79662
  trackId: id,
79603
79663
  className: t.className,
@@ -79727,7 +79787,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
79727
79787
  const stationarySettings = this.stationarySettingsFromCache(deviceId);
79728
79788
  const packageSettings = this.packageDropSettingsFromCache(deviceId);
79729
79789
  const packageClasses = new Set(packageSettings.packageDropClassFilter);
79730
- if (source === "pipeline" && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.enabled) {
79790
+ if (planeRunsStationary(source) && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.enabled) {
79731
79791
  const evidence = this.stationaryEvidence;
79732
79792
  for (const t of result.tracked) {
79733
79793
  if (packageClasses.has(t.className)) continue;
@@ -79761,7 +79821,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
79761
79821
  }
79762
79822
  evidence.retainTracks(deviceId, currentTrackIds);
79763
79823
  }
79764
- if (source === "pipeline") this.runPackageStillness({
79824
+ if (planeRunsStationary(source)) this.runPackageStillness({
79765
79825
  deviceId,
79766
79826
  tracked: result.tracked,
79767
79827
  packageClasses,
@@ -83019,7 +83079,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
83019
83079
  ...active.subLabelMeta !== void 0 ? { subLabelMeta: active.subLabelMeta } : {}
83020
83080
  } : void 0;
83021
83081
  });
83022
- if (source === "pipeline" && this.stationaryRegistry) {
83082
+ if (planeRunsStationary(source) && this.stationaryRegistry) {
83023
83083
  const registry = this.stationaryRegistry;
83024
83084
  p.setStationaryGate({ filter: (input) => {
83025
83085
  const s = this.stationarySettingsFromCache(deviceId);
@@ -1,4 +1,4 @@
1
- import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, 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 object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, 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 videoclipsCapability } from "../dist-KRjxHIQ4.mjs";
1
+ import { $ as TimelapseRuleSchema, $t as array, A as NcConditionDescriptorSchema, At as readDeviceStateFrom, B as NcTaxonomySchema, Bt as zoneAnalyticsCapability, C as MOTION_CLOSE_AFTER_MS, Ct as mayWriteToLocation, D as NC_DEFAULT_SNOOZE_MINUTES, Dt as pickClusterStepModels, E as NC_CONDITION_CATALOG, F as NcScheduleSchema, Ft as storageOccupancyCapability, Gt as createEvent, Ht as BaseAddon, I as NcSnoozeInputSchema, It as subKindsOf, J as SCENE_DIVERGED, Jt as hydrateSchema, K as SCENE_DEFAULT_ANCHOR_THRESHOLD, Kt as customAction, L as NcSnoozeSchema, Lt as systemEventFilterApplies, M as NcRulePatchSchema, Mt as resolveLocationMode, N as NcRuleSchema, Ot as pipelineAnalyticsCapability, P as NcRuleTargetSchema, Pt as sceneMonitorCapability, Q as TimelapseRulePatchSchema, Qt as _enum, R as NcSnoozeSuppressedSchema, Rt as vectorDimFromBase64, St as kebabToCamel, T as NC_ALARM_SYSTEM_EVENT_KINDS, Tt as occupancyScope, U as RECORDING_EXPORT_MAX_READ_BYTES, Ut as CamProfileSchema, V as OpsLogEntrySchema, Vt as errMsg$1, W as RetrainStatusSchema, Wt as DeviceType, X as TIMELAPSE_DENSE_FLOOR_SEC, Xt as nodePin, Y as SceneMonitorSchema, Yt as isDeviceScopedCap, Z as TimelapseRuleInputSchema, Zt as sleep$1, _ as FULL_IMAGE_BBOX, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, an as partialRecord, at as audioMetricsCapability, b as MAX_ARCHIVED_DEBUG_NOTES, bt as isScheduleActive, cn as unknown, ct as cosineSimilarity$1, dt as encodeVectorBase64, en as boolean, 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 object, it as assertTimelapseCadences, j as NcRuleInputSchema, jt as readTimelapseGeneratedAt, k as NC_TAXONOMY, kt as plateGalleryCapability, ln as EventCategory, lt as deriveRecordingMode, m as EVENT_OWNER_TYPES, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as literal, nt as addonWidgetsSourceCapability, o as COCO_TO_MACRO, on as record, ot as audioModeOf, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SCENE_DEFAULT_UNCOVERED_POLICY, qt as defineCustomActions, r as BaseDevice, rn as number, rt as alarmPanelCapability, s as DEFAULT_EVENT_COLOR, sn as string, st as buildEventKindDescriptor, t as AUDIO_MACRO_LABELS, tn as discriminatedUnion, 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 videoclipsCapability } from "../dist-C7xHFnGJ.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";
@@ -15081,20 +15081,23 @@ var OccupancyWatcher = class {
15081
15081
  * this tick (usually none). Every watched key is evaluated for `deviceId`;
15082
15082
  * fail-closed keys (absent zone) are skipped and hold no state.
15083
15083
  */
15084
- observe(deviceId, snapshot, now) {
15084
+ observe(deviceId, snapshot, now, evidence = "scene") {
15085
15085
  const edges = [];
15086
15086
  for (const spec of this.watched.values()) {
15087
15087
  const resolved = resolveOccupancyScope(snapshot, spec);
15088
15088
  if (resolved === null) continue;
15089
15089
  const key = occupancyKey(deviceId, spec.zoneId, spec.className, spec.threshold);
15090
15090
  this.reportImpossibleScope(deviceId, key, spec);
15091
+ const blindCensus = evidence === "census" && resolved.count < spec.threshold;
15091
15092
  const existing = this.states.get(key);
15092
15093
  if (existing === void 0) {
15094
+ if (blindCensus) continue;
15093
15095
  const state = seedState(deviceId, spec, resolved, now);
15094
15096
  this.states.set(key, state);
15095
15097
  this.seeded.set(key, stateToRow(key, state, now));
15096
15098
  continue;
15097
15099
  }
15100
+ if (blindCensus) continue;
15098
15101
  const edge = step(existing, spec, resolved, now);
15099
15102
  if (edge !== null) edges.push(edge);
15100
15103
  }
@@ -24708,12 +24711,12 @@ var NotificationCenter = class NotificationCenter {
24708
24711
  * COMMITTED edge is persisted (rare — only on a confirmed flip, never
24709
24712
  * per-frame) and routed to {@link onOccupancyEdge}.
24710
24713
  */
24711
- observeOccupancy(deviceId, snapshot) {
24714
+ observeOccupancy(deviceId, snapshot, evidence = "scene") {
24712
24715
  if (!this.evaluationActive || !this.occupancyEnabled) return;
24713
24716
  const now = this.now();
24714
24717
  let edges;
24715
24718
  try {
24716
- edges = this.occupancyWatcher.observe(deviceId, snapshot, now);
24719
+ edges = this.occupancyWatcher.observe(deviceId, snapshot, now, evidence);
24717
24720
  } catch (err) {
24718
24721
  this.logger.debug("occupancy observe failed", {
24719
24722
  tags: { deviceId },
@@ -24734,7 +24737,8 @@ var NotificationCenter = class NotificationCenter {
24734
24737
  previousCount: edge.previousCount,
24735
24738
  occupied: edge.occupied,
24736
24739
  observedAt: edge.observedAt ?? -1,
24737
- observedLagMs: edge.observedAt !== void 0 ? edge.timestamp - edge.observedAt : -1
24740
+ observedLagMs: edge.observedAt !== void 0 ? edge.timestamp - edge.observedAt : -1,
24741
+ committedBy: evidence
24738
24742
  }
24739
24743
  });
24740
24744
  this.persistOccupancyEdge(edge, now);
@@ -61756,6 +61760,49 @@ function hasKeyFrame(source) {
61756
61760
  return source.keyFrame !== null && source.bbox !== null;
61757
61761
  }
61758
61762
  //#endregion
61763
+ //#region src/pipeline-analytics/pipeline/stationary-planes.ts
61764
+ /**
61765
+ * WHICH detection planes carry the stationary machinery.
61766
+ *
61767
+ * A plane qualifies when it produces OBJECT BOXES on decoded frames — because
61768
+ * that is what the registry needs to decide an object is parked: a box, a
61769
+ * class, and an observed clock that only advances while frames flow.
61770
+ *
61771
+ * Both root detectors qualify. `pipeline` is the ML tree; `onboard` is the
61772
+ * camera's own firmware, and the boxes it produces are paired with a decoded
61773
+ * frame in that frame's pixel space — the same plane, entered by a different
61774
+ * door. `sensor` and `audio` do not: they carry no geometry to park.
61775
+ *
61776
+ * It used to be `source === 'pipeline'`, in five places, each explaining that
61777
+ * the gate "runs on that plane". The reason given was WHERE the flood it was
61778
+ * written for had been measured (617, the ML plane), never an argument that the
61779
+ * firmware's boxes were unfit. The cost of the accident, on device 640: a still
61780
+ * object is never promoted, so it holds `hasLiveTrack` true for as long as it
61781
+ * sits there, and every detection session runs to its 120 s cap instead of
61782
+ * closing on cooldown — `closed at the max-hold cap with a subject still
61783
+ * present`. The camera's lazy stream is then torn down and rebuilt on the next
61784
+ * detection, on a battery camera.
61785
+ *
61786
+ * One predicate and not five comparisons, because five copies of a rule drift:
61787
+ * this repo has already paid for that with two authorities on one function
61788
+ * (D62).
61789
+ */
61790
+ /** Does this detection plane run the stationary registry, gate and promotion? */
61791
+ function planeRunsStationary(source) {
61792
+ return source === "pipeline" || source === "onboard";
61793
+ }
61794
+ //#endregion
61795
+ //#region src/pipeline-analytics/pipeline/onboard-birth-authority.ts
61796
+ /**
61797
+ * May a suppression verdict RETRACT this birth?
61798
+ *
61799
+ * `false` means hold: keep the track, log the held verdict, and let the
61800
+ * ordinary lifetime decide. It never means "confirmed".
61801
+ */
61802
+ function suppressionMayRetract(source) {
61803
+ return source !== "onboard";
61804
+ }
61805
+ //#endregion
61759
61806
  //#region src/pipeline-analytics/rebuild-source-loader.ts
61760
61807
  /**
61761
61808
  * Assemble one track's rebuild inputs, reading at most ONE blob.
@@ -65526,9 +65573,10 @@ var PlateStore = class {
65526
65573
  * `PlateColumnPatch` widens the nullable media columns to accept `null`,
65527
65574
  * which is how a caller CLEARS one (`assignPlate` nulls a crop whose blob it
65528
65575
  * just deleted, so the reown that follows cannot try to move a resource that
65529
- * is gone). It used to be expressed as `null as unknown as string` at the
65530
- * call site — a cast that stopped being even nominally true when the column
65531
- * became a number (D482), and which the type can simply say instead.
65576
+ * is gone). It used to be expressed at the call site as a `null` double-cast
65577
+ * through `unknown` to `string` — a cast that stopped being even nominally
65578
+ * true when the column became a number (D482), and which the type can simply
65579
+ * say instead.
65532
65580
  */
65533
65581
  async update(plateId, data) {
65534
65582
  try {
@@ -74729,7 +74777,7 @@ var ZoneAnalyticsProvider = class {
74729
74777
  this.snapshots.set(input.deviceId, snapshot);
74730
74778
  this.noteLtsSamples(input.deviceId, snapshot, input.timestamp);
74731
74779
  this.appendHistory(input.deviceId, snapshot);
74732
- this.emitSnapshot(input.deviceId, snapshot);
74780
+ this.emitSnapshot(input.deviceId, snapshot, "scene");
74733
74781
  this.sliceThrottle.push(input.deviceId, snapshot);
74734
74782
  }
74735
74783
  /** Drop a device's snapshot + history. Called on device removal. */
@@ -74795,7 +74843,7 @@ var ZoneAnalyticsProvider = class {
74795
74843
  if (snapshot) {
74796
74844
  this.appendHistory(deviceId, snapshot);
74797
74845
  this.noteLtsSamples(deviceId, snapshot, now);
74798
- this.emitSnapshot(deviceId, snapshot);
74846
+ this.emitSnapshot(deviceId, snapshot, "census");
74799
74847
  this.sliceThrottle.push(deviceId, snapshot);
74800
74848
  }
74801
74849
  }
@@ -74824,10 +74872,10 @@ var ZoneAnalyticsProvider = class {
74824
74872
  }
74825
74873
  /** Push one snapshot to the occupancy tap, isolating any consumer throw from
74826
74874
  * the frame path (occupancy notifications must never break analytics). */
74827
- emitSnapshot(deviceId, snapshot) {
74875
+ emitSnapshot(deviceId, snapshot, evidence) {
74828
74876
  if (!this.ctx.onSnapshot) return;
74829
74877
  try {
74830
- this.ctx.onSnapshot(deviceId, snapshot);
74878
+ this.ctx.onSnapshot(deviceId, snapshot, evidence);
74831
74879
  } catch (err) {
74832
74880
  this.ctx.logger.debug("zone-analytics occupancy tap failed", {
74833
74881
  tags: { deviceId },
@@ -77447,7 +77495,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
77447
77495
  listStationaryObjects: (deviceId) => this.stationaryRegistry?.listViews(deviceId) ?? [],
77448
77496
  listStationaryDeviceIds: () => this.stationaryRegistry?.deviceIds() ?? [],
77449
77497
  resolveZones: (deviceId) => this.resolveDeviceZones(deviceId),
77450
- onSnapshot: (deviceId, snapshot) => this.notificationCenter?.observeOccupancy(deviceId, snapshot)
77498
+ onSnapshot: (deviceId, snapshot, evidence) => this.notificationCenter?.observeOccupancy(deviceId, snapshot, evidence)
77451
77499
  });
77452
77500
  this.zoneAnalytics = zoneAnalytics;
77453
77501
  const audioMetrics = new AudioMetricsProvider({
@@ -79288,7 +79336,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
79288
79336
  return;
79289
79337
  }
79290
79338
  this.dropoutSkipsByKey.set(key, 0);
79291
- if (source === "pipeline" && this.stationaryRegistry) {
79339
+ if (planeRunsStationary(source) && this.stationaryRegistry) {
79292
79340
  await this.resolveDeviceStationarySettings(deviceId);
79293
79341
  await this.resolveDevicePackageDropSettings(deviceId);
79294
79342
  await this.resolveDeviceSummarySettings(deviceId);
@@ -79348,7 +79396,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
79348
79396
  });
79349
79397
  this.trackStore?.markRiderFolded(fold.trackId);
79350
79398
  }
79351
- if (source === "pipeline") this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
79399
+ if (planeRunsStationary(source)) this.stationaryRegistry?.noteFrame(deviceId, result.timestamp);
79352
79400
  if (this.stationaryRegistry && (result.stationaryConfirmed.length > 0 || result.stationaryWoken.length > 0)) this.stationaryRegistry.applyFrameOutcome({
79353
79401
  deviceId,
79354
79402
  confirmed: result.stationaryConfirmed,
@@ -79522,8 +79570,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
79522
79570
  firstSeen: this.trackStore?.peekActive(id)?.firstSeen ?? null
79523
79571
  });
79524
79572
  if (!outcome.confirmed.has(id)) {
79573
+ if (!suppressionMayRetract(source)) {
79574
+ log.info("birth suppression held — the gate is not this root detector", {
79575
+ tags: { deviceId },
79576
+ meta: {
79577
+ trackId: id,
79578
+ className: t.className,
79579
+ source
79580
+ }
79581
+ });
79582
+ continue;
79583
+ }
79525
79584
  this.suppressedBirths.reject(key, id);
79526
79585
  this.trackStore?.dropActive(id);
79586
+ this.clearLiveTrackState(deviceId, id);
79527
79587
  log.info("birth suppressed — track record retracted", { meta: {
79528
79588
  trackId: id,
79529
79589
  className: t.className,
@@ -79653,7 +79713,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
79653
79713
  const stationarySettings = this.stationarySettingsFromCache(deviceId);
79654
79714
  const packageSettings = this.packageDropSettingsFromCache(deviceId);
79655
79715
  const packageClasses = new Set(packageSettings.packageDropClassFilter);
79656
- if (source === "pipeline" && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.enabled) {
79716
+ if (planeRunsStationary(source) && this.stationaryRegistry && this.stationaryEvidence && stationarySettings.enabled) {
79657
79717
  const evidence = this.stationaryEvidence;
79658
79718
  for (const t of result.tracked) {
79659
79719
  if (packageClasses.has(t.className)) continue;
@@ -79687,7 +79747,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
79687
79747
  }
79688
79748
  evidence.retainTracks(deviceId, currentTrackIds);
79689
79749
  }
79690
- if (source === "pipeline") this.runPackageStillness({
79750
+ if (planeRunsStationary(source)) this.runPackageStillness({
79691
79751
  deviceId,
79692
79752
  tracked: result.tracked,
79693
79753
  packageClasses,
@@ -82945,7 +83005,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
82945
83005
  ...active.subLabelMeta !== void 0 ? { subLabelMeta: active.subLabelMeta } : {}
82946
83006
  } : void 0;
82947
83007
  });
82948
- if (source === "pipeline" && this.stationaryRegistry) {
83008
+ if (planeRunsStationary(source) && this.stationaryRegistry) {
82949
83009
  const registry = this.stationaryRegistry;
82950
83010
  p.setStationaryGate({ filter: (input) => {
82951
83011
  const s = this.stationarySettingsFromCache(deviceId);
@@ -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-DaT9RhCO.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DJ9dpbgD.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.248",
3
+ "version": "1.2.249",
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",