@camstack/addon-post-analysis 1.2.208 → 1.2.209

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-CoLA3NHh.js");
5
+ const require_dist = require("../dist-BttKNVmP.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");
@@ -7520,7 +7520,8 @@ function cooldownKey(rule, subject) {
7520
7520
  const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
7521
7521
  const scopeRef = subject.systemEvent !== void 0 ? systemEventCooldownScope(subject.systemEvent) : `d:${subject.deviceId}`;
7522
7522
  const kindKey = subject.systemEvent !== void 0 ? `:k:${subject.systemEvent.kind}` : "";
7523
- return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${classKey}` : `r:${rule.id}:${scopeRef}${classKey}`;
7523
+ const phaseKey = subject.packagePhase !== void 0 ? `:p:${subject.packagePhase}` : "";
7524
+ return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${phaseKey}${classKey}` : `r:${rule.id}:${scopeRef}${phaseKey}${classKey}`;
7524
7525
  }
7525
7526
  /** True when the rule fired within its cooldown window before `now`. */
7526
7527
  function isCoolingDown(rule, lastFiredAt, now) {
@@ -9957,6 +9958,10 @@ var en_default = {
9957
9958
  "one": "{{count}} other {{classNoun}}",
9958
9959
  "other": "{{count}} other {{classNoun}}"
9959
9960
  },
9961
+ "package.delivered.title": "Parcel delivered",
9962
+ "package.delivered.body": "Delivered at {{camera}}{{inZones}}",
9963
+ "package.picked-up.title": "Parcel collected",
9964
+ "package.picked-up.body": "Collected from {{camera}}{{inZones}}",
9960
9965
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
9961
9966
  "occupancy.op.occupied": "occupied",
9962
9967
  "occupancy.op.free": "free",
@@ -10208,6 +10213,10 @@ var it_default = {
10208
10213
  "one": "{{count}} altro {{classNoun}}",
10209
10214
  "other": "{{count}} altri {{classNoun}}"
10210
10215
  },
10216
+ "package.delivered.title": "Pacco consegnato",
10217
+ "package.delivered.body": "Consegnato su {{camera}}{{inZones}}",
10218
+ "package.picked-up.title": "Pacco ritirato",
10219
+ "package.picked-up.body": "Ritirato da {{camera}}{{inZones}}",
10211
10220
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
10212
10221
  "occupancy.op.occupied": "occupata",
10213
10222
  "occupancy.op.free": "libera",
@@ -12050,6 +12059,7 @@ function bodyKeyFor(entry) {
12050
12059
  const subject = entry.payload.subject;
12051
12060
  if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
12052
12061
  if (subject.occupancy !== void 0) return "occupancy.body";
12062
+ if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
12053
12063
  if (entry.payload.enhancement === true) return "detection.body.enhanced";
12054
12064
  return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
12055
12065
  }
@@ -12057,6 +12067,8 @@ function bodyKeyFor(entry) {
12057
12067
  function titleKeyFor(entry) {
12058
12068
  const systemEvent = entry.payload.subject.systemEvent;
12059
12069
  if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
12070
+ const packagePhase = entry.payload.subject.packagePhase;
12071
+ if (packagePhase !== void 0) return `package.${packagePhase}.title`;
12060
12072
  return "detection.title";
12061
12073
  }
12062
12074
  /**
@@ -24856,6 +24868,7 @@ var NotificationCenter = class NotificationCenter {
24856
24868
  zones: renderedZones,
24857
24869
  timestamp: subject.timestamp,
24858
24870
  ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
24871
+ ...subject.packagePhase !== void 0 ? { packagePhase: subject.packagePhase } : {},
24859
24872
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
24860
24873
  }
24861
24874
  };
@@ -32769,9 +32782,14 @@ var FaceStore = class {
32769
32782
  * with that person named. High means "this looks like someone we can name",
32770
32783
  * which is what a mis-enrolment actually looks like.
32771
32784
  *
32772
- * `suspicion` combines them so a sample that looks like somebody else outranks
32773
- * one that merely looks like nobody: the first is a fixable error, the second
32774
- * is often just a difficult photograph.
32785
+ * `suspicion` is the EXCESS `bestForeign selfMean` because that is the
32786
+ * only one of the two that discriminates on real data. Measured on Roberta's
32787
+ * 106 samples, 2026-09-08: `selfMean` has a median of 0.188 and a p90 of 0.235,
32788
+ * so ANY absolute reading of "does not look like itself" fires on the whole
32789
+ * gallery. The first version of this module scored `(1 − selfMean) / 2` and
32790
+ * flagged 56 of 106 — a finding that fires on healthy samples means nothing.
32791
+ * `bestForeign` on the same gallery: median 0.283, p95 0.376, and one single
32792
+ * outlier at 0.612, which is the mis-enrolment.
32775
32793
  *
32776
32794
  * ## What it refuses to do
32777
32795
  *
@@ -32786,8 +32804,20 @@ var FaceStore = class {
32786
32804
  *
32787
32805
  * Pure — the caller loads the vectors and performs the removal.
32788
32806
  */
32789
- /** Above this a row is worth the operator's attention. */
32790
- var SUSPICION_FLAG = .5;
32807
+ /**
32808
+ * A row is FLAGGED when its best foreign match would auto-assign the face to
32809
+ * that other person — i.e. it reaches the recogniser's own
32810
+ * `similarityThreshold`.
32811
+ *
32812
+ * Tied to the live threshold rather than to a constant of its own, because
32813
+ * that is what the flag MEANS: this sample can steal a match. A second number
32814
+ * invented here would drift from the one the recogniser actually uses, and the
32815
+ * panel would accuse samples that cannot hurt anyone while staying quiet about
32816
+ * ones that can. On the live gallery (threshold 0.40) it flags 2 of 106.
32817
+ */
32818
+ function flaggedCount(rows, autoAssignThreshold) {
32819
+ return rows.filter((row) => (row.bestForeign?.score ?? 0) >= autoAssignThreshold).length;
32820
+ }
32791
32821
  /**
32792
32822
  * How far a foreign match has to stand OUT to count as looking like someone
32793
32823
  * else: `bestForeign - selfMean`. A sample that scores 0.6 against a stranger
@@ -32842,8 +32872,7 @@ function auditIdentitySamples(input) {
32842
32872
  };
32843
32873
  const selfMean = meanCosine(sample, comparable);
32844
32874
  const foreign = bestForeignMatch(sample, input.others, input.modelId);
32845
- const excess = foreignExcess(selfMean, foreign);
32846
- const suspicion = selfMean === null ? 0 : (1 - selfMean) / 2 + excess;
32875
+ const suspicion = foreignExcess(selfMean, foreign);
32847
32876
  return {
32848
32877
  sampleId: sample.sampleId,
32849
32878
  enrolledAt: sample.enrolledAt,
@@ -32854,12 +32883,15 @@ function auditIdentitySamples(input) {
32854
32883
  suspicion
32855
32884
  };
32856
32885
  });
32857
- rows.sort((a, b) => b.suspicion - a.suspicion);
32886
+ rows.sort((a, b) => {
32887
+ if (b.suspicion !== a.suspicion) return b.suspicion - a.suspicion;
32888
+ return (a.selfMean ?? Number.POSITIVE_INFINITY) - (b.selfMean ?? Number.POSITIVE_INFINITY);
32889
+ });
32858
32890
  return {
32859
32891
  identityId: input.identityId,
32860
32892
  totalSamples: input.samples.length,
32861
32893
  comparableSamples: comparable.length,
32862
- flagged: rows.filter((r) => r.suspicion >= SUSPICION_FLAG).length,
32894
+ flagged: flaggedCount(rows, input.autoAssignThreshold),
32863
32895
  rows
32864
32896
  };
32865
32897
  }
@@ -32916,6 +32948,7 @@ var FaceGalleryProvider = class {
32916
32948
  logger;
32917
32949
  refreshGallery;
32918
32950
  resolveClusterModelId;
32951
+ resolveAutoAssignThreshold;
32919
32952
  eventMediaBaseUrl;
32920
32953
  eventMediaPathPrefix;
32921
32954
  emitFaceGalleryChanged;
@@ -32928,6 +32961,7 @@ var FaceGalleryProvider = class {
32928
32961
  this.logger = deps.logger;
32929
32962
  this.refreshGallery = deps.refreshGallery;
32930
32963
  this.resolveClusterModelId = deps.resolveClusterModelId;
32964
+ this.resolveAutoAssignThreshold = deps.resolveAutoAssignThreshold;
32931
32965
  this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
32932
32966
  this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
32933
32967
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
@@ -33019,6 +33053,7 @@ var FaceGalleryProvider = class {
33019
33053
  async auditIdentitySamples(input) {
33020
33054
  const modelId = await this.resolveClusterModelId();
33021
33055
  if (modelId === null) throw new Error("auditIdentitySamples: the cluster face-embedding model could not be resolved");
33056
+ const autoAssignThreshold = await this.resolveAutoAssignThreshold();
33022
33057
  const identities = await this.identityStore.listIdentities();
33023
33058
  const target = identities.find((i) => i.id === input.identityId);
33024
33059
  if (target === void 0) throw new Error(`auditIdentitySamples: no identity ${input.identityId}`);
@@ -33044,7 +33079,8 @@ var FaceGalleryProvider = class {
33044
33079
  name: identity.name,
33045
33080
  samples: inputsFor(identity.id)
33046
33081
  })),
33047
- modelId
33082
+ modelId,
33083
+ autoAssignThreshold
33048
33084
  });
33049
33085
  const meta = new Map((samplesByIdentity.get(input.identityId) ?? []).map((sample) => [sample.id, sample]));
33050
33086
  return {
@@ -33054,7 +33090,7 @@ var FaceGalleryProvider = class {
33054
33090
  totalSamples: report.totalSamples,
33055
33091
  comparableSamples: report.comparableSamples,
33056
33092
  flagged: report.flagged,
33057
- flagThreshold: SUSPICION_FLAG,
33093
+ flagThreshold: autoAssignThreshold,
33058
33094
  rows: report.rows.map((row) => {
33059
33095
  const sample = meta.get(row.sampleId);
33060
33096
  return {
@@ -57932,6 +57968,9 @@ async function reconcileGallerySamples(deps, options) {
57932
57968
  let keyless = 0;
57933
57969
  let skippedGrace = 0;
57934
57970
  let stoppedByCeiling = false;
57971
+ let missed = 0;
57972
+ /** Verdicts held until there are enough of them to tell rot from a move. */
57973
+ const held = [];
57935
57974
  const identities = await deps.listIdentities();
57936
57975
  for (const identity of identities) {
57937
57976
  const samples = await deps.listSamples(identity.id);
@@ -57976,6 +58015,43 @@ async function reconcileGallerySamples(deps, options) {
57976
58015
  }
57977
58016
  checked += 1;
57978
58017
  if (state === "exists") continue;
58018
+ missed += 1;
58019
+ const minProbes = options.massMissMinProbes ?? 8;
58020
+ const bar = options.massMissFraction ?? .5;
58021
+ if (checked < minProbes) {
58022
+ held.push({
58023
+ identityId: identity.id,
58024
+ sampleId: sample.id,
58025
+ mediaKey: sample.mediaKey
58026
+ });
58027
+ continue;
58028
+ }
58029
+ if (missed / checked >= bar) {
58030
+ const reason = `mass miss: ${String(missed)} of ${String(checked)} probes found no blob`;
58031
+ deps.logger.warn("gallery reconcile: nearly every blob is missing — that is the volume, not the samples. Aborting and keeping every row; check where the gallery location points.", { meta: {
58032
+ checked,
58033
+ missed,
58034
+ held: held.length,
58035
+ identity: identity.name
58036
+ } });
58037
+ return {
58038
+ checked,
58039
+ reclaimed,
58040
+ keyless,
58041
+ skippedGrace,
58042
+ stoppedByCeiling,
58043
+ aborted: reason
58044
+ };
58045
+ }
58046
+ for (const pending of held.splice(0, held.length)) {
58047
+ await deps.removeSample(pending.identityId, pending.sampleId);
58048
+ reclaimed += 1;
58049
+ deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
58050
+ sampleId: pending.sampleId,
58051
+ mediaKey: pending.mediaKey,
58052
+ state: "missing-blob"
58053
+ } });
58054
+ }
57979
58055
  await deps.removeSample(identity.id, sample.id);
