@camstack/addon-post-analysis 1.2.131 → 1.2.132

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.
@@ -29876,17 +29876,60 @@ var SetSiteLocationInputSchema = object({
29876
29876
  longitude: number().min(-180).max(180)
29877
29877
  }).nullable();
29878
29878
  /**
29879
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
29879
+ * The TRANSPORT a call arrived on.
29880
+ *
29881
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
29882
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
29883
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
29884
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
29885
+ * checkable rather than asserted.
29886
+ *
29887
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
29888
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
29889
+ * connection; the viewer talks to the hub over `wsLink`
29890
+ * exclusively, so this is the plane the HTTP census could not see.
29891
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
29892
+ * never touches a socket and therefore never touched a census.
29893
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
29894
+ * that is exactly what its `0` asserts: every plane the hub has can name
29895
+ * itself. It is an output bucket, never a knob — a call that arrives on a
29896
+ * plane nobody instrumented lands here instead of vanishing from the total.
29897
+ */
29898
+ var TransportPlaneSchema = _enum([
29899
+ "http",
29900
+ "ws",
29901
+ "mesh",
29902
+ "unknown"
29903
+ ]);
29904
+ /**
29905
+ * Calls per plane. Every key is always present, `0` included — an absent plane
29906
+ * reads as "not instrumented", which is the one thing this census must never
29907
+ * make an operator wonder about.
29908
+ */
29909
+ var TransportPlaneCountsSchema = object({
29910
+ http: number(),
29911
+ ws: number(),
29912
+ mesh: number(),
29913
+ unknown: number()
29914
+ });
29915
+ /**
29916
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
29880
29917
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
29881
29918
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
29882
29919
  * already prints - never a token, never an `Authorization` header.
29920
+ *
29921
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
29922
+ * and lives for hours, so folding it into a call count makes one long-lived
29923
+ * stream look like a storm.
29883
29924
  */
29884
29925
  var RequestCensusGroupSchema = object({
29926
+ plane: TransportPlaneSchema,
29885
29927
  procedure: string(),
29886
29928
  userAgent: string(),
29887
29929
  ip: string(),
29888
29930
  principal: string(),
29889
29931
  calls: number(),
29932
+ subscriptions: number(),
29890
29933
  perMin: number()
29891
29934
  });
29892
29935
  /**
@@ -29899,6 +29942,14 @@ var RequestCensusGroupSchema = object({
29899
29942
  var RequestCensusProcedureSchema = object({
29900
29943
  procedure: string(),
29901
29944
  calls: number(),
29945
+ /**
29946
+ * The same total, split by transport. THIS is the row that answers the
29947
+ * question the census exists for: one look at `deviceManager.listAll` says
29948
+ * which plane carried the 4 960, without joining two log lines by eye.
29949
+ */
29950
+ planes: TransportPlaneCountsSchema,
29951
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
29952
+ subscriptions: number(),
29902
29953
  perMin: number()
29903
29954
  });
29904
29955
  /**
@@ -29926,14 +29977,45 @@ var RequestCensusStatusSchema = object({
29926
29977
  */
29927
29978
  procedureCalls: number(),
29928
29979
  /**
29980
+ * `procedureCalls` split by transport. The four keys sum to
29981
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
29982
+ * `planesExplainTotal` is that identity, checked rather than assumed.
29983
+ */
29984
+ planes: TransportPlaneCountsSchema,
29985
+ /**
29986
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
29987
+ * on no plane at all - which is a RESULT (a plane is missing from the
29988
+ * instrument), not a failure, and it has to be visible to be read as one.
29989
+ */
29990
+ planesExplainTotal: boolean(),
29991
+ /**
29929
29992
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
29930
- * transport resolves one context per connection - but the number that says
29931
- * whether a plane this census cannot see was busy while HTTP was quiet.
29993
+ * adapter resolves one context per connection - kept because a plane's call
29994
+ * count of zero against 37 open connections says something different from a
29995
+ * plane with no connections at all.
29932
29996
  */
29933
29997
  wsConnections: number(),
29998
+ /**
29999
+ * Client frames the WS plane looked at. `wsMessages` far above
30000
+ * `planes.ws + subscriptions` means most traffic is not operations
30001
+ * (keepalives, connection params) - which is itself an answer.
30002
+ */
30003
+ wsMessages: number(),
30004
+ /**
30005
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
30006
+ * purpose: one live-events stream opened at boot and held for six hours is
30007
+ * one subscription, and counting it as a call would let a quiet plane
30008
+ * masquerade as the storm.
30009
+ */
30010
+ subscriptions: number(),
30011
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
30012
+ subscriptionStops: number(),
29934
30013
  distinctGroups: number(),
29935
- /** Calls counted in the totals whose group attribution was shed at the
29936
- * cardinality bound. */
30014
+ /**
30015
+ * Operations counted in the totals whose CALLER attribution was shed at the
30016
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
30017
+ * which transport they arrived on, they just lost their group row.
30018
+ */
29937
30019
  unattributedCalls: number(),
29938
30020
  procedures: array(RequestCensusProcedureSchema).readonly(),
29939
30021
  groups: array(RequestCensusGroupSchema).readonly()
@@ -29845,17 +29845,60 @@ var SetSiteLocationInputSchema = object({
29845
29845
  longitude: number().min(-180).max(180)
29846
29846
  }).nullable();
