@camstack/addon-post-analysis 1.2.128 → 1.2.130

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-D6O14xf_.js");
5
+ const require_dist = require("../dist-CDHL-0gR.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");
@@ -2446,6 +2446,45 @@ var NcArtifactPlane = class {
2446
2446
  }
2447
2447
  }
2448
2448
  /**
2449
+ * Mint a FRESH signed URL for an artefact already sitting on the shelf —
2450
+ * no bytes read, no re-store. The other half of {@link publishIndexed}: that
2451
+ * one stores AND mints once at publish time; this one re-mints on demand for
2452
+ * an id a caller already holds (a history row's `artifactIds`), because the
2453
+ * link `publishIndexed` returned has its own TTL and a caller reaching for
2454
+ * the artefact hours later needs a new one.
2455
+ *
2456
+ * `null` on any of three causes, deliberately indistinguishable to the
2457
+ * caller: no index is wired (nothing to resolve against), the id is unknown
2458
+ * or its artefact has expired past the retained shelf's own age bound, or no
2459
+ * externally-reachable base URL exists for this install. A caller (the Home
2460
+ * Assistant export) must render all three as "no image right now" — never as
2461
+ * a broken link, and never by reusing a URL this call refused to mint.
2462
+ */
2463
+ async mintUrl(artifactId) {
2464
+ const index = this.deps.index;
2465
+ if (index === void 0) return null;
2466
+ try {
2467
+ const record = await index.resolve(artifactId);
2468
+ if (record === null) return null;
2469
+ if (record.expiresAt <= this.now()) return null;
2470
+ const baseUrl = await this.resolveBaseUrl();
2471
+ if (baseUrl === null) return null;
2472
+ return buildArtifactUrl({
2473
+ baseUrl,
2474
+ routePrefix: this.deps.routePrefix,
2475
+ id: artifactId,
2476
+ secret: this.deps.secret,
2477
+ expMs: this.now() + (this.deps.ttlMs ?? DEFAULT_TTL_MS)
2478
+ });
2479
+ } catch (err) {
2480
+ this.deps.logger.debug("artifact url mint failed", { meta: {
2481
+ artifactId,
2482
+ error: String(err)
2483
+ } });
2484
+ return null;
2485
+ }
2486
+ }
2487
+ /**
2449
2488
  * Absolutize a path against the SAME base URL an artifact link would use.
2450
2489
  *
2451
2490
  * For links this plane does not host but a RECIPIENT still has to open — a
@@ -8932,7 +8971,7 @@ var NcDispatcher = class {
8932
8971
  return { ok: true };
8933
8972
  }
8934
8973
  }
8935
- const notification = await this.buildNotification(entry, target);
8974
+ const { notification, artifactIds } = await this.buildNotification(entry, target);
8936
8975
  const verdict = await this.runConfirmGate(entry, notification);
8937
8976
  if (verdict !== null && verdict.decision === "suppressed") {
8938
8977
  this.deps.logger.info("notification suppressed by AI confirm", {
@@ -8990,10 +9029,11 @@ var NcDispatcher = class {
8990
9029
  ...dropped.length > 0 ? { dropped } : {}
8991
9030
  }
8992
9031
  });
8993
- return verdict !== null ? {
9032
+ return {
8994
9033
  ok: true,
8995
- confirm: verdict
8996
- } : { ok: true };
9034
+ ...verdict !== null ? { confirm: verdict } : {},
9035
+ ...artifactIds.length > 0 ? { artifactIds } : {}
9036
+ };
8997
9037
  } catch (err) {
8998
9038
  return {
8999
9039
  ok: false,
@@ -9304,6 +9344,12 @@ var NcDispatcher = class {
9304
9344
  });
9305
9345
  return input.frozen ?? "";
9306
9346
  }
9347
+ /**
9348
+ * Builds the notification AND returns the artefact ids its media resolve
9349
+ * indexed — see {@link withArtifactUrls}. A tuple rather than a field on
9350
+ * `NcNotificationInput`: the ids are internal (outbox provenance), never
9351
+ * part of what `notification-output` sends to a backend.
9352
+ */
9307
9353
  async buildNotification(entry, target) {
9308
9354
  const subject = entry.payload.subject;
9309
9355
  const systemEvent = subject.systemEvent;
@@ -9332,7 +9378,8 @@ var NcDispatcher = class {
9332
9378
  vars: { body: rendered },
9333
9379
  frozen: rendered
9334
9380
  }) : rendered;
