@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.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_dist = require("../dist-FHSaZkf_.js");
5
+ const require_dist = require("../dist-IE6UOohI.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_dist.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -34569,6 +34569,37 @@ var TWO_WHEELERS = new Set([
34569
34569
  function intersection(a, b) {
34570
34570
  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));
34571
34571
  }
34572
+ /**
34573
+ * The FOLDED subject's box: the union of the rider and the machine.
34574
+ *
34575
+ * The fold's whole claim is that the two halves are ONE subject. Until
34576
+ * 2026-09-08 the box that survived the fold was the MACHINE's alone, so every
34577
+ * consumer that cuts pixels from `TrackedDetectionOut.bbox` — the best-shot
34578
+ * thumbnail, `wideCentralSquareLayout`'s central square, the close-time
34579
+ * keyFrame→thumbnail derive — framed a motorcycle with its rider's head cut
34580
+ * off. Operator, device 617, tracks `7f50622b` and `9cbc4791`, three minutes
34581
+ * apart: "motocicli e veicoli a due ruote devono includere il conducente nel
34582
+ * keyframe" (D407).
34583
+ *
34584
+ * This is the ONE place the folded subject's rectangle is formed, and it is
34585
+ * formed BEFORE the tracker — so no crop path derives a second rectangle
34586
+ * (D52 stays intact: `deriveDetailCropRect` is still the only detail-crop
34587
+ * derivation, and it now receives a subject box that already holds the rider).
34588
+ * Pure geometry: a strict union, never a pad — the fold widens the subject, it
34589
+ * never moves it.
34590
+ */
34591
+ function riderSubjectBox(person, vehicle) {
34592
+ const x = Math.min(person.x, vehicle.x);
34593
+ const y = Math.min(person.y, vehicle.y);
34594
+ const right = Math.max(person.x + person.w, vehicle.x + vehicle.w);
34595
+ const bottom = Math.max(person.y + person.h, vehicle.y + vehicle.h);
34596
+ return {
34597
+ x,
34598
+ y,
34599
+ w: right - x,
34600
+ h: bottom - y
34601
+ };
34602
+ }
34572
34603
  /** True when `vehicle` is a two-wheeler that `person` is riding. */
