@camstack/addon-post-analysis 1.2.210 → 1.2.212

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 assertTimelapseCadences, $t as string, A as NcScheduleSchema, At as subKindsOf, Bt as hydrateSchema, Ct as plateGalleryCapability, D as NcRulePatchSchema, E as NcRuleInputSchema, Et as resolveLocationMode, F as NcTaxonomySchema, Ft as errMsg$1, G as TIMELAPSE_DENSE_FLOOR_SEC, Gt as array, H as SCENE_DEFAULT_UNCOVERED_POLICY, Ht as nodePin, I as OpsLogEntrySchema, It as BaseAddon, J as TimelapseRuleSchema, Jt as literal, K as TimelapseRuleInputSchema, Kt as boolean, Lt as CamProfileSchema, M as NcSnoozeSchema, Mt as vectorDimFromBase64, N as NcSnoozeSuppressedSchema, Nt as videoclipsCapability, O as NcRuleSchema, Ot as sceneMonitorCapability, P as NcSystemEventKindSchema, Pt as zoneAnalyticsCapability, Q as alarmPanelCapability, Qt as record, R as RECORDING_EXPORT_MAX_READ_BYTES, Rt as DeviceType, S as NC_DEFAULT_SNOOZE_MINUTES, St as pipelineAnalyticsCapability, T as NcConditionDescriptorSchema, Tt as readTimelapseGeneratedAt, U as SCENE_DIVERGED, Ut as sleep$1, V as SCENE_DEFAULT_ANCHOR_THRESHOLD, Vt as isDeviceScopedCap, W as SceneMonitorSchema, Wt as _enum, X as VISIT_MERGE_GAP_MS, Xt as object, Y as TrackSourceSchema, Yt as number, Z as addonWidgetsSourceCapability, Zt as partialRecord, _ as MACRO_LABELS, _t as kebabToCamel, a as COCO_TO_MACRO, at as defineCustomActions, b as NC_ALARM_SYSTEM_EVENT_KINDS, ct as encodeVectorBase64, d as DeclaredDevices, dt as faceGalleryCapability, en as unknown, et as audioMetricsCapability, f as EVENT_KIND_BY_CAP, ft as failureContributionCapability, g as LabelAttributionSchema, gt as isSourceCap, h as FailureCounters, ht as isScheduleActive, i as CLUSTER_MODEL_SCOPED_STEPS, it as customAction, j as NcSnoozeInputSchema, jt as systemEventFilterApplies, k as NcRuleTargetSchema, kt as storageOccupancyCapability, l as DETECTION_MACRO_CLASSES, lt as evaluateSensorEdge, m as FULL_IMAGE_BBOX, mt as isDetectionMacroClass, n as ArchivedDebugNoteSchema, nt as buildEventKindDescriptor, o as DEFAULT_EVENT_COLOR, ot as deriveRecordingMode, p as EVENT_PAD_MS, q as TimelapseRulePatchSchema, qt as discriminatedUnion, r as BaseDevice, rt as cosineSimilarity$1, t as AUDIO_MACRO_LABELS, tn as EventCategory, tt as audioModeOf, ut as evictionPolicyOfLocation, v as MAX_ARCHIVED_DEBUG_NOTES, vt as mayWriteToLocation, w as NC_TAXONOMY, wt as readDeviceStateFrom, x as NC_CONDITION_CATALOG, xt as pickClusterStepModels, y as MediaFileKindEnum, yt as notificationRulesCapability, z as RetrainStatusSchema, zt as createEvent } from "../dist-D2rFaMex.mjs";
1
+ import { $ as VISIT_MERGE_GAP_MS, $t as object, A as NcRulePatchSchema, Bt as CamProfileSchema, C as NC_ALARM_SYSTEM_EVENT_KINDS, D as NC_TAXONOMY, Dt as readDeviceStateFrom, Et as plateGalleryCapability, F as NcSnoozeSchema, Ft as vectorDimFromBase64, G as SCENE_DEFAULT_UNCOVERED_POLICY, Gt as nodePin, H as RetrainStatusSchema, Ht as createEvent, I as NcSnoozeSuppressedSchema, It as videoclipsCapability, J as TIMELAPSE_DENSE_FLOOR_SEC, Jt as array, K as SCENE_DIVERGED, Kt as sleep$1, L as NcSystemEventKindSchema, Lt as zoneAnalyticsCapability, M as NcRuleTargetSchema, Mt as storageOccupancyCapability, N as NcScheduleSchema, Nt as subKindsOf, O as NcConditionDescriptorSchema, Ot as readTimelapseGeneratedAt, P as NcSnoozeInputSchema, Pt as systemEventFilterApplies, Q as TrackSourceSchema, Qt as number, R as NcTaxonomySchema, Rt as errMsg$1, S as MediaFileKindEnum, St as notificationRulesCapability, T as NC_DEFAULT_SNOOZE_MINUTES, Tt as pipelineAnalyticsCapability, Ut as hydrateSchema, V as RECORDING_EXPORT_MAX_READ_BYTES, Vt as DeviceType, W as SCENE_DEFAULT_ANCHOR_THRESHOLD, Wt as isDeviceScopedCap, X as TimelapseRulePatchSchema, Xt as discriminatedUnion, Y as TimelapseRuleInputSchema, Yt as boolean, Z as TimelapseRuleSchema, Zt as literal, _ as LabelAttributionSchema, _t as isDetectionMacroClass, a as CLUSTER_MODEL_SCOPED_STEPS, at as buildEventKindDescriptor, b as MAX_BIRTH_DECISION_RECORDS, bt as kebabToCamel, ct as defineCustomActions, dt as encodeVectorBase64, en as partialRecord, et as addonWidgetsSourceCapability, f as DeclaredDevices, ft as evaluateSensorEdge, g as FailureCounters, h as FULL_IMAGE_BBOX, ht as failureContributionCapability, i as BirthDecisionRecordSchema, in as EventCategory, it as audioModeOf, j as NcRuleSchema, jt as sceneMonitorCapability, k as NcRuleInputSchema, kt as resolveLocationMode, lt as deriveRecordingMode, m as EVENT_PAD_MS, mt as faceGalleryCapability, n as ArchivedDebugNoteSchema, nn as string, nt as assertTimelapseCadences, o as COCO_TO_MACRO, ot as cosineSimilarity$1, p as EVENT_KIND_BY_CAP, pt as evictionPolicyOfLocation, q as SceneMonitorSchema, qt as _enum, r as BaseDevice, rn as unknown, rt as audioMetricsCapability, s as DEFAULT_EVENT_COLOR, st as customAction, t as AUDIO_MACRO_LABELS, tn as record, tt as alarmPanelCapability, u as DETECTION_MACRO_CLASSES, v as MACRO_LABELS, vt as isScheduleActive, w as NC_CONDITION_CATALOG, wt as pickClusterStepModels, xt as mayWriteToLocation, y as MAX_ARCHIVED_DEBUG_NOTES, yt as isSourceCap, z as OpsLogEntrySchema, zt as BaseAddon } from "../dist-GQbzNwJB.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";
@@ -34544,6 +34544,37 @@ var TWO_WHEELERS = new Set([
34544
34544
  function intersection(a, b) {
34545
34545
  return Math.max(0, Math.min(a.x + a.w, b.x + b.w) - Math.max(a.x, b.x)) * Math.max(0, Math.min(a.y + a.h, b.y + b.h) - Math.max(a.y, b.y));
34546
34546
  }
34547
+ /**
34548
+ * The FOLDED subject's box: the union of the rider and the machine.
34549
+ *
34550
+ * The fold's whole claim is that the two halves are ONE subject. Until
34551
+ * 2026-09-08 the box that survived the fold was the MACHINE's alone, so every
34552
+ * consumer that cuts pixels from `TrackedDetectionOut.bbox` — the best-shot
34553
+ * thumbnail, `wideCentralSquareLayout`'s central square, the close-time
34554
+ * keyFrame→thumbnail derive — framed a motorcycle with its rider's head cut
34555
+ * off. Operator, device 617, tracks `7f50622b` and `9cbc4791`, three minutes
34556
+ * apart: "motocicli e veicoli a due ruote devono includere il conducente nel
34557
+ * keyframe" (D407).
34558
+ *
34559
+ * This is the ONE place the folded subject's rectangle is formed, and it is
34560
+ * formed BEFORE the tracker — so no crop path derives a second rectangle
34561
+ * (D52 stays intact: `deriveDetailCropRect` is still the only detail-crop
34562
+ * derivation, and it now receives a subject box that already holds the rider).
34563
+ * Pure geometry: a strict union, never a pad — the fold widens the subject, it
34564
+ * never moves it.
34565
+ */
34566
+ function riderSubjectBox(person, vehicle) {
34567
+ const x = Math.min(person.x, vehicle.x);
34568
+ const y = Math.min(person.y, vehicle.y);
34569
+ const right = Math.max(person.x + person.w, vehicle.x + vehicle.w);
34570
+ const bottom = Math.max(person.y + person.h, vehicle.y + vehicle.h);
34571
+ return {
34572
+ x,
34573
+ y,
34574
+ w: right - x,
34575
+ h: bottom - y
34576
+ };
34577
+ }
34547
34578
  /** True when `vehicle` is a two-wheeler that `person` is riding. */
34548
34579
  function isRiderPair(person, vehicle) {
34549
34580
  if (person.macroClass !== "person") return false;
@@ -36310,17 +36341,64 @@ var FrameProcessor = class {
36310
36341
  const foldByVehicleBbox = /* @__PURE__ */ new Map();
36311
36342
  if (riderPairs.length > 0) {
36312
36343
  const riderIdx = new Set(riderPairs.map((p) => Number(p.personId)));
36344
+ /**
36345
+ * Move every bbox-OBJECT-keyed side table from the machine's box to the
36346
+ * folded subject's box (D407).
36347
+ *
36348
+ * This file carries frame-local facts across the tracker by bbox-object
36349
+ * REFERENCE (`labelsByBbox`, `embeddingByBbox`, `maskByBbox`, …); the
36350
+ * tracker then assigns `track.bbox = det.bbox`, and the emit block below
36351
+ * reads every one of those maps with `td.bbox`. Widening the subject
36352
+ * therefore means the widened box must inherit the machine's entries —
36353
+ * otherwise a folded rider silently loses its vehicle-classifier label
36354
+ * ("Motorcycle"), its mask, its plate read and its source id. One place,
36355
+ * exhaustive, next to the maps it moves.
36356
+ */
36357
+ const adoptSubjectBox = (from, to) => {
36358
+ const labels = labelsByBbox.get(from);
36359
+ if (labels !== void 0) labelsByBbox.set(to, labels);
36360
+ const tier = originalClassTierByBbox.get(from);
36361
+ if (tier !== void 0) originalClassTierByBbox.set(to, tier);
36362
+ const embedding = embeddingByBbox.get(from);
36363
+ if (embedding !== void 0) embeddingByBbox.set(to, embedding);
36364
+ const mask = maskByBbox.get(from);
36365
+ if (mask !== void 0) maskByBbox.set(to, mask);
36366
+ const plate = plateByBbox.get(from);
36367
+ if (plate !== void 0) plateByBbox.set(to, plate);
36368
+ const faceBbox = faceBboxByBbox.get(from);
36369
+ if (faceBbox !== void 0) faceBboxByBbox.set(to, faceBbox);
36370
+ const faceSize = nativeFaceSizeByBbox.get(from);
36371
+ if (faceSize !== void 0) nativeFaceSizeByBbox.set(to, faceSize);
36372
+ const alignedCrop = faceAlignedCropByBbox.get(from);
36373
+ if (alignedCrop !== void 0) faceAlignedCropByBbox.set(to, alignedCrop);
36374
+ const sourceId = sourceIdByBbox.get(from);
36375
+ if (sourceId !== void 0) {
36376
+ sourceIdByBbox.set(to, sourceId);
36377
+ firstLevelBboxById.set(sourceId, to);
36378
+ }
36379
+ };
36380
+ /** Folded vehicles, by their index in `filteredDetections`. */
36381
+ const foldedVehicles = /* @__PURE__ */ new Map();
36313
36382
  for (const p of riderPairs) {
36314
- const vehicle = filteredDetections[Number(p.vehicleId)];
36315
- if (vehicle === void 0) continue;
36316
- foldByVehicleBbox.set(vehicle.bbox, {
36383
+ const vehicleIdx = Number(p.vehicleId);
36384
+ const vehicle = filteredDetections[vehicleIdx];
36385
+ const person = filteredDetections[Number(p.personId)];
36386
+ if (vehicle === void 0 || person === void 0) continue;
36387
+ const subjectBox = riderSubjectBox(person.bbox, vehicle.bbox);
36388
+ adoptSubjectBox(vehicle.bbox, subjectBox);
36389
+ const folded = {
36390
+ ...vehicle,
36391
+ bbox: subjectBox
36392
+ };
36393
+ foldedVehicles.set(vehicleIdx, folded);
36394
+ foldByVehicleBbox.set(subjectBox, {
36317
36395
  overlap: p.overlap,
36318
- personScore: filteredDetections[Number(p.personId)]?.score ?? 0,
36396
+ personScore: person.score,
36319
36397
  vehicleScore: vehicle.score,
36320
36398
  vehicleClass: vehicle.originalClass ?? "two-wheeler"
36321
36399
  });
36322
36400
  }
36323
- filteredDetections = filteredDetections.filter((_, i) => !riderIdx.has(i));
36401
+ filteredDetections = filteredDetections.map((d, i) => foldedVehicles.get(i) ?? d).filter((_, i) => !riderIdx.has(i));
36324
36402
  }
36325
36403
  const gate = this.stationaryGate ? this.stationaryGate.filter({
36326
36404
  detections: filteredDetections,
@@ -48994,10 +49072,9 @@ var FaceRecognizer = class {
48994
49072
  margin: settings.margin,
48995
49073
  minIdentitySamples: settings.minIdentitySamples
48996
49074
  }) : /* @__PURE__ */ new Map();
48997
- if (this.deps.onSemanticMatch !== void 0) for (const c of matchCandidates) {
49075
+ if (this.deps.onSemanticFace !== void 0) for (const c of matchCandidates) {
48998
49076
  const match = matches.get(c.trackId);
48999
- if (match === void 0) continue;
49000
- this.deps.onSemanticMatch({
49077
+ this.deps.onSemanticFace({
49001
49078
  deviceId: input.deviceId,
49002
49079
  trackId: c.trackId,
49003
49080
  timestamp: input.timestamp,
@@ -49005,8 +49082,10 @@ var FaceRecognizer = class {
49005
49082
  frameHeight: input.frameHeight,
49006
49083
  bbox: c.bbox,
49007
49084
  confidence: c.confidence,
49008
- matchScore: match.score,
49009
- identityId: match.identityId,
49085
+ ...match !== void 0 ? {
49086
+ matchScore: match.score,
49087
+ identityId: match.identityId
49088
+ } : {},
49010
49089
  ...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
49011
49090
  });
49012
49091
  }
@@ -53279,6 +53358,95 @@ var BirthEvidenceStore = class {
53279
53358
  }
53280
53359
  };
53281
53360
  //#endregion
53361
+ //#region src/pipeline-analytics/pipeline/birth-decision-record.ts
53362
+ /**
53363
+ * birth-decision-record — turn one resolved birth verdict into the row that is
53364
+ * kept (D409).
53365
+ *
53366
+ * PURE, and separated from the store for the usual reason: the two rules worth
53367
+ * getting right — what counts as the fail-open, and when the latency proxy is
53368
+ * allowed to report a number — are decisions, not I/O, and a decision that only
53369
+ * exists inside a SQLite call cannot be tested or argued with.
53370
+ *
53371
+ * MEASUREMENT ONLY. Nothing here gates a birth, changes a verdict, or feeds
53372
+ * back into the frame path. The caller writes the row after the verdict is
53373
+ * already taken.
53374
+ */
53375
+ /**
53376
+ * The verdict as the ledger reports it.
53377
+ *
53378
+ * `exhausted-fallback` is a CONFIRMATION and is counted apart from one, because
53379
+ * a birth nobody ever looked at and a birth measured and passed are the same
53380
+ * row in every existing aggregate — which is what made "the gate stopped
53381
+ * failing open" and "the gate now suppresses everything it cannot measure"
53382
+ * indistinguishable. Exhaustion attached to a SUPPRESSION is meaningless
53383
+ * (exhaustion only ever waves a birth through) and is discarded rather than
53384
+ * inventing a fourth member.
53385
+ */
53386
+ function verdictFor(input) {
53387
+ if (!input.confirmed) return "suppressed";
53388
+ return input.exhaustedFallback ? "exhausted-fallback" : "confirmed";
53389
+ }
53390
+ /**
53391
+ * The latency proxy, and the three ways it declines to produce a number.
53392
+ *
53393
+ * It reports `null` — never a fabricated or clamped value — when there is no
53394
+ * track (a suppressed birth), no motion reference on this runner, or a
53395
+ * reference older than {@link MAX_BIRTH_LATENCY_PROXY_MS}. That last case is
53396
+ * the one that matters: on a busy scene the burst never closes and the onset is
53397
+ * minutes old, and a minutes-long "birth latency" would be read as the
53398
+ * detector's fault when it is the proxy's. When the reference is refused, the
53399
+ * onset and the burst index go with it — reporting an index against a reference
53400
+ * this row does not carry would be an invitation to join them back up.
53401
+ *
53402
+ * A NEGATIVE result is returned as measured. The analyzer's pixel-count floor
53403
+ * can be crossed after the detector's confidence floor for a subject entering
53404
+ * slowly at the frame edge, and clamping that away would delete precisely the
53405
+ * evidence that the proxy is unreliable. See `BirthDecisionRecordSchema` for
53406
+ * the full error bars.
53407
+ */
53408
+ function latencyFor(input, firstSeen) {
53409
+ const onset = input.motionOnsetAtMs;
53410
+ if (firstSeen === null || onset === null) return {
53411
+ motionOnsetAt: null,
53412
+ birthLatencyMs: null,
53413
+ birthIndexInBurst: null
53414
+ };
53415
+ if (firstSeen - onset > 6e4) return {
53416
+ motionOnsetAt: null,
53417
+ birthLatencyMs: null,
53418
+ birthIndexInBurst: null
53419
+ };
53420
+ return {
53421
+ motionOnsetAt: onset,
53422
+ birthLatencyMs: firstSeen - onset,
53423
+ birthIndexInBurst: input.birthIndexInBurst
53424
+ };
53425
+ }
53426
+ /** Build the durable row for one resolved birth decision. */
53427
+ function buildBirthDecisionRecord(input) {
53428
+ const verdict = verdictFor(input);
53429
+ const firstSeen = verdict === "suppressed" ? null : input.firstSeen;
53430
+ const latency = latencyFor(input, firstSeen);
53431
+ return {
53432
+ at: input.decidedAtMs,
53433
+ deviceId: input.deviceId,
53434
+ sourceTrackId: input.trackId,
53435
+ className: input.className,
53436
+ verdict,
53437
+ reason: input.reason ?? null,
53438
+ attempts: input.attempts,
53439
+ deferredForMs: input.deferredForMs,
53440
+ decidedOnCoastedFrame: input.matchedThisFrame === false,
53441
+ birthEvidenceAvailable: input.birthEvidenceAvailable,
53442
+ decidedByBirthEvidence: input.decidedByBirthEvidence,
53443
+ bestScore: input.bestScore ?? null,
53444
+ appliedMinConfidence: input.appliedMinConfidence ?? null,
53445
+ firstSeen,
53446
+ ...latency
53447
+ };
53448
+ }
53449
+ //#endregion
53282
53450
  //#region src/pipeline-analytics/pipeline/deferred-births.ts
53283
53451
  var DeferredBirthRegistry = class {
53284
53452
  byDevice = /* @__PURE__ */ new Map();
@@ -53353,6 +53521,57 @@ var DeferredBirthRegistry = class {
53353
53521
  }
53354
53522
  };
53355
53523
  //#endregion
53524
+ //#region src/pipeline-analytics/pipeline/motion-onset-registry.ts
53525
+ var MotionOnsetRegistry = class {
53526
+ byDevice = /* @__PURE__ */ new Map();
53527
+ /**
53528
+ * Motion went off→on at `atMs`. Opens a new burst, discarding the previous
53529
+ * one's birth count — a new burst is a new subject population.
53530
+ */
53531
+ noteRisingEdge(deviceId, atMs) {
53532
+ this.byDevice.set(deviceId, {
53533
+ onsetAtMs: atMs,
53534
+ births: 0
53535
+ });
53536
+ }
53537
+ /** The open burst's onset, or `null` for a device with no reference. */
53538
+ onsetFor(deviceId) {
53539
+ return this.byDevice.get(deviceId)?.onsetAtMs ?? null;
53540
+ }
53541
+ /**
53542
+ * Count one birth decision against this device's burst and return its 0-based
53543
+ * index — `null` when there is no burst to count it against.
53544
+ *
53545
+ * Index 0 is the only index at which the burst plausibly belongs to the same
53546
+ * subject as the birth; the ledger records the index rather than filtering
53547
+ * here, so the reader can see how often it is not 0.
53548
+ */
53549
+ noteBirth(deviceId) {
53550
+ const burst = this.byDevice.get(deviceId);
53551
+ if (burst === void 0) return null;
53552
+ const index = burst.births;
53553
+ burst.births += 1;
53554
+ return index;
53555
+ }
53556
+ /** Drop a device's reference (device removed / pipeline reset). */
53557
+ forgetDevice(deviceId) {
53558
+ this.byDevice.delete(deviceId);
53559
+ }
53560
+ };
53561
+ //#endregion
53562
+ //#region src/pipeline-analytics/pipeline/detail-semantic-gate.ts
53563
+ /** The detail `className` whose result IS the recognition. */
53564
+ var PLATE_CLASS = "plate";
53565
+ /**
53566
+ * The moment `className`'s detail result earns the semantic tier.
53567
+ *
53568
+ * `recognition` — promote as soon as the result exists (the plate read).
53569
+ * `label-write` — promote only once the track ACCEPTED the label.
53570
+ */
53571
+ function detailSemanticTrigger(className) {
53572
+ return className === PLATE_CLASS ? "recognition" : "label-write";
53573
+ }
53574
+ //#endregion
53356
53575
  //#region src/pipeline-analytics/pipeline/dropout-skip.ts
53357
53576
  function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
53358
53577
  if (!cfg.enabled) return false;
@@ -59868,6 +60087,332 @@ var IdentityStore = class {
59868
60087
  }
59869
60088
  };
59870
60089
  //#endregion
60090
+ //#region src/pipeline-analytics/store/birth-decision-ledger-store.ts
60091
+ /**
60092
+ * BirthDecisionLedgerStore — the durable record of every track-birth verdict
60093
+ * (D409).
60094
+ *
60095
+ * A declared SQL-backed collection, like the events ops-log and the debug-note
60096
+ * archive next door, and for the same reason: pipeline-analytics already owns
60097
+ * SQLite collections, and an undeclared collection crash-loops the runner. MUST
60098
+ * be declared in `onInitialize` before the first write.
60099
+ *
60100
+ * Collection name: pipeline-analytics:birth-decision-ledger
60101
+ *
60102
+ * ── Why a table rather than a log line ─────────────────────────────────────
60103
+ * The gate ALREADY logs every decision. `logs.query` is an in-memory ring that
60104
+ * measured 40 000 entries covering 38 MINUTES of fleet traffic on 2026-09-08 —
60105
+ * so a fourteenth field on that line answers nothing the next morning, and the
60106
+ * questions this data exists for ("what fraction of births on 617 were decided
60107
+ * on a coasted frame, over the week the operator complained about") are all
60108
+ * multi-day. A previous investigation in this repo was blocked for exactly
60109
+ * this.
60110
+ *
60111
+ * ── Why it is safe on the write path ──────────────────────────────────────
60112
+ * ONE INSERT PER BIRTH DECISION. Not per frame, not periodic, and never an
60113
+ * update of an existing row. Measured fleet-wide on the live hub, 2026-09-08:
60114
+ * 31 decided births in 38 minutes = ~49/hour = 0.014 writes/s. The write
60115
+ * amplification the runtime-state guard exists to prevent
60116
+ * (`scripts/check-runtime-state-durability.ts`: 12-19 writes/s rewriting
60117
+ * ~2.6-5 GB/day to maintain 161 KB) is three orders of magnitude above this and
60118
+ * is a different shape entirely — that one is a periodic REWRITE of a slice
60119
+ * whose values do not change. Nothing here advances a clock under an unchanged
60120
+ * value, because every row is new.
60121
+ *
60122
+ * ── Two properties, stated once ───────────────────────────────────────────
60123
+ * - **It is not track-owned.** A SUPPRESSED birth never had a track; a
60124
+ * confirmed one is expected to age out long before this row. The column is
60125
+ * `sourceTrackId` and not `trackId` so the ownership derivation in
60126
+ * `collection-classification.ts` cannot see it and cascade it away.
60127
+ * - **It is bounded by COUNT, not by age.** A miss rate needs a denominator
60128
+ * over whatever window the table holds; a quiet fleet should keep more of
60129
+ * that window, not less. See {@link MAX_BIRTH_DECISION_RECORDS}.
60130
+ *
60131
+ * MEASUREMENT ONLY. Nothing reads this on the frame path; nothing gates on it.
60132
+ */
60133
+ /**
60134
+ * @durable class=ledger owner=pipeline-analytics
60135
+ * write="one row per track-birth VERDICT — appended by the birth loop in
60136
+ * `processFrame` the instant the confirmation gate's verdict resolves
60137
+ * (confirmed / suppressed / exhausted fail-open). Per BIRTH, never per
60138
+ * frame: measured at ~49 rows/hour fleet-wide on 2026-09-08. Append-only,
60139
+ * never updated. BEST-EFFORT — it logs and swallows, because a failed
60140
+ * measurement must not fail the birth it measures."
60141
+ * retention="NOT track retention and NOT an age clock — a miss rate needs
60142
+ * whatever window the table holds, and an age sweep would shorten it on a
60143
+ * quiet fleet exactly when more history is affordable. Bounded by ROW
60144
+ * COUNT (MAX_BIRTH_DECISION_RECORDS, 30000, fleet-wide), oldest `at`
60145
+ * first, trimmed on append. ~6 MB at the ceiling."
60146
+ */
60147
+ var BIRTH_DECISION_LEDGER_COLLECTION = "pipeline-analytics:birth-decision-ledger";
60148
+ var BIRTH_DECISION_LEDGER_COLUMNS = [
60149
+ {
60150
+ name: "id",
60151
+ type: "TEXT",
60152
+ primaryKey: true,
60153
+ notNull: true
60154
+ },
60155
+ {
60156
+ name: "at",
60157
+ type: "INTEGER",
60158
+ notNull: true
60159
+ },
60160
+ {
60161
+ name: "deviceId",
60162
+ type: "INTEGER",
60163
+ notNull: true
60164
+ },
60165
+ {
60166
+ name: "sourceTrackId",
60167
+ type: "TEXT",
60168
+ notNull: true
60169
+ },
60170
+ {
60171
+ name: "className",
60172
+ type: "TEXT",
60173
+ notNull: true
60174
+ },
60175
+ {
60176
+ name: "verdict",
60177
+ type: "TEXT",
60178
+ notNull: true
60179
+ },
60180
+ {
60181
+ name: "reason",
60182
+ type: "TEXT"
60183
+ },
60184
+ {
60185
+ name: "attempts",
60186
+ type: "INTEGER",
60187
+ notNull: true
60188
+ },
60189
+ {
60190
+ name: "deferredForMs",
60191
+ type: "INTEGER",
60192
+ notNull: true
60193
+ },
60194
+ {
60195
+ name: "decidedOnCoastedFrame",
60196
+ type: "BOOLEAN",
60197
+ notNull: true
60198
+ },
60199
+ {
60200
+ name: "birthEvidenceAvailable",
60201
+ type: "BOOLEAN",
60202
+ notNull: true
60203
+ },
60204
+ {
60205
+ name: "decidedByBirthEvidence",
60206
+ type: "BOOLEAN",
60207
+ notNull: true
60208
+ },
60209
+ {
60210
+ name: "bestScore",
60211
+ type: "REAL"
60212
+ },
60213
+ {
60214
+ name: "appliedMinConfidence",
60215
+ type: "REAL"
60216
+ },
60217
+ {
60218
+ name: "firstSeen",
60219
+ type: "INTEGER"
60220
+ },
60221
+ {
60222
+ name: "motionOnsetAt",
60223
+ type: "INTEGER"
60224
+ },
60225
+ {
60226
+ name: "birthLatencyMs",
60227
+ type: "INTEGER"
60228
+ },
60229
+ {
60230
+ name: "birthIndexInBurst",
60231
+ type: "INTEGER"
60232
+ }
60233
+ ];
60234
+ var BIRTH_DECISION_LEDGER_INDEXES = [{
60235
+ name: "idx_birthdecision_at",
60236
+ columns: ["at"]
60237
+ }, {
60238
+ name: "idx_birthdecision_device_at",
60239
+ columns: ["deviceId", "at"]
60240
+ }];
60241
+ var BirthDecisionLedgerStore = class {
60242
+ store;
60243
+ logger;
60244
+ newId;
60245
+ maxRows;
60246
+ trimEvery;
60247
+ /** Rows appended since the last trim — see {@link trim}. */
60248
+ sinceTrim = 0;
60249
+ constructor(deps) {
60250
+ this.store = deps.store;
60251
+ this.logger = deps.logger;
60252
+ this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
60253
+ this.maxRows = deps.maxRows ?? 3e4;
60254
+ this.trimEvery = deps.trimEvery ?? DEFAULT_TRIM_EVERY;
60255
+ }
60256
+ /** The ceiling a store built without an override enforces. */
60257
+ static defaultMaxRows() {
60258
+ return MAX_BIRTH_DECISION_RECORDS;
60259
+ }
60260
+ static async declare(store) {
60261
+ await store.declareCollection.mutate({
60262
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60263
+ columns: [...BIRTH_DECISION_LEDGER_COLUMNS],
60264
+ indexes: [...BIRTH_DECISION_LEDGER_INDEXES]
60265
+ });
60266
+ }
60267
+ /**
60268
+ * Append one birth decision. NEVER THROWS.
60269
+ *
60270
+ * Best-effort, unlike the debug-note archive and deliberately so: that table
60271
+ * holds the only copy of something a PERSON typed, and this one holds a
60272
+ * measurement the system takes ~49 times an hour. A birth must not fail — nor
60273
+ * be delayed by a retry — because bookkeeping could not be written. The
60274
+ * failure is logged with `tags: { deviceId }` so a camera whose rows are
60275
+ * silently missing is attributable rather than an unexplained hole in the
60276
+ * denominator.
60277
+ */
60278
+ async append(draft) {
60279
+ try {
60280
+ const row = BirthDecisionRecordSchema.parse({
60281
+ id: this.newId(),
60282
+ ...draft
60283
+ });
60284
+ await this.store.insert.mutate({
60285
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60286
+ record: {
60287
+ id: row.id,
60288
+ data: {
60289
+ at: row.at,
60290
+ deviceId: row.deviceId,
60291
+ sourceTrackId: row.sourceTrackId,
60292
+ className: row.className,
60293
+ verdict: row.verdict,
60294
+ reason: row.reason,
60295
+ attempts: row.attempts,
60296
+ deferredForMs: row.deferredForMs,
60297
+ decidedOnCoastedFrame: row.decidedOnCoastedFrame,
60298
+ birthEvidenceAvailable: row.birthEvidenceAvailable,
60299
+ decidedByBirthEvidence: row.decidedByBirthEvidence,
60300
+ bestScore: row.bestScore,
60301
+ appliedMinConfidence: row.appliedMinConfidence,
60302
+ firstSeen: row.firstSeen,
60303
+ motionOnsetAt: row.motionOnsetAt,
60304
+ birthLatencyMs: row.birthLatencyMs,
60305
+ birthIndexInBurst: row.birthIndexInBurst
60306
+ }
60307
+ }
60308
+ });
60309
+ this.sinceTrim += 1;
60310
+ await this.trim();
60311
+ } catch (err) {
60312
+ this.logger.warn("birth-decision ledger append failed — this decision is not counted", {
60313
+ tags: { deviceId: draft.deviceId },
60314
+ meta: {
60315
+ trackId: draft.sourceTrackId,
60316
+ verdict: draft.verdict,
60317
+ error: err instanceof Error ? err.message : String(err)
60318
+ }
60319
+ });
60320
+ }
60321
+ }
60322
+ /**
60323
+ * Hold the table at its ceiling, oldest first.
60324
+ *
60325
+ * NOT an age-keyed sweep and must never become one: the boundary is computed
60326
+ * from the POSITION of the excess rows, never from a clock. Rows sharing the
60327
+ * boundary timestamp go together, which at this write rate is a rounding
60328
+ * error against a 30 000-row ceiling.
60329
+ *
60330
+ * AMORTISED. The debug-note archive counts on every append because it writes
60331
+ * a few rows a week; this one writes ~49 an hour, and a `count` round trip per
60332
+ * birth would triple the ledger's own store traffic to answer a question whose
60333
+ * answer only changes on the row that crosses the ceiling. Checking once per
60334
+ * {@link DEFAULT_TRIM_EVERY} appends bounds the overshoot at that many rows —
60335
+ * 0.3% of the ceiling — for two hours' delay in enforcing it.
60336
+ */
60337
+ async trim() {
60338
+ if (this.sinceTrim < this.trimEvery) return;
60339
+ this.sinceTrim = 0;
60340
+ try {
60341
+ const held = await this.store.count.query({ collection: BIRTH_DECISION_LEDGER_COLLECTION });
60342
+ const excess = held - this.maxRows;
60343
+ if (excess <= 0) return;
60344
+ const boundary = (await this.store.query.query({
60345
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60346
+ filter: {
60347
+ orderBy: {
60348
+ field: "at",
60349
+ direction: "asc"
60350
+ },
60351
+ limit: excess
60352
+ },
60353
+ columns: ["at"]
60354
+ })).reduce((acc, r) => {
60355
+ const at = r.data["at"];
60356
+ return typeof at === "number" && at > acc ? at : acc;
60357
+ }, 0);
60358
+ if (boundary === 0) return;
60359
+ const { deleted } = await this.store.deleteWhere.mutate({
60360
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60361
+ filter: { whereBetween: { at: [0, boundary] } }
60362
+ });
60363
+ this.logger.info("birth-decision ledger trimmed to its ceiling", { meta: {
60364
+ held,
60365
+ ceiling: this.maxRows,
60366
+ deleted
60367
+ } });
60368
+ } catch (err) {
60369
+ this.logger.warn("birth-decision ledger trim failed — the ledger will keep growing", { meta: {
60370
+ ceiling: this.maxRows,
60371
+ error: err instanceof Error ? err.message : String(err)
60372
+ } });
60373
+ }
60374
+ }
60375
+ /** Birth decisions, newest first, optionally scoped by camera / verdict / age. */
60376
+ async list(query) {
60377
+ const where = {};
60378
+ if (query.deviceId !== void 0) where["deviceId"] = query.deviceId;
60379
+ if (query.verdict !== void 0) where["verdict"] = query.verdict;
60380
+ const filter = {
60381
+ orderBy: {
60382
+ field: "at",
60383
+ direction: "desc"
60384
+ },
60385
+ limit: query.limit ?? 500
60386
+ };
60387
+ if (Object.keys(where).length > 0) filter["where"] = where;
60388
+ if (query.since !== void 0) filter["whereBetween"] = { at: [query.since, Number.MAX_SAFE_INTEGER] };
60389
+ const rows = await this.store.query.query({
60390
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60391
+ filter
60392
+ });
60393
+ const out = [];
60394
+ for (const r of rows) {
60395
+ const parsed = BirthDecisionRecordSchema.safeParse({
60396
+ id: r.id,
60397
+ ...r.data
60398
+ });
60399
+ if (parsed.success) out.push(parsed.data);
60400
+ else this.logger.debug("birth-decision ledger: skipped a malformed row", { meta: { id: r.id } });
60401
+ }
60402
+ return out;
60403
+ }
60404
+ };
60405
+ /**
60406
+ * How many appends between ceiling checks, by default. See
60407
+ * {@link BirthDecisionLedgerStore.trim}.
60408
+ *
60409
+ * 100 rows is two hours of measured fleet traffic and 0.3% of the ceiling —
60410
+ * small enough that the table's real size never meaningfully exceeds its stated
60411
+ * bound, large enough that the ledger costs one `count` per two hours instead of
60412
+ * one per birth.
60413
+ */
60414
+ var DEFAULT_TRIM_EVERY = 100;
60415
+ //#endregion
59871
60416
  //#region src/pipeline-analytics/store/debug-note-archive-store.ts
59872
60417
  /**
59873
60418
  * DebugNoteArchiveStore — the durable corpus of what operators asked to be
@@ -61493,6 +62038,13 @@ var ANALYTICS_COLLECTIONS = [
61493
62038
  why: "the archived operator notes (D405) — the ONE table here whose whole purpose is to outlive the track it names. `debug` never pinned a track against retention and D405 does not change that, so the track a note was written on IS expected to be evicted; deleting the note with it is the failure the archive was built to fix (25–27 notes from one sweep unreadable the same afternoon). Its column is called `sourceTrackId` and not `trackId` precisely so the ownership derivation cannot reach it, and its bound is a fleet-wide ROW COUNT trimmed on append — never an age sweep, which would delete the corpus for the one property that makes it valuable."
61494
62039
  }
61495
62040
  },
62041
+ {
62042
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
62043
+ classification: {
62044
+ kind: "never-orphaned",
62045
+ why: "the birth-decision ledger (D409) — a measurement of what the confirmation gate decided, not analytics data about a track. Most of its rows describe births that were SUPPRESSED and therefore never had a track at all, and the confirmed ones are expected to outlive theirs: a miss rate needs its denominator for as long as the table holds, and cascading rows away with their tracks would silently shorten the window while leaving the numerator behind. Its column is called `sourceTrackId` and not `trackId` precisely so the ownership derivation cannot reach it, and its bound is a fleet-wide ROW COUNT trimmed on append — never an age sweep."
62046
+ }
62047
+ },
61496
62048
  {
61497
62049
  collection: STATIONARY_COLLECTION,
61498
62050
  classification: {
@@ -69269,6 +69821,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69269
69821
  * operator asked for never depends on this addon's own bookkeeping.
69270
69822
  */
69271
69823
  debugNoteArchive = null;
69824
+ /** The durable birth-decision ledger (D409) — measurement, never a gate. */
69825
+ birthDecisionLedger = null;
69826
+ /** Has the "no ledger on this runner" warning already been said? Once per
69827
+ * process: the condition is a property of the runner, not of the birth. */
69828
+ birthLedgerAbsenceLogged = false;
69272
69829
  /** Event-media relocation engine (entity-routing Phase 4). */
69273
69830
  mediaRelocate = null;
69274
69831
  mediaLocationStorage = null;
@@ -69707,6 +70264,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
69707
70264
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
69708
70265
  deferredBirths = new DeferredBirthRegistry();
69709
70266
  /**
70267
+ * The last motion RISING EDGE per device — the reference instant the
70268
+ * birth-latency proxy is measured from (D409). Measurement only; nothing
70269
+ * gates on it. Fed by the two motion handlers, which are subscribed on the
70270
+ * SAME designated post-processing node as the frame path, so a camera whose
70271
+ * births land here always has its motion mirror here too.
70272
+ */
70273
+ motionOnsets = new MotionOnsetRegistry();
70274
+ /**
69710
70275
  * The pixels each deferred birth arrived with — a RESERVE per camera, evicted
69711
70276
  * by age inside that camera's own quota (D379).
69712
70277
  *
@@ -70261,6 +70826,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70261
70826
  store: api.settingsStore,
70262
70827
  logger: logger.child("debug-note-archive")
70263
70828
  });
70829
+ this.birthDecisionLedger = new BirthDecisionLedgerStore({
70830
+ store: api.settingsStore,
70831
+ logger: logger.child("birth-decision-ledger")
70832
+ });
70264
70833
  {
70265
70834
  const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
70266
70835
  this.isPostProcessingNode = ownNodeId === designated;
@@ -70581,6 +71150,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
70581
71150
  RetrainAnnotationStore.declare(api.settingsStore),
70582
71151
  OpsLogStore.declare(api.settingsStore),
70583
71152
  DebugNoteArchiveStore.declare(api.settingsStore),
71153
+ BirthDecisionLedgerStore.declare(api.settingsStore),
70584
71154
  AnalyticsLts.declare(api.settingsStore),
70585
71155
  SceneStore.declare(api.settingsStore),
70586
71156
  NotificationCenter.declare(api.settingsStore),
@@ -71052,18 +71622,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71052
71622
  },
71053
71623
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
71054
71624
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
71055
- onSemanticMatch: (match) => this.promoteSemanticRecognition({
71056
- deviceId: match.deviceId,
71057
- trackId: match.trackId,
71058
- timestamp: match.timestamp,
71059
- bbox: match.bbox,
71060
- frameWidth: match.frameWidth,
71061
- frameHeight: match.frameHeight,
71062
- kind: "face",
71063
- score: match.matchScore,
71064
- confidence: match.confidence,
71065
- ...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
71066
- }),
71625
+ onSemanticFace: (face) => this.promoteSemanticRecognition({
71626
+ deviceId: face.deviceId,
71627
+ trackId: face.trackId,
71628
+ timestamp: face.timestamp,
71629
+ bbox: face.bbox,
71630
+ frameWidth: face.frameWidth,
71631
+ frameHeight: face.frameHeight,
71632
+ kind: face.identityId !== void 0 ? "face" : "face-visible",
71633
+ score: face.matchScore ?? face.confidence,
71634
+ confidence: face.confidence,
71635
+ ...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
71636
+ }, face.identityId !== void 0 ? "identified" : "evidence"),
71067
71637
  onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
71068
71638
  logger: logger.child("FaceRecognizer")
71069
71639
  });
@@ -72245,6 +72815,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
72245
72815
  this.overlaySynthesisWarnAt.delete(deviceId);
72246
72816
  this.forgetDeviceProcessors(deviceId);
72247
72817
  this.levelStateByDevice.delete(deviceId);
72818
+ this.motionOnsets.forgetDevice(deviceId);
72248
72819
  this.audioConfirmByDevice.delete(deviceId);
72249
72820
  this.lastTrackActivityMs.delete(deviceId);
72250
72821
  this.motionEventSnapshots?.forgetDevice(deviceId);
@@ -73031,8 +73602,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73031
73602
  if (t.birthEvidence === void 0) continue;
73032
73603
  this.birthEvidence.remember(key, id, Buffer.from(t.birthEvidence.jpegBase64, "base64"), exhaustionNowMs);
73033
73604
  }
73034
- const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
73605
+ const withBirthEvidence = /* @__PURE__ */ new Set();
73606
+ const run = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
73035
73607
  const evidenceCrop = this.birthEvidence.get(key, id, exhaustionNowMs);
73608
+ if (evidenceCrop !== null) withBirthEvidence.add(id);
73036
73609
  return {
73037
73610
  trackId: id,
73038
73611
  className: t.className,
@@ -73047,6 +73620,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73047
73620
  frameWidth: result.frameWidth,
73048
73621
  frameHeight: result.frameHeight
73049
73622
  }, exhaustedIds);
73623
+ const outcome = run.outcome;
73050
73624
  const gateNowMs = Date.now();
73051
73625
  for (const { id, t } of bornCandidates) {
73052
73626
  if (outcome.undecided.has(id)) {
@@ -73075,6 +73649,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73075
73649
  } });
73076
73650
  continue;
73077
73651
  }
73652
+ this.recordBirthDecision({
73653
+ deviceId,
73654
+ trackId: id,
73655
+ className: t.className,
73656
+ confirmed: outcome.confirmed.has(id),
73657
+ exhaustedFallback: wasDeferred && exhaustedIds.has(id),
73658
+ attempts: deferredAttempts,
73659
+ deferredForMs,
73660
+ matchedThisFrame: t.matchedThisFrame,
73661
+ birthEvidenceAvailable: withBirthEvidence.has(id),
73662
+ decision: run.decisions.get(id),
73663
+ decidedAtMs: gateNowMs,
73664
+ firstSeen: this.trackStore?.peekActive(id)?.firstSeen ?? null
73665
+ });
73078
73666
  if (!outcome.confirmed.has(id)) {
73079
73667
  this.suppressedBirths.reject(key, id);
73080
73668
  this.trackStore?.dropActive(id);
@@ -73773,10 +74361,64 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73773
74361
  * candidate) and each failure path fails OPEN, so the synchronous frame path
73774
74362
  * is never blocked or reordered by a slow/failed re-detection.
73775
74363
  */
74364
+ /**
74365
+ * Record ONE resolved birth verdict in the durable ledger (D409).
74366
+ *
74367
+ * Synchronous up to the append, which is fired and forgotten: the frame path
74368
+ * awaits `confirmTrackBirths` already, and a measurement must never add a
74369
+ * store round trip to the latency of a birth. `BirthDecisionLedgerStore.append`
74370
+ * never throws, so the un-awaited promise cannot reject.
74371
+ *
74372
+ * Cost, per BIRTH and not per frame: one `Map.get`, one `Set.has`, a counter
74373
+ * bump and one INSERT. Measured fleet-wide on the live hub 2026-09-08 at ~49
74374
+ * births/hour, i.e. 0.014 writes/s — three orders of magnitude below the
74375
+ * 12-19/s the runtime-state guard was written for, and a different shape
74376
+ * (append-only, never a periodic rewrite of an unchanged slice).
74377
+ *
74378
+ * A runner with no ledger (construction failed) records nothing and says so
74379
+ * exactly once per process, rather than per birth: a measurement that cannot
74380
+ * be taken must be visible, but it must not become the noisiest line in Loki.
74381
+ */
74382
+ recordBirthDecision(input) {
74383
+ const ledger = this.birthDecisionLedger;
74384
+ if (ledger === null) {
74385
+ if (!this.birthLedgerAbsenceLogged) {
74386
+ this.birthLedgerAbsenceLogged = true;
74387
+ this.ctx.logger.warn("birth decisions are NOT being measured — no ledger on this runner", { tags: { deviceId: input.deviceId } });
74388
+ }
74389
+ return;
74390
+ }
74391
+ const motionOnsetAtMs = this.motionOnsets.onsetFor(input.deviceId);
74392
+ const birthIndexInBurst = this.motionOnsets.noteBirth(input.deviceId);
74393
+ const decision = input.decision;
74394
+ ledger.append(buildBirthDecisionRecord({
74395
+ deviceId: input.deviceId,
74396
+ trackId: input.trackId,
74397
+ className: input.className,
74398
+ confirmed: input.confirmed,
74399
+ exhaustedFallback: input.exhaustedFallback,
74400
+ attempts: input.attempts,
74401
+ deferredForMs: input.deferredForMs,
74402
+ ...input.matchedThisFrame !== void 0 ? { matchedThisFrame: input.matchedThisFrame } : {},
74403
+ birthEvidenceAvailable: input.birthEvidenceAvailable,
74404
+ decidedByBirthEvidence: decision?.cropSource === "carried",
74405
+ reason: decision?.reason ?? null,
74406
+ bestScore: decision?.bestScore ?? null,
74407
+ appliedMinConfidence: decision?.appliedMinConfidence ?? null,
74408
+ decidedAtMs: input.decidedAtMs,
74409
+ firstSeen: input.firstSeen,
74410
+ motionOnsetAtMs,
74411
+ birthIndexInBurst
74412
+ }));
74413
+ }
73776
74414
  async confirmTrackBirths(candidates, params, exhaustedIds = /* @__PURE__ */ new Set()) {
74415
+ const decisions = /* @__PURE__ */ new Map();
73777
74416
  const allConfirmed = () => ({
73778
- confirmed: new Set(candidates.map((c) => c.trackId)),
73779
- undecided: /* @__PURE__ */ new Set()
74417
+ outcome: {
74418
+ confirmed: new Set(candidates.map((c) => c.trackId)),
74419
+ undecided: /* @__PURE__ */ new Set()
74420
+ },
74421
+ decisions
73780
74422
  });
73781
74423
  if (candidates.length === 0) return allConfirmed();
73782
74424
  const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
@@ -73799,101 +74441,105 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
73799
74441
  const nodeId = frameHandle.nodeId;
73800
74442
  const candidateNativeRecovery = /* @__PURE__ */ new Map();
73801
74443
  for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
73802
- return confirmBirths(candidates, config, {
73803
- fetchCrop: (candidate) => this.captureScheduler.request({
73804
- deviceId,
73805
- trackId: candidate.trackId,
73806
- kind: "confirm",
73807
- staleAfterMs: config.timeoutMs,
73808
- exec: () => captureCrop(frameHandle, {
74444
+ return {
74445
+ outcome: await confirmBirths(candidates, config, {
74446
+ fetchCrop: (candidate) => this.captureScheduler.request({
74447
+ deviceId,
74448
+ trackId: candidate.trackId,
74449
+ kind: "confirm",
74450
+ staleAfterMs: config.timeoutMs,
74451
+ exec: () => captureCrop(frameHandle, {
74452
+ x: candidate.bbox.x,
74453
+ y: candidate.bbox.y,
74454
+ w: candidate.bbox.w,
74455
+ h: candidate.bbox.h
74456
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, 320, deviceId)
74457
+ }),
74458
+ fetchFallbackCrop: (candidate) => {
74459
+ const displayCrop = this.captureDisplayCropFn;
74460
+ if (!displayCrop) return Promise.resolve(null);
74461
+ return displayCrop(frameHandle, {
74462
+ x: candidate.bbox.x,
74463
+ y: candidate.bbox.y,
74464
+ w: candidate.bbox.w,
74465
+ h: candidate.bbox.h
74466
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
74467
+ },
74468
+ redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
74469
+ cropRectFor: (candidate) => captureCropFrameRect({
73809
74470
  x: candidate.bbox.x,
73810
74471
  y: candidate.bbox.y,
73811
74472
  w: candidate.bbox.w,
73812
74473
  h: candidate.bbox.h
73813
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, 320, deviceId)
73814
- }),
73815
- fetchFallbackCrop: (candidate) => {
73816
- const displayCrop = this.captureDisplayCropFn;
73817
- if (!displayCrop) return Promise.resolve(null);
73818
- return displayCrop(frameHandle, {
74474
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
74475
+ minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
74476
+ deviceId,
74477
+ className: candidate.className,
73819
74478
  x: candidate.bbox.x,
73820
74479
  y: candidate.bbox.y,
73821
- w: candidate.bbox.w,
73822
- h: candidate.bbox.h
73823
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
73824
- },
73825
- redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
73826
- cropRectFor: (candidate) => captureCropFrameRect({
73827
- x: candidate.bbox.x,
73828
- y: candidate.bbox.y,
73829
- w: candidate.bbox.w,
73830
- h: candidate.bbox.h
73831
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
73832
- minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
73833
- deviceId,
73834
- className: candidate.className,
73835
- x: candidate.bbox.x,
73836
- y: candidate.bbox.y,
73837
- base: config.minConfidence,
73838
- now: Date.now()
73839
- }).bar ?? config.minConfidence,
73840
- onDecision: (decision) => {
73841
- const meta = {
73842
- trackId: decision.trackId,
73843
- reason: decision.reason,
73844
- className: decision.className,
73845
- ...decision.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
73846
- ...decision.bestIncompatibleClass !== void 0 ? {
73847
- bestIncompatibleClass: decision.bestIncompatibleClass,
73848
- bestIncompatibleScore: decision.bestIncompatibleScore
73849
- } : {},
73850
- minConfidence: config.minConfidence,
73851
- ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
73852
- ...decision.reason === "native-pass" ? {
73853
- nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
73854
- nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
73855
- } : {},
73856
- ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
73857
- appliedMinConfidence: decision.appliedMinConfidence,
73858
- barRaisedByPhantomCell: true
73859
- } : {}
73860
- };
73861
- switch (decision.verdict) {
73862
- case "suppressed":
73863
- this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
73864
- tags: { deviceId },
73865
- meta
73866
- });
73867
- break;
73868
- case "undecided":
73869
- this.ctx.logger.info("confirmation gate: birth undecided", {
73870
- tags: { deviceId },
73871
- meta
73872
- });
73873
- break;
73874
- case "confirmed":
73875
- this.ctx.logger.info("confirmation gate: birth confirmed", {
74480
+ base: config.minConfidence,
74481
+ now: Date.now()
74482
+ }).bar ?? config.minConfidence,
74483
+ onDecision: (decision) => {
74484
+ decisions.set(decision.trackId, decision);
74485
+ const meta = {
74486
+ trackId: decision.trackId,
74487
+ reason: decision.reason,
74488
+ className: decision.className,
74489
+ ...decision.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
74490
+ ...decision.bestIncompatibleClass !== void 0 ? {
74491
+ bestIncompatibleClass: decision.bestIncompatibleClass,
74492
+ bestIncompatibleScore: decision.bestIncompatibleScore
74493
+ } : {},
74494
+ minConfidence: config.minConfidence,
74495
+ ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
74496
+ ...decision.reason === "native-pass" ? {
74497
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
74498
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
74499
+ } : {},
74500
+ ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
74501
+ appliedMinConfidence: decision.appliedMinConfidence,
74502
+ barRaisedByPhantomCell: true
74503
+ } : {}
74504
+ };
74505
+ switch (decision.verdict) {
74506
+ case "suppressed":
74507
+ this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
74508
+ tags: { deviceId },
74509
+ meta
74510
+ });
74511
+ break;
74512
+ case "undecided":
74513
+ this.ctx.logger.info("confirmation gate: birth undecided", {
74514
+ tags: { deviceId },
74515
+ meta
74516
+ });
74517
+ break;
74518
+ case "confirmed":
74519
+ this.ctx.logger.info("confirmation gate: birth confirmed", {
74520
+ tags: { deviceId },
74521
+ meta
74522
+ });
74523
+ break;
74524
+ }
74525
+ if (decision.secondary !== void 0) {
74526
+ this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
74527
+ this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
73876
74528
  tags: { deviceId },
73877
- meta
74529
+ meta: {
74530
+ trackId: decision.trackId,
74531
+ className: decision.className,
74532
+ secondaryClass: decision.secondary.className,
74533
+ secondaryScore: decision.secondary.score,
74534
+ secondaryBbox: decision.secondary.bbox,
74535
+ verdict: decision.verdict
74536
+ }
73878
74537
  });
73879
- break;
73880
- }
73881
- if (decision.secondary !== void 0) {
73882
- this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
73883
- this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
73884
- tags: { deviceId },
73885
- meta: {
73886
- trackId: decision.trackId,
73887
- className: decision.className,
73888
- secondaryClass: decision.secondary.className,
73889
- secondaryScore: decision.secondary.score,
73890
- secondaryBbox: decision.secondary.bbox,
73891
- verdict: decision.verdict
73892
- }
73893
- });
74538
+ }
73894
74539
  }
73895
- }
73896
- }, exhaustedIds);
74540
+ }, exhaustedIds),
74541
+ decisions
74542
+ };
73897
74543
  }
73898
74544
  async resolveGlobalFaceEnabled() {
73899
74545
  return this.faceGlobalEnabledCache.get(() => this.faceGlobalEnabledState.get());
@@ -74079,10 +74725,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74079
74725
  } } : {}
74080
74726
  });
74081
74727
  }
74728
+ if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
74082
74729
  }