9335
- const attachments = [...await this.withArtifactUrls(entry, await this.resolveAttachments(entry)), ...frozenArtifactAttachments(entry)];
9381
+ const resolvedMedia = await this.withArtifactUrls(entry, await this.resolveAttachments(entry));
9382
+ const attachments = [...resolvedMedia.attachments, ...frozenArtifactAttachments(entry)];
9336
9383
  const params = pickParams(entry.payload.params);
9337
9384
  const tapUrl = params.clickUrl === void 0 && this.deps.viewerUrlBase !== void 0 ? defaultTapUrl({
9338
9385
  deviceId: systemEvent !== void 0 ? 0 : subject.deviceId,
@@ -9342,21 +9389,24 @@ var NcDispatcher = class {
9342
9389
  }) : null;
9343
9390
  const actions = await this.mintActions(entry, target);
9344
9391
  return {
9345
- body,
9346
- title,
9347
- priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
9348
- ...params,
9349
- ...tapUrl !== null ? { clickUrl: tapUrl } : {},
9350
- tag: entry.payload.group?.key ?? entry.ruleId,
9351
- ...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
9352
- ...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
9353
- ...systemEvent !== void 0 ? { metadata: {
9354
- systemEventKind: systemEvent.kind,
9355
- subject: systemEvent.subject,
9356
- ...systemEvent.data
9357
- } } : {},
9358
- ...attachments.length > 0 ? { attachments } : {},
9359
- ...actions.length > 0 ? { actions } : {}
9392
+ notification: {
9393
+ body,
9394
+ title,
9395
+ priority: clampPriority(paramNumber(entry.payload.params, "priority") ?? entry.payload.priority),
9396
+ ...params,
9397
+ ...tapUrl !== null ? { clickUrl: tapUrl } : {},
9398
+ tag: entry.payload.group?.key ?? entry.ruleId,
9399
+ ...systemEvent !== void 0 ? systemEvent.deviceId !== void 0 ? { deviceId: systemEvent.deviceId } : {} : { deviceId: subject.deviceId },
9400
+ ...subject.eventId !== void 0 ? { eventId: subject.eventId } : {},
9401
+ ...systemEvent !== void 0 ? { metadata: {
9402
+ systemEventKind: systemEvent.kind,
9403
+ subject: systemEvent.subject,
9404
+ ...systemEvent.data
9405
+ } } : {},
9406
+ ...attachments.length > 0 ? { attachments } : {},
9407
+ ...actions.length > 0 ? { actions } : {}
9408
+ },
9409
+ artifactIds: resolvedMedia.artifactIds
9360
9410
  };
9361
9411
  }
9362
9412
  /**
@@ -9400,11 +9450,28 @@ var NcDispatcher = class {
9400
9450
  }
9401
9451
  });
9402
9452
  }
9453
+ /**
9454
+ * What one delivery's media resolve produced: the attachments ready to ship
9455
+ * (bytes plus a signed url when the plane could mint one) and the ids the
9456
+ * artefact INDEX assigned them, in the same order — still first, then gif,
9457
+ * then clip. The ids never ride on the attachment objects themselves: those
9458
+ * cross to `notification-output` and a notifier backend has no use for an
9459
+ * internal store id, only for `url`/`bytes`.
9460
+ *
9461
+ * `artifactIds` is what lets a delivery's outbox row later answer "which
9462
+ * artefact was this trigger's picture" (`getHistory.artifactIds`) without a
9463
+ * second write path — the outbox writes back exactly the ids this method
9464
+ * handed to a SUCCESSFUL send, never re-deriving them.
9465
+ */
9403
9466
  async withArtifactUrls(entry, attachments) {
9404
9467
  const index = this.deps.indexArtifact;
9405
9468
  const publish = this.deps.publishArtifact;
9406
- if (index === void 0 && publish === void 0) return [...attachments];
9469
+ if (index === void 0 && publish === void 0) return {
9470
+ attachments: [...attachments],
9471
+ artifactIds: []
9472
+ };
9407
9473
  const out = [];
9474
+ const artifactIds = [];
9408
9475
  let posterId = null;
9409
9476
  const deviceId = entry.payload.subject.deviceId;
9410
9477
  const common = {
@@ -9427,6 +9494,7 @@ var NcDispatcher = class {
9427
9494
  artifactId: null
9428
9495
  }));
