@camstack/addon-post-analysis 1.2.100 → 1.2.101

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.
@@ -18,7 +18,7 @@ var e = {
18
18
  },
19
19
  "@camstack/types": {
20
20
  name: "@camstack/types",
21
- version: "1.2.90",
21
+ version: "1.2.91",
22
22
  scope: ["default"],
23
23
  loaded: !1,
24
24
  from: "addon_pipeline_analytics_widgets",
@@ -36,7 +36,7 @@ async function r() {
36
36
  }
37
37
  },
38
38
  "@camstack/types": {
39
- version: "1.2.90",
39
+ version: "1.2.91",
40
40
  scope: "default",
41
41
  shareConfig: {
42
42
  singleton: !0,
@@ -8304,23 +8304,23 @@ var en_default = {
8304
8304
  },
8305
8305
  "class.other": "{{name}}",
8306
8306
  "group.others.person": {
8307
- "one": "1 other person",
8307
+ "one": "{{count}} other person",
8308
8308
  "other": "{{count}} other people"
8309
8309
  },
8310
8310
  "group.others.vehicle": {
8311
- "one": "1 other vehicle",
8311
+ "one": "{{count}} other vehicle",
8312
8312
  "other": "{{count}} other vehicles"
8313
8313
  },
8314
8314
  "group.others.animal": {
8315
- "one": "1 other animal",
8315
+ "one": "{{count}} other animal",
8316
8316
  "other": "{{count}} other animals"
8317
8317
  },
8318
8318
  "group.others.package": {
8319
- "one": "1 other package",
8319
+ "one": "{{count}} other package",
8320
8320
  "other": "{{count}} other packages"
8321
8321
  },
8322
8322
  "group.others.other": {
8323
- "one": "1 other {{classNoun}}",
8323
+ "one": "{{count}} other {{classNoun}}",
8324
8324
  "other": "{{count}} other {{classNoun}}"
8325
8325
  },
8326
8326
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
@@ -8518,23 +8518,23 @@ var it_default = {
8518
8518
  },
8519
8519
  "class.other": "{{name}}",
8520
8520
  "group.others.person": {
8521
- "one": "1 altra persona",
8521
+ "one": "{{count}} altra persona",
8522
8522
  "other": "{{count}} altre persone"
8523
8523
  },
8524
8524
  "group.others.vehicle": {
8525
- "one": "1 altro veicolo",
8525
+ "one": "{{count}} altro veicolo",
8526
8526
  "other": "{{count}} altri veicoli"
8527
8527
  },
8528
8528
  "group.others.animal": {
8529
- "one": "1 altro animale",
8529
+ "one": "{{count}} altro animale",
8530
8530
  "other": "{{count}} altri animali"
8531
8531
  },
8532
8532
  "group.others.package": {
8533
- "one": "1 altro pacco",
8533
+ "one": "{{count}} altro pacco",
8534
8534
  "other": "{{count}} altri pacchi"
8535
8535
  },
8536
8536
  "group.others.other": {
8537
- "one": "1 altro {{classNoun}}",
8537
+ "one": "{{count}} altro {{classNoun}}",
8538
8538
  "other": "{{count}} altri {{classNoun}}"
8539
8539
  },
8540
8540
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
@@ -31058,8 +31058,14 @@ function reportUnvouchedClips(deps, deviceId, clips) {
31058
31058
  * @durable class=config owner=pipeline-analytics
31059
31059
  * write="a signed-in viewer user saves a named snapshot of their local stores"
31060
31060
  * retention="none — a row goes only when its owner deletes it"
31061
+ *
31062
+ * Renamed from the bare `viewer-settings-snapshots` on 2026-08-19: an addon
31063
+ * collection carries its owner's prefix, and the bare name sat in the same flat
31064
+ * namespace as the kernel's `users` / `system-settings`. Rows already written
31065
+ * under the old name are adopted at boot by
31066
+ * `viewer-settings-legacy-adoption.ts`.
31061
31067
  */
31062
- var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
31068
+ var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
31063
31069
  var COLUMNS = [
31064
31070
  {
31065
31071
  name: "id",
@@ -31159,7 +31165,7 @@ var ViewerSettingsSnapshotStore = class {
31159
31165
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31160
31166
  columns: [...COLUMNS],
31161
31167
  indexes: [{
31162
- name: "idx_viewer_settings_owner",
31168
+ name: "idx_pa_viewer_settings_owner",
31163
31169
  columns: ["ownerUserId"]
31164
31170
  }]
31165
31171
  });
@@ -31258,6 +31264,183 @@ function makeViewerSettingsActionHandlers(store) {
31258
31264
  }
31259
31265
  };
31260
31266
  }
31267
+ //#endregion
31268
+ //#region src/pipeline-analytics/viewer-settings-legacy-adoption.ts
31269
+ /**
31270
+ * @durable class=config owner=pipeline-analytics
31271
+ * write="NOTHING writes it any more — it is the pre-2026-08-19 bare name of
31272
+ * `pipeline-analytics:viewer-settings-snapshots`, declared here only so its
31273
+ * surviving rows can be READ once and moved"
31274
+ * retention="drained, not pruned: a row goes as soon as the successor is
31275
+ * confirmed to hold the same id. The table is empty and inert once every
31276
+ * hub has booted this addon; it is then dropped through the kernel's
31277
+ * `RETIRED_COLLECTIONS` and this module deleted."
31278
+ *
31279
+ * Bare, and therefore a namespacing violation — hence the single
31280
+ * `ALLOWED_BARE` entry in `scripts/check-collection-namespacing.ts`, which
31281
+ * names this file and the condition for removing both.
31282
+ */
31283
+ var LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
31284
+ /**
31285
+ * The ORIGINAL column list, byte-for-byte.
31286
+ *
31287
+ * Do not "tidy" this to reference the successor's constant: they are allowed to
31288
+ * diverge, and the day they do, a shared constant would silently rebuild the
31289
+ * legacy table and destroy the rows.
31290
+ */
31291
+ var LEGACY_COLUMNS = [
31292
+ {
31293
+ name: "id",
31294
+ type: "TEXT",
31295
+ primaryKey: true,
31296
+ notNull: true
31297
+ },
31298
+ {
31299
+ name: "name",
31300
+ type: "TEXT",
31301
+ notNull: true
31302
+ },
31303
+ {
31304
+ name: "ownerUserId",
31305
+ type: "TEXT",
31306
+ notNull: true
31307
+ },
31308
+ {
31309
+ name: "ownerUsername",
31310
+ type: "TEXT",
31311
+ notNull: true
31312
+ },
31313
+ {
31314
+ name: "createdAt",
31315
+ type: "INTEGER",
31316
+ notNull: true
31317
+ },
31318
+ {
31319
+ name: "updatedAt",
31320
+ type: "INTEGER",
31321
+ notNull: true
31322
+ },
31323
+ {
31324
+ name: "blob",
31325
+ type: "JSON",
31326
+ notNull: true
31327
+ }
31328
+ ];
31329
+ /** Bounded read: this table held 1 row on the reference hub. */
31330
+ var LEGACY_READ_LIMIT = 500;
31331
+ function errText(err) {
31332
+ return err instanceof Error ? err.message : String(err);
31333
+ }
31334
+ function asRows(raw) {
31335
+ if (!Array.isArray(raw)) return [];
31336
+ const out = [];
31337
+ for (const item of raw) {
31338
+ if (typeof item !== "object" || item === null) continue;
31339
+ const id = Reflect.get(item, "id");
31340
+ const data = Reflect.get(item, "data");
31341
+ if (typeof id !== "string" || typeof data !== "object" || data === null) continue;
31342
+ out.push({
31343
+ id,
31344
+ data
31345
+ });
31346
+ }
31347
+ return out;
31348
+ }
31349
+ /**
31350
+ * Move every legacy row into the successor and clear the legacy table.
31351
+ *
31352
+ * Returns the number of legacy rows RETIRED — the completion metric, not the
31353
+ * number written. A row copied but not yet verified counts as zero and is
31354
+ * retried on the next boot, which is the safe direction.
31355
+ */
31356
+ async function adoptLegacyViewerSettingsSnapshots(deps) {
31357
+ await deps.store.declareCollection.mutate({
31358
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31359
+ columns: LEGACY_COLUMNS
31360
+ });
31361
+ let legacy;
31362
+ try {
31363
+ legacy = asRows(await deps.store.query.query({
31364
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31365
+ filter: { limit: LEGACY_READ_LIMIT }
31366
+ }));
31367
+ } catch (err) {
31368
+ deps.logger.warn("viewer-settings legacy adoption skipped — legacy read failed", { meta: {
31369
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31370
+ error: errText(err)
31371
+ } });
31372
+ return 0;
31373
+ }
31374
+ if (legacy.length === 0) return 0;
31375
+ let existing;
31376
+ try {
31377
+ existing = new Set(asRows(await deps.store.query.query({
31378
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31379
+ filter: { limit: LEGACY_READ_LIMIT }
31380
+ })).map((r) => r.id));
31381
+ } catch (err) {
31382
+ deps.logger.warn("viewer-settings legacy adoption skipped — successor read failed", { meta: {
31383
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31384
+ error: errText(err)
31385
+ } });
31386
+ return 0;
31387
+ }
31388
+ let written = 0;
31389
+ for (const row of legacy) {
31390
+ if (existing.has(row.id)) continue;
31391
+ try {
31392
+ await deps.store.set.mutate({
31393
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31394
+ key: row.id,
31395
+ value: row.data
31396
+ });
31397
+ written += 1;
31398
+ } catch (err) {
31399
+ deps.logger.warn("viewer-settings legacy adoption — one row failed to write", { meta: {
31400
+ snapshotId: row.id,
31401
+ error: errText(err)
31402
+ } });
31403
+ }
31404
+ }
31405
+ let landed;
31406
+ try {
31407
+ landed = new Set(asRows(await deps.store.query.query({
31408
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31409
+ filter: { limit: LEGACY_READ_LIMIT }
31410
+ })).map((r) => r.id));
31411
+ } catch (err) {
31412
+ deps.logger.warn("viewer-settings legacy adoption — verification read failed, legacy rows kept", { meta: {
31413
+ written,
31414
+ error: errText(err)
31415
+ } });
31416
+ return 0;
31417
+ }
31418
+ let retired = 0;
31419
+ for (const row of legacy) {
31420
+ if (!landed.has(row.id)) continue;
31421
+ try {
31422
+ await deps.store.delete.mutate({
31423
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31424
+ key: row.id
31425
+ });
31426
+ retired += 1;
31427
+ } catch (err) {
31428
+ deps.logger.warn("viewer-settings legacy adoption — one legacy row failed to delete", { meta: {
31429
+ snapshotId: row.id,
31430
+ error: errText(err)
31431
+ } });
31432
+ }
31433
+ }
31434
+ deps.logger.warn("viewer-settings snapshots adopted from the legacy bare collection", { meta: {
31435
+ from: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31436
+ to: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31437
+ found: legacy.length,
31438
+ written,
31439
+ retired,
31440
+ kept: legacy.length - retired
31441
+ } });
31442
+ return retired;
31443
+ }
31261
31444
  /**
31262
31445
  * The VIDEO's playback rate — 4×, the same as {@link NC_GIF_SPEED}.
31263
31446
  *
@@ -40020,6 +40203,129 @@ function buildRecordedStillPorts(input) {
40020
40203
  })
40021
40204
  };
40022
40205
  }
40206
+ /** Where the completion marker lives. */
40207
+ var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
40208
+ var MIGRATION_MARKER_KEY = "label-tier-4g";
40209
+ /**
40210
+ * The decision for ONE row, exposed so the rule is testable without a store.
40211
+ *
40212
+ * `null` = leave the row alone.
40213
+ */
40214
+ function decideRowMigration(data, now) {
40215
+ const label = data["label"];
40216
+ if (typeof label !== "string" || label.length === 0) return null;
40217
+ if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
40218
+ if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
40219
+ if (isMacroClass(label)) return {
40220
+ label: null,
40221
+ labelScore: null,
40222
+ labelMeta: null
40223
+ };
40224
+ return {
40225
+ label: null,
40226
+ labelScore: null,
40227
+ labelMeta: null,
40228
+ subLabel: label,
40229
+ subLabelScore: null,
40230
+ subLabelMeta: {
40231
+ stepId: MIGRATION_STEP_ID,
40232
+ decidedAt: now
40233
+ }
40234
+ };
40235
+ }
40236
+ /** Migrate one collection, paged and bounded. */
40237
+ async function migrateCollectionLabelTiers(deps, collection) {
40238
+ let scanned = 0;
40239
+ let demoted = 0;
40240
+ let cleared = 0;
40241
+ let skipped = 0;
40242
+ let offset = 0;
40243
+ let truncated = false;
40244
+ for (;;) {
40245
+ if (scanned >= 2e5) {
40246
+ truncated = true;
40247
+ break;
40248
+ }
40249
+ const rows = await deps.store.query.query({
40250
+ collection,
40251
+ filter: {
40252
+ limit: 500,
40253
+ offset
40254
+ }
40255
+ });
40256
+ if (rows.length === 0) break;
40257
+ for (const row of rows) {
40258
+ scanned++;
40259
+ const patch = decideRowMigration(row.data, deps.now());
40260
+ if (patch === null) {
40261
+ skipped++;
40262
+ continue;
40263
+ }
40264
+ try {
40265
+ await deps.store.update.mutate({
40266
+ collection,
40267
+ id: row.id,
40268
+ data: patch
40269
+ });
40270
+ if (patch["subLabel"] === void 0) cleared++;
40271
+ else demoted++;
40272
+ } catch (err) {
40273
+ deps.logger.warn("label tier migration row failed", { meta: {
40274
+ collection,
40275
+ id: row.id,
40276
+ error: String(err)
40277
+ } });
40278
+ skipped++;
40279
+ }
40280
+ }
40281
+ if (rows.length < 500) break;
40282
+ offset += rows.length;
40283
+ }
40284
+ return {
40285
+ collection,
40286
+ scanned,
40287
+ demoted,
40288
+ cleared,
40289
+ skipped,
40290
+ truncated
40291
+ };
40292
+ }
40293
+ /**
40294
+ * Run the migration across the collections that carry the tiered columns, once.
40295
+ *
40296
+ * The marker short-circuits a completed migration; it is written only when
40297
+ * NEITHER collection was truncated, so an interrupted pass resumes on the next
40298
+ * boot instead of declaring itself done.
40299
+ */
40300
+ async function runLabelTierMigration(deps, collections) {
40301
+ try {
40302
+ const marker = await deps.store.get.query({
40303
+ collection: MIGRATION_MARKER_COLLECTION,
40304
+ key: MIGRATION_MARKER_KEY
40305
+ });
40306
+ if (marker !== null && marker !== void 0) return [];
40307
+ } catch {}
40308
+ const stats = [];
40309
+ for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
40310
+ if (stats.every((s) => !s.truncated)) try {
40311
+ await deps.store.set.mutate({
40312
+ collection: MIGRATION_MARKER_COLLECTION,
40313
+ key: MIGRATION_MARKER_KEY,
40314
+ value: { completedAt: deps.now() }
40315
+ });
40316
+ } catch (err) {
40317
+ deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
40318
+ }
40319
+ for (const s of stats) deps.logger.info("label tier migration", { meta: {
40320
+ collection: s.collection,
40321
+ scanned: s.scanned,
40322
+ demoted: s.demoted,
40323
+ cleared: s.cleared,
40324
+ skipped: s.skipped,
40325
+ truncated: s.truncated
40326
+ } });
40327
+ return stats;
40328
+ }
40023
40329
  //#endregion
40024
40330
  //#region src/durable/lts-aggregator.ts
40025
40331
  /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
@@ -45211,6 +45517,55 @@ var ANALYTICS_COLLECTIONS = [
45211
45517
  why: "a five-minute statistics row summarises an INTERVAL, not an entity — there is no owner it can be orphaned from. Its bound is a ROW CAP per (subject, series, scope) enforced by the aggregator itself (105,120 rows ≈ one year), deliberately not an age sweep: D61 collapsed ten disagreeing age clocks into one, and an LTS series has no clock but its own."
45212
45518
  }
45213
45519
  },
45520
+ {
45521
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
45522
+ classification: {
45523
+ kind: "never-orphaned",
45524
+ why: "the ground truth an operator DREW. It declares `trackId` and must be classified by hand for that reason: the id is provenance, not ownership. The dataset addresses COPIES (D81), which is precisely what makes a 'trained' track evictable again — so a track ageing out leaves the annotations standing. Rows go only through `deleteForFrame`, when the operator deselects the frame. Deleting one because its track expired would destroy labelling work no sweep can recreate."
45525
+ }
45526
+ },
45527
+ {
45528
+ collection: RETRAIN_FRAMES_COLLECTION,
45529
+ classification: {
45530
+ kind: "never-orphaned",
45531
+ why: "the frame COPIES the dataset owns outright, under a `retrain` prefix no owner cascade and no age sweep walks (D81). Also declares `trackId` as provenance. Rows go only through `deleteFrame`, after their annotations, on operator deselection. NOTE: nothing caps this table or the disk it consumes — that is a deliberate decision, not an oversight, and it is the one collection here whose bound is an operator's habit."
45532
+ }
45533
+ },
45534
+ {
45535
+ collection: SCENE_COLLECTION,
45536
+ classification: {
45537
+ kind: "never-orphaned",
45538
+ why: "operator intent — one row per (deviceId, monitorId), created and destroyed by scene CRUD or by a device-removal sweep. Nothing retention does can orphan it, and no age clock applies: a scene the operator configured and never revisits is not stale, it is configured."
45539
+ }
45540
+ },
45541
+ {
45542
+ collection: SCENE_LATCH_COLLECTION,
45543
+ classification: {
45544
+ kind: "never-orphaned",
45545
+ why: "it IS owned — by the scene set — but the owner-cleanup is self-contained: `pruneExcept(the scene set)` runs on every scene reload, so a latch whose scene is gone is collected there and never by retention. Deliberately NOT `owner-indirection`: that kind puts a collection into `auditableCollections()`, and the orphan audit resolves exactly one indirection, that of `pipeline-analytics:media`."
45546
+ }
45547
+ },
45548
+ {
45549
+ collection: MIGRATION_MARKER_COLLECTION,
45550
+ classification: {
45551
+ kind: "never-orphaned",
45552
+ why: "a one-row completion marker for the label-tier 4g pass. It summarises a one-time event, so there is no entity it can be orphaned from — and its PRESENCE is what stops the migration running again, so deleting it is not cleanup, it is a re-run over every track, face and object-event row."
45553
+ }
45554
+ },
45555
+ {
45556
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
45557
+ classification: {
45558
+ kind: "never-orphaned",
45559
+ why: "a hub user's named snapshot of their own viewer layout. It is not analytics data — it happens to live in this addon because the actions ride `addons.custom`. Owned by the user who saved it and deleted only by them; no track, device or event can orphan it, and no age applies."
45560
+ }
45561
+ },
45562
+ {
45563
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
45564
+ classification: {
45565
+ kind: "never-orphaned",
45566
+ why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
45567
+ }
45568
+ },
45214
45569
  {
45215
45570
  collection: STATIONARY_COLLECTION,
45216
45571
  classification: {
@@ -49890,129 +50245,6 @@ function evaluatePeriodicSnapshot(input) {
49890
50245
  movedFraction
49891
50246
  };
49892
50247
  }
49893
- /** Where the completion marker lives. */
49894
- var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
49895
- var MIGRATION_MARKER_KEY = "label-tier-4g";
49896
- /**
49897
- * The decision for ONE row, exposed so the rule is testable without a store.
49898
- *
49899
- * `null` = leave the row alone.
49900
- */
49901
- function decideRowMigration(data, now) {
49902
- const label = data["label"];
49903
- if (typeof label !== "string" || label.length === 0) return null;
49904
- if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
49905
- if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
49906
- if (isMacroClass(label)) return {
49907
- label: null,
49908
- labelScore: null,
49909
- labelMeta: null
49910
- };
49911
- return {
49912
- label: null,
49913
- labelScore: null,
49914
- labelMeta: null,
49915
- subLabel: label,
49916
- subLabelScore: null,
49917
- subLabelMeta: {
49918
- stepId: MIGRATION_STEP_ID,
49919
- decidedAt: now
49920
- }
49921
- };
49922
- }
49923
- /** Migrate one collection, paged and bounded. */
49924
- async function migrateCollectionLabelTiers(deps, collection) {
49925
- let scanned = 0;
49926
- let demoted = 0;
49927
- let cleared = 0;
49928
- let skipped = 0;
49929
- let offset = 0;
49930
- let truncated = false;
49931
- for (;;) {
49932
- if (scanned >= 2e5) {
49933
- truncated = true;
49934
- break;
49935
- }
49936
- const rows = await deps.store.query.query({
49937
- collection,
49938
- filter: {
49939
- limit: 500,
49940
- offset
49941
- }
49942
- });
49943
- if (rows.length === 0) break;
49944
- for (const row of rows) {
49945
- scanned++;
49946
- const patch = decideRowMigration(row.data, deps.now());
49947
- if (patch === null) {
49948
- skipped++;
49949
- continue;
49950
- }
49951
- try {
49952
- await deps.store.update.mutate({
49953
- collection,
49954
- id: row.id,
49955
- data: patch
49956
- });
49957
- if (patch["subLabel"] === void 0) cleared++;
49958
- else demoted++;
49959
- } catch (err) {
49960
- deps.logger.warn("label tier migration row failed", { meta: {
49961
- collection,
49962
- id: row.id,
49963
- error: String(err)
49964
- } });
49965
- skipped++;
49966
- }
49967
- }
49968
- if (rows.length < 500) break;
49969
- offset += rows.length;
49970
- }
49971
- return {
49972
- collection,
49973
- scanned,
49974
- demoted,
49975
- cleared,
49976
- skipped,
49977
- truncated
49978
- };
49979
- }
49980
- /**
49981
- * Run the migration across the collections that carry the tiered columns, once.
49982
- *
49983
- * The marker short-circuits a completed migration; it is written only when
49984
- * NEITHER collection was truncated, so an interrupted pass resumes on the next
49985
- * boot instead of declaring itself done.
49986
- */
49987
- async function runLabelTierMigration(deps, collections) {
49988
- try {
49989
- const marker = await deps.store.get.query({
49990
- collection: MIGRATION_MARKER_COLLECTION,
49991
- key: MIGRATION_MARKER_KEY
49992
- });
49993
- if (marker !== null && marker !== void 0) return [];
49994
- } catch {}
49995
- const stats = [];
49996
- for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
49997
- if (stats.every((s) => !s.truncated)) try {
49998
- await deps.store.set.mutate({
49999
- collection: MIGRATION_MARKER_COLLECTION,
50000
- key: MIGRATION_MARKER_KEY,
50001
- value: { completedAt: deps.now() }
50002
- });
50003
- } catch (err) {
50004
- deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
50005
- }
50006
- for (const s of stats) deps.logger.info("label tier migration", { meta: {
50007
- collection: s.collection,
50008
- scanned: s.scanned,
50009
- demoted: s.demoted,
50010
- cleared: s.cleared,
50011
- skipped: s.skipped,
50012
- truncated: s.truncated
50013
- } });
50014
- return stats;
50015
- }
50016
50248
  //#endregion
50017
50249
  //#region src/pipeline-analytics/store/object-embedding-store.ts
50018
50250
  /**
@@ -52424,6 +52656,14 @@ var PipelineAnalyticsAddon = class extends require_dist.BaseAddon {
52424
52656
  ViewerSettingsSnapshotStore.declare(api.settingsStore)
52425
52657
  ]);
52426
52658
  this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
52659
+ try {
52660
+ await adoptLegacyViewerSettingsSnapshots({
52661
+ store: api.settingsStore,
52662
+ logger: this.ctx.logger.child("viewer-settings")
52663
+ });
52664
+ } catch (err) {
52665
+ this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: require_dist.errMsg(err) } });
52666
+ }
52427
52667
  runLabelTierMigration({
52428
52668
  store: api.settingsStore,
52429
52669
  logger: this.ctx.logger,
@@ -8297,23 +8297,23 @@ var en_default = {
8297
8297
  },
8298
8298
  "class.other": "{{name}}",
8299
8299
  "group.others.person": {
8300
- "one": "1 other person",
8300
+ "one": "{{count}} other person",
8301
8301
  "other": "{{count}} other people"
8302
8302
  },
8303
8303
  "group.others.vehicle": {
8304
- "one": "1 other vehicle",
8304
+ "one": "{{count}} other vehicle",
8305
8305
  "other": "{{count}} other vehicles"
8306
8306
  },
8307
8307
  "group.others.animal": {
8308
- "one": "1 other animal",
8308
+ "one": "{{count}} other animal",
8309
8309
  "other": "{{count}} other animals"
8310
8310
  },
8311
8311
  "group.others.package": {
8312
- "one": "1 other package",
8312
+ "one": "{{count}} other package",
8313
8313
  "other": "{{count}} other packages"
8314
8314
  },
8315
8315
  "group.others.other": {
8316
- "one": "1 other {{classNoun}}",
8316
+ "one": "{{count}} other {{classNoun}}",
8317
8317
  "other": "{{count}} other {{classNoun}}"
8318
8318
  },
8319
8319
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
@@ -8511,23 +8511,23 @@ var it_default = {
8511
8511
  },
8512
8512
  "class.other": "{{name}}",
8513
8513
  "group.others.person": {
8514
- "one": "1 altra persona",
8514
+ "one": "{{count}} altra persona",
8515
8515
  "other": "{{count}} altre persone"
8516
8516
  },
8517
8517
  "group.others.vehicle": {
8518
- "one": "1 altro veicolo",
8518
+ "one": "{{count}} altro veicolo",
8519
8519
  "other": "{{count}} altri veicoli"
8520
8520
  },
8521
8521
  "group.others.animal": {
8522
- "one": "1 altro animale",
8522
+ "one": "{{count}} altro animale",
8523
8523
  "other": "{{count}} altri animali"
8524
8524
  },
8525
8525
  "group.others.package": {
8526
- "one": "1 altro pacco",
8526
+ "one": "{{count}} altro pacco",
8527
8527
  "other": "{{count}} altri pacchi"
8528
8528
  },
8529
8529
  "group.others.other": {
8530
- "one": "1 altro {{classNoun}}",
8530
+ "one": "{{count}} altro {{classNoun}}",
8531
8531
  "other": "{{count}} altri {{classNoun}}"
8532
8532
  },
8533
8533
  "occupancy.body": "{{scope}} {{op}} ({{count}}/{{capacity}})",
@@ -30999,8 +30999,14 @@ function reportUnvouchedClips(deps, deviceId, clips) {
30999
30999
  * @durable class=config owner=pipeline-analytics
31000
31000
  * write="a signed-in viewer user saves a named snapshot of their local stores"
31001
31001
  * retention="none — a row goes only when its owner deletes it"
31002
+ *
31003
+ * Renamed from the bare `viewer-settings-snapshots` on 2026-08-19: an addon
31004
+ * collection carries its owner's prefix, and the bare name sat in the same flat
31005
+ * namespace as the kernel's `users` / `system-settings`. Rows already written
31006
+ * under the old name are adopted at boot by
31007
+ * `viewer-settings-legacy-adoption.ts`.
31002
31008
  */
31003
- var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
31009
+ var VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "pipeline-analytics:viewer-settings-snapshots";
31004
31010
  var COLUMNS = [
31005
31011
  {
31006
31012
  name: "id",
@@ -31099,7 +31105,7 @@ var ViewerSettingsSnapshotStore = class {
31099
31105
  collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31100
31106
  columns: [...COLUMNS],
31101
31107
  indexes: [{
31102
- name: "idx_viewer_settings_owner",
31108
+ name: "idx_pa_viewer_settings_owner",
31103
31109
  columns: ["ownerUserId"]
31104
31110
  }]
31105
31111
  });
@@ -31198,6 +31204,183 @@ function makeViewerSettingsActionHandlers(store) {
31198
31204
  }
31199
31205
  };
31200
31206
  }
31207
+ //#endregion
31208
+ //#region src/pipeline-analytics/viewer-settings-legacy-adoption.ts
31209
+ /**
31210
+ * @durable class=config owner=pipeline-analytics
31211
+ * write="NOTHING writes it any more — it is the pre-2026-08-19 bare name of
31212
+ * `pipeline-analytics:viewer-settings-snapshots`, declared here only so its
31213
+ * surviving rows can be READ once and moved"
31214
+ * retention="drained, not pruned: a row goes as soon as the successor is
31215
+ * confirmed to hold the same id. The table is empty and inert once every
31216
+ * hub has booted this addon; it is then dropped through the kernel's
31217
+ * `RETIRED_COLLECTIONS` and this module deleted."
31218
+ *
31219
+ * Bare, and therefore a namespacing violation — hence the single
31220
+ * `ALLOWED_BARE` entry in `scripts/check-collection-namespacing.ts`, which
31221
+ * names this file and the condition for removing both.
31222
+ */
31223
+ var LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION = "viewer-settings-snapshots";
31224
+ /**
31225
+ * The ORIGINAL column list, byte-for-byte.
31226
+ *
31227
+ * Do not "tidy" this to reference the successor's constant: they are allowed to
31228
+ * diverge, and the day they do, a shared constant would silently rebuild the
31229
+ * legacy table and destroy the rows.
31230
+ */
31231
+ var LEGACY_COLUMNS = [
31232
+ {
31233
+ name: "id",
31234
+ type: "TEXT",
31235
+ primaryKey: true,
31236
+ notNull: true
31237
+ },
31238
+ {
31239
+ name: "name",
31240
+ type: "TEXT",
31241
+ notNull: true
31242
+ },
31243
+ {
31244
+ name: "ownerUserId",
31245
+ type: "TEXT",
31246
+ notNull: true
31247
+ },
31248
+ {
31249
+ name: "ownerUsername",
31250
+ type: "TEXT",
31251
+ notNull: true
31252
+ },
31253
+ {
31254
+ name: "createdAt",
31255
+ type: "INTEGER",
31256
+ notNull: true
31257
+ },
31258
+ {
31259
+ name: "updatedAt",
31260
+ type: "INTEGER",
31261
+ notNull: true
31262
+ },
31263
+ {
31264
+ name: "blob",
31265
+ type: "JSON",
31266
+ notNull: true
31267
+ }
31268
+ ];
31269
+ /** Bounded read: this table held 1 row on the reference hub. */
31270
+ var LEGACY_READ_LIMIT = 500;
31271
+ function errText(err) {
31272
+ return err instanceof Error ? err.message : String(err);
31273
+ }
31274
+ function asRows(raw) {
31275
+ if (!Array.isArray(raw)) return [];
31276
+ const out = [];
31277
+ for (const item of raw) {
31278
+ if (typeof item !== "object" || item === null) continue;
31279
+ const id = Reflect.get(item, "id");
31280
+ const data = Reflect.get(item, "data");
31281
+ if (typeof id !== "string" || typeof data !== "object" || data === null) continue;
31282
+ out.push({
31283
+ id,
31284
+ data
31285
+ });
31286
+ }
31287
+ return out;
31288
+ }
31289
+ /**
31290
+ * Move every legacy row into the successor and clear the legacy table.
31291
+ *
31292
+ * Returns the number of legacy rows RETIRED — the completion metric, not the
31293
+ * number written. A row copied but not yet verified counts as zero and is
31294
+ * retried on the next boot, which is the safe direction.
31295
+ */
31296
+ async function adoptLegacyViewerSettingsSnapshots(deps) {
31297
+ await deps.store.declareCollection.mutate({
31298
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31299
+ columns: LEGACY_COLUMNS
31300
+ });
31301
+ let legacy;
31302
+ try {
31303
+ legacy = asRows(await deps.store.query.query({
31304
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31305
+ filter: { limit: LEGACY_READ_LIMIT }
31306
+ }));
31307
+ } catch (err) {
31308
+ deps.logger.warn("viewer-settings legacy adoption skipped — legacy read failed", { meta: {
31309
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31310
+ error: errText(err)
31311
+ } });
31312
+ return 0;
31313
+ }
31314
+ if (legacy.length === 0) return 0;
31315
+ let existing;
31316
+ try {
31317
+ existing = new Set(asRows(await deps.store.query.query({
31318
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31319
+ filter: { limit: LEGACY_READ_LIMIT }
31320
+ })).map((r) => r.id));
31321
+ } catch (err) {
31322
+ deps.logger.warn("viewer-settings legacy adoption skipped — successor read failed", { meta: {
31323
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31324
+ error: errText(err)
31325
+ } });
31326
+ return 0;
31327
+ }
31328
+ let written = 0;
31329
+ for (const row of legacy) {
31330
+ if (existing.has(row.id)) continue;
31331
+ try {
31332
+ await deps.store.set.mutate({
31333
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31334
+ key: row.id,
31335
+ value: row.data
31336
+ });
31337
+ written += 1;
31338
+ } catch (err) {
31339
+ deps.logger.warn("viewer-settings legacy adoption — one row failed to write", { meta: {
31340
+ snapshotId: row.id,
31341
+ error: errText(err)
31342
+ } });
31343
+ }
31344
+ }
31345
+ let landed;
31346
+ try {
31347
+ landed = new Set(asRows(await deps.store.query.query({
31348
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31349
+ filter: { limit: LEGACY_READ_LIMIT }
31350
+ })).map((r) => r.id));
31351
+ } catch (err) {
31352
+ deps.logger.warn("viewer-settings legacy adoption — verification read failed, legacy rows kept", { meta: {
31353
+ written,
31354
+ error: errText(err)
31355
+ } });
31356
+ return 0;
31357
+ }
31358
+ let retired = 0;
31359
+ for (const row of legacy) {
31360
+ if (!landed.has(row.id)) continue;
31361
+ try {
31362
+ await deps.store.delete.mutate({
31363
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31364
+ key: row.id
31365
+ });
31366
+ retired += 1;
31367
+ } catch (err) {
31368
+ deps.logger.warn("viewer-settings legacy adoption — one legacy row failed to delete", { meta: {
31369
+ snapshotId: row.id,
31370
+ error: errText(err)
31371
+ } });
31372
+ }
31373
+ }
31374
+ deps.logger.warn("viewer-settings snapshots adopted from the legacy bare collection", { meta: {
31375
+ from: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31376
+ to: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
31377
+ found: legacy.length,
31378
+ written,
31379
+ retired,
31380
+ kept: legacy.length - retired
31381
+ } });
31382
+ return retired;
31383
+ }
31201
31384
  /**
31202
31385
  * The VIDEO's playback rate — 4×, the same as {@link NC_GIF_SPEED}.
31203
31386
  *
@@ -39960,6 +40143,129 @@ function buildRecordedStillPorts(input) {
39960
40143
  })
39961
40144
  };
39962
40145
  }
40146
+ /** Where the completion marker lives. */
40147
+ var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
40148
+ var MIGRATION_MARKER_KEY = "label-tier-4g";
40149
+ /**
40150
+ * The decision for ONE row, exposed so the rule is testable without a store.
40151
+ *
40152
+ * `null` = leave the row alone.
40153
+ */
40154
+ function decideRowMigration(data, now) {
40155
+ const label = data["label"];
40156
+ if (typeof label !== "string" || label.length === 0) return null;
40157
+ if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
40158
+ if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
40159
+ if (isMacroClass(label)) return {
40160
+ label: null,
40161
+ labelScore: null,
40162
+ labelMeta: null
40163
+ };
40164
+ return {
40165
+ label: null,
40166
+ labelScore: null,
40167
+ labelMeta: null,
40168
+ subLabel: label,
40169
+ subLabelScore: null,
40170
+ subLabelMeta: {
40171
+ stepId: MIGRATION_STEP_ID,
40172
+ decidedAt: now
40173
+ }
40174
+ };
40175
+ }
40176
+ /** Migrate one collection, paged and bounded. */
40177
+ async function migrateCollectionLabelTiers(deps, collection) {
40178
+ let scanned = 0;
40179
+ let demoted = 0;
40180
+ let cleared = 0;
40181
+ let skipped = 0;
40182
+ let offset = 0;
40183
+ let truncated = false;
40184
+ for (;;) {
40185
+ if (scanned >= 2e5) {
40186
+ truncated = true;
40187
+ break;
40188
+ }
40189
+ const rows = await deps.store.query.query({
40190
+ collection,
40191
+ filter: {
40192
+ limit: 500,
40193
+ offset
40194
+ }
40195
+ });
40196
+ if (rows.length === 0) break;
40197
+ for (const row of rows) {
40198
+ scanned++;
40199
+ const patch = decideRowMigration(row.data, deps.now());
40200
+ if (patch === null) {
40201
+ skipped++;
40202
+ continue;
40203
+ }
40204
+ try {
40205
+ await deps.store.update.mutate({
40206
+ collection,
40207
+ id: row.id,
40208
+ data: patch
40209
+ });
40210
+ if (patch["subLabel"] === void 0) cleared++;
40211
+ else demoted++;
40212
+ } catch (err) {
40213
+ deps.logger.warn("label tier migration row failed", { meta: {
40214
+ collection,
40215
+ id: row.id,
40216
+ error: String(err)
40217
+ } });
40218
+ skipped++;
40219
+ }
40220
+ }
40221
+ if (rows.length < 500) break;
40222
+ offset += rows.length;
40223
+ }
40224
+ return {
40225
+ collection,
40226
+ scanned,
40227
+ demoted,
40228
+ cleared,
40229
+ skipped,
40230
+ truncated
40231
+ };
40232
+ }
40233
+ /**
40234
+ * Run the migration across the collections that carry the tiered columns, once.
40235
+ *
40236
+ * The marker short-circuits a completed migration; it is written only when
40237
+ * NEITHER collection was truncated, so an interrupted pass resumes on the next
40238
+ * boot instead of declaring itself done.
40239
+ */
40240
+ async function runLabelTierMigration(deps, collections) {
40241
+ try {
40242
+ const marker = await deps.store.get.query({
40243
+ collection: MIGRATION_MARKER_COLLECTION,
40244
+ key: MIGRATION_MARKER_KEY
40245
+ });
40246
+ if (marker !== null && marker !== void 0) return [];
40247
+ } catch {}
40248
+ const stats = [];
40249
+ for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
40250
+ if (stats.every((s) => !s.truncated)) try {
40251
+ await deps.store.set.mutate({
40252
+ collection: MIGRATION_MARKER_COLLECTION,
40253
+ key: MIGRATION_MARKER_KEY,
40254
+ value: { completedAt: deps.now() }
40255
+ });
40256
+ } catch (err) {
40257
+ deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
40258
+ }
40259
+ for (const s of stats) deps.logger.info("label tier migration", { meta: {
40260
+ collection: s.collection,
40261
+ scanned: s.scanned,
40262
+ demoted: s.demoted,
40263
+ cleared: s.cleared,
40264
+ skipped: s.skipped,
40265
+ truncated: s.truncated
40266
+ } });
40267
+ return stats;
40268
+ }
39963
40269
  //#endregion
39964
40270
  //#region src/durable/lts-aggregator.ts
39965
40271
  /** Wall-clock bucket width. Five minutes, matching HA's statistics tier. */
@@ -45151,6 +45457,55 @@ var ANALYTICS_COLLECTIONS = [
45151
45457
  why: "a five-minute statistics row summarises an INTERVAL, not an entity — there is no owner it can be orphaned from. Its bound is a ROW CAP per (subject, series, scope) enforced by the aggregator itself (105,120 rows ≈ one year), deliberately not an age sweep: D61 collapsed ten disagreeing age clocks into one, and an LTS series has no clock but its own."
45152
45458
  }
45153
45459
  },