57980
58056
  reclaimed += 1;
57981
58057
  deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
@@ -57986,6 +58062,14 @@ async function reconcileGallerySamples(deps, options) {
57986
58062
  } });
57987
58063
  }
57988
58064
  }
58065
+ for (const pending of held.splice(0, held.length)) {
58066
+ if (reclaimed >= options.maxReclaimPerRun) {
58067
+ stoppedByCeiling = true;
58068
+ break;
58069
+ }
58070
+ await deps.removeSample(pending.identityId, pending.sampleId);
58071
+ reclaimed += 1;
58072
+ }
57989
58073
  if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
57990
58074
  checked,
57991
58075
  reclaimed,
@@ -71964,6 +72048,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
71964
72048
  eventStore: stores.eventStore,
71965
72049
  logger: this.ctx.logger,
71966
72050
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
72051
+ resolveAutoAssignThreshold: async () => (await this.readDeviceSettings(0, resolveFaceSettings)).similarityThreshold,
71967
72052
  resolveClusterModelId: async () => {
71968
72053
  const api = this.ctx.api;
71969
72054
  if (!api) return null;
@@ -1,4 +1,4 @@
1
- import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-WxrHfFRz.mjs";
1
+ import { $ as audioModeOf, $t as EventCategory, A as NcSnoozeSchema, At as vectorDimFromBase64, B as SCENE_DEFAULT_UNCOVERED_POLICY, Bt as nodePin, C as NcConditionDescriptorSchema, Ct as readTimelapseGeneratedAt, D as NcRuleTargetSchema, Dt as storageOccupancyCapability, E as NcRuleSchema, Et as sceneMonitorCapability, Ft as CamProfileSchema, G as TimelapseRulePatchSchema, Gt as discriminatedUnion, H as SceneMonitorSchema, Ht as _enum, I as RECORDING_EXPORT_MAX_READ_BYTES, It as DeviceType, J as VISIT_MERGE_GAP_MS, Jt as object, K as TimelapseRuleSchema, Kt as literal, L as RetrainStatusSchema, Lt as createEvent, M as NcSystemEventKindSchema, Mt as zoneAnalyticsCapability, N as NcTaxonomySchema, Nt as errMsg$1, O as NcScheduleSchema, Ot as subKindsOf, P as OpsLogEntrySchema, Pt as BaseAddon, Q as audioMetricsCapability, Qt as unknown, Rt as hydrateSchema, S as NC_TAXONOMY, St as readDeviceStateFrom, T as NcRulePatchSchema, U as TIMELAPSE_DENSE_FLOOR_SEC, Ut as array, V as SCENE_DIVERGED, Vt as sleep$1, W as TimelapseRuleInputSchema, Wt as boolean, X as alarmPanelCapability, Xt as record, Y as addonWidgetsSourceCapability, Yt as partialRecord, Z as assertTimelapseCadences, Zt as string, _ as MediaFileKindEnum, _t as notificationRulesCapability, a as DEFAULT_EVENT_COLOR, b as NC_DEFAULT_SNOOZE_MINUTES, bt as pipelineAnalyticsCapability, c as DETECTION_MACRO_CLASSES, ct as evictionPolicyOfLocation, d as EVENT_KIND_BY_CAP, et as buildEventKindDescriptor, f as EVENT_PAD_MS, ft as isDetectionMacroClass, g as MACRO_LABELS, gt as mayWriteToLocation, h as LabelAttributionSchema, ht as kebabToCamel, i as COCO_TO_MACRO, it as deriveRecordingMode, j as NcSnoozeSuppressedSchema, jt as videoclipsCapability, k as NcSnoozeInputSchema, kt as systemEventFilterApplies, lt as faceGalleryCapability, m as FailureCounters, mt as isSourceCap, n as BaseDevice, nt as customAction, ot as encodeVectorBase64, p as FULL_IMAGE_BBOX, pt as isScheduleActive, q as TrackSourceSchema, qt as number, r as CLUSTER_MODEL_SCOPED_STEPS, rt as defineCustomActions, st as evaluateSensorEdge, t as AUDIO_MACRO_LABELS, tt as cosineSimilarity$1, u as DeclaredDevices, ut as failureContributionCapability, v as NC_ALARM_SYSTEM_EVENT_KINDS, w as NcRuleInputSchema, wt as resolveLocationMode, xt as plateGalleryCapability, y as NC_CONDITION_CATALOG, yt as pickClusterStepModels, z as SCENE_DEFAULT_ANCHOR_THRESHOLD, zt as isDeviceScopedCap } from "../dist-BV15Z8DR.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";
@@ -7513,7 +7513,8 @@ function cooldownKey(rule, subject) {
7513
7513
  const classKey = keysPerClass(rule, subject) && first !== void 0 ? `:c:${first}` : "";
7514
7514
  const scopeRef = subject.systemEvent !== void 0 ? systemEventCooldownScope(subject.systemEvent) : `d:${subject.deviceId}`;
7515
7515
  const kindKey = subject.systemEvent !== void 0 ? `:k:${subject.systemEvent.kind}` : "";
7516
- return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${classKey}` : `r:${rule.id}:${scopeRef}${classKey}`;
7516
+ const phaseKey = subject.packagePhase !== void 0 ? `:p:${subject.packagePhase}` : "";
7517
+ return rule.throttle.scope === "rule" ? `r:${rule.id}${kindKey}${phaseKey}${classKey}` : `r:${rule.id}:${scopeRef}${phaseKey}${classKey}`;
7517
7518
  }
7518
7519
  /** True when the rule fired within its cooldown window before `now`. */
7519
7520
  function isCoolingDown(rule, lastFiredAt, now) {
@@ -9950,6 +9951,10 @@ var en_default = {
9950
9951
  "one": "{{count}} other {{classNoun}}",
9951
9952
  "other": "{{count}} other {{classNoun}}"
9952
9953
  },
9954
+ "package.delivered.title": "Parcel delivered",
9955
+ "package.delivered.body": "Delivered at {{camera}}{{inZones}}",
9956
+ "package.picked-up.title": "Parcel collected",
9957
+ "package.picked-up.body": "Collected from {{camera}}{{inZones}}",
9953
9958
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
9954
9959
  "occupancy.op.occupied": "occupied",
9955
9960
  "occupancy.op.free": "free",
@@ -10201,6 +10206,10 @@ var it_default = {
10201
10206
  "one": "{{count}} altro {{classNoun}}",
10202
10207
  "other": "{{count}} altri {{classNoun}}"
10203
10208
  },
10209
+ "package.delivered.title": "Pacco consegnato",
10210
+ "package.delivered.body": "Consegnato su {{camera}}{{inZones}}",
10211
+ "package.picked-up.title": "Pacco ritirato",
10212
+ "package.picked-up.body": "Ritirato da {{camera}}{{inZones}}",
10204
10213
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
10205
10214
  "occupancy.op.occupied": "occupata",
10206
10215
  "occupancy.op.free": "libera",
@@ -12043,6 +12052,7 @@ function bodyKeyFor(entry) {
12043
12052
  const subject = entry.payload.subject;
12044
12053
  if (subject.systemEvent !== void 0) return subject.systemEvent.bodyKey ?? "";
12045
12054
  if (subject.occupancy !== void 0) return "occupancy.body";
12055
+ if (subject.packagePhase !== void 0) return `package.${subject.packagePhase}.body`;
12046
12056
  if (entry.payload.enhancement === true) return "detection.body.enhanced";
12047
12057
  return entry.recordKind === "track-end" ? "detection.body.trackEnd" : "detection.body";
12048
12058
  }
@@ -12050,6 +12060,8 @@ function bodyKeyFor(entry) {
12050
12060
  function titleKeyFor(entry) {
12051
12061
  const systemEvent = entry.payload.subject.systemEvent;
12052
12062
  if (systemEvent !== void 0) return systemEvent.titleKey ?? "";
12063
+ const packagePhase = entry.payload.subject.packagePhase;
12064
+ if (packagePhase !== void 0) return `package.${packagePhase}.title`;
12053
12065
  return "detection.title";
12054
12066
  }
12055
12067
  /**
@@ -24834,6 +24846,7 @@ var NotificationCenter = class NotificationCenter {
24834
24846
  zones: renderedZones,
24835
24847
  timestamp: subject.timestamp,
24836
24848
  ...subject.occupancy !== void 0 ? { occupancy: frozenOccupancy(rule, subject.occupancy) } : {},
24849
+ ...subject.packagePhase !== void 0 ? { packagePhase: subject.packagePhase } : {},
24837
24850
  ...subject.systemEvent !== void 0 ? { systemEvent: subject.systemEvent } : {}
24838
24851
  }
24839
24852
  };
@@ -32746,9 +32759,14 @@ var FaceStore = class {
32746
32759
  * with that person named. High means "this looks like someone we can name",
32747
32760
  * which is what a mis-enrolment actually looks like.
32748
32761
  *
32749
- * `suspicion` combines them so a sample that looks like somebody else outranks
32750
- * one that merely looks like nobody: the first is a fixable error, the second
32751
- * is often just a difficult photograph.
32762
+ * `suspicion` is the EXCESS `bestForeign selfMean` because that is the
32763
+ * only one of the two that discriminates on real data. Measured on Roberta's
32764
+ * 106 samples, 2026-09-08: `selfMean` has a median of 0.188 and a p90 of 0.235,
32765
+ * so ANY absolute reading of "does not look like itself" fires on the whole
32766
+ * gallery. The first version of this module scored `(1 − selfMean) / 2` and
32767
+ * flagged 56 of 106 — a finding that fires on healthy samples means nothing.
32768
+ * `bestForeign` on the same gallery: median 0.283, p95 0.376, and one single
32769
+ * outlier at 0.612, which is the mis-enrolment.
32752
32770
  *
32753
32771
  * ## What it refuses to do
32754
32772
  *
@@ -32763,8 +32781,20 @@ var FaceStore = class {
32763
32781
  *
32764
32782
  * Pure — the caller loads the vectors and performs the removal.
32765
32783
  */
32766
- /** Above this a row is worth the operator's attention. */
32767
- var SUSPICION_FLAG = .5;
32784
+ /**
32785
+ * A row is FLAGGED when its best foreign match would auto-assign the face to
32786
+ * that other person — i.e. it reaches the recogniser's own
32787
+ * `similarityThreshold`.
32788
+ *
32789
+ * Tied to the live threshold rather than to a constant of its own, because
32790
+ * that is what the flag MEANS: this sample can steal a match. A second number
32791
+ * invented here would drift from the one the recogniser actually uses, and the
32792
+ * panel would accuse samples that cannot hurt anyone while staying quiet about
32793
+ * ones that can. On the live gallery (threshold 0.40) it flags 2 of 106.
32794
+ */
32795
+ function flaggedCount(rows, autoAssignThreshold) {
32796
+ return rows.filter((row) => (row.bestForeign?.score ?? 0) >= autoAssignThreshold).length;
32797
+ }
32768
32798
  /**
32769
32799
  * How far a foreign match has to stand OUT to count as looking like someone
32770
32800
  * else: `bestForeign - selfMean`. A sample that scores 0.6 against a stranger
@@ -32819,8 +32849,7 @@ function auditIdentitySamples(input) {
32819
32849
  };
32820
32850
  const selfMean = meanCosine(sample, comparable);
32821
32851
  const foreign = bestForeignMatch(sample, input.others, input.modelId);
32822
- const excess = foreignExcess(selfMean, foreign);
32823
- const suspicion = selfMean === null ? 0 : (1 - selfMean) / 2 + excess;
32852
+ const suspicion = foreignExcess(selfMean, foreign);
32824
32853
  return {
32825
32854
  sampleId: sample.sampleId,
32826
32855
  enrolledAt: sample.enrolledAt,
@@ -32831,12 +32860,15 @@ function auditIdentitySamples(input) {
32831
32860
  suspicion
32832
32861
  };
32833
32862
  });
32834
- rows.sort((a, b) => b.suspicion - a.suspicion);
32863
+ rows.sort((a, b) => {
32864
+ if (b.suspicion !== a.suspicion) return b.suspicion - a.suspicion;
32865
+ return (a.selfMean ?? Number.POSITIVE_INFINITY) - (b.selfMean ?? Number.POSITIVE_INFINITY);
32866
+ });
32835
32867
  return {
32836
32868
  identityId: input.identityId,
32837
32869
  totalSamples: input.samples.length,
32838
32870
  comparableSamples: comparable.length,
32839
- flagged: rows.filter((r) => r.suspicion >= SUSPICION_FLAG).length,
32871
+ flagged: flaggedCount(rows, input.autoAssignThreshold),
32840
32872
  rows
32841
32873
  };
32842
32874
  }
@@ -32893,6 +32925,7 @@ var FaceGalleryProvider = class {
32893
32925
  logger;
32894
32926
  refreshGallery;
32895
32927
  resolveClusterModelId;
32928
+ resolveAutoAssignThreshold;
32896
32929
  eventMediaBaseUrl;
32897
32930
  eventMediaPathPrefix;
32898
32931
  emitFaceGalleryChanged;
@@ -32905,6 +32938,7 @@ var FaceGalleryProvider = class {
32905
32938
  this.logger = deps.logger;
32906
32939
  this.refreshGallery = deps.refreshGallery;
32907
32940
  this.resolveClusterModelId = deps.resolveClusterModelId;
32941
+ this.resolveAutoAssignThreshold = deps.resolveAutoAssignThreshold;
32908
32942
  this.eventMediaBaseUrl = deps.eventMediaBaseUrl;
32909
32943
  this.eventMediaPathPrefix = deps.eventMediaPathPrefix;
32910
32944
  this.emitFaceGalleryChanged = deps.emitFaceGalleryChanged;
@@ -32996,6 +33030,7 @@ var FaceGalleryProvider = class {
32996
33030
  async auditIdentitySamples(input) {
32997
33031
  const modelId = await this.resolveClusterModelId();
32998
33032
  if (modelId === null) throw new Error("auditIdentitySamples: the cluster face-embedding model could not be resolved");
33033
+ const autoAssignThreshold = await this.resolveAutoAssignThreshold();
32999
33034
  const identities = await this.identityStore.listIdentities();
33000
33035
  const target = identities.find((i) => i.id === input.identityId);
33001
33036
  if (target === void 0) throw new Error(`auditIdentitySamples: no identity ${input.identityId}`);
@@ -33021,7 +33056,8 @@ var FaceGalleryProvider = class {
33021
33056
  name: identity.name,
33022
33057
  samples: inputsFor(identity.id)
33023
33058
  })),
33024
- modelId
33059
+ modelId,
33060
+ autoAssignThreshold
33025
33061
  });
33026
33062
  const meta = new Map((samplesByIdentity.get(input.identityId) ?? []).map((sample) => [sample.id, sample]));
33027
33063
  return {
@@ -33031,7 +33067,7 @@ var FaceGalleryProvider = class {
33031
33067
  totalSamples: report.totalSamples,
33032
33068
  comparableSamples: report.comparableSamples,
33033
33069
  flagged: report.flagged,
33034
- flagThreshold: SUSPICION_FLAG,
33070
+ flagThreshold: autoAssignThreshold,
33035
33071
  rows: report.rows.map((row) => {
33036
33072
  const sample = meta.get(row.sampleId);
33037
33073
  return {
@@ -57858,6 +57894,9 @@ async function reconcileGallerySamples(deps, options) {
57858
57894
  let keyless = 0;
57859
57895
  let skippedGrace = 0;
57860
57896
  let stoppedByCeiling = false;
57897
+ let missed = 0;
57898
+ /** Verdicts held until there are enough of them to tell rot from a move. */
57899
+ const held = [];
57861
57900
  const identities = await deps.listIdentities();
57862
57901
  for (const identity of identities) {
57863
57902
  const samples = await deps.listSamples(identity.id);
@@ -57902,6 +57941,43 @@ async function reconcileGallerySamples(deps, options) {
57902
57941
  }
57903
57942
  checked += 1;
57904
57943
  if (state === "exists") continue;
57944
+ missed += 1;
57945
+ const minProbes = options.massMissMinProbes ?? 8;
57946
+ const bar = options.massMissFraction ?? .5;
57947
+ if (checked < minProbes) {
57948
+ held.push({
57949
+ identityId: identity.id,
57950
+ sampleId: sample.id,
57951
+ mediaKey: sample.mediaKey
57952
+ });
57953
+ continue;
57954
+ }
57955
+ if (missed / checked >= bar) {
57956
+ const reason = `mass miss: ${String(missed)} of ${String(checked)} probes found no blob`;
57957
+ deps.logger.warn("gallery reconcile: nearly every blob is missing — that is the volume, not the samples. Aborting and keeping every row; check where the gallery location points.", { meta: {
57958
+ checked,
57959
+ missed,
57960
+ held: held.length,
57961
+ identity: identity.name
57962
+ } });
57963
+ return {
57964
+ checked,
57965
+ reclaimed,
57966
+ keyless,
57967
+ skippedGrace,
57968
+ stoppedByCeiling,
57969
+ aborted: reason
57970
+ };
57971
+ }
57972
+ for (const pending of held.splice(0, held.length)) {
57973
+ await deps.removeSample(pending.identityId, pending.sampleId);
57974
+ reclaimed += 1;
57975
+ deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
57976
+ sampleId: pending.sampleId,
57977
+ mediaKey: pending.mediaKey,
57978
+ state: "missing-blob"
57979
+ } });
57980
+ }
57905
57981
  await deps.removeSample(identity.id, sample.id);
57906
57982
  reclaimed += 1;
57907
57983
  deps.logger.warn("gallery reconcile: sample blob is gone from the filesystem — reference removed", { meta: {
@@ -57912,6 +57988,14 @@ async function reconcileGallerySamples(deps, options) {
57912
57988
  } });
57913
57989
  }
57914
57990
  }
57991
+ for (const pending of held.splice(0, held.length)) {
57992
+ if (reclaimed >= options.maxReclaimPerRun) {
57993
+ stoppedByCeiling = true;
57994
+ break;
57995
+ }
57996
+ await deps.removeSample(pending.identityId, pending.sampleId);
57997
+ reclaimed += 1;
57998
+ }
57915
57999
  if (reclaimed > 0) deps.logger.info("gallery reconcile: dead references removed", { meta: {
57916
58000
  checked,
57917
58001
  reclaimed,
@@ -71890,6 +71974,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
71890
71974
  eventStore: stores.eventStore,
71891
71975
  logger: this.ctx.logger,
71892
71976
  refreshGallery: () => this.faceRecognizer?.refreshGallery(),
71977
+ resolveAutoAssignThreshold: async () => (await this.readDeviceSettings(0, resolveFaceSettings)).similarityThreshold,
71893
71978
  resolveClusterModelId: async () => {
71894
71979
  const api = this.ctx.api;
71895
71980
  if (!api) return null;
@@ -30,7 +30,7 @@ async function d(e) {
30
30
  }
31
31
  }
32
32
  async function f() {
33
- return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-CHmvWGlL.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DCfZE21-.mjs")).catch((e) => {
34
34
  throw l = void 0, e;
35
35
  }), l;
36
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-post-analysis",
3
- "version": "1.2.208",
3
+ "version": "1.2.209",
4
4
  "description": "Post-Analysis bundle — enrichment, embedding-encoder, pipeline-analytics. Multi-entry npm package shipping addons that consume pipeline output.",
5
5
  "keywords": [
6
6
  "camstack",