@iwonz/ywtc 3.3.0 → 3.4.0
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.
- package/CHANGELOG.md +19 -0
- package/README.md +40 -43
- package/dist/adapters/confluence.d.ts +15 -1
- package/dist/adapters/confluence.js +59 -0
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/adapters/yandex.d.ts +11 -1
- package/dist/adapters/yandex.js +64 -0
- package/dist/adapters/yandex.js.map +1 -1
- package/dist/cli-arguments.d.ts +2 -0
- package/dist/cli-arguments.js +6 -0
- package/dist/cli-arguments.js.map +1 -1
- package/dist/cli.js +6 -1
- package/dist/cli.js.map +1 -1
- package/dist/clients/confluence-cloud.d.ts +16 -1
- package/dist/clients/confluence-cloud.js +109 -0
- package/dist/clients/confluence-cloud.js.map +1 -1
- package/dist/clients/confluence.d.ts +12 -1
- package/dist/clients/confluence.js +88 -0
- package/dist/clients/confluence.js.map +1 -1
- package/dist/clients/yandex-wiki.d.ts +10 -1
- package/dist/clients/yandex-wiki.js +44 -0
- package/dist/clients/yandex-wiki.js.map +1 -1
- package/dist/comments.d.ts +66 -0
- package/dist/comments.js +478 -0
- package/dist/comments.js.map +1 -0
- package/dist/doctor.d.ts +8 -1
- package/dist/doctor.js +20 -3
- package/dist/doctor.js.map +1 -1
- package/dist/domain.d.ts +69 -1
- package/dist/domain.js +3 -1
- package/dist/domain.js.map +1 -1
- package/dist/filesystem-export.d.ts +16 -1
- package/dist/filesystem-export.js +88 -2
- package/dist/filesystem-export.js.map +1 -1
- package/dist/issues.d.ts +1 -1
- package/dist/migrator.d.ts +2 -0
- package/dist/migrator.js +488 -13
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +13 -1
- package/dist/report-catalog.js +12 -0
- package/dist/report-catalog.js.map +1 -1
- package/dist/report.d.ts +25 -2
- package/dist/report.js +23 -1
- package/dist/report.js.map +1 -1
- package/dist/state.d.ts +35 -1
- package/dist/state.js +43 -1
- package/dist/state.js.map +1 -1
- package/dist/types.d.ts +75 -0
- package/package.json +1 -1
package/dist/migrator.js
CHANGED
|
@@ -4,10 +4,12 @@ import path from 'node:path';
|
|
|
4
4
|
import PQueue from 'p-queue';
|
|
5
5
|
import { createProviderAdapter } from './adapters/factory.js';
|
|
6
6
|
import { appendGridSnapshots, assertDestinationContent, isLegacyTransferPlaceholder, normalizePageContent, normalizedPreviewHtml, rawContentFallback, renderedPreviewHtml, renderConfluenceStorage, renderYfm, setSourceResourceBinding, sourceResourceBindings, verifyDestinationContent, } from './content-model.js';
|
|
7
|
+
import { commentAttachmentUses, createCommentArchiveZip, renderCommentArchive, renderCommentArchiveIndex, resolveCommentAttachmentReferences, verifyCommentArchive, } from './comments.js';
|
|
7
8
|
import { ConfirmedDestinationMissingError, verifyConfirmedDestinationPage } from './consistency.js';
|
|
8
9
|
import { assertTransferCapabilities } from './domain.js';
|
|
9
10
|
import { FilesystemExporter } from './filesystem-export.js';
|
|
10
11
|
import { discoverPageGraph } from './graph.js';
|
|
12
|
+
import { safeFilename } from './input.js';
|
|
11
13
|
import { profileBaseUrl, sameRemoteInstance } from './locators.js';
|
|
12
14
|
import { TransferReport, } from './report.js';
|
|
13
15
|
import { DestinationNameAllocator, preferredDestinationName, verifiedCacheFile } from './resource-reconciliation.js';
|
|
@@ -137,6 +139,14 @@ function reportPage(input) {
|
|
|
137
139
|
});
|
|
138
140
|
const knownBytes = resourceItems.reduce((total, resource) => total + (resource.sourceBytes ?? 0), 0);
|
|
139
141
|
const transferredBytes = resourceItems.reduce((total, resource) => total + (resource.transferredBytes ?? 0), 0);
|
|
142
|
+
const commentsEnabled = input.commentsEnabled ?? true;
|
|
143
|
+
const inventory = input.commentInventory;
|
|
144
|
+
const commentState = input.commentState;
|
|
145
|
+
const commentStatus = !commentsEnabled
|
|
146
|
+
? 'skipped'
|
|
147
|
+
: commentState?.status
|
|
148
|
+
?? (inventory?.status === 'failed' ? 'failed' : inventory?.status === 'empty' ? 'complete' : 'planned');
|
|
149
|
+
const commentError = commentState?.lastError ?? inventory?.error;
|
|
140
150
|
return {
|
|
141
151
|
sourceCanonicalId: input.page.canonicalId,
|
|
142
152
|
sourceId: input.page.id,
|
|
@@ -166,6 +176,20 @@ function reportPage(input) {
|
|
|
166
176
|
warnings: [...new Set([...input.document.warnings, ...(input.warnings ?? [])])],
|
|
167
177
|
normalizations: [...new Set([...(input.document.normalizations ?? []), ...(input.normalizations ?? [])])],
|
|
168
178
|
...(input.error ? { error: input.error } : {}),
|
|
179
|
+
comments: {
|
|
180
|
+
status: commentStatus,
|
|
181
|
+
threads: commentState?.threads ?? inventory?.threads.length ?? 0,
|
|
182
|
+
comments: commentState?.comments ?? inventory?.comments ?? 0,
|
|
183
|
+
participants: commentState?.participants ?? inventory?.participants ?? 0,
|
|
184
|
+
attachments: commentState?.attachments ?? inventory?.attachments ?? 0,
|
|
185
|
+
bytes: commentState?.bytes ?? inventory?.knownBytes ?? 0,
|
|
186
|
+
...(commentState?.archiveName ? { archiveName: commentState.archiveName } : {}),
|
|
187
|
+
...(commentState?.destinationId ? { destinationId: commentState.destinationId } : {}),
|
|
188
|
+
...(commentState?.previousDestinationIds?.length ? { revision: true } : {}),
|
|
189
|
+
...(commentState?.status === 'stale' ? { stale: true } : {}),
|
|
190
|
+
...(commentError ? { error: commentError } : {}),
|
|
191
|
+
...(commentState?.preview ? { preview: commentState.preview } : {}),
|
|
192
|
+
},
|
|
169
193
|
};
|
|
170
194
|
}
|
|
171
195
|
function runStatus(errors, warnings, degradations) {
|
|
@@ -212,11 +236,12 @@ export class Migrator {
|
|
|
212
236
|
}
|
|
213
237
|
async run(options) {
|
|
214
238
|
const sourceStatus = options.sourceStatus ?? 'all';
|
|
239
|
+
const commentsEnabled = options.comments ?? true;
|
|
215
240
|
const source = this.adapterFactory(options.source.profile, this.config);
|
|
216
241
|
const destination = options.destination.type === 'remote'
|
|
217
242
|
? this.adapterFactory(options.destination.profile, this.config)
|
|
218
243
|
: undefined;
|
|
219
|
-
assertTransferCapabilities(source, destination, options.scope, sourceStatus);
|
|
244
|
+
assertTransferCapabilities(source, destination, options.scope, sourceStatus, commentsEnabled);
|
|
220
245
|
const status = options.onStatus ?? (() => undefined);
|
|
221
246
|
status('Checking source and destination capabilities');
|
|
222
247
|
const [sourcePlatform, destinationPlatform, root, container] = await Promise.all([
|
|
@@ -240,9 +265,10 @@ export class Migrator {
|
|
|
240
265
|
await store.load(identity, { fresh: options.fresh ?? false, dryRun: options.dryRun });
|
|
241
266
|
if (!options.dryRun)
|
|
242
267
|
await store.save();
|
|
243
|
-
const run = await store.beginRun(options.scope, options.dryRun, sourceStatus);
|
|
268
|
+
const run = await store.beginRun(options.scope, options.dryRun, sourceStatus, commentsEnabled);
|
|
244
269
|
const report = new TransferReport({
|
|
245
270
|
dryRun: options.dryRun,
|
|
271
|
+
commentsEnabled,
|
|
246
272
|
scope: options.scope,
|
|
247
273
|
sourceStatus,
|
|
248
274
|
direction: `${source.kind} → ${options.destination.type === 'directory' ? options.destination.format : destination.kind}`,
|
|
@@ -311,8 +337,9 @@ export class Migrator {
|
|
|
311
337
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
312
338
|
}
|
|
313
339
|
const resourcesByPage = new Map();
|
|
340
|
+
const commentsByPage = new Map();
|
|
314
341
|
const inventoryQueue = new PQueue({ concurrency: options.concurrency });
|
|
315
|
-
const inventory = { resources: 0, bytes: 0, unknownSizes: 0, grids: 0, links: 0, images: 0, tables: 0, codeBlocks: 0 };
|
|
342
|
+
const inventory = { resources: 0, bytes: 0, unknownSizes: 0, grids: 0, links: 0, images: 0, tables: 0, codeBlocks: 0, commentThreads: 0, comments: 0, commentParticipants: 0, commentAttachments: 0, commentBytes: 0 };
|
|
316
343
|
status(`Inspecting content and resources for ${graph.pages.length} pages`);
|
|
317
344
|
const inventoryErrors = [];
|
|
318
345
|
await Promise.all(graph.pages.map((page) => inventoryQueue.add(async () => {
|
|
@@ -322,6 +349,36 @@ export class Migrator {
|
|
|
322
349
|
const cachedPage = await loadCachedPage(options.cacheDirectory, page.canonicalId);
|
|
323
350
|
const resources = await source.listResources(page);
|
|
324
351
|
resourcesByPage.set(page.canonicalId, resources);
|
|
352
|
+
let commentInventory;
|
|
353
|
+
if (commentsEnabled) {
|
|
354
|
+
try {
|
|
355
|
+
if (!source.readComments)
|
|
356
|
+
throw new Error(`${source.profile.name} does not implement comment reads`);
|
|
357
|
+
commentInventory = await source.readComments(cachedPage);
|
|
358
|
+
resolveCommentAttachmentReferences(commentInventory, resources);
|
|
359
|
+
commentsByPage.set(page.canonicalId, commentInventory);
|
|
360
|
+
inventory.commentThreads += commentInventory.threads.length;
|
|
361
|
+
inventory.comments += commentInventory.comments;
|
|
362
|
+
inventory.commentParticipants += commentInventory.participants;
|
|
363
|
+
inventory.commentAttachments += commentInventory.attachments;
|
|
364
|
+
inventory.commentBytes += commentInventory.knownBytes;
|
|
365
|
+
for (const warning of commentInventory.warnings)
|
|
366
|
+
report.issue({
|
|
367
|
+
code: `comment-inventory-warning:${warning}`,
|
|
368
|
+
severity: 'warning',
|
|
369
|
+
phase: 'inventory',
|
|
370
|
+
pageId: page.canonicalId,
|
|
371
|
+
retryable: false,
|
|
372
|
+
message: `${page.title}: ${warning}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
const message = `${page.title}: comments could not be read: ${error instanceof Error ? error.message : String(error)}`;
|
|
377
|
+
commentInventory = { status: 'failed', threads: [], comments: 0, participants: 0, attachments: 0, knownBytes: 0, unknownSizes: 0, warnings: [], error: message };
|
|
378
|
+
commentsByPage.set(page.canonicalId, commentInventory);
|
|
379
|
+
report.issue({ code: 'comment-inventory-failed', severity: 'error', phase: 'inventory', pageId: page.canonicalId, retryable: true, message });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
325
382
|
const document = appendGridSnapshots(normalizePageContent(cachedPage), resources);
|
|
326
383
|
await cacheDocument(options.cacheDirectory, page.canonicalId, document);
|
|
327
384
|
inventory.resources += resources.filter((resource) => resource.kind !== 'grid').length;
|
|
@@ -337,6 +394,8 @@ export class Migrator {
|
|
|
337
394
|
status: 'planned',
|
|
338
395
|
resources,
|
|
339
396
|
document,
|
|
397
|
+
commentsEnabled,
|
|
398
|
+
...(commentInventory ? { commentInventory } : {}),
|
|
340
399
|
}), undefined, false);
|
|
341
400
|
status(`Inspected ${page.title} · ${resources.length} resources`);
|
|
342
401
|
}
|
|
@@ -354,6 +413,7 @@ export class Migrator {
|
|
|
354
413
|
});
|
|
355
414
|
}
|
|
356
415
|
})));
|
|
416
|
+
await store.setRunComments(run.id, { threads: inventory.commentThreads, comments: inventory.comments, attachments: inventory.commentAttachments });
|
|
357
417
|
if (inventoryErrors.length > 0) {
|
|
358
418
|
const finalStatus = options.signal?.aborted ? 'cancelled' : 'failed';
|
|
359
419
|
report.finish(finalStatus);
|
|
@@ -366,14 +426,14 @@ export class Migrator {
|
|
|
366
426
|
});
|
|
367
427
|
throw new MigrationRunError('Resource preflight failed before destination mutation', reportPaths, stateLocation.filename);
|
|
368
428
|
}
|
|
369
|
-
const planSummary = `Plan ready: root + ${graph.selection.selectedDescendants}/${graph.selection.returnedDescendants} descendants (${graph.selection.excluded.length} excluded, depth ${graph.selection.maximumDepth}) · ${graph.pages.length} pages · ${inventory.resources} files (${inventory.bytes} known bytes${inventory.unknownSizes > 0 ? ` · ${inventory.unknownSizes} sizes unknown` : ''}) · ${inventory.links} links · ${inventory.images} images · ${inventory.tables} tables (${inventory.grids} grids) · ${inventory.codeBlocks} code blocks`;
|
|
429
|
+
const planSummary = `Plan ready: root + ${graph.selection.selectedDescendants}/${graph.selection.returnedDescendants} descendants (${graph.selection.excluded.length} excluded, depth ${graph.selection.maximumDepth}) · ${graph.pages.length} pages · ${inventory.resources} files (${inventory.bytes} known bytes${inventory.unknownSizes > 0 ? ` · ${inventory.unknownSizes} sizes unknown` : ''}) · ${inventory.links} links · ${inventory.images} images · ${inventory.tables} tables (${inventory.grids} grids) · ${inventory.codeBlocks} code blocks · ${commentsEnabled ? `${inventory.commentThreads} comment threads / ${inventory.comments} entries / ${inventory.commentAttachments} files (${inventory.commentBytes} known bytes)` : 'comments skipped'}`;
|
|
370
430
|
status(planSummary);
|
|
371
431
|
this.logger.info(planSummary);
|
|
372
432
|
let output;
|
|
373
433
|
let archive;
|
|
374
434
|
try {
|
|
375
435
|
if (options.destination.type === 'directory') {
|
|
376
|
-
const result = await this.exportFilesystem(source, graph, options.destination, options, stateLocation.id, store, report, sourceFingerprints, resourcesByPage);
|
|
436
|
+
const result = await this.exportFilesystem(source, graph, options.destination, options, stateLocation.id, store, report, sourceFingerprints, resourcesByPage, commentsByPage);
|
|
377
437
|
output = result.directory;
|
|
378
438
|
archive = result.archive;
|
|
379
439
|
report.output('directory', result.directory);
|
|
@@ -393,6 +453,7 @@ export class Migrator {
|
|
|
393
453
|
report,
|
|
394
454
|
sourceFingerprints,
|
|
395
455
|
resourcesByPage,
|
|
456
|
+
commentsByPage,
|
|
396
457
|
});
|
|
397
458
|
output = container.url;
|
|
398
459
|
report.output('destination', container.url);
|
|
@@ -437,7 +498,7 @@ export class Migrator {
|
|
|
437
498
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
438
499
|
}
|
|
439
500
|
}
|
|
440
|
-
async exportFilesystem(source, graph, destination, options, identity, store, report, sourceFingerprints, resourcesByPage) {
|
|
501
|
+
async exportFilesystem(source, graph, destination, options, identity, store, report, sourceFingerprints, resourcesByPage, commentsByPage) {
|
|
441
502
|
const exporter = new FilesystemExporter(source, options.concurrency);
|
|
442
503
|
const totalResources = [...resourcesByPage.values()].reduce((total, resources) => total + resources.filter((resource) => resource.kind !== 'grid').length, 0);
|
|
443
504
|
const total = Math.max(1, graph.pages.length + totalResources);
|
|
@@ -456,6 +517,8 @@ export class Migrator {
|
|
|
456
517
|
options.onStatus?.(`Progress ${Math.min(100, Math.floor((processed / total) * 100))}% · ${processed}/${total} · ${message}`);
|
|
457
518
|
} } : {}),
|
|
458
519
|
resourcesByPage,
|
|
520
|
+
commentsByPage,
|
|
521
|
+
commentsEnabled: options.comments ?? true,
|
|
459
522
|
loadPage: (page) => loadCachedPage(options.cacheDirectory, page.canonicalId),
|
|
460
523
|
loadDocument: (page) => loadCachedDocument(options.cacheDirectory, page.canonicalId),
|
|
461
524
|
});
|
|
@@ -479,7 +542,8 @@ export class Migrator {
|
|
|
479
542
|
updatedAt: new Date().toISOString(),
|
|
480
543
|
}];
|
|
481
544
|
}));
|
|
482
|
-
const
|
|
545
|
+
const commentIncomplete = manifestPage?.comments && !['complete', 'skipped'].includes(manifestPage.comments.status);
|
|
546
|
+
const pageStatus = options.dryRun ? 'planned' : manifestPage?.degraded || commentIncomplete ? 'partial' : 'complete';
|
|
483
547
|
await store.setPage({
|
|
484
548
|
sourceId: page.id,
|
|
485
549
|
sourceCanonicalId: page.canonicalId,
|
|
@@ -495,6 +559,19 @@ export class Migrator {
|
|
|
495
559
|
status: pageStatus,
|
|
496
560
|
resources,
|
|
497
561
|
degradations: manifestPage?.warnings ?? [],
|
|
562
|
+
...(manifestPage?.comments ? { comments: {
|
|
563
|
+
status: manifestPage.comments.status,
|
|
564
|
+
...(manifestPage.comments.fingerprint ? { fingerprint: manifestPage.comments.fingerprint } : {}),
|
|
565
|
+
...(manifestPage.comments.archivePath ? { archivePath: manifestPage.comments.archivePath } : {}),
|
|
566
|
+
...(manifestPage.comments.archiveName ? { archiveName: manifestPage.comments.archiveName } : {}),
|
|
567
|
+
threads: manifestPage.comments.threads,
|
|
568
|
+
comments: manifestPage.comments.comments,
|
|
569
|
+
participants: manifestPage.comments.participants,
|
|
570
|
+
attachments: manifestPage.comments.attachments,
|
|
571
|
+
bytes: manifestPage.comments.bytes,
|
|
572
|
+
...(manifestPage.comments.error ? { lastError: manifestPage.comments.error } : {}),
|
|
573
|
+
updatedAt: new Date().toISOString(),
|
|
574
|
+
} } : {}),
|
|
498
575
|
updatedAt: new Date().toISOString(),
|
|
499
576
|
});
|
|
500
577
|
report.addPage(reportPage({
|
|
@@ -505,7 +582,35 @@ export class Migrator {
|
|
|
505
582
|
resourceStates: resources,
|
|
506
583
|
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
507
584
|
...(manifestPage?.warnings ? { warnings: manifestPage.warnings } : {}),
|
|
585
|
+
commentsEnabled: options.comments ?? true,
|
|
586
|
+
...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
|
|
587
|
+
...(manifestPage?.comments ? { commentState: {
|
|
588
|
+
status: manifestPage.comments.status,
|
|
589
|
+
...(manifestPage.comments.fingerprint ? { fingerprint: manifestPage.comments.fingerprint } : {}),
|
|
590
|
+
...(manifestPage.comments.archivePath ? { archivePath: manifestPage.comments.archivePath } : {}),
|
|
591
|
+
...(manifestPage.comments.archiveName ? { archiveName: manifestPage.comments.archiveName } : {}),
|
|
592
|
+
threads: manifestPage.comments.threads,
|
|
593
|
+
comments: manifestPage.comments.comments,
|
|
594
|
+
participants: manifestPage.comments.participants,
|
|
595
|
+
attachments: manifestPage.comments.attachments,
|
|
596
|
+
bytes: manifestPage.comments.bytes,
|
|
597
|
+
...(manifestPage.comments.error ? { lastError: manifestPage.comments.error } : {}),
|
|
598
|
+
updatedAt: new Date().toISOString(),
|
|
599
|
+
} } : {}),
|
|
508
600
|
}), { source: normalizedPreviewHtml(document), destination: normalizedPreviewHtml(document) });
|
|
601
|
+
if (manifestPage?.comments
|
|
602
|
+
&& !['complete', 'skipped', 'planned'].includes(manifestPage.comments.status)
|
|
603
|
+
&& !(manifestPage.comments.status === 'failed' && commentsByPage.get(page.canonicalId)?.status === 'failed')
|
|
604
|
+
&& !(manifestPage.comments.status === 'partial' && (commentsByPage.get(page.canonicalId)?.warnings.length ?? 0) > 0)) {
|
|
605
|
+
report.issue({
|
|
606
|
+
code: `comment-${manifestPage.comments.status}`,
|
|
607
|
+
severity: manifestPage.comments.status === 'partial' || manifestPage.comments.status === 'stale' ? 'degradation' : 'error',
|
|
608
|
+
phase: 'comment',
|
|
609
|
+
pageId: page.canonicalId,
|
|
610
|
+
retryable: manifestPage.comments.status === 'failed',
|
|
611
|
+
message: `${page.title}: ${manifestPage.comments.error ?? `comment archive is ${manifestPage.comments.status}`}`,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
509
614
|
}
|
|
510
615
|
for (const warning of result.warnings)
|
|
511
616
|
report.warning(warning);
|
|
@@ -513,18 +618,328 @@ export class Migrator {
|
|
|
513
618
|
report.error(error);
|
|
514
619
|
return result;
|
|
515
620
|
}
|
|
621
|
+
async transferRemoteComments(input) {
|
|
622
|
+
const { source, destination, page, destinationPage, inventory, resources, pageState, report, store } = input;
|
|
623
|
+
const now = () => new Date().toISOString();
|
|
624
|
+
const counts = {
|
|
625
|
+
threads: inventory?.threads.length ?? 0,
|
|
626
|
+
comments: inventory?.comments ?? 0,
|
|
627
|
+
participants: inventory?.participants ?? 0,
|
|
628
|
+
attachments: inventory?.attachments ?? 0,
|
|
629
|
+
bytes: inventory?.knownBytes ?? 0,
|
|
630
|
+
};
|
|
631
|
+
if (!inventory)
|
|
632
|
+
return { status: 'skipped', ...counts, updatedAt: now() };
|
|
633
|
+
if (inventory.status === 'failed') {
|
|
634
|
+
return { status: 'failed', ...counts, ...(inventory.error ? { lastError: inventory.error } : {}), updatedAt: now() };
|
|
635
|
+
}
|
|
636
|
+
if (inventory.status === 'empty') {
|
|
637
|
+
if (pageState.comments?.destinationId) {
|
|
638
|
+
return { ...pageState.comments, status: 'stale', lastError: 'Source no longer returns the previously archived comments; the destination archive was preserved.', updatedAt: now() };
|
|
639
|
+
}
|
|
640
|
+
return { status: 'complete', ...counts, updatedAt: now() };
|
|
641
|
+
}
|
|
642
|
+
const fingerprint = inventory.fingerprint;
|
|
643
|
+
if (!fingerprint)
|
|
644
|
+
return { status: 'failed', ...counts, lastError: 'Comment inventory fingerprint is missing', updatedAt: now() };
|
|
645
|
+
if (input.dryRun)
|
|
646
|
+
return { status: 'planned', fingerprint, ...counts, updatedAt: now() };
|
|
647
|
+
if (!destination.createArchiveComment || !destination.readArchiveComment) {
|
|
648
|
+
return { status: 'failed', fingerprint, ...counts, lastError: `${destination.profile.name} does not implement comment archive writes`, updatedAt: now() };
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
if (input.signal?.aborted)
|
|
652
|
+
throw input.signal.reason ?? new Error('Transfer cancelled');
|
|
653
|
+
const resourceById = new Map(resources.map((resource) => [resource.id, resource]));
|
|
654
|
+
const downloads = new Map();
|
|
655
|
+
const archiveFiles = [];
|
|
656
|
+
for (const use of commentAttachmentUses(inventory)) {
|
|
657
|
+
if (input.signal?.aborted)
|
|
658
|
+
throw input.signal.reason ?? new Error('Transfer cancelled');
|
|
659
|
+
const resourceId = use.reference.resourceId;
|
|
660
|
+
if (!resourceId)
|
|
661
|
+
continue;
|
|
662
|
+
const resource = resourceById.get(resourceId);
|
|
663
|
+
if (!resource)
|
|
664
|
+
continue;
|
|
665
|
+
let downloaded = downloads.get(resourceId);
|
|
666
|
+
if (!downloaded) {
|
|
667
|
+
const resourceState = pageState.resources[resourceId];
|
|
668
|
+
const filename = containedCachePath(input.cacheDirectory, resourceState?.cachePath)
|
|
669
|
+
?? cachePath(input.cacheDirectory, page, resource);
|
|
670
|
+
const cached = await verifiedCacheFile(filename, resourceState?.sha256 ? {
|
|
671
|
+
sha256: resourceState.sha256,
|
|
672
|
+
...(resourceState.size !== undefined ? { size: resourceState.size } : {}),
|
|
673
|
+
} : undefined);
|
|
674
|
+
downloaded = cached ?? await source.downloadResource(resource, filename);
|
|
675
|
+
downloads.set(resourceId, downloaded);
|
|
676
|
+
}
|
|
677
|
+
archiveFiles.push({
|
|
678
|
+
commentId: use.commentId,
|
|
679
|
+
reference: use.reference.href,
|
|
680
|
+
originalName: resource.preferredName ?? resource.name,
|
|
681
|
+
...downloaded,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
const zip = await createCommentArchiveZip({
|
|
685
|
+
pageKey: page.id,
|
|
686
|
+
fingerprint,
|
|
687
|
+
files: archiveFiles,
|
|
688
|
+
destinationDirectory: path.join(input.cacheDirectory, 'comments', sha256(page.canonicalId).slice(0, 16)),
|
|
689
|
+
});
|
|
690
|
+
let current;
|
|
691
|
+
const previous = pageState.comments;
|
|
692
|
+
if (previous?.destinationId) {
|
|
693
|
+
try {
|
|
694
|
+
current = await destination.readArchiveComment({ page: destinationPage, commentId: previous.destinationId });
|
|
695
|
+
}
|
|
696
|
+
catch (error) {
|
|
697
|
+
report.issue({
|
|
698
|
+
code: 'comment-mapping-missing', severity: 'warning', phase: 'comment', pageId: page.canonicalId,
|
|
699
|
+
retryable: true, message: `${page.title}: mapped comment ${previous.destinationId} could not be read and will be recreated: ${error instanceof Error ? error.message : String(error)}`,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
current ??= await destination.findArchiveComment?.(destinationPage, fingerprint);
|
|
704
|
+
if (current && previous?.destinationFingerprint && current.fingerprint !== previous.destinationFingerprint) {
|
|
705
|
+
const conflict = {
|
|
706
|
+
fingerprint,
|
|
707
|
+
destinationId: current.id,
|
|
708
|
+
destinationFingerprint: current.fingerprint,
|
|
709
|
+
...counts,
|
|
710
|
+
...(previous.archiveResourceId ? { archiveResourceId: previous.archiveResourceId } : {}),
|
|
711
|
+
...(previous.archiveName ? { archiveName: previous.archiveName } : {}),
|
|
712
|
+
...(previous.archivePath ? { archivePath: previous.archivePath } : {}),
|
|
713
|
+
...(previous.archiveSha256 ? { archiveSha256: previous.archiveSha256 } : {}),
|
|
714
|
+
status: 'conflict',
|
|
715
|
+
lastError: 'Destination comment archive was changed manually and was preserved.',
|
|
716
|
+
updatedAt: now(),
|
|
717
|
+
};
|
|
718
|
+
return conflict;
|
|
719
|
+
}
|
|
720
|
+
const remoteResources = await destination.listUploadedResources(destinationPage);
|
|
721
|
+
const remoteMatches = async (remote, file, trustedMapping) => {
|
|
722
|
+
if (remote.size !== undefined && remote.size !== file.size)
|
|
723
|
+
return false;
|
|
724
|
+
if (!destination.downloadUploadedResource)
|
|
725
|
+
return trustedMapping && (remote.size === undefined || remote.size === file.size);
|
|
726
|
+
const comparison = `${file.path}.destination-${sha256(remote.id).slice(0, 8)}`;
|
|
727
|
+
try {
|
|
728
|
+
const actual = await destination.downloadUploadedResource({ page: destinationPage, resource: remote, destination: comparison });
|
|
729
|
+
return actual.size === file.size && actual.sha256 === file.sha256;
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
await rm(comparison, { force: true });
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
const uploadArchive = async (file, mappedId, mimeType) => {
|
|
736
|
+
const mapped = mappedId ? remoteResources.find((resource) => resource.id === mappedId) : undefined;
|
|
737
|
+
const byName = remoteResources.find((resource) => resource.name.toLocaleLowerCase() === file.name.toLocaleLowerCase());
|
|
738
|
+
const reusable = mapped && await remoteMatches(mapped, file, true)
|
|
739
|
+
? mapped
|
|
740
|
+
: byName && await remoteMatches(byName, file, false) ? byName : undefined;
|
|
741
|
+
if (reusable)
|
|
742
|
+
return reusable;
|
|
743
|
+
const extension = path.extname(file.name);
|
|
744
|
+
const alternativeName = `${file.name.slice(0, file.name.length - extension.length)}-${file.sha256.slice(0, 8)}${extension}`;
|
|
745
|
+
const alternative = remoteResources.find((resource) => resource.name.toLocaleLowerCase() === alternativeName.toLocaleLowerCase());
|
|
746
|
+
if (alternative && await remoteMatches(alternative, file, false))
|
|
747
|
+
return alternative;
|
|
748
|
+
const uploadName = mapped || byName ? alternativeName : file.name;
|
|
749
|
+
return destination.uploadResource({
|
|
750
|
+
page: destinationPage,
|
|
751
|
+
name: uploadName,
|
|
752
|
+
filePath: file.path,
|
|
753
|
+
size: file.size,
|
|
754
|
+
...(mimeType ? { mimeType } : {}),
|
|
755
|
+
});
|
|
756
|
+
};
|
|
757
|
+
const archiveResource = zip
|
|
758
|
+
? await uploadArchive(zip, previous?.archiveResourceId, 'application/zip')
|
|
759
|
+
: undefined;
|
|
760
|
+
if (input.signal?.aborted)
|
|
761
|
+
throw input.signal.reason ?? new Error('Transfer cancelled');
|
|
762
|
+
const attachmentNames = new Map();
|
|
763
|
+
if (zip) {
|
|
764
|
+
const nameByHash = new Map(zip.files.map((entry) => [entry.sha256, entry.archiveName]));
|
|
765
|
+
for (const [resourceId, downloaded] of downloads) {
|
|
766
|
+
const name = nameByHash.get(downloaded.sha256);
|
|
767
|
+
if (name)
|
|
768
|
+
attachmentNames.set(resourceId, name);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const render = (presentation, replaces) => renderCommentArchive(inventory, destination.kind, {
|
|
772
|
+
sourceUrl: page.url,
|
|
773
|
+
...(zip ? { archiveName: archiveResource?.name ?? zip.name, archiveHref: archiveResource?.url ?? archiveResource?.name ?? zip.name } : {}),
|
|
774
|
+
...(replaces ? { archiveReplaces: replaces } : {}),
|
|
775
|
+
attachmentNames,
|
|
776
|
+
pages: input.destinationPages,
|
|
777
|
+
sourcePages: input.sourcePages,
|
|
778
|
+
presentation,
|
|
779
|
+
});
|
|
780
|
+
const archiveState = zip ? {
|
|
781
|
+
...(archiveResource ? { archiveResourceId: archiveResource.id } : {}),
|
|
782
|
+
archiveName: archiveResource?.name ?? zip.name,
|
|
783
|
+
archivePath: zip.path,
|
|
784
|
+
archiveSha256: zip.sha256,
|
|
785
|
+
} : {};
|
|
786
|
+
const replacement = destination.capabilities.updateComments ? undefined : current?.id;
|
|
787
|
+
let expectedBody = render('native', replacement);
|
|
788
|
+
let fallbackReason;
|
|
789
|
+
let fullArchiveState = {};
|
|
790
|
+
const maximumInlineBytes = 1_000_000;
|
|
791
|
+
const prepareFullArchive = async (reason) => {
|
|
792
|
+
if (fullArchiveState.fullArchiveName)
|
|
793
|
+
return;
|
|
794
|
+
const fullBody = renderCommentArchive(inventory, 'markdown', {
|
|
795
|
+
sourceUrl: page.url,
|
|
796
|
+
...(zip ? { archiveName: archiveResource?.name ?? zip.name, archiveHref: archiveResource?.url ?? archiveResource?.name ?? zip.name } : {}),
|
|
797
|
+
...(replacement ? { archiveReplaces: replacement } : {}),
|
|
798
|
+
attachmentNames,
|
|
799
|
+
pages: input.destinationPages,
|
|
800
|
+
sourcePages: input.sourcePages,
|
|
801
|
+
});
|
|
802
|
+
const fullArchiveName = `ywtc-comments-${safeFilename(page.id)}-${fingerprint.slice(0, 12)}.md`;
|
|
803
|
+
const fullArchivePath = path.join(input.cacheDirectory, 'comments', sha256(page.canonicalId).slice(0, 16), fullArchiveName);
|
|
804
|
+
await mkdir(path.dirname(fullArchivePath), { recursive: true, mode: 0o700 });
|
|
805
|
+
await writeFile(fullArchivePath, `${fullBody}\n`, { mode: 0o600 });
|
|
806
|
+
const fullArchiveSha256 = sha256(`${fullBody}\n`);
|
|
807
|
+
const fullArchiveResource = await uploadArchive({
|
|
808
|
+
path: fullArchivePath,
|
|
809
|
+
name: fullArchiveName,
|
|
810
|
+
size: Buffer.byteLength(`${fullBody}\n`),
|
|
811
|
+
sha256: fullArchiveSha256,
|
|
812
|
+
}, previous?.fullArchiveResourceId, 'text/markdown');
|
|
813
|
+
const destinationFullArchiveName = fullArchiveResource.name;
|
|
814
|
+
expectedBody = renderCommentArchiveIndex(inventory, destination.kind, {
|
|
815
|
+
sourceUrl: page.url,
|
|
816
|
+
fullArchiveName: destinationFullArchiveName,
|
|
817
|
+
fullArchiveHref: fullArchiveResource.url ?? destinationFullArchiveName,
|
|
818
|
+
...(zip ? { archiveName: archiveResource?.name ?? zip.name, archiveHref: archiveResource?.url ?? archiveResource?.name ?? zip.name } : {}),
|
|
819
|
+
});
|
|
820
|
+
fullArchiveState = {
|
|
821
|
+
fullArchiveResourceId: fullArchiveResource.id,
|
|
822
|
+
fullArchiveName: destinationFullArchiveName,
|
|
823
|
+
fullArchivePath,
|
|
824
|
+
fullArchiveSha256,
|
|
825
|
+
};
|
|
826
|
+
fallbackReason = reason;
|
|
827
|
+
};
|
|
828
|
+
if (Buffer.byteLength(expectedBody) > maximumInlineBytes) {
|
|
829
|
+
await prepareFullArchive(`Full comment archive exceeded ${maximumInlineBytes} inline bytes and was attached as Markdown.`);
|
|
830
|
+
}
|
|
831
|
+
let result = current;
|
|
832
|
+
const requiresWrite = !result || previous?.fingerprint !== fingerprint;
|
|
833
|
+
if (requiresWrite) {
|
|
834
|
+
const write = async (body) => {
|
|
835
|
+
if (input.signal?.aborted)
|
|
836
|
+
throw input.signal.reason ?? new Error('Transfer cancelled');
|
|
837
|
+
if (!result || !destination.capabilities.updateComments || !destination.updateArchiveComment) {
|
|
838
|
+
return destination.createArchiveComment({ page: destinationPage, body });
|
|
839
|
+
}
|
|
840
|
+
return destination.updateArchiveComment({ comment: result, body });
|
|
841
|
+
};
|
|
842
|
+
const presentations = fallbackReason ? [] : ['native', 'quote', 'code'];
|
|
843
|
+
let lastError;
|
|
844
|
+
if (fallbackReason) {
|
|
845
|
+
result = await write(expectedBody);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
for (const presentation of presentations) {
|
|
849
|
+
try {
|
|
850
|
+
expectedBody = render(presentation, replacement);
|
|
851
|
+
result = await write(expectedBody);
|
|
852
|
+
if (presentation !== 'native')
|
|
853
|
+
fallbackReason = `Destination rejected the native collapsible archive; ${presentation} fallback was used.`;
|
|
854
|
+
lastError = undefined;
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
lastError = error;
|
|
859
|
+
const status = error.status;
|
|
860
|
+
if (status === 413 || /(?:status[=: ]+413|too large|payload too large)/i.test(error instanceof Error ? error.message : String(error))) {
|
|
861
|
+
try {
|
|
862
|
+
await prepareFullArchive('Destination rejected the inline archive size; the full archive was attached as Markdown.');
|
|
863
|
+
result = await write(expectedBody);
|
|
864
|
+
lastError = undefined;
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
catch (fallbackError) {
|
|
868
|
+
lastError = fallbackError;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const reconciled = await destination.findArchiveComment?.(destinationPage, fingerprint);
|
|
872
|
+
if (reconciled) {
|
|
873
|
+
result = reconciled;
|
|
874
|
+
expectedBody = reconciled.body;
|
|
875
|
+
fallbackReason = presentation === 'native' ? undefined : `Destination accepted the ${presentation} fallback after an ambiguous response.`;
|
|
876
|
+
lastError = undefined;
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
if (lastError)
|
|
882
|
+
throw lastError;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (!result)
|
|
886
|
+
throw new Error('Destination comment archive was not created');
|
|
887
|
+
const verified = await destination.readArchiveComment({ page: destinationPage, commentId: result.id });
|
|
888
|
+
const verification = verifyCommentArchive(fullArchiveState.fullArchiveName ? fingerprint : inventory, verified.body, archiveResource?.name);
|
|
889
|
+
if (fullArchiveState.fullArchiveName && !verified.body.includes(fullArchiveState.fullArchiveName)) {
|
|
890
|
+
verification.ok = false;
|
|
891
|
+
verification.differences.push('full Markdown comment archive reference is missing');
|
|
892
|
+
}
|
|
893
|
+
if (!verification.ok)
|
|
894
|
+
throw new Error(`Comment verification failed: ${verification.differences.join('; ')}`);
|
|
895
|
+
const previousIds = result.id !== current?.id && current
|
|
896
|
+
? [...new Set([...(previous?.previousDestinationIds ?? []), current.id])]
|
|
897
|
+
: previous?.previousDestinationIds;
|
|
898
|
+
const state = {
|
|
899
|
+
fingerprint,
|
|
900
|
+
destinationId: verified.id,
|
|
901
|
+
destinationFingerprint: verified.fingerprint,
|
|
902
|
+
...(previousIds?.length ? { previousDestinationIds: previousIds } : {}),
|
|
903
|
+
...archiveState,
|
|
904
|
+
...fullArchiveState,
|
|
905
|
+
status: inventory.warnings.length > 0 || fallbackReason ? 'partial' : 'complete',
|
|
906
|
+
...counts,
|
|
907
|
+
preview: verified.body,
|
|
908
|
+
...(fallbackReason ? { lastError: fallbackReason } : {}),
|
|
909
|
+
updatedAt: now(),
|
|
910
|
+
};
|
|
911
|
+
pageState.comments = state;
|
|
912
|
+
await store.setPage({ ...pageState, comments: state, updatedAt: now() });
|
|
913
|
+
return state;
|
|
914
|
+
}
|
|
915
|
+
catch (error) {
|
|
916
|
+
if (input.signal?.aborted)
|
|
917
|
+
throw input.signal.reason ?? error;
|
|
918
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
919
|
+
return {
|
|
920
|
+
fingerprint,
|
|
921
|
+
...counts,
|
|
922
|
+
...(pageState.comments?.destinationId ? { destinationId: pageState.comments.destinationId } : {}),
|
|
923
|
+
...(pageState.comments?.destinationFingerprint ? { destinationFingerprint: pageState.comments.destinationFingerprint } : {}),
|
|
924
|
+
status: /conflict/i.test(message) ? 'conflict' : 'failed',
|
|
925
|
+
lastError: message,
|
|
926
|
+
updatedAt: now(),
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
}
|
|
516
930
|
async transferRemote(input) {
|
|
517
|
-
const { source, destination, container, graph, options, store, report, sourceFingerprints, resourcesByPage } = input;
|
|
931
|
+
const { source, destination, container, graph, options, store, report, sourceFingerprints, resourcesByPage, commentsByPage } = input;
|
|
518
932
|
const destinationPages = new Map();
|
|
519
933
|
const stagingFailures = new Map();
|
|
520
934
|
const sourcePages = new Map(graph.pages.map((page) => [page.canonicalId, page]));
|
|
521
935
|
const bootstrapBodies = new Map();
|
|
522
936
|
const totalResources = [...resourcesByPage.values()].reduce((total, resources) => total + resources.filter((resource) => resource.kind !== 'grid').length, 0);
|
|
523
|
-
const
|
|
524
|
-
const
|
|
937
|
+
const totalCommentArchives = (options.comments ?? true) ? graph.pages.length : 0;
|
|
938
|
+
const progress = { staged: 0, pages: 0, resources: 0, comments: 0 };
|
|
939
|
+
const totalUnits = Math.max(1, graph.pages.length * 2 + totalResources + totalCommentArchives);
|
|
525
940
|
const updateProgress = (active) => {
|
|
526
|
-
const completed = progress.staged + progress.pages + progress.resources;
|
|
527
|
-
options.onStatus?.(`Progress ${Math.min(100, Math.floor((completed / totalUnits) * 100))}% · pages ${progress.pages}/${graph.pages.length} · files ${progress.resources}/${totalResources} · ${active}`);
|
|
941
|
+
const completed = progress.staged + progress.pages + progress.resources + progress.comments;
|
|
942
|
+
options.onStatus?.(`Progress ${Math.min(100, Math.floor((completed / totalUnits) * 100))}% · pages ${progress.pages}/${graph.pages.length} · files ${progress.resources}/${totalResources} · comments ${progress.comments}/${totalCommentArchives} · ${active}`);
|
|
528
943
|
};
|
|
529
944
|
for (const page of graph.pages) {
|
|
530
945
|
const [document, cachedPage] = await Promise.all([
|
|
@@ -572,6 +987,7 @@ export class Migrator {
|
|
|
572
987
|
let created;
|
|
573
988
|
let title = saved?.destinationTitle ?? page.title;
|
|
574
989
|
let resourcesForState = saved?.resources ?? {};
|
|
990
|
+
let commentsForState = saved?.comments;
|
|
575
991
|
let previousDestinationIds = saved?.previousDestinationIds ?? [];
|
|
576
992
|
let mappingRecovered = false;
|
|
577
993
|
const findTitleConflict = (candidateTitle) => destination.findTitleConflict
|
|
@@ -671,6 +1087,7 @@ export class Migrator {
|
|
|
671
1087
|
}
|
|
672
1088
|
previousDestinationIds = [...new Set([...previousDestinationIds, mapped.id])];
|
|
673
1089
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1090
|
+
commentsForState = undefined;
|
|
674
1091
|
mappingRecovered = true;
|
|
675
1092
|
report.issue({
|
|
676
1093
|
code: 'destination-mapping-recovered',
|
|
@@ -761,6 +1178,7 @@ export class Migrator {
|
|
|
761
1178
|
}
|
|
762
1179
|
previousDestinationIds = [...new Set([...previousDestinationIds, oldPage.id])];
|
|
763
1180
|
resourcesForState = resetResourceDestinations(saved.resources);
|
|
1181
|
+
commentsForState = undefined;
|
|
764
1182
|
mappingRecovered = true;
|
|
765
1183
|
report.addHierarchyRepair({
|
|
766
1184
|
sourceCanonicalId: page.canonicalId,
|
|
@@ -847,6 +1265,7 @@ export class Migrator {
|
|
|
847
1265
|
...(created.fingerprint ? { destinationFingerprint: created.fingerprint } : {}),
|
|
848
1266
|
status: options.dryRun ? 'planned' : saved?.status ?? 'skeleton-created',
|
|
849
1267
|
resources: resourcesForState,
|
|
1268
|
+
...(commentsForState ? { comments: commentsForState } : {}),
|
|
850
1269
|
degradations: saved?.degradations ?? [],
|
|
851
1270
|
updatedAt: new Date().toISOString(),
|
|
852
1271
|
};
|
|
@@ -854,6 +1273,8 @@ export class Migrator {
|
|
|
854
1273
|
options.onStatus?.(`${options.dryRun ? 'Planned' : 'Staged content for'} ${page.title}`);
|
|
855
1274
|
}
|
|
856
1275
|
catch (error) {
|
|
1276
|
+
if (options.signal?.aborted)
|
|
1277
|
+
throw options.signal.reason ?? error;
|
|
857
1278
|
const message = `${page.title}: ${error instanceof Error ? error.message : String(error)}`;
|
|
858
1279
|
const failureStatus = /conflict/i.test(message) ? 'conflict' : 'failed';
|
|
859
1280
|
stagingFailures.set(page.canonicalId, { status: failureStatus, message });
|
|
@@ -893,6 +1314,9 @@ export class Migrator {
|
|
|
893
1314
|
document,
|
|
894
1315
|
...(failedState?.resources ? { resourceStates: failedState.resources } : {}),
|
|
895
1316
|
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
1317
|
+
commentsEnabled: options.comments ?? true,
|
|
1318
|
+
...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
|
|
1319
|
+
...(failedState?.comments ? { commentState: failedState.comments } : {}),
|
|
896
1320
|
error: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
897
1321
|
}));
|
|
898
1322
|
progress.pages += 1;
|
|
@@ -1243,7 +1667,51 @@ export class Migrator {
|
|
|
1243
1667
|
const verification = assertDestinationContent(rendered.body, verified.content, destination.kind, cachedPage);
|
|
1244
1668
|
verificationDegradations = verification.status === 'partial' ? verification.differences : [];
|
|
1245
1669
|
}
|
|
1246
|
-
const
|
|
1670
|
+
const commentInventory = commentsByPage.get(page.canonicalId);
|
|
1671
|
+
const commentState = (options.comments ?? true)
|
|
1672
|
+
? await this.transferRemoteComments({
|
|
1673
|
+
source,
|
|
1674
|
+
destination,
|
|
1675
|
+
page: cachedPage,
|
|
1676
|
+
destinationPage: verified,
|
|
1677
|
+
inventory: commentInventory,
|
|
1678
|
+
resources,
|
|
1679
|
+
pageState: state,
|
|
1680
|
+
cacheDirectory: options.cacheDirectory,
|
|
1681
|
+
dryRun: options.dryRun,
|
|
1682
|
+
destinationPages,
|
|
1683
|
+
sourcePages,
|
|
1684
|
+
report,
|
|
1685
|
+
store,
|
|
1686
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1687
|
+
})
|
|
1688
|
+
: {
|
|
1689
|
+
status: 'skipped',
|
|
1690
|
+
threads: 0,
|
|
1691
|
+
comments: 0,
|
|
1692
|
+
participants: 0,
|
|
1693
|
+
attachments: 0,
|
|
1694
|
+
bytes: 0,
|
|
1695
|
+
updatedAt: new Date().toISOString(),
|
|
1696
|
+
};
|
|
1697
|
+
if (options.comments ?? true) {
|
|
1698
|
+
progress.comments += 1;
|
|
1699
|
+
updateProgress(`comments ${commentState.status} → ${page.title}`);
|
|
1700
|
+
}
|
|
1701
|
+
if (['failed', 'conflict', 'stale', 'partial'].includes(commentState.status)
|
|
1702
|
+
&& !(commentState.status === 'failed' && commentInventory?.status === 'failed')
|
|
1703
|
+
&& !(commentState.status === 'partial' && (commentInventory?.warnings.length ?? 0) > 0 && !commentState.lastError)) {
|
|
1704
|
+
report.issue({
|
|
1705
|
+
code: `comment-${commentState.status}`,
|
|
1706
|
+
severity: commentState.status === 'partial' || commentState.status === 'stale' ? 'degradation' : 'error',
|
|
1707
|
+
phase: 'comment',
|
|
1708
|
+
pageId: page.canonicalId,
|
|
1709
|
+
retryable: commentState.status === 'failed',
|
|
1710
|
+
message: `${page.title}: ${commentState.lastError ?? `comment archive is ${commentState.status}`}`,
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
const commentPartial = !['complete', 'skipped', 'planned'].includes(commentState.status);
|
|
1714
|
+
const partial = rendered.degraded || resourceProblems.length > 0 || verificationDegradations.length > 0 || commentPartial;
|
|
1247
1715
|
const lastResourceProblem = resourceProblems.at(-1);
|
|
1248
1716
|
const clearedState = { ...state };
|
|
1249
1717
|
delete clearedState.lastError;
|
|
@@ -1255,6 +1723,7 @@ export class Migrator {
|
|
|
1255
1723
|
destinationTitle: verified.title,
|
|
1256
1724
|
...(verified.fingerprint ? { destinationFingerprint: verified.fingerprint } : {}),
|
|
1257
1725
|
status: options.dryRun ? 'planned' : partial ? 'partial' : 'complete',
|
|
1726
|
+
comments: commentState,
|
|
1258
1727
|
degradations: [...new Set([...rendered.warnings, ...resourceProblems, ...verificationDegradations])],
|
|
1259
1728
|
...(lastResourceProblem ? { lastError: lastResourceProblem } : {}),
|
|
1260
1729
|
updatedAt: new Date().toISOString(),
|
|
@@ -1277,6 +1746,9 @@ export class Migrator {
|
|
|
1277
1746
|
resourceFallbacks: fallbackDetails,
|
|
1278
1747
|
warnings: [...rendered.warnings, ...verificationDegradations],
|
|
1279
1748
|
normalizations: rendered.normalizations,
|
|
1749
|
+
commentsEnabled: options.comments ?? true,
|
|
1750
|
+
...(commentInventory ? { commentInventory } : {}),
|
|
1751
|
+
commentState,
|
|
1280
1752
|
}), { source: normalizedPreviewHtml(document), destination: renderedPreviewHtml(verified.content ?? rendered.body, destination.kind) });
|
|
1281
1753
|
}
|
|
1282
1754
|
catch (error) {
|
|
@@ -1324,6 +1796,9 @@ export class Migrator {
|
|
|
1324
1796
|
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
1325
1797
|
resourceFallbacks: fallbackDetails,
|
|
1326
1798
|
error: message,
|
|
1799
|
+
commentsEnabled: options.comments ?? true,
|
|
1800
|
+
...(commentsByPage.get(page.canonicalId) ? { commentInventory: commentsByPage.get(page.canonicalId) } : {}),
|
|
1801
|
+
...(state.comments ? { commentState: state.comments } : {}),
|
|
1327
1802
|
}), actual?.content ? { source: normalizedPreviewHtml(document), destination: renderedPreviewHtml(actual.content, destination.kind) } : undefined);
|
|
1328
1803
|
}
|
|
1329
1804
|
finally {
|