emdash 0.28.0 → 0.28.1

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.
Files changed (100) hide show
  1. package/dist/api/route-utils.mjs +2 -2
  2. package/dist/api/schemas/index.d.mts +1 -1
  3. package/dist/{api-BFURZn1i.mjs → api-DxFSorgk.mjs} +2 -2
  4. package/dist/{api-BFURZn1i.mjs.map → api-DxFSorgk.mjs.map} +1 -1
  5. package/dist/{apply-Bu-8nYHL.mjs → apply-CDJqIjPU.mjs} +8 -8
  6. package/dist/{apply-Bu-8nYHL.mjs.map → apply-CDJqIjPU.mjs.map} +1 -1
  7. package/dist/astro/index.d.mts +1 -1
  8. package/dist/astro/index.mjs +1 -1
  9. package/dist/astro/middleware/auth.d.mts +1 -1
  10. package/dist/astro/middleware.mjs +15 -15
  11. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +1 -1
  12. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +1 -1
  13. package/dist/astro/routes/api/admin/bylines/index.mjs +1 -1
  14. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +6 -6
  15. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +6 -6
  16. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +6 -6
  17. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +6 -6
  18. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +6 -6
  19. package/dist/astro/routes/api/admin/plugins/index.mjs +6 -6
  20. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +6 -6
  21. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +6 -6
  22. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +6 -6
  23. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +6 -6
  24. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +7 -7
  25. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +6 -6
  26. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +7 -7
  27. package/dist/astro/routes/api/admin/plugins/updates.mjs +6 -6
  28. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +6 -6
  29. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +6 -6
  30. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +1 -1
  31. package/dist/astro/routes/api/import/wordpress/execute.d.mts +1 -1
  32. package/dist/astro/routes/api/import/wordpress/execute.mjs +2 -2
  33. package/dist/astro/routes/api/import/wordpress/media.d.mts +1 -1
  34. package/dist/astro/routes/api/import/wordpress/prepare.mjs +1 -1
  35. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +1 -1
  36. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +2 -2
  37. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  38. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
  39. package/dist/astro/routes/api/manifest.mjs +1 -1
  40. package/dist/astro/routes/api/mcp.mjs +14 -14
  41. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  42. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -10
  43. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  44. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +6 -6
  45. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +6 -6
  46. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +6 -6
  47. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +6 -6
  48. package/dist/astro/routes/api/schema/collections/index.mjs +6 -6
  49. package/dist/astro/routes/api/schema/index.mjs +2 -2
  50. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +6 -6
  51. package/dist/astro/routes/api/schema/orphans/index.mjs +6 -6
  52. package/dist/astro/routes/api/setup/dev-bypass.mjs +3 -3
  53. package/dist/astro/routes/api/setup/index.mjs +3 -3
  54. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +2 -2
  55. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +2 -2
  56. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +2 -2
  57. package/dist/astro/routes/api/taxonomies/index.mjs +2 -2
  58. package/dist/astro/routes/api/typegen.mjs +2 -2
  59. package/dist/astro/types.d.mts +1 -1
  60. package/dist/{byline-fields-Cgrt9-aL.d.mts → byline-fields-CHE8UStY.d.mts} +3 -3
  61. package/dist/{byline-fields-Cgrt9-aL.d.mts.map → byline-fields-CHE8UStY.d.mts.map} +1 -1
  62. package/dist/{bylines-Woq87129.mjs → bylines-CAZxGSrm.mjs} +2 -2
  63. package/dist/{bylines-Woq87129.mjs.map → bylines-CAZxGSrm.mjs.map} +1 -1
  64. package/dist/cli/index.mjs +3 -3
  65. package/dist/{content-refresh-CA4uKwzH.mjs → content-refresh-Br4gMzh8.mjs} +13 -7
  66. package/dist/content-refresh-Br4gMzh8.mjs.map +1 -0
  67. package/dist/{context-CUsR9dXL.mjs → context-BRIpHARC.mjs} +2 -2
  68. package/dist/{context-CUsR9dXL.mjs.map → context-BRIpHARC.mjs.map} +1 -1
  69. package/dist/index.d.mts +1 -1
  70. package/dist/index.mjs +11 -11
  71. package/dist/media/local-runtime.d.mts +1 -1
  72. package/dist/{menus-CsZ2ZEK2.mjs → menus-b2T4clbO.mjs} +2 -2
  73. package/dist/{menus-CsZ2ZEK2.mjs.map → menus-b2T4clbO.mjs.map} +1 -1
  74. package/dist/plugin-utils.d.mts +1 -1
  75. package/dist/plugins/adapt-sandbox-entry.d.mts +1 -1
  76. package/dist/{query-ByGq6ver.mjs → query-1Cp6U9oo.mjs} +5 -5
  77. package/dist/{query-ByGq6ver.mjs.map → query-1Cp6U9oo.mjs.map} +1 -1
  78. package/dist/{registry-B65mUtPa.mjs → registry-B57vp_X9.mjs} +2 -2
  79. package/dist/{registry-B65mUtPa.mjs.map → registry-B57vp_X9.mjs.map} +1 -1
  80. package/dist/runtime.d.mts +1 -1
  81. package/dist/{schema-Blhoxp3P.mjs → schema-CX9cfNkZ.mjs} +2 -2
  82. package/dist/{schema-Blhoxp3P.mjs.map → schema-CX9cfNkZ.mjs.map} +1 -1
  83. package/dist/seed/index.mjs +3 -3
  84. package/dist/{taxonomies-BGBd16Eu.mjs → taxonomies-Bs13SN-F.mjs} +2 -2
  85. package/dist/{taxonomies-BGBd16Eu.mjs.map → taxonomies-Bs13SN-F.mjs.map} +1 -1
  86. package/dist/{taxonomies-Cnb4F-Pb.mjs → taxonomies-kH9ABDUF.mjs} +2 -2
  87. package/dist/{taxonomies-Cnb4F-Pb.mjs.map → taxonomies-kH9ABDUF.mjs.map} +1 -1
  88. package/dist/{validation-c8WjhH8z.mjs → validation-0l8-RYj4.mjs} +3 -3
  89. package/dist/{validation-c8WjhH8z.mjs.map → validation-0l8-RYj4.mjs.map} +1 -1
  90. package/dist/version-qmeO1XZY.mjs +7 -0
  91. package/dist/{version-CaccU7DM.mjs.map → version-qmeO1XZY.mjs.map} +1 -1
  92. package/dist/{wxr-taxonomies-BC3OYpvx.mjs → wxr-taxonomies-CNFcsNCr.mjs} +2 -2
  93. package/dist/{wxr-taxonomies-BC3OYpvx.mjs.map → wxr-taxonomies-CNFcsNCr.mjs.map} +1 -1
  94. package/package.json +5 -5
  95. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +17 -15
  96. package/src/database/repositories/media-usage.ts +25 -3
  97. package/src/media/usage/content-repair.ts +206 -22
  98. package/src/media/usage/content-snapshots.ts +4 -0
  99. package/dist/content-refresh-CA4uKwzH.mjs.map +0 -1
  100. package/dist/version-CaccU7DM.mjs +0 -7
