@camstack/addon-post-analysis 1.2.613 → 1.2.615

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
2
2
  __esModule: { value: true },
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
- const require_clip_model_registry = require("../clip-model-registry-CM-m6e1l.js");
5
+ const require_clip_model_registry = require("../clip-model-registry-ycbGgfo_.js");
6
6
  let node_fs = require("node:fs");
7
7
  node_fs = require_clip_model_registry.__toESM(node_fs, 1);
8
8
  let node_path = require("node:path");
@@ -4078,6 +4078,224 @@ function ncAlarmPanelPort(panel) {
4078
4078
  };
4079
4079
  }
4080
4080
  //#endregion
4081
+ //#region src/durable/durable-record-body.ts
4082
+ /**
4083
+ * Zod body for phase-3 durable ledger record kinds. The engine validates on
4084
+ * write; each owner's `fromRecord` / `toValue` remain authoritative on read.
4085
+ */
4086
+ var DurableLedgerBodySchema = require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.unknown());
4087
+ //#endregion
4088
+ //#region src/notification-center/nc-durable-record-kinds.ts
4089
+ /**
4090
+ * Record kinds for NC live-state ledgers (records plan phase 3, steps 25–36).
4091
+ *
4092
+ * Hosted in the pipeline-analytics addon (`pipeline-analytics` owner stamp).
4093
+ * Declared by the hub; every node reads and writes them through `ctx.records`.
4094
+ * The legacy settings-store handles (`durableLegacyKind`, `legacyCollectionKind`)
4095
+ * serve only a centre built WITHOUT `records` — the specs.
4096
+ */
4097
+ /** The settings-store tables these kinds replaced; read only WITHOUT `ctx.records` (the specs). */
4098
+ var NC_LIVENESS_LEGACY = "notification-center:liveness";
4099
+ var NC_DEVICE_ALERTS_LEGACY = "notification-center:device-alerts";
4100
+ var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
4101
+ var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
4102
+ var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
4103
+ var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
4104
+ var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
4105
+ var NC_META_LEGACY = "notification-center:meta";
4106
+ var NC_SNOOZES_LEGACY = "notification-center:snoozes";
4107
+ var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
4108
+ var NC_TRACK_GROUPS_LEGACY = "notification-center:track-groups";
4109
+ var NC_OUTBOX_LEGACY = "notification-center:outbox";
4110
+ var NC_ARTIFACTS_LEGACY = "notification-center:artifacts";
4111
+ var ledger = (writeTrigger, database = "settings", retention = { none: "mirror load-all at boot; rows drop only via owner prune or device-removal hook" }) => ({
4112
+ schema: DurableLedgerBodySchema,
4113
+ class: "ledger",
4114
+ database,
4115
+ writeTrigger,
4116
+ device: "optional",
4117
+ at: "none",
4118
+ retention
4119
+ });
4120
+ /**
4121
+ * The shape the four QUERIED kinds share with the mirrors. Each spells its own
4122
+ * file, trigger, indexes and retention as literals (the records guards read them).
4123
+ * `device` and `at` stay what the first (empty) declaration said — the engine
4124
+ * refuses to change either — so every read is served by a DATA-field index, and
4125
+ * the camera rides the record envelope (`deviceId`) for the D865 removal.
4126
+ */
4127
+ var QUERIED = {
4128
+ schema: DurableLedgerBodySchema,
4129
+ device: "optional",
4130
+ at: "none"
4131
+ };
4132
+ var ncDurableRecordKinds = require_clip_model_registry.defineRecordKinds({
4133
+ liveness: ledger("a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
4134
+ "device-alerts": ledger("a sustained device alert episode the centre confirmed or cleared"),
4135
+ "system-state": ledger("a system-state subject flip the state summary accepted"),
4136
+ "alert-episodes": ledger("an alert episode the centre notified (critical or error); its resolution deletes the row"),
4137
+ occupancy: ledger("a confirmed occupancy level write-through after the watcher decided an edge"),
4138
+ "device-prunes": ledger("one row recording what a deleted-device prune removed"),
4139
+ "state-summary-stamps": ledger("digest delivery stamps per rule kind"),
4140
+ meta: ledger("outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
4141
+ snoozes: ledger("operator opens or closes a snooze window (write-through)"),
4142
+ "snooze-suppressed": {
4143
+ ...QUERIED,
4144
+ class: "ledger",
4145
+ database: "events",
4146
+ writeTrigger: "aggregate count per (window, target, rule, camera) for suppressed notifications",
4147
+ indexes: [{
4148
+ name: "snooze",
4149
+ fields: ["snoozeId"]
4150
+ }],
4151
+ retention: {
4152
+ owner: "notification-center",
4153
+ reason: "deleted with its snooze window, 6 h after that window closes"
4154
+ }
4155
+ },
4156
+ "track-groups": {
4157
+ ...QUERIED,
4158
+ class: "ledger",
4159
+ database: "events",
4160
+ writeTrigger: "one row per grouped delivery window opened at admission time",
4161
+ indexes: [
4162
+ {
4163
+ name: "open",
4164
+ fields: ["sealed"]
4165
+ },
4166
+ {
4167
+ name: "opened",
4168
+ fields: ["openedAt"]
4169
+ },
4170
+ {
4171
+ name: "device_opened",
4172
+ fields: ["deviceId", "openedAt"]
4173
+ },
4174
+ {
4175
+ name: "rule_opened",
4176
+ fields: ["ruleId", "openedAt"]
4177
+ },
4178
+ {
4179
+ name: "rule_device_opened",
4180
+ fields: [
4181
+ "ruleId",
4182
+ "deviceId",
4183
+ "openedAt"
4184
+ ]
4185
+ }
4186
+ ],
4187
+ retention: {
4188
+ owner: "notification-center",
4189
+ reason: "ring: the newest NC_GROUP_MAX_ROWS_PER_DEVICE rows per camera, trimmed on write"
4190
+ }
4191
+ },
4192
+ outbox: {
4193
+ ...QUERIED,
4194
+ class: "ledger",
4195
+ database: "events",
4196
+ writeTrigger: "one row per (rule, dedupRef, target) enqueued for delivery",
4197
+ indexes: [
4198
+ {
4199
+ name: "created",
4200
+ fields: ["createdAt"]
4201
+ },
4202
+ {
4203
+ name: "rule_created",
4204
+ fields: ["ruleId", "createdAt"]
4205
+ },
4206
+ {
4207
+ name: "status_created",
4208
+ fields: ["status", "createdAt"]
4209
+ },
4210
+ {
4211
+ name: "device_created",
4212
+ fields: ["deviceId", "createdAt"]
4213
+ },
4214
+ {
4215
+ name: "rule_status_created",
4216
+ fields: [
4217
+ "ruleId",
4218
+ "status",
4219
+ "createdAt"
4220
+ ]
4221
+ },
4222
+ {
4223
+ name: "rule_device_created",
4224
+ fields: [
4225
+ "ruleId",
4226
+ "deviceId",
4227
+ "createdAt"
4228
+ ]
4229
+ },
4230
+ {
4231
+ name: "status_device_created",
4232
+ fields: [
4233
+ "status",
4234
+ "deviceId",
4235
+ "createdAt"
4236
+ ]
4237
+ },
4238
+ {
4239
+ name: "rule_status_device_created",
4240
+ fields: [
4241
+ "ruleId",
4242
+ "status",
4243
+ "deviceId",
4244
+ "createdAt"
4245
+ ]
4246
+ }
4247
+ ],
4248
+ retention: {
4249
+ owner: "notification-center",
4250
+ reason: "terminal (sent/dead) rows older than 7 days pruned at boot; a pending row retries to dead first"
4251
+ },
4252
+ maxDataBytes: require_clip_model_registry.RECORD_MAX_DATA_BYTES
4253
+ },
4254
+ artifacts: {
4255
+ ...QUERIED,
4256
+ class: "ledger",
4257
+ database: "events",
4258
+ writeTrigger: "retained notification artefact metadata indexed for lookup and expiry",
4259
+ indexes: [{
4260
+ name: "captured",
4261
+ fields: ["capturedAt"]
4262
+ }, {
4263
+ name: "expires",
4264
+ fields: ["expiresAt"]
4265
+ }],
4266
+ retention: {
4267
+ owner: "notification-center",
4268
+ reason: "falls with its bytes: expired rows on every node, orphans against the shelf on the producer node"
4269
+ }
4270
+ }
4271
+ });
4272
+ /**
4273
+ * The settings-store table each kind stands in for when a centre is built
4274
+ * WITHOUT `ctx.records` - the specs' fake store (`legacyCollectionKind`,
4275
+ * `durableLegacyKind`). Production never reads it: every node has
4276
+ * `ctx.records`, and the operator dropped these tables offline (D900).
4277
+ */
4278
+ var NC_DURABLE_LEGACY_TABLES = {
4279
+ liveness: NC_LIVENESS_LEGACY,
4280
+ "device-alerts": NC_DEVICE_ALERTS_LEGACY,
4281
+ "system-state": NC_SYSTEM_STATE_LEGACY,
4282
+ "alert-episodes": NC_ALERT_EPISODES_LEGACY,
4283
+ occupancy: NC_OCCUPANCY_LEGACY,
4284
+ "device-prunes": NC_DEVICE_PRUNES_LEGACY,
4285
+ "state-summary-stamps": NC_STATE_SUMMARY_STAMPS_LEGACY,
4286
+ meta: NC_META_LEGACY,
4287
+ snoozes: NC_SNOOZES_LEGACY,
4288
+ "snooze-suppressed": NC_SNOOZE_SUPPRESSED_LEGACY,
4289
+ "track-groups": NC_TRACK_GROUPS_LEGACY,
4290
+ outbox: NC_OUTBOX_LEGACY,
4291
+ artifacts: NC_ARTIFACTS_LEGACY
4292
+ };
4293
+ /** The legacy table of an NC durable kind by name, or `undefined` for any other kind. */
4294
+ function ncDurableLegacyTableOf(kind) {
4295
+ for (const [name, table] of Object.entries(NC_DURABLE_LEGACY_TABLES)) if (name === kind) return table;
4296
+ }
4297
+ Object.values(NC_DURABLE_LEGACY_TABLES);
4298
+ //#endregion
4081
4299
  //#region src/durable/legacy-collection-kind.ts
4082
4300
  /** Bounds of a half-open range on the legacy filter (the store has no `>=`). */
4083
4301
  var RANGE_MIN = -864e13;
@@ -4223,8 +4441,8 @@ var LegacyCollectionKind = class {
4223
4441
  /** The handle an NC owner reads and writes through: records when present, else the legacy table. */
4224
4442
  function queriedKind(definition, deps) {
4225
4443
  if (deps.records !== void 0) return deps.records.kind(definition);
4226
- const collection = definition.declaration.adoptsFrom;
4227
- if (collection === void 0) throw new Error(`kind "${definition.name}" names no legacy table (adoptsFrom)`);
4444
+ const collection = ncDurableLegacyTableOf(definition.name);
4445
+ if (collection === void 0) throw new Error(`kind "${definition.name}" has no legacy table to stand in for it`);
4228
4446
  return new LegacyCollectionKind({
4229
4447
  definition,
4230
4448
  collection,
@@ -4269,203 +4487,6 @@ async function collectRecordPages(kind, q, max) {
4269
4487
  }
4270
4488
  }
4271
4489
  //#endregion
4272
- //#region src/durable/durable-record-body.ts
4273
- /**
4274
- * Zod body for phase-3 durable ledger record kinds. The engine validates on
4275
- * write; each owner's `fromRecord` / `toValue` remain authoritative on read.
4276
- */
4277
- var DurableLedgerBodySchema = require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.unknown());
4278
- //#endregion
4279
- //#region src/notification-center/nc-durable-record-kinds.ts
4280
- /**
4281
- * Record kinds for NC live-state ledgers (records plan phase 3, steps 25–36).
4282
- *
4283
- * Hosted in the pipeline-analytics addon (`pipeline-analytics` owner stamp).
4284
- * Declared by the hub; every node reads and writes them through `ctx.records`.
4285
- * The legacy settings-store handles (`durableLegacyKind`, `legacyCollectionKind`)
4286
- * serve only a centre built WITHOUT `records` — the specs.
4287
- */
4288
- /** Legacy collection names — keep in sync with owner constants. */
4289
- var NC_LIVENESS_LEGACY = "notification-center:liveness";
4290
- var NC_DEVICE_ALERTS_LEGACY = "notification-center:device-alerts";
4291
- var NC_SYSTEM_STATE_LEGACY = "notification-center:system-state";
4292
- var NC_OCCUPANCY_LEGACY = "notification-center:occupancy";
4293
- var NC_DEVICE_PRUNES_LEGACY = "notification-center:device-prunes";
4294
- var NC_STATE_SUMMARY_STAMPS_LEGACY = "notification-center:state-summary-stamps";
4295
- var NC_ALERT_EPISODES_LEGACY = "notification-center:alert-episodes";
4296
- var NC_META_LEGACY = "notification-center:meta";
4297
- var NC_SNOOZES_LEGACY = "notification-center:snoozes";
4298
- var NC_SNOOZE_SUPPRESSED_LEGACY = "notification-center:snooze-suppressed";
4299
- var NC_TRACK_GROUPS_LEGACY = "notification-center:track-groups";
4300
- var NC_OUTBOX_LEGACY = "notification-center:outbox";
4301
- var NC_ARTIFACTS_LEGACY = "notification-center:artifacts";
4302
- var ledger = (adoptsFrom, writeTrigger, database = "settings", retention = { none: "mirror load-all at boot; rows drop only via owner prune or device-removal hook" }) => ({
4303
- schema: DurableLedgerBodySchema,
4304
- class: "ledger",
4305
- database,
4306
- writeTrigger,
4307
- device: "optional",
4308
- at: "none",
4309
- retention,
4310
- adoptsFrom
4311
- });
4312
- /**
4313
- * The shape the four QUERIED kinds share with the mirrors. Each spells its own
4314
- * file, trigger, indexes and retention as literals (the records guards read them).
4315
- * `device` and `at` stay what the first (empty) declaration said — the engine
4316
- * refuses to change either — so every read is served by a DATA-field index, and
4317
- * the camera rides the record envelope (`deviceId`) for the D865 removal.
4318
- */
4319
- var QUERIED = {
4320
- schema: DurableLedgerBodySchema,
4321
- device: "optional",
4322
- at: "none"
4323
- };
4324
- var ncDurableRecordKinds = require_clip_model_registry.defineRecordKinds({
4325
- liveness: ledger(NC_LIVENESS_LEGACY, "a liveness state TRANSITION the centre accepted (seed or flip); a no-flip writes nothing"),
4326
- "device-alerts": ledger(NC_DEVICE_ALERTS_LEGACY, "a sustained device alert episode the centre confirmed or cleared"),
4327
- "system-state": ledger(NC_SYSTEM_STATE_LEGACY, "a system-state subject flip the state summary accepted"),
4328
- "alert-episodes": ledger(NC_ALERT_EPISODES_LEGACY, "an alert episode the centre notified (critical or error); its resolution deletes the row"),
4329
- occupancy: ledger(NC_OCCUPANCY_LEGACY, "a confirmed occupancy level write-through after the watcher decided an edge"),
4330
- "device-prunes": ledger(NC_DEVICE_PRUNES_LEGACY, "one row recording what a deleted-device prune removed"),
4331
- "state-summary-stamps": ledger(NC_STATE_SUMMARY_STAMPS_LEGACY, "digest delivery stamps per rule kind"),
4332
- meta: ledger(NC_META_LEGACY, "outbox boot-reconcile watermark and other single-key meta rows", "settings", { none: "one fixed key (watermark), overwritten in place, never deleted" }),
4333
- snoozes: ledger(NC_SNOOZES_LEGACY, "operator opens or closes a snooze window (write-through)"),
4334
- "snooze-suppressed": {
4335
- ...QUERIED,
4336
- class: "ledger",
4337
- database: "events",
4338
- writeTrigger: "aggregate count per (window, target, rule, camera) for suppressed notifications",
4339
- indexes: [{
4340
- name: "snooze",
4341
- fields: ["snoozeId"]
4342
- }],
4343
- retention: {
4344
- owner: "notification-center",
4345
- reason: "deleted with its snooze window, 6 h after that window closes"
4346
- },
4347
- adoptsFrom: NC_SNOOZE_SUPPRESSED_LEGACY
4348
- },
4349
- "track-groups": {
4350
- ...QUERIED,
4351
- class: "ledger",
4352
- database: "events",
4353
- writeTrigger: "one row per grouped delivery window opened at admission time",
4354
- indexes: [
4355
- {
4356
- name: "open",
4357
- fields: ["sealed"]
4358
- },
4359
- {
4360
- name: "opened",
4361
- fields: ["openedAt"]
4362
- },
4363
- {
4364
- name: "device_opened",
4365
- fields: ["deviceId", "openedAt"]
4366
- },
4367
- {
4368
- name: "rule_opened",
4369
- fields: ["ruleId", "openedAt"]
4370
- },
4371
- {
4372
- name: "rule_device_opened",
4373
- fields: [
4374
- "ruleId",
4375
- "deviceId",
4376
- "openedAt"
4377
- ]
4378
- }
4379
- ],
4380
- retention: {
4381
- owner: "notification-center",
4382
- reason: "ring: the newest NC_GROUP_MAX_ROWS_PER_DEVICE rows per camera, trimmed on write"
4383
- },
4384
- adoptsFrom: NC_TRACK_GROUPS_LEGACY
4385
- },
4386
- outbox: {
4387
- ...QUERIED,
4388
- class: "ledger",
4389
- database: "events",
4390
- writeTrigger: "one row per (rule, dedupRef, target) enqueued for delivery",
4391
- indexes: [
4392
- {
4393
- name: "created",
4394
- fields: ["createdAt"]
4395
- },
4396
- {
4397
- name: "rule_created",
4398
- fields: ["ruleId", "createdAt"]
4399
- },
4400
- {
4401
- name: "status_created",
4402
- fields: ["status", "createdAt"]
4403
- },
4404
- {
4405
- name: "device_created",
4406
- fields: ["deviceId", "createdAt"]
4407
- },
4408
- {
4409
- name: "rule_status_created",
4410
- fields: [
4411
- "ruleId",
4412
- "status",
4413
- "createdAt"
4414
- ]
4415
- },
4416
- {
4417
- name: "rule_device_created",
4418
- fields: [
4419
- "ruleId",
4420
- "deviceId",
4421
- "createdAt"
4422
- ]
4423
- },
4424
- {
4425
- name: "status_device_created",
4426
- fields: [
4427
- "status",
4428
- "deviceId",
4429
- "createdAt"
4430
- ]
4431
- },
4432
- {
4433
- name: "rule_status_device_created",
4434
- fields: [
4435
- "ruleId",
4436
- "status",
4437
- "deviceId",
4438
- "createdAt"
4439
- ]
4440
- }
4441
- ],
4442
- retention: {
4443
- owner: "notification-center",
4444
- reason: "terminal (sent/dead) rows older than 7 days pruned at boot; a pending row retries to dead first"
4445
- },
4446
- maxDataBytes: require_clip_model_registry.RECORD_MAX_DATA_BYTES,
4447
- adoptsFrom: NC_OUTBOX_LEGACY
4448
- },
4449
- artifacts: {
4450
- ...QUERIED,
4451
- class: "ledger",
4452
- database: "events",
4453
- writeTrigger: "retained notification artefact metadata indexed for lookup and expiry",
4454
- indexes: [{
4455
- name: "captured",
4456
- fields: ["capturedAt"]
4457
- }, {
4458
- name: "expires",
4459
- fields: ["expiresAt"]
4460
- }],
4461
- retention: {
4462
- owner: "notification-center",
4463
- reason: "falls with its bytes: expired rows on every node, orphans against the shelf on the producer node"
4464
- },
4465
- adoptsFrom: NC_ARTIFACTS_LEGACY
4466
- }
4467
- });
4468
- //#endregion
4469
4490
  //#region src/notification-center/artifact-index.ts
4470
4491
  /**
4471
4492
  * @durable class=ledger owner=notification-center database=events
@@ -9441,38 +9462,12 @@ var FaceRescoreJobStatusSchema = require_clip_model_registry.object({
9441
9462
  * Record kinds owned by the pipeline-analytics addon (records plan phase 2,
9442
9463
  * steps 7-11).
9443
9464
  *
9444
- * Each kind adopts the legacy `pipeline-analytics:*` collection of the same
9445
- * name: the boot guard keeps the addon down until `scripts/hub/records-adopt.ts`
9446
- * has copied it. Declared only on the hub (agents have no `ctx.records`). The
9447
- * addon declares no `execution.group`, so the hub owner stamp is the addon id
9465
+ * Each kind replaced the legacy `pipeline-analytics:*` collection of the same
9466
+ * name; the records migration that copied them is closed (D900) and the tables
9467
+ * are gone (`retired-legacy-tables.ts`). The addon declares no
9468
+ * `execution.group`, so the hub owner stamp is the addon id
9448
9469
  * (`pipeline-analytics`).
9449
9470
  */
9450
- /** Legacy names for phase-3 analytics ledgers (keep in sync with owner constants). */
9451
- var PA_PHANTOM_CELLS = "pipeline-analytics:phantom-cells";
9452
- var PA_PACKAGE_BACKGROUND = "pipeline-analytics:package-background";
9453
- var PA_STATIONARY_EVIDENCE = "pipeline-analytics:stationary-evidence";
9454
- var PA_STATIONARY_OBJECTS = "pipeline-analytics:stationary-objects";
9455
- var PA_SCENE_LATCH = "pipeline-analytics:scene-latch";
9456
- var PA_MOTION_EVENTS = "pipeline-analytics:motion-events";
9457
- var PA_AUDIO_EVENTS = "pipeline-analytics:audio-events";
9458
- var PA_SENSOR_EVENTS = "pipeline-analytics:sensor-events";
9459
- var PA_FACES = "pipeline-analytics:faces";
9460
- var PA_PLATES = "pipeline-analytics:plates";
9461
- var PA_TRACK_ALIASES = "pipeline-analytics:track-aliases";
9462
- var PA_TRACK_PURITY = "pipeline-analytics:track-purity";
9463
- var PA_TRACK_RELATIONS = "pipeline-analytics:track-relations";
9464
- var PA_TRACKING_TAPES = "pipeline-analytics:tracking-tapes";
9465
- var PA_BIRTH_DECISION_LEDGER = "pipeline-analytics:birth-decision-ledger";
9466
- var PA_DEBUG_NOTE_ARCHIVE = "pipeline-analytics:debug-note-archive";
9467
- var PA_OPS_LOG = "pipeline-analytics:ops-log";
9468
- var PA_RETRAIN_FRAMES = "pipeline-analytics:retrain-frames";
9469
- var PA_RETRAIN_ANNOTATIONS = "pipeline-analytics:retrain-annotations";
9470
- var PA_RETRAIN_COLLECTIONS = "pipeline-analytics:retrain-collections";
9471
- var PA_RETRAIN_COLLECTION_FRAMES = "pipeline-analytics:retrain-collection-frames";
9472
- var PA_IDENTITIES = "pipeline-analytics:identities";
9473
- var PA_IDENTITY_SAMPLES = "pipeline-analytics:identity-samples";
9474
- var PA_VEHICLES = "pipeline-analytics:vehicles";
9475
- var PA_VEHICLE_SAMPLES = "pipeline-analytics:vehicle-samples";
9476
9471
  /**
9477
9472
  * Kinds that must agree through ONE `transact` sit in one file under one family
9478
9473
  * name, and the engine refuses a family split across files (records plan risk 8).
@@ -9499,7 +9494,9 @@ var MigrationMarkerBodySchema = require_clip_model_registry.object({
9499
9494
  });
9500
9495
  /** The orphan audit's resume position: one row, the whole map in one value on purpose. */
9501
9496
  var SweepCursorBodySchema = require_clip_model_registry.object({
9502
- scopes: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.number()),
9497
+ /** A number is an offset (a settings-store scope); a string is the records
9498
+ * engine's opaque keyset cursor (a scope walked through its kind). */
9499
+ scopes: require_clip_model_registry.record(require_clip_model_registry.string(), require_clip_model_registry.union([require_clip_model_registry.number(), require_clip_model_registry.string()])),
9503
9500
  /** Absent on a row written before the walk existed — absent means "from the top". */
9504
9501
  faceVectorCursor: require_clip_model_registry.number().optional(),
9505
9502
  /** One cursor per CLIP index, keyed by index name. Absent on an older row. */
@@ -9524,8 +9521,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9524
9521
  fields: ["deviceId"]
9525
9522
  }],
9526
9523
  retention: { none: "operator intent: a scene goes only through deleteScene, never by age" },
9527
- maxDataBytes: require_clip_model_registry.RECORD_MAX_DATA_BYTES,
9528
- adoptsFrom: "pipeline-analytics:scenes"
9524
+ maxDataBytes: require_clip_model_registry.RECORD_MAX_DATA_BYTES
9529
9525
  },
9530
9526
  migrations: {
9531
9527
  schema: MigrationMarkerBodySchema,
@@ -9534,8 +9530,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9534
9530
  writeTrigger: "one marker row when a boot migration (label-tier-4g, audio-track-class-d189) completes without truncating",
9535
9531
  device: "none",
9536
9532
  at: "none",
9537
- retention: { none: "its PRESENCE stops the migration re-scanning every track, face and event row on the next boot; bounded by the number of migrations ever written (two today)" },
9538
- adoptsFrom: "pipeline-analytics:migrations"
9533
+ retention: { none: "its PRESENCE stops the migration re-scanning every track, face and event row on the next boot; bounded by the number of migrations ever written (two today)" }
9539
9534
  },
9540
9535
  "sweep-cursors": {
9541
9536
  schema: SweepCursorBodySchema,
@@ -9544,8 +9539,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9544
9539
  writeTrigger: "the end of every orphan-audit pass rewrites the one row holding every scope cursor",
9545
9540
  device: "none",
9546
9541
  at: "none",
9547
- retention: { none: "one row (id orphan-audit); losing it costs a re-walk from offset 0, never a wrong delete" },
9548
- adoptsFrom: "pipeline-analytics:sweep-cursors"
9542
+ retention: { none: "one row (id orphan-audit); losing it costs a re-walk from offset 0, never a wrong delete" }
9549
9543
  },
9550
9544
  "face-rescore-state": {
9551
9545
  schema: FaceRescoreStateBodySchema,
@@ -9553,8 +9547,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9553
9547
  writeTrigger: "a whole-archive face rescore (manual or scheduled) completes",
9554
9548
  device: "none",
9555
9549
  at: "none",
9556
- retention: { none: "one row (id last-run); losing it costs one idempotent rescore on the next scheduler tick" },
9557
- adoptsFrom: "pipeline-analytics:face-rescore-state"
9550
+ retention: { none: "one row (id last-run); losing it costs one idempotent rescore on the next scheduler tick" }
9558
9551
  },
9559
9552
  "phantom-cells": {
9560
9553
  schema: DurableLedgerBodySchema,
@@ -9562,8 +9555,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9562
9555
  writeTrigger: "a phantom cell closes and the short-motionless bar records it",
9563
9556
  device: "required",
9564
9557
  at: "none",
9565
- retention: { none: "bounded by per-device cap in application code" },
9566
- adoptsFrom: PA_PHANTOM_CELLS
9558
+ retention: { none: "bounded by per-device cap in application code" }
9567
9559
  },
9568
9560
  "package-background": {
9569
9561
  schema: DurableLedgerBodySchema,
@@ -9571,8 +9563,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9571
9563
  writeTrigger: "package background state accepted for a device",
9572
9564
  device: "required",
9573
9565
  at: "none",
9574
- retention: { none: "one row per device key the owner tracks" },
9575
- adoptsFrom: PA_PACKAGE_BACKGROUND
9566
+ retention: { none: "one row per device key the owner tracks" }
9576
9567
  },
9577
9568
  "stationary-evidence": {
9578
9569
  schema: DurableLedgerBodySchema,
@@ -9580,8 +9571,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9580
9571
  writeTrigger: "stationary evidence row written on the registry sweep",
9581
9572
  device: "required",
9582
9573
  at: "none",
9583
- retention: { none: "owner sweep drops idle evidence; maxRowsPerDevice in kind metadata" },
9584
- adoptsFrom: PA_STATIONARY_EVIDENCE
9574
+ retention: { none: "owner sweep drops idle evidence; maxRowsPerDevice in kind metadata" }
9585
9575
  },
9586
9576
  "stationary-objects": {
9587
9577
  schema: DurableLedgerBodySchema,
@@ -9589,8 +9579,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9589
9579
  writeTrigger: "parked object registry upsert or flush on the sweep interval",
9590
9580
  device: "required",
9591
9581
  at: "none",
9592
- retention: { none: "rows drop when the object moves or TTL expires" },
9593
- adoptsFrom: PA_STATIONARY_OBJECTS
9582
+ retention: { none: "rows drop when the object moves or TTL expires" }
9594
9583
  },
9595
9584
  "scene-latch": {
9596
9585
  schema: DurableLedgerBodySchema,
@@ -9598,8 +9587,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9598
9587
  writeTrigger: "scene monitor latch state write-through after a verdict",
9599
9588
  device: "required",
9600
9589
  at: "none",
9601
- retention: { none: "one row per (deviceId, monitorId) scene latch" },
9602
- adoptsFrom: PA_SCENE_LATCH
9590
+ retention: { none: "one row per (deviceId, monitorId) scene latch" }
9603
9591
  },
9604
9592
  "motion-events": {
9605
9593
  schema: MotionEventBodySchema,
@@ -9616,8 +9604,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9616
9604
  retention: {
9617
9605
  owner: "device-window",
9618
9606
  reason: "terminal age-keyed root swept with the device retention window (D61/D62)"
9619
- },
9620
- adoptsFrom: PA_MOTION_EVENTS
9607
+ }
9621
9608
  },
9622
9609
  "audio-events": {
9623
9610
  schema: AudioEventBodySchema,
@@ -9634,8 +9621,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9634
9621
  retention: {
9635
9622
  owner: "device-window",
9636
9623
  reason: "terminal age-keyed root on the device window, same as motion-events"
9637
- },
9638
- adoptsFrom: PA_AUDIO_EVENTS
9624
+ }
9639
9625
  },