29847
29847
  /**
29848
- * One `(procedure, user-agent, ip, principal)` tuple of the HTTP request
29848
+ * The TRANSPORT a call arrived on.
29849
+ *
29850
+ * Every counted call carries exactly one of these, and `unknown` is a PLANE
29851
+ * rather than a gap: a plane that cannot attribute a call declares it here, so
29852
+ * the call lands in a named bucket instead of vanishing. `planes` summing to
29853
+ * `procedureCalls` is what makes "the sum of the planes explains the total"
29854
+ * checkable rather than asserted.
29855
+ *
29856
+ * - `http` — the Fastify tRPC plugin (`/trpc/*`), one context per request.
29857
+ * - `ws` — `applyWSSHandler`, counted per OPERATION rather than per
29858
+ * connection; the viewer talks to the hub over `wsLink`
29859
+ * exclusively, so this is the plane the HTTP census could not see.
29860
+ * - `mesh` — the in-process `$core-caps` bridge (`createCaller`), which
29861
+ * never touches a socket and therefore never touched a census.
29862
+ * - `unknown` — counted, plane undecidable. No hook produces it today, and
29863
+ * that is exactly what its `0` asserts: every plane the hub has can name
29864
+ * itself. It is an output bucket, never a knob — a call that arrives on a
29865
+ * plane nobody instrumented lands here instead of vanishing from the total.
29866
+ */
29867
+ var TransportPlaneSchema = _enum([
29868
+ "http",
29869
+ "ws",
29870
+ "mesh",
29871
+ "unknown"
29872
+ ]);
29873
+ /**
29874
+ * Calls per plane. Every key is always present, `0` included — an absent plane
29875
+ * reads as "not instrumented", which is the one thing this census must never
29876
+ * make an operator wonder about.
29877
+ */
29878
+ var TransportPlaneCountsSchema = object({
29879
+ http: number(),
29880
+ ws: number(),
29881
+ mesh: number(),
29882
+ unknown: number()
29883
+ });
29884
+ /**
29885
+ * One `(plane, procedure, user-agent, ip, principal)` tuple of the transport
29849
29886
  * census. `principal` is the DERIVED identity (`apocaliss92 (admin)`,
29850
29887
  * `scoped:1a2b3c4d (scoped-token)`, `anonymous`) that the tRPC error log
29851
29888
  * already prints - never a token, never an `Authorization` header.
29889
+ *
29890
+ * `subscriptions` is counted APART from `calls`: a subscription is opened once
29891
+ * and lives for hours, so folding it into a call count makes one long-lived
29892
+ * stream look like a storm.
29852
29893
  */
29853
29894
  var RequestCensusGroupSchema = object({
29895
+ plane: TransportPlaneSchema,
29854
29896
  procedure: string(),
29855
29897
  userAgent: string(),
29856
29898
  ip: string(),
29857
29899
  principal: string(),
29858
29900
  calls: number(),
29901
+ subscriptions: number(),
29859
29902
  perMin: number()
29860
29903
  });
29861
29904
  /**
@@ -29868,6 +29911,14 @@ var RequestCensusGroupSchema = object({
29868
29911
  var RequestCensusProcedureSchema = object({
29869
29912
  procedure: string(),
29870
29913
  calls: number(),
29914
+ /**
29915
+ * The same total, split by transport. THIS is the row that answers the
29916
+ * question the census exists for: one look at `deviceManager.listAll` says
29917
+ * which plane carried the 4 960, without joining two log lines by eye.
29918
+ */
29919
+ planes: TransportPlaneCountsSchema,
29920
+ /** Subscription STARTS on this procedure. Never folded into `calls`. */
29921
+ subscriptions: number(),
29871
29922
  perMin: number()
29872
29923
  });
29873
29924
  /**
@@ -29895,14 +29946,45 @@ var RequestCensusStatusSchema = object({
29895
29946
  */
29896
29947
  procedureCalls: number(),
29897
29948
  /**
29949
+ * `procedureCalls` split by transport. The four keys sum to
29950
+ * `procedureCalls` by construction - {@link RequestCensusSnapshotSchema}'s
29951
+ * `planesExplainTotal` is that identity, checked rather than assumed.
29952
+ */
29953
+ planes: TransportPlaneCountsSchema,
29954
+ /**
29955
+ * True iff `planes` sums to `procedureCalls`. False means a call was counted
29956
+ * on no plane at all - which is a RESULT (a plane is missing from the
29957
+ * instrument), not a failure, and it has to be visible to be read as one.
29958
+ */
29959
+ planesExplainTotal: boolean(),
29960
+ /**
29898
29961
  * tRPC WebSocket connections opened during the window. NOT calls - the WS
29899
- * transport resolves one context per connection - but the number that says
29900
- * whether a plane this census cannot see was busy while HTTP was quiet.
29962
+ * adapter resolves one context per connection - kept because a plane's call
29963
+ * count of zero against 37 open connections says something different from a
29964
+ * plane with no connections at all.
29901
29965
  */
29902
29966
  wsConnections: number(),
29967
+ /**
29968
+ * Client frames the WS plane looked at. `wsMessages` far above
29969
+ * `planes.ws + subscriptions` means most traffic is not operations
29970
+ * (keepalives, connection params) - which is itself an answer.
29971
+ */
29972
+ wsMessages: number(),
29973
+ /**
29974
+ * Subscription STARTS across every plane, excluded from `procedureCalls` on
29975
+ * purpose: one live-events stream opened at boot and held for six hours is
29976
+ * one subscription, and counting it as a call would let a quiet plane
29977
+ * masquerade as the storm.
29978
+ */
29979
+ subscriptions: number(),
29980
+ /** `subscription.stop` frames. Starts minus stops is what is still open. */
29981
+ subscriptionStops: number(),
29903
29982
  distinctGroups: number(),
29904
- /** Calls counted in the totals whose group attribution was shed at the
29905
- * cardinality bound. */
29983
+ /**
29984
+ * Operations counted in the totals whose CALLER attribution was shed at the
29985
+ * cardinality bound. Unrelated to the `unknown` PLANE: these calls know
29986
+ * which transport they arrived on, they just lost their group row.
29987
+ */
29906
29988
  unattributedCalls: number(),
29907
29989
  procedures: array(RequestCensusProcedureSchema).readonly(),
29908
29990
  groups: array(RequestCensusGroupSchema).readonly()
