@camstack/addon-post-analysis 1.2.613 → 1.2.614

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.
@@ -9491,7 +9491,9 @@ var MigrationMarkerBodySchema = object({
9491
9491
  });
9492
9492
  /** The orphan audit's resume position: one row, the whole map in one value on purpose. */
9493
9493
  var SweepCursorBodySchema = object({
9494
- scopes: record(string(), number()),
9494
+ /** A number is an offset (a settings-store scope); a string is the records
9495
+ * engine's opaque keyset cursor (a scope walked through its kind). */
9496
+ scopes: record(string(), union([number(), string()])),
9495
9497
  /** Absent on a row written before the walk existed — absent means "from the top". */
9496
9498
  faceVectorCursor: number().optional(),
9497
9499
  /** One cursor per CLIP index, keyed by index name. Absent on an older row. */
@@ -9965,7 +9967,7 @@ var pipelineAnalyticsRecordKinds = defineRecordKinds({
9965
9967
  //#endregion
9966
9968
  //#region src/pipeline-analytics/scene/scene-config-ledger.ts
9967
9969
  /** The engine's page cap for one query; a reseed pages by it, bounded by the ledger limit. */
9968
- var PAGE_SIZE$1 = 1e3;
9970
+ var PAGE_SIZE$2 = 1e3;
9969
9971
  /**
9970
9972
  * A row whose monitor JSON no longer validates is SKIPPED, not repaired: a
9971
9973
  * half-parsed scene would report a verdict about a region it cannot describe.
@@ -10003,7 +10005,7 @@ var SceneConfigLedger = class {
10003
10005
  let seen = 0;
10004
10006
  let cursor;
10005
10007
  for (;;) {
10006
- const limit = Math.min(PAGE_SIZE$1, this.loadLimit - seen);
10008
+ const limit = Math.min(PAGE_SIZE$2, this.loadLimit - seen);
10007
10009
  if (limit <= 0) break;
10008
10010
  const page = await this.kind.query({
10009
10011
  limit,
@@ -12050,6 +12052,28 @@ var EventLedgerCollection = class EventLedgerCollection {
12050
12052
  if (mapped.limit !== void 0 && out.length > mapped.limit) return out.slice(0, mapped.limit);
12051
12053
  return out;
12052
12054
  }
12055
+ /**
12056
+ * One page of the whole kind in `id` order, resumed from the records
12057
+ * engine's OPAQUE keyset cursor (`null` = from the top). The answer's
12058
+ * `nextCursor` is `null` at the end. A keyset cursor names the last row
12059
+ * read, so deleting rows behind it never shifts what comes next — the
12060
+ * reason a bulk walk that reclaims as it goes needs this, not an offset.
12061
+ *
12062
+ * Records only: without `ctx.records` the caller pages the settings store
12063
+ * itself, and asking here throws rather than inventing a cursor.
12064
+ */
12065
+ async pageById(cursor, limit) {
12066
+ if (this.kind === null) throw new Error(`event ledger ${this.collection}: pageById needs the records kind`);
12067
+ const page = await this.kind.query({
12068
+ order: "id",
12069
+ limit,
12070
+ ...cursor === null ? {} : { cursor }
12071
+ });
12072
+ return {
12073
+ rows: page.records.map((row) => recordsRowToLegacy(row, this.legacyAtField)),
12074
+ nextCursor: page.nextCursor
12075
+ };
12076
+ }
12053
12077
  async getManyByIds(ids) {
12054
12078
  if (ids.length === 0) return [];
12055
12079
  if (this.kind === null) return (await this.store.query.query({
@@ -13176,11 +13200,35 @@ var FaceStore = class {
13176
13200
  * carries no `embeddingModelId`, and those rows were all `arcface-r100`, so
13177
13201
  * that is what they are stamped with — a KNOWN historical fact, not a
13178
13202
  * fallback guess. A row that names some other model keeps its own.
13203
+ *
13204
+ * On a records hub the rows are paged through the faces LEDGER: the legacy
13205
+ * table is dropped there, and the adoption copied each row's body whole —
13206
+ * an `embedding` the pass had not drained yet included — so a records-era
13207
+ * row can still need converting (the same shape as `IdentityStore`).
13179
13208
  */
13180
13209
  conversionSpec() {
13210
+ const ledger = this.ledger;
13211
+ const recordsPaging = ledger.usesRecords ? {
13212
+ pageRows: (limit, offset) => ledger.query({
13213
+ orderBy: {
13214
+ field: "id",
13215
+ direction: "asc"
13216
+ },
13217
+ limit,
13218
+ offset
13219
+ }, [
13220
+ "deviceId",
13221
+ "timestamp",
13222
+ "trackId",
13223
+ "embeddingModelId",
13224
+ "embedding"
13225
+ ]),
13226
+ drainEmbedding: (id) => ledger.update(id, { embedding: [] })
13227
+ } : {};
13181
13228
  return {
13182
13229
  collection: FACES_COLLECTION,
13183
13230
  vectors: this.vectors,
13231
+ ...recordsPaging,
13184
13232
  write: (id, embedding, data) => {
13185
13233
  const deviceId = data["deviceId"];
13186
13234
  const timestamp = data["timestamp"];
@@ -67432,7 +67480,7 @@ async function settleReplaced(deps, sample, replacement, dryRun) {
67432
67480
  });
67433
67481
  return "old-removal-retried";
67434
67482
  }
67435
- function tally$1(samples, outcomes, dryRun) {
67483
+ function tally$2(samples, outcomes, dryRun) {
67436
67484
  const count = (kind) => outcomes.filter((o) => o.outcome === kind).length;
67437
67485
  const refusals = [];
67438
67486
  for (const o of outcomes) if (o.outcome === "refused" && o.reason !== void 0) refusals.push({
@@ -67504,7 +67552,7 @@ async function reenrolPhotoSamples(deps, options) {
67504
67552
  outcomes.push(outcomeOf(sample, verdict));
67505
67553
  await deps.pause(pacingMs);
67506
67554
  }
67507
- const result = tally$1(samples, outcomes, dryRun);
67555
+ const result = tally$2(samples, outcomes, dryRun);
67508
67556
  if (!dryRun && (result.reenrolled > 0 || result.oldRemovalRetried > 0)) await deps.refreshGallery();
67509
67557
  const { outcomes: _perSample, ...summary } = result;
67510
67558
  deps.logger.info("face photo re-enrol finished", { meta: { ...summary } });
@@ -78919,24 +78967,11 @@ function createDiscardJob(deps) {
78919
78967
  });
78920
78968
  await sleep(0);
78921
78969
  }
78922
- for (;;) {
78970
+ const retrainIds = (await deps.retrainFrameLedger.query({})).filter((row) => row.data["locationId"] === locationId).map((row) => row.id);
78971
+ for (let i = 0; i < retrainIds.length; i += MEDIA_PAGE) {
78923
78972
  if (cancelRequested) return owners;
78924
- const rows = await deps.store.query.query({
78925
- collection: RETRAIN_FRAMES_COLLECTION,
78926
- columns: ["id"],
78927
- filter: {
78928
- where: { locationId },
78929
- limit: MEDIA_PAGE
78930
- }
78931
- });
78932
- if (rows.length === 0) break;
78933
- for (const row of rows) {
78934
- await deps.store.delete.mutate({
78935
- collection: RETRAIN_FRAMES_COLLECTION,
78936
- key: row.id
78937
- });
78938
- deleted += 1;
78939
- }
78973
+ const page = retrainIds.slice(i, i + MEDIA_PAGE);
78974
+ deleted += await deps.retrainFrameLedger.deleteByIds(page);
78940
78975
  report({
78941
78976
  dbRowsDeleted: deleted,
78942
78977
  detail: `deleted ${deleted} media/retrain rows`
@@ -78988,6 +79023,382 @@ function createDiscardJob(deps) {
78988
79023
  };
78989
79024
  }
78990
79025
  //#endregion
79026
+ //#region src/pipeline-analytics/retention/media-forget-actions.ts
79027
+ /**
79028
+ * The operator door for the NARROW media-row forget (`media-row-forget.ts`).
79029
+ *
79030
+ * It forgets `pipeline-analytics:media` rows stamped with ONE location whose
79031
+ * file is provably gone, and nothing else: no event, track, object-event,
79032
+ * face, plate or identity row is read for deletion, ever. Contrast
79033
+ * `storage.discardLocation`, which wipes the disk AND cascades owners.
79034
+ *
79035
+ * `dryRun` defaults to `true`: an unstated intent destroys nothing. Applying
79036
+ * also requires `expectAbsent` — the `absent` count a dry run just reported —
79037
+ * so the forget only ever deletes a population a human has already read.
79038
+ */
79039
+ /** Hard ceiling on rows one call examines — the call is synchronous. */
79040
+ var MEDIA_FORGET_MAX_ROWS = 5e4;
79041
+ var MediaForgetInputSchema = object({
79042
+ locationId: string().min(1),
79043
+ /** Restrict to these cameras. Omitted = every row stamped with the location. */
79044
+ deviceIds: array(number().int()).min(1).optional(),
79045
+ dryRun: boolean().default(true),
79046
+ /**
79047
+ * Required when `dryRun` is false: the `totals.absent` of the dry run. A
79048
+ * different count at apply time refuses the whole call before any delete.
79049
+ */
79050
+ expectAbsent: number().int().nonnegative().optional(),
79051
+ /** Rows examined in this call. Default and max {@link MEDIA_FORGET_MAX_ROWS}. */
79052
+ maxRows: number().int().min(1).max(MEDIA_FORGET_MAX_ROWS).optional(),
79053
+ /** Detailed rows returned per verdict. Default 20. */
79054
+ sampleSize: number().int().min(0).max(500).optional()
79055
+ });
79056
+ var VerdictCountsSchema = object({
79057
+ rows: number(),
79058
+ /** File answered ENOENT — the only verdict that may be forgotten. */
79059
+ absent: number(),
79060
+ /** File is there: relocation is incomplete. Never forgotten. */
79061
+ present: number(),
79062
+ /** The stat failed with any errno other than ENOENT. Never forgotten. */
79063
+ error: number(),
79064
+ /** No path, a path escaping the root, or a retention-exempt owner. Never forgotten. */
79065
+ kept: number(),
79066
+ /** Rows actually deleted (0 on a dry run). */
79067
+ deleted: number()
79068
+ });
79069
+ var MediaForgetSampleSchema = object({
79070
+ mediaId: number(),
79071
+ deviceId: number(),
79072
+ kind: string(),
79073
+ ownerKind: string(),
79074
+ ownerId: string(),
79075
+ path: string(),
79076
+ /** The errno (error) or the reason (kept); absent otherwise. */
79077
+ reason: string().optional()
79078
+ });
79079
+ var mediaForgetActions = defineCustomActions({ "storage.forgetMissingMedia": customAction(MediaForgetInputSchema, object({
79080
+ locationId: string(),
79081
+ dryRun: boolean(),
79082
+ root: string(),
79083
+ /** Rows of this location that were not examined because `maxRows` was hit. */
79084
+ truncated: boolean(),
79085
+ totals: VerdictCountsSchema,
79086
+ byDevice: array(VerdictCountsSchema.extend({ deviceId: number() })),
79087
+ byKind: array(VerdictCountsSchema.extend({ kind: string() })),
79088
+ /** Every id the call deletes (dry run: would delete). The exact list. */
79089
+ absentIds: array(number()),
79090
+ samples: object({
79091
+ absent: array(MediaForgetSampleSchema),
79092
+ present: array(MediaForgetSampleSchema),
79093
+ error: array(MediaForgetSampleSchema),
79094
+ kept: array(MediaForgetSampleSchema)
79095
+ }),
79096
+ /** Absent at classification, no longer ENOENT at delete time: kept. */
79097
+ changedBeforeDelete: number()
79098
+ }), {
79099
+ kind: "mutation",
79100
+ auth: "admin"
79101
+ }) });
79102
+ //#endregion
79103
+ //#region src/pipeline-analytics/retention/media-row-forget.ts
79104
+ /**
79105
+ * The NARROW media-row forget: delete `pipeline-analytics:media` rows stamped
79106
+ * with ONE storage location whose file is provably gone — and nothing else.
79107
+ *
79108
+ * Why it exists: a location whose bytes were lost keeps rows naming it, and a
79109
+ * cascading cleanup is not an acceptable way to clear them. `reconcileFromDisk`
79110
+ * once cascaded ~132 k events away from a read it did not double-check. Here:
79111
+ *
79112
+ * - **Only media rows.** No event, track, object-event, face, plate or
79113
+ * identity row is deleted. A parent keeps existing with that media kind
79114
+ * missing — which is exactly what it already serves (`getBytesById` answers
79115
+ * `null` for a missing row as it does for a missing blob).
79116
+ * - **Only `ENOENT` is absence (D49).** Each row's file is `stat`ed under the
79117
+ * location's own root on THIS node. Any other errno keeps the row and is
79118
+ * reported; a present file keeps the row and is reported (relocation is
79119
+ * incomplete). A row with no path, a path that escapes the root, or a
79120
+ * retention-exempt owner (the enrolled gallery) is kept.
79121
+ * - **Only rows stamped with the id.** `locationId IS NULL` rows mean
79122
+ * "wherever eventMedia points now" and are never selected.
79123
+ * - **Only a location nothing writes to.** An active (writable) location is
79124
+ * refused: new rows could land while the walk runs.
79125
+ * - **Dry run first, then the same count.** Applying requires
79126
+ * `expectAbsent`; the whole population is classified before any delete,
79127
+ * and a different absent count refuses the call with nothing written.
79128
+ * Each id is re-`stat`ed right before its batch is deleted, and the delete
79129
+ * predicate repeats `locationId`, so a row repointed in between survives.
79130
+ *
79131
+ * Occupancy needs no separate bookkeeping: `storage-occupancy` sums
79132
+ * `sizeBytes WHERE locationId = ?` on demand, so the row IS the accounting.
79133
+ */
79134
+ var PAGE_SIZE$1 = 200;
79135
+ var DELETE_BATCH = 200;
79136
+ var DEFAULT_SAMPLE_SIZE = 20;
79137
+ function errnoOf(err) {
79138
+ if (err instanceof Error && "code" in err && typeof err.code === "string") return err.code;
79139
+ return err instanceof Error ? err.message : String(err);
79140
+ }
79141
+ function emptyCounts() {
79142
+ return {
79143
+ rows: 0,
79144
+ absent: 0,
79145
+ present: 0,
79146
+ error: 0,
79147
+ kept: 0,
79148
+ deleted: 0
79149
+ };
79150
+ }
79151
+ function tally$1(counts, verdict) {
79152
+ const rows = counts.rows + 1;
79153
+ switch (verdict) {
79154
+ case "absent": return {
79155
+ ...counts,
79156
+ rows,
79157
+ absent: counts.absent + 1
79158
+ };
79159
+ case "present": return {
79160
+ ...counts,
79161
+ rows,
79162
+ present: counts.present + 1
79163
+ };
79164
+ case "error": return {
79165
+ ...counts,
79166
+ rows,
79167
+ error: counts.error + 1
79168
+ };
79169
+ case "kept": return {
79170
+ ...counts,
79171
+ rows,
79172
+ kept: counts.kept + 1
79173
+ };
79174
+ }
79175
+ }
79176
+ /** The file's absolute path under `root`, or `null` when it would escape it. */
79177
+ function resolveUnder(root, relativePath) {
79178
+ const base = path.resolve(root);
79179
+ const full = path.resolve(base, relativePath);
79180
+ return full.startsWith(base + path.sep) ? full : null;
79181
+ }
79182
+ function toSample(id, data) {
79183
+ return {
79184
+ mediaId: Number(id),
79185
+ deviceId: Number(data["deviceId"] ?? 0),
79186
+ kind: String(data["kind"] ?? ""),
79187
+ ownerKind: String(data["ownerKind"] ?? ""),
79188
+ ownerId: String(data["ownerId"] ?? ""),
79189
+ path: String(data["path"] ?? "")
79190
+ };
79191
+ }
79192
+ async function probe(stat, absolutePath) {
79193
+ try {
79194
+ await stat(absolutePath);
79195
+ return { verdict: "present" };
79196
+ } catch (err) {
79197
+ const code = errnoOf(err);
79198
+ return code === "ENOENT" ? { verdict: "absent" } : {
79199
+ verdict: "error",
79200
+ reason: code
79201
+ };
79202
+ }
79203
+ }
79204
+ async function classify(stat, root, id, data) {
79205
+ const sample = toSample(id, data);
79206
+ const ownerKind = sample.ownerKind;
79207
+ if (isMediaOwnerKind(ownerKind) && isRetentionExemptOwnerKind(ownerKind)) return {
79208
+ key: id,
79209
+ sample: {
79210
+ ...sample,
79211
+ reason: "retention-exempt owner"
79212
+ },
79213
+ verdict: "kept",
79214
+ absolutePath: null
79215
+ };
79216
+ if (sample.path.length === 0) return {
79217
+ key: id,
79218
+ sample: {
79219
+ ...sample,
79220
+ reason: "no path"
79221
+ },
79222
+ verdict: "kept",
79223
+ absolutePath: null
79224
+ };
79225
+ const absolutePath = resolveUnder(root, sample.path);
79226
+ if (absolutePath === null) return {
79227
+ key: id,
79228
+ sample: {
79229
+ ...sample,
79230
+ reason: "path escapes the root"
79231
+ },
79232
+ verdict: "kept",
79233
+ absolutePath
79234
+ };
79235
+ const result = await probe(stat, absolutePath);
79236
+ return {
79237
+ key: id,
79238
+ sample: result.reason === void 0 ? sample : {
79239
+ ...sample,
79240
+ reason: result.reason
79241
+ },
79242
+ verdict: result.verdict,
79243
+ absolutePath
79244
+ };
79245
+ }
79246
+ function resolveTarget(locations, locationId, localNodeId) {
79247
+ const location = locations.find((row) => row.id === locationId);
79248
+ if (!location) throw new Error(`forgetMissingMedia refused: unknown location ${locationId}`);
79249
+ if (location.type !== "eventMedia") throw new Error(`forgetMissingMedia refused: ${locationId} is ${location.type}, pipeline-analytics owns eventMedia rows only`);
79250
+ if (mayWriteToLocation(location)) throw new Error(`forgetMissingMedia refused: ${locationId} is still a write target — set it to drain, readonly or disabled first`);
79251
+ const nodeId = location.nodeId === void 0 || location.nodeId === "" ? "hub" : location.nodeId;
79252
+ if (nodeId !== localNodeId) throw new Error(`forgetMissingMedia refused: ${locationId} lives on node ${nodeId}, this process is ${localNodeId}`);
79253
+ return {
79254
+ location,
79255
+ root: locationBasePath(location)
79256
+ };
79257
+ }
79258
+ /**
79259
+ * Classify every row stamped with `input.locationId` (bounded by `maxRows`),
79260
+ * and — only when `dryRun` is false and the absent count equals
79261
+ * `expectAbsent` — delete the absent ones, per camera, in batches.
79262
+ */
79263
+ async function forgetMissingMediaRows(deps, rawInput) {
79264
+ const input = MediaForgetInputSchema.parse(rawInput);
79265
+ if (!input.dryRun && input.expectAbsent === void 0) throw new Error("forgetMissingMedia refused: an apply needs expectAbsent (the dry run’s totals.absent)");
79266
+ const stat = deps.stat ?? ((p) => promises.stat(p));
79267
+ const pace = deps.pace ?? (() => new Promise((resolve) => setImmediate(resolve)));
79268
+ const maxRows = input.maxRows ?? MEDIA_FORGET_MAX_ROWS;
79269
+ const sampleSize = input.sampleSize ?? DEFAULT_SAMPLE_SIZE;
79270
+ const { root } = resolveTarget(await deps.listLocations(), input.locationId, deps.localNodeId);
79271
+ const classified = [];
79272
+ let cursor = 0;
79273
+ let truncated = false;
79274
+ for (;;) {
79275
+ const rows = await deps.store.query.query({
79276
+ collection: MEDIA_COLLECTION,
79277
+ columns: [
79278
+ "id",
79279
+ "deviceId",
79280
+ "ownerKind",
79281
+ "ownerId",
79282
+ "kind",
79283
+ "path"
79284
+ ],
79285
+ filter: {
79286
+ where: { locationId: input.locationId },
79287
+ ...input.deviceIds ? { whereIn: { deviceId: [...input.deviceIds] } } : {},
79288
+ whereBetween: { id: [cursor + 1, Number.MAX_SAFE_INTEGER] },
79289
+ orderBy: {
79290
+ field: "id",
79291
+ direction: "asc"
79292
+ },
79293
+ limit: PAGE_SIZE$1
79294
+ }
79295
+ });
79296
+ if (rows.length === 0) break;
79297
+ for (const row of rows) {
79298
+ if (classified.length >= maxRows) {
79299
+ truncated = true;
79300
+ break;
79301
+ }
79302
+ classified.push(await classify(stat, root, row.id, row.data));
79303
+ cursor = Math.max(cursor, Number(row.id));
79304
+ }
79305
+ if (truncated || rows.length < PAGE_SIZE$1) break;
79306
+ await pace();
79307
+ }
79308
+ const absentRows = classified.filter((c) => c.verdict === "absent");
79309
+ const deletedIds = /* @__PURE__ */ new Set();
79310
+ let changedBeforeDelete = 0;
79311
+ if (!input.dryRun) {
79312
+ 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`);
79313
+ const byDevice = /* @__PURE__ */ new Map();
79314
+ for (const row of absentRows) byDevice.set(row.sample.deviceId, [...byDevice.get(row.sample.deviceId) ?? [], row]);
79315
+ for (const [deviceId, rows] of byDevice) for (let at = 0; at < rows.length; at += DELETE_BATCH) {
79316
+ const batch = [];
79317
+ 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);
79318
+ else changedBeforeDelete += 1;
79319
+ if (batch.length === 0) continue;
79320
+ const { deleted } = await deps.store.deleteWhere.mutate({
79321
+ collection: MEDIA_COLLECTION,
79322
+ filter: {
79323
+ where: {
79324
+ locationId: input.locationId,
79325
+ deviceId
79326
+ },
79327
+ whereIn: { id: batch.map((row) => row.key) }
79328
+ }
79329
+ });
79330
+ for (const row of batch) deletedIds.add(row.sample.mediaId);
79331
+ deps.logger.info("forgetMissingMedia: forgot media rows whose file is gone", {
79332
+ tags: { deviceId },
79333
+ meta: {
79334
+ locationId: input.locationId,
79335
+ batch: batch.length,
79336
+ deleted,
79337
+ firstId: batch[0]?.sample.mediaId,
79338
+ lastId: batch[batch.length - 1]?.sample.mediaId
79339
+ }
79340
+ });
79341
+ if (deleted !== batch.length) deps.logger.warn("forgetMissingMedia: a batch deleted fewer rows than it named", {
79342
+ tags: { deviceId },
79343
+ meta: {
79344
+ locationId: input.locationId,
79345
+ batch: batch.length,
79346
+ deleted
79347
+ }
79348
+ });
79349
+ await pace();
79350
+ }
79351
+ }
79352
+ let totals = emptyCounts();
79353
+ const perDevice = /* @__PURE__ */ new Map();
79354
+ const perKind = /* @__PURE__ */ new Map();
79355
+ for (const row of classified) {
79356
+ const isDeleted = deletedIds.has(row.sample.mediaId);
79357
+ const bump = (c) => {
79358
+ const next = tally$1(c, row.verdict);
79359
+ return isDeleted ? {
79360
+ ...next,
79361
+ deleted: next.deleted + 1
79362
+ } : next;
79363
+ };
79364
+ totals = bump(totals);
79365
+ perDevice.set(row.sample.deviceId, bump(perDevice.get(row.sample.deviceId) ?? emptyCounts()));
79366
+ perKind.set(row.sample.kind, bump(perKind.get(row.sample.kind) ?? emptyCounts()));
79367
+ }
79368
+ const samplesOf = (verdict) => classified.filter((c) => c.verdict === verdict).slice(0, sampleSize).map((c) => c.sample);
79369
+ for (const [deviceId, counts] of perDevice) deps.logger.info("forgetMissingMedia: per-camera verdict", {
79370
+ tags: { deviceId },
79371
+ meta: {
79372
+ locationId: input.locationId,
79373
+ dryRun: input.dryRun,
79374
+ ...counts
79375
+ }
79376
+ });
79377
+ return {
79378
+ locationId: input.locationId,
79379
+ dryRun: input.dryRun,
79380
+ root,
79381
+ truncated,
79382
+ totals,
79383
+ byDevice: [...perDevice].map(([deviceId, c]) => ({
79384
+ deviceId,
79385
+ ...c
79386
+ })),
79387
+ byKind: [...perKind].map(([kind, c]) => ({
79388
+ kind,
79389
+ ...c
79390
+ })),
79391
+ absentIds: absentRows.map((r) => r.sample.mediaId),
79392
+ samples: {
79393
+ absent: samplesOf("absent"),
79394
+ present: samplesOf("present"),
79395
+ error: samplesOf("error"),
79396
+ kept: samplesOf("kept")
79397
+ },
79398
+ changedBeforeDelete
79399
+ };
79400
+ }
79401
+ //#endregion
78991
79402
  //#region src/pipeline-analytics/retention/media-reclaim.ts
78992
79403
  var MEDIA_RECLAIM_SCOPES = ["motion-still", "track-filmstrip"];
78993
79404
  var DEFAULT_MEDIA_RECLAIM_LIMITS = {
@@ -93265,17 +93676,8 @@ function rowIsGallery(data) {
93265
93676
  * D295 calls "a guard nobody believes".
93266
93677
  */
93267
93678
  function collectionsFor(mode) {
93268
- if (mode === "gallery") return [{
93269
- name: MEDIA_COLLECTION,
93270
- order: "timestamp"
93271
- }];
93272
- return [{
93273
- name: MEDIA_COLLECTION,
93274
- order: "timestamp"
93275
- }, {
93276
- name: RETRAIN_FRAMES_COLLECTION,
93277
- order: "copiedAt"
93278
- }];
93679
+ if (mode === "gallery") return [{ lane: "media" }];
93680
+ return [{ lane: "media" }, { lane: "retrainFrames" }];
93279
93681
  }
93280
93682
  /**
93281
93683
  * Does this pass act on this row at all?
@@ -93360,7 +93762,7 @@ var MediaRelocateEngine = class {
93360
93762
  async countUnstamped() {
93361
93763
  try {
93362
93764
  const media = await this.countUnstampedIn(MEDIA_COLLECTION);
93363
- const retrainFrames = await this.countUnstampedIn(RETRAIN_FRAMES_COLLECTION);
93765
+ const retrainFrames = await this.countUnstampedRetrainFrames();
93364
93766
  return {
93365
93767
  media,
93366
93768
  retrainFrames,
@@ -93412,6 +93814,28 @@ var MediaRelocateEngine = class {
93412
93814
  };
93413
93815
  }
93414
93816
  }
93817
+ /**
93818
+ * The retrain lane's unstamped population. The ledger's records `where` is
93819
+ * equality-only (a `null` predicate is dropped, which would count the whole
93820
+ * dataset), so the rows are read and judged by {@link rowIsUnstamped} — the
93821
+ * dataset is operator-selected and small.
93822
+ */
93823
+ async countUnstampedRetrainFrames() {
93824
+ const rows = (await this.deps.retrainFrameLedger.query({})).filter((row) => rowIsUnstamped(row.data)).length;
93825
+ return {
93826
+ present: rows > 0,
93827
+ rows
93828
+ };
93829
+ }
93830
+ /** Every row of one lane, oldest-first. */
93831
+ rowsOf(collection) {
93832
+ return collection.lane === "media" ? this.pageRows(MEDIA_COLLECTION, "timestamp") : this.retrainFrameRows();
93833
+ }
93834
+ /** The retrain-frame ledger, read whole and yielded oldest copy first. */
93835
+ async *retrainFrameRows() {
93836
+ const ordered = (await this.deps.retrainFrameLedger.query({})).toSorted((a, b) => Number(a.data["copiedAt"] ?? 0) - Number(b.data["copiedAt"] ?? 0));
93837
+ for (const row of ordered) yield row;
93838
+ }
93415
93839
  start(input) {
93416
93840
  for (const j of this.jobs.values()) if (j.state === "running") throw new Error(`a media relocation is already running (${j.jobId})`);
93417
93841
  this.cancelCensuses("a relocate pass started");
@@ -93515,22 +93939,22 @@ var MediaRelocateEngine = class {
93515
93939
  let sealable = 0;
93516
93940
  for (const collection of collectionsFor(mode)) {
93517
93941
  if (stop()) return null;
93518
- sealable += await this.deps.store.count.query({
93519
- collection: collection.name,
93942
+ sealable += collection.lane === "media" ? await this.deps.store.count.query({
93943
+ collection: MEDIA_COLLECTION,
93520
93944
  filter: { where: { [LOCATION_ID_FIELD]: null } }
93521
- });
93945
+ }) : (await this.countUnstampedRetrainFrames()).rows ?? 0;
93522
93946
  }
93523
93947
  return { rows: sealable };
93524
93948
  }
93525
93949
  let rows = 0;
93526
93950
  for (const collection of collectionsFor(mode)) {
93527
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93528
- for await (const row of this.pageRows(collection.name, collection.order)) {
93951
+ const isRetrain = collection.lane === "retrainFrames";
93952
+ for await (const row of this.rowsOf(collection)) {
93529
93953
  if (stop()) {
93530
93954
  this.deps.logger.warn("media relocate: census stopped before it finished — reporting the residue as unknown, not as empty", { meta: {
93531
93955
  mode,
93532
93956
  toLocationId,
93533
- collection: collection.name
93957
+ collection: collection.lane
93534
93958
  } });
93535
93959
  return null;
93536
93960
  }
@@ -93538,7 +93962,7 @@ var MediaRelocateEngine = class {
93538
93962
  this.deps.logger.warn("media relocate: census exceeded its budget — this collection is larger than a walk can answer", { meta: {
93539
93963
  mode,
93540
93964
  toLocationId,
93541
- collection: collection.name,
93965
+ collection: collection.lane,
93542
93966
  rowsVisited: budget.rowsVisited,
93543
93967
  rowBudget: MEDIA_CENSUS_ROW_BUDGET,
93544
93968
  deadlineMs: MEDIA_CENSUS_DEADLINE_MS
@@ -93574,15 +93998,15 @@ var MediaRelocateEngine = class {
93574
93998
  if (job.mode === "seal") {
93575
93999
  for (const collection of collectionsFor(job.mode)) {
93576
94000
  if (job.cancelRequested) break;
93577
- await this.sealCollection(collection.name, job);
94001
+ await this.sealCollection(collection, job);
93578
94002
  }
93579
94003
  job.state = job.cancelRequested ? "cancelled" : "done";
93580
94004
  return;
93581
94005
  }
93582
94006
  for (const collection of collectionsFor(job.mode)) {
93583
94007
  if (job.cancelRequested) break;
93584
- const isRetrain = collection.name === RETRAIN_FRAMES_COLLECTION;
93585
- for await (const row of this.pageRows(collection.name, collection.order)) {
94008
+ const isRetrain = collection.lane === "retrainFrames";
94009
+ for await (const row of this.rowsOf(collection)) {
93586
94010
  if (job.cancelRequested) break;
93587
94011
  const data = row.data;
93588
94012
  if (!selectsRow(job.mode, job.toLocationId, data, isRetrain, job.fromLocationId)) continue;
@@ -93594,7 +94018,7 @@ var MediaRelocateEngine = class {
93594
94018
  }
93595
94019
  try {
93596
94020
  if (await this.samePhysicalLocation(from, job.toLocationId)) {
93597
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94021
+ await this.stamp(collection, row.id, data, job.toLocationId);
93598
94022
  continue;
93599
94023
  }
93600
94024
  const bytes = await this.deps.storage.read({
@@ -93610,7 +94034,7 @@ var MediaRelocateEngine = class {
93610
94034
  location: job.toLocationId,
93611
94035
  relativePath
93612
94036
  })).length !== bytes.length) throw new Error(`relocated media size mismatch for ${relativePath}`);
93613
- await this.stamp(collection.name, row.id, data, job.toLocationId);
94037
+ await this.stamp(collection, row.id, data, job.toLocationId);
93614
94038
  await this.deps.storage.delete({
93615
94039
  location: from,
93616
94040
  relativePath
@@ -93621,7 +94045,7 @@ var MediaRelocateEngine = class {
93621
94045
  } catch (err) {
93622
94046
  failedRows++;
93623
94047
  this.deps.logger.debug("media relocate row failed", { meta: {
93624
- collection: collection.name,
94048
+ collection: collection.lane,
93625
94049
  key: row.id,
93626
94050
  error: String(err)
93627
94051
  } });
@@ -93694,7 +94118,17 @@ var MediaRelocateEngine = class {
93694
94118
  * reports a completed seal having stamped nothing. Zero unstamped rows is the
93695
94119
  * single value the migration gate reads as "safe to cut over".
93696
94120
  */
93697
- async sealCollection(collection, job) {
94121
+ async sealCollection(lane, job) {
94122
+ if (lane.lane === "retrainFrames") {
94123
+ for (const row of await this.deps.retrainFrameLedger.query({})) {
94124
+ if (job.cancelRequested) return;
94125
+ if (!rowIsUnstamped(row.data)) continue;
94126
+ await this.deps.retrainFrameLedger.update(row.id, { [LOCATION_ID_FIELD]: job.toLocationId });
94127
+ job.filesMoved += 1;
94128
+ }
94129
+ return;
94130
+ }
94131
+ const collection = MEDIA_COLLECTION;
93698
94132
  for (;;) {
93699
94133
  if (job.cancelRequested) return;
93700
94134
  const batch = await this.deps.store.query.query({
@@ -93715,9 +94149,13 @@ var MediaRelocateEngine = class {
93715
94149
  job.filesMoved += updated;
93716
94150
  }
93717
94151
  }
93718
- async stamp(collection, key, data, locationId) {
94152
+ async stamp(lane, key, data, locationId) {
94153
+ if (lane.lane === "retrainFrames") {
94154
+ await this.deps.retrainFrameLedger.update(key, { [LOCATION_ID_FIELD]: locationId });
94155
+ return;
94156
+ }
93719
94157
  await this.deps.store.set.mutate({
93720
- collection,
94158
+ collection: MEDIA_COLLECTION,
93721
94159
  key,
93722
94160
  value: {
93723
94161
  ...data,
@@ -102164,6 +102602,18 @@ function decodeCursorMap(raw) {
102164
102602
  function nonNegativeInt(value) {
102165
102603
  return typeof value === "number" && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
102166
102604
  }
102605
+ function decodeScopeCursors(raw) {
102606
+ const out = {};
102607
+ if (raw !== null && typeof raw === "object") for (const [key, cursor] of Object.entries({ ...raw })) {
102608
+ if (typeof cursor === "string" && cursor.length > 0) {
102609
+ out[key] = cursor;
102610
+ continue;
102611
+ }
102612
+ const parsed = nonNegativeInt(cursor);
102613
+ if (parsed !== null) out[key] = parsed;
102614
+ }
102615
+ return out;
102616
+ }
102167
102617
  /** Read a stored row back into a cursor map, ignoring anything unreadable. */
102168
102618
  function decodeOrphanAuditCursors(value) {
102169
102619
  const empty = {
@@ -102175,7 +102625,7 @@ function decodeOrphanAuditCursors(value) {
102175
102625
  if (value === null || typeof value !== "object") return empty;
102176
102626
  const row = { ...value };
102177
102627
  return {
102178
- scopes: decodeCursorMap(row["scopes"]),
102628
+ scopes: decodeScopeCursors(row["scopes"]),
102179
102629
  faceVectorCursor: nonNegativeInt(row["faceVectorCursor"]) ?? 0,
102180
102630
  clipIndexCursors: decodeCursorMap(row["clipIndexCursors"]),
102181
102631
  updatedAtMs: nonNegativeInt(row["updatedAtMs"]) ?? 0
@@ -103748,7 +104198,9 @@ async function auditScope(scope, deps, limits, cursor) {
103748
104198
  };
103749
104199
  const deviceIds = /* @__PURE__ */ new Set();
103750
104200
  const graceFloor = deps.now() - limits.graceMs;
103751
- let offset = Math.max(0, cursor);
104201
+ let offset = typeof cursor === "number" ? Math.max(0, cursor) : 0;
104202
+ let keysetCursor = typeof cursor === "string" ? cursor : null;
104203
+ let keyed = keysetCursor !== null;
103752
104204
  let stoppedBy = "exhausted";
103753
104205
  let collectionSize = null;
103754
104206
  try {
@@ -103765,9 +104217,16 @@ async function auditScope(scope, deps, limits, cursor) {
103765
104217
  stoppedBy = "row-ceiling";
103766
104218
  break;
103767
104219
  }
103768
- const rows = await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
104220
+ if (deps.stop?.() === true) {
104221
+ stoppedBy = "stopped";
104222
+ break;
104223
+ }
104224
+ const keyedPage = deps.pageKeyset === void 0 ? null : await deps.pageKeyset(scope.collection, keysetCursor, limits.pageSize);
104225
+ keyed = keyedPage !== null;
104226
+ const rows = keyedPage !== null ? keyedPage.rows : await deps.page(scope.collection, scope.shard, offset, limits.pageSize);
103769
104227
  if (rows.length === 0) {
103770
104228
  offset = 0;
104229
+ keysetCursor = null;
103771
104230
  break;
103772
104231
  }
103773
104232
  const refs = rows.map((row) => scope.resolve(row));
@@ -103854,9 +104313,17 @@ async function auditScope(scope, deps, limits, cursor) {
103854
104313
  if (deps.breathe) await deps.breathe();
103855
104314
  if (ceilingHit) {
103856
104315
  stoppedBy = "reclaim-ceiling";
103857
- offset += rows.length - reclaimedInPage;
104316
+ if (!keyed) offset += rows.length - reclaimedInPage;
103858
104317
  break;
103859
104318
  }
104319
+ if (keyedPage !== null) {
104320
+ if (keyedPage.nextCursor === null) {
104321
+ keysetCursor = null;
104322
+ break;
104323
+ }
104324
+ keysetCursor = keyedPage.nextCursor;
104325
+ continue;
104326
+ }
103860
104327
  const advanced = rows.length - reclaimedInPage;
103861
104328
  if (advanced === 0 && rows.length < limits.pageSize) {
103862
104329
  offset = 0;
@@ -103872,7 +104339,7 @@ async function auditScope(scope, deps, limits, cursor) {
103872
104339
  collectionSize,
103873
104340
  coveragePercent: Math.round(tally.examined / collectionSize * 1e3) / 10,
103874
104341
  stoppedBy,
103875
- nextCursor: offset
104342
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103876
104343
  } });
103877
104344
  const freedNothing = scope.sizeField === null ? tally.reclaimed === 0 : tally.bytesReclaimed === 0;
103878
104345
  if (limits.mode === "reclaim" && tally.orphans > 0 && freedNothing) deps.logger.warn("orphan audit: reclaim pass freed no bytes against a standing backlog", { meta: {
@@ -103899,7 +104366,7 @@ async function auditScope(scope, deps, limits, cursor) {
103899
104366
  unresolvable: tally.unresolvable,
103900
104367
  deviceIds: [...deviceIds],
103901
104368
  stoppedBy,
103902
- nextCursor: offset
104369
+ nextCursor: keyed ? keysetCursor ?? 0 : offset
103903
104370
  };
103904
104371
  }
103905
104372
  /**
@@ -103991,7 +104458,8 @@ var ORPHAN_AUDIT_DEFAULTS = {
103991
104458
  //#endregion
103992
104459
  //#region src/pipeline-analytics/retention/orphan-reclaimers.ts
103993
104460
  var OBJECT_EVENTS_COLLECTION = "pipeline-analytics:object-events";
103994
- var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION, "pipeline-analytics:plates"]);
104461
+ var PLATES_COLLECTION = "pipeline-analytics:plates";
104462
+ var RAW_DELETE_COLLECTIONS = new Set([OBJECT_EVENTS_COLLECTION]);
103995
104463
  /**
103996
104464
  * The `deleteRow` port: ONE row by its primary key `id` (what every
103997
104465
  * raw-delete collection above declares), answering whether a row went.
@@ -104065,6 +104533,13 @@ function buildOrphanReclaimer(ports) {
104065
104533
  rows: await ports.deleteTrackAlias(key) ? 1 : 0,
104066
104534
  bytes: null
104067
104535
  };
104536
+ if (collection === PLATES_COLLECTION) {
104537
+ const assert = trackIdAssertOf(data);
104538
+ return {
104539
+ rows: await ports.deletePlate(key, assert === null ? void 0 : assert) ? 1 : 0,
104540
+ bytes: null
104541
+ };
104542
+ }
104068
104543
  if (RAW_DELETE_COLLECTIONS.has(collection)) {
104069
104544
  const assert = trackIdAssertOf(data);
104070
104545
  if (collection === OBJECT_EVENTS_COLLECTION && assert === null) {
@@ -104086,6 +104561,87 @@ function buildOrphanReclaimer(ports) {
104086
104561
  };
104087
104562
  }
104088
104563
  //#endregion
104564
+ //#region src/pipeline-analytics/retention/orphan-audit-ledgers.ts
104565
+ /** Audited collection → its records-kind ledger. Empty without records. */
104566
+ function orphanAuditLedgers(wiring) {
104567
+ if (wiring.records === void 0) return /* @__PURE__ */ new Map();
104568
+ return new Map([
104569
+ [FACES_COLLECTION, wireFaceLedger(wiring)],
104570
+ [PLATES_COLLECTION$1, wirePlateLedger(wiring)],
104571
+ [TRACK_ALIASES_COLLECTION, wireTrackAliasLedger(wiring)]
104572
+ ]);
104573
+ }
104574
+ /** {@link OrphanAuditDeps.pageKeyset} over those ledgers; `null` for any other collection. */
104575
+ function keysetPager(ledgers) {
104576
+ return async (collection, cursor, limit) => {
104577
+ const ledger = ledgers.get(collection);
104578
+ if (ledger === void 0) return null;
104579
+ const page = await ledger.pageById(cursor, limit);
104580
+ return {
104581
+ rows: page.rows.map((row) => ({
104582
+ key: row.id,
104583
+ data: row.data
104584
+ })),
104585
+ nextCursor: page.nextCursor
104586
+ };
104587
+ };
104588
+ }
104589
+ /** The kind's count for a ledger-backed collection, `undefined` for any other. */
104590
+ async function ledgerSize(ledgers, collection) {
104591
+ const ledger = ledgers.get(collection);
104592
+ return ledger === void 0 ? void 0 : ledger.count();
104593
+ }
104594
+ /**
104595
+ * The plate reclaim through the plate ledger — records on a hub, the legacy
104596
+ * collection only without records. With `assert`, a row whose `trackId` is not
104597
+ * the dead track's is KEPT (logged), never deleted.
104598
+ */
104599
+ function plateDeleter(plates, logger) {
104600
+ return async (plateId, assert) => {
104601
+ const row = await plates.get(plateId);
104602
+ if (row === null) return false;
104603
+ if (assert !== void 0 && row.data["trackId"] !== assert.trackId) {
104604
+ const deviceId = row.data["deviceId"];
104605
+ logger.warn("orphan audit: plate row now names another track — kept", {
104606
+ ...typeof deviceId === "number" ? { tags: { deviceId } } : {},
104607
+ meta: {
104608
+ plateId,
104609
+ expectedTrackId: assert.trackId
104610
+ }
104611
+ });
104612
+ return false;
104613
+ }
104614
+ await plates.deleteKey(plateId);
104615
+ return true;
104616
+ };
104617
+ }
104618
+ function auditCollectionPorts(store, ledgers) {
104619
+ return {
104620
+ pageKeyset: keysetPager(ledgers),
104621
+ pageByOffset: async (collection, offset, limit) => {
104622
+ return (await store.query.query({
104623
+ collection,
104624
+ filter: {
104625
+ orderBy: {
104626
+ field: "id",
104627
+ direction: "asc"
104628
+ },
104629
+ limit,
104630
+ offset
104631
+ }
104632
+ })).map((row) => ({
104633
+ key: row.id,
104634
+ data: row.data
104635
+ }));
104636
+ },
104637
+ size: async (collection) => {
104638
+ const counted = await ledgerSize(ledgers, collection);
104639
+ if (counted !== void 0) return counted;
104640
+ return store.count.query({ collection });
104641
+ }
104642
+ };
104643
+ }
104644
+ //#endregion
104089
104645
  //#region src/pipeline-analytics/retention/retired-device-settings.ts
104090
104646
  /**
104091
104647
  * Every retired knob, with the sentence that explains it. The list is the
@@ -114843,6 +115399,7 @@ var customActions = {
114843
115399
  ...orphanAuditActions,
114844
115400
  ...mediaReclaimActions,
114845
115401
  ...locationDiscardActions,
115402
+ ...mediaForgetActions,
114846
115403
  ...photoEnrollActions,
114847
115404
  ...faceRescoreActions,
114848
115405
  ...stillnessCalibrationActions
@@ -116633,6 +117190,8 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116633
117190
  orphanAuditState = { ...IDLE_ORPHAN_AUDIT };
116634
117191
  /** Frozen-location leftover wipe (events/ on a drained HDD). Detached. */
116635
117192
  discardJob = null;
117193
+ /** The narrow media-row forget's wiring (`storage.forgetMissingMedia`). */
117194
+ mediaForgetDeps = null;
116636
117195
  /**
116637
117196
  * The CLIP indexes' walks (D649), one resume cursor per index, kept in their
116638
117197
  * OWN field rather than in {@link orphanAuditCursors}: `runOrphanAudit`
@@ -116841,10 +117400,17 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116841
117400
  const isHub = ownNodeId === "hub";
116842
117401
  const ncHandlers = isHub ? this.buildNcActionHandlers(api) : void 0;
116843
117402
  if (isHub) this.reconcileTapePinsInBackground();
117403
+ this.mediaForgetDeps = {
117404
+ listLocations: async () => api.storage.listLocations.query({}),
117405
+ localNodeId: ownNodeId,
117406
+ store: api.settingsStore,
117407
+ logger: logger.child("MediaForget")
117408
+ };
116844
117409
  this.discardJob = createDiscardJob({
116845
117410
  listLocations: async () => api.storage.listLocations.query({}),
116846
117411
  localNodeId: ownNodeId,
116847
117412
  store: api.settingsStore,
117413
+ retrainFrameLedger: wireRetrainFrameLedger(this.eventLedgerWiring(api.settingsStore)),
116848
117414
  deleteTrackEvents: async (trackId) => {
116849
117415
  await this.eventStore?.deleteByTrack(trackId);
116850
117416
  },
@@ -116865,6 +117431,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
116865
117431
  ...orphanAuditActions,
116866
117432
  ...mediaReclaimActions,
116867
117433
  ...locationDiscardActions,
117434
+ ...mediaForgetActions,
116868
117435
  ...photoEnrollActions,
116869
117436
  ...faceRescoreActions,
116870
117437
  ...stillnessCalibrationActions
@@ -117460,6 +118027,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
117460
118027
  });
117461
118028
  this.mediaRelocate = new MediaRelocateEngine({
117462
118029
  store: api.settingsStore,
118030
+ retrainFrameLedger: wireRetrainFrameLedger(eventLedgerWiring),
117463
118031
  storage,
117464
118032
  logger: logger.child("MediaRelocate"),
117465
118033
  now: () => Date.now(),
@@ -125202,36 +125770,29 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
125202
125770
  const trackStore = this.trackStore;
125203
125771
  const eventStore = this.eventStore;
125204
125772
  const mediaStore = this.mediaStore;
125205
- if (!trackStore || !eventStore || !mediaStore) {
125773
+ const settingsStore = this.settingsStore;
125774
+ if (!trackStore || !eventStore || !mediaStore || !settingsStore) {
125206
125775
  if (!alreadyHeld) this.orphanAuditRunning = false;
125207
125776
  return null;
125208
125777
  }
125209
125778
  try {
125210
125779
  const limits = await this.orphanAuditLimits(overrides);
125780
+ const auditLedgers = orphanAuditLedgers(this.eventLedgerWiring(settingsStore));
125781
+ const plateLedger = wirePlateLedger(this.eventLedgerWiring(settingsStore));
125782
+ const collectionPorts = auditCollectionPorts(settingsStore, auditLedgers);
125211
125783
  const deps = {
125784
+ pageKeyset: collectionPorts.pageKeyset,
125785
+ stop: () => this.shuttingDown,
125212
125786
  page: async (collection, shard, offset, limit) => {
125213
125787
  if (collection === MEDIA_COLLECTION) return (await mediaStore.pageForAudit(shard?.value ?? "", offset, limit)).map((row) => ({
125214
125788
  key: String(row.mediaId),
125215
125789
  data: row.data
125216
125790
  }));
125217
- return (await this.settingsStore?.query.query({
125218
- collection,
125219
- filter: {
125220
- orderBy: {
125221
- field: "id",
125222
- direction: "asc"
125223
- },
125224
- limit,
125225
- offset
125226
- }
125227
- }) ?? []).map((row) => ({
125228
- key: row.id,
125229
- data: row.data
125230
- }));
125791
+ return collectionPorts.pageByOffset(collection, offset, limit);
125231
125792
  },
125232
125793
  size: async (collection, shard) => {
125233
125794
  if (collection === MEDIA_COLLECTION) return mediaStore.countForAudit(shard?.value ?? "");
125234
- return await this.settingsStore?.count.query({ collection }) ?? null;
125795
+ return collectionPorts.size(collection);
125235
125796
  },
125236
125797
  liveTrackIds: (ids) => this.liveTrackIdsThroughAliases(ids, trackStore),
125237
125798
  liveEventIds: (table, ids) => eventStore.existingEventIdsIn(table, ids),
@@ -125254,6 +125815,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
125254
125815
  if (aliasStore === null) throw new Error("track alias store not bound");
125255
125816
  return await aliasStore.deleteAlias(aliasId);
125256
125817
  },
125818
+ deletePlate: plateDeleter(plateLedger, this.ctx.logger),
125257
125819
  deleteRow: settingsRowDeleterOf(() => this.settingsStore),
125258
125820
  logger: this.ctx.logger
125259
125821
  });
@@ -126675,7 +127237,12 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends BaseAddon {
126675
127237
  jobId: null
126676
127238
  },
126677
127239
  "storage.discardLocationStatus": async () => this.discardJob?.status() ?? IDLE_DISCARD,
126678
- "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false }
127240
+ "storage.discardLocationCancel": async () => this.discardJob?.cancel() ?? { requested: false },
127241
+ "storage.forgetMissingMedia": async (input) => {
127242
+ const deps = this.mediaForgetDeps;
127243
+ if (deps === null) throw new Error("storage.forgetMissingMedia: not initialised");
127244
+ return forgetMissingMediaRows(deps, input);
127245
+ }
126679
127246
  };
126680
127247
  }
126681
127248
  /**