9640
9626
  "sensor-events": {
9641
9627
  schema: SensorEventBodySchema,
@@ -9655,8 +9641,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9655
9641
  retention: {
9656
9642
  owner: "device-window",
9657
9643
  reason: "terminal age-keyed root on the camera retention window (D61)"
9658
- },
9659
- adoptsFrom: PA_SENSOR_EVENTS
9644
+ }
9660
9645
  },
9661
9646
  faces: {
9662
9647
  schema: FaceLedgerBodySchema,
@@ -9683,8 +9668,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9683
9668
  fields: ["trackId"]
9684
9669
  }
9685
9670
  ],
9686
- retention: { none: "track-owned (D61): rows leave with their track; enrolled faces exempt from capacity sweeps" },
9687
- adoptsFrom: PA_FACES
9671
+ retention: { none: "track-owned (D61): rows leave with their track; enrolled faces exempt from capacity sweeps" }
9688
9672
  },
9689
9673
  identities: {
9690
9674
  schema: IdentityConfigBodySchema,
@@ -9698,8 +9682,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9698
9682
  name: "name",
9699
9683
  fields: ["name"]
9700
9684
  }],
9701
- retention: { none: "operator delete only — losing the gallery un-enrols every person" },
9702
- adoptsFrom: PA_IDENTITIES
9685
+ retention: { none: "operator delete only — losing the gallery un-enrols every person" }
9703
9686
  },
