@camstack/addon-post-analysis 1.2.192 → 1.2.193

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.
@@ -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.77",
6
+ version: "1.2.78",
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.157",
21
+ version: "1.2.158",
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.125",
36
+ version: "1.2.126",
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.157",
39
+ version: "1.2.158",
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.77",
48
+ version: "1.2.78",
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.125",
84
+ version: "1.2.126",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -34017,6 +34017,18 @@ var LooseBindSampler = class {
34017
34017
  this.counts.clear();
34018
34018
  }
34019
34019
  };
34020
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
34021
+ * counts are per camera and per detection source. */
34022
+ var SpawnRefusalSampler = class {
34023
+ counts = /* @__PURE__ */ new Map();
34024
+ /** The running count when this refusal should be reported, else `undefined`. */
34025
+ next(reason) {
34026
+ const count = (this.counts.get(reason) ?? 0) + 1;
34027
+ this.counts.set(reason, count);
34028
+ if (count <= 10) return count;
34029
+ return count % 10 === 0 ? count : void 0;
34030
+ }
34031
+ };
34020
34032
  //#endregion
34021
34033
  //#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
34022
34034
  var DEFAULT_TRACKER_CONFIG = {
@@ -34101,6 +34113,10 @@ var SortTracker = class SortTracker {
34101
34113
  /** Sink for salvage-bind diagnostics. Null until an owner that knows the
34102
34114
  * `deviceId` wires one — see {@link setBindObserver}. */
34103
34115
  bindObserver = null;
34116
+ /** Per-reason counters behind the sampled dropped-detection diagnostics. */
34117
+ refusalSampler = new SpawnRefusalSampler();
34118
+ /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34119
+ spawnRefusalObserver = null;
34104
34120
  constructor(config = {}) {
34105
34121
  this.config = {
34106
34122
  ...DEFAULT_TRACKER_CONFIG,
@@ -34116,6 +34132,37 @@ var SortTracker = class SortTracker {
34116
34132
  setBindObserver(observer) {
34117
34133
  this.bindObserver = observer;
34118
34134
  }
34135
+ /**
34136
+ * Wire the diagnostics sink for detections the tracker DROPS — a spawn a
34137
+ * gate refused, or a box a track of another class absorbed. Same contract as
34138
+ * {@link setBindObserver}: the tracker has no `deviceId`, the caller stamps
34139
+ * it. Nothing is computed when no observer is wired.
34140
+ */
34141
+ setSpawnRefusalObserver(observer) {
34142
+ this.spawnRefusalObserver = observer;
34143
+ }
34144
+ /** Emit one dropped-detection record, subject to the refusal sampler. */
34145
+ reportRefusal(reason, det, extra = {}) {
34146
+ const observer = this.spawnRefusalObserver;
34147
+ if (!observer) return;
34148
+ const refusalCount = this.refusalSampler.next(reason);
34149
+ if (refusalCount === void 0) return;
34150
+ observer({
34151
+ reason,
34152
+ detClass: det.class,
34153
+ detScore: det.score,
34154
+ bbox: det.bbox,
34155
+ ...det.nativeRecovery !== void 0 ? { nativeRecoveryScore: det.nativeRecovery.score } : {},
34156
+ ...extra.threshold !== void 0 ? { threshold: extra.threshold } : {},
34157
+ ...extra.measured !== void 0 ? { measured: extra.measured } : {},
34158
+ ...extra.against !== void 0 ? {
34159
+ againstTrackId: extra.against.id,
34160
+ againstTrackClass: extra.against.class
34161
+ } : {},
34162
+ refusalCount,
34163
+ sampledEvery: 10
34164
+ });
34165
+ }
34119
34166
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
34120
34167
  * Nothing is computed when no observer is wired. */
34121
34168
  reportBind(stage, gate, track, det, timestamp) {
@@ -34175,9 +34222,49 @@ var SortTracker = class SortTracker {
34175
34222
  * exact class when `classGating` is off (no gate at all) — matching the legacy
34176
34223
  * contract — otherwise same association group (group == exact class unless
34177
34224
  * `classGroupAssoc` widened it to {person,animal}). */
34178
- associable(trackClass, detClass) {
34225
+ associable(trackClass, det) {
34226
+ if (!this.sameGroupAsMeasured(trackClass, det)) return false;
34179
34227
  if (!this.config.classGating) return true;
34180
- return this.assocGroup(trackClass) === this.assocGroup(detClass);
34228
+ return this.assocGroup(trackClass) === this.assocGroup(det.class);
34229
+ }
34230
+ /**
34231
+ * A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378).
34232
+ *
34233
+ * `classGroupAssoc` collapses {person, animal} into one association group for
34234
+ * one failure of the COARSE full-frame pass: a bending person re-labelled
34235
+ * `animal` for a frame, over the person's own box. Letting that box re-match
34236
+ * the person track is strictly better than minting a concurrent id.
34237
+ *
34238
+ * The runner's second pass makes the opposite measurement — the same detector
34239
+ * on a 640-800 px native square around a motion region the full-frame boxes
34240
+ * do NOT account for. Such a box is, by construction, not the subject the
34241
+ * full pass already has; absorbing it into a person track means the animal is
34242
+ * never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
34243
+ * an `animal` at `nativeRecoveryScore=0.833`.
34244
+ *
34245
+ * So the widening does not apply to a detection carrying `nativeRecovery`:
34246
+ * it associates to its OWN class or it spawns. Every other guard on that
34247
+ * spawn still runs — the per-class score floor, the area floor, the
34248
+ * `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
34249
+ * still beats a cross-class animal.
34250
+ */
34251
+ sameGroupAsMeasured(trackClass, det) {
34252
+ if (det.nativeRecovery === void 0) return true;
34253
+ return trackClass === det.class;
34254
+ }
34255
+ /**
34256
+ * Whether a matched track's box refuses `det` a birth. The gate exists for a
34257
+ * detector double-firing INSIDE a subject it already has, so it is a
34258
+ * same-subject claim: for a box the native pass measured (D378), a
34259
+ * cross-class occluder is not that claim — a dog at its owner's knee is 100%
34260
+ * contained in the person's box and is not the person. 2026-09-07 07:56:37,
34261
+ * an `animal` at `nativeRecoveryScore=0.5143` refused on `threshold=0.6`.
34262
+ * A SAME-class occluder still refuses it: that is the torso the crop cut out
34263
+ * of a person the full pass already has.
34264
+ */
34265
+ occludesSpawn(det, occluder) {
34266
+ if (!this.sameGroupAsMeasured(occluder.class, det)) return false;
34267
+ return containment(det.bbox, occluder.bbox) >= this.config.occlusionContainment;
34181
34268
  }
34182
34269
  /** Whether a spawn candidate clears its per-class minimum bbox-area floor
34183
34270
  * (fraction of frame area). Ungated when disabled, when the class has no floor,
@@ -34199,14 +34286,30 @@ var SortTracker = class SortTracker {
34199
34286
  isDuplicateSpawn(det, live, timestamp) {
34200
34287
  for (const t of live) {
34201
34288
  if (!this.dedupCompatible(t.class, det.class)) continue;
34202
- if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34289
+ const overlap = iou$3(det.bbox, t.bbox);
34290
+ if (overlap >= this.config.dedupSpawnIou) {
34291
+ this.reportRefusal("dedup-iou", det, {
34292
+ threshold: this.config.dedupSpawnIou,
34293
+ measured: overlap,
34294
+ against: t
34295
+ });
34296
+ return true;
34297
+ }
34203
34298
  if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34204
34299
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
34205
34300
  return true;
34206
34301
  }
34207
34302
  }
34208
- if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
34209
- for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34303
+ if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.nativeRecovery === void 0 && det.score < this.config.animalOverPersonMaxScore) for (const t of live) {
34304
+ const overlap = iou$3(det.bbox, t.bbox);
34305
+ if (t.class === PERSON_CLASS && overlap >= this.config.animalOverPersonIou) {
34306
+ this.reportRefusal("animal-over-person", det, {
34307
+ threshold: this.config.animalOverPersonMaxScore,
34308
+ measured: overlap,
34309
+ against: t
34310
+ });
34311
+ return true;
34312
+ }
34210
34313
  }
34211
34314
  return false;
34212
34315
  }
@@ -34321,6 +34424,7 @@ var SortTracker = class SortTracker {
34321
34424
  * class.
34322
34425
  */
34323
34426
  looseMatch(track, det, timestamp) {
34427
+ if (!this.sameGroupAsMeasured(track.class, det)) return null;
34324
34428
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34325
34429
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34326
34430
  const tc = bboxCentroid(track.bbox);
@@ -34407,7 +34511,7 @@ var SortTracker = class SortTracker {
34407
34511
  for (const di of candidateIdxs) {
34408
34512
  if (used.has(di)) continue;
34409
34513
  const det = detections[di];
34410
- if (!this.associable(track.class, det.class)) continue;
34514
+ if (!this.associable(track.class, det)) continue;
34411
34515
  const score = iou$3(pbox, det.bbox);
34412
34516
  if (score >= iouThreshold) pairs.push({
34413
34517
  track,
@@ -34486,6 +34590,7 @@ var SortTracker = class SortTracker {
34486
34590
  }
34487
34591
  if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
34488
34592
  for (const [track, det] of matched) {
34593
+ if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34489
34594
  const prevCenter = bboxCentroid({
34490
34595
  x: track.bbox.x,
34491
34596
  y: track.bbox.y,
@@ -34514,15 +34619,18 @@ var SortTracker = class SortTracker {
34514
34619
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34515
34620
  this.addClassVote(track, det);
34516
34621
  }
34517
- const occluderBoxes = [];
34518
- for (const track of matchedTracks) occluderBoxes.push(track.bbox);
34622
+ const occluders = [];
34623
+ for (const track of matchedTracks) occluders.push({
34624
+ bbox: track.bbox,
34625
+ class: track.class
34626
+ });
34519
34627
  const surviving = [];
34520
34628
  for (const track of this.tracks) {
34521
34629
  if (matchedTracks.has(track)) {
34522
34630
  surviving.push(track);
34523
34631
  continue;
34524
34632
  }
34525
- const occluded = this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(this.predicted(track), ob) >= this.config.occlusionContainment);
34633
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
34526
34634
  track.age++;
34527
34635
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34528
34636
  track.lost = true;
@@ -34601,14 +34709,25 @@ var SortTracker = class SortTracker {
34601
34709
  for (const di of unmatchedIdx) {
34602
34710
  const det = detections[di];
34603
34711
  if (!this.meetsClassScoreFloor(det)) {
34712
+ this.reportRefusal("score-floor", det, {
34713
+ threshold: this.config.classMinScores[det.class] ?? 0,
34714
+ measured: det.score
34715
+ });
34604
34716
  used.add(di);
34605
34717
  continue;
34606
34718
  }
34607
34719
  if (!this.meetsSpawnAreaFloor(det, frameArea)) {
34720
+ this.reportRefusal("area-floor", det, {
34721
+ threshold: this.config.classSpawnAreaFracs[det.class] ?? 0,
34722
+ measured: frameArea > 0 ? det.bbox.w * det.bbox.h / frameArea : 0
34723
+ });
34608
34724
  used.add(di);
34609
34725
  continue;
34610
34726
  }
34611
- if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
34727
+ if (this.config.occlusionEnabled && occluders.some((ob) => this.occludesSpawn(det, ob))) {
34728
+ this.reportRefusal("occlusion", det, { threshold: this.config.occlusionContainment });
34729
+ continue;
34730
+ }
34612
34731
  if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving, timestamp)) {
34613
34732
  used.add(di);
34614
34733
  continue;
@@ -35096,6 +35215,9 @@ var FrameProcessor = class {
35096
35215
  * Only the rescue / resurrection / growth branches report, and they report
35097
35216
  * sampled: stage 1 runs per track per frame at frame-rate.
35098
35217
  */
35218
+ setSpawnRefusalObserver(observer) {
35219
+ this.tracker.setSpawnRefusalObserver(observer);
35220
+ }
35099
35221
  setBindObserver(observer) {
35100
35222
  this.tracker.setBindObserver(observer);
35101
35223
  }
@@ -35118,6 +35240,7 @@ var FrameProcessor = class {
35118
35240
  const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
35119
35241
  const plateByBbox = /* @__PURE__ */ new Map();
35120
35242
  const maskByBbox = /* @__PURE__ */ new Map();
35243
+ const nativeRecoveryByBbox = /* @__PURE__ */ new Map();
35121
35244
  const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
35122
35245
  const bbox = {
35123
35246
  x: det.bbox.x,
@@ -35129,8 +35252,10 @@ var FrameProcessor = class {
35129
35252
  class: det.macroClass,
35130
35253
  originalClass: det.debug?.originalClass ?? det.macroClass,
35131
35254
  score: det.score,
35132
- bbox
35255
+ bbox,
35256
+ ...det.nativeRecovery !== void 0 ? { nativeRecovery: det.nativeRecovery } : {}
35133
35257
  };
35258
+ if (det.nativeRecovery !== void 0) nativeRecoveryByBbox.set(bbox, det.nativeRecovery);
35134
35259
  if (det.labels.length > 0) labelsByBbox.set(bbox, det.labels);
35135
35260
  if (det.debug?.originalClassTier !== void 0) originalClassTierByBbox.set(bbox, det.debug.originalClassTier);
35136
35261
  if (det.embedding !== void 0) embeddingByBbox.set(bbox, {
@@ -35355,6 +35480,7 @@ var FrameProcessor = class {
35355
35480
  const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
35356
35481
  const plate = plateByBbox.get(td.bbox);
35357
35482
  const sourceDetectionId = sourceIdByBbox.get(td.bbox);
35483
+ const nativeRecovery = td.matchedThisFrame !== false ? nativeRecoveryByBbox.get(td.bbox) : void 0;
35358
35484
  return {
35359
35485
  trackId: td.trackId,
35360
35486
  ...sourceDetectionId !== void 0 ? { sourceDetectionId } : {},
@@ -35377,7 +35503,8 @@ var FrameProcessor = class {
35377
35503
  plateText: plate.text,
35378
35504
  plateScore: plate.score,
35379
35505
  plateBbox: plate.bbox
35380
- } : {}
35506
+ } : {},
35507
+ ...nativeRecovery !== void 0 ? { nativeRecovery } : {}
35381
35508
  };
35382
35509
  });
35383
35510
  this.lastLabelWriteRefusals = refusals;
@@ -45238,7 +45365,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45238
45365
  for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
45239
45366
  if (!best || d.score > best.score) best = d;
45240
45367
  } else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
45241
- const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45368
+ const bar = barFor(candidate, config, deps);
45242
45369
  const confirmed = best !== void 0 && best.score >= bar;
45243
45370
  const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
45244
45371
  return {
@@ -45277,7 +45404,27 @@ function promoteSecondary(candidate, detection, bar, deps) {
45277
45404
  }
45278
45405
  };
45279
45406
  }
45407
+ /**
45408
+ * The bar this candidate faces. Never below the configured one: the
45409
+ * `minConfidenceFor` hook is monotonic by construction, and clamping here says
45410
+ * so at the point of use rather than trusting the caller to have been careful.
45411
+ */
45412
+ function barFor(candidate, config, deps) {
45413
+ return Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45414
+ }
45280
45415
  async function confirmOne(candidate, config, deps, exhausted) {
45416
+ const native = candidate.nativeRecovery;
45417
+ if (native !== void 0) {
45418
+ const bar = barFor(candidate, config, deps);
45419
+ if (native.score >= bar) return {
45420
+ trackId: candidate.trackId,
45421
+ verdict: "confirmed",
45422
+ reason: "native-pass",
45423
+ className: candidate.className,
45424
+ bestScore: native.score,
45425
+ appliedMinConfidence: bar
45426
+ };
45427
+ }
45281
45428
  const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
45282
45429
  if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
45283
45430
  try {
@@ -55167,6 +55314,77 @@ function rowToEntry(id, data) {
55167
55314
  };
55168
55315
  }
55169
55316
  //#endregion
55317
+ //#region src/pipeline-analytics/pipeline/started-births.ts
55318
+ /**
55319
+ * Memory of births whose ritual has already RUN, per device+source.
55320
+ *
55321
+ * A birth is a decision, and a decision is taken once. Until 2026-09-07 it
55322
+ * could be taken twice, on camera 615 it routinely was, and the second vote
55323
+ * won:
55324
+ *
55325
+ * 04:59:38.335Z confirmation gate: birth confirmed {trackId=f585b2ee…
55326
+ * className=animal bestScore=0.7068}
55327
+ * 04:59:38.335Z track started {trackId=f585b2ee…}
55328
+ * 04:59:38.821Z confirmation gate: birth suppressed (false positive)
55329
+ * {trackId=f585b2ee… className=animal} ← no bestScore:
55330
+ * the second crop held no compatible detection AT ALL
55331
+ * 04:59:38.821Z birth suppressed — track record retracted
55332
+ *
55333
+ * TWO conditions have to meet, and both are structural:
55334
+ *
55335
+ * 1. `processFrame` reads `lastActiveTrackIds` at the top and writes it at the
55336
+ * bottom, and AWAITS the confirmation gate in between (a crop round trip to
55337
+ * the frame's node plus a re-detection: 130–490 ms measured on this camera,
55338
+ * against 100 ms frame spacing). Several frames are therefore in flight at
55339
+ * once and each of them sees the same fresh id as unjudged.
55340
+ * 2. The other birth discriminator, `positionsCount > 1`, does not close the
55341
+ * gap either: a COASTED frame deliberately appends no position
55342
+ * (`TrackStore.upsert`, `matched: false`), so a track the full-frame pass
55343
+ * cannot see — which is the entire premise of the small-subject recovery —
55344
+ * keeps exactly one position for as long as it coasts, and
55345
+ * `classifyTrackAppearance` keeps answering `birth`.
55346
+ *
55347
+ * So the id that has already been born is remembered HERE, and both halves of
55348
+ * the birth path consult it: it is not offered to the gate again, and a verdict
55349
+ * that arrives for it anyway cannot retract it. Same lifetime discipline as
55350
+ * {@link SuppressedBirthRegistry} — held while the tracker still carries the
55351
+ * id, forgotten the frame it stops.
55352
+ */
55353
+ var StartedBirthRegistry = class {
55354
+ byDevice = /* @__PURE__ */ new Map();
55355
+ /** Record that this id's birth ritual ran — `start`, firstFrame, the lot. */
55356
+ markStarted(deviceKey, trackId) {
55357
+ let ids = this.byDevice.get(deviceKey);
55358
+ if (!ids) {
55359
+ ids = /* @__PURE__ */ new Set();
55360
+ this.byDevice.set(deviceKey, ids);
55361
+ }
55362
+ ids.add(trackId);
55363
+ }
55364
+ hasStarted(deviceKey, trackId) {
55365
+ return this.byDevice.get(deviceKey)?.has(trackId) === true;
55366
+ }
55367
+ /**
55368
+ * Forget every started id the tracker is no longer carrying. Called once per
55369
+ * frame with the ids present THIS frame — an id the tracker has dropped can
55370
+ * never be re-offered as a birth, so holding it serves nothing.
55371
+ */
55372
+ retain(deviceKey, currentTrackIds) {
55373
+ const ids = this.byDevice.get(deviceKey);
55374
+ if (!ids) return;
55375
+ for (const id of ids) if (!currentTrackIds.has(id)) ids.delete(id);
55376
+ if (ids.size === 0) this.byDevice.delete(deviceKey);
55377
+ }
55378
+ /** Started ids currently held for a device — diagnostics and tests. */
55379
+ size(deviceKey) {
55380
+ return this.byDevice.get(deviceKey)?.size ?? 0;
55381
+ }
55382
+ /** Drop a device's memory wholesale (device removed / pipeline reset). */
55383
+ clearDevice(deviceKey) {
55384
+ this.byDevice.delete(deviceKey);
55385
+ }
55386
+ };
55387
+ //#endregion
55170
55388
  //#region src/pipeline-analytics/pipeline/suppressed-births.ts
55171
55389
  /**
55172
55390
  * Memory of births the confirmation gate rejected, per device.
@@ -67527,6 +67745,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
67527
67745
  /** See `pipeline/suppressed-births.ts` — rejected births must not be
67528
67746
  * re-upserted, and must be forgotten when the tracker drops the id. */
67529
67747
  suppressedBirths = new SuppressedBirthRegistry();
67748
+ /** Ids whose birth ritual has already run — a birth is decided ONCE (D377). */
67749
+ startedBirths = new StartedBirthRegistry();
67530
67750
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
67531
67751
  deferredBirths = new DeferredBirthRegistry();
67532
67752
  /**
@@ -70745,10 +70965,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70745
70965
  if (prevIds.has(id)) continue;
70746
70966
  const t = result.tracked.find((x) => x.trackId === id);
70747
70967
  if (!t) continue;
70748
- if (classifyTrackAppearance({
70968
+ const appearance = classifyTrackAppearance({
70749
70969
  inPrevActive: false,
70750
70970
  positionsCount: positionsCountById.get(id) ?? 1
70751
- }) === "resurrection") {
70971
+ });
70972
+ if (this.startedBirths.hasStarted(key, id)) continue;
70973
+ if (appearance === "resurrection") {
70752
70974
  log.info("track resumed", {
70753
70975
  tags: { deviceId },
70754
70976
  meta: {
@@ -70784,7 +71006,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70784
71006
  const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
70785
71007
  trackId: id,
70786
71008
  className: t.className,
70787
- bbox: t.bbox
71009
+ bbox: t.bbox,
71010
+ ...t.nativeRecovery !== void 0 ? { nativeRecovery: t.nativeRecovery } : {}
70788
71011
  })), {
70789
71012
  deviceId,
70790
71013
  source,
@@ -70809,6 +71032,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70809
71032
  const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
70810
71033
  const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
70811
71034
  this.deferredBirths.resolve(key, id);
71035
+ if (this.startedBirths.hasStarted(key, id)) {
71036
+ this.deferredBirths.resolve(key, id);
71037
+ log.info("late birth verdict ignored — the track was already born", { meta: {
71038
+ trackId: id,
71039
+ className: t.className,
71040
+ source,
71041
+ verdict: outcome.confirmed.has(id) ? "confirmed" : "suppressed"
71042
+ } });
71043
+ continue;
71044
+ }
70812
71045
  if (!outcome.confirmed.has(id)) {
70813
71046
  this.suppressedBirths.reject(key, id);
70814
71047
  this.trackStore?.dropActive(id);
@@ -70846,6 +71079,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70846
71079
  })));
70847
71080
  this.failureReport.notePersonBirth(deviceId, overlap === null ? void 0 : REASON_PERSON_OVER_VEHICLE);
70848
71081
  }
71082
+ this.startedBirths.markStarted(key, id);
70849
71083
  log.info("track started", { meta: {
70850
71084
  trackId: id,
70851
71085
  className: t.className,
@@ -70913,6 +71147,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70913
71147
  });
70914
71148
  }
70915
71149
  this.suppressedBirths.retain(key, currentTrackIds);
71150
+ this.startedBirths.retain(key, currentTrackIds);
70916
71151
  this.deferredBirths.retain(key, currentTrackIds);
70917
71152
  this.lastActiveTrackIds.set(key, currentTrackIds);
70918
71153
  if (currentTrackIds.size > 0) this.lastTrackActivityMs.set(deviceId, Date.now());
@@ -71492,6 +71727,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71492
71727
  return allConfirmed();
71493
71728
  }
71494
71729
  const nodeId = frameHandle.nodeId;
71730
+ const candidateNativeRecovery = /* @__PURE__ */ new Map();
71731
+ for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
71495
71732
  return confirmBirths(candidates, config, {
71496
71733
  fetchCrop: (candidate) => this.captureScheduler.request({
71497
71734
  deviceId,
@@ -71541,6 +71778,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71541
71778
  bestIncompatibleScore: decision.bestIncompatibleScore
71542
71779
  } : {},
71543
71780
  minConfidence: config.minConfidence,
71781
+ ...decision.reason === "native-pass" ? {
71782
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
71783
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
71784
+ } : {},
71544
71785
  ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
71545
71786
  appliedMinConfidence: decision.appliedMinConfidence,
71546
71787
  barRaisedByPhantomCell: true
@@ -73802,6 +74043,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73802
74043
  }
73803
74044
  });
73804
74045
  });
74046
+ p.setSpawnRefusalObserver((record) => {
74047
+ this.ctx.logger.info("tracker: detection dropped — no track was born from it", {
74048
+ tags: { deviceId },
74049
+ meta: {
74050
+ source,
74051
+ ...record
74052
+ }
74053
+ });
74054
+ });
73805
74055
  p.setTrackLabelState((trackId) => {
73806
74056
  const active = this.trackStore?.getActiveByTrack(trackId);
73807
74057
  return active !== null && active !== void 0 ? {
@@ -33992,6 +33992,18 @@ var LooseBindSampler = class {
33992
33992
  this.counts.clear();
33993
33993
  }
33994
33994
  };
33995
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
33996
+ * counts are per camera and per detection source. */
33997
+ var SpawnRefusalSampler = class {
33998
+ counts = /* @__PURE__ */ new Map();
33999
+ /** The running count when this refusal should be reported, else `undefined`. */
34000
+ next(reason) {
34001
+ const count = (this.counts.get(reason) ?? 0) + 1;
34002
+ this.counts.set(reason, count);
34003
+ if (count <= 10) return count;
34004
+ return count % 10 === 0 ? count : void 0;
34005
+ }
34006
+ };
33995
34007
  //#endregion
33996
34008
  //#region src/pipeline-analytics/pipeline/tracker/sort-tracker.ts
33997
34009
  var DEFAULT_TRACKER_CONFIG = {
@@ -34076,6 +34088,10 @@ var SortTracker = class SortTracker {
34076
34088
  /** Sink for salvage-bind diagnostics. Null until an owner that knows the
34077
34089
  * `deviceId` wires one — see {@link setBindObserver}. */
34078
34090
  bindObserver = null;
34091
+ /** Per-reason counters behind the sampled dropped-detection diagnostics. */
34092
+ refusalSampler = new SpawnRefusalSampler();
34093
+ /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34094
+ spawnRefusalObserver = null;
34079
34095
  constructor(config = {}) {
34080
34096
  this.config = {
34081
34097
  ...DEFAULT_TRACKER_CONFIG,
@@ -34091,6 +34107,37 @@ var SortTracker = class SortTracker {
34091
34107
  setBindObserver(observer) {
34092
34108
  this.bindObserver = observer;
34093
34109
  }
34110
+ /**
34111
+ * Wire the diagnostics sink for detections the tracker DROPS — a spawn a
34112
+ * gate refused, or a box a track of another class absorbed. Same contract as
34113
+ * {@link setBindObserver}: the tracker has no `deviceId`, the caller stamps
34114
+ * it. Nothing is computed when no observer is wired.
34115
+ */
34116
+ setSpawnRefusalObserver(observer) {
34117
+ this.spawnRefusalObserver = observer;
34118
+ }
34119
+ /** Emit one dropped-detection record, subject to the refusal sampler. */
34120
+ reportRefusal(reason, det, extra = {}) {
34121
+ const observer = this.spawnRefusalObserver;
34122
+ if (!observer) return;
34123
+ const refusalCount = this.refusalSampler.next(reason);
34124
+ if (refusalCount === void 0) return;
34125
+ observer({
34126
+ reason,
34127
+ detClass: det.class,
34128
+ detScore: det.score,
34129
+ bbox: det.bbox,
34130
+ ...det.nativeRecovery !== void 0 ? { nativeRecoveryScore: det.nativeRecovery.score } : {},
34131
+ ...extra.threshold !== void 0 ? { threshold: extra.threshold } : {},
34132
+ ...extra.measured !== void 0 ? { measured: extra.measured } : {},
34133
+ ...extra.against !== void 0 ? {
34134
+ againstTrackId: extra.against.id,
34135
+ againstTrackClass: extra.against.class
34136
+ } : {},
34137
+ refusalCount,
34138
+ sampledEvery: 10
34139
+ });
34140
+ }
34094
34141
  /** Emit one salvage decision, subject to {@link LOOSE_BIND_LOG_SAMPLE_EVERY}.
34095
34142
  * Nothing is computed when no observer is wired. */
34096
34143
  reportBind(stage, gate, track, det, timestamp) {
@@ -34150,9 +34197,49 @@ var SortTracker = class SortTracker {
34150
34197
  * exact class when `classGating` is off (no gate at all) — matching the legacy
34151
34198
  * contract — otherwise same association group (group == exact class unless
34152
34199
  * `classGroupAssoc` widened it to {person,animal}). */
34153
- associable(trackClass, detClass) {
34200
+ associable(trackClass, det) {
34201
+ if (!this.sameGroupAsMeasured(trackClass, det)) return false;
34154
34202
  if (!this.config.classGating) return true;
34155
- return this.assocGroup(trackClass) === this.assocGroup(detClass);
34203
+ return this.assocGroup(trackClass) === this.assocGroup(det.class);
34204
+ }
34205
+ /**
34206
+ * A NATIVE-MEASURED BOX IS ITS OWN SUBJECT ACROSS CLASSES (D378).
34207
+ *
34208
+ * `classGroupAssoc` collapses {person, animal} into one association group for
34209
+ * one failure of the COARSE full-frame pass: a bending person re-labelled
34210
+ * `animal` for a frame, over the person's own box. Letting that box re-match
34211
+ * the person track is strictly better than minting a concurrent id.
34212
+ *
34213
+ * The runner's second pass makes the opposite measurement — the same detector
34214
+ * on a 640-800 px native square around a motion region the full-frame boxes
34215
+ * do NOT account for. Such a box is, by construction, not the subject the
34216
+ * full pass already has; absorbing it into a person track means the animal is
34217
+ * never seen at all, which on 2026-09-07 07:51:33 is exactly what happened to
34218
+ * an `animal` at `nativeRecoveryScore=0.833`.
34219
+ *
34220
+ * So the widening does not apply to a detection carrying `nativeRecovery`:
34221
+ * it associates to its OWN class or it spawns. Every other guard on that
34222
+ * spawn still runs — the per-class score floor, the area floor, the
34223
+ * `dedupSpawnIou` overlap, and `resolveDuplicates` afterwards, where a person
34224
+ * still beats a cross-class animal.
34225
+ */
34226
+ sameGroupAsMeasured(trackClass, det) {
34227
+ if (det.nativeRecovery === void 0) return true;
34228
+ return trackClass === det.class;
34229
+ }
34230
+ /**
34231
+ * Whether a matched track's box refuses `det` a birth. The gate exists for a
34232
+ * detector double-firing INSIDE a subject it already has, so it is a
34233
+ * same-subject claim: for a box the native pass measured (D378), a
34234
+ * cross-class occluder is not that claim — a dog at its owner's knee is 100%
34235
+ * contained in the person's box and is not the person. 2026-09-07 07:56:37,
34236
+ * an `animal` at `nativeRecoveryScore=0.5143` refused on `threshold=0.6`.
34237
+ * A SAME-class occluder still refuses it: that is the torso the crop cut out
34238
+ * of a person the full pass already has.
34239
+ */
34240
+ occludesSpawn(det, occluder) {
34241
+ if (!this.sameGroupAsMeasured(occluder.class, det)) return false;
34242
+ return containment(det.bbox, occluder.bbox) >= this.config.occlusionContainment;
34156
34243
  }
34157
34244
  /** Whether a spawn candidate clears its per-class minimum bbox-area floor
34158
34245
  * (fraction of frame area). Ungated when disabled, when the class has no floor,
@@ -34174,14 +34261,30 @@ var SortTracker = class SortTracker {
34174
34261
  isDuplicateSpawn(det, live, timestamp) {
34175
34262
  for (const t of live) {
34176
34263
  if (!this.dedupCompatible(t.class, det.class)) continue;
34177
- if (iou$3(det.bbox, t.bbox) >= this.config.dedupSpawnIou) return true;
34264
+ const overlap = iou$3(det.bbox, t.bbox);
34265
+ if (overlap >= this.config.dedupSpawnIou) {
34266
+ this.reportRefusal("dedup-iou", det, {
34267
+ threshold: this.config.dedupSpawnIou,
34268
+ measured: overlap,
34269
+ against: t
34270
+ });
34271
+ return true;
34272
+ }
34178
34273
  if (t.class === det.class && growthAdjacentMatch(t.bbox, det.bbox, SortTracker.growthContext(t, timestamp))) {
34179
34274
  this.reportBind("spawn-dedup", "growth", t, det, timestamp);
34180
34275
  return true;
34181
34276
  }
34182
34277
  }
34183
- if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.score < this.config.animalOverPersonMaxScore) {
34184
- for (const t of live) if (t.class === PERSON_CLASS && iou$3(det.bbox, t.bbox) >= this.config.animalOverPersonIou) return true;
34278
+ if (this.config.personAnimalDedup && det.class === ANIMAL_CLASS && det.nativeRecovery === void 0 && det.score < this.config.animalOverPersonMaxScore) for (const t of live) {
34279
+ const overlap = iou$3(det.bbox, t.bbox);
34280
+ if (t.class === PERSON_CLASS && overlap >= this.config.animalOverPersonIou) {
34281
+ this.reportRefusal("animal-over-person", det, {
34282
+ threshold: this.config.animalOverPersonMaxScore,
34283
+ measured: overlap,
34284
+ against: t
34285
+ });
34286
+ return true;
34287
+ }
34185
34288
  }
34186
34289
  return false;
34187
34290
  }
@@ -34296,6 +34399,7 @@ var SortTracker = class SortTracker {
34296
34399
  * class.
34297
34400
  */
34298
34401
  looseMatch(track, det, timestamp) {
34402
+ if (!this.sameGroupAsMeasured(track.class, det)) return null;
34299
34403
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34300
34404
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34301
34405
  const tc = bboxCentroid(track.bbox);
@@ -34382,7 +34486,7 @@ var SortTracker = class SortTracker {
34382
34486
  for (const di of candidateIdxs) {
34383
34487
  if (used.has(di)) continue;
34384
34488
  const det = detections[di];
34385
- if (!this.associable(track.class, det.class)) continue;
34489
+ if (!this.associable(track.class, det)) continue;
34386
34490
  const score = iou$3(pbox, det.bbox);
34387
34491
  if (score >= iouThreshold) pairs.push({
34388
34492
  track,
@@ -34461,6 +34565,7 @@ var SortTracker = class SortTracker {
34461
34565
  }
34462
34566
  if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
34463
34567
  for (const [track, det] of matched) {
34568
+ if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34464
34569
  const prevCenter = bboxCentroid({
34465
34570
  x: track.bbox.x,
34466
34571
  y: track.bbox.y,
@@ -34489,15 +34594,18 @@ var SortTracker = class SortTracker {
34489
34594
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34490
34595
  this.addClassVote(track, det);
34491
34596
  }
34492
- const occluderBoxes = [];
34493
- for (const track of matchedTracks) occluderBoxes.push(track.bbox);
34597
+ const occluders = [];
34598
+ for (const track of matchedTracks) occluders.push({
34599
+ bbox: track.bbox,
34600
+ class: track.class
34601
+ });
34494
34602
  const surviving = [];
34495
34603
  for (const track of this.tracks) {
34496
34604
  if (matchedTracks.has(track)) {
34497
34605
  surviving.push(track);
34498
34606
  continue;
34499
34607
  }
34500
- const occluded = this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(this.predicted(track), ob) >= this.config.occlusionContainment);
34608
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
34501
34609
  track.age++;
34502
34610
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34503
34611
  track.lost = true;
@@ -34576,14 +34684,25 @@ var SortTracker = class SortTracker {
34576
34684
  for (const di of unmatchedIdx) {
34577
34685
  const det = detections[di];
34578
34686
  if (!this.meetsClassScoreFloor(det)) {
34687
+ this.reportRefusal("score-floor", det, {
34688
+ threshold: this.config.classMinScores[det.class] ?? 0,
34689
+ measured: det.score
34690
+ });
34579
34691
  used.add(di);
34580
34692
  continue;
34581
34693
  }
34582
34694
  if (!this.meetsSpawnAreaFloor(det, frameArea)) {
34695
+ this.reportRefusal("area-floor", det, {
34696
+ threshold: this.config.classSpawnAreaFracs[det.class] ?? 0,
34697
+ measured: frameArea > 0 ? det.bbox.w * det.bbox.h / frameArea : 0
34698
+ });
34583
34699
  used.add(di);
34584
34700
  continue;
34585
34701
  }
34586
- if (this.config.occlusionEnabled && occluderBoxes.some((ob) => containment(det.bbox, ob) >= this.config.occlusionContainment)) continue;
34702
+ if (this.config.occlusionEnabled && occluders.some((ob) => this.occludesSpawn(det, ob))) {
34703
+ this.reportRefusal("occlusion", det, { threshold: this.config.occlusionContainment });
34704
+ continue;
34705
+ }
34587
34706
  if (this.config.dedupEnabled && this.isDuplicateSpawn(det, surviving, timestamp)) {
34588
34707
  used.add(di);
34589
34708
  continue;
@@ -35071,6 +35190,9 @@ var FrameProcessor = class {
35071
35190
  * Only the rescue / resurrection / growth branches report, and they report
35072
35191
  * sampled: stage 1 runs per track per frame at frame-rate.
35073
35192
  */
35193
+ setSpawnRefusalObserver(observer) {
35194
+ this.tracker.setSpawnRefusalObserver(observer);
35195
+ }
35074
35196
  setBindObserver(observer) {
35075
35197
  this.tracker.setBindObserver(observer);
35076
35198
  }
@@ -35093,6 +35215,7 @@ var FrameProcessor = class {
35093
35215
  const faceAlignedCropByBbox = /* @__PURE__ */ new Map();
35094
35216
  const plateByBbox = /* @__PURE__ */ new Map();
35095
35217
  const maskByBbox = /* @__PURE__ */ new Map();
35218
+ const nativeRecoveryByBbox = /* @__PURE__ */ new Map();
35096
35219
  const flatDetections = frame.detections.filter((d) => d.kind === "first-level").map((det) => {
35097
35220
  const bbox = {
35098
35221
  x: det.bbox.x,
@@ -35104,8 +35227,10 @@ var FrameProcessor = class {
35104
35227
  class: det.macroClass,
35105
35228
  originalClass: det.debug?.originalClass ?? det.macroClass,
35106
35229
  score: det.score,
35107
- bbox
35230
+ bbox,
35231
+ ...det.nativeRecovery !== void 0 ? { nativeRecovery: det.nativeRecovery } : {}
35108
35232
  };
35233
+ if (det.nativeRecovery !== void 0) nativeRecoveryByBbox.set(bbox, det.nativeRecovery);
35109
35234
  if (det.labels.length > 0) labelsByBbox.set(bbox, det.labels);
35110
35235
  if (det.debug?.originalClassTier !== void 0) originalClassTierByBbox.set(bbox, det.debug.originalClassTier);
35111
35236
  if (det.embedding !== void 0) embeddingByBbox.set(bbox, {
@@ -35330,6 +35455,7 @@ var FrameProcessor = class {
35330
35455
  const faceAlignedCrop = faceAlignedCropByBbox.get(td.bbox);
35331
35456
  const plate = plateByBbox.get(td.bbox);
35332
35457
  const sourceDetectionId = sourceIdByBbox.get(td.bbox);
35458
+ const nativeRecovery = td.matchedThisFrame !== false ? nativeRecoveryByBbox.get(td.bbox) : void 0;
35333
35459
  return {
35334
35460
  trackId: td.trackId,
35335
35461
  ...sourceDetectionId !== void 0 ? { sourceDetectionId } : {},
@@ -35352,7 +35478,8 @@ var FrameProcessor = class {
35352
35478
  plateText: plate.text,
35353
35479
  plateScore: plate.score,
35354
35480
  plateBbox: plate.bbox
35355
- } : {}
35481
+ } : {},
35482
+ ...nativeRecovery !== void 0 ? { nativeRecovery } : {}
35356
35483
  };
35357
35484
  });
35358
35485
  this.lastLabelWriteRefusals = refusals;
@@ -45164,7 +45291,7 @@ async function runConfirmation(candidate, config, deps, exhausted) {
45164
45291
  for (const d of detections) if (isConfirmationCompatible(candidate.className, d.macroClass)) {
45165
45292
  if (!best || d.score > best.score) best = d;
45166
45293
  } else if (!bestIncompatible || d.score > bestIncompatible.score) bestIncompatible = d;
45167
- const bar = Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45294
+ const bar = barFor(candidate, config, deps);
45168
45295
  const confirmed = best !== void 0 && best.score >= bar;
45169
45296
  const secondary = bestIncompatible ? promoteSecondary(candidate, bestIncompatible, bar, deps) : null;
45170
45297
  return {
@@ -45203,7 +45330,27 @@ function promoteSecondary(candidate, detection, bar, deps) {
45203
45330
  }
45204
45331
  };
45205
45332
  }
45333
+ /**
45334
+ * The bar this candidate faces. Never below the configured one: the
45335
+ * `minConfidenceFor` hook is monotonic by construction, and clamping here says
45336
+ * so at the point of use rather than trusting the caller to have been careful.
45337
+ */
45338
+ function barFor(candidate, config, deps) {
45339
+ return Math.max(config.minConfidence, deps.minConfidenceFor?.(candidate) ?? 0);
45340
+ }
45206
45341
  async function confirmOne(candidate, config, deps, exhausted) {
45342
+ const native = candidate.nativeRecovery;
45343
+ if (native !== void 0) {
45344
+ const bar = barFor(candidate, config, deps);
45345
+ if (native.score >= bar) return {
45346
+ trackId: candidate.trackId,
45347
+ verdict: "confirmed",
45348
+ reason: "native-pass",
45349
+ className: candidate.className,
45350
+ bestScore: native.score,
45351
+ appliedMinConfidence: bar
45352
+ };
45353
+ }
45207
45354
  const cropPx = Math.max(candidate.bbox.w, candidate.bbox.h);
45208
45355
  if (config.minCropPx > 0 && cropPx < config.minCropPx) return undecided(candidate, "below-min-crop");
45209
45356
  try {
@@ -55093,6 +55240,77 @@ function rowToEntry(id, data) {
55093
55240
  };
55094
55241
  }
55095
55242
  //#endregion
55243
+ //#region src/pipeline-analytics/pipeline/started-births.ts
55244
+ /**
55245
+ * Memory of births whose ritual has already RUN, per device+source.
55246
+ *
55247
+ * A birth is a decision, and a decision is taken once. Until 2026-09-07 it
55248
+ * could be taken twice, on camera 615 it routinely was, and the second vote
55249
+ * won:
55250
+ *
55251
+ * 04:59:38.335Z confirmation gate: birth confirmed {trackId=f585b2ee…
55252
+ * className=animal bestScore=0.7068}
55253
+ * 04:59:38.335Z track started {trackId=f585b2ee…}
55254
+ * 04:59:38.821Z confirmation gate: birth suppressed (false positive)
55255
+ * {trackId=f585b2ee… className=animal} ← no bestScore:
55256
+ * the second crop held no compatible detection AT ALL
55257
+ * 04:59:38.821Z birth suppressed — track record retracted
55258
+ *
55259
+ * TWO conditions have to meet, and both are structural:
55260
+ *
55261
+ * 1. `processFrame` reads `lastActiveTrackIds` at the top and writes it at the
55262
+ * bottom, and AWAITS the confirmation gate in between (a crop round trip to
55263
+ * the frame's node plus a re-detection: 130–490 ms measured on this camera,
55264
+ * against 100 ms frame spacing). Several frames are therefore in flight at
55265
+ * once and each of them sees the same fresh id as unjudged.
55266
+ * 2. The other birth discriminator, `positionsCount > 1`, does not close the
55267
+ * gap either: a COASTED frame deliberately appends no position
55268
+ * (`TrackStore.upsert`, `matched: false`), so a track the full-frame pass
55269
+ * cannot see — which is the entire premise of the small-subject recovery —
55270
+ * keeps exactly one position for as long as it coasts, and
55271
+ * `classifyTrackAppearance` keeps answering `birth`.
55272
+ *
55273
+ * So the id that has already been born is remembered HERE, and both halves of
55274
+ * the birth path consult it: it is not offered to the gate again, and a verdict
55275
+ * that arrives for it anyway cannot retract it. Same lifetime discipline as
55276
+ * {@link SuppressedBirthRegistry} — held while the tracker still carries the
55277
+ * id, forgotten the frame it stops.
55278
+ */
55279
+ var StartedBirthRegistry = class {
55280
+ byDevice = /* @__PURE__ */ new Map();
55281
+ /** Record that this id's birth ritual ran — `start`, firstFrame, the lot. */
55282
+ markStarted(deviceKey, trackId) {
55283
+ let ids = this.byDevice.get(deviceKey);
55284
+ if (!ids) {
55285
+ ids = /* @__PURE__ */ new Set();
55286
+ this.byDevice.set(deviceKey, ids);
55287
+ }
55288
+ ids.add(trackId);
55289
+ }
55290
+ hasStarted(deviceKey, trackId) {
55291
+ return this.byDevice.get(deviceKey)?.has(trackId) === true;
55292
+ }
55293
+ /**
55294
+ * Forget every started id the tracker is no longer carrying. Called once per
55295
+ * frame with the ids present THIS frame — an id the tracker has dropped can
55296
+ * never be re-offered as a birth, so holding it serves nothing.
55297
+ */
55298
+ retain(deviceKey, currentTrackIds) {
55299
+ const ids = this.byDevice.get(deviceKey);
55300
+ if (!ids) return;
55301
+ for (const id of ids) if (!currentTrackIds.has(id)) ids.delete(id);
55302
+ if (ids.size === 0) this.byDevice.delete(deviceKey);
55303
+ }
55304
+ /** Started ids currently held for a device — diagnostics and tests. */
55305
+ size(deviceKey) {
55306
+ return this.byDevice.get(deviceKey)?.size ?? 0;
55307
+ }
55308
+ /** Drop a device's memory wholesale (device removed / pipeline reset). */
55309
+ clearDevice(deviceKey) {
55310
+ this.byDevice.delete(deviceKey);
55311
+ }
55312
+ };
55313
+ //#endregion
55096
55314
  //#region src/pipeline-analytics/pipeline/suppressed-births.ts
55097
55315
  /**
55098
55316
  * Memory of births the confirmation gate rejected, per device.
@@ -67453,6 +67671,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
67453
67671
  /** See `pipeline/suppressed-births.ts` — rejected births must not be
67454
67672
  * re-upserted, and must be forgotten when the tracker drops the id. */
67455
67673
  suppressedBirths = new SuppressedBirthRegistry();
67674
+ /** Ids whose birth ritual has already run — a birth is decided ONCE (D377). */
67675
+ startedBirths = new StartedBirthRegistry();
67456
67676
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
67457
67677
  deferredBirths = new DeferredBirthRegistry();
67458
67678
  /**
@@ -70671,10 +70891,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70671
70891
  if (prevIds.has(id)) continue;
70672
70892
  const t = result.tracked.find((x) => x.trackId === id);
70673
70893
  if (!t) continue;
70674
- if (classifyTrackAppearance({
70894
+ const appearance = classifyTrackAppearance({
70675
70895
  inPrevActive: false,
70676
70896
  positionsCount: positionsCountById.get(id) ?? 1
70677
- }) === "resurrection") {
70897
+ });
70898
+ if (this.startedBirths.hasStarted(key, id)) continue;
70899
+ if (appearance === "resurrection") {
70678
70900
  log.info("track resumed", {
70679
70901
  tags: { deviceId },
70680
70902
  meta: {
@@ -70710,7 +70932,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70710
70932
  const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => ({
70711
70933
  trackId: id,
70712
70934
  className: t.className,
70713
- bbox: t.bbox
70935
+ bbox: t.bbox,
70936
+ ...t.nativeRecovery !== void 0 ? { nativeRecovery: t.nativeRecovery } : {}
70714
70937
  })), {
70715
70938
  deviceId,
70716
70939
  source,
@@ -70735,6 +70958,16 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70735
70958
  const deferredForMs = this.deferredBirths.elapsedMs(key, id, gateNowMs);
70736
70959
  const deferredAttempts = this.deferredBirths.attemptsFor(key, id);
70737
70960
  this.deferredBirths.resolve(key, id);
70961
+ if (this.startedBirths.hasStarted(key, id)) {
70962
+ this.deferredBirths.resolve(key, id);
70963
+ log.info("late birth verdict ignored — the track was already born", { meta: {
70964
+ trackId: id,
70965
+ className: t.className,
70966
+ source,
70967
+ verdict: outcome.confirmed.has(id) ? "confirmed" : "suppressed"
70968
+ } });
70969
+ continue;
70970
+ }
70738
70971
  if (!outcome.confirmed.has(id)) {
70739
70972
  this.suppressedBirths.reject(key, id);
70740
70973
  this.trackStore?.dropActive(id);
@@ -70772,6 +71005,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70772
71005
  })));
70773
71006
  this.failureReport.notePersonBirth(deviceId, overlap === null ? void 0 : REASON_PERSON_OVER_VEHICLE);
70774
71007
  }
71008
+ this.startedBirths.markStarted(key, id);
70775
71009
  log.info("track started", { meta: {
70776
71010
  trackId: id,
70777
71011
  className: t.className,
@@ -70839,6 +71073,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70839
71073
  });
70840
71074
  }
70841
71075
  this.suppressedBirths.retain(key, currentTrackIds);
71076
+ this.startedBirths.retain(key, currentTrackIds);
70842
71077
  this.deferredBirths.retain(key, currentTrackIds);
70843
71078
  this.lastActiveTrackIds.set(key, currentTrackIds);
70844
71079
  if (currentTrackIds.size > 0) this.lastTrackActivityMs.set(deviceId, Date.now());
@@ -71418,6 +71653,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71418
71653
  return allConfirmed();
71419
71654
  }
71420
71655
  const nodeId = frameHandle.nodeId;
71656
+ const candidateNativeRecovery = /* @__PURE__ */ new Map();
71657
+ for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
71421
71658
  return confirmBirths(candidates, config, {
71422
71659
  fetchCrop: (candidate) => this.captureScheduler.request({
71423
71660
  deviceId,
@@ -71467,6 +71704,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71467
71704
  bestIncompatibleScore: decision.bestIncompatibleScore
71468
71705
  } : {},
71469
71706
  minConfidence: config.minConfidence,
71707
+ ...decision.reason === "native-pass" ? {
71708
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
71709
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
71710
+ } : {},
71470
71711
  ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
71471
71712
  appliedMinConfidence: decision.appliedMinConfidence,
71472
71713
  barRaisedByPhantomCell: true
@@ -73728,6 +73969,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73728
73969
  }
73729
73970
  });
73730
73971
  });
73972
+ p.setSpawnRefusalObserver((record) => {
73973
+ this.ctx.logger.info("tracker: detection dropped — no track was born from it", {
73974
+ tags: { deviceId },
73975
+ meta: {
73976
+ source,
73977
+ ...record
73978
+ }
73979
+ });
73980
+ });
73731
73981
  p.setTrackLabelState((trackId) => {
73732
73982
  const active = this.trackStore?.getActiveByTrack(trackId);
73733
73983
  return active !== null && active !== void 0 ? {
@@ -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-Gc-d7G7c.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-BmbhUxPs.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.192",
3
+ "version": "1.2.193",
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",