@@ -496,7 +496,7 @@ export class MediaUsageRepository {
496
496
  .executeTakeFirst();
497
497
  deleted = Number(result.numDeletedRows ?? 0) > 0;
498
498
  if (!deleted) return;
499
- await trx.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).execute();
499
+ await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedCurrentGeneration);
500
500
  });
501
501
 
502
502
  return {
@@ -518,7 +518,11 @@ export class MediaUsageRepository {
518
518
  .executeTakeFirst();
519
519
  deleted = Number(result.numDeletedRows ?? 0) > 0;
520
520
  if (!deleted) return;
521
- await trx.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).execute();
521
+ await this.deleteSourceGenerationOccurrences(
522
+ trx,
523
+ sourceKey,
524
+ expectedSource.currentGeneration,
525
+ );
522
526
  });
523
527
 
524
528
  return {
@@ -547,7 +551,11 @@ export class MediaUsageRepository {
547
551
  .executeTakeFirst();
548
552
  deleted = Number(result.numDeletedRows ?? 0) > 0;
549
553
  if (!deleted) return;
550
- await trx.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).execute();
554
+ await this.deleteSourceGenerationOccurrences(
555
+ trx,
556
+ sourceKey,
557
+ expectedSource.currentGeneration,
558
+ );
551
559
  });
552
560
  const contentPresent = deleted ? false : await this.contentRowExists(tableName, contentId);
553
561
 
@@ -903,6 +911,20 @@ export class MediaUsageRepository {
903
911
  });
904
912
  }
905
913
 