9704
9687
  "identity-samples": {
9705
9688
  schema: IdentitySampleLedgerBodySchema,
@@ -9713,7 +9696,6 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9713
9696
  fields: ["identityId", "addedAt"]
9714
9697
  }],
9715
9698
  retention: { none: "enrollment exempt — rows leave only on removeSample or deleteIdentity" },
9716
- adoptsFrom: PA_IDENTITY_SAMPLES,
9717
9699
  transactFamily: IDENTITY_GALLERY
9718
9700
  },
9719
9701
  vehicles: {
@@ -9728,8 +9710,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9728
9710
  name: "name",
9729
9711
  fields: ["name"]
9730
9712
  }],
9731
- retention: { none: "operator delete only — losing the gallery un-enrols every vehicle" },
9732
- adoptsFrom: PA_VEHICLES
9713
+ retention: { none: "operator delete only — losing the gallery un-enrols every vehicle" }
9733
9714
  },
9734
9715
  "vehicle-samples": {
9735
9716
  schema: VehicleSampleLedgerBodySchema,
@@ -9743,7 +9724,6 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9743
9724
  fields: ["vehicleId", "addedAt"]
9744
9725
  }],
9745
9726
  retention: { none: "enrollment exempt — rows leave only on removeSample or deleteVehicle" },
