@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.
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-C6gK_SDX.mjs";
1
+ import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-WtZuLM5F.mjs";
2
2
  import { t as __exportAll } from "../embedding-encoder/index.mjs";
3
3
  import * as fs from "node:fs";
4
4
  import { promises } from "node:fs";
@@ -27646,6 +27646,89 @@ function deliveredWideCentralSquareLayout(bbox, frame) {
27646
27646
  if (centralSquareContainmentFraction(bbox, frame, layout) >= .5) return layout;
27647
27647
  return wideCentralSquareLayout(bbox, frame, { containSubject: true });
27648
27648
  }
27649
+ /**
27650
+ * Fraction of the DELIVERED 16:9 canvas that is NOT real pixels — the lateral
27651
+ * fill the composer synthesizes because the window ran off a frame edge. 0 =
27652
+ * the whole canvas is scene; 0.21 = a fifth of the delivered image is fill.
27653
+ *
27654
+ * Measured on {@link deliveredWideCentralSquareLayout} — the layout the
27655
+ * persisted `thumbnail` is actually cut with — because the operator complains
27656
+ * about the image they were SHOWN, not about a hypothetical centred window.
27657
+ * The two differ exactly where it matters: the default layout slides fully
27658
+ * in-frame and carries no fill at all, while the delivery fallback re-imposes
27659
+ * the subject's slack bound (so the square still shows the subject) and pays
27660
+ * for it in padding.
27661
+ *
27662
+ * Upper bound: a window that can only show its central square delivers
27663
+ * `1 − 9/16 = 0.4375` fill; nothing can exceed that while the square itself is
27664
+ * in frame.
27665
+ *
27666
+ * Pure geometry (no I/O). Feeds the best-frame ranking as a BOUNDED
27667
+ * within-tier factor — see `edge-clear.ts#CANVAS_FILL_MAX_PENALTY` for why it
27668
+ * is deliberately not a tier.
27669
+ */
27670
+ function canvasFillFraction(bbox, frame, layout = deliveredWideCentralSquareLayout(bbox, frame)) {
27671
+ if (layout.canvasW <= 0) return 0;
27672
+ return Math.max(0, Math.min(1, (layout.canvasW - layout.slabW) / layout.canvasW));
27673
+ }
27674
+ //#endregion
27675
+ //#region src/pipeline-analytics/pipeline/best-shot-promotion-log.ts
27676
+ function r3(value) {
27677
+ return Math.round(value * 1e3) / 1e3;
27678
+ }
27679
+ /**
27680
+ * Build the attempt line's meta. Pure.
27681
+ *
27682
+ * `decidedBy` is `'first-sighting'` when there was no held peak: the track's
27683
+ * first observation is accepted unconditionally and nothing was compared, and
27684
+ * saying `'effective-score'` there would invent a comparison that never ran.
27685
+ */
27686
+ function bestShotPromotionMeta(attempt) {
27687
+ const { candidate, held, comparison } = attempt;
27688
+ return {
27689
+ trackId: attempt.trackId,
27690
+ className: attempt.className,
27691
+ timestamp: attempt.timestamp,
27692
+ accepted: attempt.accepted,
27693
+ decidedBy: comparison?.decidedBy ?? "first-sighting",
27694
+ ...comparison === void 0 ? {} : {
27695
+ heldScore: r3(comparison.currentScore),
27696
+ candidateScore: r3(comparison.candidateScore)
27697
+ },
27698
+ candConfidence: r3(candidate.confidence),
27699
+ candEdgeClear: candidate.edgeClear,
27700
+ ...candidate.containment === void 0 ? {} : { candContainment: r3(candidate.containment) },
27701
+ ...candidate.sizeScore === void 0 ? {} : { candSizeScore: r3(candidate.sizeScore) },
27702
+ ...candidate.squareFraction === void 0 ? {} : { candSquareFraction: r3(candidate.squareFraction) },
27703
+ ...candidate.canvasFill === void 0 ? {} : { candCanvasFill: r3(candidate.canvasFill) },
27704
+ candSemantic: candidate.semantic === true,
27705
+ ...held === void 0 ? {} : {
27706
+ heldConfidence: r3(held.confidence),
27707
+ heldEdgeClear: held.edgeClear,
27708
+ ...held.canvasFill === void 0 ? {} : { heldCanvasFill: r3(held.canvasFill) }
27709
+ },
27710
+ thumbnailRequested: attempt.thumbnailRequested,
27711
+ keyFrameRequested: attempt.keyFrameRequested,
27712
+ ...attempt.suppressedBy === void 0 ? {} : { suppressedBy: attempt.suppressedBy }
27713
+ };
27714
+ }
27715
+ /**
27716
+ * Is this attempt worth a line?
27717
+ *
27718
+ * Every ACCEPTED attempt is — it is the moment the operator's cover image
27719
+ * changes, or fails to. A REFUSED attempt is logged only while the track still
27720
+ * has NO picture at all, and then at most one line per
27721
+ * {@link PROMOTION_REFUSAL_LOG_GAP_MS}: "the best shot never moved off the birth
27722
+ * frame" is exactly the shape `5f5f0f6e` presented and could not be diagnosed,
27723
+ * while a track whose thumbnail already landed is refusing candidates normally
27724
+ * and has nothing to explain.
27725
+ */
27726
+ function shouldLogPromotionAttempt(attempt, gate) {
27727
+ if (attempt.accepted) return true;
27728
+ if (gate.thumbnailLanded) return false;
27729
+ const last = gate.lastRefusalLogAtMs;
27730
+ return last === void 0 || attempt.timestamp - last >= 2e3;
27731
+ }
27649
27732
  var defaultScheduler = (tick, intervalMs) => {
27650
27733
  const handle = setInterval(tick, intervalMs);
27651
27734
  handle.unref?.();
@@ -30313,11 +30396,16 @@ function analyseDebugTrack(inventory) {
30313
30396
  const out = [];
30314
30397
  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)."));
30315
30398
  else if (closed) {
30316
- if (!hasKind(media, "keyFrame")) out.push(finding("MISSING_KEYFRAME", "error", "No keyFrame — the best-shot frame is absent."));
30399
+ const keyFrameMissing = !hasKind(media, "keyFrame");
30400
+ const lastFrameCopySourceMissing = keyFrameMissing || !hasKind(media, "keyFrameSmall");
30401
+ const noSnapshot = !hasKind(media, "snapshot");
30402
+ if (keyFrameMissing) out.push(finding("MISSING_KEYFRAME", "error", "No keyFrame — the best-shot frame is absent."));
30317
30403
  if (!hasKind(media, "firstFrame")) out.push(finding("MISSING_FIRSTFRAME", "warn", "No firstFrame for a closed track."));
30318
- if (!hasKind(media, "lastFrame")) out.push(finding("MISSING_LASTFRAME", "warn", "No lastFrame for a closed track."));
30319
- if (!hasKind(media, "thumbnail")) out.push(finding("MISSING_THUMBNAIL", "warn", "No thumbnail — every list surface renders blank."));
30320
- 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."));
30404
+ 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."));
30405
+ 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."));
30406
+ const cropExpected = facts.hasFace === true || facts.hasEmbeddedFace === true;
30407
+ const noCrop = !hasKind(media, "crop") && !hasKind(media, "faceCrop") && !hasKind(media, "plateCrop");
30408
+ 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."));
30321
30409
  }
30322
30410
  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.`));
30323
30411
  if (eventCount === 0) out.push(finding("ZERO_EVENTS", "error", "No object events — the track exists but recorded nothing."));
@@ -30401,7 +30489,9 @@ function toFacts(track) {
30401
30489
  snapshotCount: track.snapshots.length,
30402
30490
  ...track.importance === void 0 ? {} : { importance: track.importance },
30403
30491
  ...track.bestEventId === void 0 ? {} : { bestEventId: track.bestEventId },
30404
- ...track.previewMissReason === void 0 ? {} : { previewMissReason: track.previewMissReason }
30492
+ ...track.previewMissReason === void 0 ? {} : { previewMissReason: track.previewMissReason },
30493
+ ...track.hasFace === void 0 ? {} : { hasFace: track.hasFace },
30494
+ ...track.hasEmbeddedFace === void 0 ? {} : { hasEmbeddedFace: track.hasEmbeddedFace }
30405
30495
  };
30406
30496
  }
30407
30497
  /**
@@ -33487,6 +33577,33 @@ var CONTAINMENT_MAX_SIDE_PENALTY = .5;
33487
33577
  * NO best frame at all). */
33488
33578
  var CONTAINMENT_MIN = .1;
33489
33579
  /**
33580
+ * Aspect at or above which a clipped subject reads as INTACT — no penalty. 2.5
33581
+ * = the box is two-and-a-half times longer along the clipped axis than across
33582
+ * it, which is what a whole upright person standing with their feet on the
33583
+ * bottom border looks like.
33584
+ */
33585
+ var CLIP_ASPECT_NO_PENALTY = 2.5;
33586
+ /**
33587
+ * How much of a clipped subject is missing, from the SUBJECT'S OWN geometry.
33588
+ *
33589
+ * `alongClip` is the bbox's extent along the axis the border cut (height for a
33590
+ * top/bottom clip, width for a left/right clip); `acrossClip` is its extent on
33591
+ * the other axis. Returns the share of {@link CONTAINMENT_MAX_SIDE_PENALTY} to
33592
+ * apply, ramped linearly between {@link CLIP_ASPECT_FULL_PENALTY} and
33593
+ * {@link CLIP_ASPECT_NO_PENALTY}.
33594
+ *
33595
+ * Truncation COMPRESSES a box along the axis it was cut on, so the surviving
33596
+ * aspect is the signal: it is intrinsic to the subject, scale-invariant and
33597
+ * frame-invariant. Degenerate (`acrossClip <= 0`) reads as fully truncated.
33598
+ */
33599
+ function clipPenaltyScale(alongClip, acrossClip) {
33600
+ if (acrossClip <= 0) return 1;
33601
+ const aspect = alongClip / acrossClip;
33602
+ if (aspect <= 1) return 1;
33603
+ if (aspect >= 2.5) return 0;
33604
+ return 1 - (aspect - 1) / (CLIP_ASPECT_NO_PENALTY - 1);
33605
+ }
33606
+ /**
33490
33607
  * Edge-CONTAINMENT factor for a (clamped) bbox: an estimate in
33491
33608
  * `[CONTAINMENT_MIN, 1]` of the fraction of the subject that is actually in
33492
33609
  * frame. 1 = fully contained (no border clipping); lower = the subject is
@@ -33494,15 +33611,26 @@ var CONTAINMENT_MIN = .1;
33494
33611
  * walking out of view).
33495
33612
  *
33496
33613
  * Because detection bboxes are already CLAMPED to the frame, the true off-screen
33497
- * extent is unknown, so this is a purely-geometric proxy: for each border the
33498
- * bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band), the penalty is
33499
- * scaled by how much of that border the bbox spans on the perpendicular axis —
33500
- * i.e. the subject's aspect against that edge. A wide bbox flush to the
33501
- * top/bottom (a wide subject cut off top/bottom) or a tall bbox flush to the
33502
- * left/right (a tall subject cut off at the side) is heavily penalised, whereas
33503
- * a narrow subject whose feet merely graze the bottom border keeps most of its
33504
- * score. Degenerate/unknown dims (≤ 0) return 1 (neutral — matches
33505
- * {@link isEdgeClear}).
33614
+ * extent is unknown, so this is a purely-geometric proxy. For each border the
33615
+ * bbox touches (within the {@link DEFAULT_EDGE_TOLERANCE} band) the penalty is
33616
+ * scaled by {@link clipPenaltyScale} the surviving remnant's own aspect
33617
+ * ALONG the cut axis. A wide bbox flush to the top/bottom (a wide subject cut
33618
+ * off top/bottom) or a tall bbox flush to the left/right (a tall subject cut
33619
+ * off at the side) is heavily penalised, whereas a narrow subject whose feet
33620
+ * merely graze the bottom border keeps its score. Degenerate/unknown dims
33621
+ * (≤ 0) return 1 (neutral — matches {@link isEdgeClear}).
33622
+ *
33623
+ * **Why the basis changed (2026-09-08, D400, track `841f9b8e`).** The penalty
33624
+ * used to be scaled by the bbox's SPAN across the touched border as a fraction
33625
+ * of the FRAME. That calibration was written for a wide subject cut off top or
33626
+ * bottom, and it is meaningless for an upright person: a 4K frame is 3840 px
33627
+ * wide, a person clipped at the head is ~400 px wide, so `spanX = 0.104` and a
33628
+ * decapitated person lost **5.2 %** of its score — the operator's "questi tagli
33629
+ * hanno uno score troppo alto, non si vedono il dorso e testa", with the number
33630
+ * attached. The frame's width is not a fact about the subject. The remnant's
33631
+ * own aspect is: the same person now loses 47.9 %, and the wide-subject case the
33632
+ * old shape existed for is penalised MORE than before (a 900×400 vehicle flush
33633
+ * to the bottom went from 0.88 to 0.50), not less.
33506
33634
  */
33507
33635
  function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE_TOLERANCE) {
33508
33636
  if (frameWidth <= 0 || frameHeight <= 0) return 1;
@@ -33512,16 +33640,16 @@ function bboxContainment(bbox, frameWidth, frameHeight, tolerance = DEFAULT_EDGE
33512
33640
  const top = bbox.y;
33513
33641
  const right = bbox.x + bbox.w;
33514
33642
  const bottom = bbox.y + bbox.h;
33515
- const spanX = Math.max(0, Math.min(1, bbox.w / frameWidth));
33516
- const spanY = Math.max(0, Math.min(1, bbox.h / frameHeight));
33643
+ const verticalBorderScale = clipPenaltyScale(bbox.w, bbox.h);
33644
+ const horizontalBorderScale = clipPenaltyScale(bbox.h, bbox.w);
33517
33645
  let containment = 1;
33518
- const penalize = (span) => {
33519
- containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * span;
33646
+ const penalize = (scale) => {
33647
+ containment *= 1 - CONTAINMENT_MAX_SIDE_PENALTY * scale;
33520
33648
  };
33521
- if (left <= tolX) penalize(spanY);
33522
- if (right >= frameWidth - tolX) penalize(spanY);
33523
- if (top <= tolY) penalize(spanX);
33524
- if (bottom >= frameHeight - tolY) penalize(spanX);
33649
+ if (left <= tolX) penalize(verticalBorderScale);
33650
+ if (right >= frameWidth - tolX) penalize(verticalBorderScale);
33651
+ if (top <= tolY) penalize(horizontalBorderScale);
33652
+ if (bottom >= frameHeight - tolY) penalize(horizontalBorderScale);
33525
33653
  return Math.max(CONTAINMENT_MIN, Math.min(1, containment));
33526
33654
  }
33527
33655
  /** Fraction of the frame area at which a subject's size score saturates to 1.
@@ -33568,13 +33696,38 @@ var SQUARE_EVICTION_SCORE_FLOOR = .05;
33568
33696
  * raw confidence — the ABSOLUTE hysteresis margin stays meaningful — while a
33569
33697
  * bigger view still earns up to a 2× relative preference. */
33570
33698
  var SIZE_FACTOR_FLOOR = .5;
33699
+ /**
33700
+ * Maximum fraction of a candidate's effective score the DELIVERED-CANVAS fill
33701
+ * can subtract, at 100 % fill (D400).
33702
+ *
33703
+ * **Why this is a factor and not a tier.** Fill is anti-correlated with subject
33704
+ * size by construction: the 16:9 window is `side × 16/9` of the subject's
33705
+ * central square, so a big, close subject is far likelier to overflow the frame
33706
+ * than a small distant one — a small subject near the centre has zero fill
33707
+ * ALWAYS. A hard "no-fill beats fill" tier would therefore systematically
33708
+ * promote distant, tiny subjects over close ones, which is the exact defect
33709
+ * `bboxSizeScore` was added to fix (BEST_SHOT_STALE, 2026-07-22 / 2026-09-03).
33710
+ * So it enters as a bounded multiplicative factor in the same family as
33711
+ * `containment` and `sizeFactor`, strictly BELOW every tier, and as a near-
33712
+ * equal tie-break ranked ABOVE centeredness (a measurement of the delivered
33713
+ * image beats a proxy for it).
33714
+ *
33715
+ * **Bounded so it can only ever tie-break.** The real fill ceiling is 0.4375
33716
+ * (a window that can show no more than its own central square), so the largest
33717
+ * move this term can make is `0.15 × 0.4375 = 6.6 %` of the effective score. It
33718
+ * cannot overturn a lead of 7.1 % or more — and the metric that would say it
33719
+ * has stopped tie-breaking and started dominating is the share of accepted
33720
+ * best-shot promotions logged with `decidedBy: 'canvas-fill'`.
33721
+ */
33722
+ var CANVAS_FILL_MAX_PENALTY = .15;
33571
33723
  /** The effective within-tier score: detector confidence scaled by the
33572
- * edge-containment and (floored) subject-size factors — each neutral at 1
33573
- * when absent. */
33724
+ * edge-containment, (floored) subject-size and delivered-canvas-fill factors —
33725
+ * each neutral at 1 when absent. */
33574
33726
  function effectiveScore(c) {
33575
33727
  const sizeFactor = c.sizeScore === void 0 ? 1 : SIZE_FACTOR_FLOOR + (1 - SIZE_FACTOR_FLOOR) * c.sizeScore;
33576
33728
  const squareFactor = isSquareContained(c) ? 1 : Math.max(c.squareFraction ?? 0, SQUARE_EVICTION_SCORE_FLOOR);
33577
- return c.confidence * (c.containment ?? 1) * sizeFactor * squareFactor;
33729
+ const fillFactor = 1 - CANVAS_FILL_MAX_PENALTY * Math.max(0, Math.min(1, c.canvasFill ?? 0));
33730
+ return c.confidence * (c.containment ?? 1) * sizeFactor * squareFactor * fillFactor;
33578
33731
  }
33579
33732
  /**
33580
33733
  * True when `candidate` beats `current` DECISIVELY — same-or-better edge tier
@@ -33592,48 +33745,51 @@ function isDecisiveImprovement(current, candidate) {
33592
33745
  return effectiveScore(candidate) >= effectiveScore(current) * DECISIVE_IMPROVEMENT_RATIO;
33593
33746
  }
33594
33747
  /**
33595
- * Edge-aware "is `candidate` a new best over `current`?" comparator.
33596
- *
33597
- * Tier order: edge-clear ALWAYS outranks edge-touching (a whole subject beats a
33598
- * clipped one regardless of confidence). WITHIN the same tier, a strictly-higher
33599
- * confidence past the `hysteresis` margin wins. The tier upgrade
33600
- * (touching clear) bypasses hysteresis the first clear frame is always taken.
33601
- *
33602
- * EDGE-CONTAINMENT (#edge-clip): within the same tier the comparison is on the
33603
- * EFFECTIVE score `confidence × containment` (see {@link bboxContainment}), not
33604
- * raw confidence so a fully-contained detection outranks a larger/higher-
33605
- * confidence but border-CLIPPED one unless the confidence margin is decisive.
33606
- * The binary edge-clear tier still gates first (a whole subject beats a clipped
33607
- * one regardless), and within the CLEAR tier both frames have containment 1, so
33608
- * this is a no-op there; it discriminates WITHIN the touching tier (feet merely
33609
- * grazing the bottom vs half the body out the side). `containment` defaults to
33610
- * 1 when omitted (legacy face / object-embedding callers) identical behaviour.
33611
- *
33612
- * CENTERING TIE-BREAK (#27-D): within the same tier, when neither frame clearly
33613
- * wins on the effective score (the two are within the `hysteresis` band) but the
33614
- * candidate is meaningfully better CENTRED ({@link CENTER_TIE_BREAK_MARGIN}), the
33615
- * candidate wins. This only engages when both sides carry a `centerScore` (the
33616
- * best-frame path), so low-importance short tracks stop keeping an edge-of-frame
33617
- * subject over an equally-confident, better-framed one. The face /
33618
- * object-embedding callers omit `centerScore` → identical legacy behaviour.
33619
- *
33620
- * Time gating (`minGapMs`) is applied by the caller (`BestDetectionTracker`),
33621
- * not here, so this stays a pure value comparison.
33622
- */
33623
- function isEdgeAwareNewBest(current, candidate, hysteresis) {
33624
- if (isSquareContained(candidate) && !isSquareContained(current)) return true;
33625
- if (!isSquareContained(candidate) && isSquareContained(current)) return false;
33626
- if (isSemantic(candidate) && !isSemantic(current)) return true;
33627
- if (!isSemantic(candidate) && isSemantic(current)) return false;
33628
- if (candidate.edgeClear && !current.edgeClear) return true;
33629
- if (!candidate.edgeClear && current.edgeClear) return false;
33630
- const curEffective = effectiveScore(current);
33631
- const candEffective = effectiveScore(candidate);
33632
- if (candEffective > curEffective + hysteresis) return true;
33748
+ * The ONE best-frame comparison, decomposed. {@link isEdgeAwareNewBest} is this
33749
+ * function's `accepted` bit and nothing else — there is no second copy of the
33750
+ * ordering to drift.
33751
+ *
33752
+ * The tier order, highest first, and where the delivered-canvas term sits:
33753
+ * 1. **square-containment** does the delivered SQUARE show the subject at all?
33754
+ * 2. **semantic** — did a recognition succeed on this frame?
33755
+ * 3. **edge-clear** is the whole subject inside the frame?
33756
+ * 4. **effective-score** `confidence × containment × size × square × canvasFill`.
33757
+ * 5. **canvas-fill**near-equal tie-break: less synthesized lateral fill wins.
33758
+ * 6. **centeredness** near-equal tie-break of last resort.
33759
+ *
33760
+ * Canvas fill enters at 4 (bounded, see {@link CANVAS_FILL_MAX_PENALTY}) and 5,
33761
+ * never as a tier: fill is anti-correlated with subject size, so a tier would
33762
+ * hand every camera's best shot to its most distant subject. It ranks ABOVE
33763
+ * centeredness because both answer "is this a well-framed delivery?" and fill
33764
+ * MEASURES the delivered canvas while centeredness only proxies it.
33765
+ */
33766
+ function compareBestFrame(current, candidate, hysteresis) {
33767
+ const currentScore = effectiveScore(current);
33768
+ const candidateScore = effectiveScore(candidate);
33769
+ const verdict = (accepted, decidedBy) => ({
33770
+ accepted,
33771
+ decidedBy,
33772
+ currentScore,
33773
+ candidateScore
33774
+ });
33775
+ return compareBestFrameTiers(current, candidate, hysteresis, currentScore, candidateScore, verdict);
33776
+ }
33777
+ function compareBestFrameTiers(current, candidate, hysteresis, curEffective, candEffective, verdict) {
33778
+ if (isSquareContained(candidate) && !isSquareContained(current)) return verdict(true, "square-containment");
33779
+ if (!isSquareContained(candidate) && isSquareContained(current)) return verdict(false, "square-containment");
33780
+ if (isSemantic(candidate) && !isSemantic(current)) return verdict(true, "semantic");
33781
+ if (!isSemantic(candidate) && isSemantic(current)) return verdict(false, "semantic");
33782
+ if (candidate.edgeClear && !current.edgeClear) return verdict(true, "edge-clear");
33783
+ if (!candidate.edgeClear && current.edgeClear) return verdict(false, "edge-clear");
33784
+ if (candEffective > curEffective + hysteresis) return verdict(true, "effective-score");
33785
+ const nearEqual = Math.abs(candEffective - curEffective) <= hysteresis;
33786
+ if (candidate.canvasFill !== void 0 && current.canvasFill !== void 0) {
33787
+ if (nearEqual && candidate.canvasFill + .05 < current.canvasFill) return verdict(true, "canvas-fill");
33788
+ }
33633
33789
  if (candidate.centerScore !== void 0 && current.centerScore !== void 0) {
33634
- if (Math.abs(candEffective - curEffective) <= hysteresis && candidate.centerScore > current.centerScore + .1) return true;
33790
+ if (nearEqual && candidate.centerScore > current.centerScore + .1) return verdict(true, "centeredness");
33635
33791
  }
33636
- return false;
33792
+ return verdict(false, nearEqual ? "centeredness" : "effective-score");
33637
33793
  }
33638
33794
  //#endregion
33639
33795
  //#region src/pipeline-analytics/pipeline/first-frame-retry-bbox.ts
@@ -34052,6 +34208,74 @@ var LooseBindSampler = class {
34052
34208
  this.counts.clear();
34053
34209
  }
34054
34210
  };
34211
+ /**
34212
+ * Floor on the predictive-drift clamp, in box sides.
34213
+ *
34214
+ * The scaled term collapses at short intervals — at the 56 ms minimum measured
34215
+ * on camera 615 it is 0.11 sides — and a clamp that tight turns ordinary
34216
+ * detector box jitter into a refused match. A quarter of a side is slack, not
34217
+ * travel.
34218
+ */
34219
+ var DRIFT_MIN_SIDES = .25;
34220
+ /**
34221
+ * Re-acquisition reach, in box DIAGONALS per second.
34222
+ *
34223
+ * Answers `267840a1` again, on the gate that actually refused it: 908.4 px of
34224
+ * centroid travel in 880 ms against a `hypot(513.6, 721.2)` = 885.5 px diagonal
34225
+ * is 1.166 diagonals per second. 1.5 clears it with ~29 % margin and is
34226
+ * physically bounded — 1.5 of a subject's own body-diagonal per second is a
34227
+ * brisk run, and being scale-free it means the same thing for a far-field
34228
+ * person with a 90-px box as for a near one with a 900-px box.
34229
+ *
34230
+ * The number it replaces (`rescueCentroidFactor` 0.75 × diagonal, flat) was
34231
+ * doing two jobs at once: at a 2135 ms poll gap it was far too tight to bridge
34232
+ * a walking subject, and at a 150 ms burst interval it was loose enough to hand
34233
+ * one subject's id to another 452 px away (track `27855a56`, a second car, at
34234
+ * IoU 0.058). One number, two opposite failures.
34235
+ */
34236
+ var REACH_DIAGONALS_PER_SEC = 1.5;
34237
+ /**
34238
+ * Floor on the re-acquisition reach, in box diagonals.
34239
+ *
34240
+ * Deliberately TIGHTER than the 0.75 flat factor it replaces. At a 150 ms burst
34241
+ * interval the scaled term is 0.225 diagonals; the floor lifts it to 0.3, which
34242
+ * is 221 px on `27855a56`'s 738-px diagonal — comfortably below the 452 px that
34243
+ * bought that track a second car. A short gap is evidence that the subject
34244
+ * cannot have gone far, and the gate now says so.
34245
+ */
34246
+ var REACH_MIN_DIAGONALS = .3;
34247
+ /**
34248
+ * Ceiling on the re-acquisition reach, in box diagonals.
34249
+ *
34250
+ * `REACH_DIAGONALS_PER_SEC × BUDGET_CEILING_MS`. A subject unseen for the full
34251
+ * `maxMissedMs` gets 1.5 diagonals of reach, not 6.
34252
+ */
34253
+ var REACH_MAX_DIAGONALS = 1.5;
34254
+ function clampRange(value, min, max) {
34255
+ return Math.max(min, Math.min(max, value));
34256
+ }
34257
+ /**
34258
+ * How far a coasting box's velocity extrapolation may carry it on one axis.
34259
+ *
34260
+ * `side` is the box's width for the x drift and its height for the y drift, so
34261
+ * the allowance keeps the same meaning for a tall person and a wide car.
34262
+ */
34263
+ function driftAllowancePx(side, elapsedMs) {
34264
+ const elapsed = Math.max(0, elapsedMs);
34265
+ return clampRange(side * 2 * (elapsed / 1e3), side * DRIFT_MIN_SIDES, side * 2);
34266
+ }
34267
+ /**
34268
+ * How far from where a track is EXPECTED a detection may sit and still be
34269
+ * claimed as that track by the loose (rescue / resurrection) reach clause.
34270
+ *
34271
+ * Scale-free: the budget is a multiple of the track's own diagonal, so it does
34272
+ * not have to know the frame size, the camera, or how far away the subject is.
34273
+ */
34274
+ function reacquireReachPx(bbox, elapsedMs) {
34275
+ const diag = Math.hypot(bbox.w, bbox.h);
34276
+ const elapsed = Math.max(0, elapsedMs);
34277
+ return clampRange(diag * REACH_DIAGONALS_PER_SEC * (elapsed / 1e3), diag * REACH_MIN_DIAGONALS, diag * REACH_MAX_DIAGONALS);
34278
+ }
34055
34279
  //#endregion
34056
34280
  //#region src/pipeline-analytics/pipeline/tracker/rescue-growth.ts
34057
34281
  /**
@@ -34111,6 +34335,21 @@ function growthAdjacentMatch(track, det, ctx) {
34111
34335
  }
34112
34336
  /** Per-reason counter + sampling decision. One instance per tracker, so the
34113
34337
  * counts are per camera and per detection source. */
34338
+ var SpawnBesideCoastingSampler = class {
34339
+ counts = /* @__PURE__ */ new Map();
34340
+ /** The running count when this report should be emitted, else `undefined`. */
34341
+ next(reason) {
34342
+ const count = (this.counts.get(reason) ?? 0) + 1;
34343
+ this.counts.set(reason, count);
34344
+ if (count <= 10) return count;
34345
+ return count % 10 === 0 ? count : void 0;
34346
+ }
34347
+ reset() {
34348
+ this.counts.clear();
34349
+ }
34350
+ };
34351
+ /** Per-reason counter + sampling decision. One instance per tracker, so the
34352
+ * counts are per camera and per detection source. */
34114
34353
  var SpawnRefusalSampler = class {
34115
34354
  counts = /* @__PURE__ */ new Map();
34116
34355
  /** The running count when this refusal should be reported, else `undefined`. */
@@ -34135,6 +34374,7 @@ var DEFAULT_TRACKER_CONFIG = {
34135
34374
  maxTrackLifetimeMs: 3e5,
34136
34375
  rescueIouThreshold: .1,
34137
34376
  rescueCentroidFactor: .75,
34377
+ reacquireTimeScaled: true,
34138
34378
  resurrectionWindowMs: 8e3,
34139
34379
  resurrectionCrowdGuard: true,
34140
34380
  resurrectionCrowdedIouThreshold: .3,
@@ -34209,6 +34449,10 @@ var SortTracker = class SortTracker {
34209
34449
  refusalSampler = new SpawnRefusalSampler();
34210
34450
  /** Sink for dropped-detection diagnostics — see {@link setSpawnRefusalObserver}. */
34211
34451
  spawnRefusalObserver = null;
34452
+ /** Per-reason counters behind the sampled spawn-beside-coasting diagnostics. */
34453
+ spawnBesideSampler = new SpawnBesideCoastingSampler();
34454
+ /** Sink for fragmentation diagnostics — see {@link setSpawnBesideCoastingObserver}. */
34455
+ spawnBesideObserver = null;
34212
34456
  constructor(config = {}) {
34213
34457
  this.config = {
34214
34458
  ...DEFAULT_TRACKER_CONFIG,
@@ -34233,6 +34477,88 @@ var SortTracker = class SortTracker {
34233
34477
  setSpawnRefusalObserver(observer) {
34234
34478
  this.spawnRefusalObserver = observer;
34235
34479
  }
34480
+ /**
34481
+ * Wire the diagnostics sink for the case the tracker does NOT act on: a new id
34482
+ * minted beside a live, compatible, coasting track that could have claimed it.
34483
+ * Same contract as the other two sinks — the tracker has no `deviceId`, the
34484
+ * caller stamps it, and nothing is computed when no observer is wired.
34485
+ */
34486
+ setSpawnBesideCoastingObserver(observer) {
34487
+ this.spawnBesideObserver = observer;
34488
+ }
34489
+ /**
34490
+ * Report a fresh id minted beside the nearest live coasting track of its
34491
+ * association group, when there was one within
34492
+ * {@link SPAWN_BESIDE_REPORT_REACH_FACTOR} × the reach the gate used.
34493
+ *
34494
+ * `coasting` is every track surviving this frame that did NOT match a real
34495
+ * detection on it — the only ones a spawn can be the continuation of.
34496
+ */
34497
+ reportSpawnBesideCoasting(spawnedId, det, coasting, detections, frameArea, timestamp) {
34498
+ const observer = this.spawnBesideObserver;
34499
+ if (!observer) return;
34500
+ const group = this.assocGroup(det.class);
34501
+ const dc = bboxCentroid(det.bbox);
34502
+ let best;
34503
+ let bestDist = Number.POSITIVE_INFINITY;
34504
+ let bestReach = 0;
34505
+ for (const track of coasting) {
34506
+ if (this.assocGroup(track.class) !== group) continue;
34507
+ const tc = bboxCentroid(track.bbox);
34508
+ const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
34509
+ const reach = this.reacquireReach(track, timestamp, true);
34510
+ if (dist > reach * 2) continue;
34511
+ if (dist < bestDist) {
34512
+ best = track;
34513
+ bestDist = dist;
34514
+ bestReach = reach;
34515
+ }
34516
+ }
34517
+ if (!best) return;
34518
+ const declinedBy = this.explainDecline(best, det, detections, frameArea, timestamp, bestDist);
34519
+ const spawnCount = this.spawnBesideSampler.next(declinedBy);
34520
+ if (spawnCount === void 0) return;
34521
+ observer({
34522
+ spawnedTrackId: spawnedId,
34523
+ spawnedClass: det.class,
34524
+ spawnedScore: det.score,
34525
+ spawnedBbox: det.bbox,
34526
+ spawnedNativeRecovery: det.nativeRecovery !== void 0,
34527
+ coastingTrackId: best.id,
34528
+ coastingClass: best.class,
34529
+ iou: iou$3(best.bbox, det.bbox),
34530
+ predictedIou: iou$3(this.predicted(best, timestamp), det.bbox),
34531
+ centroidDistPx: bestDist,
34532
+ reachPx: bestReach,
34533
+ trackDiagPx: Math.hypot(best.bbox.w, best.bbox.h),
34534
+ gapMs: timestamp - best.lastSeen,
34535
+ trackAgeMs: timestamp - best.firstSeen,
34536
+ declinedBy,
34537
+ spawnCount,
34538
+ sampledEvery: 10
34539
+ });
34540
+ }
34541
+ /**
34542
+ * Which gate turned `track` away from `det`, evaluated in the order the
34543
+ * tracker itself evaluates them — the FIRST one that says no is the answer.
34544
+ *
34545
+ * Every clause here re-reads a fact about the frame rather than about the
34546
+ * greedy pass's claim order, so the verdict is reproducible: the one case that
34547
+ * cannot be re-derived is a pair nothing refused, which is exactly what
34548
+ * `already-claimed` means.
34549
+ */
34550
+ explainDecline(track, det, detections, frameArea, timestamp, centroidDist) {
34551
+ if (!this.sameGroupAsMeasured(track.class, det)) return "native-measured-class";
34552
+ if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return "assoc-group";
34553
+ if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "already-claimed";
34554
+ if (centroidDist <= this.reacquireReach(track, timestamp, true)) {
34555
+ if (!this.isPlausibleBirth(det, frameArea)) return "already-claimed";
34556
+ if (!this.config.reacquireTimeScaled) return "plausible-birth-veto";
34557
+ return this.isUnambiguousReach(track, det, detections) ? "already-claimed" : "crowd-guard";
34558
+ }
34559
+ if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "already-claimed";
34560
+ return "geometry";
34561
+ }
34236
34562
  /** Emit one dropped-detection record, subject to the refusal sampler. */
34237
34563
  reportRefusal(reason, det, extra = {}) {
34238
34564
  const observer = this.spawnRefusalObserver;
@@ -34501,21 +34827,39 @@ var SortTracker = class SortTracker {
34501
34827
  if (a !== void 0 && dropIds.has(a) || b !== void 0 && dropIds.has(b)) this.dupStreaks.delete(key);
34502
34828
  }
34503
34829
  }
34504
- /** Where a track is expected this frame — extrapolated by velocity while
34505
- * coasting (predictiveCoasting), else its last known bbox. A stationary
34506
- * track (speed < stationarySpeedPx) is frozen so bbox jitter can't walk the
34507
- * box off a sitting object; otherwise the drift is capped at one bbox size
34508
- * so a stale velocity can't fling the box across the frame. Public so the
34509
- * clamp is directly unit-testable. */
34510
- predicted(t) {
34830
+ /**
34831
+ * Where a track is expected at `nowMs` — extrapolated by velocity while
34832
+ * coasting (`predictiveCoasting`), else its last known bbox.
34833
+ *
34834
+ * A stationary track (speed below `stationarySpeedPx`, measured in the
34835
+ * px-per-update unit that setting is calibrated in) is frozen so bbox jitter
34836
+ * cannot walk the box off a sitting object. Otherwise the drift is the
34837
+ * velocity carried over the ELAPSED interval, capped by
34838
+ * {@link driftAllowancePx} — which is itself a function of that interval, so
34839
+ * a stale velocity cannot fling the box across the frame and a long, honest
34840
+ * gap is no longer capped at the one box width a single frame would get
34841
+ * (D399). Public so the clamp is directly unit-testable.
34842
+ */
34843
+ predicted(t, nowMs) {
34511
34844
  if (!this.config.predictiveCoasting) return t.bbox;
34512
- if (Math.hypot(t.velocity.dx, t.velocity.dy) < this.config.stationarySpeedPx) return t.bbox;
34513
- const f = t.age + 1;
34514
- const driftX = clamp(t.velocity.dx * f, -t.bbox.w, t.bbox.w);
34515
- const driftY = clamp(t.velocity.dy * f, -t.bbox.h, t.bbox.h);
34845
+ if (Math.hypot(t.velocity.dx, t.velocity.dy) * t.emitIntervalMs < this.config.stationarySpeedPx) return t.bbox;
34846
+ if (!this.config.reacquireTimeScaled) {
34847
+ const f = t.age + 1;
34848
+ const perFrameX = t.velocity.dx * t.emitIntervalMs;
34849
+ const perFrameY = t.velocity.dy * t.emitIntervalMs;
34850
+ return {
34851
+ x: t.bbox.x + clamp(perFrameX * f, -t.bbox.w, t.bbox.w),
34852
+ y: t.bbox.y + clamp(perFrameY * f, -t.bbox.h, t.bbox.h),
34853
+ w: t.bbox.w,
34854
+ h: t.bbox.h
34855
+ };
34856
+ }
34857
+ const elapsedMs = Math.max(0, nowMs - t.lastSeen);
34858
+ const allowX = driftAllowancePx(t.bbox.w, elapsedMs);
34859
+ const allowY = driftAllowancePx(t.bbox.h, elapsedMs);
34516
34860
  return {
34517
- x: t.bbox.x + driftX,
34518
- y: t.bbox.y + driftY,
34861
+ x: t.bbox.x + clamp(t.velocity.dx * elapsedMs, -allowX, allowX),
34862
+ y: t.bbox.y + clamp(t.velocity.dy * elapsedMs, -allowY, allowY),
34519
34863
  w: t.bbox.w,
34520
34864
  h: t.bbox.h
34521
34865
  };
@@ -34558,19 +34902,64 @@ var SortTracker = class SortTracker {
34558
34902
  * crouch→animal flip when `classGroupAssoc` is on, but never by an unrelated
34559
34903
  * class.
34560
34904
  */
34561
- looseMatch(track, det, timestamp) {
34905
+ looseMatch(track, det, timestamp, timeScaled = true) {
34562
34906
  if (!this.sameGroupAsMeasured(track.class, det)) return null;
34563
34907
  if (this.assocGroup(track.class) !== this.assocGroup(det.class)) return null;
34564
34908
  if (iou$3(track.bbox, det.bbox) >= this.config.rescueIouThreshold) return "iou";
34565
34909
  const tc = bboxCentroid(track.bbox);
34566
34910
  const dc = bboxCentroid(det.bbox);
34567
- const dist = Math.hypot(tc.x - dc.x, tc.y - dc.y);
34568
- const diag = Math.hypot(track.bbox.w, track.bbox.h);
34569
- if (dist <= this.config.rescueCentroidFactor * diag) return "centroid";
34911
+ if (Math.hypot(tc.x - dc.x, tc.y - dc.y) <= this.reacquireReach(track, timestamp, timeScaled)) return "centroid";
34570
34912
  if (growthAdjacentMatch(track.bbox, det.bbox, SortTracker.growthContext(track, timestamp))) return "growth";
34571
34913
  return null;
34572
34914
  }
34573
34915
  /**
34916
+ * The proximity budget the `centroid` clause is measured against.
34917
+ *
34918
+ * On the RESCUE pass (`timeScaled`) it is {@link reacquireReachPx} — a
34919
+ * multiple of the track's diagonal that grows with the elapsed gap and stops
34920
+ * at a ceiling, because a live coasting track's whereabouts genuinely become
34921
+ * less certain the longer it has gone unseen (D399).
34922
+ *
34923
+ * On the RESURRECTION pass it stays the flat `rescueCentroidFactor × diagonal`
34924
+ * it always was. That pass re-attaches a CORPSE, and it exists for one thing:
34925
+ * "a flickering stationary object keeps its identity". Elapsed time is not the
34926
+ * variable there — a stationary subject does not travel further because it
34927
+ * blinked for longer — and the corpse is strictly weaker evidence than a live
34928
+ * track, which is the same reasoning `resurrectionCrowdedIouThreshold` already
34929
+ * encodes.
34930
+ */
34931
+ reacquireReach(track, timestamp, timeScaled) {
34932
+ const diag = Math.hypot(track.bbox.w, track.bbox.h);
34933
+ if (!timeScaled || !this.config.reacquireTimeScaled) return this.config.rescueCentroidFactor * diag;
34934
+ return reacquireReachPx(track.bbox, timestamp - track.lastSeen);
34935
+ }
34936
+ /**
34937
+ * Whether a REACH bind would be unambiguous: this coasting track is the only
34938
+ * subject of its association group the scene holds, and `det` is the only box
34939
+ * of that group on the frame.
34940
+ *
34941
+ * This is what lets a reach bind survive {@link isPlausibleBirth} (D399). That
34942
+ * veto exists because "a spare track costs media; a stolen track corrupts the
34943
+ * plate, the travelled distance and the last frame of the track it was taken
34944
+ * from" — every word of which is about a scene with something to steal FROM.
34945
+ * With one track and one box there is no second subject the id could be wrong
34946
+ * about, and refusing the bind does not buy safety, it buys a fragment.
34947
+ *
34948
+ * Deliberately computed over the whole frame rather than over what is still
34949
+ * unclaimed, so the answer is a fact about the scene and reads the same at
34950
+ * bind time and at report time — the spawn-beside-coasting line has to be able
34951
+ * to say which gate declined without re-running the greedy competition.
34952
+ */
34953
+ isUnambiguousReach(track, det, detections) {
34954
+ if (this.sameGroupPeerCount(track, this.tracks) > 0) return false;
34955
+ const group = this.assocGroup(track.class);
34956
+ for (const other of detections) {
34957
+ if (other === det) continue;
34958
+ if (this.assocGroup(other.class) === group) return false;
34959
+ }
34960
+ return true;
34961
+ }
34962
+ /**
34574
34963
  * Whether `det` would mint a track of its own if no salvage branch claimed it:
34575
34964
  * it clears BOTH spawn floors (per-class score, per-class bbox-area fraction).
34576
34965
  *
@@ -34638,11 +35027,11 @@ var SortTracker = class SortTracker {
34638
35027
  * gates) so the two-tier ByteTrack association stays a single implementation.
34639
35028
  * Mutates `used` / `matchedTracks` / `matched` in place.
34640
35029
  */
34641
- greedyAssociate(detections, candidateIdxs, iouThreshold, used, matchedTracks, matched) {
35030
+ greedyAssociate(detections, candidateIdxs, iouThreshold, timestamp, used, matchedTracks, matched) {
34642
35031
  const pairs = [];
34643
35032
  for (const track of this.tracks) {
34644
35033
  if (matchedTracks.has(track)) continue;
34645
- const pbox = this.predicted(track);
35034
+ const pbox = this.predicted(track, timestamp);
34646
35035
  for (const di of candidateIdxs) {
34647
35036
  if (used.has(di)) continue;
34648
35037
  const det = detections[di];
@@ -34701,7 +35090,7 @@ var SortTracker = class SortTracker {
34701
35090
  for (let di = 0; di < detections.length; di++) if (this.config.byteTrackEnabled && detections[di].score < this.config.byteTrackHighThreshold) lowIdxs.push(di);
34702
35091
  else highIdxs.push(di);
34703
35092
  const lowSet = new Set(lowIdxs);
34704
- this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, used, matchedTracks, matched);
35093
+ this.greedyAssociate(detections, highIdxs, this.config.iouThreshold, timestamp, used, matchedTracks, matched);
34705
35094
  const rescuePairs = [];
34706
35095
  for (const track of this.tracks) {
34707
35096
  if (matchedTracks.has(track)) continue;
@@ -34710,7 +35099,7 @@ var SortTracker = class SortTracker {
34710
35099
  const det = detections[di];
34711
35100
  const gate = this.looseMatch(track, det, timestamp);
34712
35101
  if (gate === null) continue;
34713
- if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
35102
+ if (gate === "centroid" && this.isPlausibleBirth(det, frameArea) && !(this.config.reacquireTimeScaled && this.isUnambiguousReach(track, det, detections))) continue;
34714
35103
  rescuePairs.push({
34715
35104
  track,
34716
35105
  detIdx: di,
@@ -34728,7 +35117,7 @@ var SortTracker = class SortTracker {
34728
35117
  used.add(pair.detIdx);
34729
35118
  this.reportBind("rescue", pair.gate, pair.track, det, timestamp);
34730
35119
  }
34731
- if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, used, matchedTracks, matched);
35120
+ if (this.config.byteTrackEnabled && lowIdxs.length > 0) this.greedyAssociate(detections, lowIdxs, this.config.byteTrackLowIouThreshold, timestamp, used, matchedTracks, matched);
34732
35121
  for (const [track, det] of matched) {
34733
35122
  if (track.class !== det.class) this.reportRefusal("cross-class-absorbed", det, { against: track });
34734
35123
  const prevCenter = bboxCentroid({
@@ -34744,6 +35133,7 @@ var SortTracker = class SortTracker {
34744
35133
  h: det.bbox.h
34745
35134
  });
34746
35135
  const frames = track.age + 1;
35136
+ const dtMs = timestamp - track.lastSeen;
34747
35137
  track.bbox = det.bbox;
34748
35138
  track.class = det.class;
34749
35139
  track.originalClass = det.originalClass;
@@ -34751,10 +35141,13 @@ var SortTracker = class SortTracker {
34751
35141
  track.age = 0;
34752
35142
  track.hits++;
34753
35143
  track.lastSeen = timestamp;
34754
- track.velocity = {
34755
- dx: (newCenter.x - prevCenter.x) / frames,
34756
- dy: (newCenter.y - prevCenter.y) / frames
34757
- };
35144
+ if (dtMs > 0) {
35145
+ track.velocity = {
35146
+ dx: (newCenter.x - prevCenter.x) / dtMs,
35147
+ dy: (newCenter.y - prevCenter.y) / dtMs
35148
+ };
35149
+ track.emitIntervalMs = dtMs / frames;
35150
+ }
34758
35151
  track.path.push(det.bbox);
34759
35152
  if (track.path.length > MAX_PATH_LENGTH) track.path.shift();
34760
35153
  this.addClassVote(track, det);
@@ -34770,7 +35163,7 @@ var SortTracker = class SortTracker {
34770
35163
  surviving.push(track);
34771
35164
  continue;
34772
35165
  }
34773
- const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track), ob.bbox) >= this.config.occlusionContainment);
35166
+ const occluded = this.config.occlusionEnabled && occluders.some((ob) => containment(this.predicted(track, timestamp), ob.bbox) >= this.config.occlusionContainment);
34774
35167
  track.age++;
34775
35168
  if (timestamp - track.lastSeen > (occluded ? this.config.occlusionMaxMissedMs : this.config.maxMissedMs)) {
34776
35169
  track.lost = true;
@@ -34792,7 +35185,7 @@ var SortTracker = class SortTracker {
34792
35185
  for (const lost of this.lostTracks) {
34793
35186
  if (!lost.resurrectable) continue;
34794
35187
  if (timestamp - lost.lostAt > this.config.resurrectionWindowMs) continue;
34795
- const gate = this.looseMatch(lost, det, timestamp);
35188
+ const gate = this.looseMatch(lost, det, timestamp, false);
34796
35189
  if (gate === null) continue;
34797
35190
  if (gate === "centroid" && this.isPlausibleBirth(det, frameArea)) continue;
34798
35191
  const score = iou$3(lost.bbox, det.bbox);
@@ -34872,8 +35265,10 @@ var SortTracker = class SortTracker {
34872
35265
  used.add(di);
34873
35266
  continue;
34874
35267
  }
35268
+ const spawnedId = randomUUID();
35269
+ this.reportSpawnBesideCoasting(spawnedId, det, surviving.filter((t) => t.lastSeen !== timestamp), detections, frameArea, timestamp);
34875
35270
  surviving.push({
34876
- id: randomUUID(),
35271
+ id: spawnedId,
34877
35272
  bbox: det.bbox,
34878
35273
  class: det.class,
34879
35274
  originalClass: det.originalClass,
@@ -34887,6 +35282,7 @@ var SortTracker = class SortTracker {
34887
35282
  dx: 0,
34888
35283
  dy: 0
34889
35284
  },
35285
+ emitIntervalMs: 0,
34890
35286
  lost: false,
34891
35287
  lostAt: 0,
34892
35288
  resurrectable: true,
@@ -34907,7 +35303,10 @@ var SortTracker = class SortTracker {
34907
35303
  bbox: t.bbox,
34908
35304
  trackId: t.id,
34909
35305
  trackAge: t.hits,
34910
- velocity: t.velocity,
35306
+ velocity: {
35307
+ dx: t.velocity.dx * t.emitIntervalMs,
35308
+ dy: t.velocity.dy * t.emitIntervalMs
35309
+ },
34911
35310
  path: [...t.path],
34912
35311
  matchedThisFrame: t.lastSeen === timestamp,
34913
35312
  ...scene !== void 0 ? { resurrectionScene: scene } : {}
@@ -34935,6 +35334,7 @@ var SortTracker = class SortTracker {
34935
35334
  this.lostTracks = [];
34936
35335
  this.dupStreaks.clear();
34937
35336
  this.bindSampler.reset();
35337
+ this.spawnBesideSampler.reset();
34938
35338
  }
34939
35339
  };
34940
35340
  //#endregion
@@ -35369,6 +35769,12 @@ var FrameProcessor = class {
35369
35769
  setBindObserver(observer) {
35370
35770
  this.tracker.setBindObserver(observer);
35371
35771
  }
35772
+ /** Wire (or clear) the tracker's fragmentation sink — a new id minted beside a
35773
+ * live coasting track that could have claimed it (D399). Same `deviceId`
35774
+ * contract as the two sinks above. */
35775
+ setSpawnBesideCoastingObserver(observer) {
35776
+ this.tracker.setSpawnBesideCoastingObserver(observer);
35777
+ }
35372
35778
  /** Forget a track in the underlying tracker (used when the addon promotes it
35373
35779
  * to a stationary-object registry entry). */
35374
35780
  dropTrack(trackId) {
@@ -35965,7 +36371,11 @@ function promoteSemanticBestFrame(deps, input) {
35965
36371
  W: frameWidth,
35966
36372
  H: frameHeight
35967
36373
  }),
35968
- semantic: true
36374
+ semantic: true,
36375
+ canvasFill: canvasFillFraction(bbox, {
36376
+ W: frameWidth,
36377
+ H: frameHeight
36378
+ })
35969
36379
  })) {
35970
36380
  deps.logger.debug("best-shot: semantic frame did not beat the held peak", {
35971
36381
  tags: { deviceId },
@@ -36509,9 +36919,15 @@ function decideLastFramePromotion(media) {
36509
36919
  snapshotKey: newestKeyFrameSmall.key,
36510
36920
  snapshotTimestamp: newestKeyFrameSmall.timestamp
36511
36921
  };
36512
- return { promote: false };
36922
+ return {
36923
+ promote: false,
36924
+ declinedBecause: lastFrame === void 0 ? "no-key-frame" : "no-source"
36925
+ };
36513
36926
  }
36514
- if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return { promote: false };
36927
+ if (lastFrame !== void 0 && newestSnapshot.timestamp <= lastFrame.timestamp) return {
36928
+ promote: false,
36929
+ declinedBecause: "already-last"
36930
+ };
36515
36931
  return {
36516
36932
  promote: true,
36517
36933
  mode: "move",
@@ -36558,17 +36974,28 @@ function averageBboxDiagonal(boxes) {
36558
36974
  //#endregion
36559
36975
  //#region src/pipeline-analytics/pipeline/track-close.ts
36560
36976
  /**
36561
- * Compose the persisted `previewMissReason` (D276/B2). One line, greppable
36562
- * tokens: the derive's own miss branch (or what tile stands in), then the
36563
- * capture-side failure with its count. Examples:
36564
- * `no-key-frame capture=keyframe:native-missx4`
36977
+ * Compose the persisted `previewMissReason` (D276/B2, extended by D400). One
36978
+ * line, greppable tokens: the derive's own miss branch (or what tile stands
36979
+ * in), then whether the NATIVE KEY FRAME landed, then the capture-side failure
36980
+ * with its count. Examples:
36981
+ * `no-key-frame no-key-frame-at-close capture=keyframe:native-missx4`
36565
36982
  * `derive-returned-null tile=standin`
36566
- * `no-key-frame tile=raster capture=thumbnail:overflowx12`
36567
- */
36568
- function composePreviewMissReason(tile, deriveMiss, captureMiss) {
36983
+ * `native-tile tile=native no-key-frame-at-close`
36984
+ *
36985
+ * **Why the key-frame token (D400).** The reason used to be written ONLY when
36986
+ * the best-shot TILE ranked below native, so a track whose thumbnail landed
36987
+ * natively while its key frame missed carried no reason at all — three of the
36988
+ * four `MISSING_KEYFRAME` tracks in the 2026-09-07 audit were exactly that, and
36989
+ * the operator's "niente ultimo" had no cause anywhere on the row. The key
36990
+ * frame IS the native best-shot FRAME, and its absence is the head of the
36991
+ * cascade (`keyFrame ⇒ keyFrameSmall ⇒ the lastFrame copy source`), so it
36992
+ * belongs on this line whatever the tile ended up being.
36993
+ */
36994
+ function composePreviewMissReason(tile, deriveMiss, captureMiss, keyFrameLanded = true) {
36569
36995
  const parts = [];
36570
36996
  parts.push(deriveMiss ?? (tile === "none" ? "no-thumbnail" : `${tile}-tile`));
36571
36997
  if (tile !== "none") parts.push(`tile=${tile}`);
36998
+ if (!keyFrameLanded && deriveMiss !== "no-key-frame") parts.push("no-key-frame-at-close");
36572
36999
  if (captureMiss !== void 0) parts.push(`capture=${captureMiss.reason}x${captureMiss.count}`);
36573
37000
  return parts.join(" ");
36574
37001
  }
@@ -36664,9 +37091,10 @@ var TrackCloser = class {
36664
37091
  await this.commitRasterFallback(t, closure.rasterFallback.jpeg, closure.rasterFallback.timestamp);
36665
37092
  tile = "raster";
36666
37093
  }
36667
- if (tile !== "native") {
37094
+ const keyFrameLanded = ownedMedia.some((m) => m.kind === "keyFrame");
37095
+ if (tile !== "native" || !keyFrameLanded) {
36668
37096
  const captureMiss = this.deps.previewCaptureMiss?.(t.trackId);
36669
- const reason = composePreviewMissReason(tile, deriveMiss, captureMiss);
37097
+ const reason = composePreviewMissReason(tile, deriveMiss, captureMiss, keyFrameLanded);
36670
37098
  if (this.deps.setPreviewMiss !== void 0) try {
36671
37099
  await this.deps.setPreviewMiss(t.deviceId, t.trackId, reason);
36672
37100
  } catch (err) {
@@ -36679,8 +37107,8 @@ var TrackCloser = class {
36679
37107
  }
36680
37108
  });
36681
37109
  }
36682
- const bestSeen = this.deps.residents.bestSeenBbox(t.trackId);
36683
- this.deps.recordPendingPreviewUpgrade?.({
37110
+ const bestSeen = tile === "native" ? void 0 : this.deps.residents.bestSeenBbox(t.trackId);
37111
+ if (tile !== "native") this.deps.recordPendingPreviewUpgrade?.({
36684
37112
  deviceId: t.deviceId,
36685
37113
  trackId: t.trackId,
36686
37114
  ...bestSeen !== void 0 ? { bestSeen: {
@@ -36949,9 +37377,32 @@ var TrackCloser = class {
36949
37377
  */
36950
37378
  async maybePromoteLastFrame(t, ownedMedia) {
36951
37379
  const promotion = decideLastFramePromotion(ownedMedia);
36952
- if (!promotion.promote) return;
37380
+ if (!promotion.promote) {
37381
+ if (promotion.declinedBecause !== "already-last") this.deps.logger.warn("no lastFrame at close", {
37382
+ tags: { deviceId: t.deviceId },
37383
+ meta: {
37384
+ trackId: t.trackId,
37385
+ declinedBecause: promotion.declinedBecause,
37386
+ hasKeyFrame: ownedMedia.some((m) => m.kind === "keyFrame"),
37387
+ hasKeyFrameSmall: ownedMedia.some((m) => m.kind === "keyFrameSmall"),
37388
+ snapshots: ownedMedia.filter((m) => m.kind === "snapshot").length
37389
+ }
37390
+ });
37391
+ return;
37392
+ }
36953
37393
  const snapshot = ownedMedia.find((m) => m.key === promotion.snapshotKey);
36954
- if (!snapshot) return;
37394
+ if (!snapshot) {
37395
+ this.deps.logger.warn("no lastFrame at close", {
37396
+ tags: { deviceId: t.deviceId },
37397
+ meta: {
37398
+ trackId: t.trackId,
37399
+ declinedBecause: "promotion-source-vanished",
37400
+ sourceKey: promotion.snapshotKey,
37401
+ mode: promotion.mode
37402
+ }
37403
+ });
37404
+ return;
37405
+ }
36955
37406
  try {
36956
37407
  await this.deps.mediaStore()?.promoteToLastFrame({
36957
37408
  deviceId: t.deviceId,
@@ -37119,9 +37570,15 @@ var BestDetectionTracker = class {
37119
37570
  * `best`. Absent = the caller does not measure it → contained tier
37120
37571
  * (legacy behaviour). */
37121
37572
  squareFraction = /* @__PURE__ */ new Map();
37573
+ /** Held peak's DELIVERED-CANVAS fill fraction (0..1), PARALLEL to `best`.
37574
+ * Absent = the caller does not measure it → neutral (no fill). */
37575
+ canvasFill = /* @__PURE__ */ new Map();
37122
37576
  /** Held peak's SEMANTIC-tier bit (a recognition succeeded on that frame),
37123
37577
  * PARALLEL to `best`. Absent = non-semantic (legacy behaviour). */
37124
37578
  semantic = /* @__PURE__ */ new Map();
37579
+ /** The last comparison run for a track (D400 promotion log). Never read by
37580
+ * the ranking — a pure observation seam. */
37581
+ lastComparison = /* @__PURE__ */ new Map();
37125
37582
  constructor(options = {}) {
37126
37583
  this.hysteresis = options.hysteresis ?? 0;
37127
37584
  this.minGapMs = options.minGapMs ?? 0;
@@ -37141,12 +37598,13 @@ var BestDetectionTracker = class {
37141
37598
  * the classic policy holds: a confidence past the `hysteresis` margin that also
37142
37599
  * respects `minGapMs` wins. On acceptance the held peak advances.
37143
37600
  */
37144
- observe(trackId, confidence, timestamp, edgeClearIn, centerScoreIn, containmentIn, sizeScoreIn, squareFractionIn, semanticIn) {
37601
+ observe(trackId, confidence, timestamp, edgeClearIn, centerScoreIn, containmentIn, sizeScoreIn, squareFractionIn, semanticIn, canvasFillIn) {
37145
37602
  const edgeClear = this.edgeAware ? edgeClearIn : void 0;
37146
37603
  const centerScore = this.edgeAware ? centerScoreIn : void 0;
37147
37604
  const containment = this.edgeAware ? containmentIn : void 0;
37148
37605
  const sizeScore = this.sizeAware ? sizeScoreIn : void 0;
37149
37606
  const squareFraction = this.edgeAware ? squareFractionIn : void 0;
37607
+ const canvasFill = this.edgeAware ? canvasFillIn : void 0;
37150
37608
  const semanticBit = this.semanticAware ? semanticIn : void 0;
37151
37609
  const cur = this.best.get(trackId);
37152
37610
  if (cur === void 0) {
@@ -37159,6 +37617,7 @@ var BestDetectionTracker = class {
37159
37617
  if (containment !== void 0) this.containment.set(trackId, containment);
37160
37618
  if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
37161
37619
  if (squareFraction !== void 0) this.squareFraction.set(trackId, squareFraction);
37620
+ if (canvasFill !== void 0) this.canvasFill.set(trackId, canvasFill);
37162
37621
  if (semanticBit !== void 0) this.semantic.set(trackId, semanticBit);
37163
37622
  return true;
37164
37623
  }
@@ -37168,6 +37627,7 @@ var BestDetectionTracker = class {
37168
37627
  const curContainment = this.containment.get(trackId);
37169
37628
  const curSize = this.sizeScore.get(trackId);
37170
37629
  const curSquareFraction = this.squareFraction.get(trackId);
37630
+ const curCanvasFill = this.canvasFill.get(trackId);
37171
37631
  const curSemantic = this.semantic.get(trackId) ?? false;
37172
37632
  const candSemantic = semanticBit ?? false;
37173
37633
  const held = {
@@ -37177,6 +37637,7 @@ var BestDetectionTracker = class {
37177
37637
  ...curContainment !== void 0 ? { containment: curContainment } : {},
37178
37638
  ...curSize !== void 0 ? { sizeScore: curSize } : {},
37179
37639
  ...curSquareFraction !== void 0 ? { squareFraction: curSquareFraction } : {},
37640
+ ...curCanvasFill !== void 0 ? { canvasFill: curCanvasFill } : {},
37180
37641
  semantic: curSemantic
37181
37642
  };
37182
37643
  const cand = {
@@ -37186,6 +37647,7 @@ var BestDetectionTracker = class {
37186
37647
  ...containment !== void 0 ? { containment } : {},
37187
37648
  ...sizeScore !== void 0 ? { sizeScore } : {},
37188
37649
  ...squareFraction !== void 0 ? { squareFraction } : {},
37650
+ ...canvasFill !== void 0 ? { canvasFill } : {},
37189
37651
  semantic: candSemantic
37190
37652
  };
37191
37653
  const squareTierUpgrade = isSquareContained(cand) && !isSquareContained(held);
@@ -37194,7 +37656,9 @@ var BestDetectionTracker = class {
37194
37656
  const sameSemanticTier = isSemantic(cand) === isSemantic(held);
37195
37657
  const tierUpgrade = squareTierUpgrade || semanticTierUpgrade || sameSquareTier && sameSemanticTier && candClear && !curClear;
37196
37658
  const gapOk = timestamp - cur.atMs >= this.minGapMs || sizeScore !== void 0 && isDecisiveImprovement(held, cand);
37197
- const isNewBest = tierUpgrade ? true : isEdgeAwareNewBest(held, cand, this.hysteresis) && gapOk;
37659
+ const comparison = compareBestFrame(held, cand, this.hysteresis);
37660
+ this.lastComparison.set(trackId, comparison);
37661
+ const isNewBest = tierUpgrade ? true : comparison.accepted && gapOk;
37198
37662
  if (isNewBest) {
37199
37663
  this.best.set(trackId, {
37200
37664
  confidence,
@@ -37205,11 +37669,46 @@ var BestDetectionTracker = class {
37205
37669
  if (containment !== void 0) this.containment.set(trackId, containment);
37206
37670
  if (sizeScore !== void 0) this.sizeScore.set(trackId, sizeScore);
37207
37671
  if (squareFraction !== void 0) this.squareFraction.set(trackId, squareFraction);
37672
+ if (canvasFill !== void 0) this.canvasFill.set(trackId, canvasFill);
37208
37673
  this.semantic.set(trackId, candSemantic);
37209
37674
  }
37210
37675
  return isNewBest;
37211
37676
  }
37212
37677
  /**
37678
+ * The FULL candidate shape of the track's held peak — everything the ranking
37679
+ * saw, not just `{confidence, atMs}`. Read BEFORE `observe` so a promotion
37680
+ * attempt can be logged against the peak it actually competed with (D400).
37681
+ * Undefined when the track holds no peak (the first sighting).
37682
+ */
37683
+ heldCandidate(trackId) {
37684
+ const cur = this.best.get(trackId);
37685
+ if (cur === void 0) return void 0;
37686
+ const centerScore = this.centerScore.get(trackId);
37687
+ const containment = this.containment.get(trackId);
37688
+ const sizeScore = this.sizeScore.get(trackId);
37689
+ const squareFraction = this.squareFraction.get(trackId);
37690
+ const canvasFill = this.canvasFill.get(trackId);
37691
+ return {
37692
+ confidence: cur.confidence,
37693
+ edgeClear: this.edgeClear.get(trackId) ?? true,
37694
+ ...centerScore !== void 0 ? { centerScore } : {},
37695
+ ...containment !== void 0 ? { containment } : {},
37696
+ ...sizeScore !== void 0 ? { sizeScore } : {},
37697
+ ...squareFraction !== void 0 ? { squareFraction } : {},
37698
+ ...canvasFill !== void 0 ? { canvasFill } : {},
37699
+ semantic: this.semantic.get(trackId) ?? false
37700
+ };
37701
+ }
37702
+ /**
37703
+ * The comparison the LAST {@link observe} ran for this track — which term
37704
+ * decided, and both effective scores. Undefined on a first sighting (nothing
37705
+ * was compared) and after {@link delete}. Feeds the promotion log line; it is
37706
+ * never a ranking input.
37707
+ */
37708
+ lastComparisonFor(trackId) {
37709
+ return this.lastComparison.get(trackId);
37710
+ }
37711
+ /**
37213
37712
  * Stamp the HELD peak as semantic — the attribution-time re-score for a
37214
37713
  * recognition that arrived AFTER the frame was ranked (detail results are
37215
37714
  * async). Use when the recognition's source frame IS the held peak: the
@@ -37235,7 +37734,9 @@ var BestDetectionTracker = class {
37235
37734
  this.containment.delete(trackId);
37236
37735
  this.sizeScore.delete(trackId);
37237
37736
  this.squareFraction.delete(trackId);
37737
+ this.canvasFill.delete(trackId);
37238
37738
  this.semantic.delete(trackId);
37739
+ this.lastComparison.delete(trackId);
37239
37740
  }
37240
37741
  clear() {
37241
37742
  this.best.clear();
@@ -37244,7 +37745,9 @@ var BestDetectionTracker = class {
37244
37745
  this.containment.clear();
37245
37746
  this.sizeScore.clear();
37246
37747
  this.squareFraction.clear();
37748
+ this.canvasFill.clear();
37247
37749
  this.semantic.clear();
37750
+ this.lastComparison.clear();
37248
37751
  }
37249
37752
  };
37250
37753
  //#endregion
@@ -37506,9 +38009,29 @@ var TrackResidentState = class {
37506
38009
  }
37507
38010
  /** Record a detection for the track's best-FRAME ranking (edge-aware §5
37508
38011
  * policy). Returns true when it becomes the track's new best. */
37509
- observeBestFrame(deviceId, trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic) {
38012
+ observeBestFrame(deviceId, trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic, canvasFill) {
37510
38013
  this.open(deviceId, trackId);
37511
- return this.bestFrameTracker.observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic);
38014
+ return this.bestFrameTracker.observe(trackId, confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, semantic, canvasFill);
38015
+ }
38016
+ /** D400: frame instant of the track's last logged promotion REFUSAL
38017
+ * (undefined = none yet). */
38018
+ bestShotRefusalLoggedAt(trackId) {
38019
+ return this.residents.get(trackId)?.bestShotRefusalLoggedAt;
38020
+ }
38021
+ /** Stamp the track's last promotion-refusal log instant. */
38022
+ noteBestShotRefusalLogged(deviceId, trackId, timestamp) {
38023
+ this.ensure(deviceId, trackId).bestShotRefusalLoggedAt = timestamp;
38024
+ }
38025
+ /** The FULL held best-frame candidate (everything the ranking saw). Read
38026
+ * BEFORE `observeBestFrame` to log a promotion attempt against the peak it
38027
+ * competed with (D400). */
38028
+ bestFrameHeld(trackId) {
38029
+ return this.bestFrameTracker.heldCandidate(trackId);
38030
+ }
38031
+ /** Which term decided the LAST best-frame comparison for the track (D400).
38032
+ * Undefined on a first sighting. */
38033
+ bestFrameLastComparison(trackId) {
38034
+ return this.bestFrameTracker.lastComparisonFor(trackId);
37512
38035
  }
37513
38036
  /** The held best-frame peak for a track (undefined if never observed). */
37514
38037
  bestFramePeak(trackId) {
@@ -39702,6 +40225,24 @@ var TRACK_CLOSE_REASONS = ["stationary-promotion"];
39702
40225
  function parseCloseReason(value) {
39703
40226
  return TRACK_CLOSE_REASONS.find((reason) => reason === value);
39704
40227
  }
40228
+ /**
40229
+ * `positions` in TIME order (D399).
40230
+ *
40231
+ * The array is APPENDED in arrival order and the two detector passes interleave,
40232
+ * so it is not the same thing: seven of the fourteen tracks the 2026-09-08 audit
40233
+ * read had at least one position whose timestamp preceded its predecessor's, and
40234
+ * a consumer that draws the array as a polyline draws a path crossing back on
40235
+ * itself. Everything that reasons about a first, a last, or a velocity was
40236
+ * reading them as stored.
40237
+ *
40238
+ * Immutable — returns a new array; `toSorted` is stable, so positions sharing a
40239
+ * timestamp keep their arrival order relative to each other. Applied at the READ
40240
+ * boundaries only: the write path keeps arrival order on purpose, because that
40241
+ * is a fact about delivery and `totalDistance` has always been accumulated on it.
40242
+ */
40243
+ function positionsInTimeOrder(positions) {
40244
+ return positions.toSorted((a, b) => a.timestamp - b.timestamp);
40245
+ }
39705
40246
  var DEFAULT_CONFIG = {
39706
40247
  ttlMs: 3e4,
39707
40248
  maxPositionHistory: 300,
@@ -40068,7 +40609,7 @@ function cloneTrack(t) {
40068
40609
  subLabelMeta: t.subLabelMeta,
40069
40610
  firstSeen: t.firstSeen,
40070
40611
  lastSeen: t.lastSeen,
40071
- positions: t.positions.map((p) => ({
40612
+ positions: positionsInTimeOrder(t.positions).map((p) => ({
40072
40613
  ...p,
40073
40614
  bbox: { ...p.bbox }
40074
40615
  })),
@@ -41661,7 +42202,7 @@ var TrackStore = class {
41661
42202
  */
41662
42203
  const storedObservations = data["observations"];
41663
42204
  const observations = typeof storedObservations === "number" ? storedObservations : slim ? void 0 : storedPositions.length;
41664
- const positions = storedPositions;
42205
+ const positions = positionsInTimeOrder(storedPositions);
41665
42206
  const snapshots = slim ? [] : data["snapshots"] ?? [];
41666
42207
  const zones = data["zonesVisited"] ?? [];
41667
42208
  const classes = data["classes"];
@@ -71373,7 +71914,9 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71373
71914
  x: center.x,
71374
71915
  y: center.y,
71375
71916
  timestamp: result.timestamp,
71376
- bbox: { ...t.bbox }
71917
+ bbox: { ...t.bbox },
71918
+ source: t.nativeRecovery !== void 0 ? "recovery" : "frame",
71919
+ score: t.confidence
71377
71920
  },
71378
71921
  zones: t.zones,
71379
71922
  state: t.state
@@ -71866,6 +72409,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71866
72409
  mediaKey: s.mediaKey
71867
72410
  });
71868
72411
  for (const trackId of res.thumbnailTrackIds) this.residents.markThumbnailLanded(deviceId, trackId);
72412
+ const landedThumbnails = new Set(res.thumbnailTrackIds);
72413
+ for (const target of snapshotTargets) {
72414
+ if (!target.bestThumbnail || landedThumbnails.has(target.trackId)) continue;
72415
+ log.warn("best shot capture did not land", {
72416
+ tags: { deviceId },
72417
+ meta: {
72418
+ trackId: target.trackId,
72419
+ timestamp: target.timestamp,
72420
+ keyFrameRequested: target.keyFrame !== "skip"
72421
+ }
72422
+ });
72423
+ }
71869
72424
  for (const trackId of res.firstFrameTrackIds) {
71870
72425
  this.residents.clearFirstFramePending(trackId);
71871
72426
  this.residents.markFirstFrameLanded(deviceId, trackId);
@@ -72906,7 +73461,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72906
73461
  const ctx = this.ctxIfReady;
72907
73462
  if (ctx === null) return;
72908
73463
  promoteSemanticBestFrame({
72909
- 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),
73464
+ 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),
72910
73465
  recordBestSeenBbox: (deviceId, trackId, bbox) => this.residents.recordBestSeenBbox(deviceId, trackId, bbox),
72911
73466
  captureBestShot: (request) => this.captureSemanticBestShot(request),
72912
73467
  logger: ctx.logger
@@ -73111,13 +73666,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73111
73666
  W: frameWidth,
73112
73667
  H: frameHeight
73113
73668
  });
73669
+ const canvasFill = canvasFillFraction(t.bbox, {
73670
+ W: frameWidth,
73671
+ H: frameHeight
73672
+ });
73114
73673
  const firstBox = this.trackStore?.peekActive(t.trackId)?.positions[0]?.bbox;
73115
- const isNewBest = firstBox !== void 0 && thumbnailWouldShowParkedNotSubject({
73674
+ const skipParkedThumb = firstBox !== void 0 && thumbnailWouldShowParkedNotSubject({
73116
73675
  currentBbox: t.bbox,
73117
73676
  firstBbox: firstBox,
73118
73677
  className: t.className,
73119
73678
  entries: this.stationaryRegistry?.listViews(deviceId) ?? []
73120
- }) ? false : this.residents.observeBestFrame(deviceId, t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, false);
73679
+ });
73680
+ const heldBest = this.residents.bestFrameHeld(t.trackId);
73681
+ const isNewBest = skipParkedThumb ? false : this.residents.observeBestFrame(deviceId, t.trackId, t.confidence, timestamp, edgeClear, centerScore, containment, sizeScore, squareFraction, false, canvasFill);
73682
+ const comparison = skipParkedThumb ? void 0 : this.residents.bestFrameLastComparison(t.trackId);
73121
73683
  this.maybeEmitTrackLifecycleUpdate(deviceId, t, timestamp, isNewBest);
73122
73684
  const plan = planPeriodicMedia({
73123
73685
  debugMediaEnabled: media.debugMediaEnabled,
@@ -73150,6 +73712,46 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73150
73712
  state: this.residents.keyFrameCapture(t.trackId),
73151
73713
  now: timestamp
73152
73714
  }).kind;
73715
+ const suppressedBy = (() => {
73716
+ if (skipParkedThumb) return "parked-thumbnail";
73717
+ if (!isNewBest || bestThumbnail) return void 0;
73718
+ if (!plan.bestThumbnail) return "not-planned";
73719
+ if (!plausibleBox) return "implausible-box";
73720
+ if (!retryBoxPlausible) return "retry-box-collapsed";
73721
+ return "capture-in-flight";
73722
+ })();
73723
+ const promotionAttempt = {
73724
+ trackId: t.trackId,
73725
+ className: t.className,
73726
+ timestamp,
73727
+ candidate: {
73728
+ confidence: t.confidence,
73729
+ edgeClear,
73730
+ centerScore,
73731
+ containment,
73732
+ sizeScore,
73733
+ squareFraction,
73734
+ canvasFill,
73735
+ semantic: false
73736
+ },
73737
+ ...heldBest !== void 0 ? { held: heldBest } : {},
73738
+ ...comparison !== void 0 ? { comparison } : {},
73739
+ accepted: isNewBest,
73740
+ ...suppressedBy !== void 0 ? { suppressedBy } : {},
73741
+ thumbnailRequested: bestThumbnail,
73742
+ keyFrameRequested: keyFrame !== "skip"
73743
+ };
73744
+ const refusalLoggedAt = this.residents.bestShotRefusalLoggedAt(t.trackId);
73745
+ if (shouldLogPromotionAttempt(promotionAttempt, {
73746
+ thumbnailLanded: this.residents.isThumbnailLanded(t.trackId),
73747
+ ...refusalLoggedAt !== void 0 ? { lastRefusalLogAtMs: refusalLoggedAt } : {}
73748
+ })) {
73749
+ if (!isNewBest) this.residents.noteBestShotRefusalLogged(deviceId, t.trackId, timestamp);
73750
+ this.ctx.logger.info("best shot promotion", {
73751
+ tags: { deviceId },
73752
+ meta: { ...bestShotPromotionMeta(promotionAttempt) }
73753
+ });
73754
+ }
73153
73755
  if (!plan.appendSnapshot && !rollingLastFrame && !bestThumbnail && keyFrame === "skip") continue;
73154
73756
  targets.push({
73155
73757
  trackId: t.trackId,
@@ -74501,6 +75103,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74501
75103
  }
74502
75104
  });
74503
75105
  });
75106
+ p.setSpawnBesideCoastingObserver((record) => {
75107
+ this.ctx.logger.info("tracker: new id minted beside a live coasting track", {
75108
+ tags: { deviceId },
75109
+ meta: {
75110
+ source,
75111
+ ...record
75112
+ }
75113
+ });
75114
+ });
74504
75115
  p.setTrackLabelState((trackId) => {
74505
75116
  const active = this.trackStore?.getActiveByTrack(trackId);
74506
75117
  return active !== null && active !== void 0 ? {