9429
9496
  if (kind === "event-still" && pub.artifactId !== null) posterId = pub.artifactId;
9497
+ if (pub.artifactId !== null) artifactIds.push(pub.artifactId);
9430
9498
  out.push(pub.url === null ? { ...att } : {
9431
9499
  ...att,
9432
9500
  url: pub.url
@@ -9439,7 +9507,10 @@ var NcDispatcher = class {
9439
9507
  url
9440
9508
  });
9441
9509
  }
9442
- return out;
9510
+ return {
9511
+ attachments: out,
9512
+ artifactIds
9513
+ };
9443
9514
  }
9444
9515
  /** The full attachment list: the policy still (zone-cropped when the rule
9445
9516
  * froze zone ids) plus the footage cut from the broker's clip ring when
@@ -12543,13 +12614,17 @@ var NcOutbox = class {
12543
12614
  }
12544
12615
  const now = this.now();
12545
12616
  if (result.ok) {
12617
+ const payloadPatch = {
12618
+ ...result.confirm !== void 0 ? { confirmVerdict: result.confirm } : {},
12619
+ ...result.artifactIds !== void 0 && result.artifactIds.length > 0 ? { artifactIds: result.artifactIds } : {}
12620
+ };
12546
12621
  const sent = {
12547
12622
  ...entry,
12548
12623
  status: "sent",
12549
12624
  updatedAt: now,
12550
- ...result.confirm !== void 0 ? { payload: {
12625
+ ...Object.keys(payloadPatch).length > 0 ? { payload: {
12551
12626
  ...entry.payload,
12552
- confirmVerdict: result.confirm
12627
+ ...payloadPatch
12553
12628
  } } : {}
12554
12629
  };
12555
12630
  this.pending.delete(entry.id);
@@ -19583,7 +19658,8 @@ function outboxEntryToHistory(texts, entry) {
19583
19658
  zones: [...s.zones],
19584
19659
  timestamp: s.timestamp,
19585
19660
  ...s.systemEvent !== void 0 ? { systemEvent: historySystemEvent(texts, s.systemEvent) } : {}
19586
- }
19661
+ },
19662
+ ...entry.payload.artifactIds !== void 0 && entry.payload.artifactIds.length > 0 ? { artifactIds: [...entry.payload.artifactIds] } : {}
19587
19663
  };
19588
19664
  }
19589
19665
  /** The watched question in one grep-able token. */
@@ -21122,6 +21198,7 @@ var NotificationCenter = class NotificationCenter {
21122
21198
  }
21123
21199
  return { entries: mapped };
21124
21200
  },
21201
+ resolveArtifactUrl: async ({ artifactId }) => ({ url: await this.deps.resolveArtifactUrl?.(artifactId) ?? null }),
21125
21202
  listDeviceMutes: async () => ({ mutedDeviceIds: this.deviceMutes.listMuted() }),