9746
- adoptsFrom: PA_VEHICLE_SAMPLES,
9747
9727
  transactFamily: VEHICLE_GALLERY
9748
9728
  },
9749
9729
  plates: {
@@ -9767,8 +9747,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9767
9747
  fields: ["recognizedVehicleId", "timestamp"]
9768
9748
  }
9769
9749
  ],
9770
- retention: { none: "track-owned (D61): rows leave with their track; corrected and enrolled reads exempt" },
9771
- adoptsFrom: PA_PLATES
9750
+ retention: { none: "track-owned (D61): rows leave with their track; corrected and enrolled reads exempt" }
9772
9751
  },
9773
9752
  "track-aliases": {
9774
9753
  schema: PaEventsLedgerBodySchema,
@@ -9784,8 +9763,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9784
9763
  name: "device",
9785
9764
  fields: ["deviceId"]
9786
9765
  }],
9787
- retention: { none: "track-owned via survivor `trackId`; widens retention cascade (D683)" },
9788
- adoptsFrom: PA_TRACK_ALIASES
9766
+ retention: { none: "track-owned via survivor `trackId`; widens retention cascade (D683)" }
9789
9767
  },
9790
9768
  "track-purity": {
9791
9769
  schema: PaEventsLedgerBodySchema,
@@ -9801,8 +9779,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9801
9779
  name: "updated",
9802
9780
  fields: ["updatedAt"]
9803
9781
  }],
9804
- retention: { none: "row-count ceiling MAX_TRACK_PURITIES in application code (D681)" },
9805
- adoptsFrom: PA_TRACK_PURITY
9782
+ retention: { none: "row-count ceiling MAX_TRACK_PURITIES in application code (D681)" }
9806
9783
  },
9807
9784
  "track-relations": {
9808
9785
  schema: PaEventsLedgerBodySchema,
@@ -9818,8 +9795,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9818
9795
  name: "updated",
9819
9796
  fields: ["updatedAt"]
9820
9797
  }],
9821
- retention: { none: "row-count ceiling MAX_TRACK_RELATIONS in application code (D681)" },
9822
- adoptsFrom: PA_TRACK_RELATIONS
9798
+ retention: { none: "row-count ceiling MAX_TRACK_RELATIONS in application code (D681)" }
9823
9799
  },
9824
9800
  "tracking-tapes": {
9825
9801
  schema: PaEventsLedgerBodySchema,
@@ -9832,8 +9808,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9832
9808
  name: "device_created",
9833
9809
  fields: ["deviceId", "createdAt"]
9834
9810
  }],
9835
- retention: { none: "promotion budget MAX_PROMOTED_TAPES — refused, never evicted by age" },
9836
- adoptsFrom: PA_TRACKING_TAPES
9811
+ retention: { none: "promotion budget MAX_PROMOTED_TAPES — refused, never evicted by age" }
9837
9812
  },
9838
9813
  "birth-decision-ledger": {
9839
9814
  schema: PaEventsLedgerBodySchema,
@@ -9849,8 +9824,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9849
9824
  name: "device_at",
9850
9825
  fields: ["deviceId", "at"]
9851
9826
  }],
9852
- retention: { none: "row-count ceiling MAX_BIRTH_DECISION_RECORDS in application code" },
9853
- adoptsFrom: PA_BIRTH_DECISION_LEDGER
9827
+ retention: { none: "row-count ceiling MAX_BIRTH_DECISION_RECORDS in application code" }
9854
9828
  },
9855
9829
  "debug-note-archive": {
9856
9830
  schema: PaEventsLedgerBodySchema,
@@ -9866,8 +9840,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9866
9840
  name: "device_archived",
9867
9841
  fields: ["deviceId", "archivedAt"]
9868
9842
  }],
9869
- retention: { none: "row-count ceiling MAX_ARCHIVED_DEBUG_NOTES in application code" },
9870
- adoptsFrom: PA_DEBUG_NOTE_ARCHIVE
9843
+ retention: { none: "row-count ceiling MAX_ARCHIVED_DEBUG_NOTES in application code" }
9871
9844
  },
9872
9845
  "ops-log": {
9873
9846
  schema: PaEventsLedgerBodySchema,
@@ -9886,8 +9859,7 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9886
9859
  retention: {
9887
9860
  ttlMs: 720 * 60 * 60 * 1e3,
9888
9861
  from: "at"
9889
- },
9890
- adoptsFrom: PA_OPS_LOG
9862
+ }
9891
9863
  },
9892
9864
  "retrain-frames": {
9893
9865
  schema: PaEventsLedgerBodySchema,
@@ -9904,7 +9876,6 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9904
9876
  fields: ["deviceId", "complete"]
9905
9877
  }],
9906
9878
  retention: { none: "operator curation only; blobs on artifacts tenant (D781)" },
9907
- adoptsFrom: PA_RETRAIN_FRAMES,
9908
9879
  transactFamily: RETRAIN
9909
9880
  },
9910
9881
  "retrain-annotations": {
@@ -9929,7 +9900,6 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9929
9900
  }
9930
9901
  ],
9931
9902
  retention: { none: "leaves with frame deselect / operator delete" },
9932
- adoptsFrom: PA_RETRAIN_ANNOTATIONS,
9933
9903
  transactFamily: RETRAIN
9934
9904
  },
9935
9905
  "retrain-collections": {
@@ -9948,7 +9918,6 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9948
9918
  fields: ["createdAt"]
9949
9919
  }],
9950
9920
  retention: { none: "deleted with operator deleteCollection" },
9951
- adoptsFrom: PA_RETRAIN_COLLECTIONS,
9952
9921
  transactFamily: RETRAIN
9953
9922
  },
9954
9923
  "retrain-collection-frames": {
@@ -9966,14 +9935,13 @@ var pipelineAnalyticsRecordKinds = require_clip_model_registry.defineRecordKinds
9966
9935
  fields: ["frameId"]
9967
9936
  }],
9968
9937
  retention: { none: "membership only; frame pixels live in retrain-frames" },
9969
- adoptsFrom: PA_RETRAIN_COLLECTION_FRAMES,
9970
9938
  transactFamily: RETRAIN
9971
9939
  }
9972
9940
  });
9973
9941
  //#endregion
9974
9942
  //#region src/pipeline-analytics/scene/scene-config-ledger.ts