74083
74730
  const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
74084
74731
  if (resolved !== null && resolved.text !== void 0) {
74085
- if ((await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId)).trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
74732
+ const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
74733
+ if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
74086
74734
  }
74087
74735
  } else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
74088
74736
  } catch (err) {
@@ -74523,8 +75171,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
74523
75171
  * when it wins and the frame's pixels are still reachable. Callers: the
74524
75172
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
74525
75173
  * fine-classification attribution. */
74526
- promoteSemanticRecognition(input) {
74527
- this.recognizedTrackIds.add(input.trackId);
75174
+ promoteSemanticRecognition(input, origin) {
75175
+ if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
74528
75176
  const ctx = this.ctxIfReady;
74529
75177
  if (ctx === null) return;
74530
75178
  promoteSemanticBestFrame({
@@ -75223,6 +75871,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75223
75871
  detected,
75224
75872
  atMs: timestamp
75225
75873
  });
75874
+ if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
75226
75875
  this.sceneEngine?.noteMotion(deviceId, timestamp);
75227
75876
  await this.eventStore.insertMotion(ev);
75228
75877
  this.ctx.eventBus.emit({
@@ -75298,6 +75947,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
75298
75947
  detected,
75299
75948
  atMs: timestamp
75300
75949
  });
75950
+ if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
75301
75951
  this.sceneEngine?.noteMotion(deviceId, timestamp);
75302
75952
  await this.eventStore.insertMotion(ev);
75303
75953
  this.ctx.eventBus.emit({
@@ -78257,6 +78907,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
78257
78907
  return await this.debugNoteArchive?.list(input) ?? [];
78258
78908
  }
78259
78909
  /**
78910
+ * The birth-decision ledger, newest first (D409).
78911
+ *
78912
+ * A READ of a measurement table and nothing else — no join back to the
78913
+ * tracks, deliberately: a SUPPRESSED birth never had a track row and a
78914
+ * confirmed one is expected to have aged out, so a join would return exactly
78915
+ * the rows the ledger exists to preserve. A runner with no ledger answers
78916
+ * EMPTY, which is true: nothing has been measured here.
78917
+ */
78918
+ async listBirthDecisions(input) {
78919
+ return await this.birthDecisionLedger?.list(input) ?? [];
78920
+ }
78921
+ /**
78260
78922
  * Track-centric time-based retention (design §5.1). Drains every persisted
78261
78923
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
78262
78924
  * widened cascade — enrolled faces/plates + identity media are exempt (design