34573
34604
  function isRiderPair(person, vehicle) {
34574
34605
  if (person.macroClass !== "person") return false;
@@ -36335,17 +36366,64 @@ var FrameProcessor = class {
36335
36366
  const foldByVehicleBbox = /* @__PURE__ */ new Map();
36336
36367
  if (riderPairs.length > 0) {
36337
36368
  const riderIdx = new Set(riderPairs.map((p) => Number(p.personId)));
36369
+ /**
36370
+ * Move every bbox-OBJECT-keyed side table from the machine's box to the
36371
+ * folded subject's box (D407).
36372
+ *
36373
+ * This file carries frame-local facts across the tracker by bbox-object
36374
+ * REFERENCE (`labelsByBbox`, `embeddingByBbox`, `maskByBbox`, …); the
36375
+ * tracker then assigns `track.bbox = det.bbox`, and the emit block below
36376
+ * reads every one of those maps with `td.bbox`. Widening the subject
36377
+ * therefore means the widened box must inherit the machine's entries —
36378
+ * otherwise a folded rider silently loses its vehicle-classifier label
36379
+ * ("Motorcycle"), its mask, its plate read and its source id. One place,
36380
+ * exhaustive, next to the maps it moves.
36381
+ */
36382
+ const adoptSubjectBox = (from, to) => {
36383
+ const labels = labelsByBbox.get(from);
36384
+ if (labels !== void 0) labelsByBbox.set(to, labels);
36385
+ const tier = originalClassTierByBbox.get(from);
36386
+ if (tier !== void 0) originalClassTierByBbox.set(to, tier);
36387
+ const embedding = embeddingByBbox.get(from);
36388
+ if (embedding !== void 0) embeddingByBbox.set(to, embedding);
36389
+ const mask = maskByBbox.get(from);
36390
+ if (mask !== void 0) maskByBbox.set(to, mask);
36391
+ const plate = plateByBbox.get(from);
36392
+ if (plate !== void 0) plateByBbox.set(to, plate);
36393
+ const faceBbox = faceBboxByBbox.get(from);
36394
+ if (faceBbox !== void 0) faceBboxByBbox.set(to, faceBbox);
36395
+ const faceSize = nativeFaceSizeByBbox.get(from);
36396
+ if (faceSize !== void 0) nativeFaceSizeByBbox.set(to, faceSize);
36397
+ const alignedCrop = faceAlignedCropByBbox.get(from);
36398
+ if (alignedCrop !== void 0) faceAlignedCropByBbox.set(to, alignedCrop);
36399
+ const sourceId = sourceIdByBbox.get(from);
36400
+ if (sourceId !== void 0) {
36401
+ sourceIdByBbox.set(to, sourceId);
36402
+ firstLevelBboxById.set(sourceId, to);
36403
+ }
36404
+ };
36405
+ /** Folded vehicles, by their index in `filteredDetections`. */
36406
+ const foldedVehicles = /* @__PURE__ */ new Map();
36338
36407
  for (const p of riderPairs) {
36339
- const vehicle = filteredDetections[Number(p.vehicleId)];
36340
- if (vehicle === void 0) continue;
36341
- foldByVehicleBbox.set(vehicle.bbox, {
36408
+ const vehicleIdx = Number(p.vehicleId);
36409
+ const vehicle = filteredDetections[vehicleIdx];
36410
+ const person = filteredDetections[Number(p.personId)];
36411
+ if (vehicle === void 0 || person === void 0) continue;
36412
+ const subjectBox = riderSubjectBox(person.bbox, vehicle.bbox);
36413
+ adoptSubjectBox(vehicle.bbox, subjectBox);
36414
+ const folded = {
36415
+ ...vehicle,
36416
+ bbox: subjectBox
36417
+ };
36418
+ foldedVehicles.set(vehicleIdx, folded);
36419
+ foldByVehicleBbox.set(subjectBox, {
36342
36420
  overlap: p.overlap,
36343
- personScore: filteredDetections[Number(p.personId)]?.score ?? 0,
36421
+ personScore: person.score,
36344
36422
  vehicleScore: vehicle.score,
36345
36423
  vehicleClass: vehicle.originalClass ?? "two-wheeler"
36346
36424
  });
36347
36425
  }
36348
- filteredDetections = filteredDetections.filter((_, i) => !riderIdx.has(i));
36426
+ filteredDetections = filteredDetections.map((d, i) => foldedVehicles.get(i) ?? d).filter((_, i) => !riderIdx.has(i));
36349
36427
  }
36350
36428
  const gate = this.stationaryGate ? this.stationaryGate.filter({
36351
36429
  detections: filteredDetections,
@@ -49068,10 +49146,9 @@ var FaceRecognizer = class {
49068
49146
  margin: settings.margin,
49069
49147
  minIdentitySamples: settings.minIdentitySamples
49070
49148
  }) : /* @__PURE__ */ new Map();
49071
- if (this.deps.onSemanticMatch !== void 0) for (const c of matchCandidates) {
49149
+ if (this.deps.onSemanticFace !== void 0) for (const c of matchCandidates) {
49072
49150
  const match = matches.get(c.trackId);
49073
- if (match === void 0) continue;
49074
- this.deps.onSemanticMatch({
49151
+ this.deps.onSemanticFace({
49075
49152
  deviceId: input.deviceId,
49076
49153
  trackId: c.trackId,
49077
49154
  timestamp: input.timestamp,
@@ -49079,8 +49156,10 @@ var FaceRecognizer = class {
49079
49156
  frameHeight: input.frameHeight,
49080
49157
  bbox: c.bbox,
49081
49158
  confidence: c.confidence,
49082
- matchScore: match.score,
49083
- identityId: match.identityId,
49159
+ ...match !== void 0 ? {
49160
+ matchScore: match.score,
49161
+ identityId: match.identityId
49162
+ } : {},
49084
49163
  ...input.frameHandle !== void 0 ? { frameHandle: input.frameHandle } : {}
49085
49164
  });
49086
49165
  }
@@ -53353,6 +53432,95 @@ var BirthEvidenceStore = class {
53353
53432
  }
53354
53433
  };
53355
53434
  //#endregion
53435
+ //#region src/pipeline-analytics/pipeline/birth-decision-record.ts
53436
+ /**
53437
+ * birth-decision-record — turn one resolved birth verdict into the row that is
53438
+ * kept (D409).
53439
+ *
53440
+ * PURE, and separated from the store for the usual reason: the two rules worth
53441
+ * getting right — what counts as the fail-open, and when the latency proxy is
53442
+ * allowed to report a number — are decisions, not I/O, and a decision that only
53443
+ * exists inside a SQLite call cannot be tested or argued with.
53444
+ *
53445
+ * MEASUREMENT ONLY. Nothing here gates a birth, changes a verdict, or feeds
53446
+ * back into the frame path. The caller writes the row after the verdict is
53447
+ * already taken.
53448
+ */
53449
+ /**
53450
+ * The verdict as the ledger reports it.
53451
+ *
53452
+ * `exhausted-fallback` is a CONFIRMATION and is counted apart from one, because
53453
+ * a birth nobody ever looked at and a birth measured and passed are the same
53454
+ * row in every existing aggregate — which is what made "the gate stopped
53455
+ * failing open" and "the gate now suppresses everything it cannot measure"
53456
+ * indistinguishable. Exhaustion attached to a SUPPRESSION is meaningless
53457
+ * (exhaustion only ever waves a birth through) and is discarded rather than
53458
+ * inventing a fourth member.
53459
+ */
53460
+ function verdictFor(input) {
53461
+ if (!input.confirmed) return "suppressed";
53462
+ return input.exhaustedFallback ? "exhausted-fallback" : "confirmed";
53463
+ }
53464
+ /**
53465
+ * The latency proxy, and the three ways it declines to produce a number.
53466
+ *
53467
+ * It reports `null` — never a fabricated or clamped value — when there is no
53468
+ * track (a suppressed birth), no motion reference on this runner, or a
53469
+ * reference older than {@link MAX_BIRTH_LATENCY_PROXY_MS}. That last case is
53470
+ * the one that matters: on a busy scene the burst never closes and the onset is
53471
+ * minutes old, and a minutes-long "birth latency" would be read as the
53472
+ * detector's fault when it is the proxy's. When the reference is refused, the
53473
+ * onset and the burst index go with it — reporting an index against a reference
53474
+ * this row does not carry would be an invitation to join them back up.
53475
+ *
53476
+ * A NEGATIVE result is returned as measured. The analyzer's pixel-count floor
53477
+ * can be crossed after the detector's confidence floor for a subject entering
53478
+ * slowly at the frame edge, and clamping that away would delete precisely the
53479
+ * evidence that the proxy is unreliable. See `BirthDecisionRecordSchema` for
53480
+ * the full error bars.
53481
+ */
53482
+ function latencyFor(input, firstSeen) {
53483
+ const onset = input.motionOnsetAtMs;
53484
+ if (firstSeen === null || onset === null) return {
53485
+ motionOnsetAt: null,
53486
+ birthLatencyMs: null,
53487
+ birthIndexInBurst: null
53488
+ };
53489
+ if (firstSeen - onset > 6e4) return {
53490
+ motionOnsetAt: null,
53491
+ birthLatencyMs: null,
53492
+ birthIndexInBurst: null
53493
+ };
53494
+ return {
53495
+ motionOnsetAt: onset,
53496
+ birthLatencyMs: firstSeen - onset,
53497
+ birthIndexInBurst: input.birthIndexInBurst
53498
+ };
53499
+ }
53500
+ /** Build the durable row for one resolved birth decision. */
53501
+ function buildBirthDecisionRecord(input) {
53502
+ const verdict = verdictFor(input);
53503
+ const firstSeen = verdict === "suppressed" ? null : input.firstSeen;
53504
+ const latency = latencyFor(input, firstSeen);
53505
+ return {
53506
+ at: input.decidedAtMs,
53507
+ deviceId: input.deviceId,
53508
+ sourceTrackId: input.trackId,
53509
+ className: input.className,
53510
+ verdict,
53511
+ reason: input.reason ?? null,
53512
+ attempts: input.attempts,
53513
+ deferredForMs: input.deferredForMs,
53514
+ decidedOnCoastedFrame: input.matchedThisFrame === false,
53515
+ birthEvidenceAvailable: input.birthEvidenceAvailable,
53516
+ decidedByBirthEvidence: input.decidedByBirthEvidence,
53517
+ bestScore: input.bestScore ?? null,
53518
+ appliedMinConfidence: input.appliedMinConfidence ?? null,
53519
+ firstSeen,
53520
+ ...latency
53521
+ };
53522
+ }
53523
+ //#endregion
53356
53524
  //#region src/pipeline-analytics/pipeline/deferred-births.ts
53357
53525
  var DeferredBirthRegistry = class {
53358
53526
  byDevice = /* @__PURE__ */ new Map();
@@ -53427,6 +53595,57 @@ var DeferredBirthRegistry = class {
53427
53595
  }
53428
53596
  };
53429
53597
  //#endregion
53598
+ //#region src/pipeline-analytics/pipeline/motion-onset-registry.ts
53599
+ var MotionOnsetRegistry = class {
53600
+ byDevice = /* @__PURE__ */ new Map();
53601
+ /**
53602
+ * Motion went off→on at `atMs`. Opens a new burst, discarding the previous
53603
+ * one's birth count — a new burst is a new subject population.
53604
+ */
53605
+ noteRisingEdge(deviceId, atMs) {
53606
+ this.byDevice.set(deviceId, {
53607
+ onsetAtMs: atMs,
53608
+ births: 0
53609
+ });
53610
+ }
53611
+ /** The open burst's onset, or `null` for a device with no reference. */
53612
+ onsetFor(deviceId) {
53613
+ return this.byDevice.get(deviceId)?.onsetAtMs ?? null;
53614
+ }
53615
+ /**
53616
+ * Count one birth decision against this device's burst and return its 0-based
53617
+ * index — `null` when there is no burst to count it against.
53618
+ *
53619
+ * Index 0 is the only index at which the burst plausibly belongs to the same
53620
+ * subject as the birth; the ledger records the index rather than filtering
53621
+ * here, so the reader can see how often it is not 0.
53622
+ */
53623
+ noteBirth(deviceId) {
53624
+ const burst = this.byDevice.get(deviceId);
53625
+ if (burst === void 0) return null;
53626
+ const index = burst.births;
53627
+ burst.births += 1;
53628
+ return index;
53629
+ }
53630
+ /** Drop a device's reference (device removed / pipeline reset). */
53631
+ forgetDevice(deviceId) {
53632
+ this.byDevice.delete(deviceId);
53633
+ }
53634
+ };
53635
+ //#endregion
53636
+ //#region src/pipeline-analytics/pipeline/detail-semantic-gate.ts
53637
+ /** The detail `className` whose result IS the recognition. */
53638
+ var PLATE_CLASS = "plate";
53639
+ /**
53640
+ * The moment `className`'s detail result earns the semantic tier.
53641
+ *
53642
+ * `recognition` — promote as soon as the result exists (the plate read).
53643
+ * `label-write` — promote only once the track ACCEPTED the label.
53644
+ */
53645
+ function detailSemanticTrigger(className) {
53646
+ return className === PLATE_CLASS ? "recognition" : "label-write";
53647
+ }
53648
+ //#endregion
53430
53649
  //#region src/pipeline-analytics/pipeline/dropout-skip.ts
53431
53650
  function shouldSkipDropoutFrame(activeTrackCount, detectionCount, consecutiveSkips, cfg) {
53432
53651
  if (!cfg.enabled) return false;
@@ -59942,6 +60161,332 @@ var IdentityStore = class {
59942
60161
  }
59943
60162
  };
59944
60163
  //#endregion
60164
+ //#region src/pipeline-analytics/store/birth-decision-ledger-store.ts
60165
+ /**
60166
+ * BirthDecisionLedgerStore — the durable record of every track-birth verdict
60167
+ * (D409).
60168
+ *
60169
+ * A declared SQL-backed collection, like the events ops-log and the debug-note
60170
+ * archive next door, and for the same reason: pipeline-analytics already owns
60171
+ * SQLite collections, and an undeclared collection crash-loops the runner. MUST
60172
+ * be declared in `onInitialize` before the first write.
60173
+ *
60174
+ * Collection name: pipeline-analytics:birth-decision-ledger
60175
+ *
60176
+ * ── Why a table rather than a log line ─────────────────────────────────────
60177
+ * The gate ALREADY logs every decision. `logs.query` is an in-memory ring that
60178
+ * measured 40 000 entries covering 38 MINUTES of fleet traffic on 2026-09-08 —
60179
+ * so a fourteenth field on that line answers nothing the next morning, and the
60180
+ * questions this data exists for ("what fraction of births on 617 were decided
60181
+ * on a coasted frame, over the week the operator complained about") are all
60182
+ * multi-day. A previous investigation in this repo was blocked for exactly
60183
+ * this.
60184
+ *
60185
+ * ── Why it is safe on the write path ──────────────────────────────────────
60186
+ * ONE INSERT PER BIRTH DECISION. Not per frame, not periodic, and never an
60187
+ * update of an existing row. Measured fleet-wide on the live hub, 2026-09-08:
60188
+ * 31 decided births in 38 minutes = ~49/hour = 0.014 writes/s. The write
60189
+ * amplification the runtime-state guard exists to prevent
60190
+ * (`scripts/check-runtime-state-durability.ts`: 12-19 writes/s rewriting
60191
+ * ~2.6-5 GB/day to maintain 161 KB) is three orders of magnitude above this and
60192
+ * is a different shape entirely — that one is a periodic REWRITE of a slice
60193
+ * whose values do not change. Nothing here advances a clock under an unchanged
60194
+ * value, because every row is new.
60195
+ *
60196
+ * ── Two properties, stated once ───────────────────────────────────────────
60197
+ * - **It is not track-owned.** A SUPPRESSED birth never had a track; a
60198
+ * confirmed one is expected to age out long before this row. The column is
60199
+ * `sourceTrackId` and not `trackId` so the ownership derivation in
60200
+ * `collection-classification.ts` cannot see it and cascade it away.
60201
+ * - **It is bounded by COUNT, not by age.** A miss rate needs a denominator
60202
+ * over whatever window the table holds; a quiet fleet should keep more of
60203
+ * that window, not less. See {@link MAX_BIRTH_DECISION_RECORDS}.
60204
+ *
60205
+ * MEASUREMENT ONLY. Nothing reads this on the frame path; nothing gates on it.
60206
+ */
60207
+ /**
60208
+ * @durable class=ledger owner=pipeline-analytics
60209
+ * write="one row per track-birth VERDICT — appended by the birth loop in
60210
+ * `processFrame` the instant the confirmation gate's verdict resolves
60211
+ * (confirmed / suppressed / exhausted fail-open). Per BIRTH, never per
60212
+ * frame: measured at ~49 rows/hour fleet-wide on 2026-09-08. Append-only,
60213
+ * never updated. BEST-EFFORT — it logs and swallows, because a failed
60214
+ * measurement must not fail the birth it measures."
60215
+ * retention="NOT track retention and NOT an age clock — a miss rate needs
60216
+ * whatever window the table holds, and an age sweep would shorten it on a
60217
+ * quiet fleet exactly when more history is affordable. Bounded by ROW
60218
+ * COUNT (MAX_BIRTH_DECISION_RECORDS, 30000, fleet-wide), oldest `at`
60219
+ * first, trimmed on append. ~6 MB at the ceiling."
60220
+ */
60221
+ var BIRTH_DECISION_LEDGER_COLLECTION = "pipeline-analytics:birth-decision-ledger";
60222
+ var BIRTH_DECISION_LEDGER_COLUMNS = [
60223
+ {
60224
+ name: "id",
60225
+ type: "TEXT",
60226
+ primaryKey: true,
60227
+ notNull: true
60228
+ },
60229
+ {
60230
+ name: "at",
60231
+ type: "INTEGER",
60232
+ notNull: true
60233
+ },
60234
+ {
60235
+ name: "deviceId",
60236
+ type: "INTEGER",
60237
+ notNull: true
60238
+ },
60239
+ {
60240
+ name: "sourceTrackId",
60241
+ type: "TEXT",
60242
+ notNull: true
60243
+ },
60244
+ {
60245
+ name: "className",
60246
+ type: "TEXT",
60247
+ notNull: true
60248
+ },
60249
+ {
60250
+ name: "verdict",
60251
+ type: "TEXT",
60252
+ notNull: true
60253
+ },
60254
+ {
60255
+ name: "reason",
60256
+ type: "TEXT"
60257
+ },
60258
+ {
60259
+ name: "attempts",
60260
+ type: "INTEGER",
60261
+ notNull: true
60262
+ },
60263
+ {
60264
+ name: "deferredForMs",
60265
+ type: "INTEGER",
60266
+ notNull: true
60267
+ },
60268
+ {
60269
+ name: "decidedOnCoastedFrame",
60270
+ type: "BOOLEAN",
60271
+ notNull: true
60272
+ },
60273
+ {
60274
+ name: "birthEvidenceAvailable",
60275
+ type: "BOOLEAN",
60276
+ notNull: true
60277
+ },
60278
+ {
60279
+ name: "decidedByBirthEvidence",
60280
+ type: "BOOLEAN",
60281
+ notNull: true
60282
+ },
60283
+ {
60284
+ name: "bestScore",
60285
+ type: "REAL"
60286
+ },
60287
+ {
60288
+ name: "appliedMinConfidence",
60289
+ type: "REAL"
60290
+ },
60291
+ {
60292
+ name: "firstSeen",
60293
+ type: "INTEGER"
60294
+ },
60295
+ {
60296
+ name: "motionOnsetAt",
60297
+ type: "INTEGER"
60298
+ },
60299
+ {
60300
+ name: "birthLatencyMs",
60301
+ type: "INTEGER"
60302
+ },
60303
+ {
60304
+ name: "birthIndexInBurst",
60305
+ type: "INTEGER"
60306
+ }
60307
+ ];
60308
+ var BIRTH_DECISION_LEDGER_INDEXES = [{
60309
+ name: "idx_birthdecision_at",
60310
+ columns: ["at"]
60311
+ }, {
60312
+ name: "idx_birthdecision_device_at",
60313
+ columns: ["deviceId", "at"]
60314
+ }];
60315
+ var BirthDecisionLedgerStore = class {
60316
+ store;
60317
+ logger;
60318
+ newId;
60319
+ maxRows;
60320
+ trimEvery;
60321
+ /** Rows appended since the last trim — see {@link trim}. */
60322
+ sinceTrim = 0;
60323
+ constructor(deps) {
60324
+ this.store = deps.store;
60325
+ this.logger = deps.logger;
60326
+ this.newId = deps.newId ?? (() => globalThis.crypto.randomUUID());
60327
+ this.maxRows = deps.maxRows ?? 3e4;
60328
+ this.trimEvery = deps.trimEvery ?? DEFAULT_TRIM_EVERY;
60329
+ }
60330
+ /** The ceiling a store built without an override enforces. */
60331
+ static defaultMaxRows() {
60332
+ return require_dist.MAX_BIRTH_DECISION_RECORDS;
60333
+ }
60334
+ static async declare(store) {
60335
+ await store.declareCollection.mutate({
60336
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60337
+ columns: [...BIRTH_DECISION_LEDGER_COLUMNS],
60338
+ indexes: [...BIRTH_DECISION_LEDGER_INDEXES]
60339
+ });
60340
+ }
60341
+ /**
60342
+ * Append one birth decision. NEVER THROWS.
60343
+ *
60344
+ * Best-effort, unlike the debug-note archive and deliberately so: that table
60345
+ * holds the only copy of something a PERSON typed, and this one holds a
60346
+ * measurement the system takes ~49 times an hour. A birth must not fail — nor
60347
+ * be delayed by a retry — because bookkeeping could not be written. The
60348
+ * failure is logged with `tags: { deviceId }` so a camera whose rows are
60349
+ * silently missing is attributable rather than an unexplained hole in the
60350
+ * denominator.
60351
+ */
60352
+ async append(draft) {
60353
+ try {
60354
+ const row = require_dist.BirthDecisionRecordSchema.parse({
60355
+ id: this.newId(),
60356
+ ...draft
60357
+ });
60358
+ await this.store.insert.mutate({
60359
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60360
+ record: {
60361
+ id: row.id,
60362
+ data: {
60363
+ at: row.at,
60364
+ deviceId: row.deviceId,
60365
+ sourceTrackId: row.sourceTrackId,
60366
+ className: row.className,
60367
+ verdict: row.verdict,
60368
+ reason: row.reason,
60369
+ attempts: row.attempts,
60370
+ deferredForMs: row.deferredForMs,
60371
+ decidedOnCoastedFrame: row.decidedOnCoastedFrame,
60372
+ birthEvidenceAvailable: row.birthEvidenceAvailable,
60373
+ decidedByBirthEvidence: row.decidedByBirthEvidence,
60374
+ bestScore: row.bestScore,
60375
+ appliedMinConfidence: row.appliedMinConfidence,
60376
+ firstSeen: row.firstSeen,
60377
+ motionOnsetAt: row.motionOnsetAt,
60378
+ birthLatencyMs: row.birthLatencyMs,
60379
+ birthIndexInBurst: row.birthIndexInBurst
60380
+ }
60381
+ }
60382
+ });
60383
+ this.sinceTrim += 1;
60384
+ await this.trim();
60385
+ } catch (err) {
60386
+ this.logger.warn("birth-decision ledger append failed — this decision is not counted", {
60387
+ tags: { deviceId: draft.deviceId },
60388
+ meta: {
60389
+ trackId: draft.sourceTrackId,
60390
+ verdict: draft.verdict,
60391
+ error: err instanceof Error ? err.message : String(err)
60392
+ }
60393
+ });
60394
+ }
60395
+ }
60396
+ /**
60397
+ * Hold the table at its ceiling, oldest first.
60398
+ *
60399
+ * NOT an age-keyed sweep and must never become one: the boundary is computed
60400
+ * from the POSITION of the excess rows, never from a clock. Rows sharing the
60401
+ * boundary timestamp go together, which at this write rate is a rounding
60402
+ * error against a 30 000-row ceiling.
60403
+ *
60404
+ * AMORTISED. The debug-note archive counts on every append because it writes
60405
+ * a few rows a week; this one writes ~49 an hour, and a `count` round trip per
60406
+ * birth would triple the ledger's own store traffic to answer a question whose
60407
+ * answer only changes on the row that crosses the ceiling. Checking once per
60408
+ * {@link DEFAULT_TRIM_EVERY} appends bounds the overshoot at that many rows —
60409
+ * 0.3% of the ceiling — for two hours' delay in enforcing it.
60410
+ */
60411
+ async trim() {
60412
+ if (this.sinceTrim < this.trimEvery) return;
60413
+ this.sinceTrim = 0;
60414
+ try {
60415
+ const held = await this.store.count.query({ collection: BIRTH_DECISION_LEDGER_COLLECTION });
60416
+ const excess = held - this.maxRows;
60417
+ if (excess <= 0) return;
60418
+ const boundary = (await this.store.query.query({
60419
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60420
+ filter: {
60421
+ orderBy: {
60422
+ field: "at",
60423
+ direction: "asc"
60424
+ },
60425
+ limit: excess
60426
+ },
60427
+ columns: ["at"]
60428
+ })).reduce((acc, r) => {
60429
+ const at = r.data["at"];
60430
+ return typeof at === "number" && at > acc ? at : acc;
60431
+ }, 0);
60432
+ if (boundary === 0) return;
60433
+ const { deleted } = await this.store.deleteWhere.mutate({
60434
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60435
+ filter: { whereBetween: { at: [0, boundary] } }
60436
+ });
60437
+ this.logger.info("birth-decision ledger trimmed to its ceiling", { meta: {
60438
+ held,
60439
+ ceiling: this.maxRows,
60440
+ deleted
60441
+ } });
60442
+ } catch (err) {
60443
+ this.logger.warn("birth-decision ledger trim failed — the ledger will keep growing", { meta: {
60444
+ ceiling: this.maxRows,
60445
+ error: err instanceof Error ? err.message : String(err)
60446
+ } });
60447
+ }
60448
+ }
60449
+ /** Birth decisions, newest first, optionally scoped by camera / verdict / age. */
60450
+ async list(query) {
60451
+ const where = {};
60452
+ if (query.deviceId !== void 0) where["deviceId"] = query.deviceId;
60453
+ if (query.verdict !== void 0) where["verdict"] = query.verdict;
60454
+ const filter = {
60455
+ orderBy: {
60456
+ field: "at",
60457
+ direction: "desc"
60458
+ },
60459
+ limit: query.limit ?? 500
60460
+ };
60461
+ if (Object.keys(where).length > 0) filter["where"] = where;
60462
+ if (query.since !== void 0) filter["whereBetween"] = { at: [query.since, Number.MAX_SAFE_INTEGER] };
60463
+ const rows = await this.store.query.query({
60464
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
60465
+ filter
60466
+ });
60467
+ const out = [];
60468
+ for (const r of rows) {
60469
+ const parsed = require_dist.BirthDecisionRecordSchema.safeParse({
60470
+ id: r.id,
60471
+ ...r.data
60472
+ });
60473
+ if (parsed.success) out.push(parsed.data);
60474
+ else this.logger.debug("birth-decision ledger: skipped a malformed row", { meta: { id: r.id } });
60475
+ }
60476
+ return out;
60477
+ }
60478
+ };
60479
+ /**
60480
+ * How many appends between ceiling checks, by default. See
60481
+ * {@link BirthDecisionLedgerStore.trim}.
60482
+ *
60483
+ * 100 rows is two hours of measured fleet traffic and 0.3% of the ceiling —
60484
+ * small enough that the table's real size never meaningfully exceeds its stated
60485
+ * bound, large enough that the ledger costs one `count` per two hours instead of
60486
+ * one per birth.
60487
+ */
60488
+ var DEFAULT_TRIM_EVERY = 100;
60489
+ //#endregion
59945
60490
  //#region src/pipeline-analytics/store/debug-note-archive-store.ts
59946
60491
  /**
59947
60492
  * DebugNoteArchiveStore — the durable corpus of what operators asked to be
@@ -61567,6 +62112,13 @@ var ANALYTICS_COLLECTIONS = [
61567
62112
  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."
61568
62113
  }
61569
62114
  },
62115
+ {
62116
+ collection: BIRTH_DECISION_LEDGER_COLLECTION,
62117
+ classification: {
62118
+ kind: "never-orphaned",
62119
+ 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."
62120
+ }
62121
+ },
61570
62122
  {
61571
62123
  collection: STATIONARY_COLLECTION,
61572
62124
  classification: {
@@ -69343,6 +69895,11 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69343
69895
  * operator asked for never depends on this addon's own bookkeeping.
69344
69896
  */
69345
69897
  debugNoteArchive = null;
69898
+ /** The durable birth-decision ledger (D409) — measurement, never a gate. */
69899
+ birthDecisionLedger = null;
69900
+ /** Has the "no ledger on this runner" warning already been said? Once per
69901
+ * process: the condition is a property of the runner, not of the birth. */
69902
+ birthLedgerAbsenceLogged = false;
69346
69903
  /** Event-media relocation engine (entity-routing Phase 4). */
69347
69904
  mediaRelocate = null;
69348
69905
  mediaLocationStorage = null;
@@ -69781,6 +70338,14 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
69781
70338
  /** Births the gate could not MEASURE, awaiting another look on a later frame. */
69782
70339
  deferredBirths = new DeferredBirthRegistry();
69783
70340
  /**
70341
+ * The last motion RISING EDGE per device — the reference instant the
70342
+ * birth-latency proxy is measured from (D409). Measurement only; nothing
70343
+ * gates on it. Fed by the two motion handlers, which are subscribed on the
70344
+ * SAME designated post-processing node as the frame path, so a camera whose
70345
+ * births land here always has its motion mirror here too.
70346
+ */
70347
+ motionOnsets = new MotionOnsetRegistry();
70348
+ /**
69784
70349
  * The pixels each deferred birth arrived with — a RESERVE per camera, evicted
69785
70350
  * by age inside that camera's own quota (D379).
69786
70351
  *
@@ -70335,6 +70900,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70335
70900
  store: api.settingsStore,
70336
70901
  logger: logger.child("debug-note-archive")
70337
70902
  });
70903
+ this.birthDecisionLedger = new BirthDecisionLedgerStore({
70904
+ store: api.settingsStore,
70905
+ logger: logger.child("birth-decision-ledger")
70906
+ });
70338
70907
  {
70339
70908
  const designated = await step("postProcessingNodeState", () => this.postProcessingNodeState.get());
70340
70909
  this.isPostProcessingNode = ownNodeId === designated;
@@ -70655,6 +71224,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
70655
71224
  RetrainAnnotationStore.declare(api.settingsStore),
70656
71225
  OpsLogStore.declare(api.settingsStore),
70657
71226
  DebugNoteArchiveStore.declare(api.settingsStore),
71227
+ BirthDecisionLedgerStore.declare(api.settingsStore),
70658
71228
  AnalyticsLts.declare(api.settingsStore),
70659
71229
  SceneStore.declare(api.settingsStore),
70660
71230
  NotificationCenter.declare(api.settingsStore),
@@ -71126,18 +71696,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71126
71696
  },
71127
71697
  getKeyFrameMediaKey: (trackId) => this.residents.keyFrameKey(trackId),
71128
71698
  emitFaceGalleryChanged: (payload) => this.emitFaceGalleryChanged(payload),
71129
- onSemanticMatch: (match) => this.promoteSemanticRecognition({
71130
- deviceId: match.deviceId,
71131
- trackId: match.trackId,
71132
- timestamp: match.timestamp,
71133
- bbox: match.bbox,
71134
- frameWidth: match.frameWidth,
71135
- frameHeight: match.frameHeight,
71136
- kind: "face",
71137
- score: match.matchScore,
71138
- confidence: match.confidence,
71139
- ...match.frameHandle !== void 0 ? { frameHandle: match.frameHandle } : {}
71140
- }),
71699
+ onSemanticFace: (face) => this.promoteSemanticRecognition({
71700
+ deviceId: face.deviceId,
71701
+ trackId: face.trackId,
71702
+ timestamp: face.timestamp,
71703
+ bbox: face.bbox,
71704
+ frameWidth: face.frameWidth,
71705
+ frameHeight: face.frameHeight,
71706
+ kind: face.identityId !== void 0 ? "face" : "face-visible",
71707
+ score: face.matchScore ?? face.confidence,
71708
+ confidence: face.confidence,
71709
+ ...face.frameHandle !== void 0 ? { frameHandle: face.frameHandle } : {}
71710
+ }, face.identityId !== void 0 ? "identified" : "evidence"),
71141
71711
  onLiveHierarchy: (input) => this.requestTrackHierarchy(input),
71142
71712
  logger: logger.child("FaceRecognizer")
71143
71713
  });
@@ -72319,6 +72889,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
72319
72889
  this.overlaySynthesisWarnAt.delete(deviceId);
72320
72890
  this.forgetDeviceProcessors(deviceId);
72321
72891
  this.levelStateByDevice.delete(deviceId);
72892
+ this.motionOnsets.forgetDevice(deviceId);
72322
72893
  this.audioConfirmByDevice.delete(deviceId);
72323
72894
  this.lastTrackActivityMs.delete(deviceId);
72324
72895
  this.motionEventSnapshots?.forgetDevice(deviceId);
@@ -73105,8 +73676,10 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73105
73676
  if (t.birthEvidence === void 0) continue;
73106
73677
  this.birthEvidence.remember(key, id, Buffer.from(t.birthEvidence.jpegBase64, "base64"), exhaustionNowMs);
73107
73678
  }
73108
- const outcome = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
73679
+ const withBirthEvidence = /* @__PURE__ */ new Set();
73680
+ const run = await this.confirmTrackBirths(bornCandidates.map(({ id, t }) => {
73109
73681
  const evidenceCrop = this.birthEvidence.get(key, id, exhaustionNowMs);
73682
+ if (evidenceCrop !== null) withBirthEvidence.add(id);
73110
73683
  return {
73111
73684
  trackId: id,
73112
73685
  className: t.className,
@@ -73121,6 +73694,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73121
73694
  frameWidth: result.frameWidth,
73122
73695
  frameHeight: result.frameHeight
73123
73696
  }, exhaustedIds);
73697
+ const outcome = run.outcome;
73124
73698
  const gateNowMs = Date.now();
73125
73699
  for (const { id, t } of bornCandidates) {
73126
73700
  if (outcome.undecided.has(id)) {
@@ -73149,6 +73723,20 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73149
73723
  } });
73150
73724
  continue;
73151
73725
  }
73726
+ this.recordBirthDecision({
73727
+ deviceId,
73728
+ trackId: id,
73729
+ className: t.className,
73730
+ confirmed: outcome.confirmed.has(id),
73731
+ exhaustedFallback: wasDeferred && exhaustedIds.has(id),
73732
+ attempts: deferredAttempts,
73733
+ deferredForMs,
73734
+ matchedThisFrame: t.matchedThisFrame,
73735
+ birthEvidenceAvailable: withBirthEvidence.has(id),
73736
+ decision: run.decisions.get(id),
73737
+ decidedAtMs: gateNowMs,
73738
+ firstSeen: this.trackStore?.peekActive(id)?.firstSeen ?? null
73739
+ });
73152
73740
  if (!outcome.confirmed.has(id)) {
73153
73741
  this.suppressedBirths.reject(key, id);
73154
73742
  this.trackStore?.dropActive(id);
@@ -73847,10 +74435,64 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73847
74435
  * candidate) and each failure path fails OPEN, so the synchronous frame path
73848
74436
  * is never blocked or reordered by a slow/failed re-detection.
73849
74437
  */
74438
+ /**
74439
+ * Record ONE resolved birth verdict in the durable ledger (D409).
74440
+ *
74441
+ * Synchronous up to the append, which is fired and forgotten: the frame path
74442
+ * awaits `confirmTrackBirths` already, and a measurement must never add a
74443
+ * store round trip to the latency of a birth. `BirthDecisionLedgerStore.append`
74444
+ * never throws, so the un-awaited promise cannot reject.
74445
+ *
74446
+ * Cost, per BIRTH and not per frame: one `Map.get`, one `Set.has`, a counter
74447
+ * bump and one INSERT. Measured fleet-wide on the live hub 2026-09-08 at ~49
74448
+ * births/hour, i.e. 0.014 writes/s — three orders of magnitude below the
74449
+ * 12-19/s the runtime-state guard was written for, and a different shape
74450
+ * (append-only, never a periodic rewrite of an unchanged slice).
74451
+ *
74452
+ * A runner with no ledger (construction failed) records nothing and says so
74453
+ * exactly once per process, rather than per birth: a measurement that cannot
74454
+ * be taken must be visible, but it must not become the noisiest line in Loki.
74455
+ */
74456
+ recordBirthDecision(input) {
74457
+ const ledger = this.birthDecisionLedger;
74458
+ if (ledger === null) {
74459
+ if (!this.birthLedgerAbsenceLogged) {
74460
+ this.birthLedgerAbsenceLogged = true;
74461
+ this.ctx.logger.warn("birth decisions are NOT being measured — no ledger on this runner", { tags: { deviceId: input.deviceId } });
74462
+ }
74463
+ return;
74464
+ }
74465
+ const motionOnsetAtMs = this.motionOnsets.onsetFor(input.deviceId);
74466
+ const birthIndexInBurst = this.motionOnsets.noteBirth(input.deviceId);
74467
+ const decision = input.decision;
74468
+ ledger.append(buildBirthDecisionRecord({
74469
+ deviceId: input.deviceId,
74470
+ trackId: input.trackId,
74471
+ className: input.className,
74472
+ confirmed: input.confirmed,
74473
+ exhaustedFallback: input.exhaustedFallback,
74474
+ attempts: input.attempts,
74475
+ deferredForMs: input.deferredForMs,
74476
+ ...input.matchedThisFrame !== void 0 ? { matchedThisFrame: input.matchedThisFrame } : {},
74477
+ birthEvidenceAvailable: input.birthEvidenceAvailable,
74478
+ decidedByBirthEvidence: decision?.cropSource === "carried",
74479
+ reason: decision?.reason ?? null,
74480
+ bestScore: decision?.bestScore ?? null,
74481
+ appliedMinConfidence: decision?.appliedMinConfidence ?? null,
74482
+ decidedAtMs: input.decidedAtMs,
74483
+ firstSeen: input.firstSeen,
74484
+ motionOnsetAtMs,
74485
+ birthIndexInBurst
74486
+ }));
74487
+ }
73850
74488
  async confirmTrackBirths(candidates, params, exhaustedIds = /* @__PURE__ */ new Set()) {
74489
+ const decisions = /* @__PURE__ */ new Map();
73851
74490
  const allConfirmed = () => ({
73852
- confirmed: new Set(candidates.map((c) => c.trackId)),
73853
- undecided: /* @__PURE__ */ new Set()
74491
+ outcome: {
74492
+ confirmed: new Set(candidates.map((c) => c.trackId)),
74493
+ undecided: /* @__PURE__ */ new Set()
74494
+ },
74495
+ decisions
73854
74496
  });
73855
74497
  if (candidates.length === 0) return allConfirmed();
73856
74498
  const config = await this.resolveDeviceConfirmationGateSettings(params.deviceId);
@@ -73873,101 +74515,105 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
73873
74515
  const nodeId = frameHandle.nodeId;
73874
74516
  const candidateNativeRecovery = /* @__PURE__ */ new Map();
73875
74517
  for (const c of candidates) if (c.nativeRecovery !== void 0) candidateNativeRecovery.set(c.trackId, c.nativeRecovery);
73876
- return confirmBirths(candidates, config, {
73877
- fetchCrop: (candidate) => this.captureScheduler.request({
73878
- deviceId,
73879
- trackId: candidate.trackId,
73880
- kind: "confirm",
73881
- staleAfterMs: config.timeoutMs,
73882
- exec: () => captureCrop(frameHandle, {
74518
+ return {
74519
+ outcome: await confirmBirths(candidates, config, {
74520
+ fetchCrop: (candidate) => this.captureScheduler.request({
74521
+ deviceId,
74522
+ trackId: candidate.trackId,
74523
+ kind: "confirm",
74524
+ staleAfterMs: config.timeoutMs,
74525
+ exec: () => captureCrop(frameHandle, {
74526
+ x: candidate.bbox.x,
74527
+ y: candidate.bbox.y,
74528
+ w: candidate.bbox.w,
74529
+ h: candidate.bbox.h
74530
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, 320, deviceId)
74531
+ }),
74532
+ fetchFallbackCrop: (candidate) => {
74533
+ const displayCrop = this.captureDisplayCropFn;
74534
+ if (!displayCrop) return Promise.resolve(null);
74535
+ return displayCrop(frameHandle, {
74536
+ x: candidate.bbox.x,
74537
+ y: candidate.bbox.y,
74538
+ w: candidate.bbox.w,
74539
+ h: candidate.bbox.h
74540
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
74541
+ },
74542
+ redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
74543
+ cropRectFor: (candidate) => captureCropFrameRect({
73883
74544
  x: candidate.bbox.x,
73884
74545
  y: candidate.bbox.y,
73885
74546
  w: candidate.bbox.w,
73886
74547
  h: candidate.bbox.h
73887
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, 320, deviceId)
73888
- }),
73889
- fetchFallbackCrop: (candidate) => {
73890
- const displayCrop = this.captureDisplayCropFn;
73891
- if (!displayCrop) return Promise.resolve(null);
73892
- return displayCrop(frameHandle, {
74548
+ }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
74549
+ minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
74550
+ deviceId,
74551
+ className: candidate.className,
73893
74552
  x: candidate.bbox.x,
73894
74553
  y: candidate.bbox.y,
73895
- w: candidate.bbox.w,
73896
- h: candidate.bbox.h
73897
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING, candidate.trackId, 320);
73898
- },
73899
- redetect: (cropJpeg) => this.redetectCropForConfirmation(nodeId, deviceId, cropJpeg),
73900
- cropRectFor: (candidate) => captureCropFrameRect({
73901
- x: candidate.bbox.x,
73902
- y: candidate.bbox.y,
73903
- w: candidate.bbox.w,
73904
- h: candidate.bbox.h
73905
- }, frameWidth, frameHeight, CONFIRMATION_CROP_PADDING),
73906
- minConfidenceFor: (candidate) => this.phantomCells?.spawnBar({
73907
- deviceId,
73908
- className: candidate.className,
73909
- x: candidate.bbox.x,
73910
- y: candidate.bbox.y,
73911
- base: config.minConfidence,
73912
- now: Date.now()
73913
- }).bar ?? config.minConfidence,
73914
- onDecision: (decision) => {
73915
- const meta = {
73916
- trackId: decision.trackId,
73917
- reason: decision.reason,
73918
- className: decision.className,
73919
- ...decision.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
73920
- ...decision.bestIncompatibleClass !== void 0 ? {
73921
- bestIncompatibleClass: decision.bestIncompatibleClass,
73922
- bestIncompatibleScore: decision.bestIncompatibleScore
73923
- } : {},
73924
- minConfidence: config.minConfidence,
73925
- ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
73926
- ...decision.reason === "native-pass" ? {
73927
- nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
73928
- nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
73929
- } : {},
73930
- ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
73931
- appliedMinConfidence: decision.appliedMinConfidence,
73932
- barRaisedByPhantomCell: true
73933
- } : {}
73934
- };
73935
- switch (decision.verdict) {
73936
- case "suppressed":
73937
- this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
73938
- tags: { deviceId },
73939
- meta
73940
- });
73941
- break;
73942
- case "undecided":
73943
- this.ctx.logger.info("confirmation gate: birth undecided", {
73944
- tags: { deviceId },
73945
- meta
73946
- });
73947
- break;
73948
- case "confirmed":
73949
- this.ctx.logger.info("confirmation gate: birth confirmed", {
74554
+ base: config.minConfidence,
74555
+ now: Date.now()
74556
+ }).bar ?? config.minConfidence,
74557
+ onDecision: (decision) => {
74558
+ decisions.set(decision.trackId, decision);
74559
+ const meta = {
74560
+ trackId: decision.trackId,
74561
+ reason: decision.reason,
74562
+ className: decision.className,
74563
+ ...decision.bestScore !== void 0 ? { bestScore: decision.bestScore } : {},
74564
+ ...decision.bestIncompatibleClass !== void 0 ? {
74565
+ bestIncompatibleClass: decision.bestIncompatibleClass,
74566
+ bestIncompatibleScore: decision.bestIncompatibleScore
74567
+ } : {},
74568
+ minConfidence: config.minConfidence,
74569
+ ...decision.cropSource !== void 0 ? { cropSource: decision.cropSource } : {},
74570
+ ...decision.reason === "native-pass" ? {
74571
+ nativeRecoveryCropSidePx: candidateNativeRecovery.get(decision.trackId)?.cropSidePx,
74572
+ nativeRecoveryViewPx: candidateNativeRecovery.get(decision.trackId)?.viewPx
74573
+ } : {},
74574
+ ...decision.appliedMinConfidence !== void 0 && decision.appliedMinConfidence > config.minConfidence ? {
74575
+ appliedMinConfidence: decision.appliedMinConfidence,
74576
+ barRaisedByPhantomCell: true
74577
+ } : {}
74578
+ };
74579
+ switch (decision.verdict) {
74580
+ case "suppressed":
74581
+ this.ctx.logger.info("confirmation gate: birth suppressed (false positive)", {
74582
+ tags: { deviceId },
74583
+ meta
74584
+ });
74585
+ break;
74586
+ case "undecided":
74587
+ this.ctx.logger.info("confirmation gate: birth undecided", {
74588
+ tags: { deviceId },
74589
+ meta
74590
+ });
74591
+ break;
74592
+ case "confirmed":
74593
+ this.ctx.logger.info("confirmation gate: birth confirmed", {
74594
+ tags: { deviceId },
74595
+ meta
74596
+ });
74597
+ break;
74598
+ }
74599
+ if (decision.secondary !== void 0) {
74600
+ this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
74601
+ this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
73950
74602
  tags: { deviceId },
73951
- meta
74603
+ meta: {
74604
+ trackId: decision.trackId,
74605
+ className: decision.className,
74606
+ secondaryClass: decision.secondary.className,
74607
+ secondaryScore: decision.secondary.score,
74608
+ secondaryBbox: decision.secondary.bbox,
74609
+ verdict: decision.verdict
74610
+ }
73952
74611
  });
73953
- break;
73954
- }
73955
- if (decision.secondary !== void 0) {
73956
- this.secondarySubjects.add(this.procKey(deviceId, source), decision.secondary, decision.trackId, Date.now());
73957
- this.ctx.logger.info("confirmation gate: secondary subject promoted from the crop", {
73958
- tags: { deviceId },
73959
- meta: {
73960
- trackId: decision.trackId,
73961
- className: decision.className,
73962
- secondaryClass: decision.secondary.className,
73963
- secondaryScore: decision.secondary.score,
73964
- secondaryBbox: decision.secondary.bbox,
73965
- verdict: decision.verdict
73966
- }
73967
- });
74612
+ }
73968
74613
  }
73969
- }
73970
- }, exhaustedIds);
74614
+ }, exhaustedIds),
74615
+ decisions
74616
+ };
73971
74617
  }
73972
74618
  async resolveGlobalFaceEnabled() {
73973
74619
  return this.faceGlobalEnabledCache.get(() => this.faceGlobalEnabledState.get());
@@ -74153,10 +74799,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74153
74799
  } } : {}
74154
74800
  });
74155
74801
  }
74802
+ if (detailSemanticTrigger(d.className) === "recognition") this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
74156
74803
  }
74157
74804
  const resolved = d.className === "plate" ? this.plateRecognizer?.resolveLabelWithVehicle(d.label, d.score) ?? null : { text: d.label };
74158
74805
  if (resolved !== null && resolved.text !== void 0) {
74159
- if ((await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId)).trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame));
74806
+ const applied = await this.applyTrackEnrichmentLabel(deviceId, trackId, resolved.text, d, frame.timestamp, resolved.vehicleId);
74807
+ if (detailSemanticTrigger(d.className) === "label-write" && applied.trackWritten) this.promoteSemanticRecognition(semanticFrameForDetailAttribution(deviceId, trackId, d, frame), "identified");
74160
74808
  }
74161
74809
  } else if (d.className === "plate") this.failureReport.notePlateRead(deviceId, d.bbox === void 0 ? REASON_NO_BBOX : REASON_EMPTY_READ);
74162
74810
  } catch (err) {
@@ -74597,8 +75245,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
74597
75245
  * when it wins and the frame's pixels are still reachable. Callers: the
74598
75246
  * FaceRecognizer's per-frame auto-match, and `routeDetailResults`' plate /
74599
75247
  * fine-classification attribution. */
74600
- promoteSemanticRecognition(input) {
74601
- this.recognizedTrackIds.add(input.trackId);
75248
+ promoteSemanticRecognition(input, origin) {
75249
+ if (origin === "identified") this.recognizedTrackIds.add(input.trackId);
74602
75250
  const ctx = this.ctxIfReady;
74603
75251
  if (ctx === null) return;
74604
75252
  promoteSemanticBestFrame({
@@ -75297,6 +75945,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75297
75945
  detected,
75298
75946
  atMs: timestamp
75299
75947
  });
75948
+ if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
75300
75949
  this.sceneEngine?.noteMotion(deviceId, timestamp);
75301
75950
  await this.eventStore.insertMotion(ev);
75302
75951
  this.ctx.eventBus.emit({
@@ -75372,6 +76021,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
75372
76021
  detected,
75373
76022
  atMs: timestamp
75374
76023
  });
76024
+ if (transitionedOn) this.motionOnsets.noteRisingEdge(deviceId, timestamp);
75375
76025
  this.sceneEngine?.noteMotion(deviceId, timestamp);
75376
76026
  await this.eventStore.insertMotion(ev);
75377
76027
  this.ctx.eventBus.emit({
@@ -78331,6 +78981,18 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
78331
78981
  return await this.debugNoteArchive?.list(input) ?? [];
78332
78982
  }
78333
78983
  /**
78984
+ * The birth-decision ledger, newest first (D409).
78985
+ *
78986
+ * A READ of a measurement table and nothing else — no join back to the
78987
+ * tracks, deliberately: a SUPPRESSED birth never had a track row and a
78988
+ * confirmed one is expected to have aged out, so a join would return exactly
78989
+ * the rows the ledger exists to preserve. A runner with no ledger answers
78990
+ * EMPTY, which is true: nothing has been measured here.
78991
+ */
78992
+ async listBirthDecisions(input) {
78993
+ return await this.birthDecisionLedger?.list(input) ?? [];
78994
+ }
78995
+ /**
78334
78996
  * Track-centric time-based retention (design §5.1). Drains every persisted
78335
78997
  * track for the device whose `lastSeen < cutoffMs`, page by page, through the
78336
78998
  * widened cascade — enrolled faces/plates + identity media are exempt (design