@camstack/addon-post-analysis 1.2.205 → 1.2.207

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.
@@ -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-CJ0RJC87.js");
5
+ const require_dist = require("../dist-sREZj0iq.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");
@@ -27668,6 +27668,89 @@ function deliveredWideCentralSquareLayout(bbox, frame) {
27668
27668
  if (centralSquareContainmentFraction(bbox, frame, layout) >= .5) return layout;
27669
27669
  return wideCentralSquareLayout(bbox, frame, { containSubject: true });
27670
27670
  }
27671
+ /**
27672
+ * Fraction of the DELIVERED 16:9 canvas that is NOT real pixels — the lateral
27673
+ * fill the composer synthesizes because the window ran off a frame edge. 0 =
27674
+ * the whole canvas is scene; 0.21 = a fifth of the delivered image is fill.
27675
+ *
27676
+ * Measured on {@link deliveredWideCentralSquareLayout} — the layout the
27677
+ * persisted `thumbnail` is actually cut with — because the operator complains
27678
+ * about the image they were SHOWN, not about a hypothetical centred window.
27679
+ * The two differ exactly where it matters: the default layout slides fully
27680
+ * in-frame and carries no fill at all, while the delivery fallback re-imposes
27681
+ * the subject's slack bound (so the square still shows the subject) and pays
27682
+ * for it in padding.
27683
+ *
27684
+ * Upper bound: a window that can only show its central square delivers
27685
+ * `1 − 9/16 = 0.4375` fill; nothing can exceed that while the square itself is
27686
+ * in frame.
27687
+ *
27688
+ * Pure geometry (no I/O). Feeds the best-frame ranking as a BOUNDED
27689
+ * within-tier factor — see `edge-clear.ts#CANVAS_FILL_MAX_PENALTY` for why it
27690
+ * is deliberately not a tier.
27691
+ */
27692
+ function canvasFillFraction(bbox, frame, layout = deliveredWideCentralSquareLayout(bbox, frame)) {
27693
+ if (layout.canvasW <= 0) return 0;
27694
+ return Math.max(0, Math.min(1, (layout.canvasW - layout.slabW) / layout.canvasW));
27695
+ }
27696
+ //#endregion
27697
+ //#region src/pipeline-analytics/pipeline/best-shot-promotion-log.ts
27698
+ function r3(value) {
27699
+ return Math.round(value * 1e3) / 1e3;
27700
+ }
27701
+ /**
27702
+ * Build the attempt line's meta. Pure.
27703
+ *
27704
+ * `decidedBy` is `'first-sighting'` when there was no held peak: the track's
27705
+ * first observation is accepted unconditionally and nothing was compared, and
27706
+ * saying `'effective-score'` there would invent a comparison that never ran.
27707
+ */
27708
+ function bestShotPromotionMeta(attempt) {
27709
+ const { candidate, held, comparison } = attempt;
27710
+ return {
27711
+ trackId: attempt.trackId,
27712
+ className: attempt.className,
27713
+ timestamp: attempt.timestamp,
27714
+ accepted: attempt.accepted,
27715
+ decidedBy: comparison?.decidedBy ?? "first-sighting",
27716
+ ...comparison === void 0 ? {} : {
27717
+ heldScore: r3(comparison.currentScore),
27718
+ candidateScore: r3(comparison.candidateScore)
27719
+ },
27720
+ candConfidence: r3(candidate.confidence),
27721
+ candEdgeClear: candidate.edgeClear,
27722
+ ...candidate.containment === void 0 ? {} : { candContainment: r3(candidate.containment) },
27723
+ ...candidate.sizeScore === void 0 ? {} : { candSizeScore: r3(candidate.sizeScore) },
27724
+ ...candidate.squareFraction === void 0 ? {} : { candSquareFraction: r3(candidate.squareFraction) },
27725
+ ...candidate.canvasFill === void 0 ? {} : { candCanvasFill: r3(candidate.canvasFill) },
27726
+ candSemantic: candidate.semantic === true,
27727
+ ...held === void 0 ? {} : {
27728
+ heldConfidence: r3(held.confidence),
27729
+ heldEdgeClear: held.edgeClear,
27730
+ ...held.canvasFill === void 0 ? {} : { heldCanvasFill: r3(held.canvasFill) }
27731
+ },
27732
+ thumbnailRequested: attempt.thumbnailRequested,
27733
+ keyFrameRequested: attempt.keyFrameRequested,
27734
+ ...attempt.suppressedBy === void 0 ? {} : { suppressedBy: attempt.suppressedBy }
27735
+ };
27736
+ }
27737
+ /**
27738
+ * Is this attempt worth a line?
27739
+ *
27740
+ * Every ACCEPTED attempt is — it is the moment the operator's cover image
27741
+ * changes, or fails to. A REFUSED attempt is logged only while the track still
27742
+ * has NO picture at all, and then at most one line per
27743
+ * {@link PROMOTION_REFUSAL_LOG_GAP_MS}: "the best shot never moved off the birth
27744
+ * frame" is exactly the shape `5f5f0f6e` presented and could not be diagnosed,
27745
+ * while a track whose thumbnail already landed is refusing candidates normally
27746
+ * and has nothing to explain.
27747
+ */
27748
+ function shouldLogPromotionAttempt(attempt, gate) {
27749
+ if (attempt.accepted) return true;
27750
+ if (gate.thumbnailLanded) return false;
27751
+ const last = gate.lastRefusalLogAtMs;
27752
+ return last === void 0 || attempt.timestamp - last >= 2e3;
27753
+ }
27671
27754
  var defaultScheduler = (tick, intervalMs) => {
27672
27755
  const handle = setInterval(tick, intervalMs);
27673
27756
  handle.unref?.();
@@ -30336,11 +30419,16 @@ function analyseDebugTrack(inventory) {
30336
30419
  const out = [];
30337
30420
  if (media.length === 0) out.push(finding("MEDIA_EVICTED_OR_NEVER", "error", "No media at all — either nothing was captured, or it was evicted (a debug mark does not pin retention)."));
30338
30421
  else if (closed) {
30339
- if (!hasKind(media, "keyFrame")) out.push(finding("MISSING_KEYFRAME", "error", "No keyFrame — the best-shot frame is absent."));
30422
+ const keyFrameMissing = !hasKind(media, "keyFrame");
30423
+ const lastFrameCopySourceMissing = keyFrameMissing || !hasKind(media, "keyFrameSmall");
30424
+ const noSnapshot = !hasKind(media, "snapshot");
30425
+ if (keyFrameMissing) out.push(finding("MISSING_KEYFRAME", "error", "No keyFrame — the best-shot frame is absent."));
30340
30426
  if (!hasKind(media, "firstFrame")) out.push(finding("MISSING_FIRSTFRAME", "warn", "No firstFrame for a closed track."));
30341
- if (!hasKind(media, "lastFrame")) out.push(finding("MISSING_LASTFRAME", "warn", "No lastFrame for a closed track."));
30342
- if (!hasKind(media, "thumbnail")) out.push(finding("MISSING_THUMBNAIL", "warn", "No thumbnail — every list surface renders blank."));
30343
- if (!hasKind(media, "crop") && !hasKind(media, "faceCrop") && !hasKind(media, "plateCrop")) out.push(finding("MISSING_BEST_CROP", "warn", "No crop of any kind — nothing was cut for a subject."));
30427
+ if (!hasKind(media, "lastFrame")) out.push(finding("MISSING_LASTFRAME", "warn", noSnapshot && lastFrameCopySourceMissing ? `No lastFrame for a closed track — a CONSEQUENCE of ${keyFrameMissing ? "the missing keyFrame" : "the missing keyFrameSmall"}: with no snapshot to promote, the close can only copy keyFrameSmall, and there is none.` : "No lastFrame for a closed track."));
30428
+ if (!hasKind(media, "thumbnail")) out.push(finding("MISSING_THUMBNAIL", "warn", keyFrameMissing ? "No thumbnail — every list surface renders blank; with no keyFrame the close-time derive had nothing to cut from." : "No thumbnail — every list surface renders blank."));
30429
+ const cropExpected = facts.hasFace === true || facts.hasEmbeddedFace === true;
30430
+ const noCrop = !hasKind(media, "crop") && !hasKind(media, "faceCrop") && !hasKind(media, "plateCrop");
30431
+ if (cropExpected && noCrop) out.push(finding("MISSING_BEST_CROP", "warn", facts.hasEmbeddedFace === true ? "A gallery face row was recorded for this track but no crop of any kind survives — the row promises an image the media index does not have." : "A face detail ran on this track but no crop of any kind was cut for it."));
30344
30432
  }
30345
30433
  if (durationMs < 2e3) out.push(finding("SHORT_TRACK", "warn", `Track lasted ${String(durationMs)} ms, under the ${String(SHORT_TRACK_MS)} ms floor — likely a fragment.`));
30346
30434
  if (eventCount === 0) out.push(finding("ZERO_EVENTS", "error", "No object events — the track exists but recorded nothing."));
@@ -30424,7 +30512,9 @@ function toFacts(track) {
30424
30512
  snapshotCount: track.snapshots.length,
30425
30513
  ...track.importance === void 0 ? {} : { importance: track.importance },
30426
30514
  ...track.bestEventId === void 0 ? {} : { bestEventId: track.bestEventId },
30427
- ...track.previewMissReason === void 0 ? {} : { previewMissReason: track.previewMissReason }
30515
+ ...track.previewMissReason === void 0 ? {} : { previewMissReason: track.previewMissReason },
30516
+ ...track.hasFace === void 0 ? {} : { hasFace: track.hasFace },
30517
+ ...track.hasEmbeddedFace === void 0 ? {} : { hasEmbeddedFace: track.hasEmbeddedFace }
30428
30518
  };
30429
30519
  }
30430
30520
  /**
@@ -33512,6 +33602,33 @@ var CONTAINMENT_MAX_SIDE_PENALTY = .5;
33512
33602
  * NO best frame at all). */
33513
33603
  var CONTAINMENT_MIN = .1;
33514
33604
  /**
33605
+ * Aspect at or above which a clipped subject reads as INTACT — no penalty. 2.5
33606
+ * = the box is two-and-a-half times longer along the clipped axis than across
33607
+ * it, which is what a whole upright person standing with their feet on the
33608
+ * bottom border looks like.
33609
+ */
33610
+ var CLIP_ASPECT_NO_PENALTY = 2.5;
33611
+ /**
33612
+ * How much of a clipped subject is missing, from the SUBJECT'S OWN geometry.
33613
+ *
33614
+ * `alongClip` is the bbox's extent along the axis the border cut (height for a
33615
+ * top/bottom clip, width for a left/right clip); `acrossClip` is its extent on
33616
+ * the other axis. Returns the share of {@link CONTAINMENT_MAX_SIDE_PENALTY} to
33617
+ * apply, ramped linearly between {@link CLIP_ASPECT_FULL_PENALTY} and
33618
+ * {@link CLIP_ASPECT_NO_PENALTY}.
33619
+ *
33620
+ * Truncation COMPRESSES a box along the axis it was cut on, so the surviving
33621
+ * aspect is the signal: it is intrinsic to the subject, scale-invariant and
33622
+ * frame-invariant. Degenerate (`acrossClip <= 0`) reads as fully truncated.
33623
+ */
33624
+ function clipPenaltyScale(alongClip, acrossClip) {
33625
+ if (acrossClip <= 0) return 1;
33626
+ const aspect = alongClip / acrossClip;
33627
+ if (aspect <= 1) return 1;
33628
+ if (aspect >= 2.5) return 0;
33629
+ return 1 - (aspect - 1) / (CLIP_ASPECT_NO_PENALTY - 1);
33630
+ }
33631
+ /**
33515
33632
  * Edge-CONTAINMENT factor for a (clamped) bbox: an estimate in
33516
33633
  * `[CONTAINMENT_MIN, 1]` of the fraction of the subject that is actually in
33517
33634
  * frame. 1 = fully contained (no border clipping); lower = the subject is
@@ -33519,15 +33636,26 @@ var CONTAINMENT_MIN = .1;
33519
33636
  * walking out of view).
33520
33637
  *
33521
33638
  * Because detection bboxes are already CLAMPED to the frame, the true off-screen
33522
- * extent is unknown, so this is a purely-geometric proxy: for each border the
33523
- * bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band), the penalty is
33524
- * scaled by how much of that border the bbox spans on the perpendicular axis —
33525
- * i.e. the subject's aspect against that edge. A wide bbox flush to the
33526
- * top/bottom (a wide subject cut off top/bottom) or a tall bbox flush to the
33527
- * left/right (a tall subject cut off at the side) is heavily penalised, whereas
33528
- * a narrow subject whose feet merely graze the bottom border keeps most of its
33529
- * score. Degenerate/unknown dims (≤ 0) return 1 (neutral — matches
33530
- * {@link isEdgeClear}).
33639
+ * extent is unknown, so this is a purely-geometric proxy. For each border the
33640
+ * bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band) the penalty is
33641
+ * scaled by {@link clipPenaltyScale} the surviving remnant's own aspect
33642
+ * ALONG the cut axis. A wide bbox flush to the top/bottom (a wide subject cut
33643
+ * off top/bottom) or a tall bbox flush to the left/right (a tall subject cut
33644
+ * off at the side) is heavily penalised, whereas a narrow subject whose feet
33645
+ * merely graze the bottom border keeps its score. Degenerate/unknown dims
33646
+ * (≤ 0) return 1 (neutral — matches {@link isEdgeClear}).
33647
+ *
33648
+ * **Why the basis changed (2026-09-08, D400, track `841f9b8e`).** The penalty
33649
+ * used to be scaled by the bbox's SPAN across the touched border as a fraction
33650
+ * of the FRAME. That calibration was written for a wide subject cut off top or
33651
+ * bottom, and it is meaningless for an upright person: a 4K frame is 3840 px
33652
+ * wide, a person clipped at the head is ~400 px wide, so `spanX = 0.104` and a
33653
+ * decapitated person lost **5.2 %** of its score — the operator's "questi tagli
33654
+ * hanno uno score troppo alto, non si vedono il dorso e testa", with the number
33655
+ * attached. The frame's width is not a fact about the subject. The remnant's
33656
+ * own aspect is: the same person now loses 47.9 %, and the wide-subject case the
33657
+ * old shape existed for is penalised MORE than before (a 900×400 vehicle flush
33658
+ * to the bottom went from 0.88 to 0.50), not less.
33531
33659
  */
33532
33660
  function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE_TOLERANCE) {
33533
33661
  if (frameWidth <= 0 || frameHeight <= 0) return 1;
@@ -33537,16 +33665,16 @@ function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE
33537
33665
  const top = bbox.y;
33538
33666
  const right = bbox.x + bbox.w;
33539
33667
  const bottom = bbox.y + bbox.h;
33540
- const spanX = Math.max(0, Math.min(1, bbox.w / frameWidth));
33541
- const spanY = Math.max(0, Math.min(1, bbox.h / frameHeight));
33668
+ const verticalBorderScale = clipPenaltyScale(bbox.w, bbox.h);
33669
+ const horizontalBorderScale = clipPenaltyScale(bbox.h, bbox.w);
33542
33670
  let containment = 1;
33543
- const penalize = (span) => {
33544
- containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * span;
33671
+ const penalize = (scale) => {
33672
+ containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * scale;
33545
33673
  };
33546
- if (left <= tolX) penalize(spanY);
33547
- if (right >= frameWidth - tolX) penalize(spanY);
33548
- if (top <= tolY) penalize(spanX);
33549
- if (bottom >= frameHeight - tolY) penalize(spanX);
33674
+ if (left <= tolX) penalize(verticalBorderScale);
33675
+ if (right >= frameWidth - tolX) penalize(verticalBorderScale);
33676
+ if (top <= tolY) penalize(horizontalBorderScale);
33677
+ if (bottom >= frameHeight - tolY) penalize(horizontalBorderScale);
33550
33678
  return Math.max(CONTAINMENT_MIN, Math.min(1, containment));
33551
33679
  }
33552
33680
  /** Fraction of the frame area at which a subject's size score saturates to 1.
@@ -33593,13 +33721,38 @@ var SQUARE_EVICTION_SCORE_FLOOR = .05;
33593
33721
  * raw confidence — the ABSOLUTE hysteresis margin stays meaningful — while a
33594
33722
  * bigger view still earns up to a 2× relative preference. */
33595
33723
  var SIZE_FACTOR_FLOOR = .5;
33724
+ /**
33725
+ * Maximum fraction of a candidate's effective score the DELIVERED-CANVAS fill
33726
+ * can subtract, at 100 % fill (D400).
33727
+ *
33728
+ * **Why this is a factor and not a tier.** Fill is anti-correlated with subject
33729
+ * size by construction: the 16:9 window is `side × 16/9` of the subject's
33730
+ * central square, so a big, close subject is far likelier to overflow the frame
33731
+ * than a small distant one — a small subject near the centre has zero fill
33732
+ * ALWAYS. A hard "no-fill beats fill" tier would therefore systematically
33733
+ * promote distant, tiny subjects over close ones, which is the exact defect
33734
+ * `bboxSizeScore` was added to fix (BEST_SHOT_STALE, 2026-07-22 / 2026-09-03).
33735
+ * So it enters as a bounded multiplicative factor in the same family as
33736
+ * `containment` and `sizeFactor`, strictly BELOW every tier, and as a near-
33737
+ * equal tie-break ranked ABOVE centeredness (a measurement of the delivered
33738
+ * image beats a proxy for it).
33739
+ *
33740
+ * **Bounded so it can only ever tie-break.** The real fill ceiling is 0.4375
33741
+ * (a window that can show no more than its own central square), so the largest
33742
+ * move this term can make is `0.15 × 0.4375 = 6.6 %` of the effective score. It
33743
+ * cannot overturn a lead of 7.1 % or more — and the metric that would say it
33744
+ * has stopped tie-breaking and started dominating is the share of accepted
33745
+ * best-shot promotions logged with `decidedBy: 'canvas-fill'`.
33746
+ */
33747
+ var CANVAS_FILL_MAX_PENALTY = .15;
33596
33748
  /** The effective within-tier score: detector confidence scaled by the
33597
- * edge-containment and (floored) subject-size factors — each neutral at 1
33598
- * when absent. */
33749
+ * edge-containment, (floored) subject-size and delivered-canvas-fill factors —
33750
+ * each neutral at 1 when absent. */
33599
33751
  function effectiveScore(c) {
33600
33752
  const sizeFactor = c.sizeScore === void 0 ? 1 : SIZE_FACTOR_FLOOR + (1 - SIZE_FACTOR_FLOOR) * c.sizeScore;
33601
33753
  const squareFactor = isSquareContained(c) ? 1 : Math.max(c.squareFraction ?? 0, SQUARE_EVICTION_SCORE_FLOOR);
33602
- return c.confidence * (c.containment ?? 1) * sizeFactor * squareFactor;
33754
+ const fillFactor = 1 - CANVAS_FILL_MAX_PENALTY * Math.max(0, Math.min(1, c.canvasFill ?? 0));
33755
+ return c.confidence * (c.containment ?? 1) * sizeFactor * squareFactor * fillFactor;
33603
33756
  }
33604
33757
  /**
33605
33758
  * True when `candidate` beats `current` DECISIVELY — same-or-better edge tier
@@ -33617,48 +33770,51 @@ function isDecisiveImprovement(current, candidate) {
33617
33770
  return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
33618
33771
  }
33619
33772
  /**
33620
- * Edge-aware "is `candidate` a new best over `current`?" comparator.
33621
- *
33622
- * Tier order: edge-clear ALWAYS outranks edge-touching (a whole subject beats a
33623
- * clipped one regardless of confidence). WITHIN the same tier, a strictly-higher
33624
- * confidence past the `hysteresis` margin wins. The tier upgrade
33625
- * (touching clear) bypasses hysteresis the first clear frame is always taken.
33626
- *
33627
- * EDGE-CONTAINMENT (#edge-clip): within the same tier the comparison is on the
33628
- * EFFECTIVE score `confidence × containment` (see {@link bboxContainment}), not
33629
- * raw confidence so a fully-contained detection outranks a larger/higher-
33630
- * confidence but border-CLIPPED one unless the confidence margin is decisive.
33631
- * The binary edge-clear tier still gates first (a whole subject beats a clipped
33632
- * one regardless), and within the CLEAR tier both frames have containment 1, so
33633
- * this is a no-op there; it discriminates WITHIN the touching tier (feet merely
33634
- * grazing the bottom vs half the body out the side). `containment` defaults to
33635
- * 1 when omitted (legacy face / object-embedding callers) identical behaviour.
33636
- *
33637
- * CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
33638
- * wins on the effective score (the two are within the `hysteresis` band) but the
33639
- * candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
33640
- * candidate wins. This only engages when both sides carry a `centerScore` (the
33641
- * best-frame path), so low-importance short tracks stop keeping an edge-of-frame
33642
- * subject over an equally-confident, better-framed one. The face /
33643
- * object-embedding callers omit `centerScore` → identical legacy behaviour.
33644
- *
33645
- * Time gating (`minGapMs`) is applied by the caller (`BestDetectionTracker`),
33646
- * not here, so this stays a pure value comparison.
33647
- */
33648
- function isEdgeAwareNewBest(current, candidate, hysteresis) {
33649
- if (isSquareContained(candidate) && !isSquareContained(current)) return true;
33650
- if (!isSquareContained(candidate) && isSquareContained(current)) return false;
33651
- if (isSemantic(candidate) && !isSemantic(current)) return true;
33652
- if (!isSemantic(candidate) && isSemantic(current)) return false;
33653
- if (candidate.edgeClear && !current.edgeClear) return true;
33654
- if (!candidate.edgeClear && current.edgeClear) return false;
33655
- const curEffective = effectiveScore(current);
33656
- const candEffective = effectiveScore(candidate);
33657
- if (candEffective > curEffective + hysteresis) return true;
33773
+ * The ONE best-frame comparison, decomposed. {@link isEdgeAwareNewBest} is this
33774
+ * function's `accepted` bit and nothing else — there is no second copy of the
33775
+ * ordering to drift.
33776
+ *
33777
+ * The tier order, highest first, and where the delivered-canvas term sits:
33778
+ * 1. **square-containment** does the delivered SQUARE show the subject at all?
33779
+ * 2. **semantic** — did a recognition succeed on this frame?
33780
+ * 3. **edge-clear** is the whole subject inside the frame?
33781
+ * 4. **effective-score** `confidence × containment × size × square × canvasFill`.
33782
+ * 5. **canvas-fill**near-equal tie-break: less synthesized lateral fill wins.
33783
+ * 6. **centeredness** near-equal tie-break of last resort.
33784
+ *
33785
+ * Canvas fill enters at 4 (bounded, see {@link CANVAS_FILL_MAX_PENALTY}) and 5,
33786
+ * never as a tier: fill is anti-correlated with subject size, so a tier would
33787
+ * hand every camera's best shot to its most distant subject. It ranks ABOVE
33788
+ * centeredness because both answer "is this a well-framed delivery?" and fill
33789
+ * MEASURES the delivered canvas while centeredness only proxies it.
33790
+ */
33791
+ function compareBestFrame(current, candidate, hysteresis) {
33792
+ const currentScore = effectiveScore(current);
33793
+ const candidateScore = effectiveScore(candidate);
33794
+ const verdict = (accepted, decidedBy) => ({
33795
+ accepted,
33796
+ decidedBy,
33797
+ currentScore,
33798
+ candidateScore
33799
+ });
33800
+ return compareBestFrameTiers(current, candidate, hysteresis, currentScore, candidateScore, verdict);
33801
+ }
33802
+ function compareBestFrameTiers(current, candidate, hysteresis, curEffective, candEffective, verdict) {
33803
+ if (isSquareContained(candidate) && !isSquareContained(current)) return verdict(true, "square-containment");
33804
+ if (!isSquareContained(candidate) && isSquareContained(current)) return verdict(false, "square-containment");
33805
+ if (isSemantic(candidate) && !isSemantic(current)) return verdict(true, "semantic");
33806
+ if (!isSemantic(candidate) && isSemantic(current)) return verdict(false, "semantic");
33807
+ if (candidate.edgeClear && !current.edgeClear) return verdict(true, "edge-clear");
33808
+ if (!candidate.edgeClear && current.edgeClear) return verdict(false, "edge-clear");
33809
+ if (candEffective > curEffective + hysteresis) return verdict(true, "effective-score");
33810
+ const nearEqual = Math.abs(candEffective - curEffective) <= hysteresis;
33811
+ if (candidate.canvasFill !== void 0 && current.canvasFill !== void 0) {
33812
+ if (nearEqual && candidate.canvasFill + .05 < current.canvasFill) return verdict(true, "canvas-fill");
33813
+ }
33658
33814
  if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
33659
- if (Math.abs(candEffective - curEffective) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
33815
+ if (nearEqual && candidate.centerScore > current.centerScore + .1) return verdict(true, "centeredness");
33660
33816
  }
33661
- return false;
33817
+ return verdict(false, nearEqual ? "centeredness" : "effective-score");
33662
33818
  }
33663
33819
  //#endregion
33664
33820
  //#region src/pipeline-analytics/pipeline/first-frame-retry-bbox.ts
@@ -34077,6 +34233,74 @@ var LooseBindSampler = class {
34077
34233
  this.counts.clear();
34078
34234
  }
34079
34235
  };
34236
+ /**
34237
+ * Floor on the predictive-drift clamp, in box sides.
34238
+ *
34239
+ * The scaled term collapses at short intervals — at the 56 ms minimum measured
34240
+ * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
34241
+ * detector box jitter into a refused match. A quarter of a side is slack, not
34242
+ * travel.
34243
+ */
34244
+ var DRIFT_MIN_SIDES = .25;
34245
+ /**
34246
+ * Re-acquisition reach, in box DIAGONALS per second.
34247
+ *
34248
+ * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
34249
+ * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
34250
+ * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
34251
+ * physically bounded — 1.5 of a subject's own body-diagonal per second is a
34252
+ * brisk run, and being scale-free it means the same thing for a far-field
34253
+ * person with a 90-px box as for a near one with a 900-px box.
34254
+ *
34255
+ * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
34256
+ * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
34257
+ * a walking subject, and at a 150 ms burst interval it was loose enough to hand
34258
+ * one subject's id to another 452 px away (track `27855a56`, a second car, at
34259
+ * IoU 0.058). One number, two opposite failures.
34260
+ */
34261
+ var REACH_DIAGONALS_PER_SEC = 1.5;
34262
+ /**
34263
+ * Floor on the re-acquisition reach, in box diagonals.
34264
+ *
34265
+ * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
34266
+ * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
34267
+ * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
34268
+ * bought that track a second car. A short gap is evidence that the subject
34269
+ * cannot have gone far, and the gate now says so.
34270
+ */
34271
+ var REACH_MIN_DIAGONALS = .3;
34272
+ /**
34273
+ * Ceiling on the re-acquisition reach, in box diagonals.
34274
+ *
34275
+ * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
34276
+ * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
34277
+ */
34278
+ var REACH_MAX_DIAGONALS = 1.5;
34279
+ function clampRange(value, min, max) {
34280
+ return Math.max(min, Math.min(max, value));
34281
+ }
34282
+ /**
34283
+ * How far a coasting box's velocity extrapolation may carry it on one axis.
34284
+ *
34285
+ * `side` is the box's width for the x drift and its height for the y drift, so
34286
+ * the allowance keeps the same meaning for a tall person and a wide car.
34287
+ */
34288
+ function driftAllowancePx(side, elapsedMs) {
34289
+ const elapsed = Math.max(0, elapsedMs);
34290
+ return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
34291
+ }
34292
+ /**
34293
+ * How far from where a track is EXPECTED a detection may sit and still be
34294
+ * claimed as that track by the loose (rescue / resurrection) reach clause.
34295
+ *
34296
+ * Scale-free: the budget is a multiple of the track's own diagonal, so it does
34297
+ * not have to know the frame size, the camera, or how far away the subject is.
34298
+ */
34299
+ function reacquireReachPx(bbox, elapsedMs) {
34300
+ const diag = Math.hypot(bbox.w, bbox.h);
34301
+ const elapsed = Math.max(0, elapsedMs);
34302
+ return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
34303
+ }
34080
34304
  //#endregion
34081
34305
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
34082
34306
  /**
@@ -34136,6 +34360,21 @@ function growthAdjacentMatch(track, det, ctx) {
34136
34360
  }
34137
34361
  /** Per-reason counter + sampling decision. One instance per tracker, so the
34138
34362
  * counts are per camera and per detection source. */
34363
+ var SpawnBesideCoastingSampler = class {
34364
+ counts = /* @__PURE__ */ new Map();
34365
+ /** The running count when this report should be emitted, else `undefined`. */
34366
+ next(reason) {
34367
+ const count = (this.counts.get(reason) ?? 0) + 1;
34368
+ this.counts.set(reason, count);
34369
+ if (count <= 10) return count;
34370
+ return count % 10 === 0 ? count : void 0;
34371
+ }
34372
+ reset() {
34373
+ this.counts.clear();
34374
+ }
34375
+ };
34376
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
34377
+ * counts are per camera and per detection source. */
34139
34378
  var SpawnRefusalSampler = class {
34140
34379
  counts = /* @__PURE__ */ new Map();
34141
34380
  /** The running count when this refusal should be reported, else `undefined`. */
@@ -34160,6 +34399,7 @@ var DEFAULT_TRACKER_CONFIG = {
34160
34399
  maxTrackLifetimeMs: 3e5,
34161
34400
  rescueIouThreshold: .1,
34162
34401
  rescueCentroidFactor: .75,
34402
+ reacquireTimeScaled: true,
34163
34403
  resurrectionWindowMs: 8e3,
34164
34404
  resurrectionCrowdGuard: true,
34165
34405
  resurrectionCrowdedIouThreshold: .3,
@@ -34234,6 +34474,10 @@ var SortTracker = class SortTracker {
34234
34474
  refusalSampler = new SpawnRefusalSampler();
34235
34475
  /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34236
34476
  spawnRefusalObserver = null;
34477
+ /** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
34478
+ spawnBesideSampler = new SpawnBesideCoastingSampler();
34479
+ /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
34480
+ spawnBesideObserver = null;
34237
34481
  constructor(config = {}) {
34238
34482
  this.config = {
34239
34483
  ...DEFAULT_TRACKER_CONFIG,
@@ -34258,6 +34502,88 @@ var SortTracker = class SortTracker {
34258
34502
  setSpawnRefusalObserver(observer) {
34259
34503
  this.spawnRefusalObserver = observer;
34260
34504
  }
34505
+ /**
34506
+ * Wire the diagnostics sink for the case the tracker does NOT act on: a new id
34507
+ * minted beside a live, compatible, coasting track that could have claimed it.
34508
+ * Same contract as the other two sinks — the tracker has no `deviceId`, the
34509
+ * caller stamps it, and nothing is computed when no observer is wired.
34510
+ */
34511
+ setSpawnBesideCoastingObserver(observer) {
34512
+ this.spawnBesideObserver = observer;
34513
+ }
34514
+ /**
34515
+ * Report a fresh id minted beside the nearest live coasting track of its
34516
+ * association group, when there was one within
34517
+ * {@link SPAWN_BESIDE_REPORT_REACH_FACTOR} × the reach the gate used.
34518
+ *
34519
+ * `coasting` is every track surviving this frame that did NOT match a real
34520
+ * detection on it — the only ones a spawn can be the continuation of.
34521
+ */
34522
+ reportSpawnBesideCoasting(spawnedId, det, coasting, detections, frameArea, timestamp) {
34523
+ const observer = this.spawnBesideObserver;
34524
+ if (!observer) return;
34525
+ const group = this.assocGroup(det.class);
34526
+ const dc = bboxCentroid(det.bbox);
34527
+ let best;
34528
+ let bestDist = Number.POSITIVE_INFINITY;
34529
+ let bestReach = 0;
34530
+ for (const track of coasting) {
34531
+ if (this.assocGroup(track.class) !== group) continue;
34532
+ const tc = bboxCentroid(track.bbox);
34533
+ const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
34534
+ const reach = this.reacquireReach(track, timestamp, true);
34535
+ if (dist > reach * 2) continue;
34536
+ if (dist < bestDist) {
34537
+ best = track;
34538
+ bestDist = dist;
34539
+ bestReach = reach;
34540
+ }
34541
+ }
34542
+ if (!best) return;
34543
+ const declinedBy = this.explainDecline(best, det, detections, frameArea, timestamp, bestDist);
34544
+ const spawnCount = this.spawnBesideSampler.next(declinedBy);
34545
+ if (spawnCount === void 0) return;
34546
+ observer({
34547
+ spawnedTrackId: spawnedId,
34548
+ spawnedClass: det.class,
34549
+ spawnedScore: det.score,
34550
+ spawnedBbox: det.bbox,
34551
+ spawnedNativeRecovery: det.nativeRecovery !== void 0,
34552
+ coastingTrackId: best.id,
34553
+ coastingClass: best.class,
34554
+ iou: iou$3(best.bbox, det.bbox),
34555
+ predictedIou: iou$3(this.predicted(best, timestamp), det.bbox),
34556
+ centroidDistPx: bestDist,
34557
+ reachPx: bestReach,
34558
+ trackDiagPx: Math.hypot(best.bbox.w, best.bbox.h),
34559
+ gapMs: timestamp - best.lastSeen,
34560
+ trackAgeMs: timestamp - best.firstSeen,
34561
+ declinedBy,
34562
+ spawnCount,
34563
+ sampledEvery: 10
34564
+ });
34565
+ }
34566
+ /**
34567
+ * Which gate turned `track` away from `det`, evaluated in the order the
34568
+ * tracker itself evaluates them — the FIRST one that says no is the answer.
34569
+ *
34570
+ * Every clause here re-reads a fact about the frame rather than about the
34571
+ * greedy pass's claim order, so the verdict is reproducible: the one case that
34572
+ * cannot be re-derived is a pair nothing refused, which is exactly what
34573
+ * `already-claimed` means.
34574
+ */
34575
+ explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
34576
+ if (!this.sameGroupAsMeasured(track.class, det)) return "native-measured-class";
34577
+ if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return "assoc-group";
34578
+ if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
34579
+ if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
34580
+ if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
34581
+ if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
34582
+ return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
34583
+ }
34584
+ if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "already-claimed";
34585
+ return "geometry";
34586
+ }
34261
34587
  /** Emit one dropped-detection record, subject to the refusal sampler. */
34262
34588
  reportRefusal(reason, det, extra = {}) {
34263
34589
  const observer = this.spawnRefusalObserver;
@@ -34526,21 +34852,39 @@ var SortTracker = class SortTracker {
34526
34852
  if (a !== void 0 && dropIds.has(a) || b !== void 0 && dropIds.has(b)) this.dupStreaks.delete(key);
34527
34853
  }
34528
34854
  }
34529
- /** Where a track is expected this frame — extrapolated by velocity while
34530
- * coasting (predictiveCoasting), else its last known bbox. A stationary
34531
- * track (speed < stationarySpeedPx) is frozen so bbox jitter can't walk the
34532
- * box off a sitting object; otherwise the drift is capped at one bbox size
34533
- * so a stale velocity can't fling the box across the frame. Public so the
34534
- * clamp is directly unit-testable. */
34535
- predicted(t) {
34855
+ /**
34856
+ * Where a track is expected at `nowMs` — extrapolated by velocity while
34857
+ * coasting (`predictiveCoasting`), else its last known bbox.
34858
+ *
34859
+ * A stationary track (speed below `stationarySpeedPx`, measured in the
34860
+ * px-per-update unit that setting is calibrated in) is frozen so bbox jitter
34861
+ * cannot walk the box off a sitting object. Otherwise the drift is the
34862
+ * velocity carried over the ELAPSED interval, capped by
34863
+ * {@link driftAllowancePx} — which is itself a function of that interval, so
34864
+ * a stale velocity cannot fling the box across the frame and a long, honest
34865
+ * gap is no longer capped at the one box width a single frame would get
34866
+ * (D399). Public so the clamp is directly unit-testable.
34867
+ */
34868
+ predicted(t, nowMs) {
34536
34869
  if (!this.config.predictiveCoasting) return t.bbox;
34537
- if (Math.hypot(t.velocity.dx, t.velocity.dy) < this.config.stationarySpeedPx) return t.bbox;
34538
- const f = t.age + 1;
34539
- const driftX = clamp(t.velocity.dx * f, -t.bbox.w, t.bbox.w);
34540
- const driftY = clamp(t.velocity.dy * f, -t.bbox.h, t.bbox.h);
34870
+ if (Math.hypot(t.velocity.dx, t.velocity.dy) * t.emitIntervalMs < this.config.stationarySpeedPx) return t.bbox;
34871
+ if (!this.config.reacquireTimeScaled) {
34872
+ const f = t.age + 1;
34873
+ const perFrameX = t.velocity.dx * t.emitIntervalMs;
34874
+ const perFrameY = t.velocity.dy * t.emitIntervalMs;
34875
+ return {
34876
+ x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
34877
+ y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
34878
+ w: t.bbox.w,
34879
+ h: t.bbox.h
34880
+ };
34881
+ }
34882
+ const elapsedMs = Math.max(0, nowMs - t.lastSeen);
34883
+ const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
34884
+ const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
34541
34885
  return {
34542
- x: t.bbox.x + driftX,
34543
- y: t.bbox.y + driftY,
34886
+ x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
34887
+ y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
34544
34888
  w: t.bbox.w,
34545
34889
  h: t.bbox.h
34546
34890
  };
@@ -34583,19 +34927,64 @@ var SortTracker = class SortTracker {
34583
34927
  * crouch→animal flip when `classGroupAssoc` is on, but never by an unrelated
34584
34928
  * class.
34585
34929
  */
34586
- looseMatch(track, det, timestamp) {
34930
+ looseMatch(track, det, timestamp, timeScaled = true) {
34587
34931
  if (!this.sameGroupAsMeasured(track.class, det)) return null;
34588
34932
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34589
34933
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34590
34934
  const tc = bboxCentroid(track.bbox);
34591
34935
  const dc = bboxCentroid(det.bbox);
34592
- const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
34593
- const diag = Math.hypot(track.bbox.w, track.bbox.h);
34594
- if (dist <= this.config.rescueCentroidFactor * diag) return "centroid";
34936
+ if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, timeScaled)) return "centroid";
34595
34937
  if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "growth";
34596
34938
  return null;
34597
34939
  }
34598
34940
  /**
34941
+ * The proximity budget the `centroid` clause is measured against.
34942
+ *
34943
+ * On the RESCUE pass (`timeScaled`) it is {@link reacquireReachPx} — a
34944
+ * multiple of the track's diagonal that grows with the elapsed gap and stops
34945
+ * at a ceiling, because a live coasting track's whereabouts genuinely become
34946
+ * less certain the longer it has gone unseen (D399).
34947
+ *
34948
+ * On the RESURRECTION pass it stays the flat `rescueCentroidFactor × diagonal`
34949
+ * it always was. That pass re-attaches a CORPSE, and it exists for one thing:
34950
+ * "a flickering stationary object keeps its identity". Elapsed time is not the
34951
+ * variable there — a stationary subject does not travel further because it
34952
+ * blinked for longer — and the corpse is strictly weaker evidence than a live
34953
+ * track, which is the same reasoning `resurrectionCrowdedIouThreshold` already
34954
+ * encodes.
34955
+ */
34956
+ reacquireReach(track, timestamp, timeScaled) {
34957
+ const diag = Math.hypot(track.bbox.w, track.bbox.h);
34958
+ if (!timeScaled || !this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
34959
+ return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
34960
+ }
34961
+ /**
34962
+ * Whether a REACH bind would be unambiguous: this coasting track is the only
34963
+ * subject of its association group the scene holds, and `det` is the only box
34964
+ * of that group on the frame.
34965
+ *
34966
+ * This is what lets a reach bind survive {@link isPlausibleBirth} (D399). That
34967
+ * veto exists because "a spare track costs media; a stolen track corrupts the
34968
+ * plate, the travelled distance and the last frame of the track it was taken
34969
+ * from" — every word of which is about a scene with something to steal FROM.
34970
+ * With one track and one box there is no second subject the id could be wrong
34971
+ * about, and refusing the bind does not buy safety, it buys a fragment.
34972
+ *
34973
+ * Deliberately computed over the whole frame rather than over what is still
34974
+ * unclaimed, so the answer is a fact about the scene and reads the same at
34975
+ * bind time and at report time — the spawn-beside-coasting line has to be able
34976
+ * to say which gate declined without re-running the greedy competition.
34977
+ */
34978
+ isUnambiguousReach(track, det, detections) {
34979
+ if (this.sameGroupPeerCount(track, this.tracks) > 0) return false;
34980
+ const group = this.assocGroup(track.class);
34981
+ for (const other of detections) {
34982
+ if (other === det) continue;
34983
+ if (this.assocGroup(other.class) === group) return false;
34984
+ }
34985
+ return true;
34986
+ }
34987
+ /**
34599
34988
  * Whether `det` would mint a track of its own if no salvage branch claimed it:
34600
34989
  * it clears BOTH spawn floors (per-class score, per-class bbox-area fraction).
34601
34990
  *
@@ -34663,11 +35052,11 @@ var SortTracker = class SortTracker {
34663
35052
  * gates) so the two-tier ByteTrack association stays a single implementation.
34664
35053
  * Mutates `used` / `matchedTracks` / `matched` in place.
34665
35054
  */
34666
- greedyAssociate(detections, candidateIdxs, iouThreshold, used, matchedTracks, matched) {
35055
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
34667
35056
  const pairs = [];
34668
35057
  for (const track of this.tracks) {
34669
35058
  if (matchedTracks.has(track)) continue;
34670
- const pbox = this.predicted(track);
35059
+ const pbox = this.predicted(track, timestamp);
34671
35060
  for (const di of candidateIdxs) {
34672
35061
  if (used.has(di)) continue;
34673
35062
  const det = detections[di];
@@ -34726,7 +35115,7 @@ var SortTracker = class SortTracker {
34726
35115
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
34727
35116
  else highIdxs.push(di);
34728
35117
  const lowSet = new Set(lowIdxs);
34729
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, used, matchedTracks, matched);
35118
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
34730
35119
  const rescuePairs = [];
34731
35120
  for (const track of this.tracks) {
34732
35121
  if (matchedTracks.has(track)) continue;
@@ -34735,7 +35124,7 @@ var SortTracker = class SortTracker {
34735
35124
  const det = detections[di];
34736
35125
  const gate = this.looseMatch(track, det, timestamp);
34737
35126
  if (gate === null) continue;
34738
- if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
35127
+ if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
34739
35128
  rescuePairs.push({
34740
35129
  track,
34741
35130
  detIdx: di,
@@ -34753,7 +35142,7 @@ var SortTracker = class SortTracker {
34753
35142
  used.add(pair.detIdx);
34754
35143
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
34755
35144
  }
34756
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
35145
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
34757
35146
  for (const [track, det] of matched) {
34758
35147
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34759
35148
  const prevCenter = bboxCentroid({
@@ -34769,6 +35158,7 @@ var SortTracker = class SortTracker {
34769
35158
  h: det.bbox.h
34770
35159
  });
34771
35160
  const frames = track.age + 1;
35161
+ const dtMs = timestamp - track.lastSeen;
34772
35162
  track.bbox = det.bbox;
34773
35163
  track.class = det.class;
34774
35164
  track.originalClass = det.originalClass;
@@ -34776,10 +35166,13 @@ var SortTracker = class SortTracker {
34776
35166
  track.age = 0;
34777
35167
  track.hits++;
34778
35168
  track.lastSeen = timestamp;
34779
- track.velocity = {
34780
- dx: (newCenter.x - prevCenter.x) / frames,
34781
- dy: (newCenter.y - prevCenter.y) / frames
34782
- };
35169
+ if (dtMs > 0) {
35170
+ track.velocity = {
35171
+ dx: (newCenter.x - prevCenter.x) / dtMs,
35172
+ dy: (newCenter.y - prevCenter.y) / dtMs
35173
+ };
35174
+ track.emitIntervalMs = dtMs / frames;
35175
+ }
34783
35176
  track.path.push(det.bbox);
34784
35177
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34785
35178
  this.addClassVote(track, det);
@@ -34795,7 +35188,7 @@ var SortTracker = class SortTracker {
34795
35188
  surviving.push(track);
34796
35189
  continue;
34797
35190
  }
34798
- const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
35191
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track, timestamp), ob.bbox) >= this.config.occlusionContainment);
34799
35192
  track.age++;
34800
35193
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34801
35194
  track.lost = true;
@@ -34817,7 +35210,7 @@ var SortTracker = class SortTracker {
34817
35210
  for (const lost of this.lostTracks) {
34818
35211
  if (!lost.resurrectable) continue;
34819
35212
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
34820
- const gate = this.looseMatch(lost, det, timestamp);
35213
+ const gate = this.looseMatch(lost, det, timestamp, false);
34821
35214
  if (gate === null) continue;
34822
35215
  if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
34823
35216
  const score = iou$3(lost.bbox, det.bbox);
@@ -34897,8 +35290,10 @@ var SortTracker = class SortTracker {
34897
35290
  used.add(di);
34898
35291
  continue;
34899
35292
  }
35293
+ const spawnedId = (0, node_crypto.randomUUID)();
35294
+ this.reportSpawnBesideCoasting(spawnedId, det, surviving.filter((t) => t.lastSeen !== timestamp), detections, frameArea, timestamp);
34900
35295
  surviving.push({
34901
- id: (0, node_crypto.randomUUID)(),
35296
+ id: spawnedId,
34902
35297
  bbox: det.bbox,
34903
35298
  class: det.class,
34904
35299
  originalClass: det.originalClass,
@@ -34912,6 +35307,7 @@ var SortTracker = class SortTracker {
34912
35307
  dx: 0,
34913
35308
  dy: 0
34914
35309
  },
35310
+ emitIntervalMs: 0,
34915
35311
  lost: false,
34916
35312
  lostAt: 0,
34917
35313
  resurrectable: true,
@@ -34932,7 +35328,10 @@ var SortTracker = class SortTracker {
34932
35328
  bbox: t.bbox,
34933
35329
  trackId: t.id,
34934
35330
  trackAge: t.hits,
34935
- velocity: t.velocity,
35331
+ velocity: {
35332
+ dx: t.velocity.dx * t.emitIntervalMs,
35333
+ dy: t.velocity.dy * t.emitIntervalMs
35334
+ },
34936
35335
  path: [...t.path],
34937
35336
  matchedThisFrame: t.lastSeen === timestamp,
34938
35337
  ...scene !== void 0 ? { resurrectionScene: scene } : {}
@@ -34960,6 +35359,7 @@ var SortTracker = class SortTracker {
34960
35359
  this.lostTracks = [];
34961
35360
  this.dupStreaks.clear();
34962
35361
  this.bindSampler.reset();
35362
+ this.spawnBesideSampler.reset();
34963
35363
  }
34964
35364
  };
34965
35365
  //#endregion
@@ -35394,6 +35794,12 @@ var FrameProcessor = class {
35394
35794
  setBindObserver(observer) {
35395
35795
  this.tracker.setBindObserver(observer);
35396
35796
  }
35797
+ /** Wire (or clear) the tracker's fragmentation sink — a new id minted beside a
35798
+ * live coasting track that could have claimed it (D399). Same `deviceId`
35799
+ * contract as the two sinks above. */
35800
+ setSpawnBesideCoastingObserver(observer) {
35801
+ this.tracker.setSpawnBesideCoastingObserver(observer);
35802
+ }
35397
35803
  /** Forget a track in the underlying tracker (used when the addon promotes it
35398
35804
  * to a stationary-object registry entry). */
35399
35805
  dropTrack(trackId) {
@@ -35990,7 +36396,11 @@ function promoteSemanticBestFrame(deps, input) {
35990
36396
  W: frameWidth,
35991
36397
  H: frameHeight
35992
36398
  }),
35993
- semantic: true
36399
+ semantic: true,
36400
+ canvasFill: canvasFillFraction(bbox, {
36401
+ W: frameWidth,
36402
+ H: frameHeight
36403
+ })
35994
36404
  })) {
35995
36405
  deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
35996
36406
  tags: { deviceId },
@@ -36534,9 +36944,15 @@ function decideLastFramePromotion(media) {
36534
36944
  snapshotKey: newestKeyFrameSmall.key,
36535
36945
  snapshotTimestamp: newestKeyFrameSmall.timestamp
36536
36946
  };
36537
- return { promote: false };
36947
+ return {
36948
+ promote: false,
36949
+ declinedBecause: lastFrame === void 0 ? "no-key-frame" : "no-source"
36950
+ };
36538
36951
  }
36539
- if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return { promote: false };
36952
+ if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return {
36953
+ promote: false,
36954
+ declinedBecause: "already-last"
36955
+ };
36540
36956
  return {
36541
36957
  promote: true,
36542
36958
  mode: "move",
@@ -36583,17 +36999,28 @@ function averageBboxDiagonal(boxes) {
36583
36999
  //#endregion
36584
37000
  //#region src/pipeline-analytics/pipeline/track-close.ts
36585
37001
  /**
36586
- * Compose the persisted `previewMissReason` (D276/B2). One line, greppable
36587
- * tokens: the derive's own miss branch (or what tile stands in), then the
36588
- * capture-side failure with its count. Examples:
36589
- * `no-key-frame capture=keyframe:native-missx4`
37002
+ * Compose the persisted `previewMissReason` (D276/B2, extended by D400). One
37003
+ * line, greppable tokens: the derive's own miss branch (or what tile stands
37004
+ * in), then whether the NATIVE KEY FRAME landed, then the capture-side failure
37005
+ * with its count. Examples:
37006
+ * `no-key-frame no-key-frame-at-close capture=keyframe:native-missx4`
36590
37007
  * `derive-returned-null tile=standin`
36591
- * `no-key-frame tile=raster capture=thumbnail:overflowx12`
36592
- */
36593
- function composePreviewMissReason(tile, deriveMiss, captureMiss) {
37008
+ * `native-tile tile=native no-key-frame-at-close`
37009
+ *
37010
+ * **Why the key-frame token (D400).** The reason used to be written ONLY when
37011
+ * the best-shot TILE ranked below native, so a track whose thumbnail landed
37012
+ * natively while its key frame missed carried no reason at all — three of the
37013
+ * four `MISSING_KEYFRAME` tracks in the 2026-09-07 audit were exactly that, and
37014
+ * the operator's "niente ultimo" had no cause anywhere on the row. The key
37015
+ * frame IS the native best-shot FRAME, and its absence is the head of the
37016
+ * cascade (`keyFrame ⇒ keyFrameSmall ⇒ the lastFrame copy source`), so it
37017
+ * belongs on this line whatever the tile ended up being.
37018
+ */
37019
+ function composePreviewMissReason(tile, deriveMiss, captureMiss, keyFrameLanded = true) {
36594
37020
  const parts = [];
36595
37021
  parts.push(deriveMiss ?? (tile === "none" ? "no-thumbnail" : `${tile}-tile`));
36596
37022
  if (tile !== "none") parts.push(`tile=${tile}`);
37023
+ if (!keyFrameLanded && deriveMiss !== "no-key-frame") parts.push("no-key-frame-at-close");
36597
37024
  if (captureMiss !== void 0) parts.push(`capture=${captureMiss.reason}x${captureMiss.count}`);
36598
37025
  return parts.join(" ");
36599
37026
  }
@@ -36689,9 +37116,10 @@ var TrackCloser = class {
36689
37116
  await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
36690
37117
  tile = "raster";
36691
37118
  }
36692
- if (tile !== "native") {
37119
+ const keyFrameLanded = ownedMedia.some((m) => m.kind === "keyFrame");
37120
+ if (tile !== "native" || !keyFrameLanded) {
36693
37121
  const captureMiss = this.deps.previewCaptureMiss?.(t.trackId);
36694
- const reason = composePreviewMissReason(tile, deriveMiss, captureMiss);
37122
+ const reason = composePreviewMissReason(tile, deriveMiss, captureMiss, keyFrameLanded);
36695
37123
  if (this.deps.setPreviewMiss !== void 0) try {
36696
37124
  await this.deps.setPreviewMiss(t.deviceId, t.trackId, reason);
36697
37125
  } catch (err) {
@@ -36704,8 +37132,8 @@ var TrackCloser = class {
36704
37132
  }
36705
37133
  });
36706
37134
  }
36707
- const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
36708
- this.deps.recordPendingPreviewUpgrade?.({
37135
+ const bestSeen = tile === "native" ? void 0 : this.deps.residents.bestSeenBbox(t.trackId);
37136
+ if (tile !== "native") this.deps.recordPendingPreviewUpgrade?.({
36709
37137
  deviceId: t.deviceId,
36710
37138
  trackId: t.trackId,
36711
37139
  ...bestSeen !== void 0 ? { bestSeen: {
@@ -36974,9 +37402,32 @@ var TrackCloser = class {
36974
37402
  */
36975
37403
  async maybePromoteLastFrame(t, ownedMedia) {
36976
37404
  const promotion = decideLastFramePromotion(ownedMedia);
36977
- if (!promotion.promote) return;
37405
+ if (!promotion.promote) {
37406
+ if (promotion.declinedBecause !== "already-last") this.deps.logger.warn("no lastFrame at close", {
37407
+ tags: { deviceId: t.deviceId },
37408
+ meta: {
37409
+ trackId: t.trackId,
37410
+ declinedBecause: promotion.declinedBecause,
37411
+ hasKeyFrame: ownedMedia.some((m) => m.kind === "keyFrame"),
37412
+ hasKeyFrameSmall: ownedMedia.some((m) => m.kind === "keyFrameSmall"),
37413
+ snapshots: ownedMedia.filter((m) => m.kind === "snapshot").length
37414
+ }
37415
+ });
37416
+ return;
37417
+ }
36978
37418
  const snapshot = ownedMedia.find((m) => m.key === promotion.snapshotKey);
36979
- if (!snapshot) return;
37419
+ if (!snapshot) {
37420
+ this.deps.logger.warn("no lastFrame at close", {
37421
+ tags: { deviceId: t.deviceId },
37422
+ meta: {
37423
+ trackId: t.trackId,
37424
+ declinedBecause: "promotion-source-vanished",
37425
+ sourceKey: promotion.snapshotKey,
37426
+ mode: promotion.mode
37427
+ }
37428
+ });
37429
+ return;
37430
+ }
36980
37431
  try {
36981
37432
  await this.deps.mediaStore()?.promoteToLastFrame({
36982
37433
  deviceId: t.deviceId,
@@ -37144,9 +37595,15 @@ var BestDetectionTracker = class {
37144
37595
  * `best`. Absent = the caller does not measure it → contained tier
37145
37596
  * (legacy behaviour). */
37146
37597
  squareFraction = /* @__PURE__ */ new Map();
37598
+ /** Held peak's DELIVERED-CANVAS fill fraction (0..1), PARALLEL to `best`.
37599
+ * Absent = the caller does not measure it → neutral (no fill). */
37600
+ canvasFill = /* @__PURE__ */ new Map();
37147
37601
  /** Held peak's SEMANTIC-tier bit (a recognition succeeded on that frame),
37148
37602
  * PARALLEL to `best`. Absent = non-semantic (legacy behaviour). */
37149
37603
  semantic = /* @__PURE__ */ new Map();
37604
+ /** The last comparison run for a track (D400 promotion log). Never read by
37605
+ * the ranking — a pure observation seam. */
37606
+ lastComparison = /* @__PURE__ */ new Map();
37150
37607
  constructor(options = {}) {
37151
37608
  this.hysteresis = options.hysteresis ?? 0;
37152
37609
  this.minGapMs = options.minGapMs ?? 0;
@@ -37166,12 +37623,13 @@ var BestDetectionTracker = class {
37166
37623
  * the classic policy holds: a confidence past the `hysteresis` margin that also
37167
37624
  * respects `minGapMs` wins. On acceptance the held peak advances.
37168
37625
  */
37169
- observe(trackId, confidence, timestamp, edgeClearIn, centerScoreIn, containmentIn, sizeScoreIn, squareFractionIn, semanticIn) {
37626
+ observe(trackId, confidence, timestamp, edgeClearIn, centerScoreIn, containmentIn, sizeScoreIn, squareFractionIn, semanticIn, canvasFillIn) {
37170
37627
  const edgeClear = this.edgeAware ? edgeClearIn : void 0;
37171
37628
  const centerScore = this.edgeAware ? centerScoreIn : void 0;
37172
37629
  const containment = this.edgeAware ? containmentIn : void 0;
37173
37630
  const sizeScore = this.sizeAware ? sizeScoreIn : void 0;
37174
37631
  const squareFraction = this.edgeAware ? squareFractionIn : void 0;
37632
+ const canvasFill = this.edgeAware ? canvasFillIn : void 0;
37175
37633
  const semanticBit = this.semanticAware ? semanticIn : void 0;
37176
37634
  const cur = this.best.get(trackId);
37177
37635
  if (cur === void 0) {
@@ -37184,6 +37642,7 @@ var BestDetectionTracker = class {
37184
37642
  if (containment !== void 0) this.containment.set(trackId, containment);
37185
37643
  if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
37186
37644
  if (squareFraction !== void 0) this.squareFraction.set(trackId, squareFraction);
37645
+ if (canvasFill !== void 0) this.canvasFill.set(trackId, canvasFill);
37187
37646
  if (semanticBit !== void 0) this.semantic.set(trackId, semanticBit);
37188
37647
  return true;
37189
37648
  }
@@ -37193,6 +37652,7 @@ var BestDetectionTracker = class {
37193
37652
  const curContainment = this.containment.get(trackId);
37194
37653
  const curSize = this.sizeScore.get(trackId);
37195
37654
  const curSquareFraction = this.squareFraction.get(trackId);
37655
+ const curCanvasFill = this.canvasFill.get(trackId);
37196
37656
  const curSemantic = this.semantic.get(trackId) ?? false;
37197
37657
  const candSemantic = semanticBit ?? false;
37198
37658
  const held = {
@@ -37202,6 +37662,7 @@ var BestDetectionTracker = class {
37202
37662
  ...curContainment !== void 0 ? { containment: curContainment } : {},
37203
37663
  ...curSize !== void 0 ? { sizeScore: curSize } : {},
37204
37664
  ...curSquareFraction !== void 0 ? { squareFraction: curSquareFraction } : {},
37665
+ ...curCanvasFill !== void 0 ? { canvasFill: curCanvasFill } : {},
37205
37666
  semantic: curSemantic
37206
37667
  };
37207
37668
  const cand = {
@@ -37211,6 +37672,7 @@ var BestDetectionTracker = class {
37211
37672
  ...containment !== void 0 ? { containment } : {},
37212
37673
  ...sizeScore !== void 0 ? { sizeScore } : {},
37213
37674
  ...squareFraction !== void 0 ? { squareFraction } : {},
37675
+ ...canvasFill !== void 0 ? { canvasFill } : {},
37214
37676
  semantic: candSemantic
37215
37677
  };
37216
37678
  const squareTierUpgrade = isSquareContained(cand) && !isSquareContained(held);
@@ -37219,7 +37681,9 @@ var BestDetectionTracker = class {
37219
37681
  const sameSemanticTier = isSemantic(cand) === isSemantic(held);
37220
37682
  const tierUpgrade = squareTierUpgrade || semanticTierUpgrade || sameSquareTier && sameSemanticTier && candClear && !curClear;
37221
37683
  const gapOk = timestamp - cur.atMs >= this.minGapMs || sizeScore !== void 0 && isDecisiveImprovement(held, cand);
37222
- const isNewBest = tierUpgrade ? true : isEdgeAwareNewBest(held, cand, this.hysteresis) && gapOk;
37684
+ const comparison = compareBestFrame(held, cand, this.hysteresis);
37685
+ this.lastComparison.set(trackId, comparison);
37686
+ const isNewBest = tierUpgrade ? true : comparison.accepted && gapOk;
37223
37687
  if (isNewBest) {
37224
37688
  this.best.set(trackId, {
37225
37689
  confidence,
@@ -37230,11 +37694,46 @@ var BestDetectionTracker = class {
37230
37694
  if (containment !== void 0) this.containment.set(trackId, containment);
37231
37695
  if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
37232
37696
  if (squareFraction !== void 0) this.squareFraction.set(trackId, squareFraction);
37697
+ if (canvasFill !== void 0) this.canvasFill.set(trackId, canvasFill);
37233
37698
  this.semantic.set(trackId, candSemantic);
37234
37699
  }
37235
37700
  return isNewBest;
37236
37701
  }
37237
37702
  /**
37703
+ * The FULL candidate shape of the track's held peak — everything the ranking
37704
+ * saw, not just `{confidence, atMs}`. Read BEFORE `observe` so a promotion
37705
+ * attempt can be logged against the peak it actually competed with (D400).
37706
+ * Undefined when the track holds no peak (the first sighting).
37707
+ */
37708
+ heldCandidate(trackId) {
37709
+ const cur = this.best.get(trackId);
37710
+ if (cur === void 0) return void 0;
37711
+ const centerScore = this.centerScore.get(trackId);
37712
+ const containment = this.containment.get(trackId);
37713
+ const sizeScore = this.sizeScore.get(trackId);
37714
+ const squareFraction = this.squareFraction.get(trackId);
37715
+ const canvasFill = this.canvasFill.get(trackId);
37716
+ return {
37717
+ confidence: cur.confidence,
37718
+ edgeClear: this.edgeClear.get(trackId) ?? true,
37719
+ ...centerScore !== void 0 ? { centerScore } : {},
37720
+ ...containment !== void 0 ? { containment } : {},
37721
+ ...sizeScore !== void 0 ? { sizeScore } : {},
37722
+ ...squareFraction !== void 0 ? { squareFraction } : {},
37723
+ ...canvasFill !== void 0 ? { canvasFill } : {},
37724
+ semantic: this.semantic.get(trackId) ?? false
37725
+ };
37726
+ }
37727
+ /**
37728
+ * The comparison the LAST {@link observe} ran for this track — which term
37729
+ * decided, and both effective scores. Undefined on a first sighting (nothing
37730
+ * was compared) and after {@link delete}. Feeds the promotion log line; it is
37731
+ * never a ranking input.
37732
+ */
37733
+ lastComparisonFor(trackId) {
37734
+ return this.lastComparison.get(trackId);
37735
+ }
37736
+ /**
37238
37737
  * Stamp the HELD peak as semantic — the attribution-time re-score for a
37239
37738
  * recognition that arrived AFTER the frame was ranked (detail results are
37240
37739
  * async). Use when the recognition's source frame IS the held peak: the
@@ -37260,7 +37759,9 @@ var BestDetectionTracker = class {
37260
37759
  this.containment.delete(trackId);
37261
37760
  this.sizeScore.delete(trackId);
37262
37761
  this.squareFraction.delete(trackId);
37762
+ this.canvasFill.delete(trackId);
37263
37763
  this.semantic.delete(trackId);
37764
+ this.lastComparison.delete(trackId);
37264
37765
  }
37265
37766
  clear() {
37266
37767
  this.best.clear();
@@ -37269,7 +37770,9 @@ var BestDetectionTracker = class {
37269
37770
  this.containment.clear();
37270
37771
  this.sizeScore.clear();
37271
37772
  this.squareFraction.clear();
37773
+ this.canvasFill.clear();
37272
37774
  this.semantic.clear();
37775
+ this.lastComparison.clear();
37273
37776
  }
37274
37777
  };
37275
37778
  //#endregion
@@ -37531,9 +38034,29 @@ var TrackResidentState = class {
37531
38034
  }
37532
38035
  /** Record a detection for the track's best-FRAME ranking (edge-aware §5
37533
38036
  * policy). Returns true when it becomes the track's new best. */
37534
- observeBestFrame(deviceId, trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic) {
38037
+ observeBestFrame(deviceId, trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic, canvasFill) {
37535
38038
  this.open(deviceId, trackId);
37536
- return this.bestFrameTracker.observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic);
38039
+ return this.bestFrameTracker.observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic, canvasFill);
38040
+ }
38041
+ /** D400: frame instant of the track's last logged promotion REFUSAL
38042
+ * (undefined = none yet). */
38043
+ bestShotRefusalLoggedAt(trackId) {
38044
+ return this.residents.get(trackId)?.bestShotRefusalLoggedAt;
38045
+ }
38046
+ /** Stamp the track's last promotion-refusal log instant. */
38047
+ noteBestShotRefusalLogged(deviceId, trackId, timestamp) {
38048
+ this.ensure(deviceId, trackId).bestShotRefusalLoggedAt = timestamp;
38049
+ }
38050
+ /** The FULL held best-frame candidate (everything the ranking saw). Read
38051
+ * BEFORE `observeBestFrame` to log a promotion attempt against the peak it
38052
+ * competed with (D400). */
38053
+ bestFrameHeld(trackId) {
38054
+ return this.bestFrameTracker.heldCandidate(trackId);
38055
+ }
38056
+ /** Which term decided the LAST best-frame comparison for the track (D400).
38057
+ * Undefined on a first sighting. */
38058
+ bestFrameLastComparison(trackId) {
38059
+ return this.bestFrameTracker.lastComparisonFor(trackId);
37537
38060
  }
37538
38061
  /** The held best-frame peak for a track (undefined if never observed). */
37539
38062
  bestFramePeak(trackId) {
@@ -39727,6 +40250,24 @@ var TRACK_CLOSE_REASONS = ["stationary-promotion"];
39727
40250
  function parseCloseReason(value) {
39728
40251
  return TRACK_CLOSE_REASONS.find((reason) => reason === value);
39729
40252
  }
40253
+ /**
40254
+ * `positions` in TIME order (D399).
40255
+ *
40256
+ * The array is APPENDED in arrival order and the two detector passes interleave,
40257
+ * so it is not the same thing: seven of the fourteen tracks the 2026-09-08 audit
40258
+ * read had at least one position whose timestamp preceded its predecessor's, and
40259
+ * a consumer that draws the array as a polyline draws a path crossing back on
40260
+ * itself. Everything that reasons about a first, a last, or a velocity was
40261
+ * reading them as stored.
40262
+ *
40263
+ * Immutable — returns a new array; `toSorted` is stable, so positions sharing a
40264
+ * timestamp keep their arrival order relative to each other. Applied at the READ
40265
+ * boundaries only: the write path keeps arrival order on purpose, because that
40266
+ * is a fact about delivery and `totalDistance` has always been accumulated on it.
40267
+ */
40268
+ function positionsInTimeOrder(positions) {
40269
+ return positions.toSorted((a, b) => a.timestamp - b.timestamp);
40270
+ }
39730
40271
  var DEFAULT_CONFIG = {
39731
40272
  ttlMs: 3e4,
39732
40273
  maxPositionHistory: 300,
@@ -40093,7 +40634,7 @@ function cloneTrack(t) {
40093
40634
  subLabelMeta: t.subLabelMeta,
40094
40635
  firstSeen: t.firstSeen,
40095
40636
  lastSeen: t.lastSeen,
40096
- positions: t.positions.map((p) => ({
40637
+ positions: positionsInTimeOrder(t.positions).map((p) => ({
40097
40638
  ...p,
40098
40639
  bbox: { ...p.bbox }
40099
40640
  })),
@@ -41686,7 +42227,7 @@ var TrackStore = class {
41686
42227
  */
41687
42228
  const storedObservations = data["observations"];
41688
42229
  const observations = typeof storedObservations === "number" ? storedObservations : slim ? void 0 : storedPositions.length;
41689
- const positions = storedPositions;
42230
+ const positions = positionsInTimeOrder(storedPositions);
41690
42231
  const snapshots = slim ? [] : data["snapshots"] ?? [];
41691
42232
  const zones = data["zonesVisited"] ?? [];
41692
42233
  const classes = data["classes"];
@@ -71447,7 +71988,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71447
71988
  x: center.x,
71448
71989
  y: center.y,
71449
71990
  timestamp: result.timestamp,
71450
- bbox: { ...t.bbox }
71991
+ bbox: { ...t.bbox },
71992
+ source: t.nativeRecovery !== void 0 ? "recovery" : "frame",
71993
+ score: t.confidence
71451
71994
  },
71452
71995
  zones: t.zones,
71453
71996
  state: t.state
@@ -71940,6 +72483,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71940
72483
  mediaKey: s.mediaKey
71941
72484
  });
71942
72485
  for (const trackId of res.thumbnailTrackIds) this.residents.markThumbnailLanded(deviceId, trackId);
72486
+ const landedThumbnails = new Set(res.thumbnailTrackIds);
72487
+ for (const target of snapshotTargets) {
72488
+ if (!target.bestThumbnail || landedThumbnails.has(target.trackId)) continue;
72489
+ log.warn("best shot capture did not land", {
72490
+ tags: { deviceId },
72491
+ meta: {
72492
+ trackId: target.trackId,
72493
+ timestamp: target.timestamp,
72494
+ keyFrameRequested: target.keyFrame !== "skip"
72495
+ }
72496
+ });
72497
+ }
71943
72498
  for (const trackId of res.firstFrameTrackIds) {
71944
72499
  this.residents.clearFirstFramePending(trackId);
71945
72500
  this.residents.markFirstFrameLanded(deviceId, trackId);
@@ -72980,7 +73535,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72980
73535
  const ctx = this.ctxIfReady;
72981
73536
  if (ctx === null) return;
72982
73537
  promoteSemanticBestFrame({
72983
- observeBestFrame: (o) => this.residents.observeBestFrame(o.deviceId, o.trackId, o.confidence, o.timestamp, o.edgeClear, o.centerScore, o.containment, o.sizeScore, o.squareFraction, o.semantic),
73538
+ observeBestFrame: (o) => this.residents.observeBestFrame(o.deviceId, o.trackId, o.confidence, o.timestamp, o.edgeClear, o.centerScore, o.containment, o.sizeScore, o.squareFraction, o.semantic, o.canvasFill),
72984
73539
  recordBestSeenBbox: (deviceId, trackId, bbox) => this.residents.recordBestSeenBbox(deviceId, trackId, bbox),
72985
73540
  captureBestShot: (request) => this.captureSemanticBestShot(request),
72986
73541
  logger: ctx.logger
@@ -73185,13 +73740,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73185
73740
  W: frameWidth,
73186
73741
  H: frameHeight
73187
73742
  });
73743
+ const canvasFill = canvasFillFraction(t.bbox, {
73744
+ W: frameWidth,
73745
+ H: frameHeight
73746
+ });
73188
73747
  const firstBox = this.trackStore?.peekActive(t.trackId)?.positions[0]?.bbox;
73189
- const isNewBest = firstBox !== void 0 && thumbnailWouldShowParkedNotSubject({
73748
+ const skipParkedThumb = firstBox !== void 0 && thumbnailWouldShowParkedNotSubject({
73190
73749
  currentBbox: t.bbox,
73191
73750
  firstBbox: firstBox,
73192
73751
  className: t.className,
73193
73752
  entries: this.stationaryRegistry?.listViews(deviceId) ?? []
73194
- }) ? false : this.residents.observeBestFrame(deviceId, t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, false);
73753
+ });
73754
+ const heldBest = this.residents.bestFrameHeld(t.trackId);
73755
+ const isNewBest = skipParkedThumb ? false : this.residents.observeBestFrame(deviceId, t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, false, canvasFill);
73756
+ const comparison = skipParkedThumb ? void 0 : this.residents.bestFrameLastComparison(t.trackId);
73195
73757
  this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
73196
73758
  const plan = planPeriodicMedia({
73197
73759
  debugMediaEnabled: media.debugMediaEnabled,
@@ -73224,6 +73786,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73224
73786
  state: this.residents.keyFrameCapture(t.trackId),
73225
73787
  now: timestamp
73226
73788
  }).kind;
73789
+ const suppressedBy = (() => {
73790
+ if (skipParkedThumb) return "parked-thumbnail";
73791
+ if (!isNewBest || bestThumbnail) return void 0;
73792
+ if (!plan.bestThumbnail) return "not-planned";
73793
+ if (!plausibleBox) return "implausible-box";
73794
+ if (!retryBoxPlausible) return "retry-box-collapsed";
73795
+ return "capture-in-flight";
73796
+ })();
73797
+ const promotionAttempt = {
73798
+ trackId: t.trackId,
73799
+ className: t.className,
73800
+ timestamp,
73801
+ candidate: {
73802
+ confidence: t.confidence,
73803
+ edgeClear,
73804
+ centerScore,
73805
+ containment,
73806
+ sizeScore,
73807
+ squareFraction,
73808
+ canvasFill,
73809
+ semantic: false
73810
+ },
73811
+ ...heldBest !== void 0 ? { held: heldBest } : {},
73812
+ ...comparison !== void 0 ? { comparison } : {},
73813
+ accepted: isNewBest,
73814
+ ...suppressedBy !== void 0 ? { suppressedBy } : {},
73815
+ thumbnailRequested: bestThumbnail,
73816
+ keyFrameRequested: keyFrame !== "skip"
73817
+ };
73818
+ const refusalLoggedAt = this.residents.bestShotRefusalLoggedAt(t.trackId);
73819
+ if (shouldLogPromotionAttempt(promotionAttempt, {
73820
+ thumbnailLanded: this.residents.isThumbnailLanded(t.trackId),
73821
+ ...refusalLoggedAt !== void 0 ? { lastRefusalLogAtMs: refusalLoggedAt } : {}
73822
+ })) {
73823
+ if (!isNewBest) this.residents.noteBestShotRefusalLogged(deviceId, t.trackId, timestamp);
73824
+ this.ctx.logger.info("best shot promotion", {
73825
+ tags: { deviceId },
73826
+ meta: { ...bestShotPromotionMeta(promotionAttempt) }
73827
+ });
73828
+ }
73227
73829
  if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && keyFrame === "skip") continue;
73228
73830
  targets.push({
73229
73831
  trackId: t.trackId,
@@ -74575,6 +75177,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74575
75177
  }
74576
75178
  });
74577
75179
  });
75180
+ p.setSpawnBesideCoastingObserver((record) => {
75181
+ this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
75182
+ tags: { deviceId },
75183
+ meta: {
75184
+ source,
75185
+ ...record
75186
+ }
75187
+ });
75188
+ });
74578
75189
  p.setTrackLabelState((trackId) => {
74579
75190
  const active = this.trackStore?.getActiveByTrack(trackId);
74580
75191
  return active !== null && active !== void 0 ? {