@@ -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-B8_nKey5.js");
5
+ const require_dist = require("../dist-BB-Ol201.js");
6
6
  let node_fs = require("node:fs");
7
7
  let node_fs$1 = require_dist.__toESM(node_fs, 1);
8
8
  node_fs = require_dist.__toESM(node_fs);
@@ -1,4 +1,4 @@
1
- import { M as PoolMemoryWatchdog, at as hfModelUrl, gt as resolvePoolMemoryPolicy, nt as embeddingEncoderCapability, ut as parseProcStatus, wt as BaseAddon } from "../dist-C8JUWFmR.mjs";
1
+ import { M as PoolMemoryWatchdog, at as hfModelUrl, gt as resolvePoolMemoryPolicy, nt as embeddingEncoderCapability, ut as parseProcStatus, wt as BaseAddon } from "../dist-XrPlCmxK.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import * as fs from "node:fs";
4
4
  import "node:fs";
@@ -3,7 +3,7 @@ import "./dist-CYZr2fwk.mjs";
3
3
  var e = {
4
4
  "@camstack/sdk": {
5
5
  name: "@camstack/sdk",
6
- version: "1.2.35",
6
+ version: "1.2.36",
7
7
  scope: ["default"],
8
8
  loaded: !1,
9
9
  from: "addon_pipeline_analytics_widgets",
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.113",
21
+ version: "1.2.114",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -33,7 +33,7 @@ var e = {
33
33
  },
34
34
  "@camstack/ui-library": {
35
35
  name: "@camstack/ui-library",
36
- version: "1.2.76",
36
+ version: "1.2.77",
37
37
  scope: ["default"],
38
38
  loaded: !1,
39
39
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.113",
39
+ version: "1.2.114",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -45,7 +45,7 @@ async function r() {
45
45
  }
46
46
  },
47
47
  "@camstack/sdk": {
48
- version: "1.2.35",
48
+ version: "1.2.36",
49
49
  scope: "default",
50
50
  shareConfig: {
51
51
  singleton: !0,
@@ -81,7 +81,7 @@ async function r() {
81
81
  }
82
82
  },
83
83
  "@camstack/ui-library": {
84
- version: "1.2.76",
84
+ version: "1.2.77",
85
85
  scope: "default",
86
86
  shareConfig: {
87
87
  singleton: !0,
@@ -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-B8_nKey5.js");
5
+ const require_dist = require("../dist-BB-Ol201.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");
@@ -15365,33 +15365,95 @@ function remainingSummaryCandidates(kept, taken, rank) {
15365
15365
  /**
15366
15366
  * The media a tile is drawn from, best first.
15367
15367
  *
15368
- * First-level group crop only (`groupCrop`), then the group's native-frame
15369
- * pointer, then a member's clean full frame. Member `thumbnail` / plate /
15370
- * face crops stay off this ladder: a successful plate read overwrites the
15371
- * vehicle first-level crop under the same `thumbnail` kind, and a digest
15372
- * tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
15373
- * rectangle is a permanent lie in a digest tile. Duplicate driveways from
15374
- * tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
15375
- * full-frame fallback.
15368
+ * A digest tile is a SUBJECT, so the crops come first: the group's first-level
15369
+ * crop (`groupCrop`), the group's native-frame pointer, then the TRACK's own
15370
+ * subject crop (`thumbnail`, then its `thumbnailSmall` downscale), and only
15371
+ * then the clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
15372
+ *
15373
+ * ⚠ The whole-scene rungs are a FALLBACK and stay on the ladder. Measured on
15374
+ * 60 real tracks (615/617, 24 h): 52 carry a crop, 43 carry a scene rung, and
15375
+ * 2 carry a scene rung and NO crop — dropping the fallback would delete those
15376
+ * two tiles for nothing. A track with neither is dropped, never drawn as an
15377
+ * empty cell (see `NcSummaryProducer.loadTiles`).
15378
+ *
15379
+ * ## Why `thumbnail` is back (2026-08-27)
15380
+ *
15381
+ * It was removed on 2026-08-24 (160200681) because a successful plate read
15382
+ * overwrites the vehicle first-level crop under the same `thumbnail` kind.
15383
+ * **That collision belongs to the GROUP entity**: the sealer is what writes
15384
+ * `ownerKind: 'group', kind: 'thumbnail'` (`group/group-sealer.ts`), and the
15385
+ * group process is OFF (`GROUPS_PROCESS_ENABLED`, `group/group-enabled.ts`).
15386
+ * On a TRACK, plates live under `ownerKind: 'plate'` / `kind: 'plateCrop'` and
15387
+ * never touch `thumbnail`. The same commit turned the group process off, so
15388
+ * `groupCrop` and `nativeFrame` became unreachable and every digest tile fell
15389
+ * through to `keyFrame` — a whole frame — for three days.
15390
+ * `scripts/check-summary-ladder-reachable.ts` is the guard that refuses that
15391
+ * shape now.
15392
+ *
15393
+ * Boxed frames stay absent: a drawn rectangle is a permanent lie in a digest
15394
+ * tile. Plate and face crops stay absent: a digest tile of a targa is the
15395
+ * wrong picture. Duplicate driveways from tracker ID churn are collapsed by
15396
+ * `frameDedupeKey`, not by hiding the whole-scene fallback.
15376
15397
  */
15377
15398
  var SUMMARY_MEDIA_LADDER = [
15378
15399
  "groupCrop",
15379
15400
  "nativeFrame",
15401
+ "thumbnail",
15402
+ "thumbnailSmall",
15380
15403
  "keyFrame",
15381
15404
  "keyFrameSmall",
15382
15405
  "fullFrame"
15383
15406
  ];
15384
15407
  /**
15408
+ * The floor below which a `thumbnail` row is NOT a subject crop.
15409
+ *
15410
+ * `copyFaceCropIfTrackHasNoThumbnail` (`pipeline-analytics/copy-face-crop-preview.ts`)
15411
+ * copies the persisted face crop onto a track that has no preview of its own,
15412
+ * and it stores it under `kind: 'thumbnail'` — so an ArcFace **112×112**
15413
+ * alignment template lands on the ladder wearing the same kind as a real
15414
+ * subject crop. Measured: 3 of 52 track thumbnails (5.8%) are 3–10 KB
15415
+ * templates. Delivering one is a face in a digest of passages.
15416
+ *
15417
+ * 20 KiB is chosen as the smallest number that is unambiguous in BOTH
15418
+ * directions: it is 2× the largest template observed (10 KB) and above the
15419
+ * theoretical ceiling of a 112×112 JPEG at any quality, while a native-
15420
+ * resolution subject crop measures in the hundreds of KB. The second reason
15421
+ * the number is safe: a genuine crop small enough to fall under it is a
15422
+ * subject a few dozen pixels across, which is a smear in a mosaic cell — the
15423
+ * whole-scene rung below is the better picture either way.
15424
+ *
15425
+ * The check costs NOTHING: `MediaFileInfo.sizeBytes` is already on the index
15426
+ * row, so no blob is fetched and nothing is decoded to apply it.
15427
+ */
15428
+ var SUMMARY_THUMBNAIL_MIN_BYTES = 20480;
15429
+ /** The rungs {@link SUMMARY_THUMBNAIL_MIN_BYTES} is applied to. */
15430
+ var SUMMARY_SIZE_GATED_RUNGS = new Set(["thumbnail"]);
15431
+ /**
15432
+ * Is this row a picture, or a template wearing a picture's kind?
15433
+ *
15434
+ * Only a size the index REPORTED can demote a row. A `null` size is unknown,
15435
+ * and demoting on unknown would silently delete every group-owned pointer.
15436
+ */
15437
+ function rowClearsSizeFloor(row, rung) {
15438
+ if (!SUMMARY_SIZE_GATED_RUNGS.has(rung)) return true;
15439
+ if (typeof row.sizeBytes !== "number") return true;
15440
+ return row.sizeBytes >= SUMMARY_THUMBNAIL_MIN_BYTES;
15441
+ }
15442
+ /**
15385
15443
  * Pick ONE media row for a track — the cheap question first (index rows only),
15386
15444
  * so exactly one blob is ever read.
15387
15445
  *
15388
15446
  * Within a rung the row CLOSEST to the track's peak is chosen; a member may
15389
15447
  * carry more than one full-frame still, so "the first one" would systematically
15390
15448
  * pick the moment the subject entered rather than the moment it was best seen.
15449
+ *
15450
+ * A `thumbnail` under {@link SUMMARY_THUMBNAIL_MIN_BYTES} DESCENDS to the next
15451
+ * rung rather than being drawn — see that constant for the face template it
15452
+ * exists to catch.
15391
15453
  */
15392
15454
  function pickSummaryMedia(rows, aroundMs) {
15393
15455
  for (const rung of SUMMARY_MEDIA_LADDER) {
15394
- const matches = rows.filter((row) => row.kind === rung);
15456
+ const matches = rows.filter((row) => row.kind === rung && rowClearsSizeFloor(row, rung));
15395
15457
  if (matches.length === 0) continue;
15396
15458
  return matches.reduce((best, row) => Math.abs(row.timestamp - aroundMs) < Math.abs(best.timestamp - aroundMs) ? row : best);
15397
15459
  }
@@ -15703,12 +15765,13 @@ var NcSummaryProducer = class {
15703
15765
  droppedStationary: filtered.droppedStationary
15704
15766
  };
15705
15767
  }
15706
- const loaded = await this.loadTiles({
15768
+ const tileLoad = await this.loadTiles({
15707
15769
  rule,
15708
15770
  preferred: selection.tiles,
15709
15771
  refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
15710
15772
  maxTiles: rule.maxTiles
15711
15773
  });
15774
+ const loaded = tileLoad.tiles;
15712
15775
  const detections = countDetections({ tracks: selection.tiles.map((t) => ({
15713
15776
  firstSeen: t.firstSeen,
15714
15777
  lastSeen: t.lastSeen,
@@ -15766,7 +15829,8 @@ var NcSummaryProducer = class {
15766
15829
  tiles: loaded.length,
15767
15830
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
15768
15831
  camerasDropped: selection.camerasDropped,
15769
- droppedNoMedia: Math.max(0, rule.maxTiles - loaded.length),
15832
+ droppedNoMedia: tileLoad.droppedNoMedia,
15833
+ droppedDuplicateFrame: tileLoad.droppedDuplicateFrame,
15770
15834
  mosaicBytes: mosaic.bytes,
15771
15835
  mosaicPublished: mosaic.ref !== null,
15772
15836
  enqueued,
@@ -15832,14 +15896,20 @@ var NcSummaryProducer = class {
15832
15896
  }
15833
15897
  /** One blob per tile — the cheap question (index rows) always first.
15834
15898
  * Duplicate camera-seconds are skipped so tracker ID churn cannot paint
15835
- * the same driveway eleven times; leftover ranked candidates refill. */
15899
+ * the same driveway eleven times; leftover ranked candidates refill.
15900
+ *
15901
+ * It COUNTS what it drops, per reason, because only this loop knows: the
15902
+ * caller sees tiles and has no way to tell a track that had no picture from
15903
+ * a rule whose quota was simply larger than the night. */
15836
15904
  async loadTiles(input) {
15837
- const out = [];
15905
+ const tiles = [];
15838
15906
  const seenFrames = /* @__PURE__ */ new Set();
15839
15907
  const seenIds = /* @__PURE__ */ new Set();
15908
+ let droppedNoMedia = 0;
15909
+ let droppedDuplicateFrame = 0;
15840
15910
  const queue = [...input.preferred, ...input.refill];
15841
15911
  for (const candidate of queue) {
15842
- if (out.length >= input.maxTiles) break;
15912
+ if (tiles.length >= input.maxTiles) break;
15843
15913
  if (seenIds.has(candidate.trackId)) continue;
15844
15914
  seenIds.add(candidate.trackId);
15845
15915
  const tags = { deviceId: candidate.deviceId };
@@ -15847,19 +15917,22 @@ var NcSummaryProducer = class {
15847
15917
  const rows = await this.deps.listMediaInfo(candidate.trackId);
15848
15918
  const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
15849
15919
  if (picked === null) {
15850
- this.deps.logger.debug("summary tile dropped: the track carries no clean media", {
15920
+ droppedNoMedia += 1;
15921
+ this.deps.logger.info("summary tile dropped: the track carries no clean media", {
15851
15922
  tags,
15852
15923
  meta: {
15853
15924
  ruleId: input.rule.id,
15854
15925
  trackId: candidate.trackId,
15855
- mediaRows: rows.length
15926
+ mediaRows: rows.length,
15927
+ mediaKinds: [...new Set(rows.map((row) => row.kind))].sort()
15856
15928
  }
15857
15929
  });
15858
15930
  continue;
15859
15931
  }
15860
15932
  const frameKey = frameDedupeKey(candidate.deviceId, picked.timestamp);
15861
15933
  if (seenFrames.has(frameKey)) {
15862
- this.deps.logger.debug("summary tile dropped: same frame as an earlier tile", {
15934
+ droppedDuplicateFrame += 1;
15935
+ this.deps.logger.info("summary tile dropped: same frame as an earlier tile", {
15863
15936
  tags,
15864
15937
  meta: {
15865
15938
  ruleId: input.rule.id,
@@ -15871,22 +15944,25 @@ var NcSummaryProducer = class {
15871
15944
  }
15872
15945
  const jpeg = await this.deps.readMedia(picked.key);
15873
15946
  if (jpeg === null || jpeg.byteLength === 0) {
15874
- this.deps.logger.debug("summary tile dropped: the media blob could not be read", {
15947
+ droppedNoMedia += 1;
15948
+ this.deps.logger.info("summary tile dropped: the media blob could not be read", {
15875
15949
  tags,
15876
15950
  meta: {
15877
15951
  ruleId: input.rule.id,
15878
15952
  trackId: candidate.trackId,
15879
- mediaKey: picked.key
15953
+ mediaKey: picked.key,
15954
+ mediaKind: picked.kind
15880
15955
  }
15881
15956
  });
15882
15957
  continue;
15883
15958
  }
15884
15959
  seenFrames.add(frameKey);
15885
- out.push({
15960
+ tiles.push({
15886
15961
  candidate,
15887
15962
  jpeg
15888
15963
  });
15889
15964
  } catch (err) {
15965
+ droppedNoMedia += 1;
15890
15966
  this.deps.logger.warn("summary tile dropped: reading its media threw", {
15891
15967
  tags,
15892
15968
  meta: {
@@ -15897,7 +15973,11 @@ var NcSummaryProducer = class {
15897
15973
  });
15898
15974
  }
15899
15975
  }
15900
- return out;
15976
+ return {
15977
+ tiles,
15978
+ droppedNoMedia,
15979
+ droppedDuplicateFrame
15980
+ };
15901
15981
  }
15902
15982
  /**
15903
15983
  * Render the mosaic, file it where event media lives, and mint the public
@@ -41300,12 +41380,14 @@ function digestMediaRows(input) {
41300
41380
  if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
41301
41381
  key: input.mediaKey,
41302
41382
  kind: "groupCrop",
41303
- timestamp: input.timestamp
41383
+ timestamp: input.timestamp,
41384
+ sizeBytes: null
41304
41385
  });
41305
41386
  if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
41306
41387
  key: input.nativeKey,
41307
41388
  kind: "nativeFrame",
41308
- timestamp: input.timestamp
41389
+ timestamp: input.timestamp,
41390
+ sizeBytes: null
41309
41391
  });
41310
41392
  if (owned.length > 0) return owned;
41311
41393
  return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
@@ -60568,7 +60650,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60568
60650
  const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
60569
60651
  key: r.key,
60570
60652
  kind: r.kind,
60571
- timestamp: r.timestamp
60653
+ timestamp: r.timestamp,
60654
+ sizeBytes: r.sizeBytes
60572
60655
  })));
60573
60656
  return digestMediaRows({
60574
60657
  mediaKey: group.mediaKey,
@@ -60580,7 +60663,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_dist.B
60580
60663
  return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
60581
60664
  key: r.key,
60582
60665
  kind: r.kind,
60583
- timestamp: r.timestamp
60666
+ timestamp: r.timestamp,
60667
+ sizeBytes: r.sizeBytes
60584
60668
  }));
60585
60669
  },
60586
60670
  readMedia: async (key) => {
@@ -1,4 +1,4 @@
1
- import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-C8JUWFmR.mjs";
1
+ import { $ as customAction, A as NcTaxonomySchema, At as sleep, B as TIMELAPSE_DENSE_FLOOR_SEC, Bt as string, C as NcRuleSchema, Ct as errMsg, D as NcSnoozeSchema, Dt as hydrateSchema, E as NcSnoozeInputSchema, Et as createEvent, Ft as literal, G as VISIT_MERGE_GAP_MS, H as TimelapseRulePatchSchema, Ht as EventCategory, I as SCENE_DEFAULT_ANCHOR_THRESHOLD, It as number, J as assertTimelapseCadences, K as addonWidgetsSourceCapability, L as SCENE_DEFAULT_UNCOVERED_POLICY, Lt as object, Mt as array, N as RECORDING_EXPORT_MAX_READ_BYTES, Nt as boolean, O as NcSnoozeSuppressedSchema, Ot as isDeviceScopedCap, P as RetrainStatusSchema, Pt as discriminatedUnion, Q as cosineSimilarity$1, R as SCENE_DIVERGED, Rt as partialRecord, S as NcRulePatchSchema, St as zoneAnalyticsCapability, T as NcScheduleSchema, Tt as DeviceType, U as TimelapseRuleSchema, V as TimelapseRuleInputSchema, Vt as unknown, W as TrackSourceSchema, X as audioModeOf, Y as audioMetricsCapability, Z as buildEventKindDescriptor, _ as NC_DEFAULT_SNOOZE_MINUTES, _t as sceneMonitorCapability, a as DEFAULT_EVENT_COLOR, b as NcConditionDescriptorSchema, bt as vectorDimFromBase64, ct as kebabToCamel, d as EVENT_PAD_MS, dt as pickClusterStepModels, et as defineCustomActions, f as FULL_IMAGE_BBOX, ft as pipelineAnalyticsCapability, g as NC_CONDITION_CATALOG, h as NC_ALARM_SYSTEM_EVENT_KINDS, ht as readTimelapseGeneratedAt, i as CLUSTER_MODEL_SCOPED_STEPS, it as faceGalleryCapability, j as OpsLogEntrySchema, jt as _enum, k as NcSystemEventKindSchema, kt as nodePin, l as DeclaredDevices, lt as notificationRulesCapability, m as MACRO_LABELS, mt as readDeviceStateFrom, n as AnalyticsGroupRecordSchema, ot as isDetectionMacroClass, p as LabelAttributionSchema, pt as plateGalleryCapability, q as alarmPanelCapability, r as BaseDevice, rt as encodeVectorBase64, s as DETECTION_MACRO_CLASSES, st as isScheduleActive, t as AnalyticsGroupDetailSchema, tt as deriveRecordingMode, u as EVENT_KIND_BY_CAP, vt as subKindsOf, w as NcRuleTargetSchema, wt as BaseAddon, x as NcRuleInputSchema, xt as videoclipsCapability, y as NC_TAXONOMY, yt as systemEventFilterApplies, z as SceneMonitorSchema, zt as record } from "../dist-XrPlCmxK.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";
@@ -15358,33 +15358,95 @@ function remainingSummaryCandidates(kept, taken, rank) {
15358
15358
  /**
15359
15359
  * The media a tile is drawn from, best first.
15360
15360
  *
15361
- * First-level group crop only (`groupCrop`), then the group's native-frame
15362
- * pointer, then a member's clean full frame. Member `thumbnail` / plate /
15363
- * face crops stay off this ladder: a successful plate read overwrites the
15364
- * vehicle first-level crop under the same `thumbnail` kind, and a digest
15365
- * tile of a targa is the wrong picture. Boxed frames stay absent: a drawn
15366
- * rectangle is a permanent lie in a digest tile. Duplicate driveways from
15367
- * tracker ID churn are collapsed by `frameDedupeKey`, not by hiding the
15368
- * full-frame fallback.
15361
+ * A digest tile is a SUBJECT, so the crops come first: the group's first-level
15362
+ * crop (`groupCrop`), the group's native-frame pointer, then the TRACK's own
15363
+ * subject crop (`thumbnail`, then its `thumbnailSmall` downscale), and only
15364
+ * then the clean whole scene (`keyFrame` → `keyFrameSmall` → `fullFrame`).
15365
+ *
15366
+ * ⚠ The whole-scene rungs are a FALLBACK and stay on the ladder. Measured on
15367
+ * 60 real tracks (615/617, 24 h): 52 carry a crop, 43 carry a scene rung, and
15368
+ * 2 carry a scene rung and NO crop — dropping the fallback would delete those
15369
+ * two tiles for nothing. A track with neither is dropped, never drawn as an
15370
+ * empty cell (see `NcSummaryProducer.loadTiles`).
15371
+ *
15372
+ * ## Why `thumbnail` is back (2026-08-27)
15373
+ *
15374
+ * It was removed on 2026-08-24 (160200681) because a successful plate read
15375
+ * overwrites the vehicle first-level crop under the same `thumbnail` kind.
15376
+ * **That collision belongs to the GROUP entity**: the sealer is what writes
15377
+ * `ownerKind: 'group', kind: 'thumbnail'` (`group/group-sealer.ts`), and the
15378
+ * group process is OFF (`GROUPS_PROCESS_ENABLED`, `group/group-enabled.ts`).
15379
+ * On a TRACK, plates live under `ownerKind: 'plate'` / `kind: 'plateCrop'` and
15380
+ * never touch `thumbnail`. The same commit turned the group process off, so
15381
+ * `groupCrop` and `nativeFrame` became unreachable and every digest tile fell
15382
+ * through to `keyFrame` — a whole frame — for three days.
15383
+ * `scripts/check-summary-ladder-reachable.ts` is the guard that refuses that
15384
+ * shape now.
15385
+ *
15386
+ * Boxed frames stay absent: a drawn rectangle is a permanent lie in a digest
15387
+ * tile. Plate and face crops stay absent: a digest tile of a targa is the
15388
+ * wrong picture. Duplicate driveways from tracker ID churn are collapsed by
15389
+ * `frameDedupeKey`, not by hiding the whole-scene fallback.
15369
15390
  */
15370
15391
  var SUMMARY_MEDIA_LADDER = [
15371
15392
  "groupCrop",
15372
15393
  "nativeFrame",
15394
+ "thumbnail",
15395
+ "thumbnailSmall",
15373
15396
  "keyFrame",
15374
15397
  "keyFrameSmall",
15375
15398
  "fullFrame"
15376
15399
  ];
15377
15400
  /**
15401
+ * The floor below which a `thumbnail` row is NOT a subject crop.
15402
+ *
15403
+ * `copyFaceCropIfTrackHasNoThumbnail` (`pipeline-analytics/copy-face-crop-preview.ts`)
15404
+ * copies the persisted face crop onto a track that has no preview of its own,
15405
+ * and it stores it under `kind: 'thumbnail'` — so an ArcFace **112×112**
15406
+ * alignment template lands on the ladder wearing the same kind as a real
15407
+ * subject crop. Measured: 3 of 52 track thumbnails (5.8%) are 3–10 KB
15408
+ * templates. Delivering one is a face in a digest of passages.
15409
+ *
15410
+ * 20 KiB is chosen as the smallest number that is unambiguous in BOTH
15411
+ * directions: it is 2× the largest template observed (10 KB) and above the
15412
+ * theoretical ceiling of a 112×112 JPEG at any quality, while a native-
15413
+ * resolution subject crop measures in the hundreds of KB. The second reason
15414
+ * the number is safe: a genuine crop small enough to fall under it is a
15415
+ * subject a few dozen pixels across, which is a smear in a mosaic cell — the
15416
+ * whole-scene rung below is the better picture either way.
15417
+ *
15418
+ * The check costs NOTHING: `MediaFileInfo.sizeBytes` is already on the index
15419
+ * row, so no blob is fetched and nothing is decoded to apply it.
15420
+ */
15421
+ var SUMMARY_THUMBNAIL_MIN_BYTES = 20480;
15422
+ /** The rungs {@link SUMMARY_THUMBNAIL_MIN_BYTES} is applied to. */
15423
+ var SUMMARY_SIZE_GATED_RUNGS = new Set(["thumbnail"]);
15424
+ /**
15425
+ * Is this row a picture, or a template wearing a picture's kind?
15426
+ *
15427
+ * Only a size the index REPORTED can demote a row. A `null` size is unknown,
15428
+ * and demoting on unknown would silently delete every group-owned pointer.
15429
+ */
15430
+ function rowClearsSizeFloor(row, rung) {
15431
+ if (!SUMMARY_SIZE_GATED_RUNGS.has(rung)) return true;
15432
+ if (typeof row.sizeBytes !== "number") return true;
15433
+ return row.sizeBytes >= SUMMARY_THUMBNAIL_MIN_BYTES;
15434
+ }
15435
+ /**
15378
15436
  * Pick ONE media row for a track — the cheap question first (index rows only),
15379
15437
  * so exactly one blob is ever read.
15380
15438
  *
15381
15439
  * Within a rung the row CLOSEST to the track's peak is chosen; a member may
15382
15440
  * carry more than one full-frame still, so "the first one" would systematically
15383
15441
  * pick the moment the subject entered rather than the moment it was best seen.
15442
+ *
15443
+ * A `thumbnail` under {@link SUMMARY_THUMBNAIL_MIN_BYTES} DESCENDS to the next
15444
+ * rung rather than being drawn — see that constant for the face template it
15445
+ * exists to catch.
15384
15446
  */
15385
15447
  function pickSummaryMedia(rows, aroundMs) {
15386
15448
  for (const rung of SUMMARY_MEDIA_LADDER) {
15387
- const matches = rows.filter((row) => row.kind === rung);
15449
+ const matches = rows.filter((row) => row.kind === rung && rowClearsSizeFloor(row, rung));
15388
15450
  if (matches.length === 0) continue;
15389
15451
  return matches.reduce((best, row) => Math.abs(row.timestamp - aroundMs) < Math.abs(best.timestamp - aroundMs) ? row : best);
15390
15452
  }
@@ -15696,12 +15758,13 @@ var NcSummaryProducer = class {
15696
15758
  droppedStationary: filtered.droppedStationary
15697
15759
  };
15698
15760
  }
15699
- const loaded = await this.loadTiles({
15761
+ const tileLoad = await this.loadTiles({
15700
15762
  rule,
15701
15763
  preferred: selection.tiles,
15702
15764
  refill: remainingSummaryCandidates(filtered.kept, selection.tiles, rule.rank),
15703
15765
  maxTiles: rule.maxTiles
15704
15766
  });
15767
+ const loaded = tileLoad.tiles;
15705
15768
  const detections = countDetections({ tracks: selection.tiles.map((t) => ({
15706
15769
  firstSeen: t.firstSeen,
15707
15770
  lastSeen: t.lastSeen,
@@ -15759,7 +15822,8 @@ var NcSummaryProducer = class {
15759
15822
  tiles: loaded.length,
15760
15823
  cameras: new Set(loaded.map((t) => t.candidate.deviceId)).size,
15761
15824
  camerasDropped: selection.camerasDropped,
15762
- droppedNoMedia: Math.max(0, rule.maxTiles - loaded.length),
15825
+ droppedNoMedia: tileLoad.droppedNoMedia,
15826
+ droppedDuplicateFrame: tileLoad.droppedDuplicateFrame,
15763
15827
  mosaicBytes: mosaic.bytes,
15764
15828
  mosaicPublished: mosaic.ref !== null,
15765
15829
  enqueued,
@@ -15825,14 +15889,20 @@ var NcSummaryProducer = class {
15825
15889
  }
15826
15890
  /** One blob per tile — the cheap question (index rows) always first.
15827
15891
  * Duplicate camera-seconds are skipped so tracker ID churn cannot paint
15828
- * the same driveway eleven times; leftover ranked candidates refill. */
15892
+ * the same driveway eleven times; leftover ranked candidates refill.
15893
+ *
15894
+ * It COUNTS what it drops, per reason, because only this loop knows: the
15895
+ * caller sees tiles and has no way to tell a track that had no picture from
15896
+ * a rule whose quota was simply larger than the night. */
15829
15897
  async loadTiles(input) {
15830
- const out = [];
15898
+ const tiles = [];
15831
15899
  const seenFrames = /* @__PURE__ */ new Set();
15832
15900
  const seenIds = /* @__PURE__ */ new Set();
15901
+ let droppedNoMedia = 0;
15902
+ let droppedDuplicateFrame = 0;
15833
15903
  const queue = [...input.preferred, ...input.refill];
15834
15904
  for (const candidate of queue) {
15835
- if (out.length >= input.maxTiles) break;
15905
+ if (tiles.length >= input.maxTiles) break;
15836
15906
  if (seenIds.has(candidate.trackId)) continue;
15837
15907
  seenIds.add(candidate.trackId);
15838
15908
  const tags = { deviceId: candidate.deviceId };
@@ -15840,19 +15910,22 @@ var NcSummaryProducer = class {
15840
15910
  const rows = await this.deps.listMediaInfo(candidate.trackId);
15841
15911
  const picked = pickSummaryMedia(rows, (candidate.firstSeen + candidate.lastSeen) / 2);
15842
15912
  if (picked === null) {
15843
- this.deps.logger.debug("summary tile dropped: the track carries no clean media", {
15913
+ droppedNoMedia += 1;
15914
+ this.deps.logger.info("summary tile dropped: the track carries no clean media", {
15844
15915
  tags,
15845
15916
  meta: {
15846
15917
  ruleId: input.rule.id,
15847
15918
  trackId: candidate.trackId,
15848
- mediaRows: rows.length
15919
+ mediaRows: rows.length,
15920
+ mediaKinds: [...new Set(rows.map((row) => row.kind))].sort()
15849
15921
  }
15850
15922
  });
15851
15923
  continue;
15852
15924
  }
15853
15925
  const frameKey = frameDedupeKey(candidate.deviceId, picked.timestamp);
15854
15926
  if (seenFrames.has(frameKey)) {
15855
- this.deps.logger.debug("summary tile dropped: same frame as an earlier tile", {
15927
+ droppedDuplicateFrame += 1;
15928
+ this.deps.logger.info("summary tile dropped: same frame as an earlier tile", {
15856
15929
  tags,
15857
15930
  meta: {
15858
15931
  ruleId: input.rule.id,
@@ -15864,22 +15937,25 @@ var NcSummaryProducer = class {
15864
15937
  }
15865
15938
  const jpeg = await this.deps.readMedia(picked.key);
15866
15939
  if (jpeg === null || jpeg.byteLength === 0) {
15867
- this.deps.logger.debug("summary tile dropped: the media blob could not be read", {
15940
+ droppedNoMedia += 1;
15941
+ this.deps.logger.info("summary tile dropped: the media blob could not be read", {
15868
15942
  tags,
15869
15943
  meta: {
15870
15944
  ruleId: input.rule.id,
15871
15945
  trackId: candidate.trackId,
15872
- mediaKey: picked.key
15946
+ mediaKey: picked.key,
15947
+ mediaKind: picked.kind
15873
15948
  }
15874
15949
  });
15875
15950
  continue;
15876
15951
  }
15877
15952
  seenFrames.add(frameKey);
15878
- out.push({
15953
+ tiles.push({
15879
15954
  candidate,
15880
15955
  jpeg
15881
15956
  });
15882
15957
  } catch (err) {
15958
+ droppedNoMedia += 1;
15883
15959
  this.deps.logger.warn("summary tile dropped: reading its media threw", {
15884
15960
  tags,
15885
15961
  meta: {
@@ -15890,7 +15966,11 @@ var NcSummaryProducer = class {
15890
15966
  });
15891
15967
  }
15892
15968
  }
15893
- return out;
15969
+ return {
15970
+ tiles,
15971
+ droppedNoMedia,
15972
+ droppedDuplicateFrame
15973
+ };
15894
15974
  }
15895
15975
  /**
15896
15976
  * Render the mosaic, file it where event media lives, and mint the public
@@ -41237,12 +41317,14 @@ function digestMediaRows(input) {
41237
41317
  if (input.mediaKey !== null && input.mediaKey.length > 0) owned.push({
41238
41318
  key: input.mediaKey,
41239
41319
  kind: "groupCrop",
41240
- timestamp: input.timestamp
41320
+ timestamp: input.timestamp,
41321
+ sizeBytes: null
41241
41322
  });
41242
41323
  if (input.nativeKey !== null && input.nativeKey.length > 0) owned.push({
41243
41324
  key: input.nativeKey,
41244
41325
  kind: "nativeFrame",
41245
- timestamp: input.timestamp
41326
+ timestamp: input.timestamp,
41327
+ sizeBytes: null
41246
41328
  });
41247
41329
  if (owned.length > 0) return owned;
41248
41330
  return input.memberRows.filter((row) => DIGEST_MEMBER_FALLBACK_KINDS.has(row.kind));
@@ -60505,7 +60587,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
60505
60587
  const memberRows = (await Promise.all(members.map((m) => stores.mediaStore.listInfoByOwner("track", m.trackId)))).flatMap((rows) => rows.map((r) => ({
60506
60588
  key: r.key,
60507
60589
  kind: r.kind,
60508
- timestamp: r.timestamp
60590
+ timestamp: r.timestamp,
60591
+ sizeBytes: r.sizeBytes
60509
60592
  })));
60510
60593
  return digestMediaRows({
60511
60594
  mediaKey: group.mediaKey,
@@ -60517,7 +60600,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
60517
60600
  return (await stores.mediaStore.listInfoByOwner("track", id)).map((r) => ({
60518
60601
  key: r.key,
60519
60602
  kind: r.kind,
60520
- timestamp: r.timestamp
60603
+ timestamp: r.timestamp,
60604
+ sizeBytes: r.sizeBytes
60521
60605
  }));
60522
60606
  },
60523
60607
  readMedia: async (key) => {
@@ -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-CaoDYCwF.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-_ZUR9O1b.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.131",
3
+ "version": "1.2.132",
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",