914
+ private async deleteSourceGenerationOccurrences(
915
+ db: DatabaseExecutor,
916
+ sourceKey: string,
917
+ generation: string,
918
+ ): Promise<void> {
919
+ // Guarded source deletes remove only the generation that won the source CAS;
920
+ // unmatched generations become invisible orphans reclaimed by age-gated cleanup.
921
+ await db
922
+ .deleteFrom("_emdash_media_usage")
923
+ .where("source_key", "=", sourceKey)
924
+ .where("generation", "=", generation)
925
+ .execute();
926
+ }
927
+
906
928
  private async insertOccurrences(
907
929
  db: DatabaseExecutor,
908
930
  sourceKey: string,
@@ -20,6 +20,7 @@ import {
20
20
  } from "./content-refresh.js";
21
21
  import {
22
22
  CONTENT_SOURCE_SCHEMA_VERSION,
23
+ type ContentMediaUsageSnapshot,
23
24
  loadContentMediaUsageSnapshots,
24
25
  } from "./content-snapshots.js";
25
26
  import {
@@ -65,11 +66,48 @@ export interface ContentMediaUsageRepairCollectionResult {
65
66
  completedAt: string | null;
66
67
  }
67
68
 
69
+ export interface ContentMediaUsageRepairAllResult {
70
+ status: ContentMediaUsageRepairStatus;
71
+ collections: ContentMediaUsageRepairCollectionResult[];
72
+ indexedSourceCount: number;
73
+ failedSourceCount: number;
74
+ skippedSourceCount: number;
75
+ deletedSourceCount: number;
76
+ }
77
+
68
78
  export interface ContentMediaUsageCollectionScan {
69
79
  collectionSlug: string;
70
80
  contentIds: string[];
71
81
  }
72
82
 
83
+ interface ContentMediaUsageCollectionRecord {
84
+ id: string;
85
+ slug: string;
86
+ }
87
+
88
+ interface ContentMediaUsageInitialCollectionResult {
89
+ collection: ContentMediaUsageCollectionRecord;
90
+ result: ContentMediaUsageRepairCollectionResult;
91
+ }
92
+
93
+ export async function repairContentMediaUsageAll(
94
+ db: Kysely<Database>,
95
+ ): Promise<ContentMediaUsageRepairAllResult> {
96
+ const collections = await loadContentMediaUsageCollectionRecords(db);
97
+ const results: ContentMediaUsageInitialCollectionResult[] = [];
98
+
99
+ for (const collection of collections) {
100
+ results.push({
101
+ collection,
102
+ result: await repairContentMediaUsageCollectionSafely(db, collection.slug),
103
+ });
104
+ }
105
+
106
+ return aggregateContentMediaUsageRepairAll(
107
+ await filterExistingContentMediaUsageCollectionResults(db, results),
108
+ );
109
+ }
110
+
73
111
  export async function scanContentMediaUsageCollection(
74
112
  db: Kysely<Database>,
75
113
  collectionSlug: string,
@@ -105,6 +143,116 @@ export async function repairContentMediaUsageCollection(
105
143
  );
106
144
  }
107
145
 
146
+ async function loadContentMediaUsageCollectionRecords(
147
+ db: Kysely<Database>,
148
+ ): Promise<ContentMediaUsageCollectionRecord[]> {
149
+ return db
150
+ .selectFrom("_emdash_collections")
151
+ .select(["id", "slug"])
152
+ .orderBy("slug", "asc")
153
+ .execute();
154
+ }
155
+
156
+ async function repairContentMediaUsageCollectionSafely(
157
+ db: Kysely<Database>,
158
+ collectionSlug: string,
159
+ ): Promise<ContentMediaUsageRepairCollectionResult> {
160
+ try {
161
+ return await repairContentMediaUsageCollection(db, { collectionSlug });
162
+ } catch (error) {
163
+ console.error(`[media-usage] Failed to repair collection ${collectionSlug}:`, error);
164
+ const now = new Date().toISOString();
165
+ return {
166
+ scope: contentMediaUsageCollectionScope(collectionSlug),
167
+ status: "failed",
168
+ indexedSourceCount: 0,
169
+ failedSourceCount: 0,
170
+ skippedSourceCount: 0,
171
+ deletedSourceCount: 0,
172
+ lastErrorCode: CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_ERROR,
173
+ startedAt: now,
174
+ completedAt: now,
175
+ };
176
+ }
177
+ }
178
+
179
+ async function filterExistingContentMediaUsageCollectionResults(
180
+ db: Kysely<Database>,
181
+ results: readonly ContentMediaUsageInitialCollectionResult[],
182
+ ): Promise<ContentMediaUsageRepairCollectionResult[]> {
183
+ const currentCollections = await loadContentMediaUsageCollectionRecordsSafely(db);
184
+ const currentIdsBySlug = new Map(
185
+ currentCollections.map((collection) => [collection.slug, collection.id]),
186
+ );
187
+ const includedResults: ContentMediaUsageRepairCollectionResult[] = [];
188
+ const excludedResults: ContentMediaUsageRepairCollectionResult[] = [];
189
+
190
+ for (const { collection, result } of results) {
191
+ if (currentIdsBySlug.get(collection.slug) === collection.id) {
192
+ includedResults.push(result);
193
+ } else {
194
+ excludedResults.push(result);
195
+ }
196
+ }
197
+
198
+ if (excludedResults.length > 0) {
199
+ const repo = new MediaUsageRepository(db);
200
+ for (const result of excludedResults) {
201
+ await repo.deleteIndexStatus(result.scope);
202
+ }
203
+ }
204
+
205
+ return includedResults;
206
+ }
207
+
208
+ async function loadContentMediaUsageCollectionRecordsSafely(
209
+ db: Kysely<Database>,
210
+ ): Promise<ContentMediaUsageCollectionRecord[]> {
211
+ try {
212
+ return await loadContentMediaUsageCollectionRecords(db);
213
+ } catch {
214
+ // Retry once before failing; returning unpruned results can over-report deleted collections.
215
+ }
216
+
217
+ try {
218
+ return await loadContentMediaUsageCollectionRecords(db);
219
+ } catch (error) {
220
+ console.error("[media-usage] Failed to reconcile all-content repair collections:", error);
221
+ throw error;
222
+ }
223
+ }
224
+
225
+ function aggregateContentMediaUsageRepairAll(
226
+ collections: readonly ContentMediaUsageRepairCollectionResult[],
227
+ ): ContentMediaUsageRepairAllResult {
228
+ return {
229
+ status: determineRepairAllStatus(collections),
230
+ collections: [...collections],
231
+ indexedSourceCount: sumCollectionRepairCount(collections, "indexedSourceCount"),
232
+ failedSourceCount: sumCollectionRepairCount(collections, "failedSourceCount"),
233
+ skippedSourceCount: sumCollectionRepairCount(collections, "skippedSourceCount"),
234
+ deletedSourceCount: sumCollectionRepairCount(collections, "deletedSourceCount"),
235
+ };
236
+ }
237
+
238
+ function determineRepairAllStatus(
239
+ collections: readonly ContentMediaUsageRepairCollectionResult[],
240
+ ): ContentMediaUsageRepairStatus {
241
+ if (collections.length === 0) return "complete";
242
+ if (collections.every((collection) => collection.status === "complete")) return "complete";
243
+ if (collections.some((collection) => collection.status === "stale")) return "stale";
244
+ if (collections.some((collection) => collection.status === "partial")) return "partial";
245
+ if (collections.every((collection) => collection.status === "failed")) return "failed";
246
+ return "partial";
247
+ }
248
+
249
+ function sumCollectionRepairCount(
250
+ collections: readonly ContentMediaUsageRepairCollectionResult[],
251
+ key: "indexedSourceCount" | "failedSourceCount" | "skippedSourceCount" | "deletedSourceCount",
252
+ ): number {
253
+ return collections.reduce((sum, collection) => sum + collection[key], 0);
254
+ }
255
+
108
256
  async function repairContentMediaUsageCollectionUnlocked(
109
257
  db: Kysely<Database>,
110
258
  collectionSlug: string,
@@ -147,6 +295,7 @@ async function repairContentMediaUsageCollectionUnlocked(
147
295
  skippedSourceCount: 0,
148
296
  deletedSourceCount: 0,
149
297
  lastErrorCode: CONTENT_MEDIA_USAGE_REPAIR_ERROR.COLLECTION_NOT_FOUND,
298
+ missingContentIds: new Set(),
150
299
  },
151
300
  status: "failed",
152
301
  startedAt,
@@ -155,16 +304,14 @@ async function repairContentMediaUsageCollectionUnlocked(
155
304
  }
156
305
  const counts = await repairScannedContentSources(db, repo, scan);
157
306
  const finalScan = await scanContentMediaUsageCollection(db, collectionSlug);
158
- const scannedContentCount = finalScan?.contentIds.length ?? scan.contentIds.length;
159
307
  if (!finalScan) {
160
308
  counts.failedSourceCount++;
161
309
  counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.COLLECTION_NOT_FOUND;
162
- } else if (!sameContentIds(scan.contentIds, finalScan.contentIds)) {
163
- counts.skippedSourceCount++;
164
- counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
310
+ } else if (!sameContentIds(repairedContentIds(scan.contentIds, counts), finalScan.contentIds)) {
311
+ markRepairConflict(counts);
165
312
  }
166
313
  const completedAt = new Date().toISOString();
167
- const status = determineRepairStatus(counts, scannedContentCount);
314
+ const status = determineRepairStatus(counts);
168
315
  return await finalizeRepairStatus(repo, {
169
316
  ...scope,
170
317
  runToken,
@@ -191,6 +338,7 @@ async function repairContentMediaUsageCollectionUnlocked(
191
338
  skippedSourceCount: 0,
192
339
  deletedSourceCount: 0,
193
340
  lastErrorCode,
341
+ missingContentIds: new Set(),
194
342
  },
195
343
  status: "failed",
196
344
  startedAt,
@@ -205,6 +353,7 @@ interface RepairCounts {
205
353
  skippedSourceCount: number;
206
354
  deletedSourceCount: number;
207
355
  lastErrorCode: ContentMediaUsageRepairErrorCode | null;
356
+ missingContentIds: Set<string>;
208
357
  }
209
358
 
210
359
  async function repairScannedContentSources(
@@ -218,6 +367,7 @@ async function repairScannedContentSources(
218
367
  skippedSourceCount: 0,
219
368
  deletedSourceCount: 0,
220
369
  lastErrorCode: null,
370
+ missingContentIds: new Set(),
221
371
  };
222
372
 
223
373
  const fieldDiscovery = await loadContentMediaUsageFields(db, scan.collectionSlug);
@@ -256,7 +406,16 @@ async function repairContentSource(
256
406
  fieldDiscovery,
257
407
  );
258
408
  if (!snapshotsResult.success) {
409
+ if (snapshotsResult.error === CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_NOT_FOUND) {
410
+ markRepairConflict(counts);
411
+ counts.missingContentIds.add(contentId);
412
+ return;
413
+ }
259
414
  counts.lastErrorCode = snapshotsResult.error;
415
+ if (snapshotsResult.snapshots) {
416
+ // Partial snapshot failures keep absent variants in place; the attempted source is marked below.
417
+ await repairSnapshotSources(repo, snapshotsResult.snapshots, observedSources, counts);
418
+ }
260
419
  if (snapshotsResult.source) {
261
420
  const result = await repo.markSourceAttemptedIfMatching(
262
421
  {
@@ -269,8 +428,7 @@ async function repairContentSource(
269
428
  if (result.attempted) {
270
429
  counts.failedSourceCount++;
271
430
  } else {
272
- counts.skippedSourceCount++;
273
- counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
431
+ markRepairConflict(counts);
274
432
  }
275
433
  return;
276
434
  }
@@ -279,8 +437,29 @@ async function repairContentSource(
279
437
  return;
280
438
  }
281
439
 
440
+ const expectedSourceKeys = await repairSnapshotSources(
441
+ repo,
442
+ snapshotsResult.snapshots,
443
+ observedSources,
444
+ counts,
445
+ );
446
+
447
+ for (const sourceKey of sourceKeys) {
448
+ if (expectedSourceKeys.has(sourceKey)) continue;
449
+ const observedSource = observedSources.get(sourceKey);
450
+ if (!observedSource) continue;
451
+ await deleteObservedSource(repo, sourceKey, observedSource, counts);
452
+ }
453
+ }
454
+
455
+ async function repairSnapshotSources(
456
+ repo: MediaUsageRepository,
457
+ snapshots: readonly ContentMediaUsageSnapshot[],
458
+ observedSources: Map<string, MediaUsageSource>,
459
+ counts: RepairCounts,
460
+ ): Promise<Set<string>> {
282
461
  const expectedSourceKeys = new Set<string>();
283
- for (const snapshot of snapshotsResult.snapshots) {
462
+ for (const snapshot of snapshots) {
284
463
  expectedSourceKeys.add(snapshot.source.sourceKey);
285
464
  const result = await repo.replaceSourceIfMatching(
286
465
  snapshot.source,
@@ -290,17 +469,15 @@ async function repairContentSource(
290
469
  if (result.replaced) {
291
470
  counts.indexedSourceCount++;
292
471
  } else {
293
- counts.skippedSourceCount++;
294
- counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
472
+ markRepairConflict(counts);
295
473
  }
296
474
  }
475
+ return expectedSourceKeys;
476
+ }
297
477
 
298
- for (const sourceKey of sourceKeys) {
299
- if (expectedSourceKeys.has(sourceKey)) continue;
300
- const observedSource = observedSources.get(sourceKey);
301
- if (!observedSource) continue;
302
- await deleteObservedSource(repo, sourceKey, observedSource, counts);
303
- }
478
+ function markRepairConflict(counts: RepairCounts): void {
479
+ counts.skippedSourceCount++;
480
+ counts.lastErrorCode ??= CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
304
481
  }
305
482
 
306
483
  async function reconcileOrphanedContentSources(
@@ -370,8 +547,7 @@ async function deleteObservedSourceIfContentAbsent(
370
547
  }
371
548
  if (result.contentPresent) return;
372
549
  if (result.source) {
373
- counts.skippedSourceCount++;
374
- counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
550
+ markRepairConflict(counts);
375
551
  }
376
552
  }
377
553
 
@@ -387,8 +563,7 @@ async function deleteObservedSource(
387
563
  return;
388
564
  }
389
565
  if (result.source) {
390
- counts.skippedSourceCount++;
391
- counts.lastErrorCode = CONTENT_MEDIA_USAGE_REPAIR_ERROR.CONTENT_USAGE_REPAIR_CONFLICT;
566
+ markRepairConflict(counts);
392
567
  }
393
568
  }
394
569
 
@@ -439,11 +614,12 @@ async function finalizeRepairStatus(
439
614
 
440
615
  function determineRepairStatus(
441
616
  counts: RepairCounts,
442
- scannedContentCount: number,
443
617
  ): Exclude<ContentMediaUsageRepairStatus, "stale"> {
444
618
  if (counts.failedSourceCount === 0 && counts.skippedSourceCount === 0) return "complete";
445
619
  const trustedProgress = counts.indexedSourceCount + counts.deletedSourceCount;
446
- if (trustedProgress === 0 && scannedContentCount > 0) return "failed";
620
+ if (counts.failedSourceCount > 0 && trustedProgress === 0) {
621
+ return "failed";
622
+ }
447
623
  return "partial";
448
624
  }
449
625
 
@@ -477,6 +653,14 @@ function sameContentIds(left: readonly string[], right: readonly string[]): bool
477
653
  return left.every((id) => rightIds.has(id));
478
654
  }
479
655
 
656
+ function repairedContentIds(
657
+ contentIds: readonly string[],
658
+ counts: Pick<RepairCounts, "missingContentIds">,
659
+ ): string[] {
660
+ if (counts.missingContentIds.size === 0) return [...contentIds];
661
+ return contentIds.filter((contentId) => !counts.missingContentIds.has(contentId));
662
+ }
663
+
480
664
  function contentMediaUsageCollectionScope(collectionSlug: string): ContentMediaUsageRepairScope {
481
665
  return {
482
666
  adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
@@ -46,6 +46,7 @@ export type LoadContentMediaUsageSnapshotsResult =
46
46
  | "DRAFT_REVISION_MISMATCH"
47
47
  | "DRAFT_REVISION_INVALID";
48
48
  source?: MediaUsageSourceInput;
49
+ snapshots?: ContentMediaUsageSnapshot[];
49
50
  };
50
51
 
51
52
  export interface ContentMediaUsageSnapshot {
@@ -116,6 +117,7 @@ export async function loadContentMediaUsageSnapshots(
116
117
  success: false,
117
118
  error: "DRAFT_REVISION_NOT_FOUND",
118
119
  source: attemptedDraftSource,
120
+ snapshots,
119
121
  };
120
122
  }
121
123
  if (!revisionResult.success) {
@@ -123,6 +125,7 @@ export async function loadContentMediaUsageSnapshots(
123
125
  success: false,
124
126
  error: "DRAFT_REVISION_INVALID",
125
127
  source: attemptedDraftSource,
128
+ snapshots,
126
129
  };
127
130
  }
128
131
  const revision = revisionResult.revision;
@@ -131,6 +134,7 @@ export async function loadContentMediaUsageSnapshots(
131
134
  success: false,
132
135
  error: "DRAFT_REVISION_MISMATCH",
133
136
  source: attemptedDraftSource,
137
+ snapshots,
134
138
  };
135
139
  }
136
140