9975
9943
  /** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
9976
- var PAGE_SIZE$1 = 1e3;
9944
+ var PAGE_SIZE$2 = 1e3;
9977
9945
  /**
9978
9946
  * A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
9979
9947
  * half-parsed scene would report a verdict about a region it cannot describe.
@@ -10011,7 +9979,7 @@ var SceneConfigLedger = class {
10011
9979
  let seen = 0;
10012
9980
  let cursor;
10013
9981
  for (;;) {
10014
- const limit = Math.min(PAGE_SIZE$1, this.loadLimit - seen);
9982
+ const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
10015
9983
  if (limit <= 0) break;
10016
9984
  const page = await this.kind.query({
10017
9985
  limit,
@@ -12058,6 +12026,28 @@ var EventLedgerCollection = class EventLedgerCollection {
12058
12026
  if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
12059
12027
  return out;
12060
12028
  }
12029
+ /**
12030
+ * One page of the whole kind in `id` order, resumed from the records
12031
+ * engine's OPAQUE keyset cursor (`null` = from the top). The answer's
12032
+ * `nextCursor` is `null` at the end. A keyset cursor names the last row
12033
+ * read, so deleting rows behind it never shifts what comes next — the
12034
+ * reason a bulk walk that reclaims as it goes needs this, not an offset.
12035
+ *
12036
+ * Records only: without `ctx.records` the caller pages the settings store
12037
+ * itself, and asking here throws rather than inventing a cursor.
12038
+ */
12039
+ async pageById(cursor, limit) {
12040
+ if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
12041
+ const page = await this.kind.query({
12042
+ order: "id",
12043
+ limit,
12044
+ ...cursor === null ? {} : { cursor }
12045
+ });
12046
+ return {
12047
+ rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
12048
+ nextCursor: page.nextCursor
12049
+ };
12050
+ }
12061
12051
  async getManyByIds(ids) {
12062
12052
  if (ids.length === 0) return [];
12063
12053
  if (this.kind === null) return (await this.store.query.query({
@@ -13184,11 +13174,35 @@ var FaceStore = class {
13184
13174
  * carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
13185
13175
  * that is what they are stamped with — a KNOWN historical fact, not a
13186
13176
  * fallback guess. A row that names some other model keeps its own.
13177
+ *
13178
+ * On a records hub the rows are paged through the faces LEDGER: the legacy
13179
+ * table is dropped there, and the adoption copied each row's body whole —
13180
+ * an `embedding` the pass had not drained yet included — so a records-era
13181
+ * row can still need converting (the same shape as `IdentityStore`).
13187
13182
  */
13188
13183
  conversionSpec() {
13184
+ const ledger = this.ledger;
13185
+ const recordsPaging = ledger.usesRecords ? {
13186
+ pageRows: (limit, offset) => ledger.query({
13187
+ orderBy: {
13188
+ field: "id",
13189
+ direction: "asc"
13190
+ },
13191
+ limit,
13192
+ offset
13193
+ }, [
13194
+ "deviceId",
13195
+ "timestamp",
13196
+ "trackId",
13197
+ "embeddingModelId",
13198
+ "embedding"
13199
+ ]),
13200
+ drainEmbedding: (id) => ledger.update(id, { embedding: [] })
13201
+ } : {};
13189
13202
  return {
13190
13203
  collection: FACES_COLLECTION,
13191
13204
  vectors: this.vectors,
13205
+ ...recordsPaging,
13192
13206
  write: (id, embedding, data) => {
13193
13207
  const deviceId = data["deviceId"];
13194
13208
  const timestamp = data["timestamp"];
@@ -38437,22 +38451,8 @@ async function migrateLegacyRuleActions(actions, createSequence) {
38437
38451
  * The notification-center module is hosted inside the pipeline-analytics addon.
38438
38452
  * That addon declares no `execution.group`, so the UDS owner stamp is the addon
38439
38453
  * id: `pipeline-analytics`. Declared hub-only (agents have no `ctx.records`).
38440
- * One deploy covers all 7 kinds; adoption is kind-by-kind.
38441
- */
38442
- /** Legacy collection for the text-catalog document. */
38443
- var NC_TEXTS_COLLECTION$1 = "notification-center:texts";
38444
- /** Legacy collection for shared action sequences. */
38445
- var NC_SEQUENCES_COLLECTION$1 = "notification-center:sequences";
38446
- /** Legacy collection for per-rule AI sections. */
38447
- var NC_RULE_AI_COLLECTION$1 = "notification-center:rule-ai";
38448
- /** Legacy collection for calendar-events rules. */
38449
- var NC_CALENDAR_EVENTS_RULES_COLLECTION$1 = "notification-center:calendar-events-rules";
38450
- /** Legacy collection for multi-camera digest rules. */
38451
- var NC_SUMMARY_RULES_COLLECTION$1 = "notification-center:summary-rules";
38452
- /** Legacy collection for timelapse rules. */
38453
- var NC_TIMELAPSE_RULES_COLLECTION$1 = "notification-center:timelapse-rules";
38454
- /** Legacy collection for notification rules. */
38455
- var NC_RULES_COLLECTION$1 = "notification-center:rules";
38454
+ * One deploy covers all 7 kinds.
38455
+ */
38456
38456
  /**
38457
38457
  * The hub language and operator-authored per-key overrides.
38458
38458
  * One row, id `texts`. Losing it degrades to the shipped English catalog.
@@ -38494,8 +38494,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38494
38494
  writeTrigger: "an operator changes the hub language (setLanguage) or replaces the per-key override set (setOverrides)",
38495
38495
  device: "none",
38496
38496
  at: "none",
38497
- retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" },
38498
- adoptsFrom: NC_TEXTS_COLLECTION$1
38497
+ retention: { none: "one row (id `texts`); losing it costs the chosen language and household wording, never a notification — every read degrades to the shipped English catalog" }
38499
38498
  },
38500
38499
  /**
38501
38500
  * Shared action sequences — named, ordered runs of steps with their own throttle.
@@ -38507,8 +38506,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38507
38506
  writeTrigger: "an admin creates/edits/deletes a shared sequence via notificationRules.createSequence / updateSequence / deleteSequence, or the one-shot boot migration that materialises legacy inline sequences",
38508
38507
  device: "none",
38509
38508
  at: "none",
38510
- retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" },
38511
- adoptsFrom: NC_SEQUENCES_COLLECTION$1
38509
+ retention: { none: "a row goes only when an admin deletes it; bounded by the sequence set a human is willing to author" }
38512
38510
  },
38513
38511
  /**
38514
38512
  * Per-rule AI section: whether and how a rule narrates itself.
@@ -38520,8 +38518,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38520
38518
  writeTrigger: "an operator switches the AI section of a notification rule on or off, or edits its prompt / word cap / language",
38521
38519
  device: "none",
38522
38520
  at: "none",
38523
- retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" },
38524
- adoptsFrom: NC_RULE_AI_COLLECTION$1
38521
+ retention: { none: "a row goes when the operator clears the section or deletes the rule; bounded by the rule set a human is willing to author" }
38525
38522
  },
38526
38523
  /**
38527
38524
  * Calendar-events rules. One row per rule, id = rule UUID.
@@ -38536,8 +38533,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38536
38533
  name: "enabled",
38537
38534
  fields: ["enabled"]
38538
38535
  }],
38539
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38540
- adoptsFrom: NC_CALENDAR_EVENTS_RULES_COLLECTION$1
38536
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38541
38537
  },
38542
38538
  /**
38543
38539
  * Multi-camera digest rules. One row per rule, id = rule UUID.
@@ -38552,8 +38548,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38552
38548
  name: "enabled",
38553
38549
  fields: ["enabled"]
38554
38550
  }],
38555
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38556
- adoptsFrom: NC_SUMMARY_RULES_COLLECTION$1
38551
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38557
38552
  },
38558
38553
  /**
38559
38554
  * Timelapse rules. One row per rule, id = rule UUID.
@@ -38569,8 +38564,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38569
38564
  name: "enabled",
38570
38565
  fields: ["enabled"]
38571
38566
  }],
38572
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38573
- adoptsFrom: NC_TIMELAPSE_RULES_COLLECTION$1
38567
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38574
38568
  },
38575
38569
  /**
38576
38570
  * Notification rules — the main rule set. One row per rule, id = rule UUID.
@@ -38586,8 +38580,7 @@ var ncRecordKinds = require_clip_model_registry.defineRecordKinds({
38586
38580
  name: "delivery",
38587
38581
  fields: ["delivery"]
38588
38582
  }],
38589
- retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" },
38590
- adoptsFrom: NC_RULES_COLLECTION$1
38583
+ retention: { none: "a row goes only when the operator deletes the rule; bounded by the rule set a human is willing to author" }
38591
38584
  }
38592
38585
  });
38593
38586
  //#endregion
@@ -67474,7 +67467,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
67474
67467
  });
67475
67468
  return "old-removal-retried";
67476
67469
  }
67477
- function tally$1(samples, outcomes, dryRun) {
67470
+ function tally$2(samples, outcomes, dryRun) {
67478
67471
  const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
67479
67472
  const refusals = [];
67480
67473
  for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
@@ -67546,7 +67539,7 @@ async function reenrolPhotoSamples(deps, options) {
67546
67539
  outcomes.push(outcomeOf(sample, verdict));
67547
67540
  await deps.pause(pacingMs);
67548
67541
  }
67549
- const result = tally$1(samples, outcomes, dryRun);
67542
+ const result = tally$2(samples, outcomes, dryRun);
67550
67543
  if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
67551
67544
  const { outcomes: _perSample, ...summary } = result;
67552
67545
  deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
@@ -78963,24 +78956,11 @@ function createDiscardJob(deps) {
78963
78956
  });
78964
78957
  await sleep(0);
78965
78958
  }
78966
- for (;;) {
78959
+ const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
78960
+ for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
78967
78961
  if (cancelRequested) return owners;
78968
- const rows = await deps.store.query.query({
78969
- collection: RETRAIN_FRAMES_COLLECTION,
78970
- columns: ["id"],
78971
- filter: {
78972
- where: { locationId },
78973
- limit: MEDIA_PAGE
78974
- }
78975
- });
78976
- if (rows.length === 0) break;
78977
- for (const row of rows) {
78978
- await deps.store.delete.mutate({
78979
- collection: RETRAIN_FRAMES_COLLECTION,
78980
- key: row.id
78981
- });
78982
- deleted += 1;
78983
- }
78962
+ const page = retrainIds.slice(i, i + MEDIA_PAGE);
78963
+ deleted += await deps.retrainFrameLedger.deleteByIds(page);
78984
78964
  report({
78985
78965
  dbRowsDeleted: deleted,
78986
78966
  detail: `deleted ${deleted} media/retrain rows`
@@ -79032,6 +79012,383 @@ function createDiscardJob(deps) {
79032
79012
  };
79033
79013
  }
79034
79014
  //#endregion
79015
+ //#region src/pipeline-analytics/retention/media-forget-actions.ts
79016
+ /**
79017
+ * The operator door for the NARROW media-row forget (`media-row-forget.ts`).
79018
+ *
79019
+ * It forgets `pipeline-analytics:media` rows stamped with ONE location whose
79020
+ * file is provably gone, and nothing else: no event, track, object-event,
79021
+ * face, plate or identity row is read for deletion, ever. Contrast
79022
+ * `storage.discardLocation`, which wipes the disk AND cascades owners.
79023
+ *
79024
+ * `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
79025
+ * also requires `expectAbsent` — the `absent` count a dry run just reported —
79026
+ * so the forget only ever deletes a population a human has already read.
79027
+ */
79028
+ /** Hard ceiling on rows one call examines — the call is synchronous. */
79029
+ var MEDIA_FORGET_MAX_ROWS = 5e4;
79030
+ var MediaForgetInputSchema = require_clip_model_registry.object({
79031
+ locationId: require_clip_model_registry.string().min(1),
79032
+ /** Restrict to these cameras. Omitted = every row stamped with the location. */
79033
+ deviceIds: require_clip_model_registry.array(require_clip_model_registry.number().int()).min(1).optional(),
79034
+ dryRun: require_clip_model_registry.boolean().default(true),
79035
+ /**
79036
+ * Required when `dryRun` is false: the `totals.absent` of the dry run. A
79037
+ * different count at apply time refuses the whole call before any delete.
79038
+ */
79039
+ expectAbsent: require_clip_model_registry.number().int().nonnegative().optional(),
79040
+ /** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
79041
+ maxRows: require_clip_model_registry.number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
79042
+ /** Detailed rows returned per verdict. Default 20. */
79043
+ sampleSize: require_clip_model_registry.number().int().min(0).max(500).optional()
79044
+ });
79045
+ var VerdictCountsSchema = require_clip_model_registry.object({
79046
+ rows: require_clip_model_registry.number(),
79047
+ /** File answered ENOENT — the only verdict that may be forgotten. */
79048
+ absent: require_clip_model_registry.number(),
79049
+ /** File is there: relocation is incomplete. Never forgotten. */
79050
+ present: require_clip_model_registry.number(),
79051
+ /** The stat failed with any errno other than ENOENT. Never forgotten. */
79052
+ error: require_clip_model_registry.number(),
79053
+ /** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
79054
+ kept: require_clip_model_registry.number(),
79055
+ /** Rows actually deleted (0 on a dry run). */
79056
+ deleted: require_clip_model_registry.number()
79057
+ });
79058
+ var MediaForgetSampleSchema = require_clip_model_registry.object({
79059
+ mediaId: require_clip_model_registry.number(),
79060
+ deviceId: require_clip_model_registry.number(),
79061
+ kind: require_clip_model_registry.string(),
79062
+ ownerKind: require_clip_model_registry.string(),
79063
+ ownerId: require_clip_model_registry.string(),
79064
+ path: require_clip_model_registry.string(),
79065
+ /** The errno (error) or the reason (kept); absent otherwise. */
79066
+ reason: require_clip_model_registry.string().optional()
79067
+ });
79068
+ var MediaForgetResultSchema = require_clip_model_registry.object({
79069
+ locationId: require_clip_model_registry.string(),
79070
+ dryRun: require_clip_model_registry.boolean(),
79071
+ root: require_clip_model_registry.string(),
79072
+ /** Rows of this location that were not examined because `maxRows` was hit. */
79073
+ truncated: require_clip_model_registry.boolean(),
79074
+ totals: VerdictCountsSchema,
79075
+ byDevice: require_clip_model_registry.array(VerdictCountsSchema.extend({ deviceId: require_clip_model_registry.number() })),
79076
+ byKind: require_clip_model_registry.array(VerdictCountsSchema.extend({ kind: require_clip_model_registry.string() })),
79077
+ /** Every id the call deletes (dry run: would delete). The exact list. */
79078
+ absentIds: require_clip_model_registry.array(require_clip_model_registry.number()),
79079
+ samples: require_clip_model_registry.object({
79080
+ absent: require_clip_model_registry.array(MediaForgetSampleSchema),
79081
+ present: require_clip_model_registry.array(MediaForgetSampleSchema),
79082
+ error: require_clip_model_registry.array(MediaForgetSampleSchema),
79083
+ kept: require_clip_model_registry.array(MediaForgetSampleSchema)
79084
+ }),
79085
+ /** Absent at classification, no longer ENOENT at delete time: kept. */
79086
+ changedBeforeDelete: require_clip_model_registry.number()
79087
+ });
79088
+ var mediaForgetActions = require_clip_model_registry.defineCustomActions({ "storage.forgetMissingMedia": require_clip_model_registry.customAction(MediaForgetInputSchema, MediaForgetResultSchema, {
79089
+ kind: "mutation",
79090
+ auth: "admin"
79091
+ }) });
79092
+ //#endregion
79093
+ //#region src/pipeline-analytics/retention/media-row-forget.ts
79094
+ /**
79095
+ * The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
79096
+ * with ONE storage location whose file is provably gone — and nothing else.
79097
+ *
79098
+ * Why it exists: a location whose bytes were lost keeps rows naming it, and a
79099
+ * cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
79100
+ * once cascaded ~132 k events away from a read it did not double-check. Here:
79101
+ *
79102
+ * - **Only media rows.** No event, track, object-event, face, plate or
79103
+ * identity row is deleted. A parent keeps existing with that media kind
79104
+ * missing — which is exactly what it already serves (`getBytesById` answers
79105
+ * `null` for a missing row as it does for a missing blob).
79106
+ * - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
79107
+ * location's own root on THIS node. Any other errno keeps the row and is
79108
+ * reported; a present file keeps the row and is reported (relocation is
79109
+ * incomplete). A row with no path, a path that escapes the root, or a
79110
+ * retention-exempt owner (the enrolled gallery) is kept.
79111
+ * - **Only rows stamped with the id.** `locationId IS NULL` rows mean
79112
+ * "wherever eventMedia points now" and are never selected.
79113
+ * - **Only a location nothing writes to.** An active (writable) location is
79114
+ * refused: new rows could land while the walk runs.
79115
+ * - **Dry run first, then the same count.** Applying requires
79116
+ * `expectAbsent`; the whole population is classified before any delete,
79117
+ * and a different absent count refuses the call with nothing written.
79118
+ * Each id is re-`stat`ed right before its batch is deleted, and the delete
79119
+ * predicate repeats `locationId`, so a row repointed in between survives.
79120
+ *
79121
+ * Occupancy needs no separate bookkeeping: `storage-occupancy` sums
79122
+ * `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
79123
+ */
79124
+ var PAGE_SIZE$1 = 200;
79125
+ var DELETE_BATCH = 200;
79126
+ var DEFAULT_SAMPLE_SIZE = 20;
79127
+ function errnoOf(err) {
79128
+ if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
79129
+ return err instanceof Error ? err.message : String(err);
79130
+ }
79131
+ function emptyCounts() {
79132
+ return {
79133
+ rows: 0,
79134
+ absent: 0,
79135
+ present: 0,
79136
+ error: 0,
79137
+ kept: 0,
79138
+ deleted: 0
79139
+ };
79140
+ }
79141
+ function tally$1(counts, verdict) {
79142
+ const rows = counts.rows + 1;
79143
+ switch (verdict) {
79144
+ case "absent": return {
79145
+ ...counts,
79146
+ rows,
79147
+ absent: counts.absent + 1
79148
+ };
79149
+ case "present": return {
79150
+ ...counts,
79151
+ rows,
79152
+ present: counts.present + 1
79153
+ };
79154
+ case "error": return {
79155
+ ...counts,
79156
+ rows,
79157
+ error: counts.error + 1
79158
+ };
79159
+ case "kept": return {
79160
+ ...counts,
79161
+ rows,
79162
+ kept: counts.kept + 1
79163
+ };
79164
+ }
79165
+ }
79166
+ /** The file's absolute path under `root`, or `null` when it would escape it. */
79167
+ function resolveUnder(root, relativePath) {
79168
+ const base = node_path.default.resolve(root);
79169
+ const full = node_path.default.resolve(base, relativePath);
79170
+ return full.startsWith(base + node_path.default.sep) ? full : null;
79171
+ }
79172
+ function toSample(id, data) {
79173
+ return {
79174
+ mediaId: Number(id),
79175
+ deviceId: Number(data["deviceId"] ?? 0),
79176
+ kind: String(data["kind"] ?? ""),
79177
+ ownerKind: String(data["ownerKind"] ?? ""),
79178
+ ownerId: String(data["ownerId"] ?? ""),
79179
+ path: String(data["path"] ?? "")
79180
+ };
79181
+ }
79182
+ async function probe(stat, absolutePath) {
79183
+ try {
79184
+ await stat(absolutePath);
79185
+ return { verdict: "present" };
79186
+ } catch (err) {
79187
+ const code = errnoOf(err);
79188
+ return code === "ENOENT" ? { verdict: "absent" } : {
79189
+ verdict: "error",
79190
+ reason: code
79191
+ };
79192
+ }
79193
+ }
79194
+ async function classify(stat, root, id, data) {
79195
+ const sample = toSample(id, data);
79196
+ const ownerKind = sample.ownerKind;
79197
+ if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
79198
+ key: id,
79199
+ sample: {
79200
+ ...sample,
79201
+ reason: "retention-exempt owner"
79202
+ },
79203
+ verdict: "kept",
79204
+ absolutePath: null
79205
+ };
79206
+ if (sample.path.length === 0) return {
79207
+ key: id,
79208
+ sample: {
79209
+ ...sample,
79210
+ reason: "no path"
79211
+ },
79212
+ verdict: "kept",
79213
+ absolutePath: null
79214
+ };
79215
+ const absolutePath = resolveUnder(root, sample.path);
79216
+ if (absolutePath === null) return {
79217
+ key: id,
79218
+ sample: {
79219
+ ...sample,
79220
+ reason: "path escapes the root"
79221
+ },
79222
+ verdict: "kept",
79223
+ absolutePath
79224
+ };
79225
+ const result = await probe(stat, absolutePath);
79226
+ return {
79227
+ key: id,
79228
+ sample: result.reason === void 0 ? sample : {
79229
+ ...sample,
79230
+ reason: result.reason
79231
+ },
79232
+ verdict: result.verdict,
79233
+ absolutePath
79234
+ };
79235
+ }
79236
+ function resolveTarget(locations, locationId, localNodeId) {
79237
+ const location = locations.find((row) => row.id === locationId);
79238
+ if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
79239
+ if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
79240
+ if (require_clip_model_registry.mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
79241
+ const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
79242
+ if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
79243
+ return {
79244
+ location,
79245
+ root: locationBasePath(location)
79246
+ };
79247
+ }
79248
+ /**
79249
+ * Classify every row stamped with `input.locationId` (bounded by `maxRows`),
79250
+ * and — only when `dryRun` is false and the absent count equals
79251
+ * `expectAbsent` — delete the absent ones, per camera, in batches.
79252
+ */
79253
+ async function forgetMissingMediaRows(deps, rawInput) {
79254
+ const input = MediaForgetInputSchema.parse(rawInput);
79255
+ if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
79256
+ const stat = deps.stat ?? ((p) => node_fs.promises.stat(p));
79257
+ const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
79258
+ const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
79259
+ const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
79260
+ const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
79261
+ const classified = [];
79262
+ let cursor = 0;
79263
+ let truncated = false;
79264
+ for (;;) {
79265
+ const rows = await deps.store.query.query({
79266
+ collection: MEDIA_COLLECTION,
79267
+ columns: [
79268
+ "id",
79269
+ "deviceId",
79270
+ "ownerKind",
79271
+ "ownerId",
79272
+ "kind",
79273
+ "path"
79274
+ ],
79275
+ filter: {
79276
+ where: { locationId: input.locationId },
79277
+ ...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
79278
+ whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
79279
+ orderBy: {
79280
+ field: "id",
79281
+ direction: "asc"
79282
+ },
79283
+ limit: PAGE_SIZE$1
79284
+ }
79285
+ });
79286
+ if (rows.length === 0) break;
79287
+ for (const row of rows) {
79288
+ if (classified.length >= maxRows) {
79289
+ truncated = true;
79290
+ break;
79291
+ }
79292
+ classified.push(await classify(stat, root, row.id, row.data));
79293
+ cursor = Math.max(cursor, Number(row.id));
79294
+ }
79295
+ if (truncated || rows.length < PAGE_SIZE$1) break;
79296
+ await pace();
79297
+ }
79298
+ const absentRows = classified.filter((c) => c.verdict === "absent");
79299
+ const deletedIds = /* @__PURE__ */ new Set();
79300
+ let changedBeforeDelete = 0;
79301
+ if (!input.dryRun) {
79302
+ if (absentRows.length !== input.expectAbsent) throw new Error(`forgetMissingMedia refused: ${String(absentRows.length)} rows are absent now, expectAbsent was ${String(input.expectAbsent)} — run the dry run again; nothing was deleted`);
79303
+ const byDevice = /* @__PURE__ */ new Map();
79304
+ for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
79305
+ for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
79306
+ const batch = [];
79307
+ for (const row of rows.slice(at, at + DELETE_BATCH)) if ((row.absolutePath === null ? null : await probe(stat, row.absolutePath))?.verdict === "absent") batch.push(row);
79308
+ else changedBeforeDelete += 1;
79309
+ if (batch.length === 0) continue;
79310
+ const { deleted } = await deps.store.deleteWhere.mutate({
79311
+ collection: MEDIA_COLLECTION,
79312
+ filter: {
79313
+ where: {
79314
+ locationId: input.locationId,
79315
+ deviceId
79316
+ },
79317
+ whereIn: { id: batch.map((row) => row.key) }
79318
+ }
79319
+ });
79320
+ for (const row of batch) deletedIds.add(row.sample.mediaId);
79321
+ deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
79322
+ tags: { deviceId },
79323
+ meta: {
79324
+ locationId: input.locationId,
79325
+ batch: batch.length,
79326
+ deleted,
79327
+ firstId: batch[0]?.sample.mediaId,
79328
+ lastId: batch[batch.length - 1]?.sample.mediaId
79329
+ }
79330
+ });
79331
+ if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
79332
+ tags: { deviceId },
79333
+ meta: {
79334
+ locationId: input.locationId,
79335
+ batch: batch.length,
79336
+ deleted
79337
+ }
79338
+ });
79339
+ await pace();
79340
+ }
79341
+ }
79342
+ let totals = emptyCounts();
79343
+ const perDevice = /* @__PURE__ */ new Map();
79344
+ const perKind = /* @__PURE__ */ new Map();
79345
+ for (const row of classified) {
79346
+ const isDeleted = deletedIds.has(row.sample.mediaId);
79347
+ const bump = (c) => {
79348
+ const next = tally$1(c, row.verdict);
79349
+ return isDeleted ? {
79350
+ ...next,
79351
+ deleted: next.deleted + 1
79352
+ } : next;
79353
+ };
79354
+ totals = bump(totals);
79355
+ perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
79356
+ perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
79357
+ }
79358
+ const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
79359
+ for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
79360
+ tags: { deviceId },
79361
+ meta: {
79362
+ locationId: input.locationId,
79363
+ dryRun: input.dryRun,
79364
+ ...counts
79365
+ }
79366
+ });
79367
+ return {
79368
+ locationId: input.locationId,
79369
+ dryRun: input.dryRun,
79370
+ root,
79371
+ truncated,
79372
+ totals,
79373
+ byDevice: [...perDevice].map(([deviceId, c]) => ({
79374
+ deviceId,
79375
+ ...c
79376
+ })),
79377
+ byKind: [...perKind].map(([kind, c]) => ({
79378
+ kind,
79379
+ ...c
79380
+ })),
79381
+ absentIds: absentRows.map((r) => r.sample.mediaId),
79382
+ samples: {
79383
+ absent: samplesOf("absent"),
79384
+ present: samplesOf("present"),
79385
+ error: samplesOf("error"),
79386
+ kept: samplesOf("kept")
79387
+ },
79388
+ changedBeforeDelete
79389
+ };
79390
+ }
79391
+ //#endregion
79035
79392
  //#region src/pipeline-analytics/retention/media-reclaim.ts
79036
79393
  var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
79037
79394
  var DEFAULT_MEDIA_RECLAIM_LIMITS = {
@@ -93359,17 +93716,8 @@ function rowIsGallery(data) {
93359
93716
  * D295 calls "a guard nobody believes".
93360
93717
  */
93361
93718
  function collectionsFor(mode) {
93362
- if (mode === "gallery") return [{
93363
- name: MEDIA_COLLECTION,
93364
- order: "timestamp"
93365
- }];
93366
- return [{
93367
- name: MEDIA_COLLECTION,
93368
- order: "timestamp"
93369
- }, {
93370
- name: RETRAIN_FRAMES_COLLECTION,
93371
- order: "copiedAt"
93372
- }];
93719
+ if (mode === "gallery") return [{ lane: "media" }];
93720
+ return [{ lane: "media" }, { lane: "retrainFrames" }];
93373
93721
  }
93374
93722
  /**
93375
93723
  * Does this pass act on this row at all?
@@ -93454,7 +93802,7 @@ var MediaRelocateEngine = class {
93454
93802
  async countUnstamped() {
93455
93803
  try {
93456
93804
  const media = await this.countUnstampedIn(MEDIA_COLLECTION);
93457
- const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
93805
+ const retrainFrames = await this.countUnstampedRetrainFrames();
93458
93806
  return {
93459
93807
  media,
93460
93808
  retrainFrames,
@@ -93506,6 +93854,28 @@ var MediaRelocateEngine = class {
93506
93854
  };
93507
93855
  }
93508
93856
  }
93857
+ /**
93858
+ * The retrain lane's unstamped population. The ledger's records `where` is
93859
+ * equality-only (a `null` predicate is dropped, which would count the whole
93860
+ * dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
93861
+ * dataset is operator-selected and small.
93862
+ */
93863
+ async countUnstampedRetrainFrames() {
93864
+ const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
93865
+ return {
93866
+ present: rows > 0,
93867
+ rows
93868
+ };
93869
+ }
93870
+ /** Every row of one lane, oldest-first. */
93871
+ rowsOf(collection) {
93872
+ return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
93873
+ }
93874
+ /** The retrain-frame ledger, read whole and yielded oldest copy first. */
93875
+ async *retrainFrameRows() {
93876
+ const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
93877
+ for (const row of ordered) yield row;
93878
+ }
93509
93879
  start(input) {
93510
93880
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
93511
93881
  this.cancelCensuses("a relocate pass started");
@@ -93609,22 +93979,22 @@ var MediaRelocateEngine = class {
93609
93979
  let sealable = 0;
93610
93980
  for (const collection of collectionsFor(mode)) {
93611
93981
  if (stop()) return null;
93612
- sealable += await this.deps.store.count.query({
93613
- collection: collection.name,
93982
+ sealable += collection.lane === "media" ? await this.deps.store.count.query({
93983
+ collection: MEDIA_COLLECTION,
93614
93984
  filter: { where: { [LOCATION_ID_FIELD]: null } }
93615
- });
93985
+ }) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
93616
93986
  }
93617
93987
  return { rows: sealable };
93618
93988
  }
93619
93989
  let rows = 0;
93620
93990
  for (const collection of collectionsFor(mode)) {
93621
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93622
- for await (const row of this.pageRows(collection.name, collection.order)) {
93991
+ const isRetrain = collection.lane === "retrainFrames";
93992
+ for await (const row of this.rowsOf(collection)) {
93623
93993
  if (stop()) {
93624
93994
  this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
93625
93995
  mode,
93626
93996
  toLocationId,
93627
- collection: collection.name
93997
+ collection: collection.lane
93628
93998
  } });
93629
93999
  return null;
93630
94000
  }
@@ -93632,7 +94002,7 @@ var MediaRelocateEngine = class {
93632
94002
  this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
93633
94003
  mode,
93634
94004
  toLocationId,
93635
- collection: collection.name,
94005
+ collection: collection.lane,
93636
94006
  rowsVisited: budget.rowsVisited,
93637
94007
  rowBudget: MEDIA_CENSUS_ROW_BUDGET,
93638
94008
  deadlineMs: MEDIA_CENSUS_DEADLINE_MS
@@ -93668,15 +94038,15 @@ var MediaRelocateEngine = class {
93668
94038
  if (job.mode === "seal") {
93669
94039
  for (const collection of collectionsFor(job.mode)) {
93670
94040
  if (job.cancelRequested) break;
93671
- await this.sealCollection(collection.name, job);
94041
+ await this.sealCollection(collection, job);
93672
94042
  }
93673
94043
  job.state = job.cancelRequested ? "cancelled" : "done";
93674
94044
  return;
93675
94045
  }
93676
94046
  for (const collection of collectionsFor(job.mode)) {
93677
94047
  if (job.cancelRequested) break;
93678
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93679
- for await (const row of this.pageRows(collection.name, collection.order)) {
94048
+ const isRetrain = collection.lane === "retrainFrames";
94049
+ for await (const row of this.rowsOf(collection)) {
93680
94050
  if (job.cancelRequested) break;
93681
94051
  const data = row.data;
93682
94052
  if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
@@ -93688,7 +94058,7 @@ var MediaRelocateEngine = class {
93688
94058
  }
93689
94059
  try {
93690
94060
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
93691
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94061
+ await this.stamp(collection, row.id, data, job.toLocationId);
93692
94062
  continue;
93693
94063
  }
93694
94064
  const bytes = await this.deps.storage.read({
@@ -93704,7 +94074,7 @@ var MediaRelocateEngine = class {
93704
94074
  location: job.toLocationId,
93705
94075
  relativePath
93706
94076
  })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
93707
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94077
+ await this.stamp(collection, row.id, data, job.toLocationId);
93708
94078
  await this.deps.storage.delete({
93709
94079
  location: from,
93710
94080
  relativePath
@@ -93715,7 +94085,7 @@ var MediaRelocateEngine = class {
93715
94085
  } catch (err) {
93716
94086
  failedRows++;
93717
94087
  this.deps.logger.debug("media relocate row failed", { meta: {
93718
- collection: collection.name,
94088
+ collection: collection.lane,
93719
94089
  key: row.id,
93720
94090
  error: String(err)
93721
94091
  } });
@@ -93788,7 +94158,17 @@ var MediaRelocateEngine = class {
93788
94158
  * reports a completed seal having stamped nothing. Zero unstamped rows is the
93789
94159
  * single value the migration gate reads as "safe to cut over".
93790
94160
  */
93791
- async sealCollection(collection, job) {
94161
+ async sealCollection(lane, job) {
94162
+ if (lane.lane === "retrainFrames") {
94163
+ for (const row of await this.deps.retrainFrameLedger.query({})) {
94164
+ if (job.cancelRequested) return;
94165
+ if (!rowIsUnstamped(row.data)) continue;
94166
+ await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
94167
+ job.filesMoved += 1;
94168
+ }
94169
+ return;
94170
+ }
94171
+ const collection = MEDIA_COLLECTION;
93792
94172
  for (;;) {
93793
94173
  if (job.cancelRequested) return;
93794
94174
  const batch = await this.deps.store.query.query({
@@ -93809,9 +94189,13 @@ var MediaRelocateEngine = class {
93809
94189
  job.filesMoved += updated;
93810
94190
  }
93811
94191
  }
93812
- async stamp(collection, key, data, locationId) {
94192
+ async stamp(lane, key, data, locationId) {
94193
+ if (lane.lane === "retrainFrames") {
94194
+ await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
94195
+ return;
94196
+ }
93813
94197
  await this.deps.store.set.mutate({
93814
- collection,
94198
+ collection: MEDIA_COLLECTION,
93815
94199
  key,
93816
94200
  value: {
93817
94201
  ...data,
@@ -102258,6 +102642,18 @@ function decodeCursorMap(raw) {
102258
102642
  function nonNegativeInt(value) {
102259
102643
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
102260
102644
  }
102645
+ function decodeScopeCursors(raw) {
102646
+ const out = {};
102647
+ if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
102648
+ if (typeof cursor === "string" && cursor.length > 0) {
102649
+ out[key] = cursor;
102650
+ continue;
102651
+ }
102652
+ const parsed = nonNegativeInt(cursor);
102653
+ if (parsed !== null) out[key] = parsed;
102654
+ }
102655
+ return out;
102656
+ }
102261
102657
  /** Read a stored row back into a cursor map, ignoring anything unreadable. */
102262
102658
  function decodeOrphanAuditCursors(value) {
102263
102659
  const empty = {
@@ -102269,7 +102665,7 @@ function decodeOrphanAuditCursors(value) {
102269
102665
  if (value === null || typeof value !== "object") return empty;
102270
102666
  const row = { ...value };
102271
102667
  return {
102272
- scopes: decodeCursorMap(row["scopes"]),
102668
+ scopes: decodeScopeCursors(row["scopes"]),
102273
102669
  faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
102274
102670
  clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
102275
102671
  updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
@@ -103842,7 +104238,9 @@ async function auditScope(scope, deps, limits, cursor) {
103842
104238
  };
103843
104239
  const deviceIds = /* @__PURE__ */ new Set();
103844
104240
  const graceFloor = deps.now() - limits.graceMs;
103845
- let offset = Math.max(0, cursor);
104241
+ let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
104242
+ let keysetCursor = typeof cursor === "string" ? cursor : null;
104243
+ let keyed = keysetCursor !== null;
103846
104244
  let stoppedBy = "exhausted";
103847
104245
  let collectionSize = null;
103848
104246
  try {
@@ -103859,9 +104257,16 @@ async function auditScope(scope, deps, limits, cursor) {
103859
104257
  stoppedBy = "row-ceiling";
103860
104258
  break;
103861
104259
  }
103862
- const rows = await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
104260
+ if (deps.stop?.() === true) {
104261
+ stoppedBy = "stopped";
104262
+ break;
104263
+ }
104264
+ const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
104265
+ keyed = keyedPage !== null;
104266
+ const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
103863
104267
  if (rows.length === 0) {
103864
104268
  offset = 0;
104269
+ keysetCursor = null;
103865
104270
  break;
103866
104271
  }
103867
104272
  const refs = rows.map((row) => scope.resolve(row));
@@ -103948,9 +104353,17 @@ async function auditScope(scope, deps, limits, cursor) {
103948
104353
  if (deps.breathe) await deps.breathe();
103949
104354
  if (ceilingHit) {
103950
104355
  stoppedBy = "reclaim-ceiling";
103951
- offset += rows.length - reclaimedInPage;
104356
+ if (!keyed) offset += rows.length - reclaimedInPage;
103952
104357
  break;
103953
104358
  }
104359
+ if (keyedPage !== null) {
104360
+ if (keyedPage.nextCursor === null) {
104361
+ keysetCursor = null;
104362
+ break;
104363
+ }
104364
+ keysetCursor = keyedPage.nextCursor;
104365
+ continue;
104366
+ }
103954
104367
  const advanced = rows.length - reclaimedInPage;
103955
104368
  if (advanced === 0 && rows.length < limits.pageSize) {
103956
104369
  offset = 0;
@@ -103966,7 +104379,7 @@ async function auditScope(scope, deps, limits, cursor) {
103966
104379
  collectionSize,
103967
104380
  coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
103968
104381
  stoppedBy,
103969
- nextCursor: offset
104382
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103970
104383
  } });
103971
104384
  const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
103972
104385
  if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
@@ -103993,7 +104406,7 @@ async function auditScope(scope, deps, limits, cursor) {
103993
104406
  unresolvable: tally.unresolvable,
103994
104407
  deviceIds: [...deviceIds],
103995
104408
  stoppedBy,
103996
- nextCursor: offset
104409
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103997
104410
  };
103998
104411
  }
103999
104412
  /**
@@ -104085,7 +104498,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
104085
104498
  //#endregion
104086
104499
  //#region src/pipeline-analytics/retention/orphan-reclaimers.ts
104087
104500
  var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
104088
- var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION, "pipeline-analytics:plates"]);
104501
+ var PLATES_COLLECTION = "pipeline-analytics:plates";
104502
+ var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
104089
104503
  /**
104090
104504
  * The `deleteRow` port: ONE row by its primary key `id` (what every
104091
104505
  * raw-delete collection above declares), answering whether a row went.
@@ -104159,6 +104573,13 @@ function buildOrphanReclaimer(ports) {
104159
104573
  rows: await ports.deleteTrackAlias(key) ? 1 : 0,
104160
104574
  bytes: null
104161
104575
  };
104576
+ if (collection === PLATES_COLLECTION) {
104577
+ const assert = trackIdAssertOf(data);
104578
+ return {
104579
+ rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
104580
+ bytes: null
104581
+ };
104582
+ }
104162
104583
  if (RAW_DELETE_COLLECTIONS.has(collection)) {
104163
104584
  const assert = trackIdAssertOf(data);
104164
104585
  if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
@@ -104180,6 +104601,87 @@ function buildOrphanReclaimer(ports) {
104180
104601
  };
104181
104602
  }
104182
104603
  //#endregion
104604
+ //#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
104605
+ /** Audited collection → its records-kind ledger. Empty without records. */
104606
+ function orphanAuditLedgers(wiring) {
104607
+ if (wiring.records === void 0) return /* @__PURE__ */ new Map();
104608
+ return new Map([
104609
+ [FACES_COLLECTION, wireFaceLedger(wiring)],
104610
+ [PLATES_COLLECTION$1, wirePlateLedger(wiring)],
104611
+ [TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
104612
+ ]);
104613
+ }
104614
+ /** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
104615
+ function keysetPager(ledgers) {
104616
+ return async (collection, cursor, limit) => {
104617
+ const ledger = ledgers.get(collection);
104618
+ if (ledger === void 0) return null;
104619
+ const page = await ledger.pageById(cursor, limit);
104620
+ return {
104621
+ rows: page.rows.map((row) => ({
104622
+ key: row.id,
104623
+ data: row.data
104624
+ })),
104625
+ nextCursor: page.nextCursor
104626
+ };
104627
+ };
104628
+ }
104629
+ /** The kind's count for a ledger-backed collection, `undefined` for any other. */
104630
+ async function ledgerSize(ledgers, collection) {
104631
+ const ledger = ledgers.get(collection);
104632
+ return ledger === void 0 ? void 0 : ledger.count();
104633
+ }
104634
+ /**
104635
+ * The plate reclaim through the plate ledger — records on a hub, the legacy
104636
+ * collection only without records. With `assert`, a row whose `trackId` is not
104637
+ * the dead track's is KEPT (logged), never deleted.
104638
+ */
104639
+ function plateDeleter(plates, logger) {
104640
+ return async (plateId, assert) => {
104641
+ const row = await plates.get(plateId);
104642
+ if (row === null) return false;
104643
+ if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
104644
+ const deviceId = row.data["deviceId"];
104645
+ logger.warn("orphan audit: plate row now names another track — kept", {
104646
+ ...typeof deviceId === "number" ? { tags: { deviceId } } : {},
104647
+ meta: {
104648
+ plateId,
104649
+ expectedTrackId: assert.trackId
104650
+ }
104651
+ });
104652
+ return false;
104653
+ }
104654
+ await plates.deleteKey(plateId);
104655
+ return true;
104656
+ };
104657
+ }
104658
+ function auditCollectionPorts(store, ledgers) {
104659
+ return {
104660
+ pageKeyset: keysetPager(ledgers),
104661
+ pageByOffset: async (collection, offset, limit) => {
104662
+ return (await store.query.query({
104663
+ collection,
104664
+ filter: {
104665
+ orderBy: {
104666
+ field: "id",
104667
+ direction: "asc"
104668
+ },
104669
+ limit,
104670
+ offset
104671
+ }
104672
+ })).map((row) => ({
104673
+ key: row.id,
104674
+ data: row.data
104675
+ }));
104676
+ },
104677
+ size: async (collection) => {
104678
+ const counted = await ledgerSize(ledgers, collection);
104679
+ if (counted !== void 0) return counted;
104680
+ return store.count.query({ collection });
104681
+ }
104682
+ };
104683
+ }
104684
+ //#endregion
104183
104685
  //#region src/pipeline-analytics/retention/retired-device-settings.ts
104184
104686
  /**
104185
104687
  * Every retired knob, with the sentence that explains it. The list is the
@@ -114937,6 +115439,7 @@ var customActions = {
114937
115439
  ...orphanAuditActions,
114938
115440
  ...mediaReclaimActions,
114939
115441
  ...locationDiscardActions,
115442
+ ...mediaForgetActions,
114940
115443
  ...photoEnrollActions,
114941
115444
  ...faceRescoreActions,
114942
115445
  ...stillnessCalibrationActions
@@ -116727,6 +117230,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116727
117230
  orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
116728
117231
  /** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
116729
117232
  discardJob = null;
117233
+ /** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
117234
+ mediaForgetDeps = null;
116730
117235
  /**
116731
117236
  * The CLIP indexes' walks (D649), one resume cursor per index, kept in their
116732
117237
  * OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
@@ -116935,10 +117440,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116935
117440
  const isHub = ownNodeId === "hub";
116936
117441
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
116937
117442
  if (isHub) this.reconcileTapePinsInBackground();
117443
+ this.mediaForgetDeps = {
117444
+ listLocations: async () => api.storage.listLocations.query({}),
117445
+ localNodeId: ownNodeId,
117446
+ store: api.settingsStore,
117447
+ logger: logger.child("MediaForget")
117448
+ };
116938
117449
  this.discardJob = createDiscardJob({
116939
117450
  listLocations: async () => api.storage.listLocations.query({}),
116940
117451
  localNodeId: ownNodeId,
116941
117452
  store: api.settingsStore,
117453
+ retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
116942
117454
  deleteTrackEvents: async (trackId) => {
116943
117455
  await this.eventStore?.deleteByTrack(trackId);
116944
117456
  },
@@ -116959,6 +117471,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
116959
117471
  ...orphanAuditActions,
116960
117472
  ...mediaReclaimActions,
116961
117473
  ...locationDiscardActions,
117474
+ ...mediaForgetActions,
116962
117475
  ...photoEnrollActions,
116963
117476
  ...faceRescoreActions,
116964
117477
  ...stillnessCalibrationActions
@@ -117554,6 +118067,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
117554
118067
  });
117555
118068
  this.mediaRelocate = new MediaRelocateEngine({
117556
118069
  store: api.settingsStore,
118070
+ retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
117557
118071
  storage,
117558
118072
  logger: logger.child("MediaRelocate"),
117559
118073
  now: () => Date.now(),
@@ -125296,36 +125810,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125296
125810
  const trackStore = this.trackStore;
125297
125811
  const eventStore = this.eventStore;
125298
125812
  const mediaStore = this.mediaStore;
125299
- if (!trackStore || !eventStore || !mediaStore) {
125813
+ const settingsStore = this.settingsStore;
125814
+ if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
125300
125815
  if (!alreadyHeld) this.orphanAuditRunning = false;
125301
125816
  return null;
125302
125817
  }
125303
125818
  try {
125304
125819
  const limits = await this.orphanAuditLimits(overrides);
125820
+ const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
125821
+ const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
125822
+ const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
125305
125823
  const deps = {
125824
+ pageKeyset: collectionPorts.pageKeyset,
125825
+ stop: () => this.shuttingDown,
125306
125826
  page: async (collection, shard, offset, limit) => {
125307
125827
  if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
125308
125828
  key: String(row.mediaId),
125309
125829
  data: row.data
125310
125830
  }));
125311
- return (await this.settingsStore?.query.query({
125312
- collection,
125313
- filter: {
125314
- orderBy: {
125315
- field: "id",
125316
- direction: "asc"
125317
- },
125318
- limit,
125319
- offset
125320
- }
125321
- }) ?? []).map((row) => ({
125322
- key: row.id,
125323
- data: row.data
125324
- }));
125831
+ return collectionPorts.pageByOffset(collection, offset, limit);
125325
125832
  },
125326
125833
  size: async (collection, shard) => {
125327
125834
  if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
125328
- return await this.settingsStore?.count.query({ collection }) ?? null;
125835
+ return collectionPorts.size(collection);
125329
125836
  },
125330
125837
  liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
125331
125838
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
@@ -125348,6 +125855,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
125348
125855
  if (aliasStore === null) throw new Error("track alias store not bound");
125349
125856
  return await aliasStore.deleteAlias(aliasId);
125350
125857
  },
125858
+ deletePlate: plateDeleter(plateLedger, this.ctx.logger),
125351
125859
  deleteRow: settingsRowDeleterOf(() => this.settingsStore),
125352
125860
  logger: this.ctx.logger
125353
125861
  });
@@ -126769,7 +127277,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
126769
127277
  jobId: null
126770
127278
  },
126771
127279
  "storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
126772
- "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
127280
+ "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
127281
+ "storage.forgetMissingMedia": async (input) => {
127282
+ const deps = this.mediaForgetDeps;
127283
+ if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
127284
+ return forgetMissingMediaRows(deps, input);
127285
+ }
126773
127286
  };
126774
127287
  }
126775
127288
  /**