21126
21203
  setDeviceMuted: async ({ deviceId, muted }) => {
21127
21204
  await this.deviceMutes.setMuted(deviceId, muted);
@@ -27217,6 +27294,18 @@ var FaceGalleryProvider = class {
27217
27294
  * This module names that state (`model-mismatch`) so the UI can say "this face
27218
27295
  * is in an old feature space" instead of showing 0% and letting the operator
27219
27296
  * conclude the person is not there.
27297
+ *
27298
+ * ## `buildFaceRescoreReport` stays pure — the CALLER persists
27299
+ *
27300
+ * This function never touches the store; every input is passed in and nothing
27301
+ * is read or written (see its own doc below). The operator asked for this
27302
+ * exact recompute, though, and wants the answer to stick — so the ACTION that
27303
+ * wraps this report (`rescoreFaceForTrack` in `pipeline-analytics/index.ts`)
27304
+ * writes the result onto `Face.suggestedIdentityId` / `suggestedMatchScore`,
27305
+ * through `decideRescorePersistence` / `persistRescoreResult` below. That is
27306
+ * still not a background job — it is the SAME explicitly-requested call,
27307
+ * writing once. A `model-mismatch` report (`comparable: false`) is never
27308
+ * written: see `persistRescoreResult`.
27220
27309
  */
27221
27310
  /** The empty report every "cannot score" path returns, so shape never varies. */
27222
27311
  function emptyReport(verdict, input, comparable) {
@@ -27292,6 +27381,52 @@ function buildFaceRescoreReport(input) {
27292
27381
  ...best !== void 0 && second !== void 0 ? { runnerUpGap: best.score - second.score } : {}
27293
27382
  };
27294
27383
  }
27384
+ /**
27385
+ * What `face.rescoreTrack` should PERSIST for a COMPARABLE report, decided as
27386
+ * its own pure function so the rule is directly testable — separated from the
27387
+ * class that owns the store I/O (`rescoreFaceForTrack` in
27388
+ * `pipeline-analytics/index.ts`), which only wires this answer to
27389
+ * `FaceStore.setSuggestion`.
27390
+ *
27391
+ * `null` covers every verdict OTHER than `auto-assignable` / `suggestion`, and
27392
+ * also an `auto-assignable` / `suggestion` verdict whose best-ranked row is
27393
+ * not itself ELIGIBLE (`identities` is ranked by raw score across every
27394
+ * comparable identity, eligible or not — see `buildFaceRescoreReport`). Every
27395
+ * one of those is a genuine "nothing to suggest right now": never an
27396
+ * assignment, and never the winner of an `ambiguous` verdict — the matcher
27397
+ * refused to pick one, so this function does not pick one either.
27398
+ *
27399
+ * Callers MUST gate on `report.comparable` before calling this — a
27400
+ * `model-mismatch` report has no verdict worth acting on; see
27401
+ * {@link persistRescoreResult}, which does that gating and is the one thing
27402
+ * `rescoreFaceForTrack` actually calls.
27403
+ */
27404
+ function decideRescorePersistence(report) {
27405
+ if (report.verdict !== "auto-assignable" && report.verdict !== "suggestion") return null;
27406
+ const candidate = report.identities.find((row) => row.eligible);
27407
+ return candidate === void 0 ? null : {
27408
+ identityId: candidate.identityId,
27409
+ score: candidate.score
27410
+ };
27411
+ }
27412
+ /**
27413
+ * Persist `face.rescoreTrack`'s result onto the SUGGESTED tier — the ONE
27414
+ * write this explicitly-requested action makes, and the only place that
27415
+ * decides whether it happens at all.
27416
+ *
27417
+ * A non-comparable report (`model-mismatch`) short-circuits BEFORE
27418
+ * `ports.setSuggestion` is ever called: a cosine across model spaces is
27419
+ * well-formed noise, and persisting it would look exactly like a fresh,
27420
+ * meaningful read to every future reader of the row. This is the single most
27421
+ * load-bearing branch in this file — the regression it guards against writes
27422
+ * garbage that reads as data.
27423
+ */
27424
+ async function persistRescoreResult(faceId, report, ports) {
27425
+ if (!report.comparable) return "not-comparable";
27426
+ const candidate = decideRescorePersistence(report);
27427
+ await ports.setSuggestion(faceId, candidate);
27428
+ return candidate !== null ? "written" : "cleared";
27429
+ }
27295
27430
  //#endregion
27296
27431
  //#region src/pipeline-analytics/face-rescore-actions.ts
27297
27432
  /**
@@ -27392,8 +27527,10 @@ var FaceRescoreResultSchema = require_dist.object({
27392
27527
  /** The face's current assignment, so the UI can mark "already this person". */
27393
27528
  assigned: require_dist.boolean(),
27394
27529
  assignedIdentityId: require_dist.string().optional(),
27395
- /** What the track has been SHOWING until now — persisted at detection time.
27396
- * Returned so the UI can put the stale number beside the fresh one. */
27530
+ /** What the track has been SHOWING until now — read BEFORE this call
27531
+ * writes, so it is whatever a previous track close or a previous
27532
+ * `face.rescoreTrack` last persisted. Returned so the UI can put the stale
27533
+ * number beside the fresh one. */
27397
27534
  storedSuggestedIdentityId: require_dist.string().optional(),
27398
27535
  storedSuggestedMatchScore: require_dist.number().optional(),
27399
27536
  verdict: FaceRescoreVerdictSchema,
@@ -27410,9 +27547,12 @@ var FaceRescoreResultSchema = require_dist.object({
27410
27547
  });
27411
27548
  var faceRescoreActions = require_dist.defineCustomActions({
27412
27549
  /**
27413
- * Read-only by default. `admin` because the report names every enrolled
27414
- * person and how well an arbitrary face matches them — the gallery's shape,
27415
- * which is not a viewer-level fact.
27550
+ * Persists its own result onto the SUGGESTED tier (`suggestedIdentityId` /
27551
+ * `suggestedMatchScore`) — never an assignment, never automatic; see
27552
+ * `rescoreFaceForTrack` in `pipeline-analytics/index.ts` and
27553
+ * `persistRescoreResult` in `face-rescore.ts`. `admin` because the report
27554
+ * also names every enrolled person and how well an arbitrary face matches
27555
+ * them — the gallery's shape, which is not a viewer-level fact.
27416
27556
  */
27417
27557
  "face.rescoreTrack": require_dist.customAction(FaceRescoreInputSchema, FaceRescoreResultSchema, {
27418
27558
  kind: "mutation",
@@ -29407,6 +29547,51 @@ var FrameProcessor = class {
29407
29547
  }
29408
29548
  };
29409
29549
  //#endregion
29550
+ //#region src/pipeline-analytics/replay/frame-processor-pass.ts
29551
+ /**
29552
+ * Run a FRESH `FrameProcessor` over a fixed, ordered sequence of frames and
29553
+ * return the tracks it produced.
29554
+ *
29555
+ * No `TrackStore`, no bus, no media — a track here is a summary built
29556
+ * ENTIRELY from `FrameProcessor.process()`'s in-memory return value.
29557
+ */
29558
+ function runFrameProcessorPass(input) {
29559
+ const processor = new FrameProcessor(input.deviceId, {}, {}, {}, input.source);
29560
+ if (input.zones !== void 0) processor.setZones(input.zones);
29561
+ if (input.detectionRules !== void 0) processor.setDetectionRules(input.detectionRules);
29562
+ if (input.zoneMembershipMinOverlap !== void 0) processor.setZoneMembershipMinOverlap(input.zoneMembershipMinOverlap);
29563
+ const accum = /* @__PURE__ */ new Map();
29564
+ for (const { timestamp, frame } of input.frames) {
29565
+ const result = processor.process({
29566
+ timestamp,
29567
+ frame
29568
+ });
29569
+ for (const td of result.tracked) {
29570
+ const existing = accum.get(td.trackId);
29571
+ const matched = td.matchedThisFrame ? 1 : 0;
29572
+ if (existing === void 0) accum.set(td.trackId, {
29573
+ className: td.className,
29574
+ firstSeenMs: timestamp,
29575
+ lastSeenMs: timestamp,
29576
+ bbox: { ...td.bbox },
29577
+ framesMatched: matched
29578
+ });
29579
+ else {
29580
+ existing.className = td.className;
29581
+ existing.lastSeenMs = timestamp;
29582
+ existing.framesMatched += matched;
29583
+ }
29584
+ }
29585
+ }
29586
+ return { tracks: [...accum.values()].map((t) => ({
29587
+ className: t.className,
29588
+ firstSeenMs: t.firstSeenMs,
29589
+ lastSeenMs: t.lastSeenMs,
29590
+ bbox: t.bbox,
29591
+ framesMatched: t.framesMatched
29592
+ })) };
29593
+ }
29594
+ //#endregion
29410
29595
  //#region src/pipeline-analytics/pipeline/semantic-detail-bbox.ts
29411
29596
  /** Which box the semantic best-shot should crop: a plate/face DETAIL box
29412
29597
  * when we have one, otherwise the subject box. */
@@ -47629,6 +47814,37 @@ var FaceStore = class {
47629
47814
  }
47630
47815
  }
47631
47816
  /**
47817
+ * Persist a `face.rescoreTrack` result onto the SUGGESTED tier — the ONE
47818
+ * write that explicitly-requested action makes. `suggestion: null` means
47819
+ * the recompute found no candidate worth offering and CLEARS both columns
47820
+ * (a genuine "the gallery has nothing for this face right now" result, not
47821
+ * a no-op left at whatever they held before). Never touches `assigned` /
47822
+ * `recognizedIdentityId` — a suggestion is never an assignment. The caller
47823
+ * is responsible for never calling this with a non-comparable (`model-
47824
+ * mismatch`) report; this method persists whatever it is given.
47825
+ */
47826
+ async setSuggestion(faceId, suggestion) {
47827
+ const data = suggestion === null ? {
47828
+ suggestedIdentityId: null,
47829
+ suggestedMatchScore: null
47830
+ } : {
47831
+ suggestedIdentityId: suggestion.identityId,
47832
+ suggestedMatchScore: suggestion.score
47833
+ };
47834
+ try {
47835
+ await this.store.update.mutate({
47836
+ collection: FACES_COLLECTION,
47837
+ id: faceId,
47838
+ data
47839
+ });
47840
+ } catch (err) {
47841
+ this.logger.warn("FaceStore.setSuggestion failed", { meta: {
47842
+ faceId,
47843
+ error: String(err)
47844
+ } });
47845
+ }
47846
+ }
47847
+ /**
47632
47848
  * `TrackScopedStore` contract for the retention cascade: delete the buffered
47633
47849
  * face rows of the given tracks that are NOT assigned. Reuses the EXACT
47634
47850
  * `assigned` exemption that `prune`/`pruneAll` apply (see the
@@ -60020,6 +60236,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60020
60236
  cancelVision: async (requestId) => {
60021
60237
  await api.llm.cancel.mutate({ requestId });
60022
60238
  },
60239
+ resolveArtifactUrl: async (artifactId) => await this.ncArtifactPlane?.mintUrl(artifactId) ?? null,
60023
60240
  mintActionUrl: (input) => this.ncActionMintUrl?.(input) ?? Promise.resolve(null),
60024
60241
  readDeviceStates: async (ids) => {
60025
60242
  const out = /* @__PURE__ */ new Map();
@@ -60538,6 +60755,21 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60538
60755
  ...input.nodeId !== void 0 ? { nodeId: input.nodeId } : {}
60539
60756
  });
60540
60757
  }
60758
+ /**
60759
+ * The FrameProcessor pass of a replay run — see the cap doc on
60760
+ * `runReplayFrameProcessor` for why this is not the `processFrame` method
60761
+ * this addon deliberately does not have.
60762
+ *
60763
+ * `runFrameProcessorPass` is pure w.r.t. this addon's own state: it
60764
+ * constructs a FRESH `FrameProcessor` and does not touch `this.trackStore`,
60765
+ * `this.stationaryRegistry` or any other instance field — unlike
60766
+ * `getOrCreateProcessor` (the production path), which wires both.
60767
+ * `scripts/check-replay-isolation.ts` holds that boundary on the
60768
+ * `replay/` directory this delegates into.
60769
+ */
60770
+ async runReplayFrameProcessor(input) {
60771
+ return runFrameProcessorPass(input);
60772
+ }
60541
60773
  async listRetrainAnnotations(input) {
60542
60774
  return this.requireRetrain().listAnnotations(input.trackId);
60543
60775
  }
@@ -65487,6 +65719,13 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
65487
65719
  * See `face-rescore.ts` for why the number already on the track cannot answer
65488
65720
  * that: it was computed during live detection and persisted at track close,
65489
65721
  * so every enrolment made since is invisible to it.
65722
+ *
65723
+ * The report's result is PERSISTED (`persistRescoreResult`, below) onto
65724
+ * `Face.suggestedIdentityId` / `suggestedMatchScore` — the SAME columns the
65725
+ * live recognizer's suggestion band writes, so a reader never has to know
65726
+ * whether a suggestion came from track close or from this button. Still
65727
+ * never a background job: this write happens once, only because the
65728
+ * operator asked THIS question right now.
65490
65729
  */
65491
65730
  async rescoreFaceForTrack(input) {
65492
65731
  const api = this.ctx.api;
@@ -65529,6 +65768,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
65529
65768
  clusterModelId,
65530
65769
  params
65531
65770
  });
65771
+ const persisted = await persistRescoreResult(faceId, report, { setSuggestion: (id, suggestion) => faceStore.setSuggestion(id, suggestion) });
65532
65772
  this.ctx.logger.info("face rescore", {
65533
65773
  tags: { deviceId: input.deviceId },
65534
65774
  meta: {
@@ -65542,7 +65782,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
65542
65782
  best: report.identities[0]?.name ?? null,
65543
65783
  bestScore: report.identities[0]?.score ?? null,
65544
65784
  storedSuggestion: face.suggestedMatchScore ?? null,
65545
- reembedded: reembed.succeeded
65785
+ reembedded: reembed.succeeded,
65786
+ persisted
65546
65787
  }
65547
65788
  });
65548
65789
  return {