45460
+ {
45461
+ collection: RETRAIN_ANNOTATIONS_COLLECTION,
45462
+ classification: {
45463
+ kind: "never-orphaned",
45464
+ why: "the ground truth an operator DREW. It declares `trackId` and must be classified by hand for that reason: the id is provenance, not ownership. The dataset addresses COPIES (D81), which is precisely what makes a 'trained' track evictable again — so a track ageing out leaves the annotations standing. Rows go only through `deleteForFrame`, when the operator deselects the frame. Deleting one because its track expired would destroy labelling work no sweep can recreate."
45465
+ }
45466
+ },
45467
+ {
45468
+ collection: RETRAIN_FRAMES_COLLECTION,
45469
+ classification: {
45470
+ kind: "never-orphaned",
45471
+ why: "the frame COPIES the dataset owns outright, under a `retrain` prefix no owner cascade and no age sweep walks (D81). Also declares `trackId` as provenance. Rows go only through `deleteFrame`, after their annotations, on operator deselection. NOTE: nothing caps this table or the disk it consumes — that is a deliberate decision, not an oversight, and it is the one collection here whose bound is an operator's habit."
45472
+ }
45473
+ },
45474
+ {
45475
+ collection: SCENE_COLLECTION,
45476
+ classification: {
45477
+ kind: "never-orphaned",
45478
+ why: "operator intent — one row per (deviceId, monitorId), created and destroyed by scene CRUD or by a device-removal sweep. Nothing retention does can orphan it, and no age clock applies: a scene the operator configured and never revisits is not stale, it is configured."
45479
+ }
45480
+ },
45481
+ {
45482
+ collection: SCENE_LATCH_COLLECTION,
45483
+ classification: {
45484
+ kind: "never-orphaned",
45485
+ why: "it IS owned — by the scene set — but the owner-cleanup is self-contained: `pruneExcept(the scene set)` runs on every scene reload, so a latch whose scene is gone is collected there and never by retention. Deliberately NOT `owner-indirection`: that kind puts a collection into `auditableCollections()`, and the orphan audit resolves exactly one indirection, that of `pipeline-analytics:media`."
45486
+ }
45487
+ },
45488
+ {
45489
+ collection: MIGRATION_MARKER_COLLECTION,
45490
+ classification: {
45491
+ kind: "never-orphaned",
45492
+ why: "a one-row completion marker for the label-tier 4g pass. It summarises a one-time event, so there is no entity it can be orphaned from — and its PRESENCE is what stops the migration running again, so deleting it is not cleanup, it is a re-run over every track, face and object-event row."
45493
+ }
45494
+ },
45495
+ {
45496
+ collection: VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
45497
+ classification: {
45498
+ kind: "never-orphaned",
45499
+ why: "a hub user's named snapshot of their own viewer layout. It is not analytics data — it happens to live in this addon because the actions ride `addons.custom`. Owned by the user who saved it and deleted only by them; no track, device or event can orphan it, and no age applies."
45500
+ }
45501
+ },
45502
+ {
45503
+ collection: LEGACY_VIEWER_SETTINGS_SNAPSHOTS_COLLECTION,
45504
+ classification: {
45505
+ kind: "never-orphaned",
45506
+ why: "the drained predecessor of the row above, declared only so its rows can be read once and moved (`viewer-settings-legacy-adoption.ts`). Retention must never touch it: its rows go one way only, to the successor, gated on a re-read that confirms they landed. Delete this entry together with that module."
45507
+ }
45508
+ },
45154
45509
  {
45155
45510
  collection: STATIONARY_COLLECTION,
45156
45511
  classification: {
@@ -49830,129 +50185,6 @@ function evaluatePeriodicSnapshot(input) {
49830
50185
  movedFraction
49831
50186
  };
49832
50187
  }
49833
- /** Where the completion marker lives. */
49834
- var MIGRATION_MARKER_COLLECTION = "pipeline-analytics:migrations";
49835
- var MIGRATION_MARKER_KEY = "label-tier-4g";
49836
- /**
49837
- * The decision for ONE row, exposed so the rule is testable without a store.
49838
- *
49839
- * `null` = leave the row alone.
49840
- */
49841
- function decideRowMigration(data, now) {
49842
- const label = data["label"];
49843
- if (typeof label !== "string" || label.length === 0) return null;
49844
- if (typeof data["subLabel"] === "string" && data["subLabel"].length > 0) return null;
49845
- if (data["labelMeta"] !== null && data["labelMeta"] !== void 0) return null;
49846
- if (isMacroClass(label)) return {
49847
- label: null,
49848
- labelScore: null,
49849
- labelMeta: null
49850
- };
49851
- return {
49852
- label: null,
49853
- labelScore: null,
49854
- labelMeta: null,
49855
- subLabel: label,
49856
- subLabelScore: null,
49857
- subLabelMeta: {
49858
- stepId: MIGRATION_STEP_ID,
49859
- decidedAt: now
49860
- }
49861
- };
49862
- }
49863
- /** Migrate one collection, paged and bounded. */
49864
- async function migrateCollectionLabelTiers(deps, collection) {
49865
- let scanned = 0;
49866
- let demoted = 0;
49867
- let cleared = 0;
49868
- let skipped = 0;
49869
- let offset = 0;
49870
- let truncated = false;
49871
- for (;;) {
49872
- if (scanned >= 2e5) {
49873
- truncated = true;
49874
- break;
49875
- }
49876
- const rows = await deps.store.query.query({
49877
- collection,
49878
- filter: {
49879
- limit: 500,
49880
- offset
49881
- }
49882
- });
49883
- if (rows.length === 0) break;
49884
- for (const row of rows) {
49885
- scanned++;
49886
- const patch = decideRowMigration(row.data, deps.now());
49887
- if (patch === null) {
49888
- skipped++;
49889
- continue;
49890
- }
49891
- try {
49892
- await deps.store.update.mutate({
49893
- collection,
49894
- id: row.id,
49895
- data: patch
49896
- });
49897
- if (patch["subLabel"] === void 0) cleared++;
49898
- else demoted++;
49899
- } catch (err) {
49900
- deps.logger.warn("label tier migration row failed", { meta: {
49901
- collection,
49902
- id: row.id,
49903
- error: String(err)
49904
- } });
49905
- skipped++;
49906
- }
49907
- }
49908
- if (rows.length < 500) break;
49909
- offset += rows.length;
49910
- }
49911
- return {
49912
- collection,
49913
- scanned,
49914
- demoted,
49915
- cleared,
49916
- skipped,
49917
- truncated
49918
- };
49919
- }
49920
- /**
49921
- * Run the migration across the collections that carry the tiered columns, once.
49922
- *
49923
- * The marker short-circuits a completed migration; it is written only when
49924
- * NEITHER collection was truncated, so an interrupted pass resumes on the next
49925
- * boot instead of declaring itself done.
49926
- */
49927
- async function runLabelTierMigration(deps, collections) {
49928
- try {
49929
- const marker = await deps.store.get.query({
49930
- collection: MIGRATION_MARKER_COLLECTION,
49931
- key: MIGRATION_MARKER_KEY
49932
- });
49933
- if (marker !== null && marker !== void 0) return [];
49934
- } catch {}
49935
- const stats = [];
49936
- for (const collection of collections) stats.push(await migrateCollectionLabelTiers(deps, collection));
49937
- if (stats.every((s) => !s.truncated)) try {
49938
- await deps.store.set.mutate({
49939
- collection: MIGRATION_MARKER_COLLECTION,
49940
- key: MIGRATION_MARKER_KEY,
49941
- value: { completedAt: deps.now() }
49942
- });
49943
- } catch (err) {
49944
- deps.logger.warn("label tier migration marker failed", { meta: { error: String(err) } });
49945
- }
49946
- for (const s of stats) deps.logger.info("label tier migration", { meta: {
49947
- collection: s.collection,
49948
- scanned: s.scanned,
49949
- demoted: s.demoted,
49950
- cleared: s.cleared,
49951
- skipped: s.skipped,
49952
- truncated: s.truncated
49953
- } });
49954
- return stats;
49955
- }
49956
50188
  //#endregion
49957
50189
  //#region src/pipeline-analytics/store/object-embedding-store.ts
49958
50190
  /**
@@ -52364,6 +52596,14 @@ var PipelineAnalyticsAddon = class extends BaseAddon {
52364
52596
  ViewerSettingsSnapshotStore.declare(api.settingsStore)
52365
52597
  ]);
52366
52598
  this.viewerSettingsSnapshots = new ViewerSettingsSnapshotStore(api.settingsStore);
52599
+ try {
52600
+ await adoptLegacyViewerSettingsSnapshots({
52601
+ store: api.settingsStore,
52602
+ logger: this.ctx.logger.child("viewer-settings")
52603
+ });
52604
+ } catch (err) {
52605
+ this.ctx.logger.warn("viewer-settings legacy adoption threw", { meta: { error: errMsg(err) } });
52606
+ }
52367
52607
  runLabelTierMigration({
52368
52608
  store: api.settingsStore,
52369
52609
  logger: this.ctx.logger,
@@ -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-Caz0R4UZ.mjs")).catch((e) => {
33
+ return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-D8HLGYbH.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.100",
3
+ "version": "1.2.101",
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",