@iwonz/ywtc 3.2.1 → 3.2.2
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 +18 -0
- package/README.md +4 -4
- package/dist/adapters/confluence.js +8 -7
- package/dist/adapters/confluence.js.map +1 -1
- package/dist/cli-arguments.js +2 -2
- package/dist/cli-arguments.js.map +1 -1
- package/dist/cli.js +22 -15
- package/dist/cli.js.map +1 -1
- package/dist/clients/confluence.d.ts +1 -0
- package/dist/clients/confluence.js +20 -0
- package/dist/clients/confluence.js.map +1 -1
- package/dist/migrator.d.ts +2 -13
- package/dist/migrator.js +198 -53
- package/dist/migrator.js.map +1 -1
- package/dist/report-catalog.d.ts +30 -2
- package/dist/report-catalog.js +57 -9
- package/dist/report-catalog.js.map +1 -1
- package/dist/report.d.ts +74 -23
- package/dist/report.js +262 -36
- package/dist/report.js.map +1 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/migrator.js
CHANGED
|
@@ -9,7 +9,7 @@ import { assertTransferCapabilities } from './domain.js';
|
|
|
9
9
|
import { FilesystemExporter } from './filesystem-export.js';
|
|
10
10
|
import { discoverPageGraph } from './graph.js';
|
|
11
11
|
import { profileBaseUrl, sameRemoteInstance } from './locators.js';
|
|
12
|
-
import { TransferReport } from './report.js';
|
|
12
|
+
import { TransferReport, } from './report.js';
|
|
13
13
|
import { DestinationNameAllocator, preferredDestinationName, verifiedCacheFile } from './resource-reconciliation.js';
|
|
14
14
|
import { resolveStateLocation, StateStore, } from './state.js';
|
|
15
15
|
function sourceIdentity(locator, root) {
|
|
@@ -119,8 +119,24 @@ function destinationFromState(state, adapter) {
|
|
|
119
119
|
}
|
|
120
120
|
function reportPage(input) {
|
|
121
121
|
const transferable = input.resources.filter((resource) => resource.kind !== 'grid');
|
|
122
|
-
const
|
|
123
|
-
|
|
122
|
+
const resourceItems = transferable.map((resource) => {
|
|
123
|
+
const state = input.resourceStates?.[resource.id];
|
|
124
|
+
const sourceBytes = state?.size ?? resource.size;
|
|
125
|
+
const status = input.resourceStatusOverride ?? state?.status ?? 'planned';
|
|
126
|
+
return {
|
|
127
|
+
sourceId: resource.id,
|
|
128
|
+
sourceName: resource.name,
|
|
129
|
+
kind: resource.kind,
|
|
130
|
+
status,
|
|
131
|
+
...(sourceBytes !== undefined ? { sourceBytes } : {}),
|
|
132
|
+
...(status === 'complete' && sourceBytes !== undefined ? { transferredBytes: sourceBytes } : {}),
|
|
133
|
+
...(state?.destinationId ? { destinationId: state.destinationId } : {}),
|
|
134
|
+
...(state?.destinationName ? { destinationName: state.destinationName } : {}),
|
|
135
|
+
...(state?.lastError ? { error: state.lastError } : {}),
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
const knownBytes = resourceItems.reduce((total, resource) => total + (resource.sourceBytes ?? 0), 0);
|
|
139
|
+
const transferredBytes = resourceItems.reduce((total, resource) => total + (resource.transferredBytes ?? 0), 0);
|
|
124
140
|
return {
|
|
125
141
|
sourceCanonicalId: input.page.canonicalId,
|
|
126
142
|
sourceId: input.page.id,
|
|
@@ -134,12 +150,16 @@ function reportPage(input) {
|
|
|
134
150
|
...(input.page.projectedParentId ? { parentId: input.page.projectedParentId } : {}),
|
|
135
151
|
depth: input.page.depth,
|
|
136
152
|
status: input.status,
|
|
153
|
+
inventoryStatus: input.inventoryStatus ?? 'complete',
|
|
137
154
|
resources: transferable.length,
|
|
138
|
-
|
|
139
|
-
|
|
155
|
+
resourceItems,
|
|
156
|
+
...(transferable.length === 0 || resourceItems.some((resource) => resource.sourceBytes !== undefined) ? { sourceBytes: knownBytes } : {}),
|
|
157
|
+
transferredBytes,
|
|
158
|
+
bytesUnknown: resourceItems.filter((resource) => resource.sourceBytes === undefined).length,
|
|
140
159
|
links: input.document.metrics.links,
|
|
141
160
|
images: input.document.metrics.images,
|
|
142
161
|
tables: input.document.metrics.tables,
|
|
162
|
+
grids: input.resources.filter((resource) => resource.kind === 'grid').length,
|
|
143
163
|
codeBlocks: input.document.metrics.codeBlocks,
|
|
144
164
|
degraded: input.document.degraded || (input.resourceFallbacks?.length ?? 0) > 0,
|
|
145
165
|
resourceFallbacks: input.resourceFallbacks ?? [],
|
|
@@ -148,10 +168,13 @@ function reportPage(input) {
|
|
|
148
168
|
...(input.error ? { error: input.error } : {}),
|
|
149
169
|
};
|
|
150
170
|
}
|
|
151
|
-
function runStatus(errors, warnings) {
|
|
171
|
+
function runStatus(errors, warnings, degradations) {
|
|
152
172
|
if (errors > 0)
|
|
153
173
|
return 'partial';
|
|
154
|
-
return warnings > 0 ? 'completed-with-warnings' : 'completed';
|
|
174
|
+
return warnings + degradations > 0 ? 'completed-with-warnings' : 'completed';
|
|
175
|
+
}
|
|
176
|
+
function attentionCount(summary) {
|
|
177
|
+
return summary.warnings + summary.degradations;
|
|
155
178
|
}
|
|
156
179
|
export class MigrationRunError extends Error {
|
|
157
180
|
reportPaths;
|
|
@@ -233,6 +256,7 @@ export class Migrator {
|
|
|
233
256
|
...(store.loadInfo.backupFilename ? { backupPath: store.loadInfo.backupFilename } : {}),
|
|
234
257
|
},
|
|
235
258
|
});
|
|
259
|
+
report.registerPages([{ ...root, depth: 0 }]);
|
|
236
260
|
let graph;
|
|
237
261
|
let sourceFingerprints;
|
|
238
262
|
try {
|
|
@@ -244,6 +268,10 @@ export class Migrator {
|
|
|
244
268
|
...(options.signal ? { signal: options.signal } : {}),
|
|
245
269
|
onProgress: (progress) => status(`Discovery: ${progress.pages} pages, ${progress.edges} links${progress.active ? ` · ${progress.active}` : ''}`),
|
|
246
270
|
});
|
|
271
|
+
report.registerPages(graph.pages);
|
|
272
|
+
report.setEdges(graph.edges);
|
|
273
|
+
for (const warning of graph.warnings)
|
|
274
|
+
report.warning(warning);
|
|
247
275
|
if (container
|
|
248
276
|
&& options.destination.type === 'remote'
|
|
249
277
|
&& sameRemoteInstance(options.source.profile, options.destination.profile)
|
|
@@ -253,20 +281,25 @@ export class Migrator {
|
|
|
253
281
|
status(`Caching source content for ${graph.pages.length} pages`);
|
|
254
282
|
sourceFingerprints = await cacheDiscoveredPages(options.cacheDirectory, graph);
|
|
255
283
|
await store.setGraph(graph.rootId, graph.edges);
|
|
256
|
-
report.setEdges(graph.edges);
|
|
257
|
-
for (const warning of graph.warnings)
|
|
258
|
-
report.warning(warning);
|
|
259
284
|
}
|
|
260
285
|
catch (error) {
|
|
261
286
|
const finalStatus = options.signal?.aborted ? 'cancelled' : 'failed';
|
|
262
287
|
const message = error instanceof Error ? error.message : String(error);
|
|
263
|
-
report.error
|
|
288
|
+
report.updatePage(root.canonicalId, { status: 'failed', inventoryStatus: 'failed', error: message });
|
|
289
|
+
report.issue({
|
|
290
|
+
code: 'source-discovery-failed',
|
|
291
|
+
severity: 'error',
|
|
292
|
+
phase: 'discovery',
|
|
293
|
+
pageId: root.canonicalId,
|
|
294
|
+
retryable: true,
|
|
295
|
+
message,
|
|
296
|
+
});
|
|
264
297
|
report.finish(finalStatus);
|
|
265
298
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
266
299
|
if (!options.dryRun)
|
|
267
300
|
await store.finishRun(run.id, finalStatus, {
|
|
268
301
|
pages: 0,
|
|
269
|
-
warnings: report.data.summary
|
|
302
|
+
warnings: attentionCount(report.data.summary),
|
|
270
303
|
errors: report.data.summary.errors,
|
|
271
304
|
});
|
|
272
305
|
throw new MigrationRunError(message, reportPaths, stateLocation.filename, { cause: error });
|
|
@@ -293,12 +326,26 @@ export class Migrator {
|
|
|
293
326
|
inventory.images += document.metrics.images;
|
|
294
327
|
inventory.tables += document.metrics.tables;
|
|
295
328
|
inventory.codeBlocks += document.metrics.codeBlocks;
|
|
329
|
+
report.addPage(reportPage({
|
|
330
|
+
page,
|
|
331
|
+
status: 'planned',
|
|
332
|
+
resources,
|
|
333
|
+
document,
|
|
334
|
+
}), undefined, false);
|
|
296
335
|
status(`Inspected ${page.title} · ${resources.length} resources`);
|
|
297
336
|
}
|
|
298
337
|
catch (error) {
|
|
299
338
|
const message = `${page.title}: ${error instanceof Error ? error.message : String(error)}`;
|
|
300
339
|
inventoryErrors.push(message);
|
|
301
|
-
report.error
|
|
340
|
+
report.updatePage(page.canonicalId, { status: 'failed', inventoryStatus: 'failed', error: message });
|
|
341
|
+
report.issue({
|
|
342
|
+
code: 'source-inventory-failed',
|
|
343
|
+
severity: 'error',
|
|
344
|
+
phase: 'inventory',
|
|
345
|
+
pageId: page.canonicalId,
|
|
346
|
+
retryable: true,
|
|
347
|
+
message,
|
|
348
|
+
});
|
|
302
349
|
}
|
|
303
350
|
})));
|
|
304
351
|
if (inventoryErrors.length > 0) {
|
|
@@ -308,7 +355,7 @@ export class Migrator {
|
|
|
308
355
|
if (!options.dryRun)
|
|
309
356
|
await store.finishRun(run.id, finalStatus, {
|
|
310
357
|
pages: graph.pages.length,
|
|
311
|
-
warnings: report.data.summary
|
|
358
|
+
warnings: attentionCount(report.data.summary),
|
|
312
359
|
errors: report.data.summary.errors,
|
|
313
360
|
});
|
|
314
361
|
throw new MigrationRunError('Resource preflight failed before destination mutation', reportPaths, stateLocation.filename);
|
|
@@ -345,16 +392,16 @@ export class Migrator {
|
|
|
345
392
|
report.output('destination', container.url);
|
|
346
393
|
}
|
|
347
394
|
report.finish('completed');
|
|
348
|
-
const finalStatus = runStatus(report.data.summary.errors, report.data.summary.warnings);
|
|
395
|
+
const finalStatus = runStatus(report.data.summary.errors, report.data.summary.warnings, report.data.summary.degradations);
|
|
349
396
|
report.finish(finalStatus);
|
|
350
397
|
const reportPaths = await report.write(options.reportsDirectory);
|
|
351
398
|
if (!options.dryRun)
|
|
352
399
|
await store.finishRun(run.id, finalStatus, {
|
|
353
400
|
pages: graph.pages.length,
|
|
354
|
-
warnings: report.data.summary
|
|
401
|
+
warnings: attentionCount(report.data.summary),
|
|
355
402
|
errors: report.data.summary.errors,
|
|
356
403
|
});
|
|
357
|
-
status(`${finalStatus}: ${
|
|
404
|
+
status(`${finalStatus}: source ${report.data.summary.pages} pages · ${report.data.summary.complete} complete · ${report.data.summary.partial} partial · ${report.data.summary.failed + report.data.summary.blocked} failed/blocked · files ${report.data.summary.resourcesComplete}/${report.data.summary.resources} complete · ${report.data.summary.errors} errors · ${report.data.summary.warnings} warnings · ${report.data.summary.degradations} degradations`);
|
|
358
405
|
return {
|
|
359
406
|
status: finalStatus,
|
|
360
407
|
hasErrors: report.data.summary.errors > 0,
|
|
@@ -377,7 +424,7 @@ export class Migrator {
|
|
|
377
424
|
if (!options.dryRun)
|
|
378
425
|
await store.finishRun(run.id, finalStatus, {
|
|
379
426
|
pages: graph.pages.length,
|
|
380
|
-
warnings: report.data.summary
|
|
427
|
+
warnings: attentionCount(report.data.summary),
|
|
381
428
|
errors: report.data.summary.errors,
|
|
382
429
|
});
|
|
383
430
|
this.logger.error(`${message}. Report: ${reportPaths.html}`);
|
|
@@ -422,10 +469,11 @@ export class Migrator {
|
|
|
422
469
|
destinationName: resource.name,
|
|
423
470
|
...(resource.sha256 ? { sha256: resource.sha256 } : {}),
|
|
424
471
|
...(resource.size !== undefined ? { size: resource.size } : {}),
|
|
425
|
-
status: 'complete',
|
|
472
|
+
status: options.dryRun ? 'planned' : 'complete',
|
|
426
473
|
updatedAt: new Date().toISOString(),
|
|
427
474
|
}];
|
|
428
475
|
}));
|
|
476
|
+
const pageStatus = options.dryRun ? 'planned' : manifestPage?.degraded ? 'partial' : 'complete';
|
|
429
477
|
await store.setPage({
|
|
430
478
|
sourceId: page.id,
|
|
431
479
|
sourceCanonicalId: page.canonicalId,
|
|
@@ -438,17 +486,18 @@ export class Migrator {
|
|
|
438
486
|
destinationTitle: page.title,
|
|
439
487
|
sourceFingerprint: sourceFingerprints.get(page.canonicalId),
|
|
440
488
|
...(manifestPage || previous?.destinationFingerprint ? { destinationFingerprint: manifestPage ? sha256(JSON.stringify(manifestPage)) : previous.destinationFingerprint } : {}),
|
|
441
|
-
status:
|
|
489
|
+
status: pageStatus,
|
|
442
490
|
resources,
|
|
443
491
|
degradations: manifestPage?.warnings ?? [],
|
|
444
492
|
updatedAt: new Date().toISOString(),
|
|
445
493
|
});
|
|
446
494
|
report.addPage(reportPage({
|
|
447
495
|
page,
|
|
448
|
-
status:
|
|
496
|
+
status: pageStatus,
|
|
449
497
|
resources: sourceResources,
|
|
450
498
|
document,
|
|
451
|
-
|
|
499
|
+
resourceStates: resources,
|
|
500
|
+
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
452
501
|
...(manifestPage?.warnings ? { warnings: manifestPage.warnings } : {}),
|
|
453
502
|
}), { source: normalizedPreviewHtml(document), destination: normalizedPreviewHtml(document) });
|
|
454
503
|
}
|
|
@@ -461,6 +510,7 @@ export class Migrator {
|
|
|
461
510
|
async transferRemote(input) {
|
|
462
511
|
const { source, destination, container, graph, options, store, report, sourceFingerprints, resourcesByPage } = input;
|
|
463
512
|
const destinationPages = new Map();
|
|
513
|
+
const stagingFailures = new Map();
|
|
464
514
|
const sourcePages = new Map(graph.pages.map((page) => [page.canonicalId, page]));
|
|
465
515
|
const bootstrapBodies = new Map();
|
|
466
516
|
const totalResources = [...resourcesByPage.values()].reduce((total, resources) => total + resources.filter((resource) => resource.kind !== 'grid').length, 0);
|
|
@@ -496,7 +546,16 @@ export class Migrator {
|
|
|
496
546
|
const parent = page.projectedParentId ? destinationPages.get(page.projectedParentId) : container;
|
|
497
547
|
if (!parent) {
|
|
498
548
|
const message = `Destination parent is unavailable for ${page.title}`;
|
|
499
|
-
|
|
549
|
+
stagingFailures.set(page.canonicalId, { status: 'blocked', message });
|
|
550
|
+
report.updatePage(page.canonicalId, { status: 'blocked', error: message });
|
|
551
|
+
report.issue({
|
|
552
|
+
code: 'destination-parent-unavailable',
|
|
553
|
+
severity: 'error',
|
|
554
|
+
phase: 'page',
|
|
555
|
+
pageId: page.canonicalId,
|
|
556
|
+
retryable: true,
|
|
557
|
+
message,
|
|
558
|
+
});
|
|
500
559
|
progress.staged += 1;
|
|
501
560
|
updateProgress(message);
|
|
502
561
|
continue;
|
|
@@ -530,21 +589,54 @@ export class Migrator {
|
|
|
530
589
|
if (page.projectedParentId) {
|
|
531
590
|
await verifyConfirmedDestinationPage(destination, parent, this.consistencyOptions);
|
|
532
591
|
}
|
|
533
|
-
const
|
|
592
|
+
const findRecoveryChild = (candidateTitle) => destination.findRecoverablePage
|
|
593
|
+
? destination.findRecoverablePage(parent, candidateTitle)
|
|
594
|
+
: destination.findChild(parent, candidateTitle);
|
|
595
|
+
let candidate;
|
|
596
|
+
let recoveryLookupFailed = false;
|
|
597
|
+
try {
|
|
598
|
+
candidate = await findRecoveryChild(title);
|
|
599
|
+
}
|
|
600
|
+
catch (lookupError) {
|
|
601
|
+
recoveryLookupFailed = true;
|
|
602
|
+
report.issue({
|
|
603
|
+
code: 'destination-recovery-lookup-failed',
|
|
604
|
+
severity: 'warning',
|
|
605
|
+
phase: 'page',
|
|
606
|
+
pageId: page.canonicalId,
|
|
607
|
+
retryable: true,
|
|
608
|
+
message: `${page.title}: recovery lookup failed; bootstrap recreation will be attempted: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}`,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
534
611
|
if (candidate && sourceDerivedTextMatches(bootstrap.body, candidate.content, destination.kind, cachedPage)) {
|
|
535
612
|
created = candidate;
|
|
536
613
|
}
|
|
537
614
|
else {
|
|
538
615
|
let recoveryTitle = title;
|
|
539
|
-
|
|
540
|
-
if (occupied) {
|
|
616
|
+
if (candidate) {
|
|
541
617
|
const base = collisionTitle(page);
|
|
542
618
|
const hash = sha256(page.canonicalId).slice(0, 8);
|
|
543
619
|
recoveryTitle = base;
|
|
544
620
|
let sequence = 0;
|
|
545
|
-
while (
|
|
546
|
-
|
|
547
|
-
|
|
621
|
+
while (!recoveryLookupFailed) {
|
|
622
|
+
try {
|
|
623
|
+
if (!await findRecoveryChild(recoveryTitle))
|
|
624
|
+
break;
|
|
625
|
+
sequence += 1;
|
|
626
|
+
recoveryTitle = `${base} [${hash}${sequence > 1 ? `-${sequence}` : ''}]`;
|
|
627
|
+
}
|
|
628
|
+
catch (lookupError) {
|
|
629
|
+
recoveryLookupFailed = true;
|
|
630
|
+
recoveryTitle = `${base} [${hash}]`;
|
|
631
|
+
report.issue({
|
|
632
|
+
code: 'destination-recovery-lookup-failed',
|
|
633
|
+
severity: 'warning',
|
|
634
|
+
phase: 'page',
|
|
635
|
+
pageId: page.canonicalId,
|
|
636
|
+
retryable: true,
|
|
637
|
+
message: `${page.title}: suffixed-title lookup failed; bootstrap recreation will be attempted: ${lookupError instanceof Error ? lookupError.message : String(lookupError)}`,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
548
640
|
}
|
|
549
641
|
}
|
|
550
642
|
created = await destination.createPage({ container: parent, title: recoveryTitle, body: bootstrap.body });
|
|
@@ -632,10 +724,20 @@ export class Migrator {
|
|
|
632
724
|
}
|
|
633
725
|
catch (error) {
|
|
634
726
|
const message = `${page.title}: ${error instanceof Error ? error.message : String(error)}`;
|
|
635
|
-
|
|
727
|
+
const failureStatus = /conflict/i.test(message) ? 'conflict' : 'failed';
|
|
728
|
+
stagingFailures.set(page.canonicalId, { status: failureStatus, message });
|
|
729
|
+
report.updatePage(page.canonicalId, { status: failureStatus, error: message });
|
|
730
|
+
report.issue({
|
|
731
|
+
code: failureStatus === 'conflict' ? 'destination-conflict' : 'destination-page-staging-failed',
|
|
732
|
+
severity: 'error',
|
|
733
|
+
phase: 'page',
|
|
734
|
+
pageId: page.canonicalId,
|
|
735
|
+
retryable: failureStatus !== 'conflict',
|
|
736
|
+
message,
|
|
737
|
+
});
|
|
636
738
|
const savedState = store.value.pages[page.canonicalId];
|
|
637
739
|
if (savedState)
|
|
638
|
-
await store.setPage({ ...savedState, status:
|
|
740
|
+
await store.setPage({ ...savedState, status: failureStatus, lastError: message, updatedAt: new Date().toISOString() });
|
|
639
741
|
}
|
|
640
742
|
finally {
|
|
641
743
|
progress.staged += 1;
|
|
@@ -651,13 +753,16 @@ export class Migrator {
|
|
|
651
753
|
const cachedPage = await loadCachedPage(options.cacheDirectory, page.canonicalId);
|
|
652
754
|
const resources = resourcesByPage.get(page.canonicalId) ?? [];
|
|
653
755
|
if (!destinationPage) {
|
|
756
|
+
const currentFailure = stagingFailures.get(page.canonicalId);
|
|
654
757
|
const failedState = store.value.pages[page.canonicalId];
|
|
655
758
|
report.addPage(reportPage({
|
|
656
759
|
page,
|
|
657
|
-
status:
|
|
760
|
+
status: currentFailure?.status ?? 'blocked',
|
|
658
761
|
resources,
|
|
659
762
|
document,
|
|
660
|
-
|
|
763
|
+
...(failedState?.resources ? { resourceStates: failedState.resources } : {}),
|
|
764
|
+
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
765
|
+
error: currentFailure?.message ?? failedState?.lastError ?? 'Destination page with source-derived content was not created',
|
|
661
766
|
}));
|
|
662
767
|
progress.pages += 1;
|
|
663
768
|
updateProgress(`blocked ${page.title}`);
|
|
@@ -906,28 +1011,48 @@ export class Migrator {
|
|
|
906
1011
|
await writeFile(snapshotPath, cachedPage.rawContent, { mode: 0o600 });
|
|
907
1012
|
const snapshotKey = `snapshot:${page.canonicalId}`;
|
|
908
1013
|
const existingSnapshot = state.resources[snapshotKey];
|
|
1014
|
+
const snapshotFile = {
|
|
1015
|
+
path: snapshotPath,
|
|
1016
|
+
size: Buffer.byteLength(cachedPage.rawContent),
|
|
1017
|
+
sha256: sha256(cachedPage.rawContent),
|
|
1018
|
+
};
|
|
909
1019
|
try {
|
|
910
|
-
const
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1020
|
+
const knownSnapshot = remoteResources.find((item) => item.id === existingSnapshot?.destinationId)
|
|
1021
|
+
?? remoteResources.find((item) => item.name.toLocaleLowerCase() === snapshotName.toLocaleLowerCase());
|
|
1022
|
+
let snapshot = knownSnapshot && await remoteMatches(knownSnapshot, snapshotFile, true)
|
|
1023
|
+
? knownSnapshot
|
|
1024
|
+
: undefined;
|
|
1025
|
+
if (!snapshot) {
|
|
1026
|
+
try {
|
|
1027
|
+
snapshot = await destination.uploadResource({
|
|
1028
|
+
page: destinationPage,
|
|
1029
|
+
name: snapshotName,
|
|
1030
|
+
filePath: snapshotPath,
|
|
1031
|
+
size: snapshotFile.size,
|
|
1032
|
+
mimeType: page.rawFormat === 'yfm' ? 'text/markdown' : 'application/xml',
|
|
1033
|
+
...(knownSnapshot ? { existingId: knownSnapshot.id } : {}),
|
|
1034
|
+
});
|
|
915
1035
|
}
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
name
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}
|
|
1036
|
+
catch (uploadError) {
|
|
1037
|
+
remoteResources = await destination.listUploadedResources(destinationPage);
|
|
1038
|
+
const uncertain = remoteResources.find((item) => item.name.toLocaleLowerCase() === snapshotName.toLocaleLowerCase());
|
|
1039
|
+
if (uncertain && await remoteMatches(uncertain, snapshotFile, true))
|
|
1040
|
+
snapshot = uncertain;
|
|
1041
|
+
else
|
|
1042
|
+
throw uploadError;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!remoteResources.some((item) => item.id === snapshot.id))
|
|
1046
|
+
remoteResources.push(snapshot);
|
|
924
1047
|
state.resources[snapshotKey] = {
|
|
925
1048
|
sourceId: snapshotKey,
|
|
926
1049
|
sourceName: snapshotName,
|
|
927
1050
|
destinationId: snapshot.id,
|
|
928
1051
|
destinationName: snapshot.name,
|
|
929
1052
|
...(snapshot.size !== undefined ? { size: snapshot.size } : {}),
|
|
930
|
-
sha256: sha256
|
|
1053
|
+
sha256: snapshotFile.sha256,
|
|
1054
|
+
cachePath: snapshotPath,
|
|
1055
|
+
mimeType: page.rawFormat === 'yfm' ? 'text/markdown' : 'application/xml',
|
|
931
1056
|
status: 'complete',
|
|
932
1057
|
updatedAt: new Date().toISOString(),
|
|
933
1058
|
};
|
|
@@ -935,7 +1060,15 @@ export class Migrator {
|
|
|
935
1060
|
catch (error) {
|
|
936
1061
|
const detail = `Raw source snapshot was not transferred: ${error instanceof Error ? error.message : String(error)}`;
|
|
937
1062
|
resourceProblems.push(detail);
|
|
938
|
-
report.
|
|
1063
|
+
report.issue({
|
|
1064
|
+
code: 'source-snapshot-transfer-failed',
|
|
1065
|
+
severity: 'error',
|
|
1066
|
+
phase: 'resource',
|
|
1067
|
+
pageId: page.canonicalId,
|
|
1068
|
+
resourceId: snapshotKey,
|
|
1069
|
+
retryable: true,
|
|
1070
|
+
message: `${page.title}: ${detail}`,
|
|
1071
|
+
});
|
|
939
1072
|
state.resources[snapshotKey] = {
|
|
940
1073
|
sourceId: snapshotKey,
|
|
941
1074
|
sourceName: snapshotName,
|
|
@@ -954,7 +1087,11 @@ export class Migrator {
|
|
|
954
1087
|
if (destinationPage.contentFingerprint
|
|
955
1088
|
&& current.contentFingerprint
|
|
956
1089
|
&& current.contentFingerprint !== destinationPage.contentFingerprint) {
|
|
957
|
-
|
|
1090
|
+
const normalization = verifyDestinationContent(destinationPage.content ?? bootstrap.body, current.content, destination.kind, cachedPage);
|
|
1091
|
+
if (normalization.status !== 'complete') {
|
|
1092
|
+
throw new Error(`Destination conflict for ${page.title}: content changed while resources were transferred`);
|
|
1093
|
+
}
|
|
1094
|
+
rendered.normalizations.push('Destination normalized page markup without semantic content changes');
|
|
958
1095
|
}
|
|
959
1096
|
verified = await destination.updatePage({
|
|
960
1097
|
page: current,
|
|
@@ -999,7 +1136,8 @@ export class Migrator {
|
|
|
999
1136
|
status: finalState.status,
|
|
1000
1137
|
resources,
|
|
1001
1138
|
document,
|
|
1002
|
-
|
|
1139
|
+
resourceStates: finalState.resources,
|
|
1140
|
+
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
1003
1141
|
resourceFallbacks: fallbackDetails,
|
|
1004
1142
|
warnings: [...rendered.warnings, ...verificationDegradations],
|
|
1005
1143
|
normalizations: rendered.normalizations,
|
|
@@ -1019,7 +1157,14 @@ export class Migrator {
|
|
|
1019
1157
|
failureStatus = 'failed';
|
|
1020
1158
|
}
|
|
1021
1159
|
}
|
|
1022
|
-
report.
|
|
1160
|
+
report.issue({
|
|
1161
|
+
code: failureStatus === 'conflict' ? 'destination-conflict' : 'page-transfer-failed',
|
|
1162
|
+
severity: 'error',
|
|
1163
|
+
phase: 'page',
|
|
1164
|
+
pageId: page.canonicalId,
|
|
1165
|
+
retryable: failureStatus !== 'conflict',
|
|
1166
|
+
message,
|
|
1167
|
+
});
|
|
1023
1168
|
await store.setPage({
|
|
1024
1169
|
...state,
|
|
1025
1170
|
...(actual?.fingerprint ? { destinationFingerprint: actual.fingerprint } : {}),
|
|
@@ -1039,9 +1184,9 @@ export class Migrator {
|
|
|
1039
1184
|
status: failureStatus,
|
|
1040
1185
|
resources,
|
|
1041
1186
|
document,
|
|
1042
|
-
|
|
1187
|
+
resourceStates: state.resources,
|
|
1188
|
+
...(options.dryRun ? { resourceStatusOverride: 'planned' } : {}),
|
|
1043
1189
|
resourceFallbacks: fallbackDetails,
|
|
1044
|
-
warnings: resourceProblems,
|
|
1045
1190
|
error: message,
|
|
1046
1191
|
}), actual?.content ? { source: normalizedPreviewHtml(document), destination: renderedPreviewHtml(actual.content, destination.kind) } : undefined);
|
|
1047
1192
|
}
|